Files
paseo/packages/server/src/server/agent/providers/opencode/runtime-client.test.ts
T
Matt CowgerandMohamed Boudra 74613e4bc3 feat(opencode): reuse running OpenCode v2 services (#7435)
* feat(opencode): connect v2 client to an existing server

* feat(providers): reuse running agent servers

Keep shared server sessions isolated and retain native tools while refreshes reuse the running service.

* fix(providers): recognize formatted bridge manifests

Managed plugin ownership must survive harmless JSON formatting changes.

* test(providers): distinguish isolated session bindings

Use separate session identities to verify that dedicated helpers keep their environments isolated.

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-10-31 18:55:24 +07:00

250 lines
9.8 KiB
TypeScript

import { expect, test } from "vitest";
import { openCodeMajorVersion } from "./runtime-client.js";
test.each([
["1.14.46", 1],
["unexpected wrapper output", 1],
["", 1],
["opencode v2.0.10\n", 2],
["v2.0.10-beta.1", 2],
])("identifies the OpenCode runtime version %s", (output, major) => {
expect(openCodeMajorVersion(String(output))).toBe(major);
});
test.each(["3.0.0", "2.0.9", "2.0.7", "2.0.4", "2.0.3", "v2.0.1"])(
"rejects unsupported OpenCode version output %s",
(output) => {
expect(() => openCodeMajorVersion(output)).toThrow();
},
);
test("records the runtime once and retains its position across resume", async () => {
const { withOpenCodeRuntimeNotice } = await import("./runtime-notice.js");
const { V2Harness } = await import("./test-utils/v2-harness.js");
const { OpenCodeV2AgentClient } = await import("./v2/agent.js");
const { createTestLogger } = await import("../../../../test-utils/test-logger.js");
const harness = new V2Harness();
const client = new OpenCodeV2AgentClient({
logger: createTestLogger(),
runtime: harness.runtime,
});
const original = withOpenCodeRuntimeNotice(
await client.createSession({ provider: "opencode", cwd: "/tmp/project" }),
2,
);
const notice = { type: "notification", level: "info", message: "This chat uses OpenCode v2." };
try {
expect(original.initialTimeline?.map((entry) => entry.item)).toEqual([notice]);
const handle = original.describePersistence()!;
await original.close();
const resumed = withOpenCodeRuntimeNotice(await client.resumeSession(handle), 2, handle);
try {
expect(resumed.initialTimeline).toEqual([]);
const history = [];
for await (const event of resumed.streamHistory()) history.push(event);
expect(history).toEqual([
{ type: "timeline", provider: "opencode", ...original.initialTimeline![0] },
]);
expect(resumed.describePersistence()?.metadata?.openCodeRuntimeNotices).toEqual(
handle.metadata?.openCodeRuntimeNotices,
);
} finally {
await resumed.close();
}
} finally {
await client.shutdown();
}
});
test("retries a rejected version probe after the configured binary changes", async () => {
const { mkdtemp, writeFile, rm } = await import("node:fs/promises");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const { OpenCodeRuntimeClient } = await import("./runtime-client.js");
const { createTestLogger } = await import("../../../../test-utils/test-logger.js");
const root = await mkdtemp(join(tmpdir(), "opencode-version-probe-"));
const script = join(root, "version.cjs");
const client = new OpenCodeRuntimeClient(createTestLogger(), {
command: { mode: "replace", argv: [process.execPath, script] },
env: { XDG_STATE_HOME: root },
});
try {
await writeFile(script, 'console.log("opencode v2.0.3")');
await expect(client.fetchCatalog({ scope: "global", force: true })).rejects.toThrow(
"Update OpenCode to 2.0.10 or newer, then refresh the provider in Paseo",
);
await writeFile(script, 'console.log("opencode v3.0.0")');
await expect(client.fetchCatalog({ scope: "global", force: true })).rejects.toThrow(
"Unsupported OpenCode major version 3",
);
} finally {
await client.shutdown();
await rm(root, { recursive: true, force: true });
}
});
test.each([
["console.log('wrapper output')", "unrecognized output"],
["process.exit(1)", "failed command"],
])("retries a %s version probe after updating to v2 (%s)", async (initialSource) => {
const { mkdtemp, writeFile, rm } = await import("node:fs/promises");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const { OpenCodeRuntimeClient } = await import("./runtime-client.js");
const { createTestLogger } = await import("../../../../test-utils/test-logger.js");
const root = await mkdtemp(join(tmpdir(), "opencode-version-retry-"));
const script = join(root, "version.cjs");
const client = new OpenCodeRuntimeClient(createTestLogger(), {
command: { mode: "replace", argv: [process.execPath, script] },
env: { XDG_STATE_HOME: root },
});
try {
await writeFile(script, initialSource);
expect((await client.listFeatures({ provider: "opencode", cwd: root }))[0]?.label).toBe(
"Auto Accept",
);
await writeFile(script, 'console.log("opencode v2.0.10")');
expect((await client.listFeatures({ provider: "opencode", cwd: root }))[0]?.label).toBe(
"Auto-accept",
);
} finally {
await client.shutdown();
await rm(root, { recursive: true, force: true });
}
});
test("appends a first notice after old history and records a changed major version", async () => {
const { withOpenCodeRuntimeNotice } = await import("./runtime-notice.js");
const { V2Harness } = await import("./test-utils/v2-harness.js");
const { OpenCodeV2AgentClient } = await import("./v2/agent.js");
const { createTestLogger } = await import("../../../../test-utils/test-logger.js");
const harness = new V2Harness();
harness.history.push({ id: "old", type: "user", text: "Earlier prompt", time: { created: 1 } });
const client = new OpenCodeV2AgentClient({
logger: createTestLogger(),
runtime: harness.runtime,
});
const previous = {
provider: "opencode",
sessionId: "session",
metadata: { cwd: "/tmp/project" },
};
const session = withOpenCodeRuntimeNotice(await client.resumeSession(previous), 2, previous);
try {
const history = [];
for await (const event of session.streamHistory()) history.push(event);
expect(history).toEqual([
expect.objectContaining({
type: "timeline",
item: { type: "user_message", text: "Earlier prompt", messageId: "old" },
}),
expect.objectContaining({
type: "timeline",
item: { type: "notification", level: "info", message: "This chat uses OpenCode v2." },
}),
]);
await session.close();
const v1Handle = {
...previous,
metadata: {
...previous.metadata,
openCodeRuntimeNotices: [{ major: 1, timestamp: "2026-01-01T00:00:00.000Z" }],
},
};
const changed = withOpenCodeRuntimeNotice(await client.resumeSession(v1Handle), 2, v1Handle);
try {
expect(changed.initialTimeline?.map((entry) => entry.item)).toEqual([
{ type: "notification", level: "info", message: "This chat uses OpenCode v2." },
]);
expect(changed.describePersistence()?.metadata?.openCodeRuntimeNotices).toHaveLength(2);
} finally {
await changed.close();
}
} finally {
await session.close();
await client.shutdown();
}
});
test.each([
"process.exit(1)",
'console.log("custom wrapper output")',
"setTimeout(() => {}, 30000)",
])(
"preserves legacy operations when the version probe is inconclusive: %s",
async (source) => {
const { mkdtemp, writeFile, rm } = await import("node:fs/promises");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const { OpenCodeRuntimeClient } = await import("./runtime-client.js");
const { OpenCodeAgentClient } = await import("../opencode-agent.js");
const { createTestLogger } = await import("../../../../test-utils/test-logger.js");
const { OpenCodeServerManager } = await import("./server-manager.js");
const root = await mkdtemp(join(tmpdir(), "opencode-legacy-probe-"));
const script = join(root, "version.cjs");
await writeFile(script, source);
const logger = createTestLogger();
let legacyShutdown = false;
class TrackedServerManager extends OpenCodeServerManager {
override async shutdown() {
legacyShutdown = true;
await super.shutdown();
}
}
const client = new OpenCodeRuntimeClient(
logger,
{
command: { mode: "replace", argv: [process.execPath, script] },
env: { XDG_STATE_HOME: root },
},
{ serverManager: new TrackedServerManager({ logger }) },
);
const legacy = new OpenCodeAgentClient(logger);
const config = { provider: "opencode", cwd: root };
try {
expect(await client.listFeatures(config)).toEqual(await legacy.listFeatures(config));
} finally {
await client.shutdown();
await legacy.shutdown();
await rm(root, { recursive: true, force: true });
}
expect(legacyShutdown).toBe(true);
},
10000,
);
test("selects v2 without probing the binary when an external server is configured", async () => {
const { OpenCodeRuntimeClient } = await import("./runtime-client.js");
const { createTestLogger } = await import("../../../../test-utils/test-logger.js");
const client = new OpenCodeRuntimeClient(createTestLogger(), {
command: { mode: "replace", argv: [process.execPath, "-e", "process.exit(1)"] },
env: { PASEO_OPENCODE_SERVER_URL: "http://127.0.0.1:4096" },
});
try {
const features = await client.listFeatures({ provider: "opencode", cwd: "/tmp/project" });
expect(features.map((feature) => feature.label)).toEqual(["Auto-accept"]);
} finally {
await client.shutdown();
}
});
test("keeps native Paseo tools available when an external server is configured", async () => {
const { OpenCodeRuntimeClient } = await import("./runtime-client.js");
const { createTestLogger } = await import("../../../../test-utils/test-logger.js");
const { OpenCodeBridge } = await import("./bridge.js");
const bridge = new OpenCodeBridge({ paseoHome: "/tmp", logger: createTestLogger() });
const external = new OpenCodeRuntimeClient(
createTestLogger(),
{ env: { PASEO_OPENCODE_SERVER_URL: "http://127.0.0.1:4096" } },
{ bridge },
);
const local = new OpenCodeRuntimeClient(createTestLogger(), undefined, { bridge });
try {
expect(external.capabilities.supportsNativePaseoTools).toBe(true);
expect(local.capabilities.supportsNativePaseoTools).toBe(true);
} finally {
await external.shutdown();
await local.shutdown();
}
});