| 1 | // @effect-diagnostics nodeBuiltinImport:off -- Pi extensions run outside Effect; these tests exercise their native filesystem boundary. |
| 2 | import * as NodeFSP from "node:fs/promises"; |
| 3 | import * as NodePath from "node:path"; |
| 4 | import * as NodeOS from "node:os"; |
| 5 | import * as NodeModule from "node:module"; |
| 6 | import * as NodeVM from "node:vm"; |
| 7 | import { assert, describe, expect, it } from "@effect/vitest"; |
| 8 | |
| 9 | import { PI_T3_MCP_EXTENSION_SOURCE } from "./mcpExtensionSource.ts"; |
| 10 | import { loadMcpBridge } from "./mcpBridge.testkit.ts"; |
| 11 | |
| 12 | type RequestHook = ( |
| 13 | event: { payload: unknown }, |
| 14 | ctx: { model: { provider: string } }, |
| 15 | ) => Record<string, unknown> | undefined; |
| 16 | |
| 17 | interface SkillCommand { |
| 18 | readonly name: string; |
| 19 | readonly source: string; |
| 20 | readonly sourceInfo: { readonly path: string }; |
| 21 | } |
| 22 | |
| 23 | type InputHook = ( |
| 24 | event: { text: string; images?: ReadonlyArray<unknown> }, |
| 25 | ctx: { ui: { notify: (message: string, level: string) => void } }, |
| 26 | ) => Promise<{ action: string; text: string; images?: ReadonlyArray<unknown> } | undefined>; |
| 27 | |
| 28 | async function loadHooks(commands: ReadonlyArray<SkillCommand> = []) { |
| 29 | const handlers = new Map<string, unknown>(); |
| 30 | // Execute the shipped extension with MCP disabled; this path needs no Typebox. |
| 31 | const source = NodeModule.stripTypeScriptTypes( |
| 32 | PI_T3_MCP_EXTENSION_SOURCE.replace(/^import .*;$/gm, "").replace( |
| 33 | "export default async function", |
| 34 | "async function", |
| 35 | ), |
| 36 | ); |
| 37 | await NodeVM.runInNewContext(`${source}\nt3McpExtension(pi)`, { |
| 38 | process: { env: {} }, |
| 39 | NodeFSP, |
| 40 | NodePath, |
| 41 | // Frontmatter parsing belongs to Pi. These fixtures contain only skill bodies. |
| 42 | stripFrontmatter: (content: string) => content, |
| 43 | pi: { |
| 44 | on: (name: string, handler: unknown) => handlers.set(name, handler), |
| 45 | getCommands: () => commands, |
| 46 | }, |
| 47 | }); |
| 48 | return handlers; |
| 49 | } |
| 50 | |
| 51 | describe("Pi MCP tool results", () => { |
| 52 | it("returns mixed screenshot blocks and typed script output without repeating structured text", async () => { |
| 53 | const content = [ |
| 54 | { type: "text", text: "Browser screenshot" }, |
| 55 | { type: "image", data: "AAAA", mimeType: "image/png", _meta: { private: true } }, |
| 56 | { type: "text", text: "Screenshot captured" }, |
| 57 | ] as const; |
| 58 | const result = { content, structuredContent: { count: 2 }, _meta: { private: true } }; |
| 59 | const bridge = await loadMcpBridge({ modern: true, result }); |
| 60 | const tool = bridge.tools.find((tool) => tool.name === "mcp__t3-code__preview_snapshot")!; |
| 61 | const output = await tool.execute("snapshot", {}); |
| 62 | assert.deepEqual(output.content, [ |
| 63 | content[0], |
| 64 | { type: "image", data: "AAAA", mimeType: "image/png" }, |
| 65 | content[2], |
| 66 | ]); |
| 67 | assert.deepEqual(output.structuredContent, { content, structuredContent: { count: 2 } }); |
| 68 | assert.deepEqual(tool.outputSchema, { |
| 69 | type: "object", |
| 70 | properties: { |
| 71 | content: { type: "array", items: { type: "object" } }, |
| 72 | structuredContent: { type: "object", properties: { count: { type: "number" } } }, |
| 73 | isError: { type: "boolean" }, |
| 74 | _meta: { type: "object" }, |
| 75 | }, |
| 76 | required: ["content"], |
| 77 | }); |
| 78 | }); |
| 79 | |
| 80 | it.each([false, true])( |
| 81 | "preserves image-only results and error status, isError=%s", |
| 82 | async (isError) => { |
| 83 | const image = { type: "image", data: "AAAA", mimeType: "image/png" }; |
| 84 | const result = { content: [image], isError }; |
| 85 | const bridge = await loadMcpBridge({ result }); |
| 86 | const output = await bridge.tools[0]!.execute("image", {}); |
| 87 | assert.deepEqual(output.content[0], image); |
| 88 | assert.equal(output.content.length, isError ? 2 : 1); |
| 89 | if (isError) assert.include(output.content[1]?.text ?? "", "returned an error"); |
| 90 | assert.equal(output.isError, isError ? true : undefined); |
| 91 | assert.deepEqual(output.structuredContent, result); |
| 92 | }, |
| 93 | ); |
| 94 | |
| 95 | it("uses structured output as model text when content is empty", async () => { |
| 96 | const result = { content: [], structuredContent: { count: 2 } }; |
| 97 | const bridge = await loadMcpBridge({ result }); |
| 98 | const output = await bridge.tools[0]!.execute("structured", {}); |
| 99 | assert.deepEqual(output.content, [{ type: "text", text: '{"count":2}' }]); |
| 100 | assert.deepEqual(output.structuredContent, result); |
| 101 | }); |
| 102 | |
| 103 | it("propagates cancellation without returning a successful result", async () => { |
| 104 | const bridge = await loadMcpBridge(); |
| 105 | const controller = new AbortController(); |
| 106 | controller.abort(); |
| 107 | await expect(bridge.tools[0]!.execute("cancelled", {}, controller.signal)).rejects.toThrow( |
| 108 | "aborted", |
| 109 | ); |
| 110 | }); |
| 111 | }); |
| 112 | |
| 113 | describe("Pi MCP tool exposure", () => { |
| 114 | const discoveryContext = { |
| 115 | model: { api: "anthropic-messages" }, |
| 116 | ui: { notify: () => undefined }, |
| 117 | }; |
| 118 | |
| 119 | it.each(["session_start", "session_tree", "model_select", "before_agent_start"])( |
| 120 | "declares optional T3 tools for OpenAI-compatible gateways at %s", |
| 121 | async (hookName) => { |
| 122 | const bridge = await loadMcpBridge({ modern: true, toolSearchAvailable: true }); |
| 123 | const hook = bridge.handlers.get(hookName); |
| 124 | assert.isDefined(hook); |
| 125 | await hook!( |
| 126 | { systemPrompt: "Pi system prompt" }, |
| 127 | { model: { api: "openai-completions" }, ui: { notify: assert.fail } }, |
| 128 | ); |
| 129 | assert.isTrue( |
| 130 | bridge.tools |
| 131 | .filter((tool) => tool.name.startsWith("mcp__t3-code__")) |
| 132 | .every((tool) => bridge.getActiveTools().includes(tool.name)), |
| 133 | ); |
| 134 | const result = await bridge.tools |
| 135 | .find((tool) => tool.name === "mcp__t3-code__preview_snapshot")! |
| 136 | .execute("gateway-call", {}); |
| 137 | assert.equal(result.content[0]?.text, "browser snapshot"); |
| 138 | assert.equal(bridge.tools.filter((tool) => tool.exposure === "hidden").length, 4); |
| 139 | }, |
| 140 | ); |
| 141 | |
| 142 | it("keeps orchestration direct and optional bridge tools discoverable on modern Pi", async () => { |
| 143 | const bridge = await loadMcpBridge({ modern: true, toolSearchAvailable: true }); |
| 144 | await bridge.handlers.get("session_start")!({ systemPrompt: "" }, discoveryContext); |
| 145 | const start = bridge.handlers.get("before_agent_start"); |
| 146 | assert.isDefined(start); |
| 147 | const prompt = await start!({ systemPrompt: "Pi system prompt" }, discoveryContext); |
| 148 | assert.include(prompt.systemPrompt, "orchestrator_capabilities"); |
| 149 | assert.equal(bridge.servers.length, 0); |
| 150 | assert.equal(bridge.tools.length, 8); |
| 151 | assert.deepEqual(bridge.getActiveTools(), ["read", "tool_search"]); |
| 152 | assert.deepEqual( |
| 153 | bridge.tools |
| 154 | .filter((tool) => tool.exposure !== "hidden") |
| 155 | .map((tool) => [tool.name, tool.exposure]), |
| 156 | [ |
| 157 | ["mcp__t3-code__orchestrator_capabilities", "direct"], |
| 158 | ["mcp__t3-code__delegate_task", "direct"], |
| 159 | ["mcp__t3-code__task_status", "direct"], |
| 160 | ["mcp__t3-code__preview_snapshot", "deferred"], |
| 161 | ], |
| 162 | ); |
| 163 | assert.deepEqual( |
| 164 | bridge.tools.filter((tool) => tool.exposure === "hidden").map((tool) => tool.name), |
| 165 | [ |
| 166 | "mcp__t3_code__orchestrator_capabilities", |
| 167 | "mcp__t3_code__delegate_task", |
| 168 | "mcp__t3_code__task_status", |
| 169 | "mcp__t3_code__preview_snapshot", |
| 170 | ], |
| 171 | ); |
| 172 | const result = await bridge.tools |
| 173 | .find((tool) => tool.name === "mcp__t3-code__preview_snapshot")! |
| 174 | .execute("call-1", { depth: 2 }); |
| 175 | assert.equal(result.content[0]?.text, "browser snapshot"); |
| 176 | assert.equal(bridge.requests.at(-1)?.method, "tools/call"); |
| 177 | }); |
| 178 | |
| 179 | it("restores gateway declarations after model switches and older tree loadouts without excluded tools", async () => { |
| 180 | const bridge = await loadMcpBridge({ |
| 181 | modern: true, |
| 182 | toolSearchAvailable: true, |
| 183 | allowsTool: (name) => name !== "mcp__t3-code__delegate_task", |
| 184 | }); |
| 185 | const event = { systemPrompt: "" }; |
| 186 | await bridge.handlers.get("session_start")!(event, discoveryContext); |
| 187 | const gatewayContext = { model: { api: "openai-completions" }, ui: { notify: assert.fail } }; |
| 188 | await bridge.handlers.get("model_select")!(event, gatewayContext); |
| 189 | bridge.restoreActiveTools(["read", "mcp__t3-code__task_status"]); |
| 190 | await bridge.handlers.get("session_tree")!(event, gatewayContext); |
| 191 | assert.deepEqual(bridge.getActiveTools(), [ |
| 192 | "read", |
| 193 | "mcp__t3-code__task_status", |
| 194 | "tool_search", |
| 195 | "mcp__t3-code__orchestrator_capabilities", |
| 196 | "mcp__t3-code__preview_snapshot", |
| 197 | ]); |
| 198 | await bridge.handlers.get("model_select")!(event, discoveryContext); |
| 199 | assert.equal( |
| 200 | bridge.tools.find((tool) => tool.name === "mcp__t3-code__preview_snapshot")?.exposure, |
| 201 | "deferred", |
| 202 | ); |
| 203 | assert.include(bridge.getActiveTools(), "mcp__t3-code__preview_snapshot"); |
| 204 | }); |
| 205 | |
| 206 | it.each(["legacy Pi", "disabled tool search"])( |
| 207 | "keeps tool execution available with %s", |
| 208 | async (mode) => { |
| 209 | const bridge = await loadMcpBridge({ |
| 210 | modern: mode !== "legacy Pi", |
| 211 | toolSearchAvailable: mode === "disabled tool search", |
| 212 | toolSearchDisabled: mode === "disabled tool search", |
| 213 | }); |
| 214 | if (mode !== "legacy Pi") { |
| 215 | const start = bridge.handlers.get("session_start"); |
| 216 | await start!({ systemPrompt: "Pi system prompt" }, discoveryContext); |
| 217 | } |
| 218 | assert.equal(bridge.tools.filter((tool) => tool.exposure !== "hidden").length, 4); |
| 219 | assert.isTrue( |
| 220 | bridge.tools |
| 221 | .filter((tool) => tool.exposure !== "hidden") |
| 222 | .every((tool) => tool.exposure === undefined || tool.exposure === "direct"), |
| 223 | ); |
| 224 | const tool = bridge.tools.find((tool) => tool.name === "mcp__t3-code__preview_snapshot"); |
| 225 | assert.isDefined(tool); |
| 226 | const controller = new AbortController(); |
| 227 | const result = await tool!.execute("call-1", { depth: 2 }, controller.signal); |
| 228 | assert.equal(result.content[0]?.text, "browser snapshot"); |
| 229 | assert.strictEqual(bridge.transports.at(-1)?.signal, controller.signal); |
| 230 | assert.equal(bridge.transports.at(-1)?.url, "http://fixture.invalid/mcp"); |
| 231 | assert.equal(bridge.transports.at(-1)?.authorization, "Bearer fixture-token"); |
| 232 | assert.deepEqual(JSON.parse(JSON.stringify(bridge.requests.at(-1))), { |
| 233 | jsonrpc: "2.0", |
| 234 | id: 3, |
| 235 | method: "tools/call", |
| 236 | params: { name: "preview_snapshot", arguments: { depth: 2 } }, |
| 237 | }); |
| 238 | assert.isUndefined(tool?.promptSnippet); |
| 239 | assert.isUndefined(tool?.promptGuidelines); |
| 240 | }, |
| 241 | ); |
| 242 | |
| 243 | it("preserves legacy wildcard tool selection", async () => { |
| 244 | const bridge = await loadMcpBridge({ |
| 245 | modern: true, |
| 246 | toolSearchAvailable: true, |
| 247 | allowsTool: (name) => |
| 248 | name === "read" || name === "tool_search" || name.startsWith("mcp__t3-code__"), |
| 249 | }); |
| 250 | await bridge.handlers.get("session_start")!({ systemPrompt: "" }, discoveryContext); |
| 251 | assert.equal(bridge.tools.length, 4); |
| 252 | assert.equal(bridge.tools.filter((tool) => tool.exposure === "direct").length, 3); |
| 253 | const tool = bridge.tools.find((tool) => tool.name === "mcp__t3-code__preview_snapshot"); |
| 254 | assert.isDefined(tool); |
| 255 | assert.equal((await tool!.execute("selected", {})).content[0]?.text, "browser snapshot"); |
| 256 | }); |
| 257 | |
| 258 | it.each([false, true])( |
| 259 | "reconciles tree loadouts while honoring search exclusion: %s", |
| 260 | async (excludeSearch) => { |
| 261 | const bridge = await loadMcpBridge({ |
| 262 | modern: true, |
| 263 | toolSearchAvailable: true, |
| 264 | allowsTool: (name) => |
| 265 | name !== "mcp__t3-code__delegate_task" && (!excludeSearch || name !== "tool_search"), |
| 266 | }); |
| 267 | await bridge.handlers.get("session_start")!({ systemPrompt: "" }, discoveryContext); |
| 268 | bridge.restoreActiveTools([ |
| 269 | "read", |
| 270 | "mcp__t3-code__task_status", |
| 271 | "mcp__t3-code__preview_snapshot", |
| 272 | ]); |
| 273 | const tree = bridge.handlers.get("session_tree"); |
| 274 | assert.isDefined(tree); |
| 275 | await tree!({ systemPrompt: "" }, discoveryContext); |
| 276 | assert.deepEqual(bridge.getActiveTools(), [ |
| 277 | "read", |
| 278 | "mcp__t3-code__task_status", |
| 279 | "mcp__t3-code__preview_snapshot", |
| 280 | ...(!excludeSearch ? ["tool_search"] : []), |
| 281 | ]); |
| 282 | assert.isFalse( |
| 283 | bridge.tools.some( |
| 284 | (tool) => tool.exposure !== "hidden" && tool.name.endsWith("__delegate_task"), |
| 285 | ), |
| 286 | ); |
| 287 | }, |
| 288 | ); |
| 289 | }); |
| 290 | |
| 291 | describe("Pi tool discovery permissions", () => { |
| 292 | it.each(["approval-required", "auto-accept-edits", "auto"])( |
| 293 | "allows annotated T3 reads in %s while gating mutations and replacements", |
| 294 | async (runtimeMode) => { |
| 295 | const bridge = await loadMcpBridge({ modern: true, runtimeMode }); |
| 296 | const hook = bridge.handlers.get("tool_call") as unknown as ( |
| 297 | event: { toolName: string; input: unknown }, |
| 298 | ctx: { ui: { confirm: (title: string) => Promise<boolean> } }, |
| 299 | ) => Promise<{ block: true; reason: string } | undefined>; |
| 300 | const confirmations: string[] = []; |
| 301 | const ctx = { |
| 302 | ui: { |
| 303 | confirm: async (title: string) => { |
| 304 | confirmations.push(title); |
| 305 | return false; |
| 306 | }, |
| 307 | }, |
| 308 | }; |
| 309 | for (const prefix of ["mcp__t3-code__", "mcp__t3_code__"]) { |
| 310 | assert.isUndefined( |
| 311 | await hook({ toolName: prefix + "orchestrator_capabilities", input: {} }, ctx), |
| 312 | ); |
| 313 | assert.equal( |
| 314 | (await hook({ toolName: prefix + "delegate_task", input: {} }, ctx))?.block, |
| 315 | true, |
| 316 | ); |
| 317 | // task_status acknowledges result delivery, and is intentionally not read-only. |
| 318 | assert.equal( |
| 319 | (await hook({ toolName: prefix + "task_status", input: {} }, ctx))?.block, |
| 320 | true, |
| 321 | ); |
| 322 | } |
| 323 | assert.equal(confirmations.length, 4); |
| 324 | bridge.setBridgeSourcePath("/user/extensions/replacement.ts"); |
| 325 | assert.equal( |
| 326 | (await hook({ toolName: "mcp__t3-code__orchestrator_capabilities", input: {} }, ctx)) |
| 327 | ?.block, |
| 328 | true, |
| 329 | ); |
| 330 | assert.equal( |
| 331 | (await hook({ toolName: "mcp__other__orchestrator_capabilities", input: {} }, ctx))?.block, |
| 332 | true, |
| 333 | ); |
| 334 | assert.equal(confirmations.length, 6); |
| 335 | }, |
| 336 | ); |
| 337 | |
| 338 | it("allows discovery without confirmation and still gates the discovered tool", async () => { |
| 339 | type ToolCallHook = ( |
| 340 | event: { toolName: string; input: unknown }, |
| 341 | ctx: { |
| 342 | signal: AbortSignal; |
| 343 | ui: { |
| 344 | confirm: ( |
| 345 | title: string, |
| 346 | detail: string, |
| 347 | options: { signal: AbortSignal }, |
| 348 | ) => Promise<boolean>; |
| 349 | }; |
| 350 | }, |
| 351 | ) => Promise<{ block: true; reason: string } | undefined>; |
| 352 | let toolCall: ToolCallHook | undefined; |
| 353 | let searchPath = "builtin:tool-search"; |
| 354 | const source = NodeModule.stripTypeScriptTypes( |
| 355 | PI_T3_MCP_EXTENSION_SOURCE.replace(/^import .*;$/gm, "").replace( |
| 356 | "export default async function", |
| 357 | "async function", |
| 358 | ), |
| 359 | ); |
| 360 | await NodeVM.runInNewContext(`${source}\nt3McpExtension(pi)`, { |
| 361 | process: { env: { T3_PI_RUNTIME_MODE: "approval-required" } }, |
| 362 | pi: { |
| 363 | on: (name: string, handler: ToolCallHook) => { |
| 364 | if (name === "tool_call") toolCall = handler; |
| 365 | }, |
| 366 | getAllTools: () => [{ name: "tool_search", sourceInfo: { path: searchPath } }], |
| 367 | }, |
| 368 | }); |
| 369 | assert.isDefined(toolCall); |
| 370 | const confirmations: string[] = []; |
| 371 | const controller = new AbortController(); |
| 372 | const ctx = { |
| 373 | signal: controller.signal, |
| 374 | ui: { |
| 375 | confirm: async (title: string, _detail: string, options: { signal: AbortSignal }) => { |
| 376 | assert.strictEqual(options.signal, controller.signal); |
| 377 | confirmations.push(title); |
| 378 | return false; |
| 379 | }, |
| 380 | }, |
| 381 | }; |
| 382 | assert.isUndefined( |
| 383 | await toolCall!({ toolName: "tool_search", input: { query: "preview_snapshot" } }, ctx), |
| 384 | ); |
| 385 | assert.equal(confirmations.length, 0); |
| 386 | const result = await toolCall!({ toolName: "mcp__t3-code__preview_snapshot", input: {} }, ctx); |
| 387 | assert.equal(result?.block, true); |
| 388 | assert.deepEqual(confirmations, ["Allow mcp__t3-code__preview_snapshot?"]); |
| 389 | |
| 390 | // An extension that replaces the search builtin is not known to be read-only. |
| 391 | searchPath = "/extensions/custom-search.ts"; |
| 392 | const replaced = await toolCall!({ toolName: "tool_search", input: {} }, ctx); |
| 393 | assert.equal(replaced?.block, true); |
| 394 | assert.equal(confirmations.at(-1), "Allow tool_search?"); |
| 395 | }); |
| 396 | }); |
| 397 | |
| 398 | async function loadRequestHook(): Promise<RequestHook> { |
| 399 | const handlers = new Map<string, RequestHook>(); |
| 400 | // Execute the shipped extension with MCP disabled; this path needs no Typebox. |
| 401 | const source = NodeModule.stripTypeScriptTypes( |
| 402 | PI_T3_MCP_EXTENSION_SOURCE.replace(/^import .*;$/gm, "").replace( |
| 403 | "export default async function", |
| 404 | "async function", |
| 405 | ), |
| 406 | ); |
| 407 | await NodeVM.runInNewContext(`${source}\nt3McpExtension(pi)`, { |
| 408 | process: { env: {} }, |
| 409 | pi: { on: (name: string, handler: RequestHook) => handlers.set(name, handler) }, |
| 410 | }); |
| 411 | const hook = handlers.get("before_provider_request"); |
| 412 | assert.isDefined(hook); |
| 413 | return hook!; |
| 414 | } |
| 415 | |
| 416 | describe("Pi upstream output-budget workaround", () => { |
| 417 | it.each(["max_tokens", "max_completion_tokens"])( |
| 418 | "caps %s without changing the conversation or tools", |
| 419 | async (key) => { |
| 420 | const hook = await loadRequestHook(); |
| 421 | const payload = { |
| 422 | model: "moonshotai/kimi-k2.6", |
| 423 | messages: [{ role: "user", content: "hello" }], |
| 424 | tools: [{ type: "function", function: { name: "read" } }], |
| 425 | [key]: 231_969, |
| 426 | }; |
| 427 | const result = hook({ payload }, { model: { provider: "openrouter" } }); |
| 428 | assert.equal(result?.[key], 32_768); |
| 429 | assert.strictEqual(result?.messages, payload.messages); |
| 430 | assert.strictEqual(result?.tools, payload.tools); |
| 431 | assert.equal(result?.model, payload.model); |
| 432 | assert.equal(payload[key], 231_969); |
| 433 | }, |
| 434 | ); |
| 435 | |
| 436 | it("preserves smaller budgets and other providers' payloads", async () => { |
| 437 | const hook = await loadRequestHook(); |
| 438 | for (const payload of [{ max_tokens: 8192 }, { max_completion_tokens: 32_768 }, {}, null]) { |
| 439 | assert.isUndefined(hook({ payload }, { model: { provider: "openrouter" } })); |
| 440 | } |
| 441 | assert.isUndefined( |
| 442 | hook({ payload: { max_tokens: 231_969 } }, { model: { provider: "anthropic" } }), |
| 443 | ); |
| 444 | }); |
| 445 | }); |
| 446 | |
| 447 | async function loadInputHook(commands: ReadonlyArray<SkillCommand>) { |
| 448 | const handlers = await loadHooks(commands); |
| 449 | const hook = handlers.get("input"); |
| 450 | assert.isDefined(hook); |
| 451 | return hook as InputHook; |
| 452 | } |
| 453 | |
| 454 | describe("Pi skill references", () => { |
| 455 | it("loads every selected skill once while preserving inline prose, whitespace, and images", async () => { |
| 456 | const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "pi-skill-mentions-")); |
| 457 | try { |
| 458 | const alpha = NodePath.join(directory, "alpha.md"); |
| 459 | const beta = NodePath.join(directory, "beta.md"); |
| 460 | await NodeFSP.writeFile(alpha, "ALPHA_INSTRUCTIONS"); |
| 461 | await NodeFSP.writeFile(beta, "BETA_INSTRUCTIONS"); |
| 462 | const hook = await loadInputHook([ |
| 463 | { name: "skill:alpha", source: "skill", sourceInfo: { path: alpha } }, |
| 464 | { name: "skill:beta", source: "skill", sourceInfo: { path: beta } }, |
| 465 | ]); |
| 466 | const text = |
| 467 | "Please use the $alpha philosophy, then $beta and $alpha\n```ts\n const x = 1;\n```"; |
| 468 | const images = [{ type: "image", data: "fixture" }]; |
| 469 | const result = await hook({ text, images }, { ui: { notify: assert.fail } }); |
| 470 | assert.equal(result?.action, "transform"); |
| 471 | assert.isTrue(result?.text.startsWith(text + "\n\n")); |
| 472 | assert.equal(result?.text.split("ALPHA_INSTRUCTIONS").length, 2); |
| 473 | assert.equal(result?.text.split("BETA_INSTRUCTIONS").length, 2); |
| 474 | assert.include(result?.text ?? "", `References are relative to ${directory}.`); |
| 475 | assert.strictEqual(result?.images, images); |
| 476 | } finally { |
| 477 | await NodeFSP.rm(directory, { recursive: true }); |
| 478 | } |
| 479 | }); |
| 480 | |
| 481 | it("leaves unknown references and native skill commands for Pi", async () => { |
| 482 | const hook = await loadInputHook([ |
| 483 | { name: "skill:alpha", source: "skill", sourceInfo: { path: "/unused" } }, |
| 484 | ]); |
| 485 | // Punctuation-adjacent references are plain text in the composer, not selected chips. |
| 486 | for (const text of [ |
| 487 | "Explain $HOME", |
| 488 | "/skill:alpha use $alpha", |
| 489 | "Hello", |
| 490 | "$missing", |
| 491 | "Use $alpha, then continue", |
| 492 | "Use ($alpha)", |
| 493 | ]) { |
| 494 | assert.isUndefined(await hook({ text }, { ui: { notify: assert.fail } })); |
| 495 | } |
| 496 | }); |
| 497 | |
| 498 | it("loads additional chips without duplicating a leading native skill", async () => { |
| 499 | const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "pi-skill-mentions-")); |
| 500 | try { |
| 501 | const path = NodePath.join(directory, "beta.md"); |
| 502 | await NodeFSP.writeFile(path, "BETA_INSTRUCTIONS"); |
| 503 | const hook = await loadInputHook([ |
| 504 | { name: "skill:alpha", source: "skill", sourceInfo: { path: "/unused" } }, |
| 505 | { name: "skill:beta", source: "skill", sourceInfo: { path } }, |
| 506 | ]); |
| 507 | const text = "/skill:alpha use $alpha and $beta"; |
| 508 | const result = await hook({ text }, { ui: { notify: assert.fail } }); |
| 509 | assert.isTrue(result?.text.startsWith(text + "\n\n")); |
| 510 | assert.notInclude(result?.text ?? "", '<skill name="alpha"'); |
| 511 | assert.include(result?.text ?? "", "BETA_INSTRUCTIONS"); |
| 512 | } finally { |
| 513 | await NodeFSP.rm(directory, { recursive: true }); |
| 514 | } |
| 515 | }); |
| 516 | |
| 517 | it("reports an unreadable skill without deleting its reference or other selected instructions", async () => { |
| 518 | const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "pi-skill-mentions-")); |
| 519 | try { |
| 520 | const path = NodePath.join(directory, "readable.md"); |
| 521 | await NodeFSP.writeFile(path, "READABLE_INSTRUCTIONS"); |
| 522 | const hook = await loadInputHook([ |
| 523 | { |
| 524 | name: "skill:missing", |
| 525 | source: "skill", |
| 526 | sourceInfo: { path: NodePath.join(directory, "missing.md") }, |
| 527 | }, |
| 528 | { name: "skill:readable", source: "skill", sourceInfo: { path } }, |
| 529 | ]); |
| 530 | const notices: string[] = []; |
| 531 | const text = "Use $missing and $readable"; |
| 532 | const result = await hook( |
| 533 | { text }, |
| 534 | { |
| 535 | ui: { |
| 536 | notify: (message) => { |
| 537 | notices.push(message); |
| 538 | }, |
| 539 | }, |
| 540 | }, |
| 541 | ); |
| 542 | assert.isTrue(result?.text.startsWith(text + "\n\n")); |
| 543 | assert.include(result?.text ?? "", "READABLE_INSTRUCTIONS"); |
| 544 | assert.equal(notices.length, 1); |
| 545 | assert.include(notices[0] ?? "", "Could not load skill missing"); |
| 546 | } finally { |
| 547 | await NodeFSP.rm(directory, { recursive: true }); |
| 548 | } |
| 549 | }); |
| 550 | }); |