diff --git a/test/canonical-temp-root.test.ts b/test/canonical-temp-root.test.ts new file mode 100644 index 000000000..a042a7817 --- /dev/null +++ b/test/canonical-temp-root.test.ts @@ -0,0 +1,25 @@ +import { mkdtempSync, realpathSync, rmSync, symlinkSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { canonicalTempRoot } from "./support/canonical-temp-root"; + +const temporaryDirectories: string[] = []; + +afterEach(() => { + for (const directory of temporaryDirectories.splice(0)) { + rmSync(directory, { recursive: true, force: true }); + } +}); + +describe("canonicalTempRoot", () => { + it("resolves a symlinked temp-root alias before security-sensitive fixtures are created", () => { + const physicalRoot = realpathSync(tmpdir()); + const directory = mkdtempSync(join(physicalRoot, "noema-canonical-temp-root-")); + temporaryDirectories.push(directory); + const alias = join(directory, "alias"); + symlinkSync(physicalRoot, alias, "dir"); + + expect(canonicalTempRoot(alias)).toBe(physicalRoot); + }); +}); diff --git a/test/setup/canonical-temp-root.ts b/test/setup/canonical-temp-root.ts new file mode 100644 index 000000000..9253ed780 --- /dev/null +++ b/test/setup/canonical-temp-root.ts @@ -0,0 +1,8 @@ +import { canonicalTempRoot } from "../support/canonical-temp-root"; + +const physicalTempRoot = canonicalTempRoot(); +process.env.TMPDIR = physicalTempRoot; +if (process.platform === "win32") { + process.env.TEMP = physicalTempRoot; + process.env.TMP = physicalTempRoot; +} diff --git a/test/support/canonical-temp-root.ts b/test/support/canonical-temp-root.ts new file mode 100644 index 000000000..1bb841994 --- /dev/null +++ b/test/support/canonical-temp-root.ts @@ -0,0 +1,7 @@ +import { realpathSync } from "node:fs"; +import { tmpdir } from "node:os"; + +/** Resolve the platform temp root to its physical directory for security-sensitive fixtures. */ +export function canonicalTempRoot(root = tmpdir()) { + return realpathSync(root); +} diff --git a/vitest.config.ts b/vitest.config.ts index 71f83ace8..527b39100 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { include: ["test/**/*.test.ts", "test/**/*.test.mjs"], + setupFiles: ["test/setup/canonical-temp-root.ts"], // Real acquisition-integrity tests execute the production audit, whose // child-process boundary is itself capped at 30 seconds. Keep the outer // harness bounded but give it enough time to observe that explicit result