harness-everything 0.35.0 → 0.36.1

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.
@@ -0,0 +1,534 @@
1
+ const assert = require('assert');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const vm = require('vm');
5
+ const crypto = require('crypto');
6
+ const helper = require('./test-helper');
7
+
8
+ const root = helper.root;
9
+ const pluginSource = fs.readFileSync(path.join(root, 'opencode-plugin', 'index.mjs'), 'utf8');
10
+ const cliSource = fs.readFileSync(path.join(root, 'opencode-plugin', 'hooks', 'verify.js'), 'utf8');
11
+
12
+ function extractConst(source, name) {
13
+ const match = source.match(new RegExp(`^const ${name} = .*$`, 'm'));
14
+ assert.ok(match, `expected a ${name} declaration in source`);
15
+ return match[0];
16
+ }
17
+
18
+ function extractFunction(source, name) {
19
+ const start = source.indexOf(`function ${name}(`);
20
+ assert.ok(start >= 0, `expected function ${name} in source`);
21
+ const open = source.indexOf('{', start);
22
+ let depth = 0;
23
+ for (let i = open; i < source.length; i += 1) {
24
+ if (source[i] === '{') depth += 1;
25
+ else if (source[i] === '}') {
26
+ depth -= 1;
27
+ if (depth === 0) return source.slice(start, i + 1);
28
+ }
29
+ }
30
+ throw new Error(`function ${name} is not brace-balanced`);
31
+ }
32
+
33
+ function loadVerificationLogic(source) {
34
+ const context = {
35
+ process: { env: {} },
36
+ execSync: null,
37
+ getAvailableScripts: () => ({ test: true }),
38
+ };
39
+ vm.createContext(context);
40
+ const script = [
41
+ extractConst(source, 'VERIFY_TIMEOUT_ENV'),
42
+ extractConst(source, 'DEFAULT_VERIFY_TIMEOUT_MS'),
43
+ extractConst(source, 'VERIFY_MAX_BUFFER'),
44
+ extractConst(source, 'VERIFY_OUTPUT_LIMIT'),
45
+ extractFunction(source, 'verifyTimeoutMs'),
46
+ extractFunction(source, 'verificationEvidence'),
47
+ extractFunction(source, 'boundedDiagnostic'),
48
+ extractFunction(source, 'verificationDiagnostic'),
49
+ extractFunction(source, 'classifyVerificationError'),
50
+ extractFunction(source, 'runVerification'),
51
+ ].join('\n\n');
52
+ vm.runInContext(script, context, { filename: 'verification-logic.js' });
53
+ return context;
54
+ }
55
+
56
+ function loadSignatureLogic(source) {
57
+ const context = { createHash: crypto.createHash };
58
+ vm.createContext(context);
59
+ const script = [
60
+ extractConst(source, 'FAILURE_MARKER_PATTERNS'),
61
+ extractConst(source, 'CONTEXT_MARKER_PATTERNS'),
62
+ extractFunction(source, 'normalizeFailureLine'),
63
+ extractFunction(source, 'firstFailureMarker'),
64
+ extractFunction(source, 'buildFailureSignature'),
65
+ ].join('\n\n');
66
+ vm.runInContext(script, context, { filename: 'signature-logic.js' });
67
+ return context;
68
+ }
69
+
70
+ const DEFAULTS = [pluginSource, cliSource];
71
+ const LABELS = ['opencode-plugin/index.mjs', 'opencode-plugin/hooks/verify.js'];
72
+
73
+ // Source contract: budget, outcome taxonomy, and buffer hygiene are present.
74
+ for (let i = 0; i < DEFAULTS.length; i += 1) {
75
+ const source = DEFAULTS[i];
76
+ const label = LABELS[i];
77
+ assert.ok(/HARNESS_VERIFY_TIMEOUT_MS/.test(source), `${label} reads HARNESS_VERIFY_TIMEOUT_MS`);
78
+ assert.ok(!/timeout:\s*60000\b/.test(source), `${label} no longer hard-codes a 60s verification timeout`);
79
+ assert.ok(/timed out after/.test(source), `${label} labels timeout failures as timeouts`);
80
+ assert.ok(/test_failure/.test(source), `${label} classifies confirmed failing checks`);
81
+ assert.ok(/execution_error/.test(source), `${label} classifies spawn-level execution errors`);
82
+ assert.ok(/maxBuffer/.test(source), `${label} bounds captured output instead of using the 1 MiB default`);
83
+ const declared = Number(source.match(/^const DEFAULT_VERIFY_TIMEOUT_MS = (\d+);?$/m)[1]);
84
+ assert.ok(declared >= 300000, `${label} default budget (${declared}ms) clears a several-minute suite`);
85
+ }
86
+
87
+ // Both copies must agree, or the manual CLI drifts from the live plugin.
88
+ assert.strictEqual(
89
+ extractConst(pluginSource, 'DEFAULT_VERIFY_TIMEOUT_MS').match(/= (\d+)/)[1],
90
+ extractConst(cliSource, 'DEFAULT_VERIFY_TIMEOUT_MS').match(/= (\d+)/)[1],
91
+ 'the plugin and the manual CLI declare the same default budget',
92
+ );
93
+ assert.strictEqual(
94
+ extractConst(pluginSource, 'VERIFY_TIMEOUT_ENV').match(/"(.*)"/)[1],
95
+ extractConst(cliSource, 'VERIFY_TIMEOUT_ENV').match(/'([^']*)'/)[1],
96
+ 'the plugin and the manual CLI read the same env override',
97
+ );
98
+ assert.strictEqual(
99
+ extractConst(pluginSource, 'VERIFY_MAX_BUFFER').match(/= (\d+ \* \d+ \* \d+)/)[1],
100
+ extractConst(cliSource, 'VERIFY_MAX_BUFFER').match(/= (\d+ \* \d+ \* \d+)/)[1],
101
+ 'the plugin and the manual CLI allow the same output buffer',
102
+ );
103
+
104
+ // Behavior: budget resolution.
105
+ {
106
+ const context = loadVerificationLogic(pluginSource);
107
+ assert.strictEqual(context.verifyTimeoutMs(), 600000, 'unset env uses the default budget');
108
+ for (const value of ['abc', '0', '-5', '']) {
109
+ context.process.env.HARNESS_VERIFY_TIMEOUT_MS = value;
110
+ assert.strictEqual(context.verifyTimeoutMs(), 600000, `invalid override "${value}" falls back to the default`);
111
+ }
112
+ context.process.env.HARNESS_VERIFY_TIMEOUT_MS = '1234';
113
+ assert.strictEqual(context.verifyTimeoutMs(), 1234, 'a positive override wins');
114
+ assert.ok(!Number.isNaN(Number(context.verifyTimeoutMs())), 'the resolved budget is numeric');
115
+ }
116
+
117
+ // Behavior: outcome classification per failure kind.
118
+ {
119
+ const context = loadVerificationLogic(pluginSource);
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
+ }
278
+
279
+ // Behavior: the manual CLI mirror classifies the same way.
280
+ {
281
+ const context = loadVerificationLogic(cliSource);
282
+ const calls = [];
283
+ context.execSync = (command, options) => {
284
+ calls.push({ command, options });
285
+ return '';
286
+ };
287
+ let result = context.runVerification('npm test');
288
+ assert.strictEqual(result.success, true, 'the CLI verifies a passing command');
289
+ assert.strictEqual(result.outcome, 'pass', 'the CLI classifies a passing command');
290
+ assert.strictEqual(calls[0].options.timeout, 600000, 'the CLI uses the default budget');
291
+ assert.strictEqual(calls[0].options.maxBuffer, 16 * 1024 * 1024, 'the CLI uses the raised output buffer');
292
+
293
+ context.process.env.HARNESS_VERIFY_TIMEOUT_MS = '700';
294
+ context.execSync = () => {
295
+ const started = Date.now();
296
+ while (Date.now() - started < 800) { /* outlive the budget */ }
297
+ throw new Error('Command failed: npm test');
298
+ };
299
+ result = context.runVerification('npm test');
300
+ assert.strictEqual(result.success, false, 'the CLI reports the failure');
301
+ assert.strictEqual(result.timedOut, true, 'the CLI marks a timeout');
302
+ assert.strictEqual(result.outcome, 'timeout', 'the CLI classifies the timeout');
303
+ assert.ok(/timed out after 1s/.test(result.error), 'the CLI names the budget in the error');
304
+ assert.ok(/HARNESS_VERIFY_TIMEOUT_MS/.test(result.error), 'the CLI names the override in the error');
305
+
306
+ context.process.env.HARNESS_VERIFY_TIMEOUT_MS = '600000';
307
+ context.execSync = () => {
308
+ const error = new Error('Command failed');
309
+ error.status = 2;
310
+ error.stderr = 'lint error';
311
+ throw error;
312
+ };
313
+ result = context.runVerification('npm test');
314
+ assert.strictEqual(result.timedOut, false, 'the CLI does not mislabel a fast failure');
315
+ assert.strictEqual(result.outcome, 'test_failure', 'the CLI classifies the non-zero exit');
316
+ assert.strictEqual(result.error, 'lint error', 'the CLI reports stderr for a fast failure');
317
+
318
+ // P2 parity: an early external SIGTERM is not a budget timeout.
319
+ context.execSync = () => {
320
+ const error = new Error('Command failed');
321
+ error.signal = 'SIGTERM';
322
+ throw error;
323
+ };
324
+ result = context.runVerification('npm test');
325
+ assert.strictEqual(result.outcome, 'execution_error', 'the CLI treats an early external SIGTERM as an execution error');
326
+ assert.strictEqual(result.timedOut, false, 'the CLI does not claim a timeout for an early SIGTERM');
327
+ assert.ok(/signal SIGTERM/.test(result.error), 'the CLI keeps the signal in diagnostics');
328
+
329
+ context.execSync = () => {
330
+ const error = new Error('spawnSync npm test ETIMEDOUT');
331
+ error.code = 'ETIMEDOUT';
332
+ error.signal = 'SIGTERM';
333
+ error.stdout = 'Harness OS - Self-Regression Test Suite\n';
334
+ throw error;
335
+ };
336
+ result = context.runVerification('npm test');
337
+ assert.strictEqual(result.outcome, 'timeout', 'the CLI classifies the live ETIMEDOUT shape as a budget timeout');
338
+ assert.strictEqual(result.timedOut, true, 'the CLI marks the budget timeout');
339
+
340
+ // P1 parity: both streams feed the CLI diagnostic and evidence.
341
+ context.execSync = () => {
342
+ const error = new Error('Command failed');
343
+ error.status = 1;
344
+ error.stdout = '> fixture@0.0.0 test\n';
345
+ error.stderr = 'AssertionError [ERR_ASSERTION]: alpha boom\n';
346
+ throw error;
347
+ };
348
+ result = context.runVerification('npm test');
349
+ assert.strictEqual(result.outcome, 'test_failure', 'the CLI classifies a both-stream failure');
350
+ assert.ok(/alpha boom/.test(result.error), 'the CLI diagnostic includes stderr');
351
+ assert.ok(/fixture@0\.0\.0/.test(result.error), 'the CLI diagnostic includes stdout');
352
+ assert.ok(/alpha boom/.test(result.evidence), 'the CLI keeps stderr evidence');
353
+
354
+ context.execSync = () => {
355
+ const error = new Error('Command failed');
356
+ error.code = 'ENOBUFS';
357
+ throw error;
358
+ };
359
+ result = context.runVerification('npm test');
360
+ assert.strictEqual(result.outcome, 'execution_error', 'the CLI classifies spawn-level failures');
361
+ assert.ok(!/timed out/.test(result.error), 'the CLI does not report ENOBUFS as a timeout');
362
+ }
363
+
364
+ // Behavior: failure signatures key on real failing-check evidence.
365
+ {
366
+ const context = loadSignatureLogic(pluginSource);
367
+ const banner = '> harness-everything@0.34.0 test\n> node self-evolve/scripts/self-regression.js\n';
368
+ const result = (error, extra = {}) => ({ command: 'npm test', outcome: 'test_failure', exitCode: 1, signal: null, error, ...extra });
369
+
370
+ const alpha = result(`${banner}✖ alpha check (1.2ms)\nAssertionError [ERR_ASSERTION]: alpha boom`);
371
+ assert.strictEqual(context.buildFailureSignature(alpha), context.buildFailureSignature(alpha), 'the same observation yields the same signature');
372
+ const beta = result(`${banner}✖ beta check (0.8ms)\nAssertionError [ERR_ASSERTION]: beta boom`);
373
+ assert.notStrictEqual(context.buildFailureSignature(alpha), context.buildFailureSignature(beta), 'different failing checks sharing a banner do not collide');
374
+
375
+ const ansi = result(`${banner}\u001b[31m✖\u001b[39m alpha check (1.2ms)`);
376
+ assert.strictEqual(context.buildFailureSignature(ansi), context.buildFailureSignature(alpha), 'ANSI decoration and timing suffixes do not change the signature');
377
+
378
+ const tapA = result('not ok 2 - parser handles unicode');
379
+ const tapB = result('not ok 5 - parser handles unicode');
380
+ assert.strictEqual(context.buildFailureSignature(tapA), context.buildFailureSignature(tapB), 'TAP plan numbering does not change the signature');
381
+
382
+ // P1: full stdout+stderr evidence drives the marker/digest, not a prefix.
383
+ const withEvidence = (stdout, stderr) => {
384
+ const evidence = stderr ? `${stdout}\nstderr:\n${stderr}` : stdout;
385
+ return result(evidence, { evidence });
386
+ };
387
+ const sharedStdout = '> fixture@0.0.0 test\n> node fixture.js\n';
388
+ const stderrAlpha = withEvidence(sharedStdout, 'AssertionError [ERR_ASSERTION]: alpha boom');
389
+ const stderrBeta = withEvidence(sharedStdout, 'AssertionError [ERR_ASSERTION]: beta boom');
390
+ assert.notStrictEqual(
391
+ context.buildFailureSignature(stderrAlpha),
392
+ context.buildFailureSignature(stderrBeta),
393
+ 'shared stdout with different stderr assertions does not collide',
394
+ );
395
+
396
+ const prefix = 'progress '.repeat(400); // longer than the old 2 KiB leading-prefix cut
397
+ const tailAlpha = withEvidence(`${prefix}HARNESS-UNIQUE-ALPHA`, '');
398
+ const tailBeta = withEvidence(`${prefix}HARNESS-UNIQUE-BETA`, '');
399
+ assert.ok(/unrecognized-output/.test(context.buildFailureSignature(tailAlpha)), 'markerless evidence uses the digest fallback');
400
+ assert.notStrictEqual(
401
+ context.buildFailureSignature(tailAlpha),
402
+ context.buildFailureSignature(tailBeta),
403
+ 'a >2 KiB shared markerless prefix with different tails does not collide',
404
+ );
405
+ assert.strictEqual(
406
+ context.buildFailureSignature(tailAlpha),
407
+ context.buildFailureSignature(withEvidence(`${prefix}HARNESS-UNIQUE-ALPHA`, '')),
408
+ 'identical evidence still matches',
409
+ );
410
+
411
+ // P1: the Jest suite header is context; the test block is the identity.
412
+ const jestLike = (name, file = 'src/parser.test.js') => `FAIL ${file}\n ● parser › accepts ${name}\n AssertionError: ${name}\n`;
413
+ assert.strictEqual(
414
+ context.firstFailureMarker(jestLike('alpha')),
415
+ 'FAIL src/parser.test.js: ● parser › accepts alpha',
416
+ 'the Jest suite header is carried as context for the specific test block',
417
+ );
418
+ const jestAlpha = result(jestLike('alpha'));
419
+ const jestBeta = result(jestLike('beta'));
420
+ const jestGamma = result(jestLike('gamma'));
421
+ assert.notStrictEqual(context.buildFailureSignature(jestAlpha), context.buildFailureSignature(jestBeta), 'different Jest failures from the same file do not collide');
422
+ assert.notStrictEqual(context.buildFailureSignature(jestBeta), context.buildFailureSignature(jestGamma), 'the second and third Jest failures stay distinct too');
423
+ assert.strictEqual(context.buildFailureSignature(jestAlpha), context.buildFailureSignature(result(jestLike('alpha'))), 'an identical Jest failure still matches');
424
+ assert.notStrictEqual(
425
+ context.buildFailureSignature(jestAlpha),
426
+ context.buildFailureSignature(result(jestLike('alpha', 'src/other.test.js'))),
427
+ 'the file context distinguishes same-named first failures across suites',
428
+ );
429
+
430
+ const failOnly = result('FAIL src/parser.test.js\n');
431
+ assert.ok(/unrecognized-output/.test(context.buildFailureSignature(failOnly)), 'a lone suite header is not a failing-check identity');
432
+ assert.notStrictEqual(
433
+ context.buildFailureSignature(failOnly),
434
+ context.buildFailureSignature(result('FAIL src/other.test.js\n')),
435
+ 'lone suite headers from different files stay distinct via the digest',
436
+ );
437
+
438
+ assert.strictEqual(context.firstFailureMarker(' ● Console\n console.log\n noise\n'), null, 'the Jest Console section is not a failure marker');
439
+ const jestWithConsole = `FAIL src/parser.test.js\n ● Console\n\n console.log\n noise\n\n ● parser › accepts alpha\n`;
440
+ assert.strictEqual(context.firstFailureMarker(jestWithConsole), 'FAIL src/parser.test.js: ● parser › accepts alpha', 'a Console block before the failure does not confuse the marker');
441
+
442
+ // P2 follow-up: indented error lines are Mocha evidence, but they must not
443
+ // reach into a Jest Console payload, whose logged Error lines are not
444
+ // failure evidence and vary run to run.
445
+ const consolePayloadBeforeFailure = `PASS src/loggy.test.js\n ● Console\n\n console.error\n Error: connection refused at 127.0.0.1:8080\n\nFAIL src/parser.test.js\n ● parser › accepts alpha\n`;
446
+ assert.strictEqual(
447
+ context.firstFailureMarker(consolePayloadBeforeFailure),
448
+ 'FAIL src/parser.test.js: ● parser › accepts alpha',
449
+ 'an indented Error line inside a Console payload is not a failure marker',
450
+ );
451
+ const consolePayloadOnly = `PASS src/loggy.test.js\n ● Console\n\n console.error\n Error: connection refused at 127.0.0.1:8080\n`;
452
+ assert.strictEqual(context.firstFailureMarker(consolePayloadOnly), null, 'a Console payload alone leaves the run markerless');
453
+ const noisyFailure = (noise) => `PASS src/loggy.test.js\n ● Console\n\n console.error\n Error: connection refused at ${noise}\n\nFAIL src/parser.test.js\n ● parser › accepts alpha\n`;
454
+ assert.strictEqual(
455
+ context.buildFailureSignature(result(noisyFailure('port 8080'))),
456
+ context.buildFailureSignature(result(noisyFailure('port 9091'))),
457
+ 'changing Console payload noise does not change the failure signature',
458
+ );
459
+
460
+
461
+ // P1: a logged bullet deeper than Jest's " ● Console" heading is
462
+ // payload, not a new section or a test-level failure marker.
463
+ const consoleBulletOutput = (name, noise) =>
464
+ `PASS src/loggy.test.js\n ● Console\n\n console.log\n ● ${noise}\n ● Console\n\nFAIL src/parser.test.js\n ● parser › accepts ${name}\n`;
465
+ assert.strictEqual(
466
+ context.firstFailureMarker(consoleBulletOutput('alpha', 'preparing runtime')),
467
+ 'FAIL src/parser.test.js: ● parser › accepts alpha',
468
+ 'deeply indented Console bullets do not mask the actual Jest failure',
469
+ );
470
+ assert.strictEqual(
471
+ context.firstFailureMarker('PASS src/loggy.test.js\n ● Console\n\n console.log\n ● preparing runtime\n'),
472
+ null,
473
+ 'Console-only output with a nested bullet remains markerless',
474
+ );
475
+ assert.notStrictEqual(
476
+ context.buildFailureSignature(result(consoleBulletOutput('alpha', 'common log'))),
477
+ context.buildFailureSignature(result(consoleBulletOutput('beta', 'common log'))),
478
+ 'different Jest checks with the same logged Console bullet remain distinct',
479
+ );
480
+ assert.strictEqual(
481
+ context.buildFailureSignature(result(consoleBulletOutput('stable', 'preparing runtime'))),
482
+ context.buildFailureSignature(result(consoleBulletOutput('stable', 'loaded 12 plugins'))),
483
+ 'the same Jest check retains its signature when logged Console bullet content changes',
484
+ );
485
+
486
+ // P2: Mocha indents both numbered failures and assertion lines. Signatures
487
+ // must use the test identity, not the whole output (volatile stack/timing).
488
+ const mochaLike = (name, frame) =>
489
+ ` 1) should parse ${name}\n AssertionError: expected true to equal false\n at Context.<anonymous> (fixture.test.js:${frame}:7)\n`;
490
+ const mochaAlpha = result(mochaLike('alpha', 17));
491
+ const mochaBeta = result(mochaLike('beta', 18));
492
+ assert.strictEqual(
493
+ context.firstFailureMarker(mochaLike('alpha', 17)),
494
+ '1) should parse alpha',
495
+ 'indented Mocha test-number lines are recognized and normalized',
496
+ );
497
+ assert.notStrictEqual(
498
+ context.buildFailureSignature(mochaAlpha),
499
+ context.buildFailureSignature(mochaBeta),
500
+ 'different indented Mocha failures have different signatures',
501
+ );
502
+ assert.strictEqual(
503
+ context.buildFailureSignature(mochaAlpha),
504
+ context.buildFailureSignature(result(mochaLike('alpha', 93))),
505
+ 'the same Mocha failure keeps one signature despite changing stack line numbers',
506
+ );
507
+ assert.strictEqual(
508
+ context.firstFailureMarker(' AssertionError [ERR_ASSERTION]: same assertion\n'),
509
+ 'AssertionError [ERR_ASSERTION]: same assertion',
510
+ 'indented assertion-only output is recognized as failure evidence',
511
+ );
512
+ assert.notStrictEqual(
513
+ context.buildFailureSignature(result(' AssertionError: alpha boom\n')),
514
+ context.buildFailureSignature(result(' AssertionError: beta boom\n')),
515
+ 'distinct indented assertion failures remain distinct',
516
+ );
517
+ assert.strictEqual(
518
+ context.firstFailureMarker('not ok 7 - TAP check\n'),
519
+ 'not ok - TAP check',
520
+ 'TAP marker numbering is normalized independently of Mocha indentation support',
521
+ );
522
+
523
+ const bannerOnly = result(banner);
524
+ assert.strictEqual(context.buildFailureSignature(bannerOnly), context.buildFailureSignature(result(banner)), 'identical markerless output still matches');
525
+ const bannerPlusTail = result(`${banner}different tail`);
526
+ assert.notStrictEqual(context.buildFailureSignature(bannerOnly), context.buildFailureSignature(bannerPlusTail), 'markerless signatures fall back to the full-output digest');
527
+ assert.ok(/unrecognized-output/.test(context.buildFailureSignature(bannerOnly)), 'markerless signatures are labeled as unrecognized output');
528
+ assert.ok(/exit=1/.test(context.buildFailureSignature(bannerOnly)), 'the signature records the exit condition');
529
+
530
+ const nodeTest = '# failing tests:\n✖ failing tests:\n✖ real specific check (3ms)\n';
531
+ assert.strictEqual(context.firstFailureMarker(nodeTest), '✖ real specific check', 'the generic failing-tests header is skipped for the specific check');
532
+ }
533
+
534
+ console.log('mechanism-36-verify-timeout: plugin and CLI verification budget, outcome taxonomy, and signature contracts hold');
@@ -0,0 +1,71 @@
1
+ const assert = require('assert');
2
+ const { analyzeLiteral } = require('./portability-lint');
3
+
4
+ // Corpus for the portability classifier. Every Windows-hostile example is
5
+ // built at runtime from portable fragments, so this file itself passes the
6
+ // lint it tests (the lint scans all of ci/).
7
+
8
+ const BACKSLASH = String.fromCharCode(92);
9
+ const COLON = ':';
10
+ const QUOTE = String.fromCharCode(34);
11
+ const STAR = '*';
12
+ const nul = ['n', 'u', 'l'].join('');
13
+
14
+ const POSITIVES = [
15
+ { label: 'colon in filename', value: ['bad', 'name.md'].join(COLON), rule: 'WIN_INVALID_CHAR' },
16
+ { label: 'star in filename', value: ['bad', 'name.md'].join(STAR), rule: 'WIN_INVALID_CHAR' },
17
+ { label: 'quote in filename', value: ['bad', 'name.md'].join(QUOTE), rule: 'WIN_INVALID_CHAR' },
18
+ { label: 'bare reserved name', value: nul.toUpperCase(), rule: 'WIN_RESERVED_NAME' },
19
+ { label: 'reserved name with extension', value: `${nul}.txt`, rule: 'WIN_RESERVED_NAME' },
20
+ {
21
+ label: 'colon inside a drive-absolute path',
22
+ value: [`C${COLON}`, 'dir', ['bad', 'name.md'].join(COLON)].join(BACKSLASH),
23
+ rule: 'WIN_INVALID_CHAR',
24
+ },
25
+ {
26
+ label: 'reserved path segment',
27
+ value: ['docs', `${nul}.md`].join('/'),
28
+ rule: 'WIN_RESERVED_NAME',
29
+ },
30
+ {
31
+ label: 'device namespace path',
32
+ value: [BACKSLASH, BACKSLASH, '.', BACKSLASH].join('') + nul,
33
+ rule: 'WIN_DEVICE_PATH',
34
+ },
35
+ ];
36
+
37
+ const NEGATIVES = [
38
+ { label: 'node specifier', value: 'node:assert/strict' },
39
+ { label: 'https URL with port', value: 'https://example.com/a:b' },
40
+ { label: 'pkg purl', value: 'pkg:npm/left-pad@1.0.0' },
41
+ { label: 'git tree-ish selector', value: 'HEAD:refs/heads/main' },
42
+ { label: 'doc provenance tag', value: 'legacy:GRAPH-1@docs/adr/legacy.md' },
43
+ { label: 'identity tag', value: 'human:someone@example.com' },
44
+ { label: 'double-star glob', value: 'docs/prd/**' },
45
+ { label: 'single-star glob', value: 'references/*.md' },
46
+ { label: 'backslash glob', value: ['docs', 'prd', '**'].join(BACKSLASH) },
47
+ { label: 'placeholder root', value: '<this-skill-dir>/guide.md' },
48
+ { label: 'template prefix', value: '${base}:docs/prd/sample/r001.md' },
49
+ { label: 'clock fragment', value: '10:06' },
50
+ { label: 'spaced prose with colon', value: 'fixture group: control' },
51
+ { label: 'regex source with char class', value: '/[^"\\s]+\\.js' },
52
+ { label: 'regex source with inline group', value: '(?:<[^>]+>/)?[A-Za-z0-9_.-]+\\.md' },
53
+ { label: 'plain relative path', value: './src/app.js' },
54
+ { label: 'portable fixture name', value: 'fixtures/planning-v2.md' },
55
+ ];
56
+
57
+ for (const { label, value, rule } of POSITIVES) {
58
+ const rules = new Set(analyzeLiteral({ value }).map((finding) => finding.rule));
59
+ assert.ok(rules.has(rule), `expected ${rule} for ${label}, got: ${[...rules].join(', ') || '(none)'}`);
60
+ }
61
+
62
+ for (const { label, value } of NEGATIVES) {
63
+ const findings = analyzeLiteral({ value });
64
+ assert.deepStrictEqual(
65
+ findings,
66
+ [],
67
+ `expected no findings for ${label}, got: ${findings.map((f) => `${f.rule} (${f.detail})`).join('; ')}`,
68
+ );
69
+ }
70
+
71
+ console.log(`mechanism-37-portability-lint: ${POSITIVES.length} positive and ${NEGATIVES.length} negative corpus cases hold`);