feat: complete TASK-WP2-05 generation submission
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { createApp } from "../../apps/api/src/app.js";
|
||||
import { CreditService } from "../../apps/api/src/credits.js";
|
||||
import { GenerationSubmissionService, StaticGenerationModelCatalog } from "../../apps/api/src/generation-submission.js";
|
||||
import { ManagedStorage } from "../../apps/api/src/managed-storage.js";
|
||||
import { ProjectService } from "../../apps/api/src/projects.js";
|
||||
import { RegistrationService } from "../../apps/api/src/registration.js";
|
||||
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
|
||||
|
||||
const now = Date.parse("2026-08-02T13:00:00.000Z");
|
||||
const modelId = "gemini-3.1-flash-image-preview";
|
||||
const roots: string[] = [];
|
||||
const closeables: Array<{ close(): void }> = [];
|
||||
const baseHeaders = { host: "127.0.0.1:43121", origin: "http://127.0.0.1:43121" };
|
||||
|
||||
async function multipart(fields: Record<string, string>, files: Array<{ bytes: Uint8Array; name: string; type: string }> = []) {
|
||||
const form = new FormData();
|
||||
for (const [key, value] of Object.entries(fields)) form.append(key, value);
|
||||
for (const file of files) form.append("reference_files", new Blob([file.bytes], { type: file.type }), file.name);
|
||||
const serialized = new Response(form);
|
||||
return { contentType: serialized.headers.get("content-type")!, payload: Buffer.from(await serialized.arrayBuffer()) };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const value of closeables.splice(0).reverse()) value.close();
|
||||
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
|
||||
});
|
||||
|
||||
describe("TASK-WP2-05 generation API", () => {
|
||||
it("returns 412 without side effects, creates one queued task after reconfirmation, and exposes task truth", async () => {
|
||||
const dataRoot = mkdtempSync(join(tmpdir(), "dada-wp2-05-api-"));
|
||||
roots.push(dataRoot);
|
||||
mkdirSync(join(dataRoot, "db"), { recursive: true });
|
||||
const databasePath = join(dataRoot, "db", "dada.sqlite3");
|
||||
const registration = new RegistrationService({
|
||||
challengePepper: Buffer.alloc(32, 0x21), clock: () => now,
|
||||
currentPrivacyNoticeVersion: "p0a-registration-notice-v1", databasePath,
|
||||
invitePepper: Buffer.alloc(32, 0x22), resend: new MockResendAdapter(), sessionPepper: Buffer.alloc(32, 0x23),
|
||||
});
|
||||
const projects = new ProjectService({ clock: () => now, databasePath });
|
||||
const credits = new CreditService({ clock: () => now, databasePath });
|
||||
const storage = new ManagedStorage({ dataRoot, databasePath });
|
||||
const models = new StaticGenerationModelCatalog([{
|
||||
configSetVersion: 2, configVersion: 2, contractValidationStatus: "verified", creditCost: 2, enabled: true, modelId,
|
||||
promptMaxLength: 1_000, referenceLimits: { maxFileBytes: 1_024, maxFiles: 2, maxTotalBytes: 2_048 },
|
||||
runtimeAvailability: { availableForNewJobs: true, reason: null }, supportedRatios: ["3:4", "1:1", "4:3", "9:16"],
|
||||
}]);
|
||||
const generations = new GenerationSubmissionService({ clock: () => now, credits, models, storage });
|
||||
closeables.push(generations, storage, credits, projects, registration);
|
||||
const userId = randomUUID();
|
||||
registration.database.prepare(`INSERT INTO users (
|
||||
user_id, normalized_email, role, status, counts_toward_stage_limit, registration_id, created_at
|
||||
) VALUES (?, 'generation-api@example.invalid', 'user', 'active', 1, ?, ?)`).run(userId, randomUUID(), now);
|
||||
registration.database.prepare("INSERT INTO user_profiles (user_id, creator_name, social_id) VALUES (?, 'API User', '@api')").run(userId);
|
||||
registration.database.prepare("INSERT INTO credit_accounts (user_id, available_balance, reserved_balance, updated_at) VALUES (?, 10, 0, ?)").run(userId, now);
|
||||
const session = registration.issueAuthenticatedSession(userId, "user");
|
||||
const csrfToken = registration.issueUserCsrfToken(session.sessionToken);
|
||||
const app = await createApp({ browserGate: false, credits, generations, networkBoundary: { allowTestPort: true }, projects, registration });
|
||||
const common = {
|
||||
client_submission_id: randomUUID(), confirmed_credit_cost: "1", creation_mode: "new_project",
|
||||
model_config_version: "1", model_id: modelId, prompt: "提交快照", ratio: "3:4",
|
||||
};
|
||||
const staleBody = await multipart(common);
|
||||
const headers = {
|
||||
...baseHeaders, cookie: `dada_session=${session.sessionToken}`, "content-type": staleBody.contentType,
|
||||
"idempotency-key": `generation-${randomUUID()}-${randomUUID()}`, "x-csrf-token": csrfToken,
|
||||
};
|
||||
const invalidReferenceBody = await multipart({
|
||||
...common,
|
||||
client_submission_id: randomUUID(),
|
||||
confirmed_credit_cost: "2",
|
||||
model_config_version: "2",
|
||||
reference_manifest: JSON.stringify([{ file_name: "reference.png", mime_type: "image/png", size: 7 }]),
|
||||
}, [{ bytes: new TextEncoder().encode("not-png"), name: "reference.png", type: "image/png" }]);
|
||||
const invalidReference = await app.inject({
|
||||
headers: { ...headers, "content-type": invalidReferenceBody.contentType, "idempotency-key": `generation-${randomUUID()}-${randomUUID()}` },
|
||||
method: "POST", payload: invalidReferenceBody.payload, url: "/api/v1/generations",
|
||||
});
|
||||
expect(invalidReference.statusCode).toBe(400);
|
||||
expect(registration.database.prepare("SELECT COUNT(*) AS count FROM generation_jobs").get()).toEqual({ count: 0 });
|
||||
|
||||
registration.database.prepare("UPDATE credit_accounts SET available_balance = 0 WHERE user_id = ?").run(userId);
|
||||
const insufficientBody = await multipart({
|
||||
...common, client_submission_id: randomUUID(), confirmed_credit_cost: "2", model_config_version: "2",
|
||||
});
|
||||
const insufficient = await app.inject({
|
||||
headers: { ...headers, "content-type": insufficientBody.contentType, "idempotency-key": `generation-${randomUUID()}-${randomUUID()}` },
|
||||
method: "POST", payload: insufficientBody.payload, url: "/api/v1/generations",
|
||||
});
|
||||
expect(insufficient.statusCode).toBe(409);
|
||||
expect(registration.database.prepare("SELECT COUNT(*) AS count FROM generation_jobs").get()).toEqual({ count: 0 });
|
||||
expect(registration.database.prepare("SELECT COUNT(*) AS count FROM projects").get()).toEqual({ count: 0 });
|
||||
registration.database.prepare("UPDATE credit_accounts SET available_balance = 10 WHERE user_id = ?").run(userId);
|
||||
|
||||
const stale = await app.inject({ headers, method: "POST", payload: staleBody.payload, url: "/api/v1/generations" });
|
||||
expect(stale.statusCode).toBe(412);
|
||||
expect(stale.json()).toMatchObject({ error: { code: "MODEL_CONFIG_VERSION_CONFLICT", details: { latest_version: 2 } } });
|
||||
expect(registration.database.prepare("SELECT COUNT(*) AS count FROM generation_jobs").get()).toEqual({ count: 0 });
|
||||
|
||||
const confirmedBody = await multipart({ ...common, client_submission_id: randomUUID(), confirmed_credit_cost: "2", model_config_version: "2" });
|
||||
const created = await app.inject({
|
||||
headers: { ...headers, "content-type": confirmedBody.contentType, "idempotency-key": `generation-${randomUUID()}-${randomUUID()}` },
|
||||
method: "POST", payload: confirmedBody.payload, url: "/api/v1/generations",
|
||||
});
|
||||
expect(created.statusCode).toBe(201);
|
||||
expect(created.json()).toMatchObject({ created: true, task: { confirmed_credit_cost: 2, model_config_version: 2, status: "queued" } });
|
||||
const current = await app.inject({ headers: { ...baseHeaders, cookie: `dada_session=${session.sessionToken}` }, method: "GET", url: "/api/v1/generations/current" });
|
||||
const detail = await app.inject({ headers: { ...baseHeaders, cookie: `dada_session=${session.sessionToken}` }, method: "GET", url: `/api/v1/generations/${created.json().task.generation_id}` });
|
||||
expect(current.json()).toEqual(created.json().task);
|
||||
expect(detail.json()).toEqual(created.json().task);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user