Menu
popagent
publicLatest change 87f51a535b92b11ccaf217f96b05a14c907d8eac - Store project documentation in PostgreSQL by Ólafur Búi Ólafsson
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { DocumentationFiles, DocumentationConflictError } from "./documentation-files";
import { documentationIndex } from "./documentation-index";
import { storage } from "./storage";
const workspaceId = `docs-${crypto.randomUUID()}`;
const repositoryPath = `docs-${crypto.randomUUID()}`;
const docs = new DocumentationFiles();
beforeAll(async () => {
await documentationIndex.init();
await storage.db.none(`
INSERT INTO popagent_agent_workspaces (id, name, repository_path)
VALUES ($1, 'Documentation test', $2)
`, [workspaceId, repositoryPath]);
});
afterAll(async () => {
await storage.db.none("DELETE FROM popagent_agent_workspaces WHERE id = $1", [workspaceId]);
});
describe("DocumentationFiles", () => {
test("creates project folders and lists contained Markdown pages", async () => {
await docs.createFolder(workspaceId, "Architecture/Decisions");
await docs.save(workspaceId, { path: "Architecture/overview.md", content: "# System overview\n" });
await docs.save(workspaceId, { path: "Architecture/Decisions/storage.md", content: "# Storage\n" });
const tree = await docs.list(workspaceId);
expect(tree.folders.map(({ path }) => path)).toEqual(["Architecture", "Architecture/Decisions"]);
expect(tree.pages).toEqual([
expect.objectContaining({ path: "Architecture/Decisions/storage.md", title: "Storage" }),
expect.objectContaining({ path: "Architecture/overview.md", title: "System overview" }),
]);
});
test("creates, updates, moves, and deletes Markdown using content revisions", async () => {
const created = await docs.save(workspaceId, { path: "Operations/deploy.md", content: "# Deploy\n" });
await expect(docs.save(workspaceId, { path: created.path, content: "stale", revision: "bad" }))
.rejects.toBeInstanceOf(DocumentationConflictError);
const updated = await docs.save(workspaceId, { path: created.path, content: "# Release\n", revision: created.revision });
expect(updated.revision).not.toBe(created.revision);
const moved = await docs.move(workspaceId, updated.path, "Runbooks/release.md", updated.revision);
expect(moved.path).toBe("Runbooks/release.md");
await expect(docs.remove(workspaceId, moved.path, created.revision))
.rejects.toBeInstanceOf(DocumentationConflictError);
await docs.remove(workspaceId, moved.path, moved.revision);
await expect(docs.read(workspaceId, moved.path)).rejects.toThrow("Documentation page not found");
});
test("deletes a folder and all descendant pages", async () => {
await docs.save(workspaceId, { path: "History/2026/launch.md", content: "# Launch\n" });
await docs.save(workspaceId, { path: "History/archive.md", content: "# Archive\n" });
expect(await docs.removeFolder(workspaceId, "History")).toEqual([
"History/2026/launch.md",
"History/archive.md",
]);
expect((await docs.list(workspaceId)).folders.some(({ path }) => path.startsWith("History"))).toBe(false);
});
test("rejects traversal, hidden paths, and non-Markdown files", async () => {
await expect(docs.save(workspaceId, { path: "../secret.md", content: "no" })).rejects.toThrow("Invalid documentation path");
await expect(docs.save(workspaceId, { path: ".private/page.md", content: "no" })).rejects.toThrow("Invalid documentation path");
await expect(docs.save(workspaceId, { path: "notes.txt", content: "no" })).rejects.toThrow("Invalid documentation path");
});
});