Files
ThothII/backend/test/workspace-preprocessing-state.test.ts
T

140 lines
5.1 KiB
TypeScript

import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, expect, test } from "vitest";
import { PreprocessingStateStore } from "../src/workspaces/preprocessing-state.js";
const roots: string[] = [];
afterEach(() => {
roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }));
});
function fixture() {
const dataRoot = mkdtempSync(join(tmpdir(), "tht-preprocessing-state-"));
roots.push(dataRoot);
return {
dataRoot,
store: new PreprocessingStateStore({ dataRoot, workspaceId: "psd-clinical" }),
};
}
test("job state creates durable 0600 JSON and enforces same-revision resume", async () => {
const { store } = fixture();
const job = await store.beginJob({
operation: "preprocess dwh",
workspaceRevision: "a".repeat(40),
descriptorBlob: "b".repeat(40),
catalogBlob: "c".repeat(40),
configDigest: "sha256:config",
bindingDigest: "sha256:bindings",
embeddingId: "ollama/qwen3-embedding:0.6b",
embeddingDimensions: 1024,
});
const path = store.jobPath(job.runId);
expect(existsSync(path)).toBe(true);
expect(statSync(path).mode & 0o777).toBe(0o600);
expect(JSON.parse(readFileSync(path, "utf8"))).toMatchObject({
schemaVersion: 2,
operation: "preprocess dwh",
workspaceId: "psd-clinical",
workspaceRevision: "a".repeat(40),
descriptorBlob: "b".repeat(40),
catalogBlob: "c".repeat(40),
configDigest: "sha256:config",
bindingDigest: "sha256:bindings",
embeddingId: "ollama/qwen3-embedding:0.6b",
embeddingDimensions: 1024,
});
await expect(store.beginJob({
operation: "preprocess dwh",
runId: job.runId,
workspaceRevision: "d".repeat(40),
descriptorBlob: "b".repeat(40),
catalogBlob: "c".repeat(40),
configDigest: "sha256:config",
bindingDigest: "sha256:bindings",
embeddingId: "ollama/qwen3-embedding:0.6b",
embeddingDimensions: 1024,
})).rejects.toMatchObject({ code: "preprocessing_resume_mismatch" });
await expect(store.beginJob({
operation: "preprocess dwh",
runId: job.runId,
workspaceRevision: "a".repeat(40),
descriptorBlob: "b".repeat(40),
catalogBlob: "c".repeat(40),
configDigest: "sha256:config",
bindingDigest: "sha256:bindings",
embeddingId: "ollama/replacement-embedding",
embeddingDimensions: 1024,
})).rejects.toMatchObject({ code: "preprocessing_resume_mismatch" });
const resumed = await store.beginJob({
operation: "preprocess dwh",
runId: job.runId,
workspaceRevision: "a".repeat(40),
descriptorBlob: "b".repeat(40),
catalogBlob: "c".repeat(40),
configDigest: "sha256:config",
bindingDigest: "sha256:bindings",
embeddingId: "ollama/qwen3-embedding:0.6b",
embeddingDimensions: 1024,
});
expect(resumed.runId).toBe(job.runId);
});
test("writer lock rejects a concurrent contender and the kernel releases it after holder death", async () => {
const f = fixture();
const other = new PreprocessingStateStore({ dataRoot: f.dataRoot, workspaceId: "psd-clinical" });
const first = await f.store.acquireWriterLock();
await expect(other.acquireWriterLock()).rejects.toMatchObject({ code: "preprocessing_conflict" });
process.kill(first.holderPid, "SIGKILL");
const deadline = Date.now() + 5_000;
while (Date.now() < deadline) {
try {
const recovered = await other.acquireWriterLock();
await recovered.release();
return;
} catch (error) {
if ((error as { code?: string }).code !== "preprocessing_conflict") throw error;
await new Promise((resolve) => setTimeout(resolve, 50));
}
}
throw new Error("writer lock was not released after holder death");
});
test("session inventory guard blocks resumable sessions pinned to a different revision", async () => {
const { store } = fixture();
await expect(store.assertSessionInventoryCompatible("a".repeat(40), [
{ id: "open-other", status: "closed", archived: false, workspaceRevision: "b".repeat(40) },
])).rejects.toMatchObject({ code: "preprocessing_conflict" });
await expect(store.assertSessionInventoryCompatible("a".repeat(40), [
{ id: "current", status: "open", archived: false, workspaceRevision: "a".repeat(40) },
{ id: "finalized", status: "finalized", archived: false, workspaceRevision: "b".repeat(40) },
{ id: "archived", status: "closed", archived: true, workspaceRevision: "c".repeat(40) },
])).resolves.toBeUndefined();
});
test("candidate and review artifacts are digest-bound durable files", async () => {
const { store } = fixture();
const runId = "1".repeat(32);
const candidate = await store.writeFkCandidates(runId, `tables: []
`);
const review = await store.writeFkReview(runId, {
reviewedCandidatesDigest: candidate.digest,
annotationsDigest: "sha256:annotations",
workspaceRevision: "a".repeat(40),
});
expect(candidate.digest).toMatch(/^sha256:[0-9a-f]{64}$/);
expect(review.digest).toMatch(/^sha256:[0-9a-f]{64}$/);
expect(store.readFkCandidates(runId)?.digest).toBe(candidate.digest);
expect(store.readFkReview(runId)?.reviewedCandidatesDigest).toBe(candidate.digest);
});