@mandujs/core 0.21.0 → 0.22.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 (122) hide show
  1. package/package.json +94 -69
  2. package/src/auth/__tests__/login.test.ts +419 -0
  3. package/src/auth/__tests__/password.test.ts +122 -0
  4. package/src/auth/__tests__/reset.test.ts +296 -0
  5. package/src/auth/__tests__/tokens.test.ts +274 -0
  6. package/src/auth/__tests__/verification.test.ts +274 -0
  7. package/src/auth/index.ts +76 -0
  8. package/src/auth/login.ts +225 -0
  9. package/src/auth/password.ts +120 -0
  10. package/src/auth/reset.ts +243 -0
  11. package/src/auth/tokens.ts +612 -0
  12. package/src/auth/verification.ts +253 -0
  13. package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -0
  14. package/src/bundler/__tests__/cold-start.test.ts +504 -0
  15. package/src/bundler/__tests__/csp-nonce.test.ts +278 -0
  16. package/src/bundler/__tests__/dev-reliability.test.ts +619 -0
  17. package/src/bundler/__tests__/extended-watch.test.ts +710 -0
  18. package/src/bundler/__tests__/fast-refresh.test.ts +596 -0
  19. package/src/bundler/__tests__/hdr.test.ts +353 -0
  20. package/src/bundler/__tests__/hmr-client.test.ts +532 -0
  21. package/src/bundler/__tests__/manifest-schema.test.ts +266 -0
  22. package/src/bundler/__tests__/prod-smoke.test.ts +138 -0
  23. package/src/bundler/__tests__/slot-dispatch.test.ts +573 -0
  24. package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -0
  25. package/src/bundler/__tests__/vendor-cache.test.ts +455 -0
  26. package/src/bundler/build.test.ts +8 -1
  27. package/src/bundler/build.ts +310 -18
  28. package/src/bundler/css.ts +326 -323
  29. package/src/bundler/dev.ts +1611 -59
  30. package/src/bundler/fast-refresh-plugin.ts +307 -0
  31. package/src/bundler/hmr-types.ts +252 -0
  32. package/src/bundler/manifest-schema.ts +301 -0
  33. package/src/bundler/safe-build.test.ts +128 -0
  34. package/src/bundler/safe-build.ts +77 -0
  35. package/src/bundler/scenario-matrix.ts +229 -0
  36. package/src/bundler/types.ts +11 -0
  37. package/src/bundler/vendor-cache-types.ts +130 -0
  38. package/src/bundler/vendor-cache.ts +526 -0
  39. package/src/client/router.ts +214 -56
  40. package/src/db/__tests__/db.test.ts +485 -0
  41. package/src/db/index.ts +513 -0
  42. package/src/db/migrations/__tests__/runner.test.ts +661 -0
  43. package/src/db/migrations/history-table.ts +345 -0
  44. package/src/db/migrations/lock.ts +269 -0
  45. package/src/db/migrations/runner.ts +633 -0
  46. package/src/desktop/__tests__/smoke.test.ts +100 -0
  47. package/src/desktop/__tests__/window.test.ts +172 -0
  48. package/src/desktop/__tests__/worker.test.ts +266 -0
  49. package/src/desktop/index.ts +43 -0
  50. package/src/desktop/types.ts +158 -0
  51. package/src/desktop/window.ts +492 -0
  52. package/src/desktop/worker.ts +180 -0
  53. package/src/email/__tests__/email.test.ts +355 -0
  54. package/src/email/index.ts +282 -0
  55. package/src/email/resend.ts +163 -0
  56. package/src/email/smtp.ts +64 -0
  57. package/src/filling/__tests__/session-sqlite.test.ts +454 -0
  58. package/src/filling/context.ts +72 -78
  59. package/src/filling/cookie-codec.ts +299 -0
  60. package/src/filling/deps.ts +25 -1
  61. package/src/filling/filling.ts +28 -3
  62. package/src/filling/session-sqlite.ts +617 -0
  63. package/src/filling/session.ts +265 -216
  64. package/src/guard/decision-memory.test.ts +52 -22
  65. package/src/id/__tests__/id.test.ts +120 -0
  66. package/src/id/index.ts +105 -0
  67. package/src/kitchen/index.ts +2 -2
  68. package/src/kitchen/kitchen-handler.ts +86 -0
  69. package/src/kitchen/stream/activity-sse.ts +2 -1
  70. package/src/middleware/csrf.ts +328 -0
  71. package/src/middleware/index.ts +40 -0
  72. package/src/middleware/oauth/__tests__/oauth.test.ts +574 -0
  73. package/src/middleware/oauth/index.ts +505 -0
  74. package/src/middleware/oauth/providers.ts +115 -0
  75. package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -0
  76. package/src/middleware/rate-limit/index.ts +522 -0
  77. package/src/middleware/rate-limit/sqlite-store.ts +382 -0
  78. package/src/middleware/secure/__tests__/secure.test.ts +360 -0
  79. package/src/middleware/secure/csp.ts +193 -0
  80. package/src/middleware/secure/index.ts +417 -0
  81. package/src/middleware/session.ts +174 -0
  82. package/src/observability/event-bus.ts +81 -79
  83. package/src/paths.ts +37 -0
  84. package/src/perf/hmr-markers.ts +215 -0
  85. package/src/perf/index.ts +104 -0
  86. package/src/resource/__tests__/generator.test.ts +603 -2
  87. package/src/resource/ddl/__tests__/diff.test.ts +639 -0
  88. package/src/resource/ddl/__tests__/emit.test.ts +799 -0
  89. package/src/resource/ddl/__tests__/snapshot.test.ts +499 -0
  90. package/src/resource/ddl/diff.ts +392 -0
  91. package/src/resource/ddl/emit.ts +548 -0
  92. package/src/resource/ddl/persistence-types.ts +218 -0
  93. package/src/resource/ddl/snapshot.ts +447 -0
  94. package/src/resource/ddl/type-map.ts +223 -0
  95. package/src/resource/ddl/types.ts +232 -0
  96. package/src/resource/generator-repo.ts +610 -0
  97. package/src/resource/generator-schema.ts +476 -0
  98. package/src/resource/generator.ts +117 -1
  99. package/src/resource/index.ts +17 -1
  100. package/src/resource/schema.ts +30 -0
  101. package/src/router/fs-scanner.ts +3 -0
  102. package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -0
  103. package/src/runtime/__tests__/hdr-client.test.ts +223 -0
  104. package/src/runtime/__tests__/http-errors.test.ts +117 -0
  105. package/src/runtime/__tests__/not-found.test.ts +152 -0
  106. package/src/runtime/boundary.tsx +21 -1
  107. package/src/runtime/fast-refresh-runtime.ts +322 -0
  108. package/src/runtime/fast-refresh-types.ts +128 -0
  109. package/src/runtime/hmr-client.ts +409 -0
  110. package/src/runtime/http-errors.ts +113 -0
  111. package/src/runtime/index.ts +6 -0
  112. package/src/runtime/logger.ts +678 -677
  113. package/src/runtime/not-found.ts +93 -0
  114. package/src/runtime/redirect.ts +133 -0
  115. package/src/runtime/server.ts +518 -20
  116. package/src/runtime/ssr.ts +340 -10
  117. package/src/runtime/streaming-ssr.ts +222 -19
  118. package/src/scheduler/__tests__/scheduler.test.ts +514 -0
  119. package/src/scheduler/index.ts +343 -0
  120. package/src/storage/s3/__tests__/s3.test.ts +479 -0
  121. package/src/storage/s3/index.ts +412 -0
  122. package/src/testing/index.ts +58 -0
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Phase 7.2.S2 — Tier 2 vendor shim disk cache tests (Agent A).
3
+ *
4
+ * Covers `readVendorCache` / `writeVendorCache` / `restoreVendorCache` —
5
+ * the ~25 KB cache layer that closes ~80-120 ms of cold start on warm
6
+ * restarts by reusing `_vendor-react.js` / `_vendor-react-dom.js` /
7
+ * `_vendor-react-refresh.js` / `_fast-refresh-runtime.js` instead of
8
+ * re-running Bun.build.
9
+ *
10
+ * Test strategy:
11
+ * - Each case operates on its own tmpdir so cache state is hermetic.
12
+ * - We write synthetic shim files (tiny JS strings) and an explicit
13
+ * manifest — we do NOT invoke the real `buildVendorShims` because
14
+ * that would lock tests to a Bun.build round-trip per case and bloat
15
+ * runtime to ~15-20 s. Integration with the real shim build is
16
+ * covered by `buildVendorShims emits fast-refresh shims in dev mode`
17
+ * in `fast-refresh.test.ts`.
18
+ * - Hash / size / version checks live in the read layer — we drive
19
+ * each branch by mutating the manifest on disk between calls.
20
+ *
21
+ * References:
22
+ * docs/bun/phase-7-2-team-plan.md §2.2, §3 Agent A (S2)
23
+ * packages/core/src/bundler/vendor-cache.ts
24
+ */
25
+
26
+ import { describe, it, expect, beforeEach, afterEach } from "bun:test";
27
+ import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "fs";
28
+ import { tmpdir } from "os";
29
+ import path from "path";
30
+ import { createHash } from "node:crypto";
31
+ import {
32
+ readVendorCache,
33
+ writeVendorCache,
34
+ restoreVendorCache,
35
+ type VendorCacheKeyInput,
36
+ type VendorCacheWriteEntry,
37
+ } from "../vendor-cache";
38
+ import {
39
+ VENDOR_CACHE_DIR,
40
+ VENDOR_CACHE_FILENAME,
41
+ type VendorCacheManifest,
42
+ } from "../vendor-cache-types";
43
+
44
+ // ============================================
45
+ // Helpers
46
+ // ============================================
47
+
48
+ const BASELINE_KEYS: VendorCacheKeyInput = {
49
+ bunVersion: "1.3.12",
50
+ reactVersion: "19.2.0",
51
+ reactDomVersion: "19.2.0",
52
+ reactRefreshVersion: "0.18.0",
53
+ manduCoreVersion: "0.22.0",
54
+ };
55
+
56
+ function makeShim(label: string): string {
57
+ return `/* shim: ${label} */ export default { label: "${label}" };\n`;
58
+ }
59
+
60
+ function sha256(s: string): string {
61
+ return createHash("sha256").update(s).digest("hex");
62
+ }
63
+
64
+ function writeShimFile(root: string, relPath: string, contents: string): string {
65
+ const abs = path.join(root, relPath);
66
+ mkdirSync(path.dirname(abs), { recursive: true });
67
+ writeFileSync(abs, contents);
68
+ return abs;
69
+ }
70
+
71
+ function writeManifest(rootDir: string, manifest: VendorCacheManifest): string {
72
+ const cacheDir = path.join(rootDir, VENDOR_CACHE_DIR);
73
+ mkdirSync(cacheDir, { recursive: true });
74
+ const p = path.join(cacheDir, VENDOR_CACHE_FILENAME);
75
+ writeFileSync(p, JSON.stringify(manifest, null, 2));
76
+ return p;
77
+ }
78
+
79
+ /**
80
+ * Produce a complete manifest + write every referenced shim file. Returns
81
+ * the manifest (useful for mutating one field between calls).
82
+ */
83
+ function seedCache(rootDir: string): VendorCacheManifest {
84
+ const cacheDir = path.join(rootDir, VENDOR_CACHE_DIR);
85
+ mkdirSync(cacheDir, { recursive: true });
86
+
87
+ const entries: VendorCacheManifest["entries"] = {};
88
+ const names: Array<[string, string]> = [
89
+ ["react", "_react.js"],
90
+ ["react-dom", "_react-dom.js"],
91
+ ["react-dom-client", "_react-dom-client.js"],
92
+ ["jsx-runtime", "_jsx-runtime.js"],
93
+ ["jsx-dev-runtime", "_jsx-dev-runtime.js"],
94
+ ["react-refresh-runtime", "_vendor-react-refresh.js"],
95
+ ["fast-refresh-glue", "_fast-refresh-runtime.js"],
96
+ ];
97
+
98
+ for (const [logicalId, fileName] of names) {
99
+ const contents = makeShim(logicalId);
100
+ writeFileSync(path.join(cacheDir, fileName), contents);
101
+ entries[logicalId] = {
102
+ path: fileName,
103
+ size: Buffer.byteLength(contents, "utf-8"),
104
+ hash: sha256(contents),
105
+ };
106
+ }
107
+
108
+ const manifest: VendorCacheManifest = {
109
+ version: 1,
110
+ bunVersion: BASELINE_KEYS.bunVersion,
111
+ reactVersion: BASELINE_KEYS.reactVersion,
112
+ reactDomVersion: BASELINE_KEYS.reactDomVersion,
113
+ reactRefreshVersion: BASELINE_KEYS.reactRefreshVersion,
114
+ manduCoreVersion: BASELINE_KEYS.manduCoreVersion,
115
+ entries,
116
+ generatedAt: new Date().toISOString(),
117
+ };
118
+ writeManifest(rootDir, manifest);
119
+ return manifest;
120
+ }
121
+
122
+ // ============================================
123
+ // Tests
124
+ // ============================================
125
+
126
+ describe("vendor-cache — read path", () => {
127
+ let rootDir: string;
128
+
129
+ beforeEach(() => {
130
+ rootDir = mkdtempSync(path.join(tmpdir(), "mandu-vendor-cache-"));
131
+ });
132
+
133
+ afterEach(() => {
134
+ try {
135
+ rmSync(rootDir, { recursive: true, force: true });
136
+ } catch {
137
+ // Windows lock tolerance
138
+ }
139
+ });
140
+
141
+ it("no manifest → miss with reason 'no-manifest'", async () => {
142
+ const result = await readVendorCache(rootDir, BASELINE_KEYS);
143
+ expect(result.kind).toBe("miss");
144
+ if (result.kind === "miss") {
145
+ expect(result.reason).toBe("no-manifest");
146
+ }
147
+ });
148
+
149
+ it("corrupt manifest JSON → miss with reason 'no-manifest'", async () => {
150
+ const cacheDir = path.join(rootDir, VENDOR_CACHE_DIR);
151
+ mkdirSync(cacheDir, { recursive: true });
152
+ writeFileSync(path.join(cacheDir, VENDOR_CACHE_FILENAME), "{not json");
153
+
154
+ const result = await readVendorCache(rootDir, BASELINE_KEYS);
155
+ expect(result.kind).toBe("miss");
156
+ if (result.kind === "miss") {
157
+ expect(result.reason).toBe("no-manifest");
158
+ }
159
+ });
160
+
161
+ it("happy path: full seeded cache → hit", async () => {
162
+ seedCache(rootDir);
163
+ const result = await readVendorCache(rootDir, BASELINE_KEYS);
164
+ expect(result.kind).toBe("hit");
165
+ if (result.kind === "hit") {
166
+ expect(result.manifest.bunVersion).toBe(BASELINE_KEYS.bunVersion);
167
+ expect(Object.keys(result.manifest.entries).length).toBe(7);
168
+ }
169
+ });
170
+
171
+ it("Bun version changed → miss 'version-mismatch' + mismatchedField=bunVersion", async () => {
172
+ seedCache(rootDir);
173
+ const result = await readVendorCache(rootDir, {
174
+ ...BASELINE_KEYS,
175
+ bunVersion: "1.3.9",
176
+ });
177
+ expect(result.kind).toBe("miss");
178
+ if (result.kind === "miss") {
179
+ expect(result.reason).toBe("version-mismatch");
180
+ expect(result.mismatchedField).toBe("bunVersion");
181
+ }
182
+ });
183
+
184
+ it("React version changed → miss 'version-mismatch'", async () => {
185
+ seedCache(rootDir);
186
+ const result = await readVendorCache(rootDir, {
187
+ ...BASELINE_KEYS,
188
+ reactVersion: "18.3.0",
189
+ });
190
+ expect(result.kind).toBe("miss");
191
+ if (result.kind === "miss") {
192
+ expect(result.reason).toBe("version-mismatch");
193
+ expect(result.mismatchedField).toBe("reactVersion");
194
+ }
195
+ });
196
+
197
+ it("@mandujs/core version changed → miss 'version-mismatch'", async () => {
198
+ seedCache(rootDir);
199
+ const result = await readVendorCache(rootDir, {
200
+ ...BASELINE_KEYS,
201
+ manduCoreVersion: "0.99.0",
202
+ });
203
+ expect(result.kind).toBe("miss");
204
+ if (result.kind === "miss") {
205
+ expect(result.reason).toBe("version-mismatch");
206
+ expect(result.mismatchedField).toBe("manduCoreVersion");
207
+ }
208
+ });
209
+
210
+ it("hash mismatch on a cache file → miss 'hash-mismatch'", async () => {
211
+ const manifest = seedCache(rootDir);
212
+ // Overwrite `_react.js` with content that has the SAME size but
213
+ // different bytes — this forces the hash branch (not the size
214
+ // branch).
215
+ const original = makeShim("react");
216
+ const tampered = "/* shim: xxxxx */ export default { label: \"xxxxx\" };\n";
217
+ expect(Buffer.byteLength(tampered, "utf-8")).toBe(
218
+ Buffer.byteLength(original, "utf-8"),
219
+ );
220
+
221
+ writeFileSync(
222
+ path.join(rootDir, VENDOR_CACHE_DIR, "_react.js"),
223
+ tampered,
224
+ );
225
+
226
+ const result = await readVendorCache(rootDir, BASELINE_KEYS);
227
+ expect(result.kind).toBe("miss");
228
+ if (result.kind === "miss") {
229
+ expect(result.reason).toBe("hash-mismatch");
230
+ }
231
+
232
+ // Sanity — manifest entry still points at the intended file name.
233
+ expect(manifest.entries["react"]!.path).toBe("_react.js");
234
+ });
235
+
236
+ it("size mismatch on a cache file → miss 'size-mismatch' (cheap check)", async () => {
237
+ seedCache(rootDir);
238
+ writeFileSync(
239
+ path.join(rootDir, VENDOR_CACHE_DIR, "_react.js"),
240
+ makeShim("react") + "// padding\n",
241
+ );
242
+
243
+ const result = await readVendorCache(rootDir, BASELINE_KEYS);
244
+ expect(result.kind).toBe("miss");
245
+ if (result.kind === "miss") {
246
+ expect(result.reason).toBe("size-mismatch");
247
+ }
248
+ });
249
+
250
+ it("manifest references missing file → miss 'missing-entry'", async () => {
251
+ seedCache(rootDir);
252
+ rmSync(path.join(rootDir, VENDOR_CACHE_DIR, "_vendor-react-refresh.js"));
253
+
254
+ const result = await readVendorCache(rootDir, BASELINE_KEYS);
255
+ expect(result.kind).toBe("miss");
256
+ if (result.kind === "miss") {
257
+ expect(result.reason).toBe("missing-entry");
258
+ }
259
+ });
260
+
261
+ it("format version != 1 → miss 'format-version'", async () => {
262
+ const manifest = seedCache(rootDir);
263
+ // Write a manifest whose `version` field is unknown (forward-compat hedge).
264
+ const badManifest = { ...manifest, version: 99 } as unknown as VendorCacheManifest;
265
+ writeManifest(rootDir, badManifest);
266
+
267
+ const result = await readVendorCache(rootDir, BASELINE_KEYS);
268
+ expect(result.kind).toBe("miss");
269
+ if (result.kind === "miss") {
270
+ expect(result.reason).toBe("format-version");
271
+ }
272
+ });
273
+ });
274
+
275
+ describe("vendor-cache — write path", () => {
276
+ let rootDir: string;
277
+
278
+ beforeEach(() => {
279
+ rootDir = mkdtempSync(path.join(tmpdir(), "mandu-vendor-cache-w-"));
280
+ });
281
+
282
+ afterEach(() => {
283
+ try {
284
+ rmSync(rootDir, { recursive: true, force: true });
285
+ } catch {
286
+ // Windows lock tolerance
287
+ }
288
+ });
289
+
290
+ it("writes manifest + copies every shim file", async () => {
291
+ // Seed .mandu/client/ with fresh built shim files (simulate a
292
+ // successful `buildVendorShims` run).
293
+ const outDir = path.join(rootDir, ".mandu", "client");
294
+ mkdirSync(outDir, { recursive: true });
295
+ const reactAbs = writeShimFile(rootDir, ".mandu/client/_react.js", makeShim("react"));
296
+ const refreshAbs = writeShimFile(
297
+ rootDir,
298
+ ".mandu/client/_vendor-react-refresh.js",
299
+ makeShim("refresh"),
300
+ );
301
+
302
+ const entries: VendorCacheWriteEntry[] = [
303
+ { logicalId: "react", absPath: reactAbs },
304
+ { logicalId: "react-refresh-runtime", absPath: refreshAbs },
305
+ ];
306
+
307
+ const ok = await writeVendorCache(rootDir, BASELINE_KEYS, entries);
308
+ expect(ok).toBe(true);
309
+
310
+ // Manifest must exist at the right location.
311
+ const manifestPath = path.join(
312
+ rootDir,
313
+ VENDOR_CACHE_DIR,
314
+ VENDOR_CACHE_FILENAME,
315
+ );
316
+ const { readFileSync, existsSync } = await import("node:fs");
317
+ expect(existsSync(manifestPath)).toBe(true);
318
+
319
+ const parsed = JSON.parse(readFileSync(manifestPath, "utf-8")) as VendorCacheManifest;
320
+ expect(parsed.version).toBe(1);
321
+ expect(parsed.bunVersion).toBe(BASELINE_KEYS.bunVersion);
322
+ expect(parsed.entries.react).toBeDefined();
323
+ expect(parsed.entries["react-refresh-runtime"]).toBeDefined();
324
+
325
+ // Round-trip: re-read should hit.
326
+ const result = await readVendorCache(rootDir, BASELINE_KEYS);
327
+ expect(result.kind).toBe("hit");
328
+ });
329
+
330
+ it("skips entries whose source file is missing; still writes if at least one survived", async () => {
331
+ const outDir = path.join(rootDir, ".mandu", "client");
332
+ mkdirSync(outDir, { recursive: true });
333
+ const reactAbs = writeShimFile(rootDir, ".mandu/client/_react.js", makeShim("react"));
334
+
335
+ const entries: VendorCacheWriteEntry[] = [
336
+ { logicalId: "react", absPath: reactAbs },
337
+ // The second entry points at a non-existent file — writeVendorCache
338
+ // should skip it and still write the manifest for `react`.
339
+ {
340
+ logicalId: "react-refresh-runtime",
341
+ absPath: path.join(rootDir, ".mandu/client/MISSING.js"),
342
+ },
343
+ ];
344
+
345
+ const ok = await writeVendorCache(rootDir, BASELINE_KEYS, entries);
346
+ expect(ok).toBe(true);
347
+
348
+ const result = await readVendorCache(rootDir, BASELINE_KEYS);
349
+ expect(result.kind).toBe("hit");
350
+ if (result.kind === "hit") {
351
+ expect(Object.keys(result.manifest.entries)).toContain("react");
352
+ expect(Object.keys(result.manifest.entries)).not.toContain(
353
+ "react-refresh-runtime",
354
+ );
355
+ }
356
+ });
357
+
358
+ it("empty entry list → returns false (does not write a stub manifest)", async () => {
359
+ const ok = await writeVendorCache(rootDir, BASELINE_KEYS, []);
360
+ expect(ok).toBe(false);
361
+
362
+ // No manifest should exist.
363
+ const { existsSync } = await import("node:fs");
364
+ const manifestPath = path.join(
365
+ rootDir,
366
+ VENDOR_CACHE_DIR,
367
+ VENDOR_CACHE_FILENAME,
368
+ );
369
+ expect(existsSync(manifestPath)).toBe(false);
370
+ });
371
+ });
372
+
373
+ describe("vendor-cache — restore path", () => {
374
+ let rootDir: string;
375
+
376
+ beforeEach(() => {
377
+ rootDir = mkdtempSync(path.join(tmpdir(), "mandu-vendor-cache-r-"));
378
+ });
379
+
380
+ afterEach(() => {
381
+ try {
382
+ rmSync(rootDir, { recursive: true, force: true });
383
+ } catch {
384
+ // Windows lock tolerance
385
+ }
386
+ });
387
+
388
+ it("restores every cached shim into outDir and returns the path map", async () => {
389
+ const manifest = seedCache(rootDir);
390
+ const outDir = path.join(rootDir, ".mandu", "client");
391
+
392
+ const map = await restoreVendorCache(rootDir, manifest, outDir);
393
+ expect(map).not.toBeNull();
394
+ expect(map!.size).toBe(7);
395
+
396
+ for (const logicalId of Object.keys(manifest.entries)) {
397
+ const dst = map!.get(logicalId);
398
+ expect(dst).toBeDefined();
399
+ const { existsSync } = await import("node:fs");
400
+ expect(existsSync(dst!)).toBe(true);
401
+ }
402
+ });
403
+
404
+ it("returns null when a cached file is missing mid-restore", async () => {
405
+ const manifest = seedCache(rootDir);
406
+ // Remove one of the source files the manifest still references.
407
+ rmSync(path.join(rootDir, VENDOR_CACHE_DIR, "_jsx-runtime.js"));
408
+
409
+ const outDir = path.join(rootDir, ".mandu", "client");
410
+ const map = await restoreVendorCache(rootDir, manifest, outDir);
411
+ expect(map).toBeNull();
412
+ });
413
+ });
414
+
415
+ describe("vendor-cache — key resolution regression", () => {
416
+ let rootDir: string;
417
+
418
+ beforeEach(() => {
419
+ rootDir = mkdtempSync(path.join(tmpdir(), "mandu-vendor-cache-k-"));
420
+ });
421
+
422
+ afterEach(() => {
423
+ try {
424
+ rmSync(rootDir, { recursive: true, force: true });
425
+ } catch {
426
+ // Windows lock tolerance
427
+ }
428
+ });
429
+
430
+ it("resolveVendorCacheKeys: returns well-formed keys without throwing", async () => {
431
+ // The resolver walks up from `rootDir` looking for
432
+ // `node_modules/<pkg>/package.json`. Depending on where the
433
+ // tmpdir lives (same drive / filesystem as the monorepo or not)
434
+ // it may either (a) fall back to "unknown" (no node_modules on
435
+ // the way up) or (b) find a hoisted package in a parent. Either
436
+ // outcome is correct — the contract is "no throw, string
437
+ // result". We assert exactly that and nothing more brittle.
438
+ const { resolveVendorCacheKeys } = await import("../vendor-cache");
439
+ const keys = await resolveVendorCacheKeys(rootDir);
440
+
441
+ expect(typeof keys.bunVersion).toBe("string");
442
+ expect(keys.bunVersion.length).toBeGreaterThan(0);
443
+
444
+ expect(typeof keys.reactVersion).toBe("string");
445
+ expect(typeof keys.reactDomVersion).toBe("string");
446
+ expect(typeof keys.reactRefreshVersion).toBe("string");
447
+
448
+ // @mandujs/core: workspace dev resolves via monorepo walk, published
449
+ // install resolves via node_modules. Either way it should be
450
+ // non-empty — this project lives in the monorepo so we expect a
451
+ // semver-ish version from packages/core/package.json.
452
+ expect(keys.manduCoreVersion.length).toBeGreaterThan(0);
453
+ expect(keys.manduCoreVersion).not.toBe("unknown");
454
+ });
455
+ });
@@ -62,7 +62,14 @@ afterAll(async () => {
62
62
  }
63
63
  });
64
64
 
65
- describe("buildClientBundles vendor shims", () => {
65
+ // Bun.build has observed cross-worker races under bun:test's parallel file
66
+ // execution — when tests shuffle under --randomize, this file sometimes
67
+ // collides with another bundler test in a sibling worker and fails with
68
+ // "AggregateError: Bundle failed" + missing shim outputs. Gate on an env
69
+ // var so CI can run randomize-mode across everything else, then run this
70
+ // suite in a dedicated serial pass. Local `bun run test:core` (no env set)
71
+ // still runs these tests fully. Tracked as Phase 0.6.
72
+ describe.skipIf(process.env.MANDU_SKIP_BUNDLER_TESTS === "1")("buildClientBundles vendor shims", () => {
66
73
  test("build succeeds", () => {
67
74
  if (!result.success) {
68
75
  console.error("[build.test] errors:", result.errors);