import { expect, test } from "bun:test";
import { createModelChannel, defaultConfig } from "../src/stores/use-config-store";
import { runBackendGenerationTask, runBackendGenerationTaskBatch } from "../src/services/api/generation-task";
import { deleteGenerationTask, formatTaskLog, listGenerationTasks, projectBackendSafeTaskLog, splitGenerationTaskObservationIds, type GenerationTask } from "../src/services/api/task-center";
import { isLocalDreaminaBackgroundTask, localDreaminaCancellationCopy, localDreaminaDetachOutcome, projectLocalDreaminaTask } from "../src/services/local-dreamina-task-projection";
import { LocalDreaminaGenerationClientError, runLocalDreaminaGenerationTask, type LocalDreaminaGenerationInput } from "../src/services/local-dreamina-generation";
import { createGenerationBatchRetryContexts, createGenerationRetryContext, generationTaskMetadata, runBackendCanvasGenerationTask, runCanvasGenerationTaskToConsumer } from "../src/lib/canvas/canvas-project-generation";
import { runCanvasAgentGenerationOps } from "../src/pages/canvas/use-canvas-agent-operations";
import { CanvasNodeType, type CanvasNodeData } from "../src/types/canvas";
import { onlineToolToOps } from "../src/components/canvas/canvas-assistant-panel";
import { generationTaskShowsProgress, generationTaskStageLabel, generationTaskStatusLabel } from "../src/lib/generation-task-display";
import { generationErrorMessage } from "../src/lib/generation-error";
import { generationTaskNodeId, syncGenerationTaskToCanvasStore } from "../src/lib/canvas/canvas-generation-task-sync";
test("业务项目任务和无节点任务不会读取画布项目", async () => {
const task = { id: "chapter-task", type: "canvas_text", status: "succeeded", projectId: "domain-project", clientContext: { domainProjectId: "domain-project", nodeId: "chapter_character_breakdown:chapter" } } as GenerationTask;
expect(await syncGenerationTaskToCanvasStore(task)).toBe(false);
expect(await syncGenerationTaskToCanvasStore({ ...task, clientContext: undefined, inputJson: JSON.stringify({ metadata: { domainProjectId: "domain-project", nodeId: "chapter-node" } }) })).toBe(false);
expect(await syncGenerationTaskToCanvasStore({ ...task, clientContext: undefined })).toBe(false);
});
test("canvas recovery reads node identity from task summaries without full task input", () => {
const task: GenerationTask = { id: "task-summary", type: "canvas_image", status: "running", prompt: "", attempts: 1, createdAt: "", updatedAt: "", clientContext: { nodeId: "node-summary" } };
expect(generationTaskNodeId(task)).toBe("node-summary");
expect(generationTaskNodeId({ ...task, inputJson: "invalid json" })).toBe("node-summary");
expect(generationTaskNodeId({ ...task, clientContext: undefined, inputJson: JSON.stringify({ metadata: { nodeId: "node-detail" } }) })).toBe("node-detail");
expect(generationTaskNodeId({ ...task, clientContext: undefined })).toBe("");
});
function compactSource(source: string) {
return source.replace(/\s+/g, " ").trim();
}
function sourceSection(source: string, startMarker: string, endMarker: string) {
const start = source.indexOf(startMarker);
const end = source.indexOf(endMarker, start + startMarker.length);
expect(start).toBeGreaterThanOrEqual(0);
expect(end).toBeGreaterThan(start);
return compactSource(source.slice(start, end));
}
function backendModelConfig(model: string) {
const channel = createModelChannel({
id: "system-test-channel",
name: "系统测试渠道",
baseUrl: "/api/system-test-channel",
apiKey: "system",
apiFormat: "openai",
scope: "system",
models: [model],
});
const selectedModel = `${channel.id}::${model}`;
return {
...defaultConfig,
channelMode: "remote" as const,
channels: [channel],
model: selectedModel,
imageModel: selectedModel,
videoModel: selectedModel,
audioModel: selectedModel,
};
}
test("Dreamina submit failure categories have bounded user-facing messages", () => {
const cases = [
["dreamina_submit_spawn_failed", "无法启动官方即梦 CLI,任务尚未提交。"],
["dreamina_submit_exit_nonzero", "官方即梦 CLI 未接受本次提交,任务没有自动重试。"],
["dreamina_submit_timeout", "等待官方即梦 CLI 确认提交超时,为避免重复扣费,任务没有自动重试。"],
["dreamina_submit_receipt_missing", "官方即梦 CLI 未返回任务凭证,为避免重复扣费,任务没有自动重试。"],
] as const;
for (const [code, message] of cases) {
expect(generationErrorMessage(new LocalDreaminaGenerationClientError(code, "本机即梦生成请求失败", 502))).toBe(message);
}
});
test("resource storage 403 is not reported as generation channel authentication", () => {
const raw = "参考图片上传失败:OSS 上传失败:403 Forbidden UserDisableUserDisable";
expect(generationErrorMessage(raw)).toBe("对象存储账号已停用,请检查或更换对象存储配置。");
});
test("durable Dreamina submit failures keep their stable user-facing category in task center", () => {
const task = projectLocalDreaminaTask({
id: "dreamina-submit-timeout-task-0001",
provider: "dreamina-cli",
mode: "video",
operation: "image2video",
model: "seedance2.0mini",
status: "failed",
stage: "submission_unknown",
receiptRecorded: false,
errorCode: "dreamina_submit_timeout",
createdAt: "2026-08-12T08:03:53.118Z",
updatedAt: "2026-08-12T08:04:44.191Z",
});
expect(task.errorCode).toBe("dreamina_submit_timeout");
expect(task.error).toBe("等待官方即梦 CLI 确认提交超时,为避免重复扣费,任务没有自动重试。");
});
test("local Dreamina projection preserves stable outputs independently from provider success", () => {
const task = projectLocalDreaminaTask({
id: "dreamina-output-projection-task-0001",
provider: "dreamina-cli",
mode: "image",
operation: "text2image",
model: "image-3.0",
status: "succeeded",
stage: "succeeded",
receiptRecorded: true,
officialStatus: "completed",
lifecycle: "TERMINAL",
terminalOutcome: "SUCCEEDED",
resultState: "PENDING_MATERIALIZATION",
outputs: [
{
outputIndex: 0,
mediaType: "image",
providerArtifactRef: "provider-artifact-opaque",
},
],
createdAt: "2026-08-13T00:00:00.000Z",
updatedAt: "2026-08-13T00:01:00.000Z",
});
expect(task.status).toBe("succeeded");
expect(task.resultState).toBe("PENDING_MATERIALIZATION");
expect(task.outputs).toEqual([
{
outputIndex: 0,
mediaType: "image",
providerArtifactRef: "provider-artifact-opaque",
},
]);
});
test("task display does not claim a submitted Dreamina receipt is actively generating", () => {
const submitting = {
provider: "dreamina-cli",
status: "running",
stage: "submitting",
receiptRecorded: false,
} as GenerationTask;
const submitted = {
provider: "dreamina-cli",
status: "running",
stage: "submitted",
} as GenerationTask;
const generating = { ...submitted, stage: "generating" } as GenerationTask;
const officialPending = { ...submitted, officialStatus: "pending" } as GenerationTask;
const officialProcessing = { ...submitted, officialStatus: "processing" } as GenerationTask;
const officialCompleted = { ...submitted, officialStatus: "completed" } as GenerationTask;
const remoteRunning = { provider: "openai-compatible", status: "running", stage: "submitted" } as GenerationTask;
expect(generationTaskStatusLabel(submitting)).toBe("正在提交");
expect(generationTaskStageLabel(submitting)).toBe("正在提交,等待官方确认");
expect(generationTaskShowsProgress(submitting)).toBe(false);
expect(generationTaskStatusLabel(submitted)).toBe("状态待更新");
expect(generationTaskStageLabel(submitted)).toBe("已提交,等待状态更新");
expect(generationTaskShowsProgress(submitted)).toBe(false);
expect(generationTaskStatusLabel(officialPending)).toBe("官方排队中");
expect(generationTaskStageLabel(officialPending)).toBe("官方返回状态:pending");
expect(generationTaskStatusLabel(officialProcessing)).toBe("生成中");
expect(generationTaskStageLabel(officialProcessing)).toBe("官方返回状态:processing");
expect(generationTaskStatusLabel(officialCompleted)).toBe("官方已完成");
expect(generationTaskStageLabel(officialCompleted)).toBe("官方返回状态:completed");
expect(generationTaskStatusLabel(generating)).toBe("生成中");
expect(generationTaskShowsProgress(generating)).toBe(true);
for (const stage of ["等待队列调度", "后端接管任务", "正在连接上游", "调用生成模型"]) {
expect(generationTaskShowsProgress({ ...generating, provider: "managed", stage })).toBe(false);
}
expect(generationTaskShowsProgress({ ...generating, provider: "managed", stage: "上游生成中" })).toBe(true);
expect(generationTaskStatusLabel(remoteRunning)).toBe("生成中");
});
test("Dreamina submission uncertainty is not an accepted background task", () => {
const uncertain = { id: "dreamina:submit-uncertain-0001", provider: "dreamina-cli", status: "failed", stage: "submission_unknown", receiptRecorded: false, errorCode: "dreamina_submission_unknown" } as GenerationTask;
const projected = projectLocalDreaminaTask({
id: "submit-uncertain-0001",
provider: "dreamina-cli",
mode: "video",
operation: "text2video",
model: "seedance2.0",
status: "failed",
stage: "submission_unknown",
receiptRecorded: false,
lifecycle: "SUBMISSION_UNCERTAIN",
errorCode: "dreamina_submission_unknown",
createdAt: "2026-08-14T00:00:00.000Z",
updatedAt: "2026-08-14T00:00:10.000Z",
});
expect(projected.status).toBe("failed");
expect(projected.error).toBe("提交结果待确认,为避免重复扣费未自动重试。");
expect(generationTaskStatusLabel(uncertain)).toBe("提交结果待确认");
expect(generationTaskStageLabel(uncertain)).toBe("为避免重复扣费,未自动重试");
expect(generationTaskShowsProgress(uncertain)).toBe(false);
expect(isLocalDreaminaBackgroundTask(uncertain)).toBe(false);
expect(localDreaminaCancellationCopy(uncertain)).toBeUndefined();
});
test("Canvas task surfaces route Dreamina uncertainty through shared display semantics without cancellation", async () => {
const [nodeSource, detailSource, scriptSource, taskCenterSource, createSource] = await Promise.all([
Bun.file(new URL("../src/components/canvas/canvas-node-content.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/canvas/canvas-project-status-dialogs.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/components/canvas/canvas-script-node.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/tasks/index.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/create/index.tsx", import.meta.url)).text(),
]);
expect(nodeSource).toContain("generationTaskStageLabel(displayTask)");
expect(nodeSource).toContain("generationTaskShowsProgress(displayTask)");
expect(nodeSource).not.toContain("onCancelTask");
expect(nodeSource).not.toContain("取消生成");
expect(nodeSource).not.toContain('node.metadata?.taskStage || (taskId ? "任务处理中" : "正在创建任务")');
expect(detailSource).toContain("generationTaskStageLabel(task)");
expect(detailSource).toContain("generationTaskShowsProgress(task) ? void runAction(task.id)}");
expect(taskCenterSource).toContain(" {
const playerCSS = await Bun.file(new URL("../src/components/video-player.css", import.meta.url)).text();
expect(playerCSS).toContain('.canvas-video-player[data-player-variant="compact"]:not([data-fullscreen])');
expect(playerCSS).toContain("--media-fullscreen-button-size: 60px");
expect(playerCSS).toContain("--media-slider-track-height: 8px");
expect(playerCSS).toContain("min-height: 64px");
});
test("task center deletion accepts only local Dreamina records", async () => {
await expect(deleteGenerationTask("backend-task-0001")).rejects.toThrow("当前任务不支持删除");
const source = await Bun.file(new URL("../src/pages/tasks/index.tsx", import.meta.url)).text();
const compactedSource = compactSource(source);
const deleteActionSource = sourceSection(source, "const deleteLocalTask =", "const refreshLocalTaskStatus =");
expect(compactedSource).toMatch(/\{detailTask\.provider === "dreamina-cli" \? \(