feat(runtime): 即梦本机运行时 - 接入安全生成闭环与桌面本机渠道

Refs #225

Co-authored-by: zkhyww <127009345+zkhyww@users.noreply.github.com>
This commit is contained in:
ddCat
2026-08-15 12:45:29 +08:00
parent 8f2232feee
commit 57e1820f41
179 changed files with 49746 additions and 1897 deletions

View File

@@ -0,0 +1,258 @@
import { expect, test } from "bun:test";
test("Canvas consumes signed fetch SSE without bearer data in the URL or headers", async () => {
const module = await import("../src/lib/canvas/local-runtime-connection").catch(() => ({}));
const consume = (
module as {
consumeLocalRuntimeEventStream?: (client: RuntimeTransport, path: string, options: { lastEventId?: string; onEvent(event: RuntimeEvent): void }) => Promise<void>;
}
).consumeLocalRuntimeEventStream;
expect(typeof consume).toBe("function");
if (!consume) return;
const calls: Array<{ path: string; headers: Headers; method: string }> = [];
const events: RuntimeEvent[] = [];
await consume(
{
async request(path, init = {}) {
calls.push({ path, headers: new Headers(init.headers), method: String(init.method || "GET") });
return sseResponse(['id: 7\nevent: hello\ndata: {"ok":true}\n\n', "id: 8\nevent: agent_log\ndata: first\ndata: second\n\n"]);
},
},
"/events?clientId=client-1",
{
lastEventId: "6",
onEvent: (event) => events.push(event),
},
);
expect(calls).toHaveLength(1);
expect(calls[0]).toMatchObject({ path: "/events?clientId=client-1", method: "GET" });
expect(calls[0].headers.get("last-event-id")).toBe("6");
expect(calls[0].headers.has("authorization")).toBe(false);
expect(calls[0].headers.has("x-canvas-agent-token")).toBe(false);
expect(calls[0].path).not.toContain("token");
expect(events).toEqual([
{ type: "hello", data: '{"ok":true}', id: "7" },
{ type: "agent_log", data: "first\nsecond", id: "8" },
]);
});
test("Canvas SSE rejects oversized events and pre-cancelled requests", async () => {
const module = await import("../src/lib/canvas/local-runtime-connection");
let calls = 0;
const cancelled = new AbortController();
cancelled.abort();
await expect(
module.consumeLocalRuntimeEventStream(
{
request: async () => {
calls++;
return sseResponse([]);
},
},
"/events?clientId=client-1",
{ signal: cancelled.signal, onEvent: () => undefined },
),
).rejects.toMatchObject({ name: "AbortError" });
expect(calls).toBe(0);
await expect(
module.consumeLocalRuntimeEventStream(
{
request: async () => sseResponse([`event: agent_log\ndata: ${"x".repeat(300 * 1024)}\n\n`]),
},
"/events?clientId=client-1",
{ onEvent: () => undefined },
),
).rejects.toMatchObject({ code: "canvas_event_too_large" });
});
test("Canvas SSE accepts one large transport chunk containing many bounded events", async () => {
const module = await import("../src/lib/canvas/local-runtime-connection");
const chunk = Array.from({ length: 3_000 }, (_, index) => `id: ${index}\nevent: agent_log\ndata: ${"x".repeat(80)}\n\n`).join("");
let events = 0;
await module.consumeLocalRuntimeEventStream(
{
request: async () => sseResponse([chunk]),
},
"/events?clientId=client-1",
{
onEvent: () => {
events++;
},
},
);
expect(events).toBe(3_000);
});
test("Canvas Agent UI store contains no Runtime endpoint, bearer, or session fields", async () => {
const { useCanvasAgentStore } = await import("../src/stores/canvas/use-canvas-agent-store");
const state = useCanvasAgentStore.getState();
expect("url" in state).toBe(false);
expect("token" in state).toBe(false);
expect("endpoint" in state).toBe(false);
expect("session" in state).toBe(false);
});
test("Canvas connection reuses the shared Runtime store and requires the canvas module", async () => {
const module = await import("../src/lib/canvas/local-runtime-connection");
const prepare = (
module as {
prepareCanvasRuntimeConnection?: (store: RuntimeStore, signal?: AbortSignal) => Promise<void>;
}
).prepareCanvasRuntimeConnection;
expect(typeof prepare).toBe("function");
if (!prepare) return;
let connectCalls = 0;
const state = {
connection: "idle",
modules: [] as Array<{ id: string }>,
async connect() {
connectCalls++;
state.connection = "connected";
state.modules = [{ id: "canvas-agent" }, { id: "dreamina" }];
},
};
await prepare({ getState: () => state });
expect(connectCalls).toBe(1);
expect(state.connection).toBe("connected");
await prepare({ getState: () => state });
expect(connectCalls).toBe(2);
const missingState = {
connection: "connected",
modules: [{ id: "dreamina" }],
error: "",
async connect() {
return;
},
};
await expect(prepare({ getState: () => missingState })).rejects.toMatchObject({ code: "canvas_module_unavailable" });
const failedRefreshState = {
connection: "connected",
modules: [{ id: "canvas-agent" }],
error: "",
async connect() {
failedRefreshState.error = "本机运行时连接超时";
},
};
await expect(prepare({ getState: () => failedRefreshState })).rejects.toMatchObject({ code: "canvas_runtime_unavailable" });
});
test("Canvas launch mode auto-connects after rejected legacy deep-link secrets are removed", async () => {
const module = await import("../src/lib/canvas/local-runtime-connection");
const shouldAutoConnect = (
module as {
shouldAutoConnectCanvasRuntime?: (params: URLSearchParams) => boolean;
}
).shouldAutoConnectCanvasRuntime;
expect(typeof shouldAutoConnect).toBe("function");
if (!shouldAutoConnect) return;
expect(shouldAutoConnect(new URLSearchParams("mode=new"))).toBe(true);
expect(shouldAutoConnect(new URLSearchParams("mode=recent"))).toBe(true);
expect(shouldAutoConnect(new URLSearchParams("mode=choose"))).toBe(true);
expect(shouldAutoConnect(new URLSearchParams("agentUrl=http://127.0.0.1:17371"))).toBe(false);
});
test("Canvas reconnect wait resolves quietly when component cleanup aborts", async () => {
const module = await import("../src/lib/canvas/local-runtime-connection");
const waitForReconnect = (
module as {
waitForCanvasRuntimeReconnect?: (signal: AbortSignal, delayMs?: number) => Promise<void>;
}
).waitForCanvasRuntimeReconnect;
expect(typeof waitForReconnect).toBe("function");
if (!waitForReconnect) return;
const controller = new AbortController();
const waiting = waitForReconnect(controller.signal, 60_000);
controller.abort();
await expect(waiting).resolves.toBeUndefined();
});
test("Canvas state sync reports a stable error without exposing Runtime response text", async () => {
const module = await import("../src/lib/canvas/local-runtime-connection");
const postState = (
module as {
postCanvasRuntimeState?: (client: RuntimeTransport, clientId: string, snapshot: unknown) => Promise<void>;
}
).postCanvasRuntimeState;
expect(typeof postState).toBe("function");
if (!postState) return;
const secretResponse = "runtime-secret-response-body";
const calls: Array<{ path: string; headers: Headers; body: string }> = [];
let thrown: unknown;
try {
await postState(
{
async request(path, init = {}) {
calls.push({
path,
headers: new Headers(init.headers),
body: String(init.body || ""),
});
return new Response(secretResponse, { status: 500 });
},
},
"client/1",
{ nodes: [], connections: [] },
);
} catch (error) {
thrown = error;
}
expect(calls).toHaveLength(1);
expect(calls[0].path).toBe("/canvas/state?clientId=client%2F1");
expect(calls[0].headers.get("content-type")).toBe("application/json");
expect(calls[0].headers.has("authorization")).toBe(false);
expect(calls[0].headers.has("x-canvas-agent-token")).toBe(false);
expect(calls[0].body).toBe('{"nodes":[],"connections":[]}');
expect(thrown).toMatchObject({
code: "canvas_state_sync_failed",
message: "画布状态同步失败",
});
expect(JSON.stringify(thrown)).not.toContain(secretResponse);
});
type RuntimeTransport = {
request(path: string, init?: RequestInit): Promise<Response>;
};
type RuntimeEvent = {
type: string;
data: string;
id?: string;
};
function sseResponse(chunks: string[]) {
const encoder = new TextEncoder();
return new Response(
new ReadableStream<Uint8Array>({
start(controller) {
for (const chunk of chunks) controller.enqueue(encoder.encode(chunk));
controller.close();
},
}),
{ headers: { "content-type": "text/event-stream", "cache-control": "no-store" } },
);
}
type RuntimeStore = {
getState(): {
connection: string;
modules: Array<{ id: string }>;
error?: string;
connect(signal?: AbortSignal): Promise<void>;
};
};

View File

