Merge D9, D13b, D15 and review round 1 findings 3 and 4: the indexer runs without its graph state, a package inside a module's directory has its publisher (ADR 0603 §3, §4, §10; ADR 0604 §1)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
This commit is contained in:
@@ -27,6 +27,7 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
"mesh-delivery-assigner/internal/judge"
|
||||
|
||||
"mesh-delivery-assigner/internal/assign"
|
||||
@@ -434,6 +435,60 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) {
|
||||
if !self {
|
||||
t.Error("ace's own want does not say it composes its own once placed")
|
||||
}
|
||||
|
||||
// ADR 0603 (L3, D15): one source revision changes a provider (db), its new consumer (app) and a build module (tool, whose
|
||||
// recipe moved: ace runs its first build and the second, under the agreed toolchain, is the one the source revision names).
|
||||
// The keys are written as their writers write them: the indexer's modules and manifests, the builder's builds, the
|
||||
// controller's grants, the placement seat's placement and the provisioner's provisions. The build module's want moves at
|
||||
// once and the provider's with it; the consumer's waits for its provision, with the words naming it, and moves once the
|
||||
// provisioner provides it (rule 8, ADR 0570 §4): nothing waits for a turn or an order, only for a key.
|
||||
offer := `"provides":[{"name":"database","scope":"mesh","answered-by":"helper"}]`
|
||||
dbDigest := b.manifest(t, "db", `{"module":"db","version":"1",`+offer+`}`)
|
||||
appDigest := b.manifest(t, "app", `{"module":"app","version":"1","requires":["database"]}`)
|
||||
toolDigest := b.manifest(t, "tool", `{"module":"tool","version":"1"}`)
|
||||
fpOf := func(c byte) string { return delivery.SourcePrefix2 + strings.Repeat(string(c), 64) }
|
||||
toolOld := fpOf('1')
|
||||
for module, row := range map[string]struct {
|
||||
fp, digest string
|
||||
}{"db": {fpOf('2'), dbDigest}, "app": {fpOf('3'), appDigest}, "tool": {fpOf('4'), toolDigest}} {
|
||||
mod, _ := delivery.EncodeModule(delivery.Module{FP2: row.fp, Manifest: row.digest, Repository: "novox/" + module, Commit: strings.Repeat("2", 40), Path: delivery.RootPath})
|
||||
b.putJSON(t, delivery.ModulesBucket, module, mod)
|
||||
fps := []string{row.fp}
|
||||
if module == "tool" {
|
||||
fps = append(fps, toolOld)
|
||||
}
|
||||
for _, fp := range fps {
|
||||
built, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, By: "builder.novox.1", Step: "pushed", Manifest: row.digest})
|
||||
bkey, _ := delivery.BuilderBuildKey(module, fp)
|
||||
b.putJSON(t, delivery.BuilderBuildsBucket, bkey, built)
|
||||
gkey, _ := delivery.PartGrantKey(module, fp)
|
||||
grant, _ := delivery.EncodeGrant(delivery.Grant{})
|
||||
b.putJSON(t, delivery.PartGrantsBucket, gkey, grant)
|
||||
}
|
||||
b.place(t, "ace", module)
|
||||
}
|
||||
runsOld, _ := delivery.EncodeHave(delivery.Have{FP: toolOld, Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
|
||||
b.putJSON(t, delivery.HaveBucket, "ace.tool", runsOld)
|
||||
asked := func(state, why string) {
|
||||
raw, err := provisions.Encode(provisions.Provision{V: provisions.Version, Provision: "database", Consumer: provisions.End{Module: "app", Node: "ace"},
|
||||
Provider: provisions.End{Module: "db", Node: "ace"}, State: state, Why: why, At: time.Now()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.putJSON(t, provisions.ProvisionsBucket, "ace.app.database", raw)
|
||||
}
|
||||
asked(provisions.Asked, "the provider's build is not done")
|
||||
if w := b.wantNow(t, "ace", "tool", "the build module's want at the second build", func(w delivery.Want) bool { return w.FP == fpOf('4') }); w.FP != fpOf('4') {
|
||||
t.Errorf("tool's want = %+v", w)
|
||||
}
|
||||
b.wantNow(t, "ace", "db", "the provider's want at its fingerprint", func(w delivery.Want) bool { return w.FP == fpOf('2') })
|
||||
if e, err := b.buckets[delivery.AssignerWantsBucket].Get(b.ctx, "want.ace.app"); err == nil {
|
||||
t.Errorf("app was wanted while its provision was asked: %s", e.Value())
|
||||
}
|
||||
asked(provisions.Provided, "")
|
||||
if w := b.wantNow(t, "ace", "app", "the consumer's want once its provision is provided", func(w delivery.Want) bool { return w.FP == fpOf('3') }); w.FP != fpOf('3') {
|
||||
t.Errorf("app's want = %+v", w)
|
||||
}
|
||||
}
|
||||
|
||||
// 1297/B9 (the assigner's half, ADR 0528 clause 6): started whole, the assigner holds a fingerprint whose health check
|
||||
|
||||
@@ -589,6 +589,45 @@ func TestAKeyIsBuiltOnlyWithTheToolchainItsFingerprintNamesAndNothingIsWrittenOt
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0603 §3 (D9): a source revision changing the Go build module's recipe and a Go module builds the module at once with
|
||||
// the recipe the nodes run and again, a second key under the new fingerprint, once the agreement moves to the new recipe;
|
||||
// the first key is neither rebuilt nor waited on.
|
||||
func TestARecipeThatMovesBuildsTheModuleAgainUnderItsNewFingerprint(t *testing.T) {
|
||||
r := newRig(t, goBundle)
|
||||
r.consider(t, "modules/m")
|
||||
calls := len(r.bm.calls)
|
||||
fp2 := "src2:" + hex64('2')
|
||||
for _, n := range []string{"ace", "novox"} {
|
||||
b, err := delivery.EncodeToolchain(delivery.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "2", At: time.Now()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.tools.Put(buildrun.GoBuildModule, n+".go", b, false)
|
||||
}
|
||||
r.b.Reconsider(context.Background())
|
||||
r.b.Wait()
|
||||
if len(r.bm.calls) != calls {
|
||||
t.Errorf("%d calls, was %d: the first fingerprint was built again for the new recipe", len(r.bm.calls), calls)
|
||||
}
|
||||
data, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-catalog", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", FP: "src1:" + hex64('f'), FP2: fp2,
|
||||
Manifest: mdigest('a'), Path: "modules/m", Toolchains: map[string]delivery.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "2"}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.b.Module(context.Background(), "m", data)
|
||||
r.b.Wait()
|
||||
second, err := delivery.BuilderBuildKey("m", fp2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v := valueOf(t, r.st, second); v.State != delivery.BuildDone || v.Toolchains["go"].Recipe != "2" {
|
||||
t.Errorf("second build = %+v, want done with recipe 2", v)
|
||||
}
|
||||
if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone {
|
||||
t.Errorf("first build = %+v, want it left done", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodesTheyDisagreeOnLeaveTheKeyUnwritten(t *testing.T) { // 1615/AC4
|
||||
r := newRig(t, goBundle)
|
||||
r.tools.Put(buildrun.GoBuildModule, "ace.go", tcValue(t, "go", "go1.26.0"), false)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package check
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
)
|
||||
|
||||
// A change to a build module or a base is built first, and what stands on it is composed from the manifests (novox/hq ADR
|
||||
// 0603 §10): the check builds the build module (a candidate's mesh-build-go judges its own change, ADR 0535) and the bases,
|
||||
// and composes the rest from their manifests, because a module built with the new recipe is built once the agreement
|
||||
// moves, a second time (ADR 0603 §3). The verdict names the changed modules whose fingerprint names the build module or
|
||||
// the base the change alters, as warnings of its composition: words for the operator, no part decides on them.
|
||||
|
||||
// buildChangeWarnings are the composition warnings of a change that alters the Go build module, or a module other changed
|
||||
// modules are built on: one line each, naming those modules, sorted. None for a change that alters neither, or when the
|
||||
// manifests cannot be read (nothing is said that was not read).
|
||||
func (st *state) buildChangeWarnings(ctx context.Context) []string {
|
||||
changed := map[string]catalogue.Manifest{}
|
||||
for _, p := range st.c.Pulls {
|
||||
for module, cm := range p.Fps {
|
||||
if cm.FP2 == "" || cm.Manifest == "" || st.j.Read == nil {
|
||||
continue
|
||||
}
|
||||
m, err := st.j.Read.Manifest(ctx, cm.Manifest)
|
||||
if err != nil {
|
||||
st.j.logf("%s: the manifest of %s cannot be read, so the modules built with a changed build module or base are not named: %v", st.c.Key, module, err)
|
||||
return nil
|
||||
}
|
||||
changed[module] = m
|
||||
}
|
||||
}
|
||||
var out []string
|
||||
for _, t := range st.targets {
|
||||
if t.module != buildrun.GoBuildModule || !changesBuildGo(t.pull.Repository, t.changed) {
|
||||
continue
|
||||
}
|
||||
var built []string
|
||||
for module, m := range changed {
|
||||
if m.Build != nil && slices.ContainsFunc(m.Build.Artifacts, func(a catalogue.Artifact) bool { return a.Language == "go" }) {
|
||||
built = append(built, module)
|
||||
}
|
||||
}
|
||||
slices.Sort(built)
|
||||
if len(built) > 0 {
|
||||
out = append(out, fmt.Sprintf("the change alters the build module %s: %s, built with it, are composed from their manifests and built again once the agreed toolchain moves (ADR 0603 §10)",
|
||||
buildrun.GoBuildModule, strings.Join(built, ", ")))
|
||||
}
|
||||
break
|
||||
}
|
||||
g := graph.OfManifests(changed)
|
||||
for _, base := range slices.Sorted(maps.Keys(changed)) {
|
||||
if on := g.Dependents(base); len(on) > 0 {
|
||||
out = append(out, fmt.Sprintf("the change alters %s, a base of %s: they are composed from their manifests and built after it (ADR 0603 §10)",
|
||||
base, strings.Join(on, ", ")))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -3,9 +3,12 @@ package check
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
@@ -99,3 +102,51 @@ func TestACompositionNotHadIsNotRun(t *testing.T) {
|
||||
type fixedTrunks struct{ heads map[string]string }
|
||||
|
||||
func (f fixedTrunks) Heads() map[string]string { return f.heads }
|
||||
|
||||
// ADR 0603 §10: a change that alters the Go build module, or a module others stand on, names in the composition's warnings
|
||||
// (and so the check's line) the changed modules built with it, from their manifests; a change that alters neither says nothing.
|
||||
func TestAChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) {
|
||||
dig := func(c string) string { return strings.Repeat(c, 64) }
|
||||
goBuild := &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "bin", Language: "go"}}}
|
||||
for _, row := range []struct {
|
||||
name string
|
||||
changed []string
|
||||
manifest map[string]catalogue.Manifest
|
||||
want []string
|
||||
}{
|
||||
{name: "the build module changes", changed: []string{buildGoFile},
|
||||
manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}, "mail": {Module: "mail", Build: goBuild}, "docs": {Module: "docs"}},
|
||||
want: []string{"mesh-build-go: mail, web, built with it"}},
|
||||
{name: "a base changes", changed: []string{"README.md"},
|
||||
manifest: map[string]catalogue.Manifest{"image": {Module: "image"},
|
||||
"web": {Module: "web", Build: &catalogue.Build{On: []catalogue.BuildsOn{{Arg: "BASE", Module: "image", Artifact: "img"}}}}},
|
||||
want: []string{"alters image, a base of web"}},
|
||||
{name: "neither changes", changed: []string{"README.md"},
|
||||
manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}}},
|
||||
} {
|
||||
b := candidateBench(t, true)
|
||||
b.fetch.Changed = row.changed
|
||||
b.ex.Compositions = &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionPass, SDK: "x", Machines: []string{"g14"}}}
|
||||
read, fps := fixedManifests{}, map[string]delivery.CandidateModule{}
|
||||
for module, m := range row.manifest {
|
||||
d := dig(string(rune('a' + len(read))))
|
||||
read[d], fps[module] = m, delivery.CandidateModule{FP2: "src2:" + dig("f"), Manifest: d}
|
||||
}
|
||||
b.ex.Read = read
|
||||
p := pullOf(catalogueRepository, 9, head1)
|
||||
p.Fps, p.Modules = fps, slices.Sorted(maps.Keys(fps))
|
||||
v := b.judge(t, caseOf(t, p)).Verdict
|
||||
if v.State != delivery.VerdictPass || v.Composition == nil {
|
||||
t.Fatalf("%s: %+v", row.name, v)
|
||||
}
|
||||
words := compositionWords(v.Composition, v.Took.Composition)
|
||||
for _, w := range row.want {
|
||||
if !strings.Contains(words, w) {
|
||||
t.Errorf("%s: the line says %q, want it to name %q", row.name, words, w)
|
||||
}
|
||||
}
|
||||
if row.want == nil && v.Composition.Words != "" {
|
||||
t.Errorf("%s: warned %q though neither a build module nor a base changed", row.name, v.Composition.Words)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,6 +287,7 @@ func (st *state) composed(ctx context.Context, v *delivery.Verdict) {
|
||||
v.Composition = &delivery.Composition{State: delivery.CompositionNotRun, Words: "not made: " + err.Error()}
|
||||
return
|
||||
}
|
||||
comp.Warnings = append(comp.Warnings, st.buildChangeWarnings(ctx)...) // ADR 0603 §10
|
||||
withComposition(v, comp)
|
||||
if v.Composition != nil && v.Composition.State != delivery.CompositionNotRun {
|
||||
v.Composition.FPSet = st.c.FPSet // a carry keeps it while the fingerprints stay the same (ADR 0548 §5)
|
||||
|
||||
@@ -2,12 +2,14 @@ package index
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
)
|
||||
@@ -60,11 +62,18 @@ func graphNode(e graphEdges, providers func(string) []catalogue.Manifest) graph.
|
||||
}
|
||||
|
||||
// writeGraph writes the graph node of every module of edges whose value changed, at most GraphMost of them, then
|
||||
// deletes each graph key whose module the modules state no longer holds. Nil Graph writes nothing.
|
||||
// deletes each graph key whose module the modules state no longer holds. Nil Graph writes nothing, and neither does a
|
||||
// graph state that is not on the bus yet (process.ErrNotYet; main.go: until then no graph is written), said in the log;
|
||||
// a listing that fails otherwise skips the deletion pass, said, and never fails the merge or the recompute that wrote.
|
||||
func (ix *Indexer) writeGraph(ctx context.Context, edges map[string]graphEdges) error {
|
||||
if ix.Graph == nil {
|
||||
return nil
|
||||
}
|
||||
keys, listErr := ix.Graph.Keys(ctx)
|
||||
if errors.Is(listErr, process.ErrNotYet) {
|
||||
ix.logf("the module graph is not written: its state is not on the bus yet")
|
||||
return nil
|
||||
}
|
||||
names := slices.Sorted(maps.Keys(edges))
|
||||
if len(names) > GraphMost {
|
||||
ix.logf("the module graph holds at most %d modules: %d are known, so %s and the %d after it are not written",
|
||||
@@ -104,19 +113,21 @@ func (ix *Indexer) writeGraph(ctx context.Context, edges map[string]graphEdges)
|
||||
}
|
||||
}
|
||||
held, err := ix.Modules.Keys(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing the modules for the graph: %w", err)
|
||||
}
|
||||
keys, err := ix.Graph.Keys(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing the graph: %w", err)
|
||||
switch {
|
||||
case listErr != nil:
|
||||
ix.logf("the graph keys of modules the modules state no longer holds are not deleted: listing the graph: %v", listErr)
|
||||
return nil
|
||||
case err != nil:
|
||||
ix.logf("the graph keys of modules the modules state no longer holds are not deleted: listing the modules: %v", err)
|
||||
return nil
|
||||
}
|
||||
for _, key := range keys {
|
||||
if slices.Contains(held, key) {
|
||||
continue
|
||||
}
|
||||
if err := write(ctx, ix.Graph, key, func(_ []byte, found bool) ([]byte, bool, error) { return nil, found, nil }); err != nil {
|
||||
return fmt.Errorf("graph.%s: %w", key, err)
|
||||
ix.logf("graph.%s is not deleted: %v", key, err)
|
||||
continue
|
||||
}
|
||||
ix.logf("graph.%s deleted: the modules state holds no module %s", key, key)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ const (
|
||||
// key under the indexer; unbound, when set, is every read's error (a state not yet on the bus).
|
||||
type memStore struct {
|
||||
unbound error
|
||||
keysErr error // the listing's error
|
||||
vals map[string][]byte
|
||||
revs map[string]uint64
|
||||
next uint64
|
||||
@@ -36,6 +37,9 @@ func (m *memStore) Get(_ context.Context, key string) ([]byte, uint64, bool, err
|
||||
return v, m.revs[key], ok, nil
|
||||
}
|
||||
func (m *memStore) Keys(context.Context) ([]string, error) {
|
||||
if m.keysErr != nil {
|
||||
return nil, m.keysErr
|
||||
}
|
||||
var keys []string
|
||||
for k := range m.vals {
|
||||
keys = append(keys, k)
|
||||
|
||||
@@ -2,12 +2,15 @@ package index
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
|
||||
@@ -114,12 +117,17 @@ func toolchainValue(t *testing.T, language, version, recipe string) []byte {
|
||||
}
|
||||
|
||||
func reportToolchain(t *testing.T, ix *Indexer, module, node, language, version string) {
|
||||
t.Helper()
|
||||
reportToolchainRecipe(t, ix, module, node, language, version, "1")
|
||||
}
|
||||
|
||||
func reportToolchainRecipe(t *testing.T, ix *Indexer, module, node, language, version, recipe string) {
|
||||
t.Helper()
|
||||
key, err := delivery.ToolchainKey(node, language)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ix.ApplyToolchain(context.Background(), module, key, toolchainValue(t, language, version, "1"), false); err != nil {
|
||||
if err := ix.ApplyToolchain(context.Background(), module, key, toolchainValue(t, language, version, recipe), false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -172,6 +180,31 @@ func TestAgreedToolchainChangesAGoModulesReasonWithNoMerge(t *testing.T) { // 14
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0603 §3 (D9): a source revision that changes the Go build module's recipe and a Go module builds the module at once, with
|
||||
// the recipe the nodes run, and again once the agreement moves to the new one: the module never waits and never loses its
|
||||
// fingerprint between the two, and its toolchain edge names the recipe each was computed with.
|
||||
func TestARecipeChangeBuildsAGoModuleAtOnceAndAgainOnceTheAgreementMoves(t *testing.T) {
|
||||
ix, mods := newFormIndexer(t, mod{"avahi", catalogueManifest(t, "avahi")})
|
||||
for _, node := range []string{"novox", "ace"} {
|
||||
reportToolchain(t, ix, "mesh-build-go", node, "go", "go1.27.1")
|
||||
}
|
||||
first, _ := moduleOf(t, mods, "avahi")
|
||||
if first.FP2 == "" || graphOf(t, ix, "avahi").Toolchains["go"].Recipe != "1" {
|
||||
t.Fatalf("with the running recipe: fp2 %q, graph %+v", first.FP2, graphOf(t, ix, "avahi"))
|
||||
}
|
||||
// The release carrying the new recipe is merged, but the nodes still run the old one: one node reporting the new recipe is
|
||||
// no strict majority, so the fingerprint stays, nothing waits.
|
||||
reportToolchainRecipe(t, ix, "mesh-build-go", "novox", "go", "go1.27.1", "2")
|
||||
if held, _ := moduleOf(t, mods, "avahi"); held.FP2 != first.FP2 || held.None2 != "" {
|
||||
t.Errorf("one node on the new recipe: fp2 %q none2 %q, want the fingerprint of the running recipe, %q", held.FP2, held.None2, first.FP2)
|
||||
}
|
||||
reportToolchainRecipe(t, ix, "mesh-build-go", "ace", "go", "go1.27.1", "2")
|
||||
again, _ := moduleOf(t, mods, "avahi")
|
||||
if again.FP2 == "" || again.FP2 == first.FP2 || again.None2 != "" || graphOf(t, ix, "avahi").Toolchains["go"].Recipe != "2" {
|
||||
t.Errorf("the agreement moved: fp2 %q (was %q) none2 %q graph %+v, want a second fingerprint at recipe 2", again.FP2, first.FP2, again.None2, graphOf(t, ix, "avahi"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodesReportingDifferentVersionsLeaveTheReasonNamingBoth(t *testing.T) { // 1444/AC6
|
||||
// Both reports are read before the module's first fingerprint is computed (a fingerprint computed before they
|
||||
// disagree is kept: TestAModuleKeepsTheToolchainsItsFingerprintWasComputedWith).
|
||||
@@ -258,6 +291,34 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { //
|
||||
if _, held := graph.vals["avahi"]; !held {
|
||||
t.Error("graph.avahi went with another module")
|
||||
}
|
||||
// Review finding 3 (ADR 0603 §2): a graph state not on the bus yet (process.ErrNotYet), or one whose listing fails, never
|
||||
// fails a recompute; the first writes nothing and says so, the second skips the deletion pass and says so.
|
||||
graph.vals["ghost"], graph.revs["ghost"] = []byte("x"), 1
|
||||
var said []string
|
||||
ix.Logf = func(format string, args ...any) { said = append(said, fmt.Sprintf(format, args...)) }
|
||||
for _, row := range []struct {
|
||||
name string
|
||||
err error
|
||||
say string
|
||||
}{
|
||||
{"not on the bus yet", process.ErrNotYet, "not on the bus yet"},
|
||||
{"a listing that fails", errors.New("the bus is slow"), "are not deleted"},
|
||||
} {
|
||||
said, graph.keysErr = nil, row.err
|
||||
if err := ix.Recompute(context.Background()); err != nil {
|
||||
t.Errorf("%s: the recompute failed: %v", row.name, err)
|
||||
}
|
||||
if _, held := graph.vals["ghost"]; !held || !strings.Contains(strings.Join(said, "\n"), row.say) {
|
||||
t.Errorf("%s: ghost held %v, log %q, want it kept and %q said", row.name, held, said, row.say)
|
||||
}
|
||||
}
|
||||
graph.keysErr = nil
|
||||
if err := ix.Recompute(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, held := graph.vals["ghost"]; held {
|
||||
t.Error("graph.ghost is still held once the graph can be listed")
|
||||
}
|
||||
a := agentBuild("build-1792587792086548572", agentSrcY, "2026-10-21T15:03:00Z")
|
||||
_ = ix.Builds.Put(a[1].([]byte))
|
||||
if err := ix.Recompute(context.Background()); err != nil {
|
||||
|
||||
@@ -76,6 +76,11 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
|
||||
if j, err := ix.JudgeAll(ctx, nil); err != nil || j.Written != 0 || len(said) != 1 {
|
||||
t.Fatalf("a judge that judges alike: %+v %v, said %d", j, err, len(said))
|
||||
}
|
||||
// ADR 0603 §4 (D9): a source revision that changes the indexer builds the new indexer, which at its start judges every
|
||||
// manifest it holds again, none of them fresh: all of them judged, nothing written for a verdict that stands.
|
||||
if j, err := ix.JudgeAtStart(ctx); err != nil || j.Judged != 2 || j.Written != 0 || len(said) != 1 {
|
||||
t.Fatalf("the new indexer at its start: %+v %v, said %d, want both manifests judged and nothing written", j, err, len(said))
|
||||
}
|
||||
// The manifest mended: its new digest is accepted and the module's refusal goes.
|
||||
mended := hold("bad", `{"module":"bad","version":"2"}`)
|
||||
if j, err := ix.JudgeAll(ctx, map[string]bool{mended: true}); err != nil || j.Written != 1 || len(j.Refused) != 0 {
|
||||
|
||||
Reference in New Issue
Block a user