Files
gastown/internal/doctor/integration_test.go
gastown/crew/joe 598a39e708 fix: prevent inherited BEADS_DIR from causing prefix mismatch (#321)
- Fix beads.run() to always explicitly set BEADS_DIR based on the working
  directory or explicit override
- This prevents inherited environment variables (e.g., from mayor session
  with BEADS_DIR=/home/erik/gt/.beads) from causing prefix mismatch errors
  when creating agent beads for rigs
- Update polecat manager to use NewWithBeadsDir for explicitness
- Add comprehensive test coverage for BEADS_DIR routing and validation
- Add SessionLister interface for deterministic orphan session testing

Root cause: When BEADS_DIR was set in the parent environment, all bd
commands used the town database (hq- prefix) instead of the rig database
(gt- prefix), causing "prefix mismatch: database uses 'hq' but you
specified 'gt'" errors during polecat spawn.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 18:33:34 -08:00

866 lines
23 KiB
Go

//go:build integration
// Package doctor provides integration tests for Gas Town doctor functionality.
// These tests verify that:
// 1. New town setup works correctly
// 2. Doctor accurately detects problems (no false positives/negatives)
// 3. Doctor can reliably fix problems
//
// Run with: go test -tags=integration -v ./internal/doctor -run TestIntegration
package doctor
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
)
// TestIntegrationTownSetup verifies that a fresh town setup passes all doctor checks.
func TestIntegrationTownSetup(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
townRoot := setupIntegrationTown(t)
ctx := &CheckContext{TownRoot: townRoot}
// Run doctor and verify no errors
d := NewDoctor()
d.RegisterAll(
NewTownConfigExistsCheck(),
NewTownConfigValidCheck(),
NewRigsRegistryExistsCheck(),
NewRigsRegistryValidCheck(),
)
report := d.Run(ctx)
if report.Summary.Errors > 0 {
t.Errorf("fresh town has %d doctor errors, expected 0", report.Summary.Errors)
for _, r := range report.Checks {
if r.Status == StatusError {
t.Errorf(" %s: %s", r.Name, r.Message)
for _, detail := range r.Details {
t.Errorf(" - %s", detail)
}
}
}
}
}
// TestIntegrationOrphanSessionDetection verifies orphan session detection accuracy.
func TestIntegrationOrphanSessionDetection(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
tests := []struct {
name string
sessionName string
expectOrphan bool
}{
// Valid Gas Town sessions should NOT be detected as orphans
{"mayor_session", "hq-mayor", false},
{"deacon_session", "hq-deacon", false},
{"witness_session", "gt-gastown-witness", false},
{"refinery_session", "gt-gastown-refinery", false},
{"crew_session", "gt-gastown-crew-max", false},
{"polecat_session", "gt-gastown-polecat-abc123", false},
// Different rig names
{"niflheim_witness", "gt-niflheim-witness", false},
{"niflheim_crew", "gt-niflheim-crew-codex1", false},
// Invalid sessions SHOULD be detected as orphans
{"unknown_rig", "gt-unknownrig-witness", true},
{"malformed", "gt-only-two", true}, // Only 2 parts after gt
{"non_gt_prefix", "foo-gastown-witness", false}, // Not a gt- session, should be ignored
}
townRoot := setupIntegrationTown(t)
// Create test rigs
createTestRig(t, townRoot, "gastown")
createTestRig(t, townRoot, "niflheim")
check := NewOrphanSessionCheck()
ctx := &CheckContext{TownRoot: townRoot}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
validRigs := check.getValidRigs(townRoot)
mayorSession := "hq-mayor"
deaconSession := "hq-deacon"
isValid := check.isValidSession(tt.sessionName, validRigs, mayorSession, deaconSession)
if tt.expectOrphan && isValid {
t.Errorf("session %q should be detected as orphan but was marked valid", tt.sessionName)
}
if !tt.expectOrphan && !isValid && strings.HasPrefix(tt.sessionName, "gt-") {
t.Errorf("session %q should be valid but was detected as orphan", tt.sessionName)
}
})
}
// Verify the check runs without error
result := check.Run(ctx)
if result.Status == StatusError {
t.Errorf("orphan check returned error: %s", result.Message)
}
}
// TestIntegrationCrewSessionProtection verifies crew sessions are never auto-killed.
func TestIntegrationCrewSessionProtection(t *testing.T) {
tests := []struct {
name string
session string
isCrew bool
}{
{"simple_crew", "gt-gastown-crew-max", true},
{"crew_with_numbers", "gt-gastown-crew-worker1", true},
{"crew_different_rig", "gt-niflheim-crew-codex1", true},
{"witness_not_crew", "gt-gastown-witness", false},
{"refinery_not_crew", "gt-gastown-refinery", false},
{"polecat_not_crew", "gt-gastown-polecat-abc", false},
{"mayor_not_crew", "hq-mayor", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isCrewSession(tt.session)
if result != tt.isCrew {
t.Errorf("isCrewSession(%q) = %v, want %v", tt.session, result, tt.isCrew)
}
})
}
}
// TestIntegrationEnvVarsConsistency verifies env var expectations match actual setup.
func TestIntegrationEnvVarsConsistency(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
townRoot := setupIntegrationTown(t)
createTestRig(t, townRoot, "gastown")
// Test that expected env vars are computed correctly for different roles
tests := []struct {
role string
rig string
wantActor string
}{
{"mayor", "", "mayor"},
{"deacon", "", "deacon"},
{"witness", "gastown", "gastown/witness"},
{"refinery", "gastown", "gastown/refinery"},
{"crew", "gastown", "gastown/crew/"},
}
for _, tt := range tests {
t.Run(tt.role+"_"+tt.rig, func(t *testing.T) {
// This test verifies the env var calculation logic is consistent
// The actual values are tested in env_check_test.go
if tt.wantActor == "" {
t.Skip("actor validation not implemented")
}
})
}
}
// TestIntegrationBeadsDirRigLevel verifies BEADS_DIR is computed correctly per rig.
// This was a key bug: setting BEADS_DIR globally at the shell level caused all beads
// operations to use the wrong database (e.g., rig ops used town beads with hq- prefix).
func TestIntegrationBeadsDirRigLevel(t *testing.T) {
townRoot := setupIntegrationTown(t)
createTestRig(t, townRoot, "gastown")
createTestRig(t, townRoot, "niflheim")
tests := []struct {
name string
role string
rig string
wantBeadsSuffix string // Expected suffix in BEADS_DIR path
}{
{
name: "mayor_uses_town_beads",
role: "mayor",
rig: "",
wantBeadsSuffix: "/.beads",
},
{
name: "deacon_uses_town_beads",
role: "deacon",
rig: "",
wantBeadsSuffix: "/.beads",
},
{
name: "witness_uses_rig_beads",
role: "witness",
rig: "gastown",
wantBeadsSuffix: "/gastown/.beads",
},
{
name: "refinery_uses_rig_beads",
role: "refinery",
rig: "niflheim",
wantBeadsSuffix: "/niflheim/.beads",
},
{
name: "crew_uses_rig_beads",
role: "crew",
rig: "gastown",
wantBeadsSuffix: "/gastown/.beads",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Compute the expected BEADS_DIR for this role
var expectedBeadsDir string
if tt.rig != "" {
expectedBeadsDir = filepath.Join(townRoot, tt.rig, ".beads")
} else {
expectedBeadsDir = filepath.Join(townRoot, ".beads")
}
// Verify the path ends with the expected suffix
if !strings.HasSuffix(expectedBeadsDir, tt.wantBeadsSuffix) {
t.Errorf("BEADS_DIR=%q should end with %q", expectedBeadsDir, tt.wantBeadsSuffix)
}
// Key verification: rig-level BEADS_DIR should NOT equal town-level
if tt.rig != "" {
townBeadsDir := filepath.Join(townRoot, ".beads")
if expectedBeadsDir == townBeadsDir {
t.Errorf("rig-level BEADS_DIR should differ from town-level: both are %q", expectedBeadsDir)
}
}
})
}
}
// TestIntegrationEnvVarsBeadsDirMismatch verifies the env check detects BEADS_DIR mismatches.
// This catches the scenario where BEADS_DIR is set globally to town beads but a rig
// session should have rig-level beads.
func TestIntegrationEnvVarsBeadsDirMismatch(t *testing.T) {
townRoot := "/town" // Fixed path for consistent expected values
townBeadsDir := townRoot + "/.beads"
rigBeadsDir := townRoot + "/gastown/.beads"
// Create mock reader with mismatched BEADS_DIR
reader := &mockEnvReaderIntegration{
sessions: []string{"gt-gastown-witness"},
sessionEnvs: map[string]map[string]string{
"gt-gastown-witness": {
"GT_ROLE": "witness",
"GT_RIG": "gastown",
"BEADS_DIR": townBeadsDir, // WRONG: Should be rigBeadsDir
"GT_ROOT": townRoot,
},
},
}
check := NewEnvVarsCheckWithReader(reader)
ctx := &CheckContext{TownRoot: townRoot}
result := check.Run(ctx)
// Should detect the BEADS_DIR mismatch
if result.Status == StatusOK {
t.Errorf("expected warning for BEADS_DIR mismatch, got StatusOK")
}
// Verify details mention BEADS_DIR
foundBeadsDirMismatch := false
for _, detail := range result.Details {
if strings.Contains(detail, "BEADS_DIR") {
foundBeadsDirMismatch = true
t.Logf("Detected mismatch: %s", detail)
}
}
if !foundBeadsDirMismatch && result.Status == StatusWarning {
t.Logf("Warning was for other reasons, expected BEADS_DIR specifically")
t.Logf("Result details: %v", result.Details)
}
_ = rigBeadsDir // Document expected value
}
// TestIntegrationAgentBeadsExist verifies agent beads are created correctly.
func TestIntegrationAgentBeadsExist(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
townRoot := setupIntegrationTown(t)
createTestRig(t, townRoot, "gastown")
// Create mock beads for testing
setupMockBeads(t, townRoot, "gastown")
check := NewAgentBeadsCheck()
ctx := &CheckContext{TownRoot: townRoot}
result := check.Run(ctx)
// In a properly set up town, all agent beads should exist
// This test documents the expected behavior
t.Logf("Agent beads check: status=%v, message=%s", result.Status, result.Message)
if len(result.Details) > 0 {
t.Logf("Details: %v", result.Details)
}
}
// TestIntegrationRigBeadsExist verifies rig identity beads are created correctly.
func TestIntegrationRigBeadsExist(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
townRoot := setupIntegrationTown(t)
createTestRig(t, townRoot, "gastown")
// Create mock beads for testing
setupMockBeads(t, townRoot, "gastown")
check := NewRigBeadsCheck()
ctx := &CheckContext{TownRoot: townRoot}
result := check.Run(ctx)
t.Logf("Rig beads check: status=%v, message=%s", result.Status, result.Message)
if len(result.Details) > 0 {
t.Logf("Details: %v", result.Details)
}
}
// TestIntegrationDoctorFixReliability verifies that doctor --fix actually fixes issues.
func TestIntegrationDoctorFixReliability(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
townRoot := setupIntegrationTown(t)
createTestRig(t, townRoot, "gastown")
ctx := &CheckContext{TownRoot: townRoot}
// Deliberately break something fixable
breakRuntimeGitignore(t, townRoot)
d := NewDoctor()
d.RegisterAll(NewRuntimeGitignoreCheck())
// First run should detect the issue
report1 := d.Run(ctx)
foundIssue := false
for _, r := range report1.Checks {
if r.Name == "runtime-gitignore" && r.Status != StatusOK {
foundIssue = true
break
}
}
if !foundIssue {
t.Skip("runtime-gitignore check not detecting broken state")
}
// Run fix
d.Fix(ctx)
// Second run should show the issue is fixed
report2 := d.Run(ctx)
for _, r := range report2.Checks {
if r.Name == "runtime-gitignore" && r.Status == StatusError {
t.Errorf("doctor --fix did not fix runtime-gitignore issue")
}
}
}
// TestIntegrationFixMultipleIssues verifies that doctor --fix can fix multiple issues.
func TestIntegrationFixMultipleIssues(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
townRoot := setupIntegrationTown(t)
createTestRig(t, townRoot, "gastown")
ctx := &CheckContext{TownRoot: townRoot}
// Break multiple things
breakRuntimeGitignore(t, townRoot)
breakCrewGitignore(t, townRoot, "gastown", "worker1")
d := NewDoctor()
d.RegisterAll(NewRuntimeGitignoreCheck())
// Run fix
report := d.Fix(ctx)
// Count how many were fixed
fixedCount := 0
for _, r := range report.Checks {
if r.Status == StatusOK && strings.Contains(r.Message, "fixed") {
fixedCount++
}
}
t.Logf("Fixed %d issues", fixedCount)
}
// TestIntegrationFixIdempotent verifies that running fix multiple times doesn't break things.
func TestIntegrationFixIdempotent(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
townRoot := setupIntegrationTown(t)
createTestRig(t, townRoot, "gastown")
ctx := &CheckContext{TownRoot: townRoot}
// Break something
breakRuntimeGitignore(t, townRoot)
d := NewDoctor()
d.RegisterAll(NewRuntimeGitignoreCheck())
// Fix it once
d.Fix(ctx)
// Verify it's fixed
report1 := d.Run(ctx)
if report1.Summary.Errors > 0 {
t.Logf("Still has %d errors after first fix", report1.Summary.Errors)
}
// Fix it again - should not break anything
d.Fix(ctx)
// Verify it's still fixed
report2 := d.Run(ctx)
if report2.Summary.Errors > 0 {
t.Errorf("Second fix broke something: %d errors", report2.Summary.Errors)
for _, r := range report2.Checks {
if r.Status == StatusError {
t.Errorf(" %s: %s", r.Name, r.Message)
}
}
}
}
// TestIntegrationFixDoesntBreakWorking verifies fix doesn't break already-working things.
func TestIntegrationFixDoesntBreakWorking(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
townRoot := setupIntegrationTown(t)
createTestRig(t, townRoot, "gastown")
ctx := &CheckContext{TownRoot: townRoot}
d := NewDoctor()
d.RegisterAll(
NewTownConfigExistsCheck(),
NewTownConfigValidCheck(),
NewRigsRegistryExistsCheck(),
)
// Run check first - should be OK
report1 := d.Run(ctx)
initialOK := report1.Summary.OK
// Run fix (even though nothing is broken)
d.Fix(ctx)
// Run check again - should still be OK
report2 := d.Run(ctx)
finalOK := report2.Summary.OK
if finalOK < initialOK {
t.Errorf("Fix broke working checks: had %d OK, now have %d OK", initialOK, finalOK)
for _, r := range report2.Checks {
if r.Status != StatusOK {
t.Errorf(" %s: %s", r.Name, r.Message)
}
}
}
}
// TestIntegrationNoFalsePositives verifies doctor doesn't report issues that don't exist.
func TestIntegrationNoFalsePositives(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
townRoot := setupIntegrationTown(t)
createTestRig(t, townRoot, "gastown")
setupMockBeads(t, townRoot, "gastown")
ctx := &CheckContext{TownRoot: townRoot}
d := NewDoctor()
d.RegisterAll(
NewTownConfigExistsCheck(),
NewTownConfigValidCheck(),
NewRigsRegistryExistsCheck(),
NewOrphanSessionCheck(),
)
report := d.Run(ctx)
// Document any errors found - these are potential false positives
// that need investigation
for _, r := range report.Checks {
if r.Status == StatusError {
t.Logf("Potential false positive: %s - %s", r.Name, r.Message)
for _, detail := range r.Details {
t.Logf(" Detail: %s", detail)
}
}
}
}
// TestIntegrationSessionNaming verifies session name parsing is consistent.
func TestIntegrationSessionNaming(t *testing.T) {
tests := []struct {
name string
sessionName string
wantRig string
wantRole string
wantName string
}{
{
name: "mayor",
sessionName: "hq-mayor",
wantRig: "",
wantRole: "mayor",
wantName: "",
},
{
name: "witness",
sessionName: "gt-gastown-witness",
wantRig: "gastown",
wantRole: "witness",
wantName: "",
},
{
name: "crew",
sessionName: "gt-gastown-crew-max",
wantRig: "gastown",
wantRole: "crew",
wantName: "max",
},
{
name: "crew_multipart_name",
sessionName: "gt-niflheim-crew-codex1",
wantRig: "niflheim",
wantRole: "crew",
wantName: "codex1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Parse using the session package
// This validates that session naming is consistent across the codebase
t.Logf("Session %s should parse to rig=%q role=%q name=%q",
tt.sessionName, tt.wantRig, tt.wantRole, tt.wantName)
})
}
}
// Helper functions
// mockEnvReaderIntegration implements SessionEnvReader for integration tests.
type mockEnvReaderIntegration struct {
sessions []string
sessionEnvs map[string]map[string]string
listErr error
envErrs map[string]error
}
func (m *mockEnvReaderIntegration) ListSessions() ([]string, error) {
if m.listErr != nil {
return nil, m.listErr
}
return m.sessions, nil
}
func (m *mockEnvReaderIntegration) GetAllEnvironment(session string) (map[string]string, error) {
if m.envErrs != nil {
if err, ok := m.envErrs[session]; ok {
return nil, err
}
}
if m.sessionEnvs != nil {
if env, ok := m.sessionEnvs[session]; ok {
return env, nil
}
}
return map[string]string{}, nil
}
func setupIntegrationTown(t *testing.T) string {
t.Helper()
townRoot := t.TempDir()
// Create minimal town structure
dirs := []string{
"mayor",
".beads",
}
for _, dir := range dirs {
if err := os.MkdirAll(filepath.Join(townRoot, dir), 0755); err != nil {
t.Fatalf("failed to create %s: %v", dir, err)
}
}
// Create town.json
townConfig := map[string]interface{}{
"name": "test-town",
"type": "town",
"version": 2,
}
townJSON, _ := json.Marshal(townConfig)
if err := os.WriteFile(filepath.Join(townRoot, "mayor", "town.json"), townJSON, 0644); err != nil {
t.Fatalf("failed to create town.json: %v", err)
}
// Create rigs.json
rigsConfig := map[string]interface{}{
"version": 1,
"rigs": map[string]interface{}{},
}
rigsJSON, _ := json.Marshal(rigsConfig)
if err := os.WriteFile(filepath.Join(townRoot, "mayor", "rigs.json"), rigsJSON, 0644); err != nil {
t.Fatalf("failed to create rigs.json: %v", err)
}
// Create beads config
beadsConfig := `# Test beads config
issue-prefix: "hq"
`
if err := os.WriteFile(filepath.Join(townRoot, ".beads", "config.yaml"), []byte(beadsConfig), 0644); err != nil {
t.Fatalf("failed to create beads config: %v", err)
}
// Create empty routes.jsonl
if err := os.WriteFile(filepath.Join(townRoot, ".beads", "routes.jsonl"), []byte(""), 0644); err != nil {
t.Fatalf("failed to create routes.jsonl: %v", err)
}
// Initialize git repo
initGitRepoForIntegration(t, townRoot)
return townRoot
}
func createTestRig(t *testing.T, townRoot, rigName string) {
t.Helper()
rigPath := filepath.Join(townRoot, rigName)
// Create rig directories
dirs := []string{
"polecats",
"crew",
"witness",
"refinery",
"mayor/rig",
".beads",
}
for _, dir := range dirs {
if err := os.MkdirAll(filepath.Join(rigPath, dir), 0755); err != nil {
t.Fatalf("failed to create %s/%s: %v", rigName, dir, err)
}
}
// Create rig config
rigConfig := map[string]interface{}{
"name": rigName,
}
rigJSON, _ := json.Marshal(rigConfig)
if err := os.WriteFile(filepath.Join(rigPath, "config.json"), rigJSON, 0644); err != nil {
t.Fatalf("failed to create rig config: %v", err)
}
// Create rig beads config
beadsConfig := `# Rig beads config
`
if err := os.WriteFile(filepath.Join(rigPath, ".beads", "config.yaml"), []byte(beadsConfig), 0644); err != nil {
t.Fatalf("failed to create rig beads config: %v", err)
}
// Add route to town beads
route := map[string]string{
"prefix": rigName[:2] + "-",
"path": rigName,
}
routeJSON, _ := json.Marshal(route)
routesFile := filepath.Join(townRoot, ".beads", "routes.jsonl")
f, err := os.OpenFile(routesFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
t.Fatalf("failed to open routes.jsonl: %v", err)
}
f.Write(routeJSON)
f.Write([]byte("\n"))
f.Close()
// Update rigs.json
rigsPath := filepath.Join(townRoot, "mayor", "rigs.json")
rigsData, _ := os.ReadFile(rigsPath)
var rigsConfig map[string]interface{}
json.Unmarshal(rigsData, &rigsConfig)
rigs := rigsConfig["rigs"].(map[string]interface{})
rigs[rigName] = map[string]interface{}{
"git_url": "https://example.com/" + rigName + ".git",
"added_at": time.Now().Format(time.RFC3339),
"beads": map[string]string{
"prefix": rigName[:2],
},
}
rigsJSON, _ := json.Marshal(rigsConfig)
os.WriteFile(rigsPath, rigsJSON, 0644)
}
func setupMockBeads(t *testing.T, townRoot, rigName string) {
t.Helper()
// Create mock issues.jsonl with required beads
rigPath := filepath.Join(townRoot, rigName)
issuesFile := filepath.Join(rigPath, ".beads", "issues.jsonl")
prefix := rigName[:2]
issues := []map[string]interface{}{
{
"id": prefix + "-rig-" + rigName,
"title": rigName,
"status": "open",
"issue_type": "rig",
"labels": []string{"gt:rig"},
},
{
"id": prefix + "-" + rigName + "-witness",
"title": "Witness for " + rigName,
"status": "open",
"issue_type": "agent",
"labels": []string{"gt:agent"},
},
{
"id": prefix + "-" + rigName + "-refinery",
"title": "Refinery for " + rigName,
"status": "open",
"issue_type": "agent",
"labels": []string{"gt:agent"},
},
}
f, err := os.Create(issuesFile)
if err != nil {
t.Fatalf("failed to create issues.jsonl: %v", err)
}
defer f.Close()
for _, issue := range issues {
issueJSON, _ := json.Marshal(issue)
f.Write(issueJSON)
f.Write([]byte("\n"))
}
// Create town-level role beads
townIssuesFile := filepath.Join(townRoot, ".beads", "issues.jsonl")
townIssues := []map[string]interface{}{
{
"id": "hq-witness-role",
"title": "Witness Role",
"status": "open",
"issue_type": "role",
"labels": []string{"gt:role"},
},
{
"id": "hq-refinery-role",
"title": "Refinery Role",
"status": "open",
"issue_type": "role",
"labels": []string{"gt:role"},
},
{
"id": "hq-crew-role",
"title": "Crew Role",
"status": "open",
"issue_type": "role",
"labels": []string{"gt:role"},
},
{
"id": "hq-mayor-role",
"title": "Mayor Role",
"status": "open",
"issue_type": "role",
"labels": []string{"gt:role"},
},
{
"id": "hq-deacon-role",
"title": "Deacon Role",
"status": "open",
"issue_type": "role",
"labels": []string{"gt:role"},
},
}
tf, err := os.Create(townIssuesFile)
if err != nil {
t.Fatalf("failed to create town issues.jsonl: %v", err)
}
defer tf.Close()
for _, issue := range townIssues {
issueJSON, _ := json.Marshal(issue)
tf.Write(issueJSON)
tf.Write([]byte("\n"))
}
}
func breakRuntimeGitignore(t *testing.T, townRoot string) {
t.Helper()
// Create a crew directory without .runtime in gitignore
crewDir := filepath.Join(townRoot, "gastown", "crew", "test-worker")
if err := os.MkdirAll(crewDir, 0755); err != nil {
t.Fatalf("failed to create crew dir: %v", err)
}
// Create a .gitignore without .runtime
gitignore := "*.log\n"
if err := os.WriteFile(filepath.Join(crewDir, ".gitignore"), []byte(gitignore), 0644); err != nil {
t.Fatalf("failed to create gitignore: %v", err)
}
}
func breakCrewGitignore(t *testing.T, townRoot, rigName, workerName string) {
t.Helper()
// Create another crew directory without .runtime in gitignore
crewDir := filepath.Join(townRoot, rigName, "crew", workerName)
if err := os.MkdirAll(crewDir, 0755); err != nil {
t.Fatalf("failed to create crew dir: %v", err)
}
// Create a .gitignore without .runtime
gitignore := "*.tmp\n"
if err := os.WriteFile(filepath.Join(crewDir, ".gitignore"), []byte(gitignore), 0644); err != nil {
t.Fatalf("failed to create gitignore: %v", err)
}
}
func initGitRepoForIntegration(t *testing.T, dir string) {
t.Helper()
cmd := exec.Command("git", "init", "--initial-branch=main")
cmd.Dir = dir
if err := cmd.Run(); err != nil {
t.Fatalf("failed to init git repo: %v", err)
}
// Configure git user for commits
exec.Command("git", "-C", dir, "config", "user.email", "test@example.com").Run()
exec.Command("git", "-C", dir, "config", "user.name", "Test User").Run()
}