| ... | @@ -46,7 +46,7 @@ import * as IdAllocator from "../IdAllocator.ts"; | ... | @@ -46,7 +46,7 @@ import * as IdAllocator from "../IdAllocator.ts"; |
| 46 | import type { ProviderContinuationRequest } from "../ProviderContinuationRequests.ts"; | 46 | import type { ProviderContinuationRequest } from "../ProviderContinuationRequests.ts"; |
| 47 | import * as ProviderContinuationRequests from "../ProviderContinuationRequests.ts"; | 47 | import * as ProviderContinuationRequests from "../ProviderContinuationRequests.ts"; |
| 48 | import { OPENCODE_PROVIDER } from "./OpenCodeAdapterV2.ts"; | 48 | import { OPENCODE_PROVIDER } from "./OpenCodeAdapterV2.ts"; |
| 49 | import { OPENCODE_2_STILL_STOPPING } from "./OpenCode2AdapterV2.ts"; | 49 | import { OPENCODE_2_STILL_STOPPING, t3McpServerName } from "./OpenCode2AdapterV2.ts"; |
| 50 | import { openCode2ReplayRuntime } from "./OpenCode2AdapterV2.testkit.ts"; | 50 | import { openCode2ReplayRuntime } from "./OpenCode2AdapterV2.testkit.ts"; |
| 51 | | 51 | |
| 52 | const SESSION = "ses_f148ca2deffeJcwCnRQtb0YFNX"; | 52 | const SESSION = "ses_f148ca2deffeJcwCnRQtb0YFNX"; |
| ... | @@ -2702,6 +2702,86 @@ describe("OpenCode2 adapter", () => { | ... | @@ -2702,6 +2702,86 @@ describe("OpenCode2 adapter", () => { |
| 2702 | }).pipe(Effect.scoped), | 2702 | }).pipe(Effect.scoped), |
| 2703 | ); | 2703 | ); |
| 2704 | | 2704 | |
| | 2705 | it.effect("registers a long thread id's MCP server under a name OpenCode accepts", () => |
| | 2706 | Effect.gen(function* () { |
| | 2707 | // Spelled out, this delegated thread's server name would be 120 characters. |
| | 2708 | const child = ThreadId.make( |
| | 2709 | "thread:delegated-task:command%3Amcp%3A48bef2bf-6d0e-4f7a-9c3b-2e5d8a1f7c40%3Adelegate-task%3Asubproject-b-round1", |
| | 2710 | ); |
| | 2711 | const server = "t3-code-aa73fa1e03099934"; |
| | 2712 | McpProviderSession.setMcpProviderSession({ |
| | 2713 | environmentId: EnvironmentId.make("environment:opencode2-adapter"), |
| | 2714 | threadId: child, |
| | 2715 | providerSessionId: "mcp:opencode2-adapter", |
| | 2716 | providerInstanceId: instanceId, |
| | 2717 | endpoint: "http://127.0.0.1:3773/mcp", |
| | 2718 | authorizationHeader: "Bearer thread-credential", |
| | 2719 | browserToolsAvailable: false, |
| | 2720 | }); |
| | 2721 | yield* Effect.addFinalizer(() => |
| | 2722 | Effect.sync(() => McpProviderSession.clearMcpProviderSession(child)), |
| | 2723 | ); |
| | 2724 | const runtime = yield* openCode2ReplayRuntimeWithInstructions([ |
| | 2725 | ...opening, |
| | 2726 | out("session.get", { sessionID: SESSION }), |
| | 2727 | // The session already has this thread's rules, so they are not rewritten. |
| | 2728 | replyData( |
| | 2729 | "session.get", |
| | 2730 | sessionInfo({ |
| | 2731 | permissions: [ |
| | 2732 | { action: "*", resource: "*", effect: "allow" }, |
| | 2733 | { action: "t3-code-*", resource: "*", effect: "deny" }, |
| | 2734 | { action: `${server}_*`, resource: "*", effect: "allow" }, |
| | 2735 | ], |
| | 2736 | }), |
| | 2737 | ), |
| | 2738 | ...noOpenRequests, |
| | 2739 | out("mcp.add", { |
| | 2740 | server, |
| | 2741 | "location[directory]": WORK, |
| | 2742 | config: { |
| | 2743 | type: "remote", |
| | 2744 | url: "http://127.0.0.1:3773/mcp", |
| | 2745 | headers: { Authorization: "Bearer thread-credential" }, |
| | 2746 | oauth: false, |
| | 2747 | }, |
| | 2748 | }), |
| | 2749 | reply("mcp.add", null), |
| | 2750 | out("session.prompt", { sessionID: SESSION, text: "<any>" }), |
| | 2751 | promptAccepted, |
| | 2752 | event("session.execution.succeeded", { sessionID: SESSION }), |
| | 2753 | out("mcp.remove", { server, "location[directory]": WORK }), |
| | 2754 | reply("mcp.remove", null), |
| | 2755 | ]); |
| | 2756 | const thread = yield* runtime.resumeThread({ |
| | 2757 | providerThread: { ...providerThread(yield* DateTime.now), appThreadId: child }, |
| | 2758 | threadId: child, |
| | 2759 | modelSelection: bigPickle, |
| | 2760 | runtimePolicy: policy(), |
| | 2761 | }); |
| | 2762 | const terminal = yield* terminalOf(runtime).pipe(Effect.forkScoped); |
| | 2763 | yield* runtime.startTurn({ ...turnInput(thread), threadId: child }); |
| | 2764 | assert.equal((yield* Fiber.join(terminal))?.status, "completed"); |
| | 2765 | yield* runtime.unloadThread!({ providerThread: thread }); |
| | 2766 | }).pipe(Effect.scoped), |
| | 2767 | ); |
| | 2768 | |
| | 2769 | it("names each thread's MCP server within OpenCode's limits, one name per thread", () => { |
| | 2770 | const project = "thread:project:ce04e4e2-6c29-4ff0-a1d7-b089dd63e258"; |
| | 2771 | const ids = [ |
| | 2772 | `${project}:d3b2d715-c4a1-4b63-bb65-1634c3a3a8c4`, |
| | 2773 | `${project}:d3b2d715-c4a1-4b63-bb65-1634c3a3a8c5`, |
| | 2774 | "thread:delegated-task:command%3Amcp%3A48bef2bf-6d0e-4f7a-9c3b-2e5d8a1f7c40%3Adelegate-task%3Around1", |
| | 2775 | "thread:delegated-task:command%3Amcp%3A48bef2bf-6d0e-4f7a-9c3b-2e5d8a1f7c40%3Adelegate-task%3Around2", |
| | 2776 | ]; |
| | 2777 | const names = ids.map(t3McpServerName); |
| | 2778 | for (const name of names) assert.match(name, /^t3-code-[A-Za-z0-9_-]{1,56}$/); |
| | 2779 | assert.equal(new Set(names).size, ids.length); |
| | 2780 | assert.deepEqual(ids.map(t3McpServerName), names); |
| | 2781 | // A name that already fits stays readable. |
| | 2782 | assert.equal(t3McpServerName(threadId), "t3-code-thread_opencode2-adapter"); |
| | 2783 | }); |
| | 2784 | |
| 2705 | it.effect("reads user and assistant text from the session's message list", () => | 2785 | it.effect("reads user and assistant text from the session's message list", () => |
| 2706 | Effect.gen(function* () { | 2786 | Effect.gen(function* () { |
| 2707 | const runtime = yield* openCode2ReplayRuntimeWithInstructions([ | 2787 | const runtime = yield* openCode2ReplayRuntimeWithInstructions([ |