@happyvertical/smrt-vitest 0.57.45 → 0.57.47

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 CHANGED
@@ -6,7 +6,9 @@
6
6
  * external package classes aren't registered in the test manifest.
7
7
  *
8
8
  * Uses ManifestManager for unified manifest loading, which properly handles
9
- * the manifest priority order: .smrt/manifest.json (test) -> dist/manifest.json (production)
9
+ * the manifest priority order: this run's test manifest snapshot
10
+ * (`.smrt/test-runs/<run>/manifest.json`, #4128) -> .smrt/manifest.json ->
11
+ * dist/manifest.json
10
12
  *
11
13
  * @example
12
14
  * ```typescript
@@ -25,9 +27,11 @@
25
27
  *
26
28
  * @packageDocumentation
27
29
  */
28
- import { existsSync, readdirSync, readFileSync, realpathSync, unlinkSync, } from 'node:fs';
30
+ import { randomBytes } from 'node:crypto';
31
+ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, } from 'node:fs';
29
32
  import { createRequire } from 'node:module';
30
- import { dirname, join } from 'node:path';
33
+ import { hostname } from 'node:os';
34
+ import { dirname, join, resolve } from 'node:path';
31
35
  import { fileURLToPath } from 'node:url';
32
36
  import { configDefaults } from 'vitest/config';
33
37
  import { isVerbose, shouldLogManifestSummaryOnce } from './log.js';
@@ -118,6 +122,130 @@ export function setManifestRegistrationOptionsForRoot(root, options) {
118
122
  byRoot[key] = options;
119
123
  process.env[SMRT_VITEST_SETUP_OPTIONS_ENV_KEY] = JSON.stringify(byRoot);
120
124
  }
125
+ /**
126
+ * Environment variable carrying the active test manifest path per project
127
+ * root. Mirrors `SMRT_TEST_MANIFEST_PATHS_ENV_KEY` in
128
+ * `@happyvertical/smrt-core/manifest` (`test-artifacts.ts`), which owns the
129
+ * read side; kept as a literal here so {@link smrtVitestPlugin}'s `config()`
130
+ * hook can publish it synchronously, before Vitest forks any pool worker
131
+ * (see {@link SMRT_VITEST_SETUP_OPTIONS_ENV_KEY}). Parity with core is
132
+ * asserted in `src/__tests__/test-run-manifest-isolation.test.ts`.
133
+ */
134
+ export const SMRT_TEST_MANIFEST_PATHS_ENV_KEY = '__SMRT_TEST_MANIFEST_PATHS__';
135
+ /**
136
+ * Directory, relative to a project root, holding one subdirectory per test
137
+ * run's manifest snapshot (#4128).
138
+ */
139
+ export const SMRT_TEST_RUNS_DIR = '.smrt/test-runs';
140
+ /** A run directory whose owner is unknown or remote is pruned after this. */
141
+ const STALE_TEST_RUN_MS = 24 * 60 * 60 * 1000;
142
+ const testRunManifestPaths = new Map();
143
+ function isProcessAlive(pid) {
144
+ try {
145
+ process.kill(pid, 0);
146
+ return true;
147
+ }
148
+ catch (error) {
149
+ // EPERM: the process exists but belongs to someone else.
150
+ return error.code === 'EPERM';
151
+ }
152
+ }
153
+ /**
154
+ * Remove snapshot directories whose run has ended. A run recorded on this
155
+ * host is removed only once its process is gone, however long it has been
156
+ * running (a watch or `--ui` session can outlive a day). A run whose owner
157
+ * cannot be probed — no readable `owner.json`, or another host or container
158
+ * sharing the checkout — is removed once it is older than a day.
159
+ */
160
+ function pruneEndedTestRuns(root, keep) {
161
+ const runsDir = join(root, SMRT_TEST_RUNS_DIR);
162
+ let entries;
163
+ try {
164
+ entries = readdirSync(runsDir);
165
+ }
166
+ catch {
167
+ return;
168
+ }
169
+ const thisHost = hostname();
170
+ for (const entry of entries) {
171
+ const runDir = join(runsDir, entry);
172
+ if (runDir === keep)
173
+ continue;
174
+ let owner;
175
+ try {
176
+ owner = JSON.parse(readFileSync(join(runDir, 'owner.json'), 'utf8'));
177
+ }
178
+ catch {
179
+ owner = undefined;
180
+ }
181
+ let ageMs;
182
+ try {
183
+ ageMs = Date.now() - (owner?.startedAt ?? statSync(runDir).mtimeMs);
184
+ }
185
+ catch {
186
+ continue;
187
+ }
188
+ const probeable = owner?.hostname === thisHost && typeof owner.pid === 'number';
189
+ const ended = probeable
190
+ ? owner?.pid !== process.pid && !isProcessAlive(owner?.pid)
191
+ : ageMs > STALE_TEST_RUN_MS;
192
+ if (ended) {
193
+ rmSync(runDir, { recursive: true, force: true });
194
+ }
195
+ }
196
+ }
197
+ /**
198
+ * The manifest snapshot path owned by this process's test run for `root`
199
+ * (#4128): `<root>/.smrt/test-runs/<pid>-<time>-<random>/manifest.json`.
200
+ *
201
+ * Test generation scans test-inclusive sources, so it must not share a path
202
+ * with production generation: `.smrt/manifest.json` is rewritten by every
203
+ * `smrtPlugin()` build (and by a typecheck whose Turbo graph rebuilds the
204
+ * package), which used to replace the test manifest while forked test
205
+ * workers were still reading it. Each run instead owns a directory nothing
206
+ * else writes; it is stable for the life of this process, so every plugin
207
+ * instance and config restart for the same root shares it, and removed when
208
+ * the process exits.
209
+ */
210
+ export function resolveTestRunManifestPath(root) {
211
+ const key = normalizeRootKey(resolve(root));
212
+ const existing = testRunManifestPaths.get(key);
213
+ if (existing)
214
+ return existing;
215
+ const runId = `${process.pid}-${Date.now().toString(36)}-${randomBytes(4).toString('hex')}`;
216
+ const manifestPath = join(key, SMRT_TEST_RUNS_DIR, runId, 'manifest.json');
217
+ testRunManifestPaths.set(key, manifestPath);
218
+ const runDir = dirname(manifestPath);
219
+ process.once('exit', () => {
220
+ rmSync(runDir, { recursive: true, force: true });
221
+ });
222
+ return manifestPath;
223
+ }
224
+ /**
225
+ * Publish `manifestPath` as the active test manifest for `root`, merged into
226
+ * {@link SMRT_TEST_MANIFEST_PATHS_ENV_KEY}. Keys match core's reader: the
227
+ * root as given, trimmed of trailing separators, and realpath'd.
228
+ */
229
+ function registerTestRunManifestPath(root, manifestPath) {
230
+ let byRoot = {};
231
+ const raw = process.env[SMRT_TEST_MANIFEST_PATHS_ENV_KEY];
232
+ if (raw) {
233
+ try {
234
+ const parsed = JSON.parse(raw);
235
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
236
+ byRoot = parsed;
237
+ }
238
+ }
239
+ catch {
240
+ // A malformed prior value is replaced.
241
+ }
242
+ }
243
+ const trimmed = root.replace(/[/\\]+$/, '') || root;
244
+ for (const key of new Set([root, trimmed, normalizeRootKey(root)])) {
245
+ byRoot[key] = manifestPath;
246
+ }
247
+ process.env[SMRT_TEST_MANIFEST_PATHS_ENV_KEY] = JSON.stringify(byRoot);
248
+ }
121
249
  function resolveDefaultSetupFile() {
122
250
  const sourceSetupPath = fileURLToPath(new URL('./setup.ts', import.meta.url));
123
251
  if (existsSync(sourceSetupPath)) {
@@ -195,6 +323,147 @@ function readWorkspacePackageRoots(root) {
195
323
  }
196
324
  return packageRoots;
197
325
  }
326
+ /**
327
+ * The runtime target an export condition object resolves to for a Node test
328
+ * run: `import`, then `default`, then `svelte` (nested condition objects are
329
+ * followed). `types` and `browser` are never runtime targets here.
330
+ */
331
+ function exportRuntimeTarget(value) {
332
+ if (typeof value === 'string')
333
+ return value;
334
+ if (!value || typeof value !== 'object' || Array.isArray(value))
335
+ return null;
336
+ const conditions = value;
337
+ for (const condition of ['import', 'default', 'svelte']) {
338
+ const target = exportRuntimeTarget(conditions[condition]);
339
+ if (target)
340
+ return target;
341
+ }
342
+ return null;
343
+ }
344
+ /**
345
+ * Source file a built export target was compiled from (#2954): `./dist/<p>.js`
346
+ * maps to `src/<p>.ts`, `src/<p>.svelte.ts`, `src/<p>/index.ts`, or, for
347
+ * `svelte-package` layouts, the same under `src/lib/`. Returns `null` for
348
+ * targets outside `dist/` or with no matching source file.
349
+ */
350
+ function sourceForExportTarget(packageRoot, target) {
351
+ const match = /^\.\/dist\/(.+?)\.(?:m?js)$/.exec(target);
352
+ if (!match)
353
+ return null;
354
+ const built = match[1];
355
+ for (const base of ['src', 'src/lib']) {
356
+ for (const candidate of [
357
+ `${base}/${built}.ts`,
358
+ `${base}/${built}.svelte.ts`,
359
+ `${base}/${built}/index.ts`,
360
+ ]) {
361
+ const path = join(packageRoot, candidate);
362
+ if (existsSync(path))
363
+ return path;
364
+ }
365
+ }
366
+ return null;
367
+ }
368
+ /**
369
+ * Every exact (non-pattern) JavaScript subpath a workspace package exports,
370
+ * with the source file it is built from, or `null` when no source file can be
371
+ * derived. Read from the package's own `exports` map, so a newly exported
372
+ * subpath is covered without editing this file (#2954).
373
+ */
374
+ export function getWorkspaceExportSources(packageRoot) {
375
+ let exportsMap;
376
+ try {
377
+ exportsMap = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf-8')).exports;
378
+ }
379
+ catch {
380
+ return [];
381
+ }
382
+ if (!exportsMap || typeof exportsMap !== 'object')
383
+ return [];
384
+ const entries = [];
385
+ for (const [subpath, value] of Object.entries(exportsMap)) {
386
+ if (!subpath.startsWith('.') || subpath.includes('*'))
387
+ continue;
388
+ const target = exportRuntimeTarget(value);
389
+ if (!target || !/\.m?js$/.test(target))
390
+ continue;
391
+ entries.push({
392
+ subpath,
393
+ target,
394
+ source: sourceForExportTarget(packageRoot, target),
395
+ });
396
+ }
397
+ return entries;
398
+ }
399
+ /**
400
+ * The concrete runtime target (`./dist/...`) a workspace package's exports
401
+ * map gives `subpath`, exact or pattern, or `null` when it exports none.
402
+ */
403
+ export function workspaceExportTarget(packageRoot, subpath) {
404
+ let exportsMap;
405
+ try {
406
+ exportsMap = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf-8')).exports;
407
+ }
408
+ catch {
409
+ return null;
410
+ }
411
+ if (!exportsMap || typeof exportsMap !== 'object')
412
+ return null;
413
+ const entries = exportsMap;
414
+ if (subpath in entries)
415
+ return exportRuntimeTarget(entries[subpath]);
416
+ // Node picks the pattern with the longest prefix before `*`.
417
+ const patterns = Object.entries(entries)
418
+ .filter(([pattern]) => pattern.includes('*'))
419
+ .sort(([left], [right]) => right.indexOf('*') - left.indexOf('*'));
420
+ for (const [pattern, value] of patterns) {
421
+ const star = pattern.indexOf('*');
422
+ if (star < 0)
423
+ continue;
424
+ const prefix = pattern.slice(0, star);
425
+ const suffix = pattern.slice(star + 1);
426
+ if (!subpath.startsWith(prefix) ||
427
+ !subpath.endsWith(suffix) ||
428
+ subpath.length < prefix.length + suffix.length) {
429
+ continue;
430
+ }
431
+ const target = exportRuntimeTarget(value);
432
+ if (!target?.includes('*'))
433
+ continue;
434
+ return target
435
+ .split('*')
436
+ .join(subpath.slice(prefix.length, subpath.length - suffix.length));
437
+ }
438
+ return null;
439
+ }
440
+ /**
441
+ * Source file for the concrete `subpath` a consumer imports from a workspace
442
+ * package — exact or pattern export (`"./utils/*": "./dist/utils/*"`) — for
443
+ * the per-import resolution in {@link smrtVitestPlugin} (#2954). Exact
444
+ * subpaths are normally aliased statically; pattern subpaths can only be
445
+ * mapped per import. A JavaScript target maps like an exact export; another
446
+ * asset (CSS, JSON) maps to the same relative path under `src/` or
447
+ * `src/lib/`. Returns `null` when nothing is exported there or no source
448
+ * file exists.
449
+ */
450
+ export function resolveWorkspaceExportSource(packageRoot, subpath) {
451
+ const concrete = workspaceExportTarget(packageRoot, subpath);
452
+ if (!concrete)
453
+ return null;
454
+ const jsSource = sourceForExportTarget(packageRoot, concrete);
455
+ if (jsSource)
456
+ return jsSource;
457
+ const asset = /^\.\/dist\/(.+)$/.exec(concrete);
458
+ if (asset && !/\.m?js$/.test(concrete)) {
459
+ for (const base of ['src', 'src/lib']) {
460
+ const path = join(packageRoot, base, asset[1]);
461
+ if (existsSync(path))
462
+ return path;
463
+ }
464
+ }
465
+ return null;
466
+ }
198
467
  function addAliasIfPresent(aliases, find, replacement) {
199
468
  if (aliases.some((entry) => entry.find === find) ||
200
469
  !existsSync(replacement)) {
@@ -318,6 +587,21 @@ export function getWorkspaceViteAliases(root = process.cwd(), options = {}) {
318
587
  addAliasIfPresent(aliases, '@happyvertical/smrt-svelte/cookbook', join(packageRoot, 'src/components/cookbook/index.ts'));
319
588
  }
320
589
  }
590
+ // Every remaining exported subpath resolves to the source it is built from
591
+ // (#2954). The hand-written entries above win (`addAliasIfPresent` keeps the
592
+ // first entry per specifier): they cover entries whose source layout the
593
+ // derivation cannot infer. Without an alias a consumer test resolves the
594
+ // subpath through the exports map to the sibling's last BUILT dist — stale,
595
+ // or missing in a fresh worktree — while resolving the package root to
596
+ // source, which also loads two copies of shared modules.
597
+ for (const [packageName, packageRoot] of packageRoots.entries()) {
598
+ for (const { subpath, source } of getWorkspaceExportSources(packageRoot)) {
599
+ if (!source)
600
+ continue;
601
+ const specifier = subpath === '.' ? packageName : `${packageName}${subpath.slice(1)}`;
602
+ addAliasIfPresent(aliases, specifier, source);
603
+ }
604
+ }
321
605
  const { filter } = options;
322
606
  const kept = filter ? aliases.filter((entry) => filter(entry)) : aliases;
323
607
  return kept
@@ -330,6 +614,68 @@ export function getWorkspaceViteAliases(root = process.cwd(), options = {}) {
330
614
  replacement,
331
615
  }));
332
616
  }
617
+ /** Forward-slash form of a filesystem path, for prefix comparisons. */
618
+ function toPosix(path) {
619
+ return path.replace(/\\/g, '/');
620
+ }
621
+ /** The package name a bare or subpath specifier belongs to. */
622
+ function packageNameOf(specifier) {
623
+ const parts = specifier.split('/');
624
+ return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
625
+ }
626
+ /**
627
+ * Detects a workspace package specifier resolved to a sibling's BUILT output
628
+ * (#2954).
629
+ *
630
+ * Every exported subpath of a workspace package is aliased to source (see
631
+ * {@link getWorkspaceViteAliases}), so a module loaded from another
632
+ * package's `dist/` means a specifier escaped the aliases: the test would
633
+ * run the sibling's last build — stale, or missing in a fresh worktree — and
634
+ * could pass against code that no longer exists. Returns the failure message
635
+ * for such an id, or `null`. Exempt: the package under test (its own build
636
+ * output), and packages the consumer deliberately resolves to built entries,
637
+ * by dropping their generated aliases through `aliasFilter` or by aliasing
638
+ * one of their specifiers to a `dist/` file itself.
639
+ */
640
+ export function createWorkspaceDistGuard(root, exemptPackages = new Set()) {
641
+ const ownRoot = normalizeRootKey(resolve(root));
642
+ const distRoots = [];
643
+ for (const [name, packageRoot] of readWorkspacePackageRoots(root)) {
644
+ const real = normalizeRootKey(packageRoot);
645
+ if (real === ownRoot || exemptPackages.has(name))
646
+ continue;
647
+ distRoots.push({ name, distRoot: `${toPosix(real)}/dist/` });
648
+ }
649
+ return (id) => {
650
+ const file = toPosix(id.split('?')[0] ?? id);
651
+ const owner = distRoots.find(({ distRoot }) => file.startsWith(distRoot));
652
+ if (!owner)
653
+ return null;
654
+ return (`[smrt-vitest] ${file} is the built output of workspace package ${owner.name}. ` +
655
+ 'A consumer test must run its source, not a possibly stale or missing dist (#2954). ' +
656
+ `Export the subpath from ${owner.name}'s package.json with a source file the ` +
657
+ 'workspace aliases can derive (dist/<p>.js <- src/<p>.ts, src/<p>/index.ts, src/lib/...), ' +
658
+ 'or resolve published entries deliberately via aliasFilter.');
659
+ };
660
+ }
661
+ /** Workspace packages whose `dist/` a consumer config deliberately targets. */
662
+ function packagesWithDeliberateDistEntries(root, droppedSpecifiers, userAliases) {
663
+ const exempt = new Set();
664
+ for (const specifier of droppedSpecifiers) {
665
+ exempt.add(packageNameOf(specifier));
666
+ }
667
+ const roots = readWorkspacePackageRoots(root);
668
+ for (const { replacement } of userAliases) {
669
+ for (const [name, packageRoot] of roots) {
670
+ const target = typeof replacement === 'string' ? toPosix(replacement) : '';
671
+ if (target.startsWith(`${toPosix(packageRoot)}/dist/`) ||
672
+ target.startsWith(`${toPosix(normalizeRootKey(packageRoot))}/dist/`)) {
673
+ exempt.add(name);
674
+ }
675
+ }
676
+ }
677
+ return exempt;
678
+ }
333
679
  function normalizeAliasEntries(alias) {
334
680
  if (Array.isArray(alias)) {
335
681
  return alias.filter((entry) => Boolean(entry) &&
@@ -466,17 +812,6 @@ async function importSmrtCoreManifestModule() {
466
812
  return await importWorkspaceSourceModule(fallbackHref);
467
813
  }
468
814
  }
469
- async function importSmrtCoreKnowledgeModule() {
470
- const specifier = '@happyvertical/smrt-core/knowledge';
471
- try {
472
- return await import(specifier);
473
- }
474
- catch {
475
- const fallbackHref = new URL('../../core/src/knowledge.ts', import.meta.url)
476
- .href;
477
- return await importWorkspaceSourceModule(fallbackHref);
478
- }
479
- }
480
815
  async function importDiscoverBaseClassesModule() {
481
816
  const specifier = '@happyvertical/smrt-core/manifest/discover-base-classes';
482
817
  try {
@@ -491,8 +826,12 @@ async function importDiscoverBaseClassesModule() {
491
826
  * Load manifest from a package using ManifestManager
492
827
  *
493
828
  * This properly handles the manifest priority order:
494
- * 1. .smrt/manifest.json (test/dev manifest with all classes)
829
+ * 1. .smrt/manifest.json (local build manifest)
495
830
  * 2. dist/manifest.json (production manifest)
831
+ * `loadLocal()` would prefer an active test snapshot registered for the
832
+ * dependency's own root; only that root's own plugin registers one, so a
833
+ * dependency is read from its snapshot only when it runs in the same
834
+ * process or a child of its own test run.
496
835
  */
497
836
  async function loadAndRegisterManifest(packageName, verbose) {
498
837
  try {
@@ -616,12 +955,19 @@ async function loadAndRegisterLocalManifest(root, verbose) {
616
955
  }
617
956
  }
618
957
  /**
619
- * Generate local manifest using ManifestBuilder
958
+ * Generate this run's test manifest snapshot using ManifestBuilder.
620
959
  *
621
960
  * This ensures the manifest is always fresh after adding new classes/fields.
622
- * The ~1-2s overhead is minimal compared to test execution time.
961
+ * The ~1-2s overhead is minimal compared to test execution time. The scan is
962
+ * test-inclusive (inline `*.test.ts` fixture classes need metadata, #2760),
963
+ * so it is written only to `manifestPath` — the run's own snapshot
964
+ * (`outputMode: 'explicit'`) — never to `.smrt/manifest.json`,
965
+ * `dist/manifest.json`, or their paired `smrt-knowledge.json`, which belong
966
+ * to production generation (#4128). A production pair therefore stays
967
+ * byte-identical across a test run, so knowledge freshness (#3205) holds
968
+ * without regenerating it from test output.
623
969
  */
624
- async function generateLocalManifest(root, options, verbose, producerApi) {
970
+ async function generateLocalManifest(root, options, verbose, manifestPath) {
625
971
  try {
626
972
  console.log('[smrt-vitest] Generating test manifest...');
627
973
  const { ManifestBuilder } = await importSmrtCoreManifestModule();
@@ -631,6 +977,15 @@ async function generateLocalManifest(root, options, verbose, producerApi) {
631
977
  if (verbose) {
632
978
  console.log(`[smrt-vitest] Discovered ${baseClasses.length} base classes (including ${baseClasses.length - 3} from external packages)`);
633
979
  }
980
+ const runDir = dirname(manifestPath);
981
+ pruneEndedTestRuns(root, runDir);
982
+ mkdirSync(runDir, { recursive: true });
983
+ const owner = {
984
+ pid: process.pid,
985
+ hostname: hostname(),
986
+ startedAt: Date.now(),
987
+ };
988
+ writeFileSync(join(runDir, 'owner.json'), JSON.stringify(owner));
634
989
  const builder = new ManifestBuilder(root);
635
990
  const manifest = await builder.generate({
636
991
  // File discovery
@@ -648,10 +1003,10 @@ async function generateLocalManifest(root, options, verbose, producerApi) {
648
1003
  includeExternalBaseClasses: true,
649
1004
  includePrivateMethods: false,
650
1005
  includeStaticMethods: true,
651
- // Output configuration - write to .smrt directory (ManifestManager default)
652
- outputMode: 'dev',
1006
+ // Output configuration - only this run's snapshot (#4128)
1007
+ outputMode: 'explicit',
653
1008
  artifactPurpose: 'test',
654
- outputDir: '.smrt',
1009
+ outputDir: runDir,
655
1010
  outputName: 'manifest.json',
656
1011
  generateTypeStub: false,
657
1012
  // Metadata
@@ -659,7 +1014,6 @@ async function generateLocalManifest(root, options, verbose, producerApi) {
659
1014
  moduleType: 'smrt',
660
1015
  });
661
1016
  const objectCount = Object.keys(manifest.objects).length;
662
- await refreshTestKnowledgeArtifact(root, manifest, producerApi);
663
1017
  console.log(`[smrt-vitest] ✓ Generated manifest with ${objectCount} object(s)`);
664
1018
  return true;
665
1019
  }
@@ -668,50 +1022,6 @@ async function generateLocalManifest(root, options, verbose, producerApi) {
668
1022
  return false;
669
1023
  }
670
1024
  }
671
- async function refreshTestKnowledgeArtifact(root, manifest, producerApi) {
672
- const { AGENT_SURFACE_HASH_PREFIX, buildDomainKnowledgeManifest, publishAtomicArtifact, resolveFileKnowledgeConfig, } = await importSmrtCoreKnowledgeModule();
673
- const manifestPath = join(root, '.smrt/manifest.json');
674
- const knowledgePath = join(root, '.smrt/smrt-knowledge.json');
675
- let priorKnowledge;
676
- try {
677
- priorKnowledge = JSON.parse(readFileSync(knowledgePath, 'utf8'));
678
- }
679
- catch {
680
- // A missing or malformed prior artifact is replaced by current generation.
681
- }
682
- const config = producerApi
683
- ? await producerApi.resolveKnowledgeConfig(manifest)
684
- : await resolveFileKnowledgeConfig(root, manifest.packageName);
685
- if (config.enabled === false) {
686
- if (existsSync(knowledgePath))
687
- unlinkSync(knowledgePath);
688
- return;
689
- }
690
- const agentSurface = producerApi
691
- ? await producerApi.resolveKnowledgeAgentSurface()
692
- : undefined;
693
- const knowledge = buildDomainKnowledgeManifest({
694
- manifest,
695
- rootDir: root,
696
- manifestPath,
697
- config,
698
- agentSurface,
699
- });
700
- const priorSurfaceHashes = Object.entries(priorKnowledge?.sourceHashes ?? {}).filter(([key]) => key.startsWith(AGENT_SURFACE_HASH_PREFIX));
701
- if (!agentSurface &&
702
- priorKnowledge?.agentSurface &&
703
- priorSurfaceHashes.length) {
704
- // Without the producer API we cannot rescan declarations. Preserve the
705
- // last surface *and its old source hashes*: rebuilding hashes from files
706
- // under a stale declaration would incorrectly certify it as fresh.
707
- knowledge.agentSurface = priorKnowledge.agentSurface;
708
- Object.assign(knowledge.sourceHashes, Object.fromEntries(priorSurfaceHashes));
709
- }
710
- publishAtomicArtifact({
711
- path: knowledgePath,
712
- content: JSON.stringify(knowledge, null, 2),
713
- });
714
- }
715
1025
  /**
716
1026
  * Build the `oxc` transform defaults injected into the consumer config.
717
1027
  *
@@ -835,12 +1145,42 @@ export function smrtVitestPlugin(options = {}) {
835
1145
  const { packages = [], verbose: verboseOption = false, root = process.cwd(), generateManifest = true, setupFile = resolveDefaultSetupFile(), aliasFilter, } = options;
836
1146
  const verbose = isVerbose(verboseOption);
837
1147
  let manifestsLoaded = false;
1148
+ const testRunManifestPath = resolveTestRunManifestPath(root);
838
1149
  const setupFileId = setupFile;
839
1150
  const workspaceAliases = getWorkspaceViteAliases(root, {
840
1151
  filter: aliasFilter,
841
1152
  });
1153
+ const keptAliasSources = new Set(workspaceAliases.map((entry) => entry.find.source));
1154
+ // Specifiers the consumer's aliasFilter removed: those packages resolve
1155
+ // through their exports map to built entries on purpose.
1156
+ const droppedSpecifiers = aliasFilter
1157
+ ? getWorkspaceViteAliases(root)
1158
+ .filter((entry) => !keptAliasSources.has(entry.find.source))
1159
+ .map((entry) => entry.find.source.replace(/^\^|\$$/g, '').replace(/\\(.)/g, '$1'))
1160
+ : [];
1161
+ let distGuard;
1162
+ let distExempt = new Set();
1163
+ const workspacePackages = [...readWorkspacePackageRoots(root).entries()];
1164
+ const matchWorkspacePackageSpecifier = (source) => {
1165
+ for (const [name, packageRoot] of workspacePackages) {
1166
+ if (source === name)
1167
+ return { name, root: packageRoot, subpath: '.' };
1168
+ if (source.startsWith(`${name}/`)) {
1169
+ return {
1170
+ name,
1171
+ root: packageRoot,
1172
+ subpath: `.${source.slice(name.length)}`,
1173
+ };
1174
+ }
1175
+ }
1176
+ return null;
1177
+ };
842
1178
  const ensureSetupFiles = (value) => {
843
- const setupFiles = Array.isArray(value) ? [...value] : value ? [value] : [];
1179
+ // Vitest resolves `setupFiles` specifiers itself, outside `resolve.alias`,
1180
+ // so a workspace specifier such as `@happyvertical/smrt-vitest/svelte-setup`
1181
+ // would load the sibling's built dist (#2954). Map it to its source.
1182
+ const setupFiles = (Array.isArray(value) ? [...value] : value ? [value] : []).map((entry) => workspaceAliases.find((alias) => alias.find.test(entry))?.replacement ??
1183
+ entry);
844
1184
  if (!setupFiles.includes(setupFileId)) {
845
1185
  setupFiles.push(setupFileId);
846
1186
  }
@@ -880,6 +1220,43 @@ export function smrtVitestPlugin(options = {}) {
880
1220
  };
881
1221
  return {
882
1222
  name: 'smrt-vitest',
1223
+ enforce: 'pre',
1224
+ // A workspace specifier the static aliases did not rewrite — a pattern
1225
+ // export subpath, or one with no derivable source — is resolved here
1226
+ // (#2954). Pattern subpaths map to their source; anything still resolving
1227
+ // to a sibling's built JavaScript fails the test, so a stale or missing
1228
+ // dist cannot make a consumer suite pass. `enforce: 'pre'` places this
1229
+ // after Vite's alias step (aliased specifiers never reach it) but before
1230
+ // Vite's own resolver, which would otherwise resolve the exports map to
1231
+ // dist first. Code that deliberately imports a dist file by path (e.g. a
1232
+ // runtime loader under test) is not a specifier resolution and is left
1233
+ // alone.
1234
+ async resolveId(source, importer, options) {
1235
+ const workspace = distGuard
1236
+ ? matchWorkspacePackageSpecifier(source)
1237
+ : null;
1238
+ // Packages the consumer resolves to published entries on purpose keep
1239
+ // their exports-map resolution.
1240
+ if (!distGuard || !workspace || distExempt.has(workspace.name)) {
1241
+ return null;
1242
+ }
1243
+ if (workspace.subpath !== '.') {
1244
+ const patternSource = resolveWorkspaceExportSource(workspace.root, workspace.subpath);
1245
+ if (patternSource)
1246
+ return patternSource;
1247
+ }
1248
+ // The exports map decides where Node and Vite would take the
1249
+ // specifier; a built JavaScript target fails whether or not that file
1250
+ // currently exists (stale or missing dist alike).
1251
+ const target = workspaceExportTarget(workspace.root, workspace.subpath);
1252
+ if (target && /\.m?js$/.test(target)) {
1253
+ const failure = distGuard(join(normalizeRootKey(workspace.root), target));
1254
+ if (failure) {
1255
+ throw new Error(`${failure} (specifier: ${source})`);
1256
+ }
1257
+ }
1258
+ return null;
1259
+ },
883
1260
  config(userConfig) {
884
1261
  // Propagate manifest-registration options to every worker process this
885
1262
  // early — before Vitest spawns any pool worker — so ./setup.ts can
@@ -919,6 +1296,13 @@ export function smrtVitestPlugin(options = {}) {
919
1296
  // logging; a project needing distinct isolation in that shape should
920
1297
  // pass an explicit, distinct `root` to `smrtVitestPlugin()`.
921
1298
  setManifestRegistrationOptionsForRoot(root, { packages, root, verbose });
1299
+ // Publish this run's test manifest snapshot path just as early, for
1300
+ // the same reason: workers forked before `configResolved()` must
1301
+ // still read the run's own snapshot, never the production
1302
+ // `.smrt/manifest.json` a concurrent build may rewrite (#4128).
1303
+ if (generateManifest) {
1304
+ registerTestRunManifestPath(root, testRunManifestPath);
1305
+ }
922
1306
  const rootRetry = userConfig.test?.retry;
923
1307
  // Build tools may copy co-located tests into both output directories.
924
1308
  // Always retain Vitest defaults and consumer exclusions as well.
@@ -929,6 +1313,8 @@ export function smrtVitestPlugin(options = {}) {
929
1313
  ? userConfig.resolve
930
1314
  : undefined;
931
1315
  const alias = normalizeAliasEntries(resolveConfig?.alias);
1316
+ distExempt = packagesWithDeliberateDistEntries(root, droppedSpecifiers, alias);
1317
+ distGuard = createWorkspaceDistGuard(root, distExempt);
932
1318
  return {
933
1319
  // Vite 8 (rolldown/oxc) defaults — legacy @smrt() decorators and
934
1320
  // type-import elision (see resolveOxcDefaults). Fields the consumer
@@ -956,14 +1342,16 @@ export function smrtVitestPlugin(options = {}) {
956
1342
  };
957
1343
  },
958
1344
  // Run during config resolution to ensure manifests are loaded before tests
959
- async configResolved(resolvedConfig) {
1345
+ async configResolved() {
960
1346
  if (manifestsLoaded)
961
1347
  return;
962
1348
  // Step 1: Generate local manifest if enabled (default: true)
963
1349
  // This ensures manifest is always fresh after adding new classes/fields
964
1350
  if (generateManifest) {
965
- const producerApi = (resolvedConfig.plugins ?? []).find((plugin) => plugin?.name === 'smrt-auto-service')?.api;
966
- await generateLocalManifest(root, options, verbose, producerApi);
1351
+ // `config()` normally published this already; a caller driving
1352
+ // `configResolved()` alone (tests, custom harnesses) still gets it.
1353
+ registerTestRunManifestPath(root, testRunManifestPath);
1354
+ await generateLocalManifest(root, options, verbose, testRunManifestPath);
967
1355
  }
968
1356
  // Step 2: Load the local manifest so late-imported local classes are
969
1357
  // available to schema preparation before the first DB call.
@@ -1086,5 +1474,5 @@ export async function setupSmrtManifests(options = {}) {
1086
1474
  }
1087
1475
  export default smrtVitestPlugin;
1088
1476
  // Export test database utilities
1089
- export { createIsolatedTestDb, createIsolatedTestDbFromManifest, createTestDb, getAdapterDisplayName, getInMemoryDbConfig, getTestAdapter, getTestDbConfig, isPostgresAvailable, } from './test-db.js';
1477
+ export { createIsolatedTestDb, createIsolatedTestDbFromManifest, createTestDb, getAdapterDisplayName, getInMemoryDbConfig, getTestAdapter, getTestDbConfig, getTestManifestPath, isPostgresAvailable, } from './test-db.js';
1090
1478
  //# sourceMappingURL=index.js.map