@mmnto/cli 1.114.0 → 1.116.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/commands/doctor-compliance.test.js +17 -0
- package/dist/commands/doctor-compliance.test.js.map +1 -1
- package/dist/commands/doctor-seat-identity.d.ts +87 -0
- package/dist/commands/doctor-seat-identity.d.ts.map +1 -0
- package/dist/commands/doctor-seat-identity.js +427 -0
- package/dist/commands/doctor-seat-identity.js.map +1 -0
- package/dist/commands/doctor-seat-identity.test.d.ts +13 -0
- package/dist/commands/doctor-seat-identity.test.d.ts.map +1 -0
- package/dist/commands/doctor-seat-identity.test.js +390 -0
- package/dist/commands/doctor-seat-identity.test.js.map +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +3 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/doctor.test.js +1 -0
- package/dist/commands/doctor.test.js.map +1 -1
- package/dist/commands/ecl-gc.d.ts.map +1 -1
- package/dist/commands/ecl-gc.js +18 -7
- package/dist/commands/ecl-gc.js.map +1 -1
- package/dist/commands/ecl-gc.test.js +63 -1
- package/dist/commands/ecl-gc.test.js.map +1 -1
- package/dist/commands/eject.d.ts +62 -3
- package/dist/commands/eject.d.ts.map +1 -1
- package/dist/commands/eject.js +382 -47
- package/dist/commands/eject.js.map +1 -1
- package/dist/commands/eject.test.js +557 -7
- package/dist/commands/eject.test.js.map +1 -1
- package/dist/commands/gemini-beforetool-contract.test.d.ts +2 -0
- package/dist/commands/gemini-beforetool-contract.test.d.ts.map +1 -0
- package/dist/commands/gemini-beforetool-contract.test.js +106 -0
- package/dist/commands/gemini-beforetool-contract.test.js.map +1 -0
- package/dist/commands/gemini-sessionstart-contract.test.d.ts +2 -0
- package/dist/commands/gemini-sessionstart-contract.test.d.ts.map +1 -0
- package/dist/commands/gemini-sessionstart-contract.test.js +199 -0
- package/dist/commands/gemini-sessionstart-contract.test.js.map +1 -0
- package/dist/commands/init-templates.d.ts +4 -4
- package/dist/commands/init-templates.d.ts.map +1 -1
- package/dist/commands/init-templates.js +279 -16
- package/dist/commands/init-templates.js.map +1 -1
- package/dist/commands/init.test.js +25 -7
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/install-hooks-exit-contract.test.js +5 -0
- package/dist/commands/install-hooks-exit-contract.test.js.map +1 -1
- package/dist/commands/mail-degraded-e4.test.js +126 -0
- package/dist/commands/mail-degraded-e4.test.js.map +1 -1
- package/dist/commands/mail.d.ts +19 -1
- package/dist/commands/mail.d.ts.map +1 -1
- package/dist/commands/mail.js +195 -30
- package/dist/commands/mail.js.map +1 -1
- package/dist/commands/mail.test.js +291 -3
- package/dist/commands/mail.test.js.map +1 -1
- package/dist/commands/orient.d.ts.map +1 -1
- package/dist/commands/orient.js +82 -0
- package/dist/commands/orient.js.map +1 -1
- package/dist/commands/orient.test.js +144 -1
- package/dist/commands/orient.test.js.map +1 -1
- package/dist/commands/retrospect.d.ts.map +1 -1
- package/dist/commands/retrospect.js +7 -3
- package/dist/commands/retrospect.js.map +1 -1
- package/dist/commands/seat-cli-wiring.test.d.ts +20 -0
- package/dist/commands/seat-cli-wiring.test.d.ts.map +1 -0
- package/dist/commands/seat-cli-wiring.test.js +147 -0
- package/dist/commands/seat-cli-wiring.test.js.map +1 -0
- package/dist/commands/seat-poll-chain.test.d.ts +13 -0
- package/dist/commands/seat-poll-chain.test.d.ts.map +1 -0
- package/dist/commands/seat-poll-chain.test.js +103 -0
- package/dist/commands/seat-poll-chain.test.js.map +1 -0
- package/dist/commands/seat.d.ts +92 -0
- package/dist/commands/seat.d.ts.map +1 -0
- package/dist/commands/seat.js +649 -0
- package/dist/commands/seat.js.map +1 -0
- package/dist/commands/seat.test.d.ts +19 -0
- package/dist/commands/seat.test.d.ts.map +1 -0
- package/dist/commands/seat.test.js +532 -0
- package/dist/commands/seat.test.js.map +1 -0
- package/dist/commands/sessionstart-manifest-contract.test.d.ts +2 -0
- package/dist/commands/sessionstart-manifest-contract.test.d.ts.map +1 -0
- package/dist/commands/sessionstart-manifest-contract.test.js +186 -0
- package/dist/commands/sessionstart-manifest-contract.test.js.map +1 -0
- package/dist/commands/triage-pr.d.ts.map +1 -1
- package/dist/commands/triage-pr.js +2 -0
- package/dist/commands/triage-pr.js.map +1 -1
- package/dist/hooks/__tests__/auto-context.test.js +20 -0
- package/dist/hooks/__tests__/auto-context.test.js.map +1 -1
- package/dist/hooks/auto-context.d.ts +19 -0
- package/dist/hooks/auto-context.d.ts.map +1 -1
- package/dist/hooks/auto-context.js +22 -4
- package/dist/hooks/auto-context.js.map +1 -1
- package/dist/index.js +71 -0
- package/dist/index.js.map +1 -1
- package/dist/parsers/bot-review-parser.d.ts +8 -1
- package/dist/parsers/bot-review-parser.d.ts.map +1 -1
- package/dist/parsers/bot-review-parser.js +21 -1
- package/dist/parsers/bot-review-parser.js.map +1 -1
- package/dist/parsers/bot-review-parser.test.js +23 -0
- package/dist/parsers/bot-review-parser.test.js.map +1 -1
- package/dist/parsers/triage-severity-mapper.d.ts.map +1 -1
- package/dist/parsers/triage-severity-mapper.js +32 -1
- package/dist/parsers/triage-severity-mapper.js.map +1 -1
- package/dist/parsers/triage-severity-mapper.test.js +57 -0
- package/dist/parsers/triage-severity-mapper.test.js.map +1 -1
- package/package.json +2 -2
|
@@ -11,10 +11,10 @@ import * as fs from 'node:fs';
|
|
|
11
11
|
import * as os from 'node:os';
|
|
12
12
|
import * as path from 'node:path';
|
|
13
13
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
-
import { findTotemRepoRootSync, TotemError } from '@mmnto/totem';
|
|
14
|
+
import { findTotemRepoRootSync, SEAT_LIFECYCLE_FILENAME, SEAT_LIFECYCLE_SCHEMA_VERSION, TotemError, writeSeatLifecycle, } from '@mmnto/totem';
|
|
15
15
|
import { cleanTmpDir } from '../test-utils.js';
|
|
16
16
|
import { log } from '../ui.js';
|
|
17
|
-
import { composeDispatch, mailCommand, mailReply, mailSend, markSource, pollMail, resolveMailExitCode, resolveSelfSender, sanitizeEclBasename, validateDispatchContent, } from './mail.js';
|
|
17
|
+
import { composeDispatch, formatTextResult, mailCommand, mailReply, mailSend, markSource, pollMail, resolveMailExitCode, resolveSelfSender, sanitizeEclBasename, validateDispatchContent, } from './mail.js';
|
|
18
18
|
// ─── Fixture helpers ────────────────────────────────────
|
|
19
19
|
let tmpRoot;
|
|
20
20
|
let workspace;
|
|
@@ -59,6 +59,20 @@ function writeBroadcastProcessed(repo, recipientAgent, names) {
|
|
|
59
59
|
for (const n of names)
|
|
60
60
|
fs.writeFileSync(path.join(dir, n), '---\nto: broadcast\n---\n', 'utf-8');
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Declare a seat's lifecycle at `<repo>/.totem/orchestration/<seat>/lifecycle.json`
|
|
64
|
+
* (mmnto-ai/totem#2511) through CORE'S REAL WRITER — the same producer the
|
|
65
|
+
* `totem seat` verbs use — so the fixture can never be weaker than the on-disk
|
|
66
|
+
* contract the poll reads back (a hand-rolled JSON fixture could drift from the
|
|
67
|
+
* strict schema and quietly test nothing). Returns the marker path.
|
|
68
|
+
*/
|
|
69
|
+
function writeLifecycleMarker(repo, seat, state) {
|
|
70
|
+
return writeSeatLifecycle(path.join(workspace, repo), seat, {
|
|
71
|
+
schemaVersion: SEAT_LIFECYCLE_SCHEMA_VERSION,
|
|
72
|
+
state,
|
|
73
|
+
since: '2026-08-11T00:00:00.000Z',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
62
76
|
/**
|
|
63
77
|
* `totem` is the only cohort name resolveSelfAgents pre-knows; using it as
|
|
64
78
|
* the "self" repo keeps the SELF_AGENT resolution working off the basename
|
|
@@ -424,6 +438,272 @@ describe('pollMail — broadcast marks are per-seat, not union (mmnto-ai/totem#2
|
|
|
424
438
|
expect(poll({ env: TWO_SEATS }).mail).toEqual([]);
|
|
425
439
|
});
|
|
426
440
|
});
|
|
441
|
+
// ─── Lifecycle-aware broadcast denominators ────────────
|
|
442
|
+
// mmnto-ai/totem#2511: a suspended-but-dir-present seat pinned every broadcast
|
|
443
|
+
// open forever (the denominator failure). The declared lifecycle marker takes
|
|
444
|
+
// such a seat OUT OF THE DENOMINATOR only — resolution stays lifecycle-blind,
|
|
445
|
+
// so directed mail keeps surfacing (a transition does not discharge an
|
|
446
|
+
// obligation edge, OQ4) and directed marks keep subtracting (an already-handled
|
|
447
|
+
// dispatch resurfacing as unread is the ADR-106 § A2.1 false-unread bomb).
|
|
448
|
+
describe('pollMail — lifecycle-aware broadcast denominators (mmnto-ai/totem#2511)', () => {
|
|
449
|
+
const BROADCAST_FILE = '2026-08-11T0930Z-broadcast-lifecycle-note.md';
|
|
450
|
+
const TWO_SEATS = { TOTEM_SELF_AGENT: 'totem-claude,totem-gemini' };
|
|
451
|
+
function writeBroadcast() {
|
|
452
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
453
|
+
{ name: BROADCAST_FILE, to: 'broadcast', subject: 'cohort note' },
|
|
454
|
+
]);
|
|
455
|
+
}
|
|
456
|
+
it("a suspended seat leaves the denominator — the ACTIVE seats' marks alone close the broadcast", () => {
|
|
457
|
+
writeBroadcast();
|
|
458
|
+
writeLifecycleMarker('totem', 'totem-gemini', 'suspended');
|
|
459
|
+
// Only the ACTIVE seat holds a mark. Pre-#2511 the requirement was 2 and
|
|
460
|
+
// this broadcast stayed unread forever — the pinned-open denominator.
|
|
461
|
+
writeBroadcastProcessed('totem', 'totem-claude', [BROADCAST_FILE]);
|
|
462
|
+
expect(poll({ env: TWO_SEATS }).mail).toEqual([]);
|
|
463
|
+
});
|
|
464
|
+
it("a suspended seat's mark alone cannot close a broadcast the active seat has not marked", () => {
|
|
465
|
+
writeBroadcast();
|
|
466
|
+
writeLifecycleMarker('totem', 'totem-gemini', 'suspended');
|
|
467
|
+
// The SUSPENDED seat's mark, and only it. The requirement is 1 (one active
|
|
468
|
+
// seat), so a shared count map would read 1 >= 1 and vacuously consume the
|
|
469
|
+
// broadcast for the active seat that never saw it — which is why the
|
|
470
|
+
// broadcast count is folded from the ACTIVE seats' marks only.
|
|
471
|
+
writeBroadcastProcessed('totem', 'totem-gemini', [BROADCAST_FILE]);
|
|
472
|
+
expect(poll({ env: TWO_SEATS }).mail.map((m) => m.file)).toEqual([BROADCAST_FILE]);
|
|
473
|
+
});
|
|
474
|
+
it('a suspended seat stays RESOLVED: its directed mail surfaces, annotated once per poll', () => {
|
|
475
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
476
|
+
{ name: '2026-08-11T0931Z-totem-gemini-held-one.md', to: 'totem-gemini' },
|
|
477
|
+
{ name: '2026-08-11T0932Z-totem-gemini-held-two.md', to: 'totem-gemini' },
|
|
478
|
+
]);
|
|
479
|
+
writeLifecycleMarker('totem', 'totem-gemini', 'suspended');
|
|
480
|
+
const result = poll({ env: TWO_SEATS });
|
|
481
|
+
// Resolution is lifecycle-blind — the seat is still SELF.
|
|
482
|
+
expect(result.selfAgents.agents).toEqual(['totem-claude', 'totem-gemini']);
|
|
483
|
+
// Sense, never hide: both held dispatches surface (OQ4 — a suspension does
|
|
484
|
+
// not discharge the obligation edge).
|
|
485
|
+
expect(result.mail).toHaveLength(2);
|
|
486
|
+
// Bounded annotation: ONE notice for the seat, not one per file — and it
|
|
487
|
+
// rides the NON-GATING notices channel, never warnings (finding 1 of the
|
|
488
|
+
// falsification round: warnings arm three gates).
|
|
489
|
+
expect(result.warnings).toEqual([]);
|
|
490
|
+
const held = result.notices.filter((n) => n.includes('suspended seat'));
|
|
491
|
+
expect(held).toHaveLength(1);
|
|
492
|
+
expect(held[0]).toContain('totem-gemini');
|
|
493
|
+
expect(held[0]).toContain('obligation held, not discharged');
|
|
494
|
+
expect(held[0]).toContain('mmnto-ai/totem-status#127');
|
|
495
|
+
});
|
|
496
|
+
it('a suspended seat with held mail never pins the verdict to INCOMPLETE (the #2516 discriminator stays honest)', () => {
|
|
497
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
498
|
+
{ name: '2026-08-11T0939Z-totem-gemini-held.md', to: 'totem-gemini' },
|
|
499
|
+
]);
|
|
500
|
+
writeLifecycleMarker('totem', 'totem-gemini', 'suspended');
|
|
501
|
+
const rendered = formatTextResult(poll({ env: TWO_SEATS }));
|
|
502
|
+
// The scan IS complete: the annotation renders as a Note line and the
|
|
503
|
+
// verdict must not claim degradation — a suspended seat must leave the
|
|
504
|
+
// INCOMPLETE token meaning "scan not trustworthy", not "lifecycle exists".
|
|
505
|
+
expect(rendered).toContain('Note: directed mail surfacing for suspended seat');
|
|
506
|
+
expect(rendered).not.toContain('INCOMPLETE');
|
|
507
|
+
expect(rendered).toContain('1 unread:');
|
|
508
|
+
});
|
|
509
|
+
it("a suspended seat's already-consumed directed mail stays consumed (no false-unread)", () => {
|
|
510
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
511
|
+
{ name: '2026-08-11T0933Z-totem-gemini-done.md', to: 'totem-gemini', subject: 'handled' },
|
|
512
|
+
{ name: '2026-08-11T0934Z-totem-gemini-live.md', to: 'totem-gemini', subject: 'still open' },
|
|
513
|
+
]);
|
|
514
|
+
writeProcessed('totem', 'totem-gemini', ['2026-08-11T0933Z-totem-gemini-done.md']);
|
|
515
|
+
writeLifecycleMarker('totem', 'totem-gemini', 'suspended');
|
|
516
|
+
// NON-VACUITY: the unconsumed sibling still surfaces, so "the handled one is
|
|
517
|
+
// gone" is a subtraction, not an empty scan.
|
|
518
|
+
expect(poll({ env: TWO_SEATS }).mail.map((m) => m.file)).toEqual([
|
|
519
|
+
'2026-08-11T0934Z-totem-gemini-live.md',
|
|
520
|
+
]);
|
|
521
|
+
});
|
|
522
|
+
it('mail addressed to a RETIRED seat surfaces, with the retired-addressee warning (sense, never hide)', () => {
|
|
523
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
524
|
+
{ name: '2026-08-11T0935Z-totem-gemini-late.md', to: 'totem-gemini' },
|
|
525
|
+
{ name: '2026-08-11T0936Z-totem-gemini-later.md', to: 'totem-gemini' },
|
|
526
|
+
]);
|
|
527
|
+
writeLifecycleMarker('totem', 'totem-gemini', 'retired');
|
|
528
|
+
const result = poll({ env: TWO_SEATS });
|
|
529
|
+
expect(result.mail).toHaveLength(2);
|
|
530
|
+
// Notices channel, never warnings — same channel discipline as suspended.
|
|
531
|
+
expect(result.warnings).toEqual([]);
|
|
532
|
+
const retired = result.notices.filter((n) => n.includes('addressed to retired seat'));
|
|
533
|
+
expect(retired).toHaveLength(1);
|
|
534
|
+
expect(retired[0]).toContain('totem-gemini');
|
|
535
|
+
});
|
|
536
|
+
it('channel discipline: annotations ride the non-gating notices channel on BOTH paths; only the corrupt-marker warning arms the gates', () => {
|
|
537
|
+
// The `warnings` channel arms THREE gates (ecl-gc A2.2, A2.4
|
|
538
|
+
// verifyComplete, the #2516 INCOMPLETE verdict). Lifecycle annotations are
|
|
539
|
+
// informational senses on a healthy scan — they ride `notices`, which no
|
|
540
|
+
// gate reads, uniformly on the reader AND the compaction-discovery path. A
|
|
541
|
+
// corrupt marker IS a denominator-integrity anomaly and stays a warning on
|
|
542
|
+
// both paths.
|
|
543
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
544
|
+
{ name: '2026-08-11T0937Z-totem-gemini-held.md', to: 'totem-gemini' },
|
|
545
|
+
{ name: '2026-08-11T0938Z-totem-claude-note.md', to: 'totem-claude' },
|
|
546
|
+
]);
|
|
547
|
+
writeLifecycleMarker('totem', 'totem-gemini', 'suspended');
|
|
548
|
+
const readerView = poll({ env: TWO_SEATS });
|
|
549
|
+
const discoveryView = poll({ env: TWO_SEATS, includeProcessed: true });
|
|
550
|
+
for (const view of [readerView, discoveryView]) {
|
|
551
|
+
expect(view.warnings).toEqual([]);
|
|
552
|
+
expect(view.notices.filter((n) => n.includes('suspended seat'))).toHaveLength(1);
|
|
553
|
+
expect(view.mail).toHaveLength(2);
|
|
554
|
+
}
|
|
555
|
+
// Corrupt the marker: the integrity warning fires on the discovery path
|
|
556
|
+
// too (naming the marker path), redding the compaction gate by design.
|
|
557
|
+
const markerPath = path.join(workspace, 'totem', '.totem', 'orchestration', 'totem-gemini', SEAT_LIFECYCLE_FILENAME);
|
|
558
|
+
fs.writeFileSync(markerPath, '{not json', 'utf-8');
|
|
559
|
+
const corruptDiscovery = poll({ env: TWO_SEATS, includeProcessed: true });
|
|
560
|
+
const corruptWarnings = corruptDiscovery.warnings.filter((w) => w.includes(markerPath));
|
|
561
|
+
expect(corruptWarnings).toHaveLength(1);
|
|
562
|
+
});
|
|
563
|
+
it('all self seats suspended ⇒ broadcasts NEVER subtract, and the hold is NAMED with its recovery (never a clean render)', () => {
|
|
564
|
+
writeBroadcast();
|
|
565
|
+
writeLifecycleMarker('totem', 'totem-claude', 'suspended');
|
|
566
|
+
writeLifecycleMarker('totem', 'totem-gemini', 'suspended');
|
|
567
|
+
writeBroadcastProcessed('totem', 'totem-claude', [BROADCAST_FILE]);
|
|
568
|
+
writeBroadcastProcessed('totem', 'totem-gemini', [BROADCAST_FILE]);
|
|
569
|
+
// activeCount 0 ⇒ required stays 1 (`max(…, 1)`) and the active-mark map is
|
|
570
|
+
// EMPTY, so the count is 0: an empty required set is never "closed" (the
|
|
571
|
+
// inherited orchestration.go boundary). Never vacuously consumed.
|
|
572
|
+
const result = poll({ env: TWO_SEATS });
|
|
573
|
+
expect(result.mail.map((m) => m.file)).toEqual([BROADCAST_FILE]);
|
|
574
|
+
// Falsification finding 2: the permanently-pinned broadcast must not
|
|
575
|
+
// render as a clean unread line — one notice names the all-suspended
|
|
576
|
+
// cause and the reactivation recovery. Warnings stay empty (the scan is
|
|
577
|
+
// healthy; nothing here may red a gate).
|
|
578
|
+
expect(result.warnings).toEqual([]);
|
|
579
|
+
const held = result.notices.filter((n) => n.includes('suspended/retired'));
|
|
580
|
+
expect(held).toHaveLength(1);
|
|
581
|
+
expect(held[0]).toContain('--reactivate');
|
|
582
|
+
});
|
|
583
|
+
it('all seats RETIRED ⇒ the hold notice routes to a fresh ruling, never to the --reactivate the CLI refuses (re-arm D-1)', () => {
|
|
584
|
+
writeBroadcast();
|
|
585
|
+
writeLifecycleMarker('totem', 'totem-claude', 'retired');
|
|
586
|
+
writeLifecycleMarker('totem', 'totem-gemini', 'retired');
|
|
587
|
+
const result = poll({ env: TWO_SEATS });
|
|
588
|
+
expect(result.mail.map((m) => m.file)).toEqual([BROADCAST_FILE]);
|
|
589
|
+
const held = result.notices.filter((n) => n.includes('retired'));
|
|
590
|
+
expect(held).toHaveLength(1);
|
|
591
|
+
// `seat add --reactivate` hard-errors on a retired seat — the notice must
|
|
592
|
+
// never advertise a recovery the CLI structurally refuses.
|
|
593
|
+
expect(held[0]).not.toContain('--reactivate');
|
|
594
|
+
expect(held[0]).toContain('register a replacement seat');
|
|
595
|
+
});
|
|
596
|
+
it('a broadcast-NAMED file whose parsed to: is directed earns no hold notice (re-arm D-2, the codex-F2 mislabel class)', () => {
|
|
597
|
+
// The filename token alone selected the flag pre-fold; parsed truth rules
|
|
598
|
+
// now — a mislabeled directed dispatch must not render a "held broadcast"
|
|
599
|
+
// claim above a verdict that shows nothing held.
|
|
600
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
601
|
+
{
|
|
602
|
+
name: '2026-08-11T0940Z-broadcast-mislabeled.md',
|
|
603
|
+
to: 'strategy-claude',
|
|
604
|
+
subject: 'directed despite the name',
|
|
605
|
+
},
|
|
606
|
+
]);
|
|
607
|
+
writeLifecycleMarker('totem', 'totem-claude', 'suspended');
|
|
608
|
+
writeLifecycleMarker('totem', 'totem-gemini', 'suspended');
|
|
609
|
+
const result = poll({ env: TWO_SEATS });
|
|
610
|
+
// Addressed to a non-self seat: no mail surfaces, and no hold exists.
|
|
611
|
+
expect(result.mail).toEqual([]);
|
|
612
|
+
expect(result.notices.filter((n) => n.includes('broadcast dispatches'))).toEqual([]);
|
|
613
|
+
});
|
|
614
|
+
it('a marker declaring `active` behaves exactly like no marker (absent ⇒ active)', () => {
|
|
615
|
+
writeBroadcast();
|
|
616
|
+
writeLifecycleMarker('totem', 'totem-gemini', 'active');
|
|
617
|
+
writeBroadcastProcessed('totem', 'totem-claude', [BROADCAST_FILE]);
|
|
618
|
+
const result = poll({ env: TWO_SEATS });
|
|
619
|
+
// Both seats are in the denominator, so one mark does not close it.
|
|
620
|
+
expect(result.mail.map((m) => m.file)).toEqual([BROADCAST_FILE]);
|
|
621
|
+
expect(result.warnings).toEqual([]);
|
|
622
|
+
expect(result.notices).toEqual([]);
|
|
623
|
+
});
|
|
624
|
+
it('markerless tree: the FULL poll result is identical to the pre-#2511 snapshot', () => {
|
|
625
|
+
// The snapshot below was captured by running this exact fixture through
|
|
626
|
+
// HEAD's pre-#2511 `pollMail` and the post-change one side by side and
|
|
627
|
+
// deep-equalling the two results (directed + broadcast + marks + a
|
|
628
|
+
// half-marked broadcast + a roster-invalid address + a cross-sender
|
|
629
|
+
// collision, over a two-seat poll). Pinning the WHOLE object — not a
|
|
630
|
+
// spot-check — is what makes "markerless trees behave bit-identically"
|
|
631
|
+
// (#2511 data-model invariant) a test rather than a claim.
|
|
632
|
+
const DATE = '2026-08-01T1700Z';
|
|
633
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
634
|
+
{
|
|
635
|
+
name: '2026-08-01T1700Z-totem-claude-directed.md',
|
|
636
|
+
to: 'totem-claude',
|
|
637
|
+
subject: 'one',
|
|
638
|
+
date: DATE,
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
name: '2026-08-01T1701Z-totem-claude-consumed.md',
|
|
642
|
+
to: 'totem-claude',
|
|
643
|
+
subject: 'done',
|
|
644
|
+
date: DATE,
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: '2026-08-01T1702Z-broadcast-cohort.md',
|
|
648
|
+
to: 'broadcast',
|
|
649
|
+
subject: 'bcast',
|
|
650
|
+
date: DATE,
|
|
651
|
+
},
|
|
652
|
+
{ name: '2026-08-01T1703Z-broadcast-half.md', to: 'broadcast', subject: 'half', date: DATE },
|
|
653
|
+
{
|
|
654
|
+
name: '2026-08-01T1704Z-cohort-bad-address.md',
|
|
655
|
+
to: 'cohort',
|
|
656
|
+
subject: 'nobody',
|
|
657
|
+
date: DATE,
|
|
658
|
+
},
|
|
659
|
+
{ name: '2026-08-01T1705Z-collide.md', to: 'totem-gemini', subject: 'collide-a', date: DATE },
|
|
660
|
+
]);
|
|
661
|
+
writeOutbox('liquid-city', 'lc-claude', [
|
|
662
|
+
{ name: '2026-08-01T1705Z-collide.md', to: 'totem-gemini', subject: 'collide-b', date: DATE },
|
|
663
|
+
{
|
|
664
|
+
name: '2026-08-01T1706Z-totem-gemini-directed.md',
|
|
665
|
+
to: 'totem-gemini',
|
|
666
|
+
subject: 'two',
|
|
667
|
+
date: DATE,
|
|
668
|
+
},
|
|
669
|
+
]);
|
|
670
|
+
writeProcessed('totem', 'totem-claude', [
|
|
671
|
+
'2026-08-01T1701Z-totem-claude-consumed.md',
|
|
672
|
+
'2026-08-01T1703Z-broadcast-half.md',
|
|
673
|
+
]);
|
|
674
|
+
writeProcessed('totem', 'totem-gemini', ['2026-08-01T1702Z-broadcast-cohort.md']);
|
|
675
|
+
writeBroadcastProcessed('totem', 'totem-claude', ['2026-08-01T1702Z-broadcast-cohort.md']);
|
|
676
|
+
const entry = (repo, sender, file, to, subject) => ({
|
|
677
|
+
file,
|
|
678
|
+
repo,
|
|
679
|
+
from: sender,
|
|
680
|
+
to,
|
|
681
|
+
date: DATE,
|
|
682
|
+
subject,
|
|
683
|
+
filePath: path.join(workspace, repo, '.totem', 'orchestration', sender, 'outbox', file),
|
|
684
|
+
});
|
|
685
|
+
expect(poll({ env: TWO_SEATS })).toEqual({
|
|
686
|
+
selfAgents: { agents: ['totem-claude', 'totem-gemini'], source: 'env' },
|
|
687
|
+
mail: [
|
|
688
|
+
entry('liquid-city', 'lc-claude', '2026-08-01T1706Z-totem-gemini-directed.md', 'totem-gemini', 'two'),
|
|
689
|
+
entry('totem-strategy', 'strategy-claude', '2026-08-01T1703Z-broadcast-half.md', 'broadcast', 'half'),
|
|
690
|
+
entry('totem-strategy', 'strategy-claude', '2026-08-01T1700Z-totem-claude-directed.md', 'totem-claude', 'one'),
|
|
691
|
+
entry('liquid-city', 'lc-claude', '2026-08-01T1705Z-collide.md', 'totem-gemini', 'collide-b'),
|
|
692
|
+
entry('totem-strategy', 'strategy-claude', '2026-08-01T1705Z-collide.md', 'totem-gemini', 'collide-a'),
|
|
693
|
+
],
|
|
694
|
+
scanned: 6,
|
|
695
|
+
truncated: false,
|
|
696
|
+
workspace,
|
|
697
|
+
warnings: [
|
|
698
|
+
'unresolvable outbox address: totem-strategy/strategy-claude/2026-08-01T1704Z-cohort-bad-address.md — to: "cohort" matches no roster agent; invisible to every seat-scoped poll',
|
|
699
|
+
'cross-sender basename collision: 2026-08-01T1705Z-collide.md from liquid-city/lc-claude and totem-strategy/strategy-claude — a single processed/ mark would shadow both',
|
|
700
|
+
],
|
|
701
|
+
// Additive field (fold): a markerless tree emits zero notices — the
|
|
702
|
+
// channel's existence is the only delta vs the pre-#2511 result shape.
|
|
703
|
+
notices: [],
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
});
|
|
427
707
|
// ─── Cross-sender basename-collision sensor ─────────────
|
|
428
708
|
describe('pollMail — cross-sender basename-collision sensor (mmnto-ai/totem#2311)', () => {
|
|
429
709
|
const NAME = '2026-07-06T1717Z-totem-claude-blind-reply.md';
|
|
@@ -1291,7 +1571,15 @@ describe('mailCommand — --json output', () => {
|
|
|
1291
1571
|
// ─── Exit contract + NOT-DERIVED verdict (mmnto-ai/totem#2312) ─────────────
|
|
1292
1572
|
describe('resolveMailExitCode (unit)', () => {
|
|
1293
1573
|
function result(selfAgents) {
|
|
1294
|
-
return {
|
|
1574
|
+
return {
|
|
1575
|
+
selfAgents,
|
|
1576
|
+
mail: [],
|
|
1577
|
+
scanned: 0,
|
|
1578
|
+
truncated: false,
|
|
1579
|
+
workspace: '/w',
|
|
1580
|
+
warnings: [],
|
|
1581
|
+
notices: [],
|
|
1582
|
+
};
|
|
1295
1583
|
}
|
|
1296
1584
|
it('is 2 when self is unresolved (source none), 0 when resolved', () => {
|
|
1297
1585
|
expect(resolveMailExitCode(result({ agents: [], source: 'none' }))).toBe(2);
|