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: link.KilledByHand}},
|
|
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)
|
|
}
|
|
}
|