test(agency): the Part L incident scenario end to end, and failure modes
The full incident scenario now runs against the assembled daemon: an operator goal becomes a want to reduce uncertainty, an investigation intention and a diagnosis plan; telemetry is read; logs need authority, so a scoped request with risk, mitigation and fallback is raised and the intention is blocked until an operator grants it; the logs are read with personal data masked and summarised back into cognition; the operator supplies the judgement that a restart should help; a standing commitment turns the restart into a request for approval; after approval the typed capability runs and health is verified rather than assumed; the fault persists, so a prediction error weakens the supporting belief, the plan is replanned into an escalation that reuses the live log grant and does not repeat the restart; once the database is fixed a second approved restart verifies, the procedure is learnt, and "Why did you restart atlas-worker?" is answered from the recorded chain and scores as supported. Changes the scenario exposed: - New operator goals are raised by the goal monitor, so a cycle runs and wants are derived from them. - Intentions link live beliefs about their action's target, so a failed expectation is evidence against the belief it rested on and the plan lists it as an assumption. - Reconsideration reacts to a plan's own intention only; a subordinate "obtain approval" intention shares the plan and previously caused the parent's plan to be closed. - The harness accepts a clock for the executor's verification windows. Further failure-mode tests: a retracted assumption replans a waiting plan away from the restart and withdraws its approval request; conflicting commitments are raised once in a cycle while both stay represented.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package agency
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/andydixon/noema/internal/beliefs"
|
||||
"github.com/andydixon/noema/internal/planning"
|
||||
"github.com/andydixon/noema/internal/volition"
|
||||
"github.com/andydixon/noema/pkg/events"
|
||||
"github.com/andydixon/noema/tests/harness"
|
||||
)
|
||||
|
||||
// Part I: new evidence invalidates an active plan. The restart rests on a
|
||||
// belief; when an operator retracts that belief, the waiting plan is
|
||||
// replanned without the restart and its approval request is withdrawn.
|
||||
func TestRetractedAssumptionInvalidatesPlan(t *testing.T) {
|
||||
h := harness.StartWith(t, harness.Options{HostRunner: newHost()})
|
||||
ctx := context.Background()
|
||||
m := h.Mind("assume")
|
||||
h.Do(h.Admin, "POST", "/api/v1/minds/assume/ask", map[string]any{"text": "Restarting atlas-worker clears the stalled queue."})
|
||||
var b beliefs.Belief
|
||||
waitFor(t, h, m, 20*time.Second, "the belief", func() bool {
|
||||
bs, _ := h.Deps.Beliefs.Search(ctx, m.ID, "restarting atlas-worker", 3)
|
||||
if len(bs) > 0 {
|
||||
b = bs[0]
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
h.Do(h.Admin, "POST", "/api/v1/minds/assume/ask", map[string]any{"text": "Please restart atlas-worker."})
|
||||
var it volition.Intention
|
||||
waitFor(t, h, m, 25*time.Second, "a restart waiting for approval", func() bool {
|
||||
open, _ := h.Deps.Volition.Open(ctx, nil, m.ID)
|
||||
for _, x := range open {
|
||||
if x.ParentID.IsZero() && x.Status == volition.IntentionBlocked && len(x.BeliefIDs) > 0 {
|
||||
it = x
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
p, _ := h.Deps.Plans.Get(ctx, nil, it.PlanID)
|
||||
if len(p.Assumptions) == 0 || p.Assumptions[0].BeliefID != b.ID {
|
||||
t.Fatalf("plan does not record its assumption: %+v", p.Assumptions)
|
||||
}
|
||||
if code, out := h.Do(h.Admin, "POST", "/api/v1/minds/assume/beliefs/"+b.ID.String()+"/retract", map[string]any{"reason": "the queue is not the problem"}); code != 200 {
|
||||
t.Fatalf("retract: %d %s", code, out)
|
||||
}
|
||||
waitFor(t, h, m, 20*time.Second, "a replan", func() bool {
|
||||
ps, _ := h.Deps.Plans.ForIntention(ctx, nil, it.ID)
|
||||
return len(ps) == 2 && ps[0].Status == planning.PlanSuperseded
|
||||
})
|
||||
if ap, _ := h.Deps.Capabilities.Approvals(ctx, m.ID, "pending", 5); len(ap) != 0 && ap[0].Capability == "service.restart" {
|
||||
t.Fatal("approval for the invalidated restart still pending")
|
||||
}
|
||||
ps, _ := h.Deps.Plans.ForIntention(ctx, nil, it.ID)
|
||||
if ps[1].Strategy == "direct" {
|
||||
t.Fatal("replanned into the same restart")
|
||||
}
|
||||
found := false
|
||||
for _, ev := range h.EventsOf(m.ID, events.IntentionReconsidered) {
|
||||
found = found || strings.Contains(ev.Summary, "assumption no longer holds")
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("reconsideration not recorded with its reason")
|
||||
}
|
||||
}
|
||||
|
||||
// Part I: conflicting commitments are detected in a cycle and both stay represented.
|
||||
func TestConflictingCommitmentsAreRaised(t *testing.T) {
|
||||
h := harness.StartWith(t, harness.Options{HostRunner: newHost()})
|
||||
ctx := context.Background()
|
||||
m := h.Mind("conflicted")
|
||||
h.Do(h.Admin, "POST", "/api/v1/minds/conflicted/commitments", map[string]any{"description": "email the weekly report to Andy", "kind": "promise", "polarity": "do", "matcher": map[string]any{"capabilities": []string{"send_email"}}})
|
||||
h.Do(h.Admin, "POST", "/api/v1/minds/conflicted/commitments", map[string]any{"description": "never send email", "kind": "operator_instruction", "polarity": "refrain", "matcher": map[string]any{"capabilities": []string{"send_*"}}})
|
||||
h.Do(h.Admin, "POST", "/api/v1/minds/conflicted/observations", map[string]any{"text": "The weekly report is ready."})
|
||||
waitFor(t, h, m, 20*time.Second, "a commitment conflict", func() bool { return len(h.EventsOf(m.ID, events.CommitmentConflict)) == 1 })
|
||||
cs, _ := h.Deps.Volition.Commitments(ctx, nil, m.ID, []string{"active"}, 10)
|
||||
if len(cs) != 2 {
|
||||
t.Fatalf("both commitments should stay active and represented: %+v", cs)
|
||||
}
|
||||
h.Settle(m.ID)
|
||||
if n := len(h.EventsOf(m.ID, events.CommitmentConflict)); n != 1 {
|
||||
t.Fatalf("conflict raised %d times", n)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user