package witness import ( "testing" ) func TestClassifyMessage(t *testing.T) { tests := []struct { subject string expected ProtocolType }{ {"POLECAT_DONE nux", ProtoPolecatDone}, {"POLECAT_DONE ace", ProtoPolecatDone}, {"LIFECYCLE:Shutdown nux", ProtoLifecycleShutdown}, {"HELP: Tests failing", ProtoHelp}, {"HELP: Git conflict", ProtoHelp}, {"MERGED nux", ProtoMerged}, {"MERGED valkyrie", ProtoMerged}, {"MERGE_FAILED nux", ProtoMergeFailed}, {"MERGE_FAILED ace", ProtoMergeFailed}, {"🤝 HANDOFF: Patrol context", ProtoHandoff}, {"🤝HANDOFF: No space", ProtoHandoff}, {"SWARM_START", ProtoSwarmStart}, {"Unknown message", ProtoUnknown}, {"", ProtoUnknown}, } for _, tc := range tests { t.Run(tc.subject, func(t *testing.T) { result := ClassifyMessage(tc.subject) if result != tc.expected { t.Errorf("ClassifyMessage(%q) = %v, want %v", tc.subject, result, tc.expected) } }) } } func TestParsePolecatDone(t *testing.T) { subject := "POLECAT_DONE nux" body := `Exit: MERGED Issue: gt-abc123 MR: gt-mr-xyz Branch: feature-branch` payload, err := ParsePolecatDone(subject, body) if err != nil { t.Fatalf("ParsePolecatDone() error = %v", err) } if payload.PolecatName != "nux" { t.Errorf("PolecatName = %q, want %q", payload.PolecatName, "nux") } if payload.Exit != "MERGED" { t.Errorf("Exit = %q, want %q", payload.Exit, "MERGED") } if payload.IssueID != "gt-abc123" { t.Errorf("IssueID = %q, want %q", payload.IssueID, "gt-abc123") } if payload.MRID != "gt-mr-xyz" { t.Errorf("MRID = %q, want %q", payload.MRID, "gt-mr-xyz") } if payload.Branch != "feature-branch" { t.Errorf("Branch = %q, want %q", payload.Branch, "feature-branch") } } func TestParsePolecatDone_MinimalBody(t *testing.T) { subject := "POLECAT_DONE ace" body := "Exit: DEFERRED" payload, err := ParsePolecatDone(subject, body) if err != nil { t.Fatalf("ParsePolecatDone() error = %v", err) } if payload.PolecatName != "ace" { t.Errorf("PolecatName = %q, want %q", payload.PolecatName, "ace") } if payload.Exit != "DEFERRED" { t.Errorf("Exit = %q, want %q", payload.Exit, "DEFERRED") } if payload.IssueID != "" { t.Errorf("IssueID = %q, want empty", payload.IssueID) } } func TestParsePolecatDone_InvalidSubject(t *testing.T) { _, err := ParsePolecatDone("Invalid subject", "body") if err == nil { t.Error("ParsePolecatDone() expected error for invalid subject") } } func TestParseHelp(t *testing.T) { subject := "HELP: Tests failing on CI" body := `Agent: gastown/polecats/nux Issue: gt-abc123 Problem: Unit tests timeout after 30 seconds Tried: Increased timeout, checked for deadlocks` payload, err := ParseHelp(subject, body) if err != nil { t.Fatalf("ParseHelp() error = %v", err) } if payload.Topic != "Tests failing on CI" { t.Errorf("Topic = %q, want %q", payload.Topic, "Tests failing on CI") } if payload.Agent != "gastown/polecats/nux" { t.Errorf("Agent = %q, want %q", payload.Agent, "gastown/polecats/nux") } if payload.IssueID != "gt-abc123" { t.Errorf("IssueID = %q, want %q", payload.IssueID, "gt-abc123") } if payload.Problem != "Unit tests timeout after 30 seconds" { t.Errorf("Problem = %q, want %q", payload.Problem, "Unit tests timeout after 30 seconds") } if payload.Tried != "Increased timeout, checked for deadlocks" { t.Errorf("Tried = %q, want %q", payload.Tried, "Increased timeout, checked for deadlocks") } } func TestParseHelp_InvalidSubject(t *testing.T) { _, err := ParseHelp("Not a help message", "body") if err == nil { t.Error("ParseHelp() expected error for invalid subject") } } func TestParseMerged(t *testing.T) { subject := "MERGED nux" body := `Branch: feature-nux Issue: gt-abc123 Merged-At: 2025-12-30T10:30:00Z` payload, err := ParseMerged(subject, body) if err != nil { t.Fatalf("ParseMerged() error = %v", err) } if payload.PolecatName != "nux" { t.Errorf("PolecatName = %q, want %q", payload.PolecatName, "nux") } if payload.Branch != "feature-nux" { t.Errorf("Branch = %q, want %q", payload.Branch, "feature-nux") } if payload.IssueID != "gt-abc123" { t.Errorf("IssueID = %q, want %q", payload.IssueID, "gt-abc123") } if payload.MergedAt.IsZero() { t.Error("MergedAt should not be zero") } } func TestParseMerged_InvalidSubject(t *testing.T) { _, err := ParseMerged("Not merged", "body") if err == nil { t.Error("ParseMerged() expected error for invalid subject") } } func TestParseMergeFailed(t *testing.T) { subject := "MERGE_FAILED nux" body := `Branch: feature-nux Issue: gt-abc123 FailureType: tests Error: unit tests failed with 3 errors` payload, err := ParseMergeFailed(subject, body) if err != nil { t.Fatalf("ParseMergeFailed() error = %v", err) } if payload.PolecatName != "nux" { t.Errorf("PolecatName = %q, want %q", payload.PolecatName, "nux") } if payload.Branch != "feature-nux" { t.Errorf("Branch = %q, want %q", payload.Branch, "feature-nux") } if payload.IssueID != "gt-abc123" { t.Errorf("IssueID = %q, want %q", payload.IssueID, "gt-abc123") } if payload.FailureType != "tests" { t.Errorf("FailureType = %q, want %q", payload.FailureType, "tests") } if payload.Error != "unit tests failed with 3 errors" { t.Errorf("Error = %q, want %q", payload.Error, "unit tests failed with 3 errors") } if payload.FailedAt.IsZero() { t.Error("FailedAt should not be zero") } } func TestParseMergeFailed_MinimalBody(t *testing.T) { subject := "MERGE_FAILED ace" body := "FailureType: build" payload, err := ParseMergeFailed(subject, body) if err != nil { t.Fatalf("ParseMergeFailed() error = %v", err) } if payload.PolecatName != "ace" { t.Errorf("PolecatName = %q, want %q", payload.PolecatName, "ace") } if payload.FailureType != "build" { t.Errorf("FailureType = %q, want %q", payload.FailureType, "build") } if payload.Branch != "" { t.Errorf("Branch = %q, want empty", payload.Branch) } } func TestParseMergeFailed_InvalidSubject(t *testing.T) { _, err := ParseMergeFailed("Not a merge failed", "body") if err == nil { t.Error("ParseMergeFailed() expected error for invalid subject") } } func TestCleanupWispLabels(t *testing.T) { labels := CleanupWispLabels("nux", "pending") expected := []string{"cleanup", "polecat:nux", "state:pending"} if len(labels) != len(expected) { t.Fatalf("CleanupWispLabels() returned %d labels, want %d", len(labels), len(expected)) } for i, label := range labels { if label != expected[i] { t.Errorf("labels[%d] = %q, want %q", i, label, expected[i]) } } } func TestAssessHelpRequest_GitConflict(t *testing.T) { payload := &HelpPayload{ Topic: "Git issue", Problem: "Merge conflict in main.go", } assessment := AssessHelpRequest(payload) if assessment.CanHelp { t.Error("Should not be able to help with git conflicts") } if !assessment.NeedsEscalation { t.Error("Git conflicts should need escalation") } } func TestAssessHelpRequest_GitPush(t *testing.T) { payload := &HelpPayload{ Topic: "Git push failing", Problem: "Cannot push to remote", } assessment := AssessHelpRequest(payload) if !assessment.CanHelp { t.Error("Should be able to help with git push issues") } if assessment.HelpAction == "" { t.Error("HelpAction should not be empty") } } func TestAssessHelpRequest_TestFailures(t *testing.T) { payload := &HelpPayload{ Topic: "Test failures", Problem: "Tests fail on CI", } assessment := AssessHelpRequest(payload) if assessment.CanHelp { t.Error("Should not be able to help with test failures") } if !assessment.NeedsEscalation { t.Error("Test failures should need escalation") } } func TestAssessHelpRequest_RequirementsUnclear(t *testing.T) { payload := &HelpPayload{ Topic: "Requirements unclear", Problem: "Don't understand the requirements for this task", } assessment := AssessHelpRequest(payload) if assessment.CanHelp { t.Error("Should not be able to help with unclear requirements") } if !assessment.NeedsEscalation { t.Error("Unclear requirements should need escalation") } } func TestAssessHelpRequest_BuildIssues(t *testing.T) { payload := &HelpPayload{ Topic: "Build failing", Problem: "Cannot compile the project", } assessment := AssessHelpRequest(payload) if !assessment.CanHelp { t.Error("Should be able to help with build issues") } }