"""The auto-inject query carries the conversation beside a thin prompt (#4364). The notes arm used to retrieve against the operator's typed words alone, and a mid-session follow-up — "yes do that", "now fix the filter" — names nothing a record can match. The hook now reads the tail of the last assistant reply out of the transcript and sends it as `ctx`; the server appends it to a SHORT prompt only. Two halves, pinned separately: what the server builds, and what the hook extracts. """ from __future__ import annotations import json import subprocess from pathlib import Path from scribe.services.plugin_context import ( _AUTOINJECT_CONTEXT_MAX, _AUTOINJECT_CONTEXT_PROMPT_MAX, _autoinject_query, ) from tests.helpers import need_tools DEFS = Path(__file__).resolve().parents[1] / "plugin" / "hooks" / "scribe_defs.sh" # --- the query the server builds --------------------------------------------- def test_a_short_prompt_is_followed_by_the_context(): q = _autoinject_query("yes do that", "I'll move the library filters into the side column.") assert q.startswith("yes do that\n\n") assert "library filters" in q def test_a_prompt_that_says_what_it_is_about_is_left_alone(): long_prompt = "x" * (_AUTOINJECT_CONTEXT_PROMPT_MAX + 1) assert _autoinject_query(long_prompt, "anything at all") == long_prompt def test_no_context_is_the_prompt_unchanged(): assert _autoinject_query("yes do that", "") == "yes do that" assert _autoinject_query("yes do that", " \n ") == "yes do that" def test_context_is_cut_from_the_end_of_the_reply(): # The reply's close is where it says what it did and what is next — the # part the operator's follow-up is answering. ctx = "HEAD " + "m" * 2000 + " TAIL" q = _autoinject_query("ok", ctx) tail = q.split("\n\n", 1)[1] assert len(tail) == _AUTOINJECT_CONTEXT_MAX assert tail.endswith("TAIL") and "HEAD" not in tail # --- what the hook extracts -------------------------------------------------- def _rec(role: str, blocks: list[dict], sidechain: bool = False) -> str: return json.dumps({ "type": role, "isSidechain": sidechain, "message": {"role": role, "content": blocks}, }, separators=(",", ":")) def recent(tmp_path: Path, lines: list[str]) -> str: need_tools("bash", "awk", "grep", "tail") t = tmp_path / "t.jsonl" t.write_text("\n".join(lines) + "\n") r = subprocess.run( ["bash", "-c", f'set -uo pipefail\n. "{DEFS}"\nscribe_recent_context "{t}"'], capture_output=True, ) assert r.returncode == 0, r.stderr.decode() return r.stdout.decode().strip() def test_the_hook_reads_the_last_assistant_text(tmp_path): out = recent(tmp_path, [ _rec("assistant", [{"type": "text", "text": "an older reply"}]), _rec("assistant", [{"type": "tool_use", "id": "t1", "name": "Bash", "input": {}}]), _rec("assistant", [{"type": "text", "text": "Shall I file it as an issue?\nNext: the hook."}]), ]) assert out == "Shall I file it as an issue? Next: the hook." def test_a_subagent_reply_is_not_this_session(tmp_path): out = recent(tmp_path, [ _rec("assistant", [{"type": "text", "text": "the session's reply"}]), _rec("assistant", [{"type": "text", "text": "a subagent's report"}], sidechain=True), ]) assert out == "the session's reply" def test_the_hook_caps_what_it_sends(tmp_path): out = recent(tmp_path, [_rec("assistant", [{"type": "text", "text": "a" * 5000 + "END"}])]) assert len(out) == 600 and out.endswith("END") def test_no_transcript_is_silence_not_failure(tmp_path): assert recent(tmp_path, []) == ""