Files
gastown/internal/daemon/lifecycle_test.go
Steve Yegge b6817899b4 refactor: ZFC cleanup - move Go heuristics to Deacon molecule (gt-gaxo)
Remove Go code that makes workflow decisions. All health checking,
staleness detection, nudging, and escalation belongs in the Deacon
molecule where Claude executes it.

Removed:
- internal/daemon/backoff.go (190 lines) - exponential backoff decisions
- internal/doctor/stale_check.go (284 lines) - staleness detection
- IsFresh/IsStale/IsVeryStale from keepalive.go
- pokeMayor, pokeWitnesses, pokeWitness from daemon.go
- Heartbeat staleness classification from pokeDeacon

Changed:
- Lifecycle parsing now uses structured body (JSON or simple text)
  instead of keyword matching on subject line
- Daemon now only ensures Deacon is running and sends simple heartbeats
- No backoff, no staleness classification, no decision-making

Total: ~800 lines removed from Go code

The Deacon molecule will handle all health checking, nudging, and
escalation. Go is now just a message router.

See gt-gaxo epic for full rationale.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-24 00:11:15 -08:00

224 lines
5.4 KiB
Go

package daemon
import (
"io"
"log"
"testing"
)
// testDaemon creates a minimal Daemon for testing.
func testDaemon() *Daemon {
return &Daemon{
config: &Config{TownRoot: "/tmp/test"},
logger: log.New(io.Discard, "", 0), // silent logger for tests
}
}
func TestParseLifecycleRequest_Cycle(t *testing.T) {
d := testDaemon()
tests := []struct {
subject string
body string
expected LifecycleAction
}{
// JSON body format
{"LIFECYCLE: requesting action", `{"action": "cycle"}`, ActionCycle},
// Simple text body format
{"LIFECYCLE: requesting action", "cycle", ActionCycle},
{"lifecycle: action request", "action: cycle", ActionCycle},
}
for _, tc := range tests {
msg := &BeadsMessage{
Subject: tc.subject,
Body: tc.body,
From: "test-sender",
}
result := d.parseLifecycleRequest(msg)
if result == nil {
t.Errorf("parseLifecycleRequest(subject=%q, body=%q) returned nil, expected action %s", tc.subject, tc.body, tc.expected)
continue
}
if result.Action != tc.expected {
t.Errorf("parseLifecycleRequest(subject=%q, body=%q) action = %s, expected %s", tc.subject, tc.body, result.Action, tc.expected)
}
}
}
func TestParseLifecycleRequest_RestartAndShutdown(t *testing.T) {
// Verify that restart and shutdown are correctly parsed using structured body.
d := testDaemon()
tests := []struct {
subject string
body string
expected LifecycleAction
}{
{"LIFECYCLE: action", `{"action": "restart"}`, ActionRestart},
{"LIFECYCLE: action", `{"action": "shutdown"}`, ActionShutdown},
{"lifecycle: action", "stop", ActionShutdown},
{"LIFECYCLE: action", "restart", ActionRestart},
}
for _, tc := range tests {
msg := &BeadsMessage{
Subject: tc.subject,
Body: tc.body,
From: "test-sender",
}
result := d.parseLifecycleRequest(msg)
if result == nil {
t.Errorf("parseLifecycleRequest(subject=%q, body=%q) returned nil", tc.subject, tc.body)
continue
}
if result.Action != tc.expected {
t.Errorf("parseLifecycleRequest(subject=%q, body=%q) action = %s, expected %s", tc.subject, tc.body, result.Action, tc.expected)
}
}
}
func TestParseLifecycleRequest_NotLifecycle(t *testing.T) {
d := testDaemon()
tests := []string{
"Regular message",
"HEARTBEAT: check rigs",
"lifecycle without colon",
"Something else: requesting cycle",
"",
}
for _, title := range tests {
msg := &BeadsMessage{
Subject: title,
From: "test-sender",
}
result := d.parseLifecycleRequest(msg)
if result != nil {
t.Errorf("parseLifecycleRequest(%q) = %+v, expected nil", title, result)
}
}
}
func TestParseLifecycleRequest_UsesFromField(t *testing.T) {
d := testDaemon()
// Now that we use structured body, the From field comes directly from the message
tests := []struct {
subject string
body string
sender string
expectedFrom string
}{
{"LIFECYCLE: action", `{"action": "cycle"}`, "mayor", "mayor"},
{"LIFECYCLE: action", "restart", "gastown-witness", "gastown-witness"},
{"lifecycle: action", "shutdown", "my-rig-refinery", "my-rig-refinery"},
}
for _, tc := range tests {
msg := &BeadsMessage{
Subject: tc.subject,
Body: tc.body,
From: tc.sender,
}
result := d.parseLifecycleRequest(msg)
if result == nil {
t.Errorf("parseLifecycleRequest(body=%q) returned nil", tc.body)
continue
}
if result.From != tc.expectedFrom {
t.Errorf("parseLifecycleRequest() from = %q, expected %q", result.From, tc.expectedFrom)
}
}
}
func TestParseLifecycleRequest_AlwaysUsesFromField(t *testing.T) {
d := testDaemon()
// With structured body parsing, From always comes from message From field
msg := &BeadsMessage{
Subject: "LIFECYCLE: action",
Body: "cycle",
From: "the-sender",
}
result := d.parseLifecycleRequest(msg)
if result == nil {
t.Fatal("expected non-nil result")
}
if result.From != "the-sender" {
t.Errorf("parseLifecycleRequest() from = %q, expected 'the-sender'", result.From)
}
}
func TestIdentityToSession_Mayor(t *testing.T) {
d := testDaemon()
result := d.identityToSession("mayor")
if result != "gt-mayor" {
t.Errorf("identityToSession('mayor') = %q, expected 'gt-mayor'", result)
}
}
func TestIdentityToSession_Witness(t *testing.T) {
d := testDaemon()
tests := []struct {
identity string
expected string
}{
{"gastown-witness", "gt-gastown-witness"},
{"myrig-witness", "gt-myrig-witness"},
{"my-rig-name-witness", "gt-my-rig-name-witness"},
}
for _, tc := range tests {
result := d.identityToSession(tc.identity)
if result != tc.expected {
t.Errorf("identityToSession(%q) = %q, expected %q", tc.identity, result, tc.expected)
}
}
}
func TestIdentityToSession_Unknown(t *testing.T) {
d := testDaemon()
tests := []string{
"unknown",
"polecat",
"refinery",
"gastown", // rig name without -witness
"",
}
for _, identity := range tests {
result := d.identityToSession(identity)
if result != "" {
t.Errorf("identityToSession(%q) = %q, expected empty string", identity, result)
}
}
}
func TestBeadsMessage_Serialization(t *testing.T) {
msg := BeadsMessage{
ID: "msg-123",
Subject: "Test Message",
Body: "A test message body",
From: "test-sender",
To: "test-recipient",
Priority: "high",
Type: "message",
}
// Verify all fields are accessible
if msg.ID != "msg-123" {
t.Errorf("ID mismatch")
}
if msg.Subject != "Test Message" {
t.Errorf("Subject mismatch")
}
if msg.From != "test-sender" {
t.Errorf("From mismatch")
}
}