From 8cf18f053fc1c5cae1b1e45987cd1ff265eb0b1f Mon Sep 17 00:00:00 2001 From: FND Date: Sat, 5 Sep 2026 14:36:55 +0200 Subject: [PATCH 1/2] fix(test): sandbox test-run data before store imports Fixes #1455. Allocate and clean a process-owned temporary data directory in the test preload, and preserve explicit controlled-HOME fixture coverage. --- bunfig.toml | 2 +- packages/server/ai-disabled.test.ts | 51 ++--- packages/shared/data-dir.test.ts | 213 +++++++++++++++++++- packages/shared/improvement-hooks.test.ts | 3 +- packages/shared/prompts-integration.test.ts | 3 +- tests/setup/feedback-archive-off.ts | 40 ++-- 6 files changed, 259 insertions(+), 53 deletions(-) diff --git a/bunfig.toml b/bunfig.toml index f5988b706..c9cafa8c1 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -5,4 +5,4 @@ minimumReleaseAge = 604800 # 7 days in seconds minimumReleaseAgeExcludes = ["@opencode-ai/ai", "@opencode-ai/client", "@opencode-ai/plugin", "@opencode-ai/protocol", "@opencode-ai/schema", "@pierre/diffs", "@pierre/theme", "@pierre/theming", "@plannotator/atomic-editor", "@plannotator/markdown-editor", "@plannotator/webtui"] [test] -preload = ["./packages/ui/test-setup/happy-dom.ts", "./tests/setup/feedback-archive-off.ts"] +preload = ["./tests/setup/feedback-archive-off.ts", "./packages/ui/test-setup/happy-dom.ts"] diff --git a/packages/server/ai-disabled.test.ts b/packages/server/ai-disabled.test.ts index 8210fc2da..b1efc1640 100644 --- a/packages/server/ai-disabled.test.ts +++ b/packages/server/ai-disabled.test.ts @@ -1,9 +1,6 @@ import { expect, test } from "bun:test"; import type { PRMetadata } from "@plannotator/shared/pr-types"; import type { WorktreePool } from "@plannotator/shared/worktree-pool"; -import { existsSync, mkdtempSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; import { fileURLToPath } from "node:url"; const SPA_HTML = "test"; @@ -169,42 +166,36 @@ async function verifyDisabledServers(): Promise { } async function runInIsolatedDataDirectory(): Promise { - const dataDir = mkdtempSync(join(tmpdir(), "plannotator-ai-disabled-")); const childEnv = { ...process.env, [ISOLATED_CHILD_ENV]: "1", PLANNOTATOR_AI: "disabled", - PLANNOTATOR_DATA_DIR: dataDir, PLANNOTATOR_REMOTE: "0", }; delete childEnv.PLANNOTATOR_PORT; - try { - // storage.ts captures PLANNOTATOR_DATA_DIR at module load, so a child - // process is required to keep this test isolated regardless of which - // test files Bun evaluated first in the parent process. - const child = Bun.spawn( - [process.execPath, "test", fileURLToPath(import.meta.url)], - { - cwd: process.cwd(), - env: childEnv, - stdout: "pipe", - stderr: "pipe", - }, + // storage.ts captures PLANNOTATOR_DATA_DIR at module load, so a child + // process is required to keep this test isolated regardless of which + // test files Bun evaluated first in the parent process. The test preload + // owns the child process's data directory and cleans it up on exit. + const child = Bun.spawn( + [process.execPath, "test", fileURLToPath(import.meta.url)], + { + cwd: process.cwd(), + env: childEnv, + stdout: "pipe", + stderr: "pipe", + }, + ); + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]); + if (exitCode !== 0) { + throw new Error( + `Isolated disabled-AI test failed (${exitCode})\n${stdout}\n${stderr}`, ); - const [exitCode, stdout, stderr] = await Promise.all([ - child.exited, - new Response(child.stdout).text(), - new Response(child.stderr).text(), - ]); - if (exitCode !== 0) { - throw new Error( - `Isolated disabled-AI test failed (${exitCode})\n${stdout}\n${stderr}`, - ); - } - expect(existsSync(join(dataDir, "history"))).toBe(true); - } finally { - rmSync(dataDir, { recursive: true, force: true }); } } diff --git a/packages/shared/data-dir.test.ts b/packages/shared/data-dir.test.ts index 6fa472870..381c82298 100644 --- a/packages/shared/data-dir.test.ts +++ b/packages/shared/data-dir.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -81,3 +81,214 @@ describe("getPlannotatorDataDir", () => { expect(dir).toBe(join(fakeHome, ".plannotator")); }); }); + +test("bun test isolates imported stores, inherits runtime writes, and cleans only its owned directory after hooks", async () => { + const repoRoot = join(import.meta.dir, "../.."); + const home = join(fakeHome, "home"); + const xdg = join(fakeHome, "xdg"); + const tempRoot = join(fakeHome, "tmp"); + const contributor = join(fakeHome, "contributor-data"); + const override = join(fakeHome, "explicit-override"); + for (const dir of [home, xdg, tempRoot, contributor, override]) mkdirSync(dir); + const contributorConfig = JSON.stringify({ displayName: "contributor", feedbackHistory: true }); + writeFileSync(join(contributor, "config.json"), contributorConfig); + writeFileSync(join(override, "keep"), "caller-owned"); + + // These fixtures live outside the repository and are run by exact filename: + // a nested `bun test` must load the real bunfig, never rediscover this test. + const storesFile = join(fakeHome, "stores.ts"); + const runtimeFile = join(fakeHome, "runtime.ts"); + const nestedFile = join(fakeHome, "nested.test.ts"); + const fixtureFile = join(fakeHome, "preload.test.ts"); + const reportFile = join(fakeHome, "after-all.json"); + const runtimeReport = join(fakeHome, "runtime.json"); + const nestedReport = join(fakeHome, "nested.json"); + + writeFileSync(storesFile, ` +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join, sep } from "node:path"; +// Static imports are essential: storage captures DATA_DIR during evaluation. +import { saveToHistory, saveAnnotateSubmission } from ${JSON.stringify(join(import.meta.dir, "storage.ts"))}; +import { loadConfig, saveConfig, resolveFeedbackHistory } from ${JSON.stringify(join(import.meta.dir, "config.ts"))}; +import { appendFeedbackRecord } from ${JSON.stringify(join(import.meta.dir, "feedback-archive.ts"))}; +export { loadConfig, saveConfig, resolveFeedbackHistory, appendFeedbackRecord }; + +export function writeStores(project: string) { + const dataDir = process.env.PLANNOTATOR_DATA_DIR!; + const history = saveToHistory(project, "plan", "history:" + project).path; + const submission = saveAnnotateSubmission(project, "plan", "submission:" + project); + for (const path of [history, submission]) assert.ok(path.startsWith(dataDir + sep), path); + assert.equal(readFileSync(history, "utf-8"), "history:" + project); + assert.equal(readFileSync(submission, "utf-8"), "submission:" + project); + saveConfig({ displayName: project }); + assert.equal(loadConfig().displayName, project); + assert.equal(JSON.parse(readFileSync(join(dataDir, "config.json"), "utf-8")).displayName, project); + return { dataDir, history, submission }; +} + +export async function runChild(args: string[], env = process.env) { + const child = Bun.spawn({ + cmd: [process.execPath, ...args], + cwd: ${JSON.stringify(repoRoot)}, + env, + stdout: "pipe", + stderr: "pipe", + }); + const timer = setTimeout(() => child.kill(), 12_000); + try { + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]); + assert.equal(exitCode, 0, stdout + stderr); + } finally { + clearTimeout(timer); + } +} +`); + + writeFileSync(runtimeFile, ` +import assert from "node:assert/strict"; +import { writeFileSync } from "node:fs"; +import { writeStores, resolveFeedbackHistory } from ${JSON.stringify(storesFile)}; +assert.equal(resolveFeedbackHistory({ feedbackHistory: true }), false); +writeFileSync(${JSON.stringify(runtimeReport)}, JSON.stringify(writeStores("runtime"))); +`); + + writeFileSync(nestedFile, ` +import { afterAll, test } from "bun:test"; +import assert from "node:assert/strict"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname } from "node:path"; +import { writeStores } from ${JSON.stringify(storesFile)}; +let writes; +test("a nested test run owns a fresh sandbox rather than its parent's", () => { + const dataDir = process.env.PLANNOTATOR_DATA_DIR!; + assert.notEqual(dataDir, process.env.PARENT_DATA_DIR); + assert.equal(dirname(dataDir), ${JSON.stringify(tempRoot)}); + writes = writeStores("nested"); +}); +afterAll(() => { + assert.equal(readFileSync(writes.history, "utf-8"), "history:nested"); + assert.ok(existsSync(process.env.PARENT_DATA_DIR!)); + writeFileSync(${JSON.stringify(nestedReport)}, JSON.stringify(writes)); +}); +`); + + writeFileSync(fixtureFile, ` +import { afterAll, test } from "bun:test"; +import assert from "node:assert/strict"; +import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { + appendFeedbackRecord, loadConfig, resolveFeedbackHistory, runChild, saveConfig, writeStores, +} from ${JSON.stringify(storesFile)}; +const owned = process.env.PLANNOTATOR_DATA_DIR!; +const override = ${JSON.stringify(override)}; +let writes; + +function assertContributorUntouched() { + assert.deepEqual(readdirSync(${JSON.stringify(contributor)}), ["config.json"]); + assert.equal(readFileSync(${JSON.stringify(join(contributor, "config.json"))}, "utf-8"), ${JSON.stringify(contributorConfig)}); +} + +test("preloading precedes storage imports without disabling explicit overrides", async () => { + assert.equal(dirname(owned), ${JSON.stringify(tempRoot)}); + assert.ok(existsSync(owned)); + assert.deepEqual(loadConfig(), {}); + // The contributor explicitly enabled feedback history in both env and config. + assert.equal(resolveFeedbackHistory({ feedbackHistory: true }), false); + writes = writeStores("parent"); + + const savedHistory = process.env.PLANNOTATOR_FEEDBACK_HISTORY!; + try { + process.env.PLANNOTATOR_DATA_DIR = override; + process.env.PLANNOTATOR_FEEDBACK_HISTORY = "1"; + saveConfig({ displayName: "override" }); + assert.equal(loadConfig().displayName, "override"); + assert.equal(resolveFeedbackHistory(loadConfig()), true); + const input = { project: "preload", surface: "review", decision: "feedback", feedback: "override feedback" } as const; + const overrideIndex = appendFeedbackRecord(input); + assert.equal(overrideIndex, join(override, "feedback", "preload", "index.jsonl")); + assert.equal(JSON.parse(readFileSync(overrideIndex!, "utf-8")).feedback, "override feedback"); + + process.env.PLANNOTATOR_DATA_DIR = owned; + assert.equal(loadConfig().displayName, "parent"); + const restoredIndex = appendFeedbackRecord({ ...input, feedback: "restored feedback" }); + assert.equal(restoredIndex, join(owned, "feedback", "preload", "index.jsonl")); + assert.equal(JSON.parse(readFileSync(restoredIndex!, "utf-8")).feedback, "restored feedback"); + } finally { + process.env.PLANNOTATOR_DATA_DIR = owned; + process.env.PLANNOTATOR_FEEDBACK_HISTORY = savedHistory; + } + assert.equal(resolveFeedbackHistory(loadConfig()), false); + assertContributorUntouched(); + + await runChild(["test", "--timeout", "10000", ${JSON.stringify(nestedFile)}], { + ...process.env, PARENT_DATA_DIR: owned, + }); + const nested = JSON.parse(readFileSync(${JSON.stringify(nestedReport)}, "utf-8")); + assert.notEqual(nested.dataDir, owned); + assert.equal(existsSync(nested.dataDir), false); + assert.equal(readFileSync(writes.history, "utf-8"), "history:parent"); +}); + +afterAll(async () => { + // Read an earlier write before anything can recreate a prematurely removed dir. + assert.equal(readFileSync(writes.submission, "utf-8"), "submission:parent"); + await runChild(["run", ${JSON.stringify(runtimeFile)}]); + const runtime = JSON.parse(readFileSync(${JSON.stringify(runtimeReport)}, "utf-8")); + assert.equal(runtime.dataDir, owned); + assert.equal(readFileSync(runtime.history, "utf-8"), "history:runtime"); + assertContributorUntouched(); + writeFileSync(${JSON.stringify(reportFile)}, JSON.stringify(writeStores("after-all"))); + // Deliberately exit with a caller-owned override selected. Cleanup must use + // the preload's captured path, not whichever env value a test leaves behind. + process.env.PLANNOTATOR_DATA_DIR = override; +}); +`); + + const child = Bun.spawn({ + cmd: [process.execPath, "test", "--timeout", "15000", fixtureFile], + cwd: repoRoot, + // Do not inherit any real data/home/temp location, even against the unfixed + // preload. Runtime descendants then inherit only these controlled values. + env: { + PATH: process.env.PATH ?? "", + HOME: home, + USERPROFILE: home, + XDG_DATA_HOME: xdg, + TMPDIR: tempRoot, + TMP: tempRoot, + TEMP: tempRoot, + PLANNOTATOR_DATA_DIR: contributor, + PLANNOTATOR_FEEDBACK_HISTORY: "1", + }, + stdout: "pipe", + stderr: "pipe", + }); + const timer = setTimeout(() => child.kill(), 25_000); + try { + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]); + if (exitCode !== 0) throw new Error(`preload regression subprocess failed:\n${stdout}${stderr}`); + + const report = JSON.parse(readFileSync(reportFile, "utf-8")); + expect(existsSync(report.dataDir)).toBe(false); + expect(readdirSync(contributor)).toEqual(["config.json"]); + expect(readFileSync(join(contributor, "config.json"), "utf-8")).toBe(contributorConfig); + expect(readFileSync(join(override, "keep"), "utf-8")).toBe("caller-owned"); + expect(JSON.parse(readFileSync(join(override, "config.json"), "utf-8")).displayName).toBe("override"); + expect(JSON.parse(readFileSync(join(override, "feedback", "preload", "index.jsonl"), "utf-8")).feedback) + .toBe("override feedback"); + expect(existsSync(join(home, ".plannotator"))).toBe(false); + expect(existsSync(join(xdg, "plannotator"))).toBe(false); + } finally { + clearTimeout(timer); + } +}, 35_000); diff --git a/packages/shared/improvement-hooks.test.ts b/packages/shared/improvement-hooks.test.ts index b915441d1..610c1c474 100644 --- a/packages/shared/improvement-hooks.test.ts +++ b/packages/shared/improvement-hooks.test.ts @@ -60,7 +60,8 @@ async function runScenario(setup: { `, ], { - env: { ...process.env, HOME: TEST_HOME }, + // Exercise the fake HOME rather than inheriting the parent test sandbox. + env: { ...process.env, HOME: TEST_HOME, USERPROFILE: TEST_HOME, PLANNOTATOR_DATA_DIR: "" }, cwd: join(import.meta.dir, "../.."), stdout: "pipe", stderr: "pipe", diff --git a/packages/shared/prompts-integration.test.ts b/packages/shared/prompts-integration.test.ts index 1b4bbfc33..69375145a 100644 --- a/packages/shared/prompts-integration.test.ts +++ b/packages/shared/prompts-integration.test.ts @@ -34,7 +34,8 @@ function cleanTestHome() { async function runScript(script: string): Promise { const proc = Bun.spawn(["bun", "-e", script], { - env: { ...process.env, HOME: TEST_HOME }, + // Exercise the fake HOME rather than inheriting the parent test sandbox. + env: { ...process.env, HOME: TEST_HOME, USERPROFILE: TEST_HOME, PLANNOTATOR_DATA_DIR: "" }, cwd: PROJECT_ROOT, stdout: "pipe", stderr: "pipe", diff --git a/tests/setup/feedback-archive-off.ts b/tests/setup/feedback-archive-off.ts index 651387fa9..a8cbe674e 100644 --- a/tests/setup/feedback-archive-off.ts +++ b/tests/setup/feedback-archive-off.ts @@ -1,22 +1,24 @@ +import { afterAll } from "bun:test"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + /** - * Test-suite default: the durable feedback archive is OFF. - * - * The archive is default-on in production and writes to the resolved data - * directory at decision-settlement time. Most server tests boot a real plan, - * review, or annotate server WITHOUT redirecting PLANNOTATOR_DATA_DIR (plan - * and annotate history already go to the real dir because storage.ts captures - * its data directory at import time), so leaving the archive on would have - * every one of those tests deposit records in the contributor's own - * ~/.plannotator/feedback — on CI and on every machine that runs `bun test`. - * The repo's testing rules forbid touching the real user data dir, so the - * suite opts out globally here. - * - * Set unconditionally rather than only when unset: a stray - * PLANNOTATOR_FEEDBACK_HISTORY=1 in a contributor's shell must not silently - * turn the whole suite back into a writer. - * - * The archive's own tests opt back in by setting the variable inside their - * test bodies (restored in afterEach), which is also how they exercise the - * opt-out path. + * Sandbox every test-run store before production modules capture their paths. + * Override even a contributor's configured data directory; tests that need a + * different directory can set and restore the env var inside their bodies. */ +const testDataDir = mkdtempSync(join(tmpdir(), "plannotator-test-")); +process.env.PLANNOTATOR_DATA_DIR = testDataDir; + +// A preload's global afterAll runs after file hooks and their awaited subprocesses. +// Use the runner lifecycle: bun test does not reliably emit process "exit". +// Only this process owns this path: never clean up the current env value, which +// a test may have overridden, or a directory inherited from a parent test run. +afterAll(() => { + rmSync(testDataDir, { recursive: true, force: true }); +}); + +// Keep the archive off by default, even when enabled in the contributor's shell. +// Archive tests opt back in inside their bodies and restore it in afterEach. process.env.PLANNOTATOR_FEEDBACK_HISTORY = "0"; From fa3a8fbe1d49634d24a2138c18927f94f674d45b Mon Sep 17 00:00:00 2001 From: FND Date: Sat, 5 Sep 2026 15:04:35 +0200 Subject: [PATCH 2/2] fix(storage): resolve data directory when reading and writing Resolve plan and history paths from the active data-directory configuration instead of capturing a temporary test override at import time. This reproduces and fixes the four order-dependent annotate CI failures reported in #1464; keep both successful persistence and failed-write draft recovery assertions intact. --- .../server/call-flow-install-endpoint.test.ts | 12 ++----- packages/server/storage.test.ts | 36 +++++++++++++++++++ packages/shared/storage.ts | 16 ++++----- 3 files changed, 46 insertions(+), 18 deletions(-) diff --git a/packages/server/call-flow-install-endpoint.test.ts b/packages/server/call-flow-install-endpoint.test.ts index dec2f3399..5e331e218 100644 --- a/packages/server/call-flow-install-endpoint.test.ts +++ b/packages/server/call-flow-install-endpoint.test.ts @@ -6,15 +6,9 @@ import { join } from 'node:path'; import type { CallFlowInstallStage, CallFlowNodePreflight, CallFlowRuntimeInstallResult } from '@plannotator/shared/call-flow'; // PLANNOTATOR_DATA_DIR is only ever changed INSIDE tests (boot() below) and -// restored to its original value after each one. It must never be overridden -// at module-eval time: bun evaluates every test file's module before running -// tests in one shared process, and Pi's generated/storage.ts caches its data -// dir at import time. A module-eval override here makes storage's cached dir -// and later files' live getPlannotatorDataDir() calls disagree, which is -// exactly the Pi annotate-history / durable-submit CI failure this comment -// guards against. Config writes made by these tests target whatever dir the -// process's config module froze at first import; the snapshot/restore in -// afterAll below keeps those writes from leaking into a real config.json. +// restored after each one. Module-eval overrides would leak into other test +// files because Bun runs the suite in one shared process. The config +// snapshot/restore in afterAll also protects against shared config state. const originalDataDir = process.env.PLANNOTATOR_DATA_DIR; const originalPort = process.env.PLANNOTATOR_PORT; const originalPath = process.env.PATH; diff --git a/packages/server/storage.test.ts b/packages/server/storage.test.ts index be005caa7..8bdee91f2 100644 --- a/packages/server/storage.test.ts +++ b/packages/server/storage.test.ts @@ -174,3 +174,39 @@ describe("listVersions", () => { expect(versions[0].timestamp).toBeTruthy(); }); }); + +describe("PLANNOTATOR_DATA_DIR", () => { + test("isolates plan and history data when the data directory changes after import", () => { + const savedDataDir = process.env.PLANNOTATOR_DATA_DIR; + const firstDir = makeTempDir(); + const secondDir = makeTempDir(); + const project = "data-dir-project"; + const slug = "data-dir-plan"; + + try { + process.env.PLANNOTATOR_DATA_DIR = firstDir; + savePlan(slug, "# First plan"); + saveToHistory(project, slug, "# First version"); + expect(readFileSync(join(firstDir, "plans", `${slug}.md`), "utf-8")).toBe("# First plan"); + expect(getPlanVersion(project, slug, 1)).toBe("# First version"); + expect(getVersionCount(project, slug)).toBe(1); + + process.env.PLANNOTATOR_DATA_DIR = secondDir; + expect(getPlanVersion(project, slug, 1)).toBeNull(); + expect(getVersionCount(project, slug)).toBe(0); + savePlan(slug, "# Second plan"); + saveToHistory(project, slug, "# Second version"); + expect(readFileSync(join(secondDir, "plans", `${slug}.md`), "utf-8")).toBe("# Second plan"); + expect(getPlanVersion(project, slug, 1)).toBe("# Second version"); + expect(getVersionCount(project, slug)).toBe(1); + + process.env.PLANNOTATOR_DATA_DIR = firstDir; + expect(readFileSync(join(firstDir, "plans", `${slug}.md`), "utf-8")).toBe("# First plan"); + expect(getPlanVersion(project, slug, 1)).toBe("# First version"); + expect(getVersionCount(project, slug)).toBe(1); + } finally { + if (savedDataDir === undefined) delete process.env.PLANNOTATOR_DATA_DIR; + else process.env.PLANNOTATOR_DATA_DIR = savedDataDir; + } + }); +}); diff --git a/packages/shared/storage.ts b/packages/shared/storage.ts index cc4e12f66..5c3662043 100644 --- a/packages/shared/storage.ts +++ b/packages/shared/storage.ts @@ -13,8 +13,6 @@ import { sanitizeTag } from "./project"; import { resolveUserPath } from "./resolve-file"; import { getPlannotatorDataDir } from "./data-dir"; -const DATA_DIR = getPlannotatorDataDir(); - /** * Get the plan storage directory, creating it if needed. * Cross-platform: uses os.homedir() for Windows/macOS/Linux compatibility. @@ -26,7 +24,7 @@ export function getPlanDir(customPath?: string | null): string { if (customPath?.trim()) { planDir = resolveUserPath(customPath); } else { - planDir = join(DATA_DIR, "plans"); + planDir = join(getPlannotatorDataDir(), "plans"); } mkdirSync(planDir, { recursive: true }); @@ -195,7 +193,7 @@ export function readArchivedPlan(filename: string, customPath?: string | null): * Not affected by the customPath setting (that only affects decision saves). */ export function getHistoryDir(project: string, slug: string): string { - const historyDir = join(DATA_DIR, "history", project, slug); + const historyDir = join(getPlannotatorDataDir(), "history", project, slug); mkdirSync(historyDir, { recursive: true }); return historyDir; } @@ -294,7 +292,7 @@ export function getPlanVersion( slug: string, version: number ): string | null { - const historyDir = join(DATA_DIR, "history", project, slug); + const historyDir = join(getPlannotatorDataDir(), "history", project, slug); const fileName = `${String(version).padStart(3, "0")}.md`; const filePath = join(historyDir, fileName); @@ -314,7 +312,7 @@ export function getPlanVersionPath( slug: string, version: number ): string | null { - const historyDir = join(DATA_DIR, "history", project, slug); + const historyDir = join(getPlannotatorDataDir(), "history", project, slug); const fileName = `${String(version).padStart(3, "0")}.md`; const filePath = join(historyDir, fileName); return existsSync(filePath) ? filePath : null; @@ -325,7 +323,7 @@ export function getPlanVersionPath( * Returns 0 if the directory doesn't exist. */ export function getVersionCount(project: string, slug: string): number { - const historyDir = join(DATA_DIR, "history", project, slug); + const historyDir = join(getPlannotatorDataDir(), "history", project, slug); try { const entries = readdirSync(historyDir); return entries.filter((e) => /^\d+\.md$/.test(e)).length; @@ -342,7 +340,7 @@ export function listVersions( project: string, slug: string ): Array<{ version: number; timestamp: string }> { - const historyDir = join(DATA_DIR, "history", project, slug); + const historyDir = join(getPlannotatorDataDir(), "history", project, slug); try { const entries = readdirSync(historyDir); const versions: Array<{ version: number; timestamp: string }> = []; @@ -372,7 +370,7 @@ export function listVersions( export function listProjectPlans( project: string ): Array<{ slug: string; versions: number; lastModified: string }> { - const projectDir = join(DATA_DIR, "history", project); + const projectDir = join(getPlannotatorDataDir(), "history", project); try { const entries = readdirSync(projectDir, { withFileTypes: true }); const plans: Array<{ slug: string; versions: number; lastModified: string }> = [];