chapterhouse 0.8.2 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -756,6 +756,24 @@ tags: [ops]
756
756
  assert.deepEqual(await missingResponse.json(), { error: "Page not found" });
757
757
  });
758
758
  });
759
+ test("POST /api/wiki/page/pin rejects path traversal slugs", async () => {
760
+ await withStartedServer(async ({ baseUrl, authHeader }) => {
761
+ const response = await fetch(`${baseUrl}/api/wiki/page/pin`, {
762
+ method: "POST",
763
+ headers: {
764
+ authorization: authHeader,
765
+ "content-type": "application/json",
766
+ },
767
+ body: JSON.stringify({
768
+ slug: "../../etc/passwd",
769
+ pinned: true,
770
+ }),
771
+ });
772
+ assert.equal(response.status, 400);
773
+ const body = await response.json();
774
+ assert.match(body.error ?? "", /unsafe wiki path/i);
775
+ });
776
+ });
759
777
  test("server worker detail returns the stored dispatched prompt", async () => {
760
778
  await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
761
779
  const db = new Database(join(testRoot, ".chapterhouse", "chapterhouse.db"));
@@ -891,6 +909,74 @@ test("server wiki route still returns 404 for other missing wiki pages", async (
891
909
  assert.deepEqual(await response.json(), { error: "Page not found" });
892
910
  });
893
911
  });
912
+ test("GET /api/wiki/browser-pages maps wiki_pages rows to the browser contract and filters them", async () => {
913
+ await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
914
+ const db = new Database(getProjectDbPath(testRoot));
915
+ try {
916
+ db.prepare(`
917
+ INSERT INTO wiki_pages (path, title, entity_type, tags, summary, last_updated, pinned)
918
+ VALUES (?, ?, ?, ?, ?, ?, ?)
919
+ `).run("pages/topics/rust/index.md", "Rust", "topics", "[]", "Systems programming language", "2026-05-15T12:00:00.000Z", 1);
920
+ db.prepare(`
921
+ INSERT INTO wiki_pages (path, title, entity_type, tags, summary, last_updated, pinned)
922
+ VALUES (?, ?, ?, ?, ?, ?, ?)
923
+ `).run("pages/projects/chapterhouse/index.md", "Chapterhouse", "projects", "[]", "Team AI assistant", "2026-05-14T12:00:00.000Z", 0);
924
+ }
925
+ finally {
926
+ db.close();
927
+ }
928
+ const response = await fetch(`${baseUrl}/api/wiki/browser-pages?type=topics&q=systems`, {
929
+ headers: { authorization: authHeader },
930
+ });
931
+ assert.equal(response.status, 200);
932
+ assert.deepEqual(await response.json(), {
933
+ pages: [
934
+ {
935
+ slug: "topics/rust/index",
936
+ title: "Rust",
937
+ summary: "Systems programming language",
938
+ type: "topics",
939
+ last_updated: "2026-05-15T12:00:00.000Z",
940
+ pinned: true,
941
+ },
942
+ ],
943
+ });
944
+ });
945
+ });
946
+ test("GET /api/wiki/browser-pages falls back to indexed and orphan wiki pages when wiki_pages is empty", async () => {
947
+ await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
948
+ const indexedDir = join(testRoot, ".chapterhouse", "wiki", "pages", "topics", "fallback");
949
+ mkdirSync(indexedDir, { recursive: true });
950
+ writeFileSync(join(indexedDir, "index.md"), "# Fallback\n", "utf-8");
951
+ const orphanDir = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "orphan-browser");
952
+ mkdirSync(orphanDir, { recursive: true });
953
+ writeFileSync(join(orphanDir, "index.md"), "# Orphan Browser\n", "utf-8");
954
+ const db = new Database(getProjectDbPath(testRoot));
955
+ try {
956
+ db.prepare("DELETE FROM wiki_pages").run();
957
+ }
958
+ finally {
959
+ db.close();
960
+ }
961
+ const response = await fetch(`${baseUrl}/api/wiki/browser-pages?type=Unindexed&q=orphan`, {
962
+ headers: { authorization: authHeader },
963
+ });
964
+ assert.equal(response.status, 200);
965
+ const body = await response.json();
966
+ assert.deepEqual(body.pages.map((page) => ({
967
+ slug: page.slug,
968
+ title: page.title,
969
+ type: page.type,
970
+ })), [
971
+ {
972
+ slug: "projects/orphan-browser/index",
973
+ title: "pages/projects/orphan-browser/index.md",
974
+ type: "Unindexed",
975
+ },
976
+ ]);
977
+ assert.notEqual(body.pages[0]?.last_updated, "");
978
+ });
979
+ });
894
980
  test("GET /api/wiki/pages coerces empty updated fields to page mtimes", async () => {
895
981
  await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
896
982
  const indexedPath = join(testRoot, ".chapterhouse", "wiki", "pages", "topics", "coerced-updated", "index.md");
@@ -1541,8 +1627,8 @@ test("GET /api/wiki/korg/sessions returns grouped active research sessions", asy
1541
1627
  id: "compiler-research",
1542
1628
  name: "Compiler research",
1543
1629
  source_count: 2,
1544
- open_questions: 0,
1545
- last_activity: "2026-05-14T22:00:00.000Z",
1630
+ open_questions_count: 0,
1631
+ last_activity_at: "2026-05-14T22:00:00.000Z",
1546
1632
  },
1547
1633
  ],
1548
1634
  });