feat(ledger): a repo binding names the branch its ledger follows — bind_repo(ref=) (#2873, milestone 294)
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Failing after 25s
CI & Build / TypeScript typecheck (push) Canceled after 30s
CI & Build / Python tests (push) Canceled after 30s
CI & Build / Build & push image (push) Canceled after 0s
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Failing after 25s
CI & Build / TypeScript typecheck (push) Canceled after 30s
CI & Build / Python tests (push) Canceled after 30s
CI & Build / Build & push image (push) Canceled after 0s
Project 2 is bound to main, so every consolidation of the 2026-08 audit was
invisible to the ledger until the dev→main merge; the operator works on dev
(rule 1). repo_bindings.ref (migration 0082, nullable) is the branch the
coverage refresh reads; NULL keeps the forge default branch. set_binding takes
ref (name sets, "" clears, None leaves standing); bindings_for_project feeds
the refresh; bind_repo exposes ref ("-" clears). to_dict carries it.
Operator decision on #2873 (2026-08-21): per-binding ref, chosen at bind time,
default the repo default branch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -168,6 +168,13 @@ def test_coverage_line_is_evidence_carrying_and_labeled_estimate():
|
||||
assert "internal/api, web/src/components" in line
|
||||
|
||||
|
||||
def test_bind_repo_tool_takes_a_ref():
|
||||
"""#2873: the binding names the branch the ledger follows."""
|
||||
from scribe.mcp.server import build_mcp_server
|
||||
tool = build_mcp_server()._tool_manager.get_tool("bind_repo")
|
||||
assert "ref" in tool.parameters.get("properties", {})
|
||||
|
||||
|
||||
def test_coverage_routes_are_registered():
|
||||
from scribe.app import create_app
|
||||
|
||||
@@ -188,7 +195,8 @@ def _forge(tar_bytes: bytes):
|
||||
path = request.url.path
|
||||
if path == "/api/v1/repos/alice/widget":
|
||||
return httpx.Response(200, json={"default_branch": "main"})
|
||||
if path == "/api/v1/repos/alice/widget/archive/main.tar.gz":
|
||||
if path in ("/api/v1/repos/alice/widget/archive/main.tar.gz",
|
||||
"/api/v1/repos/alice/widget/archive/dev.tar.gz"):
|
||||
return httpx.Response(200, content=tar_bytes)
|
||||
return httpx.Response(404, json={"message": "not found"})
|
||||
|
||||
@@ -533,3 +541,23 @@ def test_scoped_definitions_are_vue_script_setup_and_scoped_style_only():
|
||||
extract_definitions(".card {\n x: 1;\n}\n")) == set()
|
||||
assert scoped_definitions("src/a.py", "def load():\n pass\n", extract_definitions("def load():\n pass\n")) == set()
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
async def test_binding_ref_is_the_branch_the_ledger_follows(seeded):
|
||||
"""#2873: a binding that names a ref is read at that ref (not the forge's
|
||||
default branch); "" clears it; None on a re-bind leaves it standing."""
|
||||
from scribe.services.repo_bindings import bindings_for_project, set_binding
|
||||
uid, pid = seeded["uid"], seeded["pid"]
|
||||
b = await set_binding(uid, "https://git.example.com/alice/widget.git", pid, "dev")
|
||||
assert b.ref == "dev"
|
||||
coverage = await compute_coverage(uid, pid, selector=_selector(_tarball(TREE)))
|
||||
assert coverage["repos"][0]["ref"] == "dev"
|
||||
# A re-bind without a ref keeps it; "" clears it back to the default branch.
|
||||
b = await set_binding(uid, "https://git.example.com/alice/widget.git", pid)
|
||||
assert b.ref == "dev"
|
||||
b = await set_binding(uid, "https://git.example.com/alice/widget.git", pid, "")
|
||||
assert b.ref is None
|
||||
assert [x.ref for x in await bindings_for_project(uid, pid)] == [None]
|
||||
coverage = await compute_coverage(uid, pid, selector=_selector(_tarball(TREE)))
|
||||
assert coverage["repos"][0]["ref"] == "main"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user