fix: fix configurations
CI / verify (pull_request) Successful in 10s

This commit is contained in:
2026-10-08 09:18:01 +02:00
parent dd71bcb4d4
commit deaa4bfd07
12 changed files with 232 additions and 250 deletions
+10 -1
View File
@@ -238,7 +238,16 @@ function host(fixture: Awaited<ReturnType<typeof scenario>>, directory: string)
// The adapter reads these domain objects at construction; unimplemented operations
// stay undefined and fail loudly if the plugin starts using them.
const context = {
options: { provider: "fixture" },
options: {
agents: {
"fast-lo": "fixture/gpt-6-luna#low",
"fast-hi": "fixture/gpt-6-luna#high",
"good-lo": "fixture/gpt-6.1-sol#low",
"good-hi": "fixture/gpt-6.1-sol#high",
"deep-lo": "fixture/gpt-6-astra#low",
"deep-hi": "fixture/gpt-6-astra#high",
},
},
location,
app: { version: "2.0.22-fixture" },
command: {},
+46 -6
View File
@@ -30,6 +30,16 @@ const unrelated = {
},
};
const names = ["agents", "multi-work", "quota", "optchat"];
const empty = {
agents: {
"fast-lo": "",
"fast-hi": "",
"good-lo": "",
"good-hi": "",
"deep-lo": "",
"deep-hi": "",
},
};
const helper = new URL("../scripts/install.ts", import.meta.url);
async function run(
command: string[],
@@ -123,7 +133,9 @@ test("upgrades retain selection and options; explicit selection replaces only ow
const f = await fixture();
await install(f.source, f.config, "agents,quota");
const current = await contents(f.config);
current.plugins[1].options = { mode: "gpt", provider: "openai", lunaPriority: false };
current.plugins[1].options = {
agents: { ...empty.agents, "fast-lo": "openai/gpt-6-luna#low" },
};
current.plugins.push({ package: "after", options: { untouched: 1 } });
await writeFile(join(f.config, "opencode.json"), JSON.stringify(current));
await install(f.source, f.config);
@@ -159,7 +171,7 @@ test("mixed string/object registrations upgrade without flags and preserve unrel
before,
{
package: "./plugins/stanp-opencode/agents",
options: { mode: "gpt", provider: "openai", lunaPriority: true },
options: empty,
},
{ package: "./plugins/stanp-opencode/quota" },
after,
@@ -188,7 +200,7 @@ test("owned string registrations retain their selection and explicit selection r
"before",
{
package: "./plugins/stanp-opencode/agents",
options: { mode: "gpt", provider: "openai", lunaPriority: true },
options: empty,
},
{ package: "./plugins/stanp-opencode/multi-work" },
{ package: "./plugins/stanp-opencode/quota" },
@@ -216,7 +228,7 @@ for (const target of ["./plugins/stanp-opencode", "stanp-opencode@1.0.0", "absol
"before",
{
package: "./plugins/stanp-opencode/agents",
options: { mode: "gpt", provider: "openai", lunaPriority: true },
options: empty,
},
{ package: "./plugins/stanp-opencode/multi-work" },
{ package: "./plugins/stanp-opencode/quota" },
@@ -225,6 +237,34 @@ for (const target of ["./plugins/stanp-opencode", "stanp-opencode@1.0.0", "absol
});
}
test("installer writes empty option skeletons, fills only missing keys, and is idempotent", async () => {
const f = await fixture();
await install(f.source, f.config, "agents,multi-work,quota,optchat");
const fresh = await contents(f.config);
expect(fresh.plugins.slice(1)).toEqual([
{ package: "./plugins/stanp-opencode/agents", options: empty },
{ package: "./plugins/stanp-opencode/multi-work" },
{ package: "./plugins/stanp-opencode/quota" },
{
package: "./plugins/stanp-opencode/optchat",
options: { compactionModel: "", dataDir: "" },
},
]);
fresh.plugins[1].options = { agents: { "fast-lo": "openai/gpt-6-luna#low" }, extra: 1 };
fresh.plugins[4].options = { compactionModel: "acme/cheap" };
await writeFile(join(f.config, "opencode.json"), JSON.stringify(fresh));
await install(f.source, f.config);
const filled = await contents(f.config);
expect(filled.plugins[1].options).toEqual({
agents: { ...empty.agents, "fast-lo": "openai/gpt-6-luna#low" },
extra: 1,
});
expect(filled.plugins[4].options).toEqual({ compactionModel: "acme/cheap", dataDir: "" });
expect(filled).toMatchObject(unrelated);
await install(f.source, f.config);
expect(await contents(f.config)).toEqual(filled);
});
test("local installer retains executable permissions and supports direct invocation", async () => {
const f = await fixture();
const path = new URL("../install-local.sh", import.meta.url).pathname;
@@ -244,7 +284,7 @@ test("replaces the legacy monolith without duplicates and retains Agents options
{ package: "before" },
{
package: "./plugins/stanp-opencode",
options: { mode: "gpt", provider: "openai", lunaPriority: false },
options: { agents: { "fast-lo": "openai/gpt-6-luna#low" } },
},
{ package: "stanp-opencode" },
{ package: "after" },
@@ -263,7 +303,7 @@ test("replaces the legacy monolith without duplicates and retains Agents options
.map((name) => `./plugins/stanp-opencode/${name}`),
"after",
]);
expect(result.plugins[1].options.lunaPriority).toBe(false);
expect(result.plugins[1].options.agents["fast-lo"]).toBe("openai/gpt-6-luna#low");
expect(await Bun.file(join(f.config, "plugins", "stanp-opencode.js")).exists()).toBe(false);
expect(result).toMatchObject(unrelated);
});
+23
View File
@@ -437,3 +437,26 @@ test("checkpoints are not logged, provider-executed calls end a turn, and empty
"user: next",
]);
});
test.each([{}, { compactionModel: "", dataDir: "" }])(
"without compactionModel (%j), optchat registers nothing",
async (options) => {
const registered: string[] = [];
const record = (name: string) => async () => {
registered.push(name);
return { dispose: async () => {} };
};
const context = {
options,
tool: { transform: record("tool") },
command: { transform: record("command") },
session: { hook: record("hook") },
};
// SAFETY: setup must return before touching any host service; a call is recorded and fails the test.
const setup = optchat.setup as (
context: Pick<Plugin.Context, "options">,
) => ReturnType<typeof optchat.setup>;
expect(await setup(context)).toBeUndefined();
expect(registered).toEqual([]);
},
);
+43 -41
View File
@@ -8,7 +8,7 @@ import type { DeepMutable } from "@opencode/plugin/promise/types";
import type { Skill } from "@opencode/schema/skill";
import type { ModelInfo } from "@opencode/client";
import plugin from "../src/agents.js";
import { settings, gptAgents, workerNames } from "../src/config.js";
import { model } from "../src/model.js";
import { z } from "zod";
import { AgentsRpc } from "../src/agents-rpc.js";
@@ -23,9 +23,17 @@ const models = ["gpt-6-luna", "gpt-6.1-sol", "gpt-6-astra"].map((id) => ({
settings: { reasoningEffort: effort },
})),
}));
const agents = {
"fast-lo": "test/gpt-6-luna#low",
"fast-hi": "test/gpt-6-luna#high",
"good-lo": "test/gpt-6.1-sol#low",
"good-hi": "test/gpt-6.1-sol#high",
"deep-lo": "test/gpt-6-astra#low",
"deep-hi": "test/gpt-6-astra#high",
};
async function host(
catalog: ModelInfo[] = models,
options: Plugin.Context["options"] = { provider: "test" },
options: Plugin.Context["options"] = { agents },
existingSkill?: Skill.Info,
) {
const agents = new Map<string, DeepMutable<Agent.Info>>();
@@ -233,7 +241,7 @@ test("Agents is independently loadable with no quota, storage, MCP, or custom to
test("preserves an existing user anti-overengineering skill", async () => {
const first = await host();
const original = { ...first.skills.get("anti-overengineering")!, content: "user definition" };
const h = await host(models, { provider: "test" }, original);
const h = await host(models, { agents }, original);
expect(h.skills.get("anti-overengineering")).toEqual(original);
});
@@ -250,26 +258,44 @@ test("worker availability, deep fallback, and model refresh remain provider awar
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra");
});
test("custom models use provider-qualified names and optional deep assignments", async () => {
const assignments = Object.fromEntries(
workerNames
.filter((name) => !name.startsWith("deep-"))
.map((name) => [name, { ...gptAgents[name], model: `test/${gptAgents[name].model}` }]),
test("without agents options or with the empty skeleton nothing is registered", async () => {
const skeleton = Object.fromEntries(Object.keys(agents).map((name) => [name, ""]));
for (const options of [{}, { agents: skeleton }]) {
const h = await host(models, options);
expect([...h.agents.keys()]).toEqual(["build"]);
expect(h.skills.size).toBe(0);
}
});
test("empty deep pair disables deep; partially filled required assignments fail", async () => {
const h = await host(models, { agents: { ...agents, "deep-lo": "", "deep-hi": "" } });
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol");
expect(h.agents.has("deep-lo")).toBe(false);
await expect(host(models, { agents: { ...agents, "good-hi": "" } })).rejects.toThrow(
"agents good-hi is required",
);
await expect(host(models, { agents: { "deep-lo": agents["deep-lo"] } })).rejects.toThrow(
"agents fast-lo is required",
);
});
test("assignments are provider-qualified with optional deep pair and variant", async () => {
const shallow = Object.fromEntries(
Object.entries(agents).filter(([name]) => !name.startsWith("deep-")),
);
const duplicate = models.map((model) => ({ ...model, providerID: Provider.ID.make("other") }));
const h = await host([...duplicate, ...models], { mode: "custom", agents: assignments });
const h = await host([...duplicate, ...models], { agents: shallow });
expect(String(h.agents.get("coordinate")?.model?.providerID)).toBe("test");
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol");
expect(h.agents.has("deep-hi")).toBe(false);
expect(settings.safeParse({ mode: "gpt", provider: "test", lunaPriority: true }).success).toBe(
false,
expect(model("a/b/c#high", "n")).toEqual({ providerID: "a", id: "b/c", variant: "high" });
expect(model("a/b", "n")).toEqual({ providerID: "a", id: "b" });
expect(() => model("unqualified#low", "agents fast-lo")).toThrow(
"agents fast-lo must be provider/model[#variant]: unqualified#low",
);
expect(
settings.safeParse({
mode: "custom",
agents: { ...assignments, "fast-lo": { model: "unqualified", effort: "low" } },
}).success,
).toBe(false);
await expect(
host(models, { agents: { ...agents, "fast-lo": "unqualified#low" } }),
).rejects.toThrow("agents fast-lo must be provider/model[#variant]");
});
test("live final build permissions reach workers, coordinate and the native Agents RPC", async () => {
@@ -351,30 +377,6 @@ test("reasoning uses advertised variants and falls back to request settings", as
});
});
test("Luna priority requires official or advertised support and can be disabled", async () => {
const openai = models.map((model) => ({
...model,
providerID: Provider.ID.make("openai"),
package: "@opencode/ai/providers/openai",
}));
const official = await host(openai, { provider: "openai" });
expect(official.agents.get("fast-lo")?.request?.settings.serviceTier).toBe("priority");
expect(official.agents.get("good-lo")?.request?.settings.serviceTier).toBeUndefined();
const disabled = await host(openai, { provider: "openai", lunaPriority: false });
expect(disabled.agents.get("fast-hi")?.request?.settings.serviceTier).toBeUndefined();
const proxy = openai.map((model) => ({
...model,
settings: { baseURL: "https://proxy.example/v1" },
}));
const unadvertised = await host(proxy, { provider: "openai" });
expect(unadvertised.agents.get("fast-lo")?.request?.settings.serviceTier).toBeUndefined();
const advertised = await host(
proxy.map((model) => ({ ...model, settings: { ...model.settings, serviceTier: "fast" } })),
{ provider: "openai" },
);
expect(advertised.agents.get("fast-lo")?.request?.settings.serviceTier).toBe("fast");
});
test("disabled catalog entries do not authorize worker availability", async () => {
const h = await host(
models.map((model) => ({ ...model, enabled: model.id !== "gpt-6-astra" })),