Files
ttstd-infinite-canvas/web/test/local-dreamina-model-catalog.test.ts

407 lines
17 KiB
TypeScript

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);
});
test("Dreamina catalog readiness is loading immediately and shares one in-flight bootstrap", async () => {
const module = await import("../src/stores/use-local-dreamina-model-store").catch(() => ({}));
const createStore = (
module as {
createLocalDreaminaModelStore?: (dependencies: Record<string, unknown>) => {
getState(): {
state: string;
models: Array<{ id: string }>;
ensureReady(signal?: AbortSignal): Promise<Array<{ id: string }>>;
};
};
}
).createLocalDreaminaModelStore;
expect(typeof createStore).toBe("function");
if (!createStore) return;
let requests = 0;
let release!: () => void;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
const store = createStore({
getRuntimeState: () => ({ connection: "connected", modules: [{ id: "dreamina", scopes: ["dreamina:models"] }] }),
getClient: () => ({ request: async () => catalogResponse() }),
loadSnapshot: async () => {
requests += 1;
await gate;
return {
accountBinding: "a".repeat(64),
sessionEpoch: 3,
models: [
{
provider: "dreamina-cli",
id: "seedance2.0mini",
displayName: "seedance2.0mini",
modality: "video",
operations: ["text-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",
},
],
};
},
});
const first = store.getState().ensureReady();
const second = store.getState().ensureReady();
expect(store.getState().state).toBe("loading");
expect(requests).toBe(1);
release();
const [firstModels, secondModels] = await Promise.all([first, second]);
expect(firstModels.map((item) => item.id)).toEqual(["seedance2.0mini"]);
expect(secondModels).toEqual(firstModels);
expect(store.getState()).toMatchObject({ state: "ready", cacheScope: `${"a".repeat(64)}:3` });
expect(requests).toBe(1);
});
test("Dreamina catalog timeout releases a hung shared request so generation can recover without reload", async () => {
const { createLocalDreaminaModelStore } = await import("../src/stores/use-local-dreamina-model-store");
let requests = 0;
const store = createLocalDreaminaModelStore({
getRuntimeState: () => ({ connection: "connected", modules: [{ id: "dreamina", scopes: ["dreamina:models"] }] }),
getClient: () => ({ request: async () => catalogResponse() }),
requestTimeoutMs: 10,
loadSnapshot: async (_client, signal) => {
requests += 1;
if (requests > 1) return catalogSnapshot("seedance-recovered", 4);
if (!signal) return await new Promise<ReturnType<typeof catalogSnapshot>>(() => undefined);
return await new Promise<ReturnType<typeof catalogSnapshot>>((_resolve, reject) => {
signal.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError")), { once: true });
});
},
});
const caller = new AbortController();
const first = store.getState().ensureReady(caller.signal);
caller.abort();
await expect(first).rejects.toMatchObject({ name: "AbortError" });
await Bun.sleep(20);
const recovered = await Promise.race([store.getState().ensureReady(), Bun.sleep(40).then(() => "stuck" as const)]);
expect(recovered).not.toBe("stuck");
expect(recovered).toMatchObject([{ id: "seedance-recovered" }]);
expect(store.getState()).toMatchObject({ state: "ready", cacheScope: `${"a".repeat(64)}:4` });
expect(requests).toBe(2);
});
test("Dreamina catalog forced resync ignores a late result from the previous Runtime scope", async () => {
const { createLocalDreaminaModelStore } = await import("../src/stores/use-local-dreamina-model-store");
let requests = 0;
let releaseOld!: (snapshot: ReturnType<typeof catalogSnapshot>) => void;
const oldRequest = new Promise<ReturnType<typeof catalogSnapshot>>((resolve) => {
releaseOld = resolve;
});
const store = createLocalDreaminaModelStore({
getRuntimeState: () => ({ connection: "connected", modules: [{ id: "dreamina", scopes: ["dreamina:models"] }] }),
getClient: () => ({ request: async () => catalogResponse() }),
requestTimeoutMs: 100,
loadSnapshot: async () => {
requests += 1;
return requests === 1 ? oldRequest : catalogSnapshot("seedance-current", 5);
},
});
const oldSync = store.getState().sync();
const currentSync = store.getState().sync();
const currentResult = await Promise.race([currentSync.then(() => "ready" as const), Bun.sleep(40).then(() => "stuck" as const)]);
expect(currentResult).toBe("ready");
expect(requests).toBe(2);
expect(store.getState()).toMatchObject({ state: "ready", cacheScope: `${"a".repeat(64)}:5`, models: [{ id: "seedance-current" }] });
releaseOld(catalogSnapshot("seedance-stale", 3));
await oldSync;
expect(store.getState()).toMatchObject({ state: "ready", cacheScope: `${"a".repeat(64)}:5`, models: [{ id: "seedance-current" }] });
});
test("effective config removes custom channels when administrators disable them", async () => {
const { createModelChannel, effectiveConfigForCustomChannels, normalizeConfigSnapshot } = await import("../src/stores/use-config-store");
const config = normalizeConfigSnapshot({
config: {
channels: [
createModelChannel({
id: "system-1",
scope: "system",
name: "系统渠道",
baseUrl: "/api/ai/system/system-1",
apiKey: "system",
models: ["system-model"],
modelCosts: [{ model: "system-model", capability: "text", billingMode: "fixed_request", unitPriceMicrocredits: 0 }],
}),
createModelChannel({ id: "custom-1", scope: "user", name: "自定义渠道", baseUrl: "https://example.com", apiKey: "private-key", models: ["custom-model"] }),
],
},
}).config;
const effective = effectiveConfigForCustomChannels(config, false);
expect(effective.channels.map((channel) => channel.id)).toEqual(["system-1"]);
expect(effective.models).toContain("system-1::system-model");
expect(effective.models).not.toContain("custom-1::custom-model");
expect(config.channels.map((channel) => channel.id)).toEqual(["system-1", "custom-1"]);
});
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" } },
);
}
function catalogSnapshot(id: string, sessionEpoch: number) {
return {
accountBinding: "a".repeat(64),
sessionEpoch,
models: [
{
provider: "dreamina-cli" as const,
id,
displayName: id,
modality: "video" as const,
operations: ["text-to-video" as const],
adapterSupported: true,
accountEntitlement: "unknown" as const,
currentlyObservedAvailable: "unknown" as const,
settings: { aliases: [], aspects: ["16:9"], maxReferenceImages: 9, minDuration: 4, maxDuration: 15, tiers: ["720p"] },
source: "runtime-execution-contract" as const,
},
],
};
}