Merge remote-tracking branch 'origin/codex/wp5-06' into codex/wp5-lineage
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { ManagedStorage } from "../../apps/api/src/managed-storage.js";
|
||||
import { RegistrationService } from "../../apps/api/src/registration.js";
|
||||
import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js";
|
||||
import { StickerReleaseService } from "../../apps/api/src/sticker-releases.js";
|
||||
import { ProjectPurgeCleanup } from "../../apps/worker/src/project-purge-cleanup.js";
|
||||
|
||||
const roots: string[] = [];
|
||||
const closeables: Array<{ close(): void }> = [];
|
||||
|
||||
function fixture() {
|
||||
const dataRoot = mkdtempSync(join(tmpdir(), "dada-wp5-06-cleanup-"));
|
||||
roots.push(dataRoot);
|
||||
mkdirSync(join(dataRoot, "db"), { recursive: true });
|
||||
const databasePath = join(dataRoot, "db", "dada.sqlite3");
|
||||
const registration = new RegistrationService({
|
||||
challengePepper: Buffer.alloc(32, 0x61),
|
||||
currentPrivacyNoticeVersion: "p0a-registration-notice-v1",
|
||||
databasePath,
|
||||
invitePepper: Buffer.alloc(32, 0x62),
|
||||
resend: new MockResendAdapter(),
|
||||
sessionPepper: Buffer.alloc(32, 0x63),
|
||||
});
|
||||
const storage = new ManagedStorage({ dataRoot, databasePath });
|
||||
const stickers = new StickerReleaseService({ databasePath, storage });
|
||||
closeables.push(stickers, storage, registration);
|
||||
return { dataRoot, databasePath, database: registration.database, storage };
|
||||
}
|
||||
|
||||
function seedAdmin(database: RegistrationService["database"]) {
|
||||
const adminId = randomUUID();
|
||||
database.prepare(`
|
||||
INSERT INTO users (
|
||||
user_id, normalized_email, role, status, counts_toward_stage_limit,
|
||||
registration_id, created_at
|
||||
) VALUES (?, ?, 'super_admin', 'active', 0, ?, ?)
|
||||
`).run(adminId, `${adminId}@example.invalid`, randomUUID(), Date.now());
|
||||
database.prepare("INSERT INTO admin_access (user_id, allowed) VALUES (?, 1)").run(adminId);
|
||||
return adminId;
|
||||
}
|
||||
|
||||
async function seedHistoricalPair(test: ReturnType<typeof fixture>) {
|
||||
const original = await test.storage.commitBufferFixture("sticker_original", "STK2401.png", Buffer.from("original-history"));
|
||||
const thumbnail = await test.storage.commitBufferFixture("sticker_thumbnail", "STK2401-thumbnail.png", Buffer.from("thumbnail-history"));
|
||||
const insert = test.database.prepare(`
|
||||
INSERT INTO sticker_managed_file_history (
|
||||
managed_file_id, stable_id, resource_version, file_kind, created_at
|
||||
) VALUES (?, 'STK2401', 'asset-20260701.1', ?, ?)
|
||||
`);
|
||||
insert.run(original.file_id, "original", Date.now());
|
||||
insert.run(thumbnail.file_id, "thumbnail", Date.now());
|
||||
return { original, thumbnail };
|
||||
}
|
||||
|
||||
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("TDD-WP5-CLN-001 sticker history cleanup", () => {
|
||||
it("denies the whole batch when a reference appears after the candidate snapshot", async () => {
|
||||
const test = fixture();
|
||||
const adminId = seedAdmin(test.database);
|
||||
const files = await seedHistoricalPair(test);
|
||||
const candidates = test.storage.listAssetCleanupCandidates();
|
||||
expect(candidates.items).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ file_id: files.original.file_id, file_kind: "original", reference_count: 0, stable_id: "STK2401" }),
|
||||
expect.objectContaining({ file_id: files.thumbnail.file_id, file_kind: "thumbnail", reference_count: 0, stable_id: "STK2401" }),
|
||||
]));
|
||||
const intent = test.storage.createAssetCleanupIntent({
|
||||
actorId: adminId,
|
||||
fileIds: [files.original.file_id, files.thumbnail.file_id],
|
||||
idempotencyKey: `cleanup-${randomUUID()}-${randomUUID()}`,
|
||||
snapshotVersion: candidates.candidate_snapshot_version,
|
||||
});
|
||||
|
||||
test.storage.addAssetReference(files.original.file_id, "release");
|
||||
expect(() => test.storage.confirmAssetCleanupIntent({
|
||||
actorId: adminId,
|
||||
confirmationToken: intent.confirmation_token,
|
||||
requestId: intent.request_id,
|
||||
})).toThrow("ASSET_HISTORY_REFERENCE_CONFLICT");
|
||||
|
||||
expect(test.database.prepare("SELECT status FROM asset_cleanup_requests WHERE request_id = ?").get(intent.request_id)).toEqual({ status: "denied" });
|
||||
expect(test.database.prepare("SELECT COUNT(*) AS count FROM file_cleanup_queue WHERE status = 'pending'").get()).toEqual({ count: 0 });
|
||||
expect(test.database.prepare("SELECT COUNT(*) AS count FROM admin_operation_logs WHERE target_ref = ? AND result = 'failed'").get(intent.request_id)).toEqual({ count: 1 });
|
||||
});
|
||||
|
||||
it("requires the same admin, queues without reducing capacity, then remeasures after physical deletion", async () => {
|
||||
const test = fixture();
|
||||
const adminId = seedAdmin(test.database);
|
||||
const otherAdminId = seedAdmin(test.database);
|
||||
const files = await seedHistoricalPair(test);
|
||||
const bytesBefore = test.storage.getState().managed_content_bytes;
|
||||
const candidates = test.storage.listAssetCleanupCandidates();
|
||||
const intent = test.storage.createAssetCleanupIntent({
|
||||
actorId: adminId,
|
||||
fileIds: [files.original.file_id, files.thumbnail.file_id],
|
||||
idempotencyKey: `cleanup-${randomUUID()}-${randomUUID()}`,
|
||||
snapshotVersion: candidates.candidate_snapshot_version,
|
||||
});
|
||||
|
||||
expect(() => test.storage.confirmAssetCleanupIntent({
|
||||
actorId: otherAdminId,
|
||||
confirmationToken: intent.confirmation_token,
|
||||
requestId: intent.request_id,
|
||||
})).toThrow("ASSET_CLEANUP_CANDIDATE_STALE");
|
||||
const queued = test.storage.confirmAssetCleanupIntent({
|
||||
actorId: adminId,
|
||||
confirmationToken: intent.confirmation_token,
|
||||
requestId: intent.request_id,
|
||||
});
|
||||
expect(queued).toMatchObject({ file_count: 2, status: "queued" });
|
||||
expect(test.storage.getState().managed_content_bytes).toBe(bytesBefore);
|
||||
expect(existsSync(join(test.dataRoot, files.original.relative_path))).toBe(true);
|
||||
expect(test.database.prepare("SELECT COUNT(*) AS count FROM file_cleanup_queue WHERE status = 'pending'").get()).toEqual({ count: 2 });
|
||||
|
||||
const worker = new ProjectPurgeCleanup({ dataRoot: test.dataRoot, databasePath: test.databasePath });
|
||||
const result = worker.processFileCleanup();
|
||||
worker.close();
|
||||
expect(result).toEqual({ completed: 2, failed: 0 });
|
||||
expect(existsSync(join(test.dataRoot, files.original.relative_path))).toBe(false);
|
||||
expect(existsSync(join(test.dataRoot, files.thumbnail.relative_path))).toBe(false);
|
||||
expect(test.storage.getState()).toMatchObject({ managed_content_bytes: 0, storage_status: "active" });
|
||||
expect(test.database.prepare("SELECT status FROM asset_cleanup_requests WHERE request_id = ?").get(intent.request_id)).toEqual({ status: "completed" });
|
||||
expect(test.database.prepare("SELECT operation_type, result FROM admin_operation_logs WHERE target_ref = ? ORDER BY occurred_at").all(intent.request_id)).toEqual([
|
||||
{ operation_type: "asset_cleanup_requested", result: "succeeded" },
|
||||
{ operation_type: "asset_cleanup_validated", result: "succeeded" },
|
||||
{ operation_type: "asset_cleanup_scheduled", result: "succeeded" },
|
||||
{ operation_type: "asset_cleanup_physical_completed", result: "succeeded" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user