package bundler import ( "os" "path/filepath" "strings" "testing" ) const fakeRuntime = ` (globalThis as Record).__sanderling__ = { extract: (g: () => unknown) => ({ current: g(), previous: undefined }), always: () => ({ __sanderlingFormula: true }), now: () => ({ __sanderlingFormula: true }), next: () => ({ __sanderlingFormula: true }), eventually: () => ({ __sanderlingFormula: true }), actions: (g: () => unknown) => ({ __sanderlingActionGenerator: true, generate: g }), weighted: () => ({ __sanderlingActionGenerator: true }), from: () => ({ generate: () => null }), tap: () => null, inputText: () => null, swipe: () => null, pressKey: () => null, wait: () => null, taps: {}, swipes: {}, waitOnce: {}, pressKeys: {}, }; (globalThis as Record).__sanderlingExtractors__ = () => ({}); (globalThis as Record).__sanderlingNextAction__ = () => null; export {}; ` const fakeSpec = ` const handle = (globalThis as { __sanderling__: { extract: (g: () => unknown) => unknown } }).__sanderling__.extract(() => 42); export const actionsRoot = handle; export const properties = "WEB_PROPS_MARKER"; ` func TestBundleWeb_RegistersExpectedGlobals(t *testing.T) { directory := t.TempDir() runtimePath := filepath.Join(directory, "web-runtime.ts") specPath := filepath.Join(directory, "spec.ts") if err := os.WriteFile(runtimePath, []byte(fakeRuntime), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(specPath, []byte(fakeSpec), 0o644); err != nil { t.Fatal(err) } result, err := BundleWeb(WebOptions{ EntryFile: specPath, WebRuntimeFile: runtimePath, }) if err != nil { t.Fatalf("BundleWeb: %v", err) } source := string(result.JavaScript) for _, expected := range []string{ "__sanderlingExtractors__", "__sanderlingNextAction__", "__sanderling__", "WEB_PROPS_MARKER", "globalThis.actions", } { if !strings.Contains(source, expected) { t.Errorf("bundle missing %q\nsource head:\n%s", expected, head(source, 500)) } } if !strings.HasPrefix(strings.TrimSpace(source), "(()") { t.Errorf("expected IIFE format, head:\n%s", head(source, 200)) } if result.SHA256 == "" { t.Errorf("expected non-empty sha256") } } func TestBundleWeb_RejectsMissingEntry(t *testing.T) { if _, err := BundleWeb(WebOptions{}); err == nil { t.Error("expected error for empty options") } if _, err := BundleWeb(WebOptions{EntryFile: "x.ts"}); err == nil { t.Error("expected error for missing runtime") } } // TestBundleWeb_IsDeterministic builds the same source repeatedly with multiple // Defines entries and asserts the SHA matches. This guards against any future // change that introduces map-iteration ordering into the bundle output. func TestBundleWeb_IsDeterministic(t *testing.T) { directory := t.TempDir() runtimePath := filepath.Join(directory, "web-runtime.ts") specPath := filepath.Join(directory, "spec.ts") if err := os.WriteFile(runtimePath, []byte(fakeRuntime), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(specPath, []byte(fakeSpec), 0o644); err != nil { t.Fatal(err) } options := WebOptions{ EntryFile: specPath, WebRuntimeFile: runtimePath, Defines: map[string]string{ "API_BASE": "https://example.com", "FEATURE": "true", "VERSION": "1.2.3", "TENANT": "alpha", }, } first, err := BundleWeb(options) if err != nil { t.Fatalf("first build: %v", err) } for attempt := range 10 { repeat, err := BundleWeb(options) if err != nil { t.Fatalf("attempt %d: %v", attempt, err) } if repeat.SHA256 != first.SHA256 { t.Fatalf("attempt %d: SHA changed (%s vs %s)", attempt, repeat.SHA256, first.SHA256) } } } // TestBundleWeb_InjectsSeedDefine asserts that a SANDERLING_SEED define is // inlined into the bundle so --seed reaches the web runtime PRNG. esbuild // replaces process.env.SANDERLING_SEED with the quoted literal at build time. func TestBundleWeb_InjectsSeedDefine(t *testing.T) { directory := t.TempDir() runtimePath := filepath.Join(directory, "web-runtime.ts") specPath := filepath.Join(directory, "spec.ts") runtime := fakeRuntime + "\n(globalThis as Record).__seed = process.env.SANDERLING_SEED;\n" if err := os.WriteFile(runtimePath, []byte(runtime), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(specPath, []byte(fakeSpec), 0o644); err != nil { t.Fatal(err) } result, err := BundleWeb(WebOptions{ EntryFile: specPath, WebRuntimeFile: runtimePath, Defines: map[string]string{"SANDERLING_SEED": "8675309"}, }) if err != nil { t.Fatalf("BundleWeb: %v", err) } source := string(result.JavaScript) if !strings.Contains(source, "8675309") { t.Errorf("seed literal not inlined into bundle\nsource head:\n%s", head(source, 500)) } if strings.Contains(source, "process.env.SANDERLING_SEED") { t.Errorf("define left unsubstituted in bundle") } } func head(text string, max int) string { if len(text) <= max { return text } return text[:max] + "..." }