Files
mesh-delivery/modules/mesh-delivery-indexer/internal/index/versions_test.go
T
2026-11-12 09:20:34 +01:00

532 lines
24 KiB
Go

package index
import (
"context"
"encoding/json"
"errors"
"fmt"
"slices"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshbuilds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/nodebuildgo"
)
// Design 47 G §349: a version is released once every artifact the module's entry names is published at it, said once as
// version-released; a version lower than the highest released is recorded late and not said; each module keeps its newest
// 100 versions, never its highest of any MAJOR.MINOR.
func TestAVersionIsReleasedOnceEveryArtifactIsPublished(t *testing.T) {
ctx := context.Background()
now := time.Date(2026, 11, 1, 3, 0, 0, 0, time.UTC)
var said []meshcatalog.VersionReleasedEvent
ix := &Indexer{Versions: newMem(), Heard: newMem(), Revisions: newMemObjects(1 << 20), Now: func() time.Time { return now },
SayVersionReleased: func(_ context.Context, e meshcatalog.VersionReleasedEvent, id string) error {
said = append(said, e)
return nil
}}
entries := map[string]meshcatalog.CatalogEntry{"avahi": {Module: "avahi", Manifest: map[string]any{},
Artifacts: []meshcatalog.Artifact{{Ref: "avahi:program"}, {Ref: "avahi:image"}}}}
rev, objects, err := CatalogRevisionOf(meshcatalog.CatalogRevision{Revision: 3, Kind: "trunk"}, entries)
if err != nil {
t.Fatal(err)
}
if err := KeepRevision(ctx, ix.Revisions, rev, objects); err != nil {
t.Fatal(err)
}
pub := func(artifact, version string) {
t.Helper()
if err := ix.HearPackagePublished(ctx, meshbuilds.PackagePublished{Module: "avahi", Artifact: artifact, Digest: "sha256:" + artifact + version, Version: version, Source: 7, Branch: "main"}); err != nil {
t.Fatal(err)
}
}
pub("avahi:program", "0.1.5")
if v, _ := ModuleVersions(ctx, ix.Versions, "avahi"); len(v) != 0 || len(said) != 0 {
t.Fatalf("one of two artifacts published released %v, said %v", v, said)
}
pub("avahi:image", "0.1.5")
pub("avahi:image", "0.1.5") // heard again
v, _ := ModuleVersions(ctx, ix.Versions, "avahi")
if len(v) != 1 || len(v[0].Artifacts) != 2 || v[0].Source != 7 || v[0].Late || len(said) != 1 || said[0].Version != "0.1.5" {
t.Fatalf("released %+v, said %+v", v, said)
}
if keys, _ := ix.Heard.Keys(ctx); len(keys) != 0 {
t.Errorf("the artifacts kept as published stay after the release: %v", keys)
}
// a late older version is recorded and not said
pub("avahi:program", "0.1.2")
pub("avahi:image", "0.1.2")
v, _ = ModuleVersions(ctx, ix.Versions, "avahi")
if len(v) != 2 || v[1].Version != "0.1.2" || !v[1].Late || len(said) != 1 {
t.Fatalf("the late version: %+v, said %d", v, len(said))
}
// the newest 100 stay, and the highest of each MAJOR.MINOR
for i := 6; i < 6+VersionsKept+5; i++ {
pub("avahi:program", fmt.Sprintf("0.1.%d", i))
pub("avahi:image", fmt.Sprintf("0.1.%d", i))
}
for _, mm := range []string{"0.2", "0.3"} {
pub("avahi:program", mm+".0")
pub("avahi:image", mm+".0")
}
v, _ = ModuleVersions(ctx, ix.Versions, "avahi")
if len(v) != VersionsKept || v[0].Version != "0.3.0" {
t.Fatalf("%d versions kept, newest %s", len(v), v[0].Version)
}
var oldest string
for _, e := range v {
oldest = e.Version
}
if compareVersions(oldest, "0.1.5") < 0 {
t.Errorf("the oldest kept is %s: the oldest were not the ones deleted", oldest)
}
}
// Design 47 G §372: the first fill records, once, each module's highest published version over its artifacts as released
// and says versions-filled once; a builder_versions that does not answer holds it and, after five minutes, is the finding
// call-unanswered; a fill made is not made again.
func TestTheFirstFillOfTheVersionsComesFromTheBuildersPublished(t *testing.T) {
ctx := context.Background()
now := time.Date(2026, 11, 1, 3, 0, 0, 0, time.UTC)
w := newFindings()
var filledSaid []meshcatalog.VersionsFilledEvent
ix := &Indexer{Versions: newMem(), Heard: newMem(), Findings: w, Revisions: newMemObjects(1 << 20), Now: func() time.Time { return now },
SayVersionsFilled: func(_ context.Context, e meshcatalog.VersionsFilledEvent, id string) error {
filledSaid = append(filledSaid, e)
return nil
}}
entries := map[string]meshcatalog.CatalogEntry{
"mesh-sdk": {Module: "mesh-sdk", Manifest: map[string]any{}, Artifacts: []meshcatalog.Artifact{
{Ref: "mesh-sdk:go", Package: &meshcatalog.ArtifactPackage{Ecosystem: "go", Name: "git.novox.be/novox/mesh-sdk/go"}},
{Ref: "mesh-sdk:ts", Package: &meshcatalog.ArtifactPackage{Ecosystem: "npm", Name: "@novox/mesh-sdk"}}}},
"avahi": {Module: "avahi", Manifest: map[string]any{}, Artifacts: []meshcatalog.Artifact{{Ref: "avahi:image", Kind: "image"}}},
"fresh": {Module: "fresh", Manifest: map[string]any{}, Artifacts: []meshcatalog.Artifact{{Ref: "fresh:image", Kind: "image"}}},
}
rev, objects, err := CatalogRevisionOf(meshcatalog.CatalogRevision{Revision: 3, Kind: "trunk"}, entries)
if err != nil {
t.Fatal(err)
}
if err := KeepRevision(ctx, ix.Revisions, rev, objects); err != nil {
t.Fatal(err)
}
up := false
ask := func(_ context.Context, _, name string) (BuilderVersionsAnswer, error) {
if !up {
return BuilderVersionsAnswer{}, errors.New("no responders")
}
pub := func(v, d string) BuilderVersion {
return BuilderVersion{Version: v, State: "published", Digest: d, Revision: "9"}
}
switch name {
case "git.novox.be/novox/mesh-sdk/go":
return BuilderVersionsAnswer{Name: name, Versions: []BuilderVersion{pub("0.1.14", "sha256:go14"), {Version: "0.1.15", State: "refused"}}}, nil
case "@novox/mesh-sdk":
return BuilderVersionsAnswer{Name: name, Versions: []BuilderVersion{pub("0.1.18", "sha256:ts18")}}, nil
case "avahi/image":
return BuilderVersionsAnswer{Name: name, Versions: []BuilderVersion{pub("0.2.3", "sha256:av")}}, nil
}
return BuilderVersionsAnswer{Name: name}, nil
}
if err := ix.FillVersions(ctx, ask); err == nil || len(filledSaid) != 0 {
t.Fatalf("a builder_versions unanswered filled: %v", err)
}
if len(w.Held()) != 0 {
t.Fatal("a call unanswered for no time wrote its finding")
}
now = now.Add(5 * time.Minute)
_ = ix.FillVersions(ctx, ask)
if h := w.Held(); len(h) != 1 || h[0].ID != BuilderVersionsCall {
t.Fatalf("the findings after five minutes: %+v", h)
}
up = true
if err := ix.FillVersions(ctx, ask); err != nil {
t.Fatal(err)
}
if len(w.Held()) != 0 {
t.Errorf("the finding stands after builder_versions answered: %+v", w.Held())
}
if len(filledSaid) != 1 || filledSaid[0].Modules != 2 {
t.Fatalf("versions-filled said %+v", filledSaid)
}
sdk, _ := ModuleVersions(ctx, ix.Versions, "mesh-sdk")
if len(sdk) != 1 || sdk[0].Version != "0.1.18" || len(sdk[0].Artifacts) != 1 || sdk[0].Artifacts[0].Artifact != "mesh-sdk:ts" || sdk[0].Source != 9 {
t.Errorf("mesh-sdk's filled version: %+v (the higher of Go's 0.1.14 and npm's 0.1.18)", sdk)
}
if fresh, _ := ModuleVersions(ctx, ix.Versions, "fresh"); len(fresh) != 0 {
t.Errorf("a module with none published is filled: %+v", fresh)
}
if err := ix.FillVersions(ctx, ask); err != nil || len(filledSaid) != 1 {
t.Errorf("filled again: %v, said %d", err, len(filledSaid))
}
}
// Design 47 G §374, §375, A-68: a release records, per edge, the part it was built against (kind 1, from package-published's
// dependencies) or, once delivered, the delivered version it was ordered after (kinds 2 and 4); outdated answers an edge behind
// by the part's digest alone, with the act rebuild only for a stale edge; a newer version that left the digest as it was
// makes nothing behind.
func TestOutdatedAnswersTheEdgesBehindByTheirPartsDigest(t *testing.T) {
ctx := context.Background()
ix := &Indexer{Versions: newMem(), Heard: newMem(), Revisions: newMemObjects(1 << 20)}
entries := map[string]meshcatalog.CatalogEntry{
"lib": {Module: "lib", Manifest: map[string]any{}, Artifacts: []meshcatalog.Artifact{{Ref: "lib:pkg"}}},
"dep": {Module: "dep", Manifest: map[string]any{}, Artifacts: []meshcatalog.Artifact{{Ref: "dep:program"}}},
"app": {Module: "app", Manifest: map[string]any{}, Artifacts: []meshcatalog.Artifact{{Ref: "app:program"}},
Edges: []meshcatalog.Edge{{Kind: 1, From: "lib:pkg", To: "app:program", Part: "lib:pkg", Stale: true, Word: "imports"},
{Kind: 4, From: "dep:program", To: "app:program", Part: "dep:program", Word: "provision"}}},
}
rev, objects, err := CatalogRevisionOf(meshcatalog.CatalogRevision{Revision: 3, Kind: "trunk"}, entries)
if err != nil {
t.Fatal(err)
}
if err := KeepRevision(ctx, ix.Revisions, rev, objects); err != nil {
t.Fatal(err)
}
pub := func(module, version, digest string, deps ...meshbuilds.Dependency) {
t.Helper()
if err := ix.HearPackagePublished(ctx, meshbuilds.PackagePublished{Module: module, Artifact: module + ":" + map[string]string{"lib": "pkg", "dep": "program", "app": "program"}[module], Digest: digest, Version: version, Dependencies: deps}); err != nil {
t.Fatal(err)
}
}
behind := func() []meshcatalog.Outdated {
t.Helper()
a, err := ix.Outdated(ctx, meshcatalog.OutdatedInput{})
if err != nil {
t.Fatal(err)
}
return a.Behind
}
pub("lib", "0.1.1", "d1")
pub("dep", "0.1.1", "x1")
pub("app", "0.1.1", "a1", meshbuilds.Dependency{Kind: 1, Part: "lib:pkg", Version: "0.1.1", Digest: "d1"})
v, _ := ModuleVersions(ctx, ix.Versions, "app")
if len(v[0].Edges) != 2 || v[0].Edges[0].Kind != 1 || !v[0].Edges[0].Stale || v[0].Edges[0].Word != "imports" || v[0].Edges[1].Version != "not yet delivered" {
t.Fatalf("the release's edges: %+v", v[0].Edges)
}
pub("lib", "0.1.2", "d1") // a newer version, the part's digest as it was
if b := behind(); len(b) != 0 {
t.Fatalf("a newer version that left the digest as it was is behind: %+v", b)
}
pub("lib", "0.1.3", "d3")
if b := behind(); len(b) != 1 || b[0].Module != "app" || b[0].Kind != 1 || b[0].Act != "rebuild" || b[0].Dependency.Artifact != "lib:pkg" || !strings.Contains(b[0].Newest, "d3") || !strings.Contains(b[0].Recorded, "d1") {
t.Fatalf("behind after lib's new digest: %+v", b)
}
// delivery: dep 0.1.1 is delivered, then app 0.1.1 writes its kind 4 entry; dep 0.1.2 delivered later is behind, an ordering edge
deliver := func(module, version string, parts map[string]string) {
t.Helper()
if err := ix.HearVersionDelivered(ctx, meshwants.VersionDeliveredEvent{Module: module, Version: version, Parts: parts}); err != nil {
t.Fatal(err)
}
}
deliver("dep", "0.1.1", map[string]string{"dep:program": "x1"})
deliver("app", "0.1.1", map[string]string{"app:program": "a1"})
v, _ = ModuleVersions(ctx, ix.Versions, "app")
if e := v[0].Edges[1]; e.Version != "0.1.1" || e.Digest != "x1" {
t.Fatalf("the kind 4 entry after the first delivery: %+v", e)
}
deliver("dep", "0.1.2", map[string]string{"dep:program": "x2"})
deliver("app", "0.1.1", map[string]string{"app:program": "a1"}) // delivered again: written once, not changed
v, _ = ModuleVersions(ctx, ix.Versions, "app")
if e := v[0].Edges[1]; e.Version != "0.1.1" {
t.Errorf("an entry written at the first delivery changed: %+v", e)
}
b := behind()
if len(b) != 2 || b[1].Kind != 4 || b[1].Act != "" || b[1].Dependency.Artifact != "dep:program" {
t.Fatalf("behind after dep's second delivery: %+v", b)
}
// the newest toolchain-agreed per language is kept as heard
if err := ix.HearToolchainAgreed(ctx, meshbuilds.ToolchainAgreed{Language: "go", Version: "1.25"}); err != nil {
t.Fatal(err)
}
if err := ix.HearToolchainAgreed(ctx, meshbuilds.ToolchainAgreed{Language: "go", Version: "1.26"}); err != nil {
t.Fatal(err)
}
if raw, _, found, _ := ix.Heard.Get(ctx, ToolchainPrefix+"go"); !found || !strings.Contains(string(raw), "1.26") {
t.Errorf("toolchain.go holds %s", raw)
}
}
// Design 47 G §354: an input is kept under its write time and sha256, read back by the verb input with its digest checked;
// the keys older than a day among the 20 oldest are deleted at each new keep.
func TestAnInputIsKeptByItsDigestAndSweptAfterADay(t *testing.T) {
ctx := context.Background()
now := time.Date(2026, 11, 1, 3, 0, 0, 0, time.UTC)
ix := &Indexer{Inputs: newMem(), Now: func() time.Time { return now }}
d1, err := ix.KeepInput(ctx, "ask-1", []byte("first"))
if err != nil {
t.Fatal(err)
}
if a, err := InputBody(ctx, ix.Inputs, nil, d1); err != nil || string(a) != "first" {
t.Fatalf("the input read back: %q, %v", a, err)
}
if _, err := InputBody(ctx, ix.Inputs, nil, "sha256:none"); err == nil {
t.Error("an input never kept is answered")
}
now = now.Add(23 * time.Hour)
if _, err := ix.KeepInput(ctx, "ask-2", []byte("second")); err != nil {
t.Fatal(err)
}
inputsOnly := func() []string {
var out []string
all, _ := ix.Inputs.Keys(ctx)
for _, k := range all {
if !strings.HasPrefix(k, InputsAskPrefix) {
out = append(out, k)
}
}
return out
}
if keys := inputsOnly(); len(keys) != 2 {
t.Fatalf("inside a day: %v", keys)
}
now = now.Add(2 * time.Hour)
d3, _ := ix.KeepInput(ctx, "ask-3", []byte("third"))
if keys := inputsOnly(); len(keys) != 2 {
t.Fatalf("after a day the first stays: %v", keys)
}
if all, _ := ix.Inputs.Keys(ctx); slices.Contains(all, InputsAskPrefix+"ask-1") {
t.Errorf("the name by ask of the swept input stays: %v", all)
}
if _, err := InputBody(ctx, ix.Inputs, nil, d1); err == nil {
t.Error("the input a day old is still answered")
}
if _, err := InputBody(ctx, ix.Inputs, nil, d3); err != nil {
t.Error(err)
}
}
// Design 47 G A-80, §73: what the indexer heard lives in the state heard, each key part escaped as a name, so a dotted module
// name parses and nothing of it is written to the own-build state.
func TestWhatIsHeardIsKeptEscapedInTheStateHeard(t *testing.T) {
ctx := context.Background()
ix := &Indexer{Versions: newMem(), OwnBuildState: newMem(), Heard: newMem(), Revisions: newMemObjects(1 << 20)}
if err := ix.HearVersionDelivered(ctx, meshwants.VersionDeliveredEvent{Module: "a.b", Version: "1.2.3"}); err != nil {
t.Fatal(err)
}
if err := ix.HearToolchainAgreed(ctx, meshbuilds.ToolchainAgreed{Language: "go", Version: "1.26"}); err != nil {
t.Fatal(err)
}
if _, ok := ix.deliveredOf(ctx, "a.b"); !ok {
t.Error("the delivered version of a dotted module is not read back")
}
got, _ := ix.Heard.Keys(ctx)
slices.Sort(got)
if want := []string{"delivered.a=2eb", "toolchain.go"}; !slices.Equal(got, want) {
t.Errorf("the keys of heard are %v, want %v", got, want)
}
if own, _ := ix.OwnBuildState.Keys(ctx); len(own) != 0 {
t.Errorf("the own-build state holds %v", own)
}
}
// Design 47 G §375, A-78, A-80: outdated says a kind-3 edge behind when the agreed toolchain of its artifact's language names another
// version, a kind-5 edge when the acting part has not acted for the dependent's current build (a wait-ended that ended ran or
// released), and a kind-6 edge when the delivered version's entry no longer serves the word.
func TestOutdatedAnswersKinds3And5And6(t *testing.T) {
ctx := context.Background()
ix := &Indexer{Versions: newMem(), Heard: newMem(), Revisions: newMemObjects(1 << 20)}
keep := func(n, source int64, serves []string, fp string) {
t.Helper()
entries := map[string]meshcatalog.CatalogEntry{
"prov": {Module: "prov", Manifest: map[string]any{}, Artifacts: []meshcatalog.Artifact{{Ref: "prov:program"}}, Serves: serves},
"actor": {Module: "actor", Manifest: map[string]any{}, Artifacts: []meshcatalog.Artifact{{Ref: "actor:program"}}},
"app": {Module: "app", Manifest: map[string]any{}, Fp: fp, Artifacts: []meshcatalog.Artifact{{Ref: "app:program", Language: "go"}},
Edges: []meshcatalog.Edge{{Kind: 3, From: "tc:program", To: "app:program", Part: "tc:program"},
{Kind: 5, From: "actor:program", To: "app:program", Part: "actor:program"},
{Kind: 6, From: "prov:program", To: "app:program", Part: "prov:program", Word: "seat:s.verb"}}},
}
rev, objects, err := CatalogRevisionOf(meshcatalog.CatalogRevision{Revision: n, Source: source, Kind: "trunk"}, entries)
if err != nil {
t.Fatal(err)
}
if err := KeepRevision(ctx, ix.Revisions, rev, objects); err != nil {
t.Fatal(err)
}
}
keep(1, 10, []string{"s.verb"}, "fp1")
put := func(module string, v meshcatalog.VersionEntry) {
t.Helper()
v.Module, v.V = module, 1
data, _ := json.Marshal(v)
if err := ix.Versions.Create(ctx, meshcatalog.VersionsByModuleVersionKey(module, v.Version), data); err != nil {
t.Fatal(err)
}
}
put("prov", meshcatalog.VersionEntry{Version: "0.1.1", Source: 10})
put("app", meshcatalog.VersionEntry{Version: "0.1.1", Edges: []meshcatalog.VersionEdge{
{Kind: 3, Part: "tc:program", Version: "1.25", Digest: "t1", Stale: true},
{Kind: 5, Part: "actor:program"},
{Kind: 6, Part: "prov:program", Word: "seat:s.verb", Version: "0.1.1"}}})
behind := func() map[int64]meshcatalog.Outdated {
t.Helper()
a, err := ix.Outdated(ctx, meshcatalog.OutdatedInput{})
if err != nil {
t.Fatal(err)
}
m := map[int64]meshcatalog.Outdated{}
for _, o := range a.Behind {
m[o.Kind] = o
}
return m
}
if err := ix.HearToolchainAgreed(ctx, meshbuilds.ToolchainAgreed{Language: "go", Version: "1.25"}); err != nil {
t.Fatal(err)
}
b := behind()
if _, ok := b[3]; ok {
t.Errorf("kind 3 behind at the agreed version: %+v", b[3])
}
if o, ok := b[5]; !ok || o.Act != "" {
t.Errorf("kind 5 is not behind before the acting part acted: %+v", b)
}
wait := func(how, step, build string, since time.Time) {
t.Helper()
if err := ix.HearWaitEnded(ctx, meshwants.WaitEndedEvent{Part: "actor:program", Module: "app", Step: step, How: how, Version: build, Since: since}); err != nil {
t.Fatal(err)
}
}
at := time.Date(2026, 11, 1, 3, 0, 0, 0, time.UTC)
wait("superseded", "compose", "fp1", at)
wait("ran", "delivery", "fp1", at)
if _, ok := behind()[5]; !ok {
t.Error("a superseded wait or a delivery wait counted as an act")
}
wait("ran", "compose", "fp1", at)
if _, ok := behind()[5]; ok {
t.Error("kind 5 is behind after the act for the current build")
}
keep(2, 20, []string{"s.other"}, "fp2")
if o, ok := behind()[5]; !ok || !strings.Contains(o.Newest, "fp2") {
t.Errorf("kind 5 after the dependent's new build: %+v", o)
}
wait("released", "compose", "fp2", at.Add(time.Minute))
if _, ok := behind()[5]; ok {
t.Error("a released wait is not an act")
}
if err := ix.HearToolchainAgreed(ctx, meshbuilds.ToolchainAgreed{Language: "go", Version: "1.26"}); err != nil {
t.Fatal(err)
}
if o, ok := behind()[3]; !ok || o.Act != "rebuild" || o.Newest != "1.26" || !strings.Contains(o.Recorded, "1.25") {
t.Errorf("kind 3 after the agreed toolchain changed: %+v", o)
}
// kind 6: prov 0.1.2 built at source 20 serves another word only
if _, ok := behind()[6]; ok {
t.Error("kind 6 behind with no newer delivered version")
}
put("prov", meshcatalog.VersionEntry{Version: "0.1.2", Source: 20})
if err := ix.HearVersionDelivered(ctx, meshwants.VersionDeliveredEvent{Module: "prov", Version: "0.1.2", Parts: map[string]string{"prov:program": "p2"}}); err != nil {
t.Fatal(err)
}
if o, ok := behind()[6]; !ok || o.Act != "" || !strings.Contains(o.Newest, "no longer serves") {
t.Errorf("kind 6 after a version that serves no more: %+v", o)
}
}
// Design 47 G §377, A-78 c: the first fill gives each module's newest version its edge entries, each marked filled: kind 1 from the
// dependency version published latest before the build was claimed, kinds 2, 4 and 6 from the version every placing machine runs,
// read from the assigner's placement and the stream running, kind 5 as acted for the running build; so the first outdated says only what is truly behind.
func TestTheFirstFillGivesTheNewestVersionsTheirFilledEdges(t *testing.T) {
ctx := context.Background()
t0 := time.Date(2026, 11, 1, 3, 0, 0, 0, time.UTC)
placed := map[string][]string{"prov": {"n1", "n2"}}
ix := &Indexer{Versions: newMem(), Heard: newMem(), Revisions: newMemObjects(1 << 20), Now: func() time.Time { return t0 },
Placement: func(_ context.Context, m string) ([]meshmoduleassigner.ModulePlacement, error) {
return []meshmoduleassigner.ModulePlacement{{Branch: "trunk", Nodes: placed[m]}}, nil
},
RunningWhole: func(context.Context) (map[string]map[string]nodeengine.Running, error) {
return map[string]map[string]nodeengine.Running{"n1": {"prov": {Fingerprint: "fp-prov"}}, "n2": {"prov": {Fingerprint: "fp-prov"}}}, nil
}}
entries := map[string]meshcatalog.CatalogEntry{
"lib": {Module: "lib", Manifest: map[string]any{}, Fp: "fp-lib", Artifacts: []meshcatalog.Artifact{{Ref: "lib:pkg", Package: &meshcatalog.ArtifactPackage{Ecosystem: "go", Name: "x/lib"}}}},
"actor": {Module: "actor", Manifest: map[string]any{}, Fp: "fp-actor", Artifacts: []meshcatalog.Artifact{{Ref: "actor:program"}}},
"prov": {Module: "prov", Manifest: map[string]any{}, Fp: "fp-prov", Artifacts: []meshcatalog.Artifact{{Ref: "prov:program"}}},
"app": {Module: "app", Manifest: map[string]any{}, Fp: "fp-app", Artifacts: []meshcatalog.Artifact{{Ref: "app:program"}},
Edges: []meshcatalog.Edge{{Kind: 1, From: "lib:pkg", To: "app:program", Part: "lib:pkg", Stale: true},
{Kind: 4, From: "prov:program", To: "app:program", Part: "prov:program"},
{Kind: 5, From: "actor:program", To: "app:program", Part: "actor:program"}}},
}
rev, objects, err := CatalogRevisionOf(meshcatalog.CatalogRevision{Revision: 3, Kind: "trunk"}, entries)
if err != nil {
t.Fatal(err)
}
if err := KeepRevision(ctx, ix.Revisions, rev, objects); err != nil {
t.Fatal(err)
}
ask := func(_ context.Context, _, name string) (BuilderVersionsAnswer, error) {
pub := func(v, d string, at time.Time) BuilderVersion {
return BuilderVersion{Version: v, State: "published", Digest: d, Revision: "9", At: at}
}
switch name {
case "x/lib":
return BuilderVersionsAnswer{Versions: []BuilderVersion{pub("0.1.1", "sha256:l1", t0), pub("0.1.2", "sha256:l2", t0.Add(2*time.Hour))}}, nil
case "app/program":
return BuilderVersionsAnswer{Versions: []BuilderVersion{pub("0.3.1", "sha256:a1", t0.Add(time.Hour))}}, nil
}
return BuilderVersionsAnswer{Versions: []BuilderVersion{pub("0.9.1", "sha256:"+name, t0)}}, nil
}
if err := ix.FillVersions(ctx, ask); err != nil {
t.Fatal(err)
}
app, _ := ModuleVersions(ctx, ix.Versions, "app")
if len(app) != 1 || len(app[0].Edges) != 3 {
t.Fatalf("app's filled version: %+v", app)
}
for _, e := range app[0].Edges {
if !e.Filled {
t.Errorf("a filled entry is not marked: %+v", e)
}
switch e.Kind {
case 1:
if e.Version != "0.1.1" || e.Digest != "sha256:l1" || !e.Stale {
t.Errorf("kind 1 is the version published before the build was claimed: %+v", e)
}
case 4:
if e.Version != "0.9.1" || e.Digest != "sha256:prov/program" {
t.Errorf("kind 4 is the version every machine runs: %+v", e)
}
}
}
// lib's 0.1.2 was published after app's build was claimed: that is behind, and nothing else (the delivered kind 4 and the act of kind 5 are not)
if a, err := ix.Outdated(ctx, meshcatalog.OutdatedInput{}); err != nil || len(a.Behind) != 1 || a.Behind[0].Kind != 1 {
t.Errorf("outdated after the first fill: %+v, %v", a.Behind, err)
}
}
// Design 47 G §354, A-78 d: an input is also keyed by its run ask's id, so the build seat's step-ended of that ask deletes it, and
// the key by ask no other read counts as an input.
func TestAnInputIsDeletedAtTheStepEndedOfItsAsk(t *testing.T) {
ctx := context.Background()
now := time.Date(2026, 11, 1, 3, 0, 0, 0, time.UTC)
ix := &Indexer{Inputs: newMem(), Now: func() time.Time { return now }}
d1, err := ix.KeepInput(ctx, "a.1", []byte("first"))
if err != nil {
t.Fatal(err)
}
d2, _ := ix.KeepInput(ctx, "a.2", []byte("second"))
if _, err := ix.KeepInput(ctx, "", []byte("x")); err == nil {
t.Error("an input kept for no ask")
}
if err := ix.HearInputEnded(ctx, nodebuildgo.StepEnded{Ask: "a.1"}); err != nil {
t.Fatal(err)
}
if err := ix.HearInputEnded(ctx, nodebuildgo.StepEnded{Ask: "a.1"}); err != nil {
t.Errorf("an end heard again: %v", err)
}
if _, err := InputBody(ctx, ix.Inputs, nil, d1); err == nil {
t.Error("the input of an ended ask is still answered")
}
if _, err := InputBody(ctx, ix.Inputs, nil, d2); err != nil {
t.Errorf("the input of another ask: %v", err)
}
if got, _ := ix.Inputs.Keys(ctx); len(got) != 2 {
t.Errorf("keys after one end: %v", got)
}
if a, err := InputsReadTool(ix.Inputs, nil).Run(map[string]any{}); err != nil || len(a.(InputsAnswer).Inputs) != 1 {
t.Errorf("indexer_inputs counts a key by ask: %+v, %v", a, err)
}
}