From 733186309d94dd301730be83560b790b2c514ef4 Mon Sep 17 00:00:00 2001 From: Alex Ong Date: Mon, 19 Jan 2026 14:59:22 +1100 Subject: [PATCH 1/3] Fix resume integration test hanging on interactive prompt - Add syscall.Setsid to RunResume to detach subprocess from controlling terminal, preventing huh from opening /dev/tty for interactive prompts - Split TestResume_LocalLogNewerTimestamp into two focused tests: - TestResume_LocalLogNewerTimestamp_RequiresForce: verifies non-interactive mode fails safely without overwriting local logs - TestResume_LocalLogNewerTimestamp_ForceOverwrites: verifies --force bypasses prompt and overwrites local logs Co-Authored-By: Claude Opus 4.5 Entire-Checkpoint: e58c50537669 --- .../cli/integration_test/resume_test.go | 76 ++++++++++++++++--- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/cmd/entire/cli/integration_test/resume_test.go b/cmd/entire/cli/integration_test/resume_test.go index 410919659..ce779ea93 100644 --- a/cmd/entire/cli/integration_test/resume_test.go +++ b/cmd/entire/cli/integration_test/resume_test.go @@ -8,6 +8,7 @@ import ( "os/exec" "path/filepath" "strings" + "syscall" "testing" "time" @@ -472,6 +473,8 @@ func TestResume_AfterMergingMain(t *testing.T) { } // RunResume executes the resume command and returns the combined output. +// The subprocess is detached from the controlling terminal (via Setsid) to prevent +// interactive prompts from hanging tests. This simulates non-interactive environments like CI. func (env *TestEnv) RunResume(branchName string) (string, error) { env.T.Helper() @@ -481,6 +484,8 @@ func (env *TestEnv) RunResume(branchName string) (string, error) { cmd.Env = append(os.Environ(), "ENTIRE_TEST_CLAUDE_PROJECT_DIR="+env.ClaudeProjectDir, ) + // Detach from controlling terminal so huh can't open /dev/tty for interactive prompts + cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} output, err := cmd.CombinedOutput() return string(output), err @@ -559,9 +564,10 @@ func (env *TestEnv) GitCheckoutBranch(branchName string) { } } -// TestResume_LocalLogNewerTimestamp tests that when local log has newer timestamps -// than the checkpoint, the command requires --force to proceed (without interactive prompt). -func TestResume_LocalLogNewerTimestamp(t *testing.T) { +// TestResume_LocalLogNewerTimestamp_RequiresForce tests that when local log has newer +// timestamps than the checkpoint, the command fails in non-interactive mode (no TTY) +// and does NOT overwrite the local log. This ensures safe behavior in CI environments. +func TestResume_LocalLogNewerTimestamp_RequiresForce(t *testing.T) { t.Parallel() env := NewFeatureBranchEnv(t, strategy.StrategyNameAutoCommit) @@ -599,16 +605,66 @@ func TestResume_LocalLogNewerTimestamp(t *testing.T) { // Switch to main env.GitCheckoutBranch(masterBranch) - // Resume WITHOUT --force - should fail because it needs interactive confirmation - // (which isn't available in non-interactive test mode) + // Resume WITHOUT --force in non-interactive mode (no TTY due to Setsid) + // Should fail because it can't prompt for confirmation output, err := env.RunResume(featureBranch) - // The command might succeed (if huh falls back to default) or might fail - // Either way, with --force it should definitely succeed - t.Logf("Resume without --force output: %s, err: %v", output, err) + if err == nil { + t.Errorf("expected error when resuming without --force in non-interactive mode, got success.\nOutput: %s", output) + } - // Resume WITH --force should succeed and overwrite the local log + // Verify local log was NOT overwritten (safe behavior) + data, err := os.ReadFile(existingLog) + if err != nil { + t.Fatalf("failed to read log: %v", err) + } + if !strings.Contains(string(data), "newer local work") { + t.Errorf("local log should NOT have been overwritten without --force, but content changed to: %s", string(data)) + } +} + +// TestResume_LocalLogNewerTimestamp_ForceOverwrites tests that when local log has newer +// timestamps than the checkpoint, the --force flag bypasses the confirmation prompt +// and overwrites the local log. +func TestResume_LocalLogNewerTimestamp_ForceOverwrites(t *testing.T) { + t.Parallel() + env := NewFeatureBranchEnv(t, strategy.StrategyNameAutoCommit) + + // Create a session with a specific timestamp + session := env.NewSession() + if err := env.SimulateUserPromptSubmit(session.ID); err != nil { + t.Fatalf("SimulateUserPromptSubmit failed: %v", err) + } + + content := "def hello; end" + env.WriteFile("hello.rb", content) + + session.CreateTranscript( + "Create hello method", + []FileChange{{Path: "hello.rb", Content: content}}, + ) + if err := env.SimulateStop(session.ID, session.TranscriptPath); err != nil { + t.Fatalf("SimulateStop failed: %v", err) + } + + featureBranch := env.GetCurrentBranch() + + // Create a local log with a NEWER timestamp than the checkpoint + if err := os.MkdirAll(env.ClaudeProjectDir, 0o755); err != nil { + t.Fatalf("failed to create Claude project dir: %v", err) + } + existingLog := filepath.Join(env.ClaudeProjectDir, session.ID+".jsonl") + // Use a timestamp far in the future to ensure it's newer + futureTimestamp := time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339) + newerContent := fmt.Sprintf(`{"type":"human","timestamp":"%s","message":{"content":"newer local work"}}`, futureTimestamp) + if err := os.WriteFile(existingLog, []byte(newerContent), 0o644); err != nil { + t.Fatalf("failed to write existing log: %v", err) + } + + // Switch to main env.GitCheckoutBranch(masterBranch) - output, err = env.RunResumeForce(featureBranch) + + // Resume WITH --force should succeed and overwrite the local log + output, err := env.RunResumeForce(featureBranch) if err != nil { t.Fatalf("resume --force failed: %v\nOutput: %s", err, output) } From 3fc83e06aea84dda057685b38ea3c18821da5d39 Mon Sep 17 00:00:00 2001 From: Alex Ong Date: Mon, 19 Jan 2026 15:30:56 +1100 Subject: [PATCH 2/3] Add interactive prompt tests using pty - Add github.com/creack/pty dependency for pseudo-terminal support - Add RunResumeInteractive helper that runs commands with a pty - Add waitForPromptAndRespond helper to detect prompts and send responses - Add tests for user confirming and declining overwrite prompts - Use ACCESSIBLE=1 mode so huh reads from stdin (pty) instead of /dev/tty New tests: - TestResume_LocalLogNewerTimestamp_UserConfirmsOverwrite - TestResume_LocalLogNewerTimestamp_UserDeclinesOverwrite Co-Authored-By: Claude Opus 4.5 Entire-Checkpoint: ceaea776cdb0 --- .../cli/integration_test/resume_test.go | 231 ++++++++++++++++++ go.mod | 1 + 2 files changed, 232 insertions(+) diff --git a/cmd/entire/cli/integration_test/resume_test.go b/cmd/entire/cli/integration_test/resume_test.go index ce779ea93..f13b624da 100644 --- a/cmd/entire/cli/integration_test/resume_test.go +++ b/cmd/entire/cli/integration_test/resume_test.go @@ -3,7 +3,9 @@ package integration import ( + "bytes" "fmt" + "io" "os" "os/exec" "path/filepath" @@ -14,6 +16,7 @@ import ( "entire.io/cli/cmd/entire/cli/strategy" + "github.com/creack/pty" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" ) @@ -506,6 +509,77 @@ func (env *TestEnv) RunResumeForce(branchName string) (string, error) { return string(output), err } +// RunResumeInteractive executes the resume command with a pty, allowing +// interactive prompt responses. The respond function receives the pty for +// reading output and writing input. Timeouts and cleanup are managed centrally. +// The respond function should read from the pty to find prompts and write responses. +// All output read by respond is captured and returned. +func (env *TestEnv) RunResumeInteractive(branchName string, respond func(ptyFile *os.File) string) (string, error) { + env.T.Helper() + + cmd := exec.Command(getTestBinary(), "resume", branchName) + cmd.Dir = env.RepoDir + cmd.Env = append(os.Environ(), + "ENTIRE_TEST_CLAUDE_PROJECT_DIR="+env.ClaudeProjectDir, + "TERM=xterm", + "ACCESSIBLE=1", // Use accessible mode for simpler text prompts + ) + + // Start command with a pty + ptmx, err := pty.Start(cmd) + if err != nil { + return "", fmt.Errorf("failed to start pty: %w", err) + } + defer ptmx.Close() + + // Let the respond function interact with the pty and collect output + var respondOutput string + respondDone := make(chan struct{}) + go func() { + defer close(respondDone) + respondOutput = respond(ptmx) + }() + + // Wait for respond function with timeout + select { + case <-respondDone: + // respond completed + case <-time.After(10 * time.Second): + env.T.Log("Warning: respond function timed out") + } + + // Collect any remaining output after respond is done + var remaining bytes.Buffer + remainingDone := make(chan struct{}) + go func() { + defer close(remainingDone) + _, _ = io.Copy(&remaining, ptmx) + }() + + // Wait for process to complete with timeout + cmdDone := make(chan error, 1) + go func() { + cmdDone <- cmd.Wait() + }() + + var cmdErr error + select { + case cmdErr = <-cmdDone: + // process completed + case <-time.After(10 * time.Second): + _ = cmd.Process.Kill() + cmdErr = fmt.Errorf("process timed out") + } + + // Give remaining output goroutine time to finish after process exits + select { + case <-remainingDone: + case <-time.After(1 * time.Second): + } + + return respondOutput + remaining.String(), cmdErr +} + // GitMerge merges a branch into the current branch. func (env *TestEnv) GitMerge(branchName string) { env.T.Helper() @@ -682,6 +756,163 @@ func TestResume_LocalLogNewerTimestamp_ForceOverwrites(t *testing.T) { } } +// waitForPromptAndRespond reads from the pty until it sees the expected prompt text, +// then writes the response. Returns the output read so far. +func waitForPromptAndRespond(ptyFile *os.File, promptSubstring, response string, timeout time.Duration) (string, error) { + var output bytes.Buffer + buf := make([]byte, 1024) + deadline := time.Now().Add(timeout) + + for time.Now().Before(deadline) { + // Set read deadline to avoid blocking forever + _ = ptyFile.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + n, err := ptyFile.Read(buf) + if n > 0 { + output.Write(buf[:n]) + if strings.Contains(output.String(), promptSubstring) { + // Found the prompt, send response + _, _ = ptyFile.WriteString(response) + return output.String(), nil + } + } + if err != nil && !os.IsTimeout(err) { + return output.String(), err + } + } + return output.String(), fmt.Errorf("timeout waiting for prompt containing %q", promptSubstring) +} + +// TestResume_LocalLogNewerTimestamp_UserConfirmsOverwrite tests that when the user +// confirms the overwrite prompt interactively, the local log is overwritten. +func TestResume_LocalLogNewerTimestamp_UserConfirmsOverwrite(t *testing.T) { + t.Parallel() + env := NewFeatureBranchEnv(t, strategy.StrategyNameAutoCommit) + + // Create a session with a specific timestamp + session := env.NewSession() + if err := env.SimulateUserPromptSubmit(session.ID); err != nil { + t.Fatalf("SimulateUserPromptSubmit failed: %v", err) + } + + content := "def hello; end" + env.WriteFile("hello.rb", content) + + session.CreateTranscript( + "Create hello method", + []FileChange{{Path: "hello.rb", Content: content}}, + ) + if err := env.SimulateStop(session.ID, session.TranscriptPath); err != nil { + t.Fatalf("SimulateStop failed: %v", err) + } + + featureBranch := env.GetCurrentBranch() + + // Create a local log with a NEWER timestamp than the checkpoint + if err := os.MkdirAll(env.ClaudeProjectDir, 0o755); err != nil { + t.Fatalf("failed to create Claude project dir: %v", err) + } + existingLog := filepath.Join(env.ClaudeProjectDir, session.ID+".jsonl") + futureTimestamp := time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339) + newerContent := fmt.Sprintf(`{"type":"human","timestamp":"%s","message":{"content":"newer local work"}}`, futureTimestamp) + if err := os.WriteFile(existingLog, []byte(newerContent), 0o644); err != nil { + t.Fatalf("failed to write existing log: %v", err) + } + + // Switch to main + env.GitCheckoutBranch(masterBranch) + + // Resume interactively and confirm the overwrite + output, err := env.RunResumeInteractive(featureBranch, func(ptyFile *os.File) string { + // Wait for the accessible prompt "[y/N]", then send 'y' + out, promptErr := waitForPromptAndRespond(ptyFile, "[y/N]", "y\n", 10*time.Second) + if promptErr != nil { + t.Logf("Warning: %v", promptErr) + } + return out + }) + if err != nil { + t.Fatalf("resume with user confirmation failed: %v\nOutput: %s", err, output) + } + + // Verify local log was overwritten with checkpoint content + data, err := os.ReadFile(existingLog) + if err != nil { + t.Fatalf("failed to read log: %v", err) + } + if strings.Contains(string(data), "newer local work") { + t.Errorf("local log should have been overwritten after user confirmed, but still has newer content: %s", string(data)) + } + if !strings.Contains(string(data), "Create hello method") { + t.Errorf("restored log should contain checkpoint transcript, got: %s", string(data)) + } +} + +// TestResume_LocalLogNewerTimestamp_UserDeclinesOverwrite tests that when the user +// declines the overwrite prompt interactively, the local log is preserved. +func TestResume_LocalLogNewerTimestamp_UserDeclinesOverwrite(t *testing.T) { + t.Parallel() + env := NewFeatureBranchEnv(t, strategy.StrategyNameAutoCommit) + + // Create a session with a specific timestamp + session := env.NewSession() + if err := env.SimulateUserPromptSubmit(session.ID); err != nil { + t.Fatalf("SimulateUserPromptSubmit failed: %v", err) + } + + content := "def hello; end" + env.WriteFile("hello.rb", content) + + session.CreateTranscript( + "Create hello method", + []FileChange{{Path: "hello.rb", Content: content}}, + ) + if err := env.SimulateStop(session.ID, session.TranscriptPath); err != nil { + t.Fatalf("SimulateStop failed: %v", err) + } + + featureBranch := env.GetCurrentBranch() + + // Create a local log with a NEWER timestamp than the checkpoint + if err := os.MkdirAll(env.ClaudeProjectDir, 0o755); err != nil { + t.Fatalf("failed to create Claude project dir: %v", err) + } + existingLog := filepath.Join(env.ClaudeProjectDir, session.ID+".jsonl") + futureTimestamp := time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339) + newerContent := fmt.Sprintf(`{"type":"human","timestamp":"%s","message":{"content":"newer local work"}}`, futureTimestamp) + if err := os.WriteFile(existingLog, []byte(newerContent), 0o644); err != nil { + t.Fatalf("failed to write existing log: %v", err) + } + + // Switch to main + env.GitCheckoutBranch(masterBranch) + + // Resume interactively and decline the overwrite + output, err := env.RunResumeInteractive(featureBranch, func(ptyFile *os.File) string { + // Wait for the accessible prompt "[y/N]", then send 'n' + out, promptErr := waitForPromptAndRespond(ptyFile, "[y/N]", "n\n", 10*time.Second) + if promptErr != nil { + t.Logf("Warning: %v", promptErr) + } + return out + }) + // Command should succeed (graceful exit) but not overwrite + t.Logf("Resume with user decline output: %s, err: %v", output, err) + + // Verify local log was NOT overwritten + data, err := os.ReadFile(existingLog) + if err != nil { + t.Fatalf("failed to read log: %v", err) + } + if !strings.Contains(string(data), "newer local work") { + t.Errorf("local log should NOT have been overwritten after user declined, but content changed to: %s", string(data)) + } + + // Output should indicate the resume was cancelled + if !strings.Contains(output, "cancelled") && !strings.Contains(output, "preserved") { + t.Logf("Note: Expected 'cancelled' or 'preserved' in output, got: %s", output) + } +} + // TestResume_CheckpointNewerTimestamp tests that when checkpoint has newer timestamps // than local log, resume proceeds without requiring --force. func TestResume_CheckpointNewerTimestamp(t *testing.T) { diff --git a/go.mod b/go.mod index 0bd3f3342..3eec64b1d 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.24.0 require ( github.com/charmbracelet/huh v0.8.0 + github.com/creack/pty v1.1.24 github.com/denisbrodbeck/machineid v1.0.1 github.com/go-git/go-git/v5 v5.16.4 github.com/posthog/posthog-go v1.9.0 From 3c98a801ca330420b2862d64b3349799cb40f546 Mon Sep 17 00:00:00 2001 From: Alex Ong Date: Mon, 19 Jan 2026 15:38:21 +1100 Subject: [PATCH 3/3] Refactor interactive pty helpers to dedicated file Extract generic RunCommandInteractive and WaitForPromptAndRespond helpers into interactive.go, simplifying resume_test.go. Documents the critical ACCESSIBLE=1 requirement for huh library to read from pty stdin. Co-Authored-By: Claude Opus 4.5 Entire-Checkpoint: ceaea776cdb0 --- .../cli/integration_test/interactive.go | 110 ++++++++++++++++++ .../cli/integration_test/resume_test.go | 102 +--------------- 2 files changed, 114 insertions(+), 98 deletions(-) create mode 100644 cmd/entire/cli/integration_test/interactive.go diff --git a/cmd/entire/cli/integration_test/interactive.go b/cmd/entire/cli/integration_test/interactive.go new file mode 100644 index 000000000..5b4b95d47 --- /dev/null +++ b/cmd/entire/cli/integration_test/interactive.go @@ -0,0 +1,110 @@ +//go:build integration + +package integration + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "strings" + "time" + + "github.com/creack/pty" +) + +// RunCommandInteractive executes a CLI command with a pty, allowing interactive +// prompt responses. The respond function receives the pty for reading output +// and writing input, and should return the output it read. +func (env *TestEnv) RunCommandInteractive(args []string, respond func(ptyFile *os.File) string) (string, error) { + env.T.Helper() + + cmd := exec.Command(getTestBinary(), args...) + cmd.Dir = env.RepoDir + cmd.Env = append(os.Environ(), + "ENTIRE_TEST_CLAUDE_PROJECT_DIR="+env.ClaudeProjectDir, + "TERM=xterm", + "ACCESSIBLE=1", // Required: makes huh read from stdin instead of /dev/tty + ) + + // Start command with a pty + ptmx, err := pty.Start(cmd) + if err != nil { + return "", fmt.Errorf("failed to start pty: %w", err) + } + defer ptmx.Close() + + // Let the respond function interact with the pty and collect output + var respondOutput string + respondDone := make(chan struct{}) + go func() { + defer close(respondDone) + respondOutput = respond(ptmx) + }() + + // Wait for respond function with timeout + select { + case <-respondDone: + // respond completed + case <-time.After(10 * time.Second): + env.T.Log("Warning: respond function timed out") + } + + // Collect any remaining output after respond is done + var remaining bytes.Buffer + remainingDone := make(chan struct{}) + go func() { + defer close(remainingDone) + _, _ = io.Copy(&remaining, ptmx) + }() + + // Wait for process to complete with timeout + cmdDone := make(chan error, 1) + go func() { + cmdDone <- cmd.Wait() + }() + + var cmdErr error + select { + case cmdErr = <-cmdDone: + // process completed + case <-time.After(10 * time.Second): + _ = cmd.Process.Kill() + cmdErr = fmt.Errorf("process timed out") + } + + // Give remaining output goroutine time to finish after process exits + select { + case <-remainingDone: + case <-time.After(1 * time.Second): + } + + return respondOutput + remaining.String(), cmdErr +} + +// WaitForPromptAndRespond reads from the pty until it sees the expected prompt text, +// then writes the response. Returns the output read so far. +func WaitForPromptAndRespond(ptyFile *os.File, promptSubstring, response string, timeout time.Duration) (string, error) { + var output bytes.Buffer + buf := make([]byte, 1024) + deadline := time.Now().Add(timeout) + + for time.Now().Before(deadline) { + // Set read deadline to avoid blocking forever + _ = ptyFile.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + n, err := ptyFile.Read(buf) + if n > 0 { + output.Write(buf[:n]) + if strings.Contains(output.String(), promptSubstring) { + // Found the prompt, send response + _, _ = ptyFile.WriteString(response) + return output.String(), nil + } + } + if err != nil && !os.IsTimeout(err) { + return output.String(), err + } + } + return output.String(), fmt.Errorf("timeout waiting for prompt containing %q", promptSubstring) +} diff --git a/cmd/entire/cli/integration_test/resume_test.go b/cmd/entire/cli/integration_test/resume_test.go index f13b624da..e1dfc8a80 100644 --- a/cmd/entire/cli/integration_test/resume_test.go +++ b/cmd/entire/cli/integration_test/resume_test.go @@ -3,9 +3,7 @@ package integration import ( - "bytes" "fmt" - "io" "os" "os/exec" "path/filepath" @@ -16,7 +14,6 @@ import ( "entire.io/cli/cmd/entire/cli/strategy" - "github.com/creack/pty" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" ) @@ -511,73 +508,10 @@ func (env *TestEnv) RunResumeForce(branchName string) (string, error) { // RunResumeInteractive executes the resume command with a pty, allowing // interactive prompt responses. The respond function receives the pty for -// reading output and writing input. Timeouts and cleanup are managed centrally. -// The respond function should read from the pty to find prompts and write responses. -// All output read by respond is captured and returned. +// reading output and writing input. See RunCommandInteractive for details. func (env *TestEnv) RunResumeInteractive(branchName string, respond func(ptyFile *os.File) string) (string, error) { env.T.Helper() - - cmd := exec.Command(getTestBinary(), "resume", branchName) - cmd.Dir = env.RepoDir - cmd.Env = append(os.Environ(), - "ENTIRE_TEST_CLAUDE_PROJECT_DIR="+env.ClaudeProjectDir, - "TERM=xterm", - "ACCESSIBLE=1", // Use accessible mode for simpler text prompts - ) - - // Start command with a pty - ptmx, err := pty.Start(cmd) - if err != nil { - return "", fmt.Errorf("failed to start pty: %w", err) - } - defer ptmx.Close() - - // Let the respond function interact with the pty and collect output - var respondOutput string - respondDone := make(chan struct{}) - go func() { - defer close(respondDone) - respondOutput = respond(ptmx) - }() - - // Wait for respond function with timeout - select { - case <-respondDone: - // respond completed - case <-time.After(10 * time.Second): - env.T.Log("Warning: respond function timed out") - } - - // Collect any remaining output after respond is done - var remaining bytes.Buffer - remainingDone := make(chan struct{}) - go func() { - defer close(remainingDone) - _, _ = io.Copy(&remaining, ptmx) - }() - - // Wait for process to complete with timeout - cmdDone := make(chan error, 1) - go func() { - cmdDone <- cmd.Wait() - }() - - var cmdErr error - select { - case cmdErr = <-cmdDone: - // process completed - case <-time.After(10 * time.Second): - _ = cmd.Process.Kill() - cmdErr = fmt.Errorf("process timed out") - } - - // Give remaining output goroutine time to finish after process exits - select { - case <-remainingDone: - case <-time.After(1 * time.Second): - } - - return respondOutput + remaining.String(), cmdErr + return env.RunCommandInteractive([]string{"resume", branchName}, respond) } // GitMerge merges a branch into the current branch. @@ -756,32 +690,6 @@ func TestResume_LocalLogNewerTimestamp_ForceOverwrites(t *testing.T) { } } -// waitForPromptAndRespond reads from the pty until it sees the expected prompt text, -// then writes the response. Returns the output read so far. -func waitForPromptAndRespond(ptyFile *os.File, promptSubstring, response string, timeout time.Duration) (string, error) { - var output bytes.Buffer - buf := make([]byte, 1024) - deadline := time.Now().Add(timeout) - - for time.Now().Before(deadline) { - // Set read deadline to avoid blocking forever - _ = ptyFile.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) - n, err := ptyFile.Read(buf) - if n > 0 { - output.Write(buf[:n]) - if strings.Contains(output.String(), promptSubstring) { - // Found the prompt, send response - _, _ = ptyFile.WriteString(response) - return output.String(), nil - } - } - if err != nil && !os.IsTimeout(err) { - return output.String(), err - } - } - return output.String(), fmt.Errorf("timeout waiting for prompt containing %q", promptSubstring) -} - // TestResume_LocalLogNewerTimestamp_UserConfirmsOverwrite tests that when the user // confirms the overwrite prompt interactively, the local log is overwritten. func TestResume_LocalLogNewerTimestamp_UserConfirmsOverwrite(t *testing.T) { @@ -823,8 +731,7 @@ func TestResume_LocalLogNewerTimestamp_UserConfirmsOverwrite(t *testing.T) { // Resume interactively and confirm the overwrite output, err := env.RunResumeInteractive(featureBranch, func(ptyFile *os.File) string { - // Wait for the accessible prompt "[y/N]", then send 'y' - out, promptErr := waitForPromptAndRespond(ptyFile, "[y/N]", "y\n", 10*time.Second) + out, promptErr := WaitForPromptAndRespond(ptyFile, "[y/N]", "y\n", 10*time.Second) if promptErr != nil { t.Logf("Warning: %v", promptErr) } @@ -888,8 +795,7 @@ func TestResume_LocalLogNewerTimestamp_UserDeclinesOverwrite(t *testing.T) { // Resume interactively and decline the overwrite output, err := env.RunResumeInteractive(featureBranch, func(ptyFile *os.File) string { - // Wait for the accessible prompt "[y/N]", then send 'n' - out, promptErr := waitForPromptAndRespond(ptyFile, "[y/N]", "n\n", 10*time.Second) + out, promptErr := WaitForPromptAndRespond(ptyFile, "[y/N]", "n\n", 10*time.Second) if promptErr != nil { t.Logf("Warning: %v", promptErr) }