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