@lifeaitools/rdc-skills 0.24.37 → 0.24.39
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/settings.json +15 -15
- package/.claude-plugin/marketplace.json +21 -21
- package/.claude-plugin/plugin.json +1371 -1371
- package/.github/workflows/publish.yml +34 -34
- package/.github/workflows/self-test.yml +58 -58
- package/CHANGELOG.md +310 -310
- package/LICENSE +21 -21
- package/MANIFEST.md +221 -221
- package/README.md +377 -377
- package/README.sandbox.md +3 -3
- package/assets/watcher/viewer.html +164 -164
- package/bin/rdc-skills-mcp.mjs +316 -316
- package/commands/build.md +183 -183
- package/commands/collab.md +180 -180
- package/commands/deploy.md +152 -152
- package/commands/design.md +31 -31
- package/commands/edit.md +28 -28
- package/commands/fixit.md +124 -124
- package/commands/handoff.md +173 -173
- package/commands/help.md +95 -95
- package/commands/overnight.md +220 -220
- package/commands/plan.md +158 -158
- package/commands/preplan.md +131 -131
- package/commands/prototype.md +145 -145
- package/commands/release.md +49 -49
- package/commands/report.md +99 -99
- package/commands/review.md +120 -120
- package/commands/self-test.md +113 -113
- package/commands/status.md +86 -86
- package/commands/watch.md +98 -98
- package/commands/workitems.md +137 -137
- package/git-sha.json +1 -1
- package/guides/agent-bootstrap.md +295 -295
- package/guides/agents/backend.md +104 -104
- package/guides/agents/content.md +94 -94
- package/guides/agents/cs2.md +56 -56
- package/guides/agents/data.md +87 -87
- package/guides/agents/design.md +77 -77
- package/guides/agents/frontend.md +92 -92
- package/guides/agents/infrastructure.md +81 -81
- package/guides/agents/setup.md +281 -281
- package/guides/agents/verify.md +151 -151
- package/guides/agents/viz.md +106 -106
- package/guides/backend.md +146 -146
- package/guides/content.md +147 -147
- package/guides/cs2.md +190 -190
- package/guides/data.md +123 -123
- package/guides/design.md +116 -116
- package/guides/engineering-behavior.md +43 -43
- package/guides/escalation-protocol.md +125 -125
- package/guides/frontend.md +151 -151
- package/guides/history-md-spec.md +297 -297
- package/guides/infrastructure.md +179 -179
- package/guides/lessons-learned-spec.md +153 -153
- package/guides/output-contract.md +108 -108
- package/guides/publish-md-spec.md +289 -289
- package/guides/rdc-skills-startup.md +30 -30
- package/guides/verify.md +11 -11
- package/hooks/check-cwd.js +31 -31
- package/hooks/check-rdc-environment.js +164 -164
- package/hooks/check-services.js +6 -6
- package/hooks/check-stale-work-items.js +19 -19
- package/hooks/foreground-process-gate.js +128 -128
- package/hooks/gate-watchdog-selfcheck.js +257 -257
- package/hooks/hook-logger.js +25 -25
- package/hooks/lib/run-evidence-gate.mjs +241 -241
- package/hooks/no-stop-open-epics.js +127 -127
- package/hooks/post-tool-batch-gate.js +203 -203
- package/hooks/post-work-check.js +21 -21
- package/hooks/postcompact-log.js +13 -13
- package/hooks/precompact-log.js +13 -13
- package/hooks/rate-limit-retry.js +46 -46
- package/hooks/rdc-invocation-marker.js +157 -157
- package/hooks/rdc-output-contract-gate.js +94 -94
- package/hooks/require-work-item-on-commit.js +294 -294
- package/hooks/restart-brief.js +19 -19
- package/hooks/run-hidden-hook.ps1 +47 -47
- package/hooks/task-completed-gate.js +274 -274
- package/hooks/work-item-exit-gate.js +944 -944
- package/lib/catalog.mjs +236 -236
- package/lib/cloud-rewrite.mjs +155 -155
- package/package.json +56 -56
- package/rules/work-items-rpc.md +520 -520
- package/scaffold/templates/HISTORY.md.template +39 -39
- package/scaffold/templates/PUBLISH.md.template +21 -21
- package/scaffold/templates/brochure-studio-default.html +70 -70
- package/scripts/acceptance.mjs +502 -502
- package/scripts/fixtures/guides/bad-guide.md +15 -15
- package/scripts/fixtures/guides-clean/good-guide.md +16 -16
- package/scripts/install-rdc-skills.js +1289 -1289
- package/scripts/install.ps1 +202 -202
- package/scripts/install.sh +132 -132
- package/scripts/lib/assertions.mjs +287 -287
- package/scripts/lib/manifest-schema.mjs +754 -754
- package/scripts/lib/runner.mjs +465 -465
- package/scripts/lib/sandbox.mjs +435 -435
- package/scripts/prepack.mjs +32 -32
- package/scripts/rdc-brochure.mjs +464 -464
- package/scripts/rdc-design-cli.mjs +134 -134
- package/scripts/rebuild-mcp.mjs +107 -107
- package/scripts/self-test.mjs +1460 -1460
- package/scripts/stamp-git-sha.mjs +29 -29
- package/scripts/test-guide-validator.mjs +196 -196
- package/scripts/test-rdc-hooks.mjs +145 -145
- package/scripts/uninstall.ps1 +77 -77
- package/scripts/uninstall.sh +69 -69
- package/scripts/update.ps1 +43 -43
- package/scripts/update.sh +43 -43
- package/scripts/validate-place-histories.js +461 -461
- package/scripts/validate-publish-manifests.js +424 -424
- package/scripts/watch-init.mjs +100 -100
- package/skills/brochure/SKILL.md +107 -107
- package/skills/build/SKILL.md +563 -554
- package/skills/channel-formatter/SKILL.md +533 -533
- package/skills/co-develop/SKILL.md +196 -196
- package/skills/collab/SKILL.md +239 -239
- package/skills/convert/SKILL.md +140 -140
- package/skills/deploy/SKILL.md +541 -541
- package/skills/design/SKILL.md +211 -211
- package/skills/design/reference/ownership.md +16 -16
- package/skills/design/reference/rampa.md +92 -92
- package/skills/design/reference/studio-model.md +153 -153
- package/skills/edit/SKILL.md +98 -98
- package/skills/fixit/SKILL.md +165 -165
- package/skills/fs-mcp/SKILL.md +148 -148
- package/skills/handoff/SKILL.md +236 -200
- package/skills/help/SKILL.md +143 -143
- package/skills/housekeeping/SKILL.md +189 -189
- package/skills/lifeai-brochure-author/SKILL.md +340 -340
- package/skills/overnight/SKILL.md +251 -251
- package/skills/plan/SKILL.md +345 -314
- package/skills/preplan/SKILL.md +90 -90
- package/skills/prototype/SKILL.md +150 -150
- package/skills/rdc-brochurify/SKILL.md +245 -245
- package/skills/rdc-extract-verifier-rules/SKILL.md +191 -191
- package/skills/release/SKILL.md +140 -140
- package/skills/report/SKILL.md +100 -100
- package/skills/review/SKILL.md +152 -152
- package/skills/rpms-filemap/SKILL.cloud.md +111 -111
- package/skills/rpms-filemap/SKILL.md +111 -111
- package/skills/self-test/SKILL.md +132 -132
- package/skills/status/SKILL.md +99 -99
- package/skills/terminal-config/SKILL.md +62 -62
- package/skills/tests/MATRIX.md +54 -54
- package/skills/tests/README.md +47 -47
- package/skills/tests/rdc-brochure.test.json +34 -34
- package/skills/tests/rdc-build.test.json +36 -36
- package/skills/tests/rdc-channel-formatter.test.json +45 -45
- package/skills/tests/rdc-co-develop.test.json +29 -29
- package/skills/tests/rdc-collab.test.json +29 -29
- package/skills/tests/rdc-convert.test.json +35 -35
- package/skills/tests/rdc-deploy.test.json +30 -30
- package/skills/tests/rdc-design.test.json +27 -27
- package/skills/tests/rdc-edit.test.json +29 -29
- package/skills/tests/rdc-fixit.test.json +36 -36
- package/skills/tests/rdc-fs-mcp.test.json +36 -36
- package/skills/tests/rdc-handoff.test.json +28 -28
- package/skills/tests/rdc-help.test.json +29 -29
- package/skills/tests/rdc-housekeeping.test.json +31 -31
- package/skills/tests/rdc-lifeai-brochure-author.test.json +35 -35
- package/skills/tests/rdc-overnight.test.json +37 -37
- package/skills/tests/rdc-plan.test.json +27 -27
- package/skills/tests/rdc-preplan.test.json +31 -31
- package/skills/tests/rdc-prototype.test.json +28 -28
- package/skills/tests/rdc-rdc-brochurify.test.json +23 -23
- package/skills/tests/rdc-rdc-extract-verifier-rules.test.json +34 -34
- package/skills/tests/rdc-release.test.json +29 -29
- package/skills/tests/rdc-report.test.json +28 -28
- package/skills/tests/rdc-review.test.json +29 -29
- package/skills/tests/rdc-rpms-filemap.test.json +28 -28
- package/skills/tests/rdc-self-test.test.json +24 -24
- package/skills/tests/rdc-status.test.json +29 -29
- package/skills/tests/rdc-terminal-config.test.json +29 -29
- package/skills/tests/rdc-watch.test.json +24 -24
- package/skills/tests/rdc-workitems.test.json +27 -27
- package/skills/watch/SKILL.md +97 -97
- package/skills/workitems/SKILL.md +151 -151
- package/tests/acceptance.test.mjs +59 -59
- package/tests/channel-formatter.contract.test.mjs +251 -251
- package/tests/curl-surface.test.mjs +289 -289
- package/tests/harness-gates.test.mjs +325 -325
- package/tests/help-surface.test.mjs +61 -61
- package/tests/install-rdc-skills.test.mjs +49 -49
- package/tests/manifest-contract-fields.test.mjs +78 -78
- package/tests/mcp.test.mjs +271 -271
- package/tests/require-work-item-on-commit.test.mjs +162 -162
- package/tests/run-evidence-gate.test.mjs +82 -82
- package/tests/skill-test-matrix.test.mjs +66 -66
- package/tests/validate-skills.js +27 -27
- package/tests/work-item-exit-gate-l2.test.mjs +368 -368
- package/tests/work-item-exit-gate-l3.test.mjs +197 -197
|
@@ -1,368 +1,368 @@
|
|
|
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
|
-
// Fix 1 — OUTCOME GATE: a verification that RAN but did NOT pass must DENY.
|
|
203
|
-
// isMachineArtifact passes (correct shape) but isPassingArtifact must fail.
|
|
204
|
-
// -------------------------------------------------------------------------
|
|
205
|
-
{
|
|
206
|
-
// 11a. exit_code:1 (a real machine shape, but a FAILING run) -> DENY
|
|
207
|
-
const r1 = await runL2({
|
|
208
|
-
commit: HEAD, files_changed: ['a.txt'], verification: [{ exit_code: 1 }], witness: 'validator-rerun',
|
|
209
|
-
}, CAPTURED);
|
|
210
|
-
assert('11a. verification exit_code:1 -> DENY (verification-not-passing)',
|
|
211
|
-
r1 && /verification-not-passing/.test(r1), r1 || 'no denial');
|
|
212
|
-
|
|
213
|
-
// 11b. fused run-evidence-gate artifact with verdict:'fail' -> DENY
|
|
214
|
-
const lib = await import(GATE_LIB);
|
|
215
|
-
const fusedFail = lib.runEvidenceGate({ command: process.execPath, args: ['-e', 'process.exit(1)'], label: 'fused-fail' });
|
|
216
|
-
assert('11b. fused failing run has verdict:fail', fusedFail.ran === true && fusedFail.verdict === 'fail', JSON.stringify(fusedFail));
|
|
217
|
-
const r2 = await runL2({
|
|
218
|
-
commit: HEAD, files_changed: ['a.txt'], verification: [fusedFail], witness: 'ci',
|
|
219
|
-
}, CAPTURED);
|
|
220
|
-
assert('11b. fused verdict:fail -> DENY (verification-not-passing)',
|
|
221
|
-
r2 && /verification-not-passing/.test(r2), r2 || 'no denial');
|
|
222
|
-
|
|
223
|
-
// 11c. http_status:500 (error status) -> DENY
|
|
224
|
-
const r3 = await runL2({
|
|
225
|
-
commit: HEAD, files_changed: ['a.txt'], verification: [{ http_status: 500 }], witness: 'human-review',
|
|
226
|
-
}, CAPTURED);
|
|
227
|
-
assert('11c. verification http_status:500 -> DENY (verification-not-passing)',
|
|
228
|
-
r3 && /verification-not-passing/.test(r3), r3 || 'no denial');
|
|
229
|
-
|
|
230
|
-
// 11d. failed test-runner JSON ({passed:3, failed:2}) -> DENY
|
|
231
|
-
const r4 = await runL2({
|
|
232
|
-
commit: HEAD, files_changed: ['a.txt'], verification: [{ passed: 3, failed: 2 }], witness: 'ci',
|
|
233
|
-
}, CAPTURED);
|
|
234
|
-
assert('11d. verification {passed:3,failed:2} -> DENY (verification-not-passing)',
|
|
235
|
-
r4 && /verification-not-passing/.test(r4), r4 || 'no denial');
|
|
236
|
-
|
|
237
|
-
// 11e. tsc_errors:4 -> DENY
|
|
238
|
-
const r5 = await runL2({
|
|
239
|
-
commit: HEAD, files_changed: ['a.txt'], verification: [{ tsc_errors: 4 }], witness: 'ci',
|
|
240
|
-
}, CAPTURED);
|
|
241
|
-
assert('11e. verification tsc_errors:4 -> DENY (verification-not-passing)',
|
|
242
|
-
r5 && /verification-not-passing/.test(r5), r5 || 'no denial');
|
|
243
|
-
|
|
244
|
-
// 11f. PASSING shapes still ALLOW (regression guard for the outcome gate).
|
|
245
|
-
const okExit = await runL2({
|
|
246
|
-
commit: HEAD, files_changed: ['a.txt'], verification: [{ exit_code: 0 }], witness: 'validator-rerun',
|
|
247
|
-
}, CAPTURED);
|
|
248
|
-
assert('11f. exit_code:0 still ALLOWs', okExit === null, `unexpected denial: ${okExit}`);
|
|
249
|
-
const okHttp = await runL2({
|
|
250
|
-
commit: HEAD, files_changed: ['a.txt'], verification: [{ http_status: 200 }], witness: 'ci',
|
|
251
|
-
}, CAPTURED);
|
|
252
|
-
assert('11f. http_status:200 still ALLOWs', okHttp === null, `unexpected denial: ${okHttp}`);
|
|
253
|
-
const okTests = await runL2({
|
|
254
|
-
commit: HEAD, files_changed: ['a.txt'], verification: [{ passed: 5, total: 5 }], witness: 'ci',
|
|
255
|
-
}, CAPTURED);
|
|
256
|
-
assert('11f. {passed:5,total:5} still ALLOWs', okTests === null, `unexpected denial: ${okTests}`);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// 11g. isPassingArtifact unit checks (direct, lib-level).
|
|
260
|
-
{
|
|
261
|
-
const lib = await import(GATE_LIB);
|
|
262
|
-
const { isPassingArtifact } = lib;
|
|
263
|
-
assert('11g. isPassingArtifact rejects {exit_code:1}', isPassingArtifact({ exit_code: 1 }) === false);
|
|
264
|
-
assert('11g. isPassingArtifact accepts {exit_code:0}', isPassingArtifact({ exit_code: 0 }) === true);
|
|
265
|
-
assert('11g. isPassingArtifact rejects {http_status:500}', isPassingArtifact({ http_status: 500 }) === false);
|
|
266
|
-
assert('11g. isPassingArtifact accepts {http_status:204}', isPassingArtifact({ http_status: 204 }) === true);
|
|
267
|
-
assert('11g. isPassingArtifact rejects {passed:1,failed:1}', isPassingArtifact({ passed: 1, failed: 1 }) === false);
|
|
268
|
-
assert('11g. isPassingArtifact accepts {passed:5,total:5}', isPassingArtifact({ passed: 5, total: 5 }) === true);
|
|
269
|
-
assert('11g. isPassingArtifact rejects {passed:4,total:5}', isPassingArtifact({ passed: 4, total: 5 }) === false);
|
|
270
|
-
assert('11g. isPassingArtifact rejects prose "HTTP 200"', isPassingArtifact('HTTP 200') === false);
|
|
271
|
-
const fp = lib.runEvidenceGate({ command: process.execPath, args: ['-e', 'process.exit(0)'] });
|
|
272
|
-
const ff = lib.runEvidenceGate({ command: process.execPath, args: ['-e', 'process.exit(1)'] });
|
|
273
|
-
assert('11g. isPassingArtifact accepts fused verdict:pass', isPassingArtifact(fp) === true);
|
|
274
|
-
assert('11g. isPassingArtifact rejects fused verdict:fail', isPassingArtifact(ff) === false);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// -------------------------------------------------------------------------
|
|
278
|
-
// Fix 2 — NULL ORIGINATING SESSION must be FAIL-CLOSED. A null session must
|
|
279
|
-
// NOT disable the per-session commit binding (the old `!originatingSession`
|
|
280
|
-
// short-circuit accepted ANY session's captured SHA). buildCapturedShaSet
|
|
281
|
-
// DENIES when the originating session is null/empty.
|
|
282
|
-
// -------------------------------------------------------------------------
|
|
283
|
-
{
|
|
284
|
-
const otherSessionRows = [{ sha: HEAD.toLowerCase(), session_id: 'some-OTHER-session' }];
|
|
285
|
-
|
|
286
|
-
// 12a. session_id=null + a SHA captured by a DIFFERENT session -> DENY
|
|
287
|
-
let denied = null;
|
|
288
|
-
try {
|
|
289
|
-
gate.buildCapturedShaSet(null, otherSessionRows);
|
|
290
|
-
} catch (e) {
|
|
291
|
-
if (e instanceof gate.GateDenied) denied = e.reason; else throw e;
|
|
292
|
-
}
|
|
293
|
-
assert('12a. null originating session -> DENY (no provenance to bind)',
|
|
294
|
-
denied && /no originating session/.test(denied), denied || 'no denial');
|
|
295
|
-
|
|
296
|
-
// 12b. empty-string session is treated the same (fail-closed) -> DENY
|
|
297
|
-
let denied2 = null;
|
|
298
|
-
try {
|
|
299
|
-
gate.buildCapturedShaSet('', otherSessionRows);
|
|
300
|
-
} catch (e) {
|
|
301
|
-
if (e instanceof gate.GateDenied) denied2 = e.reason; else throw e;
|
|
302
|
-
}
|
|
303
|
-
assert('12b. empty originating session -> DENY', denied2 && /no originating session/.test(denied2), denied2 || 'no denial');
|
|
304
|
-
|
|
305
|
-
// 12c. with a real originating session, ONLY that session's SHAs are kept;
|
|
306
|
-
// a DIFFERENT session's captured SHA is excluded (not laundered in).
|
|
307
|
-
const mixed = [
|
|
308
|
-
{ sha: HEAD.toLowerCase(), session_id: 'some-OTHER-session' },
|
|
309
|
-
{ sha: 'a'.repeat(40), session_id: SESS },
|
|
310
|
-
];
|
|
311
|
-
const set = gate.buildCapturedShaSet(SESS, mixed);
|
|
312
|
-
assert('12c. only originating-session SHAs bound', set.has('a'.repeat(40)) && !set.has(HEAD.toLowerCase()),
|
|
313
|
-
`set=${[...set].join(',')}`);
|
|
314
|
-
|
|
315
|
-
// 12d. end-to-end consequence: null session + DIFFERENT-session SHA means
|
|
316
|
-
// the SHA is never bound, so even a structurally valid post is denied.
|
|
317
|
-
// (Drive verifyLayer2 with the EMPTY set buildCapturedShaSet would have
|
|
318
|
-
// produced were the session real-but-mismatched; null short-circuits
|
|
319
|
-
// earlier, but this proves a cross-session SHA never reaches capture.)
|
|
320
|
-
const crossSet = gate.buildCapturedShaSet(SESS, otherSessionRows); // SESS has none of these
|
|
321
|
-
const r = await runL2({
|
|
322
|
-
commit: HEAD, files_changed: ['a.txt'], verification: [MACHINE_VERIF], witness: 'validator-rerun',
|
|
323
|
-
}, crossSet);
|
|
324
|
-
assert('12d. cross-session-only SHA not captured for this session -> DENY',
|
|
325
|
-
r && /was not captured by Layer 1/.test(r), r || 'no denial');
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// -------------------------------------------------------------------------
|
|
329
|
-
// Fix 3a — assertGitRepoAvailable: a non-git directory is fail-closed with a
|
|
330
|
-
// distinct `truth-gate repo unavailable` reason (not `ref not found`).
|
|
331
|
-
// Drive it directly with the function's own repo via TRUTH_GATE_REPO capture:
|
|
332
|
-
// since TRUTH_GATE_REPO is module-captured to `repo` (a real git tree), the
|
|
333
|
-
// happy path must NOT throw; assert that, and assert a non-git path denies by
|
|
334
|
-
// re-importing the hook under a fresh env pointed at a non-git dir.
|
|
335
|
-
{
|
|
336
|
-
// happy path: the live throwaway repo is a work tree -> no throw
|
|
337
|
-
let ok = true;
|
|
338
|
-
try { gate.assertGitRepoAvailable(); } catch { ok = false; }
|
|
339
|
-
assert('13a. assertGitRepoAvailable passes for a real git work tree', ok === true);
|
|
340
|
-
|
|
341
|
-
// non-git path: re-load the hook with RDC_TRUTH_GATE_REPO pointed at a
|
|
342
|
-
// brand-new empty (non-git) temp dir; assertGitRepoAvailable must DENY.
|
|
343
|
-
const nonGit = mkdtempSync(join(tmpdir(), 'l2-nongit-'));
|
|
344
|
-
const prevRepo = process.env.RDC_TRUTH_GATE_REPO;
|
|
345
|
-
process.env.RDC_TRUTH_GATE_REPO = nonGit;
|
|
346
|
-
delete require.cache[require.resolve(HOOK)];
|
|
347
|
-
const gate2 = require(HOOK);
|
|
348
|
-
let denied = null;
|
|
349
|
-
try { gate2.assertGitRepoAvailable(); }
|
|
350
|
-
catch (e) { if (e instanceof gate2.GateDenied) denied = e.reason; else throw e; }
|
|
351
|
-
assert('13a. non-git dir -> DENY (truth-gate repo unavailable)',
|
|
352
|
-
denied && /truth-gate repo unavailable/.test(denied), denied || 'no denial');
|
|
353
|
-
// restore env + module cache for any later use
|
|
354
|
-
process.env.RDC_TRUTH_GATE_REPO = prevRepo;
|
|
355
|
-
delete require.cache[require.resolve(HOOK)];
|
|
356
|
-
rmSync(nonGit, { recursive: true, force: true });
|
|
357
|
-
}
|
|
358
|
-
})();
|
|
359
|
-
|
|
360
|
-
rmSync(repo, { recursive: true, force: true });
|
|
361
|
-
|
|
362
|
-
// ---------------------------------------------------------------------------
|
|
363
|
-
if (failures.length > 0) {
|
|
364
|
-
console.error('\nwork-item-exit-gate L2 tests — FAIL\n');
|
|
365
|
-
for (const f of failures) console.error(` - ${f}`);
|
|
366
|
-
process.exit(1);
|
|
367
|
-
}
|
|
368
|
-
console.log('\nwork-item-exit-gate L2 tests — PASS');
|
|
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
|
+
// Fix 1 — OUTCOME GATE: a verification that RAN but did NOT pass must DENY.
|
|
203
|
+
// isMachineArtifact passes (correct shape) but isPassingArtifact must fail.
|
|
204
|
+
// -------------------------------------------------------------------------
|
|
205
|
+
{
|
|
206
|
+
// 11a. exit_code:1 (a real machine shape, but a FAILING run) -> DENY
|
|
207
|
+
const r1 = await runL2({
|
|
208
|
+
commit: HEAD, files_changed: ['a.txt'], verification: [{ exit_code: 1 }], witness: 'validator-rerun',
|
|
209
|
+
}, CAPTURED);
|
|
210
|
+
assert('11a. verification exit_code:1 -> DENY (verification-not-passing)',
|
|
211
|
+
r1 && /verification-not-passing/.test(r1), r1 || 'no denial');
|
|
212
|
+
|
|
213
|
+
// 11b. fused run-evidence-gate artifact with verdict:'fail' -> DENY
|
|
214
|
+
const lib = await import(GATE_LIB);
|
|
215
|
+
const fusedFail = lib.runEvidenceGate({ command: process.execPath, args: ['-e', 'process.exit(1)'], label: 'fused-fail' });
|
|
216
|
+
assert('11b. fused failing run has verdict:fail', fusedFail.ran === true && fusedFail.verdict === 'fail', JSON.stringify(fusedFail));
|
|
217
|
+
const r2 = await runL2({
|
|
218
|
+
commit: HEAD, files_changed: ['a.txt'], verification: [fusedFail], witness: 'ci',
|
|
219
|
+
}, CAPTURED);
|
|
220
|
+
assert('11b. fused verdict:fail -> DENY (verification-not-passing)',
|
|
221
|
+
r2 && /verification-not-passing/.test(r2), r2 || 'no denial');
|
|
222
|
+
|
|
223
|
+
// 11c. http_status:500 (error status) -> DENY
|
|
224
|
+
const r3 = await runL2({
|
|
225
|
+
commit: HEAD, files_changed: ['a.txt'], verification: [{ http_status: 500 }], witness: 'human-review',
|
|
226
|
+
}, CAPTURED);
|
|
227
|
+
assert('11c. verification http_status:500 -> DENY (verification-not-passing)',
|
|
228
|
+
r3 && /verification-not-passing/.test(r3), r3 || 'no denial');
|
|
229
|
+
|
|
230
|
+
// 11d. failed test-runner JSON ({passed:3, failed:2}) -> DENY
|
|
231
|
+
const r4 = await runL2({
|
|
232
|
+
commit: HEAD, files_changed: ['a.txt'], verification: [{ passed: 3, failed: 2 }], witness: 'ci',
|
|
233
|
+
}, CAPTURED);
|
|
234
|
+
assert('11d. verification {passed:3,failed:2} -> DENY (verification-not-passing)',
|
|
235
|
+
r4 && /verification-not-passing/.test(r4), r4 || 'no denial');
|
|
236
|
+
|
|
237
|
+
// 11e. tsc_errors:4 -> DENY
|
|
238
|
+
const r5 = await runL2({
|
|
239
|
+
commit: HEAD, files_changed: ['a.txt'], verification: [{ tsc_errors: 4 }], witness: 'ci',
|
|
240
|
+
}, CAPTURED);
|
|
241
|
+
assert('11e. verification tsc_errors:4 -> DENY (verification-not-passing)',
|
|
242
|
+
r5 && /verification-not-passing/.test(r5), r5 || 'no denial');
|
|
243
|
+
|
|
244
|
+
// 11f. PASSING shapes still ALLOW (regression guard for the outcome gate).
|
|
245
|
+
const okExit = await runL2({
|
|
246
|
+
commit: HEAD, files_changed: ['a.txt'], verification: [{ exit_code: 0 }], witness: 'validator-rerun',
|
|
247
|
+
}, CAPTURED);
|
|
248
|
+
assert('11f. exit_code:0 still ALLOWs', okExit === null, `unexpected denial: ${okExit}`);
|
|
249
|
+
const okHttp = await runL2({
|
|
250
|
+
commit: HEAD, files_changed: ['a.txt'], verification: [{ http_status: 200 }], witness: 'ci',
|
|
251
|
+
}, CAPTURED);
|
|
252
|
+
assert('11f. http_status:200 still ALLOWs', okHttp === null, `unexpected denial: ${okHttp}`);
|
|
253
|
+
const okTests = await runL2({
|
|
254
|
+
commit: HEAD, files_changed: ['a.txt'], verification: [{ passed: 5, total: 5 }], witness: 'ci',
|
|
255
|
+
}, CAPTURED);
|
|
256
|
+
assert('11f. {passed:5,total:5} still ALLOWs', okTests === null, `unexpected denial: ${okTests}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 11g. isPassingArtifact unit checks (direct, lib-level).
|
|
260
|
+
{
|
|
261
|
+
const lib = await import(GATE_LIB);
|
|
262
|
+
const { isPassingArtifact } = lib;
|
|
263
|
+
assert('11g. isPassingArtifact rejects {exit_code:1}', isPassingArtifact({ exit_code: 1 }) === false);
|
|
264
|
+
assert('11g. isPassingArtifact accepts {exit_code:0}', isPassingArtifact({ exit_code: 0 }) === true);
|
|
265
|
+
assert('11g. isPassingArtifact rejects {http_status:500}', isPassingArtifact({ http_status: 500 }) === false);
|
|
266
|
+
assert('11g. isPassingArtifact accepts {http_status:204}', isPassingArtifact({ http_status: 204 }) === true);
|
|
267
|
+
assert('11g. isPassingArtifact rejects {passed:1,failed:1}', isPassingArtifact({ passed: 1, failed: 1 }) === false);
|
|
268
|
+
assert('11g. isPassingArtifact accepts {passed:5,total:5}', isPassingArtifact({ passed: 5, total: 5 }) === true);
|
|
269
|
+
assert('11g. isPassingArtifact rejects {passed:4,total:5}', isPassingArtifact({ passed: 4, total: 5 }) === false);
|
|
270
|
+
assert('11g. isPassingArtifact rejects prose "HTTP 200"', isPassingArtifact('HTTP 200') === false);
|
|
271
|
+
const fp = lib.runEvidenceGate({ command: process.execPath, args: ['-e', 'process.exit(0)'] });
|
|
272
|
+
const ff = lib.runEvidenceGate({ command: process.execPath, args: ['-e', 'process.exit(1)'] });
|
|
273
|
+
assert('11g. isPassingArtifact accepts fused verdict:pass', isPassingArtifact(fp) === true);
|
|
274
|
+
assert('11g. isPassingArtifact rejects fused verdict:fail', isPassingArtifact(ff) === false);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// -------------------------------------------------------------------------
|
|
278
|
+
// Fix 2 — NULL ORIGINATING SESSION must be FAIL-CLOSED. A null session must
|
|
279
|
+
// NOT disable the per-session commit binding (the old `!originatingSession`
|
|
280
|
+
// short-circuit accepted ANY session's captured SHA). buildCapturedShaSet
|
|
281
|
+
// DENIES when the originating session is null/empty.
|
|
282
|
+
// -------------------------------------------------------------------------
|
|
283
|
+
{
|
|
284
|
+
const otherSessionRows = [{ sha: HEAD.toLowerCase(), session_id: 'some-OTHER-session' }];
|
|
285
|
+
|
|
286
|
+
// 12a. session_id=null + a SHA captured by a DIFFERENT session -> DENY
|
|
287
|
+
let denied = null;
|
|
288
|
+
try {
|
|
289
|
+
gate.buildCapturedShaSet(null, otherSessionRows);
|
|
290
|
+
} catch (e) {
|
|
291
|
+
if (e instanceof gate.GateDenied) denied = e.reason; else throw e;
|
|
292
|
+
}
|
|
293
|
+
assert('12a. null originating session -> DENY (no provenance to bind)',
|
|
294
|
+
denied && /no originating session/.test(denied), denied || 'no denial');
|
|
295
|
+
|
|
296
|
+
// 12b. empty-string session is treated the same (fail-closed) -> DENY
|
|
297
|
+
let denied2 = null;
|
|
298
|
+
try {
|
|
299
|
+
gate.buildCapturedShaSet('', otherSessionRows);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
if (e instanceof gate.GateDenied) denied2 = e.reason; else throw e;
|
|
302
|
+
}
|
|
303
|
+
assert('12b. empty originating session -> DENY', denied2 && /no originating session/.test(denied2), denied2 || 'no denial');
|
|
304
|
+
|
|
305
|
+
// 12c. with a real originating session, ONLY that session's SHAs are kept;
|
|
306
|
+
// a DIFFERENT session's captured SHA is excluded (not laundered in).
|
|
307
|
+
const mixed = [
|
|
308
|
+
{ sha: HEAD.toLowerCase(), session_id: 'some-OTHER-session' },
|
|
309
|
+
{ sha: 'a'.repeat(40), session_id: SESS },
|
|
310
|
+
];
|
|
311
|
+
const set = gate.buildCapturedShaSet(SESS, mixed);
|
|
312
|
+
assert('12c. only originating-session SHAs bound', set.has('a'.repeat(40)) && !set.has(HEAD.toLowerCase()),
|
|
313
|
+
`set=${[...set].join(',')}`);
|
|
314
|
+
|
|
315
|
+
// 12d. end-to-end consequence: null session + DIFFERENT-session SHA means
|
|
316
|
+
// the SHA is never bound, so even a structurally valid post is denied.
|
|
317
|
+
// (Drive verifyLayer2 with the EMPTY set buildCapturedShaSet would have
|
|
318
|
+
// produced were the session real-but-mismatched; null short-circuits
|
|
319
|
+
// earlier, but this proves a cross-session SHA never reaches capture.)
|
|
320
|
+
const crossSet = gate.buildCapturedShaSet(SESS, otherSessionRows); // SESS has none of these
|
|
321
|
+
const r = await runL2({
|
|
322
|
+
commit: HEAD, files_changed: ['a.txt'], verification: [MACHINE_VERIF], witness: 'validator-rerun',
|
|
323
|
+
}, crossSet);
|
|
324
|
+
assert('12d. cross-session-only SHA not captured for this session -> DENY',
|
|
325
|
+
r && /was not captured by Layer 1/.test(r), r || 'no denial');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// -------------------------------------------------------------------------
|
|
329
|
+
// Fix 3a — assertGitRepoAvailable: a non-git directory is fail-closed with a
|
|
330
|
+
// distinct `truth-gate repo unavailable` reason (not `ref not found`).
|
|
331
|
+
// Drive it directly with the function's own repo via TRUTH_GATE_REPO capture:
|
|
332
|
+
// since TRUTH_GATE_REPO is module-captured to `repo` (a real git tree), the
|
|
333
|
+
// happy path must NOT throw; assert that, and assert a non-git path denies by
|
|
334
|
+
// re-importing the hook under a fresh env pointed at a non-git dir.
|
|
335
|
+
{
|
|
336
|
+
// happy path: the live throwaway repo is a work tree -> no throw
|
|
337
|
+
let ok = true;
|
|
338
|
+
try { gate.assertGitRepoAvailable(); } catch { ok = false; }
|
|
339
|
+
assert('13a. assertGitRepoAvailable passes for a real git work tree', ok === true);
|
|
340
|
+
|
|
341
|
+
// non-git path: re-load the hook with RDC_TRUTH_GATE_REPO pointed at a
|
|
342
|
+
// brand-new empty (non-git) temp dir; assertGitRepoAvailable must DENY.
|
|
343
|
+
const nonGit = mkdtempSync(join(tmpdir(), 'l2-nongit-'));
|
|
344
|
+
const prevRepo = process.env.RDC_TRUTH_GATE_REPO;
|
|
345
|
+
process.env.RDC_TRUTH_GATE_REPO = nonGit;
|
|
346
|
+
delete require.cache[require.resolve(HOOK)];
|
|
347
|
+
const gate2 = require(HOOK);
|
|
348
|
+
let denied = null;
|
|
349
|
+
try { gate2.assertGitRepoAvailable(); }
|
|
350
|
+
catch (e) { if (e instanceof gate2.GateDenied) denied = e.reason; else throw e; }
|
|
351
|
+
assert('13a. non-git dir -> DENY (truth-gate repo unavailable)',
|
|
352
|
+
denied && /truth-gate repo unavailable/.test(denied), denied || 'no denial');
|
|
353
|
+
// restore env + module cache for any later use
|
|
354
|
+
process.env.RDC_TRUTH_GATE_REPO = prevRepo;
|
|
355
|
+
delete require.cache[require.resolve(HOOK)];
|
|
356
|
+
rmSync(nonGit, { recursive: true, force: true });
|
|
357
|
+
}
|
|
358
|
+
})();
|
|
359
|
+
|
|
360
|
+
rmSync(repo, { recursive: true, force: true });
|
|
361
|
+
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
if (failures.length > 0) {
|
|
364
|
+
console.error('\nwork-item-exit-gate L2 tests — FAIL\n');
|
|
365
|
+
for (const f of failures) console.error(` - ${f}`);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
console.log('\nwork-item-exit-gate L2 tests — PASS');
|