feat: complete TASK-WP2-03 project lifecycle
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { ManagedStorage } from "../../apps/api/src/managed-storage.js";
|
||||
import { ProjectService } from "../../apps/api/src/projects.js";
|
||||
import { ProjectPurgeCleanup } from "../../apps/worker/src/project-purge-cleanup.js";
|
||||
|
||||
const now = Date.parse("2026-08-02T09:00:00.000Z");
|
||||
const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", "base64");
|
||||
const roots: string[] = [];
|
||||
|
||||
function writeEvidence(value: unknown) {
|
||||
const root = process.env.DADA_EVIDENCE_DIR_PROJECT_TRASH;
|
||||
if (!root) return;
|
||||
mkdirSync(root, { recursive: true });
|
||||
writeFileSync(resolve(root, "worker-events.json"), `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true });
|
||||
});
|
||||
|
||||
describe("TDD-WP2-PROJ-003 project cleanup worker", () => {
|
||||
it("queues all associated managed files without reducing capacity, then physically removes them", async () => {
|
||||
const dataRoot = mkdtempSync(join(tmpdir(), "dada-wp2-03-worker-"));
|
||||
roots.push(dataRoot);
|
||||
mkdirSync(join(dataRoot, "db"), { recursive: true });
|
||||
const databasePath = join(dataRoot, "db", "dada.sqlite3");
|
||||
const storage = new ManagedStorage({ dataRoot, databasePath });
|
||||
const projects = new ProjectService({ clock: () => now, databasePath });
|
||||
const ownerId = randomUUID();
|
||||
const project = projects.createProjectForGeneration({ ownerId, prompt: "清理项目", ratio: "3:4", status: "failed" });
|
||||
const fileKinds = ["generated", "reference", "export"] as const;
|
||||
const files = [];
|
||||
for (const fileKind of fileKinds) {
|
||||
const committed = await storage.commitStream({
|
||||
content: Readable.from(png), expectedMimeType: "image/png", fileKind,
|
||||
fileName: `${fileKind}.png`, operationId: randomUUID(), ownerRef: ownerId, projectedWriteBytes: png.byteLength,
|
||||
});
|
||||
files.push(committed);
|
||||
projects.linkManagedResource(ownerId, project.project.projectId, committed.file_id, fileKind);
|
||||
}
|
||||
projects.saveProjectState({
|
||||
expectedStateVersion: 1, idempotencyKey: "wp2-03-location-state-00000000000001", ownerId,
|
||||
projectId: project.project.projectId,
|
||||
state: {
|
||||
canvas_state: {
|
||||
background: { adjustments: { brightness: 0, contrast: 0, crop: null, filter: "none", fit: "fill", saturation: 0, sharpness: 0, temperature: 0 }, asset_id: null },
|
||||
elements: [{
|
||||
coordinates: { latitude: 30.2741, longitude: 120.1551 }, created_at: new Date(now).toISOString(),
|
||||
element_id: randomUUID(), opacity: 1, position: { x: 0, y: 0 }, resource_version: "v1", rotation: 0,
|
||||
scale: { x: 1, y: 1 }, template_or_asset_id: "DYN004", type: "dynamic_sticker", z_index: 0,
|
||||
}], pixel_height: 1440, pixel_width: 1080, ratio: "3:4", schema_version: 1,
|
||||
},
|
||||
name: "含定位信息的项目",
|
||||
},
|
||||
});
|
||||
const bytesBeforePurge = storage.getState().managed_content_bytes;
|
||||
projects.trashProject(ownerId, project.project.projectId);
|
||||
projects.purgeProject(ownerId, project.project.projectId);
|
||||
expect(storage.getState().managed_content_bytes).toBe(bytesBeforePurge);
|
||||
expect(projects.database.prepare("SELECT COUNT(*) AS count FROM file_cleanup_queue WHERE status = 'pending'").get()).toEqual({ count: 3 });
|
||||
|
||||
const worker = new ProjectPurgeCleanup({ clock: () => now, dataRoot, databasePath });
|
||||
const cleanup = worker.run();
|
||||
worker.close();
|
||||
expect(cleanup).toEqual({
|
||||
expired: 0,
|
||||
physical: { completed: 3, failed: 0 },
|
||||
relational: { completed: 1, failed: 0 },
|
||||
});
|
||||
expect(projects.database.prepare("SELECT COUNT(*) AS count FROM projects WHERE project_id = ?").get(project.project.projectId)).toEqual({ count: 0 });
|
||||
expect(projects.database.prepare("SELECT COUNT(*) AS count FROM project_states WHERE project_id = ?").get(project.project.projectId)).toEqual({ count: 0 });
|
||||
expect(storage.getState().managed_content_bytes).toBe(0);
|
||||
expect(files.every((file) => storage.resolveManagedFile(file.file_id) === undefined)).toBe(true);
|
||||
writeEvidence({ bytes_before_queue: bytesBeforePurge, bytes_while_queued: bytesBeforePurge, cleanup, resource_kinds: [...fileKinds, "location"] });
|
||||
projects.close();
|
||||
storage.close();
|
||||
}, 20_000);
|
||||
});
|
||||
Reference in New Issue
Block a user