The controller asks no build of anybody (ADR 0501 §5, ADR 0525 clause 6, ADR 0503 §3). Every module of a walk is taken from the builder's done key; a key not done yet waits, named in the plan, and never fails it. - C2b/C2c: internal/builder and cmd/mesh-controller's asks go (askABuild, onlyTheBuildersOwn, buildOneAsked, takeIn from an outcome, buildAgentsOwn, the builder's say and its fallback, the daemon hearing an outcome). The server follows the retired seats' built subjects only to let go of them, because the installer's carried user list names them. - C2d: replay, queue, cancel, clear, kill, pause, resume, delivery-check, check-here and merge-gate are refused by name. - C3: the check queues (CHECKS_READY, CHECKS_OTHER) and their workers, the checks-running and plans-building buckets and their grants, the build queue and its hold (plans_building.go, queue.go, link/holds.go, builds_nats.go, checks_nats.go). - C4: merge_gate*.go, the pull request's repo-check path, the Checked event and its layers; a pull request asks the controller nothing but to tell mesh-config what its head declares. - C5: the builds bucket is written only by key_takein. - C10 (help and refusals): the terminal's help lists only what is served. Integration tests of the plan's tiers, sends, stops and retries now run on a stand-in builder with a done key for every module (walk_keys_scaffold_integration_test.go); the tests of the asks, the queue, the merge gate and the replays are deleted.
142 lines
5.1 KiB
Go
142 lines
5.1 KiB
Go
//go:build integration
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
"github.com/novox/mesh-controller/internal/testbus"
|
|
)
|
|
|
|
// 1315/AC3: the operator's Retry on a failed plan, acted on through the real callAction and the controller's own
|
|
// `plans` verb, sets the plan building again at its tier and ends its plan-failed condition (novox/hq ADR 0449 §1).
|
|
func TestAWarrantForRetryRetriesAFailedPlanInTheStoreAndEndsItsCondition(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
asked := asksRecorded(t)
|
|
planMovedHeard(t) // the bus the verb says its moves on, and the hand-act log
|
|
twoTiers(t, open)
|
|
before := time.Now().UTC().Add(-time.Hour)
|
|
ended := before.Add(time.Minute)
|
|
failed := inventory.Plan{ID: "plan-1315-retry", Repository: "novox/a", Branch: "main", Commit: "c0ffee",
|
|
Created: before, State: inventory.PlanFailed, Tier: 0, Tiers: [][]string{{"a"}, {"b"}},
|
|
Note: "a failed to build in tier 0",
|
|
Delivery: &inventory.PlanDelivery{},
|
|
Modules: map[string]*inventory.PlanModule{"a": {State: "failed", AskedAt: &before, Build: "build-1",
|
|
Why: "killed by hand"}},
|
|
Times: &inventory.PlanTimes{Cut: &before, Started: &before, Ended: &ended, Class: inventory.ClassLeaf}}
|
|
if err := open.inventory.SavePlan(ctx, &failed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
conn, err := nats.Connect(testbus.URL(t))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer conn.Close()
|
|
// The controller's seat answers `plans` as it does: by running the verb.
|
|
sub, err := conn.Subscribe(link.SeatToolSubject("mesh-controller", "plans"), func(m *nats.Msg) {
|
|
var args map[string]string
|
|
_ = json.Unmarshal(m.Data, &args)
|
|
answer := link.Answer{Result: json.RawMessage(`"ok"`)}
|
|
if len(args) != 1 || args["retry"] == "" {
|
|
answer = link.Answer{Error: "the retry was asked with more than the plan"}
|
|
} else if err := plansCommand(context.Background(), []string{"retry", args["retry"]}); err != nil {
|
|
answer = link.Answer{Error: err.Error()}
|
|
}
|
|
body, _ := json.Marshal(answer)
|
|
_ = m.Respond(body)
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = sub.Unsubscribe() }()
|
|
|
|
// What S23 raises for it, read from the store as the watchdog reads it.
|
|
raised := func(context.Context) ([]conditions.Condition, error) {
|
|
facts, err := gatherFailedPlans(ctx, open.inventory, time.Now())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []conditions.Condition
|
|
for _, o := range watchPlansFailed(&signalFacts{now: time.Now(), failed: facts}) {
|
|
out = append(out, conditions.Condition{Key: o.Key(), Kind: o.Kind, Severity: o.Severity, Headline: o.Headline,
|
|
Explanation: conditions.Verdict(o.Needs, o.Explanation), Needs: o.Needs, Actions: o.Actions})
|
|
}
|
|
return out, nil
|
|
}
|
|
now := time.Now()
|
|
store := memAskedStore{}
|
|
var sent []published
|
|
var acts []link.HandAct
|
|
a := &asker{open: raised, store: store,
|
|
silence: func(context.Context, string, time.Duration, string, string) error { return nil },
|
|
publish: func(_ context.Context, subject string, body []byte, id string) error {
|
|
sent = append(sent, published{subject, id, body})
|
|
return nil
|
|
},
|
|
call: callAction(conn),
|
|
record: func(_ context.Context, act link.HandAct) error { acts = append(acts, act); return nil },
|
|
now: func() time.Time { return now },
|
|
logf: t.Logf}
|
|
cs, _ := raised(ctx)
|
|
if len(cs) != 1 || len(cs[0].Actions) != 1 {
|
|
t.Fatalf("the failed plan raised %+v", cs)
|
|
}
|
|
if err := a.reconcile(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var w asks.Warrant
|
|
for _, r := range store {
|
|
for _, o := range r.Ask.Options {
|
|
if o.Label == "Retry" {
|
|
w = asks.Warrant{Ask: r.ID, Asker: "mesh-controller", About: r.Condition, Outcome: asks.OutcomeChosen,
|
|
Option: o.ID, Label: o.Label, Level: o.Level, Channel: "telegram", Proofs: []string{"P1"}, At: now,
|
|
AskDigest: r.Ask.Digest(),
|
|
By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"}}
|
|
}
|
|
}
|
|
}
|
|
if w.Ask == "" {
|
|
t.Fatalf("no ask offers Retry: %+v", store)
|
|
}
|
|
body, _ := json.Marshal(w)
|
|
if err := a.Decided(ctx, body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := a.Decided(ctx, body); err != nil { // heard again: nothing more
|
|
t.Fatal(err)
|
|
}
|
|
|
|
p, err := open.inventory.PlanByID(ctx, failed.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := p.Modules["a"]
|
|
if p.State != inventory.PlanBuilding || p.Tier != 0 || m.State != "asked" || m.Build == "" || m.Build == "build-1" {
|
|
t.Fatalf("after the warrant the plan is %s at tier %d and a is %+v", p.State, p.Tier, m)
|
|
}
|
|
if len(*asked) != 1 {
|
|
t.Fatalf("the build was asked %d times: %v", len(*asked), *asked)
|
|
}
|
|
if got := store[w.Ask]; got.Acted != "done" {
|
|
t.Fatalf("the ask ended %q", got.Acted)
|
|
}
|
|
if len(acts) != 1 || acts[0].Verb != handActWarrant || !strings.Contains(strings.Join(acts[0].Args, " "), "retry="+failed.ID) {
|
|
t.Fatalf("the warrant was recorded as %+v", acts)
|
|
}
|
|
if cs, _ = raised(ctx); len(cs) != 0 {
|
|
t.Fatalf("the plan-failed condition did not end: %+v", cs)
|
|
}
|
|
}
|