Files
ARTEX/agent/stagnation_test.go
BaaRai b237fbaa32 feat(engine): ouroboros takes - goal ambiguity gate, failover escalation, stagnation surfacing
Three orchestration mechanisms from the Q00/ouroboros evaluation (#99):

- Goal ambiguity gate at task creation: a goal must name a target (URL,
  domain, IPv4, localhost:port or absolute path) and reach a length floor,
  else the API returns 400 before any token is spent. Deliberately
  target-less goals opt out with the literal marker [targetless]. Guards
  against the live failure where a goal naming no target sent every worker
  to an unrelated asset.

- Consecutive-failure escalation on the task LLM chain: quota exhaustion
  already advances the chain; now 3 consecutive non-quota call failures on
  the same chain profile advance it too, with the true reason recorded.
  Escalation-only by design (no auto-downgrade: mid-task model swaps risk
  behavior drift; the operator keeps the manual chain reset).

- Stagnation surfacing in the planner overview: finished intents are
  fingerprinted (case-folded, digits collapsed); a direction dispatched 3+
  times with zero findings since its first attempt is flagged in a
  self-describing stagnation_warning payload that instructs the planner to
  diversify instead of grinding. Absent when nothing stagnates.

Closes #99
2026-10-21 14:46:56 +02:00

40 lines
1.4 KiB
Go

package agent
import (
"encoding/json"
"testing"
"github.com/Autumn-27/artex/db"
)
func nodeWithSummary(id int64, summary string) *db.Node {
payload, _ := json.Marshal(map[string]any{"summary": summary})
return &db.Node{ID: id, Kind: db.KindIntent, State: "done", Payload: payload}
}
func TestDetectStagnation(t *testing.T) {
done := []*db.Node{
nodeWithSummary(10, "Enumerate endpoints on https://target.example.com/api/v2"),
nodeWithSummary(22, "Enumerate endpoints on https://target.example.com/api/v3"),
nodeWithSummary(35, "enumerate endpoints on https://target.example.com/api/v9"),
nodeWithSummary(40, "Review authentication flow for token replay"),
}
if got := detectStagnation(done, nil); len(got) != 1 {
t.Fatalf("expected 1 stagnating direction, got %d: %+v", len(got), got)
} else if got[0]["repeats"] != 3 {
t.Fatalf("repeats=%v want 3", got[0]["repeats"])
}
// a finding produced after the first repeat disarms the direction
findingPayload, _ := json.Marshal(map[string]any{"summary": "IDOR found"})
findings := []*db.Node{{ID: 25, Kind: db.KindFinding, Payload: findingPayload}}
if got := detectStagnation(done, findings); len(got) != 0 {
t.Fatalf("expected no stagnation once a finding exists, got %+v", got)
}
// below the repeat threshold: nothing
if got := detectStagnation(done[:2], nil); len(got) != 0 {
t.Fatalf("expected no stagnation below threshold, got %+v", got)
}
}