Files
gastown/internal/protocol/protocol_test.go
Steve Yegge 96ffbf0188 Implement Witness-Refinery protocol handlers (gt-m5w4g.2)
Add internal/protocol/ package with handlers for:
- MERGE_READY (witness→refinery): worker done, branch ready
- MERGED (refinery→witness): merge succeeded, cleanup ok
- MERGE_FAILED (refinery→witness): merge failed, needs rework
- REWORK_REQUEST (refinery→witness): rebase needed due to conflicts

Package structure:
- types.go: Protocol message types and payload structs
- messages.go: Message builders and body parsers
- handlers.go: Handler interface and registry for dispatch
- witness_handlers.go: DefaultWitnessHandler implementation
- refinery_handlers.go: DefaultRefineryHandler implementation
- protocol_test.go: Comprehensive test coverage

Also updated docs/mail-protocol.md with:
- MERGE_FAILED and REWORK_REQUEST message type documentation
- Merge failure and rebase required flow diagrams
- Reference to internal/protocol/ package

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 10:48:00 -08:00

379 lines
9.9 KiB
Go

package protocol
import (
"bytes"
"strings"
"testing"
"time"
"github.com/steveyegge/gastown/internal/mail"
)
func TestParseMessageType(t *testing.T) {
tests := []struct {
subject string
expected MessageType
}{
{"MERGE_READY nux", TypeMergeReady},
{"MERGED Toast", TypeMerged},
{"MERGE_FAILED ace", TypeMergeFailed},
{"REWORK_REQUEST valkyrie", TypeReworkRequest},
{"MERGE_READY", TypeMergeReady}, // no polecat name
{"Unknown subject", ""},
{"", ""},
{" MERGE_READY nux ", TypeMergeReady}, // with whitespace
}
for _, tt := range tests {
t.Run(tt.subject, func(t *testing.T) {
result := ParseMessageType(tt.subject)
if result != tt.expected {
t.Errorf("ParseMessageType(%q) = %q, want %q", tt.subject, result, tt.expected)
}
})
}
}
func TestExtractPolecat(t *testing.T) {
tests := []struct {
subject string
expected string
}{
{"MERGE_READY nux", "nux"},
{"MERGED Toast", "Toast"},
{"MERGE_FAILED ace", "ace"},
{"REWORK_REQUEST valkyrie", "valkyrie"},
{"MERGE_READY", ""},
{"", ""},
{" MERGE_READY nux ", "nux"},
}
for _, tt := range tests {
t.Run(tt.subject, func(t *testing.T) {
result := ExtractPolecat(tt.subject)
if result != tt.expected {
t.Errorf("ExtractPolecat(%q) = %q, want %q", tt.subject, result, tt.expected)
}
})
}
}
func TestIsProtocolMessage(t *testing.T) {
tests := []struct {
subject string
expected bool
}{
{"MERGE_READY nux", true},
{"MERGED Toast", true},
{"MERGE_FAILED ace", true},
{"REWORK_REQUEST valkyrie", true},
{"Unknown subject", false},
{"", false},
{"Hello world", false},
}
for _, tt := range tests {
t.Run(tt.subject, func(t *testing.T) {
result := IsProtocolMessage(tt.subject)
if result != tt.expected {
t.Errorf("IsProtocolMessage(%q) = %v, want %v", tt.subject, result, tt.expected)
}
})
}
}
func TestNewMergeReadyMessage(t *testing.T) {
msg := NewMergeReadyMessage("gastown", "nux", "polecat/nux/gt-abc", "gt-abc")
if msg.Subject != "MERGE_READY nux" {
t.Errorf("Subject = %q, want %q", msg.Subject, "MERGE_READY nux")
}
if msg.From != "gastown/witness" {
t.Errorf("From = %q, want %q", msg.From, "gastown/witness")
}
if msg.To != "gastown/refinery" {
t.Errorf("To = %q, want %q", msg.To, "gastown/refinery")
}
if msg.Priority != mail.PriorityHigh {
t.Errorf("Priority = %q, want %q", msg.Priority, mail.PriorityHigh)
}
if !strings.Contains(msg.Body, "Branch: polecat/nux/gt-abc") {
t.Errorf("Body missing branch: %s", msg.Body)
}
if !strings.Contains(msg.Body, "Issue: gt-abc") {
t.Errorf("Body missing issue: %s", msg.Body)
}
}
func TestNewMergedMessage(t *testing.T) {
msg := NewMergedMessage("gastown", "nux", "polecat/nux/gt-abc", "gt-abc", "main", "abc123")
if msg.Subject != "MERGED nux" {
t.Errorf("Subject = %q, want %q", msg.Subject, "MERGED nux")
}
if msg.From != "gastown/refinery" {
t.Errorf("From = %q, want %q", msg.From, "gastown/refinery")
}
if msg.To != "gastown/witness" {
t.Errorf("To = %q, want %q", msg.To, "gastown/witness")
}
if !strings.Contains(msg.Body, "Merge-Commit: abc123") {
t.Errorf("Body missing merge commit: %s", msg.Body)
}
}
func TestNewMergeFailedMessage(t *testing.T) {
msg := NewMergeFailedMessage("gastown", "nux", "polecat/nux/gt-abc", "gt-abc", "main", "tests", "Test failed")
if msg.Subject != "MERGE_FAILED nux" {
t.Errorf("Subject = %q, want %q", msg.Subject, "MERGE_FAILED nux")
}
if !strings.Contains(msg.Body, "Failure-Type: tests") {
t.Errorf("Body missing failure type: %s", msg.Body)
}
if !strings.Contains(msg.Body, "Error: Test failed") {
t.Errorf("Body missing error: %s", msg.Body)
}
}
func TestNewReworkRequestMessage(t *testing.T) {
conflicts := []string{"file1.go", "file2.go"}
msg := NewReworkRequestMessage("gastown", "nux", "polecat/nux/gt-abc", "gt-abc", "main", conflicts)
if msg.Subject != "REWORK_REQUEST nux" {
t.Errorf("Subject = %q, want %q", msg.Subject, "REWORK_REQUEST nux")
}
if !strings.Contains(msg.Body, "Conflict-Files: file1.go, file2.go") {
t.Errorf("Body missing conflict files: %s", msg.Body)
}
if !strings.Contains(msg.Body, "git rebase origin/main") {
t.Errorf("Body missing rebase instructions: %s", msg.Body)
}
}
func TestParseMergeReadyPayload(t *testing.T) {
body := `Branch: polecat/nux/gt-abc
Issue: gt-abc
Polecat: nux
Rig: gastown
Verified: clean git state`
payload := ParseMergeReadyPayload(body)
if payload.Branch != "polecat/nux/gt-abc" {
t.Errorf("Branch = %q, want %q", payload.Branch, "polecat/nux/gt-abc")
}
if payload.Issue != "gt-abc" {
t.Errorf("Issue = %q, want %q", payload.Issue, "gt-abc")
}
if payload.Polecat != "nux" {
t.Errorf("Polecat = %q, want %q", payload.Polecat, "nux")
}
if payload.Rig != "gastown" {
t.Errorf("Rig = %q, want %q", payload.Rig, "gastown")
}
}
func TestParseMergedPayload(t *testing.T) {
ts := time.Now().Format(time.RFC3339)
body := `Branch: polecat/nux/gt-abc
Issue: gt-abc
Polecat: nux
Rig: gastown
Target: main
Merged-At: ` + ts + `
Merge-Commit: abc123`
payload := ParseMergedPayload(body)
if payload.Branch != "polecat/nux/gt-abc" {
t.Errorf("Branch = %q, want %q", payload.Branch, "polecat/nux/gt-abc")
}
if payload.MergeCommit != "abc123" {
t.Errorf("MergeCommit = %q, want %q", payload.MergeCommit, "abc123")
}
if payload.TargetBranch != "main" {
t.Errorf("TargetBranch = %q, want %q", payload.TargetBranch, "main")
}
}
func TestHandlerRegistry(t *testing.T) {
registry := NewHandlerRegistry()
handled := false
registry.Register(TypeMergeReady, func(msg *mail.Message) error {
handled = true
return nil
})
msg := &mail.Message{Subject: "MERGE_READY nux"}
if !registry.CanHandle(msg) {
t.Error("Registry should be able to handle MERGE_READY message")
}
if err := registry.Handle(msg); err != nil {
t.Errorf("Handle returned error: %v", err)
}
if !handled {
t.Error("Handler was not called")
}
// Test unregistered message type
unknownMsg := &mail.Message{Subject: "UNKNOWN message"}
if registry.CanHandle(unknownMsg) {
t.Error("Registry should not handle unknown message type")
}
}
func TestWrapWitnessHandlers(t *testing.T) {
handler := &mockWitnessHandler{}
registry := WrapWitnessHandlers(handler)
// Test MERGED
mergedMsg := &mail.Message{
Subject: "MERGED nux",
Body: "Branch: polecat/nux\nIssue: gt-abc\nPolecat: nux\nRig: gastown\nTarget: main",
}
if err := registry.Handle(mergedMsg); err != nil {
t.Errorf("HandleMerged error: %v", err)
}
if !handler.mergedCalled {
t.Error("HandleMerged was not called")
}
// Test MERGE_FAILED
failedMsg := &mail.Message{
Subject: "MERGE_FAILED nux",
Body: "Branch: polecat/nux\nIssue: gt-abc\nPolecat: nux\nRig: gastown\nTarget: main\nFailure-Type: tests\nError: failed",
}
if err := registry.Handle(failedMsg); err != nil {
t.Errorf("HandleMergeFailed error: %v", err)
}
if !handler.failedCalled {
t.Error("HandleMergeFailed was not called")
}
// Test REWORK_REQUEST
reworkMsg := &mail.Message{
Subject: "REWORK_REQUEST nux",
Body: "Branch: polecat/nux\nIssue: gt-abc\nPolecat: nux\nRig: gastown\nTarget: main",
}
if err := registry.Handle(reworkMsg); err != nil {
t.Errorf("HandleReworkRequest error: %v", err)
}
if !handler.reworkCalled {
t.Error("HandleReworkRequest was not called")
}
}
func TestWrapRefineryHandlers(t *testing.T) {
handler := &mockRefineryHandler{}
registry := WrapRefineryHandlers(handler)
msg := &mail.Message{
Subject: "MERGE_READY nux",
Body: "Branch: polecat/nux\nIssue: gt-abc\nPolecat: nux\nRig: gastown",
}
if err := registry.Handle(msg); err != nil {
t.Errorf("HandleMergeReady error: %v", err)
}
if !handler.readyCalled {
t.Error("HandleMergeReady was not called")
}
}
func TestDefaultWitnessHandler(t *testing.T) {
tmpDir := t.TempDir()
handler := NewWitnessHandler("gastown", tmpDir)
// Capture output
var buf bytes.Buffer
handler.SetOutput(&buf)
// Test HandleMerged
mergedPayload := &MergedPayload{
Branch: "polecat/nux/gt-abc",
Issue: "gt-abc",
Polecat: "nux",
Rig: "gastown",
TargetBranch: "main",
MergeCommit: "abc123",
}
if err := handler.HandleMerged(mergedPayload); err != nil {
t.Errorf("HandleMerged error: %v", err)
}
if !strings.Contains(buf.String(), "MERGED received") {
t.Errorf("Output missing expected text: %s", buf.String())
}
// Test HandleMergeFailed
buf.Reset()
failedPayload := &MergeFailedPayload{
Branch: "polecat/nux/gt-abc",
Issue: "gt-abc",
Polecat: "nux",
Rig: "gastown",
TargetBranch: "main",
FailureType: "tests",
Error: "Test failed",
}
if err := handler.HandleMergeFailed(failedPayload); err != nil {
t.Errorf("HandleMergeFailed error: %v", err)
}
if !strings.Contains(buf.String(), "MERGE_FAILED received") {
t.Errorf("Output missing expected text: %s", buf.String())
}
// Test HandleReworkRequest
buf.Reset()
reworkPayload := &ReworkRequestPayload{
Branch: "polecat/nux/gt-abc",
Issue: "gt-abc",
Polecat: "nux",
Rig: "gastown",
TargetBranch: "main",
ConflictFiles: []string{"file1.go"},
}
if err := handler.HandleReworkRequest(reworkPayload); err != nil {
t.Errorf("HandleReworkRequest error: %v", err)
}
if !strings.Contains(buf.String(), "REWORK_REQUEST received") {
t.Errorf("Output missing expected text: %s", buf.String())
}
}
// Mock handlers for testing
type mockWitnessHandler struct {
mergedCalled bool
failedCalled bool
reworkCalled bool
}
func (m *mockWitnessHandler) HandleMerged(payload *MergedPayload) error {
m.mergedCalled = true
return nil
}
func (m *mockWitnessHandler) HandleMergeFailed(payload *MergeFailedPayload) error {
m.failedCalled = true
return nil
}
func (m *mockWitnessHandler) HandleReworkRequest(payload *ReworkRequestPayload) error {
m.reworkCalled = true
return nil
}
type mockRefineryHandler struct {
readyCalled bool
}
func (m *mockRefineryHandler) HandleMergeReady(payload *MergeReadyPayload) error {
m.readyCalled = true
return nil
}