Files
PaperClipAI/packages/adapters/opencode-local/src/server/runtime-config.test.ts
T
DottaandPaperclip 3910352c92 feat(runner): add experimental Cursor Cloud provider (#18068)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Runner gives each provider durable execution, tool, and completion
contracts.
> - Cursor Cloud currently has a direct adapter outside that native
contract.
> - Cloud work needs exact ownership across HTTP, SSE, Stop, and
recovery.
> - This PR adds a qualified Cloud API driver and verified proxy to
Runner.
> - Product setup and hosted placement remain separate dependent
changes.

## Linked Issues or Issue Description

Refs #5664, #11803, #17299, and #17471. Product integration is #18069.
Managed-only API placement is #18310. The related direct-adapter
model/transcript work is not replaced. No duplicate native Cursor Cloud
integration was found.

## What Changed

- Add native `cursor_cloud` / `cursor_cloud_api` with explicit
model/repository validation, bounded HTTP/SSE, durable per-turn
checkpoints and a qualified Node proxy launched through the Rust facade.
- Commit terminal custody before publication/release; coalesce
consumers, preserve ambiguous create/Stop/recovery fences, reject late
tools, and distinguish definitely-unsent oversized requests from
ambiguous responses.
- Support scoped cold attachment, current authority, reconnect and
verified artifact staging. Keep unsupported warm/external and Windows
paths closed.
- Preserve early source cursors and verify immutable legacy prefixes
before suffix replay, including supported compaction/masking. Optional
proof publication failure falls back to complete cold verification.
- Preserve canonical records at the durable native producer with
JSON-safe privacy and the existing 8MiB bound; drain admitted successor
flushes. Reconcile upstream semantic/pack/schema/lifecycle changes and
retain S3 and legacy CLI checks.

## Verification

Core head `37c442ec8dd4`: all 66 check contexts are terminal
successful/neutral/skipped, including required `ci / verify` and `ci /
e2e`; fresh exact-head Greptile is 5/5 with zero unresolved threads.
Dependent Product `81e828d20a77` and Top `dd2959689627` include the
later client handoff correction and are running their own fresh gates.

Core: 47/47 Rust, 4/4 compiled-native retirement, 19/19 settlement and
normal Runner build passed. Runtime Top `9af1a5de42a2`: build and
recursive typecheck passed. Earlier source-scoped evidence includes 398
focused tests, five relay tests and token/type gates. The 251-assertion,
7/8-suite run failed physical PostgreSQL teardown; the unchanged stable
WSP suite passed 128 through the documented wrapper.

The downstream fixture corrections passed 7/7 browser and 121/121
startup tests without timeout/oracle relaxation; a broken-unsubscribe
control fails. Hosted runtime `9af1a5de42a2` has a passing normal build
and recursive typecheck. Latest dependent heads additionally contain a
pure client fix for stale live/history overlap, with 48 focused tests,
UI typecheck and token gates passing. Its new client is not deployed in
the frozen hosted image; server/native/API/provider source is unchanged
by these four downstream files.

Fresh hosted Linux admission on `9af1a5de42a2` retired its owned
runner/proxy/socket with zero prompts. The saved configuration survived
reopening, and Test authenticated the bound key/catalog/selected model
without a prompt. Artifact with `claude-sonnet-4-5` completed,
downloaded the exact 27 bytes, matched 121 canonical identities against
complete logs and independently reached FINISHED. The active Stop retry
was clicked within its immutable boundary, produced one canonical
interrupted turn and acknowledged run-only cancellation, retained a
valid 35.076s quiet window (maximum gap 4.584s), and independently
remained CANCELLED/artifact-absent beyond the original 180-second
command deadline. All provider-observer credential callbacks, DB clients
and loaders closed; accounting is settled and explicitly unpriced. The
first fresh Stop retains its failed 10.373s sampling gap and every
sample.

On the upgraded9af server, the historical F226 artifact was reopened and
downloaded with its exact original hash after its provider agent had
been deleted/GET404 earlier today. Historical F226 full Artifact/Stop
cleanup, Question, corrected Plan and reconnect retain their original
identities. Deleting the three newly created disposable provider agents
and the new-case post-delete grade remain blocked on explicit user
approval after automatic review rejected expanding the two original
approved IDs. No new-case cleanup or complete fresh matrix pass is
claimed; this is separate from the directly observed task/Stop behavior
and retained exact cleanup proof.

Retained failures: stale fixture 132/133; E8 48/135 with 87 reset-hook
failures; Mac 17 SQL failures/skips; earlier startup-title, title-save,
custody/Stop and deletion failures with unknown causes and bounded
recoveries. `dea023` Artifact passed 17 checks but Stop failed its false
warning and 50 duplicate log identities. Pre-repair ACK failure and
original Core CI failure remain recorded; retry alone establishes no
cause or full local-suite pass. Provider dollar cost stays UNKNOWN.

## Risks

Experimental: Cursor independently acknowledges neither the effective
model nor instruction digest before the first prompt. The caller’s
explicit values are submitted and behavior checked live. Unpriced
provider dollar cost stays UNKNOWN.

Only qualified per-turn controllers are admitted. Uncertain work and
incomplete cleanup stay fenced. Existing damaged/duplicate archives
remain failed evidence. Prefix proof adds bounded object-store I/O; the
8MiB producer bound does not guarantee future providers’ output fits.

## Model Used

OpenAI Codex, GPT-6 family, assisted with reasoning, repository tools,
code execution, browser verification, and delegated code review. The
serving model’s exact API ID and context window are not exposed. No
value is inferred from the model catalog.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used and the limits of available model
metadata
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have linked public issues or described the issue in this PR
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch describes the change and contains no internal ticket ID
- [x] I have run the scoped local tests reported above; pending gates
are stated separately
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation
- [x] I have considered and documented the risks
- [x] All required CI gates pass on the current head
- [x] Fresh Greptile is 5/5 with no unresolved findings on the current
head
- [x] I will address all reviewer comments before requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-11-03 08:38:57 -06:00

463 lines
22 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { execFileSync, spawnSync } from "node:child_process";
import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it, vi } from "vitest";
import { OPENROUTER_ATTRIBUTION_HEADERS } from "@paperclipai/adapter-utils/openrouter-attribution";
import { prepareOpenCodeRuntimeConfig } from "./runtime-config.js";
const cleanupPaths = new Set<string>();
afterEach(async () => {
await Promise.all(
[...cleanupPaths].map(async (filepath) => {
await fs.rm(filepath, { recursive: true, force: true });
cleanupPaths.delete(filepath);
}),
);
});
async function makeConfigHome(initialConfig?: Record<string, unknown>) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-test-"));
cleanupPaths.add(root);
const configDir = path.join(root, "opencode");
await fs.mkdir(configDir, { recursive: true });
if (initialConfig) {
await fs.writeFile(
path.join(configDir, "opencode.json"),
`${JSON.stringify(initialConfig, null, 2)}\n`,
"utf8",
);
}
return root;
}
describe("prepareOpenCodeRuntimeConfig", () => {
it("stages a relocatable sandbox shell hook without changing the parent environment", async () => {
const configHome = await makeConfigHome({ plugin: ["operator-plugin"], permission: { bash: "ask" } });
const input = { env: { XDG_CONFIG_HOME: configHome }, config: { dangerouslySkipPermissions: false }, sandboxToolOomPreference: true };
const prepared = await prepareOpenCodeRuntimeConfig(input);
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
const copiedHome = await makeConfigHome();
const relocated = path.join(copiedHome, "remote config 'with spaces'");
await fs.cp(prepared.env.XDG_CONFIG_HOME, relocated, { recursive: true });
await prepared.cleanup();
const pluginDir = path.join(relocated, "opencode", "plugins");
const plugins = await fs.readdir(pluginDir);
expect(plugins).toHaveLength(1);
const pluginUrl = pathToFileURL(path.join(pluginDir, plugins[0]!)).href;
const hookEnv = JSON.parse(execFileSync(process.execPath, ["--input-type=module", "-e", `
Object.defineProperty(process, "platform", { value: "linux" });
delete process.env.BASH_ENV;
const hooks = await Object.values(await import(${JSON.stringify(pluginUrl)}))[0]();
const output = { env: {} };
await hooks["shell.env"]({}, output);
console.log(JSON.stringify({ child: output.env, parent: process.env.BASH_ENV ?? null }));
`], { encoding: "utf8" }));
expect(hookEnv.parent).toBeNull();
expect(hookEnv.child.BASH_ENV).toContain(relocated);
const command = spawnSync("bash", ["-c", 'printf "%s" "$1"; exit 7', "bash", "literal ' $() value"], {
env: { ...process.env, ...hookEnv.child }, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
});
expect(command.stdout).toBe("literal ' $() value");
expect(command.status).toBe(7);
if (process.platform === "linux") {
const parentScore = await fs.readFile("/proc/self/oom_score_adj", "utf8");
for (const shellLevel of ["0", "1"]) {
expect(execFileSync("bash", ["-c", "cat /proc/self/oom_score_adj"], {
env: { ...process.env, ...hookEnv.child, SHLVL: shellLevel }, encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
}).trim()).toBe("1000");
expect(await fs.readFile("/proc/self/oom_score_adj", "utf8")).toBe(parentScore);
}
}
const config = JSON.parse(await fs.readFile(path.join(relocated, "opencode", "opencode.json"), "utf8"));
expect(config).toMatchObject({ plugin: ["operator-plugin"], permission: { bash: "ask" } });
expect(prepared.env).not.toHaveProperty("BASH_ENV");
});
it("preserves operator shell setup and leaves other platforms and local runs alone", async () => {
const configHome = await makeConfigHome();
const input = { env: { XDG_CONFIG_HOME: configHome }, config: {}, sandboxToolOomPreference: true };
const prepared = await prepareOpenCodeRuntimeConfig(input);
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
const pluginDir = path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "plugins");
const pluginUrl = pathToFileURL(path.join(pluginDir, (await fs.readdir(pluginDir))[0]!)).href;
for (const scenario of [
{ platform: "linux", parent: "/operator/setup.sh", child: {} },
{ platform: "linux", parent: "", child: {} },
{ platform: "linux", parent: null, child: { BASH_ENV: "/other-plugin/setup.sh" } },
{ platform: "darwin", parent: null, child: {} },
]) {
const result = JSON.parse(execFileSync(process.execPath, ["--input-type=module", "-e", `
const scenario = ${JSON.stringify(scenario)};
Object.defineProperty(process, "platform", { value: scenario.platform });
if (scenario.parent === null) delete process.env.BASH_ENV;
else process.env.BASH_ENV = scenario.parent;
const hooks = await Object.values(await import(${JSON.stringify(pluginUrl)}))[0]();
const output = { env: scenario.child };
await hooks["shell.env"]({}, output);
console.log(JSON.stringify(output.env));
`], { encoding: "utf8" }));
expect(result).toEqual(scenario.child);
}
const local = await prepareOpenCodeRuntimeConfig({ env: { XDG_CONFIG_HOME: configHome }, config: {} });
cleanupPaths.add(local.env.XDG_CONFIG_HOME);
expect(await fs.readdir(path.join(local.env.XDG_CONFIG_HOME, "opencode"))).toEqual(["opencode.json"]);
});
it("keeps the existing runtime usable when the optional plugin cannot be installed", async () => {
const configHome = await makeConfigHome({ theme: "system" });
const rename = vi.spyOn(fs, "rename").mockRejectedValueOnce(new Error("fixture write denied"));
try {
const input = { env: { XDG_CONFIG_HOME: configHome }, config: {}, sandboxToolOomPreference: true };
const prepared = await prepareOpenCodeRuntimeConfig(input);
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
const dir = path.join(prepared.env.XDG_CONFIG_HOME, "opencode");
expect(await fs.readdir(path.join(dir, "plugins"))).toEqual([]);
expect(await fs.readdir(dir)).toEqual(["opencode.json", "plugins"]);
expect(prepared.notes.some(note => note.startsWith("Could not prepare"))).toBe(true);
expect(JSON.parse(await fs.readFile(path.join(dir, "opencode.json"), "utf8"))).toMatchObject({ theme: "system" });
} finally {
rename.mockRestore();
}
});
it("materializes linked plugin directories without writing into the operator configuration", async () => {
const configHome = await makeConfigHome();
const operatorRoot = await makeConfigHome();
const operatorPlugins = path.join(operatorRoot, "plugins");
await fs.mkdir(operatorPlugins);
const operatorPlugin = "export const Operator = async () => ({});\n";
await fs.writeFile(path.join(operatorPlugins, "operator.js"), operatorPlugin);
await fs.symlink(operatorPlugins, path.join(configHome, "opencode", "plugins"), "dir");
const prepared = await prepareOpenCodeRuntimeConfig({
env: { XDG_CONFIG_HOME: configHome }, config: {}, sandboxToolOomPreference: true,
});
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
expect(await fs.readdir(operatorPlugins)).toEqual(["operator.js"]);
const plugins = path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "plugins");
expect((await fs.lstat(plugins)).isDirectory()).toBe(true);
expect(await fs.readdir(plugins)).toHaveLength(2);
expect(await fs.readFile(path.join(plugins, "operator.js"), "utf8")).toBe(operatorPlugin);
await prepared.cleanup();
expect(await fs.readdir(operatorPlugins)).toEqual(["operator.js"]);
expect((await fs.lstat(path.join(configHome, "opencode", "plugins"))).isSymbolicLink()).toBe(true);
});
it("attributes custom OpenRouter aliases and inline overrides without changing other endpoints", async () => {
const other = { options: { baseURL: "https://other.example/v1", headers: { "X-Title": "Other" } } };
const configHome = await makeConfigHome({ provider: { other, openrouter: other } });
const prepared = await prepareOpenCodeRuntimeConfig({ env: {
XDG_CONFIG_HOME: configHome,
OPENCODE_CONFIG_CONTENT: JSON.stringify({ provider: { alias: {
options: { baseURL: "https://openrouter.ai/api/v1", apiKey: "fixture-key", headers: { "http-referer": "https://harness.example", "X-Route": "selected" } },
models: { model: { headers: { "x-title": "Harness" } } },
} } }),
}, config: {} });
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
const { alias } = JSON.parse(prepared.env.OPENCODE_CONFIG_CONTENT).provider;
expect(alias.options).toMatchObject({ apiKey: "fixture-key", headers: { ...OPENROUTER_ATTRIBUTION_HEADERS, "X-Route": "selected" } });
expect(alias.options.headers).not.toHaveProperty("http-referer");
expect(alias.models.model.headers).toEqual(OPENROUTER_ATTRIBUTION_HEADERS);
const config = JSON.parse(await fs.readFile(path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"), "utf8"));
expect(config.provider.other).toEqual(other);
expect(config.provider.openrouter).toEqual(other);
await prepared.cleanup();
});
it("allows all tools and connected tools by default", async () => {
const configHome = await makeConfigHome({
permission: {
read: "allow",
bash: "ask",
"mcp__example__write": "deny",
},
theme: "system",
});
const prepared = await prepareOpenCodeRuntimeConfig({
env: { XDG_CONFIG_HOME: configHome },
config: {},
});
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
expect(prepared.env.XDG_CONFIG_HOME).not.toBe(configHome);
const runtimeConfig = JSON.parse(
await fs.readFile(
path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"),
"utf8",
),
) as Record<string, unknown>;
expect(runtimeConfig).toMatchObject({
theme: "system",
permission: "allow",
});
await prepared.cleanup();
cleanupPaths.delete(prepared.env.XDG_CONFIG_HOME);
await expect(fs.access(prepared.env.XDG_CONFIG_HOME)).rejects.toThrow();
});
it("merges custom providers from PAPERCLIP_OPENCODE_PROVIDERS into the config", async () => {
const configHome = await makeConfigHome({ permission: { read: "allow" } });
const providers = {
bifrost: {
npm: "@ai-sdk/openai-compatible",
name: "Bifrost EU",
options: {
baseURL: "http://gateway.example.svc.cluster.local:8080/v1",
apiKey: "{env:ANTHROPIC_API_KEY}",
},
models: { "example/model-a": { name: "Model A" } },
},
};
const prepared = await prepareOpenCodeRuntimeConfig({
env: {
XDG_CONFIG_HOME: configHome,
PAPERCLIP_OPENCODE_PROVIDERS: JSON.stringify(providers),
},
config: {},
});
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
const runtimeConfig = JSON.parse(
await fs.readFile(path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"), "utf8"),
) as Record<string, unknown>;
expect(runtimeConfig).toMatchObject({
permission: "allow",
provider: providers,
});
expect(prepared.notes.some((n) => n.includes("bifrost"))).toBe(true);
await prepared.cleanup();
});
it("reads PAPERCLIP_OPENCODE_PROVIDERS from process.env when absent from the run env", async () => {
const configHome = await makeConfigHome({ permission: { read: "allow" } });
const providers = { bifrost: { npm: "@ai-sdk/openai-compatible", models: { "example/model-a": {} } } };
process.env.PAPERCLIP_OPENCODE_PROVIDERS = JSON.stringify(providers);
try {
const prepared = await prepareOpenCodeRuntimeConfig({
env: { XDG_CONFIG_HOME: configHome },
config: {},
});
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
const runtimeConfig = JSON.parse(
await fs.readFile(path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"), "utf8"),
) as Record<string, unknown>;
expect(runtimeConfig).toMatchObject({ provider: providers });
await prepared.cleanup();
} finally {
delete process.env.PAPERCLIP_OPENCODE_PROVIDERS;
}
});
it("expands {env:VAR} placeholders in custom providers using the run/process env (bakes the literal vk)", async () => {
const configHome = await makeConfigHome({ permission: { read: "allow" } });
const providers = {
bifrost: {
npm: "@ai-sdk/openai-compatible",
options: { baseURL: "http://bifrost/v1", apiKey: "{env:ANTHROPIC_API_KEY}" },
models: { "example/model-a": {} },
},
};
const prepared = await prepareOpenCodeRuntimeConfig({
env: { XDG_CONFIG_HOME: configHome, PAPERCLIP_OPENCODE_PROVIDERS: JSON.stringify(providers), ANTHROPIC_API_KEY: "sk-bf-REALVK" },
config: {},
});
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
const runtimeConfig = JSON.parse(
await fs.readFile(path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"), "utf8"),
) as { provider: { bifrost: { options: { apiKey: string } } } };
// The {env:...} placeholder must be replaced with the literal value, so OpenCode
// does not depend on its sandboxed process env carrying the key.
expect(runtimeConfig.provider.bifrost.options.apiKey).toBe("sk-bf-REALVK");
await prepared.cleanup();
});
it("leaves an unresolvable {env:VAR} placeholder intact", async () => {
const configHome = await makeConfigHome({ permission: { read: "allow" } });
const providers = { bifrost: { options: { apiKey: "{env:DEFINITELY_UNSET_VAR_XYZ}" }, models: { "x/y": {} } } };
const prepared = await prepareOpenCodeRuntimeConfig({
env: { XDG_CONFIG_HOME: configHome, PAPERCLIP_OPENCODE_PROVIDERS: JSON.stringify(providers) },
config: {},
});
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
const runtimeConfig = JSON.parse(
await fs.readFile(path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"), "utf8"),
) as { provider: { bifrost: { options: { apiKey: string } } } };
expect(runtimeConfig.provider.bifrost.options.apiKey).toBe("{env:DEFINITELY_UNSET_VAR_XYZ}");
await prepared.cleanup();
});
it("pins small_model from PAPERCLIP_OPENCODE_SMALL_MODEL", async () => {
const configHome = await makeConfigHome({ permission: { read: "allow" } });
const prepared = await prepareOpenCodeRuntimeConfig({
env: { XDG_CONFIG_HOME: configHome, PAPERCLIP_OPENCODE_SMALL_MODEL: "example/model-a" },
config: {},
});
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
const runtimeConfig = JSON.parse(
await fs.readFile(path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"), "utf8"),
) as { small_model?: string };
expect(runtimeConfig.small_model).toBe("example/model-a");
await prepared.cleanup();
});
it("ignores malformed PAPERCLIP_OPENCODE_PROVIDERS without writing a provider block and surfaces a note", async () => {
const configHome = await makeConfigHome({ permission: { read: "allow" } });
const prepared = await prepareOpenCodeRuntimeConfig({
env: { XDG_CONFIG_HOME: configHome, PAPERCLIP_OPENCODE_PROVIDERS: "not json" },
config: {},
});
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
const runtimeConfig = JSON.parse(
await fs.readFile(path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"), "utf8"),
) as Record<string, unknown>;
expect(runtimeConfig.provider).toMatchObject({ openrouter: { options: { headers: { "HTTP-Referer": "https://paperclip.ing" } } } });
expect(prepared.notes).toContain(
"PAPERCLIP_OPENCODE_PROVIDERS contains invalid JSON; custom providers ignored.",
);
await prepared.cleanup();
});
it("surfaces a note when PAPERCLIP_OPENCODE_PROVIDERS is valid JSON but not an object", async () => {
const configHome = await makeConfigHome({ permission: { read: "allow" } });
const prepared = await prepareOpenCodeRuntimeConfig({
env: { XDG_CONFIG_HOME: configHome, PAPERCLIP_OPENCODE_PROVIDERS: "[1,2,3]" },
config: {},
});
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
const runtimeConfig = JSON.parse(
await fs.readFile(path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"), "utf8"),
) as Record<string, unknown>;
expect(runtimeConfig.provider).toMatchObject({ openrouter: { options: { headers: { "HTTP-Referer": "https://paperclip.ing" } } } });
expect(prepared.notes).toContain(
"PAPERCLIP_OPENCODE_PROVIDERS is set but is not a JSON object; custom providers ignored.",
);
await prepared.cleanup();
});
it("surfaces skipped provider entries with non-object values and keeps the usable ones", async () => {
const configHome = await makeConfigHome({ permission: { read: "allow" } });
const prepared = await prepareOpenCodeRuntimeConfig({
env: {
XDG_CONFIG_HOME: configHome,
PAPERCLIP_OPENCODE_PROVIDERS: JSON.stringify({
bifrost: "http://gateway.example/v1",
usable: { options: { baseURL: "http://gateway.example/v1" } },
}),
},
config: {},
});
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
const runtimeConfig = JSON.parse(
await fs.readFile(path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"), "utf8"),
) as { provider?: Record<string, unknown> };
expect(runtimeConfig.provider?.usable).toBeDefined();
expect(runtimeConfig.provider?.bifrost).toBeUndefined();
expect(prepared.notes).toContain(
"PAPERCLIP_OPENCODE_PROVIDERS: skipped provider(s) with non-object values: bifrost.",
);
await prepared.cleanup();
});
it("surfaces skipped provider entries when no usable entries remain", async () => {
const configHome = await makeConfigHome({ permission: { read: "allow" } });
const prepared = await prepareOpenCodeRuntimeConfig({
env: {
XDG_CONFIG_HOME: configHome,
PAPERCLIP_OPENCODE_PROVIDERS: JSON.stringify({ bifrost: "http://gateway.example/v1" }),
},
config: {},
});
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
const runtimeConfig = JSON.parse(
await fs.readFile(path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"), "utf8"),
) as Record<string, unknown>;
expect(runtimeConfig.provider).toMatchObject({ openrouter: { options: { headers: { "HTTP-Referer": "https://paperclip.ing" } } } });
expect(prepared.notes).toContain(
"PAPERCLIP_OPENCODE_PROVIDERS: skipped provider(s) with non-object values: bifrost.",
);
await prepared.cleanup();
});
it("registers a configured model missing from the catalog on its provider", async () => {
const configHome = await makeConfigHome({ permission: { read: "allow" } });
const prepared = await prepareOpenCodeRuntimeConfig({
env: { XDG_CONFIG_HOME: configHome },
config: { model: "openrouter/openai/gpt-oss-120b:nitro" },
});
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
const runtimeConfig = JSON.parse(
await fs.readFile(path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"), "utf8"),
) as { provider?: Record<string, { models?: Record<string, unknown> }> };
expect(runtimeConfig.provider?.openrouter?.models).toEqual({
"openai/gpt-oss-120b:nitro": { headers: OPENROUTER_ATTRIBUTION_HEADERS },
});
expect(prepared.notes).toContain(
"Registered configured model openrouter/openai/gpt-oss-120b:nitro in the runtime OpenCode config.",
);
await prepared.cleanup();
});
it("does not clobber an explicit model definition when registering the configured model", async () => {
const configHome = await makeConfigHome({ permission: { read: "allow" } });
const providers = {
openrouter: {
models: {
"openai/gpt-oss-120b:nitro": { name: "GPT-OSS 120B (nitro)" },
"example/other": {},
},
},
};
const prepared = await prepareOpenCodeRuntimeConfig({
env: {
XDG_CONFIG_HOME: configHome,
PAPERCLIP_OPENCODE_PROVIDERS: JSON.stringify(providers),
},
config: { model: "openrouter/openai/gpt-oss-120b:nitro" },
});
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
const runtimeConfig = JSON.parse(
await fs.readFile(path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"), "utf8"),
) as { provider?: Record<string, { models?: Record<string, unknown> }> };
expect(runtimeConfig.provider?.openrouter?.models).toEqual(Object.fromEntries(
Object.entries(providers.openrouter.models).map(([id, model]) => [id, { ...model, headers: OPENROUTER_ATTRIBUTION_HEADERS }]),
));
expect(
prepared.notes.some((note) => note.startsWith("Registered configured model")),
).toBe(false);
await prepared.cleanup();
});
it("skips model registration when the configured model is not provider/model shaped", async () => {
const configHome = await makeConfigHome({ permission: { read: "allow" } });
const prepared = await prepareOpenCodeRuntimeConfig({
env: { XDG_CONFIG_HOME: configHome },
config: { model: "not-a-provider-model" },
});
cleanupPaths.add(prepared.env.XDG_CONFIG_HOME);
const runtimeConfig = JSON.parse(
await fs.readFile(path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"), "utf8"),
) as Record<string, unknown>;
expect(runtimeConfig.provider).toMatchObject({ openrouter: { options: { headers: { "HTTP-Referer": "https://paperclip.ing" } } } });
await prepared.cleanup();
});
it("preserves permission settings while attributing OpenRouter when skip-permissions is off", async () => {
const configHome = await makeConfigHome();
const prepared = await prepareOpenCodeRuntimeConfig({
env: { XDG_CONFIG_HOME: configHome },
config: { dangerouslySkipPermissions: false },
});
const config = JSON.parse(await fs.readFile(path.join(prepared.env.XDG_CONFIG_HOME, "opencode", "opencode.json"), "utf8"));
expect(config.permission).toBeUndefined();
expect(config.provider.openrouter.options.headers["HTTP-Referer"]).toBe("https://paperclip.ing");
await prepared.cleanup();
});
});