@@ -1450,6 +1450,109 @@ describe('orchestration RPC methods', () => {
14501450 expect ( db . getTask ( result . task . id ) ?. coordinator_run_id ) . toBeNull ( )
14511451 expect ( db . getTask ( result . task . id ) ?. target_key ) . toBeNull ( )
14521452 } )
1453+
1454+ it ( 'stamps target_key from --target-worktree (#9 recipe director path)' , async ( ) => {
1455+ setup ( )
1456+ // The renderer recipe director has the director worktree id, not a terminal
1457+ // handle, so it passes a worktree selector. It resolves through the SAME
1458+ // resolveOrchestrationTargetKey the run uses, so the keys match for adoption.
1459+ const targetSpy = vi
1460+ . spyOn ( runtime , 'resolveOrchestrationTargetKey' )
1461+ . mockResolvedValue ( 'worktree:director1' )
1462+ const terminalSpy = vi . spyOn ( runtime , 'resolveOrchestrationTargetKeyForTerminal' )
1463+
1464+ const result = ( await call ( 'orchestration.taskCreate' , {
1465+ spec : 'implement' ,
1466+ targetWorktree : 'id:director1'
1467+ } ) ) as { task : { id : string } }
1468+
1469+ expect ( targetSpy ) . toHaveBeenCalledWith ( 'id:director1' )
1470+ // --target-worktree wins: the terminal resolver is not consulted.
1471+ expect ( terminalSpy ) . not . toHaveBeenCalled ( )
1472+ expect ( db . getTask ( result . task . id ) ?. target_key ) . toBe ( 'worktree:director1' )
1473+ } )
1474+
1475+ it ( 'refuses a task whose --target-worktree does not resolve (fail closed)' , async ( ) => {
1476+ setup ( )
1477+ vi . spyOn ( runtime , 'resolveOrchestrationTargetKey' ) . mockRejectedValue (
1478+ new Error ( 'selector_not_found' )
1479+ )
1480+
1481+ await expect (
1482+ call ( 'orchestration.taskCreate' , { spec : 'x' , targetWorktree : 'id:gone' } )
1483+ ) . rejects . toThrow ( / s e l e c t o r _ n o t _ f o u n d / )
1484+ } )
1485+ } )
1486+
1487+ describe ( 'orchestration.taskCreate ↔ run target_key adoption (#9 end-to-end)' , ( ) => {
1488+ // Why (#9, F1 clash seam): the task path (taskCreate --target-worktree) and the
1489+ // run path (orchestration.run --worktree) must resolve the SAME target_key, or
1490+ // run-start adoption silently fails to claim the task and the recipe never
1491+ // dispatches. These tests prove the binding through the REAL resolver and the
1492+ // REAL db.adoptUnownedTasks — not a mocked resolver returning a matched literal.
1493+ // Only the lowest-level filesystem worktree lookup is stubbed (id:X → worktree
1494+ // X); resolveOrchestrationTargetKey and adoptUnownedTasks run their real code.
1495+ function stubWorktreeLookup ( ) : void {
1496+ ; (
1497+ runtime as unknown as {
1498+ resolveWorktreeSelector : ( selector : string ) => Promise < { id : string } >
1499+ }
1500+ ) . resolveWorktreeSelector = async ( selector ) => ( { id : selector . replace ( / ^ i d : / , '' ) } )
1501+ }
1502+
1503+ it ( 'adopts a task stamped via --target-worktree into a run on the same worktree' , async ( ) => {
1504+ setup ( )
1505+ stubWorktreeLookup ( )
1506+
1507+ // Task path: the renderer recipe director stamps the task by worktree id.
1508+ const { task } = ( await call ( 'orchestration.taskCreate' , {
1509+ spec : 'implement' ,
1510+ targetWorktree : 'id:W'
1511+ } ) ) as { task : { id : string ; target_key : string | null } }
1512+
1513+ // Run path: resolve target_key the EXACT way orchestration.run does, start
1514+ // the run, then adopt the EXACT way the coordinator's executeLoop does.
1515+ const runTargetKey = await runtime . resolveOrchestrationTargetKey ( 'id:W' )
1516+ const run = db . startCoordinatorRun ( {
1517+ spec : 'recipe:implement_then_review' ,
1518+ coordinatorHandle : 'coordinator-e2e' ,
1519+ targetKey : runTargetKey ,
1520+ worktreeBacked : true ,
1521+ workerAgent : 'claude'
1522+ } )
1523+ const bound = db . adoptUnownedTasks ( run . id , db . getCoordinatorRun ( run . id ) ?. target_key ?? null )
1524+
1525+ // Both paths resolved worktree:W → adoption binds the task to the run.
1526+ expect ( task . target_key ) . toBe ( 'worktree:W' )
1527+ expect ( bound ) . toBe ( 1 )
1528+ expect ( db . getTask ( task . id ) ?. coordinator_run_id ) . toBe ( run . id )
1529+ } )
1530+
1531+ it ( 'does NOT adopt a task stamped to a different worktree (fails if keys ever diverge)' , async ( ) => {
1532+ setup ( )
1533+ stubWorktreeLookup ( )
1534+
1535+ // Stamped to a DIFFERENT worktree than the run targets. This is the negative
1536+ // control: it makes the positive test meaningful — if taskCreate and run ever
1537+ // resolved the same key for different worktree inputs (the divergence we
1538+ // guard against), this task would wrongly bind and this assertion would fail.
1539+ const { task } = ( await call ( 'orchestration.taskCreate' , {
1540+ spec : 'implement' ,
1541+ targetWorktree : 'id:OTHER'
1542+ } ) ) as { task : { id : string } }
1543+
1544+ const runTargetKey = await runtime . resolveOrchestrationTargetKey ( 'id:W' )
1545+ const run = db . startCoordinatorRun ( {
1546+ spec : 'recipe' ,
1547+ coordinatorHandle : 'coordinator-e2e-2' ,
1548+ targetKey : runTargetKey ,
1549+ worktreeBacked : true ,
1550+ workerAgent : 'claude'
1551+ } )
1552+ db . adoptUnownedTasks ( run . id , db . getCoordinatorRun ( run . id ) ?. target_key ?? null )
1553+
1554+ expect ( db . getTask ( task . id ) ?. coordinator_run_id ) . toBeNull ( )
1555+ } )
14531556 } )
14541557
14551558 describe ( 'orchestration.reset' , ( ) => {
0 commit comments