815 lines
33 KiB
TypeScript
815 lines
33 KiB
TypeScript
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,
|
|
},
|
|
};
|
|
}
|