diff --git a/modules/mesh-build-node/cmd/mesh-build-node/fetch.go b/modules/mesh-build-node/cmd/mesh-build-node/fetch.go index 9ad1acb34..397da7046 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/fetch.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/fetch.go @@ -62,11 +62,11 @@ type FetchResult struct { // Programs are the changed files that are compiled programs (issue 1577); a merge answers it, a pointer to an empty list when there are none, absent for a fetch that made no merge. Programs *[]steprun.Program `json:"programs,omitempty"` Trees map[string]sdkstep.ModuleTree `json:"trees,omitempty"` - // Group is the group's package the fetch packed and served to the TypeScript members (the call's `package`): - // "@ ". + // Group are the group's packages the fetch packed and served to the TypeScript modules (the call's + // `group-packages`): "@ " each, joined by ", ", in the order of their names. Group string `json:"group,omitempty"` - // SDK is the version of @novox/mesh-sdk placed in each TypeScript module, by directory: the latest release, the - // group's tagged version or the group's package (placeSDK; ADR 0573 §2). + // SDK is the version of @novox/mesh-sdk placed in each TypeScript module, by directory: the latest release or the + // group's package (placeSDK; ADR 0573 §2, design 47 G A-284). SDK map[string]string `json:"sdk,omitempty"` } @@ -99,9 +99,11 @@ func (m *Module) fetchTool() StepTool { "ref": map[string]any{"type": "string", "description": "refs/pull//head when only a pull request's head carries the commit"}, "dir": sdkstep.FetchRequestProperties()["dir"], "paths": sdkstep.FetchRequestProperties()["paths"], - "package": map[string]any{"type": "object", "description": "optional: the group's tagged version of the SDK, {name, version}, placed from the registry in each TypeScript member whatever its lock names (ADR 0607 §3); or the group's package member, {repository, commit, ref} (ADR 0500 §5), packed here, publishing nothing, and served to each TypeScript member whose lock names its version, in place of the registry's", - "properties": map[string]any{"name": map[string]any{"type": "string"}, "version": map[string]any{"type": "string"}, - "repository": map[string]any{"type": "string"}, "commit": map[string]any{"type": "string"}, "ref": map[string]any{"type": "string"}}}, + "group-packages": map[string]any{"type": "array", "description": "optional, a group's check: the TypeScript packages the group's members change, each a member's {repository, commit, ref} with path, the package's directory in the member (empty for its root); each is packed here, publishing nothing, and placed by its package.json name in every module of the tree that names it (the SDK in every module), in place of the registry's (design 47 G A-284, A-285)", + "items": map[string]any{"type": "object", "properties": map[string]any{ + "repository": map[string]any{"type": "string"}, "commit": map[string]any{"type": "string"}, + "ref": map[string]any{"type": "string"}, "path": map[string]any{"type": "string"}, + }, "required": []string{"repository", "commit"}}}, "builds": map[string]any{"type": "array", "description": "optional: a group run's package members, each by its build's digest, read from the artifact store and laid as the package it names in each module's node_modules (design 47 G §465)", "items": map[string]any{"type": "object", "properties": map[string]any{ "package": map[string]any{"type": "string"}, "module": map[string]any{"type": "string"}, @@ -125,7 +127,7 @@ func (m *Module) fetch(ctx context.Context, args map[string]any) (res any, err e if err != nil { return nil, err } - group, err := groupPackageArg(args["package"]) + group, err := groupPackagesArg(args["group-packages"]) if err != nil { return nil, err } @@ -147,7 +149,7 @@ func (m *Module) fetch(ctx context.Context, args map[string]any) (res any, err e return m.fetchInto(ctx, req, group, id, buildsAsked) } -func (m *Module) fetchInto(ctx context.Context, req sdkstep.FetchRequest, group *groupPackage, id string, buildsAsked []sdkstep.FetchBuild) (any, error) { +func (m *Module) fetchInto(ctx context.Context, req sdkstep.FetchRequest, group []groupPackage, id string, buildsAsked []sdkstep.FetchBuild) (any, error) { repository, commit, ref, base := req.Repository, req.Commit, req.Ref, req.Base var err error root := filepath.Join(m.Work, id) @@ -181,15 +183,6 @@ func (m *Module) fetchInto(ctx context.Context, req sdkstep.FetchRequest, group return nil, err } } - // The group's package, packed here and served to its members in place of the registry's version (ADR 0500 §5). - var served *packed - if group != nil && !group.tagged() { - p, err := m.packGroup(ctx, root, gitRun, group) - if err != nil { - return nil, err - } - served, res.Group = &p, p.Name+"@"+p.Version+" "+p.Integrity - } // A group run's package members, each read from the artifact store by its build's digest (builds.go, §465). members, err := m.readBuilds(ctx, buildsAsked) if err != nil { @@ -199,6 +192,30 @@ func (m *Module) fetchInto(ctx context.Context, req sdkstep.FetchRequest, group if err != nil { return nil, err } + // The group's packages, each packed here when a module of the tree takes it, and placed in place of the registry's + // (design 47 G A-284, A-285; ADR 0500 §5): the SDK goes to every module that is no package, another package to every + // module whose package.json names it. + served, err := m.packGroups(ctx, root, gitRun, group, func(name string) bool { + for _, d := range dirs { + dir := filepath.Join(tree, filepath.FromSlash(d)) + if self, err := readPackageJSON(dir); err == nil && self.Name == name { + continue // the package itself, built here from the tree + } + if (name == sdkPackage && !packages[d]) || namedPackages(dir)[name] { + return true + } + } + return false + }) + if err != nil { + return nil, err + } + var said []string + for _, p := range served { + said = append(said, p.Name+"@"+p.Version+" "+p.Integrity) + } + sort.Strings(said) + res.Group = strings.Join(said, ", ") for _, d := range dirs { res.Modules = append(res.Modules, d) dir := filepath.Join(tree, filepath.FromSlash(d)) @@ -207,6 +224,9 @@ func (m *Module) fetchInto(ctx context.Context, req sdkstep.FetchRequest, group if err := m.installDev(ctx, root, dir, d); err != nil { return nil, err } + if err := placeGroup(dir, served); err != nil { + return nil, err + } if err := m.layBuilds(dir, members); err != nil { return nil, err } @@ -218,12 +238,16 @@ func (m *Module) fetchInto(ctx context.Context, req sdkstep.FetchRequest, group return nil, err } if refused == "" { - // The latest published SDK, or the group's (ADR 0573 §2, ADR 0607 §3); the SDK as a member's build is laid from it. + // The latest published SDK, or the group's (ADR 0573 §2, design 47 G A-284); the SDK as a member's build is laid + // from it. if !namesPackage(members, sdkPackage) { - if err := m.placeSDK(ctx, dir, d, served, group); err != nil { + if err := m.placeSDK(ctx, dir, d, served[sdkPackage]); err != nil { return nil, err } } + if err := placeGroup(dir, served); err != nil { + return nil, err + } if err := m.layBuilds(dir, members); err != nil { return nil, err } diff --git a/modules/mesh-build-node/cmd/mesh-build-node/package.go b/modules/mesh-build-node/cmd/mesh-build-node/package.go index 562ec862f..f55341aa3 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/package.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/package.go @@ -1,12 +1,12 @@ package main -// A TypeScript package (novox/hq ADR 0498 §5, ADR 0500 §4-§6; issue 1587, the lead's decision L10): built from its own -// lock's devDependencies (the toolchain's SDK is never used to build the SDK itself), packed with npm pack, and, for a -// group, served to its TypeScript members' fetch in place of the registry's version. A member is built against the SDK -// version its own lock names: the fetch places that version in the member's node_modules, from the group's packed -// package when the group carries that version, else from the registry; a version neither holds waits for its -// publication (`waits for:`). The install of a package's devDependencies is the fetch's (it holds the network and runs -// no script); its build and pack run shut off like every run step. +// A TypeScript package (novox/hq ADR 0498 §5, ADR 0500 §4-§6; issue 1587): built from its own lock's devDependencies +// (the toolchain's SDK is never used to build the SDK itself), packed with npm pack, and, in a group's check, served to +// the fetch of every member: each package the group changes is packed and placed by its package.json name in every +// module that names it (the SDK in every module), in place of the registry's (design 47 G A-284, A-285); a package the +// group does not change comes from the registry at its latest published version (ADR 0573 §2), the SDK's a wait +// (`waits for:`) while the registry holds none. The install of a package's devDependencies is the fetch's (it holds the +// network and runs no script); its build and pack run shut off like every run step. import ( "archive/tar" @@ -227,69 +227,141 @@ func (m *Module) buildAndPack(ctx context.Context, root, dir, rel string, write return packed{Name: p.Name, Version: p.Version, Integrity: integrityOf(tgz), Digest: digestOf(tgz), tgz: tgz}, nil } -// groupPackage is the fetch's `package` argument: the group's package member (ADR 0500 §5), which the fetch packs and -// serves to its TypeScript members in place of the registry's version, or, by Name and Version, the group's tagged -// version of the SDK, which the fetch places from the registry in each TypeScript member, whatever version its lock -// names (novox/hq ADR 0607 §3, §5; design 51 *Tagged versions*): nothing is packed then. The packed member is the old -// form, kept because the running checker hands it until the checker that hands the tagged version is delivered; its -// removal is a subtask of the milestone after that delivery (issue 1966). -type groupPackage struct{ Repository, Commit, Ref, Name, Version string } +// groupPackage is one entry of the fetch's `group-packages` argument (design 47 G A-284, A-285; ADR 0500 §5): a +// TypeScript package a member of the group changes, at that member's commit, with Path its directory in the member ("" +// for the member's root). The fetch packs it, publishing nothing, and places it by its package.json name in every module +// of the tree that names it, in place of the registry's. +type groupPackage struct{ Repository, Commit, Ref, Path string } -// tagged says whether the argument names a tagged version to take from the registry, not a member to pack. -func (g *groupPackage) tagged() bool { return g != nil && g.Version != "" } - -func groupPackageArg(v any) (*groupPackage, error) { +// groupPackagesArg reads the fetch's `group-packages`: a list of {repository, commit, ref, path}; anything else is +// refused before anything is fetched. +func groupPackagesArg(v any) ([]groupPackage, error) { if v == nil { return nil, nil } - o, ok := v.(map[string]any) + list, ok := v.([]any) if !ok { - return nil, fmt.Errorf("package is {repository, commit, ref} or {name, version}") + return nil, fmt.Errorf("group-packages is a list of {repository, commit, ref, path}") } - g := &groupPackage{} - g.Name, _ = o["name"].(string) - g.Version, _ = o["version"].(string) - if g.Name != "" || g.Version != "" { - if g.Name != sdkPackage { - return nil, fmt.Errorf("package: %q is not %s, the one package the fetch places in a member", g.Name, sdkPackage) + var out []groupPackage + for _, e := range list { + o, ok := e.(map[string]any) + if !ok { + return nil, fmt.Errorf("group-packages: %v is not {repository, commit, ref, path}", e) } - if !npmVersionOK.MatchString(g.Version) { - return nil, fmt.Errorf("package: %q is not a version (0.1.15-feat-x.2)", g.Version) + var g groupPackage + g.Repository, _ = o["repository"].(string) + g.Commit, _ = o["commit"].(string) + g.Ref, _ = o["ref"].(string) + g.Path, _ = o["path"].(string) + if !repositoryOK.MatchString(g.Repository) || strings.Contains(g.Repository, "..") { + return nil, fmt.Errorf("group-packages: %q is not a repository, /", g.Repository) } - return g, nil + if !commitOK.MatchString(g.Commit) { + return nil, fmt.Errorf("group-packages: %q is not a full commit (40 hex digits)", g.Commit) + } + if g.Ref != "" && !strings.HasPrefix(g.Ref, "refs/pull/") { + return nil, fmt.Errorf("group-packages: %q is not a pull request's head", g.Ref) + } + if g.Path != "" { + c := path.Clean(g.Path) + if path.IsAbs(c) || c == ".." || strings.HasPrefix(c, "../") || strings.Contains(g.Path, "\\") { + return nil, fmt.Errorf("group-packages: %q is not a directory inside the member", g.Path) + } + if c == "." { + c = "" + } + g.Path = c + } + out = append(out, g) } - g.Repository, _ = o["repository"].(string) - g.Commit, _ = o["commit"].(string) - g.Ref, _ = o["ref"].(string) - if !repositoryOK.MatchString(g.Repository) || strings.Contains(g.Repository, "..") { - return nil, fmt.Errorf("package: %q is not a repository, /", g.Repository) - } - if !commitOK.MatchString(g.Commit) { - return nil, fmt.Errorf("package: %q is not a full commit (40 hex digits)", g.Commit) - } - if g.Ref != "" && !strings.HasPrefix(g.Ref, "refs/pull/") { - return nil, fmt.Errorf("package: %q is not a pull request's head", g.Ref) - } - return g, nil + return out, nil } -// npmVersionOK is a semantic version as npm names one, with an optional pre-release (a tagged version's). +// namedPackages are the packages a module's package.json names in any of its dependency lists; none without one. +func namedPackages(dir string) map[string]bool { + raw, err := os.ReadFile(filepath.Join(dir, "package.json")) + if err != nil { + return nil + } + var p map[string]any + if json.Unmarshal(raw, &p) != nil { + return nil + } + out := map[string]bool{} + for _, k := range []string{"dependencies", "devDependencies", "peerDependencies", "optionalDependencies"} { + deps, _ := p[k].(map[string]any) + for name := range deps { + out[name] = true + } + } + return out +} + +// packGroups fetches each of the group's packages into the workspace, once per member commit, and, when a module of the +// tree takes it (wanted, by its package.json name), installs its dependencies, builds and packs it, publishing nothing +// (ADR 0500 §4, §5). A package no module takes is not built. Answers the packed packages by name. +func (m *Module) packGroups(ctx context.Context, root string, run forgekey.Runner, gs []groupPackage, wanted func(name string) bool) (map[string]*packed, error) { + served := map[string]*packed{} + trees := map[string]string{} + for i, g := range gs { + key := g.Repository + "@" + g.Commit + tree, ok := trees[key] + if !ok { + tree = filepath.Join(root, "group", strconv.Itoa(len(trees)), "tree") + if err := os.MkdirAll(tree, 0o700); err != nil { + return nil, err + } + if err := fetchCommit(ctx, run, tree, m.remote(g.Repository), g.Commit, g.Ref, m); err != nil { + return nil, fmt.Errorf("the group's package %s at %s: %w", g.Repository, g.Commit, err) + } + trees[key] = tree + } + dir := filepath.Join(tree, filepath.FromSlash(g.Path)) + p, err := readPackageJSON(dir) + if err != nil { + return nil, fmt.Errorf("the group's package %s %q: %w", g.Repository, g.Path, err) + } + if served[p.Name] != nil { + return nil, fmt.Errorf("the group names two packages %s", p.Name) + } + if !wanted(p.Name) { + continue + } + rel := "group-" + strconv.Itoa(i) + if err := m.installDev(ctx, root, dir, rel); err != nil { + return nil, err + } + pk, err := m.buildAndPack(ctx, root, dir, rel, false, "") + if err != nil { + return nil, err + } + served[pk.Name] = &pk + } + return served, nil +} + +// npmVersionOK is a semantic version as npm names one, with an optional pre-release. var npmVersionOK = regexp.MustCompile(`^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$`) -// packGroup fetches the group's package member into the workspace, installs its dependencies, builds and packs it, -// publishing nothing (ADR 0500 §4). -func (m *Module) packGroup(ctx context.Context, root string, run forgekey.Runner, g *groupPackage) (packed, error) { - dir := filepath.Join(root, "package", "tree") - if err := os.MkdirAll(dir, 0o700); err != nil { - return packed{}, err +// placeGroup places in the module's node_modules each of the group's packed packages its package.json names, the SDK +// aside (placeSDK places it), in place of whatever the install put there. +func placeGroup(dir string, served map[string]*packed) error { + named := namedPackages(dir) + self, _ := readPackageJSON(dir) + for name, p := range served { + if name == sdkPackage || !named[name] || name == self.Name { + continue + } + into := filepath.Join(dir, "node_modules", filepath.FromSlash(name)) + if err := os.RemoveAll(into); err != nil { + return err + } + if err := unpackPackage(p.tgz, into); err != nil { + return err + } } - if err := fetchCommit(ctx, run, dir, m.remote(g.Repository), g.Commit, g.Ref, m); err != nil { - return packed{}, fmt.Errorf("the group's package %s: %w", g.Repository, err) - } - if err := m.installDev(ctx, root, dir, "package"); err != nil { - return packed{}, err - } - return m.buildAndPack(ctx, root, dir, "package", false, "") + return nil } // sdkLocked is what a module's lock names of the SDK: its version, its integrity and where it was resolved from; ok is @@ -320,56 +392,28 @@ func sdkLocked(dir string) (version, integrity string, ok bool, err error) { // placeSDK places in the module's node_modules the SDK it is built against: the latest release the registry holds, // whatever its lock names (novox/hq ADR 0573 §2: a module builds against the latest published version of each mesh -// package; the lead's decision L10's version from the lock is gone), or the group's packed package when the lock names -// its version. Every TypeScript module gets it, named in its package.json or not, since its launcher imports the SDK -// (writeLaunchers) and the toolchain holds none. The lock's integrity is checked when the lock names the version placed; -// a registry holding no release is a wait (`waits for:`) when no group package was given, and a failure naming the group -// when one was. Given the group's tagged version (tagged, ADR 0607 §3), that version is placed from the registry -// whatever the lock names, its integrity the lock's when the lock names it, else the registry's; one the registry lacks -// fails naming it. -func (m *Module) placeSDK(ctx context.Context, dir, rel string, group *packed, tagged *groupPackage) error { +// package; the lead's decision L10's version from the lock is gone), or, when the group changes the SDK, the group's +// packed package in place of the registry's, whatever the lock names (design 47 G A-284, A-285). Every TypeScript module +// gets it, named in its package.json or not, since its launcher imports the SDK (writeLaunchers) and the toolchain holds +// none. The lock's integrity is checked when the lock names the registry's version placed; a registry holding no +// release is a wait (`waits for:`). +func (m *Module) placeSDK(ctx context.Context, dir, rel string, group *packed) error { version, integrity, _, err := sdkLocked(dir) if err != nil { return err } var tgz []byte - switch { - case tagged.tagged(): - if version != tagged.Version { - if m.Say != nil && version != "" { - m.Say("%s: its lock names %s@%s; the group's tagged version %s is placed (ADR 0607 §3)", rel, sdkPackage, version, tagged.Version) - } - version, integrity = tagged.Version, "" - } - want := sdkPackage + "@" + version - got, registered, held, err := m.registryPackageChecked(ctx, sdkPackage, version) - if err != nil { - return err - } - if !held { - return fmt.Errorf("the registry holds no %s, the group's tagged version, so %s cannot be built against it", want, rel) - } - if integrity == "" { - integrity = registered - } - if integrity != "" && integrityOf(got) != integrity { - return fmt.Errorf("the registry's %s is %s, and %s names %s", want, integrityOf(got), rel, integrity) - } - tgz = got - case group != nil && group.Name == sdkPackage && version != "" && group.Version == version: - if integrity != "" && integrity != group.Integrity { - return fmt.Errorf("the lock of %s names %s with the integrity %s, and the group's package packs to %s", rel, sdkPackage+"@"+version, integrity, group.Integrity) + if group != nil { + if m.Say != nil && version != "" && version != group.Version { + m.Say("%s: its lock names %s@%s; the group's %s is placed (design 47 G A-284)", rel, sdkPackage, version, group.Version) } tgz = group.tgz - default: + } else { latest, err := m.registryLatest(ctx, sdkPackage) if err != nil { return err } if latest == "" { - if group != nil { - return fmt.Errorf("the registry holds no release of %s and the group's package is %s@%s, which the lock of %s does not name", sdkPackage, group.Name, group.Version, rel) - } return fmt.Errorf("%s%s", builds.WaitsFor, sdkPackage) } if version != latest { diff --git a/modules/mesh-build-node/cmd/mesh-build-node/package_integration_test.go b/modules/mesh-build-node/cmd/mesh-build-node/package_integration_test.go index 2f9cc78c9..5c59bb82b 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/package_integration_test.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/package_integration_test.go @@ -4,7 +4,6 @@ package main import ( "context" - "encoding/json" "os" "os/exec" "path/filepath" @@ -41,10 +40,11 @@ func forgeRepo(t *testing.T, root, name string, files map[string]string) string return git(t, work, "rev-parse", "HEAD") } -// ADR 0500 §5 with the lead's decision L10, on real git, npm and bubblewrap: the fetch packs the group's package member -// (its build script and npm pack shut off, nothing published), and a member whose lock names that version with that -// integrity gets it in its node_modules; a member whose lock names it with another integrity fails naming both. -func TestTheFetchPacksTheGroupsPackageAndServesItToAMember(t *testing.T) { +// design 47 G A-284, A-285 (ADR 0500 §5), on real git, npm and bubblewrap, try 2's case: a group changes the SDK (at +// its member's root) and adds a contracts package of another member (at its directory there); the fetch of a third +// member packs both (their build scripts and npm pack shut off, nothing published) and places each by its name in the +// module that names it, the SDK whatever its lock names; a package no module names is not built. +func TestTheFetchPacksTheGroupsPackagesAndPlacesThemByName(t *testing.T) { needBubblewrap(t) for _, tool := range []string{"node", "npm", "git"} { if _, err := exec.LookPath(tool); err != nil { @@ -53,45 +53,52 @@ func TestTheFetchPacksTheGroupsPackageAndServesItToAMember(t *testing.T) { } root := t.TempDir() sdkCommit := forgeRepo(t, root, "sdk", sdkFixture) + contracts := func(name string) map[string]string { + dir := "modules/c/ts/seats/" + name + "/" + pkg := "@novox/mesh-delivery-contracts-" + name + return map[string]string{ + dir + "package.json": `{"name":"` + pkg + `","version":"0.1.0","type":"module","files":["dist"],"scripts":{"build":"node build.mjs"}}`, + dir + "package-lock.json": `{"name":"` + pkg + `","version":"0.1.0","lockfileVersion":3,"requires":true,"packages":{"":{"name":"` + pkg + `","version":"0.1.0"}}}`, + dir + "build.mjs": "import fs from 'node:fs';\nfs.mkdirSync('dist', {recursive: true});\nfs.writeFileSync('dist/index.js', 'export const seat = \"" + name + "\";\\n');\n", + } + } + files := contracts("git") + for n, c := range contracts("unnamed") { + files[n] = c + } + deliveryCommit := forgeRepo(t, root, "delivery", files) m, _, _ := newMachine(t) m.Run = forgekey.ExecRunner m.RemoteBase = "file://" + filepath.Join(root, "forge") + "/" m.FetchOptions = []steprun.Option{steprun.AllowFileRemote()} - fakeRegistry(t, m, nil) // the registry holds no 9.9.9: only the group can serve it - // What the group's member packs to, as the member's lock would name it: packed once here the way the fetch packs it. - probe, err := m.fetch(context.Background(), map[string]any{"repository": "o/sdk", "commit": sdkCommit, - "package": map[string]any{"repository": "o/sdk", "commit": sdkCommit}}) + fakeRegistry(t, m, nil) // the registry holds neither: only the group can serve them + member := typeScriptModule(`{"name":"m","dependencies":{"@novox/mesh-sdk":"^0.1.19","@novox/mesh-delivery-contracts-git":"^0.1.0"}}`) + member["modules/m/package-lock.json"] = `{"name":"m","lockfileVersion":3,"requires":true,"packages":{"":{"name":"m"},"node_modules/@novox/mesh-sdk":{"version":"0.1.19","integrity":"sha512-old"}}}` + commit := forgeRepo(t, root, "catalog", member) + out, err := m.fetch(context.Background(), map[string]any{"repository": "o/catalog", "commit": commit, "group-packages": []any{ + map[string]any{"repository": "o/sdk", "commit": sdkCommit}, + map[string]any{"repository": "o/delivery", "commit": deliveryCommit, "path": "modules/c/ts/seats/git"}, + map[string]any{"repository": "o/delivery", "commit": deliveryCommit, "path": "modules/c/ts/seats/unnamed"}, + }}) if err != nil { t.Fatal(err) } - group := strings.Fields(probe.(FetchResult).Group) - if len(group) != 2 || group[0] != "@novox/mesh-sdk@9.9.9" || !strings.HasPrefix(group[1], "sha512-") { - t.Fatalf("the fetch answered the group %q", probe.(FetchResult).Group) + res := out.(FetchResult) + group := strings.Split(res.Group, ", ") + if len(group) != 2 || !strings.HasPrefix(group[0], "@novox/mesh-delivery-contracts-git@0.1.0 sha512-") || !strings.HasPrefix(group[1], "@novox/mesh-sdk@9.9.9 sha512-") { + t.Errorf("the fetch answered the group %q, want the two packages a module names", res.Group) } - member := func(integrity string) string { - lock, _ := json.Marshal(map[string]any{"name": "m", "lockfileVersion": 3, "requires": true, "packages": map[string]any{ - "": map[string]any{"name": "m", "dependencies": map[string]any{"@novox/mesh-sdk": "9.9.9"}}, - "node_modules/@novox/mesh-sdk": map[string]any{"version": "9.9.9", "integrity": integrity}}}) - files := typeScriptModule(`{"name":"m","dependencies":{"@novox/mesh-sdk":"9.9.9"}}`) - files["modules/m/package-lock.json"] = string(lock) - return forgeRepo(t, root, "m-"+integrity[len(integrity)-6:len(integrity)-2], files) + nm := filepath.Join(m.Work, res.Workspace, "tree", "modules", "m", "node_modules", "@novox") + for pkg, want := range map[string]string{"mesh-sdk": "9.9.9", "mesh-delivery-contracts-git": `"git"`} { + if raw, err := os.ReadFile(filepath.Join(nm, pkg, "dist", "index.js")); err != nil || !strings.Contains(string(raw), want) { + t.Errorf("the module's %s is not the group's built package: %q %v", pkg, raw, err) + } } - commit := member(group[1]) - name := "m-" + group[1][len(group[1])-6:len(group[1])-2] - out, err := m.fetch(context.Background(), map[string]any{"repository": "o/" + name, "commit": commit, - "package": map[string]any{"repository": "o/sdk", "commit": sdkCommit}}) - if err != nil { - t.Fatalf("a member locking the group's version: %v", err) + if _, err := os.Stat(filepath.Join(nm, "mesh-delivery-contracts-unnamed")); err == nil { + t.Error("a package no module names was placed") } - placed := filepath.Join(m.Work, out.(FetchResult).Workspace, "tree", "modules", "m", "node_modules", "@novox", "mesh-sdk") - if raw, err := os.ReadFile(filepath.Join(placed, "dist", "index.js")); err != nil || !strings.Contains(string(raw), "9.9.9") { - t.Errorf("the member's SDK is not the group's built package: %q %v", raw, err) - } - other := member("sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") - _, err = m.fetch(context.Background(), map[string]any{"repository": "o/m-AAAA", "commit": other, - "package": map[string]any{"repository": "o/sdk", "commit": sdkCommit}}) - if err == nil || !strings.Contains(err.Error(), group[1]) { - t.Errorf("a member locking another integrity answered %v", err) + if res.SDK["modules/m"] != "9.9.9" { + t.Errorf("the SDK placed is %v, want the group's 9.9.9", res.SDK) } } diff --git a/modules/mesh-build-node/cmd/mesh-build-node/package_test.go b/modules/mesh-build-node/cmd/mesh-build-node/package_test.go index 56087e935..4eefea9fe 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/package_test.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/package_test.go @@ -108,7 +108,7 @@ func TestAMemberGetsTheLatestPublishedSDKWhateverItsLockNames(t *testing.T) { {name: "a lock naming the latest with another integrity", locks: "0.1.15", integrity: "sha512-another", fails: "sha512-another"}, } { dir := memberLocking(t, c.locks, c.integrity) - err := m.placeSDK(context.Background(), dir, "modules/m", nil, nil) + err := m.placeSDK(context.Background(), dir, "modules/m", nil) switch { case c.fails == "" && (err != nil || placedVersion(dir) != "0.1.15"): t.Errorf("%s: %v, placed %q, want the latest release 0.1.15", c.name, err, placedVersion(dir)) @@ -117,89 +117,69 @@ func TestAMemberGetsTheLatestPublishedSDKWhateverItsLockNames(t *testing.T) { } } fakeRegistry(t, m, map[string][]byte{"0.1.16-feat-x.1": pre}) - if err := m.placeSDK(context.Background(), memberLocking(t, "0.1.15", ""), "modules/m", nil, nil); err == nil || err.Error() != builds.WaitsFor+"@novox/mesh-sdk" { + if err := m.placeSDK(context.Background(), memberLocking(t, "0.1.15", ""), "modules/m", nil); err == nil || err.Error() != builds.WaitsFor+"@novox/mesh-sdk" { t.Errorf("a registry with no release answered %v, want %s@novox/mesh-sdk", err, builds.WaitsFor) } - held = tgzOf(t, sdkPackage, "0.1.15") - fakeRegistry(t, m, map[string][]byte{"0.1.15": held}) - var dir string - // ADR 0607 §3, §5 (design 51 *Tagged versions*): given the group's tagged version, the fetch places it from the - // registry whatever the lock names, against the lock's integrity when the lock names it, else the registry's; one the - // registry lacks fails naming it, no wait (the check waited for its published). - tagged := pre - fakeRegistry(t, m, map[string][]byte{"0.1.15": held, "0.1.16-feat-x.1": tagged}) - group := &groupPackage{Name: sdkPackage, Version: "0.1.16-feat-x.1"} - for _, c := range []struct { - name, locks, integrity, fails string - }{ - {name: "a lock naming another version", locks: "0.1.15", integrity: integrityOf(held)}, - {name: "a lock naming the tagged version", locks: "0.1.16-feat-x.1", integrity: integrityOf(tagged)}, - {name: "a lock naming it with another integrity", locks: "0.1.16-feat-x.1", integrity: "sha512-other", fails: "sha512-other"}, - } { - dir = memberLocking(t, c.locks, c.integrity) - err := m.placeSDK(context.Background(), dir, "modules/m", nil, group) - switch { - case c.fails == "" && (err != nil || placedVersion(dir) != "0.1.16-feat-x.1"): - t.Errorf("%s: %v, placed %q, want the group's tagged version", c.name, err, placedVersion(dir)) - case c.fails != "" && (err == nil || !strings.Contains(err.Error(), c.fails)): - t.Errorf("%s: %v, want it to fail naming %s", c.name, err, c.fails) - } - } - dir = memberLocking(t, "0.1.15", integrityOf(held)) - missing := &groupPackage{Name: sdkPackage, Version: "0.1.16-feat-x.9"} - if err := m.placeSDK(context.Background(), dir, "modules/m", nil, missing); err == nil || !strings.Contains(err.Error(), "0.1.16-feat-x.9") || strings.HasPrefix(err.Error(), builds.WaitsFor) { - t.Errorf("a tagged version the registry lacks answered %v, want it named and no wait", err) - } } -// ADR 0500 §5 (S3, S4): the group's packed package is served in place of the registry's when the member's lock names its -// version with its integrity; another integrity fails naming both. A member locking another version, or none, gets the -// registry's latest release (ADR 0573 §2); with no release in the registry it fails naming the group's, no wait. -func TestTheGroupsPackageIsServedToAMemberLockingItsVersion(t *testing.T) { +// design 47 G A-284, A-285 (ADR 0500 §5): the group's packed SDK is placed in place of the registry's whatever the +// member's lock names, its version, its integrity or none, and the registry is not asked. +func TestTheGroupsSDKIsPlacedInPlaceOfTheRegistrys(t *testing.T) { m, _, _ := newMachine(t) asked := fakeRegistry(t, m, map[string][]byte{"0.1.14": tgzOf(t, sdkPackage, "0.1.14")}) tgz := tgzOf(t, sdkPackage, "0.1.15") group := &packed{Name: sdkPackage, Version: "0.1.15", Integrity: integrityOf(tgz), Digest: digestOf(tgz), tgz: tgz} - dir := memberLocking(t, "0.1.15", group.Integrity) - if err := m.placeSDK(context.Background(), dir, "modules/m", group, nil); err != nil || placedVersion(dir) != "0.1.15" || asked.Load() != 0 { - t.Errorf("the group's own version: %v, placed %q, the registry asked %d times", err, placedVersion(dir), asked.Load()) - } - dir = memberLocking(t, "0.1.15", "sha512-other") - if err := m.placeSDK(context.Background(), dir, "modules/m", group, nil); err == nil || !strings.Contains(err.Error(), "sha512-other") || !strings.Contains(err.Error(), group.Integrity) { - t.Errorf("another integrity answered %v, want both named", err) - } - dir = memberLocking(t, "0.1.14", integrityOf(tgzOf(t, sdkPackage, "0.1.14"))) - if err := m.placeSDK(context.Background(), dir, "modules/m", group, nil); err != nil || placedVersion(dir) != "0.1.14" { - t.Errorf("a member locking the registry's 0.1.14 in a group: %v, placed %q", err, placedVersion(dir)) - } - for _, locks := range []string{"0.1.13", ""} { - dir = memberLocking(t, locks, "sha512-x") - if err := m.placeSDK(context.Background(), dir, "modules/m", group, nil); err != nil || placedVersion(dir) != "0.1.14" { - t.Errorf("a member in a group locking %q: %v, placed %q, want the registry's latest 0.1.14", locks, err, placedVersion(dir)) + for _, lock := range [][2]string{{"0.1.15", group.Integrity}, {"0.1.15", "sha512-other"}, {"0.1.14", integrityOf(tgzOf(t, sdkPackage, "0.1.14"))}, {"0.1.13", "sha512-x"}, {"", ""}} { + dir := memberLocking(t, lock[0], lock[1]) + if err := m.placeSDK(context.Background(), dir, "modules/m", group); err != nil || placedVersion(dir) != "0.1.15" { + t.Errorf("a member locking %q %q: %v, placed %q, want the group's 0.1.15", lock[0], lock[1], err, placedVersion(dir)) } } - fakeRegistry(t, m, nil) - if err := m.placeSDK(context.Background(), memberLocking(t, "0.1.13", "sha512-x"), "modules/m", group, nil); err == nil || !strings.Contains(err.Error(), "0.1.15") || strings.HasPrefix(err.Error(), builds.WaitsFor) { - t.Errorf("no release and a group answered %v, want the group's named and no wait", err) + if asked.Load() != 0 { + t.Errorf("the registry was asked %d times", asked.Load()) } } -// The fetch's `package` argument is a repository and a full commit, with a pull request's head at most; anything else is -// refused before anything is fetched. -func TestTheGroupPackageArgumentIsRefusedWhenItIsNotOne(t *testing.T) { - for _, bad := range []any{"novox/mesh-sdk", map[string]any{"repository": "novox/mesh-sdk", "commit": "main"}, - map[string]any{"repository": "../x", "commit": strings.Repeat("a", 40)}, - map[string]any{"repository": "novox/mesh-sdk", "commit": strings.Repeat("a", 40), "ref": "refs/heads/main"}, - map[string]any{"name": "left-pad", "version": "1.0.0"}, map[string]any{"name": sdkPackage, "version": "latest"}} { - if _, err := groupPackageArg(bad); err == nil { +// design 47 G A-285: each of the group's packages is placed by its package.json name in a module that names it, in +// place of what the install put there; one it does not name, the module itself and the SDK (placeSDK's) are not. +func TestTheGroupsPackagesArePlacedInEveryModuleThatNamesThem(t *testing.T) { + served := map[string]*packed{} + for _, name := range []string{sdkPackage, "@novox/mesh-delivery-contracts-git", "@novox/mesh-delivery-contracts-mesh-catalog", "@novox/mesh-contracts-x", "@novox/module-m"} { + served[name] = &packed{Name: name, Version: "0.2.0", tgz: tgzOf(t, name, "0.2.0")} + } + dir := t.TempDir() + write(t, filepath.Join(dir, "package.json"), `{"name":"@novox/module-m","version":"0.1.0","dependencies":{"@novox/mesh-delivery-contracts-git":"^0.1.0","@novox/mesh-sdk":"^0.1.19","@novox/module-m":"0.1.0"},"devDependencies":{"@novox/mesh-delivery-contracts-mesh-catalog":"^0.1.0"}}`, 0o644) + write(t, filepath.Join(dir, "node_modules", "@novox", "mesh-delivery-contracts-git", "package.json"), `{"name":"@novox/mesh-delivery-contracts-git","version":"0.1.0"}`, 0o644) + if err := placeGroup(dir, served); err != nil { + t.Fatal(err) + } + for name, want := range map[string]string{"@novox/mesh-delivery-contracts-git": "0.2.0", "@novox/mesh-delivery-contracts-mesh-catalog": "0.2.0", + "@novox/mesh-contracts-x": "", sdkPackage: "", "@novox/module-m": ""} { + p, _ := readPackageJSON(filepath.Join(dir, "node_modules", filepath.FromSlash(name))) + if p.Version != want { + t.Errorf("%s placed at %q, want %q", name, p.Version, want) + } + } +} + +// The fetch's `group-packages` is a list of a repository and a full commit, with a pull request's head at most and a +// directory inside the member; anything else is refused before anything is fetched. +func TestTheGroupPackagesArgumentIsRefusedWhenItIsNotOne(t *testing.T) { + full := strings.Repeat("a", 40) + for _, bad := range []any{"novox/mesh-sdk", map[string]any{"repository": "novox/mesh-sdk", "commit": full}, + []any{map[string]any{"repository": "novox/mesh-sdk", "commit": "main"}}, + []any{map[string]any{"repository": "../x", "commit": full}}, + []any{map[string]any{"repository": "novox/mesh-sdk", "commit": full, "ref": "refs/heads/main"}}, + []any{map[string]any{"repository": "novox/mesh-delivery", "commit": full, "path": "../x"}}, + []any{map[string]any{"repository": "novox/mesh-delivery", "commit": full, "path": "/abs"}}, + []any{map[string]any{"name": sdkPackage, "version": "0.1.16-feat-x.1"}}} { + if _, err := groupPackagesArg(bad); err == nil { t.Errorf("%v was taken", bad) } } - g, err := groupPackageArg(map[string]any{"repository": "novox/mesh-sdk", "commit": strings.Repeat("a", 40), "ref": "refs/pull/9/head"}) - if err != nil || g.Ref != "refs/pull/9/head" { - t.Errorf("a pull request's head: %+v %v", g, err) - } - if g, err := groupPackageArg(map[string]any{"name": sdkPackage, "version": "0.1.16-feat-x.1"}); err != nil || !g.tagged() { - t.Errorf("the group's tagged version: %+v %v", g, err) + gs, err := groupPackagesArg([]any{map[string]any{"repository": "novox/mesh-sdk", "commit": full, "ref": "refs/pull/9/head"}, + map[string]any{"repository": "novox/mesh-delivery", "commit": full, "path": "modules/mesh-delivery-contracts/ts/seats/git/"}}) + if err != nil || len(gs) != 2 || gs[0].Ref != "refs/pull/9/head" || gs[0].Path != "" || gs[1].Path != "modules/mesh-delivery-contracts/ts/seats/git" { + t.Errorf("a member's root and a member's directory: %+v %v", gs, err) } }