| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { assert, describe, it } from "@effect/vitest"; |
| 3 | import { EnvironmentId, ProviderInstanceId, ThreadId } from "@t3tools/contracts"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as FileSystem from "effect/FileSystem"; |
| 6 | |
| 7 | import { |
| 8 | PI_T3_MCP_EXTENSION_FILENAME, |
| 9 | T3_MCP_BEARER_ENV, |
| 10 | T3_MCP_URL_ENV, |
| 11 | T3_PI_RUNTIME_MODE_ENV, |
| 12 | T3_PI_MCP_EXTENSION_PATH_ENV, |
| 13 | } from "./mcpExtensionSource.ts"; |
| 14 | import { |
| 15 | buildPiRpcLaunch, |
| 16 | materializePiT3McpExtension, |
| 17 | resolvePiLaunchArgs, |
| 18 | } from "./mcpInjection.ts"; |
| 19 | |
| 20 | const threadId = ThreadId.make("thread-pi-t3-mcp"); |
| 21 | |
| 22 | const mcpSession = { |
| 23 | environmentId: EnvironmentId.make("environment-pi-t3-mcp"), |
| 24 | threadId, |
| 25 | providerSessionId: "mcp-session-pi", |
| 26 | providerInstanceId: ProviderInstanceId.make("pi"), |
| 27 | endpoint: "http://127.0.0.1:43123/mcp", |
| 28 | authorizationHeader: "Bearer secret-pi-token", |
| 29 | browserToolsAvailable: true, |
| 30 | }; |
| 31 | |
| 32 | describe("pi T3 MCP injection", () => { |
| 33 | it("always adds the permission bridge and configures MCP when available", () => { |
| 34 | const resolvedArgs = resolvePiLaunchArgs( |
| 35 | "--extension=/home/user/.pi/agent/extensions/demo.ts --session-dir=/tmp/pi-sessions --provider=anthropic --model=claude-sonnet --tools='' --name=-review --extension-flag=kept", |
| 36 | ); |
| 37 | assert.isTrue(resolvedArgs.ok); |
| 38 | if (!resolvedArgs.ok) return; |
| 39 | const launch = buildPiRpcLaunch({ |
| 40 | launchArgs: resolvedArgs.args, |
| 41 | environment: { PATH: "/usr/bin" }, |
| 42 | mcpSession, |
| 43 | extensionPath: "/tmp/cache/pi-t3-mcp-extension.ts", |
| 44 | runtimeMode: "approval-required", |
| 45 | }); |
| 46 | assert.deepEqual(launch.args, [ |
| 47 | "--mode", |
| 48 | "rpc", |
| 49 | "--extension", |
| 50 | "/home/user/.pi/agent/extensions/demo.ts", |
| 51 | "--session-dir", |
| 52 | "/tmp/pi-sessions", |
| 53 | "--provider", |
| 54 | "anthropic", |
| 55 | "--model", |
| 56 | "claude-sonnet", |
| 57 | "--tools", |
| 58 | "", |
| 59 | "--name", |
| 60 | "-review", |
| 61 | "--extension-flag=kept", |
| 62 | "--extension", |
| 63 | "/tmp/cache/pi-t3-mcp-extension.ts", |
| 64 | ]); |
| 65 | assert.notInclude(launch.args, "--no-extensions"); |
| 66 | assert.equal(launch.env[T3_MCP_URL_ENV], "http://127.0.0.1:43123/mcp"); |
| 67 | assert.equal(launch.env[T3_MCP_BEARER_ENV], "secret-pi-token"); |
| 68 | assert.equal(launch.env[T3_PI_RUNTIME_MODE_ENV], "approval-required"); |
| 69 | assert.equal(launch.env[T3_PI_MCP_EXTENSION_PATH_ENV], "/tmp/cache/pi-t3-mcp-extension.ts"); |
| 70 | |
| 71 | const permissionOnly = buildPiRpcLaunch({ |
| 72 | launchArgs: [], |
| 73 | environment: { |
| 74 | [T3_MCP_URL_ENV]: "http://127.0.0.1:9999/stale", |
| 75 | [T3_MCP_BEARER_ENV]: "stale-token", |
| 76 | [T3_PI_MCP_EXTENSION_PATH_ENV]: "/stale/extension.ts", |
| 77 | }, |
| 78 | mcpSession: undefined, |
| 79 | extensionPath: "/tmp/cache/pi-t3-mcp-extension.ts", |
| 80 | runtimeMode: "auto-accept-edits", |
| 81 | }); |
| 82 | assert.deepEqual(permissionOnly.args, [ |
| 83 | "--mode", |
| 84 | "rpc", |
| 85 | "--extension", |
| 86 | "/tmp/cache/pi-t3-mcp-extension.ts", |
| 87 | ]); |
| 88 | assert.isFalse(permissionOnly.hasT3Mcp); |
| 89 | assert.isUndefined(permissionOnly.env[T3_MCP_URL_ENV]); |
| 90 | assert.isUndefined(permissionOnly.env[T3_MCP_BEARER_ENV]); |
| 91 | assert.equal(permissionOnly.env[T3_PI_RUNTIME_MODE_ENV], "auto-accept-edits"); |
| 92 | assert.equal( |
| 93 | permissionOnly.env[T3_PI_MCP_EXTENSION_PATH_ENV], |
| 94 | "/tmp/cache/pi-t3-mcp-extension.ts", |
| 95 | ); |
| 96 | }); |
| 97 | |
| 98 | it("falls back to Pi's first supported mode for legacy auto threads", () => { |
| 99 | const launch = buildPiRpcLaunch({ |
| 100 | launchArgs: [], |
| 101 | environment: {}, |
| 102 | mcpSession: undefined, |
| 103 | extensionPath: "/tmp/cache/pi-t3-mcp-extension.ts", |
| 104 | runtimeMode: "auto", |
| 105 | }); |
| 106 | |
| 107 | assert.equal(launch.env[T3_PI_RUNTIME_MODE_ENV], "approval-required"); |
| 108 | }); |
| 109 | |
| 110 | it("forces tools and user extensions off for unattended text generation", () => { |
| 111 | const launch = buildPiRpcLaunch({ |
| 112 | launchArgs: [ |
| 113 | "--tools", |
| 114 | "read,write", |
| 115 | "--extension", |
| 116 | "/home/user/.pi/agent/extensions/demo.ts", |
| 117 | "--extension=./second.ts", |
| 118 | "--provider", |
| 119 | "anthropic", |
| 120 | ], |
| 121 | environment: {}, |
| 122 | mcpSession, |
| 123 | extensionPath: "/tmp/cache/pi-t3-mcp-extension.ts", |
| 124 | ephemeral: true, |
| 125 | disableExtensions: true, |
| 126 | disableTools: true, |
| 127 | }); |
| 128 | assert.deepEqual(launch.args, [ |
| 129 | "--mode", |
| 130 | "rpc", |
| 131 | "--no-session", |
| 132 | "--provider", |
| 133 | "anthropic", |
| 134 | "--no-extensions", |
| 135 | "--no-tools", |
| 136 | ]); |
| 137 | assert.isFalse(launch.hasT3Mcp); |
| 138 | assert.deepInclude(resolvePiLaunchArgs("--mode text"), { |
| 139 | ok: false, |
| 140 | message: "Pi launch argument '--mode' is controlled by T3 Code and cannot be overridden.", |
| 141 | }); |
| 142 | assert.deepInclude(resolvePiLaunchArgs("--session old.jsonl"), { ok: false }); |
| 143 | assert.deepInclude(resolvePiLaunchArgs("prompt pi immediately"), { ok: false }); |
| 144 | assert.deepInclude(resolvePiLaunchArgs("--plan @instructions.md"), { ok: false }); |
| 145 | }); |
| 146 | |
| 147 | it("rejects --provider without --model, which Pi 1.0 refuses at startup", () => { |
| 148 | const rejection = { |
| 149 | ok: false, |
| 150 | message: "Pi launch argument '--provider' requires '--model'.", |
| 151 | }; |
| 152 | assert.deepInclude(resolvePiLaunchArgs("--provider openrouter"), rejection); |
| 153 | assert.deepInclude(resolvePiLaunchArgs("--provider=openrouter --models gpt-6"), rejection); |
| 154 | assert.isTrue(resolvePiLaunchArgs("--provider openrouter --model=deepseek/v4").ok); |
| 155 | assert.isTrue(resolvePiLaunchArgs("--model deepseek/v4").ok); |
| 156 | }); |
| 157 | |
| 158 | it.effect("materializes the MCP bridge with namespaced tool registration", () => |
| 159 | Effect.gen(function* () { |
| 160 | const fs = yield* FileSystem.FileSystem; |
| 161 | const cacheDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pi-extensions-" }); |
| 162 | const mcpDest = yield* materializePiT3McpExtension(cacheDir); |
| 163 | assert.isTrue(mcpDest.endsWith(PI_T3_MCP_EXTENSION_FILENAME)); |
| 164 | const mcpSource = yield* fs.readFileString(mcpDest); |
| 165 | assert.include(mcpSource, "export default async function t3McpExtension"); |
| 166 | assert.include(mcpSource, "before_agent_start"); |
| 167 | assert.include(mcpSource, 'pi.on("tool_call"'); |
| 168 | assert.include(mcpSource, "Allow ${event.toolName}?"); |
| 169 | assert.include(mcpSource, '"mcp-protocol-version"'); |
| 170 | assert.include(mcpSource, '"tools/call"'); |
| 171 | assert.include(mcpSource, "mcp__t3-code__"); |
| 172 | assert.include(mcpSource, "mcp__t3_code__"); |
| 173 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), |
| 174 | ); |
| 175 | }); |