@@ -0,0 +1,340 @@
import { expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { createGenerationTaskSubscriptionService, type GenerationTask } from "../src/services/api/task-center";
import { updateCreationConversationSnapshot } from "../src/services/creation-conversation-store";
test("Create exposes one accessible copy action beside each displayed user prompt", async () => {
const source = await Bun.file(new URL("../src/pages/create/index.tsx", import.meta.url)).text();
expect(source).toContain('aria-label="复制提示词"');
expect(source).toContain('copyText(visiblePrompt, "提示词已复制")');
});
test("Create submit button does not forward the browser click event as retry context", async () => {
const source = await Bun.file(new URL("../src/pages/create/index.tsx", import.meta.url)).text();
expect(source).toContain("onSubmit: () => void submit()");
expect(source).not.toContain("onSubmit: submit,");
});
test("Create keeps optimistic messages when the first local task binds immediately", () => {
const initial = [{ id: "conversation-0001", title: "新创作", updatedAt: "2026-08-14T00:00:00.000Z", messages: [] as Array<Record<string, unknown>> }];
const optimistic = updateCreationConversationSnapshot(initial, "conversation-0001", (conversation) => ({
...conversation,
messages: [
{ id: "user-0001", role: "user", content: "fixture" },
{ id: "assistant-0001", role: "assistant", content: "", status: "pending" },
],
}));
const bound = updateCreationConversationSnapshot(optimistic, "conversation-0001", (conversation) => ({
...conversation,
messages: conversation.messages.map((message) => (message.id === "assistant-0001" ? { ...message, taskIds: ["local:dreamina-cli:task-0001"] } : message)),
}));
expect(bound[0].messages).toHaveLength(2);
expect(bound[0].messages[1]).toMatchObject({ id: "assistant-0001", taskIds: ["local:dreamina-cli:task-0001"] });
});
test("Create refresh subscriptions share one durable scheduler observation without page polling", async () => {
let queryCalls = 0;
let waitCalls = 0;
let release!: () => void;
const gate = new Promise<void>((resolveGate) => {
release = resolveGate;
});
const running: GenerationTask = {
id: "dreamina:create-refresh-task-0001",
projectId: "create-project-0001",
type: "canvas_video",
status: "running",
prompt: "fixture",
attempts: 1,
clientContext: { conversationId: "conversation-0001", messageId: "message-0001" },
createdAt: "2026-08-13T00:00:00.000Z",
updatedAt: "2026-08-13T00:00:00.000Z",
};
const service = createGenerationTaskSubscriptionService({
async queryTask() {
queryCalls += 1;
return running;
},
async waitTask() {
waitCalls += 1;
await gate;
return { ...running, status: "succeeded", updatedAt: "2026-08-13T00:01:00.000Z" };
},
});
const beforeRefresh: GenerationTask[] = [];
const afterRefresh: GenerationTask[] = [];
const disconnect = service.subscribe([running.id], (task) => beforeRefresh.push(task));
await Promise.resolve();
disconnect();
const reconnect = service.subscribe([running.id], (task) => afterRefresh.push(task));
release();
await new Promise((resolveTick) => setTimeout(resolveTick, 0));
reconnect();
expect(queryCalls).toBe(1);
expect(waitCalls).toBe(1);
expect(beforeRefresh[0]).toMatchObject({ id: running.id, status: "running" });
expect(afterRefresh.at(-1)).toMatchObject({
id: running.id,
status: "succeeded",
clientContext: { conversationId: "conversation-0001", messageId: "message-0001" },
});
});
test("Create durably correlates failures that happen before the first Runtime response", async () => {
const source = await Bun.file(new URL("../src/pages/create/index.tsx", import.meta.url)).text();
expect(source).toContain("generationErrorCode?: string");
expect(source).toContain("generationOperation?: string");
expect(source).toContain("generationOperation: task.operation");
expect(source).toContain("generationErrorCode: task.errorCode");
expect(source).toContain("generationErrorCode(error)");
expect(source).toContain("createdAt: assistantMessage.createdAt");
});
test("creation results hand off exact asset ids to one new canvas and consume the route once", async () => {
const module = await import("../src/lib/canvas/canvas-asset-handoff").catch(() => ({}));
expect(typeof module.creationCanvasHandoffPath).toBe("function");
expect(typeof module.canvasAssetHandoffIds).toBe("function");
expect(typeof module.consumeCanvasAssetHandoff).toBe("function");
const path = module.creationCanvasHandoffPath(["asset one", "asset/two", "asset one", ""]);
expect(path).toBe("/canvas?mode=handoff&asset=asset+one&asset=asset%2Ftwo");
expect(module.creationCanvasHandoffPath(["asset/two"], 2)).toBeUndefined();
const params = new URLSearchParams("mode=new&asset=asset+one&asset=asset%2Ftwo&asset=asset+one&keep=1");
expect(module.canvasAssetHandoffIds(params)).toEqual(["asset one", "asset/two"]);
expect(module.consumeCanvasAssetHandoff(params).toString()).toBe("keep=1");
});
test("canvas handoff resolves generated image and video assets in requested order", async () => {
const module = await import("../src/lib/canvas/canvas-asset-handoff");
expect(typeof module.canvasAssetHandoffPayloads).toBe("function");
const assets = [
{
id: "image-1",
kind: "image" as const,
title: "image title",
coverUrl: "/image.png",
tags: ["creation"],
status: "confirmed" as const,
source: "creation",
createdAt: "2026-08-11T00:00:00.000Z",
updatedAt: "2026-08-11T00:00:00.000Z",
data: { dataUrl: "/image.png", storageKey: "resource:image", width: 1664, height: 936, bytes: 625000, mimeType: "image/png" },
},
{
id: "video-1",
kind: "video" as const,
title: "video title",
coverUrl: "/video.mp4",
tags: ["creation"],
status: "confirmed" as const,
source: "creation",
createdAt: "2026-08-11T00:00:00.000Z",
updatedAt: "2026-08-11T00:00:00.000Z",
data: { url: "/video.mp4", storageKey: "resource:video", width: 1280, height: 720, durationMs: 6000, bytes: 800000, mimeType: "video/mp4" },
},
];
expect(module.canvasAssetHandoffPayloads(assets, ["video-1", "missing", "image-1"])).toEqual({
payloads: [
{ kind: "video", url: "/video.mp4", storageKey: "resource:video", title: "video title", width: 1280, height: 720, durationMs: 6000, bytes: 800000, mimeType: "video/mp4", assetId: "video-1" },
{ kind: "image", dataUrl: "/image.png", storageKey: "resource:image", title: "image title", assetId: "image-1" },
],
missingAssetIds: ["missing"],
});
});
test("creation result handoff selects only assets owned by that message in result order", async () => {
const module = await import("../src/lib/canvas/canvas-asset-handoff");
const image = (id: string, url: string, messageId: string, resultIndex: number) => ({
id,
kind: "image" as const,
title: id,
coverUrl: url,
tags: ["creation"],
status: "confirmed" as const,
source: "creation",
createdAt: "2026-08-11T00:00:00.000Z",
updatedAt: "2026-08-11T00:00:00.000Z",
metadata: { source: "create-generation", messageId, resultIndex },
data: { dataUrl: url, width: 1664, height: 936, bytes: 1, mimeType: "image/png" },
});
const assets = [image("second", "/second.png", "message-1", 1), image("unrelated", "/unrelated.png", "message-2", 0), image("first", "/first.png", "message-1", 0)];
expect(module.creationResultAssetIds(assets, { messageId: "message-1", taskIds: [], resultUrls: ["/first.png", "/second.png"] })).toEqual(["first", "second"]);
});
test("creation result handoff recognizes unified generation-task materializer metadata", async () => {
const module = await import("../src/lib/canvas/canvas-asset-handoff");
const asset = {
id: "materialized-image",
kind: "image" as const,
title: "image",
coverUrl: "/stored.png",
tags: [],
status: "confirmed" as const,
source: "generation task",
createdAt: "2026-08-14T00:00:00.000Z",
updatedAt: "2026-08-14T00:00:00.000Z",
metadata: { source: "generation-task", taskId: "dreamina:task-1", messageId: "message-1", outputIndex: 0 },
data: { dataUrl: "/stored.png", width: 1024, height: 1024, bytes: 1, mimeType: "image/png" },
};
expect(
module.creationResultAssetIds([asset], {
messageId: "message-1",
taskIds: ["dreamina:task-1"],
resultUrls: ["/stored.png"],
}),
).toEqual(["materialized-image"]);
});
test("canvas handoff consumes its route only after merged nodes are durably persisted", async () => {
const module = await import("../src/lib/canvas/canvas-asset-handoff");
let releasePersistence = () => {};
const persistenceGate = new Promise<void>((resolve) => {
releasePersistence = resolve;
});
let persistedNodes: Array<{ id: string }> = [];
let settled = false;
const pending = module
.finalizeCanvasAssetHandoff({
searchParams: new URLSearchParams("mode=handoff&asset=image-1&keep=1"),
currentNodes: [{ id: "existing" }, { id: "image-1" }],
createdNodes: [{ id: "image-1" }, { id: "image-2" }],
persist: async (nodes: Array<{ id: string }>) => {
persistedNodes = nodes;
await persistenceGate;
},
})
.then((result: { nodes: Array<{ id: string }>; searchParams: URLSearchParams }) => {
settled = true;
return result;
});
await Promise.resolve();
expect(settled).toBe(false);
expect(persistedNodes.map((node) => node.id)).toEqual(["existing", "image-1", "image-2"]);
releasePersistence();
const result = await pending;
expect(result.nodes.map((node) => node.id)).toEqual(["existing", "image-1", "image-2"]);
expect(result.searchParams.toString()).toBe("keep=1");
});
test("canvas handoff keeps its route when durable persistence fails", async () => {
const module = await import("../src/lib/canvas/canvas-asset-handoff");
const searchParams = new URLSearchParams("mode=handoff&asset=image-1&keep=1");
await expect(
module.finalizeCanvasAssetHandoff({
searchParams,
currentNodes: [{ id: "existing" }],
createdNodes: [{ id: "image-1" }],
persist: async () => {
throw new Error("persistence failed");
},
}),
).rejects.toThrow("persistence failed");
expect(searchParams.toString()).toBe("mode=handoff&asset=image-1&keep=1");
});
test("canvas handoff with no available assets remains retryable", async () => {
const module = await import("../src/lib/canvas/canvas-asset-handoff").catch(() => ({}));
expect(typeof module.canvasAssetHandoffAttempt).toBe("function");
expect(module.canvasAssetHandoffAttempt([], new URLSearchParams("mode=handoff&asset=missing&keep=1"))).toEqual({ kind: "retry", assetIds: ["missing"], payloads: [], missingAssetIds: ["missing"] });
});
test("canvas handoff waits until every requested asset is available before creating any node", async () => {
const module = await import("../src/lib/canvas/canvas-asset-handoff");
const image = {
id: "image-1",
kind: "image" as const,
title: "image",
coverUrl: "/image.png",
tags: [],
status: "confirmed" as const,
source: "creation",
createdAt: "2026-08-11T00:00:00.000Z",
updatedAt: "2026-08-11T00:00:00.000Z",
data: { dataUrl: "/image.png", width: 1, height: 1, bytes: 1, mimeType: "image/png" },
};
expect(module.canvasAssetHandoffAttempt([image], new URLSearchParams("mode=handoff&asset=image-1&asset=late-video"))).toEqual({
kind: "retry",
assetIds: ["image-1", "late-video"],
payloads: [],
missingAssetIds: ["late-video"],
});
});
test("canvas handoff reuses nodes carrying the same asset id after a persistence retry", async () => {
const module = await import("../src/lib/canvas/canvas-asset-handoff");
expect(typeof module.uninsertedCanvasAssetHandoffPayloads).toBe("function");
const payloads = [{ kind: "image" as const, dataUrl: "/image.png", title: "image", assetId: "image-1" }];
const existingNodes = [{ id: "random-node-id", metadata: { assetId: "image-1" } }];
expect(module.uninsertedCanvasAssetHandoffPayloads(existingNodes, payloads)).toEqual([]);
});
test("creation result handoff falls back by stable result order only for a complete owned asset set", async () => {
const module = await import("../src/lib/canvas/canvas-asset-handoff");
const image = (id: string, resultIndex: number) => ({
id,
kind: "image" as const,
title: id,
coverUrl: `/stored-${id}.png`,
tags: ["creation"],
status: "confirmed" as const,
source: "creation",
createdAt: "2026-08-11T00:00:00.000Z",
updatedAt: "2026-08-11T00:00:00.000Z",
metadata: { source: "create-generation", messageId: "message-1", resultIndex },
data: { dataUrl: `/stored-${id}.png`, width: 1664, height: 936, bytes: 1, mimeType: "image/png" },
});
const complete = [image("second", 1), image("first", 0)];
expect(module.creationResultAssetIds(complete, { messageId: "message-1", taskIds: [], resultUrls: ["/official-first.png", "/official-second.png"] })).toEqual(["first", "second"]);
expect(module.creationResultAssetIds(complete.slice(0, 1), { messageId: "message-1", taskIds: [], resultUrls: ["/official-first.png", "/official-second.png"] })).toEqual([]);
});
test("Create forwards owned result assets through one new canvas and the project persists before clearing the handoff", () => {
const create = readFileSync(resolve(import.meta.dir, "../src/pages/create/index.tsx"), "utf8");
const canvasIndex = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/index.tsx"), "utf8");
const canvasProject = readFileSync(resolve(import.meta.dir, "../src/pages/canvas/project.tsx"), "utf8");
expect(create).toContain('import { creationCanvasHandoffPath, creationResultAssetIds } from "@/lib/canvas/canvas-asset-handoff"');
expect(create).toContain("const resultAssetIds = result && resultUrls.length ? creationResultAssetIds(assets, { messageId: result.id, taskIds: result.taskIds || [], resultUrls }) : [];");
expect(create).toContain('const canvasHandoffPath = result ? creationCanvasHandoffPath(resultAssetIds, resultUrls.length) : "";');
expect(create).toContain('const canvasPath = canvasHandoffPath || "/canvas";');
expect(create).toContain('<Link to={canvasPath}>{canvasHandoffPath ? "添加到画布" : "打开画布"}</Link>');
expect(canvasIndex).toContain('const handoffMode = mode === "handoff"');
expect(canvasIndex).toContain('mode !== "new" && mode !== "recent" && mode !== "handoff"');
expect(canvasProject).toContain('import { canvasAssetHandoffAttempt, finalizeCanvasAssetHandoff, uninsertedCanvasAssetHandoffPayloads } from "@/lib/canvas/canvas-asset-handoff"');
expect(canvasProject).toContain('if (!projectLoaded || !assetsHydrated || searchParams.get("mode") !== "handoff") return');
expect(canvasProject).toContain("const pendingPayloads = uninsertedCanvasAssetHandoffPayloads(nodesRef.current, payloads)");
expect(canvasProject).toContain("await flushCanvasStorePersistence()");
expect(canvasProject.indexOf("await flushCanvasStorePersistence()")).toBeLessThan(canvasProject.indexOf("setSearchParams(finalized.searchParams"));
});
test("Create image batch retry preserves per-index lineage under one attempt group", async () => {
const module = await import("../src/lib/canvas/canvas-project-generation");
const createBatchRetryContexts = (
module as {
createGenerationBatchRetryContexts?: (taskIds: readonly string[], attemptGroupId: string) => Promise<Array<{ retryOf: string; attemptGroupId: string; clientOperationId: string }>>;
}
).createGenerationBatchRetryContexts;
expect(typeof createBatchRetryContexts).toBe("function");
if (!createBatchRetryContexts) return;
const oldTaskIds = ["dreamina:create-image-old-0001", "dreamina:create-image-old-0002", "dreamina:create-image-old-0003"];
const contexts = await createBatchRetryContexts(oldTaskIds, "create-image-attempt-group-0001");
expect(contexts.map(({ retryOf, attemptGroupId }) => ({ retryOf, attemptGroupId }))).toEqual([
{ retryOf: oldTaskIds[0], attemptGroupId: "create-image-attempt-group-0001" },
{ retryOf: oldTaskIds[1], attemptGroupId: "create-image-attempt-group-0001" },
{ retryOf: oldTaskIds[2], attemptGroupId: "create-image-attempt-group-0001" },
]);
expect(new Set(contexts.map((context) => context.clientOperationId)).size).toBe(3);
});

View File

@@ -2,30 +2,36 @@ import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
function compactSource(source: string) {
return source.replace(/\s+/g, " ").trim();
}
describe("creation library button", () => {
test("places a library control beside the generation mode picker", () => {
const source = readFileSync(resolve(import.meta.dir, "../src/pages/create/index.tsx"), "utf8");
const modePickerIndex = source.indexOf("<ModePicker mode={props.mode} onModeChange={props.onModeChange} />");
const libraryButtonIndex = source.indexOf('className="creation-chat-control" onClick={props.onOpenLibrary} disabled={props.busy || !referencesSupported} aria-label="打开素材库选择参考内容"');
const dockStart = source.indexOf('<footer className="creation-chat-dock">');
const dockEnd = source.indexOf("</footer>", dockStart);
expect(modePickerIndex).toBeGreaterThanOrEqual(0);
expect(libraryButtonIndex).toBeGreaterThan(modePickerIndex);
expect(source.slice(libraryButtonIndex, libraryButtonIndex + 180)).toContain("<FolderOpen />");
expect(source.slice(libraryButtonIndex, libraryButtonIndex + 180)).toContain("<span>素材库</span>");
expect(dockStart).toBeGreaterThanOrEqual(0);
expect(dockEnd).toBeGreaterThan(dockStart);
expect(compactSource(source.slice(dockStart, dockEnd))).toMatch(
/<ModePicker mode=\{props\.mode\} onModeChange=\{props\.onModeChange\} \/> <Tooltip\b[^>]*>\s*<button(?=[^>]*className="creation-chat-control")(?=[^>]*onClick=\{props\.onOpenLibrary\})(?=[^>]*disabled=\{props\.busy \|\| !referencesSupported\})(?=[^>]*aria-label="打开素材库选择参考内容")[^>]*>\s*<FolderOpen \/>\s*<span>素材库<\/span>\s*<\/button>\s*<\/Tooltip>/,
);
});
test("uploads from the library without adding a reference before confirmation", () => {
const source = readFileSync(resolve(import.meta.dir, "../src/pages/create/index.tsx"), "utf8");
const pickerSource = readFileSync(resolve(import.meta.dir, "../src/components/assets/asset-library-picker-modal.tsx"), "utf8");
const uploadStart = source.indexOf("const uploadLibraryAssets = async");
const uploadEnd = source.indexOf("const handleFileChange", uploadStart);
expect(uploadStart).toBeGreaterThanOrEqual(0);
expect(uploadEnd).toBeGreaterThan(uploadStart);
expect(source.slice(uploadStart, uploadEnd)).not.toContain("setAttachments");
expect(source).toContain("onUpload={uploadLibraryAssets}");
expect(source).toContain("onUpload: uploadLibraryAssets");
expect(source).not.toContain("onUpload={() => fileInputRef.current?.click()}");
expect(source).toContain("上传后保存到素材库");
expect(source).toContain("正在保存到素材库,完成后会自动选中");
expect(pickerSource).toContain("保存完成后会自动选中");
expect(source).toContain("个素材已上传到素材库并自动选中");
});

View File

@@ -0,0 +1,415 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { flushGenerationAssetStorageLocks, insertOrReturnGenerationAsset, withGenerationAssetStorageLock, type GenerationAssetRecord } from "../src/services/generation-asset-repository";
import { generationArtifactStorageKey, loadOrStoreGenerationArtifact } from "../src/services/generation-artifact-sink";
test("generation asset insert waits for persistence and reload returns the same durable row", async () => {
let assets: GenerationAssetRecord[] = [];
let durableAssets: GenerationAssetRecord[] = [];
let releasePersistence!: () => void;
const persistenceGate = new Promise<void>((resolve) => {
releasePersistence = resolve;
});
let settled = false;
const inserting = insertOrReturnGenerationAsset({
storageScope: "test-scope",
effectKey: "materialize:task-persistence:0",
assetId: "generation-stable-id",
createAsset: () => ({
id: "generation-stable-id",
metadata: { generationEffectKey: "materialize:task-persistence:0" },
}),
updateAssets: (updater) => {
assets = updater(assets);
},
readAssets: () => assets,
readPersistedAssets: async () => structuredClone(durableAssets),
persistAssets: async (nextAssets) => {
await persistenceGate;
durableAssets = structuredClone(nextAssets);
},
}).then((assetId) => {
settled = true;
return assetId;
});
await new Promise<void>((resolve) => setImmediate(resolve));
assert.equal(settled, false, "Agent effect must not complete while persistence is pending");
releasePersistence();
const assetId = await inserting;
assets = structuredClone(durableAssets);
assert.equal(assets.length, 1);
assert.equal(assets[0]?.id, assetId);
assert.equal(assets[0]?.metadata?.generationEffectKey, "materialize:task-persistence:0");
});
test("generation asset insertion does not resurrect a row deleted from durable storage", async () => {
const deleted = {
id: "asset-deleted",
metadata: { title: "stale memory row" },
} satisfies GenerationAssetRecord;
let assets: GenerationAssetRecord[] = [deleted];
let durableAssets: GenerationAssetRecord[] = [];
let persistenceWrites = 0;
await insertOrReturnGenerationAsset({
storageScope: "test-deletion-scope",
effectKey: "materialize:after-delete:0",
assetId: "asset-generated",
createAsset: () => ({
id: "asset-generated",
metadata: { generationEffectKey: "materialize:after-delete:0" },
}),
updateAssets: (updater) => {
assets = updater(assets);
},
readAssets: () => assets,
readPersistedAssets: async () => structuredClone(durableAssets),
persistAssets: async (nextAssets) => {
persistenceWrites += 1;
durableAssets = structuredClone(nextAssets);
},
});
assert.deepEqual(
durableAssets.map((asset) => asset.id),
["asset-generated"],
);
assert.deepEqual(
assets.map((asset) => asset.id),
["asset-generated"],
);
assert.equal(persistenceWrites, 1);
});
test("generation asset replay preserves the hydrated in-memory preview for the same durable row", async () => {
const effectKey = "materialize:hydrated-preview:0";
const persisted = {
id: "asset-hydrated-preview",
title: "durable title",
coverUrl: "https://persisted.invalid/preview.png",
data: { dataUrl: "https://persisted.invalid/preview.png", storageKey: "generation-image:test-scope:hydrated-preview" },
metadata: { generationEffectKey: effectKey, durableMarker: "durable" },
} satisfies GenerationAssetRecord & { title: string; coverUrl: string; data: { dataUrl: string; storageKey: string } };
const hydrated = {
...persisted,
title: "stale hydrated title",
coverUrl: "blob:hydrated-cover",
data: { ...persisted.data, dataUrl: "blob:hydrated-preview" },
metadata: { generationEffectKey: effectKey, durableMarker: "stale" },
};
let assets: (typeof hydrated)[] = [hydrated];
const assetId = await insertOrReturnGenerationAsset({
storageScope: "test-scope",
effectKey,
assetId: persisted.id,
createAsset: () => {
throw new Error("existing generation asset must be reused");
},
updateAssets: (updater) => {
assets = updater(assets) as (typeof hydrated)[];
},
readAssets: () => assets,
readPersistedAssets: async () => [persisted],
persistAssets: async () => {
throw new Error("existing generation asset must not be rewritten");
},
});
assert.equal(assetId, persisted.id);
assert.equal(assets[0]?.coverUrl, "blob:hydrated-cover");
assert.equal(assets[0]?.data.dataUrl, "blob:hydrated-preview");
assert.equal(assets[0]?.data.storageKey, persisted.data.storageKey);
assert.equal(assets[0]?.title, "durable title");
assert.equal(assets[0]?.metadata?.durableMarker, "durable");
});
test("generation asset replay does not carry a blob preview across a changed storage key", async () => {
const effectKey = "materialize:hydrated-preview-storage-change:0";
const persisted = {
id: "asset-hydrated-preview-storage-change",
coverUrl: "https://persisted.invalid/new-preview.png",
data: { dataUrl: "https://persisted.invalid/new-preview.png", storageKey: "generation-image:test-scope:new-key" },
metadata: { generationEffectKey: effectKey },
} satisfies GenerationAssetRecord & { coverUrl: string; data: { dataUrl: string; storageKey: string } };
const hydrated = {
...persisted,
coverUrl: "blob:stale-cover",
data: { dataUrl: "blob:stale-preview", storageKey: "generation-image:test-scope:old-key" },
};
let assets: Array<typeof persisted | typeof hydrated> = [hydrated];
await insertOrReturnGenerationAsset({
storageScope: "test-scope",
effectKey,
assetId: persisted.id,
createAsset: () => {
throw new Error("existing generation asset must be reused");
},
updateAssets: (updater) => {
assets = updater(assets as (typeof persisted)[]) as Array<typeof persisted | typeof hydrated>;
},
readAssets: () => assets as (typeof persisted)[],
readPersistedAssets: async () => [persisted],
persistAssets: async () => {
throw new Error("existing generation asset must not be rewritten");
},
});
assert.equal(assets[0]?.coverUrl, persisted.coverUrl);
assert.equal(assets[0]?.data.dataUrl, persisted.data.dataUrl);
assert.equal(assets[0]?.data.storageKey, persisted.data.storageKey);
});
test("generation asset replay rejects a deterministic row deleted after its base revision", async () => {
let assets: GenerationAssetRecord[] = [];
await assert.rejects(
insertOrReturnGenerationAsset({
storageScope: "test-tombstone-scope",
effectKey: "materialize:tombstoned:0",
assetId: "asset-tombstoned",
createAsset: () => ({
id: "asset-tombstoned",
metadata: { generationEffectKey: "materialize:tombstoned:0" },
}),
updateAssets: (updater) => {
assets = updater(assets);
},
readAssets: () => assets,
readPersistedAssets: async () => [],
isAssetDeleted: () => true,
persistAssets: async () => {
throw new Error("tombstoned generation asset must not be persisted");
},
} as Parameters<typeof insertOrReturnGenerationAsset>[0]),
/生成素材已被用户删除/,
);
assert.deepEqual(assets, []);
});
test("concurrent generation asset inserts merge inside a per-scope storage lock", async () => {
const durableByScope = new Map<string, GenerationAssetRecord[]>();
let releaseFirstWrite!: () => void;
const firstWriteGate = new Promise<void>((resolve) => {
releaseFirstWrite = resolve;
});
let firstWriteStartedResolve!: () => void;
const firstWriteStarted = new Promise<void>((resolve) => {
firstWriteStartedResolve = resolve;
});
const insert = (scope: string, effectKey: string, assetId: string, blockWrite = false) => {
let assets: GenerationAssetRecord[] = [];
return insertOrReturnGenerationAsset({
storageScope: scope,
effectKey,
assetId,
createAsset: () => ({
id: assetId,
metadata: { generationEffectKey: effectKey },
}),
updateAssets: (updater) => {
assets = updater(assets);
},
readAssets: () => assets,
readPersistedAssets: async () => structuredClone(durableByScope.get(scope) ?? []),
persistAssets: async (nextAssets) => {
if (blockWrite) {
firstWriteStartedResolve();
await firstWriteGate;
}
durableByScope.set(scope, structuredClone(nextAssets));
},
});
};
const first = insert("account-A", "materialize:concurrent-a:0", "asset-a", true);
await firstWriteStarted;
let secondSettled = false;
const second = insert("account-A", "materialize:concurrent-b:0", "asset-b").then((assetId) => {
secondSettled = true;
return assetId;
});
let otherScopeSettled = false;
const otherScope = insert("account-B", "materialize:isolated:0", "asset-other").then(() => {
otherScopeSettled = true;
});
await otherScope;
assert.equal(otherScopeSettled, true, "another user scope must not wait on account-A's lock");
assert.equal(secondSettled, false, "same-scope insert must wait for the first write");
releaseFirstWrite();
await Promise.all([first, second, otherScope]);
assert.deepEqual(
durableByScope
.get("account-A")
?.map((asset) => asset.id)
.sort(),
["asset-a", "asset-b"],
);
assert.deepEqual(
durableByScope.get("account-B")?.map((asset) => asset.id),
["asset-other"],
);
});
test("browser generation asset writes use the storage-level Web Locks boundary", async () => {
const originalWindow = (globalThis as { window?: unknown }).window;
const originalNavigator = (globalThis as { navigator?: unknown }).navigator;
const lockNames: string[] = [];
Object.defineProperty(globalThis, "window", { configurable: true, value: {} });
Object.defineProperty(globalThis, "navigator", {
configurable: true,
value: {
locks: {
request: async <T>(name: string, callback: () => Promise<T>) => {
lockNames.push(name);
return callback();
},
},
},
});
try {
await withGenerationAssetStorageLock("account-A", async () => undefined);
assert.deepEqual(lockNames, ["infinite-canvas:generation-asset-storage-lock:account-A"]);
} finally {
if (originalWindow === undefined) delete (globalThis as { window?: unknown }).window;
else Object.defineProperty(globalThis, "window", { configurable: true, value: originalWindow });
if (originalNavigator === undefined) delete (globalThis as { navigator?: unknown }).navigator;
else Object.defineProperty(globalThis, "navigator", { configurable: true, value: originalNavigator });
}
});
test("competing browser generation asset writes fail closed before entering storage without Web Locks", async () => {
const originalWindow = (globalThis as { window?: unknown }).window;
const originalDocument = (globalThis as { document?: unknown }).document;
const originalNavigator = (globalThis as { navigator?: unknown }).navigator;
Object.defineProperty(globalThis, "window", { configurable: true, value: {} });
Object.defineProperty(globalThis, "document", { configurable: true, value: {} });
Object.defineProperty(globalThis, "navigator", { configurable: true, value: {} });
let entered = 0;
try {
const compete = () =>
withGenerationAssetStorageLock(
"account-no-web-locks",
async () => {
entered += 1;
},
{ requireCrossRealmLock: true },
);
await Promise.all([assert.rejects(compete(), /跨标签存储锁/), assert.rejects(compete(), /跨标签存储锁/)]);
assert.equal(entered, 0, "neither competing generation write may enter the critical section without a cross-tab lock");
} finally {
if (originalWindow === undefined) delete (globalThis as { window?: unknown }).window;
else Object.defineProperty(globalThis, "window", { configurable: true, value: originalWindow });
if (originalDocument === undefined) delete (globalThis as { document?: unknown }).document;
else Object.defineProperty(globalThis, "document", { configurable: true, value: originalDocument });
if (originalNavigator === undefined) delete (globalThis as { navigator?: unknown }).navigator;
else Object.defineProperty(globalThis, "navigator", { configurable: true, value: originalNavigator });
}
});
test("an aborted generation asset fence prevents the durable catalog write", async () => {
const controller = new AbortController();
controller.abort();
let persistenceWrites = 0;
await assert.rejects(
insertOrReturnGenerationAsset({
storageScope: "aborted-generation-asset",
effectKey: "materialize:aborted-generation-asset:0",
assetId: "asset-aborted-generation-asset",
signal: controller.signal,
createAsset: () => ({ id: "asset-aborted-generation-asset", metadata: { generationEffectKey: "materialize:aborted-generation-asset:0" } }),
updateAssets: () => undefined,
readAssets: () => [],
readPersistedAssets: async () => [],
persistAssets: async () => {
persistenceWrites += 1;
},
}),
(error: unknown) => error instanceof DOMException && error.name === "AbortError",
);
assert.equal(persistenceWrites, 0);
});
test("generation asset lock flush drains tails added while the flush is running", async () => {
let releaseFirst!: () => void;
const firstGate = new Promise<void>((resolve) => {
releaseFirst = resolve;
});
let firstStartedResolve!: () => void;
const firstStarted = new Promise<void>((resolve) => {
firstStartedResolve = resolve;
});
const first = withGenerationAssetStorageLock("flush-scope", async () => {
firstStartedResolve();
await firstGate;
});
await firstStarted;
let flushSettled = false;
const flushing = flushGenerationAssetStorageLocks().then(() => {
flushSettled = true;
});
let releaseSecond!: () => void;
const secondGate = new Promise<void>((resolve) => {
releaseSecond = resolve;
});
let secondStartedResolve!: () => void;
const secondStarted = new Promise<void>((resolve) => {
secondStartedResolve = resolve;
});
const second = withGenerationAssetStorageLock("flush-scope", async () => {
secondStartedResolve();
await secondGate;
});
releaseFirst();
await secondStarted;
try {
await new Promise<void>((resolve) => setImmediate(resolve));
assert.equal(flushSettled, false, "flush must wait for a tail queued during the active flush");
} finally {
releaseSecond();
await Promise.all([first, second, flushing]);
}
assert.equal(flushSettled, true);
});
test("generation artifact sink recovers after ack crash without a second materialization", async () => {
const durable = new Map<string, { storageKey: string }>();
const effectKey = "materialize:task-ack-crash:0";
const storageKey = generationArtifactStorageKey(effectKey, "image", "test-scope");
let materializations = 0;
const run = () =>
loadOrStoreGenerationArtifact({
effectKey: storageKey,
read: async (key) => durable.get(key) ?? null,
materialize: async () => {
materializations += 1;
return { storageKey };
},
write: async (key, artifact) => {
durable.set(key, artifact);
},
});
await run();
// Simulate a process crash after the durable sink succeeded but before the Agent effect ack.
for (let replay = 0; replay < 3; replay += 1) await run();
assert.equal(materializations, 1);
assert.equal(durable.size, 1);
assert.equal(durable.get(storageKey)?.storageKey, storageKey);
});

View File

@@ -0,0 +1,52 @@
import { expect, test } from "bun:test";
type Scenario = "image-cleanup" | "scope-cleanup-switch" | "scope-cleanup-late-canvas-reference" | "video-commit-race" | "audio-commit-race";
type ScenarioResponse<T> = { ok: true; result: T } | { ok: false; error: string };
function runScenario<T>(scenario: Scenario): Promise<T> {
return new Promise<T>((resolve, reject) => {
const worker = new Worker(new URL("./helpers/generation-storage-consistency.worker.ts", import.meta.url).href, { type: "module" });
const finish = () => {
worker.terminate();
};
worker.onmessage = (event: MessageEvent<ScenarioResponse<T>>) => {
finish();
if (event.data.ok) resolve(event.data.result);
else reject(new Error(event.data.error));
};
worker.onerror = (event) => {
finish();
reject(event.error ?? new Error(event.message));
};
worker.postMessage(scenario);
});
}
test("generation image cleanup removes unused generation-image blobs and preserves referenced ones", async () => {
const result = await runScenario<{ usedPresent: boolean; unusedPresent: boolean }>("image-cleanup");
expect(result.usedPresent).toBe(true);
expect(result.unusedPresent).toBe(false);
});
test("delayed cleanup keeps account-A Canvas-only image references after switching the active account", async () => {
const result = await runScenario<{ referencedPresent: boolean; unusedPresent: boolean; otherScopePresent: boolean }>("scope-cleanup-switch");
expect(result.referencedPresent).toBe(true);
expect(result.unusedPresent).toBe(false);
expect(result.otherScopePresent).toBe(true);
});
test("delayed cleanup preserves a same-scope Canvas-only reference added after cleanup was queued", async () => {
const result = await runScenario<{ referencedPresent: boolean; unusedPresent: boolean }>("scope-cleanup-late-canvas-reference");
expect(result.referencedPresent).toBe(true);
expect(result.unusedPresent).toBe(false);
});
test("generation video and audio materialization cannot race cleanup into a catalog row whose blob is missing", async () => {
for (const mediaType of ["video", "audio"] as const) {
const result = await runScenario<{ kind?: string; storageKey?: string; blobPresent: boolean; generationAssetCount: number }>(`${mediaType}-commit-race`);
expect(result.kind).toBe(mediaType);
expect(result.storageKey).toMatch(new RegExp(`^generation-${mediaType}:generation-media-commit-race-${mediaType}:`));
expect(result.blobPresent).toBe(true);
expect(result.generationAssetCount).toBe(1);
}
});

View File

@@ -0,0 +1,110 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import type { GenerationTask } from "../src/services/api/task-center";
import { applyGenerationConsumerEffect } from "../src/services/generation-consumer-dedupe";
import { createGenerationTaskMaterializer, type GenerationTaskEffectResult, type GenerationTaskEffectStore } from "../src/services/generation-task-materializer";
test("node message and agent consumers receive stable effect keys across three replays", async () => {
const completed = new Map<string, GenerationTaskEffectResult>();
const claimed = new Set<string>();
const effects: GenerationTaskEffectStore = {
async claim(effectKey) {
const result = completed.get(effectKey);
if (result) return { status: "completed", result };
if (claimed.has(effectKey)) return { status: "busy" };
claimed.add(effectKey);
return { status: "claimed", fence: 1 };
},
async renew() {
return { fence: 1 };
},
async complete(effectKey, _taskId, result) {
claimed.delete(effectKey);
completed.set(effectKey, result);
},
async release(effectKey) {
claimed.delete(effectKey);
},
};
const materializer = createGenerationTaskMaterializer({
effects,
async materializeOutput() {
throw new Error("already materialized");
},
});
const task: GenerationTask = {
id: "task-consumer-effect-keys",
type: "image",
status: "succeeded",
prompt: "redacted",
attempts: 1,
resultState: "READY",
outputs: [{ outputIndex: 0, mediaType: "image", materializedAssetId: "asset-stable-id" }],
createdAt: "2026-08-13T00:00:00.000Z",
updatedAt: "2026-08-13T00:00:00.000Z",
};
const seen: string[] = [];
for (let replay = 0; replay < 3; replay += 1) {
await materializer.attachNode(task, "node-safe-id", 0, async ({ effectKey }) => {
seen.push(effectKey);
});
await materializer.attachMessage(task, "message-safe-id", 0, async ({ effectKey }) => {
seen.push(effectKey);
});
await materializer.resumeAgent(task, "continuation-safe-id", async ({ effectKey }) => {
seen.push(effectKey);
});
}
assert.deepEqual(seen, ["attach-node:task-consumer-effect-keys:node-safe-id:0", "attach-message:task-consumer-effect-keys:message-safe-id:0", "agent-resume:task-consumer-effect-keys:continuation-safe-id"]);
});
test("production-shaped node message and agent records replay three times with one side effect", () => {
const effectKeys = {
node: "attach-node:task-consumer:node-safe-id:0",
message: "attach-message:task-consumer:message-safe-id:0",
agent: "agent-resume:task-consumer:continuation-safe-id",
};
const calls = { node: 0, message: 0, agent: 0 };
let node = { generationEffectKeys: [] as string[], content: "" };
let message = { generationEffectKeys: [] as string[], resultUrls: [] as string[] };
let agent = { generationEffectKeys: [] as string[], resumed: false };
for (let replay = 0; replay < 3; replay += 1) {
node = applyGenerationConsumerEffect(node, effectKeys.node, (current) => {
calls.node += 1;
return { ...current, content: "opaque://asset" };
}).value;
message = applyGenerationConsumerEffect(message, effectKeys.message, (current) => {
calls.message += 1;
return { ...current, resultUrls: ["opaque://asset"] };
}).value;
agent = applyGenerationConsumerEffect(agent, effectKeys.agent, (current) => {
calls.agent += 1;
return { ...current, resumed: true };
}).value;
}
node = structuredClone(node);
message = structuredClone(message);
agent = structuredClone(agent);
node = applyGenerationConsumerEffect(node, effectKeys.node, () => {
calls.node += 1;
return node;
}).value;
message = applyGenerationConsumerEffect(message, effectKeys.message, () => {
calls.message += 1;
return message;
}).value;
agent = applyGenerationConsumerEffect(agent, effectKeys.agent, () => {
calls.agent += 1;
return agent;
}).value;
assert.deepEqual(calls, { node: 1, message: 1, agent: 1 });
assert.equal(node.generationEffectKeys.length, 1);
assert.equal(message.generationEffectKeys.length, 1);
assert.equal(agent.generationEffectKeys.length, 1);
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,409 @@
import localforage from "localforage";
import { getActiveUserScope, setActiveUserScope } from "../../src/lib/user-scope";
type InstanceHook = (storeName: string, key: string, value: unknown) => Promise<void> | void;
type Scenario = "image-cleanup" | "scope-cleanup-switch" | "scope-cleanup-late-canvas-reference" | "video-commit-race" | "audio-commit-race";
function installStorageHarness() {
const originalCreateInstance = localforage.createInstance.bind(localforage);
const originalGetItem = localforage.getItem.bind(localforage);
const originalSetItem = localforage.setItem.bind(localforage);
const originalRemoveItem = localforage.removeItem.bind(localforage);
const originalWindow = (globalThis as { window?: unknown }).window;
const originalNavigator = (globalThis as { navigator?: unknown }).navigator;
const originalDocument = (globalThis as { document?: unknown }).document;
const realSetTimeout = globalThis.setTimeout.bind(globalThis);
const defaultValues = new Map<string, unknown>();
const instanceValues = new Map<string, Map<string, unknown>>();
const localStorageValues = new Map<string, string>();
const scheduled: Promise<void>[] = [];
const hooks: { onSet?: InstanceHook } = {};
const lockTails = new Map<string, Promise<void>>();
const storeValues = (storeName: string) => {
let values = instanceValues.get(storeName);
if (!values) {
values = new Map<string, unknown>();
instanceValues.set(storeName, values);
}
return values;
};
localforage.getItem = (async (key: string) => defaultValues.get(key) ?? null) as typeof localforage.getItem;
localforage.setItem = (async (key: string, value: unknown) => {
defaultValues.set(key, value);
return value;
}) as typeof localforage.setItem;
localforage.removeItem = (async (key: string) => {
defaultValues.delete(key);
}) as typeof localforage.removeItem;
localforage.createInstance = ((options: { storeName?: string }) => {
const storeName = options.storeName || "default";
const values = storeValues(storeName);
return {
getItem: async (key: string) => values.get(key) ?? null,
setItem: async (key: string, value: unknown) => {
values.set(key, value);
await hooks.onSet?.(storeName, key, value);
return value;
},
removeItem: async (key: string) => {
values.delete(key);
},
iterate: async (iterator: (value: unknown, key: string, iterationNumber: number) => unknown) => {
let index = 1;
for (const [key, value] of values) {
const result = iterator(value, key, index);
index += 1;
if (result !== undefined) return result;
}
return undefined;
},
clear: async () => values.clear(),
keys: async () => [...values.keys()],
length: async () => values.size,
} as never;
}) as typeof localforage.createInstance;
Object.defineProperty(globalThis, "window", {
configurable: true,
writable: true,
value: {
localStorage: {
getItem: (key: string) => localStorageValues.get(key) ?? null,
setItem: (key: string, value: string) => localStorageValues.set(key, value),
removeItem: (key: string) => localStorageValues.delete(key),
},
setTimeout: (handler: () => unknown, delay = 0) => {
const scheduledRun = new Promise<void>((resolve, reject) => {
realSetTimeout(() => {
Promise.resolve(handler()).then(() => resolve(), reject);
}, delay);
});
scheduled.push(scheduledRun);
return scheduled.length;
},
clearTimeout: () => undefined,
addEventListener: () => undefined,
removeEventListener: () => undefined,
},
});
Object.defineProperty(globalThis, "navigator", {
configurable: true,
writable: true,
value: {
locks: {
request: async <T>(name: string, callback: () => Promise<T>) => {
const previous = lockTails.get(name) ?? Promise.resolve();
let release!: () => void;
const tail = new Promise<void>((resolve) => {
release = resolve;
});
const queued = previous.catch(() => undefined).then(() => tail);
lockTails.set(name, queued);
await previous.catch(() => undefined);
try {
return await callback();
} finally {
release();
if (lockTails.get(name) === queued) lockTails.delete(name);
}
},
},
},
});
delete (globalThis as { document?: unknown }).document;
return {
hooks,
scheduled,
realSetTimeout,
restore() {
localforage.createInstance = originalCreateInstance as typeof localforage.createInstance;
localforage.getItem = originalGetItem;
localforage.setItem = originalSetItem;
localforage.removeItem = originalRemoveItem;
if (originalWindow === undefined) delete (globalThis as { window?: unknown }).window;
else Object.defineProperty(globalThis, "window", { configurable: true, writable: true, value: originalWindow });
if (originalNavigator === undefined) delete (globalThis as { navigator?: unknown }).navigator;
else Object.defineProperty(globalThis, "navigator", { configurable: true, writable: true, value: originalNavigator });
if (originalDocument === undefined) delete (globalThis as { document?: unknown }).document;
else Object.defineProperty(globalThis, "document", { configurable: true, writable: true, value: originalDocument });
},
};
}
async function runImageCleanup() {
const previousScope = getActiveUserScope();
const harness = installStorageHarness();
const scope = "generation-image-cleanup";
const usedKey = `generation-image:${scope}:used`;
const unusedKey = `generation-image:${scope}:unused`;
const imageStorage = await import("../../src/services/image-storage.ts?generation-image-cleanup-contract-worker");
try {
setActiveUserScope(scope);
await imageStorage.setImageBlob(usedKey, new Blob(["used"], { type: "image/png" }));
await imageStorage.setImageBlob(unusedKey, new Blob(["unused"], { type: "image/png" }));
await imageStorage.cleanupUnusedImages({ assets: [{ data: { storageKey: usedKey } }] });
return {
usedPresent: (await imageStorage.getImageBlob(usedKey)) instanceof Blob,
unusedPresent: (await imageStorage.getImageBlob(unusedKey)) instanceof Blob,
};
} finally {
setActiveUserScope(previousScope);
harness.restore();
}
}
async function runScopeCleanupAfterSwitch() {
const previousScope = getActiveUserScope();
const harness = installStorageHarness();
const scopeA = "cleanup-account-a";
const scopeB = "cleanup-account-b";
const referencedKey = `image:${scopeA}:canvas-only`;
const unusedKey = `image:${scopeA}:unused`;
const otherScopeKey = `image:${scopeB}:unrelated`;
try {
setActiveUserScope(scopeA);
const imageStorage = await import("../../src/services/image-storage.ts?scope-cleanup-switch-worker");
const { useAssetStore } = await import("../../src/stores/use-asset-store");
const { useCanvasStore } = await import("../../src/stores/canvas/use-canvas-store");
await imageStorage.setImageBlob(referencedKey, new Blob(["account-a-referenced"], { type: "image/png" }));
await imageStorage.setImageBlob(unusedKey, new Blob(["account-a-unused"], { type: "image/png" }));
await imageStorage.setImageBlob(otherScopeKey, new Blob(["account-b-unrelated"], { type: "image/png" }));
useAssetStore.setState({ assets: [] });
useCanvasStore.setState({
projects: [
{
id: "canvas-account-a",
title: "account A",
createdAt: "2026-08-14T00:00:00.000Z",
updatedAt: "2026-08-14T00:00:00.000Z",
nodes: [
{
id: "node-account-a-image",
type: "image",
title: "account A image",
position: { x: 0, y: 0 },
width: 1,
height: 1,
metadata: { storageKey: referencedKey },
},
],
connections: [],
chatSessions: [],
activeChatId: null,
backgroundMode: "dots",
showImageInfo: false,
viewport: { x: 0, y: 0, k: 1 },
directorScenes: [],
},
],
} as never);
useAssetStore.getState().cleanupImages({ assets: [] });
const cleanupRun = harness.scheduled.at(-1);
if (!cleanupRun) throw new Error("cleanup was not scheduled");
setActiveUserScope(scopeB);
useAssetStore.setState({ assets: [] });
useCanvasStore.setState({
projects: [
{
id: "canvas-account-b",
title: "account B",
createdAt: "2026-08-14T00:00:00.000Z",
updatedAt: "2026-08-14T00:00:00.000Z",
nodes: [],
connections: [],
chatSessions: [],
activeChatId: null,
backgroundMode: "dots",
showImageInfo: false,
viewport: { x: 0, y: 0, k: 1 },
directorScenes: [],
},
],
} as never);
await cleanupRun;
return {
referencedPresent: (await imageStorage.getImageBlob(referencedKey)) instanceof Blob,
unusedPresent: (await imageStorage.getImageBlob(unusedKey)) instanceof Blob,
otherScopePresent: (await imageStorage.getImageBlob(otherScopeKey)) instanceof Blob,
};
} finally {
setActiveUserScope(previousScope);
harness.restore();
}
}
async function runScopeCleanupAfterLateCanvasReference() {
const previousScope = getActiveUserScope();
const harness = installStorageHarness();
const scope = "cleanup-late-canvas-reference";
const referencedKey = `image:${scope}:late-canvas-only`;
const unusedKey = `image:${scope}:unused`;
try {
setActiveUserScope(scope);
const imageStorage = await import("../../src/services/image-storage.ts?scope-cleanup-late-canvas-reference-worker");
const { useAssetStore } = await import("../../src/stores/use-asset-store");
const { useCanvasStore } = await import("../../src/stores/canvas/use-canvas-store");
await imageStorage.setImageBlob(referencedKey, new Blob(["late-canvas-reference"], { type: "image/png" }));
await imageStorage.setImageBlob(unusedKey, new Blob(["still-unused"], { type: "image/png" }));
useAssetStore.setState({ assets: [] });
useCanvasStore.setState({ projects: [] });
useAssetStore.getState().cleanupImages({ assets: [] });
const cleanupRun = harness.scheduled.at(-1);
if (!cleanupRun) throw new Error("cleanup was not scheduled");
useCanvasStore.setState({
projects: [
{
id: "canvas-late-reference",
title: "late reference",
createdAt: "2026-08-14T00:00:00.000Z",
updatedAt: "2026-08-14T00:00:00.000Z",
nodes: [
{
id: "node-late-reference",
type: "image",
title: "late referenced image",
position: { x: 0, y: 0 },
width: 1,
height: 1,
metadata: { storageKey: referencedKey },
},
],
connections: [],
chatSessions: [],
activeChatId: null,
backgroundMode: "dots",
showImageInfo: false,
viewport: { x: 0, y: 0, k: 1 },
directorScenes: [],
},
],
} as never);
await cleanupRun;
return {
referencedPresent: (await imageStorage.getImageBlob(referencedKey)) instanceof Blob,
unusedPresent: (await imageStorage.getImageBlob(unusedKey)) instanceof Blob,
};
} finally {
setActiveUserScope(previousScope);
harness.restore();
}
}
async function runMediaCommitRace(mediaType: "video" | "audio") {
const previousScope = getActiveUserScope();
const harness = installStorageHarness();
const scope = `generation-media-commit-race-${mediaType}`;
let releaseMediaWrite!: () => void;
const mediaWriteGate = new Promise<void>((resolve) => {
releaseMediaWrite = resolve;
});
let mediaWriteStartedResolve!: () => void;
const mediaWriteStarted = new Promise<void>((resolve) => {
mediaWriteStartedResolve = resolve;
});
let blocked = false;
harness.hooks.onSet = async (storeName, key) => {
if (!blocked && storeName === "media_files" && key.startsWith(`generation-${mediaType}:${scope}:`)) {
blocked = true;
mediaWriteStartedResolve();
await mediaWriteGate;
}
};
try {
setActiveUserScope(scope);
const { materializeGenerationTaskAssets } = await import("../../src/services/project-asset-sync.ts?generation-media-commit-race-worker");
const { useAssetStore } = await import("../../src/stores/use-asset-store");
await import("../../src/stores/canvas/use-canvas-store");
const fileStorage = await import("../../src/services/file-storage");
const task = {
id: `remote-generation-media-race-${mediaType}`,
provider: "remote-test-provider",
type: mediaType,
status: "succeeded",
prompt: "redacted",
attempts: 1,
resultState: "PENDING_MATERIALIZATION",
resultJson: JSON.stringify(
mediaType === "video"
? {
mode: "video",
video: {
dataUrl: "data:video/mp4;base64,AAAA",
width: 16,
height: 9,
mimeType: "video/mp4",
},
}
: {
mode: "audio",
audio: {
dataUrl: "data:audio/mpeg;base64,AAAA",
mimeType: "audio/mpeg",
},
},
),
createdAt: "2026-08-14T00:00:00.000Z",
updatedAt: "2026-08-14T00:00:00.000Z",
} as const;
const materializing = materializeGenerationTaskAssets(task as never);
await mediaWriteStarted;
useAssetStore.getState().cleanupImages({ assets: [] });
const cleanupRun = harness.scheduled.at(-1);
if (!cleanupRun) throw new Error("cleanup was not scheduled");
await Promise.race([
cleanupRun,
new Promise<void>((resolve) => {
harness.realSetTimeout(resolve, 20);
}),
]);
releaseMediaWrite();
const materialized = await materializing;
await cleanupRun;
const assetId = materialized.outputs?.[0]?.materializedAssetId;
const asset = useAssetStore.getState().assets.find((candidate) => candidate.id === assetId);
const storageKey = asset?.kind === "video" || asset?.kind === "audio" ? asset.data.storageKey : undefined;
const blobPresent = storageKey ? (await fileStorage.getMediaBlob(storageKey)) instanceof Blob : false;
const generationAssetCount = useAssetStore.getState().assets.filter((candidate) => candidate.metadata?.generationEffectKey === `materialize:${task.id}:0`).length;
return { kind: asset?.kind, storageKey, blobPresent, generationAssetCount };
} finally {
releaseMediaWrite();
setActiveUserScope(previousScope);
harness.restore();
}
}
self.onmessage = async (event: MessageEvent<Scenario>) => {
try {
const result =
event.data === "image-cleanup"
? await runImageCleanup()
: event.data === "scope-cleanup-switch"
? await runScopeCleanupAfterSwitch()
: event.data === "scope-cleanup-late-canvas-reference"
? await runScopeCleanupAfterLateCanvasReference()
: await runMediaCommitRace(event.data === "audio-commit-race" ? "audio" : "video");
self.postMessage({ ok: true, result });
} catch (error) {
self.postMessage({ ok: false, error: error instanceof Error ? `${error.name}: ${error.message}` : String(error) });
}
};

View File

@@ -0,0 +1,123 @@
import { afterEach, describe, expect, test } from "bun:test";
import axios from "axios";
import { desktopLocalChannelUiVisible } from "../src/lib/desktop-local-channel";
import { fetchChannelModels } from "../src/services/api/image";
import { channelRequest } from "../src/services/api/custom-channel-relay";
import { backendProviderConfig } from "../src/services/api/generation-task";
import { channelConnectionSignature, createModelChannel, defaultConfig, normalizeConfigSnapshot, resolveModelRequestConfig, type ModelChannel } from "../src/stores/use-config-store";
import { defaultFeatureAvailability, useUserStore } from "../src/stores/use-user-store";
const originalAxiosPost = axios.post;
const originalWindow = globalThis.window;
afterEach(() => {
axios.post = originalAxiosPost;
useUserStore.setState({ features: defaultFeatureAvailability });
if (originalWindow === undefined) delete (globalThis as { window?: Window }).window;
else Object.defineProperty(globalThis, "window", { configurable: true, value: originalWindow });
});
function setLocalChannelRuntime(desktopLocalChannelsEnabled: boolean, hostname: string) {
useUserStore.setState({ features: { ...defaultFeatureAvailability, desktopLocalChannelsEnabled } });
Object.defineProperty(globalThis, "window", { configurable: true, value: { location: { hostname } } });
}
function forgedLocalGenerationConfig() {
const channel = createModelChannel({
id: "local",
name: "Local",
baseUrl: "http://127.0.0.1:8000",
apiKey: "test-key",
apiFormat: "openai",
models: ["local-model"],
allowLocalChannel: true,
});
return normalizeConfigSnapshot({ config: { ...defaultConfig, channels: [channel], model: "local::local-model", textModel: "local::local-model" } }).config;
}
describe("允许本机渠道前端请求合同", () => {
test("默认关闭,旧持久化记录回退 false,新记录 true 可回显并进入请求配置", () => {
const legacy = createModelChannel({ id: "legacy", name: "Legacy", baseUrl: "https://example.com", apiKey: "key", models: ["model"] });
expect(legacy.allowLocalChannel).toBe(false);
const local = createModelChannel({ id: "local", name: "Local", baseUrl: "http://127.0.0.1:8000", apiKey: "key", models: ["model"], allowLocalChannel: true });
const normalized = normalizeConfigSnapshot({ config: { ...defaultConfig, channels: [local], model: "local::model", textModel: "local::model" } }).config;
expect(normalized.channels[0]?.allowLocalChannel).toBe(true);
setLocalChannelRuntime(true, "127.0.0.1");
expect(resolveModelRequestConfig(normalized, "local::model").allowLocalChannel).toBe(true);
setLocalChannelRuntime(false, "127.0.0.1");
expect(resolveModelRequestConfig(normalized, "local::model").allowLocalChannel).toBe(false);
});
test("UI 只在 Backend capability=true 且页面文本 host 精确 loopback 时显示", () => {
expect(desktopLocalChannelUiVisible(true, "127.0.0.1")).toBe(true);
expect(desktopLocalChannelUiVisible(true, "localhost")).toBe(true);
for (const hostname of ["localhost.", "x.localhost", "127.0.0.2", "::1", "0.0.0.0", "192.168.1.5", "canvas.example.com"]) {
expect(desktopLocalChannelUiVisible(true, hostname)).toBe(false);
}
expect(desktopLocalChannelUiVisible(false, "127.0.0.1")).toBe(false);
expect(desktopLocalChannelUiVisible(false, "localhost")).toBe(false);
});
test("channelConnectionSignature 包含 allowLocalChannel,切换后旧模型拉取结果会失效", () => {
const channel = createModelChannel({ id: "local", name: "Local", baseUrl: "http://127.0.0.1:8000", apiKey: "key", models: ["model"] });
expect(channelConnectionSignature(channel)).not.toBe(channelConnectionSignature({ ...channel, allowLocalChannel: true }));
});
test("实际生成配置与 relay 在运行时重新投影 forged persisted local flag", () => {
const config = forgedLocalGenerationConfig();
setLocalChannelRuntime(false, "127.0.0.1");
const capabilityClosed = backendProviderConfig(config);
expect(capabilityClosed.allowLocalChannel).toBe(false);
expect(channelRequest(capabilityClosed, "http://127.0.0.1:8000/v1/responses").headers["x-canvas-allow-local-channel"]).toBeUndefined();
setLocalChannelRuntime(true, "canvas.example.com");
const remotePage = backendProviderConfig(config);
expect(remotePage.allowLocalChannel).toBe(false);
expect(channelRequest(remotePage, "http://127.0.0.1:8000/v1/responses").headers["x-canvas-allow-local-channel"]).toBeUndefined();
setLocalChannelRuntime(true, "localhost");
const desktop = backendProviderConfig(config);
expect(desktop.allowLocalChannel).toBe(true);
const request = channelRequest(desktop, "http://127.0.0.1:8000/v1/responses", { "Content-Type": "application/json" });
expect(request.headers["x-canvas-allow-local-channel"]).toBe("1");
expect(request.headers["x-canvas-upstream-base-url"]).toBe("http://127.0.0.1:8000/");
});
test("登录态模型拉取同样消费运行时投影而不是 forged persisted flag", async () => {
const bodies: Record<string, unknown>[] = [];
axios.post = (async (_url: string, body: Record<string, unknown>) => {
bodies.push(body);
return { data: { code: 0, data: { models: [{ id: "local-model" }] }, msg: "ok" } };
}) as typeof axios.post;
const channel: ModelChannel = forgedLocalGenerationConfig().channels[0];
setLocalChannelRuntime(false, "127.0.0.1");
await fetchChannelModels(channel, true);
expect(bodies.at(-1)?.allowLocalChannel).toBe(false);
setLocalChannelRuntime(true, "canvas.example.com");
await fetchChannelModels(channel, true);
expect(bodies.at(-1)?.allowLocalChannel).toBe(false);
setLocalChannelRuntime(true, "127.0.0.1");
await fetchChannelModels(channel, true);
expect(bodies.at(-1)?.allowLocalChannel).toBe(true);
});
test("custom relay 最终边界也会压掉直接伪造的 local flag", () => {
const config = {
baseUrl: "http://127.0.0.1:8000",
apiKey: "test-key",
apiFormat: "openai" as const,
allowLocalChannel: true,
};
setLocalChannelRuntime(false, "127.0.0.1");
expect(channelRequest(config, "http://127.0.0.1:8000/v1/responses").headers["x-canvas-allow-local-channel"]).toBeUndefined();
setLocalChannelRuntime(true, "localhost");
expect(channelRequest(config, "http://127.0.0.1:8000/v1/responses").headers["x-canvas-allow-local-channel"]).toBe("1");
});
});

View File

@@ -0,0 +1,122 @@
import assert from "node:assert/strict";
import { after, afterEach, test } from "node:test";
import os from "node:os";
import path from "node:path";
import axios from "axios";
import { createServer } from "vite";
const webRoot = process.cwd();
const originalWindow = globalThis.window;
const viteRuntime = createServer({
root: webRoot,
configFile: false,
cacheDir: path.join(os.tmpdir(), `framefield-local-channel-runtime-${process.pid}`),
resolve: { alias: { "@": path.join(webRoot, "src") } },
define: { __APP_VERSION__: JSON.stringify("test"), __APP_CHANGELOG__: JSON.stringify("") },
server: { middlewareMode: true },
appType: "custom",
logLevel: "silent",
});
const runtime = await viteRuntime;
const configStore = await runtime.ssrLoadModule("/src/stores/use-config-store.ts");
const userStore = await runtime.ssrLoadModule("/src/stores/use-user-store.ts");
const generationTask = await runtime.ssrLoadModule("/src/services/api/generation-task.ts");
const relay = await runtime.ssrLoadModule("/src/services/api/custom-channel-relay.ts");
const imageApi = await runtime.ssrLoadModule("/src/services/api/image.ts");
const originalAxiosPost = axios.post;
after(async () => {
await runtime.close();
});
afterEach(() => {
axios.post = originalAxiosPost;
userStore.useUserStore.setState({ features: userStore.defaultFeatureAvailability });
if (originalWindow === undefined) delete globalThis.window;
else Object.defineProperty(globalThis, "window", { configurable: true, value: originalWindow });
});
function setRuntime(desktopLocalChannelsEnabled, hostname) {
userStore.useUserStore.setState({ features: { ...userStore.defaultFeatureAvailability, desktopLocalChannelsEnabled } });
Object.defineProperty(globalThis, "window", { configurable: true, value: { location: { hostname } } });
}
function forgedConfig() {
const channel = configStore.createModelChannel({
id: "local",
name: "Local",
baseUrl: "http://127.0.0.1:8000",
apiKey: "test-key",
apiFormat: "openai",
models: ["local-model"],
allowLocalChannel: true,
});
return configStore.normalizeConfigSnapshot({
config: {
...configStore.defaultConfig,
channels: [channel],
model: "local::local-model",
textModel: "local::local-model",
},
}).config;
}
function requestFlag(config) {
const request = relay.channelRequest(config, "http://127.0.0.1:8000/v1/responses", { "Content-Type": "application/json" });
return request.headers["x-canvas-allow-local-channel"];
}
test("actual generation request config reprojects forged persisted local permission at runtime", () => {
const config = forgedConfig();
assert.equal(config.channels[0]?.allowLocalChannel, true, "persisted channel intentionally remains true before runtime projection");
setRuntime(false, "127.0.0.1");
const capabilityClosed = generationTask.backendProviderConfig(config);
assert.equal(capabilityClosed.allowLocalChannel, false);
assert.equal(requestFlag(capabilityClosed), undefined);
setRuntime(true, "canvas.example.com");
const remotePage = generationTask.backendProviderConfig(config);
assert.equal(remotePage.allowLocalChannel, false);
assert.equal(requestFlag(remotePage), undefined);
setRuntime(true, "localhost");
const desktop = generationTask.backendProviderConfig(config);
assert.equal(desktop.allowLocalChannel, true);
assert.equal(requestFlag(desktop), "1");
});
test("authenticated model fetch reprojects forged persisted local permission before backend request", async () => {
const bodies = [];
axios.post = async (_url, body) => {
bodies.push(body);
return { data: { code: 0, data: { models: [{ id: "local-model" }] }, msg: "ok" } };
};
const channel = forgedConfig().channels[0];
setRuntime(false, "127.0.0.1");
await imageApi.fetchChannelModels(channel, true);
assert.equal(bodies.at(-1)?.allowLocalChannel, false);
setRuntime(true, "canvas.example.com");
await imageApi.fetchChannelModels(channel, true);
assert.equal(bodies.at(-1)?.allowLocalChannel, false);
setRuntime(true, "localhost");
await imageApi.fetchChannelModels(channel, true);
assert.equal(bodies.at(-1)?.allowLocalChannel, true);
});
test("custom relay final boundary independently reprojects a directly forged local permission", () => {
const forged = { baseUrl: "http://127.0.0.1:8000", apiKey: "test-key", apiFormat: "openai", allowLocalChannel: true };
setRuntime(false, "127.0.0.1");
assert.equal(requestFlag(forged), undefined);
setRuntime(true, "canvas.example.com");
assert.equal(requestFlag(forged), undefined);
setRuntime(true, "127.0.0.1");
assert.equal(requestFlag(forged), "1");
});

View File

@@ -0,0 +1,71 @@
import assert from "node:assert/strict";
import { test } from "bun:test";
function ambiguousChannels(store) {
const shared = {
name: "Shared",
baseUrl: "http://127.0.0.1:8000",
apiKey: "same-key",
apiFormat: "openai",
models: ["same-model"],
};
return [store.createModelChannel({ ...shared, id: "local-enabled", allowLocalChannel: true }), store.createModelChannel({ ...shared, id: "local-disabled", allowLocalChannel: false })];
}
test("task-center forwards explicit task/session provider config without inferring a channel from Zustand", async () => {
const taskCenter = await import("../src/services/api/task-center");
const generationTask = await import("../src/services/api/generation-task");
const requestModule = await import("../src/services/api/request");
const store = await import("../src/stores/use-config-store");
const userStore = await import("../src/stores/use-user-store");
const previousWindow = globalThis.window;
const previousConfig = store.useConfigStore.getState().config;
userStore.useUserStore.setState({ features: { ...userStore.defaultFeatureAvailability, desktopLocalChannelsEnabled: true } });
const channels = ambiguousChannels(store);
const explicitConfig = {
...store.defaultConfig,
channels,
models: ["local-enabled::same-model", "local-disabled::same-model"],
model: "local-disabled::same-model",
textModel: "local-disabled::same-model",
};
store.useConfigStore.setState({ config: explicitConfig });
globalThis.window = { location: { hostname: "127.0.0.1" }, dispatchEvent() {} };
assert.equal(generationTask.backendProviderConfig(explicitConfig).allowLocalChannel, false);
assert.equal(generationTask.backendProviderConfig({ ...explicitConfig, model: "local-enabled::same-model" }).allowLocalChannel, true);
const bodies = [];
const originalPost = requestModule.apiClient.post;
requestModule.apiClient.post = async (url, body) => {
bodies.push({ url, body });
if (url === "/sessions") {
return { data: { code: 0, data: { session: { id: "session", status: "active", prompt: "p", createdAt: "now", updatedAt: "now" }, messages: [], tasks: [], results: [] }, msg: "ok" } };
}
return { data: { code: 0, data: { id: "task", type: "canvas_text", status: "queued", prompt: "p", attempts: 1, createdAt: "now", updatedAt: "now" }, msg: "ok" } };
};
try {
const providerConfig = {
baseUrl: "http://127.0.0.1:8000",
apiKey: "same-key",
apiFormat: "openai",
model: "same-model",
allowLocalChannel: false,
};
await taskCenter.createAgentSession({ prompt: "p", config: providerConfig });
await taskCenter.createGenerationTask({
prompt: "p",
model: "same-model",
input: { mode: "text", config: providerConfig },
});
} finally {
requestModule.apiClient.post = originalPost;
userStore.useUserStore.setState({ features: userStore.defaultFeatureAvailability });
globalThis.window = previousWindow;
store.useConfigStore.setState({ config: previousConfig });
}
assert.equal(bodies[0]?.url, "/sessions");
assert.equal(bodies[0]?.body?.config?.allowLocalChannel, false);
assert.equal(bodies[1]?.url, "/tasks");
assert.equal(bodies[1]?.body?.input?.config?.allowLocalChannel, false);
});

View File

@@ -0,0 +1,117 @@
import assert from "node:assert/strict";
import { test } from "bun:test";
async function loadPageWiring(modulePath, ownerExport, fieldsExport, switchExport) {
const previousVersion = globalThis.__APP_VERSION__;
const previousChangelog = globalThis.__APP_CHANGELOG__;
globalThis.__APP_VERSION__ = "test";
globalThis.__APP_CHANGELOG__ = "";
try {
const module = await import(modulePath);
const owner = module[ownerExport];
const FieldsComponent = module[fieldsExport];
const SwitchComponent = module[switchExport];
assert.equal(typeof owner, "function", `${ownerExport} must be exported by ${modulePath}`);
assert.equal(typeof FieldsComponent, "function", `${fieldsExport} must be the hook-free fields view actually rendered by ${modulePath}`);
assert.equal(typeof SwitchComponent, "function", `${switchExport} must be the real page Switch component`);
return { module, owner, FieldsComponent, SwitchComponent };
} finally {
if (previousVersion === undefined) delete globalThis.__APP_VERSION__;
else globalThis.__APP_VERSION__ = previousVersion;
if (previousChangelog === undefined) delete globalThis.__APP_CHANGELOG__;
else globalThis.__APP_CHANGELOG__ = previousChangelog;
}
}
function findAntdSwitchElement(node, SwitchComponent) {
if (!node || typeof node !== "object") return null;
if (node.type && typeof node.type === "object" && node.type.__ANT_SWITCH === true) return node;
if (node.type === SwitchComponent) return findAntdSwitchElement(SwitchComponent(node.props), SwitchComponent);
const children = node.props?.children;
if (Array.isArray(children)) {
for (const child of children) {
const match = findAntdSwitchElement(child, SwitchComponent);
if (match) return match;
}
return null;
}
return findAntdSwitchElement(children, SwitchComponent);
}
function adminValues(allowLocalChannel) {
return { name: "Local", baseUrl: "http://127.0.0.1:8000", allowLocalChannel, apiKey: "key" };
}
test("administrator default editor fields render the real Switch and feed the real save payload", { timeout: 30_000 }, async () => {
const page = await loadPageWiring("../src/pages/admin/channels/channels-page.tsx", "adminLocalChannelFormOwner", "AdminLocalChannelFields", "AdminLocalChannelSwitch");
assert.equal(typeof page.module.adminChannelSavePayload, "function");
const values = adminValues(false);
const form = {
setFieldValue(name, value) {
assert.equal(name, "allowLocalChannel");
values.allowLocalChannel = value;
},
};
const hiddenOwner = page.owner(false, "127.0.0.1", true);
const hiddenTree = page.FieldsComponent({ visible: hiddenOwner.visible, checked: hiddenOwner.checked, form });
assert.equal(findAntdSwitchElement(hiddenTree, page.SwitchComponent), null);
assert.equal(page.module.adminChannelSavePayload(adminValues(true), false, "127.0.0.1").allowLocalChannel, false);
const initialOwner = page.owner(true, "127.0.0.1", undefined);
const initialTree = page.FieldsComponent({ visible: initialOwner.visible, checked: initialOwner.checked, form });
const initialSwitch = findAntdSwitchElement(initialTree, page.SwitchComponent);
assert.ok(initialSwitch, "default admin editor fields must contain the real AntD Switch");
assert.equal(initialSwitch.props.checked, false);
initialSwitch.props.onChange(true);
assert.equal(values.allowLocalChannel, true, "real admin Switch must write the Form field");
assert.equal(page.module.adminChannelSavePayload(values, true, "127.0.0.1").allowLocalChannel, true);
const persistedOwner = page.owner(true, "localhost", true);
const persistedTree = page.FieldsComponent({ visible: persistedOwner.visible, checked: persistedOwner.checked, form });
const persistedSwitch = findAntdSwitchElement(persistedTree, page.SwitchComponent);
assert.ok(persistedSwitch);
assert.equal(persistedSwitch.props.checked, true);
persistedSwitch.props.onChange(false);
assert.equal(values.allowLocalChannel, false, "real admin Switch must clear the Form field");
assert.equal(page.module.adminChannelSavePayload(values, true, "localhost").allowLocalChannel, false);
const nonLoopbackOwner = page.owner(true, "canvas.example.com", true);
assert.equal(findAntdSwitchElement(page.FieldsComponent({ visible: nonLoopbackOwner.visible, checked: nonLoopbackOwner.checked, form }), page.SwitchComponent), null);
assert.equal(page.module.adminChannelSavePayload(adminValues(true), true, "canvas.example.com").allowLocalChannel, false);
});
test("user default channel fields render the real Switch and feed the real update patch", { timeout: 30_000 }, async () => {
const page = await loadPageWiring("../src/pages/settings/index.tsx", "userLocalChannelFormOwner", "UserLocalChannelFields", "UserLocalChannelSwitch");
assert.equal(typeof page.module.userLocalChannelChangePatch, "function");
const channel = { id: "local", name: "Local", baseUrl: "http://127.0.0.1:8000", apiKey: "key", apiFormat: "openai", models: ["model"], allowLocalChannel: true };
let update;
const updateChannel = (id, patch) => {
update = { id, patch };
};
const hiddenOwner = page.owner(false, "127.0.0.1", true);
const hiddenTree = page.FieldsComponent({ channel, visible: hiddenOwner.visible, checked: hiddenOwner.checked, desktopLocalChannelsEnabled: false, hostname: "127.0.0.1", updateChannel });
assert.equal(findAntdSwitchElement(hiddenTree, page.SwitchComponent), null);
assert.equal(page.module.userLocalChannelChangePatch(false, "127.0.0.1", true).allowLocalChannel, false);
const initialOwner = page.owner(true, "127.0.0.1", false);
const initialTree = page.FieldsComponent({ channel: { ...channel, allowLocalChannel: false }, visible: initialOwner.visible, checked: initialOwner.checked, desktopLocalChannelsEnabled: true, hostname: "127.0.0.1", updateChannel });
const initialSwitch = findAntdSwitchElement(initialTree, page.SwitchComponent);
assert.ok(initialSwitch, "default user channel fields must contain the real AntD Switch");
assert.equal(initialSwitch.props.checked, false);
initialSwitch.props.onChange(true);
assert.deepEqual(update, { id: "local", patch: { allowLocalChannel: true } }, "real user Switch must call updateChannel with the projected patch");
const persistedOwner = page.owner(true, "localhost", true);
const persistedTree = page.FieldsComponent({ channel, visible: persistedOwner.visible, checked: persistedOwner.checked, desktopLocalChannelsEnabled: true, hostname: "localhost", updateChannel });
const persistedSwitch = findAntdSwitchElement(persistedTree, page.SwitchComponent);
assert.ok(persistedSwitch);
assert.equal(persistedSwitch.props.checked, true);
persistedSwitch.props.onChange(false);
assert.deepEqual(update, { id: "local", patch: { allowLocalChannel: false } });
const nonLoopbackOwner = page.owner(true, "canvas.example.com", true);
assert.equal(findAntdSwitchElement(page.FieldsComponent({ channel, visible: nonLoopbackOwner.visible, checked: nonLoopbackOwner.checked, desktopLocalChannelsEnabled: true, hostname: "canvas.example.com", updateChannel }), page.SwitchComponent), null);
assert.deepEqual(page.module.userLocalChannelChangePatch(true, "canvas.example.com", true), { allowLocalChannel: false });
});

View File

@@ -0,0 +1,247 @@
import { expect, test } from "bun:test";
test("Local CLI settings automatically connects without any browser-confirmation flow or connection secrets", async () => {
const module = await import("../src/pages/settings/local-cli-settings").catch(() => ({}));
const present = (
module as {
localCliSettingsPresentation?: typeof presentationContract;
}
).localCliSettingsPresentation;
const copy = (
module as {
LOCAL_CLI_SETTINGS_COPY?: typeof compactCopyContract;
}
).LOCAL_CLI_SETTINGS_COPY;
expect(typeof present).toBe("function");
expect(copy).toEqual(compactCopyContract);
expect("openLocalRuntimePairing" in module).toBe(false);
if (!present) return;
const obsoleteConnectionState = present({
connection: "obsolete_browser_confirmation",
moduleAvailable: false,
dreamina: undefined,
});
expect(obsoleteConnectionState).toMatchObject({
runtime: { label: "尚未检测", action: "refresh" },
dreamina: { label: "连接本机服务后自动检测", action: null },
});
const reconnect = present({
connection: "origin_not_trusted",
moduleAvailable: false,
dreamina: undefined,
});
expect(reconnect).toMatchObject({
runtime: { label: "需要重新连接", action: "refresh", actionLabel: "重新连接" },
dreamina: { label: "连接本机服务后自动检测", action: null },
});
const installed = present({
connection: "connected",
moduleAvailable: true,
dreamina: {
provider: "dreamina-cli",
state: "installed",
installed: true,
authenticated: false,
code: "dreamina_login_required",
message: "Dreamina CLI 已安装,需要登录",
version: "1.2.3",
},
});
expect(installed).toMatchObject({
runtime: { label: "已连接", action: "refresh" },
dreamina: { label: "未登录", action: "login" },
});
const authenticated = present({
connection: "connected",
moduleAvailable: true,
dreamina: {
provider: "dreamina-cli",
state: "authenticated",
installed: true,
authenticated: true,
message: "Dreamina CLI 已登录",
totalCredit: 24_940,
},
});
expect(authenticated.dreamina).toMatchObject({
label: "已登录",
action: "logout",
});
expect(authenticated.dreamina.creditLabel).toBeUndefined();
const serialized = JSON.stringify([obsoleteConnectionState, installed, authenticated]);
expect(serialized).not.toContain("endpoint");
expect(serialized).not.toContain("token");
expect(serialized).not.toContain("session");
});
test("Dreamina model and credit cache scope follows accountBinding plus sessionEpoch and credits expose observedAt", async () => {
const modelStore = await import("../src/stores/use-local-dreamina-model-store").catch(() => ({}));
const scopeKey = (
modelStore as {
dreaminaModelCacheScopeKey?: (scope: { accountBinding: string; sessionEpoch: number }) => string;
}
).dreaminaModelCacheScopeKey;
expect(typeof scopeKey).toBe("function");
if (scopeKey) {
expect(scopeKey({ accountBinding: "account-binding-a", sessionEpoch: 7 })).toBe("account-binding-a:7");
expect(scopeKey({ accountBinding: "account-binding-a", sessionEpoch: 8 })).not.toBe(scopeKey({ accountBinding: "account-binding-a", sessionEpoch: 7 }));
expect(scopeKey({ accountBinding: "account-binding-b", sessionEpoch: 7 })).not.toBe(scopeKey({ accountBinding: "account-binding-a", sessionEpoch: 7 }));
}
const settings = await import("../src/pages/settings/local-cli-settings").catch(() => ({}));
const present = (
settings as {
localCliSettingsPresentation?: (input: {
connection: string;
moduleAvailable: boolean;
timeZone?: string;
dreamina?: {
provider: "dreamina-cli";
state: "authenticated";
installed: true;
authenticated: true;
message: string;
totalCredit: number;
creditObservedAt: string;
accountBinding: string;
sessionEpoch: number;
};
}) => { dreamina: { creditLabel?: string; creditObservedAtLabel?: string } };
}
).localCliSettingsPresentation;
expect(typeof present).toBe("function");
if (!present) return;
const authenticated = present({
connection: "connected",
moduleAvailable: true,
timeZone: "Asia/Shanghai",
dreamina: {
provider: "dreamina-cli",
state: "authenticated",
installed: true,
authenticated: true,
message: "Dreamina CLI 已登录",
totalCredit: 24_940,
creditObservedAt: "2026-08-13T12:34:56.000Z",
accountBinding: "account-binding-a",
sessionEpoch: 7,
},
});
expect(authenticated.dreamina.creditLabel).toBe("即梦积分 24,940");
expect(authenticated.dreamina.creditObservedAtLabel).toBe("上次刷新积分 20:34");
const invalidObservedAt = present({
connection: "connected",
moduleAvailable: true,
timeZone: "Asia/Shanghai",
dreamina: {
provider: "dreamina-cli",
state: "authenticated",
installed: true,
authenticated: true,
message: "Dreamina CLI 已登录",
totalCredit: 24_940,
creditObservedAt: "not-an-iso-time",
accountBinding: "account-binding-a",
sessionEpoch: 7,
},
});
expect(invalidObservedAt.dreamina.creditLabel).toBeUndefined();
expect(invalidObservedAt.dreamina.creditObservedAtLabel).toBeUndefined();
});
test("Local CLI settings keeps the Runtime compact and uses the official Dreamina CLI copy", async () => {
const module = await import("../src/pages/settings/local-cli-settings").catch(() => ({}));
const copy = (
module as {
LOCAL_CLI_SETTINGS_COPY?: typeof compactCopyContract;
}
).LOCAL_CLI_SETTINGS_COPY;
expect(copy).toEqual(compactCopyContract);
const source = await Bun.file(new URL("../src/pages/settings/local-cli-settings.tsx", import.meta.url)).text();
expect(source.match(/void connect\(controller\.signal\)/g)).toHaveLength(1);
expect(source).not.toContain("runtimeController");
expect(source).toContain('void runDreamina("refresh")');
expect(source.match(/官方 CLI 登录资料保存在本机;本页面不读取或上传 Cookie、浏览器 Profile 或登录令牌。/g)).toHaveLength(1);
expect(source.match(/LOCAL_CLI_SETTINGS_COPY\.dreaminaAccountSwitch/g)).toHaveLength(1);
expect(source).not.toContain("dreaminaSafety");
expect(source).not.toContain("Framefield 不读取或上传 Cookie、浏览器 Profile 或登录令牌。");
expect(source).not.toContain("无需重复授权");
expect(source).not.toContain("runtime?.version");
expect(source).not.toContain("自动发现本机 Framefield Runtime");
expect(source).not.toContain("Canvas 与 OAuth CLI 共用同一个进程");
expect(source).not.toContain("当前浏览器密钥自动重连");
expect(source).not.toContain("页面未授权,以页面发起/完成授权为准");
const architecture = await Bun.file(new URL("../../canvas-agent/README.md", import.meta.url)).text();
expect(architecture).toContain("外部程序直接切换 Dreamina CLI 账号无法被本应用实时观测");
expect(architecture).toContain("官方 CLI 的 argv 可能被同一 OS 用户通过进程列表看到");
expect(architecture).toContain("prompt、receipt 或本地路径");
});
test("settings route recognizes local-cli as a first-class section", async () => {
const module = await import("../src/pages/settings");
const isConfigSection = (module as { isConfigSection?: (value: string | null) => boolean }).isConfigSection;
expect(typeof isConfigSection).toBe("function");
if (!isConfigSection) return;
expect(isConfigSection("local-cli")).toBe(true);
expect(isConfigSection("local-runtime-token")).toBe(false);
});
test("model settings and Create subscribe to the same effective Runtime catalog after first render", async () => {
const settingsSource = await Bun.file(new URL("../src/pages/settings/index.tsx", import.meta.url)).text();
const createSource = await Bun.file(new URL("../src/pages/create/index.tsx", import.meta.url)).text();
expect(settingsSource).toContain("useEffectiveConfig");
expect(settingsSource).toContain("<ModelDefaultGrid config={effectiveConfig}");
expect(createSource).toContain("const config = useEffectiveConfig()");
});
type PresentationInput = {
connection: string;
moduleAvailable: boolean;
dreamina?: {
provider: "dreamina-cli";
state: "missing" | "installed" | "login_pending" | "authenticated" | "error";
installed: boolean;
authenticated: boolean;
code?: string;
message: string;
version?: string;
totalCredit?: number;
accountBinding?: string;
sessionEpoch?: number;
creditObservedAt?: string;
};
};
type PresentationResult = {
runtime: { label: string; action: string | null; actionLabel?: string };
dreamina: { label: string; action: string | null; creditLabel?: string; creditObservedAtLabel?: string };
};
declare function presentationContract(input: PresentationInput): PresentationResult;
const compactCopyContract = {
runtimeTitle: "本机连接",
runtimeConnected: "本机服务已连接,CLI 状态会自动同步。",
runtimeDetecting: "正在检测本机服务;请确认已启动当前版本。",
runtimeReconnect: "重新连接",
runtimeSafety: "官方 CLI 登录资料保存在本机;本页面不读取或上传 Cookie、浏览器 Profile 或登录令牌。",
runtimeRefresh: "刷新状态",
dreaminaDescription: "直接读取当前 Windows 用户的官方即梦 CLI 登录状态。",
dreaminaDisconnected: "连接本机服务后自动检测",
dreaminaDisconnectedMessage: "重新连接本机服务后,将自动读取官方 CLI 状态。",
dreaminaMembership: "账号生成权限:未知。当前页面只确认本机适配器支持与登录状态;具体账号是否可生成,以官方最终结果为准。",
dreaminaConsistency: "任务状态通过后台轮询最终同步,不是实时推送;关闭页面不会停止已经提交的官方任务。",
dreaminaCancel: "官方 Dreamina CLI 当前不提供取消命令;官方已接受的任务只能转入后台继续同步,不能伪装成已取消。",
dreaminaAccountSwitch: "本机任务运行期间,请不要在其他程序中切换 Dreamina CLI 账号;外部换号无法被本页面实时感知。",
dreaminaRefresh: "刷新状态",
} as const;

View File

@@ -0,0 +1,255 @@
import { describe, expect, test } from "bun:test";
const privateCanary = "dreamina-private-canary";
const fixedNow = Date.parse("2026-08-10T00:00:00.000Z");
test("Dreamina browser deadlines cover the complete official CLI lifecycle", async () => {
const module = await import("../src/services/local-dreamina-cli");
expect(module.DREAMINA_REQUEST_TIMEOUTS).toEqual({
status: 30_000,
login: 35_000,
logout: 20_000,
});
for (const [action, expected] of Object.entries(module.DREAMINA_REQUEST_TIMEOUTS)) {
expect(module.dreaminaRequestTimeout(action as "status" | "login" | "logout")).toBe(expected);
expect(module.dreaminaRequestTimeout(action as "status" | "login" | "logout", 1_234)).toBe(1_234);
}
});
describe("signed Dreamina CLI lifecycle client", () => {
test("status login and logout use only fixed signed Runtime requests", async () => {
const module = await import("../src/services/local-dreamina-cli").catch(() => ({}));
expect(typeof (module as Record<string, unknown>).getDreaminaStatus).toBe("function");
if (!("getDreaminaStatus" in module)) return;
const calls: Array<{ path: string; method: string; headers: Headers; body: string }> = [];
const client = {
async request(path: string, init: RequestInit = {}) {
calls.push({
path,
method: String(init.method || "GET"),
headers: new Headers(init.headers),
body: String(init.body || ""),
});
return jsonResponse({ ok: true, status: installedStatus() });
},
};
await module.getDreaminaStatus(client, { now: () => fixedNow });
await module.loginDreamina(client, { now: () => fixedNow });
await module.logoutDreamina(client, { now: () => fixedNow });
expect(calls.map((call) => [call.method, call.path, call.body])).toEqual([
["GET", "/dreamina/status", ""],
["POST", "/dreamina/login", "{}"],
["POST", "/dreamina/logout", "{}"],
]);
expect(calls.every((call) => !call.headers.has("authorization"))).toBe(true);
expect(calls.every((call) => !call.headers.has("x-canvas-agent-token"))).toBe(true);
expect(JSON.stringify(calls)).not.toContain("ownerId");
});
test("projects bounded lifecycle status without preserving injected messages", async () => {
const module = await import("../src/services/local-dreamina-cli");
const result = await module.getDreaminaStatus(
{
request: async () =>
jsonResponse({
ok: true,
status: { ...installedStatus(), message: `${privateCanary} C:\\Users\\owner\\Profile` },
}),
},
{ now: () => fixedNow },
);
expect(result).toEqual({
provider: "dreamina-cli",
state: "installed",
installed: true,
authenticated: false,
version: "1.2.3",
code: "dreamina_login_required",
message: "Dreamina CLI 已安装,需要登录",
});
expect(JSON.stringify(result)).not.toContain(privateCanary);
expect(JSON.stringify(result)).not.toContain("Profile");
});
test("projects only a bounded numeric Dreamina credit balance for authenticated status", async () => {
const module = await import("../src/services/local-dreamina-cli");
const result = await module.getDreaminaStatus(
{
request: async () =>
jsonResponse({
ok: true,
status: { ...authenticatedStatus(), totalCredit: 24_940 },
}),
},
{ now: () => fixedNow },
);
expect(result.totalCredit).toBe(24_940);
for (const totalCredit of [-1, 1.5, "24940", null, Number.MAX_SAFE_INTEGER]) {
await expect(
module.getDreaminaStatus(
{
request: async () =>
jsonResponse({
ok: true,
status: { ...authenticatedStatus(), totalCredit },
}),
},
{ now: () => fixedNow },
),
).rejects.toMatchObject({ code: "dreamina_response_invalid" });
}
await expect(
module.getDreaminaStatus(
{
request: async () =>
jsonResponse({
ok: true,
status: { ...installedStatus(), totalCredit: 24_940 },
}),
},
{ now: () => fixedNow },
),
).rejects.toMatchObject({ code: "dreamina_response_invalid" });
});
test("keeps official pending verification fields and rejects state contradictions", async () => {
const module = await import("../src/services/local-dreamina-cli");
const expiresAt = new Date(fixedNow + 60_000).toISOString();
const pending = await module.loginDreamina(
{
request: async () =>
jsonResponse({
ok: true,
status: {
provider: "dreamina-cli",
state: "login_pending",
installed: true,
authenticated: false,
code: "dreamina_login_pending",
message: privateCanary,
verificationUri: "https://jimeng.jianying.com/ai-tool/cli-auth?verification_uri=https%3A%2F%2Fjimeng.jianying.com%2Fpassport%2Fopen",
userCode: "ABCD-EFGH",
expiresAt,
},
}),
},
{ now: () => fixedNow },
);
expect(pending).toMatchObject({
state: "login_pending",
message: "请在官方页面确认 Dreamina 登录",
userCode: "ABCD-EFGH",
expiresAt,
});
expect(JSON.stringify(pending)).not.toContain(privateCanary);
await expect(
module.getDreaminaStatus(
{
request: async () =>
jsonResponse({
ok: true,
status: { ...installedStatus(), state: "authenticated", authenticated: true },
}),
},
{ now: () => fixedNow },
),
).rejects.toMatchObject({ code: "dreamina_response_invalid" });
});
test("maps unknown Runtime errors and oversized bodies to stable public failures", async () => {
const module = await import("../src/services/local-dreamina-cli");
const privateError = module.getDreaminaStatus(
{
request: async () => jsonResponse({ ok: false, code: "private_error", message: privateCanary }, 500),
},
{ now: () => fixedNow },
);
await expect(privateError).rejects.toMatchObject({
code: "dreamina_internal_error",
message: "Dreamina CLI 请求失败",
});
await expect(privateError).rejects.not.toThrow(privateCanary);
await expect(
module.getDreaminaStatus(
{
request: async () =>
new Response(JSON.stringify({ ok: true, status: installedStatus(), privateCanary }), {
headers: { "content-length": String(70 * 1024) },
}),
},
{ now: () => fixedNow },
),
).rejects.toMatchObject({ code: "dreamina_response_invalid" });
});
test("cancellation and deadlines abort the in-flight signed request", async () => {
const module = await import("../src/services/local-dreamina-cli");
const cancelled = new AbortController();
cancelled.abort();
let calls = 0;
await expect(
module.getDreaminaStatus(
{
request: async () => {
calls++;
return jsonResponse({ ok: true, status: installedStatus() });
},
},
{ signal: cancelled.signal, now: () => fixedNow },
),
).rejects.toMatchObject({ code: "dreamina_cancelled" });
expect(calls).toBe(0);
await expect(
module.getDreaminaStatus(
{
request: async (_path, init) =>
await new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener("abort", () => reject(new DOMException("aborted", "AbortError")), { once: true });
}),
},
{ timeoutMs: 5, now: () => fixedNow },
),
).rejects.toMatchObject({ code: "dreamina_timeout" });
});
});
function installedStatus() {
return {
provider: "dreamina-cli",
state: "installed",
installed: true,
authenticated: false,
version: "1.2.3",
code: "dreamina_login_required",
message: "fixture",
};
}
function authenticatedStatus() {
return {
provider: "dreamina-cli",
state: "authenticated",
installed: true,
authenticated: true,
version: "1.2.3",
message: "fixture",
};
}
function jsonResponse(body: unknown, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json", "cache-control": "no-store" },
});
}

View File

@@ -0,0 +1,197 @@
import { expect, test } from "bun:test";
import { EventEmitter } from "node:events";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { DreaminaCliRuntime } from "../../canvas-agent/src/dreamina-cli-runtime";
import { createDreaminaHttpModule } from "../../canvas-agent/src/modules/dreamina-http";
import { waitForLocalDreaminaGenerationTask } from "../src/services/local-dreamina-generation";
test("Dreamina cross-Runtime completion survives HTTP wait and the Web parser without a second provider query", async () => {
const configDir = await fs.mkdtemp(path.join(os.tmpdir(), "dreamina-web-cross-runtime-"));
const stateFile = path.join(configDir, "dreamina-runtime-state.json");
const id = "dreamina-web-cross-runtime-0001";
const videoBytes = Buffer.from("00000018667479706d703432", "hex");
await fs.writeFile(
stateFile,
JSON.stringify({
version: 1,
records: [
{
ownerId: "owner-web-cross-runtime-0001",
idempotencyKey: id,
requestHash: "a".repeat(64),
state: "accepted",
submitId: "receipt-web-cross-runtime",
updatedAt: "2026-08-13T00:00:00.000Z",
taskVersion: 1,
operation: "text2video",
mode: "video",
model: "seedance2.0mini",
createdAt: "2026-08-13T00:00:00.000Z",
nextPollAt: "2000-01-01T00:00:00.000Z",
},
],
}),
);
let releaseWinner!: () => void;
let markWinnerEntered!: () => void;
const winnerGate = new Promise<void>((resolve) => {
releaseWinner = resolve;
});
const winnerEntered = new Promise<void>((resolve) => {
markWinnerEntered = resolve;
});
let winnerQueries = 0;
let waiterQueries = 0;
const common = {
ownerId: "owner-web-cross-runtime-0001",
stateFile,
ensureReady: async () => undefined,
discover: async () => ({ installed: true as const, executable: "dreamina-fixture" }),
};
const winner = new DreaminaCliRuntime({
...common,
generationRoot: path.join(configDir, "winner-runs"),
runProcess: async (input) => {
winnerQueries += 1;
if (winnerQueries === 1) {
markWinnerEntered();
await winnerGate;
}
const output = input.args.find((arg) => arg.startsWith("--download_dir="))?.slice("--download_dir=".length);
if (output) await fs.writeFile(path.join(output, "result.mp4"), videoBytes);
return { exitCode: 0, stdout: '{"status":"completed"}', stderr: "" };
},
});
const waiter = new DreaminaCliRuntime({
...common,
generationRoot: path.join(configDir, "waiter-runs"),
runProcess: async () => {
waiterQueries += 1;
return { exitCode: 0, stdout: '{"status":"failed"}', stderr: "" };
},
});
const module = createDreaminaHttpModule({
ownerId: common.ownerId,
configDir,
dreaminaRuntime: waiter,
});
try {
await winner.start();
await winnerEntered;
const parsed = waitForLocalDreaminaGenerationTask(id, "video", {
client: {
async connect() {
return connectedFixture();
},
async request(route, init) {
expect(route).toBe("/dreamina/generate/wait");
const response = await invokeDreaminaHttp(module, route, Buffer.from(String(init?.body)));
return jsonResponse(response.status, response.body);
},
},
});
await Promise.resolve();
releaseWinner();
await expect(parsed).resolves.toEqual({
id,
provider: "dreamina-cli",
mode: "video",
operation: "text2video",
model: "seedance2.0mini",
status: "succeeded",
stage: "succeeded",
progress: 100,
receiptRecorded: true,
officialStatus: "completed",
lifecycle: "TERMINAL",
terminalOutcome: "SUCCEEDED",
syncState: "SYNC_OK",
resultState: "PENDING_MATERIALIZATION",
outputs: [
{
outputIndex: 0,
mediaType: "video",
providerArtifactRef: expect.stringMatching(/^dreamina-provider-artifact:[a-f0-9-]{36}:0$/),
},
],
context: { scope: "legacy_unscoped" },
createdAt: "2026-08-13T00:00:00.000Z",
updatedAt: expect.any(String),
result: {
mode: "video",
video: {
dataUrl: `data:video/mp4;base64,${videoBytes.toString("base64")}`,
mimeType: "video/mp4",
bytes: videoBytes.byteLength,
},
},
});
expect(waiterQueries).toBe(0);
expect(winnerQueries).toBe(2);
const durableText = [await fs.readFile(stateFile, "utf8"), await fs.readFile(path.join(configDir, "dreamina-generation-task-store.json"), "utf8")].join("\n");
expect(durableText).not.toContain("data:video/");
expect(durableText).not.toContain(videoBytes.toString("base64"));
expect(durableText).not.toContain(path.join(configDir, "winner-runs"));
expect(durableText).not.toMatch(/stdout|stderr|token|cookie/i);
} finally {
releaseWinner();
await Promise.allSettled([winner.dispose(), module.dispose?.()]);
await fs.rm(configDir, { recursive: true, force: true });
}
});
async function invokeDreaminaHttp(module: ReturnType<typeof createDreaminaHttpModule>, routePath: string, body: Buffer): Promise<{ status: number; body: unknown }> {
const route = module.routes.find((candidate) => candidate.path === routePath);
if (!route) throw new Error(`missing Dreamina route ${routePath}`);
const req = new EventEmitter() as EventEmitter & { body?: Buffer };
req.body = body;
const res = new EventEmitter() as EventEmitter & {
destroyed: boolean;
writableEnded: boolean;
statusCode: number;
json(value: unknown): void;
status(code: number): typeof res;
};
res.destroyed = false;
res.writableEnded = false;
res.statusCode = 200;
let resolve!: (value: { status: number; body: unknown }) => void;
const response = new Promise<{ status: number; body: unknown }>((done) => {
resolve = done;
});
res.json = (value) => {
res.writableEnded = true;
resolve({ status: res.statusCode, body: value });
};
res.status = (code) => {
res.statusCode = code;
return res;
};
route.handler(req as never, res as never, (error?: unknown) => {
if (error) resolve({ status: 500, body: { ok: false, code: "test_route_error" } });
});
return response;
}
function jsonResponse(status: number, value: unknown) {
return new Response(JSON.stringify(value), {
status,
headers: { "content-type": "application/json", "cache-control": "no-store" },
});
}
function connectedFixture() {
return {
state: "connected" as const,
runtimeVersion: 2,
session: {
sessionId: "session-test-000000000001",
keyId: "browser-key-test",
scopes: ["dreamina:generate"],
expiresAt: "2099-01-01T00:00:00.000Z",
},
};
}

View File

@@ -0,0 +1,814 @@
import { expect, test } from "bun:test";
import {
cancelLocalDreaminaGenerationTask,
deleteLocalDreaminaGenerationTask,
queryLocalDreaminaGenerationTask,
refreshLocalDreaminaGenerationTask,
runLocalDreaminaGenerationTask,
waitForLocalDreaminaGenerationTask,
} from "../src/services/local-dreamina-generation";
import { projectLocalDreaminaTask } from "../src/services/local-dreamina-task-projection";
import { LocalRuntimeSessionClient, type RuntimeBrowserKeyRecord, type RuntimeBrowserKeyStore } from "../src/services/local-runtime-session";
const trustedOrigin = "http://127.0.0.1:18630";
test("deleting a Dreamina task removes only the local record through one signed mutation", async () => {
const requests: Array<{ path: string; method: string; body: string }> = [];
await deleteLocalDreaminaGenerationTask("dreamina-delete-local-0001", {
client: {
async connect() {
return connectedFixture();
},
async request(path, init) {
requests.push({ path, method: String(init?.method), body: String(init?.body) });
return jsonResponse(200, { ok: true, result: { deleted: true } });
},
},
});
expect(requests).toEqual([
{
path: "/dreamina/generate/delete",
method: "POST",
body: JSON.stringify({ idempotencyKey: "dreamina-delete-local-0001" }),
},
]);
});
test("manual Dreamina status refresh sends exactly one signed status-only request", async () => {
const requests: Array<{ path: string; method: string; body: string }> = [];
const task = await refreshLocalDreaminaGenerationTask("dreamina-refresh-local-0001", {
client: {
async connect() {
return connectedFixture();
},
async request(path, init) {
requests.push({ path, method: String(init?.method), body: String(init?.body) });
return jsonResponse(
200,
taskEnvelope("dreamina-refresh-local-0001", "failed", {
officialStatus: "failed",
errorCode: "dreamina_official_failed",
}),
);
},
},
});
expect(requests).toEqual([
{
path: "/dreamina/generate/refresh",
method: "POST",
body: JSON.stringify({ idempotencyKey: "dreamina-refresh-local-0001" }),
},
]);
expect(task).toMatchObject({ status: "failed", officialStatus: "failed", errorCode: "dreamina_official_failed" });
});
test("Dreamina official failure uses stable neutral copy without exposing provider details", async () => {
await expect(
runLocalDreaminaGenerationTask(videoInput(), {
client: {
async connect() {
return connectedFixture();
},
async request() {
const envelope = taskEnvelope("dreamina-official-fail-0001", "failed", {
officialStatus: "failed",
errorCode: "dreamina_official_failed",
});
return jsonResponse(200, { ...envelope, message: "private provider detail" });
},
},
idempotencyKey: () => "dreamina-official-fail-0001",
}),
).rejects.toMatchObject({
code: "dreamina_official_failed",
message: "任务未成功。当前 Dreamina CLI 无法可靠判断是官网取消还是生成失败。",
status: 502,
});
});
test("Dreamina Web parsing preserves the shared orthogonal task contract while keeping the legacy DTO", async () => {
const task = await queryLocalDreaminaGenerationTask("dreamina-contract-web-0001", "video", {
client: {
async connect() {
return connectedFixture();
},
async request() {
return jsonResponse(200, {
ok: true,
result: {
...taskEnvelope("dreamina-contract-web-0001", "failed").result,
officialStatus: "failed",
errorCode: "dreamina_official_failed",
lifecycle: "TERMINAL",
terminalOutcome: "FAILED_OR_CANCELLED",
syncState: "SYNC_OK",
resultState: "NOT_AVAILABLE",
outputs: [],
context: { scope: "legacy_unscoped" },
providerObservation: {
source: "query_result",
observedAt: "2026-08-13T00:00:00.000Z",
accountBinding: "account-binding-fixture",
fenceEpoch: 9,
status: "failed",
},
},
});
},
},
});
const contract = task as unknown as Record<string, unknown>;
expect(contract.lifecycle).toBe("TERMINAL");
expect(contract.terminalOutcome).toBe("FAILED_OR_CANCELLED");
expect(contract.syncState).toBe("SYNC_OK");
expect(contract.resultState).toBe("NOT_AVAILABLE");
expect(contract.context).toEqual({ scope: "legacy_unscoped" });
const projected = projectLocalDreaminaTask(task, {
projectId: "current-project-must-not-bind",
prompt: "fixture",
type: "canvas_video",
attempts: 1,
});
expect(projected.projectId).toBeUndefined();
expect(projected.status).toBe("failed");
expect(projected.error).toBe("任务未成功。当前 Dreamina CLI 无法可靠判断是官网取消还是生成失败。");
});
test("Dreamina SUBMISSION_UNCERTAIN projects as failed attention without claiming official completion", () => {
const projected = projectLocalDreaminaTask({
id: "dreamina-submission-uncertain-web-0001",
provider: "dreamina-cli",
mode: "video",
operation: "text2video",
model: "seedance2.0mini",
status: "failed",
stage: "submission_unknown",
receiptRecorded: false,
lifecycle: "SUBMISSION_UNCERTAIN",
syncState: "SYNC_UNCERTAIN",
resultState: "NOT_AVAILABLE",
outputs: [],
context: { scope: "legacy_unscoped" },
createdAt: "2026-08-13T00:00:00.000Z",
updatedAt: "2026-08-13T00:01:00.000Z",
});
expect(projected.status).toBe("failed");
expect(projected.stage).toBe("submission_unknown");
expect(projected.receiptRecorded).toBe(false);
expect(projected.completedAt).toBeUndefined();
});
test("Dreamina local generation posts only to its signed Runtime route and never to backend tasks", async () => {
const requests: Array<{ path: string; method: string; body: string }> = [];
const result = await runLocalDreaminaGenerationTask(
{
model: "local:dreamina-cli:seedance2.0mini",
mode: "video",
prompt: "A short test clip",
settings: { aspect: "16:9", resolution: "720", duration: 4 },
references: [],
},
{
client: {
async connect() {
return connectedFixture();
},
async request(path, init) {
requests.push({ path, method: String(init?.method), body: String(init?.body) });
return jsonResponse(200, path.endsWith("/wait") ? taskEnvelope("seedance-web-route-0001", "succeeded") : taskEnvelope("seedance-web-route-0001", "running"));
},
},
idempotencyKey: () => "seedance-web-route-0001",
},
);
expect(result).toEqual({
mode: "video",
video: { dataUrl: "data:video/mp4;base64,AAAA", mimeType: "video/mp4", bytes: 3 },
});
expect(requests[0]).toEqual({
path: "/dreamina/generate",
method: "POST",
body: JSON.stringify({
idempotencyKey: "seedance-web-route-0001",
clientOperationId: "seedance-web-route-0001",
operation: "text-to-video",
model: "seedance2.0mini",
prompt: "A short test clip",
settings: { aspect: "16:9", resolution: "720", duration: 4 },
references: [],
context: { scope: "scoped" },
}),
});
expect(requests[1]).toEqual({
path: "/dreamina/generate/wait",
method: "POST",
body: JSON.stringify({ idempotencyKey: "seedance-web-route-0001", mode: "video" }),
});
expect(requests.some((request) => request.path.includes("/tasks"))).toBe(false);
});
test("Dreamina signed generation boundary serializes typed multimodal references and scoped task identity", async () => {
const requests: Array<{ path: string; body: string }> = [];
await runLocalDreaminaGenerationTask(
{
model: "local:dreamina-cli:seedance2.5",
mode: "video",
prompt: "Multimodal signed fixture",
settings: { aspect: "16:9", resolution: "720", duration: 4 },
idempotencyKey: "retry-client-operation-0001",
clientOperationId: "retry-client-operation-0001",
context: {
scope: "scoped",
projectId: "project-signed-0001",
nodeId: "node-signed-0001",
retryOf: "dreamina:prior-signed-0001",
attemptGroupId: "dreamina:prior-signed-0001",
},
references: [
{ kind: "image", mimeType: "image/png", bytes: new Uint8Array([0x89, 0x50, 0x4e, 0x47]), metadata: { name: "image.png", width: 16, height: 9 } },
{ kind: "video", mimeType: "video/mp4", bytes: new Uint8Array([0, 0, 0, 16, 0x66, 0x74, 0x79, 0x70]), metadata: { name: "video.mp4", durationMs: 1000 } },
{ kind: "audio", mimeType: "audio/mpeg", bytes: new Uint8Array([0x49, 0x44, 0x33, 4]), metadata: { name: "audio.mp3", durationMs: 1000 } },
],
} as Parameters<typeof runLocalDreaminaGenerationTask>[0],
{
client: {
async connect() {
return connectedFixture();
},
async request(path, init) {
requests.push({ path, body: String(init?.body) });
return jsonResponse(200, path.endsWith("/wait") ? taskEnvelope("retry-client-operation-0001", "succeeded") : taskEnvelope("retry-client-operation-0001", "running"));
},
},
},
);
expect(JSON.parse(requests[0]!.body)).toEqual({
idempotencyKey: "retry-client-operation-0001",
clientOperationId: "retry-client-operation-0001",
context: {
scope: "scoped",
projectId: "project-signed-0001",
nodeId: "node-signed-0001",
retryOf: "dreamina:prior-signed-0001",
attemptGroupId: "dreamina:prior-signed-0001",
},
operation: "reference-to-video",
model: "seedance2.5",
prompt: "Multimodal signed fixture",
settings: { aspect: "16:9", resolution: "720", duration: 4 },
references: [
{ kind: "image", mimeType: "image/png", contentBase64: "iVBORw==", metadata: { name: "image.png", width: 16, height: 9 } },
{ kind: "video", mimeType: "video/mp4", contentBase64: "AAAAEGZ0eXA=", metadata: { name: "video.mp4", durationMs: 1000 } },
{ kind: "audio", mimeType: "audio/mpeg", contentBase64: "SUQzBA==", metadata: { name: "audio.mp3", durationMs: 1000 } },
],
});
});
test("Dreamina Seedance 2.5 accepts 30 image, 10 video, and 10 audio references while every adapter limit stays strict", async () => {
const makeReference = (kind: "image" | "video" | "audio", index: number) => ({
kind,
mimeType: kind === "image" ? "image/png" : kind === "video" ? "video/mp4" : "audio/mpeg",
bytes: new Uint8Array([index + 1]),
metadata: { name: `${kind}-${index + 1}` },
});
const references = [...Array.from({ length: 30 }, (_, index) => makeReference("image", index)), ...Array.from({ length: 10 }, (_, index) => makeReference("video", index)), ...Array.from({ length: 10 }, (_, index) => makeReference("audio", index))];
let requests = 0;
await runLocalDreaminaGenerationTask(
{
model: "local:dreamina-cli:seedance2.5",
mode: "video",
prompt: "Full multimodal boundary",
settings: { aspect: "16:9", resolution: "720", duration: 4 },
references,
} as Parameters<typeof runLocalDreaminaGenerationTask>[0],
{
client: {
async connect() {
return connectedFixture();
},
async request(path) {
requests += 1;
return jsonResponse(200, path.endsWith("/wait") ? taskEnvelope("seedance-boundary-0001", "succeeded") : taskEnvelope("seedance-boundary-0001", "running"));
},
},
idempotencyKey: () => "seedance-boundary-0001",
},
);
expect(requests).toBe(2);
const invalidCases = [
{
name: "Seedance 2.5 image limit",
model: "local:dreamina-cli:seedance2.5",
references: Array.from({ length: 31 }, (_, index) => makeReference("image", index)),
},
{
name: "Seedance 2.5 video limit",
model: "local:dreamina-cli:seedance2.5",
references: Array.from({ length: 11 }, (_, index) => makeReference("video", index)),
},
{
name: "Seedance 2.5 audio limit",
model: "local:dreamina-cli:seedance2.5",
references: Array.from({ length: 11 }, (_, index) => makeReference("audio", index)),
},
{
name: "other model total limit",
model: "local:dreamina-cli:seedance2.0mini",
references: Array.from({ length: 31 }, (_, index) => makeReference("image", index)),
},
];
for (const fixture of invalidCases) {
let invalidRequests = 0;
await expect(
runLocalDreaminaGenerationTask(
{
model: fixture.model,
mode: "video",
prompt: fixture.name,
settings: { aspect: "16:9", resolution: "720", duration: 4 },
references: fixture.references,
} as Parameters<typeof runLocalDreaminaGenerationTask>[0],
{
client: {
async connect() {
return connectedFixture();
},
async request() {
invalidRequests += 1;
throw new Error("must not request");
},
},
idempotencyKey: () => "seedance-invalid-0001",
},
),
).rejects.toMatchObject({ status: 400 });
expect(invalidRequests).toBe(0);
}
});
test("Dreamina local generation blocks Seedance mini below four seconds before a Runtime request", async () => {
let requests = 0;
await expect(
runLocalDreaminaGenerationTask(
{
model: "local:dreamina-cli:seedance2.0mini",
mode: "video",
prompt: "A short test clip",
settings: { resolution: "720", duration: 3 },
references: [],
},
{
client: {
async connect() {
return connectedFixture();
},
async request() {
requests += 1;
throw new Error("must not request");
},
},
idempotencyKey: () => "seedance-web-route-0002",
},
),
).rejects.toMatchObject({ code: "local_generation_model_unavailable" });
expect(requests).toBe(0);
});
test("Dreamina generation asks to reconnect without exposing origin authorization terminology", async () => {
let requests = 0;
await expect(
runLocalDreaminaGenerationTask(
{
model: "local:dreamina-cli:seedance2.0mini",
mode: "video",
prompt: "A short test clip",
settings: { resolution: "720", duration: 4 },
references: [],
},
{
client: {
async connect() {
return { state: "origin_not_trusted" as const, runtimeVersion: 2 };
},
async request() {
requests += 1;
throw new Error("must not submit");
},
},
idempotencyKey: () => "seedance-reconnect-copy-0001",
},
),
).rejects.toMatchObject({
code: "origin_not_trusted",
message: "本机连接需要重新建立",
status: 403,
});
expect(requests).toBe(0);
});
test("Dreamina generation never replays a paid POST after a 401 or 403 session failure", async () => {
for (const status of [401, 403]) {
let requests = 0;
await expect(
runLocalDreaminaGenerationTask(
{
model: "local:dreamina-cli:seedance2.0mini",
mode: "video",
prompt: "A short test clip",
settings: { resolution: "720", duration: 4 },
references: [],
},
{
client: {
async connect() {
return connectedFixture();
},
async request() {
requests += 1;
return new Response(JSON.stringify({ ok: false, code: "scope_denied" }), { status });
},
},
idempotencyKey: () => `seedance-no-replay-${status}`,
},
),
).rejects.toMatchObject({ code: "local_generation_request_failed", status });
expect(requests).toBe(1);
}
});
test("Dreamina query-only recovery reconnects once after an expired session without replaying generate", async () => {
let connects = 0;
let queries = 0;
let revocations = 0;
const task = await queryLocalDreaminaGenerationTask("dreamina-query-reconnect-0001", "video", {
client: {
async connect() {
connects += 1;
return connectedFixture();
},
revokeLocalSession() {
revocations += 1;
},
async request(path) {
expect(path).toBe("/dreamina/generate/query");
queries += 1;
return queries === 1 ? jsonResponse(403, { ok: false, code: "scope_denied" }) : jsonResponse(200, taskEnvelope("dreamina-query-reconnect-0001", "succeeded"));
},
},
});
expect(task.status).toBe("succeeded");
expect({ connects, queries, revocations }).toEqual({ connects: 2, queries: 2, revocations: 1 });
});
test("Dreamina accepted background action preserves provider reconciliation and sends no generation replay", async () => {
const requests: Array<{ path: string; method: string }> = [];
const task = await cancelLocalDreaminaGenerationTask("dreamina-local-stop-only-0001", {
client: {
async connect() {
return connectedFixture();
},
async request(path, init) {
requests.push({ path, method: String(init?.method) });
return jsonResponse(200, taskEnvelope("dreamina-local-stop-only-0001", "running"));
},
},
});
expect(task).toMatchObject({
status: "running",
receiptRecorded: true,
});
expect(task.errorCode).toBeUndefined();
expect(requests).toEqual([{ path: "/dreamina/generate/cancel", method: "POST" }]);
});
test("Dreamina generation establishes a signed session before exactly one paid POST", async () => {
const runtime = signedRuntimeFixture();
const client = new LocalRuntimeSessionClient({
origin: trustedOrigin,
keyStore: memoryKeyStore(),
fetch: runtime.fetch,
now: () => runtime.now,
});
const result = await runLocalDreaminaGenerationTask(videoInput(), {
client,
idempotencyKey: () => "seedance-session-preflight-0001",
});
expect(result.mode).toBe("video");
expect(runtime.requests.map((request) => new URL(request.url).pathname)).toEqual(["/runtime/info", "/runtime/session/challenge", "/runtime/session/exchange", "/dreamina/generate", "/dreamina/generate/wait"]);
expect(runtime.requests.filter((request) => request.path === "/dreamina/generate")).toHaveLength(1);
});
test("Dreamina generation cancellation during signed session preflight prevents the paid POST", async () => {
const runtime = signedRuntimeFixture({ blockInfoUntilAbort: true });
const client = new LocalRuntimeSessionClient({
origin: trustedOrigin,
keyStore: memoryKeyStore(),
fetch: runtime.fetch,
now: () => runtime.now,
});
const controller = new AbortController();
const pending = runLocalDreaminaGenerationTask(
videoInput(),
{
client,
idempotencyKey: () => "seedance-session-preflight-0002",
},
controller.signal,
);
controller.abort();
await expect(pending).rejects.toMatchObject({ name: "AbortError" });
expect(runtime.requests.filter((request) => request.path === "/dreamina/generate")).toHaveLength(0);
});
test("Dreamina generation cancellation during the paid POST propagates without retry", async () => {
const runtime = signedRuntimeFixture({ blockGenerateUntilAbort: true });
const client = new LocalRuntimeSessionClient({
origin: trustedOrigin,
keyStore: memoryKeyStore(),
fetch: runtime.fetch,
now: () => runtime.now,
});
await client.connect();
runtime.requests.length = 0;
const controller = new AbortController();
const pending = runLocalDreaminaGenerationTask(
videoInput(),
{
client,
idempotencyKey: () => "seedance-session-preflight-0003",
},
controller.signal,
);
await Promise.resolve();
controller.abort();
await expect(pending).rejects.toMatchObject({ code: "dreamina_submission_unknown" });
expect(runtime.requests.filter((request) => request.path === "/dreamina/generate")).toHaveLength(1);
});
test("detaching a Dreamina wait never sends a provider cancellation", async () => {
const controller = new AbortController();
const requests: string[] = [];
let waitStarted!: () => void;
const waiting = new Promise<void>((resolve) => {
waitStarted = resolve;
});
const pending = runLocalDreaminaGenerationTask(
videoInput(),
{
client: {
async connect() {
return connectedFixture();
},
async request(path, init) {
requests.push(path);
if (path === "/dreamina/generate") return jsonResponse(200, taskEnvelope("dreamina-detached-wait-0001", "running"));
if (path === "/dreamina/generate/wait") {
waitStarted();
return await new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")), { once: true });
});
}
throw new Error(`unexpected mutation: ${path}`);
},
},
idempotencyKey: () => "dreamina-detached-wait-0001",
},
controller.signal,
);
await waiting;
controller.abort();
await expect(pending).rejects.toMatchObject({ name: "AbortError" });
expect(requests).toEqual(["/dreamina/generate", "/dreamina/generate/wait"]);
});
test("Dreamina task listing uses one signed read and projects no request content", async () => {
const module = await import("../src/services/local-dreamina-generation");
expect(typeof module.listLocalDreaminaGenerationTasks).toBe("function");
const requests: Array<{ path: string; method: string }> = [];
const tasks = await module.listLocalDreaminaGenerationTasks({
client: {
async connect() {
return connectedFixture();
},
async request(path, init) {
requests.push({ path, method: String(init?.method) });
return jsonResponse(200, {
ok: true,
result: [taskEnvelope("dreamina-list-task-0001", "running").result],
});
},
},
});
expect(requests).toEqual([{ path: "/dreamina/generate/tasks?limit=100", method: "GET" }]);
expect(tasks).toHaveLength(1);
expect(tasks[0]).toMatchObject({ id: "dreamina-list-task-0001", status: "running", receiptRecorded: true });
expect(JSON.stringify(tasks)).not.toMatch(/prompt|submitId|receipt-/i);
});
test("Dreamina task listing follows cursor pages", async () => {
const module = await import("../src/services/local-dreamina-generation");
const paths: string[] = [];
const fixture = taskEnvelope("dreamina-page-task-0001", "running").result as Record<string, unknown>;
const tasks = await module.listLocalDreaminaGenerationTasks({
client: {
async connect() {
return connectedFixture();
},
async request(path) {
paths.push(path);
return paths.length === 1
? jsonResponse(200, { ok: true, result: { tasks: [{ ...fixture, id: "dreamina-page-task-0001" }], nextCursor: "cursor_page_2" } })
: jsonResponse(200, { ok: true, result: { tasks: [{ ...fixture, id: "dreamina-page-task-0002" }] } });
},
},
});
expect(paths).toEqual(["/dreamina/generate/tasks?limit=100", "/dreamina/generate/tasks?limit=100&cursor=cursor_page_2"]);
expect(tasks.map((task) => task.id)).toEqual(["dreamina-page-task-0001", "dreamina-page-task-0002"]);
});
test("Dreamina task listing reconnects once after an expired signed read", async () => {
const module = await import("../src/services/local-dreamina-generation");
let reads = 0;
let revocations = 0;
const tasks = await module.listLocalDreaminaGenerationTasks({
client: {
async connect() {
return connectedFixture();
},
revokeLocalSession() {
revocations += 1;
},
async request() {
reads += 1;
return reads === 1 ? jsonResponse(403, { ok: false, code: "session_invalid" }) : jsonResponse(200, { ok: true, result: [taskEnvelope("dreamina-list-task-0002", "running").result] });
},
},
});
expect(reads).toBe(2);
expect(revocations).toBe(1);
expect(tasks.map((task) => task.id)).toEqual(["dreamina-list-task-0002"]);
});
function videoInput() {
return {
model: "local:dreamina-cli:seedance2.0mini" as const,
mode: "video" as const,
prompt: "A short test clip",
settings: { resolution: "720", duration: 4 },
references: [],
};
}
function memoryKeyStore(): RuntimeBrowserKeyStore & { record?: RuntimeBrowserKeyRecord } {
return {
record: undefined,
async load() {
return this.record;
},
async save(record) {
this.record = record;
},
async clear() {
this.record = undefined;
},
};
}
function signedRuntimeFixture(options: { blockInfoUntilAbort?: boolean; blockGenerateUntilAbort?: boolean } = {}) {
const requests: Array<{ url: string; path: string; method: string }> = [];
const fixture = {
now: Date.parse("2026-08-11T00:00:00.000Z"),
keyId: "",
requests,
fetch: async (input: string | URL | Request, init: RequestInit = {}): Promise<Response> => {
const url = String(input);
const path = new URL(url).pathname;
requests.push({ url, path, method: String(init.method ?? "GET") });
if (path === "/runtime/info" && options.blockInfoUntilAbort) return await rejectOnAbort(init.signal);
if (path === "/runtime/info") {
return jsonResponse(200, {
runtime: "framefield-local-runtime",
apiVersion: 2,
protocolVersion: "framefield-runtime-session-v1",
runtimeInstanceId: "dreamina-generation-fixture",
originTrusted: true,
});
}
if (path === "/runtime/session/challenge") {
const body = JSON.parse(String(init.body)) as { keyId?: string; publicKeyJwk?: JsonWebKey };
fixture.keyId = body.keyId ?? (await keyIdForJwk(body.publicKeyJwk!));
return jsonResponse(200, {
state: "challenge",
challengeId: "challenge-generation-000001",
nonce: "bm9uY2UtZ2VuZXJhdGlvbi0wMDAwMDAwMDAwMDAwMDAwMDAw",
runtimeInstanceId: "dreamina-generation-fixture",
expiresAt: new Date(fixture.now + 60_000).toISOString(),
keyId: fixture.keyId,
});
}
if (path === "/runtime/session/exchange") {
return jsonResponse(200, {
sessionId: "session-generation-0000000001",
keyId: fixture.keyId,
scopes: ["runtime:status", "dreamina:models", "dreamina:generate"],
expiresAt: new Date(fixture.now + 10 * 60_000).toISOString(),
});
}
if (path === "/dreamina/generate" && options.blockGenerateUntilAbort) return await rejectOnAbort(init.signal);
if (path === "/dreamina/generate") {
return jsonResponse(200, taskEnvelope("seedance-session-preflight-0001", "running"));
}
if (path === "/dreamina/generate/wait") {
return jsonResponse(200, taskEnvelope("seedance-session-preflight-0001", "succeeded"));
}
return jsonResponse(404, { ok: false });
},
};
return fixture;
}
async function rejectOnAbort(signal: AbortSignal | null | undefined): Promise<Response> {
if (!signal) throw new Error("expected AbortSignal");
if (signal.aborted) throw signal.reason;
return await new Promise<Response>((_resolve, reject) => {
signal.addEventListener("abort", () => reject(signal.reason), { once: true });
});
}
function jsonResponse(status: number, value: unknown) {
return new Response(JSON.stringify(value), {
status,
headers: { "content-type": "application/json", "cache-control": "no-store" },
});
}
async function keyIdForJwk(jwk: JsonWebKey) {
const canonical = JSON.stringify({ crv: "P-256", kty: "EC", x: jwk.x, y: jwk.y });
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical)));
let binary = "";
for (const byte of digest) binary += String.fromCharCode(byte);
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}
function connectedFixture() {
return {
state: "connected" as const,
runtimeVersion: 2,
session: {
sessionId: "session-test-000000000001",
keyId: "browser-key-test",
scopes: ["dreamina:generate"],
expiresAt: "2099-01-01T00:00:00.000Z",
},
};
}
function taskEnvelope(id: string, status: "running" | "succeeded" | "failed", overrides: Record<string, unknown> = {}) {
return {
ok: true,
result: {
id,
provider: "dreamina-cli",
mode: "video",
operation: "text2video",
model: "seedance2.0mini",
status,
stage: status === "succeeded" ? "succeeded" : status === "failed" ? "failed" : "submitted",
progress: status === "succeeded" ? 100 : 10,
receiptRecorded: true,
createdAt: "2026-08-12T00:00:00.000Z",
updatedAt: "2026-08-12T00:00:00.000Z",
...(status === "succeeded"
? {
result: {
mode: "video",
video: { dataUrl: "data:video/mp4;base64,AAAA", mimeType: "video/mp4", bytes: 3 },
},
}
: {}),
...overrides,
},
};
}

