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>
379 lines
9.9 KiB
Go
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
|
|
}
|