@mmnto/cli 1.116.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.
@@ -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
- const result = poll();
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({ workspace: undefined, env: { TOTEM_WORKSPACE: alt } });
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
- const result = poll({ recursive: true });
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
- const result = pollMail({ repoRoot: subdir, env: {} });
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
- const result = poll({ maxScan: 3 });
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
- const inbox = pollMail({ repoRoot: recipientRepo, workspace, env: {} });
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