diff --git a/internal/testrun/testrun.go b/internal/testrun/testrun.go index af57b28..893e5ce 100644 --- a/internal/testrun/testrun.go +++ b/internal/testrun/testrun.go @@ -350,16 +350,20 @@ func resolveRuntimeSibling(specAPIPath, userSpecPath, filename string) string { return "" } -// resolveSpecAPIPath returns the path to pkg/spec/src/index.ts inside -// a sanderling source checkout, searched upward from the spec file and the cwd. -// Returns "" when not found, in which case esbuild resolves @sanderling/spec via -// node_modules the way a downstream user's project would. +// resolveSpecAPIPath returns the path to the spec API's index.ts: a sanderling +// source checkout first, searched upward from the spec file and the cwd, then +// an installed node_modules/@sanderling/spec. Aliasing the installed copy is +// what keeps the spec and the runtime entry on one module graph; resolving the +// bare specifier through package.json "exports" would load dist/ alongside the +// runtime's src/ and give sampler-rng.ts two instances. func resolveSpecAPIPath(specPath string) string { - var candidates []string + var checkout, installed []string if absoluteSpec, err := filepath.Abs(specPath); err == nil { directory := filepath.Dir(absoluteSpec) for { - candidates = append(candidates, filepath.Join(directory, "pkg/spec/src/index.ts")) + checkout = append(checkout, filepath.Join(directory, "pkg/spec/src/index.ts")) + installed = append(installed, + filepath.Join(directory, "node_modules/@sanderling/spec/src/index.ts")) parent := filepath.Dir(directory) if parent == directory { break @@ -368,9 +372,9 @@ func resolveSpecAPIPath(specPath string) string { } } if cwd, err := os.Getwd(); err == nil { - candidates = append(candidates, filepath.Join(cwd, "pkg/spec/src/index.ts")) + checkout = append(checkout, filepath.Join(cwd, "pkg/spec/src/index.ts")) } - for _, candidate := range candidates { + for _, candidate := range append(checkout, installed...) { if _, err := os.Stat(candidate); err == nil { return candidate } diff --git a/internal/testrun/testrun_test.go b/internal/testrun/testrun_test.go index e8664ac..fd6303e 100644 --- a/internal/testrun/testrun_test.go +++ b/internal/testrun/testrun_test.go @@ -406,3 +406,53 @@ func TestResolveRuntimeSibling_PublishedPackageShipsTheRuntimes(t *testing.T) { } } } + +// TestPrepareBundleInputs_InstalledPackageSharesOneModuleGraph pins the +// downstream case: with no sanderling checkout above the spec, the aliases and +// the runtime entry must name the SAME installed copy. An unset alias let +// esbuild resolve @sanderling/spec to dist/ while the runtime came from src/, +// which loads sampler-rng.ts twice; from(), strings(), integers() and emails() +// then read an rng the picker never set and collapse to a fixed default. +func TestPrepareBundleInputs_InstalledPackageSharesOneModuleGraph(t *testing.T) { + root := t.TempDir() + installed := filepath.Join(root, "node_modules", "@sanderling", "spec") + installPublishedPackage(t, installed) + specPath := filepath.Join(root, "sanderling", "spec.ts") + if err := os.MkdirAll(filepath.Dir(specPath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(specPath, []byte(""), 0o644); err != nil { + t.Fatal(err) + } + + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Chdir(cwd) }) + if err := os.Chdir(root); err != nil { + t.Fatal(err) + } + + prep, err := prepareBundleInputs(Options{Spec: specPath}) + if err != nil { + t.Fatal(err) + } + source := filepath.Join(installed, "src") + want := map[string]string{ + "@sanderling/spec": filepath.Join(source, "index.ts"), + "@sanderling/spec/defaults": filepath.Join(source, "defaults/index.ts"), + "@sanderling/spec/defaults/properties": filepath.Join(source, "defaults/properties.ts"), + } + for key, wantValue := range want { + if prep.aliases[key] != wantValue { + t.Errorf("alias %q = %q, want %q", key, prep.aliases[key], wantValue) + } + } + if got := prep.gojaRuntimePath; got != filepath.Join(source, "goja-runtime.ts") { + t.Errorf("gojaRuntimePath = %q, want it beside the aliased index.ts", got) + } + if got := resolveWebRuntimePath(prep.specAPIPath, specPath); got != filepath.Join(source, "web-runtime.ts") { + t.Errorf("webRuntimePath = %q, want it beside the aliased index.ts", got) + } +}