@mmnto/cli 1.115.0 → 1.117.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 +18 -2
- package/dist/commands/doctor-seat-identity.d.ts.map +1 -1
- package/dist/commands/doctor-seat-identity.js +77 -2
- package/dist/commands/doctor-seat-identity.js.map +1 -1
- package/dist/commands/doctor-seat-identity.test.js +180 -20
- package/dist/commands/doctor-seat-identity.test.js.map +1 -1
- package/dist/commands/gemini-sessionstart-contract.test.js +15 -0
- package/dist/commands/gemini-sessionstart-contract.test.js.map +1 -1
- 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 +12 -1
- package/dist/commands/init-templates.js.map +1 -1
- package/dist/commands/mail-cli-wiring.test.d.ts +8 -5
- package/dist/commands/mail-cli-wiring.test.d.ts.map +1 -1
- package/dist/commands/mail-cli-wiring.test.js +95 -42
- package/dist/commands/mail-cli-wiring.test.js.map +1 -1
- package/dist/commands/mail-degraded-e4.test.js +46 -13
- package/dist/commands/mail-degraded-e4.test.js.map +1 -1
- package/dist/commands/mail.d.ts +29 -0
- package/dist/commands/mail.d.ts.map +1 -1
- package/dist/commands/mail.js +182 -14
- package/dist/commands/mail.js.map +1 -1
- package/dist/commands/mail.test.js +425 -58
- package/dist/commands/mail.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/sessionstart-manifest-contract.test.js +10 -0
- package/dist/commands/sessionstart-manifest-contract.test.js.map +1 -1
- 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/index.js +15 -2
- 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
|
@@ -128,6 +128,17 @@ afterEach(() => {
|
|
|
128
128
|
function poll(opts = {}) {
|
|
129
129
|
return pollMail({ repoRoot: selfRepoRoot(), workspace, env: {}, ...opts });
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Explicit single-seat identity for the pipeline tests (mmnto-ai/totem#2204).
|
|
133
|
+
* The `totem` fixture basename resolves TWO seats via the cohort map, so an
|
|
134
|
+
* identity-less poll is identity-AMBIGUOUS: directed dispatches are withheld
|
|
135
|
+
* as a count and the directed verdict is NOT DERIVED. Tests whose subject is
|
|
136
|
+
* the single-recipient pipeline (parse / sort / truncation / workspace
|
|
137
|
+
* derivation / round-trip) declare the recipient the way a real seat does —
|
|
138
|
+
* per-shell `TOTEM_SELF_AGENT` — so their directed fixture is served. Tests
|
|
139
|
+
* whose subject IS the multi-seat resolution pass `allSeats: true` instead.
|
|
140
|
+
*/
|
|
141
|
+
const SELF_CLAUDE = { TOTEM_SELF_AGENT: 'totem-claude' };
|
|
131
142
|
// ─── Basic filter behavior ──────────────────────────────
|
|
132
143
|
describe('pollMail — basic filter behavior', () => {
|
|
133
144
|
it('returns no mail when workspace is empty', () => {
|
|
@@ -141,7 +152,7 @@ describe('pollMail — basic filter behavior', () => {
|
|
|
141
152
|
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
142
153
|
{ name: '2026-05-18T1734Z-strategy-claude.md', to: 'totem-claude', subject: 'lane' },
|
|
143
154
|
]);
|
|
144
|
-
const result = poll();
|
|
155
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
145
156
|
expect(result.mail).toHaveLength(1);
|
|
146
157
|
expect(result.mail[0].to).toBe('totem-claude');
|
|
147
158
|
expect(result.mail[0].from).toBe('strategy-claude');
|
|
@@ -167,7 +178,7 @@ describe('pollMail — basic filter behavior', () => {
|
|
|
167
178
|
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
168
179
|
{ name: '2026-05-18T1734Z.md', to: 'Totem-Claude', subject: 'case test' },
|
|
169
180
|
]);
|
|
170
|
-
const result = poll();
|
|
181
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
171
182
|
expect(result.mail).toHaveLength(1);
|
|
172
183
|
expect(result.mail[0].to).toBe('Totem-Claude');
|
|
173
184
|
});
|
|
@@ -178,7 +189,7 @@ describe('pollMail — basic filter behavior', () => {
|
|
|
178
189
|
writeOutbox('liquid-city', 'lc-claude', [
|
|
179
190
|
{ name: '2026-05-18T1800Z.md', to: 'totem-claude', subject: 'lc heads-up' },
|
|
180
191
|
]);
|
|
181
|
-
const result = poll();
|
|
192
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
182
193
|
expect(result.mail).toHaveLength(2);
|
|
183
194
|
const repos = result.mail.map((m) => m.repo).sort();
|
|
184
195
|
expect(repos).toEqual(['liquid-city', 'totem-strategy']);
|
|
@@ -224,7 +235,7 @@ describe('pollMail — own-broadcast exclusion (mmnto-ai/totem#2364)', () => {
|
|
|
224
235
|
writeOutbox('totem', 'totem-claude', [
|
|
225
236
|
{ name: '2026-07-06T2332Z-totem-claude-note-to-self.md', to: 'totem-claude' },
|
|
226
237
|
]);
|
|
227
|
-
const result = poll();
|
|
238
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
228
239
|
expect(result.mail).toHaveLength(1);
|
|
229
240
|
});
|
|
230
241
|
it('includeProcessed keeps the own broadcast in the RAW addressed-inbound set (ADR-106 § A2.1)', () => {
|
|
@@ -287,7 +298,7 @@ describe('pollMail — symlinked agent/outbox dirs are not traversed (mmnto-ai/t
|
|
|
287
298
|
writeRogueMail(outside, '2026-07-14T0003Z.md');
|
|
288
299
|
const orchDir = path.join(workspace, 'totem-strategy', '.totem', 'orchestration');
|
|
289
300
|
symlinkDir(path.join(tmpRoot, 'outside2', 'rogue-agent'), path.join(orchDir, 'rogue-agent'));
|
|
290
|
-
const result = poll();
|
|
301
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
291
302
|
expect(result.mail).toHaveLength(1);
|
|
292
303
|
expect(result.mail[0].subject).toBe('real mail');
|
|
293
304
|
});
|
|
@@ -300,7 +311,7 @@ describe('pollMail — processed/ exclusion', () => {
|
|
|
300
311
|
{ name: '2026-05-18T1918Z.md', to: 'totem-claude', subject: 'still unread' },
|
|
301
312
|
]);
|
|
302
313
|
writeProcessed('totem', 'totem-claude', ['2026-05-18T1734Z.md']);
|
|
303
|
-
const result = poll();
|
|
314
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
304
315
|
expect(result.mail).toHaveLength(1);
|
|
305
316
|
expect(result.mail[0].file).toBe('2026-05-18T1918Z.md');
|
|
306
317
|
});
|
|
@@ -318,8 +329,12 @@ describe('pollMail — processed/ exclusion', () => {
|
|
|
318
329
|
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
319
330
|
{ name: '2026-05-18T1700Z.md', to: 'totem-gemini', subject: 'already done' },
|
|
320
331
|
]);
|
|
332
|
+
// Multi-seat union semantics ARE the subject here, so the union is served
|
|
333
|
+
// by name (mmnto-ai/totem#2204) — under the identity gate a directed
|
|
334
|
+
// dispatch to totem-gemini is withheld regardless of its mark, and the
|
|
335
|
+
// assertion would pass without the subtraction ever running.
|
|
321
336
|
writeProcessed('totem', 'totem-gemini', ['2026-05-18T1700Z.md']);
|
|
322
|
-
const result = poll();
|
|
337
|
+
const result = poll({ allSeats: true });
|
|
323
338
|
expect(result.mail).toEqual([]);
|
|
324
339
|
});
|
|
325
340
|
it('includeProcessed returns the RAW addressed-inbound set (does NOT subtract processed) — ADR-106 § A2.1', () => {
|
|
@@ -331,11 +346,11 @@ describe('pollMail — processed/ exclusion', () => {
|
|
|
331
346
|
]);
|
|
332
347
|
writeProcessed('totem', 'totem-claude', ['2026-05-18T1734Z.md']);
|
|
333
348
|
// Default (reader) view subtracts the mark → 1 unread.
|
|
334
|
-
expect(poll().mail).toHaveLength(1);
|
|
349
|
+
expect(poll({ env: SELF_CLAUDE }).mail).toHaveLength(1);
|
|
335
350
|
// Pre-dedupe view keeps BOTH → the raw addressed-inbound set. NON-VACUITY:
|
|
336
351
|
// feeding the default 1-item list back to compaction would delete the
|
|
337
352
|
// handled mark (the A2.1 false-unread bomb).
|
|
338
|
-
const raw = poll({ includeProcessed: true });
|
|
353
|
+
const raw = poll({ env: SELF_CLAUDE, includeProcessed: true });
|
|
339
354
|
expect(raw.mail.map((m) => m.file).sort()).toEqual([
|
|
340
355
|
'2026-05-18T1734Z.md',
|
|
341
356
|
'2026-05-18T1918Z.md',
|
|
@@ -710,7 +725,7 @@ describe('pollMail — cross-sender basename-collision sensor (mmnto-ai/totem#23
|
|
|
710
725
|
it('warns once when two distinct senders converge on one addressed-inbound basename', () => {
|
|
711
726
|
writeOutbox('totem-strategy', 'strategy-gemini', [{ name: NAME, to: 'totem-claude' }]);
|
|
712
727
|
writeOutbox('totem-strategy', 'strategy-agy', [{ name: NAME, to: 'totem-claude' }]);
|
|
713
|
-
const result = poll();
|
|
728
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
714
729
|
const collisionWarnings = result.warnings.filter((w) => w.startsWith('cross-sender basename collision'));
|
|
715
730
|
expect(collisionWarnings).toHaveLength(1);
|
|
716
731
|
expect(collisionWarnings[0]).toContain(NAME);
|
|
@@ -722,7 +737,7 @@ describe('pollMail — cross-sender basename-collision sensor (mmnto-ai/totem#23
|
|
|
722
737
|
it('fires on a broadcast + directed mix (both are addressed-inbound for this seat)', () => {
|
|
723
738
|
writeOutbox('totem-strategy', 'strategy-claude', [{ name: NAME, to: 'broadcast' }]);
|
|
724
739
|
writeOutbox('liquid-city', 'lc-claude', [{ name: NAME, to: 'totem-claude' }]);
|
|
725
|
-
const result = poll();
|
|
740
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
726
741
|
expect(result.warnings.filter((w) => w.startsWith('cross-sender basename collision'))).toHaveLength(1);
|
|
727
742
|
});
|
|
728
743
|
it('does NOT fire on same-sender copies across repos (broadcast fan-out reads)', () => {
|
|
@@ -730,7 +745,7 @@ describe('pollMail — cross-sender basename-collision sensor (mmnto-ai/totem#23
|
|
|
730
745
|
// shadowing all copies is correct handled-semantics, not a drop hazard.
|
|
731
746
|
writeOutbox('totem-strategy', 'strategy-claude', [{ name: NAME, to: 'broadcast' }]);
|
|
732
747
|
writeOutbox('liquid-city', 'strategy-claude', [{ name: NAME, to: 'broadcast' }]);
|
|
733
|
-
const result = poll();
|
|
748
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
734
749
|
expect(result.warnings).toEqual([]);
|
|
735
750
|
});
|
|
736
751
|
it('keys the sender on the outbox-owner seat, not the forgeable from: field', () => {
|
|
@@ -741,26 +756,26 @@ describe('pollMail — cross-sender basename-collision sensor (mmnto-ai/totem#23
|
|
|
741
756
|
{ name: NAME, to: 'totem-claude', from: 'someone-else' },
|
|
742
757
|
]);
|
|
743
758
|
writeOutbox('liquid-city', 'strategy-claude', [{ name: NAME, to: 'totem-claude' }]);
|
|
744
|
-
expect(poll().warnings).toEqual([]);
|
|
759
|
+
expect(poll({ env: SELF_CLAUDE }).warnings).toEqual([]);
|
|
745
760
|
writeOutbox('totem-status', 'status-claude', [
|
|
746
761
|
{ name: NAME, to: 'totem-claude', from: 'strategy-claude' },
|
|
747
762
|
]);
|
|
748
763
|
writeOutbox('skynet-sports', 'skynet-claude', [
|
|
749
764
|
{ name: NAME, to: 'totem-claude', from: 'strategy-claude' },
|
|
750
765
|
]);
|
|
751
|
-
expect(poll().warnings.filter((w) => w.startsWith('cross-sender basename collision'))).toHaveLength(1);
|
|
766
|
+
expect(poll({ env: SELF_CLAUDE }).warnings.filter((w) => w.startsWith('cross-sender basename collision'))).toHaveLength(1);
|
|
752
767
|
});
|
|
753
768
|
it('does NOT fire when neither same-basename dispatch is addressed to this seat', () => {
|
|
754
769
|
writeOutbox('totem-strategy', 'strategy-gemini', [{ name: NAME, to: 'lc-claude' }]);
|
|
755
770
|
writeOutbox('totem-strategy', 'strategy-agy', [{ name: NAME, to: 'lc-claude' }]);
|
|
756
|
-
const result = poll();
|
|
771
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
757
772
|
expect(result.warnings).toEqual([]);
|
|
758
773
|
});
|
|
759
774
|
it('emits ONE warning naming all sender paths when three seats collide', () => {
|
|
760
775
|
writeOutbox('totem-strategy', 'strategy-gemini', [{ name: NAME, to: 'totem-claude' }]);
|
|
761
776
|
writeOutbox('totem-strategy', 'strategy-agy', [{ name: NAME, to: 'totem-claude' }]);
|
|
762
777
|
writeOutbox('liquid-city', 'lc-claude', [{ name: NAME, to: 'totem-claude' }]);
|
|
763
|
-
const result = poll();
|
|
778
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
764
779
|
const collisionWarnings = result.warnings.filter((w) => w.startsWith('cross-sender basename collision'));
|
|
765
780
|
expect(collisionWarnings).toHaveLength(1);
|
|
766
781
|
expect(collisionWarnings[0]).toContain('totem-strategy/strategy-gemini');
|
|
@@ -776,8 +791,8 @@ describe('pollMail — cross-sender basename-collision sensor (mmnto-ai/totem#23
|
|
|
776
791
|
writeOutbox('totem-strategy', 'strategy-gemini', [{ name: NAME, to: 'totem-claude' }]);
|
|
777
792
|
writeOutbox('totem-strategy', 'strategy-agy', [{ name: NAME, to: 'totem-claude' }]);
|
|
778
793
|
writeProcessed('totem', 'totem-claude', [NAME]);
|
|
779
|
-
expect(poll().warnings).toEqual([]);
|
|
780
|
-
const raw = poll({ includeProcessed: true });
|
|
794
|
+
expect(poll({ env: SELF_CLAUDE }).warnings).toEqual([]);
|
|
795
|
+
const raw = poll({ env: SELF_CLAUDE, includeProcessed: true });
|
|
781
796
|
expect(raw.warnings.filter((w) => w.startsWith('cross-sender basename collision'))).toHaveLength(1);
|
|
782
797
|
});
|
|
783
798
|
});
|
|
@@ -849,7 +864,7 @@ describe('pollMail — outbox roster-validation sensor (mmnto-ai/totem#2335)', (
|
|
|
849
864
|
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
850
865
|
{ name: 'stray.md', to: 'unused', raw: 'to: cohort\njust a note\n' },
|
|
851
866
|
]);
|
|
852
|
-
expect(poll().warnings).toEqual([]);
|
|
867
|
+
expect(poll({ env: SELF_CLAUDE }).warnings).toEqual([]);
|
|
853
868
|
});
|
|
854
869
|
it('warns once per unresolvable dispatch (per-file, not basename-deduped)', () => {
|
|
855
870
|
// Distinct from the #2311 basename sensor (which dedupes by basename): each
|
|
@@ -869,7 +884,7 @@ describe('pollMail — sort + metadata', () => {
|
|
|
869
884
|
{ name: 'b.md', to: 'totem-claude', date: '2026-05-18T2000Z', subject: 'newer' },
|
|
870
885
|
{ name: 'c.md', to: 'totem-claude', date: '2026-05-18T1500Z', subject: 'middle' },
|
|
871
886
|
]);
|
|
872
|
-
const result = poll();
|
|
887
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
873
888
|
expect(result.mail.map((m) => m.subject)).toEqual(['newer', 'middle', 'older']);
|
|
874
889
|
});
|
|
875
890
|
it('falls back to filename sort when date is absent', () => {
|
|
@@ -877,7 +892,7 @@ describe('pollMail — sort + metadata', () => {
|
|
|
877
892
|
{ name: '2026-05-18T1000Z.md', to: 'totem-claude', raw: '---\nto: totem-claude\n---\n' },
|
|
878
893
|
{ name: '2026-05-18T2000Z.md', to: 'totem-claude', raw: '---\nto: totem-claude\n---\n' },
|
|
879
894
|
]);
|
|
880
|
-
const result = poll();
|
|
895
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
881
896
|
expect(result.mail[0].file).toBe('2026-05-18T2000Z.md');
|
|
882
897
|
expect(result.mail[1].file).toBe('2026-05-18T1000Z.md');
|
|
883
898
|
});
|
|
@@ -885,7 +900,7 @@ describe('pollMail — sort + metadata', () => {
|
|
|
885
900
|
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
886
901
|
{ name: 'x.md', to: 'Totem-Claude', subject: 'mixed case to' },
|
|
887
902
|
]);
|
|
888
|
-
const result = poll();
|
|
903
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
889
904
|
expect(result.mail[0].to).toBe('Totem-Claude');
|
|
890
905
|
});
|
|
891
906
|
it('captures `from:` from frontmatter when present', () => {
|
|
@@ -897,7 +912,7 @@ describe('pollMail — sort + metadata', () => {
|
|
|
897
912
|
subject: 'overridden from field',
|
|
898
913
|
},
|
|
899
914
|
]);
|
|
900
|
-
const result = poll();
|
|
915
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
901
916
|
expect(result.mail[0].from).toBe('override-sender');
|
|
902
917
|
});
|
|
903
918
|
it('falls back to outbox-dir name when from: is absent', () => {
|
|
@@ -908,14 +923,14 @@ describe('pollMail — sort + metadata', () => {
|
|
|
908
923
|
raw: '---\nto: totem-claude\nsubject: no from field\n---\n',
|
|
909
924
|
},
|
|
910
925
|
]);
|
|
911
|
-
const result = poll();
|
|
926
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
912
927
|
expect(result.mail[0].from).toBe('strategy-claude');
|
|
913
928
|
});
|
|
914
929
|
it('reports `(no subject)` when subject is absent', () => {
|
|
915
930
|
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
916
931
|
{ name: 'x.md', to: 'totem-claude', raw: '---\nto: totem-claude\n---\n' },
|
|
917
932
|
]);
|
|
918
|
-
const result = poll();
|
|
933
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
919
934
|
expect(result.mail[0].subject).toBe('(no subject)');
|
|
920
935
|
});
|
|
921
936
|
});
|
|
@@ -952,14 +967,17 @@ describe('pollMail — frontmatter parsing', () => {
|
|
|
952
967
|
raw: '---\r\nfrom: strategy-claude\r\nto: totem-claude\r\nsubject: crlf\r\n---\r\n\r\nBody.\r\n',
|
|
953
968
|
},
|
|
954
969
|
]);
|
|
955
|
-
const result = poll();
|
|
970
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
956
971
|
expect(result.mail).toHaveLength(1);
|
|
957
972
|
expect(result.mail[0].subject).toBe('crlf');
|
|
958
973
|
});
|
|
959
974
|
it('non-.md files are ignored', () => {
|
|
960
975
|
const outbox = writeOutbox('totem-strategy', 'strategy-claude', []);
|
|
961
976
|
fs.writeFileSync(path.join(outbox, 'note.txt'), '---\nto: totem-claude\n---\nshould be skipped', 'utf-8');
|
|
962
|
-
|
|
977
|
+
// Identity declared (mmnto-ai/totem#2204): the stray is addressed to a
|
|
978
|
+
// union seat, so under an identity-ambiguous poll the empty result would
|
|
979
|
+
// hold even if the extension filter regressed.
|
|
980
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
963
981
|
expect(result.mail).toEqual([]);
|
|
964
982
|
});
|
|
965
983
|
});
|
|
@@ -1014,7 +1032,7 @@ describe('pollMail — workspace', () => {
|
|
|
1014
1032
|
recursive: true,
|
|
1015
1033
|
});
|
|
1016
1034
|
fs.writeFileSync(path.join(alt, 'totem-strategy', '.totem', 'orchestration', 'strategy-claude', 'outbox', 'x.md'), '---\nto: totem-claude\nsubject: alt-workspace\n---\n', 'utf-8');
|
|
1017
|
-
const result = poll({ workspace: alt });
|
|
1035
|
+
const result = poll({ workspace: alt, env: SELF_CLAUDE });
|
|
1018
1036
|
expect(result.mail).toHaveLength(1);
|
|
1019
1037
|
expect(result.workspace).toBe(path.resolve(alt));
|
|
1020
1038
|
});
|
|
@@ -1024,7 +1042,10 @@ describe('pollMail — workspace', () => {
|
|
|
1024
1042
|
recursive: true,
|
|
1025
1043
|
});
|
|
1026
1044
|
fs.writeFileSync(path.join(alt, 'totem-strategy', '.totem', 'orchestration', 'strategy-claude', 'outbox', 'x.md'), '---\nto: totem-claude\n---\n', 'utf-8');
|
|
1027
|
-
const result = poll({
|
|
1045
|
+
const result = poll({
|
|
1046
|
+
workspace: undefined,
|
|
1047
|
+
env: { TOTEM_WORKSPACE: alt, ...SELF_CLAUDE },
|
|
1048
|
+
});
|
|
1028
1049
|
expect(result.workspace).toBe(path.resolve(alt));
|
|
1029
1050
|
expect(result.mail).toHaveLength(1);
|
|
1030
1051
|
});
|
|
@@ -1040,9 +1061,9 @@ describe('pollMail — workspace', () => {
|
|
|
1040
1061
|
const wrapper = mkDir(path.join(workspace, 'wrapper-dir'));
|
|
1041
1062
|
fs.mkdirSync(path.join(wrapper, 'nested-strategy', '.totem', 'orchestration', 'strategy-claude', 'outbox'), { recursive: true });
|
|
1042
1063
|
fs.writeFileSync(path.join(wrapper, 'nested-strategy', '.totem', 'orchestration', 'strategy-claude', 'outbox', 'x.md'), '---\nto: totem-claude\nsubject: nested\n---\n', 'utf-8');
|
|
1043
|
-
const flat = poll({ recursive: false });
|
|
1064
|
+
const flat = poll({ recursive: false, env: SELF_CLAUDE });
|
|
1044
1065
|
expect(flat.mail).toEqual([]);
|
|
1045
|
-
const recursive = poll({ recursive: true });
|
|
1066
|
+
const recursive = poll({ recursive: true, env: SELF_CLAUDE });
|
|
1046
1067
|
expect(recursive.mail).toHaveLength(1);
|
|
1047
1068
|
expect(recursive.mail[0].subject).toBe('nested');
|
|
1048
1069
|
// Label uses the immediate parent dir (where `.totem/orchestration/`
|
|
@@ -1056,7 +1077,10 @@ describe('pollMail — workspace', () => {
|
|
|
1056
1077
|
recursive: true,
|
|
1057
1078
|
});
|
|
1058
1079
|
fs.writeFileSync(path.join(node, '.totem', 'orchestration', 'strategy-claude', 'outbox', 'x.md'), '---\nto: totem-claude\nsubject: spam\n---\n', 'utf-8');
|
|
1059
|
-
|
|
1080
|
+
// Identity declared (mmnto-ai/totem#2204): the planted dispatch is
|
|
1081
|
+
// addressed to a union seat, so an identity-ambiguous poll would withhold
|
|
1082
|
+
// it and the empty result would hold even if the skip-list regressed.
|
|
1083
|
+
const result = poll({ recursive: true, env: SELF_CLAUDE });
|
|
1060
1084
|
expect(result.mail).toEqual([]);
|
|
1061
1085
|
});
|
|
1062
1086
|
});
|
|
@@ -1077,7 +1101,7 @@ describe('pollMail — subdirectory workspace derivation (mmnto-ai/totem#2312)',
|
|
|
1077
1101
|
]);
|
|
1078
1102
|
// No `workspace` override — derivation must find `<workspace>` (parent of
|
|
1079
1103
|
// the resolved root `<workspace>/totem`).
|
|
1080
|
-
const result = pollMail({ repoRoot: subdir, env:
|
|
1104
|
+
const result = pollMail({ repoRoot: subdir, env: SELF_CLAUDE });
|
|
1081
1105
|
expect(result.workspace).toBe(path.resolve(workspace));
|
|
1082
1106
|
expect(result.mail).toHaveLength(1);
|
|
1083
1107
|
expect(result.mail[0].to).toBe('totem-claude');
|
|
@@ -1094,7 +1118,9 @@ describe('pollMail — subdirectory workspace derivation (mmnto-ai/totem#2312)',
|
|
|
1094
1118
|
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
1095
1119
|
{ name: '2026-05-18T1800Z-totem-claude.md', to: 'totem-claude', subject: 'git-marked' },
|
|
1096
1120
|
]);
|
|
1097
|
-
|
|
1121
|
+
// `allSeats` (mmnto-ai/totem#2204), not an env identity: the map-derived
|
|
1122
|
+
// resolution IS what this test asserts, so the union must stay served.
|
|
1123
|
+
const result = pollMail({ repoRoot: subdir, env: {}, allSeats: true });
|
|
1098
1124
|
expect(result.workspace).toBe(path.resolve(workspace));
|
|
1099
1125
|
expect(result.selfAgents.agents).toContain('totem-claude');
|
|
1100
1126
|
expect(result.mail).toHaveLength(1);
|
|
@@ -1140,7 +1166,7 @@ describe('pollMail — MAX_SCAN truncation', () => {
|
|
|
1140
1166
|
files.push({ name: `${num}.md`, to: 'totem-claude', subject: `n${i}` });
|
|
1141
1167
|
}
|
|
1142
1168
|
writeOutbox('totem-strategy', 'strategy-claude', files);
|
|
1143
|
-
const result = poll({ maxScan: 500 });
|
|
1169
|
+
const result = poll({ maxScan: 500, env: SELF_CLAUDE });
|
|
1144
1170
|
expect(result.truncated).toBe(true);
|
|
1145
1171
|
// Contract: scanned never exceeds the cap. Documents the pre-increment
|
|
1146
1172
|
// off-by-one fix from CR R1 (#1971).
|
|
@@ -1187,7 +1213,7 @@ describe('pollMail — MAX_SCAN truncation', () => {
|
|
|
1187
1213
|
});
|
|
1188
1214
|
}
|
|
1189
1215
|
writeOutbox('zebra-repo', 'zebra-sender', newFiles);
|
|
1190
|
-
const result = poll({ maxScan: 500 });
|
|
1216
|
+
const result = poll({ maxScan: 500, env: SELF_CLAUDE });
|
|
1191
1217
|
expect(result.truncated).toBe(true);
|
|
1192
1218
|
expect(result.scanned).toBe(500);
|
|
1193
1219
|
// All 5 fresh entries must be in the result. Pre-fix, they would have
|
|
@@ -1214,7 +1240,7 @@ describe('pollMail — self-token scan priority (mmnto-ai/totem#2144)', () => {
|
|
|
1214
1240
|
...others,
|
|
1215
1241
|
{ name: '2020-01-01T0000Z-totem-claude-ancient.md', to: 'totem-claude', subject: 'ancient' },
|
|
1216
1242
|
]);
|
|
1217
|
-
const result = poll({ maxScan: 10 });
|
|
1243
|
+
const result = poll({ maxScan: 10, env: SELF_CLAUDE });
|
|
1218
1244
|
expect(result.truncated).toBe(true);
|
|
1219
1245
|
expect(result.mail.some((m) => m.subject === 'ancient')).toBe(true);
|
|
1220
1246
|
});
|
|
@@ -1237,7 +1263,7 @@ describe('pollMail — self-token scan priority (mmnto-ai/totem#2144)', () => {
|
|
|
1237
1263
|
// token says lc-claude, header says totem-claude.
|
|
1238
1264
|
{ name: '2026-12-31T2359Z-lc-claude-mislabeled.md', to: 'totem-claude', subject: 'rescue' },
|
|
1239
1265
|
]);
|
|
1240
|
-
const result = poll({ maxScan: 5 });
|
|
1266
|
+
const result = poll({ maxScan: 5, env: SELF_CLAUDE });
|
|
1241
1267
|
expect(result.truncated).toBe(true);
|
|
1242
1268
|
expect(result.mail.some((m) => m.subject === 'rescue')).toBe(true);
|
|
1243
1269
|
});
|
|
@@ -1278,7 +1304,9 @@ describe('pollMail — self-token scan priority (mmnto-ai/totem#2144)', () => {
|
|
|
1278
1304
|
});
|
|
1279
1305
|
}
|
|
1280
1306
|
writeOutbox('totem-strategy', 'strategy-claude', selfFlood);
|
|
1281
|
-
|
|
1307
|
+
// Explicit identity: the NAMING behavior under test is the ungated arm —
|
|
1308
|
+
// the gated arm reports the directed tail as a count (#2204, leg F1).
|
|
1309
|
+
const result = poll({ env: { TOTEM_SELF_AGENT: 'totem-claude' }, maxScan: 3 });
|
|
1282
1310
|
expect(result.truncated).toBe(true);
|
|
1283
1311
|
const directed = result.warnings.filter((w) => w.includes('beyond the scan horizon'));
|
|
1284
1312
|
expect(directed).toHaveLength(1);
|
|
@@ -1296,7 +1324,7 @@ describe('pollMail — self-token scan priority (mmnto-ai/totem#2144)', () => {
|
|
|
1296
1324
|
});
|
|
1297
1325
|
}
|
|
1298
1326
|
writeOutbox('totem-strategy', 'strategy-claude', otherFlood);
|
|
1299
|
-
const generic = poll({ maxScan: 3 });
|
|
1327
|
+
const generic = poll({ env: { TOTEM_SELF_AGENT: 'totem-claude' }, maxScan: 3 });
|
|
1300
1328
|
expect(generic.truncated).toBe(true);
|
|
1301
1329
|
expect(generic.warnings.every((w) => !w.includes('beyond the scan horizon'))).toBe(true);
|
|
1302
1330
|
});
|
|
@@ -1372,7 +1400,7 @@ describe('pollMail — dir-derived seats (mmnto-ai/totem#2141)', () => {
|
|
|
1372
1400
|
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
1373
1401
|
{ name: '2026-06-01T0000Z-totem-codex-hello.md', to: 'totem-codex', subject: 'for codex' },
|
|
1374
1402
|
]);
|
|
1375
|
-
const result = poll();
|
|
1403
|
+
const result = poll({ allSeats: true });
|
|
1376
1404
|
expect(result.selfAgents.source).toBe('dirs+map');
|
|
1377
1405
|
expect(result.selfAgents.agents).toContain('totem-codex');
|
|
1378
1406
|
expect(result.mail.some((m) => m.subject === 'for codex')).toBe(true);
|
|
@@ -1398,7 +1426,7 @@ describe('pollMail — frontmatter forge defense', () => {
|
|
|
1398
1426
|
raw: 'to: totem-claude\nsubject: forged\nbody text\n',
|
|
1399
1427
|
},
|
|
1400
1428
|
]);
|
|
1401
|
-
const result = poll();
|
|
1429
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
1402
1430
|
expect(result.mail).toEqual([]);
|
|
1403
1431
|
// A stray .md is non-mail by contract; warning on it every poll would be
|
|
1404
1432
|
// permanent, unclearable noise (mmnto-ai/totem#2118 design note).
|
|
@@ -1431,7 +1459,7 @@ describe('pollMail — frontmatter forge defense', () => {
|
|
|
1431
1459
|
raw: '---\nfrom: strategy-claude\nto: totem-claude\nsubject: tight\n---\n',
|
|
1432
1460
|
},
|
|
1433
1461
|
]);
|
|
1434
|
-
const result = poll();
|
|
1462
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
1435
1463
|
expect(result.mail).toHaveLength(1);
|
|
1436
1464
|
expect(result.mail[0].subject).toBe('tight');
|
|
1437
1465
|
});
|
|
@@ -1448,7 +1476,7 @@ describe('pollMail — frontmatter-only dispatches (#2118)', () => {
|
|
|
1448
1476
|
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
1449
1477
|
{ name: '2026-06-07T2015Z-totem-claude.md', to: 'unused', raw },
|
|
1450
1478
|
]);
|
|
1451
|
-
const result = poll();
|
|
1479
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
1452
1480
|
expect(result.mail).toHaveLength(1);
|
|
1453
1481
|
expect(result.mail[0].to).toBe('totem-claude');
|
|
1454
1482
|
expect(result.mail[0].date).toBe('2026-06-07T2015Z');
|
|
@@ -1459,7 +1487,7 @@ describe('pollMail — frontmatter-only dispatches (#2118)', () => {
|
|
|
1459
1487
|
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
1460
1488
|
{ name: 'crlf-large.md', to: 'unused', raw },
|
|
1461
1489
|
]);
|
|
1462
|
-
const result = poll();
|
|
1490
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
1463
1491
|
expect(result.mail).toHaveLength(1);
|
|
1464
1492
|
expect(result.mail[0].to).toBe('totem-claude');
|
|
1465
1493
|
});
|
|
@@ -1484,7 +1512,7 @@ describe('pollMail — frontmatter-only dispatches (#2118)', () => {
|
|
|
1484
1512
|
raw: '---\nfrom: strategy-claude\n\nto: totem-claude\nsubject: post-blank\n---\n',
|
|
1485
1513
|
},
|
|
1486
1514
|
]);
|
|
1487
|
-
const result = poll();
|
|
1515
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
1488
1516
|
expect(result.mail).toHaveLength(1);
|
|
1489
1517
|
expect(result.mail[0].subject).toBe('post-blank');
|
|
1490
1518
|
});
|
|
@@ -1501,7 +1529,7 @@ describe('pollMail — frontmatter-only dispatches (#2118)', () => {
|
|
|
1501
1529
|
raw: '---\nfrom: strategy-claude\nto: totem-claude\nsubject: ws close\n--- \nBody.\n',
|
|
1502
1530
|
},
|
|
1503
1531
|
]);
|
|
1504
|
-
const result = poll();
|
|
1532
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
1505
1533
|
expect(result.mail).toHaveLength(1);
|
|
1506
1534
|
expect(result.mail[0].subject).toBe('ws close');
|
|
1507
1535
|
});
|
|
@@ -1535,7 +1563,7 @@ describe('pollMail — frontmatter-only dispatches (#2118)', () => {
|
|
|
1535
1563
|
raw: '---\nfrom: strategy-claude\nto: totem-claude\nsubject: with footer\n---\n\nFull content in subject above.\n',
|
|
1536
1564
|
},
|
|
1537
1565
|
]);
|
|
1538
|
-
const result = poll();
|
|
1566
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
1539
1567
|
expect(result.mail).toHaveLength(1);
|
|
1540
1568
|
expect(result.mail[0].subject).toBe('with footer');
|
|
1541
1569
|
});
|
|
@@ -1554,7 +1582,7 @@ describe('mailCommand — --json output', () => {
|
|
|
1554
1582
|
return true;
|
|
1555
1583
|
});
|
|
1556
1584
|
try {
|
|
1557
|
-
await mailCommand({ json: true, repoRoot: selfRepoRoot(), workspace, env:
|
|
1585
|
+
await mailCommand({ json: true, repoRoot: selfRepoRoot(), workspace, env: SELF_CLAUDE });
|
|
1558
1586
|
}
|
|
1559
1587
|
finally {
|
|
1560
1588
|
spy.mockRestore();
|
|
@@ -1641,7 +1669,7 @@ describe('mailCommand — exit contract + NOT-DERIVED verdict (mmnto-ai/totem#23
|
|
|
1641
1669
|
});
|
|
1642
1670
|
let exitCode;
|
|
1643
1671
|
try {
|
|
1644
|
-
({ exitCode } = await mailCommand({ repoRoot: selfRepoRoot(), workspace, env:
|
|
1672
|
+
({ exitCode } = await mailCommand({ repoRoot: selfRepoRoot(), workspace, env: SELF_CLAUDE }));
|
|
1645
1673
|
}
|
|
1646
1674
|
finally {
|
|
1647
1675
|
spy.mockRestore();
|
|
@@ -1690,7 +1718,7 @@ describe('pollMail — structured warnings on FS failures', () => {
|
|
|
1690
1718
|
const selfProcessed = path.join(selfRepoRoot(), '.totem', 'orchestration', 'totem-claude');
|
|
1691
1719
|
mkDir(selfProcessed);
|
|
1692
1720
|
fs.writeFileSync(path.join(selfProcessed, 'processed'), 'not a directory');
|
|
1693
|
-
const result = poll();
|
|
1721
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
1694
1722
|
// Mail still surfaces (degraded — no exclusion filter), warning recorded.
|
|
1695
1723
|
expect(result.mail).toHaveLength(1);
|
|
1696
1724
|
expect(result.warnings.some((w) => w.startsWith('processed/ scan failed'))).toBe(true);
|
|
@@ -1714,7 +1742,7 @@ describe('parseHeader — timestamp:/date: precedence (mmnto-ai/totem#2042)', ()
|
|
|
1714
1742
|
]),
|
|
1715
1743
|
},
|
|
1716
1744
|
]);
|
|
1717
|
-
const result = poll();
|
|
1745
|
+
const result = poll({ env: SELF_CLAUDE });
|
|
1718
1746
|
expect(result.mail).toHaveLength(1);
|
|
1719
1747
|
expect(result.mail[0].date).toBe('2026-06-09T17:34:37.127Z');
|
|
1720
1748
|
});
|
|
@@ -1732,13 +1760,13 @@ describe('parseHeader — timestamp:/date: precedence (mmnto-ai/totem#2042)', ()
|
|
|
1732
1760
|
]),
|
|
1733
1761
|
},
|
|
1734
1762
|
]);
|
|
1735
|
-
expect(poll().mail[0].date).toBe('2026-06-09T17:34:37.127Z');
|
|
1763
|
+
expect(poll({ env: SELF_CLAUDE }).mail[0].date).toBe('2026-06-09T17:34:37.127Z');
|
|
1736
1764
|
});
|
|
1737
1765
|
it('still falls back to legacy date: (backwards-compat read)', () => {
|
|
1738
1766
|
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
1739
1767
|
{ name: '2026-06-09T1734Z-legacy.md', to: 'totem-claude', date: '2026-05-18T1700Z' },
|
|
1740
1768
|
]);
|
|
1741
|
-
expect(poll().mail[0].date).toBe('2026-05-18T1700Z');
|
|
1769
|
+
expect(poll({ env: SELF_CLAUDE }).mail[0].date).toBe('2026-05-18T1700Z');
|
|
1742
1770
|
});
|
|
1743
1771
|
});
|
|
1744
1772
|
// ─── Outbound: send / reply (mmnto-ai/totem#2042) ───────
|
|
@@ -1766,7 +1794,14 @@ describe('mailSend — actuator (mmnto-ai/totem#2042)', () => {
|
|
|
1766
1794
|
expect(res.warnings).toEqual([]);
|
|
1767
1795
|
// The poller (sensor) surfaces exactly what the actuator emitted.
|
|
1768
1796
|
const recipientRepo = markedRepoRoot('totem-strategy');
|
|
1769
|
-
|
|
1797
|
+
// The recipient repo (`totem-strategy`) hosts TWO seats, so the poll
|
|
1798
|
+
// declares which one it reads as (mmnto-ai/totem#2204) — the round-trip
|
|
1799
|
+
// is about the wire shape, not about union resolution.
|
|
1800
|
+
const inbox = pollMail({
|
|
1801
|
+
repoRoot: recipientRepo,
|
|
1802
|
+
workspace,
|
|
1803
|
+
env: { TOTEM_SELF_AGENT: 'strategy-claude' },
|
|
1804
|
+
});
|
|
1770
1805
|
expect(inbox.mail).toHaveLength(1);
|
|
1771
1806
|
expect(inbox.mail[0].to).toBe('strategy-claude');
|
|
1772
1807
|
expect(inbox.mail[0].from).toBe('totem-claude');
|
|
@@ -2061,7 +2096,7 @@ describe('validateDispatchContent / composeDispatch / resolveSelfSender (units)'
|
|
|
2061
2096
|
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2062
2097
|
{ name: '2026-06-09T1734Z-roundtrip.md', to: 'totem-claude', raw: md },
|
|
2063
2098
|
]);
|
|
2064
|
-
const result = pollMail({ repoRoot: selfRepoRoot(), workspace, env:
|
|
2099
|
+
const result = pollMail({ repoRoot: selfRepoRoot(), workspace, env: SELF_CLAUDE });
|
|
2065
2100
|
expect(result.mail).toHaveLength(1);
|
|
2066
2101
|
expect(result.mail[0].subject).toBe(subject); // unquoted on read
|
|
2067
2102
|
});
|
|
@@ -2083,7 +2118,7 @@ describe('validateDispatchContent / composeDispatch / resolveSelfSender (units)'
|
|
|
2083
2118
|
},
|
|
2084
2119
|
]);
|
|
2085
2120
|
}
|
|
2086
|
-
const result = pollMail({ repoRoot: selfRepoRoot(), workspace, env:
|
|
2121
|
+
const result = pollMail({ repoRoot: selfRepoRoot(), workspace, env: SELF_CLAUDE });
|
|
2087
2122
|
expect(result.mail).toHaveLength(2);
|
|
2088
2123
|
const esc = String.fromCharCode(0x1b);
|
|
2089
2124
|
for (const entry of result.mail) {
|
|
@@ -2123,7 +2158,7 @@ describe('validateDispatchContent / composeDispatch / resolveSelfSender (units)'
|
|
|
2123
2158
|
].join('\n'),
|
|
2124
2159
|
},
|
|
2125
2160
|
]);
|
|
2126
|
-
const result = pollMail({ repoRoot: selfRepoRoot(), workspace, env:
|
|
2161
|
+
const result = pollMail({ repoRoot: selfRepoRoot(), workspace, env: SELF_CLAUDE });
|
|
2127
2162
|
expect(result.mail).toHaveLength(2);
|
|
2128
2163
|
for (const entry of result.mail) {
|
|
2129
2164
|
expect(entry.subject).not.toContain(esc);
|
|
@@ -2467,4 +2502,336 @@ describe('mailSend — the filename emitter is colon-free / ADS-safe (mmnto-ai/t
|
|
|
2467
2502
|
expect(path.basename(res.filePath)).not.toContain(':');
|
|
2468
2503
|
});
|
|
2469
2504
|
});
|
|
2505
|
+
// ─── Identity gate (mmnto-ai/totem#2204) ────────────────
|
|
2506
|
+
// The fixture repo basename `totem` resolves TWO seats via the cohort map
|
|
2507
|
+
// (totem-claude, totem-gemini), so an env-less poll here is exactly the
|
|
2508
|
+
// ambiguous shape the gate exists for — the 2026-08-13 BLIND-round
|
|
2509
|
+
// contamination class: the listing (subjects included) is the exposure
|
|
2510
|
+
// surface, so withheld mail is a COUNT, never per-item detail.
|
|
2511
|
+
describe('pollMail — identity gate (mmnto-ai/totem#2204)', () => {
|
|
2512
|
+
it('withholds union-directed mail as a count, serves broadcasts, warns, and exits 2', () => {
|
|
2513
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2514
|
+
{ name: '2026-08-14T0900Z-totem-claude-directed.md', to: 'totem-claude', subject: 'secret' },
|
|
2515
|
+
{ name: '2026-08-14T0901Z-broadcast-ruling.md', to: 'broadcast', subject: 'ruling' },
|
|
2516
|
+
]);
|
|
2517
|
+
const result = poll();
|
|
2518
|
+
expect(result.selfAgents.agents).toEqual(['totem-claude', 'totem-gemini']);
|
|
2519
|
+
expect(result.selfAgents.source).toBe('map');
|
|
2520
|
+
expect(result.seatGate).toEqual({ withheldDirected: 1 });
|
|
2521
|
+
expect(result.mail).toHaveLength(1);
|
|
2522
|
+
expect(result.mail[0].to).toBe('broadcast');
|
|
2523
|
+
// COUNT only — no per-item detail about withheld mail on ANY channel.
|
|
2524
|
+
const allText = [...result.warnings, ...result.notices].join('\n');
|
|
2525
|
+
expect(allText).toContain('identity-ambiguous poll');
|
|
2526
|
+
expect(allText).not.toContain('secret');
|
|
2527
|
+
expect(allText).not.toContain('directed.md');
|
|
2528
|
+
expect(resolveMailExitCode(result)).toBe(2);
|
|
2529
|
+
});
|
|
2530
|
+
it('fires at zero withheld — the ambiguity, not the volume, is the signal', () => {
|
|
2531
|
+
const result = poll();
|
|
2532
|
+
expect(result.seatGate).toEqual({ withheldDirected: 0 });
|
|
2533
|
+
expect(result.warnings.some((w) => w.includes('identity-ambiguous poll'))).toBe(true);
|
|
2534
|
+
expect(resolveMailExitCode(result)).toBe(2);
|
|
2535
|
+
});
|
|
2536
|
+
it('counts only would-have-served items: processed-subtracted mail is not withheld', () => {
|
|
2537
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2538
|
+
{ name: '2026-08-14T0900Z-totem-claude-done.md', to: 'totem-claude' },
|
|
2539
|
+
]);
|
|
2540
|
+
writeProcessed('totem', 'totem-claude', ['2026-08-14T0900Z-totem-claude-done.md']);
|
|
2541
|
+
const result = poll();
|
|
2542
|
+
expect(result.seatGate).toEqual({ withheldDirected: 0 });
|
|
2543
|
+
});
|
|
2544
|
+
it('does not count directed mail addressed outside the union', () => {
|
|
2545
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2546
|
+
{ name: '2026-08-14T0900Z-lc-claude-other.md', to: 'lc-claude' },
|
|
2547
|
+
]);
|
|
2548
|
+
const result = poll();
|
|
2549
|
+
expect(result.seatGate).toEqual({ withheldDirected: 0 });
|
|
2550
|
+
});
|
|
2551
|
+
it('explicit env identity is exempt: single-seat serve, no gate, exit 0', () => {
|
|
2552
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2553
|
+
{ name: '2026-08-14T0900Z-totem-claude-directed.md', to: 'totem-claude' },
|
|
2554
|
+
]);
|
|
2555
|
+
const result = poll({ env: { TOTEM_SELF_AGENT: 'totem-claude' } });
|
|
2556
|
+
expect(result.selfAgents.source).toBe('env');
|
|
2557
|
+
expect(result.seatGate).toBeUndefined();
|
|
2558
|
+
expect(result.mail).toHaveLength(1);
|
|
2559
|
+
expect(resolveMailExitCode(result)).toBe(0);
|
|
2560
|
+
});
|
|
2561
|
+
it('--all-seats serves the union by name: pre-gate mail set, exit 0, notice', () => {
|
|
2562
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2563
|
+
{ name: '2026-08-14T0900Z-totem-claude-directed.md', to: 'totem-claude' },
|
|
2564
|
+
{ name: '2026-08-14T0901Z-totem-gemini-directed.md', to: 'totem-gemini' },
|
|
2565
|
+
]);
|
|
2566
|
+
const result = poll({ allSeats: true });
|
|
2567
|
+
expect(result.seatGate).toBeUndefined();
|
|
2568
|
+
// Byte-identical union serve (design invariant 5): the exact pre-gate set,
|
|
2569
|
+
// not just its size.
|
|
2570
|
+
expect(result.mail.map((m) => m.file).sort()).toEqual([
|
|
2571
|
+
'2026-08-14T0900Z-totem-claude-directed.md',
|
|
2572
|
+
'2026-08-14T0901Z-totem-gemini-directed.md',
|
|
2573
|
+
]);
|
|
2574
|
+
expect(result.notices.some((n) => n.includes('union dashboard view'))).toBe(true);
|
|
2575
|
+
expect(result.warnings.some((w) => w.includes('identity-ambiguous'))).toBe(false);
|
|
2576
|
+
expect(resolveMailExitCode(result)).toBe(0);
|
|
2577
|
+
});
|
|
2578
|
+
it('single-seat config resolution is exempt from the gate (no consumer-repo regression)', () => {
|
|
2579
|
+
const root = markedRepoRoot('cfg-solo');
|
|
2580
|
+
const orch = mkDir(path.join(root, '.totem', 'orchestration'));
|
|
2581
|
+
fs.writeFileSync(path.join(orch, 'config.json'), JSON.stringify({ host_agents: ['solo-seat'] }), 'utf-8');
|
|
2582
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2583
|
+
{ name: '2026-08-14T0900Z-solo-seat-directed.md', to: 'solo-seat' },
|
|
2584
|
+
]);
|
|
2585
|
+
const result = pollMail({ repoRoot: root, workspace, env: {} });
|
|
2586
|
+
expect(result.selfAgents).toEqual({ agents: ['solo-seat'], source: 'config' });
|
|
2587
|
+
expect(result.seatGate).toBeUndefined();
|
|
2588
|
+
expect(result.mail).toHaveLength(1);
|
|
2589
|
+
expect(resolveMailExitCode(result)).toBe(0);
|
|
2590
|
+
});
|
|
2591
|
+
it('gated truncation reports the union-directed tail as a count, never by name (leg F1)', () => {
|
|
2592
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2593
|
+
{ name: '2026-08-14T0900Z-totem-claude-secret-topic.md', to: 'totem-claude' },
|
|
2594
|
+
{ name: '2026-08-14T0902Z-broadcast-bulletin.md', to: 'broadcast' },
|
|
2595
|
+
]);
|
|
2596
|
+
// maxScan 1: the newer broadcast is scanned, the directed file falls
|
|
2597
|
+
// beyond the horizon — an ECL basename is recipient + compressed subject,
|
|
2598
|
+
// so under the gate the dropped tail must not be named.
|
|
2599
|
+
const result = poll({ maxScan: 1 });
|
|
2600
|
+
expect(result.truncated).toBe(true);
|
|
2601
|
+
const joined = [...result.warnings, ...result.notices].join('\n');
|
|
2602
|
+
expect(joined).toContain('withheld from naming');
|
|
2603
|
+
expect(joined).not.toContain('secret-topic');
|
|
2604
|
+
});
|
|
2605
|
+
it('ungated truncation still names the directed tail (leg F1 control)', () => {
|
|
2606
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2607
|
+
{ name: '2026-08-14T0900Z-totem-claude-secret-topic.md', to: 'totem-claude' },
|
|
2608
|
+
{ name: '2026-08-14T0902Z-broadcast-bulletin.md', to: 'broadcast' },
|
|
2609
|
+
]);
|
|
2610
|
+
const result = poll({ env: { TOTEM_SELF_AGENT: 'totem-claude' }, maxScan: 1 });
|
|
2611
|
+
expect(result.truncated).toBe(true);
|
|
2612
|
+
expect(result.warnings.join('\n')).toContain('secret-topic');
|
|
2613
|
+
});
|
|
2614
|
+
it('gated arm keys INCOMPLETE on non-gate warnings — empty broadcast view (leg F2)', () => {
|
|
2615
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2616
|
+
{
|
|
2617
|
+
name: '2026-08-14T0900Z-totem-claude-broken.md',
|
|
2618
|
+
to: 'totem-claude',
|
|
2619
|
+
raw: '---\nfrom: strategy-claude\n---\n',
|
|
2620
|
+
},
|
|
2621
|
+
]);
|
|
2622
|
+
const text = formatTextResult(poll());
|
|
2623
|
+
expect(text).toContain('Directed inbox NOT DERIVED');
|
|
2624
|
+
expect(text).toContain('Scan INCOMPLETE');
|
|
2625
|
+
});
|
|
2626
|
+
it('gated poll keeps lifecycle sensing with withheld wording — no filename leak (re-arm 1/2a)', () => {
|
|
2627
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2628
|
+
{ name: '2026-08-14T0900Z-totem-gemini-secret-slug.md', to: 'totem-gemini' },
|
|
2629
|
+
]);
|
|
2630
|
+
writeLifecycleMarker('totem', 'totem-gemini', 'suspended');
|
|
2631
|
+
const result = poll();
|
|
2632
|
+
expect(result.seatGate).toEqual({ withheldDirected: 1 });
|
|
2633
|
+
expect(result.mail).toHaveLength(0);
|
|
2634
|
+
const notice = result.notices.find((n) => n.includes('suspended seat'));
|
|
2635
|
+
expect(notice).toBeDefined();
|
|
2636
|
+
// The notice must not assert a surfacing the same render withholds
|
|
2637
|
+
// (the mislabeled-claim class the D-2 test forbids for broadcasts).
|
|
2638
|
+
expect(notice).toContain('WITHHELD');
|
|
2639
|
+
expect(notice).not.toContain('surfacing');
|
|
2640
|
+
expect([...result.warnings, ...result.notices].join('\n')).not.toContain('secret');
|
|
2641
|
+
});
|
|
2642
|
+
it('gated poll suppresses the collision sensor for withheld basenames; --all-seats control fires it (re-arm 2b)', () => {
|
|
2643
|
+
const N = '2026-08-14T0900Z-totem-claude-converged.md';
|
|
2644
|
+
writeOutbox('totem-strategy', 'strategy-claude', [{ name: N, to: 'totem-claude' }]);
|
|
2645
|
+
writeOutbox('liquid-city', 'lc-claude', [{ name: N, to: 'totem-claude' }]);
|
|
2646
|
+
const gated = poll();
|
|
2647
|
+
expect(gated.seatGate).toEqual({ withheldDirected: 2 });
|
|
2648
|
+
expect(gated.warnings.join('\n')).not.toContain('cross-sender basename collision');
|
|
2649
|
+
// Control: the identical fixture DOES fire the sensor when the union view
|
|
2650
|
+
// is explicitly requested — proving the gated suppression is what the
|
|
2651
|
+
// first assertion measured, not a fixture that never collides.
|
|
2652
|
+
const control = poll({ allSeats: true });
|
|
2653
|
+
expect(control.warnings.join('\n')).toContain('cross-sender basename collision');
|
|
2654
|
+
});
|
|
2655
|
+
it('a broadcast-NAMED directed dispatch beyond the horizon is not named under the gate (re-arm 3)', () => {
|
|
2656
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2657
|
+
{ name: '2026-08-14T0900Z-broadcast-actually-directed-secret.md', to: 'totem-claude' },
|
|
2658
|
+
{ name: '2026-08-14T0902Z-broadcast-real.md', to: 'broadcast' },
|
|
2659
|
+
]);
|
|
2660
|
+
// The filename token is untrusted for delivery truth (codex F2): under the
|
|
2661
|
+
// gate an UNOPENED tail is never named, broadcast-token or not.
|
|
2662
|
+
const result = poll({ maxScan: 1 });
|
|
2663
|
+
expect(result.truncated).toBe(true);
|
|
2664
|
+
const joined = result.warnings.join('\n');
|
|
2665
|
+
expect(joined).toContain('withheld from naming');
|
|
2666
|
+
expect(joined).not.toContain('actually-directed-secret');
|
|
2667
|
+
});
|
|
2668
|
+
it('gated arm keys INCOMPLETE on non-gate warnings — broadcast list qualified (leg F2)', () => {
|
|
2669
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2670
|
+
{
|
|
2671
|
+
name: '2026-08-14T0900Z-totem-claude-broken.md',
|
|
2672
|
+
to: 'totem-claude',
|
|
2673
|
+
raw: '---\nfrom: strategy-claude\n---\n',
|
|
2674
|
+
},
|
|
2675
|
+
{ name: '2026-08-14T0901Z-broadcast-ok.md', to: 'broadcast' },
|
|
2676
|
+
]);
|
|
2677
|
+
const text = formatTextResult(poll());
|
|
2678
|
+
expect(text).toContain('1 unread broadcast dispatch(es) — scan INCOMPLETE');
|
|
2679
|
+
});
|
|
2680
|
+
it('gate leaves the broadcast per-seat floor untouched (listing ≠ accounting)', () => {
|
|
2681
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2682
|
+
{ name: '2026-08-14T0900Z-broadcast-ruling.md', to: 'broadcast' },
|
|
2683
|
+
]);
|
|
2684
|
+
writeBroadcastProcessed('totem', 'totem-claude', ['2026-08-14T0900Z-broadcast-ruling.md']);
|
|
2685
|
+
const oneMark = poll();
|
|
2686
|
+
expect(oneMark.mail).toHaveLength(1); // floor is 2 active seats — still unread
|
|
2687
|
+
writeBroadcastProcessed('totem', 'totem-gemini', ['2026-08-14T0900Z-broadcast-ruling.md']);
|
|
2688
|
+
const bothMarks = poll();
|
|
2689
|
+
expect(bothMarks.mail).toHaveLength(0); // both marks clear it, same as ungated
|
|
2690
|
+
});
|
|
2691
|
+
it('gate + truncation compose: both warnings present', () => {
|
|
2692
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2693
|
+
{ name: '2026-08-14T0900Z-broadcast-a.md', to: 'broadcast' },
|
|
2694
|
+
{ name: '2026-08-14T0901Z-broadcast-b.md', to: 'broadcast' },
|
|
2695
|
+
]);
|
|
2696
|
+
const result = poll({ maxScan: 1 });
|
|
2697
|
+
expect(result.truncated).toBe(true);
|
|
2698
|
+
expect(result.warnings.some((w) => w.includes('identity-ambiguous'))).toBe(true);
|
|
2699
|
+
});
|
|
2700
|
+
it('formatTextResult gated arm leads with the withheld qualifier', () => {
|
|
2701
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2702
|
+
{ name: '2026-08-14T0900Z-totem-claude-directed.md', to: 'totem-claude' },
|
|
2703
|
+
{ name: '2026-08-14T0901Z-broadcast-ruling.md', to: 'broadcast', subject: 'ruling' },
|
|
2704
|
+
]);
|
|
2705
|
+
const text = formatTextResult(poll());
|
|
2706
|
+
expect(text).toContain('Directed inbox NOT DERIVED');
|
|
2707
|
+
expect(text).toContain('1 directed dispatch(es) withheld');
|
|
2708
|
+
expect(text).toContain('1 unread broadcast dispatch(es):');
|
|
2709
|
+
expect(text).not.toContain('directed.md');
|
|
2710
|
+
});
|
|
2711
|
+
it('formatTextResult gated arm with no broadcasts says so', () => {
|
|
2712
|
+
const text = formatTextResult(poll());
|
|
2713
|
+
expect(text).toContain('No unread broadcast mail in the scanned locations.');
|
|
2714
|
+
});
|
|
2715
|
+
});
|
|
2716
|
+
describe('mailCommand — --as selector (mmnto-ai/totem#2204)', () => {
|
|
2717
|
+
/** Text-path mailCommand prints via log.info (stderr) — silence per the
|
|
2718
|
+
* suite's #2312 idiom so test output stays clean. */
|
|
2719
|
+
async function runMailCommand(opts) {
|
|
2720
|
+
const spy = vi.spyOn(log, 'info').mockImplementation(() => { });
|
|
2721
|
+
try {
|
|
2722
|
+
return await mailCommand(opts);
|
|
2723
|
+
}
|
|
2724
|
+
finally {
|
|
2725
|
+
spy.mockRestore();
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
it('serves exactly the named union seat via env-injection (source env)', async () => {
|
|
2729
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2730
|
+
{ name: '2026-08-14T0900Z-totem-claude-directed.md', to: 'totem-claude' },
|
|
2731
|
+
{ name: '2026-08-14T0901Z-totem-gemini-directed.md', to: 'totem-gemini' },
|
|
2732
|
+
]);
|
|
2733
|
+
const { result, exitCode } = await runMailCommand({
|
|
2734
|
+
repoRoot: selfRepoRoot(),
|
|
2735
|
+
workspace,
|
|
2736
|
+
env: {},
|
|
2737
|
+
asSeat: 'totem-claude',
|
|
2738
|
+
});
|
|
2739
|
+
expect(result.selfAgents).toEqual({ agents: ['totem-claude'], source: 'env' });
|
|
2740
|
+
expect(result.mail).toHaveLength(1);
|
|
2741
|
+
expect(result.mail[0].to).toBe('totem-claude');
|
|
2742
|
+
expect(result.seatGate).toBeUndefined();
|
|
2743
|
+
expect(exitCode).toBe(0);
|
|
2744
|
+
});
|
|
2745
|
+
it('canonicalizes case through the resolver', async () => {
|
|
2746
|
+
const { result } = await runMailCommand({
|
|
2747
|
+
repoRoot: selfRepoRoot(),
|
|
2748
|
+
workspace,
|
|
2749
|
+
env: {},
|
|
2750
|
+
asSeat: 'TOTEM-CLAUDE',
|
|
2751
|
+
});
|
|
2752
|
+
expect(result.selfAgents.agents).toEqual(['totem-claude']);
|
|
2753
|
+
});
|
|
2754
|
+
it('overrides an ambient env identity (flag-over-env, validated against the structural union)', async () => {
|
|
2755
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2756
|
+
{ name: '2026-08-14T0900Z-totem-gemini-directed.md', to: 'totem-gemini' },
|
|
2757
|
+
]);
|
|
2758
|
+
// A shell seated as totem-claude asks for the sibling seat's view: the
|
|
2759
|
+
// ambient identity must not shadow the structural union down to itself.
|
|
2760
|
+
const { result } = await runMailCommand({
|
|
2761
|
+
repoRoot: selfRepoRoot(),
|
|
2762
|
+
workspace,
|
|
2763
|
+
env: { TOTEM_SELF_AGENT: 'totem-claude' },
|
|
2764
|
+
asSeat: 'totem-gemini',
|
|
2765
|
+
});
|
|
2766
|
+
expect(result.selfAgents).toEqual({ agents: ['totem-gemini'], source: 'env' });
|
|
2767
|
+
expect(result.mail).toHaveLength(1);
|
|
2768
|
+
expect(result.mail[0].to).toBe('totem-gemini');
|
|
2769
|
+
});
|
|
2770
|
+
it('refuses a seat outside the resolved union (the foreign-anchor trap)', async () => {
|
|
2771
|
+
await expect(runMailCommand({ repoRoot: selfRepoRoot(), workspace, env: {}, asSeat: 'lc-claude' })).rejects.toThrow(/not a seat resolved for this repo/);
|
|
2772
|
+
});
|
|
2773
|
+
it('refusal carries the resolver diagnostics (#2141 config-omits-present-dir, leg F3)', async () => {
|
|
2774
|
+
const root = markedRepoRoot('cfg-repo');
|
|
2775
|
+
const orch = mkDir(path.join(root, '.totem', 'orchestration'));
|
|
2776
|
+
fs.writeFileSync(path.join(orch, 'config.json'), JSON.stringify({ host_agents: ['seat-alpha'] }), 'utf-8');
|
|
2777
|
+
mkDir(path.join(orch, 'seat-beta'));
|
|
2778
|
+
// seat-beta's dir IS its registration; config omits it, so --as seat-beta
|
|
2779
|
+
// is refused — the error must carry the resolver's own explanation.
|
|
2780
|
+
await expect(runMailCommand({ repoRoot: root, workspace, env: {}, asSeat: 'seat-beta' })).rejects.toThrow(/omits present seat dir/);
|
|
2781
|
+
});
|
|
2782
|
+
it('falls back to the env-declared list when the structural union is empty (worktree shape, leg F4)', async () => {
|
|
2783
|
+
const root = markedRepoRoot('scratch-wt');
|
|
2784
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2785
|
+
{ name: '2026-08-14T0900Z-seat-a-directed.md', to: 'seat-a' },
|
|
2786
|
+
]);
|
|
2787
|
+
const { result } = await runMailCommand({
|
|
2788
|
+
repoRoot: root,
|
|
2789
|
+
workspace,
|
|
2790
|
+
env: { TOTEM_SELF_AGENT: 'seat-a,seat-b' },
|
|
2791
|
+
asSeat: 'seat-a',
|
|
2792
|
+
});
|
|
2793
|
+
expect(result.selfAgents).toEqual({ agents: ['seat-a'], source: 'env' });
|
|
2794
|
+
expect(result.mail).toHaveLength(1);
|
|
2795
|
+
});
|
|
2796
|
+
it('a seat outside the env-declared list is still refused in the worktree shape (leg F4)', async () => {
|
|
2797
|
+
const root = markedRepoRoot('scratch-wt');
|
|
2798
|
+
await expect(runMailCommand({
|
|
2799
|
+
repoRoot: root,
|
|
2800
|
+
workspace,
|
|
2801
|
+
env: { TOTEM_SELF_AGENT: 'seat-a,seat-b' },
|
|
2802
|
+
asSeat: 'seat-c',
|
|
2803
|
+
})).rejects.toThrow(/not a seat resolved for this repo/);
|
|
2804
|
+
});
|
|
2805
|
+
it('anchors processed-mark subtraction to the selected seat (design invariant 3)', async () => {
|
|
2806
|
+
writeOutbox('totem-strategy', 'strategy-claude', [
|
|
2807
|
+
{ name: '2026-08-14T0900Z-totem-claude-handled.md', to: 'totem-claude' },
|
|
2808
|
+
]);
|
|
2809
|
+
writeProcessed('totem', 'totem-claude', ['2026-08-14T0900Z-totem-claude-handled.md']);
|
|
2810
|
+
const { result } = await runMailCommand({
|
|
2811
|
+
repoRoot: selfRepoRoot(),
|
|
2812
|
+
workspace,
|
|
2813
|
+
env: {},
|
|
2814
|
+
asSeat: 'totem-claude',
|
|
2815
|
+
});
|
|
2816
|
+
expect(result.mail).toHaveLength(0);
|
|
2817
|
+
});
|
|
2818
|
+
it('refuses --as together with --all-seats', async () => {
|
|
2819
|
+
await expect(runMailCommand({
|
|
2820
|
+
repoRoot: selfRepoRoot(),
|
|
2821
|
+
workspace,
|
|
2822
|
+
env: {},
|
|
2823
|
+
asSeat: 'totem-claude',
|
|
2824
|
+
allSeats: true,
|
|
2825
|
+
})).rejects.toThrow(/contradictory/);
|
|
2826
|
+
});
|
|
2827
|
+
it('rejects an unsafe --as value with the correctly-labeled error (re-arm 4)', async () => {
|
|
2828
|
+
// Pins the CODE, not only the message: an --as refusal is poll-option
|
|
2829
|
+
// validation (CONFIG_INVALID), never the send path's MAIL_SEND_FAILED
|
|
2830
|
+
// (GCA on #2639).
|
|
2831
|
+
await expect(runMailCommand({ repoRoot: selfRepoRoot(), workspace, env: {}, asSeat: '../evil' })).rejects.toMatchObject({
|
|
2832
|
+
code: 'CONFIG_INVALID',
|
|
2833
|
+
message: expect.stringMatching(/invalid --as/),
|
|
2834
|
+
});
|
|
2835
|
+
});
|
|
2836
|
+
});
|
|
2470
2837
|
//# sourceMappingURL=mail.test.js.map
|