Add protocol message parsing and handlers for polecat→witness communication: - POLECAT_DONE: Parse completion messages, create cleanup wisps - HELP: Parse help requests, assess if Witness can help or escalate to Mayor - MERGED: Parse refinery merge confirmations - LIFECYCLE:Shutdown: Handle daemon-triggered shutdowns - SWARM_START: Parse batch work initialization Files added: - internal/witness/protocol.go: Message classification and parsing - internal/witness/handlers.go: Handler implementations with wisp/mail integration - internal/witness/protocol_test.go: Unit tests for all parsing functions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
251 lines
6.5 KiB
Go
251 lines
6.5 KiB
Go
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},
|
|
{"🤝 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 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")
|
|
}
|
|
}
|