cli: human render for query by default; --json for raw; ensure_ascii=False
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.
This commit is contained in:
parent
6291f7c190
commit
31701ad524
2 changed files with 282 additions and 41 deletions
175
aborist/cli.py
175
aborist/cli.py
|
|
@ -137,7 +137,7 @@ def _cmd_ingest(args: argparse.Namespace) -> int:
|
|||
)
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps(result.__dict__, indent=2))
|
||||
print(json.dumps(result.__dict__, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -167,7 +167,7 @@ def _cmd_search(args: argparse.Namespace) -> int:
|
|||
}
|
||||
for h in hits
|
||||
],
|
||||
indent=2,
|
||||
indent=2, ensure_ascii=False
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
|
@ -189,7 +189,7 @@ def _cmd_verify(args: argparse.Namespace) -> int:
|
|||
result = verify_random_sample(conn, n=args.n)
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps(result, indent=2))
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
return 0 if result["failed"] == 0 else 1
|
||||
|
||||
|
||||
|
|
@ -238,7 +238,7 @@ def _cmd_distill(args: argparse.Namespace) -> int:
|
|||
per_shard.append({"shard": sp.name, **r})
|
||||
for k in totals:
|
||||
totals[k] += r[k]
|
||||
print(json.dumps({**totals, "shards": per_shard}, indent=2))
|
||||
print(json.dumps({**totals, "shards": per_shard}, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
conn = connect(args.db)
|
||||
|
|
@ -254,7 +254,7 @@ def _cmd_distill(args: argparse.Namespace) -> int:
|
|||
)
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps(result, indent=2))
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -300,7 +300,7 @@ def _cmd_ask(args: argparse.Namespace) -> int:
|
|||
)
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps(result, indent=2))
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
return 0 if result.get("status") in ("cache_hit", "cache_miss_then_written") else 1
|
||||
|
||||
|
||||
|
|
@ -357,7 +357,10 @@ def _cmd_query(args: argparse.Namespace) -> int:
|
|||
max_context_chars=args.max_context_chars,
|
||||
)
|
||||
|
||||
print(json.dumps(result, indent=2))
|
||||
if args.json:
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(_render_query_human(result, args.question))
|
||||
return (
|
||||
0
|
||||
if result.get("status") in ("cache_hit", "cache_miss_then_written")
|
||||
|
|
@ -365,6 +368,92 @@ def _cmd_query(args: argparse.Namespace) -> int:
|
|||
)
|
||||
|
||||
|
||||
def _render_query_human(result: dict, question: str) -> str:
|
||||
"""Pretty-print a query result for terminal reading.
|
||||
|
||||
Layout:
|
||||
question
|
||||
AUDIT_MODE N/M verified via verifier_method Xs (cached|fresh)
|
||||
|
||||
answer text...
|
||||
|
||||
sources (K):
|
||||
[1] Title — host/path (shard.db)
|
||||
[2] ...
|
||||
|
||||
unverified (J):
|
||||
- "..."
|
||||
|
||||
cache_key: 35ab7d33… <run with --json for full record>
|
||||
|
||||
Errors / no-source paths fall back to a short status line.
|
||||
"""
|
||||
status = result.get("status")
|
||||
if status not in ("cache_hit", "cache_miss_then_written"):
|
||||
msg = result.get("msg") or status or "unknown error"
|
||||
return f" {status or 'error'}: {msg}"
|
||||
|
||||
audit = result.get("audit_mode", "?")
|
||||
n_quotes = result.get("n_quotes", 0) or 0
|
||||
n_verified = result.get("n_verified", 0) or 0
|
||||
method = result.get("verifier_method", "?")
|
||||
timings = result.get("timings") or {}
|
||||
total_ms = timings.get("total_ms")
|
||||
elapsed = f"{total_ms / 1000:.1f}s" if isinstance(total_ms, (int, float)) else "?"
|
||||
cache_status = "cached" if status == "cache_hit" else "fresh"
|
||||
|
||||
lines: list[str] = []
|
||||
lines.append(question)
|
||||
lines.append(
|
||||
f" {audit} {n_verified}/{n_quotes} verified via {method} "
|
||||
f"{elapsed} ({cache_status})"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
answer = result.get("answer_text") or ""
|
||||
lines.append(answer)
|
||||
lines.append("")
|
||||
|
||||
sources = result.get("sources") or []
|
||||
if sources:
|
||||
lines.append(f"sources ({len(sources)}):")
|
||||
for i, s in enumerate(sources, start=1):
|
||||
uri = s.get("document_uri", "")
|
||||
title = (s.get("title") or "").strip() or _short_path(uri)
|
||||
shard = s.get("shard")
|
||||
shard_part = f" ({shard})" if shard else ""
|
||||
lines.append(f" [{i}] {title} — {_strip_scheme(uri)}{shard_part}")
|
||||
lines.append("")
|
||||
|
||||
unverified = result.get("unverified_quotes") or []
|
||||
if unverified:
|
||||
lines.append(f"unverified ({len(unverified)}):")
|
||||
for q in unverified:
|
||||
qtxt = q if len(q) <= 100 else q[:97] + "..."
|
||||
lines.append(f' - "{qtxt}"')
|
||||
lines.append("")
|
||||
|
||||
cache_key = (result.get("cache_key") or "")[:8]
|
||||
lines.append(f"cache_key: {cache_key}… <run with --json for full record>")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _strip_scheme(uri: str) -> str:
|
||||
"""`https://en.wikipedia.org/wiki/X` -> `en.wikipedia.org/wiki/X`."""
|
||||
for prefix in ("https://", "http://"):
|
||||
if uri.startswith(prefix):
|
||||
return uri[len(prefix):]
|
||||
return uri
|
||||
|
||||
|
||||
def _short_path(uri: str) -> str:
|
||||
"""Last URL segment as a fallback display name."""
|
||||
s = _strip_scheme(uri).rstrip("/")
|
||||
if "/" in s:
|
||||
return s.rsplit("/", 1)[1]
|
||||
return s
|
||||
|
||||
|
||||
def _falsify_cache_key(
|
||||
cache_key_value: str,
|
||||
*,
|
||||
|
|
@ -857,7 +946,7 @@ def _cmd_burn(args: argparse.Namespace) -> int:
|
|||
print(f"unknown burn kind: {kind}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
print(json.dumps(result, indent=2))
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
return 0 if result.get("status") == "burned" else 1
|
||||
|
||||
|
||||
|
|
@ -872,7 +961,7 @@ def _cmd_providence(args: argparse.Namespace) -> int:
|
|||
shards_dir=Path(args.global_shards_dir) if args.global_shards_dir else None,
|
||||
db_path=Path(args.db) if args.db else None,
|
||||
)
|
||||
print(json.dumps(result, indent=2))
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
return 0 if result.get("status") == "falsified" else 1
|
||||
conn = (
|
||||
connect_query(args.db, shards_dir=args.global_shards_dir)
|
||||
|
|
@ -914,7 +1003,7 @@ def _cmd_providence(args: argparse.Namespace) -> int:
|
|||
}
|
||||
for r in rows
|
||||
]
|
||||
print(json.dumps(out, indent=2))
|
||||
print(json.dumps(out, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -1030,7 +1119,7 @@ def _cmd_reclassify(args: argparse.Namespace) -> int:
|
|||
"skipped_cold": skipped_cold,
|
||||
"distribution": {p: dict(d) for p, d in distribution.items()},
|
||||
"records": grid,
|
||||
}, indent=2))
|
||||
}, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
# Single-policy reclassify. Default tracks DEFAULT_ENTITY_POLICY
|
||||
|
|
@ -1131,7 +1220,7 @@ def _cmd_reclassify(args: argparse.Namespace) -> int:
|
|||
|
||||
summary["transitions"] = dict(summary["transitions"])
|
||||
summary["dry_run"] = bool(args.dry_run)
|
||||
print(json.dumps(summary, indent=2))
|
||||
print(json.dumps(summary, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -1162,7 +1251,7 @@ def _cmd_emergent(args: argparse.Namespace) -> int:
|
|||
ranked = sorted(counts.items(), key=lambda kv: -kv[1])[: args.limit]
|
||||
print(json.dumps(
|
||||
[{"quote": q, "count": c} for q, c in ranked],
|
||||
indent=2,
|
||||
indent=2, ensure_ascii=False
|
||||
))
|
||||
else:
|
||||
rows = conn.execute(
|
||||
|
|
@ -1191,7 +1280,7 @@ def _cmd_emergent(args: argparse.Namespace) -> int:
|
|||
}
|
||||
for r in rows
|
||||
]
|
||||
print(json.dumps(out, indent=2))
|
||||
print(json.dumps(out, indent=2, ensure_ascii=False))
|
||||
finally:
|
||||
conn.close()
|
||||
return 0
|
||||
|
|
@ -1214,7 +1303,7 @@ def _cmd_evict(args: argparse.Namespace) -> int:
|
|||
)
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps(result, indent=2))
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -1249,7 +1338,7 @@ def _cmd_rehydrate(args: argparse.Namespace) -> int:
|
|||
results.append(res)
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps(results, indent=2))
|
||||
print(json.dumps(results, indent=2, ensure_ascii=False))
|
||||
drift = sum(1 for r in results if r.get("status") == "drift_detected")
|
||||
return 1 if drift else 0
|
||||
|
||||
|
|
@ -1418,7 +1507,7 @@ def _cmd_activity(args: argparse.Namespace) -> int:
|
|||
"recent_derives": derive_events[: args.limit],
|
||||
"recent_falsifications": falsifications[: args.limit],
|
||||
},
|
||||
indent=2,
|
||||
indent=2, ensure_ascii=False
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
|
@ -1434,7 +1523,7 @@ def _cmd_stats(args: argparse.Namespace) -> int:
|
|||
result = stats(conn)
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps(result, indent=2))
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -1575,7 +1664,7 @@ def _cmd_analyze(args: argparse.Namespace) -> int:
|
|||
}
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps(report, indent=2))
|
||||
print(json.dumps(report, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -1603,7 +1692,7 @@ def _cmd_snapshot_create(args: argparse.Namespace) -> int:
|
|||
)
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps(result, indent=2))
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
# Cross-shard: compute against UNION, write to args.db.
|
||||
|
|
@ -1666,7 +1755,7 @@ def _cmd_snapshot_create(args: argparse.Namespace) -> int:
|
|||
"reason": args.reason,
|
||||
"scope": "shards-union",
|
||||
},
|
||||
indent=2,
|
||||
indent=2, ensure_ascii=False
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
|
@ -1680,7 +1769,7 @@ def _cmd_snapshot_list(args: argparse.Namespace) -> int:
|
|||
rows = list_snapshots(conn, limit=args.limit)
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps(rows, indent=2))
|
||||
print(json.dumps(rows, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -1696,7 +1785,7 @@ def _cmd_snapshot_verify(args: argparse.Namespace) -> int:
|
|||
result = verify_snapshot(conn, args.snapshot_root)
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps(result, indent=2))
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
return 0 if result["matches"] else 1
|
||||
|
||||
|
||||
|
|
@ -1712,7 +1801,7 @@ def _cmd_snapshot_diff(args: argparse.Namespace) -> int:
|
|||
result = diff_against_current(conn, args.snapshot_root)
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps(result, indent=2))
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -1751,7 +1840,7 @@ def _cmd_mesh_status(args: argparse.Namespace) -> int:
|
|||
}
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps(out, indent=2))
|
||||
print(json.dumps(out, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -1777,7 +1866,7 @@ def _cmd_mesh_init(args: argparse.Namespace) -> int:
|
|||
"note": "share sign_pub_hex + dh_pub_hex with the founder of any group "
|
||||
"you want to join. Run 'mesh enable' to flip the gating flag on.",
|
||||
},
|
||||
indent=2,
|
||||
indent=2, ensure_ascii=False
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
|
@ -1791,7 +1880,7 @@ def _cmd_mesh_enable(args: argparse.Namespace) -> int:
|
|||
set_enabled(conn, True)
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps({"enabled": True}, indent=2))
|
||||
print(json.dumps({"enabled": True}, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -1803,7 +1892,7 @@ def _cmd_mesh_disable(args: argparse.Namespace) -> int:
|
|||
set_enabled(conn, False)
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps({"enabled": False}, indent=2))
|
||||
print(json.dumps({"enabled": False}, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -1815,7 +1904,7 @@ def _cmd_mesh_members(args: argparse.Namespace) -> int:
|
|||
try:
|
||||
epoch = current_epoch(conn)
|
||||
if epoch is None:
|
||||
print(json.dumps({"error": "mesh not initialized"}, indent=2))
|
||||
print(json.dumps({"error": "mesh not initialized"}, indent=2, ensure_ascii=False))
|
||||
return 2
|
||||
roster = roster_at(conn, epoch)
|
||||
finally:
|
||||
|
|
@ -1834,7 +1923,7 @@ def _cmd_mesh_members(args: argparse.Namespace) -> int:
|
|||
for m in roster
|
||||
],
|
||||
},
|
||||
indent=2,
|
||||
indent=2, ensure_ascii=False
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
|
@ -1867,7 +1956,7 @@ def _cmd_mesh_add(args: argparse.Namespace) -> int:
|
|||
return 2
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps({"new_epoch": epoch, "added": args.member_id}, indent=2))
|
||||
print(json.dumps({"new_epoch": epoch, "added": args.member_id}, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -1885,7 +1974,7 @@ def _cmd_mesh_kick(args: argparse.Namespace) -> int:
|
|||
print(
|
||||
json.dumps(
|
||||
{"new_epoch": epoch, "kicked": args.member_id, "reason": args.reason},
|
||||
indent=2,
|
||||
indent=2, ensure_ascii=False
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
|
@ -1902,7 +1991,7 @@ def _cmd_mesh_rotate(args: argparse.Namespace) -> int:
|
|||
return 2
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps({"new_epoch": epoch, "reason": args.reason}, indent=2))
|
||||
print(json.dumps({"new_epoch": epoch, "reason": args.reason}, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -1923,12 +2012,12 @@ def _cmd_mesh_serve(args: argparse.Namespace) -> int:
|
|||
conn.close()
|
||||
|
||||
srv = MeshWireServer(args.db, host=args.host, port=args.port)
|
||||
print(json.dumps({"status": "serving", "url": srv.url, "db": str(args.db)}))
|
||||
print(json.dumps({"status": "serving", "url": srv.url, "db": str(args.db)}, ensure_ascii=False))
|
||||
sys.stdout.flush()
|
||||
try:
|
||||
srv.serve()
|
||||
except KeyboardInterrupt:
|
||||
print(json.dumps({"status": "stopped", "reason": "SIGINT"}))
|
||||
print(json.dumps({"status": "stopped", "reason": "SIGINT"}, ensure_ascii=False))
|
||||
finally:
|
||||
srv.stop()
|
||||
return 0
|
||||
|
|
@ -1967,7 +2056,7 @@ def _cmd_mesh_sync(args: argparse.Namespace) -> int:
|
|||
try:
|
||||
peer_info = client.info()
|
||||
except Exception as e:
|
||||
print(json.dumps({"status": "peer_unreachable", "peer": args.peer, "error": str(e)}, indent=2))
|
||||
print(json.dumps({"status": "peer_unreachable", "peer": args.peer, "error": str(e)}, indent=2, ensure_ascii=False))
|
||||
return 2
|
||||
for r in rows:
|
||||
try:
|
||||
|
|
@ -1991,7 +2080,7 @@ def _cmd_mesh_sync(args: argparse.Namespace) -> int:
|
|||
"errors": len(errors),
|
||||
"sent": sent[: args.verbose],
|
||||
"error_samples": errors[:5],
|
||||
}, indent=2))
|
||||
}, indent=2, ensure_ascii=False))
|
||||
return 0 if not errors else 1
|
||||
|
||||
|
||||
|
|
@ -2031,7 +2120,7 @@ def _cmd_mesh_pull(args: argparse.Namespace) -> int:
|
|||
"document_root": already["document_root"],
|
||||
"document_uri": already["document_uri"],
|
||||
"shard": str(args.db),
|
||||
}, indent=2))
|
||||
}, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
try:
|
||||
|
|
@ -2098,7 +2187,7 @@ def _cmd_mesh_pull(args: argparse.Namespace) -> int:
|
|||
"document_uri": delivered_uri,
|
||||
"shard": str(args.db),
|
||||
"audit_event_hash": event_hash,
|
||||
}, indent=2))
|
||||
}, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -2168,7 +2257,7 @@ def _cmd_crawl(args: argparse.Namespace) -> int:
|
|||
"discovered": len(urls),
|
||||
**result,
|
||||
},
|
||||
indent=2,
|
||||
indent=2, ensure_ascii=False
|
||||
))
|
||||
return 0
|
||||
|
||||
|
|
@ -2196,7 +2285,7 @@ def _cmd_crawler_recrawl_check(args: argparse.Namespace) -> int:
|
|||
)
|
||||
finally:
|
||||
conn.close()
|
||||
print(json.dumps(result, indent=2))
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -2440,6 +2529,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|||
"--dry-run", dest="dry_run", action="store_true",
|
||||
help="use StubClient — assembles context but skips the LLM call",
|
||||
)
|
||||
query_cmd.add_argument(
|
||||
"--json", action="store_true",
|
||||
help="emit the raw record as indented JSON (default: human render)",
|
||||
)
|
||||
query_cmd.set_defaults(func=_cmd_query)
|
||||
|
||||
prov_cmd = sub.add_parser(
|
||||
|
|
|
|||
148
tests/test_cli_render.py
Normal file
148
tests/test_cli_render.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue