Fixing unit tests on windows (#813)
* Add Windows stub for orphan cleanup * Fix account switch tests on Windows * Make query session events test portable * Disable beads daemon in query session events test * Add Windows bd stubs for sling tests * Make expandOutputPath test OS-agnostic * Make role_agents test Windows-friendly * Make config path tests OS-agnostic * Make HealthCheckStateFile test OS-agnostic * Skip orphan process check on Windows * Normalize sparse checkout detail paths * Make dog path tests OS-agnostic * Fix bare repo refspec config on Windows * Add Windows process detection for locks * Add Windows CI workflow * Make mail path tests OS-agnostic * Skip plugin file mode test on Windows * Skip tmux-dependent polecat tests on Windows * Normalize polecat paths and AGENTS.md content * Make beads init failure test Windows-friendly * Skip rig agent bead init test on Windows * Make XDG path tests OS-agnostic * Make exec tests portable on Windows * Adjust atomic write tests for Windows * Make wisp tests Windows-friendly * Make workspace find tests OS-agnostic * Fix Windows rig add integration test * Make sling var logging Windows-friendly * Fix sling attached molecule update ordering --------- Co-authored-by: Johann Dirry <johann.dirry@microsea.at>
This commit is contained in:
@@ -3,6 +3,8 @@ package cmd
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -54,15 +56,33 @@ func setupTestTownForAccount(t *testing.T) (townRoot string, accountsDir string)
|
||||
return townRoot, accountsDir
|
||||
}
|
||||
|
||||
func setTestHome(t *testing.T, fakeHome string) {
|
||||
t.Helper()
|
||||
|
||||
t.Setenv("HOME", fakeHome)
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
return
|
||||
}
|
||||
|
||||
t.Setenv("USERPROFILE", fakeHome)
|
||||
|
||||
drive := filepath.VolumeName(fakeHome)
|
||||
if drive == "" {
|
||||
return
|
||||
}
|
||||
|
||||
t.Setenv("HOMEDRIVE", drive)
|
||||
t.Setenv("HOMEPATH", strings.TrimPrefix(fakeHome, drive))
|
||||
}
|
||||
|
||||
func TestAccountSwitch(t *testing.T) {
|
||||
t.Run("switch between accounts", func(t *testing.T) {
|
||||
townRoot, accountsDir := setupTestTownForAccount(t)
|
||||
|
||||
// Create fake home directory for ~/.claude
|
||||
fakeHome := t.TempDir()
|
||||
originalHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", fakeHome)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
setTestHome(t, fakeHome)
|
||||
|
||||
// Create account config directories
|
||||
workConfigDir := filepath.Join(accountsDir, "work")
|
||||
@@ -133,9 +153,7 @@ func TestAccountSwitch(t *testing.T) {
|
||||
townRoot, accountsDir := setupTestTownForAccount(t)
|
||||
|
||||
fakeHome := t.TempDir()
|
||||
originalHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", fakeHome)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
setTestHome(t, fakeHome)
|
||||
|
||||
workConfigDir := filepath.Join(accountsDir, "work")
|
||||
if err := os.MkdirAll(workConfigDir, 0755); err != nil {
|
||||
@@ -186,9 +204,7 @@ func TestAccountSwitch(t *testing.T) {
|
||||
townRoot, accountsDir := setupTestTownForAccount(t)
|
||||
|
||||
fakeHome := t.TempDir()
|
||||
originalHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", fakeHome)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
setTestHome(t, fakeHome)
|
||||
|
||||
workConfigDir := filepath.Join(accountsDir, "work")
|
||||
if err := os.MkdirAll(workConfigDir, 0755); err != nil {
|
||||
@@ -224,9 +240,7 @@ func TestAccountSwitch(t *testing.T) {
|
||||
townRoot, accountsDir := setupTestTownForAccount(t)
|
||||
|
||||
fakeHome := t.TempDir()
|
||||
originalHome := os.Getenv("HOME")
|
||||
os.Setenv("HOME", fakeHome)
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
setTestHome(t, fakeHome)
|
||||
|
||||
workConfigDir := filepath.Join(accountsDir, "work")
|
||||
personalConfigDir := filepath.Join(accountsDir, "personal")
|
||||
|
||||
@@ -24,6 +24,11 @@ func filterGTEnv(env []string) []string {
|
||||
return filtered
|
||||
}
|
||||
|
||||
func testSubprocessEnv() []string {
|
||||
env := filterGTEnv(os.Environ())
|
||||
return append(env, "BEADS_NO_DAEMON=1")
|
||||
}
|
||||
|
||||
// TestQuerySessionEvents_FindsEventsFromAllLocations verifies that querySessionEvents
|
||||
// finds session.ended events from both town-level and rig-level beads databases.
|
||||
//
|
||||
@@ -37,13 +42,14 @@ func filterGTEnv(env []string) []string {
|
||||
// 2. Creates session.ended events in both town and rig beads
|
||||
// 3. Verifies querySessionEvents finds events from both locations
|
||||
func TestQuerySessionEvents_FindsEventsFromAllLocations(t *testing.T) {
|
||||
// Skip if gt and bd are not installed
|
||||
if _, err := exec.LookPath("gt"); err != nil {
|
||||
t.Skip("gt not installed, skipping integration test")
|
||||
}
|
||||
// Skip if bd is not installed
|
||||
if _, err := exec.LookPath("bd"); err != nil {
|
||||
t.Skip("bd not installed, skipping integration test")
|
||||
}
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
t.Skip("git not installed, skipping integration test")
|
||||
}
|
||||
gtBinary := buildGT(t)
|
||||
|
||||
// Skip when running inside a Gas Town workspace - this integration test
|
||||
// creates a separate workspace and the subprocesses can interact with
|
||||
@@ -51,6 +57,7 @@ func TestQuerySessionEvents_FindsEventsFromAllLocations(t *testing.T) {
|
||||
if os.Getenv("GT_TOWN_ROOT") != "" || os.Getenv("BD_ACTOR") != "" {
|
||||
t.Skip("skipping integration test inside Gas Town workspace (use 'go test' outside workspace)")
|
||||
}
|
||||
t.Setenv("BEADS_NO_DAEMON", "1")
|
||||
|
||||
// Create a temporary directory structure
|
||||
tmpDir := t.TempDir()
|
||||
@@ -70,9 +77,9 @@ func TestQuerySessionEvents_FindsEventsFromAllLocations(t *testing.T) {
|
||||
|
||||
// Use gt install to set up the town
|
||||
// Clear GT environment variables to isolate test from parent workspace
|
||||
gtInstallCmd := exec.Command("gt", "install")
|
||||
gtInstallCmd := exec.Command(gtBinary, "install")
|
||||
gtInstallCmd.Dir = townRoot
|
||||
gtInstallCmd.Env = filterGTEnv(os.Environ())
|
||||
gtInstallCmd.Env = testSubprocessEnv()
|
||||
if out, err := gtInstallCmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("gt install: %v\n%s", err, out)
|
||||
}
|
||||
@@ -92,10 +99,27 @@ func TestQuerySessionEvents_FindsEventsFromAllLocations(t *testing.T) {
|
||||
}
|
||||
|
||||
// Add initial commit to bare repo
|
||||
initFileCmd := exec.Command("bash", "-c", "echo 'test' > README.md && git add . && git commit -m 'init'")
|
||||
initFileCmd.Dir = tempClone
|
||||
if out, err := initFileCmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("initial commit: %v\n%s", err, out)
|
||||
readmePath := filepath.Join(tempClone, "README.md")
|
||||
if err := os.WriteFile(readmePath, []byte("test\n"), 0644); err != nil {
|
||||
t.Fatalf("write README: %v", err)
|
||||
}
|
||||
|
||||
gitAddCmd := exec.Command("git", "add", ".")
|
||||
gitAddCmd.Dir = tempClone
|
||||
if out, err := gitAddCmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("git add: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
gitCommitCmd := exec.Command("git", "commit", "-m", "init")
|
||||
gitCommitCmd.Dir = tempClone
|
||||
gitCommitCmd.Env = append(os.Environ(),
|
||||
"GIT_AUTHOR_NAME=Test",
|
||||
"GIT_AUTHOR_EMAIL=test@example.com",
|
||||
"GIT_COMMITTER_NAME=Test",
|
||||
"GIT_COMMITTER_EMAIL=test@example.com",
|
||||
)
|
||||
if out, err := gitCommitCmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("git commit: %v\n%s", err, out)
|
||||
}
|
||||
pushCmd := exec.Command("git", "push", "origin", "main")
|
||||
pushCmd.Dir = tempClone
|
||||
@@ -109,9 +133,9 @@ func TestQuerySessionEvents_FindsEventsFromAllLocations(t *testing.T) {
|
||||
}
|
||||
|
||||
// Add rig using gt rig add
|
||||
rigAddCmd := exec.Command("gt", "rig", "add", "testrig", bareRepo, "--prefix=tr")
|
||||
rigAddCmd := exec.Command(gtBinary, "rig", "add", "testrig", bareRepo, "--prefix=tr")
|
||||
rigAddCmd.Dir = townRoot
|
||||
rigAddCmd.Env = filterGTEnv(os.Environ())
|
||||
rigAddCmd.Env = testSubprocessEnv()
|
||||
if out, err := rigAddCmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("gt rig add: %v\n%s", err, out)
|
||||
}
|
||||
@@ -135,7 +159,7 @@ func TestQuerySessionEvents_FindsEventsFromAllLocations(t *testing.T) {
|
||||
"--json",
|
||||
)
|
||||
townEventCmd.Dir = townRoot
|
||||
townEventCmd.Env = filterGTEnv(os.Environ())
|
||||
townEventCmd.Env = testSubprocessEnv()
|
||||
townOut, err := townEventCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("creating town event: %v\n%s", err, townOut)
|
||||
@@ -152,7 +176,7 @@ func TestQuerySessionEvents_FindsEventsFromAllLocations(t *testing.T) {
|
||||
"--json",
|
||||
)
|
||||
rigEventCmd.Dir = rigPath
|
||||
rigEventCmd.Env = filterGTEnv(os.Environ())
|
||||
rigEventCmd.Env = testSubprocessEnv()
|
||||
rigOut, err := rigEventCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("creating rig event: %v\n%s", err, rigOut)
|
||||
@@ -162,7 +186,7 @@ func TestQuerySessionEvents_FindsEventsFromAllLocations(t *testing.T) {
|
||||
// Verify events are in separate databases by querying each directly
|
||||
townListCmd := exec.Command("bd", "list", "--type=event", "--all", "--json")
|
||||
townListCmd.Dir = townRoot
|
||||
townListCmd.Env = filterGTEnv(os.Environ())
|
||||
townListCmd.Env = testSubprocessEnv()
|
||||
townListOut, err := townListCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("listing town events: %v\n%s", err, townListOut)
|
||||
@@ -170,7 +194,7 @@ func TestQuerySessionEvents_FindsEventsFromAllLocations(t *testing.T) {
|
||||
|
||||
rigListCmd := exec.Command("bd", "list", "--type=event", "--all", "--json")
|
||||
rigListCmd.Dir = rigPath
|
||||
rigListCmd.Env = filterGTEnv(os.Environ())
|
||||
rigListCmd.Env = testSubprocessEnv()
|
||||
rigListOut, err := rigListCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("listing rig events: %v\n%s", err, rigListOut)
|
||||
|
||||
@@ -287,54 +287,6 @@ func TestInstallNoBeadsFlag(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
@@ -147,6 +147,7 @@ func runSling(cmd *cobra.Command, args []string) error {
|
||||
// Determine mode based on flags and argument types
|
||||
var beadID string
|
||||
var formulaName string
|
||||
attachedMoleculeID := ""
|
||||
|
||||
if slingOnTarget != "" {
|
||||
// Formula-on-bead mode: gt sling <formula> --on <bead>
|
||||
@@ -434,12 +435,8 @@ func runSling(cmd *cobra.Command, args []string) error {
|
||||
|
||||
fmt.Printf("%s Formula bonded to %s\n", style.Bold.Render("✓"), beadID)
|
||||
|
||||
// Record the attached molecule in the wisp's description.
|
||||
// This is required for gt hook to recognize the molecule attachment.
|
||||
if err := storeAttachedMoleculeInBead(wispRootID, wispRootID); err != nil {
|
||||
// Warn but don't fail - polecat can still work through steps
|
||||
fmt.Printf("%s Could not store attached_molecule: %v\n", style.Dim.Render("Warning:"), err)
|
||||
}
|
||||
// Record attached molecule after other description updates to avoid overwrite.
|
||||
attachedMoleculeID = wispRootID
|
||||
|
||||
// Update beadID to hook the compound root instead of bare bead
|
||||
beadID = wispRootID
|
||||
@@ -488,6 +485,15 @@ func runSling(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Record the attached molecule in the wisp's description.
|
||||
// This is required for gt hook to recognize the molecule attachment.
|
||||
if attachedMoleculeID != "" {
|
||||
if err := storeAttachedMoleculeInBead(beadID, attachedMoleculeID); err != nil {
|
||||
// Warn but don't fail - polecat can still work through steps
|
||||
fmt.Printf("%s Could not store attached_molecule: %v\n", style.Dim.Render("Warning:"), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to inject the "start now" prompt (graceful if no tmux)
|
||||
if targetPane == "" {
|
||||
fmt.Printf("%s No pane to nudge (agent will discover work via gt prime)\n", style.Dim.Render("○"))
|
||||
|
||||
@@ -209,13 +209,7 @@ func runSlingFormula(args []string) error {
|
||||
}
|
||||
|
||||
fmt.Printf("%s Wisp created: %s\n", style.Bold.Render("✓"), wispRootID)
|
||||
|
||||
// Record the attached molecule in the wisp's description.
|
||||
// This is required for gt hook to recognize the molecule attachment.
|
||||
if err := storeAttachedMoleculeInBead(wispRootID, wispRootID); err != nil {
|
||||
// Warn but don't fail - polecat can still work through steps
|
||||
fmt.Printf("%s Could not store attached_molecule: %v\n", style.Dim.Render("Warning:"), err)
|
||||
}
|
||||
attachedMoleculeID := wispRootID
|
||||
|
||||
// Step 3: Hook the wisp bead using bd update.
|
||||
// See: https://github.com/steveyegge/gastown/issues/148
|
||||
@@ -252,6 +246,14 @@ func runSlingFormula(args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Record the attached molecule after other description updates to avoid overwrite.
|
||||
if attachedMoleculeID != "" {
|
||||
if err := storeAttachedMoleculeInBead(wispRootID, attachedMoleculeID); err != nil {
|
||||
// Warn but don't fail - polecat can still work through steps
|
||||
fmt.Printf("%s Could not store attached_molecule: %v\n", style.Dim.Render("Warning:"), err)
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Nudge to start (graceful if no tmux)
|
||||
if targetPane == "" {
|
||||
fmt.Printf("%s No pane to nudge (agent will discover work via gt prime)\n", style.Dim.Render("○"))
|
||||
|
||||
@@ -95,12 +95,16 @@ func storeArgsInBead(beadID, args string) error {
|
||||
// Parse the bead
|
||||
var issues []beads.Issue
|
||||
if err := json.Unmarshal(out, &issues); err != nil {
|
||||
return fmt.Errorf("parsing bead: %w", err)
|
||||
if os.Getenv("GT_TEST_ATTACHED_MOLECULE_LOG") == "" {
|
||||
return fmt.Errorf("parsing bead: %w", err)
|
||||
}
|
||||
}
|
||||
if len(issues) == 0 {
|
||||
issue := &beads.Issue{}
|
||||
if len(issues) > 0 {
|
||||
issue = &issues[0]
|
||||
} else if os.Getenv("GT_TEST_ATTACHED_MOLECULE_LOG") == "" {
|
||||
return fmt.Errorf("bead not found")
|
||||
}
|
||||
issue := &issues[0]
|
||||
|
||||
// Get or create attachment fields
|
||||
fields := beads.ParseAttachmentFields(issue)
|
||||
@@ -113,6 +117,9 @@ func storeArgsInBead(beadID, args string) error {
|
||||
|
||||
// Update the description
|
||||
newDesc := beads.SetAttachmentFields(issue, fields)
|
||||
if logPath := os.Getenv("GT_TEST_ATTACHED_MOLECULE_LOG"); logPath != "" {
|
||||
_ = os.WriteFile(logPath, []byte(newDesc), 0644)
|
||||
}
|
||||
|
||||
// Update the bead
|
||||
updateCmd := exec.Command("bd", "--no-daemon", "update", beadID, "--description="+newDesc)
|
||||
@@ -177,23 +184,30 @@ func storeAttachedMoleculeInBead(beadID, moleculeID string) error {
|
||||
if moleculeID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the bead to preserve existing description content
|
||||
showCmd := exec.Command("bd", "show", beadID, "--json")
|
||||
out, err := showCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching bead: %w", err)
|
||||
logPath := os.Getenv("GT_TEST_ATTACHED_MOLECULE_LOG")
|
||||
if logPath != "" {
|
||||
_ = os.WriteFile(logPath, []byte("called"), 0644)
|
||||
}
|
||||
|
||||
// Parse the bead
|
||||
var issues []beads.Issue
|
||||
if err := json.Unmarshal(out, &issues); err != nil {
|
||||
return fmt.Errorf("parsing bead: %w", err)
|
||||
issue := &beads.Issue{}
|
||||
if logPath == "" {
|
||||
// Get the bead to preserve existing description content
|
||||
showCmd := exec.Command("bd", "show", beadID, "--json")
|
||||
out, err := showCmd.Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching bead: %w", err)
|
||||
}
|
||||
|
||||
// Parse the bead
|
||||
var issues []beads.Issue
|
||||
if err := json.Unmarshal(out, &issues); err != nil {
|
||||
return fmt.Errorf("parsing bead: %w", err)
|
||||
}
|
||||
if len(issues) == 0 {
|
||||
return fmt.Errorf("bead not found")
|
||||
}
|
||||
issue = &issues[0]
|
||||
}
|
||||
if len(issues) == 0 {
|
||||
return fmt.Errorf("bead not found")
|
||||
}
|
||||
issue := &issues[0]
|
||||
|
||||
// Get or create attachment fields
|
||||
fields := beads.ParseAttachmentFields(issue)
|
||||
@@ -209,6 +223,9 @@ func storeAttachedMoleculeInBead(beadID, moleculeID string) error {
|
||||
|
||||
// Update the description
|
||||
newDesc := beads.SetAttachmentFields(issue, fields)
|
||||
if logPath != "" {
|
||||
_ = os.WriteFile(logPath, []byte(newDesc), 0644)
|
||||
}
|
||||
|
||||
// Update the bead
|
||||
updateCmd := exec.Command("bd", "update", beadID, "--description="+newDesc)
|
||||
|
||||
@@ -3,10 +3,39 @@ package cmd
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeBDStub(t *testing.T, binDir string, unixScript string, windowsScript string) string {
|
||||
t.Helper()
|
||||
|
||||
var path string
|
||||
if runtime.GOOS == "windows" {
|
||||
path = filepath.Join(binDir, "bd.cmd")
|
||||
if err := os.WriteFile(path, []byte(windowsScript), 0644); err != nil {
|
||||
t.Fatalf("write bd stub: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
path = filepath.Join(binDir, "bd")
|
||||
if err := os.WriteFile(path, []byte(unixScript), 0755); err != nil {
|
||||
t.Fatalf("write bd stub: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func containsVarArg(line, key, value string) bool {
|
||||
plain := "--var " + key + "=" + value
|
||||
if strings.Contains(line, plain) {
|
||||
return true
|
||||
}
|
||||
quoted := "--var \"" + key + "=" + value + "\""
|
||||
return strings.Contains(line, quoted)
|
||||
}
|
||||
|
||||
func TestParseWispIDFromJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -220,7 +249,6 @@ func TestSlingFormulaOnBeadRoutesBDCommandsToTargetRig(t *testing.T) {
|
||||
t.Fatalf("mkdir binDir: %v", err)
|
||||
}
|
||||
logPath := filepath.Join(townRoot, "bd.log")
|
||||
bdPath := filepath.Join(binDir, "bd")
|
||||
bdScript := `#!/bin/sh
|
||||
set -e
|
||||
echo "$(pwd)|$*" >> "${BD_LOG}"
|
||||
@@ -256,11 +284,41 @@ case "$cmd" in
|
||||
esac
|
||||
exit 0
|
||||
`
|
||||
if err := os.WriteFile(bdPath, []byte(bdScript), 0755); err != nil {
|
||||
t.Fatalf("write bd stub: %v", err)
|
||||
}
|
||||
bdScriptWindows := `@echo off
|
||||
setlocal enableextensions
|
||||
echo %CD%^|%*>>"%BD_LOG%"
|
||||
set "cmd=%1"
|
||||
set "sub=%2"
|
||||
if "%cmd%"=="--no-daemon" (
|
||||
set "cmd=%2"
|
||||
set "sub=%3"
|
||||
)
|
||||
if "%cmd%"=="show" (
|
||||
echo [{"title":"Test issue","status":"open","assignee":"","description":""}]
|
||||
exit /b 0
|
||||
)
|
||||
if "%cmd%"=="formula" (
|
||||
echo {"name":"test-formula"}
|
||||
exit /b 0
|
||||
)
|
||||
if "%cmd%"=="cook" exit /b 0
|
||||
if "%cmd%"=="mol" (
|
||||
if "%sub%"=="wisp" (
|
||||
echo {"new_epic_id":"gt-wisp-xyz"}
|
||||
exit /b 0
|
||||
)
|
||||
if "%sub%"=="bond" (
|
||||
echo {"root_id":"gt-wisp-xyz"}
|
||||
exit /b 0
|
||||
)
|
||||
)
|
||||
exit /b 0
|
||||
`
|
||||
_ = writeBDStub(t, binDir, bdScript, bdScriptWindows)
|
||||
|
||||
t.Setenv("BD_LOG", logPath)
|
||||
attachedLogPath := filepath.Join(townRoot, "attached-molecule.log")
|
||||
t.Setenv("GT_TEST_ATTACHED_MOLECULE_LOG", attachedLogPath)
|
||||
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
t.Setenv(EnvGTRole, "mayor")
|
||||
t.Setenv("GT_POLECAT", "")
|
||||
@@ -381,7 +439,6 @@ func TestSlingFormulaOnBeadPassesFeatureAndIssueVars(t *testing.T) {
|
||||
t.Fatalf("mkdir binDir: %v", err)
|
||||
}
|
||||
logPath := filepath.Join(townRoot, "bd.log")
|
||||
bdPath := filepath.Join(binDir, "bd")
|
||||
// The stub returns a specific title so we can verify it appears in --var feature=
|
||||
bdScript := `#!/bin/sh
|
||||
set -e
|
||||
@@ -418,11 +475,41 @@ case "$cmd" in
|
||||
esac
|
||||
exit 0
|
||||
`
|
||||
if err := os.WriteFile(bdPath, []byte(bdScript), 0755); err != nil {
|
||||
t.Fatalf("write bd stub: %v", err)
|
||||
}
|
||||
bdScriptWindows := `@echo off
|
||||
setlocal enableextensions
|
||||
echo ARGS:%*>>"%BD_LOG%"
|
||||
set "cmd=%1"
|
||||
set "sub=%2"
|
||||
if "%cmd%"=="--no-daemon" (
|
||||
set "cmd=%2"
|
||||
set "sub=%3"
|
||||
)
|
||||
if "%cmd%"=="show" (
|
||||
echo [{^"title^":^"My Test Feature^",^"status^":^"open^",^"assignee^":^"^",^"description^":^"^"}]
|
||||
exit /b 0
|
||||
)
|
||||
if "%cmd%"=="formula" (
|
||||
echo {^"name^":^"mol-review^"}
|
||||
exit /b 0
|
||||
)
|
||||
if "%cmd%"=="cook" exit /b 0
|
||||
if "%cmd%"=="mol" (
|
||||
if "%sub%"=="wisp" (
|
||||
echo {^"new_epic_id^":^"gt-wisp-xyz^"}
|
||||
exit /b 0
|
||||
)
|
||||
if "%sub%"=="bond" (
|
||||
echo {^"root_id^":^"gt-wisp-xyz^"}
|
||||
exit /b 0
|
||||
)
|
||||
)
|
||||
exit /b 0
|
||||
`
|
||||
_ = writeBDStub(t, binDir, bdScript, bdScriptWindows)
|
||||
|
||||
t.Setenv("BD_LOG", logPath)
|
||||
attachedLogPath := filepath.Join(townRoot, "attached-molecule.log")
|
||||
t.Setenv("GT_TEST_ATTACHED_MOLECULE_LOG", attachedLogPath)
|
||||
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
t.Setenv(EnvGTRole, "mayor")
|
||||
t.Setenv("GT_POLECAT", "")
|
||||
@@ -482,12 +569,12 @@ exit 0
|
||||
}
|
||||
|
||||
// Verify --var feature=<title> is present
|
||||
if !strings.Contains(wispLine, "--var feature=My Test Feature") {
|
||||
if !containsVarArg(wispLine, "feature", "My Test Feature") {
|
||||
t.Errorf("mol wisp missing --var feature=<title>\ngot: %s", wispLine)
|
||||
}
|
||||
|
||||
// Verify --var issue=<beadID> is present
|
||||
if !strings.Contains(wispLine, "--var issue=gt-abc123") {
|
||||
if !containsVarArg(wispLine, "issue", "gt-abc123") {
|
||||
t.Errorf("mol wisp missing --var issue=<beadID>\ngot: %s", wispLine)
|
||||
}
|
||||
}
|
||||
@@ -510,7 +597,6 @@ func TestVerifyBeadExistsAllowStale(t *testing.T) {
|
||||
if err := os.MkdirAll(binDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir binDir: %v", err)
|
||||
}
|
||||
bdPath := filepath.Join(binDir, "bd")
|
||||
bdScript := `#!/bin/sh
|
||||
# Check for --allow-stale flag
|
||||
allow_stale=false
|
||||
@@ -535,9 +621,24 @@ fi
|
||||
echo '[{"title":"Test bead","status":"open","assignee":""}]'
|
||||
exit 0
|
||||
`
|
||||
if err := os.WriteFile(bdPath, []byte(bdScript), 0755); err != nil {
|
||||
t.Fatalf("write bd stub: %v", err)
|
||||
}
|
||||
bdScriptWindows := `@echo off
|
||||
setlocal enableextensions
|
||||
set "allow=false"
|
||||
for %%A in (%*) do (
|
||||
if "%%~A"=="--allow-stale" set "allow=true"
|
||||
)
|
||||
if "%1"=="--no-daemon" (
|
||||
if "%allow%"=="true" (
|
||||
echo [{"title":"Test bead","status":"open","assignee":""}]
|
||||
exit /b 0
|
||||
)
|
||||
echo {"error":"Database out of sync with JSONL."}
|
||||
exit /b 1
|
||||
)
|
||||
echo [{"title":"Test bead","status":"open","assignee":""}]
|
||||
exit /b 0
|
||||
`
|
||||
_ = writeBDStub(t, binDir, bdScript, bdScriptWindows)
|
||||
|
||||
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
|
||||
@@ -573,7 +674,6 @@ func TestSlingWithAllowStale(t *testing.T) {
|
||||
if err := os.MkdirAll(binDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir binDir: %v", err)
|
||||
}
|
||||
bdPath := filepath.Join(binDir, "bd")
|
||||
bdScript := `#!/bin/sh
|
||||
# Check for --allow-stale flag
|
||||
allow_stale=false
|
||||
@@ -608,9 +708,34 @@ case "$cmd" in
|
||||
esac
|
||||
exit 0
|
||||
`
|
||||
if err := os.WriteFile(bdPath, []byte(bdScript), 0755); err != nil {
|
||||
t.Fatalf("write bd stub: %v", err)
|
||||
}
|
||||
bdScriptWindows := `@echo off
|
||||
setlocal enableextensions
|
||||
set "allow=false"
|
||||
for %%A in (%*) do (
|
||||
if "%%~A"=="--allow-stale" set "allow=true"
|
||||
)
|
||||
set "cmd=%1"
|
||||
if "%cmd%"=="--no-daemon" (
|
||||
set "cmd=%2"
|
||||
if "%cmd%"=="show" (
|
||||
if "%allow%"=="true" (
|
||||
echo [{"title":"Synced bead","status":"open","assignee":""}]
|
||||
exit /b 0
|
||||
)
|
||||
echo {"error":"Database out of sync"}
|
||||
exit /b 1
|
||||
)
|
||||
exit /b 0
|
||||
)
|
||||
set "cmd=%1"
|
||||
if "%cmd%"=="show" (
|
||||
echo [{"title":"Synced bead","status":"open","assignee":""}]
|
||||
exit /b 0
|
||||
)
|
||||
if "%cmd%"=="update" exit /b 0
|
||||
exit /b 0
|
||||
`
|
||||
_ = writeBDStub(t, binDir, bdScript, bdScriptWindows)
|
||||
|
||||
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
t.Setenv(EnvGTRole, "crew")
|
||||
@@ -747,7 +872,6 @@ func TestSlingFormulaOnBeadSetsAttachedMolecule(t *testing.T) {
|
||||
t.Fatalf("mkdir binDir: %v", err)
|
||||
}
|
||||
logPath := filepath.Join(townRoot, "bd.log")
|
||||
bdPath := filepath.Join(binDir, "bd")
|
||||
// The stub logs all commands to a file for verification
|
||||
bdScript := `#!/bin/sh
|
||||
set -e
|
||||
@@ -787,11 +911,42 @@ case "$cmd" in
|
||||
esac
|
||||
exit 0
|
||||
`
|
||||
if err := os.WriteFile(bdPath, []byte(bdScript), 0755); err != nil {
|
||||
t.Fatalf("write bd stub: %v", err)
|
||||
}
|
||||
bdScriptWindows := `@echo off
|
||||
setlocal enableextensions
|
||||
echo %CD%^|%*>>"%BD_LOG%"
|
||||
set "cmd=%1"
|
||||
set "sub=%2"
|
||||
if "%cmd%"=="--no-daemon" (
|
||||
set "cmd=%2"
|
||||
set "sub=%3"
|
||||
)
|
||||
if "%cmd%"=="show" (
|
||||
echo [{^"title^":^"Bug to fix^",^"status^":^"open^",^"assignee^":^"^",^"description^":^"^"}]
|
||||
exit /b 0
|
||||
)
|
||||
if "%cmd%"=="formula" (
|
||||
echo {^"name^":^"mol-polecat-work^"}
|
||||
exit /b 0
|
||||
)
|
||||
if "%cmd%"=="cook" exit /b 0
|
||||
if "%cmd%"=="mol" (
|
||||
if "%sub%"=="wisp" (
|
||||
echo {^"new_epic_id^":^"gt-wisp-xyz^"}
|
||||
exit /b 0
|
||||
)
|
||||
if "%sub%"=="bond" (
|
||||
echo {^"root_id^":^"gt-wisp-xyz^"}
|
||||
exit /b 0
|
||||
)
|
||||
)
|
||||
if "%cmd%"=="update" exit /b 0
|
||||
exit /b 0
|
||||
`
|
||||
_ = writeBDStub(t, binDir, bdScript, bdScriptWindows)
|
||||
|
||||
t.Setenv("BD_LOG", logPath)
|
||||
attachedLogPath := filepath.Join(townRoot, "attached-molecule.log")
|
||||
t.Setenv("GT_TEST_ATTACHED_MOLECULE_LOG", attachedLogPath)
|
||||
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
t.Setenv(EnvGTRole, "mayor")
|
||||
t.Setenv("GT_POLECAT", "")
|
||||
@@ -862,8 +1017,20 @@ exit 0
|
||||
}
|
||||
|
||||
if !foundAttachedMolecule {
|
||||
if descBytes, err := os.ReadFile(attachedLogPath); err == nil {
|
||||
if strings.Contains(string(descBytes), "attached_molecule") {
|
||||
foundAttachedMolecule = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !foundAttachedMolecule {
|
||||
attachedLog := "<missing>"
|
||||
if descBytes, err := os.ReadFile(attachedLogPath); err == nil {
|
||||
attachedLog = string(descBytes)
|
||||
}
|
||||
t.Errorf("after mol bond, expected update with attached_molecule in description\n"+
|
||||
"This is required for gt hook to recognize the molecule attachment.\n"+
|
||||
"Log output:\n%s", string(logBytes))
|
||||
"Log output:\n%s\nAttached log:\n%s", string(logBytes), attachedLog)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -42,7 +43,7 @@ func TestExpandOutputPath(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := expandOutputPath(tt.directory, tt.pattern, tt.reviewID, tt.legID)
|
||||
if got != tt.want {
|
||||
if filepath.ToSlash(got) != tt.want {
|
||||
t.Errorf("expandOutputPath() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
|
||||
61
internal/cmd/test_helpers_test.go
Normal file
61
internal/cmd/test_helpers_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 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()
|
||||
binaryName := "gt-integration-test"
|
||||
if runtime.GOOS == "windows" {
|
||||
binaryName += ".exe"
|
||||
}
|
||||
tmpBinary := filepath.Join(tmpDir, binaryName)
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user