Files
ttstd-infinite-canvas/web/test/generation-task-durability.node.test.ts
2026-09-14 01:11:56 +08:00

134 lines
5.7 KiB
TypeScript

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";
import { buildBackendToolRequests } from "../src/services/api/image";
test("managed cloud agent task keeps tool definitions and tool results", () => {
const requests = buildBackendToolRequests(
[
{ role: "user", content: "读取画布" },
{ type: "function_call", call_id: "call-1", name: "canvas_get_state", arguments: "{}" },
{ role: "tool", tool_call_id: "call-1", content: '{"nodes":[]}' },
],
[{ type: "function", function: { name: "canvas_get_state", parameters: { type: "object" } } }],
"required",
);
assert.deepEqual(Object.keys(requests), ["canonical"]);
assert.equal(requests.canonical.toolChoice, "required");
assert.equal(requests.canonical.systemPrompt, "");
assert.deepEqual(requests.canonical.tools, [{ type: "function", function: { name: "canvas_get_state", parameters: { type: "object" } } }]);
assert.deepEqual(requests.canonical.messages, [
{ role: "user", content: "读取画布" },
{ type: "function_call", call_id: "call-1", name: "canvas_get_state", arguments: "{}" },
{ role: "tool", tool_call_id: "call-1", content: '{"nodes":[]}' },
]);
});
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);
});