145 lines
5.0 KiB
TypeScript
145 lines
5.0 KiB
TypeScript
import { test, type TestContext } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { createServer } from "node:http";
|
|
import { spawn } from "node:child_process";
|
|
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
import { resolve, sep } from "node:path";
|
|
|
|
const script = resolve("scripts/reprocess.mjs");
|
|
const id = "00000000-0000-4000-8000-000000000001";
|
|
const baseDataset = {
|
|
id,
|
|
source: "fabric",
|
|
state: "review",
|
|
revision_id: null,
|
|
rejection_reason: null,
|
|
map_path: "old-map.webp",
|
|
cover_path: "old-cover.webp",
|
|
panorama_count: 12,
|
|
};
|
|
const baseJob = {
|
|
id: "00000000-0000-4000-8000-000000000002",
|
|
state: "complete",
|
|
updated_at: "2026-10-04T12:00:00Z",
|
|
attempts: 3,
|
|
progress: 100,
|
|
error: null,
|
|
};
|
|
|
|
async function run(
|
|
t: TestContext,
|
|
options: {
|
|
published?: boolean;
|
|
active?: boolean;
|
|
jobConflict?: boolean;
|
|
} = {},
|
|
) {
|
|
const dataset = {
|
|
...baseDataset,
|
|
...(options.published ? { state: "published", revision_id: id } : {}),
|
|
};
|
|
const job = { ...baseJob, ...(options.active ? { state: "running" } : {}) };
|
|
const input = { validated: true, manifest: {} };
|
|
const patches: { table: string; body: Record<string, unknown> }[] = [];
|
|
const server = createServer(async (req, res) => {
|
|
const url = new URL(req.url!, "http://localhost");
|
|
const table = url.pathname.split("/").at(-1)!;
|
|
const row =
|
|
table === "datasets" ? dataset : table === "upload_jobs" ? job : input;
|
|
res.setHeader("Content-Type", "application/json");
|
|
if (req.method === "GET") {
|
|
res.end(JSON.stringify(row));
|
|
return;
|
|
}
|
|
let raw = "";
|
|
for await (const chunk of req) raw += chunk;
|
|
const body = JSON.parse(raw);
|
|
patches.push({ table, body });
|
|
if (options.jobConflict && table === "upload_jobs") {
|
|
res.end("[]");
|
|
return;
|
|
}
|
|
Object.assign(row, body);
|
|
res.end(
|
|
url.searchParams.has("select")
|
|
? JSON.stringify([{ id: table === "datasets" ? id : job.id }])
|
|
: "null",
|
|
);
|
|
});
|
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
const parent = resolve(".cache");
|
|
await mkdir(parent, { recursive: true });
|
|
const directory = await mkdtemp(`${parent}/reprocess-tests-`);
|
|
t.after(async () => {
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
assert.ok(resolve(directory).startsWith(parent + sep));
|
|
await rm(directory, { recursive: true, force: true });
|
|
});
|
|
const address = server.address();
|
|
assert.ok(address && typeof address !== "string");
|
|
const result = await new Promise<{ code: number | null; output: string }>(
|
|
(resolve, reject) => {
|
|
const child = spawn(process.execPath, [script, id], {
|
|
cwd: directory,
|
|
windowsHide: true,
|
|
env: {
|
|
...process.env,
|
|
NEXT_PUBLIC_SUPABASE_URL: `http://127.0.0.1:${address.port}`,
|
|
SUPABASE_SECRET_KEY: "test-only-secret",
|
|
},
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
let output = "";
|
|
child.stdout.on("data", (data) => (output += data));
|
|
child.stderr.on("data", (data) => (output += data));
|
|
child.on("error", reject);
|
|
child.on("exit", (code) => resolve({ code, output }));
|
|
},
|
|
);
|
|
return { ...result, dataset, job, input, patches };
|
|
}
|
|
|
|
test("reprocessing keeps the upload and resets unpublished preview and exhausted job metadata", async (t) => {
|
|
const result = await run(t);
|
|
assert.equal(result.code, 0, result.output);
|
|
assert.equal(result.dataset.state, "processing");
|
|
assert.equal(result.dataset.map_path, null);
|
|
assert.equal(result.dataset.cover_path, null);
|
|
assert.equal(result.dataset.panorama_count, 0);
|
|
assert.equal(result.input.validated, false);
|
|
assert.equal(result.job.state, "queued");
|
|
assert.equal(result.job.attempts, 0);
|
|
assert.equal(result.job.progress, 0);
|
|
assert.equal(result.job.error, null);
|
|
assert.ok(
|
|
result.patches.every(({ table }) =>
|
|
["datasets", "upload_jobs", "dataset_inputs"].includes(table),
|
|
),
|
|
);
|
|
});
|
|
|
|
test("reprocessing cannot mutate published revisions", async (t) => {
|
|
const result = await run(t, { published: true });
|
|
assert.notEqual(result.code, 0);
|
|
assert.match(result.output, /Published revisions cannot be changed/);
|
|
assert.deepEqual(result.patches, []);
|
|
});
|
|
|
|
test("reprocessing cannot take over an active worker job", async (t) => {
|
|
const result = await run(t, { active: true });
|
|
assert.notEqual(result.code, 0);
|
|
assert.match(result.output, /already queued or running/);
|
|
assert.deepEqual(result.patches, []);
|
|
});
|
|
|
|
test("a queue conflict restores the previous draft metadata and validation status", async (t) => {
|
|
const result = await run(t, { jobConflict: true });
|
|
assert.notEqual(result.code, 0);
|
|
assert.match(result.output, /Job state changed/);
|
|
assert.equal(result.dataset.state, baseDataset.state);
|
|
assert.equal(result.dataset.map_path, baseDataset.map_path);
|
|
assert.equal(result.dataset.panorama_count, baseDataset.panorama_count);
|
|
assert.equal(result.input.validated, true);
|
|
assert.equal(result.job.state, "complete");
|
|
});
|