@@ -469,7 +459,7 @@ export function ChannelModelManager({ channel, onChanged }: { channel: ModelChan
]}
>
-
上游共返回 {fetchPreviewModels.length} 个模型。默认已全选,可批量全选或取消全选;已存在的模型不会重复导入。
+
上游共返回 {fetchPreviewModels.length} 个模型。默认勾选已存在的模型,新模型默认不勾选;可批量全选或取消全选,已存在的模型不会重复导入。
(`/admin/channels/${encodeURIComponent(channelId)}/models/fetch`)");
expect(apiSource).toContain("http.post<{ models: string[]; added: number }>(`/admin/channels/${encodeURIComponent(channelId)}/models/import`, { models })");
expect(component).toContain('title="选择要导入的模型"');
- expect(component).toContain("默认已全选");
+ expect(component).toContain("默认勾选已存在的模型,新模型默认不勾选");
expect(component).toContain("setFetchPreviewOpen(true)");
- expect(component).toContain("setSelectedFetchModels(result.models)");
+ expect(component).toContain("setSelectedFetchModels(selectExistingImportModels(result.models,");
expect(component).toContain("已选择 {selectedFetchModels.length} / {fetchPreviewModels.length} 个模型");
expect(component).toContain("disabled={importing || !visibleFetchModels.length || allVisibleFetchModelsSelected}");
expect(component).toContain("onClick={() => setSelectedFetchModels((current) => selectVisibleModels(current, visibleFetchModels))}");
diff --git a/web/test/agent-live2d-lifecycle.test.ts b/web/test/agent-live2d-lifecycle.test.ts
index f223a915b..b2ec09629 100644
--- a/web/test/agent-live2d-lifecycle.test.ts
+++ b/web/test/agent-live2d-lifecycle.test.ts
@@ -1,8 +1,12 @@
import { expect, test } from "bun:test";
+import { readFileSync } from "node:fs";
import panelSource from "../src/components/canvas/canvas-cloud-agent-panel.tsx" with { type: "text" };
import partsSource from "../src/components/canvas/canvas-cloud-agent-panel-parts.tsx" with { type: "text" };
+// 同一路径一旦被 text 加载器注册,bun 1.3.9 的模块缓存会让其它测试的具名导入失效,这里直接读源码文本。
+const avatarSource = readFileSync(new URL("../src/components/canvas/live2d-avatar.tsx", import.meta.url), "utf8");
+
test("Live2D launcher remains mounted while the Agent panel is open", () => {
expect(panelSource).toContain("
{
expect(partsSource).toContain("hidden = false");
expect(partsSource).toContain('display: "none"');
});
+
+test("launcher waits for appearance configuration before displaying the default orb", () => {
+ expect(partsSource).toContain("const appearanceResolved = useAppearanceStore((state) => state.resolved)");
+ expect(partsSource).toContain(") : appearanceResolved ? (");
+ expect(partsSource).toContain("正在加载助手形象");
+});
+
+test("Live2D fallback is reserved for load or render failures, not initial loading", () => {
+ expect(avatarSource).toContain("{failed ? (");
+ expect(avatarSource).toContain(") : !ready ? (");
+ expect(avatarSource).not.toContain("{failed || !ready ? {
+ test("new drafts use 未分类 and imported drafts display the upstream ID without overwriting custom names", () => {
+ expect(initialChannelModelValues(null, protocols).displayName).toBe("未分类");
+ const imported = { modelKey: "product-id", providerModelKey: "upstream-id", displayName: "未分类", channelLabel: "", priceTiers: [] } as unknown as ChannelModel;
+ expect(initialChannelModelValues(imported, protocols)).toMatchObject({ displayName: "未分类", channelLabel: "upstream-id" });
+ expect(initialChannelModelValues({ ...imported, displayName: "product-id", channelLabel: "手动命名" }, protocols)).toMatchObject({ displayName: "product-id", channelLabel: "手动命名" });
+ });
test("Kemei Seedream selection initializes the image profile and preserves pricing", () => {
const draft = initialChannelModelValues(null, protocols);
draft.priceTiers[0].unitPrice = 19;
diff --git a/web/test/channel-model-import-selection.test.ts b/web/test/channel-model-import-selection.test.ts
index b9c6fe52b..8be6b5f69 100644
--- a/web/test/channel-model-import-selection.test.ts
+++ b/web/test/channel-model-import-selection.test.ts
@@ -1,6 +1,6 @@
import { expect, test } from "bun:test";
-import { deselectVisibleModels, filterImportModels, selectVisibleModels } from "../src/pages/admin/components/channel-model-import-selection";
+import { deselectVisibleModels, filterImportModels, selectExistingImportModels, selectVisibleModels } from "../src/pages/admin/components/channel-model-import-selection";
const MODELS = [
"MiniMax-H3",
@@ -50,3 +50,9 @@ test("取消选中只移除可见的,隐藏的勾选不动", () => {
test("无关键字时取消选中等于清空,与旧行为一致", () => {
expect(deselectVisibleModels(MODELS, filterImportModels(MODELS, ""))).toEqual([]);
});
+
+test("已有模型默认勾选,新模型默认不勾选,沿用上游标识匹配规则", () => {
+ const available = ["models/gpt-a", "new-model", " GPT-B ", "gpt-c"];
+ expect(selectExistingImportModels(available, ["gpt-a", "models/gpt-b", "gpt-c"])).toEqual(["models/gpt-a", " GPT-B ", "gpt-c"]);
+ expect(selectExistingImportModels(available, [])).toEqual([]);
+});
diff --git a/web/test/live2d-avatar-loading.test.tsx b/web/test/live2d-avatar-loading.test.tsx
new file mode 100644
index 000000000..508d4fb02
--- /dev/null
+++ b/web/test/live2d-avatar-loading.test.tsx
@@ -0,0 +1,27 @@
+import { expect, test } from "bun:test";
+import { renderToStaticMarkup } from "react-dom/server";
+
+import { Live2DAvatar } from "../src/components/canvas/live2d-avatar";
+
+function renderLoadingAvatar(reducedMotion = false) {
+ return renderToStaticMarkup(默认球} />);
+}
+
+test("Live2D first render shows loading rather than the failure fallback", () => {
+ const markup = renderLoadingAvatar();
+ expect(markup).not.toContain("data-default-avatar");
+ expect(markup).not.toContain("默认球");
+ expect(markup).toContain('role="status"');
+ expect(markup).toContain("正在加载 Live2D 模型");
+ expect(markup).toContain('aria-busy="true"');
+ expect(markup).toContain("visibility:hidden");
+ expect(markup).toContain("width:165px;height:220px");
+});
+
+test("Live2D loading respects reduced motion without showing the default avatar", () => {
+ expect(renderLoadingAvatar()).toContain("motion-safe:animate-spin");
+ const markup = renderLoadingAvatar(true);
+ expect(markup).not.toContain("animate-spin");
+ expect(markup).not.toContain("data-default-avatar");
+ expect(markup).toContain("正在加载 Live2D 模型");
+});
diff --git a/web/test/model-picker-groups.test.ts b/web/test/model-picker-groups.test.ts
index 468502038..f6cd3e3d8 100644
--- a/web/test/model-picker-groups.test.ts
+++ b/web/test/model-picker-groups.test.ts
@@ -35,7 +35,7 @@ test("same model display name groups all channels and preserves their prices", (
expect(groups).toHaveLength(1);
expect(config.channels.map((channel) => channel.modelCosts![0].description)).toEqual(["", "优惠渠道-993的使用说明", "特惠渠道-730的使用说明"]);
expect(groups.map((group) => [group.label, group.kind, group.models.map((item) => [item.label, item.models])])).toEqual([
- ["Seedance 2.0", "product", [["系统模型", ["a::seedance-2.0"]], ["优惠渠道-993", ["b::seedance-2.0"]], ["特惠渠道-730", ["c::seedance-2.0"]]]],
+ ["Seedance 2.0", "product", [["seedance-2.0", ["a::seedance-2.0"]], ["优惠渠道-993", ["b::seedance-2.0"]], ["特惠渠道-730", ["c::seedance-2.0"]]]],
]);
expect(config.channels.map((channel) => priceTierSummaryLabel(priceTiersForCurrentSelection(channel.modelCosts![0].logicalPriceTiers!, "video", config)))).toEqual(["0.3 积分/秒", "0.3 积分/秒", "0.2 积分/秒"]);
});
@@ -155,3 +155,18 @@ test("managed logical models and personal channels never become system product r
expect(groups[0].models).toHaveLength(1);
expect(groupModelsForPicker(config, [])).toEqual([]);
});
+
+test("new models share 未分类 while explicit groups equal to model IDs remain intact", () => {
+ const channels = systemChannelModelChannels([{ id: "upstream", name: "Provider", displayName: "Provider", models: [
+ model("upstream-a", 300000, "a", "未分类"),
+ model("upstream-b", 300000, "b", "未分类"),
+ model("upstream-c", 300000, "c", "c"),
+ ] }]);
+ const config = normalizeConfigSnapshot({ config: { ...defaultConfig, channels } }).config;
+ const groups = groupModelsForPicker(config, selectableModelsByCapability(config, "video"));
+ expect(groups.map((group) => group.label)).toEqual(["未分类", "c"]);
+ expect(groups[0].models.map((item) => [item.label, item.models])).toEqual([
+ ["upstream-a", ["upstream::a"]], ["upstream-b", ["upstream::b"]],
+ ]);
+ expect(groups[1].models[0].models).toEqual(["upstream::c"]);
+});