phase: 145_git_source_links
Build and Push Containers / build-and-push-app (push) Successful in 2m23s
Build and Push Containers / build-and-push-db (push) Successful in 13s

**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:
2026-10-01 11:13:43 -04:00
parent 486ceb4019
commit 32a3ec4f5e
37 changed files with 2856 additions and 22 deletions
+330 -2
View File
@@ -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) ----------