Test archive artifacts against build-agent's own digests (issue 1589)

This commit is contained in:
2026-10-21 09:13:32 +00:00
parent bfd6b7c7ce
commit 8bfa626a8b
@@ -0,0 +1,197 @@
package main
import (
"context"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// Issue 1589: the build step packs an archive artifact (novox/hq ADR 0478 §1), as the build agent packs it
// (mesh-controller internal/builder: `pack` for a directory, upstreamArchive for a pinned file). The digests below are
// the build agent's own, captured from its code (testdata/captured/README).
const (
buildAgentDirectoryDigest = "sha256:9503d3f91a95be6d8faf8ec696054007cfefe4f5b4713dfc8f4284b026aa6443"
buildAgentWithinDigest = "sha256:9bacf4aab06ccfe90100aeb80b1bd94508a4d461b7e5665c599d7c5cb36c790f"
buildAgentFileDigest = "sha256:0d605adbdef9e41d8290720296ab13dc820861ab2ef36fafa9de02558d03716b"
releaseSHA256 = "2c9e5753c06902072422c4e9424154e8177684cc74af0bcc8dfe2bdf223a7e95"
)
// copyTree copies a directory, keeping modes.
func copyTree(t *testing.T, from, to string) {
t.Helper()
err := filepath.Walk(from, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, _ := filepath.Rel(from, p)
// A module.json in the repository would be taken for a module (issue 1527): fixtures keep it as module.json.fixture.
if filepath.Base(rel) == "module.json.fixture" {
rel = strings.TrimSuffix(rel, ".fixture")
}
target := filepath.Join(to, rel)
if info.IsDir() {
return os.MkdirAll(target, 0o755)
}
body, err := os.ReadFile(p)
if err != nil {
return err
}
return os.WriteFile(target, body, info.Mode().Perm())
})
if err != nil {
t.Fatal(err)
}
}
// archiveModule is a module whose workspace ws1 holds the fixture tree of testdata/repos/archive; manifest, when given,
// replaces modules/demo/module.json.
func archiveModule(t *testing.T, manifest string) (*Module, *keptStates, *[]cmdRecord) {
t.Helper()
m, states, calls := buildModule(t, manifestWith(goBundle), "x")
tree := filepath.Join(m.Work, "ws1", "tree")
if err := os.RemoveAll(tree); err != nil {
t.Fatal(err)
}
copyTree(t, filepath.Join("testdata", "repos", "archive"), tree)
if manifest != "" {
if err := os.WriteFile(filepath.Join(tree, "modules", "demo", "module.json"), []byte(manifest), 0o644); err != nil {
t.Fatal(err)
}
}
return m, states, calls
}
// pinnedModule is an archive module whose artifact is pinned to path of a TLS server serving the release, under the
// name example.com (a manifest must say https and a named host); the module's client dials the server whatever the name.
func pinnedModule(t *testing.T, path, sha, extra string) (*Module, *keptStates) {
t.Helper()
body, err := os.ReadFile(filepath.Join("testdata", "upstream-release.tar.gz"))
if err != nil {
t.Fatal(err)
}
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/release.tar.gz" {
http.NotFound(w, r)
return
}
w.Write(body)
}))
t.Cleanup(srv.Close)
m, states, _ := archiveModule(t, pinned("https://example.com"+path, sha, extra))
client := srv.Client()
client.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, network, srv.Listener.Addr().String())
}
m.HTTP = client
return m, states
}
// 1589/AC1: an archive of a directory of the module's tree has the digest the build agent's `pack` gives that tree,
// and the module runs no command for it.
func TestAnArchiveOfADirectoryHasTheBuildAgentsDigest(t *testing.T) {
m, states, calls := archiveModule(t, "")
res, st := runBuild(t, m, states, "files")
if st.State != delivery.StepPassed {
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
}
if res.Digest != buildAgentDirectoryDigest {
t.Errorf("digest %s, the build agent's is %s", res.Digest, buildAgentDirectoryDigest)
}
if res.Artifact != "files" || res.Size == 0 || res.Binary != "" {
t.Errorf("result = %+v", res)
}
if info, err := os.Stat(res.Path); err != nil || info.Size() != int64(res.Size) {
t.Errorf("the archive at %q: %v", res.Path, err)
}
if len(*calls) != 0 {
t.Errorf("an archive ran %d commands", len(*calls))
}
}
// An archive does not leave a stray directory in the module's tree: packing the same tree twice is one digest.
func TestPackingAnArchiveTwiceIsOneDigest(t *testing.T) {
m, states, _ := archiveModule(t, "")
a, _ := runBuild(t, m, states, "files")
b, _ := runBuild(t, m, states, "files")
if a.Digest == "" || a.Digest != b.Digest {
t.Errorf("%s and %s", a.Digest, b.Digest)
}
}
func TestAnArchiveFromOutsideTheModuleOrMissingFailsTheStep(t *testing.T) {
for from, want := range map[string]string{
"../../..": "outside",
"nothing": "nothing",
} {
m, states, _ := archiveModule(t, `{"module":"demo","version":"1","build":{"artifacts":[{"name":"files","kind":"archive","from":"`+from+`"}]}}`)
if _, st := runBuild(t, m, states, "files"); st.State != delivery.StepFailed || !strings.Contains(st.Reason, want) {
t.Errorf("from %q: %s %q", from, st.State, st.Reason)
}
}
}
func pinned(url, sha, extra string) string {
return `{"module":"demo","version":"1","build":{"artifacts":[{"name":"files","kind":"archive","url":"` + url +
`","sha256":"` + sha + `","version":"1.0"` + extra + `}]}}`
}
// 1589/AC2: a pinned file is fetched, checked against its sha256 and laid out as the artifact says; the digest is the
// build agent's for the same file.
func TestAPinnedArchiveIsUnpackedWithinAndHasTheBuildAgentsDigest(t *testing.T) {
m, states := pinnedModule(t, "/release.tar.gz", releaseSHA256, `,"within":"release/app"`)
res, st := runBuild(t, m, states, "files")
if st.State != delivery.StepPassed || res.Digest != buildAgentWithinDigest {
t.Fatalf("%s %q: digest %s, the build agent's is %s", st.State, st.Reason, res.Digest, buildAgentWithinDigest)
}
}
func TestAPinnedFileKeptWholeHasTheBuildAgentsDigest(t *testing.T) {
m, states := pinnedModule(t, "/release.tar.gz", releaseSHA256, `,"file":"tool.bin"`)
res, st := runBuild(t, m, states, "files")
if st.State != delivery.StepPassed || res.Digest != buildAgentFileDigest {
t.Fatalf("%s %q: digest %s, the build agent's is %s", st.State, st.Reason, res.Digest, buildAgentFileDigest)
}
}
// A file that is not the pin's is refused, naming both digests, and nothing is packed.
func TestAPinnedFileOfAnotherDigestIsRefused(t *testing.T) {
wrong := strings.Repeat("ab", 32)
m, states := pinnedModule(t, "/release.tar.gz", wrong, `,"within":"release/app"`)
_, st := runBuild(t, m, states, "files")
if st.State != delivery.StepFailed || !strings.Contains(st.Reason, "sha256:"+wrong) || !strings.Contains(st.Reason, "sha256:"+releaseSHA256) {
t.Errorf("the step ended %s: %q", st.State, st.Reason)
}
if entries, _ := os.ReadDir(filepath.Join(m.Work, "ws1", "artifacts")); len(entries) != 0 {
t.Errorf("an archive was left: %v", entries)
}
}
func TestAPinnedFileTheServerRefusesFailsTheStep(t *testing.T) {
m, states := pinnedModule(t, "/gone", releaseSHA256, `,"file":"x"`)
if _, st := runBuild(t, m, states, "files"); st.State != delivery.StepFailed || !strings.Contains(st.Reason, "404") {
t.Errorf("the step ended %s: %q", st.State, st.Reason)
}
}
// Entries outside `within` are refused, as the build agent refuses them.
func TestAPinnedArchiveWithEntriesOutsideWithinIsRefused(t *testing.T) {
m, states := pinnedModule(t, "/release.tar.gz", releaseSHA256, `,"within":"release/other"`)
if _, st := runBuild(t, m, states, "files"); st.State != delivery.StepFailed || !strings.Contains(st.Reason, "release/other") {
t.Errorf("the step ended %s: %q", st.State, st.Reason)
}
}
// Other kinds are still refused by name.
func TestAnImageArtifactIsStillRefusedByName(t *testing.T) {
m, states, _ := archiveModule(t, `{"module":"demo","version":"1","build":{"artifacts":[{"name":"files","kind":"image","from":"files"}]}}`)
if _, st := runBuild(t, m, states, "files"); st.State != delivery.StepFailed || !strings.Contains(st.Reason, "image") {
t.Errorf("the step ended %s: %q", st.State, st.Reason)
}
}