package builder import ( "context" "os" "path/filepath" "strings" "testing" ) const aBundle = `{"module":"greeter","version":"1", "build":{"artifacts":[ {"name":"code","kind":"bundle","language":"typescript","entrypoints":["index.js"]}]}, "resources":[ {"id":"files","type":"archive","path":"/opt/greeter","artifact":"code"}]}` // compiling is a runner that behaves like a toolchain: when asked to compile, it leaves output // where the toolchain says output lands. Without this the pack step has nothing to pack, and the // test would be asserting on a failure rather than on a build. type compiling struct{ *recorded } func (c compiling) run(ctx context.Context, dir, name string, args ...string) (string, error) { out, err := c.recorded.run(ctx, dir, name, args...) if name != "docker" || len(args) == 0 || args[0] != "run" { return out, err } // **Writes where it was TOLD to**, rather than to a fixed directory. A fake that always wrote // to one place would pass whether or not the builder gave each artifact its own — which is the // thing being tested. where := "" for i, a := range args { if a == "--outDir" && i+1 < len(args) { where = args[i+1] } } if where == "" { return out, err } made := filepath.Join(dir, where) if err := os.MkdirAll(made, 0o755); err != nil { return "", err } if err := os.WriteFile(filepath.Join(made, "index.js"), []byte("console.log(1)"), 0o644); err != nil { return "", err } return out, err } // **A module says what it is written in, and needs no Dockerfile.** This is the whole point of the // bundle recipe: the same module previously needed a hand-written recipe repeating an incantation // that is easy to get wrong in ways that fail somewhere else. func TestABundleIsCompiledAndPackedWithNoDockerfile(t *testing.T) { r, workspace := aRepository(t, aBundle, map[string]string{"index.ts": "console.log(1)"}) held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)} got, err := Build(context.Background(), compiling{r}.run, r, "https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, GitCredential{}, nil) if err != nil { t.Fatalf("a module with a language and no Dockerfile did not build: %v", err) } // Compiled in the toolchain the mesh chose, not in one the module named. var compiled string for _, line := range r.ran { if strings.HasPrefix(line, "docker run") { compiled = line } } if compiled == "" { t.Fatalf("nothing was compiled:\n%s", strings.Join(r.ran, "\n")) } if !strings.Contains(compiled, "mesh-tools/build@sha256:") { t.Fatalf("the compile did not run in the mesh's own toolchain: %s", compiled) } if strings.Contains(strings.Join(r.ran, "\n"), "docker build") { t.Fatalf("a bundle invoked a Dockerfile build, which is the thing it exists to avoid:\n%s", strings.Join(r.ran, "\n")) } // And pinned by a digest of what came out, like any other artifact. digest, _ := got.Manifest.Resources[0]["digest"].(string) if !strings.HasPrefix(digest, "sha256:") { t.Fatalf("the bundle was not pinned: %v", got.Manifest.Resources[0]) } // **One file per entrypoint and launcher, in the toolchain's bundler** (novox/hq ADR 0193). A // second run in the same toolchain image bundles each into the artifact's bundled output, the SDK // inlined, refusing by name in an image that predates the bundler; and the toolchain's // node_modules is no longer copied into a bundle that keeps nothing external. var bundling []string for _, line := range r.ran { if strings.HasPrefix(line, "docker run") && strings.Contains(line, "esbuild") { bundling = append(bundling, line) } } if len(bundling) != 2 { t.Fatalf("want one bundling run for the entrypoints and one for the launchers:\n%s", strings.Join(r.ran, "\n")) } for _, want := range []string{"mesh-tools/build@sha256:", "predates one-file bundles", "--bundle", "--format=esm", "--platform=node", "--outdir=" + Out("code") + ".bundled", Out("code") + "/index.js"} { if !strings.Contains(bundling[0], want) { t.Errorf("the entrypoints' bundling lacks %q: %s", want, bundling[0]) } } if !strings.Contains(bundling[1], Out("code")+"/index.serve.mjs") || !strings.Contains(bundling[1], "--out-extension:.js=.mjs") { t.Errorf("the launcher is not bundled under its own name: %s", bundling[1]) } if strings.Contains(strings.Join(r.ran, "\n"), "/app/runtime") { t.Errorf("the toolchain's node_modules was copied into a bundle that keeps nothing external:\n%s", strings.Join(r.ran, "\n")) } if strings.Index(strings.Join(r.ran, "\n"), "--outDir") > strings.Index(strings.Join(r.ran, "\n"), "esbuild") { t.Fatal("the bundler ran before the compile wrote its output") } } // A bundle naming packages it keeps external is bundled with them as imports, and carries the // toolchain's node_modules for them — the one case it still does. func TestABundleKeepingAPackageExternalCarriesTheToolchainsModules(t *testing.T) { manifest := strings.Replace(aBundle, `"entrypoints":["index.js"]`, `"entrypoints":["index.js"],"external":["sharp"]`, 1) r, workspace := aRepository(t, manifest, map[string]string{"index.ts": "console.log(1)"}) held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)} if _, err := Build(context.Background(), compiling{r}.run, r, "https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, GitCredential{}, nil); err != nil { t.Fatal(err) } all := strings.Join(r.ran, "\n") if !strings.Contains(all, "--external:sharp") || !strings.Contains(all, "/app/runtime") { t.Errorf("an external package was not kept as an import with the toolchain's modules beside it:\n%s", all) } } // A language whose bundle carries its own dependencies copies nothing in: a Go binary is static. func TestOnlyALanguageWithARuntimeDirectoryCopiesDependenciesIn(t *testing.T) { ts, _ := ToolchainFor("typescript") if ts.Dependencies != "/app/runtime" { t.Fatalf("typescript bundles run with %q", ts.Dependencies) } for _, language := range []string{"go", "python"} { chain, _ := ToolchainFor(language) if chain.Dependencies != "" { t.Fatalf("%s copies %q into every bundle, and its bundles carry their own", language, chain.Dependencies) } } } // **Refused before anything is built, naming what to build first.** A base the mesh has not built // is not a compile that fails on its first line — it is a question somebody can answer, and saying // it early is the difference between a fixable message and one about a missing image. func TestABundleWhoseToolchainIsNotHeldIsRefusedFirst(t *testing.T) { r, workspace := aRepository(t, aBundle, map[string]string{"index.ts": "console.log(1)"}) _, err := Build(context.Background(), compiling{r}.run, r, "https://forge.invalid/greeter.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil) if err == nil { t.Fatal("a bundle was built with no toolchain to compile it in") } if !strings.Contains(err.Error(), "mesh-tools") { t.Fatalf("the refusal does not name what has to be built first: %v", err) } for _, line := range r.ran { if strings.HasPrefix(line, "docker run") { t.Fatalf("a compile was attempted before the refusal: %s", line) } } } // A language the mesh does not build is refused the same way, and names what it can build. func TestABundleInAnUnknownLanguageIsRefused(t *testing.T) { manifest := strings.Replace(aBundle, `"language":"typescript"`, `"language":"cobol"`, 1) r, workspace := aRepository(t, manifest, map[string]string{"index.ts": "x"}) _, err := Build(context.Background(), compiling{r}.run, r, "https://forge.invalid/greeter.git", "", "", workspace, map[string]string{"mesh-tools/build": "registry.invalid/x@sha256:" + strings.Repeat("c", 64)}, Npmrc{}, GitCredential{}, nil) if err == nil { t.Fatal("a language nothing can compile was accepted") } if !strings.Contains(err.Error(), "typescript") { t.Fatalf("the refusal does not say what would have worked: %v", err) } } // **One module, two bundles, and neither packs the other.** // // The case that matters for real modules: a module is one piece of software and may still carry a // daemon in one language and tools in another (ADR 0040). An earlier version of this compiled // every bundle into the toolchain's single output directory, so two of them would overwrite each // other and then be packed together — one artifact containing both, twice. func TestTwoBundlesInOneModuleArePackedSeparately(t *testing.T) { const two = `{"module":"greeter","version":"1", "build":{"artifacts":[ {"name":"daemon","kind":"bundle","language":"typescript","entrypoints":["index.js"]}, {"name":"tools","kind":"bundle","language":"typescript","entrypoints":["index.js"]}]}, "resources":[ {"id":"a","type":"archive","path":"/opt/greeter/daemon","artifact":"daemon"}, {"id":"b","type":"archive","path":"/opt/greeter/tools","artifact":"tools"}]}` r, workspace := aRepository(t, two, map[string]string{"index.ts": "console.log(1)"}) held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)} got, err := Build(context.Background(), compiling{r}.run, r, "https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, GitCredential{}, nil) if err != nil { t.Fatalf("a module with two bundles did not build: %v", err) } // Compiled into two different places. Only the compile lines: the copy of each bundle's // dependencies names the same directory again, deliberately. var outputs []string for _, line := range r.ran { if !strings.Contains(line, "--outDir") { continue } for _, part := range strings.Fields(line) { if strings.HasPrefix(part, ".mesh-build/") { outputs = append(outputs, part) } } } if len(outputs) != 2 || outputs[0] == outputs[1] { t.Fatalf("two bundles did not get their own output directories: %v", outputs) } // And published as two artifacts, each with its own digest. if len(r.archives) != 2 { t.Fatalf("expected two archives published, got %v", r.archives) } first, _ := got.Manifest.Resources[0]["digest"].(string) second, _ := got.Manifest.Resources[1]["digest"].(string) if first == "" || second == "" { t.Fatalf("a bundle was not pinned: %v", got.Manifest.Resources) } }