@mmnto/totem 2.5.0 → 2.7.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/config-schema.d.ts +6 -1
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config-schema.js +6 -1
- package/dist/config-schema.js.map +1 -1
- package/dist/gate-engine.test.js +4 -0
- package/dist/gate-engine.test.js.map +1 -1
- package/dist/gate-types.d.ts +4 -3
- package/dist/gate-types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/merge-ready.d.ts +42 -4
- package/dist/merge-ready.d.ts.map +1 -1
- package/dist/merge-ready.js +652 -49
- package/dist/merge-ready.js.map +1 -1
- package/dist/merge-ready.test.js +550 -21
- package/dist/merge-ready.test.js.map +1 -1
- package/dist/orchestration-resolver.d.ts +9 -0
- package/dist/orchestration-resolver.d.ts.map +1 -1
- package/dist/orchestration-resolver.js +44 -7
- package/dist/orchestration-resolver.js.map +1 -1
- package/dist/orchestration-resolver.test.js +30 -25
- package/dist/orchestration-resolver.test.js.map +1 -1
- package/package.json +1 -1
package/dist/merge-ready.test.js
CHANGED
|
@@ -2,18 +2,25 @@
|
|
|
2
2
|
* merge-ready (mmnto-ai/totem#2800) — the gate's invariants, driven by the
|
|
3
3
|
* checked-in fixtures under `gate-fixtures/merge-ready/`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* exported {@link MERGE_READY_QUERY} (R4):
|
|
5
|
+
* FIVE of those fixtures are REAL `gh api graphql` captures taken with the
|
|
6
|
+
* exported {@link MERGE_READY_QUERY} (R4): four PR captures —
|
|
7
7
|
* mmnto-ai/liquid-city#363 (green rollup, one unresolved HIGH inline),
|
|
8
|
-
* mmnto-ai/totem-strategy#1251,
|
|
9
|
-
* re-pointed to the head)
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
8
|
+
* mmnto-ai/totem-strategy#1251, mmnto-ai/totem#2827 (the comment that
|
|
9
|
+
* re-pointed to the head) and mmnto-ai/totem#2871 (a HIGH resolved through the
|
|
10
|
+
* disposition path while its anchor survives on the head — the discharge
|
|
11
|
+
* specimen, mmnto-ai/totem#2861) — plus the benign corpus, every bot inline
|
|
12
|
+
* thread on mmnto-ai/totem#2820-2839, which measures the severity read's
|
|
13
|
+
* ADR-109 false-positive budget. The other 72 are synthetic and labelled
|
|
14
|
+
* `synthetic-` in their names — one per invariant the captures cannot
|
|
15
|
+
* exercise (all four PRs are merged, so GitHub answers `mergeStateStatus:
|
|
16
|
+
* UNKNOWN` for each and no capture can carry a BEHIND / DIRTY / BLOCKED head,
|
|
17
|
+
* or a bare-resolved HIGH thread); the five PR-round-1 rows
|
|
18
|
+
* (mmnto-ai/totem#2844) cover the strict connection reads and the predicate
|
|
19
|
+
* order ahead of the unreadable-commit arm; the ten mmnto-ai/totem#2861 rows
|
|
20
|
+
* cover the discharge read — its two evidence arms, the bare-resolve negative
|
|
21
|
+
* control in the field shape, the required resolve, the per-thread split, the
|
|
22
|
+
* second comments page, the incomplete window with and without evidence, the
|
|
23
|
+
* deleted-account reply and the strict PR comments connection.
|
|
17
24
|
* The README beside them lists every file with its sha256 and instant, and a
|
|
18
25
|
* test recomputes those receipts from the files on disk.
|
|
19
26
|
*
|
|
@@ -25,7 +32,7 @@ import * as path from 'node:path';
|
|
|
25
32
|
import { fileURLToPath } from 'node:url';
|
|
26
33
|
import { describe, expect, it } from 'vitest';
|
|
27
34
|
import { TotemError } from './errors.js';
|
|
28
|
-
import { evaluateMergeReady, hasHighSeverityMarker, MERGE_READY_BRANCH_QUERY, MERGE_READY_OVERRIDE_ENV, MERGE_READY_QUERY, MERGE_READY_SOURCE, mergeReadyEvaluator, parseMergeReadyPayload, } from './merge-ready.js';
|
|
35
|
+
import { dispositionedRootIds, evaluateMergeReady, hasHighSeverityMarker, MERGE_READY_BRANCH_QUERY, MERGE_READY_OVERRIDE_ENV, MERGE_READY_QUERY, MERGE_READY_SOURCE, mergeReadyEvaluator, parseMergeReadyPayload, } from './merge-ready.js';
|
|
29
36
|
const FIXTURE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), 'gate-fixtures', 'merge-ready');
|
|
30
37
|
function loadFixture(name) {
|
|
31
38
|
return JSON.parse(fs.readFileSync(path.join(FIXTURE_DIR, name), 'utf-8'));
|
|
@@ -76,6 +83,7 @@ describe('merge-ready — the fixture README matches the fixtures', () => {
|
|
|
76
83
|
'benign-corpus-bot-inlines.json',
|
|
77
84
|
'liquid-city-363.json',
|
|
78
85
|
'totem-2827.json',
|
|
86
|
+
'totem-2871.json',
|
|
79
87
|
'totem-strategy-1251.json',
|
|
80
88
|
];
|
|
81
89
|
it('every sha256 in the README is the sha256 of the file it names', () => {
|
|
@@ -111,8 +119,8 @@ describe('merge-ready — the fixture README matches the fixtures', () => {
|
|
|
111
119
|
const synthetic = files.filter((f) => f.startsWith('synthetic-'));
|
|
112
120
|
const captures = files.filter((f) => !f.startsWith('synthetic-'));
|
|
113
121
|
expect(captures.sort()).toEqual(CAPTURES);
|
|
114
|
-
expect(synthetic.length).toBe(
|
|
115
|
-
expect(files.length).toBe(
|
|
122
|
+
expect(synthetic.length).toBe(72);
|
|
123
|
+
expect(files.length).toBe(77);
|
|
116
124
|
});
|
|
117
125
|
it('the README query sha is the sha of the exported query (round 4, F7)', () => {
|
|
118
126
|
// A receipt for the QUERY, re-derived like the file receipts: change the
|
|
@@ -134,7 +142,13 @@ describe('merge-ready — the checked-in captures', () => {
|
|
|
134
142
|
expect(e.verdict.disposition).toBe('deny');
|
|
135
143
|
expect(e.verdict.provenance.ref).toBe('unresolved-bot-threads');
|
|
136
144
|
expect(e.verdict.provenance.source).toBe(MERGE_READY_SOURCE);
|
|
137
|
-
expect(e.detail.checks).toEqual({
|
|
145
|
+
expect(e.detail.checks).toEqual({
|
|
146
|
+
total: 6,
|
|
147
|
+
success: 6,
|
|
148
|
+
pending: 0,
|
|
149
|
+
failing: 0,
|
|
150
|
+
superseded: 0,
|
|
151
|
+
});
|
|
138
152
|
expect(e.detail.threads.unresolvedBot).toBe(1);
|
|
139
153
|
// The root comment is GCA's, spelled WITHOUT the `[bot]` suffix on the
|
|
140
154
|
// GraphQL surface — the trap `bot-identity.ts` exists to hold.
|
|
@@ -171,6 +185,54 @@ describe('merge-ready — the checked-in captures', () => {
|
|
|
171
185
|
expect(e.detail.highInline).toBe(1);
|
|
172
186
|
expect(e.verdict.disposition).toBe('deny');
|
|
173
187
|
});
|
|
188
|
+
it('mmnto-ai/totem#2871 (capture): a CodeRabbit Major resolved THROUGH THE DISPOSITION PATH while its anchor survives on the head is discharged (mmnto-ai/totem#2861)', () => {
|
|
189
|
+
// The real specimen of predicate 4's own territory: the finding was
|
|
190
|
+
// declined with reason in the round disposition (a PR-level comment that
|
|
191
|
+
// post-dates the thread's root), the thread resolved by
|
|
192
|
+
// `totem resolve-threads --apply`, and the anchored line never changed —
|
|
193
|
+
// so `comment.commit.oid` IS the head and, before the cure, this read
|
|
194
|
+
// `deny · high-severity-inline · highInline 1` (calibration row
|
|
195
|
+
// mmnto-ai/totem#2871 on the issue). Read straight off the capture first,
|
|
196
|
+
// so the shape stays a statement about GitHub's data.
|
|
197
|
+
const fixture = loadFixture('totem-2871.json');
|
|
198
|
+
const body = fixture.pages[0].body;
|
|
199
|
+
const pr = body.data.repository.pullRequest;
|
|
200
|
+
const onHead = pr.reviewThreads.nodes.filter((t) => t.comments.nodes[0].commit?.oid === pr.headRefOid &&
|
|
201
|
+
hasHighSeverityMarker(t.comments.nodes[0].body));
|
|
202
|
+
expect(onHead).toHaveLength(1);
|
|
203
|
+
const thread = onHead[0];
|
|
204
|
+
expect(thread.isResolved).toBe(true);
|
|
205
|
+
expect(thread.comments.nodes[0].author?.login).toBe('coderabbitai');
|
|
206
|
+
const rootAt = Date.parse(thread.comments.nodes[0].createdAt);
|
|
207
|
+
const humanAfterRoot = pr.comments.nodes.filter((c) => c.author?.__typename !== 'Bot' && Date.parse(c.createdAt) > rootAt);
|
|
208
|
+
expect(humanAfterRoot.length).toBeGreaterThan(0);
|
|
209
|
+
// A human comment after the root NAMES the thread on a `disposition:`
|
|
210
|
+
// line — the calibration replay line posted on the PR under the 2026-09-16
|
|
211
|
+
// ruling — asserted through the SHIPPED predicate, never a copy (r2-f8).
|
|
212
|
+
const rootId = thread.comments.nodes[0].databaseId;
|
|
213
|
+
expect(rootId).toBe(4000747291);
|
|
214
|
+
expect(humanAfterRoot.some((c) => dispositionedRootIds(c.body).includes(rootId))).toBe(true);
|
|
215
|
+
const e = evaluate('totem-2871.json', { tier: 'pilot' });
|
|
216
|
+
// 17 runs under 16 names on the 2026-09-18 re-capture: one name ran twice
|
|
217
|
+
// (both SUCCESS) and is judged by its later run (mmnto-ai/totem#2879) — a
|
|
218
|
+
// live specimen of the collapse inside the discharge specimen.
|
|
219
|
+
expect(e.detail.checks).toEqual({
|
|
220
|
+
total: 16,
|
|
221
|
+
success: 16,
|
|
222
|
+
pending: 0,
|
|
223
|
+
failing: 0,
|
|
224
|
+
superseded: 1,
|
|
225
|
+
});
|
|
226
|
+
expect(e.detail.threads.unresolvedBot).toBe(0);
|
|
227
|
+
expect(e.detail.highInline).toBe(0);
|
|
228
|
+
expect(e.detail.dischargedHigh).toBe(1);
|
|
229
|
+
expect(e.detail.dischargedBy).toEqual({ inThreadReply: 0, prLevelDisposition: 1 });
|
|
230
|
+
// Merged, so GitHub answers UNKNOWN: the read lands on the arm that is
|
|
231
|
+
// reachable ONLY when predicates 1–4 have passed — the replay's witness.
|
|
232
|
+
expect(e.verdict.provenance.ref).toBe('unevaluable');
|
|
233
|
+
expect(e.verdict.reason).toMatch(/mergeStateStatus: UNKNOWN/);
|
|
234
|
+
expect(e.notices.some((n) => n.includes('1 HIGH/Major bot inline(s) on the head commit discharged'))).toBe(true);
|
|
235
|
+
});
|
|
174
236
|
it('sends the exported query, as argv, with no shell string anywhere', () => {
|
|
175
237
|
const e = evaluate('liquid-city-363.json');
|
|
176
238
|
expect(e.calls[0]).toEqual(['--version']);
|
|
@@ -191,7 +253,13 @@ describe('merge-ready — predicate 1 (checks)', () => {
|
|
|
191
253
|
expect(e.verdict.disposition).toBe('deny');
|
|
192
254
|
expect(e.verdict.provenance.ref).toBe('checks');
|
|
193
255
|
expect(e.verdict.reason).toContain('Totem Lint');
|
|
194
|
-
expect(e.detail.checks).toEqual({
|
|
256
|
+
expect(e.detail.checks).toEqual({
|
|
257
|
+
total: 2,
|
|
258
|
+
success: 1,
|
|
259
|
+
pending: 0,
|
|
260
|
+
failing: 1,
|
|
261
|
+
superseded: 0,
|
|
262
|
+
});
|
|
195
263
|
});
|
|
196
264
|
it('a still-running check denies and names it', () => {
|
|
197
265
|
const e = evaluate('synthetic-pending-check.json');
|
|
@@ -203,11 +271,275 @@ describe('merge-ready — predicate 1 (checks)', () => {
|
|
|
203
271
|
it('R5 — zero checks passes predicate 1 as a FACT, with the count and ONE stderr line', () => {
|
|
204
272
|
const e = evaluate('synthetic-zero-checks.json');
|
|
205
273
|
expect(e.verdict.disposition).toBe('allow');
|
|
206
|
-
expect(e.detail.checks).toEqual({
|
|
274
|
+
expect(e.detail.checks).toEqual({
|
|
275
|
+
total: 0,
|
|
276
|
+
success: 0,
|
|
277
|
+
pending: 0,
|
|
278
|
+
failing: 0,
|
|
279
|
+
superseded: 0,
|
|
280
|
+
});
|
|
207
281
|
const zeroLines = e.notices.filter((n) => n.includes('ZERO status checks'));
|
|
208
282
|
expect(zeroLines).toHaveLength(1);
|
|
209
283
|
expect(zeroLines[0]).toMatch(/branch protection/i);
|
|
210
284
|
});
|
|
285
|
+
// ─── Same-named runs are judged by the LATEST run (mmnto-ai/totem#2879) ──
|
|
286
|
+
//
|
|
287
|
+
// The rollup lists EVERY check run on the head, a concurrency group's
|
|
288
|
+
// cancelled duplicate beside the run that superseded it, and its order is
|
|
289
|
+
// not chronological (mmnto-ai/totem#2877's head listed the later D1 run
|
|
290
|
+
// first). The latest run is the greatest `databaseId`. Two fixtures list
|
|
291
|
+
// the greater id in opposite positions so that neither "first listed" nor
|
|
292
|
+
// "last listed" survives as a mutant.
|
|
293
|
+
it('2879 — a concurrency-cancelled run beside the LATER success of the same name passes predicate 1, listed order notwithstanding', () => {
|
|
294
|
+
// The later (success) run is listed FIRST; a last-listed-wins read would
|
|
295
|
+
// judge the cancelled one and deny.
|
|
296
|
+
const e = evaluate('synthetic-check-superseded-cancelled.json');
|
|
297
|
+
expect(e.verdict.disposition).toBe('allow');
|
|
298
|
+
expect(e.detail.checks).toEqual({
|
|
299
|
+
total: 2,
|
|
300
|
+
success: 2,
|
|
301
|
+
pending: 0,
|
|
302
|
+
failing: 0,
|
|
303
|
+
superseded: 1,
|
|
304
|
+
});
|
|
305
|
+
const lines = e.notices.filter((n) => n.includes('times on the head commit'));
|
|
306
|
+
expect(lines).toHaveLength(1);
|
|
307
|
+
expect(lines[0]).toContain('check "Auto-close required check (D1)" from "github-actions/CI" ran 2 times on the head commit — judged by its latest run 5003 (success)');
|
|
308
|
+
expect(lines[0]).toContain('1 superseded run(s) not counted (mmnto-ai/totem#2879)');
|
|
309
|
+
});
|
|
310
|
+
it('2879 — a success and then a LATER cancelled run of the same name DENIES naming the check: the earlier success does not stand in', () => {
|
|
311
|
+
// The greater id (cancelled) is listed SECOND; a first-listed-wins read
|
|
312
|
+
// would judge the success and allow.
|
|
313
|
+
const e = evaluate('synthetic-check-cancelled-after-success.json');
|
|
314
|
+
expect(e.verdict.disposition).toBe('deny');
|
|
315
|
+
expect(e.verdict.provenance.ref).toBe('checks');
|
|
316
|
+
expect(e.verdict.reason).toContain('1 of 2 status checks are failing (Auto-close required check (D1))');
|
|
317
|
+
expect(e.detail.checks).toEqual({
|
|
318
|
+
total: 2,
|
|
319
|
+
success: 1,
|
|
320
|
+
pending: 0,
|
|
321
|
+
failing: 1,
|
|
322
|
+
superseded: 1,
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
it('2879 — ONE cancelled run with no later run of its name still denies: the negative that must hold', () => {
|
|
326
|
+
const e = evaluate('synthetic-check-single-cancelled.json');
|
|
327
|
+
expect(e.verdict.disposition).toBe('deny');
|
|
328
|
+
expect(e.verdict.provenance.ref).toBe('checks');
|
|
329
|
+
expect(e.verdict.reason).toContain('Auto-close required check (D1)');
|
|
330
|
+
expect(e.detail.checks).toEqual({
|
|
331
|
+
total: 2,
|
|
332
|
+
success: 1,
|
|
333
|
+
pending: 0,
|
|
334
|
+
failing: 1,
|
|
335
|
+
superseded: 0,
|
|
336
|
+
});
|
|
337
|
+
expect(e.notices.join('\n')).not.toMatch(/times on the head commit/);
|
|
338
|
+
});
|
|
339
|
+
it('2879 — two runs of one name where one carries no readable databaseId is UNREADABLE at both tiers, never the first or the last one listed', () => {
|
|
340
|
+
for (const tier of ['strict', 'pilot']) {
|
|
341
|
+
const e = evaluate('synthetic-check-duplicate-id-missing.json', { tier });
|
|
342
|
+
expect(e.verdict.disposition, tier).toBe(tier === 'pilot' ? 'warn' : 'deny');
|
|
343
|
+
expect(e.verdict.provenance.ref, tier).toBe('unevaluable');
|
|
344
|
+
expect(e.verdict.reason, tier).toContain('no readable databaseId');
|
|
345
|
+
expect(e.verdict.reason, tier).toContain('Auto-close required check (D1)');
|
|
346
|
+
expect(e.detail.checks, tier).toEqual({
|
|
347
|
+
total: 0,
|
|
348
|
+
success: 0,
|
|
349
|
+
pending: 0,
|
|
350
|
+
failing: 0,
|
|
351
|
+
superseded: 0,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
it('2879 — the query selects databaseId on CheckRun, so the judgment reads what gh answers', () => {
|
|
356
|
+
expect(MERGE_READY_QUERY).toContain('... on CheckRun { name status conclusion databaseId checkSuite { databaseId app { slug } workflowRun { workflow { databaseId name } } } }');
|
|
357
|
+
});
|
|
358
|
+
// ─── Bot round 1 (Greptile P1, P2): the producer is part of the key, and
|
|
359
|
+
// an id must be a SAFE integer ────────────────────────────────────────────
|
|
360
|
+
it('2879 — two INDEPENDENT checks that share a name never collapse: a later success from another workflow does not hide a failure (bot round 1, Greptile P1)', () => {
|
|
361
|
+
const e = evaluate('synthetic-check-same-name-two-producers.json');
|
|
362
|
+
expect(e.verdict.disposition).toBe('deny');
|
|
363
|
+
expect(e.verdict.provenance.ref).toBe('checks');
|
|
364
|
+
expect(e.verdict.reason).toContain('1 of 3 status checks are failing (test)');
|
|
365
|
+
expect(e.detail.checks).toEqual({
|
|
366
|
+
total: 3,
|
|
367
|
+
success: 2,
|
|
368
|
+
pending: 0,
|
|
369
|
+
failing: 1,
|
|
370
|
+
superseded: 0,
|
|
371
|
+
});
|
|
372
|
+
expect(e.notices.join('\n')).not.toMatch(/times on the head commit/);
|
|
373
|
+
});
|
|
374
|
+
it('2879 — two runs of one name where one carries no readable producer are UNREADABLE at both tiers: reruns cannot be told from independent checks (bot round 1, Greptile P1)', () => {
|
|
375
|
+
for (const tier of ['strict', 'pilot']) {
|
|
376
|
+
const e = evaluate('synthetic-check-duplicate-producer-missing.json', { tier });
|
|
377
|
+
expect(e.verdict.disposition, tier).toBe(tier === 'pilot' ? 'warn' : 'deny');
|
|
378
|
+
expect(e.verdict.provenance.ref, tier).toBe('unevaluable');
|
|
379
|
+
expect(e.verdict.reason, tier).toContain('no readable producer');
|
|
380
|
+
expect(e.verdict.reason, tier).toContain('Auto-close required check (D1)');
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
it('2879 — an id beyond the safe-integer range does not read: a rerun group carrying one is UNREADABLE, never ordered on a rounded number (bot round 1, Greptile P2)', () => {
|
|
384
|
+
for (const tier of ['strict', 'pilot']) {
|
|
385
|
+
const e = evaluate('synthetic-check-duplicate-id-unsafe.json', { tier });
|
|
386
|
+
expect(e.verdict.disposition, tier).toBe(tier === 'pilot' ? 'warn' : 'deny');
|
|
387
|
+
expect(e.verdict.provenance.ref, tier).toBe('unevaluable');
|
|
388
|
+
expect(e.verdict.reason, tier).toContain('no readable databaseId');
|
|
389
|
+
expect(e.verdict.reason, tier).toContain('from "github-actions/Auto-close guard"');
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
// ─── The re-armed leg's F1 and F6: the producer is the workflow's ID, and
|
|
393
|
+
// two same-named runs in one suite fail closed ─────────────────────────────
|
|
394
|
+
it('2879 — two workflow FILES sharing a display name are two producers: their same-named jobs are both judged and the failure denies (re-armed leg F1)', () => {
|
|
395
|
+
const e = evaluate('synthetic-check-same-name-two-workflow-files.json');
|
|
396
|
+
expect(e.verdict.disposition).toBe('deny');
|
|
397
|
+
expect(e.verdict.provenance.ref).toBe('checks');
|
|
398
|
+
expect(e.verdict.reason).toContain('1 of 3 status checks are failing (build)');
|
|
399
|
+
expect(e.detail.checks).toEqual({
|
|
400
|
+
total: 3,
|
|
401
|
+
success: 2,
|
|
402
|
+
pending: 0,
|
|
403
|
+
failing: 1,
|
|
404
|
+
superseded: 0,
|
|
405
|
+
});
|
|
406
|
+
expect(e.notices.join('\n')).not.toMatch(/times on the head commit/);
|
|
407
|
+
});
|
|
408
|
+
it('2879 — two same-named jobs of ONE workflow run share a check suite: independent checks the key cannot tell apart, UNREADABLE at both tiers (re-armed leg F1)', () => {
|
|
409
|
+
for (const tier of ['strict', 'pilot']) {
|
|
410
|
+
const e = evaluate('synthetic-check-two-jobs-one-suite.json', { tier });
|
|
411
|
+
expect(e.verdict.disposition, tier).toBe(tier === 'pilot' ? 'warn' : 'deny');
|
|
412
|
+
expect(e.verdict.provenance.ref, tier).toBe('unevaluable');
|
|
413
|
+
expect(e.verdict.reason, tier).toContain('sit in one check suite');
|
|
414
|
+
expect(e.verdict.reason, tier).toContain('check "test" from "github-actions/CI"');
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
it('2879 — a rerun group where one run has no readable check-suite id is UNREADABLE at both tiers (fourth leg F2)', () => {
|
|
418
|
+
for (const tier of ['strict', 'pilot']) {
|
|
419
|
+
const e = evaluate('synthetic-check-rerun-suite-id-missing.json', { tier });
|
|
420
|
+
expect(e.verdict.disposition, tier).toBe(tier === 'pilot' ? 'warn' : 'deny');
|
|
421
|
+
expect(e.verdict.provenance.ref, tier).toBe('unevaluable');
|
|
422
|
+
expect(e.verdict.reason, tier).toContain('no readable check-suite id');
|
|
423
|
+
expect(e.verdict.reason, tier).toContain('check "Auto-close required check (D1)" from "github-actions/Auto-close guard"');
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
it('2879 — an Actions run with no readable workflow id in a same-named group is a producer that did not read: UNREADABLE at both tiers, never one producer for every workflow (fourth leg F2, third leg F5)', () => {
|
|
427
|
+
for (const tier of ['strict', 'pilot']) {
|
|
428
|
+
const e = evaluate('synthetic-check-actions-workflow-id-missing.json', { tier });
|
|
429
|
+
expect(e.verdict.disposition, tier).toBe(tier === 'pilot' ? 'warn' : 'deny');
|
|
430
|
+
expect(e.verdict.provenance.ref, tier).toBe('unevaluable');
|
|
431
|
+
expect(e.verdict.reason, tier).toContain('no readable producer');
|
|
432
|
+
expect(e.verdict.reason, tier).toContain('the workflow id of an Actions run');
|
|
433
|
+
expect(e.verdict.reason, tier).toContain('Auto-close required check (D1)');
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
it('2879 — a non-Actions app that posts two same-named runs in its one suite is UNREADABLE at both tiers, and the reason names the suite, not a workflow run (fourth leg F4)', () => {
|
|
437
|
+
for (const tier of ['strict', 'pilot']) {
|
|
438
|
+
const e = evaluate('synthetic-check-app-two-runs-one-suite.json', { tier });
|
|
439
|
+
expect(e.verdict.disposition, tier).toBe(tier === 'pilot' ? 'warn' : 'deny');
|
|
440
|
+
expect(e.verdict.provenance.ref, tier).toBe('unevaluable');
|
|
441
|
+
expect(e.verdict.reason, tier).toContain('check "lint" from "some-ci-app"');
|
|
442
|
+
expect(e.verdict.reason, tier).toContain('two runs of one app');
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
it('2879 — two same-named checks from different producers where one has no id are both judged: the id orders reruns and nothing else (re-armed leg F6)', () => {
|
|
446
|
+
const e = evaluate('synthetic-check-two-producers-one-null-id.json');
|
|
447
|
+
expect(e.verdict.disposition).toBe('allow');
|
|
448
|
+
expect(e.detail.checks).toEqual({
|
|
449
|
+
total: 3,
|
|
450
|
+
success: 3,
|
|
451
|
+
pending: 0,
|
|
452
|
+
failing: 0,
|
|
453
|
+
superseded: 0,
|
|
454
|
+
});
|
|
455
|
+
expect(e.notices.join('\n')).not.toMatch(/times on the head commit|could not derive/);
|
|
456
|
+
});
|
|
457
|
+
// ─── The fold of the leg's F1–F4, F7: arithmetic and tolerances, not only
|
|
458
|
+
// the two-run happy path ─────────────────────────────────────────────────
|
|
459
|
+
it('2879 — superseded counts RUNS, one disclosure line per name, and an earlier FAILED run is superseded like a cancelled one (leg F1, F2, F7)', () => {
|
|
460
|
+
// Three D1 runs (FAILURE, CANCELLED, SUCCESS) beside two Totem Lint runs:
|
|
461
|
+
// a mutant counting NAMES would say 2; the lines carry their whole names.
|
|
462
|
+
const e = evaluate('synthetic-check-three-runs-two-names.json');
|
|
463
|
+
expect(e.verdict.disposition).toBe('allow');
|
|
464
|
+
expect(e.detail.checks).toEqual({
|
|
465
|
+
total: 3,
|
|
466
|
+
success: 3,
|
|
467
|
+
pending: 0,
|
|
468
|
+
failing: 0,
|
|
469
|
+
superseded: 3,
|
|
470
|
+
});
|
|
471
|
+
const lines = e.notices.filter((n) => n.includes('times on the head commit'));
|
|
472
|
+
expect(lines).toHaveLength(2);
|
|
473
|
+
expect(lines[0]).toContain('check "Auto-close required check (D1)" from "github-actions/CI" ran 3 times on the head commit — judged by its latest run 5004 (success)');
|
|
474
|
+
expect(lines[0]).toContain('2 superseded run(s) not counted');
|
|
475
|
+
expect(lines[1]).toContain('check "Totem Lint" from "github-actions/CI" ran 2 times on the head commit — judged by its latest run 5006 (success)');
|
|
476
|
+
expect(lines[1]).toContain('1 superseded run(s) not counted');
|
|
477
|
+
for (const line of lines)
|
|
478
|
+
expect(line).not.toContain('…');
|
|
479
|
+
});
|
|
480
|
+
it('2879 — a check that ran ONCE with no readable databaseId is judged on its conclusion, in BOTH directions: the id orders same-named runs and nothing else (leg F3, r2-F7)', () => {
|
|
481
|
+
// Two lone null-id runs: one SUCCESS, one FAILURE. The over-strict mutant
|
|
482
|
+
// (any unreadable id is unevaluable) would say "could not derive"; the
|
|
483
|
+
// lenient mutant (a lone null-id run reads as success) would allow.
|
|
484
|
+
const e = evaluate('synthetic-check-single-null-id.json');
|
|
485
|
+
expect(e.verdict.disposition).toBe('deny');
|
|
486
|
+
expect(e.verdict.provenance.ref).toBe('checks');
|
|
487
|
+
expect(e.verdict.reason).toContain('1 of 3 status checks are failing (Totem Doctor (--strict))');
|
|
488
|
+
expect(e.detail.checks).toEqual({
|
|
489
|
+
total: 3,
|
|
490
|
+
success: 2,
|
|
491
|
+
pending: 0,
|
|
492
|
+
failing: 1,
|
|
493
|
+
superseded: 0,
|
|
494
|
+
});
|
|
495
|
+
expect(e.notices.join('\n')).not.toMatch(/times on the head commit|could not derive/);
|
|
496
|
+
});
|
|
497
|
+
it('2879 — a check name longer than the evidence bound is disclosed WHOLE: the notice is sanitised, never sliced (r2-F1)', () => {
|
|
498
|
+
const e = evaluate('synthetic-check-long-name-superseded.json');
|
|
499
|
+
expect(e.verdict.disposition).toBe('allow');
|
|
500
|
+
expect(e.detail.checks).toEqual({
|
|
501
|
+
total: 2,
|
|
502
|
+
success: 2,
|
|
503
|
+
pending: 0,
|
|
504
|
+
failing: 0,
|
|
505
|
+
superseded: 1,
|
|
506
|
+
});
|
|
507
|
+
const lines = e.notices.filter((n) => n.includes('times on the head commit'));
|
|
508
|
+
expect(lines).toHaveLength(1);
|
|
509
|
+
const longName = 'spine-adopt harnesses on ubuntu-latest (specimens, seed20, matrix shard 7 of 12, reusable workflow harness/run-adopt.yml @ main, with the full fixture corpus and the extended timeout budget for cold-store starts)';
|
|
510
|
+
expect(longName.length).toBeGreaterThan(160);
|
|
511
|
+
expect(lines[0]).toContain(`check ${JSON.stringify(longName)} from "github-actions/CI" ran 2 times on the head commit`);
|
|
512
|
+
expect(lines[0]).not.toContain('…');
|
|
513
|
+
});
|
|
514
|
+
it("2879 — two runs whose name did not read are two checks, never one that ran twice (r2-F4; the first leg's F10)", () => {
|
|
515
|
+
const e = evaluate('synthetic-check-unnamed-runs.json');
|
|
516
|
+
expect(e.verdict.disposition).toBe('allow');
|
|
517
|
+
expect(e.detail.checks).toEqual({
|
|
518
|
+
total: 3,
|
|
519
|
+
success: 3,
|
|
520
|
+
pending: 0,
|
|
521
|
+
failing: 0,
|
|
522
|
+
superseded: 0,
|
|
523
|
+
});
|
|
524
|
+
expect(e.notices.join('\n')).not.toMatch(/times on the head commit|could not derive/);
|
|
525
|
+
});
|
|
526
|
+
it('2879 — same-named runs on different pages of the checks connection are judged together, after the last page (leg F4)', () => {
|
|
527
|
+
const e = evaluate('synthetic-check-duplicate-across-pages.json');
|
|
528
|
+
expect(e.verdict.disposition).toBe('allow');
|
|
529
|
+
expect(e.detail.checks).toEqual({
|
|
530
|
+
total: 2,
|
|
531
|
+
success: 2,
|
|
532
|
+
pending: 0,
|
|
533
|
+
failing: 0,
|
|
534
|
+
superseded: 1,
|
|
535
|
+
});
|
|
536
|
+
const graphql = e.calls.filter((c) => c[0] === 'api');
|
|
537
|
+
expect(graphql).toHaveLength(2);
|
|
538
|
+
expect(graphql[1]).toContain('checksAfter=checks-page-1');
|
|
539
|
+
const lines = e.notices.filter((n) => n.includes('times on the head commit'));
|
|
540
|
+
expect(lines).toHaveLength(1);
|
|
541
|
+
expect(lines[0]).toContain('judged by its latest run 5003 (success)');
|
|
542
|
+
});
|
|
211
543
|
// ─── The rollup must BELONG to the head commit (fold F7) ────────────────
|
|
212
544
|
//
|
|
213
545
|
// Predicate 1 reads `commits(last: 1)`. Every way that node can fail to be
|
|
@@ -314,15 +646,209 @@ describe('merge-ready — predicates 2 and 4 (bot threads)', () => {
|
|
|
314
646
|
expect(e.verdict.disposition).toBe('allow');
|
|
315
647
|
expect(e.detail.highInline).toBe(0);
|
|
316
648
|
});
|
|
317
|
-
it('a RESOLVED HIGH inline
|
|
318
|
-
// Predicate 4's own territory (
|
|
319
|
-
//
|
|
320
|
-
//
|
|
649
|
+
it('the NEGATIVE CONTROL — a RESOLVED HIGH inline still on the HEAD commit with no disposition on record denies at predicate 4 (mmnto-ai/totem#2861)', () => {
|
|
650
|
+
// Predicate 4's own territory (fold F2 of mmnto-ai/totem#2800, narrowed by
|
|
651
|
+
// the 2861 ruling), in the FIELD shape the legs constructed (f1, r2-f1): a
|
|
652
|
+
// bare UI resolve on a PR that keeps accumulating human comments. The only
|
|
653
|
+
// disposition naming this thread (root id 1001) PRE-dates the root; after
|
|
654
|
+
// it come a Bot's summary, a `[bot]`-suffixed login carrying the line (a
|
|
655
|
+
// counterfactual shape — GraphQL answers `Bot` for every App — kept to
|
|
656
|
+
// exercise the suffix arm), human chatter, and a later round's disposition
|
|
657
|
+
// that names ANOTHER thread (9999) and says so; the only in-thread reply is
|
|
658
|
+
// the bot's own. Predicate 2 passes, nothing discharges, the finding still
|
|
659
|
+
// applies, and the reason names the line it looked for AHEAD of the quoted
|
|
660
|
+
// body so it survives the matched bound (f6).
|
|
321
661
|
const e = evaluate('synthetic-head-commit-high-inline.json');
|
|
322
662
|
expect(e.verdict.disposition).toBe('deny');
|
|
323
663
|
expect(e.verdict.provenance.ref).toBe('high-severity-inline');
|
|
324
664
|
expect(e.detail.threads.unresolvedBot).toBe(0); // predicate 2 did NOT fire
|
|
325
665
|
expect(e.detail.highInline).toBe(1);
|
|
666
|
+
expect(e.detail.dischargedHigh).toBe(0);
|
|
667
|
+
expect(e.detail.dischargedBy).toEqual({ inThreadReply: 0, prLevelDisposition: 0 });
|
|
668
|
+
// The reason names the LINE the read looked for — with this thread's own
|
|
669
|
+
// id — never "no disposition exists" (r2-f3: a round disposition that
|
|
670
|
+
// answered other threads is a real event).
|
|
671
|
+
expect(e.verdict.reason).toContain('"disposition: 1001 <verb>"');
|
|
672
|
+
expect(e.verdict.reason).toMatch(/and no non-bot reply in its thread/);
|
|
673
|
+
expect(e.verdict.reason).toMatch(/a round disposition that did not name this thread/);
|
|
674
|
+
// The id survives the 160-character bound on `matched` because the line
|
|
675
|
+
// LEADS the clause (r3-f3: a clause that led with prose cut it off).
|
|
676
|
+
expect(e.verdict.provenance.matched).toContain('"disposition: 1001 <verb>"');
|
|
677
|
+
expect(e.notices.some((n) => n.includes('discharged'))).toBe(false);
|
|
678
|
+
// The fixture really carries the post-dating human chatter the field has.
|
|
679
|
+
const fixture = loadFixture('synthetic-head-commit-high-inline.json');
|
|
680
|
+
const pr = fixture.pages[0].body.data.repository.pullRequest;
|
|
681
|
+
const humanAfterRoot = pr.comments.nodes.filter((c) => c.author.__typename === 'User' && c.createdAt > '2026-09-08T03:00:00Z');
|
|
682
|
+
// Three post-dating User-typed nodes: the `[bot]`-suffixed login NAMING
|
|
683
|
+
// this thread (skipped by the suffix arm, not by content), the human
|
|
684
|
+
// chatter, and the other round's disposition naming 9999 — asserted
|
|
685
|
+
// through the shipped predicate. The last is the r2-f1 shape: under a
|
|
686
|
+
// round-keyed read it discharged; under the id it cannot.
|
|
687
|
+
const suffixed = humanAfterRoot.filter((c) => /\[bot\]$/i.test(c.author.login));
|
|
688
|
+
const humans = humanAfterRoot.filter((c) => !/\[bot\]$/i.test(c.author.login));
|
|
689
|
+
expect(suffixed.length).toBe(1);
|
|
690
|
+
expect(dispositionedRootIds(suffixed[0].body)).toContain(1001);
|
|
691
|
+
expect(humans.length).toBe(2);
|
|
692
|
+
expect(humans.every((c) => !dispositionedRootIds(c.body).includes(1001))).toBe(true);
|
|
693
|
+
expect(humans.some((c) => dispositionedRootIds(c.body).includes(9999))).toBe(true);
|
|
694
|
+
});
|
|
695
|
+
// ─── The discharge read (mmnto-ai/totem#2861) ────────────────────────────
|
|
696
|
+
//
|
|
697
|
+
// A HIGH on the head whose thread was RESOLVED through the disposition path
|
|
698
|
+
// — the resolve-threads evidence rule on the same read — no longer applies.
|
|
699
|
+
// The bare resolve above is the fail-closed arm; these are the two evidence
|
|
700
|
+
// arms and the edges of each.
|
|
701
|
+
it('a resolved HIGH on the head with a non-bot PR-level disposition line naming it AFTER its root is discharged', () => {
|
|
702
|
+
const e = evaluate('synthetic-high-inline-discharged-pr-level.json');
|
|
703
|
+
expect(e.verdict.disposition).toBe('allow');
|
|
704
|
+
expect(e.detail.highInline).toBe(0);
|
|
705
|
+
expect(e.detail.dischargedHigh).toBe(1);
|
|
706
|
+
expect(e.detail.dischargedBy).toEqual({ inThreadReply: 0, prLevelDisposition: 1 });
|
|
707
|
+
// The audit breadcrumb: what the predicate RELEASED, and by which arm, is
|
|
708
|
+
// on stderr.
|
|
709
|
+
const line = e.notices.find((n) => n.includes('discharged through the disposition path'));
|
|
710
|
+
expect(line).toBeDefined();
|
|
711
|
+
expect(line).toContain('1 HIGH/Major bot inline(s)');
|
|
712
|
+
expect(line).toContain('1 by a PR-level disposition line naming the thread');
|
|
713
|
+
expect(line).toContain('mmnto-ai/totem#4242');
|
|
714
|
+
});
|
|
715
|
+
it('a resolved HIGH on the head with a non-bot IN-THREAD reply and no PR-level comment is discharged', () => {
|
|
716
|
+
const e = evaluate('synthetic-high-inline-discharged-in-thread.json');
|
|
717
|
+
expect(e.verdict.disposition).toBe('allow');
|
|
718
|
+
expect(e.detail.highInline).toBe(0);
|
|
719
|
+
expect(e.detail.dischargedHigh).toBe(1);
|
|
720
|
+
expect(e.detail.dischargedBy).toEqual({ inThreadReply: 1, prLevelDisposition: 0 });
|
|
721
|
+
expect(e.notices.join('\n')).toContain('1 by a non-bot in-thread reply');
|
|
722
|
+
});
|
|
723
|
+
it('a root with no readable databaseId is a thread no line can name — it stays applying, and the page stays readable (r3-f4)', () => {
|
|
724
|
+
// The schema types `databaseId` nullable. A page-scoped refusal would
|
|
725
|
+
// route the PR into the unevaluable class, which pilot maps to `warn` —
|
|
726
|
+
// the same downgrade fold 1 removed for the incomplete window. So the
|
|
727
|
+
// thread alone fails closed, at both tiers, and the reason says why.
|
|
728
|
+
for (const tier of ['strict', 'pilot']) {
|
|
729
|
+
const e = evaluate('synthetic-high-inline-root-id-missing.json', { tier });
|
|
730
|
+
expect(e.verdict.disposition, tier).toBe('deny');
|
|
731
|
+
expect(e.verdict.provenance.ref, tier).toBe('high-severity-inline');
|
|
732
|
+
expect(e.verdict.reason, tier).toMatch(/no line can name it/);
|
|
733
|
+
expect(e.detail.highInline, tier).toBe(1);
|
|
734
|
+
expect(e.detail.dischargedHigh, tier).toBe(0);
|
|
735
|
+
expect(e.notices.join('\n'), tier).not.toMatch(/could not derive/);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
it('evidence FOUND discharges even when the thread window is incomplete (f7)', () => {
|
|
739
|
+
// The window is incomplete (`hasNextPage`) and carries only the bot's own
|
|
740
|
+
// reply, but the PR-level disposition after the root is on record: the
|
|
741
|
+
// completeness test never precedes the evidence tests.
|
|
742
|
+
const e = evaluate('synthetic-high-inline-discharged-window-incomplete.json');
|
|
743
|
+
expect(e.verdict.disposition).toBe('allow');
|
|
744
|
+
expect(e.detail.dischargedHigh).toBe(1);
|
|
745
|
+
expect(e.detail.dischargedBy).toEqual({ inThreadReply: 0, prLevelDisposition: 1 });
|
|
746
|
+
});
|
|
747
|
+
it('a deleted-account reply (author: null) is a human reply — the resolve-threads rule', () => {
|
|
748
|
+
const e = evaluate('synthetic-high-inline-deleted-author-reply.json');
|
|
749
|
+
expect(e.verdict.disposition).toBe('allow');
|
|
750
|
+
expect(e.detail.dischargedHigh).toBe(1);
|
|
751
|
+
});
|
|
752
|
+
it('an UNRESOLVED HIGH with a post-dating disposition is NOT discharged: the resolve is required, predicate 2 denies first', () => {
|
|
753
|
+
const e = evaluate('synthetic-high-inline-unresolved-with-disposition.json');
|
|
754
|
+
expect(e.verdict.disposition).toBe('deny');
|
|
755
|
+
expect(e.verdict.provenance.ref).toBe('unresolved-bot-threads');
|
|
756
|
+
expect(e.detail.highInline).toBe(1);
|
|
757
|
+
expect(e.detail.dischargedHigh).toBe(0);
|
|
758
|
+
});
|
|
759
|
+
it('discharge is judged PER THREAD: one discharged beside one bare denies at predicate 4 naming the bare one', () => {
|
|
760
|
+
const e = evaluate('synthetic-high-inline-mixed-discharge.json');
|
|
761
|
+
expect(e.verdict.disposition).toBe('deny');
|
|
762
|
+
expect(e.verdict.provenance.ref).toBe('high-severity-inline');
|
|
763
|
+
expect(e.detail.highInline).toBe(1);
|
|
764
|
+
expect(e.detail.dischargedHigh).toBe(1);
|
|
765
|
+
expect(e.detail.dischargedBy).toEqual({ inThreadReply: 1, prLevelDisposition: 0 });
|
|
766
|
+
expect(e.verdict.reason).toContain('coderabbitai');
|
|
767
|
+
expect(e.verdict.reason).toContain('"disposition: 1002 <verb>"');
|
|
768
|
+
expect(e.verdict.reason).toMatch(/a round disposition that did not name this thread/);
|
|
769
|
+
});
|
|
770
|
+
it('the pr-level fixture disposition names root 1001 on its line, read through the shipped predicate', () => {
|
|
771
|
+
// Read straight off the fixture rather than only through the verdict: the
|
|
772
|
+
// negative control's post-dating comments name nothing but 9999 (asserted
|
|
773
|
+
// above), while the pr-level fixture's disposition body names the thread.
|
|
774
|
+
const pr = loadFixture('synthetic-high-inline-discharged-pr-level.json').pages[0].body.data.repository.pullRequest;
|
|
775
|
+
const human = pr.comments.nodes.filter((c) => c.author.__typename === 'User');
|
|
776
|
+
expect(human).toHaveLength(1);
|
|
777
|
+
expect(dispositionedRootIds(human[0].body)).toEqual([1001]);
|
|
778
|
+
});
|
|
779
|
+
it('the disposition line is read at line start anywhere in the body, fenced included, and never inside an HTML comment, a quote, a list or a span', () => {
|
|
780
|
+
const line = 'disposition: 4000747291 declined';
|
|
781
|
+
// The skills render machine lines inside text fences; a fenced line counts.
|
|
782
|
+
expect(dispositionedRootIds(`## Round 1 disposition\n\n\`\`\`text\n${line}\n\`\`\`\n`)).toEqual([4000747291]);
|
|
783
|
+
expect(dispositionedRootIds(`prose\n\n${line}`)).toEqual([4000747291]);
|
|
784
|
+
expect(dispositionedRootIds(` ${line}`)).toEqual([4000747291]);
|
|
785
|
+
expect(dispositionedRootIds(`\t${line}`)).toEqual([4000747291]);
|
|
786
|
+
expect(dispositionedRootIds(`## Round 1 disposition\r\n\r\n${line}\r\n`)).toEqual([4000747291]);
|
|
787
|
+
// Several lines, in order, without duplicates; a verb is not required.
|
|
788
|
+
expect(dispositionedRootIds('disposition: 11 fixed\ndisposition: 22 declined\ndisposition: 11 nit\ndisposition: 33')).toEqual([11, 22, 33]);
|
|
789
|
+
// Not at line start: an inline span mid-sentence, a blockquote, a list
|
|
790
|
+
// item, a table cell. Not the token: a prose sentence, a different key.
|
|
791
|
+
expect(dispositionedRootIds(`as recorded: \`${line}\`.`)).toEqual([]);
|
|
792
|
+
expect(dispositionedRootIds(`> ${line}`)).toEqual([]);
|
|
793
|
+
expect(dispositionedRootIds(`- ${line}`)).toEqual([]);
|
|
794
|
+
expect(dispositionedRootIds(`| ${line} |`)).toEqual([]);
|
|
795
|
+
expect(dispositionedRootIds('the disposition: 4000747291 was declined')).toEqual([]);
|
|
796
|
+
expect(dispositionedRootIds('dispositions: 4000747291 declined')).toEqual([]);
|
|
797
|
+
// An HTML comment is stripped before the read — terminated, or left open
|
|
798
|
+
// to the end of the body the way a renderer hides it (r3-f9).
|
|
799
|
+
expect(dispositionedRootIds(`<!--\n${line}\n-->`)).toEqual([]);
|
|
800
|
+
expect(dispositionedRootIds(`<!-- note\n${line}`)).toEqual([]);
|
|
801
|
+
expect(dispositionedRootIds(`${line}\n<!-- note`)).toEqual([4000747291]);
|
|
802
|
+
// The id is the exact decimal the seat copied: a longer run of digits is
|
|
803
|
+
// a different id, and nothing coerced — a leading zero, a fraction, a
|
|
804
|
+
// glued letter or dash is not this id (r3-f5: `Number()` had equated
|
|
805
|
+
// `01001`, `1001.5` and `1001x` to 1001).
|
|
806
|
+
expect(dispositionedRootIds('disposition: 40007472911 declined')).toEqual([40007472911]);
|
|
807
|
+
for (const lenient of [
|
|
808
|
+
'disposition: 04000747291 declined',
|
|
809
|
+
'disposition: 4000747291.0 declined',
|
|
810
|
+
'disposition: 4000747291.5 declined',
|
|
811
|
+
'disposition: 4000747291x declined',
|
|
812
|
+
'disposition: 4000747291-fixed',
|
|
813
|
+
'disposition: +4000747291 declined',
|
|
814
|
+
'disposition:4000747291 declined',
|
|
815
|
+
'Disposition: 4000747291 declined',
|
|
816
|
+
]) {
|
|
817
|
+
expect(dispositionedRootIds(lenient), lenient).toEqual([]);
|
|
818
|
+
}
|
|
819
|
+
expect(dispositionedRootIds('disposition: 4000747291')).toEqual([4000747291]);
|
|
820
|
+
expect(dispositionedRootIds('disposition: 4000747291\r\n')).toEqual([4000747291]);
|
|
821
|
+
expect(dispositionedRootIds('no line at all')).toEqual([]);
|
|
822
|
+
// A fenced QUOTE of a prior disposition names the same ids it named — the
|
|
823
|
+
// disclosed residue of accepting fenced lines; it can only re-name threads
|
|
824
|
+
// an earlier disposition already named.
|
|
825
|
+
expect(dispositionedRootIds(`For the record, the round disposition read:\n\n\`\`\`\n${line}\n\`\`\``)).toEqual([4000747291]);
|
|
826
|
+
});
|
|
827
|
+
it('PR-level evidence on the SECOND comments page is found, with the cursor sent', () => {
|
|
828
|
+
const e = evaluate('synthetic-comments-second-page-evidence.json');
|
|
829
|
+
expect(e.verdict.disposition).toBe('allow');
|
|
830
|
+
expect(e.detail.dischargedHigh).toBe(1);
|
|
831
|
+
expect(e.detail.threads.pagesRead).toBe(2);
|
|
832
|
+
expect(e.calls[2]).toContain('commentsAfter=CURSOR-C1');
|
|
833
|
+
});
|
|
834
|
+
it('a resolved HIGH with more comments than the window and no evidence in what was read is a bare resolve that DENIES at both tiers, the reason naming the window (f2)', () => {
|
|
835
|
+
// Pre-cure this shape was a predicate-4 FACT that denied at every tier; a
|
|
836
|
+
// first fold routed it to the unevaluable class, which pilot maps to
|
|
837
|
+
// `warn` — a downgrade the first leg caught (f2). It is a fact-side deny
|
|
838
|
+
// again, judged on what was read, and the reason says the window was
|
|
839
|
+
// incomplete rather than calling the thread unanswered outright.
|
|
840
|
+
for (const tier of ['strict', 'pilot']) {
|
|
841
|
+
const e = evaluate('synthetic-high-inline-resolved-window-incomplete.json', { tier });
|
|
842
|
+
expect(e.verdict.disposition, tier).toBe('deny');
|
|
843
|
+
expect(e.verdict.provenance.ref, tier).toBe('high-severity-inline');
|
|
844
|
+
expect(e.verdict.reason, tier).toMatch(/no non-bot reply in the ten comments read \(the thread has more/);
|
|
845
|
+
expect(e.verdict.reason, tier).toContain('"disposition: 1001 <verb>"');
|
|
846
|
+
// The id sits inside the 160-character bound on `matched` (r3-f3).
|
|
847
|
+
expect(e.verdict.provenance.matched, tier).toContain('"disposition: 1001 <verb>"');
|
|
848
|
+
expect(e.detail.highInline, tier).toBe(1);
|
|
849
|
+
expect(e.detail.dischargedHigh, tier).toBe(0);
|
|
850
|
+
expect(e.notices.join('\n'), tier).not.toMatch(/could not derive/);
|
|
851
|
+
}
|
|
326
852
|
});
|
|
327
853
|
it('reads comment.commit (where the finding applies NOW), never originalCommit', () => {
|
|
328
854
|
// The falsifier for the inert predicate this fold removed: both fixtures
|
|
@@ -627,6 +1153,8 @@ describe('merge-ready — pagination and the unevaluable class', () => {
|
|
|
627
1153
|
['synthetic-reviews-connection-missing.json', /reviews connection was missing/],
|
|
628
1154
|
['synthetic-threads-connection-missing.json', /review threads connection was missing/],
|
|
629
1155
|
['synthetic-threads-pageinfo-missing.json', /review threads connection carried no pageInfo/],
|
|
1156
|
+
// The evidence surface (mmnto-ai/totem#2861) is held to the same bar.
|
|
1157
|
+
['synthetic-comments-connection-missing.json', /PR comments connection was missing/],
|
|
630
1158
|
]) {
|
|
631
1159
|
const strict = evaluate(file);
|
|
632
1160
|
expect(strict.verdict.disposition, file).toBe('deny');
|
|
@@ -671,6 +1199,7 @@ describe('merge-ready — pagination and the unevaluable class', () => {
|
|
|
671
1199
|
'synthetic-pagination-second-page-fails.json',
|
|
672
1200
|
'synthetic-head-moved.json',
|
|
673
1201
|
'synthetic-merge-state-unknown.json',
|
|
1202
|
+
'synthetic-comments-connection-missing.json',
|
|
674
1203
|
]) {
|
|
675
1204
|
for (const tier of ['strict', 'pilot']) {
|
|
676
1205
|
const e = evaluate(file, { tier });
|