View File

@@ -0,0 +1,240 @@
import { expect, test } from "bun:test";
import { getDreaminaModelCatalog } from "../src/services/local-dreamina-model-catalog";
import { LocalRuntimeClientError } from "../src/services/local-runtime-session";
test("Dreamina model discovery reads only the signed Runtime catalog", async () => {
const requests: string[] = [];
const catalog = await getDreaminaModelCatalog({
async request(path) {
requests.push(path);
return new Response(
JSON.stringify({
ok: true,
provider: "dreamina-cli",
accountBinding: "a".repeat(64),
sessionEpoch: 3,
models: [
{
provider: "dreamina-cli",
id: "seedance2.0mini",
displayName: "seedance2.0mini",
modality: "video",
operations: ["text-to-video", "image-to-video", "reference-to-video"],
adapterSupported: true,
accountEntitlement: "unknown",
currentlyObservedAvailable: "unknown",
settings: { aliases: [], aspects: ["16:9"], maxReferenceImages: 9, minDuration: 4, maxDuration: 15, tiers: ["720p"] },
source: "runtime-execution-contract",
},
],
}),
{ status: 200, headers: { "content-type": "application/json" } },
);
},
});
expect(catalog).toHaveLength(1);
expect(catalog[0]).toMatchObject({
adapterSupported: true,
accountEntitlement: "unknown",
currentlyObservedAvailable: "unknown",
});
expect(catalog[0]?.settings.minDuration).toBe(4);
expect(requests).toEqual(["/dreamina/models"]);
});
test("Dreamina model discovery returns its authenticated cache scope with one signed GET", async () => {
const { getDreaminaModelCatalogSnapshot } = await import("../src/services/local-dreamina-model-catalog");
let requests = 0;
const snapshot = await getDreaminaModelCatalogSnapshot({
async request() {
requests += 1;
return catalogResponse();
},
});
expect(requests).toBe(1);
expect(snapshot).toMatchObject({ accountBinding: "a".repeat(64), sessionEpoch: 3 });
expect(snapshot.models).toHaveLength(1);
});
test("Dreamina model discovery treats a signed scope denial as an error, never a successful empty catalog", async () => {
await expect(
getDreaminaModelCatalog({
async request() {
return new Response(
JSON.stringify({
ok: false,
code: "scope_denied",
message: "public failure",
}),
{ status: 403, headers: { "content-type": "application/json" } },
);
},
}),
).rejects.toThrow("Dreamina model catalog is unavailable");
});
test("Dreamina catalog recovery replaces an obsolete 401/403 session and retries only the safe GET once", async () => {
const module = await import("../src/services/local-dreamina-model-catalog").catch(() => ({}));
const recover = (
module as {
getDreaminaModelCatalogWithSessionRecovery?: (client: { request(path: string, init?: RequestInit): Promise<Response>; connect(signal?: AbortSignal): Promise<{ state: string }>; revokeLocalSession(): void }) => Promise<unknown[]>;
}
).getDreaminaModelCatalogWithSessionRecovery;
expect(typeof recover).toBe("function");
if (!recover) return;
for (const rejectedStatus of [401, 403]) {
const requests: Array<{ path: string; method: string }> = [];
let revoked = 0;
let connected = 0;
const models = await recover({
async request(path, init) {
requests.push({ path, method: String(init?.method) });
if (requests.length === 1) {
return new Response(JSON.stringify({ ok: false }), { status: rejectedStatus });
}
return catalogResponse();
},
async connect() {
connected += 1;
return { state: "connected" };
},
revokeLocalSession() {
revoked += 1;
},
});
expect(models).toHaveLength(1);
expect(requests).toEqual([
{ path: "/dreamina/models", method: "GET" },
{ path: "/dreamina/models", method: "GET" },
]);
expect({ revoked, connected }).toEqual({ revoked: 1, connected: 1 });
}
});
test("Dreamina catalog snapshot recovery preserves the authenticated cache scope after one safe reconnect", async () => {
const module = await import("../src/services/local-dreamina-model-catalog").catch(() => ({}));
const recover = (
module as {
getDreaminaModelCatalogSnapshotWithSessionRecovery?: (client: {
request(path: string, init?: RequestInit): Promise<Response>;
connect(signal?: AbortSignal): Promise<{ state: string }>;
revokeLocalSession(): void;
}) => Promise<{ accountBinding: string; sessionEpoch: number; models: unknown[] }>;
}
).getDreaminaModelCatalogSnapshotWithSessionRecovery;
expect(typeof recover).toBe("function");
if (!recover) return;
let requests = 0;
let revoked = 0;
let connected = 0;
const snapshot = await recover({
async request() {
requests += 1;
if (requests === 1) return new Response(JSON.stringify({ ok: false }), { status: 401 });
return catalogResponse();
},
async connect() {
connected += 1;
return { state: "connected" };
},
revokeLocalSession() {
revoked += 1;
},
});
expect(snapshot).toMatchObject({ accountBinding: "a".repeat(64), sessionEpoch: 3 });
expect(snapshot.models).toHaveLength(1);
expect({ requests, revoked, connected }).toEqual({ requests: 2, revoked: 1, connected: 1 });
});
test("Dreamina catalog recovery reconnects once when the browser rejects an expired session before fetch", async () => {
const { getDreaminaModelCatalogWithSessionRecovery } = await import("../src/services/local-dreamina-model-catalog");
let requests = 0;
let revoked = 0;
let connected = 0;
const models = await getDreaminaModelCatalogWithSessionRecovery({
async request() {
requests += 1;
if (requests === 1) throw new LocalRuntimeClientError("session_required", "public failure", 401);
return catalogResponse();
},
async connect() {
connected += 1;
return { state: "connected" as const };
},
revokeLocalSession() {
revoked += 1;
},
});
expect(models).toHaveLength(1);
expect({ requests, revoked, connected }).toEqual({ requests: 2, revoked: 1, connected: 1 });
});
test("effective config projects an asynchronously arriving Dreamina catalog without persisting the local channel", async () => {
const module = await import("../src/stores/use-config-store").catch(() => ({}));
const project = (
module as {
effectiveConfigWithDreamina?: (
config: typeof import("../src/stores/use-config-store").defaultConfig,
state: "idle" | "loading" | "ready" | "error",
models: Array<Record<string, unknown>>,
) => typeof import("../src/stores/use-config-store").defaultConfig;
}
).effectiveConfigWithDreamina;
expect(typeof project).toBe("function");
if (!project) return;
const { defaultConfig } = await import("../src/stores/use-config-store");
const pending = project(defaultConfig, "loading", []);
const ready = project(defaultConfig, "ready", [
{
provider: "dreamina-cli",
id: "seedance2.0mini",
displayName: "seedance2.0mini",
modality: "video",
operations: ["text-to-video", "image-to-video", "reference-to-video"],
adapterSupported: true,
accountEntitlement: "unknown",
currentlyObservedAvailable: "unknown",
settings: { aliases: [], aspects: ["16:9"], maxReferenceImages: 9, minDuration: 4, maxDuration: 15, tiers: ["720p"] },
source: "runtime-execution-contract",
},
]);
expect(pending.videoModels).not.toContain("local:dreamina-cli:seedance2.0mini");
expect(ready.videoModels).toContain("local:dreamina-cli:seedance2.0mini");
expect(defaultConfig.channels.some((channel) => channel.id === "local:dreamina-cli")).toBe(false);
});
function catalogResponse() {
return new Response(
JSON.stringify({
ok: true,
provider: "dreamina-cli",
accountBinding: "a".repeat(64),
sessionEpoch: 3,
models: [
{
provider: "dreamina-cli",
id: "seedance2.0mini",
displayName: "seedance2.0mini",
modality: "video",
operations: ["text-to-video", "image-to-video", "reference-to-video"],
adapterSupported: true,
accountEntitlement: "unknown",
currentlyObservedAvailable: "unknown",
settings: { aliases: [], aspects: ["16:9"], maxReferenceImages: 9, minDuration: 4, maxDuration: 15, tiers: ["720p"] },
source: "runtime-execution-contract",
},
],
}),
{ status: 200, headers: { "content-type": "application/json" } },
);
}

