fix: use __MIRE_PROMPT_READY__ as the common marker

This commit is contained in:
2026-03-21 21:10:43 +00:00
parent fe42c40d1e
commit 97a4696a1c
8 changed files with 90 additions and 76 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ const recordShellName = "shell.sh"
const ( const (
compareMarkerEnvName = "MIRE_COMPARE_MARKER" compareMarkerEnvName = "MIRE_COMPARE_MARKER"
compareMarkerEnabledValue = "1" compareMarkerEnabledValue = "1"
compareOutputMarker = "__MIRE_E2E_BEGIN__" compareOutputMarker = "__MIRE_PROMPT_READY__"
) )
//go:embed record_shell.sh //go:embed record_shell.sh
+12 -2
View File
@@ -19,11 +19,21 @@ for path in /tmp/mire-setup-scripts/*.sh; do
source "$path" source "$path"
cd "${HOME:?}" cd "${HOME:?}"
done done
EOF
if [ "${MIRE_COMPARE_MARKER:-0}" = "1" ]; then if [ "${MIRE_COMPARE_MARKER:-0}" = "1" ]; then
printf '__MIRE_E2E_BEGIN__\n' __mire_prompt_ready_original=${PROMPT_COMMAND-}
__mire_prompt_ready() {
printf '__MIRE_PROMPT_READY__\n'
if [ -n "${__mire_prompt_ready_original:-}" ]; then
PROMPT_COMMAND=$__mire_prompt_ready_original
eval "$PROMPT_COMMAND"
else
unset PROMPT_COMMAND
fi
}
PROMPT_COMMAND=__mire_prompt_ready
fi fi
EOF
set -- \ set -- \
--ro-bind / / \ --ro-bind / / \
+5 -1
View File
@@ -132,7 +132,8 @@ func TestBuildRecordShellScriptUsesExpectedCommands(t *testing.T) {
`set -- "$@" --ro-bind "$host_path" "$visible_path"`, `set -- "$@" --ro-bind "$host_path" "$visible_path"`,
"${MIRE_SETUP_SCRIPTS-}", "${MIRE_SETUP_SCRIPTS-}",
`if [ "${MIRE_COMPARE_MARKER:-0}" = "1" ]; then`, `if [ "${MIRE_COMPARE_MARKER:-0}" = "1" ]; then`,
"printf '__MIRE_E2E_BEGIN__\\n'", "printf '__MIRE_PROMPT_READY__\\n'",
"PROMPT_COMMAND=__mire_prompt_ready",
"--bind \"$host_home\" \"$visible_home\"", "--bind \"$host_home\" \"$visible_home\"",
"--bind \"$host_tmp\" '/tmp'", "--bind \"$host_tmp\" '/tmp'",
"--setenv HOME \"$visible_home\"", "--setenv HOME \"$visible_home\"",
@@ -147,6 +148,9 @@ func TestBuildRecordShellScriptUsesExpectedCommands(t *testing.T) {
t.Fatalf("wrapper = %q, want substring %q", body, want) t.Fatalf("wrapper = %q, want substring %q", body, want)
} }
} }
if strings.Contains(body, "printf '__MIRE_E2E_BEGIN__\\n'") {
t.Fatalf("wrapper = %q, want old compare marker removed", body)
}
} }
func TestRecordSessionEnvIncludesConfiguredSandboxEnv(t *testing.T) { func TestRecordSessionEnvIncludesConfiguredSandboxEnv(t *testing.T) {
+8 -30
View File
@@ -47,8 +47,8 @@ type testMismatchError struct {
} }
const ( const (
replayPromptReadyMarker = "\x1b[?2004h$ " replayPromptReadyMarker = compareOutputMarker
replayPromptReadyTimeout = 30 * time.Second replayPromptReadyTimeout = 2 * time.Second
) )
func (e *testMismatchError) Error() string { func (e *testMismatchError) Error() string {
@@ -260,24 +260,16 @@ func replayScenario(scenario testScenario, shellPath string, _ testIO, sandboxCo
compareMarkerEnvName: compareMarkerEnabledValue, compareMarkerEnvName: compareMarkerEnabledValue,
}) })
outputLog := io.Writer(rawOutFile)
var promptWriter *replayPromptWriter
var promptReady <-chan struct{}
if replayNeedsInteractivePrompt(input) {
ready := make(chan struct{}) ready := make(chan struct{})
promptWriter = newReplayPromptWriter(rawOutFile, replayPromptReadyMarker, ready) promptWriter := newReplayPromptWriter(rawOutFile, replayPromptReadyMarker, ready)
outputLog = promptWriter
promptReady = ready
promptTimeout := time.AfterFunc(replayPromptReadyTimeout, promptWriter.release) promptTimeout := time.AfterFunc(replayPromptReadyTimeout, promptWriter.release)
defer promptTimeout.Stop() defer promptTimeout.Stop()
}
if err := screen.Replay(screen.ReplayRequest{ if err := screen.Replay(screen.ReplayRequest{
Cmd: cmd, Cmd: cmd,
Input: input, Input: input,
InputReady: promptReady, InputReady: ready,
OutputLog: outputLog, OutputLog: promptWriter,
}); err != nil { }); err != nil {
rawOutFile.Close() rawOutFile.Close()
return fmt.Errorf("replay failed: %v", err) return fmt.Errorf("replay failed: %v", err)
@@ -285,8 +277,8 @@ func replayScenario(scenario testScenario, shellPath string, _ testIO, sandboxCo
if err := rawOutFile.Close(); err != nil { if err := rawOutFile.Close(); err != nil {
return fmt.Errorf("failed to close replay output: %v", err) return fmt.Errorf("failed to close replay output: %v", err)
} }
if promptWriter != nil && !promptWriter.seen { if !promptWriter.seen {
return fmt.Errorf("replay shell never reached the initial interactive prompt; inspect %q or rerun `mire init`", shellPath) return fmt.Errorf("replay shell never emitted %q; rerun `mire init` or refresh %q", compareOutputMarker, shellPath)
} }
got, err := loadRecordedOutput(rawOut) got, err := loadRecordedOutput(rawOut)
@@ -328,7 +320,7 @@ func trimReplayOutputToMarker(data []byte, shellPath string) ([]byte, error) {
idx := bytes.Index(data, []byte(compareOutputMarker)) idx := bytes.Index(data, []byte(compareOutputMarker))
if idx == -1 { if idx == -1 {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"missing compare marker %q in replay output; rerun `mire init` or refresh %q", "missing replay start marker %q in replay output; rerun `mire init` or refresh %q",
compareOutputMarker, compareOutputMarker,
shellPath, shellPath,
) )
@@ -345,20 +337,6 @@ func trimReplayOutputToMarker(data []byte, shellPath string) ([]byte, error) {
} }
} }
func replayNeedsInteractivePrompt(input []byte) bool {
for _, b := range input {
switch b {
case '\r', '\n', eofByte:
continue
}
if b < 0x20 || b == 0x7f || b == 0x1b {
return true
}
}
return false
}
type replayPromptWriter struct { type replayPromptWriter struct {
dst io.Writer dst io.Writer
marker []byte marker []byte
+31 -23
View File
@@ -123,6 +123,35 @@ func TestReplayScenarioUsesRecordedInputAndKeepsReplayOutputQuiet(t *testing.T)
} }
} }
func TestReplayScenarioWaitsForPromptReadyMarkerBeforeSendingInput(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,
"echo ab\x7fc\nexit\n",
"\x1b[?2004h$ echo ab\b \bc\r\n\x1b[?2004l\rac\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, testIO{
out: &bytes.Buffer{},
err: &bytes.Buffer{},
}, defaultSandboxConfig())
if err != nil {
t.Fatalf("replayScenario() error = %v", err)
}
}
func TestReplayScenarioFailsWhenCompareMarkerMissing(t *testing.T) { func TestReplayScenarioFailsWhenCompareMarkerMissing(t *testing.T) {
testDir := filepath.Join(t.TempDir(), "e2e") testDir := filepath.Join(t.TempDir(), "e2e")
shellPath := filepath.Join(testDir, "shell.sh") shellPath := filepath.Join(testDir, "shell.sh")
@@ -146,29 +175,8 @@ func TestReplayScenarioFailsWhenCompareMarkerMissing(t *testing.T) {
if err == nil { if err == nil {
t.Fatal("replayScenario() error = nil, want error") t.Fatal("replayScenario() error = nil, want error")
} }
if !strings.Contains(err.Error(), "missing compare marker") || !strings.Contains(err.Error(), "rerun `mire init`") { if !strings.Contains(err.Error(), "__MIRE_PROMPT_READY__") || !strings.Contains(err.Error(), "rerun `mire init`") {
t.Fatalf("replayScenario() error = %q, want compare marker refresh hint", err.Error()) t.Fatalf("replayScenario() error = %q, want prompt-ready marker refresh hint", err.Error())
}
}
func TestReplayNeedsInteractivePrompt(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
{name: "plain text", input: "echo hello\nexit\n", want: false},
{name: "backspace", input: "echo ab\x7fc\n", want: true},
{name: "escape sequence", input: "echo a\x1b[D\n", want: true},
{name: "eof suffix only", input: "echo hello\r" + string([]byte{eofByte}), want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := replayNeedsInteractivePrompt([]byte(tt.input)); got != tt.want {
t.Fatalf("replayNeedsInteractivePrompt(%q) = %v, want %v", tt.input, got, tt.want)
}
})
} }
} }
+13 -3
View File
@@ -8,6 +8,7 @@ import (
"os/exec" "os/exec"
"os/signal" "os/signal"
"syscall" "syscall"
"time"
"github.com/creack/pty" "github.com/creack/pty"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
@@ -18,6 +19,7 @@ const (
defaultRows = 24 defaultRows = 24
defaultCols = 80 defaultCols = 80
terminalEOF = byte(0x04) terminalEOF = byte(0x04)
replayReadySettleDelay = 10 * time.Millisecond
) )
type RecordRequest struct { type RecordRequest struct {
@@ -91,9 +93,11 @@ func Replay(req ReplayRequest) error {
defer ptmx.Close() defer ptmx.Close()
outputDone := copyAsync(combineWriters(req.OutputLog), ptmx) outputDone := copyAsync(combineWriters(req.OutputLog), ptmx)
inputDone := copyAsyncWhenReady(ptmx, bytes.NewReader(replayInput(req.Input)), req.InputReady) processDone := make(chan struct{})
inputDone := copyAsyncWhenReady(ptmx, bytes.NewReader(replayInput(req.Input)), req.InputReady, processDone)
waitErr := req.Cmd.Wait() waitErr := req.Cmd.Wait()
close(processDone)
ptmx.Close() ptmx.Close()
outputErr := <-outputDone outputErr := <-outputDone
@@ -129,11 +133,17 @@ func copyAsync(dst io.Writer, src io.Reader) <-chan error {
return done return done
} }
func copyAsyncWhenReady(dst io.Writer, src io.Reader, ready <-chan struct{}) <-chan error { func copyAsyncWhenReady(dst io.Writer, src io.Reader, ready <-chan struct{}, stop <-chan struct{}) <-chan error {
done := make(chan error, 1) done := make(chan error, 1)
go func() { go func() {
if ready != nil { if ready != nil {
<-ready select {
case <-ready:
time.Sleep(replayReadySettleDelay)
case <-stop:
done <- nil
return
}
} }
_, err := io.Copy(dst, src) _, err := io.Copy(dst, src)
done <- normalizeCopyError(err) done <- normalizeCopyError(err)
+6 -2
View File
@@ -388,8 +388,12 @@ while [ "$#" -gt 0 ]; do
;; ;;
esac esac
done done
if [ "${MIRE_COMPARE_MARKER:-0}" = "1" ]; then
/bin/stty -echo 2>/dev/null || true /bin/stty -echo 2>/dev/null || true
printf '%s\n' '__MIRE_PROMPT_READY__'
else
/bin/stty -echo 2>/dev/null || true
fi
while IFS= read -r line || [ -n "$line" ]; do while IFS= read -r line || [ -n "$line" ]; do
printf '%s\n' "$line" printf '%s\n' "$line"
if [ "$line" = "exit" ]; then if [ "$line" = "exit" ]; then