diff --git a/e2e/agents/copilot-cli.go b/e2e/agents/copilot-cli.go index 30e2a7f0b..16159c0ed 100644 --- a/e2e/agents/copilot-cli.go +++ b/e2e/agents/copilot-cli.go @@ -2,11 +2,14 @@ package agents import ( "context" + "encoding/json" "errors" "fmt" "os" "os/exec" + "path/filepath" "strings" + "sync" "syscall" "time" ) @@ -25,7 +28,7 @@ func (c *CopilotCLI) Name() string { return "copilot-cli" } func (c *CopilotCLI) Binary() string { return "copilot" } func (c *CopilotCLI) EntireAgent() string { return "copilot-cli" } func (c *CopilotCLI) PromptPattern() string { return `❯` } -func (c *CopilotCLI) TimeoutMultiplier() float64 { return 1.5 } +func (c *CopilotCLI) TimeoutMultiplier() float64 { return 2.5 } func (c *CopilotCLI) IsTransientError(out Output, err error) bool { if errors.Is(err, context.DeadlineExceeded) { @@ -58,6 +61,13 @@ func (c *CopilotCLI) Bootstrap() error { } func (c *CopilotCLI) RunPrompt(ctx context.Context, dir string, prompt string, opts ...Option) (Output, error) { + // Copilot CLI v1.0.8+ requires folder trust before loading repo-level + // hooks. In non-interactive (-p) mode there is no trust dialog, so we + // pre-add the directory to the trusted_folders list. + if err := ensureCopilotTrust(dir); err != nil { + return Output{}, fmt.Errorf("ensure copilot folder trust: %w", err) + } + cfg := &runConfig{Model: "gpt-4.1"} for _, o := range opts { o(cfg) @@ -176,3 +186,63 @@ func (c *CopilotCLI) StartSession(ctx context.Context, dir string) (Session, err return s, nil } + +// copilotTrustMu serializes concurrent read-modify-write of ~/.copilot/config.json. +var copilotTrustMu sync.Mutex + +// ensureCopilotTrust adds dir to ~/.copilot/config.json trusted_folders if not +// already present. Copilot CLI v1.0.8+ requires folder trust before loading +// repo-level hooks; without this, hooks silently don't fire in -p mode. +func ensureCopilotTrust(dir string) error { + absDir, err := filepath.Abs(dir) + if err != nil { + return err + } + + home, err := os.UserHomeDir() + if err != nil { + return err + } + configPath := filepath.Join(home, ".copilot", "config.json") + + copilotTrustMu.Lock() + defer copilotTrustMu.Unlock() + + // Read existing config (or start fresh if it doesn't exist). + var cfg map[string]any + data, err := os.ReadFile(configPath) + switch { + case err == nil: + if err := json.Unmarshal(data, &cfg); err != nil { + return fmt.Errorf("parse copilot config %s: %w", configPath, err) + } + case errors.Is(err, os.ErrNotExist): + cfg = make(map[string]any) + default: + return fmt.Errorf("read copilot config %s: %w", configPath, err) + } + + // Check if already trusted. + var folders []any + if raw, ok := cfg["trusted_folders"]; ok { + if arr, ok := raw.([]any); ok { + folders = arr + } + } + for _, f := range folders { + if s, ok := f.(string); ok && s == absDir { + return nil // already trusted + } + } + + // Add and write back. + cfg["trusted_folders"] = append(folders, absDir) + out, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil { + return err + } + return os.WriteFile(configPath, out, 0o644) +} diff --git a/e2e/tests/attribution_test.go b/e2e/tests/attribution_test.go index 50804f467..07ce850bd 100644 --- a/e2e/tests/attribution_test.go +++ b/e2e/tests/attribution_test.go @@ -29,7 +29,7 @@ func TestLineAttributionReasonable(t *testing.T) { s.Git(t, "add", "docs/") s.Git(t, "commit", "-m", "Add example.md") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") sm := testutil.ReadSessionMetadata(t, s.Dir, cpID, 0) @@ -62,7 +62,7 @@ func TestInteractiveAttributionOnAgentCommit(t *testing.T) { s.WaitFor(t, session, prompt, 90*time.Second) testutil.AssertNewCommits(t, s, 1) - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") sm := testutil.ReadSessionMetadata(t, s.Dir, cpID, 0) @@ -93,7 +93,7 @@ func TestInteractiveAttributionMultiCommitSameSession(t *testing.T) { s.WaitFor(t, session, prompt, 60*time.Second) testutil.AssertNewCommits(t, s, 1) - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpBranch1 := testutil.GitOutput(t, s.Dir, "rev-parse", "entire/checkpoints/v1") // Second prompt: modify same file and commit again. @@ -101,7 +101,7 @@ func TestInteractiveAttributionMultiCommitSameSession(t *testing.T) { s.WaitFor(t, session, prompt, 90*time.Second) testutil.AssertNewCommits(t, s, 2) - testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch1, 15*time.Second) + testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch1, s.CheckpointTimeout()) cpID2 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") sm := testutil.WaitForSessionMetadata(t, s.Dir, cpID2, 0, 10*time.Second) @@ -133,7 +133,7 @@ func TestInteractiveShadowBranchCleanedAfterAgentCommit(t *testing.T) { s.WaitFor(t, session, prompt, 90*time.Second) testutil.AssertNewCommits(t, s, 1) - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) testutil.AssertNoShadowBranches(t, s.Dir) }) @@ -170,7 +170,7 @@ func TestAttributionMixedHumanAndAgent(t *testing.T) { s.Git(t, "add", "agent.txt", "human.txt") s.Git(t, "commit", "-m", "Add agent and human files") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") sm := testutil.ReadSessionMetadata(t, s.Dir, cpID, 0) diff --git a/e2e/tests/checkpoint_metadata_test.go b/e2e/tests/checkpoint_metadata_test.go index ad018f432..8574dd335 100644 --- a/e2e/tests/checkpoint_metadata_test.go +++ b/e2e/tests/checkpoint_metadata_test.go @@ -27,7 +27,7 @@ func TestCheckpointMetadataDeepValidation(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Add validated.go") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") testutil.ValidateCheckpointDeep(t, s.Dir, testutil.DeepCheckpointValidation{ diff --git a/e2e/tests/deleted_files_test.go b/e2e/tests/deleted_files_test.go index ec21e3224..06a83597d 100644 --- a/e2e/tests/deleted_files_test.go +++ b/e2e/tests/deleted_files_test.go @@ -39,7 +39,7 @@ func TestDeletedFilesCommitDeletion(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Add replacement") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID1 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") testutil.AssertCheckpointExists(t, s.Dir, cpID1) diff --git a/e2e/tests/edge_cases_test.go b/e2e/tests/edge_cases_test.go index 6fa041865..325e4ee78 100644 --- a/e2e/tests/edge_cases_test.go +++ b/e2e/tests/edge_cases_test.go @@ -26,7 +26,7 @@ func TestAgentContinuesAfterCommit(t *testing.T) { t.Fatalf("agent prompt 1 failed: %v", err) } - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID1 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") cpBranchAfterFirst := testutil.GitOutput(t, s.Dir, "rev-parse", "entire/checkpoints/v1") @@ -41,7 +41,7 @@ func TestAgentContinuesAfterCommit(t *testing.T) { s.Git(t, "commit", "-m", "Add blue.md") // Wait for checkpoint branch to advance past the first checkpoint. - deadline := time.Now().Add(15 * time.Second) + deadline := time.Now().Add(s.CheckpointTimeout()) for time.Now().Before(deadline) { after := testutil.GitOutput(t, s.Dir, "rev-parse", "entire/checkpoints/v1") if after != cpBranchAfterFirst { @@ -70,7 +70,7 @@ func TestAgentAmendsCommit(t *testing.T) { } testutil.AssertFileExists(t, s.Dir, "docs/red.md") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") // Agent creates another file and amends the previous commit. @@ -109,7 +109,7 @@ func TestDirtyWorkingTree(t *testing.T) { t.Fatalf("agent failed: %v", err) } - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) testutil.AssertFileExists(t, s.Dir, "docs/red.md") // Human's uncommitted file should be untouched. @@ -176,13 +176,13 @@ func TestAgentCommitsMidTurnUserCommitsRemainder(t *testing.T) { testutil.AssertNewCommits(t, s, 1) - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpBranchAfterAgent := testutil.GitOutput(t, s.Dir, "rev-parse", "entire/checkpoints/v1") s.Git(t, "add", "user_remainder.go") s.Git(t, "commit", "-m", "Add user remainder") - testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranchAfterAgent, 15*time.Second) + testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranchAfterAgent, s.CheckpointTimeout()) userCpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") agentCpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD~1") diff --git a/e2e/tests/existing_files_test.go b/e2e/tests/existing_files_test.go index d0a4460fb..366ca7bd8 100644 --- a/e2e/tests/existing_files_test.go +++ b/e2e/tests/existing_files_test.go @@ -37,7 +37,7 @@ func TestModifyExistingTrackedFile(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Update config.go") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) testutil.AssertCheckpointAdvanced(t, s) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") @@ -72,7 +72,7 @@ func TestMixedNewAndModifiedFiles(t *testing.T) { s.Git(t, "add", "src/main.go") s.Git(t, "commit", "-m", "Update main.go") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID1 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") cpBranch1 := testutil.GitOutput(t, s.Dir, "rev-parse", "entire/checkpoints/v1") @@ -80,7 +80,7 @@ func TestMixedNewAndModifiedFiles(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Add utils.go and types.go") - testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch1, 15*time.Second) + testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch1, s.CheckpointTimeout()) cpID2 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") assert.NotEqual(t, cpID1, cpID2, "checkpoint IDs should be distinct") @@ -114,7 +114,7 @@ func TestInteractiveContentOverlapRevertNewFile(t *testing.T) { // Wait for session to transition from ACTIVE to IDLE. The prompt // pattern may appear before the turn-end hook completes (race between // TUI rendering and hook execution, especially with OpenCode). - testutil.WaitForSessionIdle(t, s.Dir, 15*time.Second) + testutil.WaitForSessionIdle(t, s.Dir, s.CheckpointTimeout()) // Session is now idle (turn ended, waiting for next prompt). // User replaces the content entirely. @@ -167,7 +167,7 @@ func TestModifiedFileAlwaysGetsCheckpoint(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Rewrite config.go") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) testutil.AssertCheckpointAdvanced(t, s) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") diff --git a/e2e/tests/external_agent_test.go b/e2e/tests/external_agent_test.go index 02d667607..2d76a622c 100644 --- a/e2e/tests/external_agent_test.go +++ b/e2e/tests/external_agent_test.go @@ -29,7 +29,7 @@ func TestExternalAgentSingleSessionManualCommit(t *testing.T) { s.Git(t, "commit", "-m", "Add hello file via external agent") testutil.AssertNewCommits(t, s, 1) - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) testutil.AssertCheckpointAdvanced(t, s) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") @@ -69,7 +69,7 @@ func TestExternalAgentMultipleTurnsManualCommit(t *testing.T) { s.Git(t, "commit", "-m", "Add alpha and beta via external agent") testutil.AssertNewCommits(t, s, 1) - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) testutil.AssertCheckpointAdvanced(t, s) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") @@ -99,7 +99,7 @@ func TestExternalAgentDeepCheckpointValidation(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Deep checkpoint validation test") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") @@ -125,7 +125,7 @@ func TestExternalAgentSessionMetadata(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Metadata test commit") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") diff --git a/e2e/tests/interactive_test.go b/e2e/tests/interactive_test.go index 9f604ccdd..b8c9944aa 100644 --- a/e2e/tests/interactive_test.go +++ b/e2e/tests/interactive_test.go @@ -29,7 +29,7 @@ func TestInteractiveMultiStep(t *testing.T) { s.WaitFor(t, session, prompt, 60*time.Second) testutil.AssertNewCommits(t, s, 1) - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) testutil.AssertCommitLinkedToCheckpoint(t, s.Dir, "HEAD") testutil.AssertNoShadowBranches(t, s.Dir) }) diff --git a/e2e/tests/mid_turn_commit_test.go b/e2e/tests/mid_turn_commit_test.go index 382a4a0a6..c1855e4b1 100644 --- a/e2e/tests/mid_turn_commit_test.go +++ b/e2e/tests/mid_turn_commit_test.go @@ -52,7 +52,7 @@ func TestMidTurnCommit_DifferentFilesThanPreviousTurn(t *testing.T) { // CRITICAL: The mid-turn commit should have triggered condensation // to entire/checkpoints/v1, even though committed files differ from // Turn 1's tracked files. This is the regression assertion. - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) testutil.AssertCheckpointAdvanced(t, s) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") diff --git a/e2e/tests/multi_session_test.go b/e2e/tests/multi_session_test.go index dfe3e7007..1c8634716 100644 --- a/e2e/tests/multi_session_test.go +++ b/e2e/tests/multi_session_test.go @@ -29,7 +29,7 @@ func TestMultiSessionManualCommit(t *testing.T) { testutil.AssertFileExists(t, s.Dir, "docs/*.md") testutil.AssertNewCommits(t, s, 1) - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) testutil.AssertCheckpointAdvanced(t, s) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") @@ -56,7 +56,7 @@ func TestMultiSessionSequential(t *testing.T) { testutil.AssertFileExists(t, s.Dir, "docs/*.md") testutil.AssertNewCommits(t, s, 2) - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) testutil.AssertCheckpointAdvanced(t, s) cpID1 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") diff --git a/e2e/tests/resume_remote_test.go b/e2e/tests/resume_remote_test.go index a587fab24..a05a27984 100644 --- a/e2e/tests/resume_remote_test.go +++ b/e2e/tests/resume_remote_test.go @@ -40,7 +40,7 @@ func TestResumeFromClonedRepo(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Add hello doc") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) // Push feature branch and metadata branch to the bare remote. s.Git(t, "push", "-u", "origin", "feature") @@ -108,7 +108,7 @@ func TestResumeMetadataBranchAlreadyLocal(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Add hello doc") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) // Switch back to main and resume the feature branch. // The metadata branch exists locally (was created during commit). diff --git a/e2e/tests/resume_test.go b/e2e/tests/resume_test.go index 95e0e56d5..3d4c17034 100644 --- a/e2e/tests/resume_test.go +++ b/e2e/tests/resume_test.go @@ -40,7 +40,7 @@ func TestResumeFromFeatureBranch(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Add hello doc") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) // Switch back to main and resume the feature branch. s.Git(t, "checkout", mainBranch) @@ -86,7 +86,7 @@ func TestResumeSquashMergeMultipleCheckpoints(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Add red doc") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cp1Ref := testutil.GitOutput(t, s.Dir, "rev-parse", "entire/checkpoints/v1") _, err = s.RunPrompt(t, ctx, @@ -97,7 +97,7 @@ func TestResumeSquashMergeMultipleCheckpoints(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Add blue doc") - testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cp1Ref, 15*time.Second) + testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cp1Ref, s.CheckpointTimeout()) // Record checkpoint IDs from both feature branch commits. cpID1 := testutil.GetCheckpointTrailer(t, s.Dir, "HEAD~1") @@ -214,7 +214,7 @@ func TestResumeOlderCheckpointWithNewerCommits(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Add hello doc") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) // Add a human-only commit on top (no agent involvement, no checkpoint). require.NoError(t, os.MkdirAll(filepath.Join(s.Dir, "notes"), 0o755)) diff --git a/e2e/tests/rewind_test.go b/e2e/tests/rewind_test.go index 999158e93..92d877153 100644 --- a/e2e/tests/rewind_test.go +++ b/e2e/tests/rewind_test.go @@ -87,7 +87,7 @@ func TestRewindAfterCommit(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Add red.md") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) // Get rewind points after commit — old shadow IDs should be gone. pointsAfter := entire.RewindList(t, s.Dir) @@ -173,7 +173,7 @@ func TestRewindSquashMergeMultipleCheckpoints(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Add red doc") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cp1Ref := testutil.GitOutput(t, s.Dir, "rev-parse", "entire/checkpoints/v1") _, err = s.RunPrompt(t, ctx, @@ -184,7 +184,7 @@ func TestRewindSquashMergeMultipleCheckpoints(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Add blue doc") - testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cp1Ref, 15*time.Second) + testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cp1Ref, s.CheckpointTimeout()) // Record checkpoint IDs from both feature branch commits. cpID1 := testutil.GetCheckpointTrailer(t, s.Dir, "HEAD~1") diff --git a/e2e/tests/session_lifecycle_test.go b/e2e/tests/session_lifecycle_test.go index f111fcb42..44913fe86 100644 --- a/e2e/tests/session_lifecycle_test.go +++ b/e2e/tests/session_lifecycle_test.go @@ -33,14 +33,14 @@ func TestEndedSessionUserCommitsAfterExit(t *testing.T) { s.Git(t, "add", "ended_a.go", "ended_b.go") s.Git(t, "commit", "-m", "Add ended files A and B") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID1 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") cpBranchAfterFirst := testutil.GitOutput(t, s.Dir, "rev-parse", "entire/checkpoints/v1") s.Git(t, "add", "ended_c.go") s.Git(t, "commit", "-m", "Add ended file C") - testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranchAfterFirst, 15*time.Second) + testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranchAfterFirst, s.CheckpointTimeout()) cpID2 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") assert.NotEqual(t, cpID1, cpID2, "each commit should have its own checkpoint ID") @@ -66,7 +66,7 @@ func TestSessionDepletedManualEditNoCheckpoint(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Add depleted.go") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") testutil.AssertCheckpointExists(t, s.Dir, cpID) diff --git a/e2e/tests/single_session_test.go b/e2e/tests/single_session_test.go index fef974e0a..51b2827df 100644 --- a/e2e/tests/single_session_test.go +++ b/e2e/tests/single_session_test.go @@ -53,7 +53,7 @@ func TestSingleSessionManualCommit(t *testing.T) { testutil.AssertFileExists(t, s.Dir, "docs/*.md") testutil.AssertNewCommits(t, s, 1) - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) testutil.AssertCheckpointAdvanced(t, s) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") @@ -76,7 +76,7 @@ func TestSingleSessionAgentCommitInTurn(t *testing.T) { testutil.AssertFileExists(t, s.Dir, "docs/*.md") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) testutil.AssertCheckpointAdvanced(t, s) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") @@ -99,7 +99,7 @@ func TestSingleSessionSubagentCommitInTurn(t *testing.T) { testutil.AssertFileExists(t, s.Dir, "docs/*.md") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) testutil.AssertCheckpointAdvanced(t, s) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") diff --git a/e2e/tests/split_commits_test.go b/e2e/tests/split_commits_test.go index cb8c41459..55bdc0c31 100644 --- a/e2e/tests/split_commits_test.go +++ b/e2e/tests/split_commits_test.go @@ -31,7 +31,7 @@ func TestUserSplitsAgentChanges(t *testing.T) { s.Git(t, "add", "docs/a.md", "docs/b.md") s.Git(t, "commit", "-m", "Add a.md and b.md") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID1 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") cpBranch1 := testutil.GitOutput(t, s.Dir, "rev-parse", "entire/checkpoints/v1") @@ -39,7 +39,7 @@ func TestUserSplitsAgentChanges(t *testing.T) { s.Git(t, "add", "-A") s.Git(t, "commit", "-m", "Commit remaining changes (including c.md and d.md)") - testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch1, 15*time.Second) + testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch1, s.CheckpointTimeout()) cpID2 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") assert.NotEqual(t, cpID1, cpID2, "checkpoint IDs should be distinct") @@ -74,7 +74,7 @@ func TestPartialStaging(t *testing.T) { s.Git(t, "add", "src/main.go") s.Git(t, "commit", "-m", "Add hello world") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID1 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") cpBranch1 := testutil.GitOutput(t, s.Dir, "rev-parse", "entire/checkpoints/v1") @@ -88,7 +88,7 @@ func TestPartialStaging(t *testing.T) { s.Git(t, "add", "-A") s.Git(t, "commit", "-m", "Add goodbye world") - testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch1, 15*time.Second) + testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch1, s.CheckpointTimeout()) cpID2 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") assert.NotEqual(t, cpID1, cpID2, "checkpoint IDs should be distinct") @@ -126,7 +126,7 @@ func TestSplitModificationsToExistingFiles(t *testing.T) { s.Git(t, "add", "src/model.go") s.Git(t, "commit", "-m", "Update model.go") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID1 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") cpBranch1 := testutil.GitOutput(t, s.Dir, "rev-parse", "entire/checkpoints/v1") @@ -134,7 +134,7 @@ func TestSplitModificationsToExistingFiles(t *testing.T) { s.Git(t, "add", "src/view.go") s.Git(t, "commit", "-m", "Update view.go") - testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch1, 15*time.Second) + testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch1, s.CheckpointTimeout()) cpID2 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") cpBranch2 := testutil.GitOutput(t, s.Dir, "rev-parse", "entire/checkpoints/v1") @@ -142,7 +142,7 @@ func TestSplitModificationsToExistingFiles(t *testing.T) { s.Git(t, "add", "-A") s.Git(t, "commit", "-m", "Commit remaining changes") - testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch2, 15*time.Second) + testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch2, s.CheckpointTimeout()) cpID3 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") // All three checkpoints should be distinct and valid. diff --git a/e2e/tests/stash_workflows_test.go b/e2e/tests/stash_workflows_test.go index e7388b55f..f4ac0fac8 100644 --- a/e2e/tests/stash_workflows_test.go +++ b/e2e/tests/stash_workflows_test.go @@ -36,7 +36,7 @@ func TestPartialCommitStashNewPrompt(t *testing.T) { s.Git(t, "add", "docs/b.md", "docs/c.md") s.Git(t, "stash") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID1 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") cpBranch1 := testutil.GitOutput(t, s.Dir, "rev-parse", "entire/checkpoints/v1") @@ -53,7 +53,7 @@ func TestPartialCommitStashNewPrompt(t *testing.T) { s.Git(t, "add", "docs/d.md", "docs/e.md") s.Git(t, "commit", "-m", "Add d.md and e.md") - testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch1, 15*time.Second) + testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch1, s.CheckpointTimeout()) cpID2 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") assert.NotEqual(t, cpID1, cpID2, "checkpoint IDs should be distinct") @@ -85,7 +85,7 @@ func TestStashSecondPromptUnstashCommitAll(t *testing.T) { s.Git(t, "add", "docs/b.md", "docs/c.md") s.Git(t, "stash") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID1 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") cpBranch1 := testutil.GitOutput(t, s.Dir, "rev-parse", "entire/checkpoints/v1") @@ -103,7 +103,7 @@ func TestStashSecondPromptUnstashCommitAll(t *testing.T) { s.Git(t, "add", "docs/") s.Git(t, "commit", "-m", "Add b, c, d, e") - testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch1, 15*time.Second) + testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch1, s.CheckpointTimeout()) cpID2 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") assert.NotEqual(t, cpID1, cpID2, "checkpoint IDs should be distinct") @@ -148,7 +148,7 @@ func TestStashModificationsToTrackedFiles(t *testing.T) { // Stash b.go modifications. s.Git(t, "stash") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) cpID1 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") cpBranch1 := testutil.GitOutput(t, s.Dir, "rev-parse", "entire/checkpoints/v1") @@ -157,7 +157,7 @@ func TestStashModificationsToTrackedFiles(t *testing.T) { s.Git(t, "add", "src/b.go") s.Git(t, "commit", "-m", "Update b.go") - testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch1, 15*time.Second) + testutil.WaitForCheckpointAdvanceFrom(t, s.Dir, cpBranch1, s.CheckpointTimeout()) cpID2 := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") assert.NotEqual(t, cpID1, cpID2, "checkpoint IDs should be distinct") diff --git a/e2e/tests/subagent_commit_flow_test.go b/e2e/tests/subagent_commit_flow_test.go index 07ad9e337..2936b33a2 100644 --- a/e2e/tests/subagent_commit_flow_test.go +++ b/e2e/tests/subagent_commit_flow_test.go @@ -26,7 +26,7 @@ func TestSubagentCommitFlow(t *testing.T) { s.Git(t, "add", ".") s.Git(t, "commit", "-m", "Add red.md via subagent") - testutil.WaitForCheckpoint(t, s, 15*time.Second) + testutil.WaitForCheckpoint(t, s, s.CheckpointTimeout()) testutil.AssertCheckpointAdvanced(t, s) cpID := testutil.AssertHasCheckpointTrailer(t, s.Dir, "HEAD") diff --git a/e2e/testutil/repo.go b/e2e/testutil/repo.go index e0e5e2bb2..fa52a5d29 100644 --- a/e2e/testutil/repo.go +++ b/e2e/testutil/repo.go @@ -412,6 +412,12 @@ func (s *RepoState) WaitFor(t *testing.T, session agents.Session, pattern string } } +// CheckpointTimeout returns the checkpoint polling timeout scaled by the +// agent's TimeoutMultiplier. The base timeout is 15 seconds. +func (s *RepoState) CheckpointTimeout() time.Duration { + return time.Duration(float64(15*time.Second) * s.Agent.TimeoutMultiplier()) +} + // IsExternalAgent returns true if the agent implements the ExternalAgent // interface and reports itself as external. func (s *RepoState) IsExternalAgent() bool {