Files
beads/cmd/bd/agent_routing_test.go
dave a70c3a8cbe feat: extract Gas Town types from beads core (bd-i54l)
Remove Gas Town-specific issue types (agent, role, rig, convoy, slot)
from beads core. These types are now identified by labels instead:
- gt:agent, gt:role, gt:rig, gt:convoy, gt:slot

Changes:
- internal/types/types.go: Remove TypeAgent, TypeRole, TypeRig, TypeConvoy, TypeSlot constants
- cmd/bd/agent.go: Create agents with TypeTask + gt:agent label
- cmd/bd/merge_slot.go: Create slots with TypeTask + gt:slot label
- internal/storage/sqlite/queries.go, transaction.go: Query convoys by gt:convoy label
- internal/rpc/server_issues_epics.go: Check gt:agent label for role_type/rig label auto-add
- cmd/bd/create.go: Check gt:agent label for role_type/rig label auto-add
- internal/ui/styles.go: Remove agent/role/rig type colors
- cmd/bd/export_obsidian.go: Remove agent/role/rig/convoy type tag mappings
- Update all affected tests

This enables beads to be a generic issue tracker while Gas Town
uses labels for its specific type semantics.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Executed-By: beads/crew/dave
Rig: beads
Role: crew
2026-01-06 22:18:37 -08:00

264 lines
8.5 KiB
Go

