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

36 lines
2.9 KiB
TypeScript

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");
});
});