mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
e0aee7451b
* feat: add PLANNOTATOR_DATA_DIR env var to customize data directory * fix: update missed hardcoded paths to use PLANNOTATOR_DATA_DIR OpenCode plugin and VS Code extension still used hardcoded ~/.plannotator paths, causing the IPC registry and plan backing file to diverge from the server when PLANNOTATOR_DATA_DIR is set. Also exports data-dir from @plannotator/shared and documents the new env var in AGENTS.md. Co-authored-by: Chris Werner Rau <14326070+cwrau@users.noreply.github.com> Co-authored-by: João O. Santos <34689526+Joao-O-Santos@users.noreply.github.com> * fix: vendor data-dir.ts into Pi extension and rewrite imports The Pi extension copies shared/server modules into generated/ at build time. Without vendoring data-dir.ts and rewriting the parent-relative imports, typecheck fails on all generated files that import getPlannotatorDataDir. * refactor: eliminate duplicated data-dir logic and clean up call sites - VS Code extension: replace inlined getPlannotatorDataDir() copy with import from the canonical packages/shared/data-dir.ts (esbuild bundles it, so no runtime dependency needed) - storage.ts: hoist repeated getPlannotatorDataDir() calls to a module-level DATA_DIR constant, matching the pattern config.ts uses - data-dir.ts: remove inaccurate docstring claim about relative path resolution (the code does not call resolve()) - improvement-hooks.ts: hoist to DATA_DIR constant, clarify comments on the two-level hook lookup (hooks/ subdir vs root fallback) * fix: resolve relative PLANNOTATOR_DATA_DIR to absolute path A relative value like ./data would break readArchivedPlan's path traversal guard, which compares a resolve()'d absolute path against the still-relative planDir prefix. Always return an absolute path so all callers get consistent path shapes. * fix: use @plannotator/shared/data-dir imports in server package Switch from relative ../shared/data-dir imports to the package export, matching the convention every other server file follows. Update Pi vendor script sed rules to match the new import style. * fix: use package imports in server and respect data dir in compound skill Server modules: switch from relative ../shared/data-dir imports to @plannotator/shared/data-dir, matching the convention every other server file follows. Update Pi vendor script sed rules to match. Compound skill: update hardcoded ~/.plannotator paths to check PLANNOTATOR_DATA_DIR first, so the skill reads plans and writes the improvement hook to the correct location when users set a custom data directory. Co-authored-by: Chris Werner Rau <14326070+cwrau@users.noreply.github.com> Co-authored-by: João O. Santos <34689526+Joao-O-Santos@users.noreply.github.com> * fix: remove remaining hardcoded ~/.plannotator assumptions - Settings UI: replace hardcoded path in label and placeholder with generic text that doesn't assume a specific data directory - quickLabels: update agent tip to reference PLANNOTATOR_DATA_DIR so the agent checks the correct plans directory - codex-review: hoist getPlannotatorDataDir() to module-level DATA_DIR constant, eliminating redundant per-call resolution in debugLog() - Tests: make submit-plan and storage tests resilient to PLANNOTATOR_DATA_DIR being set in the environment - Install scripts (sh, ps1, cmd): check PLANNOTATOR_DATA_DIR before falling back to ~/.plannotator for config.json attestation lookup * fix: expand tilde in install script and update test assertions install.sh: PLANNOTATOR_DATA_DIR set to ~/... stays literal inside double quotes, so the config file check silently failed. Add case statement to expand ~ the same way the runtime data-dir.ts does. install.test.ts: update three assertions that checked for hardcoded ~/.plannotator paths — now verify PLANNOTATOR_DATA_DIR awareness instead. * docs: add PLANNOTATOR_DATA_DIR to env var reference with VS Code note Document the new env var on the marketing site's environment variables reference page. Include a footnote about ensuring VS Code inherits the variable when launched from the Dock. --------- Co-authored-by: Michael Ramos <mdramos8@gmail.com> Co-authored-by: Chris Werner Rau <14326070+cwrau@users.noreply.github.com> Co-authored-by: João O. Santos <34689526+Joao-O-Santos@users.noreply.github.com>
177 lines
5.5 KiB
TypeScript
177 lines
5.5 KiB
TypeScript
/**
|
|
* Plan Storage Tests
|
|
*
|
|
* Run: bun test packages/server/storage.test.ts
|
|
*/
|
|
|
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
import { mkdtempSync, rmSync, readFileSync, readdirSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { generateSlug, getPlanDir, savePlan, saveToHistory, getPlanVersion, getVersionCount, listVersions } from "./storage";
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
function makeTempDir(): string {
|
|
const dir = mkdtempSync(join(tmpdir(), "plannotator-storage-test-"));
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs.splice(0)) {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("generateSlug", () => {
|
|
test("uses first heading and date", () => {
|
|
const slug = generateSlug("# My Plan\n\nSome content");
|
|
const date = new Date().toISOString().split("T")[0];
|
|
expect(slug).toMatch(/^my-plan-\d{4}-\d{2}-\d{2}$/);
|
|
expect(slug).toEndWith(date);
|
|
});
|
|
|
|
test("falls back to 'plan' when no heading", () => {
|
|
const slug = generateSlug("No heading here");
|
|
expect(slug).toMatch(/^plan-\d{4}-\d{2}-\d{2}$/);
|
|
});
|
|
|
|
test("same heading on same day produces same slug", () => {
|
|
const a = generateSlug("# Deploy Strategy\nVersion A");
|
|
const b = generateSlug("# Deploy Strategy\nVersion B");
|
|
expect(a).toBe(b);
|
|
});
|
|
|
|
test("different headings produce different slugs", () => {
|
|
const a = generateSlug("# Plan A");
|
|
const b = generateSlug("# Plan B");
|
|
expect(a).not.toBe(b);
|
|
});
|
|
});
|
|
|
|
describe("getPlanDir", () => {
|
|
test("creates directory at custom path", () => {
|
|
const dir = makeTempDir();
|
|
const customPath = join(dir, "custom", "plans");
|
|
const result = getPlanDir(customPath);
|
|
expect(result).toBe(customPath);
|
|
// Directory should exist
|
|
expect(readdirSync(customPath)).toBeDefined();
|
|
});
|
|
|
|
test("expands tilde in custom path", () => {
|
|
const result = getPlanDir("~/.plannotator/test-plans");
|
|
expect(result).not.toContain("~");
|
|
expect(result).toMatch(/\.plannotator\/test-plans$/);
|
|
});
|
|
|
|
test("uses default when no custom path", () => {
|
|
const result = getPlanDir();
|
|
expect(result).toMatch(/plans$/);
|
|
expect(result).toBe(getPlanDir(null));
|
|
});
|
|
|
|
test("uses default for null", () => {
|
|
const result = getPlanDir(null);
|
|
expect(result).toMatch(/plans$/);
|
|
});
|
|
|
|
test("uses default for whitespace-only custom path", () => {
|
|
const result = getPlanDir(" ");
|
|
expect(result).toMatch(/plans$/);
|
|
expect(result).not.toBe(process.cwd());
|
|
});
|
|
});
|
|
|
|
describe("savePlan", () => {
|
|
test("writes markdown file to disk", () => {
|
|
const dir = makeTempDir();
|
|
const path = savePlan("test-slug", "# Content", dir);
|
|
expect(path).toBe(join(dir, "test-slug.md"));
|
|
expect(readFileSync(path, "utf-8")).toBe("# Content");
|
|
});
|
|
});
|
|
|
|
describe("saveToHistory", () => {
|
|
test("creates first version as 001.md", () => {
|
|
const slug = `first-version-${Date.now()}`;
|
|
const result = saveToHistory("test-project", slug, "# V1");
|
|
expect(result.version).toBe(1);
|
|
expect(result.path).toEndWith("001.md");
|
|
expect(result.isNew).toBe(true);
|
|
expect(readFileSync(result.path, "utf-8")).toBe("# V1");
|
|
});
|
|
|
|
test("increments version number", () => {
|
|
const slug = `inc-test-${Date.now()}`;
|
|
const v1 = saveToHistory("test-project", slug, "# V1");
|
|
const v2 = saveToHistory("test-project", slug, "# V2");
|
|
expect(v1.version).toBe(1);
|
|
expect(v2.version).toBe(2);
|
|
expect(v2.path).toEndWith("002.md");
|
|
});
|
|
|
|
test("deduplicates identical content", () => {
|
|
const slug = `dedup-test-${Date.now()}`;
|
|
const v1 = saveToHistory("test-project", slug, "# Same");
|
|
const v2 = saveToHistory("test-project", slug, "# Same");
|
|
expect(v1.version).toBe(1);
|
|
expect(v2.version).toBe(1);
|
|
expect(v2.isNew).toBe(false);
|
|
});
|
|
|
|
test("saves when content differs", () => {
|
|
const slug = `diff-test-${Date.now()}`;
|
|
const v1 = saveToHistory("test-project", slug, "# V1");
|
|
const v2 = saveToHistory("test-project", slug, "# V2");
|
|
expect(v2.isNew).toBe(true);
|
|
expect(v2.version).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe("getPlanVersion", () => {
|
|
test("reads saved version content", () => {
|
|
const slug = `read-test-${Date.now()}`;
|
|
saveToHistory("test-project", slug, "# Read Me");
|
|
const content = getPlanVersion("test-project", slug, 1);
|
|
expect(content).toBe("# Read Me");
|
|
});
|
|
|
|
test("returns null for nonexistent version", () => {
|
|
const content = getPlanVersion("test-project", "nonexistent", 99);
|
|
expect(content).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("getVersionCount", () => {
|
|
test("returns 0 for nonexistent project", () => {
|
|
expect(getVersionCount("nope", "nope")).toBe(0);
|
|
});
|
|
|
|
test("counts versions correctly", () => {
|
|
const slug = `count-test-${Date.now()}`;
|
|
saveToHistory("test-project", slug, "# V1");
|
|
saveToHistory("test-project", slug, "# V2");
|
|
saveToHistory("test-project", slug, "# V3");
|
|
expect(getVersionCount("test-project", slug)).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe("listVersions", () => {
|
|
test("returns empty for nonexistent project", () => {
|
|
expect(listVersions("nope", "nope")).toEqual([]);
|
|
});
|
|
|
|
test("lists versions in ascending order", () => {
|
|
const slug = `list-test-${Date.now()}`;
|
|
saveToHistory("test-project", slug, "# V1");
|
|
saveToHistory("test-project", slug, "# V2");
|
|
const versions = listVersions("test-project", slug);
|
|
expect(versions).toHaveLength(2);
|
|
expect(versions[0].version).toBe(1);
|
|
expect(versions[1].version).toBe(2);
|
|
expect(versions[0].timestamp).toBeTruthy();
|
|
});
|
|
});
|