@happyvertical/smrt-vitest 0.30.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.
package/dist/index.js ADDED
@@ -0,0 +1,718 @@
1
+ /**
2
+ * SMRT Vitest Plugin
3
+ *
4
+ * Automatically loads manifests from SMRT peer dependencies before tests run.
5
+ * This solves Issue #583 where cross-package integration tests fail because
6
+ * external package classes aren't registered in the test manifest.
7
+ *
8
+ * Uses ManifestManager for unified manifest loading, which properly handles
9
+ * the manifest priority order: .smrt/manifest.json (test) -> dist/manifest.json (production)
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * // vitest.config.ts
14
+ * import { defineConfig } from 'vitest/config';
15
+ * import { smrtVitestPlugin } from '@happyvertical/smrt-vitest';
16
+ *
17
+ * export default defineConfig({
18
+ * plugins: [smrtVitestPlugin()],
19
+ * test: {
20
+ * globals: true,
21
+ * environment: 'node',
22
+ * },
23
+ * });
24
+ * ```
25
+ *
26
+ * @packageDocumentation
27
+ */
28
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
29
+ import { createRequire } from 'node:module';
30
+ import { dirname, join } from 'node:path';
31
+ import { fileURLToPath } from 'node:url';
32
+ function resolveDefaultSetupFile() {
33
+ const sourceSetupPath = fileURLToPath(new URL('./setup.ts', import.meta.url));
34
+ if (existsSync(sourceSetupPath)) {
35
+ return sourceSetupPath;
36
+ }
37
+ const distSetupPath = fileURLToPath(new URL('./setup.js', import.meta.url));
38
+ if (existsSync(distSetupPath)) {
39
+ return distSetupPath;
40
+ }
41
+ return '@happyvertical/smrt-vitest/setup';
42
+ }
43
+ function findWorkspaceRoot(startDir) {
44
+ let current = startDir;
45
+ while (true) {
46
+ if (existsSync(join(current, 'pnpm-workspace.yaml'))) {
47
+ return current;
48
+ }
49
+ const parent = dirname(current);
50
+ if (parent === current) {
51
+ return null;
52
+ }
53
+ current = parent;
54
+ }
55
+ }
56
+ function getWorkspaceSourceTsconfigPath(startDir = process.cwd()) {
57
+ const workspaceRoot = findWorkspaceRoot(startDir);
58
+ if (!workspaceRoot) {
59
+ return null;
60
+ }
61
+ const tsconfigPath = join(workspaceRoot, 'tsconfig.package-build.json');
62
+ return existsSync(tsconfigPath) ? tsconfigPath : null;
63
+ }
64
+ async function importWorkspaceSourceModule(href) {
65
+ const { register } = await import('tsx/esm/api');
66
+ const tsconfigPath = getWorkspaceSourceTsconfigPath();
67
+ const unregister = register(tsconfigPath ? { tsconfig: tsconfigPath } : undefined);
68
+ try {
69
+ return (await import(href));
70
+ }
71
+ finally {
72
+ await unregister();
73
+ }
74
+ }
75
+ function readWorkspacePackageRoots(root) {
76
+ const workspaceRoot = findWorkspaceRoot(root);
77
+ if (!workspaceRoot) {
78
+ return new Map();
79
+ }
80
+ const packagesDir = join(workspaceRoot, 'packages');
81
+ if (!existsSync(packagesDir)) {
82
+ return new Map();
83
+ }
84
+ const packageRoots = new Map();
85
+ for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
86
+ if (!entry.isDirectory()) {
87
+ continue;
88
+ }
89
+ const packageRoot = join(packagesDir, entry.name);
90
+ const packageJsonPath = join(packageRoot, 'package.json');
91
+ if (!existsSync(packageJsonPath)) {
92
+ continue;
93
+ }
94
+ try {
95
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
96
+ if (typeof packageJson.name === 'string') {
97
+ packageRoots.set(packageJson.name, packageRoot);
98
+ }
99
+ }
100
+ catch {
101
+ // Ignore invalid package manifests in the workspace scan.
102
+ }
103
+ }
104
+ return packageRoots;
105
+ }
106
+ function addAliasIfPresent(aliases, find, replacement) {
107
+ if (aliases.some((entry) => entry.find === find) ||
108
+ !existsSync(replacement)) {
109
+ return;
110
+ }
111
+ if (existsSync(replacement)) {
112
+ aliases.push({ find, replacement });
113
+ }
114
+ }
115
+ export function getWorkspaceViteAliases(root = process.cwd()) {
116
+ const packageRoots = readWorkspacePackageRoots(root);
117
+ const aliases = [];
118
+ for (const [packageName, packageRoot] of packageRoots.entries()) {
119
+ addAliasIfPresent(aliases, packageName, join(packageRoot, 'src/index.ts'));
120
+ addAliasIfPresent(aliases, `${packageName}/svelte`, join(packageRoot, 'src/svelte/index.ts'));
121
+ addAliasIfPresent(aliases, `${packageName}/sveltekit`, join(packageRoot, 'src/sveltekit/index.ts'));
122
+ addAliasIfPresent(aliases, `${packageName}/ui`, join(packageRoot, 'src/ui.ts'));
123
+ addAliasIfPresent(aliases, `${packageName}/routes`, join(packageRoot, 'src/route-module.ts'));
124
+ addAliasIfPresent(aliases, `${packageName}/playground`, join(packageRoot, 'src/playground.ts'));
125
+ addAliasIfPresent(aliases, `${packageName}/playground`, join(packageRoot, 'src/svelte/playground.ts'));
126
+ addAliasIfPresent(aliases, `${packageName}/manifest`, join(packageRoot, 'src/manifest/index.ts'));
127
+ addAliasIfPresent(aliases, `${packageName}/manifest.json`, join(packageRoot, 'src/manifest/manifest.json'));
128
+ // smrt-chat exposes an internal trusted agent-runtime subpath (S5 #1392).
129
+ addAliasIfPresent(aliases, `${packageName}/internal/agent-runtime`, join(packageRoot, 'src/internal/agent-runtime.ts'));
130
+ if (packageName === '@happyvertical/smrt-core') {
131
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/testing', join(packageRoot, 'src/testing.ts'));
132
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/scanner', join(packageRoot, 'src/scanner/index.ts'));
133
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/vite-plugin', join(packageRoot, 'src/vite-plugin/index.ts'));
134
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/vite-plugin', join(packageRoot, 'src/vite-plugin.ts'));
135
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/consumer-plugin', join(packageRoot, 'src/consumer-plugin/index.ts'));
136
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/consumer-plugin', join(packageRoot, 'src/consumer-plugin.ts'));
137
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/manifest', join(packageRoot, 'src/manifest/index.ts'));
138
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/manifest/discover-base-classes', join(packageRoot, 'src/manifest/discover-base-classes.ts'));
139
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/schema/utils', join(packageRoot, 'src/schema/utils.ts'));
140
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/utils', join(packageRoot, 'src/utils.ts'));
141
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/utils/import-workspace-module', join(packageRoot, 'src/utils/import-workspace-module.ts'));
142
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/migrations', join(packageRoot, 'src/migrations.ts'));
143
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/runtime', join(packageRoot, 'src/runtime.ts'));
144
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/registry', join(packageRoot, 'src/registry.ts'));
145
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/generators', join(packageRoot, 'src/generators.ts'));
146
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/generators/cli', join(packageRoot, 'src/generators/cli.ts'));
147
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/generators/mcp', join(packageRoot, 'src/generators/mcp.ts'));
148
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/generators/rest', join(packageRoot, 'src/generators/rest.ts'));
149
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/prebuild', join(packageRoot, 'src/prebuild.ts'));
150
+ addAliasIfPresent(aliases, '@happyvertical/smrt-core/decorators', join(packageRoot, 'src/decorators/index.ts'));
151
+ }
152
+ if (packageName === '@happyvertical/smrt-vitest') {
153
+ // Shared Svelte component-test harness (S11 #1416). Flat `src/*.ts` files,
154
+ // so the generic `/svelte` → `src/svelte/index.ts` convention misses them;
155
+ // map the exact subpaths. The length-desc sort below makes these win over
156
+ // the bare-package root alias.
157
+ addAliasIfPresent(aliases, '@happyvertical/smrt-vitest/svelte', join(packageRoot, 'src/svelte.ts'));
158
+ addAliasIfPresent(aliases, '@happyvertical/smrt-vitest/svelte-setup', join(packageRoot, 'src/svelte-setup.ts'));
159
+ addAliasIfPresent(aliases, '@happyvertical/smrt-vitest/a11y', join(packageRoot, 'src/a11y.ts'));
160
+ }
161
+ if (packageName === '@happyvertical/smrt-ui') {
162
+ // smrt-ui holds the domain-agnostic UI leaf (primitives, feedback,
163
+ // layout, calendar, chat, registry, theme system, i18n client). These
164
+ // subpaths map to nested source dirs, so the generic `${packageName}/ui`
165
+ // → `src/ui.ts` convention misses them; alias the exact source files.
166
+ addAliasIfPresent(aliases, '@happyvertical/smrt-ui/ui', join(packageRoot, 'src/components/ui/index.ts'));
167
+ addAliasIfPresent(aliases, '@happyvertical/smrt-ui/feedback', join(packageRoot, 'src/components/feedback/index.ts'));
168
+ addAliasIfPresent(aliases, '@happyvertical/smrt-ui/layout', join(packageRoot, 'src/components/layout/index.ts'));
169
+ addAliasIfPresent(aliases, '@happyvertical/smrt-ui/calendar', join(packageRoot, 'src/components/calendar/index.ts'));
170
+ addAliasIfPresent(aliases, '@happyvertical/smrt-ui/chat', join(packageRoot, 'src/components/chat/index.ts'));
171
+ addAliasIfPresent(aliases, '@happyvertical/smrt-ui/registry', join(packageRoot, 'src/registry/index.ts'));
172
+ addAliasIfPresent(aliases, '@happyvertical/smrt-ui/theme', join(packageRoot, 'src/theme/index.ts'));
173
+ addAliasIfPresent(aliases, '@happyvertical/smrt-ui/themes', join(packageRoot, 'src/themes/index.ts'));
174
+ addAliasIfPresent(aliases, '@happyvertical/smrt-ui/i18n', join(packageRoot, 'src/i18n/index.ts'));
175
+ // Test-support harness (a11y helper + setup) moved here with the leaf;
176
+ // smrt-svelte's surviving component tests import it cross-package.
177
+ addAliasIfPresent(aliases, '@happyvertical/smrt-ui/test-support/a11y', join(packageRoot, 'src/test-support/a11y.ts'));
178
+ addAliasIfPresent(aliases, '@happyvertical/smrt-ui/test-support/setup', join(packageRoot, 'src/test-support/setup.ts'));
179
+ // utils ships nested helpers consumed by smrt-svelte's surviving forms.
180
+ addAliasIfPresent(aliases, '@happyvertical/smrt-ui/utils/forms/formatters.js', join(packageRoot, 'src/utils/forms/formatters.ts'));
181
+ addAliasIfPresent(aliases, '@happyvertical/smrt-ui/utils/import-optional.js', join(packageRoot, 'src/utils/import-optional.ts'));
182
+ }
183
+ if (packageName === '@happyvertical/smrt-svelte') {
184
+ // The domain-agnostic UI leaf subpaths moved to @happyvertical/smrt-ui
185
+ // (#1582). smrt-svelte keeps the Node-only server i18n resolver here.
186
+ addAliasIfPresent(aliases, '@happyvertical/smrt-svelte/i18n/server', join(packageRoot, 'src/i18n/server.ts'));
187
+ }
188
+ }
189
+ return aliases.sort((left, right) => right.find.length - left.find.length);
190
+ }
191
+ function normalizeAliasEntries(alias) {
192
+ if (Array.isArray(alias)) {
193
+ return alias.filter((entry) => Boolean(entry) &&
194
+ typeof entry === 'object' &&
195
+ 'find' in entry &&
196
+ 'replacement' in entry);
197
+ }
198
+ if (alias && typeof alias === 'object') {
199
+ return Object.entries(alias).map(([find, replacement]) => ({
200
+ find,
201
+ replacement: String(replacement),
202
+ }));
203
+ }
204
+ return [];
205
+ }
206
+ /**
207
+ * Discover SMRT packages from package.json dependencies
208
+ */
209
+ function discoverSmrtPackages(root, additionalPackages = []) {
210
+ const packageJsonPath = join(root, 'package.json');
211
+ if (!existsSync(packageJsonPath)) {
212
+ console.warn('[smrt-vitest] No package.json found at', root);
213
+ return additionalPackages;
214
+ }
215
+ try {
216
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
217
+ const allDeps = {
218
+ ...packageJson.dependencies,
219
+ ...packageJson.peerDependencies,
220
+ ...packageJson.devDependencies,
221
+ };
222
+ // Find all @happyvertical/smrt-* packages (except smrt-vitest itself)
223
+ const smrtPackages = Object.keys(allDeps).filter((pkg) => pkg.startsWith('@happyvertical/smrt-') &&
224
+ pkg !== '@happyvertical/smrt-vitest');
225
+ // Combine with additional packages, removing duplicates
226
+ const allPackages = [...new Set([...smrtPackages, ...additionalPackages])];
227
+ return allPackages;
228
+ }
229
+ catch (error) {
230
+ console.error('[smrt-vitest] Failed to read package.json:', error);
231
+ return additionalPackages;
232
+ }
233
+ }
234
+ /**
235
+ * Find the root directory of a package
236
+ * Tries require.resolve first, then falls back to node_modules lookup
237
+ */
238
+ function findPackageRoot(packageName) {
239
+ const require = createRequire(`${process.cwd()}/package.json`);
240
+ // Method 1: Try require.resolve to find package entry, then walk up to package.json
241
+ try {
242
+ const pkgMainPath = require.resolve(packageName);
243
+ let dir = dirname(pkgMainPath);
244
+ for (let i = 0; i < 10; i++) {
245
+ const pkgJsonPath = join(dir, 'package.json');
246
+ if (existsSync(pkgJsonPath)) {
247
+ try {
248
+ const content = readFileSync(pkgJsonPath, 'utf-8');
249
+ const json = JSON.parse(content);
250
+ if (json.name === packageName) {
251
+ return dir;
252
+ }
253
+ }
254
+ catch {
255
+ // Keep walking up
256
+ }
257
+ }
258
+ const parent = dirname(dir);
259
+ if (parent === dir)
260
+ break;
261
+ dir = parent;
262
+ }
263
+ }
264
+ catch {
265
+ // Fall through to Method 2
266
+ }
267
+ // Method 2: Direct node_modules lookup (for file: protocol linked packages)
268
+ const nodeModulesPath = join(process.cwd(), 'node_modules', packageName);
269
+ const pkgJsonPath = join(nodeModulesPath, 'package.json');
270
+ if (existsSync(pkgJsonPath)) {
271
+ try {
272
+ const content = readFileSync(pkgJsonPath, 'utf-8');
273
+ const json = JSON.parse(content);
274
+ if (json.name === packageName) {
275
+ return nodeModulesPath;
276
+ }
277
+ }
278
+ catch {
279
+ // Fall through
280
+ }
281
+ }
282
+ // Method 3: Workspace package (sibling in monorepo)
283
+ const packageShortName = packageName.split('/').pop() || '';
284
+ const packageWithoutScope = packageShortName.replace(/^smrt-/, '');
285
+ const workspacePaths = [
286
+ join(process.cwd(), '..', packageWithoutScope),
287
+ join(process.cwd(), '..', packageShortName),
288
+ ];
289
+ for (const workspacePath of workspacePaths) {
290
+ const workspacePkgPath = join(workspacePath, 'package.json');
291
+ if (existsSync(workspacePkgPath)) {
292
+ try {
293
+ const content = readFileSync(workspacePkgPath, 'utf-8');
294
+ const json = JSON.parse(content);
295
+ if (json.name === packageName) {
296
+ return workspacePath;
297
+ }
298
+ }
299
+ catch {
300
+ // Keep trying
301
+ }
302
+ }
303
+ }
304
+ return null;
305
+ }
306
+ async function importSmrtCoreModule() {
307
+ const specifier = '@happyvertical/smrt-core';
308
+ try {
309
+ return await import(specifier);
310
+ }
311
+ catch {
312
+ const fallbackHref = new URL('../../core/src/index.ts', import.meta.url)
313
+ .href;
314
+ return await importWorkspaceSourceModule(fallbackHref);
315
+ }
316
+ }
317
+ async function importSmrtCoreManifestModule() {
318
+ const specifier = '@happyvertical/smrt-core/manifest';
319
+ try {
320
+ return await import(specifier);
321
+ }
322
+ catch {
323
+ const fallbackHref = new URL('../../core/src/manifest/index.ts', import.meta.url).href;
324
+ return await importWorkspaceSourceModule(fallbackHref);
325
+ }
326
+ }
327
+ async function importDiscoverBaseClassesModule() {
328
+ const specifier = '@happyvertical/smrt-core/manifest/discover-base-classes';
329
+ try {
330
+ return await import(specifier);
331
+ }
332
+ catch {
333
+ const fallbackHref = new URL('../../core/src/manifest/discover-base-classes.ts', import.meta.url).href;
334
+ return await importWorkspaceSourceModule(fallbackHref);
335
+ }
336
+ }
337
+ /**
338
+ * Load manifest from a package using ManifestManager
339
+ *
340
+ * This properly handles the manifest priority order:
341
+ * 1. .smrt/manifest.json (test/dev manifest with all classes)
342
+ * 2. dist/manifest.json (production manifest)
343
+ */
344
+ async function loadAndRegisterManifest(packageName, verbose) {
345
+ try {
346
+ const { ObjectRegistry } = await importSmrtCoreModule();
347
+ const { ManifestManager } = await importSmrtCoreManifestModule();
348
+ // Find the package root directory
349
+ const packageRoot = findPackageRoot(packageName);
350
+ if (!packageRoot) {
351
+ if (verbose) {
352
+ console.log(`[smrt-vitest] Could not find package root for ${packageName}`);
353
+ }
354
+ return false;
355
+ }
356
+ // Use ManifestManager to load manifest with proper priority
357
+ // (.smrt/manifest.json -> dist/manifest.json)
358
+ const manager = new ManifestManager(packageRoot);
359
+ const manifest = manager.loadLocal();
360
+ if (!manifest) {
361
+ if (verbose) {
362
+ console.log(`[smrt-vitest] No manifest found for ${packageName}`);
363
+ }
364
+ return false;
365
+ }
366
+ const registered = registerManifestObjects(ObjectRegistry, manifest, manifest.packageName || packageName);
367
+ if (verbose || registered > 0) {
368
+ console.log(`[smrt-vitest] Loaded ${registered} classes from ${packageName}`);
369
+ }
370
+ return true;
371
+ }
372
+ catch (error) {
373
+ if (verbose) {
374
+ console.error(`[smrt-vitest] Failed to load manifest from ${packageName}:`, error);
375
+ }
376
+ return false;
377
+ }
378
+ }
379
+ function registerManifestObjects(ObjectRegistry, manifest, packageName) {
380
+ if (!manifest?.objects) {
381
+ return 0;
382
+ }
383
+ let registered = 0;
384
+ for (const [name, objectDef] of Object.entries(manifest.objects)) {
385
+ if (!ObjectRegistry.hasClass(name)) {
386
+ ObjectRegistry.registerFromManifest(name, objectDef, packageName);
387
+ registered++;
388
+ }
389
+ }
390
+ return registered;
391
+ }
392
+ async function loadAndRegisterLocalManifest(root, verbose) {
393
+ try {
394
+ const { ObjectRegistry } = await importSmrtCoreModule();
395
+ const { ManifestManager } = await importSmrtCoreManifestModule();
396
+ const manager = new ManifestManager(root);
397
+ const manifest = manager.loadLocal();
398
+ if (!manifest) {
399
+ if (verbose) {
400
+ console.log('[smrt-vitest] No local manifest found');
401
+ }
402
+ return false;
403
+ }
404
+ const registered = registerManifestObjects(ObjectRegistry, manifest, manifest.packageName);
405
+ if (verbose || registered > 0) {
406
+ console.log(`[smrt-vitest] Loaded ${registered} classes from local manifest`);
407
+ }
408
+ return true;
409
+ }
410
+ catch (error) {
411
+ if (verbose) {
412
+ console.error('[smrt-vitest] Failed to load local manifest:', error);
413
+ }
414
+ return false;
415
+ }
416
+ }
417
+ /**
418
+ * Generate local manifest using ManifestBuilder
419
+ *
420
+ * This ensures the manifest is always fresh after adding new classes/fields.
421
+ * The ~1-2s overhead is minimal compared to test execution time.
422
+ */
423
+ async function generateLocalManifest(_root, options, verbose) {
424
+ try {
425
+ console.log('[smrt-vitest] Generating test manifest...');
426
+ const { ManifestBuilder } = await importSmrtCoreManifestModule();
427
+ const { discoverBaseClasses } = await importDiscoverBaseClassesModule();
428
+ // Discover base classes from external SMRT packages
429
+ const baseClasses = await discoverBaseClasses();
430
+ if (verbose) {
431
+ console.log(`[smrt-vitest] Discovered ${baseClasses.length} base classes (including ${baseClasses.length - 3} from external packages)`);
432
+ }
433
+ const builder = new ManifestBuilder();
434
+ const manifest = await builder.generate({
435
+ // File discovery
436
+ include: options.include || ['src/**/*.ts'],
437
+ exclude: options.exclude || [
438
+ '**/*.d.ts',
439
+ '**/node_modules/**',
440
+ '**/dist/**',
441
+ ],
442
+ // Scanner configuration
443
+ baseClasses,
444
+ followImports: true,
445
+ loadViteConfig: true,
446
+ discoverExternalPackages: true,
447
+ includeExternalBaseClasses: true,
448
+ includePrivateMethods: false,
449
+ includeStaticMethods: true,
450
+ // Output configuration - write to .smrt directory (ManifestManager default)
451
+ outputDir: '.smrt',
452
+ outputName: 'manifest.json',
453
+ generateTypeStub: false,
454
+ // Metadata
455
+ injectPackageInfo: true,
456
+ moduleType: 'smrt',
457
+ });
458
+ const objectCount = Object.keys(manifest.objects).length;
459
+ console.log(`[smrt-vitest] ✓ Generated manifest with ${objectCount} object(s)`);
460
+ return true;
461
+ }
462
+ catch (error) {
463
+ console.error('[smrt-vitest] Failed to generate manifest:', error);
464
+ return false;
465
+ }
466
+ }
467
+ /**
468
+ * Resolve the per-test `retry` injected into the vitest config.
469
+ *
470
+ * Precedence: `SMRT_VITEST_RETRY` (a digits-only, non-negative integer) wins;
471
+ * otherwise an explicit consumer value is preserved as-is — including the object
472
+ * form (`{ count, delay }`) — so options aren't dropped; otherwise the default is
473
+ * 2 retries in CI (`process.env.CI`) and 0 everywhere else, so local runs surface
474
+ * flaky tests immediately while the shared cross-package CI job tolerates rare
475
+ * transient timing flakes.
476
+ */
477
+ function resolveRetry(explicitRetry) {
478
+ const override = process.env.SMRT_VITEST_RETRY;
479
+ // Digits-only so "2x"/"2.5"/"-1" fall through to the explicit/default value
480
+ // rather than being silently coerced by parseInt.
481
+ if (override != null && /^\d+$/.test(override)) {
482
+ return Number.parseInt(override, 10);
483
+ }
484
+ if (explicitRetry != null) {
485
+ return explicitRetry;
486
+ }
487
+ return process.env.CI ? 2 : 0;
488
+ }
489
+ /**
490
+ * Create the SMRT Vitest plugin
491
+ *
492
+ * This plugin automatically generates and loads manifests before tests run,
493
+ * enabling cross-package integration tests without needing to run `smrt test` first.
494
+ *
495
+ * @param options - Plugin configuration options
496
+ * @returns Vitest plugin
497
+ *
498
+ * @example Basic usage
499
+ * ```typescript
500
+ * import { defineConfig } from 'vitest/config';
501
+ * import { smrtVitestPlugin } from '@happyvertical/smrt-vitest';
502
+ *
503
+ * export default defineConfig({
504
+ * plugins: [smrtVitestPlugin()],
505
+ * });
506
+ * ```
507
+ *
508
+ * @example With additional packages
509
+ * ```typescript
510
+ * import { defineConfig } from 'vitest/config';
511
+ * import { smrtVitestPlugin } from '@happyvertical/smrt-vitest';
512
+ *
513
+ * export default defineConfig({
514
+ * plugins: [
515
+ * smrtVitestPlugin({
516
+ * packages: ['@my-org/custom-smrt-package'],
517
+ * verbose: true,
518
+ * }),
519
+ * ],
520
+ * });
521
+ * ```
522
+ *
523
+ * @example Disable auto-generation (use pre-built manifest)
524
+ * ```typescript
525
+ * export default defineConfig({
526
+ * plugins: [
527
+ * smrtVitestPlugin({
528
+ * generateManifest: false, // Use existing manifest only
529
+ * }),
530
+ * ],
531
+ * });
532
+ * ```
533
+ */
534
+ export function smrtVitestPlugin(options = {}) {
535
+ const { packages = [], verbose = false, root = process.cwd(), generateManifest = true, setupFile = resolveDefaultSetupFile(), } = options;
536
+ let manifestsLoaded = false;
537
+ const setupFileId = setupFile;
538
+ const workspaceAliases = getWorkspaceViteAliases(root);
539
+ const ensureSetupFiles = (value) => {
540
+ const setupFiles = Array.isArray(value) ? [...value] : value ? [value] : [];
541
+ if (!setupFiles.includes(setupFileId)) {
542
+ setupFiles.push(setupFileId);
543
+ }
544
+ return setupFiles;
545
+ };
546
+ const applyTestDefaultsToProjects = (projects, rootRetry) => {
547
+ projects?.forEach((project) => {
548
+ if (!project || typeof project !== 'object' || !('test' in project)) {
549
+ return;
550
+ }
551
+ const projectConfig = project;
552
+ projectConfig.test = {
553
+ ...projectConfig.test,
554
+ // Vitest does NOT inherit the root `test.retry` into per-project configs,
555
+ // so apply it here, falling back to the root retry (then the CI default)
556
+ // when the project has none. resolveRetry preserves an explicit
557
+ // per-project value unless SMRT_VITEST_RETRY forces one.
558
+ retry: resolveRetry(projectConfig.test?.retry ?? rootRetry),
559
+ setupFiles: ensureSetupFiles(projectConfig.test?.setupFiles),
560
+ };
561
+ });
562
+ };
563
+ return {
564
+ name: 'smrt-vitest',
565
+ config(userConfig) {
566
+ const rootRetry = userConfig.test?.retry;
567
+ applyTestDefaultsToProjects(userConfig.test?.projects, rootRetry);
568
+ const setupFiles = ensureSetupFiles(userConfig.test?.setupFiles);
569
+ const resolveConfig = userConfig.resolve && typeof userConfig.resolve === 'object'
570
+ ? userConfig.resolve
571
+ : undefined;
572
+ const alias = normalizeAliasEntries(resolveConfig?.alias);
573
+ return {
574
+ resolve: {
575
+ alias: [...workspaceAliases, ...alias],
576
+ },
577
+ test: {
578
+ setupFiles,
579
+ // Re-run a failed test before failing the run, in CI only. Several
580
+ // packages have rare, CI-environment-specific timing flakes that pass
581
+ // on re-run (observed: every flaky "Test Packages" failure went green
582
+ // on rerun, on a different package each time, none reproducible
583
+ // locally). Retry keeps the shared cross-package CI job reliable
584
+ // WITHOUT masking real failures — a deterministic failure still fails
585
+ // all attempts, and vitest reports retried tests as "flaky" so they
586
+ // stay visible. An explicit `test.retry` in the consumer config is
587
+ // preserved (including the object form); override with
588
+ // SMRT_VITEST_RETRY=<n>.
589
+ retry: resolveRetry(rootRetry),
590
+ },
591
+ };
592
+ },
593
+ // Run during config resolution to ensure manifests are loaded before tests
594
+ async configResolved() {
595
+ if (manifestsLoaded)
596
+ return;
597
+ // Step 1: Generate local manifest if enabled (default: true)
598
+ // This ensures manifest is always fresh after adding new classes/fields
599
+ if (generateManifest) {
600
+ await generateLocalManifest(root, options, verbose);
601
+ }
602
+ // Step 2: Load the local manifest so late-imported local classes are
603
+ // available to schema preparation before the first DB call.
604
+ await loadAndRegisterLocalManifest(root, verbose);
605
+ // Step 3: Discover and load manifests from SMRT peer dependencies
606
+ const smrtPackages = discoverSmrtPackages(root, packages);
607
+ if (smrtPackages.length === 0) {
608
+ if (verbose) {
609
+ console.log('[smrt-vitest] No SMRT packages found to load');
610
+ }
611
+ }
612
+ else {
613
+ if (verbose) {
614
+ console.log(`[smrt-vitest] Discovered ${smrtPackages.length} SMRT packages:`, smrtPackages);
615
+ }
616
+ // Load manifests from all discovered packages
617
+ const results = await Promise.all(smrtPackages.map((pkg) => loadAndRegisterManifest(pkg, verbose)));
618
+ const successCount = results.filter(Boolean).length;
619
+ console.log(`[smrt-vitest] Loaded manifests from ${successCount}/${smrtPackages.length} packages`);
620
+ }
621
+ // Step 4: Validate local manifest is loaded
622
+ try {
623
+ const { ManifestManager } = await importSmrtCoreManifestModule();
624
+ const manager = new ManifestManager(root);
625
+ const localManifest = manager.loadLocal();
626
+ if (localManifest) {
627
+ console.log(`[smrt-vitest] ✓ Local manifest: ${Object.keys(localManifest.objects).length} objects`);
628
+ }
629
+ else if (!generateManifest) {
630
+ // Only show warning if auto-generation is disabled
631
+ // (if enabled and still missing, generateLocalManifest already logged an error)
632
+ const devPath = manager.getOutputPath('dev');
633
+ const buildPath = manager.getOutputPath('build');
634
+ console.warn(`
635
+ ╔═══════════════════════════════════════════════════════════════════════╗
636
+ ║ [smrt-vitest] WARNING: No local manifest found ║
637
+ ╠═══════════════════════════════════════════════════════════════════════╣
638
+ ║ Tests may fail with "No field metadata found" errors. ║
639
+ ║ ║
640
+ ║ Checked locations: ║
641
+ ║ • ${devPath.padEnd(55)}║
642
+ ║ • ${buildPath.padEnd(55)}║
643
+ ║ ║
644
+ ║ To fix, either: ║
645
+ ║ • Enable generateManifest: true in plugin options (default) ║
646
+ ║ • Run: smrt generate:test ║
647
+ ║ • Run: npm run build (if manifest is part of build) ║
648
+ ╚═══════════════════════════════════════════════════════════════════════╝
649
+ `);
650
+ }
651
+ }
652
+ catch (error) {
653
+ if (verbose) {
654
+ console.warn('[smrt-vitest] Could not validate local manifest:', error);
655
+ }
656
+ }
657
+ manifestsLoaded = true;
658
+ },
659
+ };
660
+ }
661
+ /**
662
+ * Discover and register SMRT manifests from peer dependencies.
663
+ *
664
+ * An imperative alternative to {@link smrtVitestPlugin} for environments
665
+ * where a Vite plugin is not available (e.g., a plain `globalSetup` file or
666
+ * a custom test runner bootstrap).
667
+ *
668
+ * The function reads `package.json` in the working directory, finds all
669
+ * `@happyvertical/smrt-*` dependencies, locates their manifest files, and
670
+ * registers every class in the global `ObjectRegistry`. It does **not**
671
+ * generate a new manifest — use `smrtVitestPlugin()` with
672
+ * `generateManifest: true` (the default) if auto-generation is needed.
673
+ *
674
+ * @param options - Same options accepted by {@link smrtVitestPlugin}.
675
+ * Relevant fields: `packages`, `verbose`, `root`.
676
+ * @returns A promise that resolves once all manifests have been loaded.
677
+ *
678
+ * @example
679
+ * ```typescript
680
+ * // vitest.config.ts
681
+ * import { defineConfig } from 'vitest/config';
682
+ *
683
+ * export default defineConfig({
684
+ * test: {
685
+ * globalSetup: ['@happyvertical/smrt-vitest/setup'],
686
+ * },
687
+ * });
688
+ * ```
689
+ *
690
+ * @example Calling directly in a custom bootstrap
691
+ * ```typescript
692
+ * import { setupSmrtManifests } from '@happyvertical/smrt-vitest';
693
+ *
694
+ * await setupSmrtManifests({ verbose: true });
695
+ * ```
696
+ *
697
+ * @see {@link smrtVitestPlugin} for the recommended Vite-plugin approach that
698
+ * also handles manifest generation.
699
+ */
700
+ export async function setupSmrtManifests(options = {}) {
701
+ const { packages = [], verbose = false, root = process.cwd() } = options;
702
+ await loadAndRegisterLocalManifest(root, verbose);
703
+ const smrtPackages = discoverSmrtPackages(root, packages);
704
+ if (smrtPackages.length === 0) {
705
+ return;
706
+ }
707
+ if (verbose) {
708
+ console.log(`[smrt-vitest] Discovered ${smrtPackages.length} SMRT packages:`, smrtPackages);
709
+ }
710
+ // Load manifests from all discovered packages
711
+ const results = await Promise.all(smrtPackages.map((pkg) => loadAndRegisterManifest(pkg, verbose)));
712
+ const successCount = results.filter(Boolean).length;
713
+ console.log(`[smrt-vitest] Loaded manifests from ${successCount}/${smrtPackages.length} packages`);
714
+ }
715
+ export default smrtVitestPlugin;
716
+ // Export test database utilities
717
+ export { createIsolatedTestDb, createIsolatedTestDbFromManifest, createTestDb, getAdapterDisplayName, getInMemoryDbConfig, getTestAdapter, getTestDbConfig, isPostgresAvailable, } from './test-db.js';
718
+ //# sourceMappingURL=index.js.map