| ... | ... | @@ -277,6 +277,9 @@ describe("CheckpointReactor", () => { |
| 277 | 277 | readonly seedFilesystemCheckpoints?: boolean; |
| 278 | 278 | readonly projectWorkspaceRoot?: string; |
| 279 | 279 | readonly threadWorktreePath?: string | null; |
| 280 | readonly threadBranch?: string | null; |
| 281 | readonly secondThreadSharingWorktree?: boolean; |
| 282 | readonly localStatusRefName?: string | null; |
| 280 | 283 | readonly providerSessionCwd?: string; |
| 281 | 284 | readonly providerName?: ProviderDriverKind; |
| 282 | 285 | readonly gitStatusRefreshCalls?: Array<string>; |
| ... | ... | @@ -315,7 +318,8 @@ describe("CheckpointReactor", () => { |
| 315 | 318 | isRepo: true, |
| 316 | 319 | hasPrimaryRemote: false, |
| 317 | 320 | isDefaultRef: true, |
| 318 | | refName: "main", |
| 321 | refName: |
| 322 | options?.localStatusRefName !== undefined ? options.localStatusRefName : "main", |
| 319 | 323 | hasWorkingTreeChanges: false, |
| 320 | 324 | workingTree: { files: [], insertions: 0, deletions: 0 }, |
| 321 | 325 | }), |
| ... | ... | @@ -370,22 +374,45 @@ describe("CheckpointReactor", () => { |
| 370 | 374 | }), |
| 371 | 375 | ); |
| 372 | 376 | await Effect.runPromise( |
| 373 | | engine.dispatch({ |
| 374 | | type: "thread.create", |
| 375 | | commandId: CommandId.make("cmd-thread-create"), |
| 376 | | threadId: ThreadId.make("thread-1"), |
| 377 | | projectId: asProjectId("project-1"), |
| 378 | | title: "Thread", |
| 379 | | modelSelection: { |
| 380 | | instanceId: ProviderInstanceId.make("codex"), |
| 381 | | model: "gpt-5-codex", |
| 382 | | }, |
| 383 | | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, |
| 384 | | runtimeMode: "approval-required", |
| 385 | | branch: null, |
| 386 | | worktreePath: options?.threadWorktreePath ?? cwd, |
| 387 | | createdAt, |
| 388 | | }), |
| 377 | engine |
| 378 | .dispatch({ |
| 379 | type: "thread.create", |
| 380 | commandId: CommandId.make("cmd-thread-create"), |
| 381 | threadId: ThreadId.make("thread-1"), |
| 382 | projectId: asProjectId("project-1"), |
| 383 | title: "Thread", |
| 384 | modelSelection: { |
| 385 | instanceId: ProviderInstanceId.make("codex"), |
| 386 | model: "gpt-5-codex", |
| 387 | }, |
| 388 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, |
| 389 | runtimeMode: "approval-required", |
| 390 | branch: options?.threadBranch ?? null, |
| 391 | worktreePath: options?.threadWorktreePath ?? cwd, |
| 392 | createdAt, |
| 393 | }) |
| 394 | .pipe( |
| 395 | options?.secondThreadSharingWorktree |
| 396 | ? Effect.andThen( |
| 397 | engine.dispatch({ |
| 398 | type: "thread.create", |
| 399 | commandId: CommandId.make("cmd-thread-create-2"), |
| 400 | threadId: ThreadId.make("thread-2"), |
| 401 | projectId: asProjectId("project-1"), |
| 402 | title: "Thread 2", |
| 403 | modelSelection: { |
| 404 | instanceId: ProviderInstanceId.make("codex"), |
| 405 | model: "gpt-5-codex", |
| 406 | }, |
| 407 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, |
| 408 | runtimeMode: "approval-required", |
| 409 | branch: null, |
| 410 | worktreePath: options?.threadWorktreePath ?? cwd, |
| 411 | createdAt, |
| 412 | }), |
| 413 | ) |
| 414 | : Effect.asVoid, |
| 415 | ), |
| 389 | 416 | ); |
| 390 | 417 | |
| 391 | 418 | if (options?.seedFilesystemCheckpoints ?? true) { |
| ... | ... | @@ -518,6 +545,86 @@ describe("CheckpointReactor", () => { |
| 518 | 545 | expect(gitStatusRefreshCalls).toEqual([harness.cwd]); |
| 519 | 546 | }); |
| 520 | 547 | |
| 548 | it("adopts a drifted checkout as the thread branch on a dedicated worktree", async () => { |
| 549 | const harness = await createHarness({ |
| 550 | seedFilesystemCheckpoints: false, |
| 551 | threadBranch: "t3code/original-branch", |
| 552 | localStatusRefName: "t3code/renamed-by-agent", |
| 553 | }); |
| 554 | |
| 555 | harness.provider.emit({ |
| 556 | type: "turn.completed", |
| 557 | eventId: EventId.make("evt-turn-completed-branch-drift"), |
| 558 | provider: ProviderDriverKind.make("codex"), |
| 559 | createdAt: "2026-01-01T00:00:00.000Z", |
| 560 | threadId: ThreadId.make("thread-1"), |
| 561 | turnId: asTurnId("turn-branch-drift"), |
| 562 | payload: { state: "completed" }, |
| 563 | }); |
| 564 | |
| 565 | await harness.drain(); |
| 566 | await waitForEvent( |
| 567 | harness.engine, |
| 568 | (event) => |
| 569 | event.type === "thread.meta-updated" && |
| 570 | (event as unknown as { payload: { branch?: string } }).payload.branch === |
| 571 | "t3code/renamed-by-agent", |
| 572 | ); |
| 573 | |
| 574 | const snapshot = await harness.readModel(); |
| 575 | const thread = snapshot.threads.find((entry) => entry.id === ThreadId.make("thread-1")); |
| 576 | expect(thread?.branch).toBe("t3code/renamed-by-agent"); |
| 577 | }); |
| 578 | |
| 579 | it("does not adopt a drifted checkout when the worktree is shared by another thread", async () => { |
| 580 | const harness = await createHarness({ |
| 581 | seedFilesystemCheckpoints: false, |
| 582 | threadBranch: "t3code/original-branch", |
| 583 | localStatusRefName: "t3code/renamed-by-agent", |
| 584 | secondThreadSharingWorktree: true, |
| 585 | }); |
| 586 | |
| 587 | harness.provider.emit({ |
| 588 | type: "turn.completed", |
| 589 | eventId: EventId.make("evt-turn-completed-branch-drift-shared"), |
| 590 | provider: ProviderDriverKind.make("codex"), |
| 591 | createdAt: "2026-01-01T00:00:00.000Z", |
| 592 | threadId: ThreadId.make("thread-1"), |
| 593 | turnId: asTurnId("turn-branch-drift-shared"), |
| 594 | payload: { state: "completed" }, |
| 595 | }); |
| 596 | |
| 597 | await harness.drain(); |
| 598 | |
| 599 | const snapshot = await harness.readModel(); |
| 600 | const thread = snapshot.threads.find((entry) => entry.id === ThreadId.make("thread-1")); |
| 601 | expect(thread?.branch).toBe("t3code/original-branch"); |
| 602 | }); |
| 603 | |
| 604 | it("does not adopt a temporary placeholder checkout as the thread branch", async () => { |
| 605 | const harness = await createHarness({ |
| 606 | seedFilesystemCheckpoints: false, |
| 607 | threadBranch: "t3code/original-branch", |
| 608 | localStatusRefName: "t3code/0a1b2c3d", |
| 609 | }); |
| 610 | |
| 611 | harness.provider.emit({ |
| 612 | type: "turn.completed", |
| 613 | eventId: EventId.make("evt-turn-completed-branch-drift-temp"), |
| 614 | provider: ProviderDriverKind.make("codex"), |
| 615 | createdAt: "2026-01-01T00:00:00.000Z", |
| 616 | threadId: ThreadId.make("thread-1"), |
| 617 | turnId: asTurnId("turn-branch-drift-temp"), |
| 618 | payload: { state: "completed" }, |
| 619 | }); |
| 620 | |
| 621 | await harness.drain(); |
| 622 | |
| 623 | const snapshot = await harness.readModel(); |
| 624 | const thread = snapshot.threads.find((entry) => entry.id === ThreadId.make("thread-1")); |
| 625 | expect(thread?.branch).toBe("t3code/original-branch"); |
| 626 | }); |
| 627 | |
| 521 | 628 | it("ignores auxiliary thread turn completion while primary turn is active", async () => { |
| 522 | 629 | const harness = await createHarness({ seedFilesystemCheckpoints: false }); |
| 523 | 630 | const createdAt = "2026-01-01T00:00:00.000Z"; |