chapterhouse 0.8.1 → 0.8.2
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.
- package/dist/api/server.js +91 -3
- package/dist/api/server.test.js +222 -1
- package/package.json +1 -1
package/dist/api/server.js
CHANGED
|
@@ -16,8 +16,9 @@ import { createAuthMiddleware, getBootstrapAuthResponse } from "./auth.js";
|
|
|
16
16
|
import { assertAgentEditAccess } from "./agent-edit-access.js";
|
|
17
17
|
import { createConcurrentConnectionLimiter, createFixedWindowRateLimiter } from "./rate-limit.js";
|
|
18
18
|
import { createTeamRouter } from "./team.js";
|
|
19
|
-
import { writePage, deletePage, pageExists, listPages, ensureWikiStructure, assertPagePath, } from "../wiki/fs.js";
|
|
19
|
+
import { writePage, deletePage, pageExists, listPages, ensureWikiStructure, assertPagePath, getWikiDir, } from "../wiki/fs.js";
|
|
20
20
|
import { parseWikiFrontmatter } from "../wiki/frontmatter.js";
|
|
21
|
+
import { normalizeWikiPath } from "../wiki/path-utils.js";
|
|
21
22
|
import { loadRegistry, saveRegistry } from "../wiki/project-registry.js";
|
|
22
23
|
import { getProjectRulesPath, listTopLevelSoftRules, loadProjectRules, loadProjectRuleSummary, renderInitialProjectRulesPage, saveProjectRulesHardFields, saveProjectRulesSoftRules, } from "../wiki/project-rules.js";
|
|
23
24
|
import { readWikiPage, teamWikiSync } from "../wiki/team-sync.js";
|
|
@@ -192,6 +193,78 @@ function createProjectDetailPayload(slug, cwd) {
|
|
|
192
193
|
softRules: listTopLevelSoftRules(rules.soft),
|
|
193
194
|
};
|
|
194
195
|
}
|
|
196
|
+
function coerceWikiPageUpdated(path, updated) {
|
|
197
|
+
const normalized = updated?.trim();
|
|
198
|
+
if (normalized) {
|
|
199
|
+
return normalized;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
return statSync(join(getWikiDir(), path)).mtime.toISOString();
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return "unknown";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function parseWikiSourcePages(value) {
|
|
209
|
+
if (!value) {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const parsed = JSON.parse(value);
|
|
214
|
+
return Array.isArray(parsed)
|
|
215
|
+
? parsed.filter((entry) => typeof entry === "string").map((entry) => normalizeWikiPath(entry))
|
|
216
|
+
: [];
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function listWikiSources(page) {
|
|
223
|
+
const rows = getDb().prepare(`
|
|
224
|
+
SELECT source_type, origin, raw_path, pages_updated, status, session_id, ingested_at
|
|
225
|
+
FROM wiki_sources
|
|
226
|
+
ORDER BY ingested_at DESC, id ASC
|
|
227
|
+
`).all();
|
|
228
|
+
return rows
|
|
229
|
+
.filter((row) => !page || parseWikiSourcePages(row.pages_updated).includes(page))
|
|
230
|
+
.map((row) => ({
|
|
231
|
+
source_url: row.origin,
|
|
232
|
+
path: row.raw_path ?? undefined,
|
|
233
|
+
kind: row.source_type,
|
|
234
|
+
status: row.status,
|
|
235
|
+
session_id: row.session_id,
|
|
236
|
+
ingested_at: row.ingested_at,
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
function wikiPathToSlug(path) {
|
|
240
|
+
const segments = normalizeWikiPath(path).split("/").filter(Boolean);
|
|
241
|
+
const file = segments[segments.length - 1] ?? path;
|
|
242
|
+
const base = file.endsWith(".md") ? file.slice(0, -3) : file;
|
|
243
|
+
if (base === "index" && segments.length >= 2) {
|
|
244
|
+
return segments[segments.length - 2] ?? base;
|
|
245
|
+
}
|
|
246
|
+
return base;
|
|
247
|
+
}
|
|
248
|
+
function listWikiLinks(page) {
|
|
249
|
+
const rows = page
|
|
250
|
+
? getDb().prepare(`
|
|
251
|
+
SELECT from_page, to_page, link_type
|
|
252
|
+
FROM wiki_links
|
|
253
|
+
WHERE from_page = ? OR to_page = ?
|
|
254
|
+
ORDER BY from_page ASC, to_page ASC, link_type ASC
|
|
255
|
+
`).all(page, page)
|
|
256
|
+
: getDb().prepare(`
|
|
257
|
+
SELECT from_page, to_page, link_type
|
|
258
|
+
FROM wiki_links
|
|
259
|
+
ORDER BY from_page ASC, to_page ASC, link_type ASC
|
|
260
|
+
`).all();
|
|
261
|
+
return rows.map((row) => ({
|
|
262
|
+
source_slug: wikiPathToSlug(row.from_page),
|
|
263
|
+
target_slug: wikiPathToSlug(row.to_page),
|
|
264
|
+
link_type: row.link_type,
|
|
265
|
+
...(page ? { direction: row.from_page === page ? "outgoing" : "incoming" } : {}),
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
195
268
|
// Load a configured API token when present; startup validation below enforces auth.
|
|
196
269
|
let apiToken = null;
|
|
197
270
|
try {
|
|
@@ -312,6 +385,13 @@ function readPathParam(req) {
|
|
|
312
385
|
}
|
|
313
386
|
return raw;
|
|
314
387
|
}
|
|
388
|
+
function readOptionalPageFilter(req) {
|
|
389
|
+
const raw = req.query.page;
|
|
390
|
+
if (typeof raw !== "string" || !raw.trim()) {
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
return normalizeWikiPath(raw.trim());
|
|
394
|
+
}
|
|
315
395
|
function assertValidPagePath(path) {
|
|
316
396
|
try {
|
|
317
397
|
assertPagePath(path);
|
|
@@ -1448,7 +1528,7 @@ app.get("/api/wiki/pages", async (req, res) => {
|
|
|
1448
1528
|
summary: e.summary,
|
|
1449
1529
|
section: e.section,
|
|
1450
1530
|
tags: e.tags || [],
|
|
1451
|
-
updated: e.updated
|
|
1531
|
+
updated: coerceWikiPageUpdated(e.path, e.updated),
|
|
1452
1532
|
scope: getWikiPageScope(e.path),
|
|
1453
1533
|
}));
|
|
1454
1534
|
const orphanResults = listPages()
|
|
@@ -1459,11 +1539,19 @@ app.get("/api/wiki/pages", async (req, res) => {
|
|
|
1459
1539
|
summary: "",
|
|
1460
1540
|
section: "Unindexed",
|
|
1461
1541
|
tags: [],
|
|
1462
|
-
updated:
|
|
1542
|
+
updated: coerceWikiPageUpdated(p, undefined),
|
|
1463
1543
|
scope: getWikiPageScope(p),
|
|
1464
1544
|
}));
|
|
1465
1545
|
res.json([...indexedResults, ...orphanResults]);
|
|
1466
1546
|
});
|
|
1547
|
+
app.get("/api/wiki/sources", async (req, res) => {
|
|
1548
|
+
ensureWikiStructure();
|
|
1549
|
+
res.json({ sources: listWikiSources(readOptionalPageFilter(req)) });
|
|
1550
|
+
});
|
|
1551
|
+
app.get("/api/wiki/links", async (req, res) => {
|
|
1552
|
+
ensureWikiStructure();
|
|
1553
|
+
res.json({ links: listWikiLinks(readOptionalPageFilter(req)) });
|
|
1554
|
+
});
|
|
1467
1555
|
app.get("/api/wiki/page", async (req, res) => {
|
|
1468
1556
|
const path = assertValidPagePath(readPathParam(req));
|
|
1469
1557
|
const authorizationHeader = typeof req.headers.authorization === "string"
|
package/dist/api/server.test.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, utimesSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import Database from "better-sqlite3";
|
|
7
|
+
import { z } from "zod";
|
|
7
8
|
import { DEFAULT_API_SERVER_STARTUP_TIMEOUT_MS, STANDALONE_API_SERVER_STARTUP_TIMEOUT_MS, getFreePort, stopChild, waitForApiServerReady, } from "../test/api-server.js";
|
|
8
9
|
test("supports API_TOKEN env var and personal health route helpers", async () => {
|
|
9
10
|
const runtime = await import("./server-runtime.js");
|
|
@@ -83,6 +84,23 @@ test("formats named SSE status events", async () => {
|
|
|
83
84
|
assert.equal(sse.formatSseEvent("status", { status: "dreaming", message: "Consolidating memories..." }), 'event: status\ndata: {"status":"dreaming","message":"Consolidating memories..."}\n\n');
|
|
84
85
|
});
|
|
85
86
|
const repoRoot = process.cwd();
|
|
87
|
+
const wikiLinkResponseSchema = z.object({
|
|
88
|
+
links: z.array(z.object({
|
|
89
|
+
source_slug: z.string(),
|
|
90
|
+
target_slug: z.string(),
|
|
91
|
+
link_type: z.string(),
|
|
92
|
+
}).passthrough()),
|
|
93
|
+
});
|
|
94
|
+
const wikiSourcesResponseSchema = z.object({
|
|
95
|
+
sources: z.array(z.object({
|
|
96
|
+
source_url: z.string().optional(),
|
|
97
|
+
path: z.string().optional(),
|
|
98
|
+
kind: z.string(),
|
|
99
|
+
status: z.string(),
|
|
100
|
+
session_id: z.string().nullable().optional(),
|
|
101
|
+
ingested_at: z.string().nullable().optional(),
|
|
102
|
+
}).passthrough()),
|
|
103
|
+
});
|
|
86
104
|
function getProjectDbPath(testRoot) {
|
|
87
105
|
return join(testRoot, ".chapterhouse", "chapterhouse.db");
|
|
88
106
|
}
|
|
@@ -873,6 +891,60 @@ test("server wiki route still returns 404 for other missing wiki pages", async (
|
|
|
873
891
|
assert.deepEqual(await response.json(), { error: "Page not found" });
|
|
874
892
|
});
|
|
875
893
|
});
|
|
894
|
+
test("GET /api/wiki/pages coerces empty updated fields to page mtimes", async () => {
|
|
895
|
+
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
896
|
+
const indexedPath = join(testRoot, ".chapterhouse", "wiki", "pages", "topics", "coerced-updated", "index.md");
|
|
897
|
+
const indexedMtime = new Date("2026-05-15T12:00:00.000Z");
|
|
898
|
+
mkdirSync(join(testRoot, ".chapterhouse", "wiki", "pages", "topics", "coerced-updated"), { recursive: true });
|
|
899
|
+
writeFileSync(indexedPath, "# Coerced Updated\n", "utf-8");
|
|
900
|
+
utimesSync(indexedPath, indexedMtime, indexedMtime);
|
|
901
|
+
const orphanPath = join(testRoot, ".chapterhouse", "wiki", "pages", "topics", "orphan-updated", "index.md");
|
|
902
|
+
const orphanMtime = new Date("2026-05-15T13:30:00.000Z");
|
|
903
|
+
mkdirSync(join(testRoot, ".chapterhouse", "wiki", "pages", "topics", "orphan-updated"), { recursive: true });
|
|
904
|
+
writeFileSync(orphanPath, "# Orphan Updated\n", "utf-8");
|
|
905
|
+
utimesSync(orphanPath, orphanMtime, orphanMtime);
|
|
906
|
+
const db = new Database(getProjectDbPath(testRoot));
|
|
907
|
+
try {
|
|
908
|
+
db.prepare(`
|
|
909
|
+
INSERT INTO wiki_pages (path, title, tags, summary, last_updated)
|
|
910
|
+
VALUES (?, ?, ?, ?, ?)
|
|
911
|
+
`).run("pages/topics/coerced-updated/index.md", "Coerced Updated", "[]", "Missing updated value", "");
|
|
912
|
+
}
|
|
913
|
+
finally {
|
|
914
|
+
db.close();
|
|
915
|
+
}
|
|
916
|
+
const response = await fetch(`${baseUrl}/api/wiki/pages`, {
|
|
917
|
+
headers: { authorization: authHeader },
|
|
918
|
+
});
|
|
919
|
+
assert.equal(response.status, 200);
|
|
920
|
+
const pages = await response.json();
|
|
921
|
+
assert.equal(pages.find((page) => page.path === "pages/topics/coerced-updated/index.md")?.updated, indexedMtime.toISOString());
|
|
922
|
+
assert.equal(pages.find((page) => page.path === "pages/topics/orphan-updated/index.md")?.updated, orphanMtime.toISOString());
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
test("GET /api/wiki/pages returns a non-empty updated value for pages with frontmatter", async () => {
|
|
926
|
+
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
927
|
+
const pageDir = join(testRoot, ".chapterhouse", "wiki", "pages", "projects", "frontmatter-updated");
|
|
928
|
+
mkdirSync(pageDir, { recursive: true });
|
|
929
|
+
writeFileSync(join(pageDir, "index.md"), `---
|
|
930
|
+
title: Frontmatter Updated
|
|
931
|
+
summary: Has explicit metadata.
|
|
932
|
+
updated: 2026-05-14
|
|
933
|
+
---
|
|
934
|
+
|
|
935
|
+
# Frontmatter Updated
|
|
936
|
+
`, "utf-8");
|
|
937
|
+
const response = await fetch(`${baseUrl}/api/wiki/pages`, {
|
|
938
|
+
headers: { authorization: authHeader },
|
|
939
|
+
});
|
|
940
|
+
assert.equal(response.status, 200);
|
|
941
|
+
const pages = await response.json();
|
|
942
|
+
const page = pages.find((entry) => entry.path === "pages/projects/frontmatter-updated/index.md");
|
|
943
|
+
assert.ok(page, "expected frontmatter page to be listed");
|
|
944
|
+
assert.equal(typeof page.updated, "string");
|
|
945
|
+
assert.notEqual(page.updated, "");
|
|
946
|
+
});
|
|
947
|
+
});
|
|
876
948
|
test("server projects route returns an empty list when the registry is missing", async () => {
|
|
877
949
|
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
878
950
|
const response = await fetch(`${baseUrl}/api/projects`, {
|
|
@@ -1476,4 +1548,153 @@ test("GET /api/wiki/korg/sessions returns grouped active research sessions", asy
|
|
|
1476
1548
|
});
|
|
1477
1549
|
});
|
|
1478
1550
|
});
|
|
1551
|
+
test("GET /api/wiki/sources returns an empty sources payload when no sources exist", async () => {
|
|
1552
|
+
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
1553
|
+
const response = await fetch(`${baseUrl}/api/wiki/sources`, {
|
|
1554
|
+
headers: { authorization: authHeader },
|
|
1555
|
+
});
|
|
1556
|
+
assert.equal(response.status, 200);
|
|
1557
|
+
const body = await response.json();
|
|
1558
|
+
assert.deepEqual(body, { sources: [] });
|
|
1559
|
+
assert.ok(wikiSourcesResponseSchema.safeParse(body).success, "response should match WikiSourcesResponseSchema");
|
|
1560
|
+
});
|
|
1561
|
+
});
|
|
1562
|
+
test("GET /api/wiki/sources filters sources by page", async () => {
|
|
1563
|
+
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
1564
|
+
const db = new Database(getProjectDbPath(testRoot));
|
|
1565
|
+
try {
|
|
1566
|
+
db.prepare(`
|
|
1567
|
+
INSERT INTO wiki_sources (id, source_type, origin, title, ingested_at, raw_path, parsed_content, pages_updated, status, session_id, session_name)
|
|
1568
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1569
|
+
`).run("src-filter-match", "pdf", "https://example.com/roadmap.pdf", "Roadmap", "2026-05-15T10:00:00.000Z", "sources/roadmap.pdf", "roadmap body", JSON.stringify(["pages/projects/wiki-api/index.md"]), "complete", "sess-1", "Wiki API");
|
|
1570
|
+
db.prepare(`
|
|
1571
|
+
INSERT INTO wiki_sources (id, source_type, origin, title, ingested_at, raw_path, parsed_content, pages_updated, status, session_id, session_name)
|
|
1572
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1573
|
+
`).run("src-filter-miss", "text", "notes about another page", "Other", "2026-05-15T11:00:00.000Z", "sources/other.md", "other body", JSON.stringify(["pages/projects/other/index.md"]), "active", null, null);
|
|
1574
|
+
}
|
|
1575
|
+
finally {
|
|
1576
|
+
db.close();
|
|
1577
|
+
}
|
|
1578
|
+
const response = await fetch(`${baseUrl}/api/wiki/sources?page=${encodeURIComponent("pages/projects/wiki-api/index.md")}`, {
|
|
1579
|
+
headers: { authorization: authHeader },
|
|
1580
|
+
});
|
|
1581
|
+
assert.equal(response.status, 200);
|
|
1582
|
+
const body = await response.json();
|
|
1583
|
+
const parsed = wikiSourcesResponseSchema.safeParse(body);
|
|
1584
|
+
if (!parsed.success) {
|
|
1585
|
+
assert.fail(parsed.error.message);
|
|
1586
|
+
}
|
|
1587
|
+
assert.deepEqual(parsed.data, {
|
|
1588
|
+
sources: [
|
|
1589
|
+
{
|
|
1590
|
+
source_url: "https://example.com/roadmap.pdf",
|
|
1591
|
+
path: "sources/roadmap.pdf",
|
|
1592
|
+
kind: "pdf",
|
|
1593
|
+
status: "complete",
|
|
1594
|
+
session_id: "sess-1",
|
|
1595
|
+
ingested_at: "2026-05-15T10:00:00.000Z",
|
|
1596
|
+
},
|
|
1597
|
+
],
|
|
1598
|
+
});
|
|
1599
|
+
});
|
|
1600
|
+
});
|
|
1601
|
+
test("GET /api/wiki/links returns all links when no page filter is provided", async () => {
|
|
1602
|
+
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
1603
|
+
const db = new Database(getProjectDbPath(testRoot));
|
|
1604
|
+
try {
|
|
1605
|
+
db.prepare(`
|
|
1606
|
+
INSERT INTO wiki_links (from_page, to_page, link_type, extracted_at)
|
|
1607
|
+
VALUES (?, ?, ?, ?)
|
|
1608
|
+
`).run("pages/projects/wiki-api/index.md", "pages/topics/contracts/index.md", "references", "2026-05-15T12:00:00.000Z");
|
|
1609
|
+
db.prepare(`
|
|
1610
|
+
INSERT INTO wiki_links (from_page, to_page, link_type, extracted_at)
|
|
1611
|
+
VALUES (?, ?, ?, ?)
|
|
1612
|
+
`).run("pages/people/trinity/index.md", "pages/projects/wiki-api/index.md", "implements", "2026-05-15T12:01:00.000Z");
|
|
1613
|
+
}
|
|
1614
|
+
finally {
|
|
1615
|
+
db.close();
|
|
1616
|
+
}
|
|
1617
|
+
const response = await fetch(`${baseUrl}/api/wiki/links`, {
|
|
1618
|
+
headers: { authorization: authHeader },
|
|
1619
|
+
});
|
|
1620
|
+
assert.equal(response.status, 200);
|
|
1621
|
+
const body = await response.json();
|
|
1622
|
+
const parsed = wikiLinkResponseSchema.safeParse(body);
|
|
1623
|
+
if (!parsed.success) {
|
|
1624
|
+
assert.fail(parsed.error.message);
|
|
1625
|
+
}
|
|
1626
|
+
assert.deepEqual(parsed.data, {
|
|
1627
|
+
links: [
|
|
1628
|
+
{
|
|
1629
|
+
source_slug: "trinity",
|
|
1630
|
+
target_slug: "wiki-api",
|
|
1631
|
+
link_type: "implements",
|
|
1632
|
+
},
|
|
1633
|
+
{
|
|
1634
|
+
source_slug: "wiki-api",
|
|
1635
|
+
target_slug: "contracts",
|
|
1636
|
+
link_type: "references",
|
|
1637
|
+
},
|
|
1638
|
+
],
|
|
1639
|
+
});
|
|
1640
|
+
});
|
|
1641
|
+
});
|
|
1642
|
+
test("GET /api/wiki/links returns an empty typed links payload when a page has no graph edges", async () => {
|
|
1643
|
+
await withStartedServer(async ({ baseUrl, authHeader }) => {
|
|
1644
|
+
const response = await fetch(`${baseUrl}/api/wiki/links?page=${encodeURIComponent("pages/projects/wiki-api/index.md")}`, {
|
|
1645
|
+
headers: { authorization: authHeader },
|
|
1646
|
+
});
|
|
1647
|
+
assert.equal(response.status, 200);
|
|
1648
|
+
const body = await response.json();
|
|
1649
|
+
assert.deepEqual(body, { links: [] });
|
|
1650
|
+
assert.ok(wikiLinkResponseSchema.safeParse(body).success, "response should match WikiLinksResponseSchema");
|
|
1651
|
+
});
|
|
1652
|
+
});
|
|
1653
|
+
test("GET /api/wiki/links filters incoming and outgoing links for a page", async () => {
|
|
1654
|
+
await withStartedServer(async ({ baseUrl, authHeader, testRoot }) => {
|
|
1655
|
+
const db = new Database(getProjectDbPath(testRoot));
|
|
1656
|
+
try {
|
|
1657
|
+
db.prepare(`
|
|
1658
|
+
INSERT INTO wiki_links (from_page, to_page, link_type, extracted_at)
|
|
1659
|
+
VALUES (?, ?, ?, ?)
|
|
1660
|
+
`).run("pages/projects/wiki-api/index.md", "pages/topics/contracts/index.md", "references", "2026-05-15T12:00:00.000Z");
|
|
1661
|
+
db.prepare(`
|
|
1662
|
+
INSERT INTO wiki_links (from_page, to_page, link_type, extracted_at)
|
|
1663
|
+
VALUES (?, ?, ?, ?)
|
|
1664
|
+
`).run("pages/people/trinity/index.md", "pages/projects/wiki-api/index.md", "implements", "2026-05-15T12:01:00.000Z");
|
|
1665
|
+
db.prepare(`
|
|
1666
|
+
INSERT INTO wiki_links (from_page, to_page, link_type, extracted_at)
|
|
1667
|
+
VALUES (?, ?, ?, ?)
|
|
1668
|
+
`).run("pages/projects/other/index.md", "pages/topics/unrelated/index.md", "references", "2026-05-15T12:02:00.000Z");
|
|
1669
|
+
}
|
|
1670
|
+
finally {
|
|
1671
|
+
db.close();
|
|
1672
|
+
}
|
|
1673
|
+
const response = await fetch(`${baseUrl}/api/wiki/links?page=${encodeURIComponent("pages/projects/wiki-api/index.md")}`, {
|
|
1674
|
+
headers: { authorization: authHeader },
|
|
1675
|
+
});
|
|
1676
|
+
assert.equal(response.status, 200);
|
|
1677
|
+
const body = await response.json();
|
|
1678
|
+
const parsed = wikiLinkResponseSchema.safeParse(body);
|
|
1679
|
+
if (!parsed.success) {
|
|
1680
|
+
assert.fail(parsed.error.message);
|
|
1681
|
+
}
|
|
1682
|
+
assert.deepEqual(parsed.data, {
|
|
1683
|
+
links: [
|
|
1684
|
+
{
|
|
1685
|
+
source_slug: "trinity",
|
|
1686
|
+
target_slug: "wiki-api",
|
|
1687
|
+
link_type: "implements",
|
|
1688
|
+
direction: "incoming",
|
|
1689
|
+
},
|
|
1690
|
+
{
|
|
1691
|
+
source_slug: "wiki-api",
|
|
1692
|
+
target_slug: "contracts",
|
|
1693
|
+
link_type: "references",
|
|
1694
|
+
direction: "outgoing",
|
|
1695
|
+
},
|
|
1696
|
+
],
|
|
1697
|
+
});
|
|
1698
|
+
});
|
|
1699
|
+
});
|
|
1479
1700
|
//# sourceMappingURL=server.test.js.map
|
package/package.json
CHANGED