import { describe, expect, it, afterEach } from "vitest"; import { mkdtemp, readFile, chmod, writeFile, symlink, lstat } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { PreprocessingStateStore } from "../src/workspaces/preprocessing-state.js"; const roots: string[] = []; afterEach(async () => { for (const root of roots.splice(0)) await import("node:fs/promises").then(fs => fs.rm(root, { recursive: true, force: true })); }); async function makeStore() { const root = await mkdtemp(join(tmpdir(), "thoth-state-")); roots.push(root); return { root, store: new PreprocessingStateStore(root) }; } const input = { workspaceId: "abc-workspace" as never, revision: "a".repeat(40), operation: "schema" }; describe("durable preprocessing state", () => { it("creates and replays exact state with immutable identity", async () => { const { root, store } = await makeStore(); const state = await store.create(input); const replay = await store.loadForResume({ ...input, runId: state.runId }); expect(replay).toEqual(state); await expect(store.transition(state.runId, { phase: "introspected", workspaceId: "evil" } as never)).rejects.toThrow("preprocessing_conflict"); }); it("rejects tampered, traversal, mode and version state before returning it", async () => { const { root, store } = await makeStore(); const state = await store.create(input); const path = join(root, "preprocessing", "jobs", `${state.runId}.json`); const original = JSON.parse(await readFile(path, "utf8")); await writeFile(path, JSON.stringify({ ...original, schemaVersion: 9 })); await expect(store.load({ ...input, runId: state.runId })).rejects.toThrow(); await writeFile(path, JSON.stringify(original)); await chmod(path, 0o644); await expect(store.load({ ...input, runId: state.runId })).rejects.toThrow("preprocessing_conflict"); await expect(store.load({ ...input, runId: "../" + state.runId })).rejects.toThrow(); }); it("writes candidate artifacts atomically with bounded bytes and immutable links", async () => { const { root, store } = await makeStore(); const state = await store.create(input); const bytes = new TextEncoder().encode("tables: []\n"); const artifact = await store.writeFkCandidate(state.runId, bytes); expect(artifact.bytes).toBe(bytes.byteLength); const candidate = lstat(join(root, "preprocessing", "fk-candidates", `${state.runId}.yaml`)); expect((await candidate).nlink).toBe(1); await expect(store.writeFkCandidate(state.runId, new Uint8Array(1 << 20))).rejects.toThrow("preprocessing_conflict"); }); it("rejects a symlinked state root", async () => { const real = await mkdtemp(join(tmpdir(), "thoth-state-real-")); const link = join(tmpdir(), `thoth-state-link-${Date.now()}`); roots.push(real, link); await symlink(real, link); expect(() => new PreprocessingStateStore(link)).toThrow("preprocessing_conflict"); }); });