//go:build integration package main import ( "context" "strings" "testing" "time" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) func TestTheReloginConditionSaysTheWaitAndClearsAfterTheLogin(t *testing.T) { withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks) open := aMesh(t) ctx := t.Context() k, _ := withConditionsInMemory(t) at := h0 say := func(rs ...link.ResourceHealth) { t.Helper() at = at.Add(time.Second) for i := range rs { rs[i].Since = at } if err := stateHealth(ctx, open.inventory, k, "laptop", link.Health{Contract: link.ReadinessContract, At: at, Resources: rs}, at); err != nil { t.Fatal(err) } } asLink := func(r inventory.ResourceHealth) link.ResourceHealth { return link.ResourceHealth{Module: r.Module, Resource: r.Resource, Kind: r.Kind, Target: r.Target, State: r.State, Reason: r.Reason, Account: r.Account} } account, unit := asLink(relogin("lights", "operator")), asLink(userUnit("lights", "operator")) openNow := func() []conditions.Condition { t.Helper() list, err := k.Open(ctx) if err != nil { t.Fatal(err) } return list } say(account, unit) say(account, unit) list := openNow() if len(list) != 1 || list[0].Key != "module.lights.laptop.relogin-needed" { t.Fatalf("raised %+v; want lights' relogin-needed alone", list) } c := list[0] if c.Severity != conditions.Warning || c.Resolver != conditions.ResolverOperator || !strings.HasPrefix(c.Summary, "relogin needed on laptop:") { t.Fatalf("the condition is %s %s %q", c.Severity, c.Resolver, c.Summary) } // After the new login the account is healthy, and the unit, still failing, is the module's own fault. account.State, account.Reason = link.StateHealthy, "" say(account, unit) list = openNow() if len(list) != 1 || list[0].Key != "module.lights.laptop.unhealthy" { t.Fatalf("after the login: %+v; want lights' own unhealthy condition alone", list) } unit.State, unit.Reason = link.StateHealthy, "" say(account, unit) if list = openNow(); len(list) != 0 { t.Fatalf("after the unit runs: %+v; want nothing open", list) } } func TestAModuleHealthyOnItsOwnKeepsItsPassWhenItsSendFails(t *testing.T) { withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks) b := aBacklog(t) ctx := t.Context() inv := b.open.inventory releaseHeard = func(context.Context, *stores) (map[string]bool, error) { return map[string]bool{"laptop": true}, nil } backlogFacts := gatherGateFacts gatherGateFacts = func(ctx context.Context, open *stores, component string) (gateFacts, error) { f, err := backlogFacts(ctx, open, component) f.health = map[string]inventory.NodeHealth{"laptop": {Node: "laptop", HeardAt: time.Now(), Resources: []inventory.ResourceHealth{ {Module: "app", Resource: "app.web", Kind: "container", Target: "app", State: link.StateHealthy}, {Module: "late", Resource: "late.web", Kind: "container", Target: "late", State: link.StateUnhealthy, Reason: "down"}}}} return f, err } gateEvery, gateBound = 0, 300*time.Millisecond deadline := time.Now().Add(5 * time.Second) for time.Now().Before(deadline) { advancePlans(ctx, b.open) if p := b.release(t); p.State != inventory.PlanRolling { break } time.Sleep(20 * time.Millisecond) } p := b.release(t) if p.State != inventory.PlanFailed { t.Fatalf("the walk is %s: %s; want it failed on late", p.State, p.Note) } if v, found, err := inv.GateOf(ctx, "build-app-c2"); err != nil || !found || v.Verdict != inventory.GatePassed || !strings.Contains(v.Why, "on its own") { t.Fatalf("app's verdict: %+v (found %v, %v); want its own pass kept", v, found, err) } if failed, _ := inv.GateFailed(ctx, "build-late-c2"); !failed { t.Fatal("late's build is not marked failed at its gate") } current, _ := inv.CurrentBuilds(ctx) if current["app"].Commit != "c2" || current["late"].Commit != "c1" { t.Fatalf("registered: app %s, late %s; want app kept at c2 and late put back to c1", current["app"].Commit, current["late"].Commit) } if !strings.Contains(p.Note, "passed on their own and kept: app") { t.Errorf("the walk does not say what kept its pass: %s", p.Note) } } func TestAConditionThatBecameTheOtherKindSaysSoWhenItClears(t *testing.T) { withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks) open := aMesh(t) ctx := t.Context() store := conditions.NewInMemory() told := &conditions.Told{} k := conditions.NewKeeper(ctx, conditions.Options{Store: store, History: store, Teller: told}) t.Cleanup(func() { k.Close(context.Background()) }) at := h0 say := func(rs ...link.ResourceHealth) { t.Helper() at = at.Add(time.Second) for i := range rs { rs[i].Since = at } if err := stateHealth(ctx, open.inventory, k, "laptop", link.Health{Contract: link.ReadinessContract, At: at, Resources: rs}, at); err != nil { t.Fatal(err) } } unit := link.ResourceHealth{Module: "lights", Resource: "lights.daemon", Kind: link.KindUnit, Target: "lights.service", Account: "operator", State: link.StateUnhealthy, Reason: "failed in the account's own service manager (exit-code)"} account := link.ResourceHealth{Module: "lights", Resource: "lights.operator", Kind: link.KindAccount, Target: "operator", Account: "operator", State: link.StateUnhealthy, Reason: link.ReasonRelogin + ": operator is in the group lights"} say(unit) say(unit) // lights not working say(account, unit) say(account, unit) // now it waits for a login var resolved []string for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline) && len(resolved) == 0; { time.Sleep(20 * time.Millisecond) for _, e := range told.Said() { if e.Change == conditions.ChangeCleared { resolved = append(resolved, e.Condition.Key+": "+e.Condition.Resolved) // The clearing line is held to the plain rule too (ADR 0253). w := conditions.Words{Headline: e.Condition.Headline, Explanation: e.Condition.Explanation, Resolved: e.Condition.Resolved, Needs: e.Condition.Needs} if why, ok := conditions.PlainWords(w, "laptop"); !ok { t.Errorf("%s: its clearing words are not plain: %s", e.Condition.Key, why) } } } } if len(resolved) != 1 || !strings.Contains(resolved[0], "module.lights.laptop.unhealthy: lights on laptop now waits only for a new login") { t.Fatalf("cleared %v; want the not-working condition cleared as now waiting for a login", resolved) } }