Define versioned Snapshot/Fact/Conflict/TaskState contracts with independent plan/git/verification/deployment axes and acceptance fixtures. Co-authored-by: Cursor <cursoragent@cursor.com>
153 lines
5.1 KiB
Go
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")
|
|
}
|
|
}
|