|
|
|
@@ -0,0 +1,119 @@
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"reflect"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
|
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// keptRefusals is a store that keeps refusals, by id.
|
|
|
|
|
type keptRefusals map[string]string
|
|
|
|
|
|
|
|
|
|
func (k keptRefusals) RecordRefused(_ context.Context, id, _, why string) error {
|
|
|
|
|
if _, ok := k[id]; !ok {
|
|
|
|
|
k[id] = why
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// failedPlanOf is a plan already failed on gitea, its tier also asking module under id: open() does not count it, so
|
|
|
|
|
// no outcome taken in reaches it (as plan-1792748906937927069 on 2026-10-23 11:51).
|
|
|
|
|
func failedPlanOf(module, id string, asked time.Time) inventory.Plan {
|
|
|
|
|
return inventory.Plan{ID: "plan-1713", State: inventory.PlanFailed, Tier: 0,
|
|
|
|
|
Tiers: [][]string{{"gitea", module}}, Note: "gitea failed to build in tier 0",
|
|
|
|
|
Modules: map[string]*inventory.PlanModule{
|
|
|
|
|
"gitea": {State: "failed", AskedAt: &asked, Build: "build-1713-gitea", Why: "not registered"},
|
|
|
|
|
module: {State: "asked", AskedAt: &asked, Build: id},
|
|
|
|
|
}}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// retried is what a Retry of the plan asks again, its records read as recordsOfAsked reads them: the build record of
|
|
|
|
|
// id (which says the builder's words, worked) with every refusal kept for it in the store and in this process.
|
|
|
|
|
func retried(p *inventory.Plan, store keptRefusals, id, module string, at time.Time) []string {
|
|
|
|
|
byID := map[string]inventory.Build{id: {ID: id, Module: module, Commit: "6c0024e4", At: at}}
|
|
|
|
|
refused := map[string]string{}
|
|
|
|
|
for k, why := range store {
|
|
|
|
|
refused[k] = why
|
|
|
|
|
}
|
|
|
|
|
for k, why := range refusalsKeptFor([]string{id}) {
|
|
|
|
|
refused[k] = why
|
|
|
|
|
}
|
|
|
|
|
overlayRefusals(nil, byID, refused)
|
|
|
|
|
return toRetry(p, nil, byID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1713/AC1: given a build of a module heard and recorded and not registered while its plan is failed and the plans are
|
|
|
|
|
// not held, when the controller takes it in, then the refusal is kept by id, and a Retry asks the module again instead
|
|
|
|
|
// of settling it built and sending its old version (plan-1792748906937927069 sent mosquitto's on 2026-10-23 12:01).
|
|
|
|
|
func TestARefusalWhileThePlanIsFailedIsKeptAndRetried(t *testing.T) {
|
|
|
|
|
asked := time.Date(2026, 10, 23, 11, 51, 0, 0, time.UTC)
|
|
|
|
|
id := "build-1713-ac1-mosquitto"
|
|
|
|
|
p := failedPlanOf("mosquitto", id, asked)
|
|
|
|
|
store := keptRefusals{}
|
|
|
|
|
var took []string
|
|
|
|
|
takeRefused(t.Context(), store, link.BuildResult{ID: id, Commit: "6c0024e4"}, "mosquitto",
|
|
|
|
|
"the trunk requires no status", refusedOutcome{
|
|
|
|
|
built: func(module, why string) { took = append(took, module+": "+why) },
|
|
|
|
|
failedBuild: func(link.BuildResult) { t.Fatal("a refusal with its module was taken by id") },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if !reflect.DeepEqual(took, []string{"mosquitto: the trunk requires no status"}) {
|
|
|
|
|
t.Fatalf("the plans were told %v", took)
|
|
|
|
|
}
|
|
|
|
|
if store[id] == "" {
|
|
|
|
|
t.Fatal("the refusal was not kept in the store")
|
|
|
|
|
}
|
|
|
|
|
if got := retried(&p, keptRefusals{}, id, "mosquitto", asked.Add(time.Minute)); !reflect.DeepEqual(got,
|
|
|
|
|
[]string{"gitea", "mosquitto"}) {
|
|
|
|
|
t.Fatalf("a Retry, the refusal kept only in this controller, asks %v; mosquitto was settled %q",
|
|
|
|
|
got, p.Modules["mosquitto"].State)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1713/AC2: given a build that came back without a readable manifest and was not registered, when the controller takes
|
|
|
|
|
// it in, then the asking plan is failed by the build's id and the refusal is kept by id, so a Retry asks it again.
|
|
|
|
|
func TestARefusalWithoutAModuleFailsItsPlanByIDAndIsRetried(t *testing.T) {
|
|
|
|
|
asked := time.Date(2026, 10, 23, 11, 51, 0, 0, time.UTC)
|
|
|
|
|
id := "build-1713-ac2-ssh-client"
|
|
|
|
|
p := failedPlanOf("ssh-client", id, asked)
|
|
|
|
|
store := keptRefusals{}
|
|
|
|
|
var failed []link.BuildResult
|
|
|
|
|
takeRefused(t.Context(), store, link.BuildResult{ID: id, Commit: "6c0024e4"}, "",
|
|
|
|
|
"what came back is not a manifest", refusedOutcome{
|
|
|
|
|
built: func(module, _ string) { t.Fatalf("a refusal without a module was taken as %q", module) },
|
|
|
|
|
failedBuild: func(r link.BuildResult) { failed = append(failed, r) },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if len(failed) != 1 || failed[0].ID != id || !strings.Contains(failed[0].Failed, "not a manifest") {
|
|
|
|
|
t.Fatalf("the asking plan was not failed by the build's id: %+v", failed)
|
|
|
|
|
}
|
|
|
|
|
if store[id] == "" {
|
|
|
|
|
t.Fatal("the refusal was not kept in the store")
|
|
|
|
|
}
|
|
|
|
|
if got := retried(&p, store, id, "", asked.Add(time.Minute)); !reflect.DeepEqual(got,
|
|
|
|
|
[]string{"gitea", "ssh-client"}) {
|
|
|
|
|
t.Fatalf("a Retry asks %v; ssh-client was settled %q", got, p.Modules["ssh-client"].State)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 1713/AC3: given a refused build kept by id, when the records settle a building plan's tier, then the module and the
|
|
|
|
|
// plan are failed, never built: the plan never reports done for it.
|
|
|
|
|
func TestAPlanNeverSettlesARefusedBuildBuilt(t *testing.T) {
|
|
|
|
|
asked := time.Date(2026, 10, 23, 11, 51, 0, 0, time.UTC)
|
|
|
|
|
id := "build-1713-ac3-mesh-build-go"
|
|
|
|
|
takeRefused(t.Context(), keptRefusals{}, link.BuildResult{ID: id}, "mesh-build-go", "not registered",
|
|
|
|
|
refusedOutcome{built: func(string, string) {}, failedBuild: func(link.BuildResult) {}})
|
|
|
|
|
p := inventory.Plan{ID: "plan-1713-ac3", State: inventory.PlanBuilding, Tiers: [][]string{{"mesh-build-go"}},
|
|
|
|
|
Modules: map[string]*inventory.PlanModule{"mesh-build-go": {State: "asked", AskedAt: &asked, Build: id}}}
|
|
|
|
|
byID := map[string]inventory.Build{id: {ID: id, Module: "mesh-build-go", At: asked.Add(time.Minute)}}
|
|
|
|
|
overlayRefusals(nil, byID, refusalsKeptFor([]string{id}))
|
|
|
|
|
settleFromRecords(&p, p.Tiers[0], nil, byID)
|
|
|
|
|
if s := p.Modules["mesh-build-go"]; s.State != "failed" || p.State != inventory.PlanFailed {
|
|
|
|
|
t.Fatalf("a refused build settled as %q, the plan %q", s.State, p.State)
|
|
|
|
|
}
|
|
|
|
|
}
|