diff --git a/e2e/demo/in b/e2e/demo/in index de407f5..cde3caa 100644 --- a/e2e/demo/in +++ b/e2e/demo/in @@ -1,2 +1,2 @@ -echo a +djfksd fsdf exit diff --git a/e2e/demo/out b/e2e/demo/out index 30ef0e9..d1ea67a 100644 --- a/e2e/demo/out +++ b/e2e/demo/out @@ -1,4 +1,4 @@ -[?2004h$ echo a -[?2004l a +[?2004h$ djfksd fsdf +[?2004l bash: djfksd: command not found [?2004h$ exit [?2004l exit diff --git a/internal/mire/record_test.go b/internal/mire/record_test.go index ef18c6f..2b97e15 100644 --- a/internal/mire/record_test.go +++ b/internal/mire/record_test.go @@ -505,6 +505,44 @@ func TestRecordScenarioStripsInterruptsFromSavedFixtures(t *testing.T) { } } +func TestRecordScenarioAllowsCommandNotFoundDuringVerification(t *testing.T) { + testutil.RequireCommands(t, "bwrap", "bash") + + root := t.TempDir() + testDir := filepath.Join(root, "e2e") + target := filepath.Join(testDir, "suite", "spec") + testutil.MustMkdirAll(t, target) + mustWriteRecordShell(t, testDir) + + err := testutil.WithWorkingDir(t, root, func() error { + return withPromptedRecordStreams( + t, + "a\nexit\n", + "y\n", + func(rio recordIO) error { + rio.out = ioDiscard{} + return recordScenario(target, recordShellPath(testDir), rio, defaultSandboxConfig(), nil, nil, nil, false) + }, + ) + }) + if err != nil { + t.Fatalf("recordScenario() error = %v", err) + } + + recordedIn := testutil.ReadFile(t, filepath.Join(target, "in")) + if recordedIn != "a\nexit\n" { + t.Fatalf("saved in = %q, want %q", recordedIn, "a\nexit\n") + } + + recordedOut := testutil.ReadFile(t, filepath.Join(target, "out")) + if !strings.Contains(recordedOut, "bash: a: command not found") { + t.Fatalf("saved out = %q, want command-not-found output", recordedOut) + } + if !strings.Contains(recordedOut, "exit\r\n") { + t.Fatalf("saved out = %q, want exit preserved", recordedOut) + } +} + func TestRecordFailsWhenRecorderShellMissing(t *testing.T) { root := t.TempDir() testDir := filepath.Join(root, "e2e") diff --git a/internal/mire/replay.go b/internal/mire/replay.go index cd87f02..ae2a243 100644 --- a/internal/mire/replay.go +++ b/internal/mire/replay.go @@ -2,6 +2,7 @@ package mire import ( "bytes" + "errors" "fmt" "io" "os" @@ -142,7 +143,7 @@ func replayScenarioOutputFromInput(scenario testScenario, shellPath string, sand } return nil, fmt.Errorf("replay shell never emitted %q; rerun `mire init` or refresh %q", compareOutputMarker, shellPath) } - if replayResult.ProcessErr != nil { + if replayResult.ProcessErr != nil && !isNonFatalReplayProcessErr(replayResult.ProcessErr) { return nil, fmt.Errorf("replay failed: %v", replayResult.ProcessErr) } if replayResult.OutputErr != nil { @@ -164,6 +165,11 @@ func replayScenarioOutputFromInput(scenario testScenario, shellPath string, sand return got, nil } +func isNonFatalReplayProcessErr(err error) bool { + var exitErr *exec.ExitError + return errors.As(err, &exitErr) +} + func trimReplayOutputToMarker(data []byte, shellPath string) ([]byte, error) { idx := bytes.Index(data, []byte(compareOutputMarker)) if idx == -1 { diff --git a/internal/mire/test_test.go b/internal/mire/test_test.go index 108d9f9..bba8ed4 100644 --- a/internal/mire/test_test.go +++ b/internal/mire/test_test.go @@ -132,6 +132,32 @@ func TestReplayScenarioIgnoresConfiguredDiffLines(t *testing.T) { } } +func TestReplayScenarioAllowsNonZeroShellExitWhenOutputMatches(t *testing.T) { + testutil.RequireCommands(t, "bwrap", "bash") + + testDir := filepath.Join(t.TempDir(), "e2e") + shellPath := filepath.Join(testDir, "shell.sh") + mustWriteRecordShell(t, testDir) + scenarioDir := filepath.Join(testDir, "suite", "spec") + testutil.WriteScenarioFixtures( + t, + scenarioDir, + "a\nexit\n", + "\x1b[?2004h$ a\r\n\x1b[?2004l\rbash: a: command not found\r\n\x1b[?2004h$ exit\r\n\x1b[?2004l\rexit\r\n", + ) + + err := replayScenario(testScenario{ + dir: scenarioDir, + relPath: filepath.Join("suite", "spec"), + inPath: filepath.Join(scenarioDir, "in"), + outPath: filepath.Join(scenarioDir, "out"), + setupScripts: nil, + }, shellPath, nil, defaultSandboxConfig(), nil, nil) + if err != nil { + t.Fatalf("replayScenario() error = %v", err) + } +} + func TestReplayScenarioWaitsForPromptReadyMarkerBeforeSendingInput(t *testing.T) { testutil.RequireCommands(t, "bwrap", "bash")