phase: 145_git_source_links
**Phase 145 final verification pass — all green, no code changes needed** - **Verified (no defects found):** resolver `origin_for_source` + `_url_basename` (scp/`.git`/trailing-slash, sanitize, ambiguity WARNING); `origin:`/`branch:` lines in all four read shapes via shared `_origin_lines`; HIGH `<tools>` origin rule (verbatim + only-byte-change pins); `DocContent.origin_url/origin_branch` (absent-never-null serializer); `renderDocument` "Original" anchor (copyable verbatim URL, `_blank`/`noopener`, branch in title); mock scripted origin-link flow (disjoint trigger — grep-verified vs all other E2E files) - **Tests:** `uv run pytest --cov=app --cov-report=term-missing` → 3402 passed, `app/` coverage **99%** (>90% gate); `uv run pytest tests/e2e/test_git_source_links.py -v --no-cov` → **4 passed** in isolation; `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings - **Completion criteria:** (1) git read results carry origin/branch, local byte-identical — ✅ (unit pins + E2E control); (2) HIGH `<tools>` rule pinned, LOW byte-identical — ✅; (3) modal AND /document.html show Original link, branch in label when pinned, nothing for local — ✅; (4) full suite green + coverage + E2E + lint/types — ✅; (5) no completed-phase behavior change (read header line 1, mock prefix, done frame/SourceRef untouched, LOW sha) — ✅ - **Extra regression check:** re-ran shared-mock E2E suites `test_read_line_ranges.py` (2 passed) and `test_document_viewer.py` (8 passed) — no cross-suite regressions from the `mock_llm.py` addition - **Documentation:** README.md reviewed in full (env table ⊇ `.env.example`, pages/routes, config defaults, scripts, 17-color palette, 300 s guard, 2 s sync poll, phase-145 provenance section matches implementation) and AGENTS.md (72rem container, no-CDN test, smoke test, gitignore, gates) — **verified consistent, no changes needed** - **Notable:** phase-116 screenshot PNG was regenerated during this phase's E2E runs (byte re-encode only, visually identical, correct no-anchor local-source meta row) — left in tree for the harness commit - **Next pending phase:** `146_search_tool_scope` (then 147, 148, 149)
This commit is contained in:
+330
-2
@@ -269,8 +269,11 @@ def test_agent_tools_names_and_parameters() -> None:
|
||||
"and `limit` (number of lines) to read exactly that "
|
||||
"part. Do not re-read a document you have already read "
|
||||
"in full — its full text is already in your prompt. "
|
||||
"Call one tool at a time — wait for this result before "
|
||||
"your next call."
|
||||
"For a document from a git source, the result also "
|
||||
"carries an `origin` line (and `branch` when pinned) — "
|
||||
"include that original source in your answer (see your "
|
||||
"tools instructions). Call one tool at a time — wait "
|
||||
"for this result before your next call."
|
||||
)
|
||||
# The byte-preserved contracts inside the rewrite, pinned as
|
||||
# substrings: the combined source/path identity, the
|
||||
@@ -293,6 +296,17 @@ def test_agent_tools_names_and_parameters() -> None:
|
||||
"Call one tool at a time — wait for this result before "
|
||||
"your next call."
|
||||
) in read["description"]
|
||||
# Phase 145 (task 02): the origin clause mirrors the TOOLS_SECTION
|
||||
# rule (the description points, the section teaches — the
|
||||
# phase-124 show_image "see the instructions" shape): a git-
|
||||
# sourced document's read result carries an `origin` line (and
|
||||
# `branch` when pinned); include that original source in the
|
||||
# answer.
|
||||
assert (
|
||||
"For a document from a git source, the result also carries "
|
||||
"an `origin` line (and `branch` when pinned) — include that "
|
||||
"original source in your answer (see your tools instructions)."
|
||||
) in read["description"]
|
||||
read_params = read["parameters"]
|
||||
assert read_params["type"] == "object"
|
||||
# Phase 131 (task 01, LOCKED A2): offset/limit are OPTIONAL —
|
||||
@@ -3649,6 +3663,320 @@ def test_unknown_tool_name_refused(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
assert llm.requests[1][1] == AGENT_TOOLS # rejected → tools stay offered
|
||||
|
||||
|
||||
# ---------- phase 145 (task 01): the origin/branch lines in the read result ----------
|
||||
|
||||
#: The fixture git row's URL (basename ``skills`` = the fixture source
|
||||
#: name) and the four origin outcomes the read-branch matrix runs:
|
||||
#: git row matched (branch NULL), git row + pinned branch, local source
|
||||
#: (no git row), two rows sharing the basename (ambiguous — the source
|
||||
#: name is ``repo`` for the last one).
|
||||
_GIT_URL = "https://github.com/owner/skills"
|
||||
_GIT_ROWS = [GitSource(url=_GIT_URL, kind="git")]
|
||||
_GIT_PINNED_ROWS = [GitSource(url=_GIT_URL, kind="git", branch="release/1.2")]
|
||||
_LOCAL_ROWS = [GitSource(url="/srv/skills", kind="local", path="/srv/skills")]
|
||||
_AMBIGUOUS_ROWS = [
|
||||
GitSource(url="https://a.example/org/repo.git", kind="git"),
|
||||
GitSource(url="git@b.example:other/repo.git", kind="git"),
|
||||
]
|
||||
|
||||
|
||||
def _git_rows_factory(rows: list[GitSource]) -> Callable[[], Session]:
|
||||
"""A db factory whose mock session answers the resolver's query with
|
||||
the git rows of *rows* (like the real ``kind = 'git'`` WHERE clause
|
||||
— the seeded local rows are filtered out, proving they are never
|
||||
candidates): the REAL ``origin_for_source`` runs in the read branch,
|
||||
no database."""
|
||||
git_rows = [row for row in rows if row.kind == "git"]
|
||||
|
||||
def _factory() -> Session:
|
||||
mock = cast("Session", MagicMock())
|
||||
mock.__enter__ = MagicMock(return_value=mock)
|
||||
mock.__exit__ = MagicMock(return_value=False)
|
||||
mock.scalars = MagicMock(return_value=MagicMock(all=MagicMock(return_value=git_rows)))
|
||||
return mock
|
||||
|
||||
return _factory
|
||||
|
||||
|
||||
def _one_read(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
doc: Document,
|
||||
rows: list[GitSource],
|
||||
arguments: dict[str, Any],
|
||||
read_max_chars: int | None = None,
|
||||
) -> str:
|
||||
"""One scripted read turn against the REAL resolver; returns the
|
||||
exact tool-result string (the successful-read bookkeeping is
|
||||
asserted — the matrix asserts the bytes)."""
|
||||
monkeypatch.setattr(agent, "find_document", lambda db, source, path: doc)
|
||||
holder = AgentHolder()
|
||||
llm = ScriptedLLM(
|
||||
[ToolCallPiece(id="c1", name="read", arguments=arguments)],
|
||||
[StreamPiece("content", "ans")],
|
||||
)
|
||||
kwargs: dict[str, Any] = {"read_max_chars": read_max_chars} if read_max_chars else {}
|
||||
asyncio.run(_run(llm, holder, _settings(**kwargs), db_factory=_git_rows_factory(rows)))
|
||||
assert holder.read_docs == [doc] and holder.tool_calls == 1
|
||||
return llm.requests[1][0][3]["content"]
|
||||
|
||||
|
||||
def test_origin_lines_plain_read_under_cap(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Shape 1 (plain read at or under the cap): the origin block rides
|
||||
IMMEDIATELY AFTER the ``date:`` line — ``Document …: / date: /
|
||||
origin: / branch: / content`` — in all four origin outcomes; the
|
||||
first line (the mock's ``_READ_RESULT_PREFIX`` contract) is
|
||||
untouched, and the local/ambiguous results are byte-identical to
|
||||
pre-phase (the ambiguity still warns)."""
|
||||
doc = _doc("skills", "docs/a.md", "A", "A-CONTENT")
|
||||
plain = {"path": "skills/docs/a.md"}
|
||||
|
||||
result = _one_read(monkeypatch, doc, _GIT_ROWS, plain)
|
||||
assert result == (
|
||||
"Document skills/docs/a.md:\n"
|
||||
"date: 2024-06-15\n"
|
||||
f"origin: {_GIT_URL}\n"
|
||||
"A-CONTENT"
|
||||
)
|
||||
# The E2E mock's header contract: first line, byte-identical.
|
||||
assert result.startswith("Document ")
|
||||
assert result.splitlines()[0] == "Document skills/docs/a.md:"
|
||||
|
||||
result = _one_read(monkeypatch, doc, _GIT_PINNED_ROWS, plain)
|
||||
assert result == (
|
||||
"Document skills/docs/a.md:\n"
|
||||
"date: 2024-06-15\n"
|
||||
f"origin: {_GIT_URL}\n"
|
||||
"branch: release/1.2\n"
|
||||
"A-CONTENT"
|
||||
)
|
||||
|
||||
result = _one_read(monkeypatch, doc, _LOCAL_ROWS, plain)
|
||||
assert result == "Document skills/docs/a.md:\ndate: 2024-06-15\nA-CONTENT"
|
||||
|
||||
repo_doc = _doc("repo", "docs/a.md", "A", "A-CONTENT")
|
||||
caplog.clear()
|
||||
result = _one_read(monkeypatch, repo_doc, _AMBIGUOUS_ROWS, {"path": "repo/docs/a.md"})
|
||||
assert result == "Document repo/docs/a.md:\ndate: 2024-06-15\nA-CONTENT"
|
||||
# The ambiguity is logged at WARNING (the agent's INFO per-turn log
|
||||
# lines are captured too — filter to the resolver's records).
|
||||
warnings = [
|
||||
r for r in caplog.records if r.levelname == "WARNING" and r.name == "app.rag.git_sources"
|
||||
]
|
||||
assert len(warnings) == 1
|
||||
assert "repo" in warnings[0].message
|
||||
|
||||
|
||||
def test_origin_lines_truncated_plain_read(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Shape 2 (plain read cut at the cap): the origin block sits at the
|
||||
same position (after ``date:``, before the cut content), the
|
||||
truncation marker + the pinned notice unchanged, the holder
|
||||
truncation tuple unchanged (the ``read_truncations`` signal is
|
||||
untouched)."""
|
||||
cap = 20
|
||||
content = "x" * (cap + 1)
|
||||
doc = _doc("skills", "big.md", "Big", content)
|
||||
plain = {"path": "skills/big.md"}
|
||||
|
||||
result = _one_read(monkeypatch, doc, _GIT_ROWS, plain, read_max_chars=cap)
|
||||
assert result == (
|
||||
"Document skills/big.md:\n"
|
||||
"date: 2024-06-15\n"
|
||||
f"origin: {_GIT_URL}\n"
|
||||
+ content[:cap]
|
||||
+ "\n"
|
||||
+ TRUNCATION_MARKER
|
||||
+ "\n"
|
||||
+ READ_TRUNCATION_NOTICE.format(shown=cap, total=len(content))
|
||||
)
|
||||
|
||||
result = _one_read(monkeypatch, doc, _GIT_PINNED_ROWS, plain, read_max_chars=cap)
|
||||
assert result == (
|
||||
"Document skills/big.md:\n"
|
||||
"date: 2024-06-15\n"
|
||||
f"origin: {_GIT_URL}\n"
|
||||
"branch: release/1.2\n"
|
||||
+ content[:cap]
|
||||
+ "\n"
|
||||
+ TRUNCATION_MARKER
|
||||
+ "\n"
|
||||
+ READ_TRUNCATION_NOTICE.format(shown=cap, total=len(content))
|
||||
)
|
||||
|
||||
result = _one_read(monkeypatch, doc, _LOCAL_ROWS, plain, read_max_chars=cap)
|
||||
assert result == (
|
||||
"Document skills/big.md:\n"
|
||||
"date: 2024-06-15\n"
|
||||
+ content[:cap]
|
||||
+ "\n"
|
||||
+ TRUNCATION_MARKER
|
||||
+ "\n"
|
||||
+ READ_TRUNCATION_NOTICE.format(shown=cap, total=len(content))
|
||||
)
|
||||
|
||||
repo_doc = _doc("repo", "big.md", "Big", content)
|
||||
result = _one_read(
|
||||
monkeypatch, repo_doc, _AMBIGUOUS_ROWS, {"path": "repo/big.md"}, read_max_chars=cap
|
||||
)
|
||||
assert result == (
|
||||
"Document repo/big.md:\n"
|
||||
"date: 2024-06-15\n"
|
||||
+ content[:cap]
|
||||
+ "\n"
|
||||
+ TRUNCATION_MARKER
|
||||
+ "\n"
|
||||
+ READ_TRUNCATION_NOTICE.format(shown=cap, total=len(content))
|
||||
)
|
||||
|
||||
|
||||
def test_origin_lines_range_read_under_cap(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Shape 3 (range read at or under the cap): the origin block rides
|
||||
between the ``date:`` line and the ``lines:`` header (the same
|
||||
position as in the plain shapes); the local/ambiguous results stay
|
||||
byte-identical to pre-phase."""
|
||||
doc = _doc("skills", "b.md", "B", "one\ntwo\nthree\nfour\nfive")
|
||||
ranged = {"path": "skills/b.md", "offset": 2, "limit": 2}
|
||||
|
||||
result = _one_read(monkeypatch, doc, _GIT_ROWS, ranged)
|
||||
assert result == (
|
||||
"Document skills/b.md:\n"
|
||||
"date: 2024-06-15\n"
|
||||
f"origin: {_GIT_URL}\n"
|
||||
"lines: 2-3 of 5\n"
|
||||
"two\nthree"
|
||||
)
|
||||
|
||||
result = _one_read(monkeypatch, doc, _GIT_PINNED_ROWS, ranged)
|
||||
assert result == (
|
||||
"Document skills/b.md:\n"
|
||||
"date: 2024-06-15\n"
|
||||
f"origin: {_GIT_URL}\n"
|
||||
"branch: release/1.2\n"
|
||||
"lines: 2-3 of 5\n"
|
||||
"two\nthree"
|
||||
)
|
||||
|
||||
result = _one_read(monkeypatch, doc, _LOCAL_ROWS, ranged)
|
||||
assert result == "Document skills/b.md:\ndate: 2024-06-15\nlines: 2-3 of 5\ntwo\nthree"
|
||||
|
||||
repo_doc = _doc("repo", "b.md", "B", "one\ntwo\nthree\nfour\nfive")
|
||||
caplog.clear()
|
||||
result = _one_read(
|
||||
monkeypatch, repo_doc, _AMBIGUOUS_ROWS, {"path": "repo/b.md", "offset": 2, "limit": 2}
|
||||
)
|
||||
assert result == "Document repo/b.md:\ndate: 2024-06-15\nlines: 2-3 of 5\ntwo\nthree"
|
||||
# The ambiguity is logged at WARNING (the agent's INFO per-turn log
|
||||
# lines are captured too — filter to the resolver's records).
|
||||
warnings = [
|
||||
r for r in caplog.records if r.levelname == "WARNING" and r.name == "app.rag.git_sources"
|
||||
]
|
||||
assert len(warnings) == 1
|
||||
assert "repo" in warnings[0].message
|
||||
|
||||
|
||||
def test_origin_lines_truncated_range_read(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Shape 4 (range read whose SLICE exceeds the cap): the origin block
|
||||
at the same position; the cap on the slice + the shared marker + the
|
||||
pinned notice (``{total}`` = the SLICE's length) all unchanged."""
|
||||
cap = 40
|
||||
doc = _doc("skills", "c.md", "C", "a" * 30 + "\n" + "b" * 30 + "\n" + "c" * 30)
|
||||
slice_text = "a" * 30 + "\n" + "b" * 30 # the requested lines 1-2 (61 chars)
|
||||
ranged = {"path": "skills/c.md", "offset": 1, "limit": 2}
|
||||
|
||||
result = _one_read(monkeypatch, doc, _GIT_ROWS, ranged, read_max_chars=cap)
|
||||
assert result == (
|
||||
"Document skills/c.md:\n"
|
||||
"date: 2024-06-15\n"
|
||||
f"origin: {_GIT_URL}\n"
|
||||
"lines: 1-2 of 3\n"
|
||||
+ slice_text[:cap]
|
||||
+ "\n"
|
||||
+ TRUNCATION_MARKER
|
||||
+ "\n"
|
||||
+ READ_TRUNCATION_NOTICE.format(shown=cap, total=len(slice_text))
|
||||
)
|
||||
|
||||
result = _one_read(monkeypatch, doc, _GIT_PINNED_ROWS, ranged, read_max_chars=cap)
|
||||
assert result == (
|
||||
"Document skills/c.md:\n"
|
||||
"date: 2024-06-15\n"
|
||||
f"origin: {_GIT_URL}\n"
|
||||
"branch: release/1.2\n"
|
||||
"lines: 1-2 of 3\n"
|
||||
+ slice_text[:cap]
|
||||
+ "\n"
|
||||
+ TRUNCATION_MARKER
|
||||
+ "\n"
|
||||
+ READ_TRUNCATION_NOTICE.format(shown=cap, total=len(slice_text))
|
||||
)
|
||||
|
||||
result = _one_read(monkeypatch, doc, _LOCAL_ROWS, ranged, read_max_chars=cap)
|
||||
assert result == (
|
||||
"Document skills/c.md:\n"
|
||||
"date: 2024-06-15\n"
|
||||
"lines: 1-2 of 3\n"
|
||||
+ slice_text[:cap]
|
||||
+ "\n"
|
||||
+ TRUNCATION_MARKER
|
||||
+ "\n"
|
||||
+ READ_TRUNCATION_NOTICE.format(shown=cap, total=len(slice_text))
|
||||
)
|
||||
|
||||
repo_doc = _doc("repo", "c.md", "C", "a" * 30 + "\n" + "b" * 30 + "\n" + "c" * 30)
|
||||
result = _one_read(
|
||||
monkeypatch, repo_doc, _AMBIGUOUS_ROWS, {"path": "repo/c.md", "offset": 1, "limit": 2},
|
||||
read_max_chars=cap,
|
||||
)
|
||||
assert result == (
|
||||
"Document repo/c.md:\n"
|
||||
"date: 2024-06-15\n"
|
||||
"lines: 1-2 of 3\n"
|
||||
+ slice_text[:cap]
|
||||
+ "\n"
|
||||
+ TRUNCATION_MARKER
|
||||
+ "\n"
|
||||
+ READ_TRUNCATION_NOTICE.format(shown=cap, total=len(slice_text))
|
||||
)
|
||||
|
||||
|
||||
def test_origin_lines_image_doc_keeps_marker_position(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The image-doc shape keeps its phase-122 pin: the marker stays
|
||||
IMMEDIATELY ABOVE the content, and the origin lines ride ABOVE the
|
||||
marker (``Document …: / date: / origin: / branch: / marker /
|
||||
content``); a local image doc is byte-identical to pre-phase."""
|
||||
doc = _doc("skills", "img.png", "Img", "IMG-DESC")
|
||||
doc.is_image = True
|
||||
plain = {"path": "skills/img.png"}
|
||||
|
||||
result = _one_read(monkeypatch, doc, _GIT_PINNED_ROWS, plain)
|
||||
assert result == (
|
||||
"Document skills/img.png:\n"
|
||||
"date: 2024-06-15\n"
|
||||
f"origin: {_GIT_URL}\n"
|
||||
"branch: release/1.2\n"
|
||||
f"{agent.IMAGE_DOC_MARKER}\n"
|
||||
"IMG-DESC"
|
||||
)
|
||||
|
||||
doc = _doc("skills", "img.png", "Img", "IMG-DESC")
|
||||
doc.is_image = True
|
||||
result = _one_read(monkeypatch, doc, _LOCAL_ROWS, plain)
|
||||
assert result == (
|
||||
"Document skills/img.png:\ndate: 2024-06-15\n"
|
||||
f"{agent.IMAGE_DOC_MARKER}\n"
|
||||
"IMG-DESC"
|
||||
)
|
||||
|
||||
|
||||
# ---------- grep (the phase-68 A5 contract under the new name) ----------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user