11import { expect , it } from "@effect/vitest" ;
22import {
33 EnvironmentId ,
4+ GitCommandError ,
45 ProjectId ,
56 ProviderInstanceId ,
67 ThreadId ,
@@ -93,7 +94,11 @@ const TestCryptoLive = Layer.sync(Crypto.Crypto, () => {
9394
9495const makeTestLayer = (
9596 commands : OrchestrationCommand [ ] ,
96- options : { readonly enqueueCalls ?: number [ ] } = { } ,
97+ options : {
98+ readonly enqueueCalls ?: number [ ] ;
99+ readonly gitStatus ?: GitWorkflowService [ "Service" ] [ "status" ] ;
100+ readonly sourceThread ?: OrchestrationThreadShell ;
101+ } = { } ,
97102) => {
98103 const bootstrapTurnStartDispatcherLayer = Layer . mock (
99104 BootstrapTurnStartDispatcher . BootstrapTurnStartDispatcher ,
@@ -126,7 +131,7 @@ const makeTestLayer = (
126131 Layer . provide (
127132 Layer . mock ( ProjectionSnapshotQuery ) ( {
128133 getProjectShellById : ( ) => Effect . succeed ( Option . some ( project ) ) ,
129- getThreadShellById : ( ) => Effect . succeed ( Option . some ( sourceThread ) ) ,
134+ getThreadShellById : ( ) => Effect . succeed ( Option . some ( options . sourceThread ?? sourceThread ) ) ,
130135 } ) ,
131136 ) ,
132137 Layer . provide (
@@ -147,24 +152,26 @@ const makeTestLayer = (
147152 nextCursor : null ,
148153 totalCount : 1 ,
149154 } ) ,
150- status : ( ) =>
151- Effect . succeed ( {
152- isRepo : true ,
153- hasPrimaryRemote : true ,
154- isDefaultRef : false ,
155- refName : "feature/source" ,
156- hasWorkingTreeChanges : false ,
157- workingTree : {
158- files : [ ] ,
159- insertions : 0 ,
160- deletions : 0 ,
161- } ,
162- hasUpstream : true ,
163- aheadCount : 0 ,
164- behindCount : 0 ,
165- aheadOfDefaultCount : 0 ,
166- pr : null ,
167- } ) ,
155+ status :
156+ options . gitStatus ??
157+ ( ( ) =>
158+ Effect . succeed ( {
159+ isRepo : true ,
160+ hasPrimaryRemote : true ,
161+ isDefaultRef : false ,
162+ refName : "feature/source" ,
163+ hasWorkingTreeChanges : false ,
164+ workingTree : {
165+ files : [ ] ,
166+ insertions : 0 ,
167+ deletions : 0 ,
168+ } ,
169+ hasUpstream : true ,
170+ aheadCount : 0 ,
171+ behindCount : 0 ,
172+ aheadOfDefaultCount : 0 ,
173+ pr : null ,
174+ } ) ) ,
168175 } ) ,
169176 ) ,
170177 Layer . provide (
@@ -190,7 +197,11 @@ const makeTestLayer = (
190197const callStartTool = (
191198 arguments_ : Record < string , unknown > ,
192199 commands : OrchestrationCommand [ ] ,
193- options : { readonly enqueueCalls ?: number [ ] } = { } ,
200+ options : {
201+ readonly enqueueCalls ?: number [ ] ;
202+ readonly gitStatus ?: GitWorkflowService [ "Service" ] [ "status" ] ;
203+ readonly sourceThread ?: OrchestrationThreadShell ;
204+ } = { } ,
194205) =>
195206 Effect . gen ( function * ( ) {
196207 const server = yield * McpServer . McpServer ;
@@ -272,23 +283,75 @@ it.effect("passes explicit setup script requests for existing worktrees", () =>
272283it . effect ( "starts current-checkout threads with warning metadata" , ( ) =>
273284 Effect . gen ( function * ( ) {
274285 const commands : OrchestrationCommand [ ] = [ ] ;
286+ const sourceWorktreePath = "/repo/.worktrees/source" ;
275287 const result = yield * callStartTool (
276- { prompt : "Read current diff" , mode : "current_checkout" } ,
288+ { prompt : "Read current diff" , mode : "current_checkout" , runSetupScript : true } ,
277289 commands ,
290+ { sourceThread : { ...sourceThread , worktreePath : sourceWorktreePath } } ,
278291 ) ;
279292
280293 expect ( result . isError ) . toBe ( false ) ;
281294 expect ( result . structuredContent ) . toMatchObject ( {
282295 projectId,
283296 mode : "current_checkout" ,
284297 branch : "feature/source" ,
285- worktreePath : null ,
298+ worktreePath : sourceWorktreePath ,
286299 } ) ;
287300 expect ( result . structuredContent ) . toHaveProperty ( "warning" ) ;
288301 const command = commands [ 0 ] ;
289302 expect ( command ?. type ) . toBe ( "thread.turn.start" ) ;
290303 if ( command ?. type !== "thread.turn.start" ) return ;
291304 expect ( command . bootstrap ?. prepareWorktree ) . toBeUndefined ( ) ;
292- expect ( command . bootstrap ?. createThread ?. worktreePath ) . toBeNull ( ) ;
305+ expect ( command . bootstrap ?. createThread ?. worktreePath ) . toBe ( sourceWorktreePath ) ;
306+ expect ( command . bootstrap ?. runSetupScript ) . toBe ( true ) ;
307+ } ) ,
308+ ) ;
309+
310+ it . effect ( "fails existing worktree mode when git status cannot resolve the branch" , ( ) =>
311+ Effect . gen ( function * ( ) {
312+ const commands : OrchestrationCommand [ ] = [ ] ;
313+ const result = yield * callStartTool (
314+ { prompt : "Use bad checkout" , mode : "existing_worktree" , worktreePath : "/missing" } ,
315+ commands ,
316+ {
317+ gitStatus : ( ) =>
318+ Effect . fail (
319+ new GitCommandError ( {
320+ operation : "status" ,
321+ command : "git status" ,
322+ cwd : "/missing" ,
323+ detail : "not a git repository" ,
324+ } ) ,
325+ ) ,
326+ } ,
327+ ) ;
328+
329+ expect ( result . isError ) . toBe ( true ) ;
330+ expect ( commands ) . toHaveLength ( 0 ) ;
331+ } ) ,
332+ ) ;
333+
334+ it . effect ( "fails current checkout mode when fallback branch lookup fails" , ( ) =>
335+ Effect . gen ( function * ( ) {
336+ const commands : OrchestrationCommand [ ] = [ ] ;
337+ const result = yield * callStartTool (
338+ { prompt : "Use current checkout" , mode : "current_checkout" } ,
339+ commands ,
340+ {
341+ sourceThread : { ...sourceThread , branch : null , worktreePath : "/missing" } ,
342+ gitStatus : ( ) =>
343+ Effect . fail (
344+ new GitCommandError ( {
345+ operation : "status" ,
346+ command : "git status" ,
347+ cwd : "/missing" ,
348+ detail : "not a git repository" ,
349+ } ) ,
350+ ) ,
351+ } ,
352+ ) ;
353+
354+ expect ( result . isError ) . toBe ( true ) ;
355+ expect ( commands ) . toHaveLength ( 0 ) ;
293356 } ) ,
294357) ;
0 commit comments