mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-26 18:30:57 +08:00
665 lines
21 KiB
TypeScript
665 lines
21 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { parse as parseToml } from "smol-toml";
|
|
import {
|
|
extractCodexAgentsString,
|
|
extractCodexBaseUrl,
|
|
extractCodexExperimentalBearerToken,
|
|
extractCodexModelName,
|
|
extractCodexTopLevelInt,
|
|
removeCodexTopLevelField,
|
|
setCodexAgentsString,
|
|
setCodexBaseUrl,
|
|
setCodexModelName,
|
|
setCodexTopLevelInt,
|
|
updateCodexExperimentalBearerToken,
|
|
} from "@/utils/providerConfigUtils";
|
|
|
|
describe("Codex TOML utils", () => {
|
|
it("removes base_url line when set to empty", () => {
|
|
const input = [
|
|
'model_provider = "openai"',
|
|
'base_url = "https://api.example.com/v1"',
|
|
'model = "gpt-5-codex"',
|
|
"",
|
|
].join("\n");
|
|
|
|
const output = setCodexBaseUrl(input, "");
|
|
|
|
expect(output).not.toMatch(/^\s*base_url\s*=/m);
|
|
expect(extractCodexBaseUrl(output)).toBeUndefined();
|
|
expect(extractCodexModelName(output)).toBe("gpt-5-codex");
|
|
});
|
|
|
|
it("removes only the top-level model line when set to empty", () => {
|
|
const input = [
|
|
'model_provider = "openai"',
|
|
'base_url = "https://api.example.com/v1"',
|
|
'model = "gpt-5-codex"',
|
|
"",
|
|
"[profiles.default]",
|
|
'model = "profile-model"',
|
|
"",
|
|
].join("\n");
|
|
|
|
const output = setCodexModelName(input, "");
|
|
|
|
expect(output).not.toMatch(/^model\s*=\s*"gpt-5-codex"$/m);
|
|
expect(output).toMatch(/^\[profiles\.default\]\nmodel = "profile-model"$/m);
|
|
expect(extractCodexModelName(output)).toBeUndefined();
|
|
expect(extractCodexBaseUrl(output)).toBe("https://api.example.com/v1");
|
|
});
|
|
|
|
it("updates existing values when non-empty", () => {
|
|
const input = [
|
|
'model_provider = "openai"',
|
|
"base_url = 'https://old.example/v1'",
|
|
'model = "old-model"',
|
|
"",
|
|
].join("\n");
|
|
|
|
const output1 = setCodexBaseUrl(input, " https://new.example/v1 \n");
|
|
expect(extractCodexBaseUrl(output1)).toBe("https://new.example/v1");
|
|
|
|
const output2 = setCodexModelName(output1, " new-model \n");
|
|
expect(extractCodexModelName(output2)).toBe("new-model");
|
|
});
|
|
|
|
it("updates a double-quoted base_url containing single quotes without duplicating it", () => {
|
|
const input = [
|
|
'model_provider = "custom"',
|
|
'model = "gpt-5.4"',
|
|
"",
|
|
"[model_providers.custom]",
|
|
'name = "custom"',
|
|
'base_url = "https://su\'us.codes/v1"',
|
|
'wire_api = "responses"',
|
|
"requires_openai_auth = true",
|
|
"",
|
|
].join("\n");
|
|
|
|
const output = setCodexBaseUrl(input, "https://su'us'd.codes/v1");
|
|
|
|
expect(extractCodexBaseUrl(output)).toBe("https://su'us'd.codes/v1");
|
|
expect(output.match(/^\s*base_url\s*=/gm)).toHaveLength(1);
|
|
expect(output).toContain("base_url = \"https://su'us'd.codes/v1\"");
|
|
});
|
|
|
|
it("collapses duplicate base_url lines when editing the active provider section", () => {
|
|
const input = [
|
|
'model_provider = "custom"',
|
|
'model = "gpt-5.4"',
|
|
"",
|
|
"[model_providers.custom]",
|
|
'name = "custom"',
|
|
'base_url = "https://old.example/v1"',
|
|
'base_url = "https://older.example/v1"',
|
|
'wire_api = "responses"',
|
|
"requires_openai_auth = true",
|
|
"",
|
|
].join("\n");
|
|
|
|
const output = setCodexBaseUrl(input, "https://new.example/v1");
|
|
|
|
expect(extractCodexBaseUrl(output)).toBe("https://new.example/v1");
|
|
expect(output.match(/^\s*base_url\s*=/gm)).toHaveLength(1);
|
|
expect(output).toContain('base_url = "https://new.example/v1"');
|
|
expect(output).not.toContain("older.example");
|
|
});
|
|
|
|
it("reads and writes base_url in the active provider section", () => {
|
|
const input = [
|
|
'model_provider = "custom"',
|
|
'model = "gpt-5.4"',
|
|
"",
|
|
"[model_providers.custom]",
|
|
'name = "custom"',
|
|
'wire_api = "responses"',
|
|
"",
|
|
"[profiles.default]",
|
|
'approval_policy = "never"',
|
|
"",
|
|
].join("\n");
|
|
|
|
const output = setCodexBaseUrl(input, "https://api.example.com/v1");
|
|
|
|
expect(output).toContain(
|
|
'[model_providers.custom]\nname = "custom"\nwire_api = "responses"\nbase_url = "https://api.example.com/v1"',
|
|
);
|
|
expect(extractCodexBaseUrl(output)).toBe("https://api.example.com/v1");
|
|
});
|
|
|
|
it("recovers a single misplaced base_url from another section", () => {
|
|
const input = [
|
|
'model_provider = "custom"',
|
|
'model = "gpt-5.4"',
|
|
"",
|
|
"[model_providers.custom]",
|
|
'name = "custom"',
|
|
'wire_api = "responses"',
|
|
"",
|
|
"[profiles.default]",
|
|
'approval_policy = "never"',
|
|
'base_url = "https://wrong.example/v1"',
|
|
"",
|
|
].join("\n");
|
|
|
|
expect(extractCodexBaseUrl(input)).toBe("https://wrong.example/v1");
|
|
|
|
const output = setCodexBaseUrl(input, "https://fixed.example/v1");
|
|
|
|
expect(output).toContain(
|
|
'[model_providers.custom]\nname = "custom"\nwire_api = "responses"\nbase_url = "https://fixed.example/v1"',
|
|
);
|
|
expect(output).not.toContain("https://wrong.example/v1");
|
|
expect(output.match(/base_url\s*=/g)).toHaveLength(1);
|
|
});
|
|
|
|
it("does not treat mcp_servers base_url as provider base_url", () => {
|
|
const input = [
|
|
'model_provider = "azure"',
|
|
'model = "gpt-4"',
|
|
"",
|
|
"[model_providers.azure]",
|
|
'name = "Azure OpenAI"',
|
|
'wire_api = "responses"',
|
|
"",
|
|
"[mcp_servers.my_server]",
|
|
'base_url = "http://localhost:8080"',
|
|
"",
|
|
].join("\n");
|
|
|
|
expect(extractCodexBaseUrl(input)).toBeUndefined();
|
|
|
|
const output = setCodexBaseUrl(input, "https://new.azure/v1");
|
|
|
|
expect(output).toContain(
|
|
'[model_providers.azure]\nname = "Azure OpenAI"\nwire_api = "responses"\nbase_url = "https://new.azure/v1"',
|
|
);
|
|
expect(output).toContain(
|
|
'[mcp_servers.my_server]\nbase_url = "http://localhost:8080"',
|
|
);
|
|
});
|
|
|
|
it("writes base_url into a selected custom table when there is no model_provider", () => {
|
|
const output = setCodexBaseUrl(
|
|
'model = "gpt-5.4"\n',
|
|
"https://relay.example/v1",
|
|
);
|
|
|
|
expect(output).toBe(
|
|
[
|
|
'model_provider = "custom"',
|
|
'model = "gpt-5.4"',
|
|
"",
|
|
"[model_providers.custom]",
|
|
'name = "custom"',
|
|
'wire_api = "responses"',
|
|
'base_url = "https://relay.example/v1"',
|
|
].join("\n"),
|
|
);
|
|
expect(extractCodexBaseUrl(output)).toBe("https://relay.example/v1");
|
|
});
|
|
|
|
it("moves a legacy top-level base_url and wire_api into the custom table", () => {
|
|
const input = [
|
|
'base_url = "https://old.example/v1"',
|
|
'model = "gpt-5.4"',
|
|
'wire_api = "chat"',
|
|
"",
|
|
"[mcp_servers.fs]",
|
|
'base_url = "http://localhost:8080"',
|
|
"",
|
|
].join("\n");
|
|
|
|
const output = setCodexBaseUrl(input, "https://new.example/v1");
|
|
|
|
expect(output).toMatch(/^model_provider = "custom"\nmodel = "gpt-5.4"\n/);
|
|
expect(output).toContain(
|
|
'[model_providers.custom]\nname = "custom"\nwire_api = "chat"\nbase_url = "https://new.example/v1"',
|
|
);
|
|
expect(output).toContain(
|
|
'[mcp_servers.fs]\nbase_url = "http://localhost:8080"',
|
|
);
|
|
expect(output).not.toContain("old.example");
|
|
expect(output.match(/^wire_api\s*=/gm)).toHaveLength(1);
|
|
expect(extractCodexBaseUrl(output)).toBe("https://new.example/v1");
|
|
});
|
|
|
|
it("selects an existing custom table instead of adding another one", () => {
|
|
const input = [
|
|
'model = "gpt-5.4"',
|
|
"",
|
|
"[model_providers.custom]",
|
|
'name = "custom"',
|
|
'wire_api = "responses"',
|
|
"",
|
|
].join("\n");
|
|
|
|
const output = setCodexBaseUrl(input, "https://relay.example/v1");
|
|
|
|
expect(output.match(/^\[model_providers\.custom\]$/gm)).toHaveLength(1);
|
|
expect(output).toMatch(/^model_provider = "custom"$/m);
|
|
expect(extractCodexBaseUrl(output)).toBe("https://relay.example/v1");
|
|
});
|
|
|
|
it("finds the active provider table when its header quotes the key", () => {
|
|
for (const header of [
|
|
'[model_providers."custom"]',
|
|
"[model_providers.'custom']",
|
|
"[ model_providers . custom ]",
|
|
'["model_providers".custom]',
|
|
]) {
|
|
const input = [
|
|
'model_provider = "custom"',
|
|
"",
|
|
header,
|
|
'name = "custom"',
|
|
'wire_api = "responses"',
|
|
"",
|
|
"[features]",
|
|
"",
|
|
].join("\n");
|
|
|
|
const output = setCodexBaseUrl(input, "https://relay.example/v1");
|
|
|
|
const parsed = parseToml(output) as Record<string, any>;
|
|
expect(parsed.model_providers.custom, header).toEqual({
|
|
name: "custom",
|
|
wire_api: "responses",
|
|
base_url: "https://relay.example/v1",
|
|
});
|
|
expect(output.split(/^\s*\[/m), header).toHaveLength(3);
|
|
expect(extractCodexBaseUrl(output), header).toBe(
|
|
"https://relay.example/v1",
|
|
);
|
|
expect(
|
|
extractCodexBaseUrl(setCodexBaseUrl(output, "")),
|
|
header,
|
|
).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it("writes base_url into a quoted provider table whose id needs quoting", () => {
|
|
const input = [
|
|
'model_provider = "my.relay"',
|
|
"",
|
|
'[model_providers."my.relay"]',
|
|
'name = "my.relay"',
|
|
"",
|
|
].join("\n");
|
|
|
|
const output = setCodexBaseUrl(input, "https://relay.example/v1");
|
|
|
|
const parsed = parseToml(output) as Record<string, any>;
|
|
expect(Object.keys(parsed.model_providers)).toEqual(["my.relay"]);
|
|
expect(parsed.model_providers["my.relay"].base_url).toBe(
|
|
"https://relay.example/v1",
|
|
);
|
|
});
|
|
|
|
it("updates the bearer token in a provider table whose header quotes the key", () => {
|
|
const input = [
|
|
'model_provider = "custom"',
|
|
"",
|
|
'[model_providers."custom"]',
|
|
'name = "custom"',
|
|
'experimental_bearer_token = "old"',
|
|
"",
|
|
].join("\n");
|
|
|
|
const output = updateCodexExperimentalBearerToken(input, "new");
|
|
|
|
expect(extractCodexExperimentalBearerToken(output)).toBe("new");
|
|
expect(
|
|
(parseToml(output) as Record<string, any>).model_providers.custom
|
|
.experimental_bearer_token,
|
|
).toBe("new");
|
|
});
|
|
|
|
it("skips provider tables quoted inside a multi-line string", () => {
|
|
for (const delimiter of ['"""', "'''"]) {
|
|
const instructions = [
|
|
"Example:",
|
|
'[model_providers."custom"]',
|
|
'base_url = "https://example.invalid/v1"',
|
|
'experimental_bearer_token = "example-token"',
|
|
"",
|
|
].join("\n");
|
|
const input = [
|
|
'model_provider = "custom"',
|
|
`developer_instructions = ${delimiter}`,
|
|
`${instructions}${delimiter}`,
|
|
"",
|
|
"[model_providers.custom]",
|
|
'name = "custom"',
|
|
'base_url = "https://old.invalid/v1"',
|
|
'experimental_bearer_token = "old-token"',
|
|
"",
|
|
].join("\n");
|
|
|
|
expect(extractCodexBaseUrl(input), delimiter).toBe(
|
|
"https://old.invalid/v1",
|
|
);
|
|
expect(extractCodexExperimentalBearerToken(input), delimiter).toBe(
|
|
"old-token",
|
|
);
|
|
|
|
const output = updateCodexExperimentalBearerToken(
|
|
setCodexBaseUrl(input, "https://new.invalid/v1"),
|
|
"new-token",
|
|
);
|
|
|
|
const parsed = parseToml(output) as Record<string, any>;
|
|
expect(parsed.developer_instructions, delimiter).toBe(instructions);
|
|
expect(parsed.model_providers.custom, delimiter).toEqual({
|
|
name: "custom",
|
|
base_url: "https://new.invalid/v1",
|
|
experimental_bearer_token: "new-token",
|
|
});
|
|
}
|
|
});
|
|
|
|
it("never redefines a custom table the line scan cannot see", () => {
|
|
for (const input of [
|
|
'model = "m"\n\n[model_providers.custom] # mine\nname = "custom"\nwire_api = "responses"\n',
|
|
'model = "m"\nmodel_providers = { custom = { name = "custom", wire_api = "responses" } }\n',
|
|
'model = "m"\n\n[model_providers]\ncustom = { name = "custom", wire_api = "responses" }\n',
|
|
]) {
|
|
const output = setCodexBaseUrl(input, "https://r.example/v1");
|
|
const parsed = parseToml(output) as Record<string, any>;
|
|
expect(parsed.model_providers.custom.wire_api, input).toBe("responses");
|
|
expect(extractCodexBaseUrl(output), input).toBe("https://r.example/v1");
|
|
}
|
|
});
|
|
|
|
it("keeps assignment-like lines inside a multi-line string", () => {
|
|
const instructions =
|
|
'line one\nmodel_provider = example\nwire_api = "chat"\n';
|
|
const input = `model = "m"\nbase_instructions = """\n${instructions}"""\n`;
|
|
|
|
const output = setCodexBaseUrl(input, "https://r.example/v1");
|
|
|
|
const parsed = parseToml(output) as Record<string, any>;
|
|
expect(parsed.base_instructions).toBe(instructions);
|
|
expect(parsed.base_url).toBe("https://r.example/v1");
|
|
});
|
|
|
|
it("reads model only from the top-level config", () => {
|
|
const input = [
|
|
'model_provider = "custom"',
|
|
"",
|
|
"[profiles.default]",
|
|
'model = "profile-model"',
|
|
"",
|
|
].join("\n");
|
|
|
|
expect(extractCodexModelName(input)).toBeUndefined();
|
|
});
|
|
|
|
it("handles single-quoted values", () => {
|
|
const input = "base_url = 'https://api.example.com/v1'\nmodel = 'gpt-5'\n";
|
|
|
|
expect(extractCodexBaseUrl(input)).toBe("https://api.example.com/v1");
|
|
expect(extractCodexModelName(input)).toBe("gpt-5");
|
|
});
|
|
|
|
it("reads, writes, and removes top-level integer metadata fields", () => {
|
|
const input = [
|
|
'model_provider = "custom"',
|
|
'model = "deepseek-v4-flash"',
|
|
"",
|
|
"[model_providers.custom]",
|
|
'name = "DeepSeek"',
|
|
"",
|
|
].join("\n");
|
|
|
|
const withContext = setCodexTopLevelInt(
|
|
input,
|
|
"model_context_window",
|
|
128000,
|
|
);
|
|
const withCompact = setCodexTopLevelInt(
|
|
withContext,
|
|
"model_auto_compact_token_limit",
|
|
90000,
|
|
);
|
|
|
|
expect(extractCodexTopLevelInt(withCompact, "model_context_window")).toBe(
|
|
128000,
|
|
);
|
|
expect(
|
|
extractCodexTopLevelInt(withCompact, "model_auto_compact_token_limit"),
|
|
).toBe(90000);
|
|
expect(withCompact).toMatch(/^model_context_window = 128000$/m);
|
|
expect(withCompact).toMatch(/^model_auto_compact_token_limit = 90000$/m);
|
|
|
|
const removed = removeCodexTopLevelField(
|
|
withCompact,
|
|
"model_context_window",
|
|
);
|
|
|
|
expect(
|
|
extractCodexTopLevelInt(removed, "model_context_window"),
|
|
).toBeUndefined();
|
|
expect(removed).toContain("[model_providers.custom]");
|
|
});
|
|
|
|
// P3 回归: 不能在 config 没用 bearer token 模式时, 误为它新增一行
|
|
it("updateCodexExperimentalBearerToken leaves config without the token alone", () => {
|
|
const input = [
|
|
'model_provider = "openai"',
|
|
'base_url = "https://api.example.com/v1"',
|
|
"",
|
|
].join("\n");
|
|
|
|
expect(updateCodexExperimentalBearerToken(input, "new-key")).toBe(input);
|
|
expect(updateCodexExperimentalBearerToken(input, "")).toBe(input);
|
|
});
|
|
|
|
// P3 回归: bearer 模式下清空 API key 必须真正擦掉 token, 让 pickCodexApiKey 的 fallback 找不到
|
|
it("updateCodexExperimentalBearerToken removes the token line when set to empty", () => {
|
|
const input = [
|
|
'model_provider = "thirdparty"',
|
|
"",
|
|
"[model_providers.thirdparty]",
|
|
'name = "Thirdparty"',
|
|
'base_url = "https://thirdparty.example/v1"',
|
|
'experimental_bearer_token = "old-key"',
|
|
"requires_openai_auth = true",
|
|
"",
|
|
].join("\n");
|
|
|
|
const cleared = updateCodexExperimentalBearerToken(input, "");
|
|
|
|
expect(extractCodexExperimentalBearerToken(cleared)).toBeUndefined();
|
|
expect(cleared).toMatch(/requires_openai_auth = true/);
|
|
expect(cleared).toMatch(/base_url = "https:\/\/thirdparty\.example\/v1"/);
|
|
});
|
|
|
|
it("updateCodexExperimentalBearerToken replaces the token inside the active model_providers section", () => {
|
|
const input = [
|
|
'model_provider = "thirdparty"',
|
|
"",
|
|
"[model_providers.thirdparty]",
|
|
'experimental_bearer_token = "old-key"',
|
|
"",
|
|
].join("\n");
|
|
|
|
const updated = updateCodexExperimentalBearerToken(input, "new-key");
|
|
|
|
expect(extractCodexExperimentalBearerToken(updated)).toBe("new-key");
|
|
expect(updated).not.toMatch(/old-key/);
|
|
});
|
|
|
|
it("updateCodexExperimentalBearerToken escapes basic TOML strings and keeps comments", () => {
|
|
const input = [
|
|
'model_provider = "thirdparty"',
|
|
"",
|
|
"[model_providers.thirdparty]",
|
|
'experimental_bearer_token = "old-key" # vendor token',
|
|
"",
|
|
].join("\n");
|
|
|
|
const updated = updateCodexExperimentalBearerToken(input, 'abc"def\\ghi');
|
|
|
|
expect(updated).toContain(
|
|
'experimental_bearer_token = "abc\\"def\\\\ghi" # vendor token',
|
|
);
|
|
expect(extractCodexExperimentalBearerToken(updated)).toBe('abc"def\\ghi');
|
|
});
|
|
|
|
it("updateCodexExperimentalBearerToken escapes all TOML control characters", () => {
|
|
const input = [
|
|
'model_provider = "thirdparty"',
|
|
"",
|
|
"[model_providers.thirdparty]",
|
|
'experimental_bearer_token = "old-key"',
|
|
"",
|
|
].join("\n");
|
|
|
|
const updated = updateCodexExperimentalBearerToken(
|
|
input,
|
|
"a\u0000b\u0001c\u001fd",
|
|
);
|
|
|
|
expect(updated).toContain(
|
|
'experimental_bearer_token = "a\\u0000b\\u0001c\\u001fd"',
|
|
);
|
|
expect(extractCodexExperimentalBearerToken(updated)).toBe(
|
|
"a\u0000b\u0001c\u001fd",
|
|
);
|
|
});
|
|
|
|
it("updateCodexExperimentalBearerToken can replace an already escaped basic string", () => {
|
|
const input = [
|
|
'model_provider = "thirdparty"',
|
|
"",
|
|
"[model_providers.thirdparty]",
|
|
'experimental_bearer_token = "old\\"key" # vendor token',
|
|
"",
|
|
].join("\n");
|
|
|
|
const updated = updateCodexExperimentalBearerToken(input, "new-key");
|
|
|
|
expect(updated).toContain(
|
|
'experimental_bearer_token = "new-key" # vendor token',
|
|
);
|
|
expect(extractCodexExperimentalBearerToken(updated)).toBe("new-key");
|
|
});
|
|
|
|
it("extractCodexExperimentalBearerToken ignores reserved provider tables", () => {
|
|
const input = [
|
|
'model_provider = "openai"',
|
|
'experimental_bearer_token = "top-level-key"',
|
|
"",
|
|
"[model_providers.openai]",
|
|
'experimental_bearer_token = "stale-table-key"',
|
|
"",
|
|
].join("\n");
|
|
|
|
expect(extractCodexExperimentalBearerToken(input)).toBe("top-level-key");
|
|
});
|
|
|
|
it("extractCodexExperimentalBearerToken reads only top-level model_provider", () => {
|
|
const input = [
|
|
'experimental_bearer_token = "top-level-key"',
|
|
"",
|
|
"[profiles.work]",
|
|
'model_provider = "fake"',
|
|
"",
|
|
"[model_providers.fake]",
|
|
'experimental_bearer_token = "wrong-key"',
|
|
"",
|
|
].join("\n");
|
|
|
|
expect(extractCodexExperimentalBearerToken(input)).toBe("top-level-key");
|
|
});
|
|
});
|
|
|
|
describe("Codex [agents] subagent defaults", () => {
|
|
const base = [
|
|
'model_provider = "custom"',
|
|
'model = "gpt-a"',
|
|
"",
|
|
"[model_providers.custom]",
|
|
'base_url = "https://a.example/v1"',
|
|
].join("\n");
|
|
|
|
it("adds an [agents] table and removes it again when cleared", () => {
|
|
const withModel = setCodexAgentsString(
|
|
base,
|
|
"default_subagent_model",
|
|
"gpt-a-mini",
|
|
);
|
|
expect(parseToml(withModel)).toMatchObject({
|
|
model: "gpt-a",
|
|
agents: { default_subagent_model: "gpt-a-mini" },
|
|
});
|
|
|
|
const withEffort = setCodexAgentsString(
|
|
withModel,
|
|
"default_subagent_reasoning_effort",
|
|
"low",
|
|
);
|
|
expect(
|
|
extractCodexAgentsString(withEffort, "default_subagent_reasoning_effort"),
|
|
).toBe("low");
|
|
|
|
const cleared = setCodexAgentsString(
|
|
setCodexAgentsString(withEffort, "default_subagent_model", ""),
|
|
"default_subagent_reasoning_effort",
|
|
"",
|
|
);
|
|
expect(cleared.trim()).toBe(base);
|
|
});
|
|
|
|
it("edits only its key inside an existing [agents] table", () => {
|
|
const input = [
|
|
base,
|
|
"",
|
|
"[agents]",
|
|
"max_threads = 4",
|
|
'default_subagent_model = "old"',
|
|
"",
|
|
"[agents.reviewer]",
|
|
'description = "review"',
|
|
].join("\n");
|
|
|
|
const updated = setCodexAgentsString(
|
|
input,
|
|
"default_subagent_model",
|
|
'k3"mini',
|
|
);
|
|
const parsed = parseToml(updated) as any;
|
|
expect(parsed.agents.default_subagent_model).toBe('k3"mini');
|
|
expect(parsed.agents.max_threads).toBe(4);
|
|
expect(parsed.agents.reviewer.description).toBe("review");
|
|
expect(extractCodexAgentsString(updated, "default_subagent_model")).toBe(
|
|
'k3"mini',
|
|
);
|
|
|
|
const cleared = setCodexAgentsString(updated, "default_subagent_model", "");
|
|
const afterClear = parseToml(cleared) as any;
|
|
expect(afterClear.agents.default_subagent_model).toBeUndefined();
|
|
expect(afterClear.agents.max_threads).toBe(4);
|
|
});
|
|
|
|
it("adds [agents] after an existing [agents.<role>] table", () => {
|
|
const input = `${base}\n\n[agents.reviewer]\ndescription = "review"\n`;
|
|
const updated = setCodexAgentsString(input, "default_subagent_model", "m");
|
|
const parsed = parseToml(updated) as any;
|
|
expect(parsed.agents.default_subagent_model).toBe("m");
|
|
expect(parsed.agents.reviewer.description).toBe("review");
|
|
});
|
|
|
|
it("leaves an inline agents table untouched instead of breaking the TOML", () => {
|
|
const input = `agents = { max_threads = 4 }\n${base}\n`;
|
|
expect(extractCodexAgentsString(input, "default_subagent_model")).toBe(
|
|
undefined,
|
|
);
|
|
const updated = setCodexAgentsString(input, "default_subagent_model", "m");
|
|
expect(() => parseToml(updated)).not.toThrow();
|
|
expect((parseToml(updated) as any).agents).toEqual({ max_threads: 4 });
|
|
});
|
|
});
|