View File

@@ -0,0 +1,60 @@
import { expect, test } from "bun:test";
const legacyToken = "legacy-master-token-canary";
test("legacy query and storage secrets are removed before application modules load", async () => {
const module = await import("../src/services/local-runtime-bootstrap").catch(() => ({}));
const runBootstrap = (
module as {
runLocalRuntimeBootstrap?: (environment: BootstrapEnvironment, loadApplication: (state: { legacyDeepLinkRejected: boolean }) => void) => { legacyDeepLinkRejected: boolean };
}
).runLocalRuntimeBootstrap;
expect(typeof runBootstrap).toBe("function");
if (!runBootstrap) return;
let href = `http://127.0.0.1:3001/canvas/demo?keep=1&agentUrl=http%3A%2F%2F127.0.0.1%3A17371&agentToken=${legacyToken}#timeline`;
const storage = new Map([
["canvas-agent-url", "http://127.0.0.1:17371"],
["canvas-agent-token", legacyToken],
["unrelated", "keep"],
]);
let rendered = false;
const state = runBootstrap(
{
get href() {
return href;
},
replaceUrl(url) {
href = new URL(url, href).toString();
},
removeStorageItem(key) {
storage.delete(key);
},
},
(bootstrapState) => {
rendered = true;
expect(href).toBe("http://127.0.0.1:3001/canvas/demo?keep=1#timeline");
expect(storage.has("canvas-agent-url")).toBe(false);
expect(storage.has("canvas-agent-token")).toBe(false);
expect(storage.get("unrelated")).toBe("keep");
expect(JSON.stringify(bootstrapState)).not.toContain(legacyToken);
},
);
expect(rendered).toBe(true);
expect(state).toEqual({ legacyDeepLinkRejected: true });
expect(href).not.toContain(legacyToken);
const readState = (
module as {
readLocalRuntimeBootstrapState?: () => { legacyDeepLinkRejected: boolean };
}
).readLocalRuntimeBootstrapState;
expect(typeof readState).toBe("function");
expect(readState?.()).toEqual({ legacyDeepLinkRejected: true });
});
type BootstrapEnvironment = {
readonly href: string;
replaceUrl(url: string): void;
removeStorageItem(key: string): void;
};

