Open History to see the last commit
1import * as AssetSigner from "../../assets/AssetSigner.ts";
2import * as DocumentText from "../../provider/DocumentText.ts";
3import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
4import * as NodeServices from "@effect/platform-node/NodeServices";
5import { expect, it } from "@effect/vitest";
6import {
7 DEFAULT_SERVER_SETTINGS,
8 ChatImageAttachment,
9 CommandId,
10 EnvironmentId,
11 NodeId,
12 OrchestratorMcpThreadReadResult,
13 ProjectId,
14 ProviderDriverKind,
15 ProviderInstanceId,
16 RunId,
17 ScheduledTaskError,
18 ScheduledTask,
19 RuntimeRequestId,
20 ThreadId,
21 TurnItemId,
22 type OrchestrationV2ProjectedTurnItem,
23 type OrchestrationV2ThreadShell,
24} from "@t3tools/contracts";
25import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
26import * as Deferred from "effect/Deferred";
27import * as Effect from "effect/Effect";
28import * as TestClock from "effect/testing/TestClock";
29import * as Layer from "effect/Layer";
30import * as Schema from "effect/Schema";
31import { McpAttachmentInput } from "./attachment/input.ts";
32import { McpSchema, McpServer } from "effect/ai";
33import { FetchHttpClient } from "effect/http";
34β
35import {
36 OrchestratorCommandRejectedError,
37 OrchestratorDispatchError,
38 OrchestratorProjectionError,
39 OrchestratorProviderSelectionBusyError,
40} from "../../orchestration-v2/Orchestrator.ts";
41β
42import * as ServerSecretStore from "../../auth/ServerSecretStore.ts";
43import * as ServerConfig from "../../config.ts";
44import * as ProviderAdapterRegistry from "../../orchestration-v2/ProviderAdapterRegistry.ts";
45import * as ThreadLaunchService from "../../orchestration-v2/ThreadLaunchService.ts";
46import * as GitVcsDriver from "../../vcs/GitVcsDriver.ts";
47import * as ThreadManagement from "../../orchestration-v2/ThreadManagementService.ts";
48import * as ThreadSearch from "../../orchestration-v2/ThreadSearch.ts";
49import * as PreviewBrowser from "../../preview/PreviewBrowser.ts";
50import * as ProjectService from "../../project/ProjectService.ts";
51import * as ProviderRegistry from "../../provider/ProviderRegistry.ts";
52import * as SecretRequests from "../../secrets/SecretRequests.ts";
53import * as RemoteDelegation from "../../peer/RemoteDelegation.ts";
54import * as ScheduledTaskService from "../../scheduledTasks/ScheduledTaskService.ts";
55import * as ServerSettings from "../../serverSettings.ts";
56import * as McpHttpServer from "../McpHttpServer.ts";
57import * as McpInvocationContext from "../McpInvocationContext.ts";
58import * as McpToolAccessTestkit from "../McpToolAccess.testkit.ts";
59import * as AnalyticsService from "../../telemetry/AnalyticsService.ts";
60import { dispatchFailure } from "../threadAccess.ts";
61import * as EnvironmentHandlers from "./environment/handlers.ts";
62import * as AttachmentHandlers from "./attachment/handlers.ts";
63import { ThreadToolkit } from "./thread/tools.ts";
64import * as PeerForwarding from "../../peer/PeerForwarding.ts";
65import * as PeerLinkRequests from "../../peer/PeerLinkRequests.ts";
66import * as PeerLinks from "../../peer/PeerLinks.ts";
67β
68const layerOrchestratorToolkit = McpHttpServer.layerOrchestratorToolkit.pipe(
69 Layer.provide(Layer.mock(ThreadLaunchService.ThreadLaunchService)({})),
70 Layer.provide(Layer.mock(GitVcsDriver.GitVcsDriver)({})),
71);
72β
73const declaredFailure = (result: McpSchema.CallToolResult) => {
74 const text = result.content[0];
75 return result.isError === true && text?.type === "text" ? JSON.parse(text.text) : undefined;
76};
77import { htmlRenderFromToolItem } from "@t3tools/shared/toolOutput";
78β
79const decodeMcpAttachmentInput = Schema.decodeUnknownEffect(McpAttachmentInput);
80const decodeThreadReadResult = Schema.decodeUnknownEffect(OrchestratorMcpThreadReadResult);
81const decodeThreadSearchResult = Schema.decodeUnknownEffect(
82 ThreadToolkit.tools.t3_thread_search.successSchema,
83);
84β
85const layerThreadToolkit = McpHttpServer.layerThreadToolkit.pipe(
86 Layer.provide(Layer.mock(ThreadSearch.ThreadSearch)({})),
87 Layer.provide(Layer.mock(ScheduledTaskService.ScheduledTaskService)({})),
88 Layer.provide(Layer.mock(PeerForwarding.PeerForwarding)({})),
89 Layer.provide(Layer.mock(PeerLinkRequests.PeerLinkRequests)({})),
90 Layer.provide(Layer.mock(PeerLinks.PeerLinks)({})),
91);
92β
93const threadId = ThreadId.make("mcp-core-thread");
94const scope: McpInvocationContext.McpInvocationScope = {
95 environmentId: EnvironmentId.make("mcp-core-environment"),
96 requestNamespace: "mcp-core-session",
97 thread: {
98 threadId,
99 providerSessionId: "mcp-core-session",
100 providerInstanceId: ProviderInstanceId.make("codex"),
101 },
102 client: undefined,
103 issuedAt: 0,
104 capabilities: new Set(["orchestration"]),
105};
106const client = McpSchema.McpServerClient.of({
107 clientId: 1,
108 protocolVersion: "2025-06-18",
109 clientCapabilities: {},
110 clientInfo: { name: "mcp-core", version: "1" },
111 initializePayload: {
112 protocolVersion: "2025-06-18",
113 capabilities: {},
114 clientInfo: { name: "mcp-core", version: "1" },
115 },
116 getClient: Effect.die("unused"),
117});
118β
119it.effect("checks capability through the production registration", () =>
120 Effect.gen(function* () {
121 const server = yield* McpServer.McpServer;
122 expect(server.tools.some(({ tool }) => tool.name === "t3_thread_organize")).toBe(true);
123 const result = yield* server
124 .callTool({ name: "t3_thread_organize", arguments: { action: "pin" } })
125 .pipe(
126 Effect.provideService(McpInvocationContext.McpInvocationContext, {
127 ...scope,
128 capabilities: new Set<never>(),
129 }),
130 Effect.provideService(McpSchema.McpServerClient, client),
131 );
132 expect(declaredFailure(result)).toMatchObject({ code: "capability_denied" });
133 expect(result.isError).toBe(true);
134 expect(result.structuredContent).toBeUndefined();
135 }).pipe(
136 Effect.provide(
137 layerThreadToolkit.pipe(
138 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
139 Layer.provide(NodeCrypto.layer),
140 Layer.provide(McpToolAccessTestkit.liveThreadsLayer),
141 ),
142 ),
143 ),
144);
145β
146it.effect("an answered question reports already answered instead of not found", () => {
147 const shell = McpToolAccessTestkit.liveThreadShell(threadId);
148 const requestId = RuntimeRequestId.make("answered-question");
149 const projection = McpToolAccessTestkit.idleThreadProjection(shell);
150 return Effect.gen(function* () {
151 const server = yield* McpServer.McpServer;
152 const respond = (id: string) =>
153 server
154 .callTool({
155 name: "t3_pending_request",
156 arguments: { action: "respond", requestId: id, answers: { size: "large" } },
157 })
158 .pipe(
159 Effect.provideService(McpInvocationContext.McpInvocationContext, scope),
160 Effect.provideService(McpSchema.McpServerClient, client),
161 );
162 expect(declaredFailure(yield* respond(requestId))).toMatchObject({
163 code: "invalid_request",
164 message: "The user-input request was already answered.",
165 });
166 expect(declaredFailure(yield* respond("missing-question"))).toMatchObject({
167 code: "invalid_request",
168 message: "The pending user-input request was not found.",
169 });
170 }).pipe(
171 Effect.provide(
172 layerThreadToolkit.pipe(
173 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
174 Layer.provide(NodeCrypto.layer),
175 Layer.provide(
176 Layer.mock(ThreadManagement.ThreadManagementService)({
177 getThreadShell: () => Effect.succeed(shell),
178 getProjectThreadRecords: () =>
179 Effect.succeed({
180 ...projection,
181 runtimeRequests: [
182 {
183 id: requestId,
184 nodeId: NodeId.make("root"),
185 providerTurnId: null,
186 nativeRequestRef: null,
187 kind: "user_input",
188 status: "resolved",
189 responseCapability: { type: "message" },
190 createdAt: shell.createdAt,
191 resolvedAt: shell.updatedAt,
192 answers: { size: "large" },
193 },
194 ],
195 turnItems: [
196 {
197 id: TurnItemId.make("answered-question-item"),
198 threadId,
199 runId: null,
200 nodeId: null,
201 providerThreadId: null,
202 providerTurnId: null,
203 nativeItemRef: null,
204 parentItemId: null,
205 ordinal: 0,
206 status: "completed",
207 title: null,
208 startedAt: shell.createdAt,
209 completedAt: shell.updatedAt,
210 updatedAt: shell.updatedAt,
211 type: "user_input_request",
212 requestId,
213 questions: [],
214 },
215 ],
216 }),
217 dispatch: () => Effect.die("an answered question must not dispatch again"),
218 }),
219 ),
220 ),
221 ),
222 );
223});
224β
225it.effect("a pending question read keeps option previews", () => {
226 const shell = McpToolAccessTestkit.liveThreadShell(threadId);
227 const requestId = RuntimeRequestId.make("previewed-question");
228 const projection = McpToolAccessTestkit.idleThreadProjection(shell);
229 const questions = [
230 {
231 id: "post",
232 header: "Review",
233 question: "Post it?",
234 options: [{ label: "Post", description: "Publish the draft", preview: "Full proposed text" }],
235 },
236 ];
237 return Effect.gen(function* () {
238 const server = yield* McpServer.McpServer;
239 const result = yield* server
240 .callTool({ name: "t3_pending_request", arguments: { action: "read", requestId } })
241 .pipe(
242 Effect.provideService(McpInvocationContext.McpInvocationContext, scope),
243 Effect.provideService(McpSchema.McpServerClient, client),
244 );
245 expect(result.structuredContent).toEqual({ requestId, questions });
246 }).pipe(
247 Effect.provide(
248 layerThreadToolkit.pipe(
249 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
250 Layer.provide(NodeCrypto.layer),
251 Layer.provide(
252 Layer.mock(ThreadManagement.ThreadManagementService)({
253 getThreadShell: () => Effect.succeed(shell),
254 getProjectThreadRecords: () =>
255 Effect.succeed({
256 ...projection,
257 runtimeRequests: [
258 {
259 id: requestId,
260 nodeId: NodeId.make("root"),
261 providerTurnId: null,
262 nativeRequestRef: null,
263 kind: "user_input",
264 status: "pending",
265 responseCapability: { type: "message" },
266 createdAt: shell.createdAt,
267 resolvedAt: null,
268 },
269 ],
270 turnItems: [
271 {
272 id: TurnItemId.make("previewed-question-item"),
273 threadId,
274 runId: null,
275 nodeId: null,
276 providerThreadId: null,
277 providerTurnId: null,
278 nativeItemRef: null,
279 parentItemId: null,
280 ordinal: 0,
281 status: "running",
282 title: null,
283 startedAt: shell.createdAt,
284 completedAt: null,
285 updatedAt: shell.updatedAt,
286 type: "user_input_request",
287 requestId,
288 questions,
289 },
290 ],
291 }),
292 }),
293 ),
294 ),
295 ),
296 );
297});
298β
299it("bounds public command rejections and redacts internal dispatch causes", () => {
300 const command = { commandId: CommandId.make("mcp-core-command"), commandType: "thread.settle" };
301 expect(
302 dispatchFailure(new OrchestratorDispatchError({ ...command, cause: "π".repeat(1001) }))
303 .message,
304 ).toBe("π".repeat(1000));
305 expect(
306 dispatchFailure(
307 new OrchestratorCommandRejectedError({ ...command, cause: "Run is not queued." }),
308 ).message,
309 ).toBe("Run is not queued.");
310 const busy = new OrchestratorProviderSelectionBusyError({
311 commandId: command.commandId,
312 threadId,
313 });
314 expect(dispatchFailure(busy)).toMatchObject({ code: "invalid_request", message: busy.message });
315 for (const cause of [
316 undefined,
317 "",
318 new Error("private-storage-path"),
319 { message: "private-storage-path" },
320 ]) {
321 expect(dispatchFailure(new OrchestratorDispatchError({ ...command, cause }))).toMatchObject({
322 code: "orchestration_error",
323 message: "The operation could not be completed.",
324 });
325 expect(
326 dispatchFailure(new OrchestratorCommandRejectedError({ ...command, cause })),
327 ).toMatchObject({
328 code: "orchestration_error",
329 message: "The operation could not be completed.",
330 });
331 }
332 expect(
333 dispatchFailure(new OrchestratorProjectionError({ threadId, cause: "private-storage-path" })),
334 ).toMatchObject({
335 code: "orchestration_error",
336 message: "The operation could not be completed.",
337 });
338});
339β
340const fullAccessThreadId = ThreadId.make("mcp-core-full-access-thread");
341β
342it.effect("returns an HTML render reference that Codex and Claude tool rows both carry", () =>
343 Effect.gen(function* () {
344 const server = yield* McpServer.McpServer;
345 const result = yield* server
346 .callTool({
347 name: "html_render",
348 arguments: { html: "<p>Revenue</p>", title: "Revenue", height: 240 },
349 })
350 .pipe(
351 Effect.provideService(McpInvocationContext.McpInvocationContext, scope),
352 Effect.provideService(McpSchema.McpServerClient, client),
353 );
354 const reference = htmlRenderFromToolItem({
355 toolName: "t3-code.html_render",
356 output: result.structuredContent,
357 });
358 expect(reference).toMatchObject({ title: "Revenue", height: 240 });
359 expect(
360 htmlRenderFromToolItem({ toolName: "mcp__t3-code__html_render", output: result.content }),
361 ).toEqual(reference);
362 // Only a full-access thread may have the server read a file for it.
363 const byPath = (name: string, caller: McpInvocationContext.McpInvocationScope) =>
364 server
365 .callTool({
366 name,
367 arguments: {
368 path: "/t3-missing-dir/revenue.html",
369 ...(name === "html_render" ? { title: "Revenue", height: 240 } : {}),
370 },
371 })
372 .pipe(
373 Effect.provideService(McpInvocationContext.McpInvocationContext, caller),
374 Effect.provideService(McpSchema.McpServerClient, client),
375 // html_preview reports failures as plain text, html_render as a declared failure.
376 Effect.map((result) => (result.isError ? JSON.stringify(result.content) : undefined)),
377 );
378 const fullAccess = { ...scope, thread: { ...scope.thread!, threadId: fullAccessThreadId } };
379 for (const name of ["html_preview", "html_render"]) {
380 expect(yield* byPath(name, scope), name).toContain("only full-access threads");
381 expect(yield* byPath(name, fullAccess), name).toContain("not a readable HTML document");
382 }
383 }).pipe(
384 Effect.provide(
385 McpHttpServer.layerHtmlToolkit.pipe(
386 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
387 Layer.provide(McpProviderSessions.layer),
388 Layer.provide(PreviewBrowser.layer),
389 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-html-render-" })),
390 Layer.provide(
391 DocumentText.layer.pipe(
392 Layer.provideMerge(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)),
393 ),
394 ),
395 Layer.provide(FetchHttpClient.layer),
396 Layer.provide(
397 Layer.mock(ThreadManagement.ThreadManagementService)({
398 getThreadShell: (id) =>
399 Effect.succeed({
400 id,
401 deletedAt: null,
402 archivedAt: null,
403 activeRunId: RunId.make("mcp-core-run"),
404 providerInstanceId: ProviderInstanceId.make("codex"),
405 runtimeMode: id === fullAccessThreadId ? "full-access" : "approval-required",
406 interactionMode: "default",
407 } as OrchestrationV2ThreadShell),
408 }),
409 ),
410 ),
411 ),
412 ),
413);
414β
415it.effect("records only failed calls of tools that hand off no work", () => {
416 // Recording runs detached from the tool response, so the first event is awaited.
417 const recorded = Deferred.makeUnsafe<{ event: string; properties: object }>();
418 return Effect.gen(function* () {
419 const server = yield* McpServer.McpServer;
420 const organize = (capabilities: McpInvocationContext.McpInvocationScope["capabilities"]) =>
421 server.callTool({ name: "t3_thread_organize", arguments: { action: "pin" } }).pipe(
422 Effect.provideService(McpInvocationContext.McpInvocationContext, {
423 ...scope,
424 capabilities,
425 }),
426 Effect.provideService(McpSchema.McpServerClient, client),
427 );
428 expect((yield* organize(scope.capabilities)).isError).not.toBe(true);
429 expect(declaredFailure(yield* organize(new Set<never>()))).toMatchObject({
430 code: "capability_denied",
431 });
432 // The successful call ran first, so it would be the first event if it were recorded.
433 expect(yield* Deferred.await(recorded)).toMatchObject({
434 event: "mcp.tool.invoked",
435 properties: { tool: "t3_thread_organize", outcome: "error" },
436 });
437 }).pipe(
438 Effect.provide(
439 layerThreadToolkit.pipe(
440 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
441 Layer.provide(NodeCrypto.layer),
442 Layer.provide(
443 Layer.mock(ThreadManagement.ThreadManagementService)({
444 getThreadShell: (id) => Effect.succeed(McpToolAccessTestkit.liveThreadShell(id)),
445 getProjectThreadRecords: ({ threadId: id }) =>
446 Effect.succeed(
447 McpToolAccessTestkit.idleThreadProjection(McpToolAccessTestkit.liveThreadShell(id)),
448 ),
449 dispatch: () => Effect.succeed({ sequence: 1 } as never),
450 }),
451 ),
452 Layer.provide(
453 Layer.succeed(
454 AnalyticsService.AnalyticsService,
455 AnalyticsService.AnalyticsService.of({
456 record: (event, properties) =>
457 Deferred.succeed(recorded, { event, properties: properties ?? {} }).pipe(
458 Effect.asVoid,
459 ),
460 flush: Effect.void,
461 recordOwners: () => Effect.void,
462 }),
463 ),
464 ),
465 ),
466 ),
467 );
468});
469β
470it.effect("rejects an unknown organize target instead of archiving the calling thread", () => {
471 const dispatched: Array<unknown> = [];
472 return Effect.gen(function* () {
473 const server = yield* McpServer.McpServer;
474 const outcome = yield* server
475 .callTool({
476 name: "t3_thread_organize",
477 arguments: { action: "archive", threadIds: ["other-thread"] },
478 })
479 .pipe(
480 Effect.provideService(McpInvocationContext.McpInvocationContext, scope),
481 Effect.provideService(McpSchema.McpServerClient, client),
482 Effect.result,
483 );
484 expect(dispatched).toEqual([]);
485 expect(outcome).toMatchObject({
486 _tag: "Failure",
487 failure: { message: expect.stringContaining('Unknown argument "threadIds"') },
488 });
489 }).pipe(
490 Effect.provide(
491 layerThreadToolkit.pipe(
492 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
493 Layer.provide(NodeCrypto.layer),
494 Layer.provide(
495 Layer.mock(ThreadManagement.ThreadManagementService)({
496 getThreadShell: (id) => Effect.succeed(McpToolAccessTestkit.liveThreadShell(id)),
497 getProjectThreadRecords: ({ threadId: id }) =>
498 Effect.succeed(
499 McpToolAccessTestkit.idleThreadProjection(McpToolAccessTestkit.liveThreadShell(id)),
500 ),
501 dispatch: (command) => {
502 dispatched.push(command);
503 return Effect.succeed({ sequence: 1 } as never);
504 },
505 }),
506 ),
507 ),
508 ),
509 );
510});
511β
512it.effect("keeps unexpected handler defects private through the production registration", () =>
513 Effect.gen(function* () {
514 const server = yield* McpServer.McpServer;
515 const result = yield* server
516 .callTool({ name: "t3_thread_organize", arguments: { action: "pin" } })
517 .pipe(
518 Effect.provideService(McpInvocationContext.McpInvocationContext, scope),
519 Effect.provideService(McpSchema.McpServerClient, client),
520 );
521 expect(result.isError).toBe(true);
522 expect(result.structuredContent).toBeUndefined();
523 expect(result.content).toEqual([
524 { type: "text", text: "Tool execution failed due to an internal server error." },
525 ]);
526 }).pipe(
527 Effect.provide(
528 layerThreadToolkit.pipe(
529 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
530 Layer.provide(NodeCrypto.layer),
531 Layer.provide(
532 Layer.mock(ThreadManagement.ThreadManagementService)({
533 getThreadShell: () => Effect.die(new Error("private-storage-path")),
534 }),
535 ),
536 ),
537 ),
538 ),
539);
540β
541it("keeps MCP preference output allowlisted and Unicode-bounded", () => {
542 const settings = {
543 ...DEFAULT_SERVER_SETTINGS,
544 privateCredential: "must-not-escape",
545 sourceControlWritingStyle: {
546 ...DEFAULT_SERVER_SETTINGS.sourceControlWritingStyle,
547 customInstructions: "π".repeat(4001),
548 },
549 };
550 const result = EnvironmentHandlers.preferences(settings);
551 expect(result).not.toHaveProperty("privateCredential");
552 expect(result).not.toHaveProperty("providers");
553 expect(result.sourceControlWritingStyle).toMatchObject({
554 customInstructions: "π".repeat(4000),
555 truncated: true,
556 });
557});
558β
559it.effect("resolves reused attachment references from stored metadata", () =>
560 Effect.gen(function* () {
561 const stored = ChatImageAttachment.make({
562 type: "image",
563 id: "owned-image",
564 name: "original.png",
565 mimeType: "image/png",
566 sizeBytes: 12,
567 source: {
568 kind: "snap-shot",
569 capturedAt: "2026-09-10T00:00:00.000Z",
570 appName: "Terminal",
571 windowTitle: "Test",
572 accessibility: { format: "flat-text", text: "Stored context", truncated: false },
573 },
574 });
575 const forged = yield* decodeMcpAttachmentInput({
576 type: "image",
577 id: stored.id,
578 name: "changed.jpg",
579 mimeType: "image/jpeg",
580 sizeBytes: 99,
581 });
582 const result = yield* AttachmentHandlers.resolveAttachmentReferences([forged], [stored]);
583 expect(result).toEqual([stored]);
584 const failure = yield* AttachmentHandlers.resolveAttachmentReferences(
585 [{ ...forged, id: "other-image" }],
586 [stored],
587 ).pipe(Effect.flip);
588 expect(failure.code).toBe("invalid_request");
589 }),
590);
591β
592const clientScope = (
593 access: McpInvocationContext.McpClientCaller["access"],
594): McpInvocationContext.McpInvocationScope => ({
595 environmentId: EnvironmentId.make("mcp-core-environment"),
596 requestNamespace: "client:session-1",
597 thread: undefined,
598 client: { sessionId: "session-1", label: "Claude Code", access },
599 issuedAt: 0,
600 capabilities: new Set(["orchestration", "worktree", "pull-requests"]),
601});
602β
603it.effect("a client caller targets any thread within its ceiling and cannot act as a thread", () =>
604 Effect.gen(function* () {
605 const server = yield* McpServer.McpServer;
606 const call = (
607 name: string,
608 args: Record<string, unknown>,
609 invocation: McpInvocationContext.McpInvocationScope,
610 ) =>
611 server
612 .callTool({ name, arguments: args })
613 .pipe(
614 Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
615 Effect.provideService(McpSchema.McpServerClient, client),
616 );
617β
618 const untargeted = yield* call("t3_thread_organize", { action: "pin" }, clientScope("auto"));
619 expect(declaredFailure(untargeted)).toMatchObject({ code: "target_required" });
620β
621 const pinned = yield* call(
622 "t3_thread_organize",
623 { action: "pin", threadId: "other-project-thread" },
624 clientScope("auto"),
625 );
626 expect(pinned.isError).toBe(false);
627 expect(pinned.structuredContent).toMatchObject({ sequence: 7 });
628β
629 const aboveCeiling = yield* call(
630 "t3_thread_organize",
631 { action: "pin", threadId: "other-project-thread" },
632 clientScope("approval-required"),
633 );
634 expect(declaredFailure(aboveCeiling)).toMatchObject({
635 code: "runtime_mode_escalation_denied",
636 });
637β
638 const forked = yield* call(
639 "t3_thread_transfer",
640 { action: "fork", sourcePoint: { type: "latest_stable" } },
641 clientScope("auto"),
642 );
643 expect(declaredFailure(forked)).toMatchObject({ code: "target_required" });
644 }).pipe(
645 Effect.provide(
646 layerThreadToolkit.pipe(
647 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
648 Layer.provide(NodeCrypto.layer),
649 Layer.provide(
650 Layer.mock(ThreadManagement.ThreadManagementService)({
651 getThreadShell: (id) =>
652 Effect.succeed(McpToolAccessTestkit.liveThreadShell(id, { runtimeMode: "auto" })),
653 getProjectThreadRecords: () =>
654 Effect.succeed({
655 thread: {
656 id: ThreadId.make("other-project-thread"),
657 projectId: "other-project",
658 runtimeMode: "auto",
659 interactionMode: "default",
660 deletedAt: null,
661 },
662 } as never),
663 dispatch: () => Effect.succeed({ sequence: 7, storedEvents: [] }),
664 }),
665 ),
666 ),
667 ),
668 ),
669);
670β
671it.effect("search names the calling thread, so an agent can tell its own match apart", () =>
672 Effect.gen(function* () {
673 const server = yield* McpServer.McpServer;
674 const search = (invocation: McpInvocationContext.McpInvocationScope) =>
675 server
676 .callTool({ name: "t3_thread_search", arguments: { query: "release notes" } })
677 .pipe(
678 Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
679 Effect.provideService(McpSchema.McpServerClient, client),
680 );
681β
682 const fromThread = yield* search(scope);
683 expect(fromThread.structuredContent).toMatchObject({
684 currentThreadId: threadId,
685 matches: [{ threadId }, { threadId: "earlier-thread" }],
686 });
687β
688 const fromClient = yield* search(clientScope("auto"));
689 expect(fromClient.structuredContent).toMatchObject({ currentThreadId: null });
690β
691 // A linked environment on an older version answers without the field.
692 const fromOlderPeer = yield* decodeThreadSearchResult({ matches: [] });
693 expect(fromOlderPeer).toEqual({ matches: [] });
694 }).pipe(
695 Effect.provide(
696 McpHttpServer.layerThreadToolkit.pipe(
697 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
698 Layer.provide(NodeCrypto.layer),
699 Layer.provide(McpToolAccessTestkit.liveThreadsLayer),
700 Layer.provide(
701 Layer.mock(ThreadSearch.ThreadSearch)({
702 searchThreads: () =>
703 Effect.succeed({
704 matches: [threadId, ThreadId.make("earlier-thread")].map((id) => ({
705 threadId: id,
706 projectId: ProjectId.make("project:mcp-test"),
707 title: "Release notes",
708 source: "title" as const,
709 snippet: "",
710 messageCreatedAt: null,
711 })),
712 }),
713 }),
714 ),
715 Layer.provide(Layer.mock(ScheduledTaskService.ScheduledTaskService)({})),
716 Layer.provide(Layer.mock(PeerForwarding.PeerForwarding)({})),
717 Layer.provide(Layer.mock(PeerLinkRequests.PeerLinkRequests)({})),
718 Layer.provide(Layer.mock(PeerLinks.PeerLinks)({})),
719 ),
720 ),
721 ),
722);
723β
724it.effect("a read-only client reads threads and is refused every write before it runs", () =>
725 Effect.gen(function* () {
726 const server = yield* McpServer.McpServer;
727 const call = (name: string, args: Record<string, unknown>) =>
728 server
729 .callTool({ name, arguments: args })
730 .pipe(
731 Effect.provideService(
732 McpInvocationContext.McpInvocationContext,
733 clientScope("read-only"),
734 ),
735 Effect.provideService(McpSchema.McpServerClient, client),
736 );
737β
738 const configuration = yield* call("t3_thread_configure", {
739 threadId: "other-project-thread",
740 });
741 expect(configuration.isError).toBe(false);
742 expect(configuration.structuredContent).toMatchObject({ runtimeMode: "auto" });
743β
744 const pinned = yield* call("t3_thread_organize", {
745 action: "pin",
746 threadId: "other-project-thread",
747 });
748 expect(declaredFailure(pinned)).toMatchObject({ code: "capability_denied" });
749 expect(dispatched).toEqual([]);
750β
751 const configure = yield* call("t3_thread_configure", {
752 threadId: "other-project-thread",
753 modelSelection: { instanceId: "codex", model: "gpt-5" },
754 });
755 expect(declaredFailure(configure)).toMatchObject({ code: "capability_denied" });
756 expect(dispatched).toEqual([]);
757 }).pipe(
758 Effect.provide(
759 layerThreadToolkit.pipe(
760 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
761 Layer.provide(NodeCrypto.layer),
762 Layer.provide(
763 Layer.mock(ThreadManagement.ThreadManagementService)({
764 getThreadShell: () =>
765 Effect.succeed({
766 id: ThreadId.make("other-project-thread"),
767 projectId: "other-project",
768 deletedAt: null,
769 } as never),
770 getProjectThreadRecords: () =>
771 Effect.succeed({
772 thread: {
773 id: ThreadId.make("other-project-thread"),
774 projectId: "other-project",
775 modelSelection: { instanceId: "codex", model: "gpt-5" },
776 runtimeMode: "auto",
777 interactionMode: "default",
778 deletedAt: null,
779 },
780 } as never),
781 dispatch: () =>
782 Effect.sync(() => {
783 dispatched.push("dispatch");
784 return { sequence: 7 } as never;
785 }),
786 }),
787 ),
788 ),
789 ),
790 ),
791);
792const dispatched: Array<string> = [];
793β
794it.effect("refuses act-as-caller tools to a client caller", () =>
795 Effect.gen(function* () {
796 const server = yield* McpServer.McpServer;
797 const result = yield* server
798 .callTool({
799 name: "delegate_task",
800 arguments: { task: "Review", mode: "async" },
801 })
802 .pipe(
803 Effect.provideService(
804 McpInvocationContext.McpInvocationContext,
805 clientScope("full-access"),
806 ),
807 Effect.provideService(McpSchema.McpServerClient, client),
808 );
809 expect(declaredFailure(result)).toMatchObject({ code: "thread_credential_required" });
810 }).pipe(
811 Effect.provide(
812 layerOrchestratorToolkit.pipe(
813 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
814 Layer.provide(NodeCrypto.layer),
815 Layer.provide(Layer.mock(ThreadManagement.ThreadManagementService)({})),
816 Layer.provide(ServerSettings.layerTest()),
817 Layer.provide(Layer.mock(ProviderRegistry.ProviderRegistry)({})),
818 Layer.provide(Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({})),
819 Layer.provide(Layer.mock(ScheduledTaskService.ScheduledTaskService)({})),
820 Layer.provide(Layer.mock(PeerForwarding.PeerForwarding)({})),
821 Layer.provide(Layer.mock(PeerLinkRequests.PeerLinkRequests)({})),
822 Layer.provide(Layer.mock(PeerLinks.PeerLinks)({})),
823 Layer.provide(Layer.mock(ProjectService.ProjectService)({})),
824 Layer.provide(Layer.mock(SecretRequests.SecretRequests)({})),
825 Layer.provide(Layer.mock(RemoteDelegation.RemoteDelegation)({})),
826 ),
827 ),
828 ),
829);
830β
831it.effect("a caller cannot rewrite a scheduled task that runs above its own modes", () =>
832 Effect.gen(function* () {
833 const server = yield* McpServer.McpServer;
834 const call = (name: string, args: Record<string, unknown>) =>
835 server
836 .callTool({ name, arguments: args })
837 .pipe(
838 Effect.provideService(McpInvocationContext.McpInvocationContext, clientScope("auto")),
839 Effect.provideService(McpSchema.McpServerClient, client),
840 );
841 const update = yield* call("schedule_task", {
842 scheduledTaskId: "task-full-access",
843 prompt: "Run something else",
844 });
845 expect(declaredFailure(update)).toMatchObject({ code: "runtime_mode_escalation_denied" });
846 const remove = yield* call("delete_scheduled_task", { scheduledTaskId: "task-full-access" });
847 expect(declaredFailure(remove)).toMatchObject({ code: "runtime_mode_escalation_denied" });
848 const allowed = yield* call("schedule_task", {
849 scheduledTaskId: "task-auto",
850 enabled: false,
851 });
852 expect(allowed.isError).toBe(false);
853 const fresh = yield* call("schedule_task", {
854 scheduledTaskId: "task-auto",
855 freshSession: true,
856 });
857 expect(fresh.structuredContent).toMatchObject({ freshSession: true });
858 const incomplete = yield* call("schedule_task", { prompt: "Run nightly" });
859 expect(declaredFailure(incomplete)).toMatchObject({ code: "invalid_request" });
860 }).pipe(
861 Effect.provide(
862 layerOrchestratorToolkit.pipe(
863 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
864 Layer.provide(NodeCrypto.layer),
865 Layer.provide(Layer.mock(ThreadManagement.ThreadManagementService)({})),
866 Layer.provide(ServerSettings.layerTest()),
867 Layer.provide(Layer.mock(ProviderRegistry.ProviderRegistry)({})),
868 Layer.provide(Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({})),
869 Layer.provide(
870 Layer.mock(ScheduledTaskService.ScheduledTaskService)({
871 list: () =>
872 Effect.succeed({
873 tasks: [
874 scheduledTask("task-full-access", "full-access"),
875 scheduledTask("task-auto", "auto"),
876 ],
877 }),
878 upsert: (input) =>
879 Effect.succeed({
880 task: { ...(scheduledTask(input.id ?? "task-auto", "auto") as object), ...input },
881 } as never),
882 }),
883 ),
884 Layer.provide(Layer.mock(ProjectService.ProjectService)({})),
885 Layer.provide(Layer.mock(SecretRequests.SecretRequests)({})),
886 Layer.provide(Layer.mock(PeerForwarding.PeerForwarding)({})),
887 Layer.provide(Layer.mock(PeerLinkRequests.PeerLinkRequests)({})),
888 Layer.provide(Layer.mock(PeerLinks.PeerLinks)({})),
889 Layer.provide(Layer.mock(RemoteDelegation.RemoteDelegation)({})),
890 ),
891 ),
892 ),
893);
894β
895it.effect.each([
896 {
897 scenario: "explains why a webhook task cannot run manually",
898 tasks: [
899 {
900 ...Schema.decodeSync(ScheduledTask)(scheduledTask("task-webhook", "full-access")),
901 schedule: { type: "webhook" as const, signature: null },
902 },
903 ],
904 error: new ScheduledTaskError({
905 message: "Webhook tasks run when their URL receives a request.",
906 cause: new Error("private-webhook-cause"),
907 }),
908 code: "orchestration_error",
909 message: "Webhook tasks run when their URL receives a request.",
910 },
911 {
912 scenario: "reports a missing task before trying to run it",
913 tasks: [],
914 error: new ScheduledTaskError({ message: "must not run a missing task" }),
915 code: "invalid_request",
916 message: "The scheduled task was not found.",
917 },
918])("run_scheduled_task_now $scenario", ({ tasks, error, code, message }) =>
919 Effect.gen(function* () {
920 const server = yield* McpServer.McpServer;
921 const result = yield* server
922 .callTool({ name: "run_scheduled_task_now", arguments: { taskId: "task-webhook" } })
923 .pipe(
924 Effect.provideService(
925 McpInvocationContext.McpInvocationContext,
926 clientScope("full-access"),
927 ),
928 Effect.provideService(McpSchema.McpServerClient, client),
929 );
930 expect(result.isError).toBe(true);
931 expect(result.structuredContent).toBeUndefined();
932 expect(declaredFailure(result)).toEqual({ _tag: "OrchestratorMcpFailure", code, message });
933 expect(JSON.stringify(result)).not.toContain("private-");
934 }).pipe(
935 Effect.provide(
936 McpHttpServer.layerThreadToolkit.pipe(
937 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
938 Layer.provide(NodeCrypto.layer),
939 Layer.provide(Layer.mock(ThreadManagement.ThreadManagementService)({})),
940 Layer.provide(Layer.mock(ThreadSearch.ThreadSearch)({})),
941 Layer.provide(Layer.mock(PeerForwarding.PeerForwarding)({})),
942 Layer.provide(Layer.mock(PeerLinkRequests.PeerLinkRequests)({})),
943 Layer.provide(Layer.mock(PeerLinks.PeerLinks)({})),
944 Layer.provide(
945 Layer.mock(ScheduledTaskService.ScheduledTaskService)({
946 list: () => Effect.succeed({ tasks }),
947 runNow: () => Effect.fail(error),
948 }),
949 ),
950 ),
951 ),
952 ),
953);
954β
955function scheduledTask(id: string, runtimeMode: "auto" | "full-access"): never {
956 return {
957 id,
958 title: id,
959 prompt: "Check the build",
960 enabled: true,
961 projectId: "project-a",
962 threadId: null,
963 schedule: { type: "interval", everyMs: 3_600_000 },
964 workspaceStrategy: { type: "worktree", baseRef: "main", startFromOrigin: true },
965 modelSelection: { instanceId: "codex", model: "gpt-5" },
966 runtimeMode,
967 interactionMode: "default",
968 createdBy: "user",
969 creationSource: "web",
970 nextRunAt: null,
971 lastRunStatus: "never",
972 lastRunAt: null,
973 lastRunThreadId: null,
974 lastRunError: null,
975 runCount: 0,
976 createdAt: "2026-10-01T00:00:00.000Z",
977 updatedAt: "2026-10-01T00:00:00.000Z",
978 } as never;
979}
980β
981it.effect.each(["t3_thread_interrupt", "t3_workflow_stop"])(
982 "a caller cannot use %s on a thread that runs above its own modes",
983 (toolName) =>
984 Effect.gen(function* () {
985 const server = yield* McpServer.McpServer;
986 const result = yield* server
987 .callTool({
988 name: toolName,
989 arguments: {
990 threadId: "full-access-thread",
991 ...(toolName === "t3_workflow_stop" ? { subagentId: "workflow-coordinator" } : {}),
992 },
993 })
994 .pipe(
995 Effect.provideService(McpInvocationContext.McpInvocationContext, clientScope("auto")),
996 Effect.provideService(McpSchema.McpServerClient, client),
997 );
998 expect(declaredFailure(result)).toMatchObject({ code: "runtime_mode_escalation_denied" });
999 }).pipe(
1000 Effect.provide(
1001 layerOrchestratorToolkit.pipe(
1002 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
1003 Layer.provide(NodeCrypto.layer),
1004 Layer.provide(
1005 Layer.mock(ThreadManagement.ThreadManagementService)({
1006 getThreadShell: () =>
1007 Effect.succeed({
1008 projectId: "project-a",
1009 deletedAt: null,
1010 runtimeMode: "full-access",
1011 interactionMode: "default",
1012 } as never),
1013 getProjectThreadRecords: () =>
1014 Effect.succeed({
1015 thread: {
1016 id: ThreadId.make("full-access-thread"),
1017 projectId: "project-a",
1018 runtimeMode: "full-access",
1019 interactionMode: "default",
1020 deletedAt: null,
1021 },
1022 runs: [],
1023 } as never),
1024 stopWorkflow: () => Effect.die("workflow stop must not dispatch above the ceiling"),
1025 interruptThread: () => Effect.die("interrupt must not dispatch above the ceiling"),
1026 }),
1027 ),
1028 Layer.provide(ServerSettings.layerTest()),
1029 Layer.provide(Layer.mock(ProviderRegistry.ProviderRegistry)({})),
1030 Layer.provide(Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({})),
1031 Layer.provide(Layer.mock(ScheduledTaskService.ScheduledTaskService)({})),
1032 Layer.provide(Layer.mock(ProjectService.ProjectService)({})),
1033 Layer.provide(Layer.mock(SecretRequests.SecretRequests)({})),
1034 Layer.provide(Layer.mock(RemoteDelegation.RemoteDelegation)({})),
1035 Layer.provide(Layer.mock(PeerForwarding.PeerForwarding)({})),
1036 Layer.provide(Layer.mock(PeerLinkRequests.PeerLinkRequests)({})),
1037 Layer.provide(Layer.mock(PeerLinks.PeerLinks)({})),
1038 ),
1039 ),
1040 ),
1041);
1042β
1043it.effect("discovers workflow stop targets and requires orchestration capability", () => {
1044 const threadId = ThreadId.make("workflow-parent");
1045 const shell = McpToolAccessTestkit.liveThreadShell(threadId);
1046 const subagentId = NodeId.make("workflow-coordinator");
1047 const itemId = TurnItemId.make("workflow-coordinator-activity");
1048 const row: OrchestrationV2ProjectedTurnItem = {
1049 position: 0,
1050 visibility: "local",
1051 sourceThreadId: threadId,
1052 sourceItemId: itemId,
1053 item: {
1054 id: itemId,
1055 threadId,
1056 runId: shell.activeRunId,
1057 nodeId: subagentId,
1058 providerThreadId: null,
1059 providerTurnId: null,
1060 nativeItemRef: null,
1061 parentItemId: null,
1062 ordinal: 0,
1063 status: "running",
1064 title: "Workflow: checkout review",
1065 startedAt: shell.createdAt,
1066 completedAt: null,
1067 updatedAt: shell.updatedAt,
1068 type: "subagent",
1069 subagentId,
1070 origin: "provider_native",
1071 driver: ProviderDriverKind.make("claudeAgent"),
1072 providerInstanceId: ProviderInstanceId.make("claudeAgent"),
1073 childThreadId: ThreadId.make("workflow-child"),
1074 prompt: "Review checkout",
1075 result: null,
1076 },
1077 };
1078 const stopped: Array<{ readonly threadId: ThreadId; readonly subagentId: string }> = [];
1079 return Effect.gen(function* () {
1080 const server = yield* McpServer.McpServer;
1081 const scope = clientScope("full-access");
1082 const read = yield* server
1083 .callTool({ name: "t3_thread_read", arguments: { threadId, view: "activity" } })
1084 .pipe(
1085 Effect.provideService(McpInvocationContext.McpInvocationContext, scope),
1086 Effect.provideService(McpSchema.McpServerClient, client),
1087 );
1088 expect(read.isError).toBe(false);
1089 const result = yield* decodeThreadReadResult(read.structuredContent);
1090 expect(result.items[0]?.subagentId).toBe(subagentId);
1091 const call = (invocation: McpInvocationContext.McpInvocationScope) =>
1092 server
1093 .callTool({
1094 name: "t3_workflow_stop",
1095 arguments: { threadId, subagentId: result.items[0]?.subagentId },
1096 })
1097 .pipe(
1098 Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
1099 Effect.provideService(McpSchema.McpServerClient, client),
1100 );
1101 const denied = yield* call({ ...scope, capabilities: new Set(["worktree"]) });
1102 expect(declaredFailure(denied)).toMatchObject({ code: "capability_denied" });
1103 expect(stopped).toEqual([]);
1104 const allowed = yield* call(scope);
1105 expect(allowed.isError).toBe(false);
1106 expect(stopped).toEqual([{ threadId: "workflow-parent", subagentId: "workflow-coordinator" }]);
1107 }).pipe(
1108 Effect.provide(
1109 layerOrchestratorToolkit.pipe(
1110 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
1111 Layer.provide(NodeCrypto.layer),
1112 Layer.provide(
1113 Layer.mock(ThreadManagement.ThreadManagementService)({
1114 getThreadShell: (id) => Effect.succeed(McpToolAccessTestkit.liveThreadShell(id)),
1115 getProjectThreadRecords: () =>
1116 Effect.succeed(McpToolAccessTestkit.idleThreadProjection(shell)),
1117 getTimelinePage: () =>
1118 Effect.succeed({ items: [row], totalItems: 1, hasMore: false, hasEarlier: false }),
1119 stopWorkflow: (input) =>
1120 Effect.sync(() => {
1121 stopped.push(input);
1122 }),
1123 }),
1124 ),
1125 Layer.provide(ServerSettings.layerTest()),
1126 Layer.provide(Layer.mock(ProviderRegistry.ProviderRegistry)({})),
1127 Layer.provide(Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({})),
1128 Layer.provide(Layer.mock(ScheduledTaskService.ScheduledTaskService)({})),
1129 Layer.provide(Layer.mock(PeerForwarding.PeerForwarding)({})),
1130 Layer.provide(Layer.mock(PeerLinkRequests.PeerLinkRequests)({})),
1131 Layer.provide(Layer.mock(PeerLinks.PeerLinks)({})),
1132 Layer.provide(Layer.mock(ProjectService.ProjectService)({})),
1133 Layer.provide(Layer.mock(SecretRequests.SecretRequests)({})),
1134 Layer.provide(Layer.mock(RemoteDelegation.RemoteDelegation)({})),
1135 ),
1136 ),
1137 );
1138});
1139β
1140it.effect("only the caller that prepared a pending upload can discard it", () =>
1141 Effect.gen(function* () {
1142 const server = yield* McpServer.McpServer;
1143 const call = (
1144 name: string,
1145 args: Record<string, unknown>,
1146 invocation: McpInvocationContext.McpInvocationScope,
1147 ) =>
1148 server
1149 .callTool({ name, arguments: args })
1150 .pipe(
1151 Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
1152 Effect.provideService(McpSchema.McpServerClient, client),
1153 );
1154 const prepared = yield* call(
1155 "t3_attachment_prepare_upload",
1156 { upload: { name: "shot.png", mimeType: "image/png", sizeBytes: 4 } },
1157 scope,
1158 );
1159 const { attachmentId } = prepared.structuredContent as { readonly attachmentId: string };
1160 const otherThread = {
1161 ...scope,
1162 requestNamespace: "other-session",
1163 thread: { ...scope.thread!, threadId: ThreadId.make("other-thread") },
1164 };
1165β
1166 const refused = yield* call("t3_attachment_discard", { attachmentId }, otherThread);
1167 expect(declaredFailure(refused)).toMatchObject({ code: "invalid_request" });
1168β
1169 yield* TestClock.adjust(24 * 60 * 60 * 1000 + 5 * 60_000);
1170 yield* call(
1171 "t3_attachment_prepare_upload",
1172 { upload: { name: "later.png", mimeType: "image/png", sizeBytes: 4 } },
1173 scope,
1174 );
1175β
1176 const discarded = yield* call(
1177 "t3_attachment_discard",
1178 { attachmentId },
1179 { ...scope, requestNamespace: "mcp-core-session-2" },
1180 );
1181 expect(discarded.isError).toBe(false);
1182 }).pipe(
1183 Effect.provide(
1184 McpHttpServer.layerAttachmentToolkit.pipe(
1185 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
1186 Layer.provide(McpToolAccessTestkit.liveThreadsLayer),
1187 Layer.provide(AssetSigner.layer.pipe(Layer.provideMerge(ServerSecretStore.layer))),
1188 Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-mcp-attachment-" })),
1189 Layer.provide(
1190 DocumentText.layer.pipe(
1191 Layer.provideMerge(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)),
1192 ),
1193 ),
1194 Layer.provide(NodeCrypto.layer),
1195 ),
1196 ),
1197 ),
1198);
1199β
1200it.effect(
1201 "t3_thread_list settled:false with an empty statuses list lists the unsettled threads",
1202 () => {
1203 const unsettled = McpToolAccessTestkit.liveThreadShell(ThreadId.make("unsettled-thread"));
1204 const settled = {
1205 ...McpToolAccessTestkit.liveThreadShell(ThreadId.make("settled-thread")),
1206 settledOverride: "settled",
1207 } as const;
1208 return Effect.gen(function* () {
1209 const server = yield* McpServer.McpServer;
1210 const result = yield* server
1211 .callTool({
1212 name: "t3_thread_list",
1213 arguments: { projectId: unsettled.projectId, settled: false, statuses: [] },
1214 })
1215 .pipe(
1216 Effect.provideService(
1217 McpInvocationContext.McpInvocationContext,
1218 clientScope("full-access"),
1219 ),
1220 Effect.provideService(McpSchema.McpServerClient, client),
1221 );
1222 expect(result.isError).toBe(false);
1223 expect(result.structuredContent).toMatchObject({
1224 threads: [{ threadId: unsettled.id, settled: false }],
1225 total: 1,
1226 });
1227 }).pipe(
1228 Effect.provide(
1229 layerOrchestratorToolkit.pipe(
1230 Layer.provideMerge(McpHttpServer.layerInProcessMcpServer),
1231 Layer.provide(NodeCrypto.layer),
1232 Layer.provide(
1233 Layer.mock(ThreadManagement.ThreadManagementService)({
1234 listProjectThreads: () => Effect.succeed([unsettled, settled]),
1235 }),
1236 ),
1237 Layer.provide(ServerSettings.layerTest()),
1238 Layer.provide(Layer.mock(ProviderRegistry.ProviderRegistry)({})),
1239 Layer.provide(Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({})),
1240 Layer.provide(Layer.mock(ScheduledTaskService.ScheduledTaskService)({})),
1241 Layer.provide(
1242 Layer.mock(ProjectService.ProjectService)({
1243 getById: (projectId) =>
1244 Effect.succeedSome({
1245 id: projectId,
1246 title: "MCP test",
1247 workspaceRoot: "/repo",
1248 defaultModelSelection: null,
1249 scripts: [],
1250 createdAt: "2026-10-18T00:00:00.000Z",
1251 updatedAt: "2026-10-18T00:00:00.000Z",
1252 deletedAt: null,
1253 }),
1254 }),
1255 ),
1256 Layer.provide(Layer.mock(SecretRequests.SecretRequests)({})),
1257 Layer.provide(Layer.mock(PeerForwarding.PeerForwarding)({})),
1258 Layer.provide(Layer.mock(PeerLinkRequests.PeerLinkRequests)({})),
1259 Layer.provide(Layer.mock(PeerLinks.PeerLinks)({})),
1260 Layer.provide(Layer.mock(RemoteDelegation.RemoteDelegation)({})),
1261 ),
1262 ),
1263 );
1264 },
1265);