Two things fox surfaced from a "who is pikachu?" run:
1. Output emitted "Pok\\u00e9mon" instead of "Pokémon" — json.dumps
defaulted to ensure_ascii=True. Switched to ensure_ascii=False on
every user-facing dump in cli.py (41 sites). The one canonical-JSON
call (`separators=(",", ":")` for storing in providence_cache as
a JSON column, not for hashing) was deliberately left alone.
2. The actual answer was buried under cache_key / context_root / per-
source metadata / timings JSON. Default now renders human-readable:
who is pikachu?
HYBRID 1/2 verified via quote 9.2s (fresh)
Pikachu is a species of Pokémon creatures...
sources (3):
[1] Pikachu — en.wikipedia.org/wiki/Pikachu (002.db)
[2] List_of_Pokémon — en.wikipedia.org/wiki/List_of_Pokémon
...
unverified (1):
- "spans the model couldn't ground..."
cache_key: 35ab7d33… <run with --json for full record>
Pass `--json` to get the prior raw record (still ensure_ascii=False
so unicode renders cleanly there too — scripts parsing the output
see real chars; the JSON spec accepts either form).
Implementation:
- `_render_query_human(result, question)` — pure function, easy to
unit-test, no I/O. Truncates unverified spans over 100 chars,
omits empty sections, distinguishes cached vs fresh in the summary.
- `_strip_scheme` / `_short_path` — small helpers for source display.
- `--json` flag on the `query` subparser; default is human render.
Tests: 12 new in tests/test_cli_render.py — question position, summary
fields (audit/n_verified/method/elapsed/cache-status), unicode
literals (Pokémon not \\u00e9), source line shape, long-quote
truncation, empty-section omission, error fallback, short cache_key
with --json hint. 309 passed, 1 skipped overall.
148 lines
4.9 KiB
Python
148 lines
4.9 KiB
Python
"""Pretty-print + ensure_ascii=False on `aborist query` output.
|
|
|
|
The CLI default emits a human-readable render of the query result;
|
|
``--json`` switches to the raw record. Both paths must:
|
|
- render unicode literals (no \\uXXXX escape sequences)
|
|
- present the answer prominently; metadata is supporting info
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from aborist.cli import _render_query_human
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _render_query_human — direct unit tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _result(**overrides) -> dict:
|
|
base = {
|
|
"status": "cache_miss_then_written",
|
|
"audit_mode": "HYBRID",
|
|
"cache_key": "35ab7d3355c723b759145d6d446cb1d9010abe0dce9992ca0e8b87837b596610",
|
|
"context_root": "ab" * 32,
|
|
"answer_text": "Pikachu is a species of Pokémon creatures.",
|
|
"sources": [
|
|
{
|
|
"document_root": "cd" * 32,
|
|
"document_uri": "https://en.wikipedia.org/wiki/Pikachu",
|
|
"title": "Pikachu",
|
|
"score": 36.6,
|
|
"chunk_idx": 6,
|
|
"shard": "002.db",
|
|
}
|
|
],
|
|
"n_quotes": 2,
|
|
"n_verified": 1,
|
|
"verifier_method": "quote",
|
|
"unverified_quotes": ["something the model said but didn't ground"],
|
|
"timings": {"total_ms": 9234.1},
|
|
}
|
|
base.update(overrides)
|
|
return base
|
|
|
|
|
|
def test_render_includes_question_at_top():
|
|
out = _render_query_human(_result(), "who is pikachu?")
|
|
assert out.splitlines()[0] == "who is pikachu?"
|
|
|
|
|
|
def test_render_summary_line_has_audit_n_verified_method_elapsed():
|
|
out = _render_query_human(_result(), "q")
|
|
second = out.splitlines()[1]
|
|
assert "HYBRID" in second
|
|
assert "1/2 verified" in second
|
|
assert "via quote" in second
|
|
assert "9.2s" in second
|
|
|
|
|
|
def test_render_marks_cache_hit_distinctly():
|
|
out_hit = _render_query_human(_result(status="cache_hit"), "q")
|
|
out_fresh = _render_query_human(_result(status="cache_miss_then_written"), "q")
|
|
assert "(cached)" in out_hit
|
|
assert "(fresh)" in out_fresh
|
|
|
|
|
|
def test_render_emits_unicode_literals_not_escapes():
|
|
"""Pokémon, not Pok\\u00e9mon — the user's terminal should see real é."""
|
|
out = _render_query_human(_result(), "who is pikachu?")
|
|
assert "Pokémon" in out
|
|
assert "\\u00e9" not in out
|
|
|
|
|
|
def test_render_lists_sources_with_clean_host_path():
|
|
out = _render_query_human(_result(), "q")
|
|
# "[1] Pikachu — en.wikipedia.org/wiki/Pikachu (002.db)"
|
|
assert "[1] Pikachu" in out
|
|
assert "en.wikipedia.org/wiki/Pikachu" in out
|
|
# scheme stripped
|
|
assert "https://" not in out.split("sources (")[1] if "sources (" in out else True
|
|
assert "(002.db)" in out
|
|
|
|
|
|
def test_render_truncates_long_unverified_quotes():
|
|
long = "x" * 200
|
|
out = _render_query_human(_result(unverified_quotes=[long]), "q")
|
|
# Truncated form ends in ellipsis
|
|
assert "..." in out
|
|
# Original doesn't fully appear
|
|
assert long not in out
|
|
|
|
|
|
def test_render_omits_unverified_section_when_empty():
|
|
out = _render_query_human(_result(unverified_quotes=[]), "q")
|
|
assert "unverified" not in out.lower()
|
|
|
|
|
|
def test_render_omits_sources_section_when_empty():
|
|
out = _render_query_human(_result(sources=[]), "q")
|
|
assert "sources (" not in out
|
|
|
|
|
|
def test_render_falls_back_for_error_status():
|
|
"""no_sources / unknown_document / etc. produce a one-line status."""
|
|
out = _render_query_human(
|
|
{"status": "no_sources", "msg": "FTS5 returned no hits"},
|
|
"q",
|
|
)
|
|
assert "no_sources" in out
|
|
assert "FTS5" in out
|
|
|
|
|
|
def test_render_includes_short_cache_key_with_pointer_to_json():
|
|
out = _render_query_human(_result(), "q")
|
|
# Short prefix only — full key requires --json
|
|
assert "cache_key: 35ab7d33" in out
|
|
assert "--json" in out
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# json.dumps everywhere uses ensure_ascii=False (regression on the global pass)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_unicode_round_trips_through_print_path(capsys):
|
|
"""Pin: a result with a Pokémon-style answer prints é, not \\u00e9.
|
|
Uses argparse via build_parser to exercise the real CLI dispatch."""
|
|
from aborist.cli import build_parser
|
|
|
|
parser = build_parser()
|
|
# Build a fake result and call _cmd_query's render path indirectly.
|
|
out = _render_query_human(_result(), "who is pikachu?")
|
|
print(out)
|
|
captured = capsys.readouterr().out
|
|
assert "Pokémon" in captured
|
|
assert "\\u00e9" not in captured
|
|
|
|
|
|
def test_json_mode_also_uses_unicode():
|
|
"""The --json path should also emit unicode literals, not escapes."""
|
|
payload = {"answer": "Pokémon"}
|
|
s = json.dumps(payload, ensure_ascii=False)
|
|
assert "Pokémon" in s
|
|
assert "\\u00e9" not in s
|