Skip to content

Commit 46276ba

Browse files
committed
fix(auto): escape Rich markup in persisted answer log rendering
Addresses ouroboros-agent[bot] review on #720: - _print_status now wraps source / question / answer fields with rich.markup.escape() before passing them to console.print(). The persisted text comes straight from the interview backend and may legitimately contain "[" / "]" sequences. Without the escape Rich would interpret those as markup styles, either swallowing the text from the rendered output or raising a markup parse error and making ooo auto --resume <id> --status fail for otherwise-valid sessions. - New regression test feeds "Use [bold]uv[/] toolchain" + "Use uv [from existing setup]" through the status surface and asserts both literal bracketed segments survive verbatim. - Reformat tests/unit/auto/test_answer_source_log.py per repo ruff format policy. Refs #639
1 parent 642e158 commit 46276ba

2 files changed

Lines changed: 44 additions & 9 deletions

File tree

‎src/ouroboros/cli/commands/auto.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from pathlib import Path
99
from typing import Annotated
1010

11+
from rich.markup import escape as _rich_escape
1112
import typer
1213

1314
from ouroboros.auto.adapters import (
@@ -282,9 +283,15 @@ def _print_status(state: AutoPipelineState) -> None:
282283
console.print(f"Recent auto answers (last {len(recent)}):")
283284
for entry in recent:
284285
round_value = entry.get("round", "?")
285-
source = entry.get("source", "?")
286-
question = entry.get("question", "")
287-
answer = entry.get("answer", "")
286+
source = _rich_escape(str(entry.get("source", "?")))
287+
# Persisted question/answer text comes straight from the
288+
# interview backend and may contain "[" / "]" sequences that
289+
# Rich would otherwise interpret as markup, breaking the
290+
# rendered text or raising a parse error. Escape both fields
291+
# before printing so the status surface stays robust against
292+
# arbitrary backend output.
293+
question = _rich_escape(str(entry.get("question", "")))
294+
answer = _rich_escape(str(entry.get("answer", "")))
288295
console.print(f" round {round_value} \\[{source}] Q: {question}")
289296
console.print(f" A: {answer}")
290297
console.print(f"Resume: [bold]ooo auto --resume {state.auto_session_id}[/]")

‎tests/unit/auto/test_answer_source_log.py‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,7 @@ def test_cli_status_renders_recent_source_tagged_answers(monkeypatch, tmp_path)
117117

118118
monkeypatch.setattr("ouroboros.cli.commands.auto.AutoStore", lambda: store)
119119

120-
cli_result = CliRunner().invoke(
121-
app, ["auto", "--resume", state.auto_session_id, "--status"]
122-
)
120+
cli_result = CliRunner().invoke(app, ["auto", "--resume", state.auto_session_id, "--status"])
123121
output = _strip_ansi(cli_result.output)
124122

125123
assert cli_result.exit_code == 0
@@ -131,6 +129,38 @@ def test_cli_status_renders_recent_source_tagged_answers(monkeypatch, tmp_path)
131129
assert "A: Local-only, no network calls" in output
132130

133131

132+
def test_cli_status_escapes_rich_markup_in_persisted_answer_text(monkeypatch, tmp_path) -> None:
133+
"""Backend question/answer text that contains ``[`` must not be parsed as Rich markup.
134+
135+
Without the escape, ``console.print`` would interpret ``[bold]`` as a
136+
style and either swallow it from the output or raise a markup parse
137+
error, making ``ooo auto --status`` brittle for sessions whose
138+
interview text legitimately contains square brackets.
139+
"""
140+
store = AutoStore(tmp_path)
141+
state = AutoPipelineState(goal="Build a CLI", cwd="/tmp/project")
142+
state.transition(AutoPhase.INTERVIEW, "asking interview round 4/12")
143+
state.auto_answer_log = [
144+
{
145+
"round": 1,
146+
"source": "repo_fact",
147+
"question": "Use [bold]uv[/] toolchain or pip?",
148+
"answer": "Use uv [from existing setup]",
149+
},
150+
]
151+
store.save(state)
152+
153+
monkeypatch.setattr("ouroboros.cli.commands.auto.AutoStore", lambda: store)
154+
155+
cli_result = CliRunner().invoke(app, ["auto", "--resume", state.auto_session_id, "--status"])
156+
output = _strip_ansi(cli_result.output)
157+
158+
assert cli_result.exit_code == 0
159+
# The literal bracketed segments must survive verbatim.
160+
assert "[bold]uv[/]" in output
161+
assert "[from existing setup]" in output
162+
163+
134164
def test_cli_status_omits_recent_section_when_log_empty(monkeypatch, tmp_path) -> None:
135165
store = AutoStore(tmp_path)
136166
state = AutoPipelineState(goal="Build a CLI", cwd="/tmp/project")
@@ -139,9 +169,7 @@ def test_cli_status_omits_recent_section_when_log_empty(monkeypatch, tmp_path) -
139169

140170
monkeypatch.setattr("ouroboros.cli.commands.auto.AutoStore", lambda: store)
141171

142-
cli_result = CliRunner().invoke(
143-
app, ["auto", "--resume", state.auto_session_id, "--status"]
144-
)
172+
cli_result = CliRunner().invoke(app, ["auto", "--resume", state.auto_session_id, "--status"])
145173
output = _strip_ansi(cli_result.output)
146174

147175
assert cli_result.exit_code == 0

0 commit comments

Comments
 (0)