@mmnto/totem 1.92.0 → 1.93.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.
- package/dist/artifacts/verdict.d.ts +1088 -0
- package/dist/artifacts/verdict.d.ts.map +1 -0
- package/dist/artifacts/verdict.js +863 -0
- package/dist/artifacts/verdict.js.map +1 -0
- package/dist/artifacts/verdict.test.d.ts +25 -0
- package/dist/artifacts/verdict.test.d.ts.map +1 -0
- package/dist/artifacts/verdict.test.js +878 -0
- package/dist/artifacts/verdict.test.js.map +1 -0
- package/dist/config-schema.d.ts +155 -0
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config-schema.js +22 -0
- package/dist/config-schema.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/sys/git.d.ts +21 -0
- package/dist/sys/git.d.ts.map +1 -1
- package/dist/sys/git.js +15 -3
- package/dist/sys/git.js.map +1 -1
- package/dist/sys/git.test.js +58 -1
- package/dist/sys/git.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verdict-artifact tests (mmnto-ai/totem#2106, Proposal 302/304 R2 review runner).
|
|
3
|
+
*
|
|
4
|
+
* The invariants locked in by the design doc + panel rounds 1 and the dual
|
|
5
|
+
* as-built round (rev 4):
|
|
6
|
+
* - content identity excludes `createdAt`; EEXIST is logical-identity dedup, a
|
|
7
|
+
* differing record at the same address is a hard identity violation.
|
|
8
|
+
* - content-address verification on load (finding 4): a schema-valid artifact
|
|
9
|
+
* stored under the WRONG 64-hex filename is rejected loud on direct load;
|
|
10
|
+
* warned + skipped during a lineage scan.
|
|
11
|
+
* - lane union: `completed` requires `runArtifactHash`; counts are validated,
|
|
12
|
+
* never mirrored on trust; a panelArtifactHash needs ≥2 completed lanes.
|
|
13
|
+
* - the persisted boundary re-derives `settled` (finding 5) — a fabricated flag
|
|
14
|
+
* is rejected.
|
|
15
|
+
* - cross-field invariants (finding 9): panel ⟺ diversity ⟺ ≥2 completed lanes;
|
|
16
|
+
* round-chain shape; nonempty unique lanes; source-discriminated lineage input.
|
|
17
|
+
* - LANE-BLINDNESS (Prop 302): no warm/cold runner discriminator KEY exists, and
|
|
18
|
+
* no runner class can be smuggled through a laneId VALUE (G1).
|
|
19
|
+
* - all-terminal-failure writes an honest, LEGAL verdict (G3).
|
|
20
|
+
* - the covariate line is core-owned, format v1 (G4).
|
|
21
|
+
* - tolerant-within-major (F1); composite lineage key with no delimiter-
|
|
22
|
+
* injection ambiguity; implicit round linkage = latest by round.index.
|
|
23
|
+
*/
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
import * as os from 'node:os';
|
|
26
|
+
import * as path from 'node:path';
|
|
27
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
28
|
+
import { TotemError, TotemParseError } from '../errors.js';
|
|
29
|
+
import { cleanTmpDir } from '../test-utils.js';
|
|
30
|
+
import { calculateDeterministicHash } from './hash.js';
|
|
31
|
+
import { classifyDiversity } from './panel.js';
|
|
32
|
+
import { computeLineageKey, computeVerdictArtifactContentHash, deriveCacheEligible, deriveSettled, findLatestVerdictForLineage, listVerdictArtifacts, loadVerdictArtifact, renderCovariateLine, saveVerdictArtifact, VERDICT_ARTIFACT_SCHEMA_VERSION, VerdictArtifactSchema, verdictsDir, } from './verdict.js';
|
|
33
|
+
// ─── Fixtures ──────────────────────────────────────────────────────────────
|
|
34
|
+
//
|
|
35
|
+
// laneIds follow the backend-derived vocabulary `lane-<index>:<backend>` (G1),
|
|
36
|
+
// built from the lane's fan position + resolved backend / configured lane.
|
|
37
|
+
/** Test sink for the now-required scan `onWarn` (core is console-free; PR #2337 CR). */
|
|
38
|
+
const noWarn = () => { };
|
|
39
|
+
function completedLane(index, provider = 'gemini') {
|
|
40
|
+
const resolvedBackend = `${provider}:${provider}-model`;
|
|
41
|
+
return {
|
|
42
|
+
status: 'completed',
|
|
43
|
+
laneId: `lane-${index}:${resolvedBackend}`,
|
|
44
|
+
resolvedBackend,
|
|
45
|
+
runArtifactHash: 'a'.repeat(64),
|
|
46
|
+
verdictSummary: { critical: 0, warn: 0, info: 0 },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function abstainedLane(index, provider = 'anthropic') {
|
|
50
|
+
const resolvedBackend = `${provider}:${provider}-model`;
|
|
51
|
+
return {
|
|
52
|
+
status: 'abstained',
|
|
53
|
+
laneId: `lane-${index}:${resolvedBackend}`,
|
|
54
|
+
resolvedBackend,
|
|
55
|
+
runArtifactHash: 'b'.repeat(64),
|
|
56
|
+
reason: 'output unextractable by the shared extractor',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function failedLane(index, configured = 'openai:gpt-model') {
|
|
60
|
+
return {
|
|
61
|
+
status: 'failed',
|
|
62
|
+
laneId: `lane-${index}:${configured}`,
|
|
63
|
+
typedReason: 'quota-exhausted',
|
|
64
|
+
// rev-6 item 3: the configured lane the id suffix binds to (present even pre-resolution).
|
|
65
|
+
configuredLane: configured,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function verdict(overrides = {}) {
|
|
69
|
+
const lanes = overrides.lanes ?? [completedLane(0)];
|
|
70
|
+
const v = {
|
|
71
|
+
schemaVersion: VERDICT_ARTIFACT_SCHEMA_VERSION,
|
|
72
|
+
diffScope: { source: 'staged', diffHash: 'd'.repeat(64) },
|
|
73
|
+
lanes,
|
|
74
|
+
attemptedLaneCount: lanes.length,
|
|
75
|
+
completedLaneCount: lanes.filter((l) => l.status === 'completed').length,
|
|
76
|
+
postChecks: [],
|
|
77
|
+
findings: [],
|
|
78
|
+
round: { index: 0, lineageKey: 'lineage-fixture' },
|
|
79
|
+
reviewedState: 'matched',
|
|
80
|
+
settled: true, // recomputed below unless the caller pins it explicitly
|
|
81
|
+
createdAt: '2026-07-10T00:00:00.000Z',
|
|
82
|
+
...overrides,
|
|
83
|
+
};
|
|
84
|
+
// Keep `settled` consistent with the finding-5 re-derivation by default so
|
|
85
|
+
// structural fixtures satisfy the superRefine. A negative test that WANTS an
|
|
86
|
+
// inconsistent `settled` passes it explicitly in `overrides` (honored here).
|
|
87
|
+
if (!('settled' in overrides))
|
|
88
|
+
v.settled = deriveSettled(v);
|
|
89
|
+
return v;
|
|
90
|
+
}
|
|
91
|
+
const FORBIDDEN_RUNNER_KEY = /(^|[^a-z])(runner|lanemode|lane-mode|warm|cold)([^a-z]|$)/i;
|
|
92
|
+
// ─── Schema: diff scope, lane union, counts, panel guard ────────────────────
|
|
93
|
+
describe('VerdictArtifactSchema — structure', () => {
|
|
94
|
+
it('accepts a minimal valid 1.0.0 verdict', () => {
|
|
95
|
+
expect(VerdictArtifactSchema.safeParse(verdict()).success).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
it('diffScope: refs required only where the source makes them meaningful', () => {
|
|
98
|
+
const dh = 'd'.repeat(64);
|
|
99
|
+
const withScope = (diffScope) => VerdictArtifactSchema.safeParse({ ...verdict(), diffScope }).success;
|
|
100
|
+
// explicit-range needs base AND head.
|
|
101
|
+
expect(withScope({ source: 'explicit-range', diffHash: dh, base: 'HEAD~3', head: 'HEAD' })).toBe(true);
|
|
102
|
+
expect(withScope({ source: 'explicit-range', diffHash: dh, base: 'HEAD~3' })).toBe(false);
|
|
103
|
+
// branch-vs-base needs base only.
|
|
104
|
+
expect(withScope({ source: 'branch-vs-base', diffHash: dh, base: 'main' })).toBe(true);
|
|
105
|
+
expect(withScope({ source: 'branch-vs-base', diffHash: dh })).toBe(false);
|
|
106
|
+
// staged / uncommitted carry no refs.
|
|
107
|
+
expect(withScope({ source: 'staged', diffHash: dh })).toBe(true);
|
|
108
|
+
expect(withScope({ source: 'uncommitted', diffHash: dh })).toBe(true);
|
|
109
|
+
// diffHash is always required.
|
|
110
|
+
expect(withScope({ source: 'staged' })).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
it('rejects attemptedLaneCount ≠ lanes.length (counts never mirrored on trust)', () => {
|
|
113
|
+
const bad = structuredClone(verdict());
|
|
114
|
+
bad.attemptedLaneCount = 5;
|
|
115
|
+
expect(VerdictArtifactSchema.safeParse(bad).success).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
it('rejects a completedLaneCount that disagrees with the completed lanes', () => {
|
|
118
|
+
const bad = structuredClone(verdict({ lanes: [completedLane(0), failedLane(1)] }));
|
|
119
|
+
bad.completedLaneCount = 2; // only one lane is actually completed
|
|
120
|
+
expect(VerdictArtifactSchema.safeParse(bad).success).toBe(false);
|
|
121
|
+
// sanity: the honest count parses
|
|
122
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ lanes: [completedLane(0), failedLane(1)] }))
|
|
123
|
+
.success).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
it('the lane union enforces runArtifactHash on a completed lane', () => {
|
|
126
|
+
const bad = structuredClone(verdict());
|
|
127
|
+
delete bad.lanes[0].runArtifactHash;
|
|
128
|
+
expect(VerdictArtifactSchema.safeParse(bad).success).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
it('panelArtifactHash requires ≥2 completed lanes', () => {
|
|
131
|
+
// one completed lane + a panel hash (+ diversity so only the ≥2 rule is tested) ⇒ rejected
|
|
132
|
+
const one = {
|
|
133
|
+
...verdict(),
|
|
134
|
+
panelArtifactHash: 'e'.repeat(64),
|
|
135
|
+
diversity: classifyDiversity(['gemini']),
|
|
136
|
+
};
|
|
137
|
+
expect(VerdictArtifactSchema.safeParse(one).success).toBe(false);
|
|
138
|
+
// two completed lanes + a panel hash + diversity ⇒ accepted
|
|
139
|
+
const two = {
|
|
140
|
+
...verdict({ lanes: [completedLane(0), completedLane(1, 'anthropic')] }),
|
|
141
|
+
panelArtifactHash: 'e'.repeat(64),
|
|
142
|
+
diversity: classifyDiversity(['gemini', 'anthropic']),
|
|
143
|
+
};
|
|
144
|
+
expect(VerdictArtifactSchema.safeParse(two).success).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
it('postChecks reuse the persisted #2103 vocabulary verbatim (ruleName/tier/verdict/message)', () => {
|
|
147
|
+
const v = verdict({
|
|
148
|
+
postChecks: [
|
|
149
|
+
{ ruleName: 'structured-output', tier: 'decidable', verdict: 'pass', message: 'parses' },
|
|
150
|
+
{
|
|
151
|
+
ruleName: 'provenance-fail-safe-down',
|
|
152
|
+
tier: 'sensor',
|
|
153
|
+
verdict: 'abstain',
|
|
154
|
+
message: 'n/a',
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
expect(VerdictArtifactSchema.safeParse(v).success).toBe(true);
|
|
159
|
+
// verdict vocabulary is the CheckVerdict enum — an alien value is rejected.
|
|
160
|
+
const bad = structuredClone(v);
|
|
161
|
+
bad.postChecks[0].verdict = 'accepted';
|
|
162
|
+
expect(VerdictArtifactSchema.safeParse(bad).success).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
it('findings align with ShieldFinding fields (confidence optional, 0..1)', () => {
|
|
165
|
+
const v = verdict({
|
|
166
|
+
findings: [
|
|
167
|
+
{ severity: 'CRITICAL', confidence: 0.9, file: 'src/x.ts', line: 42, message: 'boom' },
|
|
168
|
+
{ severity: 'INFO', message: 'a cross-cutting note with no file/line/confidence' },
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
expect(VerdictArtifactSchema.safeParse(v).success).toBe(true);
|
|
172
|
+
const outOfRange = structuredClone(v);
|
|
173
|
+
outOfRange.findings[0].confidence = 1.5;
|
|
174
|
+
expect(VerdictArtifactSchema.safeParse(outOfRange).success).toBe(false);
|
|
175
|
+
const badSeverity = structuredClone(v);
|
|
176
|
+
badSeverity.findings[0].severity = 'BLOCKER';
|
|
177
|
+
expect(VerdictArtifactSchema.safeParse(badSeverity).success).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
// ─── laneId value-channel lane-blindness (Prop 302 G1) ──────────────────────
|
|
181
|
+
// Closed STRUCTURALLY: the suffix must equal the lane's binding field, so free
|
|
182
|
+
// text (runner classes included) has no channel. The former substring blacklist
|
|
183
|
+
// was removed — it false-positived legitimate model names (PR #2337 greptile P2).
|
|
184
|
+
describe('laneId value-channel lane-blindness (Prop 302 G1, structural)', () => {
|
|
185
|
+
it('accepts a well-formed backend-derived laneId', () => {
|
|
186
|
+
expect(VerdictArtifactSchema.safeParse(verdict()).success).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
it('rejects a laneId smuggling a runner class (warm) — suffix diverges from the binding field', () => {
|
|
189
|
+
const smuggled = verdict({
|
|
190
|
+
lanes: [{ ...completedLane(0), laneId: 'lane-0:warm-resident:some-model' }],
|
|
191
|
+
});
|
|
192
|
+
expect(VerdictArtifactSchema.safeParse(smuggled).success).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
it('rejects each runner-class smuggle attempt (cold/headless/sdk-runner) structurally', () => {
|
|
195
|
+
for (const smuggle of ['cold', 'headless', 'sdk-runner']) {
|
|
196
|
+
const v = verdict({ lanes: [{ ...completedLane(0), laneId: `lane-0:${smuggle}-x:m` }] });
|
|
197
|
+
expect(VerdictArtifactSchema.safeParse(v).success).toBe(false);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
it('ACCEPTS a legitimate model name containing a runner-class token when the suffix matches the binding (no substring false positive)', () => {
|
|
201
|
+
const backend = 'anthropic:claude-cold-inference-mini';
|
|
202
|
+
const lane = { ...completedLane(0), laneId: `lane-0:${backend}`, resolvedBackend: backend };
|
|
203
|
+
const v = verdict({ lanes: [lane], diversity: undefined, panelArtifactHash: undefined });
|
|
204
|
+
const parsed = VerdictArtifactSchema.safeParse(v);
|
|
205
|
+
expect(parsed.success).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
it('rejects a laneId that violates the backend-derived shape', () => {
|
|
208
|
+
const malformed = verdict({ lanes: [{ ...completedLane(0), laneId: 'l1' }] });
|
|
209
|
+
expect(VerdictArtifactSchema.safeParse(malformed).success).toBe(false);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
// ─── Structural laneId validation (rev-5 item 6) ─────────────────────────────
|
|
213
|
+
describe('structural laneId validation (rev-5 item 6)', () => {
|
|
214
|
+
it('rejects a laneId whose index does not match the lane array position', () => {
|
|
215
|
+
// completedLane(5) builds `lane-5:gemini:gemini-model` but sits at array index 0.
|
|
216
|
+
const indexMismatch = verdict({ lanes: [completedLane(5)] });
|
|
217
|
+
const parsed = VerdictArtifactSchema.safeParse(indexMismatch);
|
|
218
|
+
expect(parsed.success).toBe(false);
|
|
219
|
+
if (!parsed.success) {
|
|
220
|
+
expect(parsed.error.issues.some((i) => /array position/.test(i.message))).toBe(true);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
it("rejects codex's example: `lane-99:gemini:fake` with resolvedBackend `anthropic:real`", () => {
|
|
224
|
+
const smuggled = verdict({
|
|
225
|
+
lanes: [
|
|
226
|
+
{
|
|
227
|
+
status: 'completed',
|
|
228
|
+
laneId: 'lane-99:gemini:fake',
|
|
229
|
+
resolvedBackend: 'anthropic:real',
|
|
230
|
+
runArtifactHash: 'a'.repeat(64),
|
|
231
|
+
verdictSummary: { critical: 0, warn: 0, info: 0 },
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
});
|
|
235
|
+
expect(VerdictArtifactSchema.safeParse(smuggled).success).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
it('rejects a completed lane whose suffix disagrees with resolvedBackend at the RIGHT index', () => {
|
|
238
|
+
const suffixMismatch = verdict({
|
|
239
|
+
lanes: [{ ...completedLane(0), laneId: 'lane-0:gemini:other-model' }],
|
|
240
|
+
});
|
|
241
|
+
const parsed = VerdictArtifactSchema.safeParse(suffixMismatch);
|
|
242
|
+
expect(parsed.success).toBe(false);
|
|
243
|
+
if (!parsed.success) {
|
|
244
|
+
expect(parsed.error.issues.some((i) => /must equal resolvedBackend/.test(i.message))).toBe(true);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
it('rejects an abstained lane with a suffix/backend mismatch', () => {
|
|
248
|
+
const bad = verdict({
|
|
249
|
+
lanes: [{ ...abstainedLane(0), laneId: 'lane-0:gemini:not-the-backend' }],
|
|
250
|
+
});
|
|
251
|
+
expect(VerdictArtifactSchema.safeParse(bad).success).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
it('a failed lane binds its laneId suffix to configuredLane; resolvedBackend may differ (quota-fallback) (rev-6 item 3)', () => {
|
|
254
|
+
// suffix (failedLane fixture: openai:gpt-model) === configuredLane ⇒ accepted.
|
|
255
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ lanes: [failedLane(0)] })).success).toBe(true);
|
|
256
|
+
// A resolvedBackend that DIFFERS from configuredLane is a legitimate quota-fallback
|
|
257
|
+
// record — the suffix still binds to configuredLane, so this is ACCEPTED.
|
|
258
|
+
const quotaFallback = verdict({
|
|
259
|
+
lanes: [{ ...failedLane(0), resolvedBackend: 'openai:fallback-model' }],
|
|
260
|
+
});
|
|
261
|
+
expect(VerdictArtifactSchema.safeParse(quotaFallback).success).toBe(true);
|
|
262
|
+
// codex's tautology: an INVENTED suffix that disagrees with the declared
|
|
263
|
+
// configuredLane ⇒ rejected (the suffix is no longer free-floating).
|
|
264
|
+
const suffixMismatch = verdict({
|
|
265
|
+
lanes: [
|
|
266
|
+
{
|
|
267
|
+
status: 'failed',
|
|
268
|
+
laneId: 'lane-0:gemini:completely-invented',
|
|
269
|
+
typedReason: 'config-error',
|
|
270
|
+
configuredLane: 'openai:gpt-model',
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
});
|
|
274
|
+
const parsed = VerdictArtifactSchema.safeParse(suffixMismatch);
|
|
275
|
+
expect(parsed.success).toBe(false);
|
|
276
|
+
if (!parsed.success) {
|
|
277
|
+
expect(parsed.error.issues.some((i) => /must equal configuredLane/.test(i.message))).toBe(true);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
it('conforming artifacts pass (mixed statuses, indexes in order)', () => {
|
|
281
|
+
const mixed = verdict({ lanes: [completedLane(0), abstainedLane(1), failedLane(2)] });
|
|
282
|
+
expect(VerdictArtifactSchema.safeParse(mixed).success).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
// ─── Diversity grounded in completed-lane backends (rev-5 item 7) ─────────────
|
|
286
|
+
describe('diversity summary re-derived from completed lanes (rev-5 item 7)', () => {
|
|
287
|
+
it('rejects a fabricated cross-vendor diversity over two same-vendor lanes', () => {
|
|
288
|
+
const twoAnthropic = verdict({
|
|
289
|
+
lanes: [completedLane(0, 'anthropic'), completedLane(1, 'anthropic')],
|
|
290
|
+
});
|
|
291
|
+
const fabricated = {
|
|
292
|
+
...twoAnthropic,
|
|
293
|
+
panelArtifactHash: 'e'.repeat(64),
|
|
294
|
+
// classifyDiversity output for a FAKE provider mix — internally consistent
|
|
295
|
+
// (would pass the panel-style pure-function checks) but NOT grounded in the
|
|
296
|
+
// lanes' resolved backends, which are both anthropic.
|
|
297
|
+
diversity: classifyDiversity(['anthropic', 'gemini']),
|
|
298
|
+
};
|
|
299
|
+
const parsed = VerdictArtifactSchema.safeParse(fabricated);
|
|
300
|
+
expect(parsed.success).toBe(false);
|
|
301
|
+
if (!parsed.success) {
|
|
302
|
+
expect(parsed.error.issues.some((i) => /derived from the completed lanes/.test(i.message))).toBe(true);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
it("rejects codex's forged diversity: distinctProviders/unrecognized/confidence fabricated over same-vendor lanes (rev-6 item 2)", () => {
|
|
306
|
+
// Two anthropic lanes should derive providers ['anthropic','anthropic'], distinct 1,
|
|
307
|
+
// same-vendor-isolated, [] unrecognized, verified. This hand-forged diversity lies on
|
|
308
|
+
// EVERY field — it even collapses the multiset to one entry — yet passes the field
|
|
309
|
+
// types (PanelDiversitySchema has no re-derivation of its own). The verdict boundary
|
|
310
|
+
// must reject it via the FULL re-derivation (not just the provider set + class).
|
|
311
|
+
const twoAnthropic = verdict({
|
|
312
|
+
lanes: [completedLane(0, 'anthropic'), completedLane(1, 'anthropic')],
|
|
313
|
+
});
|
|
314
|
+
const forged = {
|
|
315
|
+
...twoAnthropic,
|
|
316
|
+
panelArtifactHash: 'e'.repeat(64),
|
|
317
|
+
diversity: {
|
|
318
|
+
providers: ['anthropic'],
|
|
319
|
+
distinctProviders: 99,
|
|
320
|
+
class: 'cross-vendor',
|
|
321
|
+
unrecognizedProviders: ['totally-made-up'],
|
|
322
|
+
diversityConfidence: 'coarse',
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
const parsed = VerdictArtifactSchema.safeParse(forged);
|
|
326
|
+
expect(parsed.success).toBe(false);
|
|
327
|
+
if (!parsed.success) {
|
|
328
|
+
const msgs = parsed.error.issues.map((i) => i.message).join('\n');
|
|
329
|
+
// The full re-derivation fires on the multiset, distinctProviders, and confidence.
|
|
330
|
+
expect(msgs).toMatch(/multiset/);
|
|
331
|
+
expect(msgs).toMatch(/distinctProviders/);
|
|
332
|
+
expect(msgs).toMatch(/diversityConfidence/);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
it('accepts the honest same-vendor summary for the same lanes', () => {
|
|
336
|
+
const twoAnthropic = verdict({
|
|
337
|
+
lanes: [completedLane(0, 'anthropic'), completedLane(1, 'anthropic')],
|
|
338
|
+
});
|
|
339
|
+
const honest = {
|
|
340
|
+
...twoAnthropic,
|
|
341
|
+
panelArtifactHash: 'e'.repeat(64),
|
|
342
|
+
diversity: classifyDiversity(['anthropic', 'anthropic']),
|
|
343
|
+
};
|
|
344
|
+
expect(VerdictArtifactSchema.safeParse(honest).success).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
it('accepts an honest cross-vendor summary grounded in genuinely mixed lanes', () => {
|
|
347
|
+
const mixed = verdict({
|
|
348
|
+
lanes: [completedLane(0, 'gemini'), completedLane(1, 'anthropic')],
|
|
349
|
+
});
|
|
350
|
+
const honest = {
|
|
351
|
+
...mixed,
|
|
352
|
+
panelArtifactHash: 'e'.repeat(64),
|
|
353
|
+
diversity: classifyDiversity(['gemini', 'anthropic']),
|
|
354
|
+
};
|
|
355
|
+
expect(VerdictArtifactSchema.safeParse(honest).success).toBe(true);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
// ─── createdAt validation (rev-5 item 8) ──────────────────────────────────────
|
|
359
|
+
describe('createdAt is a validated ISO datetime (rev-5 item 8)', () => {
|
|
360
|
+
it('rejects a malformed createdAt at parse', () => {
|
|
361
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ createdAt: 'yesterday-ish' })).success).toBe(false);
|
|
362
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ createdAt: '2026-07-10' })).success).toBe(false);
|
|
363
|
+
});
|
|
364
|
+
it('accepts a well-formed ISO datetime', () => {
|
|
365
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ createdAt: '2026-07-10T12:34:56.789Z' })).success).toBe(true);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
// ─── settled re-derivation at the persisted boundary (finding 5) ─────────────
|
|
369
|
+
describe('persisted boundary re-derives settled (finding 5)', () => {
|
|
370
|
+
it('rejects settled:true when the tree drifted mid-fan', () => {
|
|
371
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ reviewedState: 'drifted', settled: true })).success).toBe(false);
|
|
372
|
+
// honest counterpart passes
|
|
373
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ reviewedState: 'drifted', settled: false }))
|
|
374
|
+
.success).toBe(true);
|
|
375
|
+
});
|
|
376
|
+
it('rejects settled:true when a lane failed (fan incomplete)', () => {
|
|
377
|
+
const lanes = [completedLane(0), failedLane(1)];
|
|
378
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ lanes, settled: true })).success).toBe(false);
|
|
379
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ lanes, settled: false })).success).toBe(true);
|
|
380
|
+
});
|
|
381
|
+
it('rejects settled:true with an actionable WARN finding', () => {
|
|
382
|
+
const findings = [{ severity: 'WARN', message: 'drip' }];
|
|
383
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ findings, settled: true })).success).toBe(false);
|
|
384
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ findings, settled: false })).success).toBe(true);
|
|
385
|
+
});
|
|
386
|
+
it('rejects settled:true with a decidable-tier post-check failure', () => {
|
|
387
|
+
const postChecks = [
|
|
388
|
+
{ ruleName: 'review-structured-verdict', tier: 'decidable', verdict: 'fail', message: 'no' },
|
|
389
|
+
];
|
|
390
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ postChecks, settled: true })).success).toBe(false);
|
|
391
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ postChecks, settled: false })).success).toBe(true);
|
|
392
|
+
// a SENSOR-tier fail never gates settled (ADR-109) — settled:true stays honest.
|
|
393
|
+
const sensor = [
|
|
394
|
+
{ ruleName: 'provenance', tier: 'sensor', verdict: 'fail', message: 'advisory' },
|
|
395
|
+
];
|
|
396
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ postChecks: sensor, settled: true })).success).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
it('a genuinely dry current round is settled:true', () => {
|
|
399
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ settled: true })).success).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
// ─── deriveSettled / deriveCacheEligible — pure single source of truth ───────
|
|
403
|
+
describe('deriveSettled / deriveCacheEligible (pure predicates)', () => {
|
|
404
|
+
it('a dry matched fan is both settled and cache-eligible', () => {
|
|
405
|
+
const v = verdict();
|
|
406
|
+
expect(deriveSettled(v)).toBe(true);
|
|
407
|
+
expect(deriveCacheEligible(v)).toBe(true);
|
|
408
|
+
});
|
|
409
|
+
it('a WARN-only fan is cache-eligible but NOT settled (settled is stricter)', () => {
|
|
410
|
+
const input = {
|
|
411
|
+
lanes: [completedLane(0)],
|
|
412
|
+
findings: [{ severity: 'WARN', message: 'drip' }],
|
|
413
|
+
postChecks: [],
|
|
414
|
+
reviewedState: 'matched',
|
|
415
|
+
};
|
|
416
|
+
expect(deriveSettled(input)).toBe(false);
|
|
417
|
+
expect(deriveCacheEligible(input)).toBe(true);
|
|
418
|
+
});
|
|
419
|
+
it('a CRITICAL fan is neither settled nor cache-eligible', () => {
|
|
420
|
+
const input = {
|
|
421
|
+
lanes: [completedLane(0)],
|
|
422
|
+
findings: [{ severity: 'CRITICAL', message: 'boom' }],
|
|
423
|
+
postChecks: [],
|
|
424
|
+
reviewedState: 'matched',
|
|
425
|
+
};
|
|
426
|
+
expect(deriveSettled(input)).toBe(false);
|
|
427
|
+
expect(deriveCacheEligible(input)).toBe(false);
|
|
428
|
+
});
|
|
429
|
+
it('lane dropout or drift fails both (a persistent finding can never settle by dropout)', () => {
|
|
430
|
+
const dropout = {
|
|
431
|
+
lanes: [completedLane(0), failedLane(1)],
|
|
432
|
+
findings: [],
|
|
433
|
+
postChecks: [],
|
|
434
|
+
reviewedState: 'matched',
|
|
435
|
+
};
|
|
436
|
+
expect(deriveSettled(dropout)).toBe(false);
|
|
437
|
+
expect(deriveCacheEligible(dropout)).toBe(false);
|
|
438
|
+
const drift = {
|
|
439
|
+
lanes: [completedLane(0)],
|
|
440
|
+
findings: [],
|
|
441
|
+
postChecks: [],
|
|
442
|
+
reviewedState: 'drifted',
|
|
443
|
+
};
|
|
444
|
+
expect(deriveSettled(drift)).toBe(false);
|
|
445
|
+
expect(deriveCacheEligible(drift)).toBe(false);
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
// ─── Cross-field invariants (finding 9) ──────────────────────────────────────
|
|
449
|
+
describe('cross-field invariants (finding 9)', () => {
|
|
450
|
+
it('panelArtifactHash ⟺ diversity, both directions', () => {
|
|
451
|
+
const twoCompleted = verdict({ lanes: [completedLane(0), completedLane(1, 'anthropic')] });
|
|
452
|
+
const diversity = classifyDiversity(['gemini', 'anthropic']);
|
|
453
|
+
// panel without diversity → reject
|
|
454
|
+
expect(VerdictArtifactSchema.safeParse({ ...twoCompleted, panelArtifactHash: 'e'.repeat(64) })
|
|
455
|
+
.success).toBe(false);
|
|
456
|
+
// diversity without panel → reject
|
|
457
|
+
expect(VerdictArtifactSchema.safeParse({ ...twoCompleted, diversity }).success).toBe(false);
|
|
458
|
+
// both present with ≥2 completed → accept
|
|
459
|
+
expect(VerdictArtifactSchema.safeParse({
|
|
460
|
+
...twoCompleted,
|
|
461
|
+
panelArtifactHash: 'e'.repeat(64),
|
|
462
|
+
diversity,
|
|
463
|
+
}).success).toBe(true);
|
|
464
|
+
});
|
|
465
|
+
it('≥2 completed lanes require a panel (reverse direction)', () => {
|
|
466
|
+
// two completed lanes but NO panel / diversity → reject (a panel is assembled from all usable lanes)
|
|
467
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ lanes: [completedLane(0), completedLane(1)] }))
|
|
468
|
+
.success).toBe(false);
|
|
469
|
+
});
|
|
470
|
+
it('round.index === 0 ⟺ priorVerdictHash absent, both directions', () => {
|
|
471
|
+
// round 0 with a prior → reject
|
|
472
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ round: { index: 0, lineageKey: 'lk', priorVerdictHash: 'a'.repeat(64) } })).success).toBe(false);
|
|
473
|
+
// round > 0 without a prior → reject
|
|
474
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ round: { index: 1, lineageKey: 'lk' } })).success).toBe(false);
|
|
475
|
+
// round > 0 with a prior → accept
|
|
476
|
+
expect(VerdictArtifactSchema.safeParse(verdict({ round: { index: 1, lineageKey: 'lk', priorVerdictHash: 'a'.repeat(64) } })).success).toBe(true);
|
|
477
|
+
// round 0 without a prior (the default) → accept
|
|
478
|
+
expect(VerdictArtifactSchema.safeParse(verdict()).success).toBe(true);
|
|
479
|
+
});
|
|
480
|
+
it('lanes must be nonempty', () => {
|
|
481
|
+
const empty = {
|
|
482
|
+
...verdict(),
|
|
483
|
+
lanes: [],
|
|
484
|
+
attemptedLaneCount: 0,
|
|
485
|
+
completedLaneCount: 0,
|
|
486
|
+
};
|
|
487
|
+
expect(VerdictArtifactSchema.safeParse(empty).success).toBe(false);
|
|
488
|
+
});
|
|
489
|
+
it('laneIds must be unique across the fan', () => {
|
|
490
|
+
// two failed lanes with an identical laneId (0 completed ⇒ no panel confound)
|
|
491
|
+
const dup = verdict({ lanes: [failedLane(0), failedLane(0)] });
|
|
492
|
+
expect(VerdictArtifactSchema.safeParse(dup).success).toBe(false);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
// ─── Gate G3: all-terminal-failure is a LEGAL, honest verdict ────────────────
|
|
496
|
+
describe('all-lanes-failed verdict is legal (gate G3)', () => {
|
|
497
|
+
it('accepts an honest total-failure fan (no panel, no diversity, settled:false)', () => {
|
|
498
|
+
const totalFailure = verdict({
|
|
499
|
+
lanes: [failedLane(0), failedLane(1)],
|
|
500
|
+
findings: [],
|
|
501
|
+
round: { index: 0, lineageKey: 'lk' },
|
|
502
|
+
});
|
|
503
|
+
expect(totalFailure.completedLaneCount).toBe(0);
|
|
504
|
+
expect(totalFailure.attemptedLaneCount).toBe(2);
|
|
505
|
+
expect(totalFailure.settled).toBe(false);
|
|
506
|
+
expect(totalFailure.panelArtifactHash).toBeUndefined();
|
|
507
|
+
expect(totalFailure.diversity).toBeUndefined();
|
|
508
|
+
expect(VerdictArtifactSchema.safeParse(totalFailure).success).toBe(true);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
// ─── Gate G4: core-owned covariate line, format v1 ───────────────────────────
|
|
512
|
+
describe('renderCovariateLine (gate G4, format v1)', () => {
|
|
513
|
+
it('emits exactly `local-lane: <hash8> round=<n> settled=<bool> lanes=<c>/<a>`', () => {
|
|
514
|
+
const v = verdict(); // 1/1 completed, round 0, dry ⇒ settled true
|
|
515
|
+
const contentHash = computeVerdictArtifactContentHash(v);
|
|
516
|
+
expect(renderCovariateLine({ artifact: v, contentHash })).toBe(`local-lane: ${contentHash.slice(0, 8)} round=0 settled=true lanes=1/1`);
|
|
517
|
+
});
|
|
518
|
+
it('reflects a degraded, unsettled round (completed < attempted)', () => {
|
|
519
|
+
const v = verdict({ lanes: [completedLane(0), failedLane(1)] });
|
|
520
|
+
const contentHash = computeVerdictArtifactContentHash(v);
|
|
521
|
+
expect(renderCovariateLine({ artifact: v, contentHash })).toBe(`local-lane: ${contentHash.slice(0, 8)} round=0 settled=false lanes=1/2`);
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
// ─── LANE-BLINDNESS structural test (Prop 302) ──────────────────────────────
|
|
525
|
+
describe('lane-blindness: no runner/lane-mode discriminator (Prop 302)', () => {
|
|
526
|
+
it('the known top-level key set contains no warm/cold/runner key', () => {
|
|
527
|
+
// Populate EVERY field (incl. the optionals) so Object.keys is the full known set.
|
|
528
|
+
const full = {
|
|
529
|
+
...verdict({ lanes: [completedLane(0), completedLane(1, 'anthropic')] }),
|
|
530
|
+
panelArtifactHash: 'e'.repeat(64),
|
|
531
|
+
diversity: classifyDiversity(['gemini', 'anthropic']),
|
|
532
|
+
};
|
|
533
|
+
expect(VerdictArtifactSchema.safeParse(full).success).toBe(true);
|
|
534
|
+
const topKeys = Object.keys(full).sort();
|
|
535
|
+
expect(topKeys).toEqual([
|
|
536
|
+
'attemptedLaneCount',
|
|
537
|
+
'completedLaneCount',
|
|
538
|
+
'createdAt',
|
|
539
|
+
'diffScope',
|
|
540
|
+
'diversity',
|
|
541
|
+
'findings',
|
|
542
|
+
'lanes',
|
|
543
|
+
'panelArtifactHash',
|
|
544
|
+
'postChecks',
|
|
545
|
+
'reviewedState',
|
|
546
|
+
'round',
|
|
547
|
+
'schemaVersion',
|
|
548
|
+
'settled',
|
|
549
|
+
]);
|
|
550
|
+
for (const k of topKeys)
|
|
551
|
+
expect(k).not.toMatch(FORBIDDEN_RUNNER_KEY);
|
|
552
|
+
});
|
|
553
|
+
it('no lane variant carries a runner-mode key; the discriminator is status', () => {
|
|
554
|
+
const mixed = verdict({
|
|
555
|
+
lanes: [completedLane(0), abstainedLane(1), failedLane(2)],
|
|
556
|
+
});
|
|
557
|
+
expect(VerdictArtifactSchema.safeParse(mixed).success).toBe(true);
|
|
558
|
+
for (const lane of mixed.lanes) {
|
|
559
|
+
expect('status' in lane).toBe(true);
|
|
560
|
+
for (const k of Object.keys(lane))
|
|
561
|
+
expect(k).not.toMatch(FORBIDDEN_RUNNER_KEY);
|
|
562
|
+
}
|
|
563
|
+
const byStatus = Object.fromEntries(mixed.lanes.map((l) => [l.status, Object.keys(l).sort()]));
|
|
564
|
+
expect(byStatus.completed).toEqual([
|
|
565
|
+
'laneId',
|
|
566
|
+
'resolvedBackend',
|
|
567
|
+
'runArtifactHash',
|
|
568
|
+
'status',
|
|
569
|
+
'verdictSummary',
|
|
570
|
+
]);
|
|
571
|
+
expect(byStatus.abstained).toEqual([
|
|
572
|
+
'laneId',
|
|
573
|
+
'reason',
|
|
574
|
+
'resolvedBackend',
|
|
575
|
+
'runArtifactHash',
|
|
576
|
+
'status',
|
|
577
|
+
]);
|
|
578
|
+
expect(byStatus.failed).toEqual(['configuredLane', 'laneId', 'status', 'typedReason']);
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
// ─── Schema-version tolerance (F1) ──────────────────────────────────────────
|
|
582
|
+
describe('schema-version tolerance (F1)', () => {
|
|
583
|
+
it('accepts any 1.x minor, rejects ≥2.x loud', () => {
|
|
584
|
+
const v = verdict();
|
|
585
|
+
expect(VerdictArtifactSchema.safeParse({ ...v, schemaVersion: '1.9.3' }).success).toBe(true);
|
|
586
|
+
const major = VerdictArtifactSchema.safeParse({ ...v, schemaVersion: '2.0.0' });
|
|
587
|
+
expect(major.success).toBe(false);
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
// ─── computeLineageKey ──────────────────────────────────────────────────────
|
|
591
|
+
describe('computeLineageKey — source-discriminated selector, injection-proof', () => {
|
|
592
|
+
const REPO = '/abs/worktree';
|
|
593
|
+
it('distinct branches sharing a merge base produce distinct keys', () => {
|
|
594
|
+
const a = computeLineageKey({
|
|
595
|
+
repoIdentity: REPO,
|
|
596
|
+
branch: 'feat/x',
|
|
597
|
+
source: 'branch-vs-base',
|
|
598
|
+
base: 'main',
|
|
599
|
+
mergeBase: 'sha-shared',
|
|
600
|
+
});
|
|
601
|
+
const b = computeLineageKey({
|
|
602
|
+
repoIdentity: REPO,
|
|
603
|
+
branch: 'feat/y',
|
|
604
|
+
source: 'branch-vs-base',
|
|
605
|
+
base: 'main',
|
|
606
|
+
mergeBase: 'sha-shared',
|
|
607
|
+
});
|
|
608
|
+
expect(a).not.toBe(b);
|
|
609
|
+
});
|
|
610
|
+
it('distinct worktree identities on the same branch/base produce distinct keys', () => {
|
|
611
|
+
const a = computeLineageKey({
|
|
612
|
+
repoIdentity: '/abs/wt-a',
|
|
613
|
+
branch: 'feat/x',
|
|
614
|
+
source: 'branch-vs-base',
|
|
615
|
+
base: 'main',
|
|
616
|
+
mergeBase: 'sha-shared',
|
|
617
|
+
});
|
|
618
|
+
const b = computeLineageKey({
|
|
619
|
+
repoIdentity: '/abs/wt-b',
|
|
620
|
+
branch: 'feat/x',
|
|
621
|
+
source: 'branch-vs-base',
|
|
622
|
+
base: 'main',
|
|
623
|
+
mergeBase: 'sha-shared',
|
|
624
|
+
});
|
|
625
|
+
expect(a).not.toBe(b);
|
|
626
|
+
});
|
|
627
|
+
it('two different explicit ranges on one branch produce distinct keys (codex rev-2 gate 2)', () => {
|
|
628
|
+
// Same branch, same repo — only the range endpoints differ.
|
|
629
|
+
const a = computeLineageKey({
|
|
630
|
+
repoIdentity: REPO,
|
|
631
|
+
branch: 'feat/x',
|
|
632
|
+
source: 'explicit-range',
|
|
633
|
+
base: 'HEAD~3',
|
|
634
|
+
head: 'HEAD',
|
|
635
|
+
});
|
|
636
|
+
const b = computeLineageKey({
|
|
637
|
+
repoIdentity: REPO,
|
|
638
|
+
branch: 'feat/x',
|
|
639
|
+
source: 'explicit-range',
|
|
640
|
+
base: 'HEAD~5',
|
|
641
|
+
head: 'HEAD',
|
|
642
|
+
});
|
|
643
|
+
expect(a).not.toBe(b);
|
|
644
|
+
});
|
|
645
|
+
it('a delimiter-injection attempt does not collide', () => {
|
|
646
|
+
// 'a' + 'b|c' vs 'a|b' + 'c' — a naive join('|') would collide these.
|
|
647
|
+
const a = computeLineageKey({
|
|
648
|
+
repoIdentity: REPO,
|
|
649
|
+
branch: 'a',
|
|
650
|
+
source: 'branch-vs-base',
|
|
651
|
+
base: 'main',
|
|
652
|
+
mergeBase: 'b|c',
|
|
653
|
+
});
|
|
654
|
+
const b = computeLineageKey({
|
|
655
|
+
repoIdentity: REPO,
|
|
656
|
+
branch: 'a|b',
|
|
657
|
+
source: 'branch-vs-base',
|
|
658
|
+
base: 'main',
|
|
659
|
+
mergeBase: 'c',
|
|
660
|
+
});
|
|
661
|
+
expect(a).not.toBe(b);
|
|
662
|
+
});
|
|
663
|
+
it('is stable for identical components and discriminates on source', () => {
|
|
664
|
+
const staged = { repoIdentity: REPO, branch: 'a', source: 'staged' };
|
|
665
|
+
const uncommitted = { repoIdentity: REPO, branch: 'a', source: 'uncommitted' };
|
|
666
|
+
expect(computeLineageKey(staged)).toBe(computeLineageKey(staged));
|
|
667
|
+
expect(computeLineageKey(staged)).not.toBe(computeLineageKey(uncommitted));
|
|
668
|
+
});
|
|
669
|
+
it('selectorForm participates in the key (finding-10 stability seam)', () => {
|
|
670
|
+
const withoutForm = computeLineageKey({
|
|
671
|
+
repoIdentity: REPO,
|
|
672
|
+
branch: 'a',
|
|
673
|
+
source: 'uncommitted',
|
|
674
|
+
});
|
|
675
|
+
const withForm = computeLineageKey({
|
|
676
|
+
repoIdentity: REPO,
|
|
677
|
+
branch: 'a',
|
|
678
|
+
source: 'uncommitted',
|
|
679
|
+
selectorForm: '--diff main',
|
|
680
|
+
});
|
|
681
|
+
expect(withoutForm).not.toBe(withForm);
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
// ─── Storage: content-address, dedup, identity violation, lineage ───────────
|
|
685
|
+
describe('verdict storage (mirrors run/panel storage)', () => {
|
|
686
|
+
let totemDir;
|
|
687
|
+
beforeEach(() => {
|
|
688
|
+
totemDir = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-verdict-'));
|
|
689
|
+
});
|
|
690
|
+
afterEach(() => {
|
|
691
|
+
cleanTmpDir(totemDir);
|
|
692
|
+
});
|
|
693
|
+
it('writes at artifacts/verdicts/<hash>.json and reads it back', () => {
|
|
694
|
+
const v = verdict();
|
|
695
|
+
const saved = saveVerdictArtifact(totemDir, v);
|
|
696
|
+
expect(saved.existed).toBe(false);
|
|
697
|
+
expect(saved.path).toBe(path.join(totemDir, 'artifacts', 'verdicts', `${saved.hash}.json`));
|
|
698
|
+
expect(verdictsDir(totemDir)).toBe(path.join(totemDir, 'artifacts', 'verdicts'));
|
|
699
|
+
const loaded = loadVerdictArtifact(totemDir, saved.hash);
|
|
700
|
+
expect(loaded.artifact).toEqual(v);
|
|
701
|
+
// rev-6 item 1: the load carries the verified stored address (= the filename stem).
|
|
702
|
+
expect(loaded.contentHash).toBe(saved.hash);
|
|
703
|
+
});
|
|
704
|
+
it('content address excludes createdAt — identical rounds dedup across time', () => {
|
|
705
|
+
const early = verdict({ createdAt: '2026-07-10T00:00:00.000Z' });
|
|
706
|
+
const late = verdict({ createdAt: '2026-07-11T09:30:00.000Z' });
|
|
707
|
+
expect(computeVerdictArtifactContentHash(early)).toBe(computeVerdictArtifactContentHash(late));
|
|
708
|
+
const first = saveVerdictArtifact(totemDir, early);
|
|
709
|
+
const second = saveVerdictArtifact(totemDir, late);
|
|
710
|
+
expect(second.existed).toBe(true); // logical-identity dedup, no throw
|
|
711
|
+
expect(second.hash).toBe(first.hash);
|
|
712
|
+
// first-write-wins: the stored createdAt is the early one.
|
|
713
|
+
expect(loadVerdictArtifact(totemDir, first.hash).artifact.createdAt).toBe('2026-07-10T00:00:00.000Z');
|
|
714
|
+
});
|
|
715
|
+
it('rejects a schema-valid artifact stored under a mismatched content address (finding 4)', () => {
|
|
716
|
+
const v = verdict();
|
|
717
|
+
const realHash = computeVerdictArtifactContentHash(v);
|
|
718
|
+
const wrongHash = 'a'.repeat(64);
|
|
719
|
+
expect(wrongHash).not.toBe(realHash); // sanity
|
|
720
|
+
fs.mkdirSync(verdictsDir(totemDir), { recursive: true });
|
|
721
|
+
fs.writeFileSync(path.join(verdictsDir(totemDir), `${wrongHash}.json`), JSON.stringify(v));
|
|
722
|
+
expect(() => loadVerdictArtifact(totemDir, wrongHash)).toThrow(TotemError);
|
|
723
|
+
expect(() => loadVerdictArtifact(totemDir, wrongHash)).toThrow(/content-address|does not match/i);
|
|
724
|
+
});
|
|
725
|
+
it('EEXIST with content differing modulo createdAt is a hard identity violation', () => {
|
|
726
|
+
const target = verdict();
|
|
727
|
+
const hash = computeVerdictArtifactContentHash(target);
|
|
728
|
+
// Plant a DIFFERENT but schema-valid verdict at target's content address.
|
|
729
|
+
const impostor = verdict({ diffScope: { source: 'staged', diffHash: 'e'.repeat(64) } });
|
|
730
|
+
expect(computeVerdictArtifactContentHash(impostor)).not.toBe(hash); // sanity
|
|
731
|
+
fs.mkdirSync(verdictsDir(totemDir), { recursive: true });
|
|
732
|
+
fs.writeFileSync(path.join(verdictsDir(totemDir), `${hash}.json`), JSON.stringify(impostor));
|
|
733
|
+
expect(() => saveVerdictArtifact(totemDir, target)).toThrow(TotemError);
|
|
734
|
+
expect(() => saveVerdictArtifact(totemDir, target)).toThrow(/content-address|does not match|identity/i);
|
|
735
|
+
});
|
|
736
|
+
it('loads a minor-newer verdict from disk; rejects a major-newer one loud', () => {
|
|
737
|
+
const v17 = verdict({ schemaVersion: '1.7.0' });
|
|
738
|
+
const minorHash = computeVerdictArtifactContentHash(v17);
|
|
739
|
+
fs.mkdirSync(verdictsDir(totemDir), { recursive: true });
|
|
740
|
+
fs.writeFileSync(path.join(verdictsDir(totemDir), `${minorHash}.json`), JSON.stringify(v17));
|
|
741
|
+
expect(loadVerdictArtifact(totemDir, minorHash).artifact.schemaVersion).toBe('1.7.0');
|
|
742
|
+
const v20 = verdict({ schemaVersion: '2.0.0' });
|
|
743
|
+
const majorHash = computeVerdictArtifactContentHash(v20);
|
|
744
|
+
fs.writeFileSync(path.join(verdictsDir(totemDir), `${majorHash}.json`), JSON.stringify(v20));
|
|
745
|
+
expect(() => loadVerdictArtifact(totemDir, majorHash)).toThrow(/2\.0\.0/);
|
|
746
|
+
});
|
|
747
|
+
it('rejects a non-hash id before touching the filesystem', () => {
|
|
748
|
+
expect(() => loadVerdictArtifact(totemDir, '../../secrets')).toThrow(/sha256/i);
|
|
749
|
+
});
|
|
750
|
+
it('a missing but well-formed hash throws TotemParseError', () => {
|
|
751
|
+
expect(() => loadVerdictArtifact(totemDir, 'f'.repeat(64))).toThrow(TotemParseError);
|
|
752
|
+
});
|
|
753
|
+
it('listVerdictArtifacts returns [] when nothing has been written', () => {
|
|
754
|
+
expect(listVerdictArtifacts(totemDir, noWarn)).toEqual([]);
|
|
755
|
+
});
|
|
756
|
+
it('a corrupt / mis-addressed artifact is warned + skipped during a lineage scan, not crashed', () => {
|
|
757
|
+
const lineageKey = computeLineageKey({
|
|
758
|
+
repoIdentity: '/r',
|
|
759
|
+
branch: 'a',
|
|
760
|
+
source: 'staged',
|
|
761
|
+
});
|
|
762
|
+
const good = saveVerdictArtifact(totemDir, verdict({ round: { index: 0, lineageKey } }));
|
|
763
|
+
// Plant a schema-valid verdict under a WRONG filename (finding-4 mismatch).
|
|
764
|
+
fs.writeFileSync(path.join(verdictsDir(totemDir), `${'c'.repeat(64)}.json`), JSON.stringify(verdict({ round: { index: 0, lineageKey } })));
|
|
765
|
+
const warnings = [];
|
|
766
|
+
const found = findLatestVerdictForLineage(totemDir, lineageKey, (m) => warnings.push(m));
|
|
767
|
+
// the good one still wins; the mis-addressed one is announced + dropped
|
|
768
|
+
expect(found.contentHash).toBe(good.hash);
|
|
769
|
+
expect(warnings.some((w) => /content-address|corrupt|mis-addressed/i.test(w))).toBe(true);
|
|
770
|
+
});
|
|
771
|
+
it('findLatestVerdictForLineage picks the highest round.index in the lineage', () => {
|
|
772
|
+
const lineageKey = computeLineageKey({
|
|
773
|
+
repoIdentity: '/r',
|
|
774
|
+
branch: 'a',
|
|
775
|
+
source: 'staged',
|
|
776
|
+
});
|
|
777
|
+
const other = computeLineageKey({
|
|
778
|
+
repoIdentity: '/r',
|
|
779
|
+
branch: 'z',
|
|
780
|
+
source: 'staged',
|
|
781
|
+
});
|
|
782
|
+
// Same lineage, three rounds with distinct content (index differs ⇒ distinct address).
|
|
783
|
+
saveVerdictArtifact(totemDir, verdict({ round: { index: 0, lineageKey } }));
|
|
784
|
+
saveVerdictArtifact(totemDir, verdict({ round: { index: 1, lineageKey, priorVerdictHash: 'a'.repeat(64) } }));
|
|
785
|
+
const r2 = saveVerdictArtifact(totemDir, verdict({ round: { index: 2, lineageKey, priorVerdictHash: 'b'.repeat(64) } }));
|
|
786
|
+
// A different lineage that must be ignored even though its index is higher.
|
|
787
|
+
saveVerdictArtifact(totemDir, verdict({ round: { index: 9, lineageKey: other, priorVerdictHash: 'c'.repeat(64) } }));
|
|
788
|
+
const latest = findLatestVerdictForLineage(totemDir, lineageKey, noWarn);
|
|
789
|
+
expect(latest?.artifact.round.index).toBe(2);
|
|
790
|
+
expect(latest.contentHash).toBe(r2.hash);
|
|
791
|
+
expect(findLatestVerdictForLineage(totemDir, 'no-such-lineage', noWarn)).toBeUndefined();
|
|
792
|
+
});
|
|
793
|
+
it('findLatestVerdictForLineage breaks round-index ties by lexical content hash, NOT createdAt (rev-5 item 8)', () => {
|
|
794
|
+
const lineageKey = computeLineageKey({
|
|
795
|
+
repoIdentity: '/r',
|
|
796
|
+
branch: 'a',
|
|
797
|
+
source: 'staged',
|
|
798
|
+
});
|
|
799
|
+
// Same round.index, distinct content (priorVerdictHash differs ⇒ distinct address).
|
|
800
|
+
const a = verdict({ round: { index: 1, lineageKey, priorVerdictHash: 'a'.repeat(64) } });
|
|
801
|
+
const b = verdict({ round: { index: 1, lineageKey, priorVerdictHash: 'c'.repeat(64) } });
|
|
802
|
+
const hashA = computeVerdictArtifactContentHash(a);
|
|
803
|
+
const hashB = computeVerdictArtifactContentHash(b);
|
|
804
|
+
const [winner, loser] = hashA.localeCompare(hashB) > 0 ? [a, b] : [b, a];
|
|
805
|
+
const winnerHash = computeVerdictArtifactContentHash(winner);
|
|
806
|
+
// Give the LOSER the LATER createdAt: a createdAt tie-break would pick it — the
|
|
807
|
+
// identity-bound hash tie-break must not (createdAt excluded from identity, so
|
|
808
|
+
// this does not perturb the hashes computed above).
|
|
809
|
+
winner.createdAt = '2026-07-10T00:00:00.000Z';
|
|
810
|
+
loser.createdAt = '2026-07-11T00:00:00.000Z';
|
|
811
|
+
saveVerdictArtifact(totemDir, winner);
|
|
812
|
+
saveVerdictArtifact(totemDir, loser);
|
|
813
|
+
const found = findLatestVerdictForLineage(totemDir, lineageKey, noWarn);
|
|
814
|
+
expect(found.contentHash).toBe(winnerHash);
|
|
815
|
+
// Deterministic + timestamp-independent: the earlier-stamped record won on hash.
|
|
816
|
+
expect(found.artifact.createdAt).toBe('2026-07-10T00:00:00.000Z');
|
|
817
|
+
});
|
|
818
|
+
// ── rev-5 item 5: content-address verification over the RAW logical payload ──
|
|
819
|
+
it('rejects an unknown-field tamper under the old filename (rev-5 item 5a)', () => {
|
|
820
|
+
const v = verdict();
|
|
821
|
+
const saved = saveVerdictArtifact(totemDir, v);
|
|
822
|
+
// Tamper: inject an unknown key into the stored bytes, keeping the filename.
|
|
823
|
+
// The tolerant Zod parse STRIPS the key, so a normalized-output recompute would
|
|
824
|
+
// still match the address and load silently — the raw-payload hash must not.
|
|
825
|
+
const filePath = path.join(verdictsDir(totemDir), `${saved.hash}.json`);
|
|
826
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
827
|
+
raw['injectedUnknownField'] = 'tampered';
|
|
828
|
+
fs.writeFileSync(filePath, JSON.stringify(raw));
|
|
829
|
+
expect(() => loadVerdictArtifact(totemDir, saved.hash)).toThrow(TotemError);
|
|
830
|
+
expect(() => loadVerdictArtifact(totemDir, saved.hash)).toThrow(/content-address/i);
|
|
831
|
+
});
|
|
832
|
+
it('a simulated forward-minor artifact addressed over its RAW payload (extra field included) loads and verifies (rev-5 item 5b)', () => {
|
|
833
|
+
// A future 1.x writer adds an additive field this reader's schema strips. Its
|
|
834
|
+
// content address was computed over the RAW payload INCLUDING that field — a
|
|
835
|
+
// normalized-output recompute would wrongly reject it; the raw-payload hash verifies.
|
|
836
|
+
const future = {
|
|
837
|
+
...verdict({ schemaVersion: '1.7.0' }),
|
|
838
|
+
futureAdditiveField: 'from-a-newer-minor',
|
|
839
|
+
};
|
|
840
|
+
const { createdAt: _excluded, ...identity } = future;
|
|
841
|
+
const rawHash = calculateDeterministicHash(identity);
|
|
842
|
+
fs.mkdirSync(verdictsDir(totemDir), { recursive: true });
|
|
843
|
+
fs.writeFileSync(path.join(verdictsDir(totemDir), `${rawHash}.json`), JSON.stringify(future));
|
|
844
|
+
const loaded = loadVerdictArtifact(totemDir, rawHash);
|
|
845
|
+
expect(loaded.artifact.schemaVersion).toBe('1.7.0');
|
|
846
|
+
// The tolerant reader strips the unknown field from the RETURNED shape.
|
|
847
|
+
expect('futureAdditiveField' in loaded.artifact).toBe(false);
|
|
848
|
+
// rev-6 item 1: the VERIFIED stored address survives the tolerant parse.
|
|
849
|
+
expect(loaded.contentHash).toBe(rawHash);
|
|
850
|
+
});
|
|
851
|
+
// ── rev-6 item 1: the verified stored address survives the tolerant parse ──
|
|
852
|
+
it('a forward-minor artifact: covariate line, lineage selection, and round linkage all use the RAW file address (rev-6 item 1)', () => {
|
|
853
|
+
const lineageKey = computeLineageKey({ repoIdentity: '/r', branch: 'a', source: 'staged' });
|
|
854
|
+
// A future 1.x writer added an additive field; the on-disk address was computed over
|
|
855
|
+
// the RAW payload INCLUDING it. A recompute over the Zod-stripped shape DIVERGES.
|
|
856
|
+
const future = {
|
|
857
|
+
...verdict({ schemaVersion: '1.7.0', round: { index: 0, lineageKey } }),
|
|
858
|
+
futureAdditiveField: 'from-a-newer-minor',
|
|
859
|
+
};
|
|
860
|
+
const { createdAt: _excluded, ...identity } = future;
|
|
861
|
+
const rawHash = calculateDeterministicHash(identity);
|
|
862
|
+
fs.mkdirSync(verdictsDir(totemDir), { recursive: true });
|
|
863
|
+
fs.writeFileSync(path.join(verdictsDir(totemDir), `${rawHash}.json`), JSON.stringify(future));
|
|
864
|
+
const loaded = loadVerdictArtifact(totemDir, rawHash);
|
|
865
|
+
// The falsifier is LIVE: the normalized recompute diverges from the raw file address.
|
|
866
|
+
expect(loaded.contentHash).toBe(rawHash);
|
|
867
|
+
expect(computeVerdictArtifactContentHash(loaded.artifact)).not.toBe(rawHash);
|
|
868
|
+
// (a) covariate line emits the RAW file address (hash8), not the divergent recompute.
|
|
869
|
+
expect(renderCovariateLine(loaded)).toBe(`local-lane: ${rawHash.slice(0, 8)} round=0 settled=${loaded.artifact.settled} lanes=1/1`);
|
|
870
|
+
// (b) lineage selection returns the RAW file address as its contentHash.
|
|
871
|
+
const found = findLatestVerdictForLineage(totemDir, lineageKey, noWarn);
|
|
872
|
+
expect(found.contentHash).toBe(rawHash);
|
|
873
|
+
// (c) round linkage would set priorVerdictHash = found.contentHash — byte-equal to the
|
|
874
|
+
// filename, so it points at a REAL file (not a nonexistent recompute address).
|
|
875
|
+
expect(fs.existsSync(path.join(verdictsDir(totemDir), `${found.contentHash}.json`))).toBe(true);
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
//# sourceMappingURL=verdict.test.js.map
|