Files
ttstd-infinite-canvas/canvas-agent/test/dreamina-generation.test.ts

372 lines
17 KiB
TypeScript

import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import { DreaminaGenerationAdapter, LocalDreaminaGenerationError } from "../src/dreamina-generation.js";
import { projectDreaminaModelCatalog } from "../src/dreamina-model-catalog.js";
test("Dreamina generation coalesces an equal idempotency request before invoking the official CLI", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "dreamina-generation-test-"));
const calls: string[] = [];
const adapter = new DreaminaGenerationAdapter({
root,
models: projectDreaminaModelCatalog(),
runtime: {
async generateToResult(input) {
calls.push(input.idempotencyKey);
return { mode: "video", video: { dataUrl: "data:video/mp4;base64,AAAA", mimeType: "video/mp4", bytes: 3 } };
},
},
});
const request = {
idempotencyKey: "seedance-mini-key-0001",
operation: "text-to-video",
model: "seedance2.0mini",
prompt: "A short test clip",
settings: { aspect: "16:9", resolution: "720p", duration: 4 },
references: [],
};
try {
const [first, second] = await Promise.all([adapter.run(request), adapter.run(request)]);
assert.deepEqual(first, second);
assert.deepEqual(calls, ["seedance-mini-key-0001"]);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("Dreamina generation rejects a Seedance mini duration below the official four-second minimum", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "dreamina-generation-test-"));
const adapter = new DreaminaGenerationAdapter({
root,
models: projectDreaminaModelCatalog(),
runtime: { async generateToResult() { throw new Error("must not execute"); } },
});
try {
await assert.rejects(
adapter.run({
idempotencyKey: "seedance-mini-key-0002",
operation: "text-to-video",
model: "seedance2.0mini",
prompt: "A short test clip",
settings: { resolution: "720p", duration: 3 },
references: [],
}),
(error: unknown) => error instanceof LocalDreaminaGenerationError
&& error.code === "local_generation_model_unavailable",
);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("Dreamina image generation maps auto or omitted product resolution to the official cross-model two-k tier", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "dreamina-generation-test-"));
const runtimeInputs: unknown[] = [];
const adapter = new DreaminaGenerationAdapter({
root,
models: projectDreaminaModelCatalog(),
runtime: {
async generateToResult(input) {
runtimeInputs.push(input);
return { mode: "image", images: [{ dataUrl: "data:image/png;base64,AAAA", mimeType: "image/png", bytes: 3 }] };
},
},
});
try {
const models = ["3.0", "3.1", "4.0", "4.1", "4.5", "4.6", "4.7", "5.0", "5.0Pro"] as const;
for (const productResolution of [undefined, "auto"] as const) {
for (const model of models) {
const index = runtimeInputs.length;
await adapter.run({
idempotencyKey: `dreamina-image-default-${String(index + 1).padStart(4, "0")}`,
operation: "text-to-image",
model,
prompt: "A small test image",
settings: { aspect: "1:1", ...(productResolution ? { resolution: productResolution } : {}), count: 1 },
references: [],
});
assert.deepEqual(runtimeInputs[index] && {
operation: (runtimeInputs[index] as { operation: string }).operation,
modelVersion: (runtimeInputs[index] as { modelVersion: string }).modelVersion,
resolutionType: (runtimeInputs[index] as { resolutionType: string }).resolutionType,
}, { operation: "text2image", modelVersion: model, resolutionType: "2k" });
}
}
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("Dreamina image generation preserves an explicit unsupported tier for strict model validation", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "dreamina-generation-test-"));
let runtimeCalls = 0;
const adapter = new DreaminaGenerationAdapter({
root,
models: projectDreaminaModelCatalog(),
runtime: {
async generateToResult() {
runtimeCalls += 1;
return { mode: "image", images: [{ dataUrl: "data:image/png;base64,AAAA", mimeType: "image/png", bytes: 3 }] };
},
},
});
try {
await assert.rejects(adapter.run({
idempotencyKey: "dreamina-image-explicit-0001",
operation: "text-to-image",
model: "5.0",
prompt: "A small test image",
settings: { aspect: "1:1", resolution: "1k", count: 1 },
references: [],
}), (error: unknown) => error instanceof LocalDreaminaGenerationError
&& error.code === "local_generation_request_invalid");
assert.equal(runtimeCalls, 0);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("Dreamina video generation maps product resolution values only in the final CLI adapter", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "dreamina-generation-test-"));
const runtimeInputs: unknown[] = [];
const adapter = new DreaminaGenerationAdapter({
root,
models: projectDreaminaModelCatalog(),
runtime: {
async generateToResult(input) {
runtimeInputs.push(input);
return { mode: "video", video: { dataUrl: "data:video/mp4;base64,AAAA", mimeType: "video/mp4", bytes: 3 } };
},
},
});
try {
const cases = [["480", "480p", "seedance2.5"], ["720", "720p", "seedance2.0mini"], ["1080", "1080p", "seedance2.0_vip"], ["2160", "4k", "seedance2.0_vip"]] as const;
for (const [index, [productResolution, cliResolution, model]] of cases.entries()) {
await adapter.run({
idempotencyKey: `dreamina-video-resolution-${String(index + 1).padStart(4, "0")}`,
operation: "text-to-video",
model,
prompt: "A short test clip",
settings: { aspect: "16:9", resolution: productResolution, duration: 4 },
references: [],
});
assert.equal((runtimeInputs[index] as { videoResolution: string }).videoResolution, cliResolution);
}
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("Dreamina adapter returns the Runtime task after references are durably staged, before result polling", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "dreamina-generation-test-"));
let referencePath = "";
const adapter = new DreaminaGenerationAdapter({
root,
models: projectDreaminaModelCatalog(),
runtime: {
async enqueue(input) {
referencePath = "referenceImages" in input ? input.referenceImages[0] : "";
assert(referencePath && await fs.stat(referencePath).then((item) => item.isFile()));
return {
id: input.idempotencyKey,
provider: "dreamina-cli",
mode: "video",
operation: input.operation,
model: "seedance2.0mini",
status: "running",
stage: "submitted",
progress: 10,
receiptRecorded: true,
createdAt: "2026-08-12T00:00:00.000Z",
updatedAt: "2026-08-12T00:00:00.000Z",
};
},
async waitForTask() { throw new Error("submit must not wait for query_result"); },
async generateToResult() { throw new Error("submit must not use legacy generation"); },
},
});
try {
const task = await adapter.submit({
idempotencyKey: "dreamina-adapter-async-0001",
operation: "image-to-video",
model: "seedance2.0mini",
prompt: "Animate the reference",
settings: { resolution: "720", duration: 4 },
references: [{ kind: "image", mimeType: "image/png", contentBase64: Buffer.from("89504e470d0a1a0a", "hex").toString("base64") }],
});
assert.equal(task.stage, "submitted");
await assert.rejects(fs.access(referencePath));
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("Dreamina final adapter selects multimodal2video and keeps typed references in separate staged groups", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "dreamina-generation-test-"));
let observedInput: Record<string, unknown> | undefined;
let observedOptions: Record<string, unknown> | undefined;
const stagedExtensions: string[] = [];
const adapter = new DreaminaGenerationAdapter({
root,
models: projectDreaminaModelCatalog(),
runtime: {
async enqueue(input, options) {
observedInput = input as unknown as Record<string, unknown>;
observedOptions = options as unknown as Record<string, unknown>;
const paths = [
...("referenceImages" in input ? input.referenceImages : []),
...("referenceVideos" in input ? input.referenceVideos : []),
...("referenceAudios" in input ? input.referenceAudios : []),
];
for (const reference of paths) {
assert.equal((await fs.stat(reference)).isFile(), true);
stagedExtensions.push(path.extname(reference));
}
return {
id: input.idempotencyKey,
provider: "dreamina-cli",
mode: "video",
operation: input.operation,
model: "seedance2.5",
status: "running",
stage: "submitted",
receiptRecorded: true,
createdAt: "2026-08-13T00:00:00.000Z",
updatedAt: "2026-08-13T00:00:00.000Z",
};
},
async waitForTask() { throw new Error("submit must not wait"); },
async generateToResult() { throw new Error("submit must not use legacy generation"); },
},
});
try {
await adapter.submit({
idempotencyKey: "retry-client-operation-0001",
clientOperationId: "retry-client-operation-0001",
context: {
scope: "scoped",
projectId: "project-agent-multimodal",
nodeId: "node-agent-multimodal",
retryOf: "dreamina:prior-agent-0001",
attemptGroupId: "dreamina:prior-agent-0001",
},
operation: "reference-to-video",
model: "seedance2.5",
prompt: "Use every reference",
settings: { aspect: "16:9", resolution: "720", duration: 4 },
references: [
{ kind: "image", mimeType: "image/png", contentBase64: Buffer.from("89504e470d0a1a0a", "hex").toString("base64"), metadata: { name: "image.png", width: 16, height: 9 } },
{ kind: "video", mimeType: "video/mp4", contentBase64: Buffer.from("000000106674797069736f6d", "hex").toString("base64"), metadata: { name: "video.mp4", durationMs: 1000 } },
{ kind: "audio", mimeType: "audio/mpeg", contentBase64: Buffer.from("49443304", "hex").toString("base64"), metadata: { name: "audio.mp3", durationMs: 1000 } },
],
});
assert.equal(observedInput?.operation, "multimodal2video");
assert.equal((observedInput?.referenceImages as unknown[])?.length, 1);
assert.equal((observedInput?.referenceVideos as unknown[])?.length, 1);
assert.equal((observedInput?.referenceAudios as unknown[])?.length, 1);
assert.deepEqual(stagedExtensions.sort(), [".mp3", ".mp4", ".png"]);
assert.equal(observedOptions?.clientOperationId, "retry-client-operation-0001");
assert.deepEqual(observedOptions?.taskContext, {
scope: "scoped",
projectId: "project-agent-multimodal",
nodeId: "node-agent-multimodal",
retryOf: "dreamina:prior-agent-0001",
attemptGroupId: "dreamina:prior-agent-0001",
});
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
test("only the final Dreamina adapter selects video CLI operations for every reference shape", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "dreamina-operation-table-"));
const observed: Array<Record<string, unknown>> = [];
const adapter = new DreaminaGenerationAdapter({
root,
models: projectDreaminaModelCatalog(),
runtime: {
async generateToResult(input) {
observed.push(input as unknown as Record<string, unknown>);
return { mode: "video", video: { dataUrl: "data:video/mp4;base64,AAAA", mimeType: "video/mp4", bytes: 3 } };
},
},
});
const image = { kind: "image", mimeType: "image/png", contentBase64: Buffer.from("89504e470d0a1a0a", "hex").toString("base64") };
const video = { kind: "video", mimeType: "video/mp4", contentBase64: Buffer.from("000000106674797069736f6d", "hex").toString("base64") };
const audio = { kind: "audio", mimeType: "audio/mpeg", contentBase64: Buffer.from("49443304", "hex").toString("base64") };
const cases = [
{ name: "zero-image", operation: "text-to-video", references: [], expected: "text2video", groups: [0, 0, 0] },
{ name: "one-image", operation: "image-to-video", references: [image], expected: "image2video", groups: [1, 0, 0] },
{ name: "two-images", operation: "image-to-video", references: [image, image], expected: "frames2video", groups: [2, 0, 0] },
{ name: "three-images", operation: "reference-to-video", references: [image, image, image], expected: "multimodal2video", groups: [3, 0, 0] },
{ name: "video-only", operation: "reference-to-video", references: [video], expected: "multimodal2video", groups: [0, 1, 0] },
{ name: "audio-only", operation: "reference-to-video", references: [audio], expected: "multimodal2video", groups: [0, 0, 1] },
{ name: "mixed", operation: "reference-to-video", references: [image, video, audio], expected: "multimodal2video", groups: [1, 1, 1] },
] as const;
try {
for (const [index, item] of cases.entries()) {
await adapter.run({
idempotencyKey: `operation-table-${String(index).padStart(2, "0")}-0001`,
operation: item.operation,
model: "seedance2.5",
prompt: "fixture",
settings: { aspect: "16:9", resolution: "720", duration: 4 },
references: item.references,
});
const input = observed.at(-1)!;
assert.equal(input.operation, item.expected, item.name);
assert.deepEqual([
(input.referenceImages as unknown[] | undefined)?.length ?? 0,
(input.referenceVideos as unknown[] | undefined)?.length ?? 0,
(input.referenceAudios as unknown[] | undefined)?.length ?? 0,
], item.groups, item.name);
}
await adapter.run({
idempotencyKey: "operation-table-max-groups-0001",
operation: "reference-to-video",
model: "seedance2.5",
prompt: "fixture",
settings: { aspect: "16:9", resolution: "720", duration: 4 },
references: [
...Array.from({ length: 30 }, () => image),
...Array.from({ length: 10 }, () => video),
...Array.from({ length: 10 }, () => audio),
],
});
assert.deepEqual([
(observed.at(-1)?.referenceImages as unknown[] | undefined)?.length ?? 0,
(observed.at(-1)?.referenceVideos as unknown[] | undefined)?.length ?? 0,
(observed.at(-1)?.referenceAudios as unknown[] | undefined)?.length ?? 0,
], [30, 10, 10]);
for (const [name, references] of [
["images", Array.from({ length: 31 }, () => image)],
["videos", Array.from({ length: 11 }, () => video)],
["audios", Array.from({ length: 11 }, () => audio)],
] as const) {
await assert.rejects(adapter.run({
idempotencyKey: `operation-limit-${name}-0001`,
operation: "reference-to-video",
model: "seedance2.5",
prompt: "fixture",
settings: { aspect: "16:9", resolution: "720", duration: 4 },
references,
}), (error: unknown) => error instanceof LocalDreaminaGenerationError
&& error.code === "local_generation_request_invalid");
}
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});