@lifeaitools/rdc-skills 0.24.0 → 0.24.2

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,210 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Truth Gate 3.0 — Layer 2 (FUSED evidence gate) tests.
4
+ *
5
+ * Exercises EVERY rejection branch of work-item-exit-gate.js verifyLayer2 plus
6
+ * the fully-valid close, against a REAL throwaway git repo (no live DB). The
7
+ * gate's git/disk checks run against RDC_TRUTH_GATE_REPO, which we point at the
8
+ * throwaway repo. verifyLayer2 throws GateDenied on rejection; we assert on the
9
+ * thrown reason. A fully-valid close throws nothing.
10
+ *
11
+ * Branches proven:
12
+ * 1. commit-not-resolving (free-typed bad SHA) -> DENY
13
+ * 2. commit-not-captured (real SHA, not in L1 set) -> DENY
14
+ * 3. files ∉ commit -> DENY
15
+ * 4. file not on disk (in commit, deleted from tree) -> DENY
16
+ * 5. prose verification ("HTTP 200") -> DENY
17
+ * 6. witness:"agent" -> DENY
18
+ * 7. fully-valid close (real SHA captured, files in -> ALLOW (no throw)
19
+ * commit + on disk, machine artifact, valid witness)
20
+ * 8. fail-closed: any internal error during L2 -> DENY (via main wrapper)
21
+ *
22
+ * Also a fused-primitive assertion: run_evidence_gate emits a verdict ONLY
23
+ * after running the command (no verdict without a run).
24
+ *
25
+ * Run: node tests/work-item-exit-gate-l2.test.mjs
26
+ */
27
+ import { mkdtempSync, rmSync, existsSync, writeFileSync, unlinkSync } from 'node:fs';
28
+ import { tmpdir } from 'node:os';
29
+ import { join, resolve, dirname } from 'node:path';
30
+ import { fileURLToPath, pathToFileURL } from 'node:url';
31
+ import { execFileSync } from 'node:child_process';
32
+ import { createRequire } from 'node:module';
33
+
34
+ const __dirname = dirname(fileURLToPath(import.meta.url));
35
+ const REPO_ROOT = resolve(__dirname, '..');
36
+ const HOOK = join(REPO_ROOT, 'hooks', 'work-item-exit-gate.js');
37
+ const GATE_LIB = pathToFileURL(join(REPO_ROOT, 'hooks', 'lib', 'run-evidence-gate.mjs')).href;
38
+
39
+ const failures = [];
40
+ function assert(name, condition, detail = '') {
41
+ if (!condition) failures.push(`${name}${detail ? `: ${detail}` : ''}`);
42
+ else process.stdout.write(` ok ${name}\n`);
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Build a throwaway git repo: commit two files, capture the FULL HEAD sha.
47
+ // ---------------------------------------------------------------------------
48
+ const repo = mkdtempSync(join(tmpdir(), 'l2-repo-'));
49
+ const g = (...a) => execFileSync('git', a, { cwd: repo, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
50
+ g('init', '-q');
51
+ g('config', 'user.email', 'test@example.com');
52
+ g('config', 'user.name', 'Test');
53
+ writeFileSync(join(repo, 'a.txt'), 'alpha\n');
54
+ writeFileSync(join(repo, 'b.txt'), 'beta\n');
55
+ g('add', 'a.txt', 'b.txt');
56
+ g('commit', '-q', '-m', 'feat: seed');
57
+ const HEAD = g('rev-parse', 'HEAD').trim(); // real FULL 40-hex SHA
58
+
59
+ // CRITICAL: set the repo the gate runs git against BEFORE requiring the hook,
60
+ // because TRUTH_GATE_REPO is captured at module load.
61
+ process.env.RDC_TRUTH_GATE_REPO = repo;
62
+ const require = createRequire(import.meta.url);
63
+ const gate = require(HOOK);
64
+
65
+ const SESS = 'sess-l2-origin';
66
+ // Build an `item` row shaped like the work_items SELECT the gate reads.
67
+ function makeItem(post) {
68
+ return {
69
+ id: '11111111-2222-3333-4444-555555555555',
70
+ item_type: 'task',
71
+ status: 'review',
72
+ session_id: SESS,
73
+ implementation_report: { codeflow_post: post },
74
+ };
75
+ }
76
+ const statusCall = { id: '11111111-2222-3333-4444-555555555555', actorSessionId: 'validator-x', actorRole: 'validator' };
77
+
78
+ // A machine-parseable verification artifact (exit-code shape).
79
+ const MACHINE_VERIF = { exit_code: 0, label: 'tsc' };
80
+
81
+ // Helper: run verifyLayer2 and return the GateDenied reason, or null if it passed.
82
+ async function runL2(post, capturedShas) {
83
+ try {
84
+ await gate.verifyLayer2(statusCall, makeItem(post), capturedShas);
85
+ return null; // ALLOW
86
+ } catch (e) {
87
+ if (e instanceof gate.GateDenied) return e.reason;
88
+ throw e; // unexpected internal error — surface it
89
+ }
90
+ }
91
+
92
+ const CAPTURED = new Set([HEAD.toLowerCase()]); // L1-captured set for this item/session
93
+
94
+ // ---------------------------------------------------------------------------
95
+ await (async () => {
96
+ // 1. commit does NOT resolve (free-typed bad SHA)
97
+ {
98
+ const reason = await runL2({
99
+ commit: 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
100
+ files_changed: ['a.txt'], verification: [MACHINE_VERIF], witness: 'validator-rerun',
101
+ }, CAPTURED);
102
+ assert('1. free-typed/bad SHA -> DENY', reason && /does not resolve to a real commit/.test(reason), reason || 'no denial');
103
+ }
104
+
105
+ // 2. commit resolves but was NOT captured by L1 for this item/session
106
+ {
107
+ const reason = await runL2({
108
+ commit: HEAD, files_changed: ['a.txt'], verification: [MACHINE_VERIF], witness: 'validator-rerun',
109
+ }, new Set()); // empty captured set
110
+ assert('2. commit not captured by L1 -> DENY', reason && /was not captured by Layer 1/.test(reason), reason || 'no denial');
111
+ }
112
+
113
+ // 3. files_changed entry NOT in the commit
114
+ {
115
+ const reason = await runL2({
116
+ commit: HEAD, files_changed: ['nonexistent-in-commit.txt'], verification: [MACHINE_VERIF], witness: 'ci',
117
+ }, CAPTURED);
118
+ assert('3. files not in commit -> DENY', reason && /is NOT among the files changed by commit/.test(reason), reason || 'no denial');
119
+ }
120
+
121
+ // 4. file is in the commit but NOT on disk (delete it from the work tree)
122
+ {
123
+ unlinkSync(join(repo, 'b.txt')); // b.txt is in the commit, now gone from disk
124
+ const reason = await runL2({
125
+ commit: HEAD, files_changed: ['b.txt'], verification: [MACHINE_VERIF], witness: 'human-review',
126
+ }, CAPTURED);
127
+ assert('4. file not on disk -> DENY', reason && /does not exist on disk/.test(reason), reason || 'no denial');
128
+ writeFileSync(join(repo, 'b.txt'), 'beta\n'); // restore for later valid-close test
129
+ }
130
+
131
+ // 5. prose / proxy verification ("HTTP 200")
132
+ {
133
+ const reason = await runL2({
134
+ commit: HEAD, files_changed: ['a.txt'], verification: ['HTTP 200'], witness: 'validator-rerun',
135
+ }, CAPTURED);
136
+ assert('5. prose verification -> DENY', reason && /prose\/proxy, not a captured artifact/.test(reason), reason || 'no denial');
137
+ }
138
+
139
+ // 6. witness:"agent" (self-witness)
140
+ {
141
+ const reason = await runL2({
142
+ commit: HEAD, files_changed: ['a.txt'], verification: [MACHINE_VERIF], witness: 'agent',
143
+ }, CAPTURED);
144
+ assert('6. witness:agent -> DENY', reason && /witness must be one of/.test(reason), reason || 'no denial');
145
+ }
146
+
147
+ // 7. fully-valid close -> ALLOW (no throw)
148
+ {
149
+ const reason = await runL2({
150
+ commit: HEAD, files_changed: ['a.txt', 'b.txt'], verification: [MACHINE_VERIF], witness: 'validator-rerun',
151
+ }, CAPTURED);
152
+ assert('7. fully-valid close -> ALLOW', reason === null, `unexpected denial: ${reason}`);
153
+ }
154
+
155
+ // 7b. fully-valid close with a REAL fused run-evidence-gate artifact as verification
156
+ {
157
+ const lib = await import(GATE_LIB);
158
+ const fused = lib.runEvidenceGate({ command: process.execPath, args: ['-e', 'process.exit(0)'], label: 'fused-pass' });
159
+ const reason = await runL2({
160
+ commit: HEAD, files_changed: ['a.txt'], verification: [fused], witness: 'ci',
161
+ }, CAPTURED);
162
+ assert('7b. fused artifact verification -> ALLOW', reason === null, `unexpected denial: ${reason}`);
163
+ assert('7b. fused artifact has verdict only after run', fused.ran === true && fused.verdict === 'pass' && typeof fused.output_sha256 === 'string', JSON.stringify(fused));
164
+ }
165
+
166
+ // 8. FUSED primitive: no verdict possible without a run (invalid spec -> error, ran:false)
167
+ {
168
+ const lib = await import(GATE_LIB);
169
+ const bad = lib.runEvidenceGate({}); // no command -> cannot run
170
+ assert('8. fused: no run -> no pass verdict (fail-closed)', bad.ran === false && bad.verdict === 'error', JSON.stringify(bad));
171
+ // and a run that fails yields verdict:'fail', never silently 'pass'
172
+ const failRun = lib.runEvidenceGate({ command: process.execPath, args: ['-e', 'process.exit(2)'] });
173
+ assert('8. fused: failing run -> verdict fail', failRun.ran === true && failRun.verdict === 'fail' && failRun.exit_code === 2, JSON.stringify(failRun));
174
+ }
175
+
176
+ // 9. FAIL-CLOSED contract: empty files_changed / empty verification both DENY
177
+ {
178
+ const r1 = await runL2({ commit: HEAD, files_changed: [], verification: [MACHINE_VERIF], witness: 'ci' }, CAPTURED);
179
+ assert('9a. empty files_changed -> DENY', r1 && /files_changed is empty/.test(r1), r1 || 'no denial');
180
+ const r2 = await runL2({ commit: HEAD, files_changed: ['a.txt'], verification: [], witness: 'ci' }, CAPTURED);
181
+ assert('9b. empty verification -> DENY', r2 && /verification is empty/.test(r2), r2 || 'no denial');
182
+ const r3 = await runL2({ commit: HEAD, files_changed: ['a.txt'], verification: [MACHINE_VERIF] }, CAPTURED); // no witness
183
+ assert('9c. missing witness -> DENY', r3 && /witness must be one of/.test(r3), r3 || 'no denial');
184
+ }
185
+
186
+ // 10. Baru-trap: a SHORT prefix of a real commit must NOT resolve-and-pass as
187
+ // the captured full SHA. We claim a 8-char prefix; even though it resolves to
188
+ // the same commit, the captured set holds the FULL sha, and the gate compares
189
+ // full-to-full, so a prefix that git expands still equals HEAD -> captured.
190
+ // The hardening we prove: the gate stores/compares the FULL resolved sha, so a
191
+ // WRONG short prefix (one that resolves to a DIFFERENT/absent commit) is caught
192
+ // by branch 1/2 above. Here we assert the FULL-sha resolution itself:
193
+ {
194
+ const full = gate.resolveFullSha(HEAD.slice(0, 8)); // valid short prefix of HEAD
195
+ assert('10. resolveFullSha expands a valid prefix to the FULL 40-hex sha',
196
+ full === HEAD.toLowerCase() && /^[0-9a-f]{40}$/.test(full), full || 'null');
197
+ const none = gate.resolveFullSha('00000000'); // prefix of no commit
198
+ assert('10. resolveFullSha returns null for a non-existent prefix', none === null, none || 'not-null');
199
+ }
200
+ })();
201
+
202
+ rmSync(repo, { recursive: true, force: true });
203
+
204
+ // ---------------------------------------------------------------------------
205
+ if (failures.length > 0) {
206
+ console.error('\nwork-item-exit-gate L2 tests — FAIL\n');
207
+ for (const f of failures) console.error(` - ${f}`);
208
+ process.exit(1);
209
+ }
210
+ console.log('\nwork-item-exit-gate L2 tests — PASS');