diff --git a/cmd/root_test.go b/cmd/root_test.go index 770778a..ae59493 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -299,7 +299,7 @@ func addFakeRecordDependencies(t *testing.T, names ...string) { path := filepath.Join(binDir, name) body := "#!/bin/sh\nexit 0\n" if name == "script" { - body = "#!/bin/sh\nin=''\nout=''\nwhile [ \"$#\" -gt 0 ]; do\n case \"$1\" in\n -I)\n in=\"$2\"\n shift 2\n ;;\n -O)\n out=\"$2\"\n shift 2\n ;;\n *)\n shift\n ;;\n esac\ndone\nprintf 'fake recorded input\\n' > \"$in\"\nprintf 'fake recorded output\\n' > \"$out\"\nexit 0\n" + body = "#!/bin/sh\nin=''\nout=''\nwhile [ \"$#\" -gt 0 ]; do\n case \"$1\" in\n -I)\n in=\"$2\"\n shift 2\n ;;\n -O)\n out=\"$2\"\n shift 2\n ;;\n *)\n shift\n ;;\n esac\ndone\ncat <<'EOF' > \"$in\"\nfake recorded input\nEOF\ncat <<'EOF' > \"$out\"\nScript started on 2026-03-18 11:13:38+00:00 [TERM=\"xterm-256color\"]\nfake recorded output\nScript done on 2026-03-18 11:13:44+00:00 [COMMAND_EXIT_CODE=\"0\"]\nEOF\nexit 0\n" } if err := os.WriteFile(path, []byte(body), 0o755); err != nil { t.Fatalf("WriteFile(%q) error = %v", path, err) diff --git a/internal/miro/post_process.go b/internal/miro/post_process.go new file mode 100644 index 0000000..81abf1d --- /dev/null +++ b/internal/miro/post_process.go @@ -0,0 +1,82 @@ +package miro + +import ( + "bytes" + "fmt" + "os" +) + +var ( + scriptStartPrefix = []byte("Script started on ") + scriptDonePrefix = []byte("Script done on ") +) + +func loadRecordedOutput(path string) ([]byte, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + return stripScriptWrapper(data) +} + +func stripScriptWrapper(data []byte) ([]byte, error) { + hasHeader := bytes.HasPrefix(data, scriptStartPrefix) + + headerEnd := bytes.IndexByte(data, '\n') + if hasHeader && headerEnd == -1 { + return nil, fmt.Errorf("could not parse script wrapper from recorded output: incomplete header line") + } + + body := data + if hasHeader { + body = data[headerEnd+1:] + } + + footerStart, footerState := findScriptFooter(body) + switch { + case hasHeader && footerState == footerMissing: + return nil, fmt.Errorf("could not parse script wrapper from recorded output: missing footer line") + case !hasHeader && footerState == footerFound: + return nil, fmt.Errorf("could not parse script wrapper from recorded output: missing header line") + case !hasHeader && footerState == footerMissing: + return nil, fmt.Errorf("could not parse script wrapper from recorded output: missing header and footer lines") + case footerState == footerMalformed: + return nil, fmt.Errorf("could not parse script wrapper from recorded output: incomplete footer line") + } + + return body[:footerStart], nil +} + +type footerMatchState int + +const ( + footerMissing footerMatchState = iota + footerMalformed + footerFound +) + +func findScriptFooter(data []byte) (int, footerMatchState) { + candidate := -1 + switch { + case bytes.HasPrefix(data, scriptDonePrefix): + candidate = 0 + default: + idx := bytes.LastIndex(data, append([]byte{'\n'}, scriptDonePrefix...)) + if idx != -1 { + candidate = idx + 1 + } + } + + if candidate == -1 { + return 0, footerMissing + } + + footer := data[candidate:] + newline := bytes.IndexByte(footer, '\n') + if newline == -1 || candidate+newline+1 != len(data) { + return 0, footerMalformed + } + + return candidate, footerFound +} diff --git a/internal/miro/record_session.go b/internal/miro/record_session.go index 0182cdc..0c0d9a1 100644 --- a/internal/miro/record_session.go +++ b/internal/miro/record_session.go @@ -48,10 +48,15 @@ func recordScenario(target string, rio recordIO) error { return ErrRecordingDiscarded } - if err := copyRecordFile(rawIn, filepath.Join(target, "in")); err != nil { + recordedIn, recordedOut, err := loadRecordedFixtures(rawIn, rawOut) + if err != nil { return err } - if err := copyRecordFile(rawOut, filepath.Join(target, "out")); err != nil { + + if err := os.WriteFile(filepath.Join(target, "in"), recordedIn, 0o644); err != nil { + return err + } + if err := os.WriteFile(filepath.Join(target, "out"), recordedOut, 0o644); err != nil { return err } @@ -131,11 +136,16 @@ func recordFixturesExist(target string) (bool, error) { return false, nil } -func copyRecordFile(src, dst string) error { - data, err := os.ReadFile(src) +func loadRecordedFixtures(rawIn, rawOut string) ([]byte, []byte, error) { + recordedIn, err := os.ReadFile(rawIn) if err != nil { - return err + return nil, nil, err } - return os.WriteFile(dst, data, 0o644) + recordedOut, err := loadRecordedOutput(rawOut) + if err != nil { + return nil, nil, err + } + + return recordedIn, recordedOut, nil } diff --git a/internal/miro/record_test.go b/internal/miro/record_test.go index 43ec5cd..aab175c 100644 --- a/internal/miro/record_test.go +++ b/internal/miro/record_test.go @@ -33,6 +33,13 @@ func TestRecordCreatesRelativePath(t *testing.T) { t.Fatalf("Stat(%q) error = %v", filepath.Join(want, name), err) } } + + if got := readFile(t, filepath.Join(want, "in")); got != "fake recorded input\n" { + t.Fatalf("saved in = %q, want %q", got, "fake recorded input\n") + } + if got := readFile(t, filepath.Join(want, "out")); got != "fake recorded output\n" { + t.Fatalf("saved out = %q, want %q", got, "fake recorded output\n") + } } func TestRecordStripsExplicitTestDirPrefix(t *testing.T) { @@ -60,6 +67,10 @@ func TestRecordStripsExplicitTestDirPrefix(t *testing.T) { t.Fatalf("Stat(%q) error = %v", filepath.Join(want, name), err) } } + + if got := readFile(t, filepath.Join(want, "out")); got != "fake recorded output\n" { + t.Fatalf("saved out = %q, want %q", got, "fake recorded output\n") + } } func TestRecordRejectsAbsolutePathOutsideTestDir(t *testing.T) { @@ -193,6 +204,17 @@ func withStdin(t *testing.T, input string, fn func()) { fn() } +func readFile(t *testing.T, path string) string { + t.Helper() + + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("ReadFile(%q) error = %v", path, err) + } + + return string(data) +} + type ioDiscard struct{} func (ioDiscard) Write(p []byte) (int, error) { @@ -207,7 +229,7 @@ func addFakeRecordDependencies(t *testing.T, names ...string) { path := filepath.Join(binDir, name) body := "#!/bin/sh\nexit 0\n" if name == "script" { - body = "#!/bin/sh\nin=''\nout=''\nwhile [ \"$#\" -gt 0 ]; do\n case \"$1\" in\n -I)\n in=\"$2\"\n shift 2\n ;;\n -O)\n out=\"$2\"\n shift 2\n ;;\n *)\n shift\n ;;\n esac\ndone\nprintf 'fake recorded input\\n' > \"$in\"\nprintf 'fake recorded output\\n' > \"$out\"\nexit 0\n" + body = "#!/bin/sh\nin=''\nout=''\nwhile [ \"$#\" -gt 0 ]; do\n case \"$1\" in\n -I)\n in=\"$2\"\n shift 2\n ;;\n -O)\n out=\"$2\"\n shift 2\n ;;\n *)\n shift\n ;;\n esac\ndone\nif [ -n \"${FAKE_SCRIPT_LOG_IN+x}\" ]; then\n printf '%s' \"$FAKE_SCRIPT_LOG_IN\" > \"$in\"\nelse\n cat <<'EOF' > \"$in\"\nfake recorded input\nEOF\nfi\nif [ -n \"${FAKE_SCRIPT_LOG_OUT+x}\" ]; then\n printf '%s' \"$FAKE_SCRIPT_LOG_OUT\" > \"$out\"\nelse\n cat <<'EOF' > \"$out\"\nScript started on 2026-03-18 11:13:38+00:00 [TERM=\"xterm-256color\"]\nfake recorded output\nScript done on 2026-03-18 11:13:44+00:00 [COMMAND_EXIT_CODE=\"0\"]\nEOF\nfi\nexit 0\n" } if err := os.WriteFile(path, []byte(body), 0o755); err != nil { t.Fatalf("WriteFile(%q) error = %v", path, err)