@mandujs/core 0.41.2 → 0.43.0

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 (88) hide show
  1. package/package.json +21 -4
  2. package/src/auth/__tests__/login.test.ts +420 -419
  3. package/src/auth/__tests__/reset.test.ts +296 -296
  4. package/src/brain/adapters/anthropic-oauth.ts +421 -420
  5. package/src/brain/adapters/index.ts +2 -1
  6. package/src/brain/adapters/ollama.ts +1 -1
  7. package/src/brain/adapters/openai-oauth.ts +534 -533
  8. package/src/brain/brain.ts +2 -1
  9. package/src/brain/redactor.ts +196 -196
  10. package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -149
  11. package/src/bundler/__tests__/cold-start.test.ts +504 -504
  12. package/src/bundler/__tests__/fast-refresh.test.ts +607 -606
  13. package/src/bundler/__tests__/hdr.test.ts +1 -1
  14. package/src/bundler/analyzer.ts +958 -958
  15. package/src/bundler/build.ts +104 -14
  16. package/src/bundler/dev.ts +125 -0
  17. package/src/bundler/hmr-types.ts +1 -0
  18. package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -0
  19. package/src/bundler/plugins/index.ts +14 -0
  20. package/src/bundler/plugins/react-compiler-lint.ts +253 -0
  21. package/src/bundler/plugins/react-compiler.ts +162 -0
  22. package/src/bundler/types.ts +12 -0
  23. package/src/change/integrity.ts +2 -1
  24. package/src/client/index.ts +10 -0
  25. package/src/client/island.ts +38 -11
  26. package/src/client/router.ts +6 -1
  27. package/src/config/mandu.ts +57 -0
  28. package/src/config/validate.ts +42 -0
  29. package/src/content/collection.ts +844 -809
  30. package/src/content/content-layer.ts +316 -314
  31. package/src/content/content.test.ts +433 -433
  32. package/src/content/digest.ts +133 -133
  33. package/src/content/generate-types.ts +168 -168
  34. package/src/content/index.ts +6 -1
  35. package/src/content/llms-txt.ts +277 -277
  36. package/src/contract/define.ts +474 -474
  37. package/src/contract/route-helpers.ts +2 -1
  38. package/src/contract/zod-utils.ts +158 -155
  39. package/src/db/index.ts +513 -513
  40. package/src/desktop/__tests__/smoke.test.ts +100 -100
  41. package/src/desktop/webview-fallback.ts +583 -583
  42. package/src/desktop/window.ts +3 -1
  43. package/src/dev-error-overlay/overlay-client.ts +300 -300
  44. package/src/devtools/ai/mcp-connector.ts +499 -498
  45. package/src/devtools/client/components/kitchen-root.tsx +7 -2
  46. package/src/email/resend.ts +163 -163
  47. package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -0
  48. package/src/guard/ast-analyzer.ts +806 -806
  49. package/src/guard/graph.ts +898 -898
  50. package/src/guard/index.ts +16 -0
  51. package/src/guard/statistics.ts +578 -578
  52. package/src/guard/tsgolint-bridge.ts +512 -0
  53. package/src/i18n/locale-resolver.ts +214 -214
  54. package/src/id/__tests__/id.test.ts +120 -120
  55. package/src/intent/index.ts +321 -321
  56. package/src/island/index.ts +39 -23
  57. package/src/kitchen/api/contract-api.ts +15 -8
  58. package/src/kitchen/kitchen-ui.ts +2137 -2137
  59. package/src/lockfile/index.ts +3 -2
  60. package/src/middleware/oauth/__tests__/oauth.test.ts +575 -574
  61. package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -642
  62. package/src/middleware/secure/index.ts +417 -417
  63. package/src/observability/event-bus.ts +2 -2
  64. package/src/observability/metrics.ts +334 -334
  65. package/src/observability/tracing.ts +694 -694
  66. package/src/openapi/generator.ts +1 -1
  67. package/src/perf/user-marks.ts +553 -553
  68. package/src/plugins/registry.ts +387 -387
  69. package/src/resource/ddl/diff.ts +392 -392
  70. package/src/resource/ddl/snapshot.ts +448 -447
  71. package/src/resource/generator-schema.ts +477 -476
  72. package/src/resource/parser.ts +4 -2
  73. package/src/resource/schema.ts +1 -1
  74. package/src/router/fs-patterns.ts +422 -422
  75. package/src/runtime/fast-refresh-types.ts +126 -128
  76. package/src/runtime/image-handler.ts +206 -195
  77. package/src/runtime/router.test.ts +476 -476
  78. package/src/runtime/security.ts +155 -155
  79. package/src/runtime/server.ts +36 -19
  80. package/src/runtime/session-key.ts +328 -328
  81. package/src/scheduler/__tests__/scheduler.test.ts +514 -514
  82. package/src/seo/resolve/index.ts +353 -353
  83. package/src/spec/load.ts +1 -1
  84. package/src/testing/reporter.ts +676 -676
  85. package/src/testing/server.ts +196 -196
  86. package/src/testing/snapshot.ts +444 -444
  87. package/src/utils/__tests__/lru-cache.test.ts +186 -186
  88. package/src/utils/bun.ts +8 -8