package main
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/steveyegge/beads/internal/types"
)
// TestAgentStateWithRouting tests that bd agent state respects routes.jsonl
// for cross-repo agent resolution. This is a regression test for the bug where
// bd agent state failed to find agents in routed databases while bd show worked.
func TestAgentStateWithRouting(t *testing.T) {
ctx := context.Background()
// Create temp directory structure:
// tmpDir/
// .beads/
// beads.db (town database)
// routes.jsonl (routing config)
// rig/
// .beads/
// beads.db (rig database with agent)
tmpDir := t.TempDir()
// Create town .beads directory
townBeadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(townBeadsDir, 0755); err != nil {
t.Fatalf("Failed to create town beads dir: %v", err)
}
// Create rig .beads directory
rigBeadsDir := filepath.Join(tmpDir, "rig", ".beads")
if err := os.MkdirAll(rigBeadsDir, 0755); err != nil {
t.Fatalf("Failed to create rig beads dir: %v", err)
}
// Initialize town database using helper (prefix without trailing hyphen)
townDBPath := filepath.Join(townBeadsDir, "beads.db")
townStore := newTestStoreWithPrefix(t, townDBPath, "hq")
// Initialize rig database using helper (prefix without trailing hyphen)
rigDBPath := filepath.Join(rigBeadsDir, "beads.db")
rigStore := newTestStoreWithPrefix(t, rigDBPath, "gt")
// Create an agent bead in the rig database (using task type with gt:agent label)
agentBead := &types.Issue{
ID: "gt-testrig-polecat-test",
Title: "Agent: gt-testrig-polecat-test",
IssueType: types.TypeTask, // Use task type; gt:agent label marks it as agent
Status: types.StatusOpen,
RoleType: "polecat",
Rig: "testrig",
}
if err := rigStore.CreateIssue(ctx, agentBead, "test"); err != nil {
t.Fatalf("Failed to create agent bead: %v", err)
}
if err := rigStore.AddLabel(ctx, agentBead.ID, "gt:agent", "test"); err != nil {
t.Fatalf("Failed to add gt:agent label: %v", err)
}
// Create routes.jsonl in town .beads directory
routesContent := `{"prefix":"gt-","path":"rig"}`
routesPath := filepath.Join(townBeadsDir, "routes.jsonl")
if err := os.WriteFile(routesPath, []byte(routesContent), 0644); err != nil {
t.Fatalf("Failed to write routes.jsonl: %v", err)
}
// Set up global state for routing to work
oldDbPath := dbPath
dbPath = townDBPath
t.Cleanup(func() { dbPath = oldDbPath })
// Test the routed resolution
result, err := resolveAndGetIssueWithRouting(ctx, townStore, "gt-testrig-polecat-test")
if err != nil {
t.Fatalf("resolveAndGetIssueWithRouting failed: %v", err)
}
if result == nil {
t.Fatal("resolveAndGetIssueWithRouting returned nil result")
}
defer result.Close()
if result.Issue == nil {
t.Fatal("resolveAndGetIssueWithRouting returned nil issue")
}
if result.Issue.ID != "gt-testrig-polecat-test" {
t.Errorf("Expected issue ID %q, got %q", "gt-testrig-polecat-test", result.Issue.ID)
}
if !result.Routed {
t.Error("Expected result.Routed to be true for cross-repo lookup")
}
if result.Issue.IssueType != types.TypeTask {
t.Errorf("Expected issue type %q, got %q", types.TypeTask, result.Issue.IssueType)
}
t.Logf("Successfully resolved agent %s via routing", result.Issue.ID)
}
// TestNeedsRoutingFunction tests the needsRouting function
func TestNeedsRoutingFunction(t *testing.T) {
// Without dbPath set, needsRouting should return false
oldDbPath := dbPath
dbPath = ""
t.Cleanup(func() { dbPath = oldDbPath })
if needsRouting("any-id") {
t.Error("needsRouting should return false when dbPath is empty")
}
}
// TestAgentHeartbeatWithRouting tests that bd agent heartbeat respects routes.jsonl
func TestAgentHeartbeatWithRouting(t *testing.T) {
ctx := context.Background()
tmpDir := t.TempDir()
// Create town .beads directory
townBeadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(townBeadsDir, 0755); err != nil {
t.Fatalf("Failed to create town beads dir: %v", err)
}
// Create rig .beads directory
rigBeadsDir := filepath.Join(tmpDir, "rig", ".beads")
if err := os.MkdirAll(rigBeadsDir, 0755); err != nil {
t.Fatalf("Failed to create rig beads dir: %v", err)
}
// Initialize databases (prefix without trailing hyphen)
townDBPath := filepath.Join(townBeadsDir, "beads.db")
townStore := newTestStoreWithPrefix(t, townDBPath, "hq")
rigDBPath := filepath.Join(rigBeadsDir, "beads.db")
rigStore := newTestStoreWithPrefix(t, rigDBPath, "gt")
// Create an agent bead in the rig database (using task type with gt:agent label)
agentBead := &types.Issue{
ID: "gt-test-witness",
Title: "Agent: gt-test-witness",
IssueType: types.TypeTask, // Use task type; gt:agent label marks it as agent
Status: types.StatusOpen,
RoleType: "witness",
Rig: "test",
}
if err := rigStore.CreateIssue(ctx, agentBead, "test"); err != nil {
t.Fatalf("Failed to create agent bead: %v", err)
}
if err := rigStore.AddLabel(ctx, agentBead.ID, "gt:agent", "test"); err != nil {
t.Fatalf("Failed to add gt:agent label: %v", err)
}
// Create routes.jsonl
routesContent := `{"prefix":"gt-","path":"rig"}`
routesPath := filepath.Join(townBeadsDir, "routes.jsonl")
if err := os.WriteFile(routesPath, []byte(routesContent), 0644); err != nil {
t.Fatalf("Failed to write routes.jsonl: %v", err)
}
// Set up global state
oldDbPath := dbPath
dbPath = townDBPath
t.Cleanup(func() { dbPath = oldDbPath })
// Test that we can resolve the agent from the town directory
result, err := resolveAndGetIssueWithRouting(ctx, townStore, "gt-test-witness")
if err != nil {
t.Fatalf("resolveAndGetIssueWithRouting failed: %v", err)
}
if result == nil || result.Issue == nil {
t.Fatal("resolveAndGetIssueWithRouting returned nil")
}
defer result.Close()
if result.Issue.ID != "gt-test-witness" {
t.Errorf("Expected issue ID %q, got %q", "gt-test-witness", result.Issue.ID)
}
if !result.Routed {
t.Error("Expected result.Routed to be true")
}
t.Logf("Successfully resolved agent %s via routing for heartbeat test", result.Issue.ID)
}
// TestAgentShowWithRouting tests that bd agent show respects routes.jsonl
func TestAgentShowWithRouting(t *testing.T) {
ctx := context.Background()
tmpDir := t.TempDir()
// Create town .beads directory
townBeadsDir := filepath.Join(tmpDir, ".beads")
if err := os.MkdirAll(townBeadsDir, 0755); err != nil {
t.Fatalf("Failed to create town beads dir: %v", err)
}
// Create rig .beads directory
rigBeadsDir := filepath.Join(tmpDir, "rig", ".beads")
if err := os.MkdirAll(rigBeadsDir, 0755); err != nil {
t.Fatalf("Failed to create rig beads dir: %v", err)
}
// Initialize databases (prefix without trailing hyphen)
townDBPath := filepath.Join(townBeadsDir, "beads.db")
townStore := newTestStoreWithPrefix(t, townDBPath, "hq")
rigDBPath := filepath.Join(rigBeadsDir, "beads.db")
rigStore := newTestStoreWithPrefix(t, rigDBPath, "gt")
// Create an agent bead in the rig database (using task type with gt:agent label)
agentBead := &types.Issue{
ID: "gt-myrig-crew-alice",
Title: "Agent: gt-myrig-crew-alice",
IssueType: types.TypeTask, // Use task type; gt:agent label marks it as agent
Status: types.StatusOpen,
RoleType: "crew",
Rig: "myrig",
}
if err := rigStore.CreateIssue(ctx, agentBead, "test"); err != nil {
t.Fatalf("Failed to create agent bead: %v", err)
}
if err := rigStore.AddLabel(ctx, agentBead.ID, "gt:agent", "test"); err != nil {
t.Fatalf("Failed to add gt:agent label: %v", err)
}
// Create routes.jsonl
routesContent := `{"prefix":"gt-","path":"rig"}`
routesPath := filepath.Join(townBeadsDir, "routes.jsonl")
if err := os.WriteFile(routesPath, []byte(routesContent), 0644); err != nil {
t.Fatalf("Failed to write routes.jsonl: %v", err)
}
// Set up global state
oldDbPath := dbPath
dbPath = townDBPath
t.Cleanup(func() { dbPath = oldDbPath })
// Test that we can resolve the agent from the town directory
result, err := resolveAndGetIssueWithRouting(ctx, townStore, "gt-myrig-crew-alice")
if err != nil {
t.Fatalf("resolveAndGetIssueWithRouting failed: %v", err)
}
if result == nil || result.Issue == nil {
t.Fatal("resolveAndGetIssueWithRouting returned nil")
}
defer result.Close()
if result.Issue.ID != "gt-myrig-crew-alice" {
t.Errorf("Expected issue ID %q, got %q", "gt-myrig-crew-alice", result.Issue.ID)
}
if result.Issue.IssueType != types.TypeTask {
t.Errorf("Expected issue type %q, got %q", types.TypeTask, result.Issue.IssueType)
}
t.Logf("Successfully resolved agent %s via routing for show test", result.Issue.ID)
}