View File

@@ -0,0 +1,357 @@
import { expect, test } from "bun:test";
import fs from "node:fs/promises";
import { LocalRuntimeClientError } from "../src/services/local-runtime-session";
const privateCanary = "runtime-private-canary";
test("Runtime status projects only exact public module descriptors", async () => {
const module = await import("../src/services/local-runtime").catch(() => ({}));
const readStatus = (
module as {
readLocalRuntimeStatus?: (client: RuntimeTransport) => Promise<unknown>;
}
).readLocalRuntimeStatus;
expect(typeof readStatus).toBe("function");
if (!readStatus) return;
const requests: Array<{ path: string; init?: RequestInit }> = [];
const client: RuntimeTransport = {
async request(path, init) {
requests.push({ path, init });
return jsonResponse({
ok: true,
runtime: { id: "framefield-local-runtime", version: "0.1.0", apiVersion: 2 },
modules: [
{
id: "canvas-agent",
displayName: "Canvas Agent",
apiVersion: 1,
scopes: ["canvas:connect"],
},
{
id: "dreamina",
displayName: "Dreamina CLI",
apiVersion: 1,
scopes: ["dreamina:status", "dreamina:login", "dreamina:logout", "dreamina:models", "dreamina:generate"],
},
],
});
},
};
const status = await readStatus(client);
expect(requests).toHaveLength(1);
expect(requests[0]).toMatchObject({ path: "/runtime/status", init: { method: "GET" } });
expect(status).toEqual({
runtime: { id: "framefield-local-runtime", version: "0.1.0", apiVersion: 2 },
modules: [
{ id: "canvas-agent", displayName: "Canvas Agent", apiVersion: 1, scopes: ["canvas:connect"] },
{ id: "dreamina", displayName: "Dreamina CLI", apiVersion: 1, scopes: ["dreamina:status", "dreamina:login", "dreamina:logout", "dreamina:models", "dreamina:generate"] },
],
});
expect(JSON.stringify(status)).not.toContain("ownerId");
expect(JSON.stringify(status)).not.toContain("token");
});
test("Runtime status rejects sensitive or contradictory response fields without echoing them", async () => {
const module = await import("../src/services/local-runtime").catch(() => ({}));
const readStatus = (
module as {
readLocalRuntimeStatus?: (client: RuntimeTransport) => Promise<unknown>;
}
).readLocalRuntimeStatus;
expect(typeof readStatus).toBe("function");
if (!readStatus) return;
const fixtures = [
{
ok: true,
runtime: { id: "framefield-local-runtime", version: "0.1.0", apiVersion: 2, ownerId: privateCanary },
modules: [],
},
{
ok: true,
runtime: { id: "framefield-local-runtime", version: "0.1.0", apiVersion: 2 },
modules: [
{ id: "dreamina", displayName: "Dreamina CLI", apiVersion: 1, scopes: ["dreamina:status"] },
{ id: "dreamina", displayName: "Dreamina CLI", apiVersion: 1, scopes: ["dreamina:status"] },
],
},
{
ok: false,
code: "private_error",
message: privateCanary,
},
];
for (const [index, body] of fixtures.entries()) {
let caught: unknown;
try {
await readStatus({ request: async () => jsonResponse(body, index === 2 ? 500 : 200) });
} catch (error) {
caught = error;
}
expect(caught).toBeInstanceOf(Error);
expect(String(caught)).not.toContain(privateCanary);
}
});
test("Runtime store automatically connects and reads descriptors without storing endpoint or bearer data", async () => {
const module = await import("../src/stores/use-local-runtime-store").catch(() => ({}));
const createStore = (
module as {
createLocalRuntimeStore?: (dependencies: unknown) => RuntimeStore;
}
).createLocalRuntimeStore;
expect(typeof createStore).toBe("function");
if (!createStore) return;
let statusRequests = 0;
const client = {
async connect() {
return {
state: "connected" as const,
runtimeVersion: 2,
session: {
sessionId: "public-session-handle",
keyId: "k".repeat(43),
scopes: ["runtime:status"],
expiresAt: "2026-08-10T00:10:00.000Z",
},
};
},
async request() {
statusRequests++;
return runtimeStatusResponse();
},
};
const store = createStore({ client, timeoutMs: 1_000 });
await store.getState().connect();
const connected = store.getState();
expect(connected.connection).toBe("connected");
expect(connected.modules.map((item) => item.id)).toEqual(["canvas-agent", "dreamina"]);
expect(statusRequests).toBe(1);
expect("endpoint" in connected).toBe(false);
expect("token" in connected).toBe(false);
expect("session" in connected).toBe(false);
});
test("Runtime store replaces a stale signed session after Runtime restart before publishing connected", async () => {
const module = await import("../src/stores/use-local-runtime-store");
let connects = 0;
let revocations = 0;
let statusReads = 0;
const store = module.createLocalRuntimeStore({
client: {
async connect() {
connects += 1;
return {
state: "connected" as const,
runtimeVersion: 2,
session: { sessionId: `session-${connects}`, keyId: "k".repeat(43), scopes: ["runtime:status"], expiresAt: "2099-01-01T00:00:00.000Z" },
};
},
revokeLocalSession() {
revocations += 1;
},
async request() {
statusReads += 1;
if (statusReads === 1) throw new LocalRuntimeClientError("session_required", "stale", 401);
return runtimeStatusResponse();
},
},
timeoutMs: 1_000,
});
await store.getState().connect();
expect({ connects, revocations, statusReads }).toEqual({ connects: 2, revocations: 1, statusReads: 2 });
expect(store.getState()).toMatchObject({ connection: "connected", connecting: false, error: "" });
expect(store.getState().modules.map((item) => item.id)).toEqual(["canvas-agent", "dreamina"]);
});
test("Runtime store exposes an actionable reconnect message without origin or authorization terminology", async () => {
const module = await import("../src/stores/use-local-runtime-store");
const store = module.createLocalRuntimeStore({
client: {
connect: async () => ({ state: "origin_not_trusted" as const, runtimeVersion: 2 }),
request: async () => {
throw new Error("must not read Runtime status");
},
},
timeoutMs: 1_000,
});
await store.getState().connect();
expect(store.getState()).toMatchObject({
connection: "origin_not_trusted",
connecting: false,
error: "本机连接需要重新建立",
});
expect(store.getState().error).not.toMatch(/来源|Origin|授权|Runtime/i);
});
test("Runtime store ignores an older connection result after a newer request succeeds", async () => {
const module = await import("../src/stores/use-local-runtime-store").catch(() => ({}));
const createStore = (
module as {
createLocalRuntimeStore?: (dependencies: unknown) => RuntimeStore;
}
).createLocalRuntimeStore;
expect(typeof createStore).toBe("function");
if (!createStore) return;
let resolveFirst!: (value: { state: "origin_not_trusted"; runtimeVersion: number }) => void;
const first = new Promise<{ state: "origin_not_trusted"; runtimeVersion: number }>((resolve) => {
resolveFirst = resolve;
});
let calls = 0;
const store = createStore({
client: {
connect: async () =>
calls++ === 0
? await first
: {
state: "connected" as const,
runtimeVersion: 2,
session: {
sessionId: "public-session-handle",
keyId: "k".repeat(43),
scopes: ["runtime:status"],
expiresAt: "2026-08-10T00:10:00.000Z",
},
},
request: async () => runtimeStatusResponse(),
},
timeoutMs: 1_000,
});
const stale = store.getState().connect();
await Promise.resolve();
await store.getState().connect();
resolveFirst({ state: "origin_not_trusted", runtimeVersion: 2 });
await stale;
expect(store.getState().connection).toBe("connected");
expect(store.getState().runtime?.id).toBe("framefield-local-runtime");
});
test("Runtime store does not dispatch an already-cancelled connection", async () => {
const module = await import("../src/stores/use-local-runtime-store");
let connectCalls = 0;
const store = module.createLocalRuntimeStore({
client: {
connect: async () => {
connectCalls++;
throw new DOMException("aborted", "AbortError");
},
request: async () => runtimeStatusResponse(),
},
timeoutMs: 1_000,
});
const controller = new AbortController();
controller.abort();
await store.getState().connect(controller.signal);
expect(connectCalls).toBe(0);
expect(store.getState()).toMatchObject({ connection: "idle", connecting: false, error: "" });
});
test("Runtime store exposes a bounded timeout instead of hanging", async () => {
const module = await import("../src/stores/use-local-runtime-store");
const store = module.createLocalRuntimeStore({
client: {
connect: async (signal?: AbortSignal) =>
await new Promise((_resolve, reject) => {
signal?.addEventListener("abort", () => reject(new DOMException("aborted", "AbortError")), { once: true });
}),
request: async () => runtimeStatusResponse(),
},
timeoutMs: 5,
});
await store.getState().connect();
expect(store.getState()).toMatchObject({
connection: "unreachable",
connecting: false,
error: "本机服务连接超时",
});
});
test("the application root starts local Runtime discovery before provider catalogs bootstrap", async () => {
const source = await fs.readFile(new URL("../src/components/layout/client-root-init.tsx", import.meta.url), "utf8");
expect(source).toContain("useLocalRuntimeBootstrap");
expect(source.indexOf("useLocalRuntimeBootstrap()")).toBeLessThan(source.indexOf("useLocalDreaminaModelBootstrap()"));
});
test("Runtime bootstrap schedules one connect and aborts it on cleanup", async () => {
const module = (await import("../src/stores/use-local-runtime-store")) as {
startLocalRuntimeBootstrap?: (connect: (signal?: AbortSignal) => Promise<void>, schedule: (run: () => void) => () => void) => () => void;
};
expect(typeof module.startLocalRuntimeBootstrap).toBe("function");
if (!module.startLocalRuntimeBootstrap) return;
let scheduled: (() => void) | undefined;
let cancelled = 0;
const signals: AbortSignal[] = [];
const cleanup = module.startLocalRuntimeBootstrap(
async (signal) => {
if (signal) signals.push(signal);
},
(run) => {
scheduled = run;
return () => {
cancelled += 1;
};
},
);
expect(signals).toHaveLength(0);
scheduled?.();
scheduled?.();
expect(signals).toHaveLength(1);
cleanup();
expect({ cancelled, aborted: signals[0]?.aborted }).toEqual({ cancelled: 1, aborted: true });
});
type RuntimeTransport = {
request(path: string, init?: RequestInit): Promise<Response>;
};
function jsonResponse(body: unknown, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json", "cache-control": "no-store" },
});
}
function runtimeStatusResponse() {
return jsonResponse({
ok: true,
runtime: { id: "framefield-local-runtime", version: "0.1.0", apiVersion: 2 },
modules: [
{ id: "canvas-agent", displayName: "Canvas Agent", apiVersion: 1, scopes: ["canvas:connect"] },
{
id: "dreamina",
displayName: "Dreamina CLI",
apiVersion: 1,
scopes: ["dreamina:status", "dreamina:login", "dreamina:logout"],
},
],
});
}
type RuntimeStore = {
getState(): {
connection: string;
connecting: boolean;
runtime: null | { id: string };
modules: Array<{ id: string }>;
connect(): Promise<void>;
};
};

