Files
Agent-System/internal/engineeringdashboard/model_test.go
T
weichenandCursor e46ec0e4e7 feat(engineering-dashboard): U1 freeze snapshot and facet model
Define versioned Snapshot/Fact/Conflict/TaskState contracts with
independent plan/git/verification/deployment axes and acceptance fixtures.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 06:59:02 +00:00

153 lines
5.1 KiB
Go

package engineeringdashboard
import (
"encoding/json"
"strings"
"testing"
"time"
)
func TestModel_DocDoneDoesNotChangeGitOrDeployFacets(t *testing.T) {
task := SampleRDevTaskFixture()
task.DocStatus = DocStatusDone
if task.Git.Branch != "" || task.Git.Cleanliness != "unknown" {
t.Fatalf("git facets mutated from doc_status: %+v", task.Git)
}
for env, d := range task.Deployment {
if d.AppReadiness != "unknown" || d.ContainerStatus != "unknown" {
t.Fatalf("deployment facet for %s changed from doc_status: %+v", env, d)
}
}
}
func TestModel_ClaimedWithoutRuntimeKeepsActivityUnknown(t *testing.T) {
herdr, pi := SampleHerdrVsPIClaims()
if herdr.Claimed && herdr.Activity != "unknown" {
t.Fatalf("claimed must not imply running: %+v", herdr)
}
if herdr.AgentKind == pi.AgentKind {
t.Fatalf("herdr and product PI runner must not share kind: %s", herdr.AgentKind)
}
if err := ValidateAgentKind(herdr.AgentKind); err != nil {
t.Fatal(err)
}
if err := ValidateAgentKind(pi.AgentKind); err != nil {
t.Fatal(err)
}
}
func TestModel_ConflictRetainsAllFactRefs(t *testing.T) {
f1 := FactID("env:main", "health", "current")
f2 := FactID("env:main", "health", "docker")
c := NewConflict("env:main", "health", ConflictDeclaredVsObserved, []string{f2, f1}, "declared healthy vs events restarting")
if len(c.FactRefs) != 2 {
t.Fatalf("expected 2 fact refs, got %v", c.FactRefs)
}
if c.FactRefs[0] != f1 || c.FactRefs[1] != f2 {
t.Fatalf("refs not sorted/retained: %v", c.FactRefs)
}
}
func TestModel_JSONRoundTripPreservesIdentity(t *testing.T) {
now := time.Date(2026, 9, 17, 8, 0, 0, 0, time.UTC)
s := Snapshot{
SchemaVersion: SchemaVersion,
GenerationID: "gen_1",
GeneratedAt: now,
CollectionDuration: 1500 * time.Millisecond,
Repository: RepositoryIdentity{Root: "/repo", Branch: "dev", Head: "abc", Dirty: false},
Sources: []SourceState{{
Name: "docs", Status: SourceOK, Freshness: FreshnessFresh, ObservedAt: now, DurationMS: 10,
}},
Facts: []Fact{{
ID: "fact_a", Subject: "task:rdev", Field: "doc_status", Value: "planned",
Provenance: ProvenanceDeclared, SourceRef: "docs", ObservedAt: now,
}},
Tasks: []TaskState{SampleRDevTaskFixture()},
Git: []GitBranchView{SampleDevAheadGitFixture()},
Environments: []EnvironmentState{SampleMainEventsForkFixture()},
}
s.RevisionID = ComputeRevisionID(s)
raw, err := MarshalSnapshotJSON(s)
if err != nil {
t.Fatal(err)
}
got, err := UnmarshalSnapshotJSON(raw)
if err != nil {
t.Fatal(err)
}
if got.SchemaVersion != SchemaVersion || got.GenerationID != "gen_1" || got.RevisionID != s.RevisionID {
t.Fatalf("identity mismatch: %+v", got)
}
if got.CollectionDuration != 1500*time.Millisecond {
t.Fatalf("duration mismatch: %v", got.CollectionDuration)
}
var wire map[string]any
if err := json.Unmarshal(raw, &wire); err != nil {
t.Fatal(err)
}
if _, ok := wire["collection_duration_ms"]; !ok {
t.Fatal("missing collection_duration_ms in wire JSON")
}
}
func TestModel_RevisionIDIgnoresTimestamps(t *testing.T) {
base := Snapshot{
SchemaVersion: SchemaVersion,
Repository: RepositoryIdentity{Root: "/r", Branch: "dev", Head: "h1"},
Facts: []Fact{{
ID: "f1", Subject: "s", Field: "x", Value: "1",
Provenance: ProvenanceObserved, SourceRef: "git", ObservedAt: time.Unix(1, 0),
}},
GeneratedAt: time.Unix(100, 0),
CollectionDuration: time.Second,
}
a := base
a.RevisionID = ComputeRevisionID(a)
b := base
b.GeneratedAt = time.Unix(999, 0)
b.CollectionDuration = 5 * time.Second
b.Facts[0].ObservedAt = time.Unix(50, 0)
b.RevisionID = ComputeRevisionID(b)
if a.RevisionID != b.RevisionID {
t.Fatalf("revision should ignore timestamps: %s vs %s", a.RevisionID, b.RevisionID)
}
c := base
c.Facts[0].Value = "2"
c.RevisionID = ComputeRevisionID(c)
if c.RevisionID == a.RevisionID {
t.Fatal("revision must change when fact value changes")
}
}
func TestModel_UnknownEnumsFail(t *testing.T) {
if err := ValidateProvenance(Provenance("nope")); err == nil || !strings.Contains(err.Error(), "unknown provenance") {
t.Fatalf("expected provenance error, got %v", err)
}
if err := ValidateFreshness(Freshness("nope")); err == nil || !strings.Contains(err.Error(), "unknown freshness") {
t.Fatalf("expected freshness error, got %v", err)
}
if err := ValidateAgentKind(AgentKind("nope")); err == nil || !strings.Contains(err.Error(), "unknown agent kind") {
t.Fatalf("expected agent kind error, got %v", err)
}
}
func TestModel_AcceptanceSamplesAxes(t *testing.T) {
rdev := SampleRDevTaskFixture()
if rdev.DocStatus != DocStatusPlanned || rdev.Claim.Claimed {
t.Fatalf("rdev should be planned/unclaimed: %+v", rdev)
}
git := SampleDevAheadGitFixture()
if git.Ahead <= 0 || !strings.Contains(git.RemoteNote, "unconfirmed") {
t.Fatalf("dev ahead sample invalid: %+v", git)
}
main := SampleMainEventsForkFixture()
if main.Readiness != "ok" || main.Facets.ServicesOK != "partial" {
t.Fatalf("main fork sample invalid: %+v", main)
}
if len(main.DriftNotes) == 0 {
t.Fatal("main sample must retain drift notes, not aggregate green")
}
}