Files
wehub-resource-sync 23f7624596
ADR-166 MCP Bridge Security Lock / Static-source security lock (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / Compose default binds loopback + Mongo has auth (push) Failing after 2s
CodeQL Advanced / Analyze (rust) (push) Failing after 0s
ADR-166 MCP Bridge Security Lock / plugin-agent-federation bindHost default (push) Failing after 1s
ADR-166 MCP Bridge Security Lock / Runtime behavior — 401 + terminal gate + fail-closed (push) Failing after 4s
business-pods-smoke / smoke (push) Failing after 1s
all-plugins-smoke / smoke-all (push) Failing after 2s
CI/CD Pipeline / Security & Code Quality (push) Failing after 1s
CI/CD Pipeline / Test Suite (ubuntu-latest) (push) Failing after 1s
CI/CD Pipeline / Build & Package (macos-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (ubuntu-latest) (push) Has been skipped
CI/CD Pipeline / Build & Package (windows-latest) (push) Has been skipped
CI/CD Pipeline / Documentation & Examples (push) Failing after 1s
Clone Tracker (14-day rolling) / Snapshot clones for ruflo ecosystem (push) Failing after 1s
CodeQL Advanced / Analyze (actions) (push) Failing after 1s
CodeQL Advanced / Analyze (javascript-typescript) (push) Failing after 1s
federation-peer-rust / stable-noop (push) Failing after 1s
metaharness-ci / score (push) Failing after 1s
metaharness-ci / router-compat (push) Failing after 0s
metaharness-ci / similarity-tests (push) Failing after 0s
no-agentbbs-smoke / smoke-without-agentbbs (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (windows-latest) (push) Has been skipped
codex-integration-audit / Codex integration audit (push) Failing after 1s
helpers-manifest-guard / guard (push) Failing after 1s
🔗 Cross-Agent Integration Tests / 🤝 Agent Coordination Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🧠 Memory Sharing Integration (push) Has been skipped
🔗 Cross-Agent Integration Tests / 🛡️ Fault Tolerance Tests (push) Has been skipped
🔗 Cross-Agent Integration Tests / ⚡ Performance Integration Tests (push) Has been skipped
metaharness-ci / mcp-scan (push) Failing after 1s
metaharness-ci / eject-dryrun (push) Failing after 1s
metaharness-ci / metaharness-real-data (push) Failing after 0s
no-cli-optdep-bloat-2561 / guard (push) Failing after 1s
no-metaharness-smoke / smoke-without-metaharness (push) Failing after 1s
no-phantom-agentic-flow-subpath / guard (push) Failing after 1s
🔄 Automated Rollback Manager / 🚨 Failure Detection (push) Failing after 1s
V3 CI/CD Pipeline / Plugin hooks smoke / ubuntu-latest / Node 22 (push) Failing after 1s
V3 CI/CD Pipeline / ruflo-graph-intelligence build + test smoke (#2044, ADR-123) (push) Failing after 1s
CVE Audit Gate / Audit root (critical-blocking) (push) Failing after 2s
cost-tracker-smoke / smoke (push) Failing after 3s
oia-audit-weekly / audit (push) Failing after 2s
ruflo-agent-smoke / ruflo-agent structural smoke (push) Failing after 1s
📊 Status Badges Update / 📊 Update Status Badges (push) Failing after 1s
V3 CI/CD Pipeline / Static regression guards (#2267 YAML + (push) Failing after 1s
V3 CI/CD Pipeline / Test V3 Packages (push) Failing after 0s
V3 CI/CD Pipeline / agent_execute provider routing smoke (#2042) (push) Failing after 0s
CVE Audit Gate / Audit v3 (critical-blocking) (push) Failing after 1s
federation-peer-rust / stable-native (push) Failing after 2s
🔗 Cross-Agent Integration Tests / 🚀 Integration Test Setup (push) Failing after 2s
neural-trader-smoke / runtime-smoke (push) Failing after 1s
V3 CI/CD Pipeline / Build V3 (macos-latest) (push) Has been skipped
V3 CI/CD Pipeline / Build V3 (ubuntu-latest) (push) Has been skipped
V3 CI/CD Pipeline / Type Check V3 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 24 (push) Failing after 1s
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / ubuntu-latest / Node 22 (push) Failing after 2s
V3 CI/CD Pipeline / browser rvf create flag smoke (#2015) (push) Failing after 0s
V3 CI/CD Pipeline / Dependency review (#2046) (push) Has been skipped
V3 CI/CD Pipeline / Supply-chain audit (#2046) (push) Failing after 0s
V3 CI/CD Pipeline / witness marker drift smoke (#2021) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader portfolio CG smoke (#2068, ADR-126 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / neural-trader backtest signing smoke (#2068, ADR-126 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / kg-extract type-import classification smoke (#2049) (push) Failing after 0s
V3 CI/CD Pipeline / witness verify precondition smoke (#1880) (push) Failing after 2s
V3 CI/CD Pipeline / neural-trader pipeline risk-gate smoke (#2068, ADR-126 Phase 5) (push) Failing after 0s
V3 CI/CD Pipeline / neural-trader feature attribution smoke (#2068, ADR-126 Phase 6) (push) Failing after 0s
V3 CI/CD Pipeline / plugin-registry signature verification smoke (#1922, CWE-347) (push) Failing after 4s
V3 CI/CD Pipeline / memory stats legacy-DB smoke (#2120) (push) Failing after 4s
V3 CI/CD Pipeline / github deprecated actions smoke (#2089, ADR-127 Phase 3) (push) Failing after 1s
V3 CI/CD Pipeline / graph query + pathfinder smoke (ADR-130 P2+P5) (push) Has been skipped
V3 CI/CD Pipeline / graph trajectory hooks smoke (ADR-130 P3) (push) Has been skipped
V3 CI/CD Pipeline / graph plugin adapter smoke (ADR-130 P4) (push) Has been skipped
V3 CI/CD Pipeline / graph benchmark (ADR-130 P6) (push) Has been skipped
V3 CI/CD Pipeline / statusline generator delegation smoke (#2195) (push) Failing after 1s
V3 CI/CD Pipeline / wizard init regression guard (#2206 (push) Failing after 1s
V3 CI/CD Pipeline / memory no-stray-db smoke (ADR-125 P7) (push) Failing after 1s
V3 CI/CD Pipeline / github-safe injection smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github actions pin smoke (#2089, ADR-127 Phase 1) (push) Failing after 1s
V3 CI/CD Pipeline / github attribution opt-in smoke (#2089, ADR-127 Phase 4) (push) Failing after 1s
V3 CI/CD Pipeline / pre-bash hook safety smoke (#2017) (push) Failing after 1s
V3 CI/CD Pipeline / Memory import smoke / ubuntu-latest (push) Failing after 0s
V3 CI/CD Pipeline / MCP protocol smoke / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / ruvllm WASM auto-init smoke (#2086) (push) Failing after 4s
V3 CI/CD Pipeline / MCP paired-tool round-trip smoke (#1889) (push) Failing after 1s
V3 CI/CD Pipeline / Plugin package install-safety (#1902/#1903/#1904) (push) Failing after 1s
V3 CI/CD Pipeline / Tool description discoverability (ADR-112) (push) Failing after 3s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (22) (push) Failing after 1s
V3 CI/CD Pipeline / CLI npx-install smoke (#1147 / (24) (push) Failing after 1s
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / ubuntu-latest (push) Failing after 2s
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / ubuntu-latest (push) Failing after 1s
V3 CI/CD Pipeline / Vector-index dimension audit (#1947) (push) Failing after 0s
V3 CI/CD Pipeline / Hook-command install safety (#1921) (push) Failing after 1s
V3 CI/CD Pipeline / ToolOutputGuardrail smoke (ADR-131, (push) Failing after 1s
V3 CI/CD Pipeline / init-bundle invariants smoke (#2095, ADR-128 Phase 5) (push) Failing after 1s
V3 CI/CD Pipeline / wasm provider bridge smoke (ADR-129 P1) (push) Failing after 2s
V3 CI/CD Pipeline / wasm gallery CRUD smoke (ADR-129 P3) (push) Failing after 1s
V3 CI/CD Pipeline / wasm plugin bridge smoke (ADR-129 P4) (push) Failing after 0s
V3 CI/CD Pipeline / wasm compose smoke (ADR-129 P2) (push) Failing after 4s
V3 CI/CD Pipeline / graph schema smoke (ADR-130 P1) (push) Failing after 0s
Validate Marketplace / validate (push) Failing after 1s
🔍 Verification Pipeline / 🚀 Setup Verification (push) Failing after 1s
🔍 Verification Pipeline / 🛡️ Security Verification (push) Has been skipped
🔍 Verification Pipeline / 📝 Code Quality (push) Has been skipped
🔍 Verification Pipeline / 🧪 Test Verification (${{ matrix.os }}, Node ${{ matrix.node }}) (push) Has been skipped
🔍 Verification Pipeline / 🏗️ Build Verification (push) Has been skipped
🔍 Verification Pipeline / 📚 Documentation Verification (push) Has been skipped
CVE Audit Gate / High-severity report (warn only) (push) Has been cancelled
🔄 Automated Rollback Manager / 🔄 Execute Rollback (push) Has been cancelled
🔄 Automated Rollback Manager / ✅ Post-Rollback Verification (push) Has been cancelled
🔄 Automated Rollback Manager / 📊 Rollback Monitoring (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook execution smoke (#2132) / windows-latest (push) Has been cancelled
🔄 Automated Rollback Manager / ⏳ Manual Rollback Approval (push) Has been cancelled
V3 CI/CD Pipeline / MCP protocol smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Memory import smoke / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows hook shim smoke (#2132) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Windows init hooks smoke (#2132) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / macos-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / ubuntu-latest (push) Has been cancelled
V3 CI/CD Pipeline / Witness verify (signed manifest) / windows-latest (push) Has been cancelled
V3 CI/CD Pipeline / Publish to npm (alpha) (push) Has been cancelled
V3 CI/CD Pipeline / Smoke (no better-sqlite3) / macos-latest / Node 22 (push) Has been cancelled
V3 CI/CD Pipeline / Plugin hooks smoke / macos-latest / Node 22 (push) Has been cancelled
CI/CD Pipeline / Deploy & Release (push) Has been cancelled
CI/CD Pipeline / CI Status (push) Has been cancelled
🔗 Cross-Agent Integration Tests / 📊 Integration Test Report (push) Has been cancelled
🔄 Automated Rollback Manager / 🔍 Pre-Rollback Validation (push) Has been cancelled
🔍 Verification Pipeline / ⚡ Performance Verification (push) Has been cancelled
🔍 Verification Pipeline / 📊 Verification Report (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:02:19 +08:00

375 lines
17 KiB
TypeScript

/**
* weft-export.test.ts — ADR-150 weight-eft slice.
*
* Covers:
* 1. Archive-builder: ruflo run records → DarwinTrajectory[] mapping + the
* honest resolved-proxy accounting.
* 2. distill-export E2E: synthetic archive → weight-eft export produces
* sft.jsonl / dpo.jsonl / export-report.json (weight-eft installed in the
* worktree; skipped if the optional dep is genuinely absent).
* 3. Degraded path: weight-eft absent (injected failing importer) → {degraded:true}.
* 4. Remote-train: pure command construction (ssh/rsync/ruvllm argv) + dry-run
* output. NEVER touches a live host — spawn is stubbed.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { rmSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
buildArchiveFromRecords,
runExport,
runPlan,
buildRemoteTrainInvocation,
runRemoteTrain,
loadWeightEft,
DEFAULT_BASE_MODEL,
type DarwinTrajectory,
type SpawnLike,
} from '../src/services/weight-eft.js';
import type { RunTranscriptRecord } from '../src/ruvector/run-transcript-recorder.js';
import {
recordRunTranscript,
readRunTranscripts,
tierForModel,
runTranscriptRecorderStatus,
__resetRunTranscriptRecorderForTests,
} from '../src/ruvector/run-transcript-recorder.js';
// NOTE: the CLI command surface (neuralCommand) is intentionally NOT imported
// here — it pulls in `@claude-flow/cli-core/output`, whose built subpath is not
// resolvable under vitest in a fresh worktree (the same reason the repo's
// existing __tests__/commands.test.ts loads no tests in this env). We exercise
// the command's SERVICE layer (runExport → files on disk) directly, and the
// built-CLI command is smoke-tested out-of-band via `node dist … neural distill`.
// --- fixtures ---------------------------------------------------------------
function rec(over: Partial<RunTranscriptRecord>): RunTranscriptRecord {
return {
v: 1,
ts: new Date().toISOString(),
instance_id: 'inst-1',
task_hash: 'deadbeef',
model: 'z-ai/glm-5.2',
tier: 'cheap',
resolved: true,
resolved_source: 'api-success',
messages: [
{ role: 'system', content: 'You are a coding agent.' },
{ role: 'user', content: 'Fix the null-deref in cache.ts' },
{ role: 'assistant', content: 'Here is the patch.' },
],
model_patch: 'diff --git a/cache.ts b/cache.ts\n+ if (x) return;',
sample: 0,
source: 'agent-execute',
...over,
};
}
/** A resolved + a failed sample on the SAME instance/model — enough for a DPO pair. */
function dpoPairRecords(): RunTranscriptRecord[] {
return [
rec({ instance_id: 'inst-dpo', sample: 0, resolved: true }),
rec({
instance_id: 'inst-dpo', sample: 1, resolved: false, model_patch: '',
messages: [
{ role: 'system', content: 'You are a coding agent.' },
{ role: 'user', content: 'Fix the null-deref in cache.ts' },
{ role: 'assistant', content: 'I could not solve it.' },
],
}),
];
}
let weftAvailable = false;
beforeEach(async () => { weftAvailable = (await loadWeightEft()) !== null; });
// --- 0. capture module: run-transcript recorder (opt-in) --------------------
describe('run-transcript-recorder (the capture path)', () => {
let dir: string;
let prevEnabled: string | undefined;
let prevPath: string | undefined;
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'weft-cap-'));
prevEnabled = process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS;
prevPath = process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS_PATH;
__resetRunTranscriptRecorderForTests();
});
afterEach(() => {
if (prevEnabled === undefined) delete process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS; else process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS = prevEnabled;
if (prevPath === undefined) delete process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS_PATH; else process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS_PATH = prevPath;
__resetRunTranscriptRecorderForTests();
rmSync(dir, { recursive: true, force: true });
});
it('is OFF by default — recordRunTranscript is a no-op (no file written)', () => {
delete process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS;
const p = join(dir, 'run-transcripts.jsonl');
process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS_PATH = p;
__resetRunTranscriptRecorderForTests();
const r = recordRunTranscript({ task: 't', model: 'claude-haiku-4', tier: 'cheap', resolved: true, resolvedSource: 'api-success', messages: [{ role: 'user', content: 'hi' }] });
expect(r.recorded).toBe(false);
expect(existsSync(p)).toBe(false);
});
it('when enabled, writes a record that round-trips through readRunTranscripts', () => {
const p = join(dir, 'run-transcripts.jsonl');
process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS = '1';
process.env.CLAUDE_FLOW_RUN_TRANSCRIPTS_PATH = p;
__resetRunTranscriptRecorderForTests();
const r = recordRunTranscript({
task: 'Fix bug', model: 'claude-haiku-4', tier: 'cheap', resolved: true,
resolvedSource: 'output-verifier', messages: [{ role: 'user', content: 'Fix bug' }],
modelPatch: 'diff', source: 'agent-execute',
});
expect(r.recorded).toBe(true);
expect(r.instanceId).toBe(`run-${r.taskHash}`);
const { records, malformed } = readRunTranscripts(p);
expect(malformed).toBe(0);
expect(records).toHaveLength(1);
expect(records[0]).toMatchObject({ v: 1, model: 'claude-haiku-4', tier: 'cheap', resolved: true, resolved_source: 'output-verifier', model_patch: 'diff' });
expect(runTranscriptRecorderStatus().enabled).toBe(true);
});
it('tierForModel maps haiku→cheap, sonnet/opus→frontier', () => {
expect(tierForModel('claude-haiku-4')).toBe('cheap');
expect(tierForModel('claude-sonnet-4')).toBe('frontier');
expect(tierForModel('anthropic/claude-opus')).toBe('frontier');
expect(tierForModel(undefined)).toBe('frontier');
});
});
// --- 1. archive-builder -----------------------------------------------------
describe('buildArchiveFromRecords', () => {
it('maps ruflo run records to DarwinTrajectory[] preserving the contract fields', () => {
const { trajectories, stats } = buildArchiveFromRecords([
rec({ instance_id: 'a', tier: 'cheap', resolved: true }),
rec({ instance_id: 'b', tier: 'frontier', resolved: false, model: 'anthropic/claude-opus' }),
]);
expect(trajectories).toHaveLength(2);
const a = trajectories[0];
expect(a).toMatchObject({
instance_id: 'a', model: 'z-ai/glm-5.2', tier: 'cheap', resolved: true, sample: 0,
});
expect(a.messages[0].role).toBe('system');
expect(a.model_patch).toContain('diff --git');
expect(trajectories[1]).toMatchObject({ instance_id: 'b', tier: 'frontier', resolved: false });
expect(stats.total).toBe(2);
expect(stats.resolved).toBe(1);
expect(stats.byTier).toEqual({ cheap: 1, frontier: 1 });
});
it('skips records missing an instance_id or messages (never throws)', () => {
const { trajectories, stats } = buildArchiveFromRecords([
rec({ instance_id: 'ok' }),
rec({ instance_id: '', messages: [] }),
rec({ instance_id: 'empty', messages: [] }),
]);
expect(trajectories.map((t) => t.instance_id)).toEqual(['ok']);
expect(stats.skipped).toBe(2);
});
it('tallies resolved provenance and emits an honest PROXY note (no gold oracle)', () => {
const { stats, proxyNote } = buildArchiveFromRecords([
rec({ instance_id: 'a', resolved_source: 'api-success' }),
rec({ instance_id: 'b', resolved_source: 'output-verifier' }),
]);
expect(stats.byResolvedSource).toEqual({ 'api-success': 1, 'output-verifier': 1 });
expect(proxyNote).toMatch(/PROXY/);
expect(proxyNote).toMatch(/no SWE-bench gold oracle/i);
});
});
// --- 2. distill-export E2E (real weight-eft) --------------------------------
describe('runExport (E2E against real weight-eft)', () => {
it('produces SFT + DPO jsonl and a guard report from a synthetic archive', async () => {
if (!weftAvailable) { console.warn('weight-eft not installed — skipping E2E'); return; }
const { trajectories } = buildArchiveFromRecords(dpoPairRecords());
const res = await runExport({ archive: trajectories, evalHoldout: [] });
expect(res.degraded).toBe(false);
if (res.degraded) return;
// SFT: at least the resolved trajectory becomes a training row.
expect(res.sftRows).toBeGreaterThanOrEqual(1);
expect(res.sftJsonl.trim().split('\n').length).toBe(res.sftRows);
expect(JSON.parse(res.sftJsonl.trim().split('\n')[0])).toHaveProperty('messages');
// DPO: the resolved+failed pair on the same instance yields a preference row.
expect(res.dpoRows).toBeGreaterThanOrEqual(1);
const dpo0 = JSON.parse(res.dpoJsonl.trim().split('\n')[0]);
expect(dpo0).toHaveProperty('chosen');
expect(dpo0).toHaveProperty('rejected');
// Report carries the guard accounting.
expect(res.report).toMatchObject({ sftRows: res.sftRows, dpoRows: res.dpoRows });
expect(res.report).toHaveProperty('droppedRewardHacked');
expect(res.report).toHaveProperty('excludedByHoldout');
});
it('honors the contamination guard: holdout instances are excluded', async () => {
if (!weftAvailable) return;
const { trajectories } = buildArchiveFromRecords([rec({ instance_id: 'held' }), rec({ instance_id: 'kept', task_hash: 'x' })]);
const res = await runExport({ archive: trajectories, evalHoldout: ['held'] });
expect(res.degraded).toBe(false);
if (res.degraded) return;
expect(res.report.excludedByHoldout).toBe(1);
expect(res.report.sftInstanceIds).not.toContain('held');
});
it('runPlan emits a two-stage GPU plan with a ruvllm command ($0 dry-run)', async () => {
if (!weftAvailable) return;
const res = await runPlan({ sftPath: '/tmp/sft.jsonl', dpoPath: '/tmp/dpo.jsonl' });
expect(res.degraded).toBe(false);
if (res.degraded) return;
expect(res.base).toEqual(DEFAULT_BASE_MODEL);
expect(res.sft.command).toMatch(/ruvllm|microlora|sft/i);
expect(res.dpo.command).toBeTruthy();
});
});
// --- 2b. export → files on disk (the command's service path) ----------------
describe('distill-export writes sft.jsonl / dpo.jsonl / export-report.json', () => {
let dir: string;
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'weft-cli-')); });
afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
it('persists the exact artifacts the CLI `neural distill export` emits', async () => {
if (!weftAvailable) return;
const { trajectories, stats, proxyNote } = buildArchiveFromRecords(dpoPairRecords());
const res = await runExport({ archive: trajectories, evalHoldout: [] });
expect(res.degraded).toBe(false);
if (res.degraded) return;
// Mirror the command's write path.
const outDir = join(dir, 'out');
mkdirSync(outDir, { recursive: true });
writeFileSync(join(outDir, 'sft.jsonl'), res.sftJsonl);
writeFileSync(join(outDir, 'dpo.jsonl'), res.dpoJsonl);
writeFileSync(join(outDir, 'export-report.json'), JSON.stringify({ report: res.report, archiveStats: stats, proxyNote }, null, 2));
expect(existsSync(join(outDir, 'sft.jsonl'))).toBe(true);
expect(existsSync(join(outDir, 'dpo.jsonl'))).toBe(true);
expect(existsSync(join(outDir, 'export-report.json'))).toBe(true);
const report = JSON.parse(readFileSync(join(outDir, 'export-report.json'), 'utf8'));
expect(report).toHaveProperty('report');
expect(report.proxyNote).toMatch(/PROXY/);
expect(readFileSync(join(outDir, 'sft.jsonl'), 'utf8').trim().length).toBeGreaterThan(0);
});
});
// --- 3. degraded path (weight-eft absent) -----------------------------------
describe('graceful degradation (ADR-150)', () => {
const failingImporter = async () => { throw new Error("Cannot find module '@metaharness/weight-eft'"); };
it('runExport returns {degraded:true} when weight-eft cannot load', async () => {
const { trajectories } = buildArchiveFromRecords([rec({})]);
const res = await runExport({ archive: trajectories, importer: failingImporter });
expect(res).toEqual({ degraded: true, reason: 'weight-eft-not-available' });
});
it('runPlan degrades without throwing', async () => {
const res = await runPlan({ sftPath: 'a', dpoPath: 'b', importer: failingImporter });
expect(res.degraded).toBe(true);
});
it('loadWeightEft returns null (not throw) on a broken import', async () => {
expect(await loadWeightEft(failingImporter)).toBeNull();
});
});
// --- 4. remote-train construction + dry-run ---------------------------------
describe('buildRemoteTrainInvocation (pure command construction)', () => {
it('builds correct ssh/rsync/ruvllm argv for host + model + paths', () => {
const plan = buildRemoteTrainInvocation({
host: 'gpu-box', base: 'Qwen/Qwen2.5-Coder-7B-Instruct',
sftPath: '/local/sft.jsonl', dpoPath: '/local/dpo.jsonl',
adapterDir: '/local/adapters', sshUser: 'ruv', sshPort: 2222,
runId: 'run42', adapterPrefix: 'ruflo-weft',
});
// ssh reachability probe targets user@host on the chosen port
expect(plan.preflight[0].argv).toEqual(['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=8', '-p', '2222', 'ruv@gpu-box', 'true']);
// rsync sends the LOCAL sft file up to the remote workdir
const rsyncSft = plan.steps.find((s) => s.label === 'rsync sft up')!;
expect(rsyncSft.argv[0]).toBe('rsync');
expect(rsyncSft.argv).toContain('/local/sft.jsonl');
expect(rsyncSft.argv[rsyncSft.argv.length - 1]).toContain('ruv@gpu-box:');
// ruvllm sft uses the base model + writes an sft adapter
const sftStep = plan.steps.find((s) => s.label.startsWith('ruvllm sft'))!;
expect(sftStep.argv.join(' ')).toContain('ruvllm microlora sft --base Qwen/Qwen2.5-Coder-7B-Instruct --data sft.jsonl');
expect(sftStep.argv.join(' ')).toContain('ruflo-weft-run42-sft');
// ruvllm dpo initializes from the sft adapter (on-policy)
const dpoStep = plan.steps.find((s) => s.label.startsWith('ruvllm dpo'))!;
expect(dpoStep.argv.join(' ')).toContain('ruvllm microlora dpo --base Qwen/Qwen2.5-Coder-7B-Instruct --init-from ruflo-weft-run42-sft');
// adapter fetched back into the local dir
const back = plan.steps.find((s) => s.label === 'rsync adapter back')!;
expect(back.argv[back.argv.length - 1]).toContain('/local/adapters/ruflo-weft-run42-dpo');
expect(plan.humanCommands.length).toBe(plan.preflight.length + plan.steps.length);
});
it('defaults host param from args and uses port 22 + default base', () => {
const plan = buildRemoteTrainInvocation({ host: 'h', sftPath: 's', dpoPath: 'd', runId: 'r' });
expect(plan.base).toBe(DEFAULT_BASE_MODEL.id);
expect(plan.preflight[0].argv).toContain('-p');
expect(plan.preflight[0].argv[plan.preflight[0].argv.indexOf('-p') + 1]).toBe('22');
});
});
describe('runRemoteTrain (spend-gated; spawn stubbed — never touches a live host)', () => {
const okSpawn = (): SpawnLike => ({ status: 0, stdout: 'ok', stderr: '' });
const args = { host: 'gpu-box', sftPath: '/l/sft.jsonl', dpoPath: '/l/dpo.jsonl', runId: 'r1' };
it('BARE dry-run is fully OFFLINE — contacts nothing (no implicit remote exec)', async () => {
const spawned: string[] = [];
const spawn = (cmd: string, argv: string[]): SpawnLike => { spawned.push([cmd, ...argv].join(' ')); return okSpawn(); };
const res = await runRemoteTrain({ ...args, spawn });
expect(res.degraded).toBe(false);
if (res.degraded) return;
expect(res.mode).toBe('dry-run');
expect(res.steps).toBeUndefined();
expect(res.plan.humanCommands.some((c) => c.includes('ruvllm microlora sft'))).toBe(true);
// NOTHING spawned — not even a reachability ssh (adversarial RC hardening).
expect(spawned).toEqual([]);
expect(res.reason).toMatch(/offline/i);
});
it('--preflight opts into the read-only probes (still no training)', async () => {
const spawned: string[] = [];
const spawn = (cmd: string, argv: string[]): SpawnLike => { spawned.push([cmd, ...argv].join(' ')); return okSpawn(); };
const res = await runRemoteTrain({ ...args, preflight: true, spawn });
expect(res.degraded).toBe(false);
if (res.degraded) return;
expect(res.mode).toBe('dry-run');
// the 3 read-only preflight probes ran; no rsync / ruvllm training
expect(spawned.length).toBeGreaterThan(0);
expect(spawned.every((c) => c.startsWith('ssh'))).toBe(true);
expect(spawned.some((c) => c.includes('rsync') || c.includes('microlora'))).toBe(false);
});
it('--execute WITHOUT --yes is refused (real GPU spend needs the second gate)', async () => {
const res = await runRemoteTrain({ ...args, execute: true, spawn: okSpawn });
expect(res.degraded).toBe(false);
if (res.degraded) return;
expect(res.mode).toBe('refused');
expect(res.reason).toMatch(/--yes/);
});
it('--execute --yes on an UNREACHABLE host aborts before any data transfer', async () => {
// ssh reachability probe fails → preflight-failed, no rsync/training
const spawned: string[] = [];
const spawn = (cmd: string, argv: string[]): SpawnLike => {
spawned.push([cmd, ...argv].join(' '));
return { status: 255, stdout: '', stderr: 'ssh: connect timed out' };
};
const res = await runRemoteTrain({ ...args, execute: true, yes: true, spawn });
expect(res.degraded).toBe(false);
if (res.degraded) return;
expect(res.mode).toBe('preflight-failed');
expect(spawned.some((c) => c.includes('rsync') || c.includes('microlora'))).toBe(false);
});
});