View File

@@ -0,0 +1,229 @@
import { describe, expect, test } from "bun:test";
import { LOCAL_RUNTIME_ENDPOINT, LocalRuntimeClientError, LocalRuntimeSessionClient, resolveLocalRuntimeEndpoint, type RuntimeBrowserKeyRecord, type RuntimeBrowserKeyStore } from "../src/services/local-runtime-session";
const origin = "http://127.0.0.1:3001";
const runtimeInstanceId = "web-runtime-fixture";
const canary = "master-token-canary-must-not-leak";
describe("Local Runtime signed browser session", () => {
test("build-time endpoint accepts one exact isolated loopback origin without scanning", () => {
expect(resolveLocalRuntimeEndpoint(undefined)).toBe("http://127.0.0.1:17371");
expect(resolveLocalRuntimeEndpoint("http://127.0.0.1:31731")).toBe("http://127.0.0.1:31731");
for (const invalid of ["http://localhost:31731", "http://127.0.0.1:31731/path", "http://127.0.0.1:31731?next=1", "https://127.0.0.1:31731", "http://127.0.0.1:17371,http://127.0.0.1:31731"]) {
expect(() => resolveLocalRuntimeEndpoint(invalid)).toThrow();
}
});
test("first trusted connection persists only a non-extractable browser key and returns a signed session", async () => {
const keyStore = memoryKeyStore();
const runtime = runtimeFetchFixture();
const client = new LocalRuntimeSessionClient({
origin,
keyStore,
fetch: runtime.fetch,
now: () => runtime.now,
});
const result = await client.connect();
expect(result.state).toBe("connected");
if (result.state !== "connected") throw new Error("expected connected");
expect(JSON.stringify(result)).not.toContain("pair");
expect(keyStore.record?.privateKey.extractable).toBe(false);
expect(keyStore.record?.privateKey.usages).toEqual(["sign"]);
expect(keyStore.record?.publicKeyJwk.d).toBeUndefined();
expect(JSON.stringify(keyStore.record)).not.toContain(canary);
expect(client.currentSession()?.keyId).toBe(keyStore.record?.keyId);
expect(runtime.requests[0].url).toBe(`${LOCAL_RUNTIME_ENDPOINT}/runtime/info`);
expect(runtime.requests.every((request) => request.credentials === "omit")).toBe(true);
expect(runtime.requests.every((request) => request.redirect === "error")).toBe(true);
});
test("silent trusted connection creates only an in-memory session and signs each protected request", async () => {
const keyStore = memoryKeyStore();
const runtime = runtimeFetchFixture();
const client = new LocalRuntimeSessionClient({
origin,
keyStore,
fetch: runtime.fetch,
now: () => runtime.now,
});
expect((await client.connect()).state).toBe("connected");
const session = client.currentSession();
expect(session?.sessionId).toBe("session-fixture-000000000001");
expect(JSON.stringify(keyStore.record)).not.toContain(session?.sessionId ?? "missing");
const response = await client.request("/dreamina/status", { method: "GET" });
expect(response.status).toBe(200);
const protectedRequest = runtime.requests.at(-1)!;
expect(protectedRequest.url).toBe(`${LOCAL_RUNTIME_ENDPOINT}/dreamina/status`);
expect(protectedRequest.url).not.toContain("token");
expect(protectedRequest.headers.get("x-framefield-runtime-session")).toBe(session?.sessionId);
expect(protectedRequest.headers.get("x-framefield-runtime-nonce")).toMatch(/^[A-Za-z0-9_-]{22}$/);
expect(protectedRequest.headers.get("x-framefield-runtime-proof")).toMatch(/^[A-Za-z0-9_-]{86}$/);
expect(protectedRequest.headers.has("authorization")).toBe(false);
expect(protectedRequest.headers.has("x-canvas-agent-token")).toBe(false);
expect(JSON.stringify(runtime.requests)).not.toContain(canary);
});
test("a new page reuses the browser key but obtains a new session without a bearer fallback", async () => {
const keyStore = memoryKeyStore();
const runtime = runtimeFetchFixture();
const first = new LocalRuntimeSessionClient({ origin, keyStore, fetch: runtime.fetch, now: () => runtime.now });
await first.connect();
const savedKeyId = keyStore.record?.keyId;
const second = new LocalRuntimeSessionClient({ origin, keyStore, fetch: runtime.fetch, now: () => runtime.now });
const result = await second.connect();
expect(result.state).toBe("connected");
expect(second.currentSession()?.keyId).toBe(savedKeyId);
const challengeBody = runtime.requests.filter((request) => request.url.endsWith("/runtime/session/challenge")).at(-1)?.body;
expect(challengeBody).toBe(JSON.stringify({ keyId: savedKeyId }));
expect(challengeBody).not.toContain("publicKeyJwk");
});
test("untrusted Runtime error bodies cannot enter browser errors", async () => {
const client = new LocalRuntimeSessionClient({
origin,
keyStore: memoryKeyStore(),
fetch: async () =>
new Response(
JSON.stringify({
code: "private_runtime_error",
message: canary,
detail: "C:\\Users\\owner\\Cookies",
}),
{ status: 500, headers: { "content-type": "application/json" } },
),
});
let caught: unknown;
try {
await client.connect();
} catch (error) {
caught = error;
}
expect(caught).toBeInstanceOf(LocalRuntimeClientError);
expect(caught).toMatchObject({ code: "runtime_request_failed", message: "本机运行时请求失败", status: 500 });
expect(String(caught)).not.toContain(canary);
expect(String(caught)).not.toContain("Cookies");
});
test("client fails closed if an obsolete browser-confirmation response is returned", async () => {
const runtime = runtimeFetchFixture();
runtime.obsoleteChallenge = true;
const client = new LocalRuntimeSessionClient({ origin, keyStore: memoryKeyStore(), fetch: runtime.fetch, now: () => runtime.now });
await expect(client.connect()).rejects.toBeInstanceOf(LocalRuntimeClientError);
});
});
function memoryKeyStore(): RuntimeBrowserKeyStore & { record?: RuntimeBrowserKeyRecord } {
return {
record: undefined,
async load() {
return this.record;
},
async save(record) {
this.record = record;
},
async clear() {
this.record = undefined;
},
};
}
function runtimeFetchFixture() {
const requests: Array<{
url: string;
method: string;
headers: Headers;
body?: string;
credentials?: RequestCredentials;
redirect?: RequestRedirect;
}> = [];
const fixture = {
now: Date.parse("2026-08-10T00:00:00.000Z"),
obsoleteChallenge: false,
keyId: "",
requests,
fetch: async (input: string | URL | Request, init: RequestInit = {}) => {
const url = String(input);
const headers = new Headers(init.headers);
const body = typeof init.body === "string" ? init.body : undefined;
requests.push({
url,
method: init.method ?? "GET",
headers,
body,
credentials: init.credentials,
redirect: init.redirect,
});
if (url.endsWith("/runtime/info")) {
return jsonResponse(200, {
runtime: "framefield-local-runtime",
apiVersion: 2,
protocolVersion: "framefield-runtime-session-v1",
runtimeInstanceId,
originTrusted: true,
});
}
if (url.endsWith("/runtime/session/challenge")) {
const payload = JSON.parse(body ?? "{}") as { keyId?: string; publicKeyJwk?: JsonWebKey };
fixture.keyId = payload.keyId ?? (await keyIdForJwk(payload.publicKeyJwk!));
const base = {
challengeId: "challenge-fixture-000001",
nonce: "bm9uY2UtZml4dHVyZS0wMDAwMDAwMDAwMDAwMDAwMDAwMDA",
runtimeInstanceId,
expiresAt: new Date(fixture.now + 60_000).toISOString(),
keyId: fixture.keyId,
};
return jsonResponse(
200,
fixture.obsoleteChallenge
? {
state: "obsolete_browser_confirmation",
...base,
}
: { state: "challenge", ...base },
);
}
if (url.endsWith("/runtime/session/exchange")) {
return jsonResponse(200, {
sessionId: "session-fixture-000000000001",
keyId: fixture.keyId,
scopes: ["runtime:status", "dreamina:status", "dreamina:login", "dreamina:logout", "canvas:connect"],
expiresAt: new Date(fixture.now + 10 * 60_000).toISOString(),
});
}
if (url.endsWith("/dreamina/status")) {
return jsonResponse(200, { ok: true, status: { provider: "dreamina-cli", state: "installed" } });
}
return jsonResponse(404, { ok: false });
},
};
return fixture;
}
function jsonResponse(status: number, value: unknown) {
return new Response(JSON.stringify(value), {
status,
headers: { "content-type": "application/json", "cache-control": "no-store" },
});
}
async function keyIdForJwk(jwk: JsonWebKey) {
const canonical = JSON.stringify({ crv: "P-256", kty: "EC", x: jwk.x, y: jwk.y });
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical)));
return base64Url(digest);
}
function base64Url(value: Uint8Array) {
let binary = "";
for (const byte of value) binary += String.fromCharCode(byte);
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}

File diff suppressed because it is too large Load Diff