@openparachute/vault 0.7.3-rc.13 → 0.7.3-rc.3

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.
Files changed (45) hide show
  1. package/README.md +3 -5
  2. package/core/src/content-range.test.ts +0 -127
  3. package/core/src/content-range.ts +0 -100
  4. package/core/src/core.test.ts +4 -66
  5. package/core/src/expand.ts +3 -11
  6. package/core/src/mcp.ts +6 -577
  7. package/core/src/notes.ts +4 -201
  8. package/core/src/search-fts-v25.test.ts +1 -9
  9. package/core/src/search-query.test.ts +0 -42
  10. package/core/src/search-query.ts +0 -27
  11. package/core/src/seed-packs.test.ts +1 -117
  12. package/core/src/seed-packs.ts +1 -217
  13. package/core/src/types.ts +0 -6
  14. package/core/src/vault-projection.ts +0 -55
  15. package/package.json +1 -1
  16. package/src/add-pack.test.ts +0 -32
  17. package/src/auth-hub-jwt.test.ts +1 -118
  18. package/src/auth.ts +0 -64
  19. package/src/cli.ts +1 -5
  20. package/src/mcp-http.ts +4 -33
  21. package/src/mcp-tools.ts +14 -61
  22. package/src/oauth-discovery.ts +0 -31
  23. package/src/onboarding-seed.test.ts +0 -64
  24. package/src/routes.ts +95 -92
  25. package/src/routing.test.ts +4 -229
  26. package/src/routing.ts +23 -152
  27. package/src/scopes.ts +0 -22
  28. package/src/server.ts +0 -7
  29. package/src/storage.test.ts +1 -200
  30. package/src/transcription-worker.test.ts +0 -151
  31. package/src/transcription-worker.ts +52 -113
  32. package/src/vault.test.ts +11 -48
  33. package/core/src/attachment/bytes-provider.ts +0 -65
  34. package/core/src/attachment/policy.test.ts +0 -66
  35. package/core/src/attachment/policy.ts +0 -131
  36. package/core/src/attachment/tickets.test.ts +0 -45
  37. package/core/src/attachment/tickets.ts +0 -117
  38. package/core/src/attachment-tickets-tool.test.ts +0 -286
  39. package/core/src/display-title.test.ts +0 -190
  40. package/core/src/lede.test.ts +0 -96
  41. package/core/src/search-title-boost.test.ts +0 -125
  42. package/src/attachment-bytes.ts +0 -68
  43. package/src/attachment-tickets.test.ts +0 -475
  44. package/src/attachment-tickets.ts +0 -340
  45. package/src/read-attachment.test.ts +0 -436
