fix(cli): address Volva code-vs-contract drift (issue #3)
Volva code-review surfaced 5 findings against the TDD-passing implementation; all 5 addressed. Drift fixes (code): - Add `assert argv is None or all(isinstance(a, str) for a in argv)` at both `main` and `_parse_args` entry points (PRE-001 was unenforced). - `main` now catches `SystemExit` and returns `exc.code` verbatim — argparse's --help (SystemExit(0)) was escaping through main as an unhandled exception. Contract amended in-place to spell out the SystemExit-from-argparse-clean-exits passthrough in both `main` and `_parse_args` ERROR_ROUTING. New `help_exits_cleanly` test added per the contract amendment. - Add the PRE-001 union-type assert at `_render_event` entry — unmatched Event variants would have silently no-op'd. - `_run_turn` now awaits `cancel_task` in the `finally` block before returning. Under fast-stream + slow-cancel scenarios the `[cancel_failed]` line could miss being written before _run_turn returns, AND _amain could close the AsyncClient while the cancel POST was still in flight. `_cancel_and_log` swallows all errors per INV-009 so the await is safe. Test gap fix: - New `_FlushCountingIO` subclass counts flush() calls; `test_text_to_stdout_only` and `test_done_writes_newline_and_label` now assert `flush_count == 1` to verify INV-010 (per-chunk flush). Previously the tests would have passed even with flush removed. Meta-note carried in persistent-memory: TDD caught central behavior (stdout/stderr routing, exit-code mapping, create-session ordering, SIGINT idempotence); the cross-model code review consistently catches assert-boundary + observability-shape gaps across all three issues (#1: 4 findings, #2: 3 findings, #3: 5 findings). 118/118 tests GREEN; ruff clean; drift check clean.
This commit is contained in:
+36
-3
@@ -34,6 +34,18 @@ from ratatoskr.sse_client import (
|
||||
)
|
||||
|
||||
|
||||
class _FlushCountingIO(io.StringIO):
|
||||
"""StringIO subclass that counts flush() calls — used to verify INV-010."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.flush_count = 0
|
||||
|
||||
def flush(self) -> None:
|
||||
self.flush_count += 1
|
||||
super().flush()
|
||||
|
||||
|
||||
def _sse_chunk(sse_id: str, body: dict[str, object]) -> bytes:
|
||||
return f"id: {sse_id}\ndata: {json.dumps(body)}\n\n".encode()
|
||||
|
||||
@@ -176,15 +188,16 @@ SID = SseId(42, 5)
|
||||
class TestRenderEvent:
|
||||
def test_text_to_stdout_only(self) -> None:
|
||||
"""text_to_stdout_only [happy,tracer]: Text → stdout=="hello"; stderr empty; flushed."""
|
||||
stdout = io.StringIO()
|
||||
stdout = _FlushCountingIO()
|
||||
stderr = io.StringIO()
|
||||
_render_event(Text(sse_id=SID, content="hello"), stdout=stdout, stderr=stderr)
|
||||
assert stdout.getvalue() == "hello"
|
||||
assert stderr.getvalue() == ""
|
||||
assert stdout.flush_count == 1 # INV-010: per-chunk flush
|
||||
|
||||
def test_done_writes_newline_and_label(self) -> None:
|
||||
"""done_writes_newline_and_label: stdout=="\\n"; stderr "[done]" + turn_id + model."""
|
||||
stdout = io.StringIO()
|
||||
"""done_writes_newline_and_label: stdout=="\\n" (flushed); stderr "[done]" labels."""
|
||||
stdout = _FlushCountingIO()
|
||||
stderr = io.StringIO()
|
||||
evt = Done(
|
||||
sse_id=SID,
|
||||
@@ -196,6 +209,7 @@ class TestRenderEvent:
|
||||
)
|
||||
_render_event(evt, stdout=stdout, stderr=stderr)
|
||||
assert stdout.getvalue() == "\n"
|
||||
assert stdout.flush_count == 1 # INV-010: post-Done newline flushed
|
||||
out_err = stderr.getvalue()
|
||||
assert out_err.startswith("[done]")
|
||||
assert "turn_id=42" in out_err
|
||||
@@ -957,3 +971,22 @@ class TestMain:
|
||||
monkeypatch.setattr(cli_mod, "_amain", fake_amain)
|
||||
rc = main(None)
|
||||
assert rc == 0
|
||||
|
||||
def test_help_exits_cleanly(
|
||||
self,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
"""help_exits_cleanly [happy]: --help → main returns 0; _amain never called."""
|
||||
amain_calls: list[int] = []
|
||||
|
||||
async def fake_amain(args: ParsedArgs) -> int:
|
||||
amain_calls.append(1)
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr(cli_mod, "_amain", fake_amain)
|
||||
rc = main(["--help"])
|
||||
assert rc == 0
|
||||
assert amain_calls == []
|
||||
# argparse prints help text to stdout
|
||||
assert "ratatoskr" in capsys.readouterr().out
|
||||
|
||||
Reference in New Issue
Block a user