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
4 CommandId,4 CommandId,
5 EnvironmentId,5 EnvironmentId,
6 type OrchestrationV2ThreadProjection,6 type OrchestrationV2ThreadProjection,
7 type OrchestrationV2ThreadShell,
7 type Project,8 type Project,
8 ProjectId,9 ProjectId,
9 ProviderInstanceId,10 ProviderInstanceId,
@@ -153,6 +154,12 @@ interface HarnessOptions { · 143 unmodified lines
153 readonly worktreePath: string | null;154 readonly worktreePath: string | null;
154 readonly active?: boolean;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
158const makeHarness = (options: HarnessOptions = {}) => {165const makeHarness = (options: HarnessOptions = {}) => {
@@ -242,31 +249,52 @@ const makeHarness = (options: HarnessOptions = {}) => { · 83 unmodified lines
242 : Option.none(),249 : Option.none(),
243 ),250 ),
244 );251 );
245 const listProjectThreads = vi.fn(() =>252 const projectThreadShells = (
246 Effect.succeed(253 options.projectThreads ?? [
247 (254 {
248 options.projectThreads ?? [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,279 ...(projectThreadShells[0] ?? makeProjection({}).thread),
251 title: "Worktree test thread",280 id: options.archivedProjectThread.id,
252 branch: thread?.branch ?? null,
253 worktreePath: thread?.worktreePath ?? null,
254 },
255 ]
256 ).map(
257 (item) =>
258 ({
259 id: item.id,
260 projectId,281 projectId,
261 title: item.title,282 title: options.archivedProjectThread.title,
262 branch: item.branch,283 branch: options.archivedProjectThread.branch,
263 worktreePath: item.worktreePath,284 worktreePath: options.archivedProjectThread.worktreePath,
264 status: item.active === true ? "running" : "idle",285 activeRunId: null,
265 activeRunId: item.active === true ? "run-active" : null,286 archivedAt: "2026-01-02T00:00:00.000Z",
266 lineage: { relationshipToParent: "none" },287 lineage: { relationshipToParent: "none" },
267 }) as never,288 },
268 ),289 ] as ReadonlyArray<OrchestrationV2ThreadShell>);
269 ),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 const removeWorktree = vi.fn((_: unknown) =>299 const removeWorktree = vi.fn((_: unknown) =>
272 options.removeWorktreeFails300 options.removeWorktreeFails
@@ -465,6 +493,7 @@ const makeHarness = (options: HarnessOptions = {}) => { · 192 unmodified lines
465 Layer.mergeAll(493 Layer.mergeAll(
466 Layer.mock(ThreadManagementService)({494 Layer.mock(ThreadManagementService)({
467 dispatch,495 dispatch,
496 getShellSnapshot,
468 getThreadProjection,497 getThreadProjection,
469 listProjectThreads,498 listProjectThreads,
470 sendToThread,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 it.effect("attributes a nested recorded cwd to its physical worktree root", () => {1508 it.effect("attributes a nested recorded cwd to its physical worktree root", () => {
1452 const nestedPath = `${workspaceRoot}/packages/app`;1509 const nestedPath = `${workspaceRoot}/packages/app`;
1453 const harness = makeHarness({1510 const harness = makeHarness({
apps/server/src/mcp/WorktreeMcpService.ts+8−3
@@ -150,9 +150,14 @@ const make = Effect.gen(function* () { · 149 unmodified lines
150 const loadProjectThreads = (150 const loadProjectThreads = (
151 projectId: ProjectId,151 projectId: ProjectId,
152 ): Effect.Effect<ReadonlyArray<OrchestrationV2ThreadShell>, WorktreeMcpFailure> =>152 ): Effect.Effect<ReadonlyArray<OrchestrationV2ThreadShell>, WorktreeMcpFailure> =>
153 threadManagement153 threadManagement.getShellSnapshot().pipe(
154 .listProjectThreads({ projectId, includeSubagents: true })154 Effect.map((snapshot) =>
155 .pipe(asOperationFailed(`Unable to list threads in project ${projectId}`));155 [...snapshot.threads, ...snapshot.archivedThreads].filter(
156 (thread) => thread.projectId === projectId,
157 ),
158 ),
159 asOperationFailed(`Unable to list threads in project ${projectId}`),
160 );
156 161
157 const readWorkspaceStatus = (workspacePath: string) =>162 const readWorkspaceStatus = (workspacePath: string) =>
158 gitWorkflow163 gitWorkflow