import { createHash } from "node:crypto"; import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { createRequire } from "node:module"; import { afterEach, describe, expect, it } from "vitest"; import { DATA_TRANSFER_POLICY, defaultInstanceConfigPath, defaultLocalDataRoot, initializeLocalDataRoot, inspectInitializedLocalDataRoot, resolvePathWithinRoot, validateLocalDataRoot, validateReadOnlyAssetRoot, type LocalDataRootBoundaries, } from "../../apps/api/src/local-data-root.js"; const requireFromApi = createRequire(new URL("../../apps/api/package.json", import.meta.url)); const Database = requireFromApi("better-sqlite3"); const temporaryDirectories: string[] = []; function temporaryDirectory() { const directory = mkdtempSync(join(tmpdir(), "dada-wp0-04-")); temporaryDirectories.push(directory); return directory; } function fixture() { const base = temporaryDirectory(); const projectRoot = join(base, "project"); const downloadsRoot = join(base, "Downloads"); const assetRoot = join(base, "read-only-assets"); for (const directory of [projectRoot, downloadsRoot, assetRoot]) mkdirSync(directory); const boundaries: LocalDataRootBoundaries = { downloadsRoot, programRoot: process.cwd(), projectRoots: [projectRoot], readOnlyAssetRoots: [assetRoot], repositoryRoot: process.cwd(), }; return { assetRoot, base, boundaries, downloadsRoot, projectRoot }; } function snapshot(directory: string): string[] { if (!existsSync(directory)) return []; return readdirSync(directory, { recursive: true }).map(String).sort(); } afterEach(() => { for (const directory of temporaryDirectories.splice(0)) { if (resolve(directory).startsWith(resolve(tmpdir()))) rmSync(directory, { force: true, recursive: true }); } }); describe("TDD-WP0-DATA-001-root-validation", () => { it("derives default data and configuration paths from LOCALAPPDATA without a hardcoded user", () => { const localAppData = join(temporaryDirectory(), "LocalAppData"); expect(defaultLocalDataRoot({ LOCALAPPDATA: localAppData })).toBe(join(localAppData, "Dada", "P0A", "data")); expect(defaultInstanceConfigPath({ LOCALAPPDATA: localAppData })).toBe( join(localAppData, "Dada", "P0A", "config", "instance.json"), ); expect(() => defaultLocalDataRoot({})).toThrow("local_app_data_unavailable"); }); it("rejects repository, project, Downloads, read-only asset, junction and escaped paths without writes", () => { const { assetRoot, base, boundaries, downloadsRoot, projectRoot } = fixture(); const outside = join(base, "junction-target"); const junction = join(base, "junction-root"); mkdirSync(outside); symlinkSync(outside, junction, "junction"); expect(lstatSync(junction).isSymbolicLink()).toBe(true); const cases = [ { path: join(process.cwd(), ".dada-local", "forbidden"), reason: "repository_root" }, { path: join(projectRoot, "data"), reason: "project_root" }, { path: join(downloadsRoot, "data"), reason: "downloads_root" }, { path: join(assetRoot, "data"), reason: "read_only_asset_root" }, { path: join(junction, "data"), reason: "symbolic_link" }, ] as const; const before = snapshot(base); for (const [index, candidate] of cases.entries()) { expect(validateLocalDataRoot(candidate.path, boundaries)).toEqual({ ok: false, reason: candidate.reason, }); expect(() => initializeLocalDataRoot({ boundaries, configFile: join(base, `invalid-config-${index}`, "instance.json"), dataRoot: candidate.path, })).toThrow(candidate.reason); } expect(() => resolvePathWithinRoot(join(base, "safe"), "../escaped/file.png")).toThrow("path_escape"); const after = snapshot(base); expect(after).toEqual(before); const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_DATA_ROOT; if (evidenceDirectory) { mkdirSync(evidenceDirectory, { recursive: true }); writeFileSync(join(evidenceDirectory, "fs-before.json"), `${JSON.stringify({ entries: before }, null, 2)}\n`); } }); it("creates the fixed layout and SQLite only during explicit initialization", () => { const { base, boundaries } = fixture(); const dataRoot = join(base, "compliant-data"); const configFile = join(base, "config", "instance.json"); const initialized = initializeLocalDataRoot({ boundaries, configFile, dataRoot }); expect(initialized.status).toBe("ready"); for (const relativePath of [ "db/dada.sqlite3", "content/references", "content/generated", "content/exports", "managed-assets", "derived-assets", "staging", "logs/api", "logs/worker", "logs/supervisor", ]) { expect(existsSync(join(dataRoot, relativePath))).toBe(true); } const database = new Database(join(dataRoot, "db", "dada.sqlite3"), { readonly: true }); expect(database.prepare("select schema_version from instance_metadata").get()).toEqual({ schema_version: 1 }); database.close(); expect(JSON.parse(readFileSync(configFile, "utf8"))).toMatchObject({ initialized: true, schema_version: 1 }); const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_DATA_ROOT; if (evidenceDirectory) { mkdirSync(evidenceDirectory, { recursive: true }); writeFileSync(join(evidenceDirectory, "config-result.json"), `${JSON.stringify({ database: "db/dada.sqlite3", fixed_layout: true, path_exposed: false, status: initialized.status, }, null, 2)}\n`); } }); it("reports data_missing without recreating a deleted initialized root or database", () => { const { base, boundaries } = fixture(); const dataRoot = join(base, "initialized-data"); const configFile = join(base, "config", "instance.json"); initializeLocalDataRoot({ boundaries, configFile, dataRoot }); rmSync(join(dataRoot, "db", "dada.sqlite3")); expect(inspectInitializedLocalDataRoot({ boundaries, configFile })).toMatchObject({ database_exists: false, root_exists: true, status: "data_missing", }); expect(existsSync(join(dataRoot, "db", "dada.sqlite3"))).toBe(false); rmSync(dataRoot, { recursive: true }); expect(inspectInitializedLocalDataRoot({ boundaries, configFile })).toMatchObject({ database_exists: false, root_exists: false, status: "data_missing", }); expect(existsSync(dataRoot)).toBe(false); const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_DATA_ROOT; if (evidenceDirectory) { mkdirSync(evidenceDirectory, { recursive: true }); writeFileSync(join(evidenceDirectory, "fs-after.json"), `${JSON.stringify({ database_recreated: false, root_recreated: false, status: "data_missing", }, null, 2)}\n`); } }); it("validates a read-only manifest by hash without copying or writing its source root", () => { const { assetRoot, base } = fixture(); const manifest = join(assetRoot, "catalog.json"); writeFileSync(manifest, "{\"assets\":[]}\n"); const expectedSha256 = createHash("sha256").update(readFileSync(manifest)).digest("hex"); const before = snapshot(assetRoot); expect(validateReadOnlyAssetRoot({ dataRoot: join(base, "data"), expectedSha256, manifestRelativePath: "catalog.json", root: assetRoot, rootRef: "fixture_assets", })).toMatchObject({ ok: true, root_ref: "fixture_assets" }); expect(snapshot(assetRoot)).toEqual(before); }); }); describe("TDD-WP0-DATA-002-no-backup-migration", () => { it("does not recover deleted data or treat retained downloads as migration input", () => { const { base, boundaries } = fixture(); const dataRoot = join(base, "instance-a"); const configFile = join(base, "config-a", "instance.json"); initializeLocalDataRoot({ boundaries, configFile, dataRoot }); const businessMarker = join(dataRoot, "content", "generated", "private-record.json"); mkdirSync(dirname(businessMarker), { recursive: true }); writeFileSync(businessMarker, "private business data"); const retainedDownload = join(base, "user-downloads", "finished.png"); mkdirSync(dirname(retainedDownload)); writeFileSync(retainedDownload, "downloaded image"); rmSync(dataRoot, { recursive: true }); expect(inspectInitializedLocalDataRoot({ boundaries, configFile }).status).toBe("data_missing"); expect(existsSync(dataRoot)).toBe(false); expect(readFileSync(retainedDownload, "utf8")).toBe("downloaded image"); const replacementRoot = join(base, "instance-b"); initializeLocalDataRoot({ boundaries, configFile: join(base, "config-b", "instance.json"), dataRoot: replacementRoot, }); expect(existsSync(join(replacementRoot, "content", "generated", "private-record.json"))).toBe(false); expect(DATA_TRANSFER_POLICY).toEqual({ allowed_downloads: ["original_generation", "jpg", "png"], application_backup: false, business_import: false, editable_project_archive: false, p0b_migration: false, recovery: false, }); }); });