phase: 160_tool_budget_telemetry
All green. Final report: **Phase 160 completion pass — verified (final verification; all 5 tasks already in `complete/`)** - **Verified, no code defects found:** budget trailers (`TOOL_BUDGET_REMAINING/LAST/SPENT`), cap-path rejection + `ROUND_CAP_FALLBACK_ANSWER` + `ROUND_CAP_WRAPUP_INSTRUCTION`, `done.budget_exhausted` (ABSENT-when-false), `rounds=/cap_hit=` log fields, `agent_max_rounds=10` default, frontend Continue button + restore + shared-page exclusion, mock markers. - **Tests/lint:** `uv run pytest` 153/153 green (EXIT 0); `uv run pytest --cov=app --cov-report=term-missing` TOTAL **99%** (>90%); `uv run ruff check .` clean; `uv run pyright` 0 errors/0 warnings. - **Criteria:** ① decreasing trailer pinned by unit tests (9→…→spent) ✅ ② cap path ends readable, no tool lines after cap (E2E) ✅ ③ `budget_exhausted` true-only-on-cap, absent otherwise (integration) ✅ ④ Continue button → one fresh `continue` turn, fresh budget, record intact (E2E) ✅ ⑤ flag survives refresh; shared chat never renders it (`shared.js` has no `budget_exhausted` path) ✅ ⑥ `max_rounds=0` byte-identical single `tools=None` request; default still 10 ✅ ⑦ full suite/coverage/lint green ✅ ⑧ `test_tool_budget_telemetry.py` **7/7** in isolation; all 6 regression E2E suites green in isolation ✅ ⑨ commit + phase move = harness (I make no git history/index changes). - **Docs:** README.md — fixed: the "Continue researching" button and cap behavior were undocumented (added a Chat-section paragraph + an Agent-tools-section paragraph, incl. the phase-71 recorded-revision note); env-var table, commands, and architecture all verified consistent otherwise. AGENTS.md — verified consistent, no changes needed. - **Decisions:** no code changes required — phase was fully implemented; only README docs added. No live-infrastructure touched. - **Next pending phase:** `161_reasoning_bubble`.
This commit is contained in:
@@ -565,6 +565,88 @@ def test_done_event_answer_image_ref_drops_its_own_null_image_url() -> None:
|
||||
assert "image_url" not in dump["answer_image"]
|
||||
|
||||
|
||||
# --- the ``budget_exhausted`` done key (phase 160, task 03) ---
|
||||
|
||||
|
||||
def test_done_event_without_budget_exhausted_drops_the_key() -> None:
|
||||
"""``budget_exhausted=False`` (the default — every deflected /
|
||||
in-cap / pre-phase turn) → ``model_dump()`` has NO
|
||||
``budget_exhausted`` key — ABSENT, never ``false`` — and a full
|
||||
done frame (sources + related + suggestions, no flag) serializes
|
||||
byte-identically to the pre-phase expectation (the
|
||||
byte-identical-off contract, PLAN §13)."""
|
||||
event = ChatDoneEvent(
|
||||
deflected=False,
|
||||
sources=[SourceRef(source="docs", path="a.md", title="A")],
|
||||
related=[SourceRef(source="docs", path="b.md", title="B")],
|
||||
suggestions=["Maybe try X?"],
|
||||
)
|
||||
dump = event.model_dump()
|
||||
assert "budget_exhausted" not in dump
|
||||
# The pre-phase-160 key set, exactly (answer_image absent too).
|
||||
assert set(dump) == {"type", "deflected", "sources", "related", "suggestions"}
|
||||
assert _json_dumps(dump, sort_keys=True) == _json_dumps(
|
||||
{
|
||||
"type": "done",
|
||||
"deflected": False,
|
||||
"sources": [{"source": "docs", "path": "a.md", "title": "A"}],
|
||||
"related": [{"source": "docs", "path": "b.md", "title": "B"}],
|
||||
"suggestions": ["Maybe try X?"],
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
raw = _json_dumps(dump)
|
||||
assert "budget_exhausted" not in raw
|
||||
|
||||
|
||||
def test_done_event_with_budget_exhausted_carries_the_key() -> None:
|
||||
"""``budget_exhausted=True`` (a cap-reached turn) → the key is
|
||||
present and ``true``, the rest of the frame untouched; an
|
||||
explicit ``False`` behaves exactly like the default (absent)."""
|
||||
dump = ChatDoneEvent(deflected=False, sources=[], budget_exhausted=True).model_dump()
|
||||
assert dump["budget_exhausted"] is True
|
||||
assert dump["type"] == "done"
|
||||
assert dump["deflected"] is False
|
||||
assert dump["related"] == []
|
||||
assert dump["suggestions"] == []
|
||||
explicit_false = ChatDoneEvent(
|
||||
deflected=False, sources=[], budget_exhausted=False
|
||||
).model_dump()
|
||||
assert "budget_exhausted" not in explicit_false
|
||||
|
||||
|
||||
def test_done_event_budget_exhausted_and_answer_image_are_independent() -> None:
|
||||
"""Both optional keys shown on the same turn: the two omission
|
||||
rules are independent — a cap-reached turn with a shown answer
|
||||
image carries BOTH keys; the answer-image ref's own serializer
|
||||
still applies inside the frame."""
|
||||
dump = ChatDoneEvent(
|
||||
deflected=False,
|
||||
sources=[],
|
||||
answer_image=SourceRef(**_SHOWN_REF),
|
||||
budget_exhausted=True,
|
||||
).model_dump()
|
||||
assert dump["budget_exhausted"] is True
|
||||
assert dump["answer_image"] == _SHOWN_REF
|
||||
|
||||
|
||||
def test_done_event_without_budget_exhausted_parses_a_pre_phase_frame() -> None:
|
||||
"""Back-compat: a pre-phase-160 done frame (no
|
||||
``budget_exhausted`` key) parses with the default ``False`` and
|
||||
re-dumps byte-identically (PLAN §4 — the field is additive in
|
||||
both directions)."""
|
||||
old_payload = {
|
||||
"type": "done",
|
||||
"deflected": True,
|
||||
"sources": [],
|
||||
"related": [],
|
||||
"suggestions": [],
|
||||
}
|
||||
event = ChatDoneEvent(**old_payload)
|
||||
assert event.budget_exhausted is False
|
||||
assert event.model_dump() == old_payload
|
||||
|
||||
|
||||
# --- the ``ChatMessage`` boundary (the saved-chat record key) ---
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user