| @@ -4,6 +4,7 @@ import { · 3 unmodified lines |
| 4 | 4 | CommandId, |
| 5 | 5 | EnvironmentId, |
| 6 | 6 | type OrchestrationV2ThreadProjection, |
| 7 | + type OrchestrationV2ThreadShell, |
| 7 | 8 | type Project, |
| 8 | 9 | ProjectId, |
| 9 | 10 | ProviderInstanceId, |
| @@ -153,6 +154,12 @@ interface HarnessOptions { · 143 unmodified lines |
| 153 | 154 | readonly worktreePath: string | null; |
| 154 | 155 | readonly active?: boolean; |
| 155 | 156 | }>; |
| 157 | + readonly archivedProjectThread?: { |
| 158 | + readonly id: ThreadId; |
| 159 | + readonly title: string; |
| 160 | + readonly branch: string | null; |
| 161 | + readonly worktreePath: string | null; |
| 162 | + }; |
| 156 | 163 | } |
| 157 | 164 | |
| 158 | 165 | const makeHarness = (options: HarnessOptions = {}) => { |
| @@ -242,31 +249,52 @@ const makeHarness = (options: HarnessOptions = {}) => { · 83 unmodified lines |
| 242 | 249 | : Option.none(), |
| 243 | 250 | ), |
| 244 | 251 | ); |
| 245 | | − const listProjectThreads = vi.fn(() => |
| 246 | | − Effect.succeed( |
| 247 | | − ( |
| 248 | | − options.projectThreads ?? [ |
| 252 | + const projectThreadShells = ( |
| 253 | + options.projectThreads ?? [ |
| 254 | + { |
| 255 | + id: threadId, |
| 256 | + title: "Worktree test thread", |
| 257 | + branch: thread?.branch ?? null, |
| 258 | + worktreePath: thread?.worktreePath ?? null, |
| 259 | + }, |
| 260 | + ] |
| 261 | + ).map( |
| 262 | + (item) => |
| 263 | + ({ |
| 264 | + id: item.id, |
| 265 | + projectId, |
| 266 | + title: item.title, |
| 267 | + branch: item.branch, |
| 268 | + worktreePath: item.worktreePath, |
| 269 | + status: item.active === true ? "running" : "idle", |
| 270 | + activeRunId: item.active === true ? "run-active" : null, |
| 271 | + lineage: { relationshipToParent: "none" }, |
| 272 | + }) as OrchestrationV2ThreadShell, |
| 273 | + ); |
| 274 | + const archivedThreadShells = |
| 275 | + options.archivedProjectThread === undefined |
| 276 | + ? [] |
| 277 | + : ([ |
| 249 | 278 | { |
| 250 | | − id: threadId, |
| 251 | | − title: "Worktree test thread", |
| 252 | | − branch: thread?.branch ?? null, |
| 253 | | − worktreePath: thread?.worktreePath ?? null, |
| 254 | | − }, |
| 255 | | − ] |
| 256 | | − ).map( |
| 257 | | − (item) => |
| 258 | | − ({ |
| 259 | | − id: item.id, |
| 279 | + ...(projectThreadShells[0] ?? makeProjection({}).thread), |
| 280 | + id: options.archivedProjectThread.id, |
| 260 | 281 | projectId, |
| 261 | | − title: item.title, |
| 262 | | − branch: item.branch, |
| 263 | | − worktreePath: item.worktreePath, |
| 264 | | − status: item.active === true ? "running" : "idle", |
| 265 | | − activeRunId: item.active === true ? "run-active" : null, |
| 282 | + title: options.archivedProjectThread.title, |
| 283 | + branch: options.archivedProjectThread.branch, |
| 284 | + worktreePath: options.archivedProjectThread.worktreePath, |
| 285 | + activeRunId: null, |
| 286 | + archivedAt: "2026-01-02T00:00:00.000Z", |
| 266 | 287 | lineage: { relationshipToParent: "none" }, |
| 267 | | − }) as never, |
| 268 | | − ), |
| 269 | | − ), |
| 288 | + }, |
| 289 | + ] as ReadonlyArray<OrchestrationV2ThreadShell>); |
| 290 | + const listProjectThreads = vi.fn(() => Effect.succeed(projectThreadShells)); |
| 291 | + const getShellSnapshot = vi.fn(() => |
| 292 | + Effect.succeed({ |
| 293 | + schemaVersion: 1, |
| 294 | + snapshotSequence: 1, |
| 295 | + threads: projectThreadShells, |
| 296 | + archivedThreads: archivedThreadShells, |
| 297 | + } as never), |
| 270 | 298 | ); |
| 271 | 299 | const removeWorktree = vi.fn((_: unknown) => |
| 272 | 300 | options.removeWorktreeFails |
| @@ -465,6 +493,7 @@ const makeHarness = (options: HarnessOptions = {}) => { · 192 unmodified lines |
| 465 | 493 | Layer.mergeAll( |
| 466 | 494 | Layer.mock(ThreadManagementService)({ |
| 467 | 495 | dispatch, |
| 496 | + getShellSnapshot, |
| 468 | 497 | getThreadProjection, |
| 469 | 498 | listProjectThreads, |
| 470 | 499 | sendToThread, |
| @@ -1448,6 +1477,34 @@ describe("t3_worktree_list", () => { · 977 unmodified lines |
| 1448 | 1477 | }); |
| 1449 | 1478 | }); |
| 1450 | 1479 | |
| 1480 | + it.effect("includes archived thread bindings retained on a physical checkout", () => { |
| 1481 | + const archivedThreadId = ThreadId.make("thread-archived-list-owner"); |
| 1482 | + const harness = makeHarness({ |
| 1483 | + worktrees: [{ path: workspaceRoot, refName: "dev" }], |
| 1484 | + archivedProjectThread: { |
| 1485 | + id: archivedThreadId, |
| 1486 | + title: "Archived checkout owner", |
| 1487 | + branch: "dev", |
| 1488 | + worktreePath: workspaceRoot, |
| 1489 | + }, |
| 1490 | + }); |
| 1491 | + return Effect.gen(function* () { |
| 1492 | + const result = yield* runList(harness, { limit: 1 }); |
| 1493 | + |
| 1494 | + expect(result.worktrees[0]).toMatchObject({ |
| 1495 | + path: workspaceRoot, |
| 1496 | + bindingCount: 2, |
| 1497 | + bindings: expect.arrayContaining([ |
| 1498 | + expect.objectContaining({ |
| 1499 | + threadId: archivedThreadId, |
| 1500 | + recordedWorktreePath: workspaceRoot, |
| 1501 | + active: false, |
| 1502 | + }), |
| 1503 | + ]), |
| 1504 | + }); |
| 1505 | + }); |
| 1506 | + }); |
| 1507 | + |
| 1451 | 1508 | it.effect("attributes a nested recorded cwd to its physical worktree root", () => { |
| 1452 | 1509 | const nestedPath = `${workspaceRoot}/packages/app`; |
| 1453 | 1510 | const harness = makeHarness({ |