History

fix(mcp): list archived workspace bindings

Julius Marminge committed 2 months ago

Commit 96d40cf1 parent 927e36f2 files · +87 −25Browse files

Unified · Split

2 files
  1. apps/server/src/mcp/WorktreeMcpService.test.ts +79 −22
  2. apps/server/src/mcp/WorktreeMcpService.ts +8 −3
apps/server/src/mcp/WorktreeMcpService.test.ts+79−22
@@ -4,6 +4,7 @@ import { · 3 unmodified lines
44 CommandId,
55 EnvironmentId,
66 type OrchestrationV2ThreadProjection,
7+ type OrchestrationV2ThreadShell,
78 type Project,
89 ProjectId,
910 ProviderInstanceId,
@@ -153,6 +154,12 @@ interface HarnessOptions { · 143 unmodified lines
153154 readonly worktreePath: string | null;
154155 readonly active?: boolean;
155156 }>;
157+ readonly archivedProjectThread?: {
158+ readonly id: ThreadId;
159+ readonly title: string;
160+ readonly branch: string | null;
161+ readonly worktreePath: string | null;
162+ };
156163 }
157164
158165 const makeHarness = (options: HarnessOptions = {}) => {
@@ -242,31 +249,52 @@ const makeHarness = (options: HarnessOptions = {}) => { · 83 unmodified lines
242249 : Option.none(),
243250 ),
244251 );
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+ : ([
249278 {
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,
260281 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",
266287 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),
270298 );
271299 const removeWorktree = vi.fn((_: unknown) =>
272300 options.removeWorktreeFails
@@ -465,6 +493,7 @@ const makeHarness = (options: HarnessOptions = {}) => { · 192 unmodified lines
465493 Layer.mergeAll(
466494 Layer.mock(ThreadManagementService)({
467495 dispatch,
496+ getShellSnapshot,
468497 getThreadProjection,
469498 listProjectThreads,
470499 sendToThread,
@@ -1448,6 +1477,34 @@ describe("t3_worktree_list", () => { · 977 unmodified lines
14481477 });
14491478 });
14501479
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+
14511508 it.effect("attributes a nested recorded cwd to its physical worktree root", () => {
14521509 const nestedPath = `${workspaceRoot}/packages/app`;
14531510 const harness = makeHarness({
apps/server/src/mcp/WorktreeMcpService.ts+8−3
@@ -150,9 +150,14 @@ const make = Effect.gen(function* () { · 149 unmodified lines
150150 const loadProjectThreads = (
151151 projectId: ProjectId,
152152 ): Effect.Effect<ReadonlyArray<OrchestrationV2ThreadShell>, WorktreeMcpFailure> =>
153− threadManagement
154− .listProjectThreads({ projectId, includeSubagents: true })
155− .pipe(asOperationFailed(`Unable to list threads in project ${projectId}`));
153+ threadManagement.getShellSnapshot().pipe(
154+ Effect.map((snapshot) =>
155+ [...snapshot.threads, ...snapshot.archivedThreads].filter(
156+ (thread) => thread.projectId === projectId,
157+ ),
158+ ),
159+ asOperationFailed(`Unable to list threads in project ${projectId}`),
160+ );
156161
157162 const readWorkspaceStatus = (workspacePath: string) =>
158163 gitWorkflow