phase: 146_search_tool_scope
Build and Push Containers / build-and-push-app (push) Successful in 2m24s
Build and Push Containers / build-and-push-db (push) Successful in 13s

Phase 146 final verification pass complete — all green, tree untouched by me.

**Verified (final pass — both tasks already in `complete/`):**
- Scoped grep streams `{"type":"tool","name":"grep","argument":<pattern>,"path":<source/path>}`; bare grep/read/ls frames dump byte-identical (unit matrix in `test_sse_events.py` + builder pins in `test_chat_gate.py` L1364–1448)
- Tool line `🔎 Searching for <pattern> in <source/path>` (two `<code>` spans, `.grep-scope` sibling, no new emoji); bare line byte-identical (unit Node test + E2E)
- Status label "… is searching for X in Y" scoped-only; `path` persisted additively on the saved tool record and re-rendered on `/?chat=<id>` restore; pre-phase records restore byte-identically (E2E tests 2 & 3)

**Results (exact commands):**
- `uv run pytest --cov=app --cov-report=term-missing` → **3419 passed**, `app/` coverage **99%** (>90% ✓)
- `uv run pytest tests/e2e/test_search_tool_scope.py -v --no-cov` → **3 passed** in isolation ✓
- `uv run ruff check . && uv run pyright` → clean (0 errors) ✓

**Completion criteria:** all 6 ✓ (wire shape, UI line, status label, save/restore, gates, no completed-phase behavior change — full suite green includes all prior-phase pins).

**Documentation:** README.md (incl. its phase-146 section) and AGENTS.md reviewed in full against the codebase — verified consistent, no changes needed (`.env.example` ⊆ README config tables; no stale vars; phase-100 72rem rule in AGENTS.md §5 matches shipped CSS; no-CDN/smoke-DB/gpgsign claims all confirmed).

**Notable:** none — no defects found, no live-infrastructure touched, nothing committed per harness protocol.

**Next pending phase:** `147_sync_changed_only`.
This commit is contained in:
2026-10-01 12:48:04 -04:00
parent 32a3ec4f5e
commit ab3f5f685b
27 changed files with 2151 additions and 29 deletions
+63
View File
@@ -167,6 +167,69 @@ def test_tool_event_range_fields_absent_not_null_when_omitted() -> None:
assert set(dumped.keys()) == {"type", "name", "argument"}, dumped
def test_tool_frame_scoped_grep_carries_path() -> None:
"""Phase 146 (task 01): a SCOPED grep's frame carries the scope —
the re-searched document's combined ``source/path`` — as the
additive ``path`` field (the ONLY case the field is present on the
wire), AFTER ``argument`` (the model's ``pattern``)."""
frame = sse_event(
ChatToolEvent(name="grep", argument="pattern", path="srcs/doc.md").model_dump()
)
assert frame == (
'data: {"type": "tool", "name": "grep", "argument": "pattern", '
'"path": "srcs/doc.md"}\n\n'
)
assert _payload(frame) == {
"type": "tool",
"name": "grep",
"argument": "pattern",
"path": "srcs/doc.md",
}
def test_tool_frame_bare_grep_has_no_path_key() -> None:
"""Phase 146 (task 01): a bare grep's frame has NO ``path`` key at
all — ABSENT, never ``null`` (the omission pattern; a ``null`` value
would change the pre-phase wire bytes)."""
dumped = ChatToolEvent(name="grep", argument="pattern").model_dump()
assert "path" not in dumped
assert dumped == {"type": "tool", "name": "grep", "argument": "pattern"}
def test_tool_frame_non_scoped_tools_dump_the_exact_pre_phase_shape() -> None:
"""Phase 146 (task 01): every frame that is not a scoped grep —
``read`` with and without a range, ``ls`` with and without an
argument — dumps to the EXACT pre-phase dict (the equality pins:
byte-identity of the non-scoped frames is the point of the
additive-omission serializer). A non-string ``path`` never reaches
the model at all: the frame builder (``app.api.chat``) string-guards
it to ``None`` before construction (pinned at the builder level in
``tests/unit/test_chat_gate.py``)."""
assert (
ChatToolEvent(name="read", argument="S/p.md").model_dump()
== {"type": "tool", "name": "read", "argument": "S/p.md"}
)
assert (
ChatToolEvent(name="read", argument="S/p.md", offset=1500, limit=30).model_dump()
== {
"type": "tool",
"name": "read",
"argument": "S/p.md",
"offset": 1500,
"limit": 30,
}
)
assert ChatToolEvent(name="ls").model_dump() == {
"type": "tool",
"name": "ls",
"argument": None,
}
assert (
ChatToolEvent(name="ls", argument="Homelab").model_dump()
== {"type": "tool", "name": "ls", "argument": "Homelab"}
)
def test_retry_frame_serializes_exactly() -> None:
"""Phase 67: the ``retry`` frame is exactly ``{type: "retry",
attempt: int, max_attempts: int}`` — a transient status the client's