import { execFile } from "node:child_process"; import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; import { afterEach, expect, test } from "vitest"; import { WorkspaceRegistry } from "../src/workspaces/registry.js"; import type { WorkspaceRegistryConfig } from "../src/workspaces/types.js"; const runFile = promisify(execFile); const temporaryRoots: string[] = []; afterEach(() => { temporaryRoots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })); }); async function git(cwd: string, args: string[]): Promise { const { stdout } = await runFile("git", args, { cwd }); return stdout.trim(); } const descriptor = `workspace: schema_version: 4 id: research name: Research language: en dwh: engine: postgres database: analytics schema: mart supported_transports: [postgres_direct] evidence: source: type: filesystem uri: research/evidence `; function config(root: string, remoteUrl: string): WorkspaceRegistryConfig { return { root, remoteUrl, branch: "main", gitAuthorName: "Evidence Registry Test", gitAuthorEmail: "evidence-registry@example.invalid", installationId: "test", secretRoots: [], maxImportBytes: 1024, maxImportEntries: 1, }; } async function fixture(): Promise<{ root: string; remote: string; commit: string }> { const root = mkdtempSync(join(tmpdir(), "thoth-registry-evidence-")); temporaryRoots.push(root); const remote = join(root, "remote.git"); const source = join(root, "source"); await git(root, ["init", "--bare", "--initial-branch=main", remote]); mkdirSync(source); await git(source, ["init", "--initial-branch=main"]); await git(source, ["config", "user.name", "Evidence Registry Test"]); await git(source, ["config", "user.email", "evidence-registry@example.invalid"]); writeFileSync(join(source, "thoth-workspaces.yaml"), "schema_version: 1\nworkspaces:\n - id: research\n name: Research\n"); mkdirSync(join(source, "research", "evidence"), { recursive: true }); writeFileSync(join(source, "research", "workspace.yaml"), descriptor); writeFileSync(join(source, "research", "evidence", "guide.md"), "# guide\n"); await git(source, ["add", "-A"]); await git(source, ["commit", "-m", "initial"]); await git(source, ["remote", "add", "origin", remote]); await git(source, ["push", "origin", "main"]); const commit = await git(source, ["rev-parse", "HEAD"]); return { root, remote, commit }; } test("activation materializes filesystem Evidence and chains its manifest into snapshot.json", async () => { const fixtureValue = await fixture(); const registryRoot = join(fixtureValue.root, "registry"); const registry = new WorkspaceRegistry(config(registryRoot, fixtureValue.remote)); await registry.bootstrap(); const evidence = join(registryRoot, "snapshots", fixtureValue.commit, "research", "evidence"); const manifestPath = join(registryRoot, "snapshots", fixtureValue.commit, "research", "evidence.manifest.json"); expect(readFileSync(join(evidence, "guide.md"), "utf8")).toBe("# guide\n"); const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); expect(manifest).toMatchObject({ schemaVersion: 1, workspace: "research", commit: fixtureValue.commit, entryCount: 1 }); const snapshotManifest = JSON.parse(readFileSync(join(registryRoot, "snapshots", fixtureValue.commit, "snapshot.json"), "utf8")); expect(snapshotManifest.files["research/evidence.manifest.json"]).toMatch(/^[0-9a-f]{64}$/); // Re-activation verifies the existing materialized root. await expect(registry.bootstrap()).resolves.toMatchObject({ degraded: false }); }); test("activation fails closed when the materialized Evidence manifest is tampered", async () => { const fixtureValue = await fixture(); const registryRoot = join(fixtureValue.root, "registry"); const registry = new WorkspaceRegistry(config(registryRoot, fixtureValue.remote)); await registry.bootstrap(); const manifestPath = join(registryRoot, "snapshots", fixtureValue.commit, "research", "evidence.manifest.json"); writeFileSync(manifestPath, `${JSON.stringify({ schemaVersion: 1, workspace: "research", commit: fixtureValue.commit, tree: "0".repeat(40), entryCount: 0, totalBytes: 0, files: {} })}\n`); await expect(registry.bootstrap()).rejects.toMatchObject({ code: "workspace_invalid" }); });