1import { expect, it } from "@effect/vitest";
2import {
3 EnvironmentId,
4 McpCapabilityUnavailableError,
5 PreviewAutomationUnavailableError,
6 ProviderInstanceId,
7 ThreadId,
8} from "@t3tools/contracts";
9import * as Effect from "effect/Effect";
10
11import * as McpInvocationContext from "./McpInvocationContext.ts";
12
13it.effect("reports the scoped credential context when preview capability is unavailable", () => {
14 const invocation: McpInvocationContext.McpInvocationScope = {
15 environmentId: EnvironmentId.make("environment-1"),
16 requestNamespace: "provider-session-1",
17 thread: {
18 threadId: ThreadId.make("thread-1"),
19 providerSessionId: "provider-session-1",
20 providerInstanceId: ProviderInstanceId.make("codex"),
21 },
22 client: undefined,
23 capabilities: new Set(),
24 issuedAt: 1,
25 };
26
27 return Effect.gen(function* () {
28 const error = yield* McpInvocationContext.requireMcpCapability("preview").pipe(
29 Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
30 Effect.flip,
31 );
32
33 expect(error).toBeInstanceOf(PreviewAutomationUnavailableError);
34 expect(error).toMatchObject({
35 capability: "preview",
36 environmentId: invocation.environmentId,
37 threadId: invocation.thread?.threadId,
38 providerSessionId: invocation.thread?.providerSessionId,
39 providerInstanceId: invocation.thread?.providerInstanceId,
40 });
41 expect(error.message).toContain("MCP credential does not grant the preview capability");
42 expect(error.message).toContain("use a headless browser from the shell");
43 });
44});
45
46it.effect("reports other missing capabilities with the neutral error", () => {
47 const invocation: McpInvocationContext.McpInvocationScope = {
48 environmentId: EnvironmentId.make("environment-1"),
49 requestNamespace: "provider-session-1",
50 thread: {
51 threadId: ThreadId.make("thread-1"),
52 providerSessionId: "provider-session-1",
53 providerInstanceId: ProviderInstanceId.make("codex"),
54 },
55 client: undefined,
56 capabilities: new Set(["preview"]),
57 issuedAt: 1,
58 };
59
60 return Effect.gen(function* () {
61 const error = yield* McpInvocationContext.requireMcpCapability("pull-requests").pipe(
62 Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
63 Effect.flip,
64 );
65
66 expect(error).toBeInstanceOf(McpCapabilityUnavailableError);
67 expect(error).toMatchObject({
68 capability: "pull-requests",
69 threadId: invocation.thread?.threadId,
70 });
71
72 const scope = yield* McpInvocationContext.requireMcpCapability("preview").pipe(
73 Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
74 );
75 expect(scope).toBe(invocation);
76 });
77});
78
79it.effect("refuses thread-owned capabilities to a caller signed in from outside a thread", () => {
80 const invocation: McpInvocationContext.McpInvocationScope = {
81 environmentId: EnvironmentId.make("environment-1"),
82 requestNamespace: "client:session-1",
83 thread: undefined,
84 client: { sessionId: "session-1", label: "Claude Code", access: "auto" },
85 capabilities: new Set(["preview", "orchestration"]),
86 issuedAt: 1,
87 };
88
89 return Effect.gen(function* () {
90 const error = yield* McpInvocationContext.requireThreadMcpCapability("preview").pipe(
91 Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
92 Effect.flip,
93 );
94 expect(error).toBeInstanceOf(PreviewAutomationUnavailableError);
95 expect(error).toMatchObject({ capability: "preview", environmentId: "environment-1" });
96 expect(error.threadId).toBeUndefined();
97 });
98});