1+ import crypto from "node:crypto" ;
12import fs from "node:fs" ;
23import os from "node:os" ;
34import path from "node:path" ;
@@ -95,13 +96,16 @@ describe("ProviderCommandReactor", () => {
9596 async function createHarness ( input ?: {
9697 readonly baseDir ?: string ;
9798 readonly threadModel ?: string ;
99+ readonly worktreePath ?: string | null ;
98100 } ) {
99101 const now = new Date ( ) . toISOString ( ) ;
100102 const baseDir = input ?. baseDir ?? fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "okcode-reactor-" ) ) ;
101103 createdBaseDirs . add ( baseDir ) ;
102104 const { stateDir } = deriveServerPathsSync ( baseDir , undefined ) ;
103105 createdStateDirs . add ( stateDir ) ;
104106 const threadModel = input ?. threadModel ?? "gpt-5-codex" ;
107+ const projectWorkspaceRoot = path . join ( baseDir , "project" ) ;
108+ fs . mkdirSync ( projectWorkspaceRoot , { recursive : true } ) ;
105109 const runtimeEventPubSub = Effect . runSync ( PubSub . unbounded < ProviderRuntimeEvent > ( ) ) ;
106110 let nextSessionIndex = 1 ;
107111 const runtimeSessions : Array < ProviderSession > = [ ] ;
@@ -244,7 +248,7 @@ describe("ProviderCommandReactor", () => {
244248 commandId : CommandId . makeUnsafe ( "cmd-project-create" ) ,
245249 projectId : asProjectId ( "project-1" ) ,
246250 title : "Provider Project" ,
247- workspaceRoot : "/tmp/provider-project" ,
251+ workspaceRoot : projectWorkspaceRoot ,
248252 defaultModel : threadModel ,
249253 createdAt : now ,
250254 } ) ,
@@ -260,7 +264,7 @@ describe("ProviderCommandReactor", () => {
260264 interactionMode : DEFAULT_PROVIDER_INTERACTION_MODE ,
261265 runtimeMode : "approval-required" ,
262266 branch : null ,
263- worktreePath : null ,
267+ worktreePath : input ?. worktreePath ?? null ,
264268 createdAt : now ,
265269 } ) ,
266270 ) ;
@@ -275,6 +279,7 @@ describe("ProviderCommandReactor", () => {
275279 stopSession,
276280 renameBranch,
277281 generateBranchName,
282+ projectWorkspaceRoot,
278283 stateDir,
279284 drain,
280285 } ;
@@ -305,7 +310,7 @@ describe("ProviderCommandReactor", () => {
305310 await waitFor ( ( ) => harness . sendTurn . mock . calls . length === 1 ) ;
306311 expect ( harness . startSession . mock . calls [ 0 ] ?. [ 0 ] ) . toEqual ( ThreadId . makeUnsafe ( "thread-1" ) ) ;
307312 expect ( harness . startSession . mock . calls [ 0 ] ?. [ 1 ] ) . toMatchObject ( {
308- cwd : "/tmp/provider-project" ,
313+ cwd : harness . projectWorkspaceRoot ,
309314 model : "gpt-5-codex" ,
310315 runtimeMode : "approval-required" ,
311316 } ) ;
@@ -316,6 +321,47 @@ describe("ProviderCommandReactor", () => {
316321 expect ( thread ?. session ?. runtimeMode ) . toBe ( "approval-required" ) ;
317322 } ) ;
318323
324+ it ( "falls back to the project root and clears stale worktree paths before session start" , async ( ) => {
325+ const missingWorktreePath = path . join (
326+ os . tmpdir ( ) ,
327+ `okcode-missing-worktree-${ crypto . randomUUID ( ) } ` ,
328+ ) ;
329+ const harness = await createHarness ( {
330+ worktreePath : missingWorktreePath ,
331+ } ) ;
332+ const now = new Date ( ) . toISOString ( ) ;
333+
334+ await Effect . runPromise (
335+ harness . engine . dispatch ( {
336+ type : "thread.turn.start" ,
337+ commandId : CommandId . makeUnsafe ( "cmd-turn-start-stale-worktree" ) ,
338+ threadId : ThreadId . makeUnsafe ( "thread-1" ) ,
339+ message : {
340+ messageId : asMessageId ( "user-message-stale-worktree" ) ,
341+ role : "user" ,
342+ text : "recover stale worktree" ,
343+ attachments : [ ] ,
344+ } ,
345+ interactionMode : DEFAULT_PROVIDER_INTERACTION_MODE ,
346+ runtimeMode : "approval-required" ,
347+ createdAt : now ,
348+ } ) ,
349+ ) ;
350+
351+ await waitFor ( ( ) => harness . startSession . mock . calls . length === 1 ) ;
352+ expect ( harness . startSession . mock . calls [ 0 ] ?. [ 1 ] ) . toMatchObject ( {
353+ cwd : harness . projectWorkspaceRoot ,
354+ model : "gpt-5-codex" ,
355+ runtimeMode : "approval-required" ,
356+ } ) ;
357+
358+ const readModel = await Effect . runPromise ( harness . engine . getReadModel ( ) ) ;
359+ const thread = readModel . threads . find (
360+ ( entry ) => entry . id === ThreadId . makeUnsafe ( "thread-1" ) ,
361+ ) ;
362+ expect ( thread ?. worktreePath ) . toBeNull ( ) ;
363+ } ) ;
364+
319365 it ( "forwards codex model options through session start and turn send" , async ( ) => {
320366 const harness = await createHarness ( ) ;
321367 const now = new Date ( ) . toISOString ( ) ;
0 commit comments