From 8f6e2d2174d23ef51aab70d77e74b52640e27d2d Mon Sep 17 00:00:00 2001 From: dementus Date: Fri, 2 Jan 2026 18:28:31 -0800 Subject: [PATCH] test: Add integration tests for gt install command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- internal/cmd/install_integration_test.go | 261 +++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 internal/cmd/install_integration_test.go diff --git a/internal/cmd/install_integration_test.go b/internal/cmd/install_integration_test.go new file mode 100644 index 00000000..3c63be18 --- /dev/null +++ b/internal/cmd/install_integration_test.go @@ -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) + } +}