Skip to content
Source
Sign inSign up

McpInvocationContext.test.ts

97 lines3.6 KB

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 other missing capabilities with the neutral error", () => {
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(["preview"]),
24 issuedAt: 1,
25 };
26​
27 return Effect.gen(function* () {
28 const error = yield* McpInvocationContext.requireMcpCapability("pull-requests").pipe(
29 Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
30 Effect.flip,
31 );
32​
33 expect(error).toBeInstanceOf(McpCapabilityUnavailableError);
34 expect(error).toMatchObject({
35 capability: "pull-requests",
36 threadId: invocation.thread?.threadId,
37 });
38​
39 const scope = yield* McpInvocationContext.requireMcpCapability("preview").pipe(
40 Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
41 );
42 expect(scope).toBe(invocation);
43 });
44});
45​
46it.effect("refuses thread-owned capabilities to a caller signed in from outside a thread", () => {
47 const invocation: McpInvocationContext.McpInvocationScope = {
48 environmentId: EnvironmentId.make("environment-1"),
49 requestNamespace: "client:session-1",
50 thread: undefined,
51 client: { sessionId: "session-1", label: "Claude Code", access: "auto" },
52 capabilities: new Set(["preview", "orchestration"]),
53 issuedAt: 1,
54 };
55​
56 return Effect.gen(function* () {
57 const error = yield* McpInvocationContext.requireThreadMcpCapability("preview").pipe(
58 Effect.provideService(McpInvocationContext.McpInvocationContext, invocation),
59 Effect.flip,
60 );
61 expect(error).toBeInstanceOf(PreviewAutomationUnavailableError);
62 expect(error).toMatchObject({ capability: "preview", environmentId: "environment-1" });
63 expect(error.threadId).toBeUndefined();
64 });
65});
66​
67it("acts at an administrator's ceiling for a client approved above it", () => {
68 const scope = (
69 access: McpInvocationContext.McpClientCaller["access"],
70 narrowedTo?: McpInvocationContext.ClientModes,
71 ): McpInvocationContext.McpInvocationScope => ({
72 environmentId: EnvironmentId.make("environment-1"),
73 requestNamespace: "client:session-1",
74 thread: undefined,
75 client: { sessionId: "session-1", label: "Claude Code", access, narrowedTo },
76 capabilities: new Set(["orchestration"]),
77 issuedAt: 1,
78 });
79 const modes = (invocation: McpInvocationContext.McpInvocationScope, ceiling?: "auto") =>
80 McpInvocationContext.clientModeCeiling(
81 McpInvocationContext.withRuntimeModeCeiling(invocation, ceiling).client,
82 );
83​
84 expect(modes(scope("full-access"), "auto")).toEqual({
85 runtimeMode: "auto",
86 interactionMode: "default",
87 });
88 // A narrower request and the approved mode still win below the ceiling.
89 expect(
90 modes(
91 scope("full-access", { runtimeMode: "approval-required", interactionMode: "plan" }),
92 "auto",
93 ),
94 ).toEqual({ runtimeMode: "approval-required", interactionMode: "plan" });
95 expect(modes(scope("auto-accept-edits"), "auto").runtimeMode).toBe("auto-accept-edits");
96 expect(modes(scope("full-access")).runtimeMode).toBe("full-access");
97});