Release / Version consistency (push) Canceled after 0s
Release / Build (macos-latest) (push) Canceled after 0s
Release / Build (ubuntu-latest) (push) Canceled after 0s
Release / Build (windows-latest) (push) Canceled after 0s
Release / Attach macOS install script (push) Canceled after 0s
修复「改名后模型别名保留旧前缀,僵尸条目删了又出现」这一类配置不一致。 - 改名时同步重键模型别名、全局默认模型、供应商记住的默认模型、子代理池条目 - 改名冲突(撞已存在供应商名 / 遗留裸别名)加 -2/-3 后缀,不再静默吞模型 - 修复复制供应商复用移动语义 rekey 导致源供应商模型被清空 - 加载时不再合并「供应商已不存在」的 SQLite 僵尸模型 - 删除供应商/模型时清理子代理池条目、池默认回落、default_model 置空、 SQLite 用量查询配置(不误删另一 agent,读取失败即跳过) - 防呆:复制供应商改为 dirty-only,放弃修改不再落盘 - 测试基建:统一 crate 级 test_state 锁,消除环境变量并发竞争 测试:前端 146(新增 16)、Rust 197(新增 4)
125 lines
4.8 KiB
TypeScript
125 lines
4.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { purgeSubagentPoolAliases, rekeySubagentPoolAliases } from "./referenceCleanup";
|
|
|
|
const rawWithPool = {
|
|
thinking: { enabled: true },
|
|
secondary_model: {
|
|
default_model: "opencode-go/longcat-2.5-preview-free",
|
|
model: "opencode-go/longcat-2.5-preview-free",
|
|
models: {
|
|
"TokenHub/deepseek-v4.1-flash": "",
|
|
"opencode-go/longcat-2.5-preview-free": "",
|
|
"opencode-go/mimo-v2.6-flash": "",
|
|
"opencode-go-copy/deepseek-v4-flash": "route to deepseek",
|
|
},
|
|
force: false,
|
|
},
|
|
services: { moonshot_fetch: { api_key: "" } },
|
|
};
|
|
|
|
describe("rekeySubagentPoolAliases", () => {
|
|
it("re-keys pool keys and defaults when the provider is renamed", () => {
|
|
const next = rekeySubagentPoolAliases(rawWithPool, {
|
|
"opencode-go-copy/deepseek-v4-flash": "opencode-go/deepseek-v4-flash",
|
|
}) as typeof rawWithPool;
|
|
|
|
const section = next.secondary_model as Record<string, any>;
|
|
expect(Object.keys(section.models).sort()).toEqual([
|
|
"TokenHub/deepseek-v4.1-flash",
|
|
"opencode-go/deepseek-v4-flash",
|
|
"opencode-go/longcat-2.5-preview-free",
|
|
"opencode-go/mimo-v2.6-flash",
|
|
]);
|
|
// Descriptions survive the re-key.
|
|
expect(section.models["opencode-go/deepseek-v4-flash"]).toBe("route to deepseek");
|
|
// Untouched defaults stay put.
|
|
expect(section.default_model).toBe("opencode-go/longcat-2.5-preview-free");
|
|
// Unrelated sections are preserved.
|
|
expect(next.thinking).toEqual({ enabled: true });
|
|
});
|
|
|
|
it("follows the default when it referenced a renamed alias", () => {
|
|
const raw = {
|
|
secondary_model: {
|
|
default_model: "old/m1",
|
|
model: "old/m1",
|
|
models: { "old/m1": "" },
|
|
},
|
|
};
|
|
const next = rekeySubagentPoolAliases(raw, { "old/m1": "new/m1" }) as any;
|
|
expect(next.secondary_model.default_model).toBe("new/m1");
|
|
expect(next.secondary_model.model).toBe("new/m1");
|
|
expect(Object.keys(next.secondary_model.models)).toEqual(["new/m1"]);
|
|
});
|
|
|
|
it("returns the same reference when nothing matches", () => {
|
|
expect(rekeySubagentPoolAliases(rawWithPool, { "a/b": "c/d" })).toBe(rawWithPool);
|
|
});
|
|
|
|
it("never injects an empty models table into the implicit single-model form", () => {
|
|
// `{ model: "old/m1" }` with no pool table: re-keying the default must
|
|
// not add `[secondary_model.models]` — an empty table is invalid for the
|
|
// engine (its default has to be a key of the table).
|
|
const raw = { secondary_model: { model: "old/m1" } };
|
|
const next = rekeySubagentPoolAliases(raw, { "old/m1": "new/m1" }) as any;
|
|
expect(next.secondary_model).toEqual({ model: "new/m1" });
|
|
expect(next.secondary_model.models).toBeUndefined();
|
|
});
|
|
|
|
it("never injects a models table into the force form", () => {
|
|
const raw = { secondary_model: { force: true, default_model: "old/m1", model: "old/m1" } };
|
|
const next = rekeySubagentPoolAliases(raw, { "old/m1": "new/m1" }) as any;
|
|
expect(next.secondary_model).toEqual({
|
|
force: true,
|
|
default_model: "new/m1",
|
|
model: "new/m1",
|
|
});
|
|
expect(next.secondary_model.models).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("purgeSubagentPoolAliases", () => {
|
|
it("removes deleted aliases and keeps the rest of the section intact", () => {
|
|
const next = purgeSubagentPoolAliases(rawWithPool, [
|
|
"opencode-go-copy/deepseek-v4-flash",
|
|
]) as typeof rawWithPool;
|
|
const section = next.secondary_model as Record<string, any>;
|
|
expect(Object.keys(section.models).sort()).toEqual([
|
|
"TokenHub/deepseek-v4.1-flash",
|
|
"opencode-go/longcat-2.5-preview-free",
|
|
"opencode-go/mimo-v2.6-flash",
|
|
]);
|
|
expect(section.default_model).toBe("opencode-go/longcat-2.5-preview-free");
|
|
expect(section.force).toBe(false);
|
|
expect(next.thinking).toEqual({ enabled: true });
|
|
});
|
|
|
|
it("falls back to the first remaining key when the default was purged", () => {
|
|
const next = purgeSubagentPoolAliases(rawWithPool, [
|
|
"opencode-go/longcat-2.5-preview-free",
|
|
]) as any;
|
|
const section = next.secondary_model;
|
|
expect(section.models).not.toHaveProperty("opencode-go/longcat-2.5-preview-free");
|
|
expect(section.default_model).toBe("TokenHub/deepseek-v4.1-flash");
|
|
expect(section.model).toBe("TokenHub/deepseek-v4.1-flash");
|
|
});
|
|
|
|
it("drops the whole section when the pool becomes empty", () => {
|
|
const raw = {
|
|
thinking: { enabled: true },
|
|
secondary_model: {
|
|
default_model: "p/m",
|
|
model: "p/m",
|
|
models: { "p/m": "" },
|
|
},
|
|
};
|
|
const next = purgeSubagentPoolAliases(raw, ["p/m"]) as any;
|
|
expect(next).not.toHaveProperty("secondary_model");
|
|
expect(next.thinking).toEqual({ enabled: true });
|
|
});
|
|
|
|
it("returns the same reference when no pool entry is affected", () => {
|
|
expect(purgeSubagentPoolAliases(rawWithPool, ["other/m9"])).toBe(rawWithPool);
|
|
});
|
|
});
|