import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { sandboxEnv, withProcessEnv } from "../../test/harness"; // The template's own slug placeholder is the extension name under test. const slug = "{{extension-slug}}"; const enable = `PI_${slug.toUpperCase().replaceAll("-", "_")}_DEBUG`; const posix = process.platform !== "win32"; let root: string | undefined; afterEach(() => { if (root) rmSync(root, { recursive: true, force: true }); root = undefined; vi.resetModules(); }); function sandbox() { root = mkdtempSync(join(tmpdir(), "pi-ext-debug-")); const env = sandboxEnv(root, { env: { [enable]: "1" } }); const directory = join(root, ".test-state", "pi-ext", "debug", slug); return { env, state: join(root, ".test-state"), directory }; } /** Load a fresh template copy, log one event and one span, then close. */ function logOnce(env: NodeJS.ProcessEnv) { return withProcessEnv(env, async () => { vi.resetModules(); const { defineDebug } = await import("./templates/pi-ext-debug.ts"); const { dbg, span, closeDebug } = defineDebug({ events: ["session.start"], spans: ["request"], fields: { count: "number" }, } as const); expect(() => { dbg?.("session.start", { count: 1 }); span?.("request")("finish", { count: 2 }); }).not.toThrow(); closeDebug(); }); } const records = (directory: string) => readdirSync(directory).flatMap((file) => readFileSync(join(directory, file), "utf8") .trim() .split("\n") .map((line) => JSON.parse(line)), ); describe("pi-ext-debug template directories", () => { it("creates missing private directories and a private file", async () => { const { env, directory } = sandbox(); await logOnce(env); expect(records(directory).map((record) => record.event)).toEqual([ "session.start", "request.start", "request.finish", ]); if (posix) { for (const part of [ join(directory, "..", ".."), join(directory, ".."), directory, ]) expect(lstatSync(part).mode & 0o777).toBe(0o700); const [file] = readdirSync(directory); expect(lstatSync(join(directory, file)).mode & 0o777).toBe(0o600); } }); it("reuses existing private directories for a new file", async () => { const { env, directory } = sandbox(); await logOnce(env); await logOnce(env); expect(readdirSync(directory)).toHaveLength(2); expect(records(directory)).toHaveLength(6); }); it.runIf(posix)("refuses a directory readable by others", async () => { const { env, state, directory } = sandbox(); mkdirSync(join(state, "pi-ext"), { recursive: true, mode: 0o700 }); chmodSync(join(state, "pi-ext"), 0o755); await logOnce(env); expect(existsSync(directory)).toBe(false); }); it.runIf(posix)("refuses a symlinked extension directory", async () => { const { env, state, directory } = sandbox(); const target = join(state, "elsewhere"); mkdirSync(target, { recursive: true, mode: 0o700 }); mkdirSync(join(directory, ".."), { recursive: true, mode: 0o700 }); chmodSync(join(state, "pi-ext"), 0o700); symlinkSync(target, directory); await logOnce(env); expect(readdirSync(target)).toEqual([]); }); });