@@ -1,286 +0,0 @@
1
- /**
2
- * `request-attachment-upload` / `request-attachment-download` — the two
3
- * MCP mint tools generated by `generateMcpTools` (see mcp.ts's
4
- * `GenerateMcpToolsOpts.attachmentTickets`). Exercises the "tools omitted
5
- * when unwired" contract (D10) and the mint-time policy (note/attachment
6
- * resolution, tag-scope, size cap, extension blocklist) against a fake
7
- * in-memory `AttachmentTicketProvider` — the spend side (actually writing
8
- * bytes) is bun-only and covered in src/attachment-tickets.test.ts.
9
- */
10
-
11
- import { describe, test, expect, beforeEach } from "bun:test";
12
- import { Database } from "bun:sqlite";
13
- import { BunSqliteStore } from "./store.ts";
14
- import { generateMcpTools, type McpToolDef } from "./mcp.ts";
15
- import type { AttachmentTicket, AttachmentTicketProvider } from "./attachment/tickets.ts";
16
- import { MAX_TICKET_UPLOAD_BYTES } from "./attachment/tickets.ts";
17
- import type { Note } from "./types.ts";
18
-
19
- class FakeTicketProvider implements AttachmentTicketProvider {
20
- tickets = new Map<string, AttachmentTicket>();
21
- async put(t: AttachmentTicket): Promise<void> {
22
- this.tickets.set(t.id, t);
23
- }
24
- async take(id: string): Promise<AttachmentTicket | null> {
25
- const t = this.tickets.get(id);
26
- if (!t) return null;
27
- this.tickets.delete(id);
28
- return t;
29
- }
30
- }
31
-
32
- let store: BunSqliteStore;
33
-
34
- beforeEach(() => {
35
- store = new BunSqliteStore(new Database(":memory:"));
36
- });
37
-
38
- function findTool(tools: McpToolDef[], name: string): McpToolDef {
39
- const t = tools.find((x) => x.name === name);
40
- if (!t) throw new Error(`tool "${name}" not found`);
41
- return t;
42
- }
43
-
44
- async function callTool(tool: McpToolDef, params: Record<string, unknown>): Promise<any> {
45
- return await tool.execute(params);
46
- }
47
-
48
- async function expectToolError(tool: McpToolDef, params: Record<string, unknown>): Promise<any> {
49
- try {
50
- await tool.execute(params);
51
- throw new Error("expected the tool to throw");
52
- } catch (err) {
53
- return err as any;
54
- }
55
- }
56
-
57
- describe("D10 — tools omitted when unwired", () => {
58
- test("no attachmentTickets opt → neither ticket tool is generated", () => {
59
- const tools = generateMcpTools(store);
60
- expect(tools.find((t) => t.name === "request-attachment-upload")).toBeUndefined();
61
- expect(tools.find((t) => t.name === "request-attachment-download")).toBeUndefined();
62
- });
63
-
64
- test("attachmentTickets opt provided → both ticket tools are generated with the right verbs", () => {
65
- const provider = new FakeTicketProvider();
66
- const tools = generateMcpTools(store, {
67
- attachmentTickets: { provider, vaultName: "v1", urlBase: "https://host/vault/v1" },
68
- });
69
- const upload = findTool(tools, "request-attachment-upload");
70
- const download = findTool(tools, "request-attachment-download");
71
- expect(upload.requiredVerb).toBe("write");
72
- expect(download.requiredVerb).toBe("read");
73
- });
74
- });
75
-
76
- describe("request-attachment-upload — mint", () => {
77
- test("happy path: returns the ticket envelope and stores a matching provider record", async () => {
78
- const note = await store.createNote("# Target\n", { path: "target" });
79
- const provider = new FakeTicketProvider();
80
- const tools = generateMcpTools(store, {
81
- attachmentTickets: { provider, vaultName: "v1", urlBase: "https://host/vault/v1" },
82
- });
83
- const tool = findTool(tools, "request-attachment-upload");
84
-
85
- const before = Date.now();
86
- const result = await callTool(tool, { note: note.id, filename: "photo.png", size_bytes: 1024 });
87
- expect(result.method).toBe("PUT");
88
- expect(result.url).toMatch(/^https:\/\/host\/vault\/v1\/tickets\/[0-9a-f]{64}$/);
89
- expect(result.headers["content-type"]).toBe("image/png");
90
- expect(result.max_bytes).toBe(1024);
91
- expect(result.curl_example).toContain("curl -X PUT");
92
- expect(result.curl_example).toContain(result.url);
93
-
94
- const ticketId = result.url.split("/tickets/")[1] as string;
95
- const stored = provider.tickets.get(ticketId)!;
96
- expect(stored.kind).toBe("upload");
97
- expect(stored.noteId).toBe(note.id);
98
- expect(stored.filename).toBe("photo.png");
99
- expect(stored.mimeType).toBe("image/png");
100
- expect(stored.sizeBytes).toBe(1024);
101
- expect(stored.vaultName).toBe("v1");
102
- // TTL for a 1024-byte (<1 MiB) declared size is the 10-minute base.
103
- expect(stored.expiresAt - before).toBeGreaterThanOrEqual(10 * 60 * 1000 - 1000);
104
- expect(stored.expiresAt - before).toBeLessThanOrEqual(10 * 60 * 1000 + 5000);
105
- });
106
-
107
- test("mime_type is inferred from the filename extension when omitted", async () => {
108
- const note = await store.createNote("# T\n", { path: "t2" });
109
- const provider = new FakeTicketProvider();
110
- const tools = generateMcpTools(store, {
111
- attachmentTickets: { provider, vaultName: "v1", urlBase: "https://host/vault/v1" },
112
- });
113
- const result = await callTool(findTool(tools, "request-attachment-upload"), {
114
- note: note.id,
115
- filename: "notes.csv",
116
- size_bytes: 10,
117
- });
118
- expect(result.headers["content-type"]).toBe("text/csv; charset=utf-8");
119
- });
120
-
121
- test("transcribe: true rides through to the stored ticket", async () => {
122
- const note = await store.createNote("# T\n", { path: "t3" });
123
- const provider = new FakeTicketProvider();
124
- const tools = generateMcpTools(store, {
125
- attachmentTickets: { provider, vaultName: "v1", urlBase: "https://host/vault/v1" },
126
- });
127
- const result = await callTool(findTool(tools, "request-attachment-upload"), {
128
- note: note.id,
129
- filename: "memo.wav",
130
- size_bytes: 10,
131
- transcribe: true,
132
- });
133
- const ticketId = result.url.split("/tickets/")[1] as string;
134
- expect(provider.tickets.get(ticketId)!.transcribe).toBe(true);
135
- });
136
-
137
- test("missing note / filename / size_bytes → missing_required_field with how_to", async () => {
138
- const provider = new FakeTicketProvider();
139
- const tools = generateMcpTools(store, {
140
- attachmentTickets: { provider, vaultName: "v1", urlBase: "https://host/vault/v1" },
141
- });
142
- const tool = findTool(tools, "request-attachment-upload");
143
-
144
- const e1 = await expectToolError(tool, { filename: "a.png", size_bytes: 1 });
145
- expect(e1.error_type).toBe("missing_required_field");
146
- expect(e1.field).toBe("note");
147
- expect(typeof e1.how_to).toBe("string");
148
-
149
- const e2 = await expectToolError(tool, { note: "x", size_bytes: 1 });
150
- expect(e2.error_type).toBe("missing_required_field");
151
- expect(e2.field).toBe("filename");
152
-
153
- const e3 = await expectToolError(tool, { note: "x", filename: "a.png" });
154
- expect(e3.error_type).toBe("invalid_query");
155
- expect(e3.field).toBe("size_bytes");
156
- });
157
-
158
- test("size_bytes over the 100 MiB cap → file_too_large with limit/got/how_to", async () => {
159
- const note = await store.createNote("# T\n", { path: "t4" });
160
- const provider = new FakeTicketProvider();
161
- const tools = generateMcpTools(store, {
162
- attachmentTickets: { provider, vaultName: "v1", urlBase: "https://host/vault/v1" },
163
- });
164
- const err = await expectToolError(findTool(tools, "request-attachment-upload"), {
165
- note: note.id,
166
- filename: "huge.zip",
167
- size_bytes: MAX_TICKET_UPLOAD_BYTES + 1,
168
- });
169
- expect(err.error_type).toBe("file_too_large");
170
- expect(err.limit).toBe(MAX_TICKET_UPLOAD_BYTES);
171
- expect(err.got).toBe(MAX_TICKET_UPLOAD_BYTES + 1);
172
- expect(typeof err.how_to).toBe("string");
173
- expect(provider.tickets.size).toBe(0);
174
- });
175
-
176
- test("blocked (active-content) extension → blocked_upload_extension, no ticket minted", async () => {
177
- const note = await store.createNote("# T\n", { path: "t5" });
178
- const provider = new FakeTicketProvider();
179
- const tools = generateMcpTools(store, {
180
- attachmentTickets: { provider, vaultName: "v1", urlBase: "https://host/vault/v1" },
181
- });
182
- const err = await expectToolError(findTool(tools, "request-attachment-upload"), {
183
- note: note.id,
184
- filename: "evil.html",
185
- size_bytes: 10,
186
- });
187
- expect(err.error_type).toBe("blocked_upload_extension");
188
- expect(err.extension).toBe(".html");
189
- expect(provider.tickets.size).toBe(0);
190
- });
191
-
192
- test("unknown note → not_found", async () => {
193
- const provider = new FakeTicketProvider();
194
- const tools = generateMcpTools(store, {
195
- attachmentTickets: { provider, vaultName: "v1", urlBase: "https://host/vault/v1" },
196
- });
197
- const err = await expectToolError(findTool(tools, "request-attachment-upload"), {
198
- note: "does-not-exist",
199
- filename: "a.png",
200
- size_bytes: 10,
201
- });
202
- expect(err.error_type).toBe("not_found");
203
- });
204
-
205
- test("tag-scoped noteVisible: false → uniform not_found (no existence oracle)", async () => {
206
- const note = await store.createNote("# T\n", { path: "t6" });
207
- const provider = new FakeTicketProvider();
208
- const tools = generateMcpTools(store, {
209
- attachmentTickets: {
210
- provider,
211
- vaultName: "v1",
212
- urlBase: "https://host/vault/v1",
213
- noteVisible: async (_n: Note) => false,
214
- },
215
- });
216
- const err = await expectToolError(findTool(tools, "request-attachment-upload"), {
217
- note: note.id,
218
- filename: "a.png",
219
- size_bytes: 10,
220
- });
221
- expect(err.error_type).toBe("not_found");
222
- expect(provider.tickets.size).toBe(0);
223
- });
224
- });
225
-
226
- describe("request-attachment-download — mint", () => {
227
- test("happy path: returns the ticket envelope with mime_type + known size, stores a matching provider record", async () => {
228
- const note = await store.createNote("# T\n", { path: "t7" });
229
- const attachment = await store.addAttachment(note.id, "2026-07-17/x.png", "image/png", { size: 42 });
230
- const provider = new FakeTicketProvider();
231
- const tools = generateMcpTools(store, {
232
- attachmentTickets: { provider, vaultName: "v1", urlBase: "https://host/vault/v1" },
233
- });
234
- const result = await callTool(findTool(tools, "request-attachment-download"), {
235
- attachment_id: attachment.id,
236
- });
237
- expect(result.method).toBe("GET");
238
- expect(result.mime_type).toBe("image/png");
239
- expect(result.size_bytes).toBe(42);
240
- expect(result.curl_example).toContain(result.url);
241
-
242
- const ticketId = result.url.split("/tickets/")[1] as string;
243
- const stored = provider.tickets.get(ticketId)!;
244
- expect(stored.kind).toBe("download");
245
- expect(stored.attachmentId).toBe(attachment.id);
246
- });
247
-
248
- test("missing attachment_id → missing_required_field", async () => {
249
- const provider = new FakeTicketProvider();
250
- const tools = generateMcpTools(store, {
251
- attachmentTickets: { provider, vaultName: "v1", urlBase: "https://host/vault/v1" },
252
- });
253
- const err = await expectToolError(findTool(tools, "request-attachment-download"), {});
254
- expect(err.error_type).toBe("missing_required_field");
255
- });
256
-
257
- test("unknown attachment → not_found", async () => {
258
- const provider = new FakeTicketProvider();
259
- const tools = generateMcpTools(store, {
260
- attachmentTickets: { provider, vaultName: "v1", urlBase: "https://host/vault/v1" },
261
- });
262
- const err = await expectToolError(findTool(tools, "request-attachment-download"), {
263
- attachment_id: "does-not-exist",
264
- });
265
- expect(err.error_type).toBe("not_found");
266
- });
267
-
268
- test("tag-scoped noteVisible: false on the owning note → not_found, same shape as unknown attachment", async () => {
269
- const note = await store.createNote("# T\n", { path: "t8" });
270
- const attachment = await store.addAttachment(note.id, "2026-07-17/y.png", "image/png");
271
- const provider = new FakeTicketProvider();
272
- const tools = generateMcpTools(store, {
273
- attachmentTickets: {
274
- provider,
275
- vaultName: "v1",
276
- urlBase: "https://host/vault/v1",
277
- noteVisible: async (_n: Note) => false,
278
- },
279
- });
280
- const err = await expectToolError(findTool(tools, "request-attachment-download"), {
281
- attachment_id: attachment.id,
282
- });
283
- expect(err.error_type).toBe("not_found");
284
- expect(provider.tickets.size).toBe(0);
285
- });
286
- });
@@ -1,190 +0,0 @@
1
- /**
2
- * Title axis (ratified 2026-07-17): a note's title IS its first line —
3
- * derived from content, never stored. This file covers:
4
- * - `computeDisplayTitle` derivation (first non-empty line, heading
5
- * markers stripped, truncation, null-on-empty);
6
- * - `displayTitle` presence on the lean `NoteIndex` shape, both via the
7
- * MCP `query-notes` list path and the REST `GET /notes` list path.
8
- *
9
- * The search title-BOOST (ranking notes whose first line matches query
10
- * terms above the rest) lives in `search-title-boost.test.ts` — a
11
- * different concern (ranking) from derivation (this file).
12
- */
13
- import { describe, it, expect, beforeEach } from "bun:test";
14
- import { Database } from "bun:sqlite";
15
- import { SqliteStore } from "./store.js";
16
- import { generateMcpTools } from "./mcp.js";
17
- import { computeDisplayTitle, toNoteIndex, DISPLAY_TITLE_MAX_LEN } from "./notes.js";
18
- import type { Note } from "./types.js";
19
-
20
- describe("computeDisplayTitle", () => {
21
- it("returns the first line when content is a single line", () => {
22
- expect(computeDisplayTitle("Grocery List")).toBe("Grocery List");
23
- });
24
-
25
- it("returns the first NON-EMPTY line, skipping leading blank lines", () => {
26
- expect(computeDisplayTitle("\n\n \nReal Title\nbody text")).toBe("Real Title");
27
- });
28
-
29
- it("strips a leading markdown heading marker + its whitespace", () => {
30
- expect(computeDisplayTitle("# Grocery List\nmilk, eggs")).toBe("Grocery List");
31
- expect(computeDisplayTitle("## Section Two\nbody")).toBe("Section Two");
32
- expect(computeDisplayTitle("###### h6\nbody")).toBe("h6");
33
- });
34
-
35
- it("strips a heading marker with no space after it", () => {
36
- expect(computeDisplayTitle("###Title\nbody")).toBe("Title");
37
- });
38
-
39
- it("treats a heading-marker-only line as blank and moves to the next line", () => {
40
- expect(computeDisplayTitle("##\nReal Title")).toBe("Real Title");
41
- });
42
-
43
- it("does not strip markers past the first six (never a valid heading)", () => {
44
- // 7 leading `#`s: the regex only matches 1-6, so the 7th `#` survives
45
- // as ordinary content.
46
- expect(computeDisplayTitle("####### Title")).toBe("# Title");
47
- });
48
-
49
- it("returns null for empty content", () => {
50
- expect(computeDisplayTitle("")).toBeNull();
51
- });
52
-
53
- it("returns null for null/undefined content", () => {
54
- expect(computeDisplayTitle(null)).toBeNull();
55
- expect(computeDisplayTitle(undefined)).toBeNull();
56
- });
57
-
58
- it("returns null when content is whitespace-only", () => {
59
- expect(computeDisplayTitle(" \n\t\n ")).toBeNull();
60
- });
61
-
62
- it("returns null when every line is a bare heading marker", () => {
63
- expect(computeDisplayTitle("#\n##\n### ")).toBeNull();
64
- });
65
-
66
- it("truncates to DISPLAY_TITLE_MAX_LEN code points", () => {
67
- const longTitle = "a".repeat(200);
68
- const result = computeDisplayTitle(longTitle)!;
69
- expect(result.length).toBe(DISPLAY_TITLE_MAX_LEN);
70
- expect(result).toBe("a".repeat(DISPLAY_TITLE_MAX_LEN));
71
- });
72
-
73
- it("does not truncate a title at or under the limit", () => {
74
- const exact = "a".repeat(DISPLAY_TITLE_MAX_LEN);
75
- expect(computeDisplayTitle(exact)).toBe(exact);
76
- const short = "short title";
77
- expect(computeDisplayTitle(short)).toBe(short);
78
- });
79
-
80
- it("truncates by Unicode code point, not UTF-16 code unit (no split surrogate pairs)", () => {
81
- // Astral-plane emoji are 2 UTF-16 code units each but 1 code point.
82
- const emojiTitle = "😀".repeat(150);
83
- const result = computeDisplayTitle(emojiTitle)!;
84
- expect(Array.from(result).length).toBe(DISPLAY_TITLE_MAX_LEN);
85
- });
86
-
87
- describe("leading frontmatter block (misuse path — direct create with raw frontmatter)", () => {
88
- it("skips a closed frontmatter block and derives the first line of the DOCUMENT", () => {
89
- expect(computeDisplayTitle("---\ntitle: X\n---\n# Real Title")).toBe("Real Title");
90
- });
91
-
92
- it("skips a closed frontmatter block with multiple fields + blank lines after", () => {
93
- expect(
94
- computeDisplayTitle("---\ntitle: X\ntags: [a, b]\n---\n\n\nReal Title\nbody"),
95
- ).toBe("Real Title");
96
- });
97
-
98
- it("falls back to treating an UNTERMINATED opening `---` as ordinary content", () => {
99
- // No closing fence anywhere in the content — a note whose real first
100
- // line is literally "---" (e.g. a markdown horizontal rule opener)
101
- // must not be mangled.
102
- expect(computeDisplayTitle("---\nnot frontmatter, just a rule\nmore text")).toBe("---");
103
- });
104
-
105
- it("returns null when content is EXACTLY a closed frontmatter block (nothing after)", () => {
106
- expect(computeDisplayTitle("---\ntitle: X\n---\n")).toBeNull();
107
- expect(computeDisplayTitle("---\ntitle: X\n---")).toBeNull();
108
- });
109
-
110
- it("returns null when only blank lines follow a closed frontmatter block", () => {
111
- expect(computeDisplayTitle("---\ntitle: X\n---\n\n \n")).toBeNull();
112
- });
113
-
114
- it("does not treat a closing fence found past the bounded scan window as a frontmatter close", () => {
115
- // Opening `---` with the closing fence past FRONTMATTER_SCAN_LINES
116
- // (100): the scan gives up and falls back to ordinary-content
117
- // behavior, deriving from line 0.
118
- const filler = Array.from({ length: 150 }, (_, i) => `line ${i}`).join("\n");
119
- expect(computeDisplayTitle(`---\n${filler}\n---\nReal Title`)).toBe("---");
120
- });
121
-
122
- it("normal content with no leading `---` is byte-identical to prior behavior", () => {
123
- expect(computeDisplayTitle("# Grocery List\nmilk, eggs")).toBe("Grocery List");
124
- expect(computeDisplayTitle("Just a note\nbody text")).toBe("Just a note");
125
- });
126
- });
127
- });
128
-
129
- describe("toNoteIndex — displayTitle wiring", () => {
130
- const baseNote: Note = {
131
- id: "n1",
132
- content: "# Real Title\nbody",
133
- createdAt: new Date().toISOString(),
134
- tags: [],
135
- metadata: {},
136
- };
137
-
138
- it("carries the computed display title onto the lean shape", () => {
139
- const index = toNoteIndex(baseNote);
140
- expect(index.displayTitle).toBe("Real Title");
141
- });
142
-
143
- it("is null when content is empty", () => {
144
- const index = toNoteIndex({ ...baseNote, content: "" });
145
- expect(index.displayTitle).toBeNull();
146
- });
147
-
148
- it("is null when content is missing entirely (defensive — matches preview's ?? \"\" fallback)", () => {
149
- const { content: _drop, ...rest } = baseNote;
150
- const index = toNoteIndex(rest as Note);
151
- expect(index.displayTitle).toBeNull();
152
- });
153
- });
154
-
155
- let store: SqliteStore;
156
- let db: Database;
157
-
158
- beforeEach(() => {
159
- db = new Database(":memory:");
160
- store = new SqliteStore(db);
161
- });
162
-
163
- describe("displayTitle — list-shape presence (MCP)", () => {
164
- it("query-notes list mode (include_content default false) carries displayTitle", async () => {
165
- await store.createNote("# Meeting Notes\nagenda: budget", { path: "a" });
166
- const tools = generateMcpTools(store);
167
- const queryNotes = tools.find((t) => t.name === "query-notes")!;
168
- const result = await queryNotes.execute({}) as any[];
169
- expect(result).toHaveLength(1);
170
- expect(result[0].displayTitle).toBe("Meeting Notes");
171
- expect(result[0].content).toBeUndefined();
172
- });
173
-
174
- it("query-notes include_content=true does NOT carry displayTitle (full Note shape, unchanged)", async () => {
175
- await store.createNote("# Meeting Notes\nagenda: budget", { path: "a" });
176
- const tools = generateMcpTools(store);
177
- const queryNotes = tools.find((t) => t.name === "query-notes")!;
178
- const result = await queryNotes.execute({ include_content: true }) as any[];
179
- expect(result[0].content).toBeTruthy();
180
- expect(result[0].displayTitle).toBeUndefined();
181
- });
182
-
183
- it("displayTitle is null on the lean shape for an empty note", async () => {
184
- await store.createNote("", { path: "empty" });
185
- const tools = generateMcpTools(store);
186
- const queryNotes = tools.find((t) => t.name === "query-notes")!;
187
- const result = await queryNotes.execute({}) as any[];
188
- expect(result[0].displayTitle).toBeNull();
189
- });
190
- });
@@ -1,96 +0,0 @@
1
- /**
2
- * `computeLede` — the first non-empty paragraph AFTER a note's title line,
3
- * used by `expand_mode: "summary"` (core/src/expand.ts) as a fallback when
4
- * `metadata.summary` is absent (summaries-as-content, 2026-07-17 dialogue).
5
- *
6
- * Shares its title-line rule (including the leading-frontmatter skip) with
7
- * `computeDisplayTitle` — see `display-title.test.ts` for that function's
8
- * own coverage. This file covers only the lede-specific behavior: where the
9
- * paragraph starts, how it's collapsed, the length cap, and the
10
- * title-only/no-content null cases.
11
- */
12
- import { describe, it, expect } from "bun:test";
13
- import { computeLede, LEDE_MAX_LEN } from "./notes.js";
14
-
15
- describe("computeLede", () => {
16
- it("returns the first paragraph after a heading title, blank-line separated", () => {
17
- expect(
18
- computeLede("# Title\n\nThis is the lede paragraph.\n\nSecond paragraph — not the lede."),
19
- ).toBe("This is the lede paragraph.");
20
- });
21
-
22
- it("joins a multi-line paragraph into one collapsed line", () => {
23
- expect(
24
- computeLede("# Title\n\nThis lede paragraph\nspans two lines.\n\nSecond paragraph."),
25
- ).toBe("This lede paragraph spans two lines.");
26
- });
27
-
28
- it("treats text immediately following the title (no blank line) as the lede", () => {
29
- expect(computeLede("Grocery List\nmilk, eggs\ncheese")).toBe("milk, eggs cheese");
30
- });
31
-
32
- it("collapses internal whitespace runs to single spaces", () => {
33
- expect(computeLede("Title\n\nLine one with extra spaces.")).toBe(
34
- "Line one with extra spaces.",
35
- );
36
- });
37
-
38
- it("returns null for a title-only note (does not repeat the title)", () => {
39
- expect(computeLede("Just a title")).toBeNull();
40
- expect(computeLede("# Just a title")).toBeNull();
41
- });
42
-
43
- it("returns null when only blank lines follow the title", () => {
44
- expect(computeLede("# Title\n\n\n \n")).toBeNull();
45
- });
46
-
47
- it("returns null for empty, null, or undefined content", () => {
48
- expect(computeLede("")).toBeNull();
49
- expect(computeLede(null)).toBeNull();
50
- expect(computeLede(undefined)).toBeNull();
51
- });
52
-
53
- it("returns null when content is whitespace-only (no title at all)", () => {
54
- expect(computeLede(" \n\t\n ")).toBeNull();
55
- });
56
-
57
- describe("leading frontmatter block", () => {
58
- it("finds the lede after a closed frontmatter block AND its title line", () => {
59
- expect(
60
- computeLede("---\ntitle: X\n---\n# Real Title\n\nThe lede text."),
61
- ).toBe("The lede text.");
62
- });
63
-
64
- it("finds the lede when the title immediately follows frontmatter with no blank line", () => {
65
- expect(
66
- computeLede("---\ntitle: X\n---\nReal Title\nlede text right after."),
67
- ).toBe("lede text right after.");
68
- });
69
-
70
- it("returns null for a frontmatter-only note with no body after the title", () => {
71
- expect(computeLede("---\ntitle: X\n---\nReal Title")).toBeNull();
72
- });
73
- });
74
-
75
- describe("length cap", () => {
76
- it("truncates to LEDE_MAX_LEN code points", () => {
77
- const longParagraph = "a".repeat(1000);
78
- const result = computeLede(`# Title\n\n${longParagraph}`)!;
79
- expect(result.length).toBe(LEDE_MAX_LEN);
80
- expect(result).toBe("a".repeat(LEDE_MAX_LEN));
81
- });
82
-
83
- it("does not truncate a lede at or under the cap", () => {
84
- const exact = "a".repeat(LEDE_MAX_LEN);
85
- expect(computeLede(`# Title\n\n${exact}`)).toBe(exact);
86
- const short = "short lede";
87
- expect(computeLede(`# Title\n\n${short}`)).toBe(short);
88
- });
89
-
90
- it("truncates by Unicode code point, not UTF-16 code unit (no split surrogate pairs)", () => {
91
- const emojiParagraph = "😀".repeat(500);
92
- const result = computeLede(`# Title\n\n${emojiParagraph}`)!;
93
- expect(Array.from(result).length).toBe(LEDE_MAX_LEN);
94
- });
95
- });
96
- });