@@ -844,31 +844,13 @@ describe.skip('pg — TODO retire-session-names #175: rewrite fixtures for UUID
844844 } ) ;
845845
846846 // ---------------------------------------------------------------------------
847- // Master-aware resume (Group 1, master-aware-spawn wish):
848- // resolveResumeSessionId must probe `dir:<recipientId>` when worker == null.
847+ // resolveResumeSessionId — identity-only contract (wish #175 G6)
848+ // Caller resolves name → id at the CLI boundary; this helper requires a
849+ // non-null worker with a canonical id. The pre-G6 `dir:<recipientId>`
850+ // fallback for null workers is removed.
849851 // ---------------------------------------------------------------------------
850852
851- describe ( 'resolveResumeSessionId (master-aware fallback)' , ( ) => {
852- /**
853- * Insert a `dir:<name>` master agent row directly. Mirrors
854- * `agent-directory.add()` but skips the `agents.yaml` round-trip so the
855- * test keeps a tight surface around the chokepoint behavior.
856- * Crucially sets `auto_resume=true` (fresh-DB default is `false` since
857- * migration 044) so the chokepoint returns `resume=true` for permanent
858- * rows that have a session UUID on file.
859- */
860- async function seedMasterDirRow ( name : string , opts : { team : string ; repoPath : string } ) : Promise < void > {
861- const { getConnection } = await import ( './db.js' ) ;
862- const sql = await getConnection ( ) ;
863- await sql `
864- INSERT INTO agents (id, role, custom_name, team, repo_path, started_at, auto_resume, state, metadata)
865- VALUES (
866- ${ `dir:${ name } ` } , ${ name } , ${ name } , ${ opts . team } , ${ opts . repoPath } ,
867- now(), true, ${ null } , ${ sql . json ( { } ) }
868- )
869- ` ;
870- }
871-
853+ describe ( 'resolveResumeSessionId (identity-only)' , ( ) => {
872854 function templateFor ( role : string , team : string ) : WorkerTemplate {
873855 return {
874856 id : `${ team } -${ role } ` ,
@@ -880,53 +862,79 @@ describe.skip('pg — TODO retire-session-names #175: rewrite fixtures for UUID
880862 } ;
881863 }
882864
883- test ( 'master agent: dir:<name> with current_executor.claude_session_id resolves via chokepoint' , async ( ) => {
865+ test ( 'master agent (dir:<name> id): resolves via chokepoint when worker row carries the id' , async ( ) => {
866+ const registry = await import ( './agent-registry.js' ) ;
884867 const executorReg = await import ( './executor-registry.js' ) ;
885868 const { resolveResumeSessionId } = await import ( './protocol-router.js' ) ;
886869
887870 const sessionId = 'fa1fac7b-1234-4abc-9def-000000000001' ;
888- await seedMasterDirRow ( 'master-db' , { team : 'team-db' , repoPath : tempDir } ) ;
871+ const now = new Date ( ) . toISOString ( ) ;
872+ // Master persistence id-shape (`dir:<name>`) is preserved. The wish #175
873+ // change is that the caller must pass a worker row with this id —
874+ // resolveResumeSessionId no longer fabricates `dir:${recipientId}` from
875+ // a name when worker is null.
876+ await registry . register ( {
877+ id : 'dir:master-db' ,
878+ paneId : '%51' ,
879+ session : 'test-session' ,
880+ provider : 'claude' ,
881+ transport : 'tmux' ,
882+ role : 'master-db' ,
883+ team : 'team-db' ,
884+ customName : 'master-db' ,
885+ state : 'idle' ,
886+ startedAt : now ,
887+ lastStateChange : now ,
888+ repoPath : tempDir ,
889+ worktree : null ,
890+ autoResume : true ,
891+ } ) ;
889892 await executorReg . createAndLinkExecutor ( 'dir:master-db' , 'claude' , 'tmux' , {
890893 claudeSessionId : sessionId ,
891894 state : 'idle' ,
892895 } ) ;
893896
894- const result = await resolveResumeSessionId ( null , templateFor ( 'master-db' , 'team-db' ) , 'master-db' ) ;
897+ const worker = await registry . get ( 'dir:master-db' ) ;
898+ expect ( worker ) . toBeTruthy ( ) ;
899+ const result = await resolveResumeSessionId ( worker ! , templateFor ( 'master-db' , 'team-db' ) ) ;
895900 expect ( result ) . toBe ( sessionId ) ;
896901 } ) ;
897902
898- test ( 'master agent jsonl-fallback: no executor on row, jsonl on disk resolves via chokepoint ' , async ( ) => {
903+ test ( 'null worker is rejected at the contract boundary (G6 tightened) ' , async ( ) => {
899904 const { resolveResumeSessionId } = await import ( './protocol-router.js' ) ;
900- const executorReg = await import ( './executor-registry.js' ) ;
901-
902- const recoveredSessionId = 'fa1fac7b-1234-4abc-9def-000000000002' ;
903- await seedMasterDirRow ( 'master-jsonl' , { team : 'team-jsonl' , repoPath : tempDir } ) ;
904- // No executor → DB happy path misses; getResumeSessionId falls through
905- // to the JSONL scanner. Override scanner to return our recovered UUID
906- // without touching the real ~/.claude/projects/* tree.
907- executorReg . _resumeJsonlScannerDeps . scanForSession = async ( cwd , identity ) => {
908- if ( cwd === tempDir && identity ?. team === 'team-jsonl' && identity ?. customName === 'master-jsonl' ) {
909- return recoveredSessionId ;
910- }
911- return null ;
912- } ;
913-
914- try {
915- const result = await resolveResumeSessionId ( null , templateFor ( 'master-jsonl' , 'team-jsonl' ) , 'master-jsonl' ) ;
916- expect ( result ) . toBe ( recoveredSessionId ) ;
917- } finally {
918- executorReg . _resumeJsonlScannerDeps . scanForSession = null ;
919- }
905+ // After wish #175 G6, callers must resolve to an id before invoking this
906+ // helper. Passing null violates the contract — type-system enforces this
907+ // at compile time, but we guard at runtime to catch any erased-type
908+ // callers from JS.
909+ await expect (
910+ // @ts -expect-error — passing null intentionally violates the new contract
911+ resolveResumeSessionId ( null , templateFor ( 'eph' , 'team-eph' ) ) ,
912+ ) . rejects . toThrow ( ) ;
920913 } ) ;
921914
922- test ( 'ephemeral spawn: no dir:<name> row, no worker → undefined (fresh --session-id)' , async ( ) => {
915+ test ( 'non-claude provider returns undefined regardless of worker state' , async ( ) => {
916+ const registry = await import ( './agent-registry.js' ) ;
923917 const { resolveResumeSessionId } = await import ( './protocol-router.js' ) ;
924918
925- const result = await resolveResumeSessionId (
926- null ,
927- templateFor ( 'ephemeral-role' , 'team-eph' ) ,
928- 'unknown-ephemeral-task' ,
929- ) ;
919+ const now = new Date ( ) . toISOString ( ) ;
920+ await registry . register ( {
921+ id : '99999999-2222-3333-4444-555555555555' ,
922+ paneId : '%52' ,
923+ session : 'test-session' ,
924+ provider : 'codex' ,
925+ transport : 'tmux' ,
926+ role : 'codex-role' ,
927+ team : 'team-codex' ,
928+ state : 'idle' ,
929+ startedAt : now ,
930+ lastStateChange : now ,
931+ repoPath : tempDir ,
932+ worktree : null ,
933+ } ) ;
934+ const worker = await registry . get ( '99999999-2222-3333-4444-555555555555' ) ;
935+ expect ( worker ) . toBeTruthy ( ) ;
936+ const tpl : WorkerTemplate = { ...templateFor ( 'codex-role' , 'team-codex' ) , provider : 'codex' } ;
937+ const result = await resolveResumeSessionId ( worker ! , tpl ) ;
930938 expect ( result ) . toBeUndefined ( ) ;
931939 } ) ;
932940
@@ -938,7 +946,7 @@ describe.skip('pg — TODO retire-session-names #175: rewrite fixtures for UUID
938946 const sessionId = 'fa1fac7b-1234-4abc-9def-000000000003' ;
939947 const now = new Date ( ) . toISOString ( ) ;
940948 await registry . register ( {
941- id : 'live-master-worker ' ,
949+ id : '88888888-2222-3333-4444-555555555555 ' ,
942950 paneId : '%50' ,
943951 session : 'test-session' ,
944952 provider : 'claude' ,
@@ -953,15 +961,53 @@ describe.skip('pg — TODO retire-session-names #175: rewrite fixtures for UUID
953961 worktree : null ,
954962 autoResume : true ,
955963 } ) ;
956- await executorReg . createAndLinkExecutor ( 'live-master-worker ' , 'claude' , 'tmux' , {
964+ await executorReg . createAndLinkExecutor ( '88888888-2222-3333-4444-555555555555 ' , 'claude' , 'tmux' , {
957965 claudeSessionId : sessionId ,
958966 state : 'idle' ,
959967 } ) ;
960968
961- const worker = await registry . get ( 'live-master-worker ' ) ;
969+ const worker = await registry . get ( '88888888-2222-3333-4444-555555555555 ' ) ;
962970 expect ( worker ) . toBeTruthy ( ) ;
963- const result = await resolveResumeSessionId ( worker ! , templateFor ( 'master-live' , 'team-live' ) , 'master-live' ) ;
971+ const result = await resolveResumeSessionId ( worker ! , templateFor ( 'master-live' , 'team-live' ) ) ;
964972 expect ( result ) . toBe ( sessionId ) ;
965973 } ) ;
966974 } ) ;
975+
976+ // ---------------------------------------------------------------------------
977+ // findSpawnTemplate / cleanupDeadWorkers identity-only contracts (wish #175 G6)
978+ // These helpers are not exported — we exercise them via send-path behavior:
979+ // - findSpawnTemplate: a recipient-as-role that only matches a template by
980+ // its `role` (no worker carrying that role) must NOT auto-spawn anymore.
981+ // - cleanupDeadWorkers: only removes the row whose id matches; sibling
982+ // dead rows with the same role survive.
983+ // ---------------------------------------------------------------------------
984+
985+ describe ( 'identity-only spawn/cleanup contracts (G6)' , ( ) => {
986+ test ( 'findSpawnTemplate refuses to match by recipient name when no worker exists' , async ( ) => {
987+ const registry = await import ( './agent-registry.js' ) ;
988+ const router = await import ( './protocol-router.js' ) ;
989+
990+ router . _deps . isPaneAlive = async ( paneId : string ) => alivePanes . has ( paneId ) ;
991+ router . _deps . waitForWorkerReady = async ( ) => true ;
992+ process . env . TMUX = '/tmp/tmux-test/default,123,0' ;
993+
994+ const now = new Date ( ) . toISOString ( ) ;
995+ // Template exists keyed by role 'engineer' / team 'g6-team'.
996+ await registry . saveTemplate ( {
997+ id : 'g6-team-engineer' ,
998+ team : 'g6-team' ,
999+ role : 'engineer' ,
1000+ provider : 'claude' ,
1001+ cwd : tempDir ,
1002+ lastSpawnedAt : now ,
1003+ } ) ;
1004+
1005+ // No worker row, no directory entry. Pre-G6 router would have matched
1006+ // the template by recipientId ('engineer') and spawned. Post-G6: refuses.
1007+ const spawnCountBefore = spawnCallCount ;
1008+ const result = await router . sendMessage ( tempDir , 'alice' , 'engineer' , 'hi' , 'g6-team' ) ;
1009+ expect ( spawnCallCount ) . toBe ( spawnCountBefore ) ;
1010+ expect ( result . delivered ) . toBe ( false ) ;
1011+ } ) ;
1012+ } ) ;
9671013} ) ;
0 commit comments