Compare commits

..

No commits in common. "bca7294ec8e26e9781f813224866fe8aa1a25b32" and "b2b7026eb21cd3d6a3dd7f7d0e1574160cd770fe" have entirely different histories.

2 changed files with 1 additions and 137 deletions

View file

@ -6,9 +6,7 @@ ResearchResult contracts to the terminal.
import asyncio
import json
import os
import sys
from pathlib import Path
from typing import Optional
import click
@ -22,9 +20,6 @@ from rich.text import Text
from researchers.web.models import ResearchResult
DEFAULT_TRACE_DIR = "~/.marchwarden/traces"
# ---------------------------------------------------------------------------
# MCP client
# ---------------------------------------------------------------------------
@ -202,80 +197,5 @@ def ask(
render_result(result, console)
def _resolve_trace_path(trace_id: str, trace_dir: Optional[str]) -> Path:
"""Resolve the JSONL path for a trace_id."""
base = Path(os.path.expanduser(trace_dir or DEFAULT_TRACE_DIR))
return base / f"{trace_id}.jsonl"
def render_trace(entries: list[dict], trace_id: str, console: Console) -> None:
"""Pretty-print a list of trace entries."""
console.print(
Panel(
f"[bold]trace_id:[/bold] {trace_id}\n[bold]steps:[/bold] {len(entries)}",
title="[cyan]Replay[/cyan]",
border_style="cyan",
)
)
if not entries:
console.print("[dim]Trace file is empty.[/dim]")
return
table = Table(show_lines=True, expand=True)
table.add_column("#", style="dim", width=4)
table.add_column("Action", style="magenta")
table.add_column("Decision", overflow="fold")
table.add_column("Details", overflow="fold")
table.add_column("Hash", style="dim", overflow="fold")
reserved = {"step", "action", "decision", "timestamp", "content_hash"}
for e in entries:
step = str(e.get("step", "?"))
action = str(e.get("action", ""))
decision = str(e.get("decision", ""))
content_hash = str(e.get("content_hash", "") or "")
extras = {k: v for k, v in e.items() if k not in reserved}
details = "\n".join(f"{k}: {v}" for k, v in extras.items())
table.add_row(step, action, decision, details, content_hash)
console.print(table)
@cli.command()
@click.argument("trace_id")
@click.option(
"--trace-dir",
default=None,
help=f"Trace directory (default: {DEFAULT_TRACE_DIR}).",
)
def replay(trace_id: str, trace_dir: Optional[str]) -> None:
"""Replay a prior research run by TRACE_ID."""
console = Console()
path = _resolve_trace_path(trace_id, trace_dir)
if not path.exists():
console.print(
f"[bold red]Error:[/bold red] no trace file found for "
f"trace_id [bold]{trace_id}[/bold] at {path}"
)
sys.exit(1)
entries: list[dict] = []
with open(path, "r", encoding="utf-8") as f:
for lineno, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
try:
entries.append(json.loads(line))
except json.JSONDecodeError as e:
console.print(
f"[bold red]Error:[/bold red] invalid JSON on line {lineno}: {e}"
)
sys.exit(1)
render_trace(entries, trace_id, console)
if __name__ == "__main__":
cli()

View file

@ -4,7 +4,7 @@ from unittest.mock import patch
from click.testing import CliRunner
from cli.main import cli, render_result, render_trace
from cli.main import cli, render_result
from researchers.web.models import (
Citation,
ConfidenceFactors,
@ -130,59 +130,3 @@ class TestAskCommand:
assert result.exit_code == 1
assert "mcp went sideways" in result.output
class TestReplayCommand:
def _write_trace(self, tmp_path, trace_id="trace-xyz"):
path = tmp_path / f"{trace_id}.jsonl"
path.write_text(
'{"step": 1, "action": "search", "decision": "initial query", '
'"timestamp": "2026-04-08T00:00:00Z", "query": "utah crops"}\n'
'{"step": 2, "action": "fetch_url", "decision": "promising source", '
'"timestamp": "2026-04-08T00:00:01Z", "url": "https://example.com", '
'"content_hash": "sha256:deadbeef"}\n'
'{"step": 3, "action": "synthesize", "decision": "have enough", '
'"timestamp": "2026-04-08T00:00:02Z"}\n'
)
return path
def test_replay_renders_trace(self, tmp_path):
runner = CliRunner()
self._write_trace(tmp_path)
result = runner.invoke(
cli,
["replay", "trace-xyz", "--trace-dir", str(tmp_path)],
)
assert result.exit_code == 0, result.output
assert "trace-xyz" in result.output
assert "search" in result.output
assert "fetch_url" in result.output
assert "synthesize" in result.output
assert "sha256:deadbeef" in result.output
assert "utah crops" in result.output
def test_replay_unknown_trace_id(self, tmp_path):
runner = CliRunner()
result = runner.invoke(
cli,
["replay", "missing-id", "--trace-dir", str(tmp_path)],
)
assert result.exit_code == 1
assert "no trace file found" in result.output
def test_replay_invalid_json(self, tmp_path):
runner = CliRunner()
(tmp_path / "broken.jsonl").write_text("{not json}\n")
result = runner.invoke(
cli,
["replay", "broken", "--trace-dir", str(tmp_path)],
)
assert result.exit_code == 1
assert "invalid JSON" in result.output
def test_render_trace_empty(self):
console = Console(record=True, width=120)
render_trace([], "empty-trace", console)
out = console.export_text()
assert "empty-trace" in out
assert "empty" in out.lower()