control-arm 0.1.0 → 1.2.0

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.
@@ -1,41 +0,0 @@
1
- /**
2
- * The tool's control arm. Eight repos where the right answer is known by construction.
3
- *
4
- * A green run here is the only reason to believe a verdict this tool prints about
5
- * anybody else's code. It is also the thing that would catch the failure mode that
6
- * matters most — a FALSE BLIND — because 01 and 07 must never come back BLIND.
7
- */
8
- import { test } from 'node:test';
9
- import assert from 'node:assert/strict';
10
- import path from 'node:path';
11
- import { buildFixtures } from '../fixtures/build.mjs';
12
- import { verifyCommit } from '../src/verify.mjs';
13
- import { removeWorktrees } from '../src/worktree.mjs';
14
-
15
- const fixtures = buildFixtures();
16
-
17
- for (const [name, f] of Object.entries(fixtures)) {
18
- test(`${name} -> ${f.expect} (${f.why})`, async () => {
19
- const workDir = path.join(f.dir, '.ca-work');
20
- try {
21
- const r = await verifyCommit({ repo: f.dir, workDir, sha: f.sha, runs: 1, timeoutMs: 30_000 });
22
- assert.equal(r.verdict, f.expect,
23
- `expected ${f.expect}, got ${r.verdict}\n` +
24
- r.cases.map(c => ` ${c.verdict} ${c.name} — ${c.reason}`).join('\n') +
25
- (r.note ? `\n note: ${r.note}` : ''));
26
- } finally {
27
- await removeWorktrees(f.dir, workDir);
28
- }
29
- });
30
- }
31
-
32
- test('a FALSE BLIND is the failure that matters: no discriminating fixture may read BLIND', async () => {
33
- for (const name of ['01-caught-value', '07-caught-mixed']) {
34
- const f = fixtures[name];
35
- const workDir = path.join(f.dir, '.ca-work-fb');
36
- try {
37
- const r = await verifyCommit({ repo: f.dir, workDir, sha: f.sha, runs: 1, timeoutMs: 30_000 });
38
- assert.notEqual(r.verdict, 'BLIND', `${name} was accused of being blind — this is the output that destroys trust in the tool`);
39
- } finally { await removeWorktrees(f.dir, workDir); }
40
- }
41
- });
@@ -1,69 +0,0 @@
1
- /**
2
- * The vitest/jest adapter, at the level where it can actually be wrong.
3
- *
4
- * WHAT THIS DOES AND DOES NOT COVER — stated because the gap matters.
5
- *
6
- * The node:test path has 9 end-to-end fixtures: real git repos, both arms, known verdict.
7
- * These two runners do NOT, because a fixture would have to `npm install` vitest or jest
8
- * per repo — slow, network-dependent, and version-drifting. The full two-arm path for
9
- * each is verified on ONE real commit apiece — a vitest component commit and a jest
10
- * React Native commit — which is a smoke test, not a control arm.
11
- *
12
- * So this file covers the part that is BOTH untested end-to-end AND most likely to be
13
- * wrong: the text-matching in `classifyMessages`. node:test hands over `ERR_ASSERTION`;
14
- * these two give formatted strings, so the adapter has to read prose. Every string below
15
- * is a real shape emitted by vitest or jest, not an invented one.
16
- */
17
- import { test } from 'node:test';
18
- import assert from 'node:assert/strict';
19
- import { classifyMessages } from '../src/runner-json.mjs';
20
-
21
- const isDisagreement = m => classifyMessages(m).code === 'ERR_ASSERTION';
22
- const isCannotRun = m => classifyMessages(m).code === 'ERR_TEST_FAILURE';
23
- const isUnknown = m => classifyMessages(m).code === null;
24
-
25
- test('vitest/chai disagreement is read as an assertion', () => {
26
- assert.ok(isDisagreement(["AssertionError: expected 'Urgent' to deeply equal 'Normal'"]));
27
- assert.ok(isDisagreement(['AssertionError: expected 40 to be 65 // Object.is equality']));
28
- });
29
-
30
- test('jest/expect disagreement is read as an assertion', () => {
31
- assert.ok(isDisagreement(['expect(received).toBe(expected) // Object.is equality\n\nExpected: "Normal"\nReceived: "Urgent"']));
32
- assert.ok(isDisagreement(['Error: expect(received).toEqual(expected)\n\n- Expected\n+ Received']));
33
- });
34
-
35
- test('a module that could not load is NOT an assertion', () => {
36
- assert.ok(isCannotRun(["Error: Cannot find module '../src/newHelper' from 'lib/__tests__/x.test.tsx'"]));
37
- assert.ok(isCannotRun(["SyntaxError: The requested module './bucket.js' does not provide an export named 'SEP'"]));
38
- assert.ok(isCannotRun(['Error: Failed to resolve import "./notYet" from "lib/x.test.tsx". Does the file exist?']));
39
- assert.ok(isCannotRun(['TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".tsx"']));
40
- });
41
-
42
- test('CANNOT-RUN WINS when both shapes appear — the ordering that matters', () => {
43
- // A failed module load frequently ALSO prints an expect() frame from the stack. If
44
- // the disagreement patterns were checked first, this would read CAUGHT, and a commit
45
- // whose test never ran would be credited with catching its bug.
46
- const both = ['SyntaxError: does not provide an export named \'SEP\'\n at expect(received).toBe(expected)\n expected \'a\' to be \'b\''];
47
- assert.ok(isCannotRun(both), 'a load failure that also prints an expect() frame must not read as a disagreement');
48
- assert.notEqual(classifyMessages(both).code, 'ERR_ASSERTION');
49
- });
50
-
51
- test('an UNRECOGNISED message is unknown — never guessed into an assertion', () => {
52
- // This is the design: an unrecognised shape costs COVERAGE (it reads INCONCLUSIVE),
53
- // it cannot manufacture a finding. A drifting regex must fail safe, not fail loud.
54
- assert.ok(isUnknown(['Something entirely unexpected happened in a custom matcher']));
55
- assert.ok(isUnknown([]));
56
- assert.ok(isUnknown(['']));
57
- assert.notEqual(classifyMessages(['whatever']).code, 'ERR_ASSERTION');
58
- });
59
-
60
- test('CONTROL ARM: a naive "any failure is a disagreement" classifier gets these wrong', () => {
61
- // TEST-001 — run the broken implementation and assert it still reproduces the bug.
62
- const naive = msgs => (msgs && msgs.length ? 'ERR_ASSERTION' : null);
63
- const loadFail = ["Error: Cannot find module '../src/newHelper'"];
64
- assert.equal(naive(loadFail), 'ERR_ASSERTION'); // the naive one says CAUGHT
65
- assert.equal(classifyMessages(loadFail).code, 'ERR_TEST_FAILURE'); // ours says it never ran
66
- const weird = ['custom matcher blew up'];
67
- assert.equal(naive(weird), 'ERR_ASSERTION');
68
- assert.equal(classifyMessages(weird).code, null);
69
- });
@@ -1,78 +0,0 @@
1
- /**
2
- * Runner selection, over synthetic trees.
3
- *
4
- * Picking the WRONG runner is a silent failure: vitest run from the repo root finds no
5
- * config and reports "No test files found", which reads INCONCLUSIVE — a whole surface
6
- * disappears without ever saying why. So the walk-up has to be pinned.
7
- */
8
- import { test } from 'node:test';
9
- import assert from 'node:assert/strict';
10
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
11
- import { tmpdir } from 'node:os';
12
- import path from 'node:path';
13
- import { selectRunner } from '../src/select-runner.mjs';
14
-
15
- function tree(spec) {
16
- const root = mkdtempSync(path.join(tmpdir(), 'ca-sel-'));
17
- for (const [rel, content] of Object.entries(spec)) {
18
- const p = path.join(root, rel);
19
- mkdirSync(path.dirname(p), { recursive: true });
20
- writeFileSync(p, content);
21
- }
22
- return root;
23
- }
24
-
25
- test('a vitest config in the workspace wins, and names that workspace as cwd', async () => {
26
- const root = tree({
27
- 'package.json': '{"workspaces":["apps/*"]}',
28
- 'apps/web/vitest.config.ts': 'export default {}',
29
- 'apps/web/lib/__tests__/x.test.tsx': '',
30
- });
31
- try {
32
- const r = await selectRunner(root, 'apps/web/lib/__tests__/x.test.tsx');
33
- assert.equal(r.flavour, 'vitest');
34
- assert.equal(r.pkgDir, 'apps/web', 'must run FROM apps/web — vitest resolves config relative to cwd');
35
- } finally { rmSync(root, { recursive: true, force: true }); }
36
- });
37
-
38
- test('a jest config in the workspace wins', async () => {
39
- const root = tree({ 'package.json': '{}', 'apps/mobile/jest.config.js': '', 'apps/mobile/__tests__/y.test.ts': '' });
40
- try {
41
- const r = await selectRunner(root, 'apps/mobile/__tests__/y.test.ts');
42
- assert.equal(r.flavour, 'jest');
43
- assert.equal(r.pkgDir, 'apps/mobile');
44
- } finally { rmSync(root, { recursive: true, force: true }); }
45
- });
46
-
47
- test('root tests fall to node:test when nothing else claims them', async () => {
48
- const root = tree({ 'package.json': '{"scripts":{"test":"node --test tests/*.test.js"}}', 'tests/a.test.js': '' });
49
- try {
50
- const r = await selectRunner(root, 'tests/a.test.js');
51
- assert.equal(r.flavour, 'node');
52
- assert.equal(r.pkgDir, '');
53
- } finally { rmSync(root, { recursive: true, force: true }); }
54
- });
55
-
56
- test('a repo that declares NOTHING falls through to node:test, not to a guess', async () => {
57
- // The safe default: node:test either works, or reports a load failure, which reads
58
- // INCONCLUSIVE. Guessing vitest here would produce "No test files found" — the same
59
- // silent disappearance this test exists to prevent.
60
- const root = tree({ 'tests/a.test.js': '' });
61
- try {
62
- assert.equal((await selectRunner(root, 'tests/a.test.js')).flavour, 'node');
63
- } finally { rmSync(root, { recursive: true, force: true }); }
64
- });
65
-
66
- test('a workspace config beats a root test script — the nearest owner wins', async () => {
67
- // The root script often delegates (`npm run test --workspaces`), while the config
68
- // that actually governs the file sits in the package. Nearest wins, walking up.
69
- const root = tree({
70
- 'package.json': '{"scripts":{"test":"node --test"}}',
71
- 'apps/web/vitest.config.ts': '',
72
- 'apps/web/x.test.tsx': '',
73
- });
74
- try {
75
- const r = await selectRunner(root, 'apps/web/x.test.tsx');
76
- assert.equal(r.flavour, 'vitest');
77
- } finally { rmSync(root, { recursive: true, force: true }); }
78
- });
@@ -1,113 +0,0 @@
1
- /**
2
- * The decision logic, exhaustively, with no git and no subprocesses.
3
- *
4
- * Every case here is a shape observed in a real run, not an invented one.
5
- */
6
- import { test } from 'node:test';
7
- import assert from 'node:assert/strict';
8
- import { classify, reduceRuns, rollUp, isDisagreement, CAUGHT, BLIND, NON_DISCRIMINATING, INCONCLUSIVE, FLAKY, SKIPPED } from '../src/verdict.mjs';
9
-
10
- const pass = { status: 'pass' };
11
- const assertFail = { status: 'fail', code: 'ERR_ASSERTION', errorName: 'AssertionError', message: "expected 1, got 2" };
12
- const errFail = { status: 'fail', code: 'ERR_TEST_FAILURE', errorName: 'SyntaxError', message: "does not provide an export named 'TITLE_SEPARATOR'" };
13
-
14
- test('CAUGHT: green on the fix, assertion-red on the parent', () => {
15
- assert.equal(classify({ armA: pass, armB: assertFail }).verdict, CAUGHT);
16
- });
17
-
18
- test('NON-DISCRIMINATING: green on the fix AND green on the parent', () => {
19
- assert.equal(classify({ armA: pass, armB: pass }).verdict, NON_DISCRIMINATING);
20
- });
21
-
22
- test('a case green on both arms is never called BLIND — a regression guard is indistinguishable', () => {
23
- // Observed in practice: 3 of one commit's 4 cases were deliberate regression guards.
24
- assert.notEqual(classify({ armA: pass, armB: pass }).verdict, BLIND);
25
- });
26
-
27
- test('a non-assertion failure on the parent is INCONCLUSIVE, never CAUGHT — the test never ran', () => {
28
- const r = classify({ armA: pass, armB: errFail });
29
- assert.equal(r.verdict, INCONCLUSIVE);
30
- assert.match(r.reason, /never ran/);
31
- });
32
-
33
- test('exit code alone would have gotten that wrong', () => {
34
- // Both of these are "the process exited non-zero". Only one is a verdict.
35
- assert.equal(classify({ armA: pass, armB: assertFail }).verdict, CAUGHT);
36
- assert.equal(classify({ armA: pass, armB: errFail }).verdict, INCONCLUSIVE);
37
- });
38
-
39
- test('unproven module identity withholds the verdict even when arm B passed', () => {
40
- // THE false-BLIND guard. Arm B green + identity unproven is precisely the symlink
41
- // trap that made this tool report a false BLIND before it had an identity gate.
42
- const r = classify({ armA: pass, armB: pass, identity: { proven: false, reason: 'resolved OUTSIDE the worktree' } });
43
- assert.equal(r.verdict, INCONCLUSIVE);
44
- assert.notEqual(r.verdict, BLIND);
45
- });
46
-
47
- test('a case that is red on the fix is INCONCLUSIVE — the commit does not stand up', () => {
48
- assert.equal(classify({ armA: assertFail, armB: assertFail }).verdict, INCONCLUSIVE);
49
- });
50
-
51
- test('a case missing from the parent run is INCONCLUSIVE, not BLIND', () => {
52
- assert.equal(classify({ armA: pass, armB: null }).verdict, INCONCLUSIVE);
53
- });
54
-
55
- test('isDisagreement is narrow: an unknown error name is not guessed into CAUGHT', () => {
56
- assert.equal(isDisagreement({ status: 'fail', errorName: 'WeirdCustomError' }), false);
57
- assert.equal(isDisagreement({ status: 'fail', errorName: 'JestAssertionError' }), true);
58
- });
59
-
60
- test('FLAKY: runs that disagree between CAUGHT and BLIND', () => {
61
- const r = reduceRuns([{ verdict: CAUGHT }, { verdict: NON_DISCRIMINATING }, { verdict: CAUGHT }]);
62
- assert.equal(r.verdict, FLAKY);
63
- });
64
-
65
- test('an INCONCLUSIVE run does not erase a decided one, but is disclosed', () => {
66
- const r = reduceRuns([{ verdict: CAUGHT, reason: 'assertion' }, { verdict: INCONCLUSIVE, reason: 'timeout' }, { verdict: CAUGHT, reason: 'assertion' }]);
67
- assert.equal(r.verdict, CAUGHT);
68
- assert.match(r.reason, /1 of 3 runs inconclusive/);
69
- });
70
-
71
- test('rollUp: one discriminating case carries the commit, guards and all', () => {
72
- assert.equal(rollUp([{ verdict: NON_DISCRIMINATING }, { verdict: NON_DISCRIMINATING }, { verdict: CAUGHT }]), CAUGHT);
73
- });
74
-
75
- test('rollUp: a SKIPPED case blocks BLIND — it might have been the discriminating one', () => {
76
- // Observed: all four tests written FOR a bug were { skip: SKIP } on a host with no
77
- // TEST_DATABASE_URL, while older cases in the same file ran and did not discriminate.
78
- // Calling that blind judges the commit on the tests NOT written for it.
79
- assert.equal(rollUp([{ verdict: NON_DISCRIMINATING }, { verdict: SKIPPED }]), INCONCLUSIVE);
80
- assert.equal(rollUp([{ verdict: NON_DISCRIMINATING }, { verdict: NON_DISCRIMINATING }, { verdict: SKIPPED }]), INCONCLUSIVE);
81
- });
82
-
83
- test('rollUp: BLIND only when every case ran and NOT ONE discriminated', () => {
84
- assert.equal(rollUp([{ verdict: NON_DISCRIMINATING }, { verdict: NON_DISCRIMINATING }]), BLIND);
85
- // one case could not be judged -> the commit cannot be called blind
86
- assert.equal(rollUp([{ verdict: NON_DISCRIMINATING }, { verdict: INCONCLUSIVE }]), INCONCLUSIVE);
87
- });
88
-
89
- test('CONTROL ARM: a verdict engine that only read exit codes fails these', () => {
90
- // TEST-001 — the old, naive implementation, executed here, asserted to still be wrong.
91
- const naive = ({ armB }) => (armB && armB.status === 'fail' ? CAUGHT : BLIND);
92
- assert.equal(naive({ armB: errFail }), CAUGHT); // it says CAUGHT...
93
- assert.equal(classify({ armA: pass, armB: errFail }).verdict, INCONCLUSIVE); // ...we say no
94
- assert.equal(naive({ armB: pass }), BLIND); // and calls every regression guard blind // and it cannot see
95
- assert.equal(classify({ armA: pass, armB: pass, identity: { proven: false, reason: 'x' } }).verdict, INCONCLUSIVE);
96
- });
97
-
98
- test('a CAUGHT on new code is still CAUGHT — the verdict does not lie, the CLAIM narrows', async () => {
99
- // Issue #3. On a feature the base lacks the code entirely, so essentially any test
100
- // touching it fails there — `expected 0 to be greater than 0` is a real AssertionError
101
- // that says nothing about whether the test is well aimed. Reclassifying it would be
102
- // dishonest (it IS a disagreement); counting it as evidence would be worse.
103
- const { commitKind } = await import('../src/verify.mjs');
104
-
105
- assert.equal(commitKind('feat(web): add seoTitle', false).newCode, true,
106
- 'a feature is new code whatever its diff looks like');
107
- assert.equal(commitKind('fix(core): a hyphen decided a title', false).newCode, false,
108
- 'a fix that changes lines is a repair, and its CAUGHT is strong evidence');
109
- assert.equal(commitKind('fix(api): add a missing export', true).newCode, true,
110
- 'a fix whose source diff only ADDS may still be reading something that was absent');
111
- assert.equal(commitKind('no conventional prefix at all', false).kind, 'unknown',
112
- 'an unrecognised subject must not be guessed into fix or feature');
113
- });