harness-everything 0.47.1 → 0.47.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/ci.yml +14 -0
- package/CHANGELOG.md +18 -0
- package/benchmarks/issue290-pilot/README.md +199 -0
- package/benchmarks/issue290-pilot/analyze.py +299 -0
- package/benchmarks/issue290-pilot/analyze.test.py +146 -0
- package/benchmarks/issue290-pilot/check.py +19 -0
- package/benchmarks/issue290-pilot/evidence/archive.json +7 -0
- package/benchmarks/issue290-pilot/evidence/group-summary.json +684 -0
- package/benchmarks/issue290-pilot/evidence/inventory.json +3728 -0
- package/benchmarks/issue290-pilot/evidence/ledger.json +4644 -0
- package/benchmarks/issue290-pilot/evidence/replay/pairs.json +482 -0
- package/benchmarks/issue290-pilot/evidence/replay/provenance.json +24 -0
- package/benchmarks/issue290-pilot/evidence/replay/tables.md +37 -0
- package/benchmarks/issue290-pilot/evidence.tar.gz +0 -0
- package/benchmarks/issue290-pilot/export.py +110 -0
- package/benchmarks/issue290-pilot/export.test.py +65 -0
- package/benchmarks/issue290-pilot/fable-supplement.py +55 -0
- package/benchmarks/issue290-pilot/fixture.test.py +28 -0
- package/benchmarks/issue290-pilot/fixtures/polyglot/instructions.md +19 -0
- package/benchmarks/issue290-pilot/fixtures/polyglot/package.json +29 -0
- package/benchmarks/issue290-pilot/fixtures/polyglot/simple-linked-list.js +44 -0
- package/benchmarks/issue290-pilot/fixtures/polyglot/simple-linked-list.spec.js +131 -0
- package/benchmarks/issue290-pilot/fixtures/quixbugs/LICENSE +7 -0
- package/benchmarks/issue290-pilot/fixtures/quixbugs/cases.json +7 -0
- package/benchmarks/issue290-pilot/fixtures/quixbugs/find_in_sorted.py +31 -0
- package/benchmarks/issue290-pilot/fixtures/quixbugs/test_find_in_sorted.py +15 -0
- package/benchmarks/issue290-pilot/frozen-documents.json +41 -0
- package/benchmarks/issue290-pilot/grader.cjs +43 -0
- package/benchmarks/issue290-pilot/grader.test.cjs +10 -0
- package/benchmarks/issue290-pilot/hook.test.cjs +27 -0
- package/benchmarks/issue290-pilot/host-hook.cjs +35 -0
- package/benchmarks/issue290-pilot/metrics.test.py +131 -0
- package/benchmarks/issue290-pilot/preflight/amendment.md +32 -0
- package/benchmarks/issue290-pilot/preflight/environment.json +15 -0
- package/benchmarks/issue290-pilot/preflight/fable-amendment.md +20 -0
- package/benchmarks/issue290-pilot/preflight/fable-frozen-files.json +4 -0
- package/benchmarks/issue290-pilot/preflight/fable-infrastructure-amendment.md +27 -0
- package/benchmarks/issue290-pilot/preflight/fable-retry-frozen-files.json +4 -0
- package/benchmarks/issue290-pilot/preflight/fable-supplement-v1.py +47 -0
- package/benchmarks/issue290-pilot/preflight/frozen-files.json +18 -0
- package/benchmarks/issue290-pilot/preflight/graders.json +67 -0
- package/benchmarks/issue290-pilot/pricing-assumptions.json +29 -0
- package/benchmarks/issue290-pilot/protocol.json +32 -0
- package/benchmarks/issue290-pilot/read_metrics.py +166 -0
- package/benchmarks/issue290-pilot/replay.py +167 -0
- package/benchmarks/issue290-pilot/reporting.py +82 -0
- package/benchmarks/issue290-pilot/run.py +262 -0
- package/benchmarks/issue290-pilot/sources.json +10 -0
- package/benchmarks/issue290-pilot/verification/analyzer-tests.log +5 -0
- package/benchmarks/issue290-pilot/verification/final-gates.json +58 -0
- package/benchmarks/issue290-pilot/verification/fixture-tests.log +5 -0
- package/benchmarks/issue290-pilot/verification/gates.json +58 -0
- package/benchmarks/issue290-pilot/verification/npm-audit.json +84 -0
- package/benchmarks/issue290-pilot/verification/offline-correction.json +59 -0
- package/benchmarks/issue290-pilot/verification/post-report/helper-tests.log +22 -0
- package/benchmarks/issue290-pilot/verification/post-report/test-consistency.log +626 -0
- package/benchmarks/issue290-pilot/verification/post-report/test-references.log +6 -0
- package/benchmarks/issue290-pilot/verification/post-report/waza-tokens.log +710 -0
- package/benchmarks/issue290-pilot/verification/test-collision.log +28 -0
- package/benchmarks/issue290-pilot/verification/test-consistency.log +28 -0
- package/benchmarks/issue290-pilot/verification/test-docs-capabilities.log +131 -0
- package/benchmarks/issue290-pilot/verification/test-mechanism.log +1829 -0
- package/benchmarks/issue290-pilot/verification/test-mutations.log +16 -0
- package/benchmarks/issue290-pilot/verification/test-platform-compatibility.log +9 -0
- package/benchmarks/issue290-pilot/verification/test-plugin-openai.log +6 -0
- package/benchmarks/issue290-pilot/verification/test-plugin-submission.log +5 -0
- package/benchmarks/issue290-pilot/verification/test-references.log +6 -0
- package/benchmarks/issue290-pilot/verification/test-release.log +59 -0
- package/benchmarks/issue290-pilot/verification/test-repo-contract.log +5 -0
- package/benchmarks/issue290-pilot/verification/test-routing-invariants.log +785 -0
- package/benchmarks/issue290-pilot/verification/test-routing-skills.log +28 -0
- package/benchmarks/issue290-pilot/verification/test.log +3373 -0
- package/benchmarks/issue290-pilot/verification/verification-report.md +76 -0
- package/benchmarks/issue290-pilot/verification/waza-tokens.log +710 -0
- package/benchmarks/issue290-pilot/verification/with-dependencies/gates.json +26 -0
- package/benchmarks/issue290-pilot/verification/with-dependencies/test-collision.log +8 -0
- package/benchmarks/issue290-pilot/verification/with-dependencies/test-consistency.log +626 -0
- package/benchmarks/issue290-pilot/verification/with-dependencies/test-mechanism.log +1693 -0
- package/benchmarks/issue290-pilot/verification/with-dependencies/test-release.log +38 -0
- package/benchmarks/issue290-pilot/verification/with-dependencies/test-routing-skills.log +44 -0
- package/benchmarks/issue290-pilot/verification/with-dependencies/test.log +3253 -0
- package/ci/lib/opencode-verification-loader.js +24 -0
- package/ci/mechanism-2p-opencode-v2-entrypoint.test.js +8 -1
- package/ci/mechanism-325-f3-derived-views.test.js +42 -2
- package/ci/mechanism-338-opencode-async.test.js +229 -0
- package/ci/mechanism-341-opencode-lane.test.js +14 -3
- package/ci/mechanism-36-verify-timeout.test.js +7 -168
- package/docs/platform-capabilities.md +8 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-check.js +5 -4
- package/hooks/scripts/lib/doc-reconciliation-provider.js +1 -1
- package/opencode-plugin/README.md +28 -30
- package/opencode-plugin/index.mjs +249 -112
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-check.js +5 -4
- package/sbom.cdx.json +7 -7
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const vm = require('vm');
|
|
4
|
+
const acorn = require('acorn');
|
|
5
|
+
const childProcess = require('child_process');
|
|
6
|
+
|
|
7
|
+
// Load the private runner without adding exports to the portable plugin's
|
|
8
|
+
// host-discovery surface. Parsing preserves async declarations and boundaries.
|
|
9
|
+
module.exports = function loadRunner(root, overrides = {}) {
|
|
10
|
+
const source = fs.readFileSync(path.join(root, 'opencode-plugin/index.mjs'), 'utf8');
|
|
11
|
+
const names = new Set(['FAILURE_MARKER_PATTERNS', 'CONTEXT_MARKER_PATTERNS',
|
|
12
|
+
'VERIFY_TIMEOUT_ENV', 'DEFAULT_VERIFY_TIMEOUT_MS', 'VERIFY_OUTPUT_LIMIT',
|
|
13
|
+
'VERIFY_LINE_LIMIT', 'VERIFY_KILL_GRACE_MS', 'normalizeFailureLine',
|
|
14
|
+
'firstFailureMarker', 'buildFailureSignature', 'getAvailableScripts',
|
|
15
|
+
'verifyTimeoutMs', 'createVerificationCapture', 'terminateVerificationChild',
|
|
16
|
+
'runVerificationCommand', 'runVerification']);
|
|
17
|
+
const ast = acorn.parse(source, { ecmaVersion: 'latest', sourceType: 'module' });
|
|
18
|
+
const declarations = ast.body.filter(node => names.has(node.id?.name || node.declarations?.[0]?.id?.name));
|
|
19
|
+
const context = { process, Buffer, setTimeout, clearTimeout, setInterval, clearInterval,
|
|
20
|
+
createHash: require('crypto').createHash, spawn: childProcess.spawn,
|
|
21
|
+
existsSync: fs.existsSync, readFileSync: fs.readFileSync, join: path.join, ...overrides };
|
|
22
|
+
return vm.runInNewContext(declarations.map(node => source.slice(node.start, node.end)).join('\n')
|
|
23
|
+
+ '\n;({ runVerification, runVerificationCommand, createVerificationCapture, buildFailureSignature, verifyTimeoutMs })', context);
|
|
24
|
+
};
|
|
@@ -77,6 +77,13 @@ helper.check(
|
|
|
77
77
|
|
|
78
78
|
const push = (event) => eventQueue.push(event);
|
|
79
79
|
const settle = () => new Promise((resolve) => setTimeout(resolve, 50));
|
|
80
|
+
const waitFor = async (predicate) => {
|
|
81
|
+
const deadline = Date.now() + 5000;
|
|
82
|
+
while (!predicate()) {
|
|
83
|
+
if (Date.now() > deadline) throw new Error('V2 async verification did not settle');
|
|
84
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
85
|
+
}
|
|
86
|
+
};
|
|
80
87
|
|
|
81
88
|
const mod = await import(pathToFileURL(path.join(helper.root, 'opencode-plugin', 'index.mjs')).href);
|
|
82
89
|
const definition = mod.default;
|
|
@@ -113,7 +120,7 @@ helper.check(
|
|
|
113
120
|
for (let i = 1; i <= 3; i++) {
|
|
114
121
|
await after({ tool: 'edit', sessionID: 's1', id: `c${i}`, input: {} });
|
|
115
122
|
push({ type: 'session.idle', sessionID: 's1' });
|
|
116
|
-
await
|
|
123
|
+
await waitFor(() => prompts.length === i);
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
helper.check(
|
|
@@ -228,7 +228,7 @@ function caseD4(leg) {
|
|
|
228
228
|
fs.writeFileSync(candidate, differing);
|
|
229
229
|
const before = sha256(path.join(ws, 'docs', 'index.md'));
|
|
230
230
|
const result = runCompare(leg.dir, { workspace: ws, config: CONFIG, view: 'docs/index.md', candidate, status: 'ok' });
|
|
231
|
-
same(result.status, 1,
|
|
231
|
+
same(result.status, 1, `exit; stdout=${truncate(result.stdout)}; stderr=${truncate(result.stderr)}`);
|
|
232
232
|
if (!JSON.stringify(jsonOut(result)).includes('stale')) throw new Error(`result state is not stale: ${truncate(result.stdout)}`);
|
|
233
233
|
same(sha256(path.join(ws, 'docs', 'index.md')), before, 'target preserved');
|
|
234
234
|
}
|
|
@@ -364,6 +364,46 @@ function caseD13(leg) {
|
|
|
364
364
|
}
|
|
365
365
|
assertPerLeg('case-d13:crlf-candidate-not-normalized', leg => caseD13(leg));
|
|
366
366
|
|
|
367
|
+
// Windows file IDs can exceed Number's exact integer range. Exercise the real
|
|
368
|
+
// CLI with deterministic stat identities so runner allocation cannot hide it.
|
|
369
|
+
assertPerLeg('case-d13b:64-bit-file-identities-remain-distinct', leg => {
|
|
370
|
+
const { ws } = viewWorkspace();
|
|
371
|
+
writeTarget(ws);
|
|
372
|
+
const dir = candidateDir();
|
|
373
|
+
const candidate = path.join(dir, 'candidate.md');
|
|
374
|
+
const target = path.join(ws, 'docs', 'index.md');
|
|
375
|
+
fs.writeFileSync(candidate, 'different bytes\n');
|
|
376
|
+
const large = 2n ** 60n;
|
|
377
|
+
same(Number(large), Number(large + 1n), 'fixture must collide as Number');
|
|
378
|
+
for (const scenario of ['distinct-inode', 'distinct-device', 'alias']) {
|
|
379
|
+
const preload = path.join(dir, `${scenario}.cjs`);
|
|
380
|
+
fs.writeFileSync(preload, `
|
|
381
|
+
const fs = require('fs');
|
|
382
|
+
const path = require('path');
|
|
383
|
+
const original = fs.statSync;
|
|
384
|
+
fs.statSync = (file, options) => {
|
|
385
|
+
const result = original(file, options);
|
|
386
|
+
const name = path.resolve(String(file));
|
|
387
|
+
if (name === ${JSON.stringify(candidate)} || name === ${JSON.stringify(target)}) {
|
|
388
|
+
const candidate = name === ${JSON.stringify(candidate)};
|
|
389
|
+
const convert = value => options && options.bigint ? value : Number(value);
|
|
390
|
+
result.ino = convert(${large}n + (candidate && ${JSON.stringify(scenario)} === 'distinct-inode' ? 1n : 0n));
|
|
391
|
+
result.dev = convert(${large}n + (candidate && ${JSON.stringify(scenario)} === 'distinct-device' ? 1n : 0n));
|
|
392
|
+
}
|
|
393
|
+
return result;
|
|
394
|
+
};
|
|
395
|
+
`);
|
|
396
|
+
const result = runCompare(leg.dir, {
|
|
397
|
+
workspace: ws, config: CONFIG, view: 'docs/index.md', candidate, status: 'ok',
|
|
398
|
+
extra: { env: { ...process.env, NODE_OPTIONS: `--require "${preload.replace(/\\/g, '/')}"` } }
|
|
399
|
+
});
|
|
400
|
+
const alias = scenario === 'alias';
|
|
401
|
+
same(result.status, alias ? 2 : 1, `${scenario} exit`);
|
|
402
|
+
same(jsonOut(result).result, alias ? 'comparison-unavailable' : 'stale', `${scenario} verdict`);
|
|
403
|
+
if (alias) same(jsonOut(result).reason, 'candidate aliases the target', 'alias protection retained');
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
367
407
|
// D14: every compare-derived lane above (plus D1) printed exactly one JSON object on stdout.
|
|
368
408
|
{
|
|
369
409
|
const id = 'case-d14:stdout-single-json-object';
|
|
@@ -487,7 +527,7 @@ function mp3Apply() {
|
|
|
487
527
|
const dir = scriptCopy('f3 derived mp3 ');
|
|
488
528
|
// The guard's only decision line — elsewhere in the mutant the script stays intact, so the
|
|
489
529
|
// aliased run proceeds to a real byte comparison (exit 0, equal) instead of crashing.
|
|
490
|
-
applyMutation(dir, 'return x.ino >
|
|
530
|
+
applyMutation(dir, 'return x.ino > 0n && x.ino === y.ino && x.dev === y.dev;', 'return false;', 'mp3 sameFile identity guard');
|
|
491
531
|
return dir;
|
|
492
532
|
}
|
|
493
533
|
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
|
+
const { pathToFileURL } = require('url');
|
|
6
|
+
const helper = require('./test-helper');
|
|
7
|
+
|
|
8
|
+
(async () => {
|
|
9
|
+
const workspace = helper.tempDir('.mechanism-test-338-async');
|
|
10
|
+
fs.mkdirSync(workspace, { recursive: true });
|
|
11
|
+
execFileSync('git', ['init', '-q'], { cwd: workspace });
|
|
12
|
+
const prompts = [];
|
|
13
|
+
const client = { session: { prompt: async value => { prompts.push(value.body.parts[0].text); } } };
|
|
14
|
+
const mod = await import(pathToFileURL(path.join(helper.root, 'opencode-plugin/index.mjs')).href);
|
|
15
|
+
const { HarnessEnforcement } = mod;
|
|
16
|
+
const hooks = await HarnessEnforcement({ client, directory: workspace });
|
|
17
|
+
const runner = require('./lib/opencode-verification-loader')(helper.root);
|
|
18
|
+
const configure = scripts => fs.writeFileSync(path.join(workspace, 'package.json'), JSON.stringify({ name: 'async-fixture', scripts }));
|
|
19
|
+
// npm uses cmd.exe on Windows: JSON string escaping is not shell quoting.
|
|
20
|
+
// Keep source out of shell commands. setup-node places this Node on PATH;
|
|
21
|
+
// a quoted absolute executable adds another cmd.exe /s quoting boundary.
|
|
22
|
+
const nodeFile = file => `node "${file}"`;
|
|
23
|
+
let fixtureID = 0;
|
|
24
|
+
const node = program => {
|
|
25
|
+
const file = `program-${++fixtureID}.cjs`;
|
|
26
|
+
fs.writeFileSync(path.join(workspace, file), `require('fs').writeFileSync('${file}.started', 'yes');\n${program}`);
|
|
27
|
+
return nodeFile(file);
|
|
28
|
+
};
|
|
29
|
+
let calls = 0;
|
|
30
|
+
const edit = sessionID => hooks['tool.execute.after']({ tool: 'edit', sessionID, callID: `338-${++calls}`, args: { filePath: 'code.js' } }, {});
|
|
31
|
+
const idle = sessionID => hooks.event({ event: { type: 'session.idle', properties: { sessionID } } });
|
|
32
|
+
const waitFor = async predicate => {
|
|
33
|
+
const deadline = Date.now()+5000;
|
|
34
|
+
while (!predicate()) {
|
|
35
|
+
if (Date.now() > deadline) throw new Error('fixture did not become ready');
|
|
36
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const slowCommand = node('setTimeout(() => {}, 600)');
|
|
41
|
+
configure({ test: slowCommand });
|
|
42
|
+
await edit('heartbeat');
|
|
43
|
+
let heartbeats = 0;
|
|
44
|
+
const ticker = setInterval(() => { heartbeats++; }, 20);
|
|
45
|
+
await idle('heartbeat');
|
|
46
|
+
clearInterval(ticker);
|
|
47
|
+
helper.check('338. slow verification leaves the plugin event loop responsive', heartbeats >= 3 && fs.existsSync(path.join(workspace, 'program-1.cjs.started')), `heartbeats=${heartbeats}; ${prompts.at(-1) || ''}`);
|
|
48
|
+
|
|
49
|
+
const largeBytes = 320 * 65536;
|
|
50
|
+
const largeCommand = node("for(let i=0;i<320;i++)process.stdout.write('x'.repeat(65536)); process.stdout.write('',()=>require('fs').writeFileSync('large-output-bytes.txt',String(320*65536))); console.error('AssertionError: large-output-check');process.exitCode=1");
|
|
51
|
+
configure({ test: largeCommand });
|
|
52
|
+
await edit('large-output');
|
|
53
|
+
await idle('large-output');
|
|
54
|
+
helper.check('338. output beyond the former 16 MiB cap preserves a real test verdict', fs.existsSync(path.join(workspace, 'large-output-bytes.txt')) && fs.readFileSync(path.join(workspace, 'large-output-bytes.txt'), 'utf8') === String(largeBytes) && /verification failed/.test(prompts.at(-1)) && /large-output-check/.test(prompts.at(-1)), prompts.at(-1));
|
|
55
|
+
// A producer receipt proves the npm script ran; an independent expected
|
|
56
|
+
// stream digest additionally proves the runner consumed every emitted byte.
|
|
57
|
+
const largeResult = await runner.runVerificationCommand(workspace, largeCommand, 10000);
|
|
58
|
+
const stdoutDigest = crypto.createHash('sha256').update('x'.repeat(largeBytes)).digest('hex');
|
|
59
|
+
const stderrDigest = crypto.createHash('sha256').update('AssertionError: large-output-check').digest('hex');
|
|
60
|
+
const expectedDigest = crypto.createHash('sha256').update(`stdout:${stdoutDigest}\nstderr:${stderrDigest}`).digest('hex').slice(0, 16);
|
|
61
|
+
helper.check('338. the complete >16 MiB producer stream reaches the runner',
|
|
62
|
+
largeBytes > 16*1024*1024 && largeResult.outcome === 'test_failure' && largeResult.exitCode === 1
|
|
63
|
+
&& largeResult.failureMarker === 'AssertionError: large-output-check' && largeResult.evidenceDigest === expectedDigest,
|
|
64
|
+
JSON.stringify(largeResult));
|
|
65
|
+
helper.check('338. streamed diagnostics stay bounded', prompts.at(-1).length < 2600, `prompt bytes=${prompts.at(-1).length}`);
|
|
66
|
+
|
|
67
|
+
configure({ test: node("setTimeout(() => require('fs').writeFileSync('budget-test-finished.txt','yes'), 2200)"), lint: node("require('fs').writeFileSync('budget-lint-started.txt','yes');setTimeout(() => {}, 2200)"), build: node("require('fs').writeFileSync('must-not-build.txt','ran')") });
|
|
68
|
+
process.env.HARNESS_VERIFY_TIMEOUT_MS = '4000';
|
|
69
|
+
await edit('total-budget');
|
|
70
|
+
const started = Date.now();
|
|
71
|
+
const previous = prompts.length;
|
|
72
|
+
await idle('total-budget');
|
|
73
|
+
delete process.env.HARNESS_VERIFY_TIMEOUT_MS;
|
|
74
|
+
helper.check('338. all scripts share one verification budget', prompts.length === previous+1 && /timed out/.test(prompts.at(-1)) && fs.existsSync(path.join(workspace, 'budget-test-finished.txt')) && fs.existsSync(path.join(workspace, 'budget-lint-started.txt')), `duration=${Date.now()-started}; ${prompts.at(-1)}`);
|
|
75
|
+
helper.check('338. an expired sequence never launches later scripts', !fs.existsSync(path.join(workspace, 'must-not-build.txt')));
|
|
76
|
+
|
|
77
|
+
const missing = await runner.runVerificationCommand(path.join(workspace, 'missing-cwd'), 'npm test', 2000);
|
|
78
|
+
helper.check('338. spawn ENOENT is an execution error with no verdict', missing.outcome === 'execution_error' && missing.exitCode === null && /ENOENT/.test(missing.error), JSON.stringify(missing));
|
|
79
|
+
const pass = await runner.runVerificationCommand(workspace, node('process.exit(0)'), 2000);
|
|
80
|
+
helper.check('338. actual exit zero is pass', pass.success && pass.outcome === 'pass' && pass.exitCode === 0, JSON.stringify(pass));
|
|
81
|
+
const failed = await runner.runVerificationCommand(workspace, node("console.log('FAIL parser.test.js');console.error('AssertionError: parse alpha');process.exitCode=2"), 2000);
|
|
82
|
+
helper.check('338. nonzero exit preserves stderr marker and stdout suite context', failed.outcome === 'test_failure' && failed.exitCode === 2 && failed.failureMarker === 'FAIL parser.test.js: AssertionError: parse alpha' && /parse alpha/.test(failed.error), JSON.stringify(failed));
|
|
83
|
+
|
|
84
|
+
const signature = (out, err, chunks = 10000) => {
|
|
85
|
+
const stdout = runner.createVerificationCapture(), stderr = runner.createVerificationCapture();
|
|
86
|
+
for (let i=0; i<out.length; i+=chunks) stdout.write(out.slice(i, i+chunks));
|
|
87
|
+
for (let i=0; i<err.length; i+=chunks) stderr.write(err.slice(i, i+chunks));
|
|
88
|
+
const a = stdout.finish(), b = stderr.finish();
|
|
89
|
+
const marker = a.unsafeIdentity || b.unsafeIdentity ? null : a.marker
|
|
90
|
+
? `${a.markerContext ? `${a.markerContext}: ` : ''}${a.marker}`
|
|
91
|
+
: b.marker ? `${b.markerContext || a.context ? `${b.markerContext || a.context}: ` : ''}${b.marker}` : null;
|
|
92
|
+
return runner.buildFailureSignature({ command: 'npm test', outcome: 'test_failure', exitCode: 1,
|
|
93
|
+
failureMarker: marker, evidenceDigest: crypto.createHash('sha256').update(`stdout:${a.digest}\nstderr:${b.digest}`).digest('hex').slice(0,16) });
|
|
94
|
+
};
|
|
95
|
+
const longPrefix = `not ok 1 - ${'same'.repeat(1500)}`;
|
|
96
|
+
helper.check('338. truncated test identities use complete stream digests', signature(`${longPrefix} ALPHA\n`, '') !== signature(`${longPrefix} BETA\n`, ''));
|
|
97
|
+
const observedLong = [];
|
|
98
|
+
for (const name of ['ALPHA', 'BETA']) {
|
|
99
|
+
fs.writeFileSync(path.join(workspace, 'long-marker.js'), `console.log(${JSON.stringify(longPrefix+name)});process.exitCode=1`);
|
|
100
|
+
observedLong.push(await runner.runVerificationCommand(workspace, nodeFile('long-marker.js'), 2000));
|
|
101
|
+
}
|
|
102
|
+
helper.check('338. real long-marker child verdicts cannot collide after truncation', observedLong.every(value => value.outcome === 'test_failure' && value.failureMarker === null) && runner.buildFailureSignature(observedLong[0]) !== runner.buildFailureSignature(observedLong[1]));
|
|
103
|
+
helper.check('338. truncated suite contexts cannot merge distinct failures', signature(`FAIL ${longPrefix}ALPHA\n`, 'AssertionError: same assertion\n') !== signature(`FAIL ${longPrefix}BETA\n`, 'AssertionError: same assertion\n'));
|
|
104
|
+
const stableFailures = [];
|
|
105
|
+
for (const banner of ['ALPHA', 'BETA']) {
|
|
106
|
+
fs.writeFileSync(path.join(workspace, 'volatile-banner.js'), `process.stdout.write('${banner}'+'x'.repeat(17*1024*1024));console.error('AssertionError: stable failure');process.exitCode=1`);
|
|
107
|
+
stableFailures.push(await runner.runVerificationCommand(workspace, nodeFile('volatile-banner.js'), 3000));
|
|
108
|
+
}
|
|
109
|
+
helper.check('338. huge volatile non-marker banners preserve a later stable failure identity', stableFailures.every(value => value.failureMarker === 'AssertionError: stable failure') && runner.buildFailureSignature(stableFailures[0]) === runner.buildFailureSignature(stableFailures[1]));
|
|
110
|
+
const context = 'FAIL parser.test.js\n ● Console\n console.log\n ● payload\n';
|
|
111
|
+
const stderr = ' ● parser › alpha (1.2ms)\n';
|
|
112
|
+
helper.check('338. chunk boundaries preserve marker and stream context', signature(context, stderr, 1) === signature(context, stderr, 10000) && /FAIL parser.test.js: ● parser › alpha/.test(signature(context, stderr, 1)));
|
|
113
|
+
helper.check('338. markerless whitespace normalization survives chunk boundaries', signature(' progress \n still running ', ' last line ', 1) === signature('progress still running', 'last line', 10000));
|
|
114
|
+
|
|
115
|
+
// Both shell and nested child remain in the inherited process group.
|
|
116
|
+
fs.writeFileSync(path.join(workspace, 'grandchild.js'), "const fs=require('fs');process.on('SIGTERM',()=>{});setInterval(()=>fs.appendFileSync('heartbeat.txt','x'),20)");
|
|
117
|
+
fs.writeFileSync(path.join(workspace, 'worker.js'), "require('child_process').spawn(process.execPath,['grandchild.js'],{stdio:'inherit'});setInterval(()=>{},1000)");
|
|
118
|
+
const cleanupResult = await runner.runVerificationCommand(workspace, nodeFile('worker.js'), 700);
|
|
119
|
+
const heartbeat = path.join(workspace, 'heartbeat.txt');
|
|
120
|
+
const size = fs.existsSync(heartbeat) ? fs.statSync(heartbeat).size : -1;
|
|
121
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
122
|
+
helper.check('338. timeout stops nested SIGTERM-ignoring child output', cleanupResult.outcome === 'timeout' && size > 0 && fs.statSync(heartbeat).size === size, JSON.stringify({ cleanupResult, size }));
|
|
123
|
+
if (process.platform !== 'win32') {
|
|
124
|
+
const externalRunner = require('./lib/opencode-verification-loader')(helper.root, { spawn: (command, options) => {
|
|
125
|
+
const child = require('child_process').spawn(command, options);
|
|
126
|
+
setTimeout(() => { try { child.kill('SIGTERM'); } catch {} }, 300);
|
|
127
|
+
return child;
|
|
128
|
+
} });
|
|
129
|
+
const signalled = await externalRunner.runVerificationCommand(workspace, nodeFile('worker.js'), 3000);
|
|
130
|
+
const afterSignal = fs.statSync(heartbeat).size;
|
|
131
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
132
|
+
helper.check('338. early external signals are execution errors and also clean the group', signalled.outcome === 'execution_error' && signalled.signal === 'SIGTERM' && !signalled.timedOut && fs.statSync(heartbeat).size === afterSignal, JSON.stringify(signalled));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fs.writeFileSync(path.join(workspace, 'ready.js'), "require('fs').writeFileSync('ready.txt','yes');setInterval(()=>{},1000)");
|
|
136
|
+
configure({ test: nodeFile('ready.js') });
|
|
137
|
+
await edit('edited-during-run');
|
|
138
|
+
const beforeConcurrent = prompts.length;
|
|
139
|
+
const active = idle('edited-during-run');
|
|
140
|
+
await waitFor(() => fs.existsSync(path.join(workspace, 'ready.txt')));
|
|
141
|
+
await Promise.all([idle('edited-during-run'), edit('edited-during-run')]);
|
|
142
|
+
await active;
|
|
143
|
+
const real = fs.realpathSync(workspace);
|
|
144
|
+
const slug = path.basename(real).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'workspace';
|
|
145
|
+
const hash = crypto.createHash('sha1').update(process.platform === 'win32' ? real.toLowerCase() : real).digest('hex').slice(0,12);
|
|
146
|
+
const sessions = path.join(process.env.HARNESS_STATE_HOME, 'workspaces', `${slug}-${hash}`, 'state/sessions');
|
|
147
|
+
const editedState = JSON.parse(fs.readFileSync(path.join(sessions, 'edited-during-run/edit-state.json')));
|
|
148
|
+
helper.check('338. a concurrent edit cancels stale verification without duplicate follow-ups', editedState.verificationPending === true && editedState.editsSinceVerification === 2 && prompts.length === beforeConcurrent, JSON.stringify(editedState));
|
|
149
|
+
configure({ test: node('process.exit(0)') });
|
|
150
|
+
await idle('edited-during-run');
|
|
151
|
+
helper.check('338. a fresh idle verifies the newer edits', JSON.parse(fs.readFileSync(path.join(sessions, 'edited-during-run/edit-state.json'))).verificationPending === false);
|
|
152
|
+
|
|
153
|
+
fs.unlinkSync(path.join(workspace, 'ready.txt'));
|
|
154
|
+
configure({ test: nodeFile('ready.js') });
|
|
155
|
+
await edit('deleted-during-run');
|
|
156
|
+
const deleting = idle('deleted-during-run');
|
|
157
|
+
await waitFor(() => fs.existsSync(path.join(workspace, 'ready.txt')));
|
|
158
|
+
await hooks.event({ event: { type: 'session.deleted', properties: { sessionID: 'deleted-during-run' } } });
|
|
159
|
+
await deleting;
|
|
160
|
+
helper.check('338. session deletion aborts the runner without resurrecting state', !fs.existsSync(path.join(sessions, 'deleted-during-run')) && prompts.length === beforeConcurrent);
|
|
161
|
+
|
|
162
|
+
// The V2 adapter must keep consuming reset/delete events while idle
|
|
163
|
+
// verification is pending, and its disposer must await child cleanup.
|
|
164
|
+
const queue = [], registrations = new Map();
|
|
165
|
+
const dispose = await mod.default.setup({
|
|
166
|
+
location: { directory: workspace },
|
|
167
|
+
tool: { hook: async (name, callback) => { registrations.set(name, callback); } },
|
|
168
|
+
session: { prompt: async value => { prompts.push(value.text); } },
|
|
169
|
+
event: { subscribe: async function* ({ signal }) {
|
|
170
|
+
while (!signal.aborted) {
|
|
171
|
+
if (queue.length) yield queue.shift();
|
|
172
|
+
else await new Promise(resolve => setTimeout(resolve, 5));
|
|
173
|
+
}
|
|
174
|
+
} },
|
|
175
|
+
});
|
|
176
|
+
fs.writeFileSync(path.join(workspace, 'v2.js'), "const fs=require('fs');fs.writeFileSync('v2-ready.txt','yes');setInterval(()=>fs.appendFileSync('v2-heartbeat.txt','x'),20)");
|
|
177
|
+
configure({ test: nodeFile('v2.js') });
|
|
178
|
+
const v2Edit = sessionID => registrations.get('execute.after')({ tool: 'edit', sessionID, id: `338-v2-${++calls}`, input: { filePath: 'code.js' } });
|
|
179
|
+
const v2Ready = path.join(workspace, 'v2-ready.txt');
|
|
180
|
+
const v2Heartbeat = path.join(workspace, 'v2-heartbeat.txt');
|
|
181
|
+
for (const type of ['session.deleted', 'session.created']) {
|
|
182
|
+
const sessionID = `v2-${type}`;
|
|
183
|
+
if (fs.existsSync(v2Ready)) fs.unlinkSync(v2Ready);
|
|
184
|
+
await v2Edit(sessionID);
|
|
185
|
+
queue.push({ type: 'session.idle', sessionID });
|
|
186
|
+
await waitFor(() => fs.existsSync(v2Ready) && fs.existsSync(v2Heartbeat));
|
|
187
|
+
queue.push({ type, sessionID });
|
|
188
|
+
await waitFor(() => !fs.existsSync(path.join(sessions, sessionID)));
|
|
189
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
190
|
+
const stopped = fs.statSync(v2Heartbeat).size;
|
|
191
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
192
|
+
helper.check(`338. V2 consumes ${type} during verification and stops its child`, fs.statSync(v2Heartbeat).size === stopped && prompts.length === beforeConcurrent);
|
|
193
|
+
}
|
|
194
|
+
fs.unlinkSync(v2Ready);
|
|
195
|
+
await v2Edit('v2-disposed');
|
|
196
|
+
queue.push({ type: 'session.idle', sessionID: 'v2-disposed' });
|
|
197
|
+
await waitFor(() => fs.existsSync(v2Ready));
|
|
198
|
+
await dispose();
|
|
199
|
+
const stopped = fs.statSync(v2Heartbeat).size;
|
|
200
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
201
|
+
helper.check('338. V2 disposal waits for active child cleanup without a stale follow-up', fs.statSync(v2Heartbeat).size === stopped && prompts.length === beforeConcurrent);
|
|
202
|
+
// A new idle can arrive while an edit-cancelled child is still being reaped.
|
|
203
|
+
// That idle must verify the newer revision after cleanup, rather than vanish.
|
|
204
|
+
fs.unlinkSync(path.join(workspace, 'ready.txt'));
|
|
205
|
+
configure({ test: nodeFile('ready.js') });
|
|
206
|
+
await edit('idle-during-cleanup');
|
|
207
|
+
const cancellingIdle = idle('idle-during-cleanup');
|
|
208
|
+
await waitFor(() => fs.existsSync(path.join(workspace, 'ready.txt')));
|
|
209
|
+
await edit('idle-during-cleanup');
|
|
210
|
+
configure({ test: node('process.exit(0)') });
|
|
211
|
+
await Promise.all([cancellingIdle, idle('idle-during-cleanup')]);
|
|
212
|
+
const afterCleanup = JSON.parse(fs.readFileSync(path.join(sessions, 'idle-during-cleanup/edit-state.json')));
|
|
213
|
+
helper.check('338. idle arriving during cancellation verifies the latest edit after cleanup',
|
|
214
|
+
afterCleanup.verificationPending === false && afterCleanup.editsSinceVerification === 0 && prompts.length === beforeConcurrent,
|
|
215
|
+
JSON.stringify(afterCleanup));
|
|
216
|
+
|
|
217
|
+
fs.unlinkSync(path.join(workspace, 'ready.txt'));
|
|
218
|
+
configure({ test: nodeFile('ready.js') });
|
|
219
|
+
await edit('deleted-while-waiting');
|
|
220
|
+
const runningBeforeDelete = idle('deleted-while-waiting');
|
|
221
|
+
await waitFor(() => fs.existsSync(path.join(workspace, 'ready.txt')));
|
|
222
|
+
await edit('deleted-while-waiting');
|
|
223
|
+
const waitingDuringDelete = idle('deleted-while-waiting');
|
|
224
|
+
await hooks.event({ event: { type: 'session.deleted', properties: { sessionID: 'deleted-while-waiting' } } });
|
|
225
|
+
await Promise.all([runningBeforeDelete, waitingDuringDelete]);
|
|
226
|
+
helper.check('338. deletion invalidates an idle waiting for cancelled-child cleanup',
|
|
227
|
+
!fs.existsSync(path.join(sessions, 'deleted-while-waiting')) && prompts.length === beforeConcurrent);
|
|
228
|
+
helper.finish();
|
|
229
|
+
})().catch(error => { console.error(error); process.exit(1); });
|
|
@@ -153,6 +153,14 @@ async function startPlugin(workspace) {
|
|
|
153
153
|
return { registered, stream, dispose };
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
async function waitForIdleConsumer(predicate) {
|
|
157
|
+
const deadline = Date.now() + 5000;
|
|
158
|
+
while (!predicate()) {
|
|
159
|
+
assert.ok(Date.now() < deadline, 'asynchronous idle consumer did not complete');
|
|
160
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
156
164
|
function canonicalJson(value) {
|
|
157
165
|
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
158
166
|
if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonicalJson(value[key])]));
|
|
@@ -235,6 +243,7 @@ test('OpenCode capture → idle Stop whole path is refused without live qualific
|
|
|
235
243
|
}
|
|
236
244
|
|
|
237
245
|
await plugin.stream.dispatch({ type: 'session.idle', sessionID: sessionId });
|
|
246
|
+
await waitForIdleConsumer(() => assessmentSessions.filter(id => id === sessionId).length === 1);
|
|
238
247
|
const reportPath = path.join(sessionDir, 'doc-reconciliation-pilot.json');
|
|
239
248
|
const report = fs.existsSync(reportPath) ? JSON.parse(fs.readFileSync(reportPath, 'utf8')) : null;
|
|
240
249
|
if (assessmentSessions.filter(id => id === sessionId).length !== 1) {
|
|
@@ -245,7 +254,7 @@ test('OpenCode capture → idle Stop whole path is refused without live qualific
|
|
|
245
254
|
}
|
|
246
255
|
assert.deepEqual(findings, []);
|
|
247
256
|
} finally {
|
|
248
|
-
if (dispose) dispose();
|
|
257
|
+
if (dispose) await dispose();
|
|
249
258
|
if (originalAssess) require(PILOT).assess = originalAssess;
|
|
250
259
|
fixture.cleanup();
|
|
251
260
|
restoreEnv();
|
|
@@ -277,6 +286,7 @@ test('session idle dispatches the real consumer once across every existing early
|
|
|
277
286
|
const { sessionDir } = seedWorkflow(fixture, sessionId);
|
|
278
287
|
writeEditState(sessionDir, state);
|
|
279
288
|
await plugin.stream.dispatch({ type: 'session.idle', sessionID: sessionId });
|
|
289
|
+
await waitForIdleConsumer(() => assessmentSessions.filter(id => id === sessionId).length === 1);
|
|
280
290
|
const reportPath = path.join(sessionDir, 'doc-reconciliation-pilot.json');
|
|
281
291
|
const report = fs.existsSync(reportPath) ? JSON.parse(fs.readFileSync(reportPath, 'utf8')) : null;
|
|
282
292
|
if (assessmentSessions.filter(id => id === sessionId).length !== 1) {
|
|
@@ -288,12 +298,13 @@ test('session idle dispatches the real consumer once across every existing early
|
|
|
288
298
|
}
|
|
289
299
|
const cleanupSession = 'idle-no-verification-pending';
|
|
290
300
|
await plugin.stream.dispatch({ type: 'session.idle', sessionID: cleanupSession });
|
|
301
|
+
await waitForIdleConsumer(() => assessmentSessions.filter(id => id === cleanupSession).length === 2);
|
|
291
302
|
if (assessmentSessions.filter(id => id === cleanupSession).length !== 2) {
|
|
292
303
|
findings.push(`finally did not release the per-session idle guard for a second event: ${assessmentSessions.filter(id => id === cleanupSession).length} assessments`);
|
|
293
304
|
}
|
|
294
305
|
assert.deepEqual(findings, []);
|
|
295
306
|
} finally {
|
|
296
|
-
if (dispose) dispose();
|
|
307
|
+
if (dispose) await dispose();
|
|
297
308
|
pilot.assess = originalAssess;
|
|
298
309
|
fixture.cleanup();
|
|
299
310
|
restoreEnv();
|
|
@@ -332,7 +343,7 @@ test('V1/no-status, pilot-OFF, no-workflow, and Claude host controls stay fail-c
|
|
|
332
343
|
assert.equal(claudeContext.status, 'unavailable', 'OpenCode must not enter through Claude provider semantics');
|
|
333
344
|
assert.ok(claudeContext.diagnostics.some(item => item.code === 'unsupported-host'));
|
|
334
345
|
} finally {
|
|
335
|
-
if (dispose) dispose();
|
|
346
|
+
if (dispose) await dispose();
|
|
336
347
|
fixture.cleanup();
|
|
337
348
|
restoreEnv();
|
|
338
349
|
}
|
|
@@ -79,7 +79,7 @@ for (let i = 0; i < DEFAULTS.length; i += 1) {
|
|
|
79
79
|
assert.ok(/timed out after/.test(source), `${label} labels timeout failures as timeouts`);
|
|
80
80
|
assert.ok(/test_failure/.test(source), `${label} classifies confirmed failing checks`);
|
|
81
81
|
assert.ok(/execution_error/.test(source), `${label} classifies spawn-level execution errors`);
|
|
82
|
-
assert.ok(/maxBuffer/.test(source), `${label} bounds
|
|
82
|
+
assert.ok(i === 0 ? /VERIFY_LINE_LIMIT = 4096/.test(source) : /maxBuffer/.test(source), `${label} bounds retained output`);
|
|
83
83
|
const declared = Number(source.match(/^const DEFAULT_VERIFY_TIMEOUT_MS = (\d+);?$/m)[1]);
|
|
84
84
|
assert.ok(declared >= 300000, `${label} default budget (${declared}ms) clears a several-minute suite`);
|
|
85
85
|
}
|
|
@@ -95,15 +95,12 @@ assert.strictEqual(
|
|
|
95
95
|
extractConst(cliSource, 'VERIFY_TIMEOUT_ENV').match(/'([^']*)'/)[1],
|
|
96
96
|
'the plugin and the manual CLI read the same env override',
|
|
97
97
|
);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
extractConst(cliSource, 'VERIFY_MAX_BUFFER').match(/= (\d+ \* \d+ \* \d+)/)[1],
|
|
101
|
-
'the plugin and the manual CLI allow the same output buffer',
|
|
102
|
-
);
|
|
98
|
+
// The plugin now streams output; the standalone CLI retains its 16 MiB
|
|
99
|
+
// synchronous capture. Actual async runner contracts are exercised in 338.
|
|
103
100
|
|
|
104
101
|
// Behavior: budget resolution.
|
|
105
102
|
{
|
|
106
|
-
const context = loadVerificationLogic(
|
|
103
|
+
const context = loadVerificationLogic(cliSource);
|
|
107
104
|
assert.strictEqual(context.verifyTimeoutMs(), 600000, 'unset env uses the default budget');
|
|
108
105
|
for (const value of ['abc', '0', '-5', '']) {
|
|
109
106
|
context.process.env.HARNESS_VERIFY_TIMEOUT_MS = value;
|
|
@@ -114,167 +111,9 @@ assert.strictEqual(
|
|
|
114
111
|
assert.ok(!Number.isNaN(Number(context.verifyTimeoutMs())), 'the resolved budget is numeric');
|
|
115
112
|
}
|
|
116
113
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const calls = [];
|
|
121
|
-
context.execSync = (command, options) => {
|
|
122
|
-
calls.push({ command, options });
|
|
123
|
-
return '';
|
|
124
|
-
};
|
|
125
|
-
let result = context.runVerification('/nonexistent-workspace');
|
|
126
|
-
assert.strictEqual(result.allPassed, true, 'a passing script verifies');
|
|
127
|
-
assert.strictEqual(result.results[0].outcome, 'pass', 'a passing script is classified pass');
|
|
128
|
-
assert.strictEqual(calls.length, 1, 'one command ran');
|
|
129
|
-
assert.strictEqual(calls[0].command, 'npm test', 'the test script is the command');
|
|
130
|
-
assert.strictEqual(calls[0].options.timeout, 600000, 'execSync receives the default budget');
|
|
131
|
-
assert.strictEqual(calls[0].options.maxBuffer, 16 * 1024 * 1024, 'execSync receives the raised output buffer');
|
|
132
|
-
|
|
133
|
-
context.process.env.HARNESS_VERIFY_TIMEOUT_MS = '120';
|
|
134
|
-
calls.length = 0;
|
|
135
|
-
context.execSync = (command, options) => {
|
|
136
|
-
calls.push({ command, options });
|
|
137
|
-
return '';
|
|
138
|
-
};
|
|
139
|
-
context.runVerification('/nonexistent-workspace');
|
|
140
|
-
assert.strictEqual(calls[0].options.timeout, 120, 'execSync receives the overridden budget');
|
|
141
|
-
|
|
142
|
-
delete context.process.env.HARNESS_VERIFY_TIMEOUT_MS;
|
|
143
|
-
context.execSync = () => {
|
|
144
|
-
const error = new Error('spawnSync npm test ETIMEDOUT');
|
|
145
|
-
error.code = 'ETIMEDOUT';
|
|
146
|
-
error.signal = 'SIGTERM'; // the live execSync timeout kill shape
|
|
147
|
-
error.stdout = 'Harness OS - Self-Regression Test Suite\n';
|
|
148
|
-
throw error;
|
|
149
|
-
};
|
|
150
|
-
result = context.runVerification('/nonexistent-workspace');
|
|
151
|
-
assert.strictEqual(result.allPassed, false, 'a timeout is a verification failure');
|
|
152
|
-
assert.strictEqual(result.results[0].timedOut, true, 'the timeout is recorded as a timeout');
|
|
153
|
-
assert.strictEqual(result.results[0].outcome, 'timeout', 'a timeout is classified as timeout, not test_failure');
|
|
154
|
-
assert.ok(/timed out after 600s/.test(result.results[0].error), 'the failure names the budget');
|
|
155
|
-
assert.ok(/HARNESS_VERIFY_TIMEOUT_MS/.test(result.results[0].error), 'the failure names the override');
|
|
156
|
-
assert.ok(/Self-Regression/.test(result.results[0].error), 'the failure keeps the last output for diagnosis');
|
|
157
|
-
|
|
158
|
-
context.process.env.HARNESS_VERIFY_TIMEOUT_MS = '50';
|
|
159
|
-
context.execSync = () => {
|
|
160
|
-
const started = Date.now();
|
|
161
|
-
while (Date.now() - started < 120) { /* simulate a kill at the budget without an error code */ }
|
|
162
|
-
const error = new Error('Command failed: npm test');
|
|
163
|
-
error.stdout = 'partial output';
|
|
164
|
-
throw error;
|
|
165
|
-
};
|
|
166
|
-
result = context.runVerification('/nonexistent-workspace');
|
|
167
|
-
assert.strictEqual(result.results[0].timedOut, true, 'a kill at the budget is recognized without an ETIMEDOUT code');
|
|
168
|
-
assert.strictEqual(result.results[0].outcome, 'timeout', 'a kill at the budget is classified as timeout');
|
|
169
|
-
|
|
170
|
-
// P2: a signal is external termination; only budget expiry is a timeout.
|
|
171
|
-
context.process.env.HARNESS_VERIFY_TIMEOUT_MS = '600000';
|
|
172
|
-
context.execSync = () => {
|
|
173
|
-
const error = new Error('Command failed: npm test');
|
|
174
|
-
error.signal = 'SIGTERM'; // killed early by a user, host, or process manager
|
|
175
|
-
throw error; // status stays null: no verdict from the suite
|
|
176
|
-
};
|
|
177
|
-
result = context.runVerification('/nonexistent-workspace');
|
|
178
|
-
assert.strictEqual(result.results[0].outcome, 'execution_error', 'an early external SIGTERM is an execution error, not a timeout');
|
|
179
|
-
assert.strictEqual(result.results[0].timedOut, false, 'an early external SIGTERM does not claim a timeout');
|
|
180
|
-
assert.strictEqual(result.results[0].signal, 'SIGTERM', 'the signal is retained for diagnostics');
|
|
181
|
-
assert.ok(/signal SIGTERM/.test(result.results[0].error), 'the execution error names the signal');
|
|
182
|
-
|
|
183
|
-
context.process.env.HARNESS_VERIFY_TIMEOUT_MS = '50';
|
|
184
|
-
context.execSync = () => {
|
|
185
|
-
const started = Date.now();
|
|
186
|
-
while (Date.now() - started < 80) { /* killed once the budget already elapsed, like execSync's own timeout */ }
|
|
187
|
-
const error = new Error('spawnSync npm test SIGTERM');
|
|
188
|
-
error.signal = 'SIGTERM';
|
|
189
|
-
throw error;
|
|
190
|
-
};
|
|
191
|
-
result = context.runVerification('/nonexistent-workspace');
|
|
192
|
-
assert.strictEqual(result.results[0].outcome, 'timeout', 'a signal kill with no exit status once the budget elapsed is a timeout');
|
|
193
|
-
|
|
194
|
-
context.execSync = () => {
|
|
195
|
-
const started = Date.now();
|
|
196
|
-
while (Date.now() - started < 80) { /* slow, but it still produced a verdict */ }
|
|
197
|
-
const error = new Error('Command failed: npm test');
|
|
198
|
-
error.status = 1;
|
|
199
|
-
error.stdout = 'confirmed failure after a slow run';
|
|
200
|
-
throw error;
|
|
201
|
-
};
|
|
202
|
-
result = context.runVerification('/nonexistent-workspace');
|
|
203
|
-
assert.strictEqual(result.results[0].outcome, 'test_failure', 'a real exit status stays a confirmed failure even past the budget');
|
|
204
|
-
assert.strictEqual(result.results[0].timedOut, false, 'a confirmed verdict is not a timeout');
|
|
205
|
-
|
|
206
|
-
context.process.env.HARNESS_VERIFY_TIMEOUT_MS = '600000';
|
|
207
|
-
context.execSync = () => {
|
|
208
|
-
const error = new Error('Command failed: npm test');
|
|
209
|
-
error.status = 1;
|
|
210
|
-
error.stdout = 'real assertion failure';
|
|
211
|
-
throw error;
|
|
212
|
-
};
|
|
213
|
-
result = context.runVerification('/nonexistent-workspace');
|
|
214
|
-
assert.strictEqual(result.results[0].timedOut, false, 'a fast failure is not mislabeled as a timeout');
|
|
215
|
-
assert.strictEqual(result.results[0].outcome, 'test_failure', 'a non-zero exit is a confirmed test failure');
|
|
216
|
-
assert.strictEqual(result.results[0].error, 'real assertion failure', 'a fast failure reports its output');
|
|
217
|
-
|
|
218
|
-
context.execSync = () => {
|
|
219
|
-
const error = new Error('spawnSync npm test ENOBUFS');
|
|
220
|
-
error.code = 'ENOBUFS';
|
|
221
|
-
error.stdout = 'x'.repeat(300);
|
|
222
|
-
throw error;
|
|
223
|
-
};
|
|
224
|
-
result = context.runVerification('/nonexistent-workspace');
|
|
225
|
-
assert.strictEqual(result.results[0].outcome, 'execution_error', 'ENOBUFS is an execution error, not a test failure');
|
|
226
|
-
assert.ok(/execution error: ENOBUFS/.test(result.results[0].error), 'the execution error names its cause');
|
|
227
|
-
assert.ok(!/timed out/.test(result.results[0].error), 'ENOBUFS is not reported as a timeout');
|
|
228
|
-
|
|
229
|
-
context.execSync = () => {
|
|
230
|
-
const error = new Error('spawnSync npm test ENOENT');
|
|
231
|
-
error.code = 'ENOENT';
|
|
232
|
-
throw error;
|
|
233
|
-
};
|
|
234
|
-
result = context.runVerification('/nonexistent-workspace');
|
|
235
|
-
assert.strictEqual(result.results[0].outcome, 'execution_error', 'a missing executable is an execution error');
|
|
236
|
-
assert.strictEqual(result.results[0].exitCode, null, 'a spawn-level error has no exit status');
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Behavior: test_failure evidence spans both streams in full (P1).
|
|
240
|
-
{
|
|
241
|
-
const context = loadVerificationLogic(pluginSource);
|
|
242
|
-
context.process.env.HARNESS_VERIFY_TIMEOUT_MS = '600000';
|
|
243
|
-
context.execSync = () => {
|
|
244
|
-
const error = new Error('Command failed: npm test');
|
|
245
|
-
error.status = 1;
|
|
246
|
-
error.stdout = '> fixture@0.0.0 test\n> node fixture.js\n';
|
|
247
|
-
error.stderr = 'AssertionError [ERR_ASSERTION]: alpha boom\n';
|
|
248
|
-
throw error;
|
|
249
|
-
};
|
|
250
|
-
const bothStreams = context.runVerification('/nonexistent-workspace').results[0];
|
|
251
|
-
assert.strictEqual(bothStreams.outcome, 'test_failure', 'a both-stream failure is a confirmed test failure');
|
|
252
|
-
assert.ok(/alpha boom/.test(bothStreams.error), 'the user-visible diagnostic includes the stderr assertion');
|
|
253
|
-
assert.ok(/node fixture\.js/.test(bothStreams.error), 'the user-visible diagnostic keeps the stdout banner');
|
|
254
|
-
assert.ok(/alpha boom/.test(bothStreams.evidence), 'the kept evidence includes stderr');
|
|
255
|
-
assert.ok(/node fixture\.js/.test(bothStreams.evidence), 'the kept evidence includes stdout');
|
|
256
|
-
|
|
257
|
-
context.execSync = () => {
|
|
258
|
-
const error = new Error('Command failed: npm test');
|
|
259
|
-
error.status = 1;
|
|
260
|
-
error.stdout = `${'A'.repeat(2000)}TAIL-MARKER`;
|
|
261
|
-
throw error;
|
|
262
|
-
};
|
|
263
|
-
const longOutput = context.runVerification('/nonexistent-workspace').results[0];
|
|
264
|
-
assert.ok(longOutput.error.length < 2200, 'the user-visible diagnostic stays bounded');
|
|
265
|
-
assert.ok(/TAIL-MARKER/.test(longOutput.error), 'the trailing failure evidence survives diagnostic truncation');
|
|
266
|
-
assert.ok(/chars omitted/.test(longOutput.error), 'the elision is explicit in the diagnostic');
|
|
267
|
-
assert.ok(longOutput.evidence.length > 2000 && /TAIL-MARKER/.test(longOutput.evidence), 'the full evidence is retained untruncated for signature analysis');
|
|
268
|
-
|
|
269
|
-
context.execSync = () => {
|
|
270
|
-
const error = new Error('Command failed: npm test');
|
|
271
|
-
error.code = 'ENOBUFS';
|
|
272
|
-
error.stdout = `${'A'.repeat(2000)}TAIL-MARKER`;
|
|
273
|
-
throw error;
|
|
274
|
-
};
|
|
275
|
-
const spawnError = context.runVerification('/nonexistent-workspace').results[0];
|
|
276
|
-
assert.strictEqual(spawnError.evidence, undefined, 'only confirmed test failures keep full evidence');
|
|
277
|
-
}
|
|
114
|
+
// Plugin outcome, evidence and cleanup behavior is covered with real child
|
|
115
|
+
// processes in mechanism-338-opencode-async.test.js, and downstream Rule-of-3
|
|
116
|
+
// behavior remains in mechanism-39-opencode-verify-outcomes.test.js.
|
|
278
117
|
|
|
279
118
|
// Behavior: the manual CLI mirror classifies the same way.
|
|
280
119
|
{
|
|
@@ -183,6 +183,14 @@ evidence supports project-scope `.js` loading and edit/verification state on
|
|
|
183
183
|
OpenCode 1.18.31 (macOS): [live-host evidence](../benchmarks/results/live-host/opencode-2026-09-16/README.md). The
|
|
184
184
|
retained snapshot predates #190 and remains historical evidence only. Current OpenCode removes the permanent post-reflection hard lock; only the third-failure reflection boundary pauses edits. Reflection was operator-seeded, then agent-rewritten, so do not promote this into a behavioral-effectiveness claim. `.mjs` auto-discovery is broken on this host version (issue #127). npm-package installation and behavioral V2-hook fire remain unverified.
|
|
185
185
|
|
|
186
|
+
The plugin verification runner uses asynchronous child processes with a shared
|
|
187
|
+
sequence budget and bounded streamed diagnostics. Deterministic mechanism
|
|
188
|
+
coverage checks event-loop responsiveness, large-output verdicts, timeout/process
|
|
189
|
+
cleanup, and concurrent edit/session reset handling. The standalone manual CLI
|
|
190
|
+
retains its synchronous per-command runner. This change supplies no additional
|
|
191
|
+
live-host evidence; the historical installation and behavior boundaries above
|
|
192
|
+
remain unchanged.
|
|
193
|
+
|
|
186
194
|
## Official sources
|
|
187
195
|
|
|
188
196
|
The matrix records the official documentation reviewed for each platform. Important source families include [OpenAI plugin packaging](https://developers.openai.com/plugins/build/plugins), [OpenAI public submission](https://developers.openai.com/plugins/deploy/submission), [Codex skills](https://learn.chatgpt.com/docs/build-skills), [Codex hooks](https://learn.chatgpt.com/docs/hooks), [Claude skills](https://code.claude.com/docs/en/skills), [OpenCode skills](https://opencode.ai/docs/skills), [GitHub Copilot Agent Skills](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-cloud-agent/add-skills), [Cursor skills](https://cursor.com/docs/skills), [Continue rules](https://docs.continue.dev/customize/rules), and [Hermes skills](https://hermes-agent.nousresearch.com/docs/user-guide/features/skills).
|