36 lines
2.9 KiB
TypeScript
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");
|
|
});
|
|
});
|