@@ -1,433 +1,433 @@
1
- /**
2
- * Content Layer Tests
3
- */
4
-
5
- import { describe, test, expect, beforeEach, afterEach } from "bun:test";
6
- import { generateDigest, generateFileDigest, combineDigests, digestsMatch } from "./digest";
7
- import { createDataStore, type ContentDataStore } from "./data-store";
8
- import { createMetaStore, type ContentMetaStore } from "./meta-store";
9
- import { createLoaderContext, createSimpleMarkdownRenderer } from "./loader-context";
10
- import { z } from "zod";
11
- import fs from "fs";
12
- import path from "path";
13
- import os from "os";
14
-
15
- // ============================================================================
16
- // Digest Tests
17
- // ============================================================================
18
-
19
- describe("digest", () => {
20
- test("generateDigest creates consistent hash for same data", () => {
21
- const data = { title: "Hello", count: 42 };
22
- const hash1 = generateDigest(data);
23
- const hash2 = generateDigest(data);
24
-
25
- expect(hash1).toBe(hash2);
26
- expect(hash1.length).toBe(16);
27
- });
28
-
29
- test("generateDigest creates different hash for different data", () => {
30
- const hash1 = generateDigest({ a: 1 });
31
- const hash2 = generateDigest({ a: 2 });
32
-
33
- expect(hash1).not.toBe(hash2);
34
- });
35
-
36
- test("generateDigest handles nested objects", () => {
37
- const data = {
38
- user: { name: "John", age: 30 },
39
- tags: ["a", "b"],
40
- };
41
-
42
- const hash = generateDigest(data);
43
- expect(hash.length).toBe(16);
44
- });
45
-
46
- test("generateDigest is order-independent for object keys", () => {
47
- const hash1 = generateDigest({ a: 1, b: 2 });
48
- const hash2 = generateDigest({ b: 2, a: 1 });
49
-
50
- expect(hash1).toBe(hash2);
51
- });
52
-
53
- test("generateFileDigest works with string content", () => {
54
- const content = "# Hello World\n\nThis is content.";
55
- const hash = generateFileDigest(content);
56
-
57
- expect(hash.length).toBe(16);
58
- });
59
-
60
- test("combineDigests combines multiple digests", () => {
61
- const d1 = generateDigest({ a: 1 });
62
- const d2 = generateDigest({ b: 2 });
63
- const combined = combineDigests([d1, d2]);
64
-
65
- expect(combined.length).toBe(16);
66
- expect(combined).not.toBe(d1);
67
- expect(combined).not.toBe(d2);
68
- });
69
-
70
- test("digestsMatch compares digests correctly", () => {
71
- const d1 = generateDigest({ x: 1 });
72
- const d2 = generateDigest({ x: 1 });
73
- const d3 = generateDigest({ x: 2 });
74
-
75
- expect(digestsMatch(d1, d2)).toBe(true);
76
- expect(digestsMatch(d1, d3)).toBe(false);
77
- expect(digestsMatch(undefined, d1)).toBe(false);
78
- });
79
- });
80
-
81
- // ============================================================================
82
- // DataStore Tests
83
- // ============================================================================
84
-
85
- describe("DataStore", () => {
86
- let store: ContentDataStore;
87
- let tempDir: string;
88
-
89
- beforeEach(() => {
90
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mandu-test-"));
91
- store = createDataStore({
92
- filePath: path.join(tempDir, "store.json"),
93
- autoSave: false,
94
- });
95
- });
96
-
97
- afterEach(() => {
98
- store.dispose();
99
- fs.rmSync(tempDir, { recursive: true, force: true });
100
- });
101
-
102
- test("getStore returns a DataStore interface", () => {
103
- const posts = store.getStore("posts");
104
-
105
- expect(posts.get).toBeDefined();
106
- expect(posts.set).toBeDefined();
107
- expect(posts.delete).toBeDefined();
108
- expect(posts.entries).toBeDefined();
109
- });
110
-
111
- test("set and get work correctly", () => {
112
- const posts = store.getStore("posts");
113
-
114
- posts.set({
115
- id: "hello-world",
116
- data: { title: "Hello World" },
117
- digest: "abc123",
118
- });
119
-
120
- const entry = posts.get("hello-world");
121
- expect(entry).toBeDefined();
122
- expect(entry?.data.title).toBe("Hello World");
123
- });
124
-
125
- test("set returns true for new/changed entries", () => {
126
- const posts = store.getStore("posts");
127
-
128
- const result1 = posts.set({
129
- id: "post1",
130
- data: { title: "Post 1" },
131
- digest: "digest1",
132
- });
133
- expect(result1).toBe(true);
134
-
135
- // Same digest, no change
136
- const result2 = posts.set({
137
- id: "post1",
138
- data: { title: "Post 1" },
139
- digest: "digest1",
140
- });
141
- expect(result2).toBe(false);
142
-
143
- // Different digest, changed
144
- const result3 = posts.set({
145
- id: "post1",
146
- data: { title: "Post 1 Updated" },
147
- digest: "digest2",
148
- });
149
- expect(result3).toBe(true);
150
- });
151
-
152
- test("delete removes entry", () => {
153
- const posts = store.getStore("posts");
154
-
155
- posts.set({ id: "to-delete", data: {}, digest: "x" });
156
- expect(posts.has("to-delete")).toBe(true);
157
-
158
- posts.delete("to-delete");
159
- expect(posts.has("to-delete")).toBe(false);
160
- });
161
-
162
- test("entries returns all entries", () => {
163
- const posts = store.getStore("posts");
164
-
165
- posts.set({ id: "p1", data: { n: 1 }, digest: "a" });
166
- posts.set({ id: "p2", data: { n: 2 }, digest: "b" });
167
-
168
- const entries = posts.entries();
169
- expect(entries.length).toBe(2);
170
- expect(entries.map(([id]) => id).sort()).toEqual(["p1", "p2"]);
171
- });
172
-
173
- test("size returns entry count", () => {
174
- const posts = store.getStore("posts");
175
-
176
- expect(posts.size()).toBe(0);
177
-
178
- posts.set({ id: "p1", data: {}, digest: "a" });
179
- posts.set({ id: "p2", data: {}, digest: "b" });
180
-
181
- expect(posts.size()).toBe(2);
182
- });
183
-
184
- test("clear removes all entries", () => {
185
- const posts = store.getStore("posts");
186
-
187
- posts.set({ id: "p1", data: {}, digest: "a" });
188
- posts.set({ id: "p2", data: {}, digest: "b" });
189
-
190
- posts.clear();
191
- expect(posts.size()).toBe(0);
192
- });
193
-
194
- test("save and load persist data", async () => {
195
- const posts = store.getStore("posts");
196
-
197
- posts.set({
198
- id: "persistent",
199
- data: { title: "Persistent Post" },
200
- digest: "persistent-digest",
201
- });
202
-
203
- await store.save();
204
-
205
- // Create new store and load
206
- const store2 = createDataStore({
207
- filePath: path.join(tempDir, "store.json"),
208
- autoSave: false,
209
- });
210
-
211
- await store2.load();
212
-
213
- const posts2 = store2.getStore("posts");
214
- const entry = posts2.get("persistent");
215
-
216
- expect(entry).toBeDefined();
217
- expect(entry?.data.title).toBe("Persistent Post");
218
-
219
- store2.dispose();
220
- });
221
- });
222
-
223
- // ============================================================================
224
- // MetaStore Tests
225
- // ============================================================================
226
-
227
- describe("MetaStore", () => {
228
- let metaStore: ContentMetaStore;
229
- let tempDir: string;
230
-
231
- beforeEach(() => {
232
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mandu-meta-test-"));
233
- metaStore = createMetaStore({
234
- filePath: path.join(tempDir, "meta.json"),
235
- autoSave: false,
236
- });
237
- });
238
-
239
- afterEach(() => {
240
- fs.rmSync(tempDir, { recursive: true, force: true });
241
- });
242
-
243
- test("getStore returns MetaStore interface", () => {
244
- const meta = metaStore.getStore("posts");
245
-
246
- expect(meta.get).toBeDefined();
247
- expect(meta.set).toBeDefined();
248
- expect(meta.has).toBeDefined();
249
- });
250
-
251
- test("set and get work correctly", () => {
252
- const meta = metaStore.getStore("api-data");
253
-
254
- meta.set("lastSync", "2024-01-15T10:00:00Z");
255
- meta.set("cursor", "abc123");
256
-
257
- expect(meta.get("lastSync")).toBe("2024-01-15T10:00:00Z");
258
- expect(meta.get("cursor")).toBe("abc123");
259
- expect(meta.get("nonexistent")).toBeUndefined();
260
- });
261
-
262
- test("has checks existence", () => {
263
- const meta = metaStore.getStore("collection");
264
-
265
- expect(meta.has("key")).toBe(false);
266
-
267
- meta.set("key", "value");
268
- expect(meta.has("key")).toBe(true);
269
- });
270
-
271
- test("delete removes key", () => {
272
- const meta = metaStore.getStore("collection");
273
-
274
- meta.set("toDelete", "value");
275
- expect(meta.has("toDelete")).toBe(true);
276
-
277
- meta.delete("toDelete");
278
- expect(meta.has("toDelete")).toBe(false);
279
- });
280
-
281
- test("entries returns all key-value pairs", () => {
282
- const meta = metaStore.getStore("collection");
283
-
284
- meta.set("k1", "v1");
285
- meta.set("k2", "v2");
286
-
287
- const entries = meta.entries();
288
- expect(entries.length).toBe(2);
289
- });
290
- });
291
-
292
- // ============================================================================
293
- // LoaderContext Tests
294
- // ============================================================================
295
-
296
- describe("LoaderContext", () => {
297
- let dataStore: ContentDataStore;
298
- let metaStore: ContentMetaStore;
299
- let tempDir: string;
300
-
301
- beforeEach(() => {
302
- tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mandu-ctx-test-"));
303
- dataStore = createDataStore({ autoSave: false });
304
- metaStore = createMetaStore({ autoSave: false });
305
- });
306
-
307
- afterEach(() => {
308
- dataStore.dispose();
309
- fs.rmSync(tempDir, { recursive: true, force: true });
310
- });
311
-
312
- test("creates context with all properties", () => {
313
- const context = createLoaderContext({
314
- collection: "posts",
315
- store: dataStore.getStore("posts"),
316
- meta: metaStore.getStore("posts"),
317
- config: { root: tempDir },
318
- });
319
-
320
- expect(context.collection).toBe("posts");
321
- expect(context.store).toBeDefined();
322
- expect(context.meta).toBeDefined();
323
- expect(context.logger).toBeDefined();
324
- expect(context.parseData).toBeDefined();
325
- expect(context.generateDigest).toBeDefined();
326
- });
327
-
328
- test("parseData validates with schema", async () => {
329
- const schema = z.object({
330
- title: z.string(),
331
- count: z.number(),
332
- });
333
-
334
- const context = createLoaderContext({
335
- collection: "posts",
336
- store: dataStore.getStore("posts"),
337
- meta: metaStore.getStore("posts"),
338
- config: { root: tempDir },
339
- schema,
340
- });
341
-
342
- // Valid data
343
- const valid = await context.parseData({
344
- id: "test",
345
- data: { title: "Hello", count: 5 },
346
- });
347
-
348
- expect((valid as Record<string, unknown>).title).toBe("Hello");
349
- expect((valid as Record<string, unknown>).count).toBe(5);
350
-
351
- // Invalid data should throw
352
- await expect(
353
- context.parseData({
354
- id: "invalid",
355
- data: { title: 123, count: "not a number" },
356
- })
357
- ).rejects.toThrow();
358
- });
359
-
360
- test("parseData passes through without schema", async () => {
361
- const context = createLoaderContext({
362
- collection: "posts",
363
- store: dataStore.getStore("posts"),
364
- meta: metaStore.getStore("posts"),
365
- config: { root: tempDir },
366
- });
367
-
368
- const data = await context.parseData({
369
- id: "test",
370
- data: { anything: "goes" },
371
- });
372
-
373
- expect((data as Record<string, unknown>).anything).toBe("goes");
374
- });
375
-
376
- test("generateDigest creates hash", () => {
377
- const context = createLoaderContext({
378
- collection: "posts",
379
- store: dataStore.getStore("posts"),
380
- meta: metaStore.getStore("posts"),
381
- config: { root: tempDir },
382
- });
383
-
384
- const hash = context.generateDigest({ test: "data" });
385
- expect(hash.length).toBe(16);
386
- });
387
- });
388
-
389
- // ============================================================================
390
- // Markdown Renderer Tests
391
- // ============================================================================
392
-
393
- describe("SimpleMarkdownRenderer", () => {
394
- const render = createSimpleMarkdownRenderer();
395
-
396
- test("renders headings", async () => {
397
- const result = await render("# Heading 1\n## Heading 2");
398
-
399
- expect(result.html).toContain("<h1>Heading 1</h1>");
400
- expect(result.html).toContain("<h2>Heading 2</h2>");
401
- });
402
-
403
- test("extracts headings", async () => {
404
- const result = await render("# Title\n## Section 1\n### Subsection");
405
-
406
- expect(result.headings).toBeDefined();
407
- expect(result.headings?.length).toBe(3);
408
- expect(result.headings?.[0]).toEqual({
409
- depth: 1,
410
- text: "Title",
411
- slug: "title",
412
- });
413
- });
414
-
415
- test("renders bold and italic", async () => {
416
- const result = await render("**bold** and *italic*");
417
-
418
- expect(result.html).toContain("<strong>bold</strong>");
419
- expect(result.html).toContain("<em>italic</em>");
420
- });
421
-
422
- test("renders links", async () => {
423
- const result = await render("[Link Text](https://example.com)");
424
-
425
- expect(result.html).toContain('<a href="https://example.com">Link Text</a>');
426
- });
427
-
428
- test("renders code", async () => {
429
- const result = await render("`inline code`");
430
-
431
- expect(result.html).toContain("<code>inline code</code>");
432
- });
433
- });
1
+ /**
2
+ * Content Layer Tests
3
+ */
4
+
5
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
6
+ import { generateDigest, generateFileDigest, combineDigests, digestsMatch } from "./digest";
7
+ import { createDataStore, type ContentDataStore } from "./data-store";
8
+ import { createMetaStore, type ContentMetaStore } from "./meta-store";
9
+ import { createLoaderContext, createSimpleMarkdownRenderer } from "./loader-context";
10
+ import { z } from "zod";
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import os from "os";
14
+
15
+ // ============================================================================
16
+ // Digest Tests
17
+ // ============================================================================
18
+
19
+ describe("digest", () => {
20
+ test("generateDigest creates consistent hash for same data", () => {
21
+ const data = { title: "Hello", count: 42 };
22
+ const hash1 = generateDigest(data);
23
+ const hash2 = generateDigest(data);
24
+
25
+ expect(hash1).toBe(hash2);
26
+ expect(hash1.length).toBe(16);
27
+ });
28
+
29
+ test("generateDigest creates different hash for different data", () => {
30
+ const hash1 = generateDigest({ a: 1 });
31
+ const hash2 = generateDigest({ a: 2 });
32
+
33
+ expect(hash1).not.toBe(hash2);
34
+ });
35
+
36
+ test("generateDigest handles nested objects", () => {
37
+ const data = {
38
+ user: { name: "John", age: 30 },
39
+ tags: ["a", "b"],
40
+ };
41
+
42
+ const hash = generateDigest(data);
43
+ expect(hash.length).toBe(16);
44
+ });
45
+
46
+ test("generateDigest is order-independent for object keys", () => {
47
+ const hash1 = generateDigest({ a: 1, b: 2 });
48
+ const hash2 = generateDigest({ b: 2, a: 1 });
49
+
50
+ expect(hash1).toBe(hash2);
51
+ });
52
+
53
+ test("generateFileDigest works with string content", () => {
54
+ const content = "# Hello World\n\nThis is content.";
55
+ const hash = generateFileDigest(content);
56
+
57
+ expect(hash.length).toBe(16);
58
+ });
59
+
60
+ test("combineDigests combines multiple digests", () => {
61
+ const d1 = generateDigest({ a: 1 });
62
+ const d2 = generateDigest({ b: 2 });
63
+ const combined = combineDigests([d1, d2]);
64
+
65
+ expect(combined.length).toBe(16);
66
+ expect(combined).not.toBe(d1);
67
+ expect(combined).not.toBe(d2);
68
+ });
69
+
70
+ test("digestsMatch compares digests correctly", () => {
71
+ const d1 = generateDigest({ x: 1 });
72
+ const d2 = generateDigest({ x: 1 });
73
+ const d3 = generateDigest({ x: 2 });
74
+
75
+ expect(digestsMatch(d1, d2)).toBe(true);
76
+ expect(digestsMatch(d1, d3)).toBe(false);
77
+ expect(digestsMatch(undefined, d1)).toBe(false);
78
+ });
79
+ });
80
+
81
+ // ============================================================================
82
+ // DataStore Tests
83
+ // ============================================================================
84
+
85
+ describe("DataStore", () => {
86
+ let store: ContentDataStore;
87
+ let tempDir: string;
88
+
89
+ beforeEach(() => {
90
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mandu-test-"));
91
+ store = createDataStore({
92
+ filePath: path.join(tempDir, "store.json"),
93
+ autoSave: false,
94
+ });
95
+ });
96
+
97
+ afterEach(() => {
98
+ store.dispose();
99
+ fs.rmSync(tempDir, { recursive: true, force: true });
100
+ });
101
+
102
+ test("getStore returns a DataStore interface", () => {
103
+ const posts = store.getStore("posts");
104
+
105
+ expect(posts.get).toBeDefined();
106
+ expect(posts.set).toBeDefined();
107
+ expect(posts.delete).toBeDefined();
108
+ expect(posts.entries).toBeDefined();
109
+ });
110
+
111
+ test("set and get work correctly", () => {
112
+ const posts = store.getStore("posts");
113
+
114
+ posts.set({
115
+ id: "hello-world",
116
+ data: { title: "Hello World" },
117
+ digest: "abc123",
118
+ });
119
+
120
+ const entry = posts.get("hello-world");
121
+ expect(entry).toBeDefined();
122
+ expect(entry?.data.title).toBe("Hello World");
123
+ });
124
+
125
+ test("set returns true for new/changed entries", () => {
126
+ const posts = store.getStore("posts");
127
+
128
+ const result1 = posts.set({
129
+ id: "post1",
130
+ data: { title: "Post 1" },
131
+ digest: "digest1",
132
+ });
133
+ expect(result1).toBe(true);
134
+
135
+ // Same digest, no change
136
+ const result2 = posts.set({
137
+ id: "post1",
138
+ data: { title: "Post 1" },
139
+ digest: "digest1",
140
+ });
141
+ expect(result2).toBe(false);
142
+
143
+ // Different digest, changed
144
+ const result3 = posts.set({
145
+ id: "post1",
146
+ data: { title: "Post 1 Updated" },
147
+ digest: "digest2",
148
+ });
149
+ expect(result3).toBe(true);
150
+ });
151
+
152
+ test("delete removes entry", () => {
153
+ const posts = store.getStore("posts");
154
+
155
+ posts.set({ id: "to-delete", data: {}, digest: "x" });
156
+ expect(posts.has("to-delete")).toBe(true);
157
+
158
+ posts.delete("to-delete");
159
+ expect(posts.has("to-delete")).toBe(false);
160
+ });
161
+
162
+ test("entries returns all entries", () => {
163
+ const posts = store.getStore("posts");
164
+
165
+ posts.set({ id: "p1", data: { n: 1 }, digest: "a" });
166
+ posts.set({ id: "p2", data: { n: 2 }, digest: "b" });
167
+
168
+ const entries = posts.entries();
169
+ expect(entries.length).toBe(2);
170
+ expect(entries.map(([id]) => id).sort()).toEqual(["p1", "p2"]);
171
+ });
172
+
173
+ test("size returns entry count", () => {
174
+ const posts = store.getStore("posts");
175
+
176
+ expect(posts.size()).toBe(0);
177
+
178
+ posts.set({ id: "p1", data: {}, digest: "a" });
179
+ posts.set({ id: "p2", data: {}, digest: "b" });
180
+
181
+ expect(posts.size()).toBe(2);
182
+ });
183
+
184
+ test("clear removes all entries", () => {
185
+ const posts = store.getStore("posts");
186
+
187
+ posts.set({ id: "p1", data: {}, digest: "a" });
188
+ posts.set({ id: "p2", data: {}, digest: "b" });
189
+
190
+ posts.clear();
191
+ expect(posts.size()).toBe(0);
192
+ });
193
+
194
+ test("save and load persist data", async () => {
195
+ const posts = store.getStore("posts");
196
+
197
+ posts.set({
198
+ id: "persistent",
199
+ data: { title: "Persistent Post" },
200
+ digest: "persistent-digest",
201
+ });
202
+
203
+ await store.save();
204
+
205
+ // Create new store and load
206
+ const store2 = createDataStore({
207
+ filePath: path.join(tempDir, "store.json"),
208
+ autoSave: false,
209
+ });
210
+
211
+ await store2.load();
212
+
213
+ const posts2 = store2.getStore("posts");
214
+ const entry = posts2.get("persistent");
215
+
216
+ expect(entry).toBeDefined();
217
+ expect(entry?.data.title).toBe("Persistent Post");
218
+
219
+ store2.dispose();
220
+ });
221
+ });
222
+
223
+ // ============================================================================
224
+ // MetaStore Tests
225
+ // ============================================================================
226
+
227
+ describe("MetaStore", () => {
228
+ let metaStore: ContentMetaStore;
229
+ let tempDir: string;
230
+
231
+ beforeEach(() => {
232
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mandu-meta-test-"));
233
+ metaStore = createMetaStore({
234
+ filePath: path.join(tempDir, "meta.json"),
235
+ autoSave: false,
236
+ });
237
+ });
238
+
239
+ afterEach(() => {
240
+ fs.rmSync(tempDir, { recursive: true, force: true });
241
+ });
242
+
243
+ test("getStore returns MetaStore interface", () => {
244
+ const meta = metaStore.getStore("posts");
245
+
246
+ expect(meta.get).toBeDefined();
247
+ expect(meta.set).toBeDefined();
248
+ expect(meta.has).toBeDefined();
249
+ });
250
+
251
+ test("set and get work correctly", () => {
252
+ const meta = metaStore.getStore("api-data");
253
+
254
+ meta.set("lastSync", "2024-01-15T10:00:00Z");
255
+ meta.set("cursor", "abc123");
256
+
257
+ expect(meta.get("lastSync")).toBe("2024-01-15T10:00:00Z");
258
+ expect(meta.get("cursor")).toBe("abc123");
259
+ expect(meta.get("nonexistent")).toBeUndefined();
260
+ });
261
+
262
+ test("has checks existence", () => {
263
+ const meta = metaStore.getStore("collection");
264
+
265
+ expect(meta.has("key")).toBe(false);
266
+
267
+ meta.set("key", "value");
268
+ expect(meta.has("key")).toBe(true);
269
+ });
270
+
271
+ test("delete removes key", () => {
272
+ const meta = metaStore.getStore("collection");
273
+
274
+ meta.set("toDelete", "value");
275
+ expect(meta.has("toDelete")).toBe(true);
276
+
277
+ meta.delete("toDelete");
278
+ expect(meta.has("toDelete")).toBe(false);
279
+ });
280
+
281
+ test("entries returns all key-value pairs", () => {
282
+ const meta = metaStore.getStore("collection");
283
+
284
+ meta.set("k1", "v1");
285
+ meta.set("k2", "v2");
286
+
287
+ const entries = meta.entries();
288
+ expect(entries.length).toBe(2);
289
+ });
290
+ });
291
+
292
+ // ============================================================================
293
+ // LoaderContext Tests
294
+ // ============================================================================
295
+
296
+ describe("LoaderContext", () => {
297
+ let dataStore: ContentDataStore;
298
+ let metaStore: ContentMetaStore;
299
+ let tempDir: string;
300
+
301
+ beforeEach(() => {
302
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mandu-ctx-test-"));
303
+ dataStore = createDataStore({ autoSave: false });
304
+ metaStore = createMetaStore({ autoSave: false });
305
+ });
306
+
307
+ afterEach(() => {
308
+ dataStore.dispose();
309
+ fs.rmSync(tempDir, { recursive: true, force: true });
310
+ });
311
+
312
+ test("creates context with all properties", () => {
313
+ const context = createLoaderContext({
314
+ collection: "posts",
315
+ store: dataStore.getStore("posts"),
316
+ meta: metaStore.getStore("posts"),
317
+ config: { root: tempDir },
318
+ });
319
+
320
+ expect(context.collection).toBe("posts");
321
+ expect(context.store).toBeDefined();
322
+ expect(context.meta).toBeDefined();
323
+ expect(context.logger).toBeDefined();
324
+ expect(context.parseData).toBeDefined();
325
+ expect(context.generateDigest).toBeDefined();
326
+ });
327
+
328
+ test("parseData validates with schema", async () => {
329
+ const schema = z.object({
330
+ title: z.string(),
331
+ count: z.number(),
332
+ });
333
+
334
+ const context = createLoaderContext({
335
+ collection: "posts",
336
+ store: dataStore.getStore("posts"),
337
+ meta: metaStore.getStore("posts"),
338
+ config: { root: tempDir },
339
+ schema,
340
+ });
341
+
342
+ // Valid data
343
+ const valid = await context.parseData({
344
+ id: "test",
345
+ data: { title: "Hello", count: 5 },
346
+ });
347
+
348
+ expect((valid as Record<string, unknown>).title).toBe("Hello");
349
+ expect((valid as Record<string, unknown>).count).toBe(5);
350
+
351
+ // Invalid data should throw
352
+ await expect(
353
+ context.parseData({
354
+ id: "invalid",
355
+ data: { title: 123, count: "not a number" },
356
+ })
357
+ ).rejects.toThrow();
358
+ });
359
+
360
+ test("parseData passes through without schema", async () => {
361
+ const context = createLoaderContext({
362
+ collection: "posts",
363
+ store: dataStore.getStore("posts"),
364
+ meta: metaStore.getStore("posts"),
365
+ config: { root: tempDir },
366
+ });
367
+
368
+ const data = await context.parseData({
369
+ id: "test",
370
+ data: { anything: "goes" },
371
+ });
372
+
373
+ expect((data as Record<string, unknown>).anything).toBe("goes");
374
+ });
375
+
376
+ test("generateDigest creates hash", () => {
377
+ const context = createLoaderContext({
378
+ collection: "posts",
379
+ store: dataStore.getStore("posts"),
380
+ meta: metaStore.getStore("posts"),
381
+ config: { root: tempDir },
382
+ });
383
+
384
+ const hash = context.generateDigest({ test: "data" });
385
+ expect(hash.length).toBe(16);
386
+ });
387
+ });
388
+
389
+ // ============================================================================
390
+ // Markdown Renderer Tests
391
+ // ============================================================================
392
+
393
+ describe("SimpleMarkdownRenderer", () => {
394
+ const render = createSimpleMarkdownRenderer();
395
+
396
+ test("renders headings", async () => {
397
+ const result = await render("# Heading 1\n## Heading 2");
398
+
399
+ expect(result.html).toContain("<h1>Heading 1</h1>");
400
+ expect(result.html).toContain("<h2>Heading 2</h2>");
401
+ });
402
+
403
+ test("extracts headings", async () => {
404
+ const result = await render("# Title\n## Section 1\n### Subsection");
405
+
406
+ expect(result.headings).toBeDefined();
407
+ expect(result.headings?.length).toBe(3);
408
+ expect(result.headings?.[0]).toEqual({
409
+ depth: 1,
410
+ text: "Title",
411
+ slug: "title",
412
+ });
413
+ });
414
+
415
+ test("renders bold and italic", async () => {
416
+ const result = await render("**bold** and *italic*");
417
+
418
+ expect(result.html).toContain("<strong>bold</strong>");
419
+ expect(result.html).toContain("<em>italic</em>");
420
+ });
421
+
422
+ test("renders links", async () => {
423
+ const result = await render("[Link Text](https://example.com)");
424
+
425
+ expect(result.html).toContain('<a href="https://example.com">Link Text</a>');
426
+ });
427
+
428
+ test("renders code", async () => {
429
+ const result = await render("`inline code`");
430
+
431
+ expect(result.html).toContain("<code>inline code</code>");
432
+ });
433
+ });