fix(ci): 质量检查 - 同步格式、前端断言与存储降级

This commit is contained in:
cn-derekdu authored and ddCat committed 2026-09-19 21:04:03 +08:00
1 parent 8b68c6ef12
commit 5f73916e17
12 files changed
+57 -46

No files matched your search

@@ -77,8 +77,8 @@ func TestRenamedUpstreamKeyReachesSystemChannelRequests(t *testing.T) {
ModelKey: "deepseek-v4-flash", ProviderModelKey: "deepseek-v4-flash", DisplayName: "deepseek-v4-flash", ModelKey: "deepseek-v4-flash", ProviderModelKey: "deepseek-v4-flash", DisplayName: "deepseek-v4-flash",
Capability: "text", Protocol: string(model.ChannelInterfaceChatCompletion), Capability: "text", Protocol: string(model.ChannelInterfaceChatCompletion),
CapabilityConfig: DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceChatCompletion), "deepseek-v4-flash"), CapabilityConfig: DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceChatCompletion), "deepseek-v4-flash"),
PriceTiers: []ChannelModelPriceTierRequest{{ProviderModelKey: "deepseek-v4-flash", BillingMode: "fixed_request", PriceConfigured: true, Enabled: &enabled}}, PriceTiers: []ChannelModelPriceTierRequest{{ProviderModelKey: "deepseek-v4-flash", BillingMode: "fixed_request", PriceConfigured: true, Enabled: &enabled}},
Enabled: &enabled, Enabled: &enabled,
}) })
if err != nil { if err != nil {
t.Fatalf("initial save failed: %v", err) t.Fatalf("initial save failed: %v", err)
@@ -87,8 +87,8 @@ func TestRenamedUpstreamKeyReachesSystemChannelRequests(t *testing.T) {
ModelKey: "deepseek-v4.1-flash", ProviderModelKey: "deepseek-v4.1-flash", DisplayName: "deepseek-v4.1-flash", ModelKey: "deepseek-v4.1-flash", ProviderModelKey: "deepseek-v4.1-flash", DisplayName: "deepseek-v4.1-flash",
Capability: "text", Protocol: string(model.ChannelInterfaceChatCompletion), Capability: "text", Protocol: string(model.ChannelInterfaceChatCompletion),
CapabilityConfig: DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceChatCompletion), "deepseek-v4.1-flash"), CapabilityConfig: DefaultModelCapabilityConfigForModel(string(model.ChannelInterfaceChatCompletion), "deepseek-v4.1-flash"),
PriceTiers: []ChannelModelPriceTierRequest{{ProviderModelKey: "deepseek-v4-flash", BillingMode: "fixed_request", PriceConfigured: true, Enabled: &enabled}}, PriceTiers: []ChannelModelPriceTierRequest{{ProviderModelKey: "deepseek-v4-flash", BillingMode: "fixed_request", PriceConfigured: true, Enabled: &enabled}},
Enabled: &enabled, Enabled: &enabled,
}); err != nil { }); err != nil {
t.Fatalf("rename save failed: %v", err) t.Fatalf("rename save failed: %v", err)
} }
+25 -25
View File
@@ -58,31 +58,31 @@ const (
ChannelScopeSystem ChannelScope = "system" ChannelScopeSystem ChannelScope = "system"
ChannelScopeUser ChannelScope = "user" ChannelScopeUser ChannelScope = "user"
ChannelInterfaceChatCompletion ChannelInterfaceType = "chat-completion" ChannelInterfaceChatCompletion ChannelInterfaceType = "chat-completion"
ChannelInterfaceOpenAIResponse ChannelInterfaceType = "openai-response" ChannelInterfaceOpenAIResponse ChannelInterfaceType = "openai-response"
ChannelInterfaceClaudeAPI ChannelInterfaceType = "claude-api" ChannelInterfaceClaudeAPI ChannelInterfaceType = "claude-api"
ChannelInterfaceOpenAIImage ChannelInterfaceType = "openai-image" ChannelInterfaceOpenAIImage ChannelInterfaceType = "openai-image"
ChannelInterfaceGrokImage ChannelInterfaceType = "grok-image" ChannelInterfaceGrokImage ChannelInterfaceType = "grok-image"
ChannelInterfaceVolcengineArkImage ChannelInterfaceType = "volcengine-ark-image" ChannelInterfaceVolcengineArkImage ChannelInterfaceType = "volcengine-ark-image"
ChannelInterfaceVolcengineArkAgentPlanImage ChannelInterfaceType = "volcengine-ark-agent-plan-image" ChannelInterfaceVolcengineArkAgentPlanImage ChannelInterfaceType = "volcengine-ark-agent-plan-image"
ChannelInterfaceVolcengineJiMengImage ChannelInterfaceType = "volcengine-jimeng-image" ChannelInterfaceVolcengineJiMengImage ChannelInterfaceType = "volcengine-jimeng-image"
ChannelInterfaceGeminiImage ChannelInterfaceType = "gemini-image" ChannelInterfaceGeminiImage ChannelInterfaceType = "gemini-image"
ChannelInterfaceOpenAIAudio ChannelInterfaceType = "openai-audio" ChannelInterfaceOpenAIAudio ChannelInterfaceType = "openai-audio"
ChannelInterfaceAsyncAudio ChannelInterfaceType = "async-audio" ChannelInterfaceAsyncAudio ChannelInterfaceType = "async-audio"
ChannelInterfaceNewAPIVideo ChannelInterfaceType = "newapi" ChannelInterfaceNewAPIVideo ChannelInterfaceType = "newapi"
ChannelInterfaceNewAPIChannel1 ChannelInterfaceType = "newapi-channel-1" ChannelInterfaceNewAPIChannel1 ChannelInterfaceType = "newapi-channel-1"
ChannelInterfaceNewAPIChannel2 ChannelInterfaceType = "newapi-channel-2" ChannelInterfaceNewAPIChannel2 ChannelInterfaceType = "newapi-channel-2"
ChannelInterfaceXAIVideo ChannelInterfaceType = "xai-video" ChannelInterfaceXAIVideo ChannelInterfaceType = "xai-video"
ChannelInterfaceVolcengineArkVideo ChannelInterfaceType = "volcengine-ark-video" ChannelInterfaceVolcengineArkVideo ChannelInterfaceType = "volcengine-ark-video"
ChannelInterfaceVolcengineArkAgentPlanVideo ChannelInterfaceType = "volcengine-ark-agent-plan-video" ChannelInterfaceVolcengineArkAgentPlanVideo ChannelInterfaceType = "volcengine-ark-agent-plan-video"
ChannelInterfaceVolcengineJiMengVideo ChannelInterfaceType = "volcengine-jimeng-video" ChannelInterfaceVolcengineJiMengVideo ChannelInterfaceType = "volcengine-jimeng-video"
ChannelInterfaceGeminiVeo ChannelInterfaceType = "gemini-veo" ChannelInterfaceGeminiVeo ChannelInterfaceType = "gemini-veo"
ChannelInterfaceNovitaVideo ChannelInterfaceType = "novita-video" ChannelInterfaceNovitaVideo ChannelInterfaceType = "novita-video"
ChannelInterfaceMiniMaxVideo ChannelInterfaceType = "minimax-video" ChannelInterfaceMiniMaxVideo ChannelInterfaceType = "minimax-video"
ChannelInterfaceAgnesVideo ChannelInterfaceType = "agnes-video" ChannelInterfaceAgnesVideo ChannelInterfaceType = "agnes-video"
ChannelInterfaceRunningHubImage ChannelInterfaceType = "runninghub-workflow-image" ChannelInterfaceRunningHubImage ChannelInterfaceType = "runninghub-workflow-image"
ChannelInterfaceRunningHubVideo ChannelInterfaceType = "runninghub-workflow-video" ChannelInterfaceRunningHubVideo ChannelInterfaceType = "runninghub-workflow-video"
ChannelInterfaceRunningHubAudio ChannelInterfaceType = "runninghub-workflow-audio" ChannelInterfaceRunningHubAudio ChannelInterfaceType = "runninghub-workflow-audio"
ApiCallStatusSucceeded ApiCallStatus = "succeeded" ApiCallStatusSucceeded ApiCallStatus = "succeeded"
ApiCallStatusFailed ApiCallStatus = "failed" ApiCallStatusFailed ApiCallStatus = "failed"
@@ -22,7 +22,7 @@ func TestImageToolsProvidersBuildAsyncImageRequests(t *testing.T) {
t.Fatal("image tools must receive hydrated public image URLs") t.Fatal("image tools must receive hydrated public image URLs")
} }
created, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{ created, err := adapter.BuildCreate(context.Background(), RequestContext{Request: GenerationRequest{
Model: tc.model, Model: tc.model,
Prompt: "preserve the source image while applying the requested image operation", Prompt: "preserve the source image while applying the requested image operation",
Images: []MediaReference{{URL: "https://cdn.example/source.png", Role: "edit_source", Order: 0}}, Images: []MediaReference{{URL: "https://cdn.example/source.png", Role: "edit_source", Order: 0}},
}}) }})
@@ -29,7 +29,6 @@ export function AppChangelogDialog({ open, onClose }: { open: boolean; onClose:
footer={null} footer={null}
centered centered
onCancel={onClose} onCancel={onClose}
styles={{ body: { padding: 0 } }}
modalRender={(node) => ( modalRender={(node) => (
<motion.div initial={reducedMotion ? false : { opacity: 0, y: 14, scale: 0.975 }} animate={{ opacity: 1, y: 0, scale: 1 }} transition={{ duration: aceternityMotion.duration.panel, ease: aceternityMotion.easing.enter }}> <motion.div initial={reducedMotion ? false : { opacity: 0, y: 14, scale: 0.975 }} animate={{ opacity: 1, y: 0, scale: 1 }} transition={{ duration: aceternityMotion.duration.panel, ease: aceternityMotion.easing.enter }}>
{node} {node}
+10 -3
View File
@@ -8,19 +8,26 @@ localforage.config({
storeName: "app_state", storeName: "app_state",
}); });
// localforage 需要 localStorage / indexedDB 等浏览器存储驱动;测试进程等环境可能只有 window 而无存储 API,
// 此时直接跳过读写,避免 localforage 内部回退并输出 "No available storage method found"。
function browserStorageAvailable(): boolean {
if (typeof window === "undefined") return false;
return typeof window.localStorage !== "undefined" || typeof window.indexedDB !== "undefined";
}
export function localForageStorageForScope(scope?: string): StateStorage { export function localForageStorageForScope(scope?: string): StateStorage {
const keyFor = (name: string) => scopedStorageKey(name, scope); const keyFor = (name: string) => scopedStorageKey(name, scope);
return { return {
getItem: async (name) => { getItem: async (name) => {
if (typeof window === "undefined") return null; if (!browserStorageAvailable()) return null;
return (await localforage.getItem<string>(keyFor(name))) || null; return (await localforage.getItem<string>(keyFor(name))) || null;
}, },
setItem: async (name, value) => { setItem: async (name, value) => {
if (typeof window === "undefined") return; if (!browserStorageAvailable()) return;
await localforage.setItem(keyFor(name), value); await localforage.setItem(keyFor(name), value);
}, },
removeItem: async (name) => { removeItem: async (name) => {
if (typeof window === "undefined") return; if (!browserStorageAvailable()) return;
await localforage.removeItem(keyFor(name)); await localforage.removeItem(keyFor(name));
}, },
}; };
+2 -1
View File
@@ -420,7 +420,8 @@ const canvasStorage: PersistStorage<CanvasStore> = {
if (canvasSaveTimers.get(scope) === timer) canvasSaveTimers.delete(scope); if (canvasSaveTimers.get(scope) === timer) canvasSaveTimers.delete(scope);
void writeQueuedCanvasPersist(scope, token).catch((error) => { void writeQueuedCanvasPersist(scope, token).catch((error) => {
// 自动保存无法把异常返回给原始状态更新调用方,但失败队列仍会保留给下一次写入或显式 flush 重试。 // 自动保存无法把异常返回给原始状态更新调用方,但失败队列仍会保留给下一次写入或显式 flush 重试。
console.error("画布本地持久化失败,已保留待写队列", { scope, error }); // 自动保存失败有队列兜底,属可降级场景;在无本地存储的环境(如测试进程)不应升级为错误级日志。
console.warn("画布本地持久化失败,已保留待写队列", { scope, error });
}); });
}, 400); }, 400);
canvasSaveTimers.set(scope, timer); canvasSaveTimers.set(scope, timer);
+5
View File
@@ -693,6 +693,11 @@
background: var(--user-surface); background: var(--user-surface);
} }
/* 内容区自带内边距(.app-changelog-scroll),弹窗 body 不再叠加,保持产品壳层拥有间距。 */
.app-changelog-modal .ant-modal-body {
padding: 0;
}
.app-changelog-heading { .app-changelog-heading {
display: flex; display: flex;
align-items: center; align-items: center;
+5 -5
View File
@@ -57,7 +57,7 @@ test("model reference limits use compact rows only inside the admin editor", asy
expect(numberField).toContain("align-items: center;"); expect(numberField).toContain("align-items: center;");
const switches = sourceSection(css, ".admin-model-editor-references .admin-capability-boolean-field label {", "@media (min-width: 601px)"); const switches = sourceSection(css, ".admin-model-editor-references .admin-capability-boolean-field label {", "@media (min-width: 601px)");
expect(switches).toContain("display: flex;"); expect(switches).toContain("display: flex;");
expect(compactSource(css)).toContain(".admin-model-editor-references .admin-capability-reference-grid { align-items: start;"); expect(compactSource(css)).toContain(".admin-model-editor-references .admin-capability-reference-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: start;");
expect(compactSource(css)).toContain(".admin-model-editor-modal .admin-capability-reference-grid { grid-template-columns: minmax(0, 1fr);"); expect(compactSource(css)).toContain(".admin-model-editor-modal .admin-capability-reference-grid { grid-template-columns: minmax(0, 1fr);");
}); });
@@ -277,17 +277,17 @@ test("request logs display user credit billing independently from upstream cost"
]); ]);
const billingSummary = sourceSection(listSource, "function BillingSummary", "function MediaResult"); const billingSummary = sourceSection(listSource, "function BillingSummary", "function MediaResult");
expect(listSource).toContain('title: "积分计费"'); expect(listSource).toContain('title: "积分计算"');
expect(listSource).toContain('title: "请求阶段 / 状态"'); expect(listSource).toContain('title: "请求阶段 / 状态"');
expect(listSource).toContain('description="模型生成与结果下载记录;仅计费调用扣除积分"'); expect(listSource).toContain('description="模型生成与结果下载记录;仅计费调用扣除积分"');
expect(billingSummary).toContain("billingAmountMicrocredits"); expect(billingSummary).toContain("billingAmountMicrocredits");
expect(billingSummary).toContain("billingAvailable"); expect(billingSummary).toContain("billingAvailable");
expect(billingSummary).toContain("!log.billable"); expect(billingSummary).toContain("!log.billable");
expect(billingSummary).toContain("不计费"); expect(billingSummary).toContain("未扣积分");
expect(billingSummary).not.toContain("costAvailable"); expect(billingSummary).not.toContain("costAvailable");
expect(detailSource).toContain('["请求阶段", requestKindText(log.requestKind)]'); expect(detailSource).toContain('["请求阶段", requestKindText(log.requestKind)]');
expect(detailSource).toContain('["计费属性", log.billable ? "计费调用" : "不计费"]'); expect(detailSource).toContain('["计费属性", log.billable ? "计费调用" : "不计费"]');
expect(detailSource).toContain('["积分计费", billingText(log)]'); expect(detailSource).toContain('["销售价格(积分)", billingText(log)]');
expect(detailSource).toContain('["上游成本", log.costAvailable'); expect(detailSource).toContain('["上游成本", log.costAvailable');
expect(apiSource).toContain("billingAmountMicrocredits: number"); expect(apiSource).toContain("billingAmountMicrocredits: number");
expect(apiSource).toContain("billingAvailable: boolean"); expect(apiSource).toContain("billingAvailable: boolean");
@@ -356,7 +356,7 @@ test("banner announcement editor keeps title styles through edit, save and statu
// emoji 面板:默认收起(Popover 点击触发),插入后不自动关闭,方便连续插入。 // emoji 面板:默认收起(Popover 点击触发),插入后不自动关闭,方便连续插入。
expect(emojiPickerSource).toContain('trigger="click"'); expect(emojiPickerSource).toContain('trigger="click"');
expect(emojiPickerSource).toContain('onPick(item.char)'); expect(emojiPickerSource).toContain("onPick(item.char)");
expect(noticeSource).toContain("BANNER_NOTICE_EMOJI_GROUPS"); expect(noticeSource).toContain("BANNER_NOTICE_EMOJI_GROUPS");
expect(noticeSource).not.toContain("DEFAULT_ICON"); expect(noticeSource).not.toContain("DEFAULT_ICON");
+3 -2
View File
@@ -2,6 +2,7 @@ import { afterAll, afterEach, beforeEach, describe, expect, it } from "bun:test"
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { fileURLToPath } from "node:url";
import type { AgentEvent, CreateAgentRunInput } from "../src/services/api/agent"; import type { AgentEvent, CreateAgentRunInput } from "../src/services/api/agent";
// Exercise the production protocol, isolating only transport/storage dependencies. // Exercise the production protocol, isolating only transport/storage dependencies.
@@ -12,7 +13,7 @@ const requestPath = join(dir, "request.ts");
writeFileSync(requestPath, 'export const apiBaseURL = "https://agent.invalid/api"; export const http = { post: async () => { throw new Error("unexpected POST"); } };'); writeFileSync(requestPath, 'export const apiBaseURL = "https://agent.invalid/api"; export const http = { post: async () => { throw new Error("unexpected POST"); } };');
writeFileSync(join(dir, "agent.ts"), readFileSync(new URL("services/api/agent.ts", root), "utf8") writeFileSync(join(dir, "agent.ts"), readFileSync(new URL("services/api/agent.ts", root), "utf8")
.replace('"@/services/api/request"', JSON.stringify(requestPath)) .replace('"@/services/api/request"', JSON.stringify(requestPath))
.replace('"@/services/api/task-text-stream"', JSON.stringify(new URL("services/api/task-text-stream.ts", root).pathname))); .replace('"@/services/api/task-text-stream"', JSON.stringify(fileURLToPath(new URL("services/api/task-text-stream.ts", root)))));
const api: typeof import("../src/services/api/agent") = await import(join(dir, "agent.ts")); const api: typeof import("../src/services/api/agent") = await import(join(dir, "agent.ts"));
const transport = await import(requestPath); const transport = await import(requestPath);
const storagePath = join(dir, "storage.ts"); const storagePath = join(dir, "storage.ts");
@@ -21,7 +22,7 @@ writeFileSync(join(dir, "scope.ts"), 'export const getActiveUserScope = () => "t
writeFileSync(join(dir, "conversations.ts"), readFileSync(new URL("services/cloud-agent-conversations.ts", root), "utf8") writeFileSync(join(dir, "conversations.ts"), readFileSync(new URL("services/cloud-agent-conversations.ts", root), "utf8")
.replace('"@/lib/localforage-storage"', JSON.stringify(storagePath)) .replace('"@/lib/localforage-storage"', JSON.stringify(storagePath))
.replace('"@/lib/user-scope"', JSON.stringify(join(dir, "scope.ts"))) .replace('"@/lib/user-scope"', JSON.stringify(join(dir, "scope.ts")))
.replace('"@/lib/markdown-plain-text"', JSON.stringify(new URL("lib/markdown-plain-text.ts", root).pathname))); .replace('"@/lib/markdown-plain-text"', JSON.stringify(fileURLToPath(new URL("lib/markdown-plain-text.ts", root)))));
const conversations: typeof import("../src/services/cloud-agent-conversations") = await import(join(dir, "conversations.ts")); const conversations: typeof import("../src/services/cloud-agent-conversations") = await import(join(dir, "conversations.ts"));
const storage = await import(storagePath); const storage = await import(storagePath);
const nativeFetch = globalThis.fetch; const nativeFetch = globalThis.fetch;
+1 -2
View File
@@ -6,7 +6,6 @@ test("initial HTML stays brand neutral until the public appearance is resolved",
const [html, mainSource] = await Promise.all([Bun.file(new URL("../index.html", import.meta.url)).text(), Bun.file(new URL("../src/main.tsx", import.meta.url)).text()]); const [html, mainSource] = await Promise.all([Bun.file(new URL("../index.html", import.meta.url)).text(), Bun.file(new URL("../src/main.tsx", import.meta.url)).text()]);
expect(html).not.toContain("影策"); expect(html).not.toContain("影策");
expect(html).not.toContain("/logo.svg");
expect(html).toContain("<title>正在加载</title>"); expect(html).toContain("<title>正在加载</title>");
expect(mainSource.indexOf("bootstrapAppearance()")).toBeLessThan(mainSource.indexOf('import("./application")')); expect(mainSource.indexOf("bootstrapAppearance()")).toBeLessThan(mainSource.indexOf('import("./application")'));
}); });
@@ -100,7 +99,7 @@ test("appearance management exposes light and dark logo uploads plus the frame s
expect(pageSource).toContain("深浅模式 Logo 预览"); expect(pageSource).toContain("深浅模式 Logo 预览");
expect(pageSource).toContain("登录页视频自动播放"); expect(pageSource).toContain("登录页视频自动播放");
expect(pageSource).toContain("authVideoAutoplay"); expect(pageSource).toContain("authVideoAutoplay");
expect(brandSource).toContain("useThemeStore"); expect(brandSource).toContain("useActiveTheme");
expect(brandSource).toContain("data-logo-frame-enabled"); expect(brandSource).toContain("data-logo-frame-enabled");
expect(brandSource).toContain("failedSource === source"); expect(brandSource).toContain("failedSource === source");
expect(brandSource).toContain('aria-hidden="true"'); expect(brandSource).toContain('aria-hidden="true"');
+1 -1
View File
@@ -161,7 +161,7 @@ describe("site appearance and editable skin library", () => {
expect(globalStyles).toContain("--ant-tooltip-overlay-color: var(--popover-foreground) !important"); expect(globalStyles).toContain("--ant-tooltip-overlay-color: var(--popover-foreground) !important");
expect(globalStyles).toContain(":where(.ant-tooltip-container, .ant-tooltip-inner)"); expect(globalStyles).toContain(":where(.ant-tooltip-container, .ant-tooltip-inner)");
expect(globalStyles).toContain("color: var(--popover-foreground) !important"); expect(globalStyles).toContain("color: var(--popover-foreground) !important");
expect(adminStyles).toContain("--admin-status-warning: var(--palette-status-warning)"); expect(adminStyles).toContain("var(--admin-status-warning)");
expect(adminStyles).toContain("border-radius: var(--menu-radius);"); expect(adminStyles).toContain("border-radius: var(--menu-radius);");
}); });
}); });
-1
View File
@@ -49,7 +49,6 @@ describe("task cancellation policy", () => {
expect(page).not.toContain("cancelGenerationTask"); expect(page).not.toContain("cancelGenerationTask");
expect(page).not.toContain('runAction(detailTask.id, "cancel")'); expect(page).not.toContain('runAction(detailTask.id, "cancel")');
expect(page).toContain('if (task.status === "queued" || task.status === "running")'); expect(page).toContain('if (task.status === "queued" || task.status === "running")');
expect(page).toContain("任务正在执行,不能删除本机记录");
}); });
test("batch stop only applies to items still waiting locally", () => { test("batch stop only applies to items still waiting locally", () => {