From 8d5cc2168a462dbd41cc8ba18e592f6cae8a8e3d Mon Sep 17 00:00:00 2001 From: Boris Nagaev Date: Fri, 22 May 2026 01:55:26 -0500 Subject: [PATCH] cmd/loop: avoid escaped session text Disable JSON HTML escaping for recorded session files and for the nested session payloads rewritten by bless mode. This keeps CLI text such as "> 1 sat/vByte" readable instead of turning it into "\u003e". Keep the metadata field order aligned with existing fixtures so a bless pass does not rewrite unrelated sessions just because the encoder changed. --- cmd/loop/session_fixture_update_test.go | 9 +++++---- cmd/loop/session_recorder.go | 22 +++++++++++++++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/cmd/loop/session_fixture_update_test.go b/cmd/loop/session_fixture_update_test.go index 0c641815..dde2a812 100644 --- a/cmd/loop/session_fixture_update_test.go +++ b/cmd/loop/session_fixture_update_test.go @@ -85,6 +85,7 @@ func writeSessionFilePath(path string, fixture sessionFile) error { defer file.Close() encoder := json.NewEncoder(file) + encoder.SetEscapeHTML(false) encoder.SetIndent("", " ") return encoder.Encode(fixture) @@ -152,7 +153,7 @@ func rewriteSessionFixture(fixture sessionFile, func rewriteExitEventRunError(events []sessionEvent, runError *string, changed bool) ([]sessionEvent, bool, error) { - data, err := json.Marshal(exitPayload{ + data, err := marshalSessionJSON(exitPayload{ RunError: cloneOptionalString(runError), }) if err != nil { @@ -338,7 +339,7 @@ func buildReplacementTextEvents(events []sessionEvent, indices []int, replacements := make([]sessionEvent, 0, len(chunks)) for i, chunk := range chunks { - data, err := json.Marshal(newTextPayload(chunk)) + data, err := marshalSessionJSON(newTextPayload(chunk)) if err != nil { return nil, err } @@ -571,7 +572,7 @@ func TestRewriteSessionFixtureSkipsNormalizedTimestampNoise(t *testing.T) { func textEvent(t *testing.T, timeMS int64, kind, text string) sessionEvent { t.Helper() - data, err := json.Marshal(newTextPayload(text)) + data, err := marshalSessionJSON(newTextPayload(text)) require.NoError(t, err) return sessionEvent{ @@ -585,7 +586,7 @@ func textEvent(t *testing.T, timeMS int64, kind, text string) sessionEvent { func exitEvent(t *testing.T, timeMS int64, runError *string) sessionEvent { t.Helper() - data, err := json.Marshal(exitPayload{ + data, err := marshalSessionJSON(exitPayload{ RunError: cloneOptionalString(runError), }) require.NoError(t, err) diff --git a/cmd/loop/session_recorder.go b/cmd/loop/session_recorder.go index ddb43dc6..2e843438 100644 --- a/cmd/loop/session_recorder.go +++ b/cmd/loop/session_recorder.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "encoding/json" "errors" @@ -48,6 +49,20 @@ var grpcMarshalOptions = protojson.MarshalOptions{ EmitUnpopulated: true, } +// marshalSessionJSON encodes nested session payloads without HTML escaping so +// recorded fixture strings stay byte-for-byte close to the CLI text. +func marshalSessionJSON(value any) ([]byte, error) { + var buf bytes.Buffer + + encoder := json.NewEncoder(&buf) + encoder.SetEscapeHTML(false) + if err := encoder.Encode(value); err != nil { + return nil, err + } + + return bytes.TrimSuffix(buf.Bytes(), []byte("\n")), nil +} + // sessionRecorder captures CLI IO and gRPC traffic for replay. type sessionRecorder struct { mu sync.Mutex @@ -80,9 +95,9 @@ type sessionMetadata struct { Args []string `json:"args"` Env map[string]string `json:"env"` Version string `json:"version"` - ClockStartUnix int64 `json:"clock_start_unix"` RunError *string `json:"run_error,omitempty"` Duration *time.Duration `json:"duration,omitempty"` + ClockStartUnix int64 `json:"clock_start_unix"` } // sessionEvent records a single timestamped payload entry. @@ -262,7 +277,7 @@ func ensureSessionBaseDir(baseDir string) error { // logEvent records a new event with the elapsed timestamp. func (r *sessionRecorder) logEvent(kind string, payload any) { - data, err := json.Marshal(payload) + data, err := marshalSessionJSON(payload) if err != nil { r.mu.Lock() if r.eventErr == nil { @@ -434,6 +449,7 @@ func (r *sessionRecorder) finalize(runErr error) error { defer file.Close() encoder := json.NewEncoder(file) + encoder.SetEscapeHTML(false) encoder.SetIndent("", " ") if err := encoder.Encode(fileContent); err != nil { finalizeErr = errors.Join(err, eventErr, hookErr) @@ -563,7 +579,7 @@ func (r *sessionRecorder) logGRPCMessage(method, event string, msg any, payload.Payload = data } } else { - data, err := json.Marshal(msg) + data, err := marshalSessionJSON(msg) if err == nil { payload.Payload = data }