Remove Gas Town-specific type constants (TypeMolecule, TypeGate, TypeConvoy, TypeMergeRequest, TypeSlot, TypeAgent, TypeRole, TypeRig, TypeEvent, TypeMessage) from internal/types/types.go. Beads now only has core work types built-in: - bug, feature, task, epic, chore All Gas Town types are now purely custom types with no special handling in beads. Use string literals like "gate" or "molecule" when needed, and configure types.custom in config.yaml for validation. Changes: - Remove Gas Town type constants from types.go - Remove mr/mol aliases from Normalize() - Update bd types command to only show core types - Replace all constant usages with string literals throughout codebase - Update tests to use string literals This decouples beads from Gas Town, making it a generic issue tracker. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
447 lines
13 KiB
Go
447 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
// setupGatedTestDB creates a temporary file-based test database
|
|
func setupGatedTestDB(t *testing.T) (*sqlite.SQLiteStorage, func()) {
|
|
t.Helper()
|
|
tmpDir, err := os.MkdirTemp("", "bd-test-gated-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
|
|
testDB := filepath.Join(tmpDir, "test.db")
|
|
store, err := sqlite.New(context.Background(), testDB)
|
|
if err != nil {
|
|
os.RemoveAll(tmpDir)
|
|
t.Fatalf("Failed to create test database: %v", err)
|
|
}
|
|
|
|
// Set issue_prefix (required for beads)
|
|
ctx := context.Background()
|
|
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
|
|
store.Close()
|
|
os.RemoveAll(tmpDir)
|
|
t.Fatalf("Failed to set issue_prefix: %v", err)
|
|
}
|
|
|
|
// Configure Gas Town custom types for test compatibility (bd-find4)
|
|
if err := store.SetConfig(ctx, "types.custom", "molecule,gate,convoy,merge-request,slot,agent,role,rig,event,message"); err != nil {
|
|
store.Close()
|
|
os.RemoveAll(tmpDir)
|
|
t.Fatalf("Failed to set types.custom: %v", err)
|
|
}
|
|
|
|
cleanup := func() {
|
|
store.Close()
|
|
os.RemoveAll(tmpDir)
|
|
}
|
|
|
|
return store, cleanup
|
|
}
|
|
|
|
// =============================================================================
|
|
// mol ready --gated Tests (bd-lhalq: Gate-resume discovery)
|
|
// =============================================================================
|
|
|
|
// TestFindGateReadyMolecules_NoGates tests finding gate-ready molecules when no gates exist
|
|
func TestFindGateReadyMolecules_NoGates(t *testing.T) {
|
|
ctx := context.Background()
|
|
store, cleanup := setupGatedTestDB(t)
|
|
defer cleanup()
|
|
|
|
// Create a regular molecule (no gates)
|
|
mol := &types.Issue{
|
|
ID: "test-mol-001",
|
|
Title: "Test Molecule",
|
|
IssueType: types.TypeEpic,
|
|
Status: types.StatusInProgress,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
step := &types.Issue{
|
|
ID: "test-mol-001.step1",
|
|
Title: "Step 1",
|
|
IssueType: types.TypeTask,
|
|
Status: types.StatusOpen,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
if err := store.CreateIssue(ctx, mol, "test"); err != nil {
|
|
t.Fatalf("Failed to create molecule: %v", err)
|
|
}
|
|
if err := store.CreateIssue(ctx, step, "test"); err != nil {
|
|
t.Fatalf("Failed to create step: %v", err)
|
|
}
|
|
|
|
// Add parent-child relationship
|
|
if err := store.AddDependency(ctx, &types.Dependency{
|
|
IssueID: step.ID,
|
|
DependsOnID: mol.ID,
|
|
Type: types.DepParentChild,
|
|
}, "test"); err != nil {
|
|
t.Fatalf("Failed to add dependency: %v", err)
|
|
}
|
|
|
|
// Find gate-ready molecules
|
|
molecules, err := findGateReadyMolecules(ctx, store)
|
|
if err != nil {
|
|
t.Fatalf("findGateReadyMolecules failed: %v", err)
|
|
}
|
|
|
|
if len(molecules) != 0 {
|
|
t.Errorf("Expected 0 gate-ready molecules, got %d", len(molecules))
|
|
}
|
|
}
|
|
|
|
// TestFindGateReadyMolecules_ClosedGate tests finding molecules with closed gates
|
|
func TestFindGateReadyMolecules_ClosedGate(t *testing.T) {
|
|
ctx := context.Background()
|
|
store, cleanup := setupGatedTestDB(t)
|
|
defer cleanup()
|
|
|
|
// Create molecule structure:
|
|
// mol-001
|
|
// └── gate-await-ci (closed)
|
|
// └── step1 (blocked by gate-await-ci, should become ready)
|
|
|
|
mol := &types.Issue{
|
|
ID: "test-mol-002",
|
|
Title: "Test Molecule with Gate",
|
|
IssueType: types.TypeEpic,
|
|
Status: types.StatusInProgress,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
gate := &types.Issue{
|
|
ID: "test-mol-002.gate-await-ci",
|
|
Title: "Gate: gh:run ci-workflow",
|
|
IssueType: "gate",
|
|
Status: types.StatusClosed, // Gate has closed
|
|
AwaitType: "gh:run",
|
|
AwaitID: "ci-workflow",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
step := &types.Issue{
|
|
ID: "test-mol-002.step1",
|
|
Title: "Deploy after CI",
|
|
IssueType: types.TypeTask,
|
|
Status: types.StatusOpen,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
if err := store.CreateIssue(ctx, mol, "test"); err != nil {
|
|
t.Fatalf("Failed to create molecule: %v", err)
|
|
}
|
|
if err := store.CreateIssue(ctx, gate, "test"); err != nil {
|
|
t.Fatalf("Failed to create gate: %v", err)
|
|
}
|
|
if err := store.CreateIssue(ctx, step, "test"); err != nil {
|
|
t.Fatalf("Failed to create step: %v", err)
|
|
}
|
|
|
|
// Add parent-child relationships
|
|
if err := store.AddDependency(ctx, &types.Dependency{
|
|
IssueID: gate.ID,
|
|
DependsOnID: mol.ID,
|
|
Type: types.DepParentChild,
|
|
}, "test"); err != nil {
|
|
t.Fatalf("Failed to add gate parent-child: %v", err)
|
|
}
|
|
if err := store.AddDependency(ctx, &types.Dependency{
|
|
IssueID: step.ID,
|
|
DependsOnID: mol.ID,
|
|
Type: types.DepParentChild,
|
|
}, "test"); err != nil {
|
|
t.Fatalf("Failed to add step parent-child: %v", err)
|
|
}
|
|
|
|
// Add blocking dependency: step depends on gate (gate blocks step)
|
|
if err := store.AddDependency(ctx, &types.Dependency{
|
|
IssueID: step.ID,
|
|
DependsOnID: gate.ID,
|
|
Type: types.DepBlocks,
|
|
}, "test"); err != nil {
|
|
t.Fatalf("Failed to add blocking dependency: %v", err)
|
|
}
|
|
|
|
// Find gate-ready molecules
|
|
molecules, err := findGateReadyMolecules(ctx, store)
|
|
if err != nil {
|
|
t.Fatalf("findGateReadyMolecules failed: %v", err)
|
|
}
|
|
|
|
if len(molecules) != 1 {
|
|
t.Errorf("Expected 1 gate-ready molecule, got %d", len(molecules))
|
|
return
|
|
}
|
|
|
|
if molecules[0].MoleculeID != mol.ID {
|
|
t.Errorf("Expected molecule ID %s, got %s", mol.ID, molecules[0].MoleculeID)
|
|
}
|
|
if molecules[0].ClosedGate == nil {
|
|
t.Error("Expected closed gate to be set")
|
|
} else if molecules[0].ClosedGate.ID != gate.ID {
|
|
t.Errorf("Expected closed gate ID %s, got %s", gate.ID, molecules[0].ClosedGate.ID)
|
|
}
|
|
if molecules[0].ReadyStep == nil {
|
|
t.Error("Expected ready step to be set")
|
|
} else if molecules[0].ReadyStep.ID != step.ID {
|
|
t.Errorf("Expected ready step ID %s, got %s", step.ID, molecules[0].ReadyStep.ID)
|
|
}
|
|
}
|
|
|
|
// TestFindGateReadyMolecules_OpenGate tests that open gates don't trigger ready
|
|
func TestFindGateReadyMolecules_OpenGate(t *testing.T) {
|
|
ctx := context.Background()
|
|
store, cleanup := setupGatedTestDB(t)
|
|
defer cleanup()
|
|
|
|
// Create molecule with OPEN gate
|
|
mol := &types.Issue{
|
|
ID: "test-mol-003",
|
|
Title: "Test Molecule with Open Gate",
|
|
IssueType: types.TypeEpic,
|
|
Status: types.StatusInProgress,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
gate := &types.Issue{
|
|
ID: "test-mol-003.gate-await-ci",
|
|
Title: "Gate: gh:run ci-workflow",
|
|
IssueType: "gate",
|
|
Status: types.StatusOpen, // Gate is still open
|
|
AwaitType: "gh:run",
|
|
AwaitID: "ci-workflow",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
step := &types.Issue{
|
|
ID: "test-mol-003.step1",
|
|
Title: "Deploy after CI",
|
|
IssueType: types.TypeTask,
|
|
Status: types.StatusOpen,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
if err := store.CreateIssue(ctx, mol, "test"); err != nil {
|
|
t.Fatalf("Failed to create molecule: %v", err)
|
|
}
|
|
if err := store.CreateIssue(ctx, gate, "test"); err != nil {
|
|
t.Fatalf("Failed to create gate: %v", err)
|
|
}
|
|
if err := store.CreateIssue(ctx, step, "test"); err != nil {
|
|
t.Fatalf("Failed to create step: %v", err)
|
|
}
|
|
|
|
// Add parent-child relationships
|
|
if err := store.AddDependency(ctx, &types.Dependency{
|
|
IssueID: gate.ID,
|
|
DependsOnID: mol.ID,
|
|
Type: types.DepParentChild,
|
|
}, "test"); err != nil {
|
|
t.Fatalf("Failed to add gate parent-child: %v", err)
|
|
}
|
|
if err := store.AddDependency(ctx, &types.Dependency{
|
|
IssueID: step.ID,
|
|
DependsOnID: mol.ID,
|
|
Type: types.DepParentChild,
|
|
}, "test"); err != nil {
|
|
t.Fatalf("Failed to add step parent-child: %v", err)
|
|
}
|
|
|
|
// Add blocking dependency: step depends on gate
|
|
if err := store.AddDependency(ctx, &types.Dependency{
|
|
IssueID: step.ID,
|
|
DependsOnID: gate.ID,
|
|
Type: types.DepBlocks,
|
|
}, "test"); err != nil {
|
|
t.Fatalf("Failed to add blocking dependency: %v", err)
|
|
}
|
|
|
|
// Find gate-ready molecules
|
|
molecules, err := findGateReadyMolecules(ctx, store)
|
|
if err != nil {
|
|
t.Fatalf("findGateReadyMolecules failed: %v", err)
|
|
}
|
|
|
|
if len(molecules) != 0 {
|
|
t.Errorf("Expected 0 gate-ready molecules (gate is open), got %d", len(molecules))
|
|
}
|
|
}
|
|
|
|
// TestFindGateReadyMolecules_HookedMolecule tests that hooked molecules are filtered out
|
|
func TestFindGateReadyMolecules_HookedMolecule(t *testing.T) {
|
|
ctx := context.Background()
|
|
store, cleanup := setupGatedTestDB(t)
|
|
defer cleanup()
|
|
|
|
// Create molecule with closed gate, but molecule is hooked
|
|
mol := &types.Issue{
|
|
ID: "test-mol-004",
|
|
Title: "Test Hooked Molecule",
|
|
IssueType: types.TypeEpic,
|
|
Status: types.StatusHooked, // Already hooked by an agent
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
gate := &types.Issue{
|
|
ID: "test-mol-004.gate-await-ci",
|
|
Title: "Gate: gh:run ci-workflow",
|
|
IssueType: "gate",
|
|
Status: types.StatusClosed,
|
|
AwaitType: "gh:run",
|
|
AwaitID: "ci-workflow",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
step := &types.Issue{
|
|
ID: "test-mol-004.step1",
|
|
Title: "Deploy after CI",
|
|
IssueType: types.TypeTask,
|
|
Status: types.StatusOpen,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
if err := store.CreateIssue(ctx, mol, "test"); err != nil {
|
|
t.Fatalf("Failed to create molecule: %v", err)
|
|
}
|
|
if err := store.CreateIssue(ctx, gate, "test"); err != nil {
|
|
t.Fatalf("Failed to create gate: %v", err)
|
|
}
|
|
if err := store.CreateIssue(ctx, step, "test"); err != nil {
|
|
t.Fatalf("Failed to create step: %v", err)
|
|
}
|
|
|
|
// Add parent-child relationships
|
|
if err := store.AddDependency(ctx, &types.Dependency{
|
|
IssueID: gate.ID,
|
|
DependsOnID: mol.ID,
|
|
Type: types.DepParentChild,
|
|
}, "test"); err != nil {
|
|
t.Fatalf("Failed to add gate parent-child: %v", err)
|
|
}
|
|
if err := store.AddDependency(ctx, &types.Dependency{
|
|
IssueID: step.ID,
|
|
DependsOnID: mol.ID,
|
|
Type: types.DepParentChild,
|
|
}, "test"); err != nil {
|
|
t.Fatalf("Failed to add step parent-child: %v", err)
|
|
}
|
|
|
|
// Add blocking dependency
|
|
if err := store.AddDependency(ctx, &types.Dependency{
|
|
IssueID: step.ID,
|
|
DependsOnID: gate.ID,
|
|
Type: types.DepBlocks,
|
|
}, "test"); err != nil {
|
|
t.Fatalf("Failed to add blocking dependency: %v", err)
|
|
}
|
|
|
|
// Find gate-ready molecules
|
|
molecules, err := findGateReadyMolecules(ctx, store)
|
|
if err != nil {
|
|
t.Fatalf("findGateReadyMolecules failed: %v", err)
|
|
}
|
|
|
|
if len(molecules) != 0 {
|
|
t.Errorf("Expected 0 gate-ready molecules (molecule is hooked), got %d", len(molecules))
|
|
}
|
|
}
|
|
|
|
// TestFindGateReadyMolecules_MultipleGates tests handling multiple closed gates
|
|
func TestFindGateReadyMolecules_MultipleGates(t *testing.T) {
|
|
ctx := context.Background()
|
|
store, cleanup := setupGatedTestDB(t)
|
|
defer cleanup()
|
|
|
|
// Create two molecules, each with a closed gate
|
|
for i := 1; i <= 2; i++ {
|
|
molID := fmt.Sprintf("test-multi-%d", i)
|
|
mol := &types.Issue{
|
|
ID: molID,
|
|
Title: fmt.Sprintf("Multi Gate Mol %d", i),
|
|
IssueType: types.TypeEpic,
|
|
Status: types.StatusInProgress,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
gate := &types.Issue{
|
|
ID: fmt.Sprintf("%s.gate", molID),
|
|
Title: "Gate: gh:run",
|
|
IssueType: "gate",
|
|
Status: types.StatusClosed,
|
|
AwaitType: "gh:run",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
step := &types.Issue{
|
|
ID: fmt.Sprintf("%s.step1", molID),
|
|
Title: "Step 1",
|
|
IssueType: types.TypeTask,
|
|
Status: types.StatusOpen,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
if err := store.CreateIssue(ctx, mol, "test"); err != nil {
|
|
t.Fatalf("Failed to create molecule %d: %v", i, err)
|
|
}
|
|
if err := store.CreateIssue(ctx, gate, "test"); err != nil {
|
|
t.Fatalf("Failed to create gate %d: %v", i, err)
|
|
}
|
|
if err := store.CreateIssue(ctx, step, "test"); err != nil {
|
|
t.Fatalf("Failed to create step %d: %v", i, err)
|
|
}
|
|
|
|
// Add dependencies
|
|
if err := store.AddDependency(ctx, &types.Dependency{
|
|
IssueID: gate.ID,
|
|
DependsOnID: mol.ID,
|
|
Type: types.DepParentChild,
|
|
}, "test"); err != nil {
|
|
t.Fatalf("Failed to add gate parent-child %d: %v", i, err)
|
|
}
|
|
if err := store.AddDependency(ctx, &types.Dependency{
|
|
IssueID: step.ID,
|
|
DependsOnID: mol.ID,
|
|
Type: types.DepParentChild,
|
|
}, "test"); err != nil {
|
|
t.Fatalf("Failed to add step parent-child %d: %v", i, err)
|
|
}
|
|
if err := store.AddDependency(ctx, &types.Dependency{
|
|
IssueID: step.ID,
|
|
DependsOnID: gate.ID,
|
|
Type: types.DepBlocks,
|
|
}, "test"); err != nil {
|
|
t.Fatalf("Failed to add blocking dep %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
// Find gate-ready molecules
|
|
molecules, err := findGateReadyMolecules(ctx, store)
|
|
if err != nil {
|
|
t.Fatalf("findGateReadyMolecules failed: %v", err)
|
|
}
|
|
|
|
if len(molecules) != 2 {
|
|
t.Errorf("Expected 2 gate-ready molecules, got %d", len(molecules))
|
|
}
|
|
}
|
|
|