test: Add integration tests for gt install command

- TestInstallCreatesCorrectStructure: validates directory structure
  (mayor/, rigs/, CLAUDE.md) and config files (town.json, rigs.json)
- TestInstallBeadsHasCorrectPrefix: verifies beads uses hq- prefix
- TestInstallIdempotent: tests --force flag behavior
- TestInstallNoBeadsFlag: tests --no-beads option

Run with: go test -tags=integration ./internal/cmd/ -run TestInstall

(gt-htlmp.2)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
dementus
2026-01-02 18:28:31 -08:00
committed by Steve Yegge
parent 89785378bf
commit 8f6e2d2174

View File

@@ -0,0 +1,261 @@
//go:build integration
package cmd
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/steveyegge/gastown/internal/config"
)
// TestInstallCreatesCorrectStructure validates that a fresh gt install
// creates the expected directory structure and configuration files.
func TestInstallCreatesCorrectStructure(t *testing.T) {
tmpDir := t.TempDir()
hqPath := filepath.Join(tmpDir, "test-hq")
// Build gt binary for testing
gtBinary := buildGT(t)
// Run gt install
cmd := exec.Command(gtBinary, "install", hqPath, "--name", "test-town")
cmd.Env = append(os.Environ(), "HOME="+tmpDir)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("gt install failed: %v\nOutput: %s", err, output)
}
// Verify directory structure
assertDirExists(t, hqPath, "HQ root")
assertDirExists(t, filepath.Join(hqPath, "mayor"), "mayor/")
assertDirExists(t, filepath.Join(hqPath, "rigs"), "rigs/")
// Verify mayor/town.json
townPath := filepath.Join(hqPath, "mayor", "town.json")
assertFileExists(t, townPath, "mayor/town.json")
townConfig, err := config.LoadTownConfig(townPath)
if err != nil {
t.Fatalf("failed to load town.json: %v", err)
}
if townConfig.Type != "town" {
t.Errorf("town.json type = %q, want %q", townConfig.Type, "town")
}
if townConfig.Name != "test-town" {
t.Errorf("town.json name = %q, want %q", townConfig.Name, "test-town")
}
// Verify mayor/rigs.json
rigsPath := filepath.Join(hqPath, "mayor", "rigs.json")
assertFileExists(t, rigsPath, "mayor/rigs.json")
rigsConfig, err := config.LoadRigsConfig(rigsPath)
if err != nil {
t.Fatalf("failed to load rigs.json: %v", err)
}
if len(rigsConfig.Rigs) != 0 {
t.Errorf("rigs.json should be empty, got %d rigs", len(rigsConfig.Rigs))
}
// Verify mayor/state.json
statePath := filepath.Join(hqPath, "mayor", "state.json")
assertFileExists(t, statePath, "mayor/state.json")
stateData, err := os.ReadFile(statePath)
if err != nil {
t.Fatalf("failed to read state.json: %v", err)
}
var state map[string]interface{}
if err := json.Unmarshal(stateData, &state); err != nil {
t.Fatalf("failed to parse state.json: %v", err)
}
if state["role"] != "mayor" {
t.Errorf("state.json role = %q, want %q", state["role"], "mayor")
}
// Verify CLAUDE.md exists
claudePath := filepath.Join(hqPath, "CLAUDE.md")
assertFileExists(t, claudePath, "CLAUDE.md")
}
// TestInstallBeadsHasCorrectPrefix validates that beads is initialized
// with the correct "hq-" prefix for town-level beads.
func TestInstallBeadsHasCorrectPrefix(t *testing.T) {
// Skip if bd is not available
if _, err := exec.LookPath("bd"); err != nil {
t.Skip("bd not installed, skipping beads prefix test")
}
tmpDir := t.TempDir()
hqPath := filepath.Join(tmpDir, "test-hq")
// Build gt binary for testing
gtBinary := buildGT(t)
// Run gt install (includes beads init by default)
cmd := exec.Command(gtBinary, "install", hqPath)
cmd.Env = append(os.Environ(), "HOME="+tmpDir)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("gt install failed: %v\nOutput: %s", err, output)
}
// Verify .beads/ directory exists
beadsDir := filepath.Join(hqPath, ".beads")
assertDirExists(t, beadsDir, ".beads/")
// Verify beads database was created
dbPath := filepath.Join(beadsDir, "beads.db")
assertFileExists(t, dbPath, ".beads/beads.db")
// Verify prefix by running bd config get issue_prefix
// Use --no-daemon to avoid daemon startup issues in test environment
bdCmd := exec.Command("bd", "--no-daemon", "config", "get", "issue_prefix")
bdCmd.Dir = hqPath
prefixOutput, err := bdCmd.Output() // Use Output() to get only stdout
if err != nil {
// If Output() fails, try CombinedOutput for better error info
combinedOut, _ := exec.Command("bd", "--no-daemon", "config", "get", "issue_prefix").CombinedOutput()
t.Fatalf("bd config get issue_prefix failed: %v\nOutput: %s", err, combinedOut)
}
prefix := strings.TrimSpace(string(prefixOutput))
if prefix != "hq" {
t.Errorf("beads issue_prefix = %q, want %q", prefix, "hq")
}
}
// TestInstallIdempotent validates that running gt install twice
// on the same directory fails without --force flag.
func TestInstallIdempotent(t *testing.T) {
tmpDir := t.TempDir()
hqPath := filepath.Join(tmpDir, "test-hq")
gtBinary := buildGT(t)
// First install should succeed
cmd := exec.Command(gtBinary, "install", hqPath, "--no-beads")
cmd.Env = append(os.Environ(), "HOME="+tmpDir)
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("first install failed: %v\nOutput: %s", err, output)
}
// Second install without --force should fail
cmd = exec.Command(gtBinary, "install", hqPath, "--no-beads")
cmd.Env = append(os.Environ(), "HOME="+tmpDir)
output, err := cmd.CombinedOutput()
if err == nil {
t.Fatal("second install should have failed without --force")
}
if !strings.Contains(string(output), "already a Gas Town HQ") {
t.Errorf("expected 'already a Gas Town HQ' error, got: %s", output)
}
// Third install with --force should succeed
cmd = exec.Command(gtBinary, "install", hqPath, "--no-beads", "--force")
cmd.Env = append(os.Environ(), "HOME="+tmpDir)
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("install with --force failed: %v\nOutput: %s", err, output)
}
}
// TestInstallNoBeadsFlag validates that --no-beads skips beads initialization.
func TestInstallNoBeadsFlag(t *testing.T) {
tmpDir := t.TempDir()
hqPath := filepath.Join(tmpDir, "test-hq")
gtBinary := buildGT(t)
// Run gt install with --no-beads
cmd := exec.Command(gtBinary, "install", hqPath, "--no-beads")
cmd.Env = append(os.Environ(), "HOME="+tmpDir)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("gt install --no-beads failed: %v\nOutput: %s", err, output)
}
// Verify .beads/ directory does NOT exist
beadsDir := filepath.Join(hqPath, ".beads")
if _, err := os.Stat(beadsDir); !os.IsNotExist(err) {
t.Errorf(".beads/ should not exist with --no-beads flag")
}
}
// buildGT builds the gt binary and returns its path.
// It caches the build across tests in the same run.
var cachedGTBinary string
func buildGT(t *testing.T) string {
t.Helper()
if cachedGTBinary != "" {
// Verify cached binary still exists
if _, err := os.Stat(cachedGTBinary); err == nil {
return cachedGTBinary
}
// Binary was cleaned up, rebuild
cachedGTBinary = ""
}
// Find project root (where go.mod is)
wd, err := os.Getwd()
if err != nil {
t.Fatalf("failed to get working directory: %v", err)
}
// Walk up to find go.mod
projectRoot := wd
for {
if _, err := os.Stat(filepath.Join(projectRoot, "go.mod")); err == nil {
break
}
parent := filepath.Dir(projectRoot)
if parent == projectRoot {
t.Fatal("could not find project root (go.mod)")
}
projectRoot = parent
}
// Build gt binary to a persistent temp location (not per-test)
tmpDir := os.TempDir()
tmpBinary := filepath.Join(tmpDir, "gt-integration-test")
cmd := exec.Command("go", "build", "-o", tmpBinary, "./cmd/gt")
cmd.Dir = projectRoot
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("failed to build gt: %v\nOutput: %s", err, output)
}
cachedGTBinary = tmpBinary
return tmpBinary
}
// assertDirExists checks that the given path exists and is a directory.
func assertDirExists(t *testing.T, path, name string) {
t.Helper()
info, err := os.Stat(path)
if err != nil {
t.Errorf("%s does not exist: %v", name, err)
return
}
if !info.IsDir() {
t.Errorf("%s is not a directory", name)
}
}
// assertFileExists checks that the given path exists and is a file.
func assertFileExists(t *testing.T, path, name string) {
t.Helper()
info, err := os.Stat(path)
if err != nil {
t.Errorf("%s does not exist: %v", name, err)
return
}
if info.IsDir() {
t.Errorf("%s is a directory, expected file", name)
}
}