Remediates the REQUEST_CHANGES verdict (review 658) at head2d5d5c9d. B1 — refs/remotes/<remote>/HEAD is a stale local cache, not authority. The previous derivation read that symref as "git's own record of the remote default branch". It is a cache written once at clone time; an ordinary fetch never refreshes it, and only an explicit `git remote set-head` updates it. On the real target this issue exists to unblock, the cache still named `main` while the checkout tracked and sat exactly on `dev`, so the guard compared HEAD a37ac427c18b against MDCPS/main 9a84325a1b68 and blocked a checkout that was not behind anything. The resolver now derives, in order: 1. the identity remote, exact case preserved; 2. the checkout's own configured upstream — branch.<current>.remote plus branch.<current>.merge — accepted only when it names that remote and its remote-tracking ref actually exists; 3. otherwise exactly one present master/main/dev remote-tracking ref. The cached symref is demoted to an observation. It is still read and reported as cached_remote_head_branch / cached_remote_head_conflicts, and it is named in refusal text so an operator can see the misleading signal, but it never decides the branch and never breaks a tie between ambiguous candidates. Requiring the tracking ref to exist also makes `proven` honest: every proven target now names a ref that resolves. Verified read-only against /Users/jasonwalker/Development/weekly-briefings: MDCPS/dev, source configured_branch_upstream, cached_remote_head_conflicts true, checkout not stale. PRGS is unchanged — prgs/master, identical SHA. B2 — an inferred remote must not be laundered into explicit caller intent. assess_target_repository_parity resolved the identity remote itself and passed it back into resolve_target_base_ref, which reads a caller-supplied remote as "the caller already disambiguated" and skips its ambiguity gate. On a target whose remotes claim different repositories the gate refused while the report named a different repository with stale=false and no reasons. The parameter is renamed `explicit_remote` through the resolver and both root_checkout_guard entry points so the two meanings cannot be confused, and the reporting path no longer supplies one. Identity resolution for reporting moves to the new ambiguity-aware assess_identity_remote, so an ambiguous target now yields a null slug, no tracking ref, and the same reason_code the gate emits. resolve_identity_remote / repository_identity_slug keep their first-wins behaviour for the #706/#973 canonical-root validation path, which compares against an independently trusted expected slug and needs no ambiguity verdict. Nothing fetches, sets a remote HEAD, writes a ref, adds a remote, invents a branch, or changes any repository's default branch. A test snapshots refs, remotes, local config, HEAD, branch, working-tree status, and the cached symref across every resolver entry point and asserts all are unchanged. Tests: tests/test_issue_983_cross_repo_base_ref.py rewritten to 37 tests. The principal MDCPS/dev fixture now reproduces the real defect — upstream dev, cached refs/remotes/MDCPS/HEAD -> main, both refs present at different commits — rather than manufacturing the cache state the real checkout does not have. Added: cache-alone never proves a target, cache never breaks a tie, gate and report agree on ambiguous remote and ambiguous branch, explicit disambiguation stays distinct from inferred identity, no production caller passes explicit_remote, and the read-only proof above. Focused suite 37 passed. Nineteen affected suites 441 passed, 167 subtests passed. Full suite 28 failed, 6204 passed, 6 skipped, 1106 subtests passed; clean baseline at the same base commit108cbfa28 failed, 6166 passed, 6 skipped, 1106 subtests passed. Sorted FAILED lists are byte-identical (sha1 092dae4bc8c4e77d14504d90690d50e0fcd2f637) — zero introduced failures. Refs #983 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
693 lines
31 KiB
Python
693 lines
31 KiB
Python
"""Regression tests for Issue #983: derived target base ref for cross-repository checkouts.
|
|
|
|
The mutation guard previously assumed ``prgs/master`` and the parity report
|
|
assumed ``origin/master``. Any repository using neither — for example remote
|
|
``MDCPS`` on integration branch ``dev`` — could not prove base equivalence, so
|
|
every gated mutation failed closed with no reachable remedy.
|
|
|
|
Two further defects were found by review at head ``2d5d5c9d`` and are covered
|
|
here:
|
|
|
|
* **B1** — the first fix derived the integration branch from
|
|
``refs/remotes/<remote>/HEAD``. That symref is a *local cache* written at clone
|
|
time and never refreshed by fetch, so on the real Weekly Briefings target it
|
|
still named ``main`` while the checkout tracked and sat exactly on ``dev``. The
|
|
authoritative signal is the checkout's own configured upstream. The fixtures
|
|
below therefore reproduce the **disagreement**: the cache says ``main``, the
|
|
configured upstream says ``dev``, and ``dev`` must win.
|
|
* **B2** — the parity report passed its internally inferred identity remote back
|
|
into the resolver, which reads a caller-supplied remote as "the caller already
|
|
disambiguated" and skips its ambiguity gate. Gating and reporting could then
|
|
evaluate different repositories. An inferred remote is now never laundered into
|
|
explicit caller intent, and ambiguity fails closed on both sides.
|
|
|
|
These tests build hermetic git repositories on disk (no network, no fetch) and
|
|
assert the derived target end to end: identity remote, integration branch,
|
|
tracking ref, fail-closed refusals, and agreement between the mutation guard and
|
|
the parity report.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
import anti_stomp_preflight
|
|
import canonical_repository_root as crr
|
|
import master_parity_gate
|
|
import root_checkout_guard
|
|
|
|
MDCPS_URL = "https://gitea.example.net/MDCPS/WeeklyBriefings-Meta.git"
|
|
MDCPS_SLUG = "MDCPS/WeeklyBriefings-Meta"
|
|
PRGS_URL = "https://gitea.prgs.cc/Scaled-Tech-Consulting/Gitea-Tools.git"
|
|
PRGS_SLUG = "Scaled-Tech-Consulting/Gitea-Tools"
|
|
|
|
|
|
def _git(root: str, *args: str) -> str:
|
|
res = subprocess.run(
|
|
["git", "-C", root, *args],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
return (res.stdout or "").strip()
|
|
|
|
|
|
def _make_repo(root: str, *, remote: str | None, url: str | None) -> str:
|
|
"""Initialise a repository with one commit and an optional named remote."""
|
|
os.makedirs(root, exist_ok=True)
|
|
_git(root, "init", "--quiet")
|
|
_git(root, "config", "user.email", "[email protected]")
|
|
_git(root, "config", "user.name", "Issue983 Test")
|
|
_git(root, "config", "commit.gpgsign", "false")
|
|
with open(os.path.join(root, "seed.txt"), "w", encoding="utf-8") as fh:
|
|
fh.write("seed\n")
|
|
_git(root, "add", "seed.txt")
|
|
_git(root, "commit", "--quiet", "-m", "seed")
|
|
if remote and url:
|
|
_git(root, "remote", "add", remote, url)
|
|
return _git(root, "rev-parse", "HEAD")
|
|
|
|
|
|
def _set_remote_branch(root: str, remote: str, branch: str, sha: str) -> None:
|
|
"""Create refs/remotes/<remote>/<branch> without contacting a network."""
|
|
_git(root, "update-ref", f"refs/remotes/{remote}/{branch}", sha)
|
|
|
|
|
|
def _set_remote_head(root: str, remote: str, branch: str) -> None:
|
|
"""Write the *cached* refs/remotes/<remote>/HEAD symref.
|
|
|
|
This is the signal B1 proved untrustworthy. Fixtures use it to reproduce a
|
|
stale cache, never to manufacture the answer under test.
|
|
"""
|
|
_git(
|
|
root,
|
|
"symbolic-ref",
|
|
f"refs/remotes/{remote}/HEAD",
|
|
f"refs/remotes/{remote}/{branch}",
|
|
)
|
|
|
|
|
|
def _set_upstream(root: str, remote: str, branch: str) -> None:
|
|
"""Configure the current branch's upstream, exactly as git tracking does.
|
|
|
|
Writes ``branch.<current>.remote`` / ``branch.<current>.merge`` directly
|
|
rather than via ``--set-upstream-to`` so no ref is required to pre-exist and
|
|
no network is touched.
|
|
"""
|
|
current = _git(root, "symbolic-ref", "--short", "HEAD")
|
|
_git(root, "config", f"branch.{current}.remote", remote)
|
|
_git(root, "config", f"branch.{current}.merge", f"refs/heads/{branch}")
|
|
|
|
|
|
def _checkout_new_branch(root: str, branch: str) -> None:
|
|
_git(root, "checkout", "--quiet", "-b", branch)
|
|
|
|
|
|
def _advance(root: str, message: str) -> str:
|
|
with open(os.path.join(root, "seed.txt"), "a", encoding="utf-8") as fh:
|
|
fh.write(message + "\n")
|
|
_git(root, "add", "seed.txt")
|
|
_git(root, "commit", "--quiet", "-m", message)
|
|
return _git(root, "rev-parse", "HEAD")
|
|
|
|
|
|
class _RepoCase(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self._tmp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self._tmp.cleanup)
|
|
self.root = os.path.join(self._tmp.name, "repo")
|
|
|
|
def _weekly_briefings_shape(self) -> str:
|
|
"""The real Weekly Briefings target, including its stale cache.
|
|
|
|
Remote ``MDCPS``; checked out on ``dev``; upstream configured to
|
|
``MDCPS/dev``; both ``dev`` and ``main`` present as tracking refs; and
|
|
``refs/remotes/MDCPS/HEAD`` still cached at ``main`` from clone time.
|
|
"""
|
|
head = _make_repo(self.root, remote="MDCPS", url=MDCPS_URL)
|
|
_checkout_new_branch(self.root, "dev")
|
|
_set_remote_branch(self.root, "MDCPS", "dev", head)
|
|
stale = _advance(self.root, "main diverged long ago")
|
|
_set_remote_branch(self.root, "MDCPS", "main", stale)
|
|
_git(self.root, "reset", "--hard", "--quiet", head)
|
|
_set_remote_head(self.root, "MDCPS", "main") # stale clone-time cache
|
|
_set_upstream(self.root, "MDCPS", "dev") # authoritative
|
|
return head
|
|
|
|
|
|
class TestPrgsBehaviourPreserved(_RepoCase):
|
|
"""Required coverage 1: PRGS prgs/master compatibility."""
|
|
|
|
def _prgs(self) -> str:
|
|
head = _make_repo(self.root, remote="prgs", url=PRGS_URL)
|
|
_set_remote_branch(self.root, "prgs", "master", head)
|
|
_set_remote_head(self.root, "prgs", "master")
|
|
_set_upstream(self.root, "prgs", "master")
|
|
return head
|
|
|
|
def test_prgs_master_resolves_unchanged(self):
|
|
head = self._prgs()
|
|
|
|
got = crr.resolve_target_base_ref(self.root)
|
|
self.assertTrue(got["proven"], got["reasons"])
|
|
self.assertEqual(got["remote"], "prgs")
|
|
self.assertEqual(got["branch"], "master")
|
|
self.assertEqual(got["tracking_ref"], "refs/remotes/prgs/master")
|
|
self.assertEqual(got["repository_slug"], PRGS_SLUG)
|
|
self.assertEqual(got["source"], crr.BASE_REF_SOURCE_CONFIGURED_UPSTREAM)
|
|
# Cache and upstream agree here, which is the ordinary PRGS state.
|
|
self.assertFalse(got["cached_remote_head_conflicts"])
|
|
self.assertEqual(root_checkout_guard.resolve_remote_master_sha(self.root), head)
|
|
|
|
def test_prgs_resolves_without_a_configured_upstream(self):
|
|
"""A PRGS checkout with no tracking config still resolves master."""
|
|
head = _make_repo(self.root, remote="prgs", url=PRGS_URL)
|
|
_set_remote_branch(self.root, "prgs", "master", head)
|
|
|
|
got = crr.resolve_target_base_ref(self.root)
|
|
self.assertTrue(got["proven"], got["reasons"])
|
|
self.assertEqual(got["tracking_ref"], "refs/remotes/prgs/master")
|
|
self.assertEqual(got["source"], crr.BASE_REF_SOURCE_UNIQUE_CANDIDATE)
|
|
|
|
def test_legacy_explicit_remote_refs_path_is_untouched(self):
|
|
"""An explicit remote_refs override still short-circuits derivation."""
|
|
head = _make_repo(self.root, remote="prgs", url=PRGS_URL)
|
|
_set_remote_branch(self.root, "prgs", "master", head)
|
|
|
|
state = root_checkout_guard.resolve_remote_master_ref_state(
|
|
self.root, remote_refs=root_checkout_guard.REMOTE_MASTER_REFS
|
|
)
|
|
self.assertEqual(state["sha"], head)
|
|
self.assertEqual(state["source"], "explicit_remote_refs")
|
|
|
|
|
|
class TestStaleCachedRemoteHead(_RepoCase):
|
|
"""Required coverage 3: configured upstream MDCPS/dev vs stale cache -> main.
|
|
|
|
This is B1. The fixture deliberately does **not** point
|
|
``refs/remotes/MDCPS/HEAD`` at ``dev``; it reproduces the disagreement that
|
|
was live on ``/Users/jasonwalker/Development/weekly-briefings``.
|
|
"""
|
|
|
|
def test_configured_upstream_beats_stale_cached_remote_head(self):
|
|
head = self._weekly_briefings_shape()
|
|
|
|
# Precondition: the fixture really is in the defective state.
|
|
self.assertEqual(
|
|
_git(self.root, "symbolic-ref", "refs/remotes/MDCPS/HEAD"),
|
|
"refs/remotes/MDCPS/main",
|
|
)
|
|
self.assertEqual(_git(self.root, "config", "--get", "branch.dev.merge"), "refs/heads/dev")
|
|
self.assertNotEqual(
|
|
_git(self.root, "rev-parse", "refs/remotes/MDCPS/main"),
|
|
_git(self.root, "rev-parse", "refs/remotes/MDCPS/dev"),
|
|
)
|
|
|
|
got = crr.resolve_target_base_ref(self.root)
|
|
self.assertTrue(got["proven"], got["reasons"])
|
|
self.assertEqual(got["branch"], "dev")
|
|
self.assertEqual(got["tracking_ref"], "refs/remotes/MDCPS/dev")
|
|
self.assertEqual(got["source"], crr.BASE_REF_SOURCE_CONFIGURED_UPSTREAM)
|
|
# The stale cache is reported, never obeyed.
|
|
self.assertEqual(got["cached_remote_head_branch"], "main")
|
|
self.assertTrue(got["cached_remote_head_conflicts"])
|
|
self.assertEqual(root_checkout_guard.resolve_remote_master_sha(self.root), head)
|
|
|
|
def test_checkout_on_its_integration_tip_is_not_blocked(self):
|
|
"""The live symptom: a checkout exactly on its tip was reported stale."""
|
|
head = self._weekly_briefings_shape()
|
|
|
|
state = root_checkout_guard.resolve_remote_master_ref_state(self.root)
|
|
self.assertEqual(state["sha"], head)
|
|
self.assertTrue(state["cached_remote_head_conflicts"])
|
|
|
|
assessment = root_checkout_guard.assess_root_checkout_guard(
|
|
workspace_path=self.root,
|
|
canonical_repo_root=self.root,
|
|
current_branch="dev",
|
|
head_sha=head,
|
|
porcelain_status="",
|
|
remote_master_sha=state["sha"],
|
|
remote_master_ref=state["ref"],
|
|
)
|
|
self.assertTrue(assessment["proven"], assessment["reasons"])
|
|
|
|
report = master_parity_gate.assess_target_repository_parity(
|
|
canonical_root=self.root, source="test"
|
|
)
|
|
self.assertFalse(report["stale"])
|
|
self.assertEqual(report["base_branch"], "dev")
|
|
self.assertEqual(report["reasons"], [])
|
|
|
|
def test_cached_remote_head_alone_never_proves_a_target(self):
|
|
"""With no configured upstream, the cache cannot supply the branch."""
|
|
head = _make_repo(self.root, remote="MDCPS", url=MDCPS_URL)
|
|
# Only a non-candidate branch exists, and only the cache names it.
|
|
_set_remote_branch(self.root, "MDCPS", "trunk", head)
|
|
_set_remote_head(self.root, "MDCPS", "trunk")
|
|
|
|
got = crr.resolve_target_base_ref(self.root)
|
|
self.assertFalse(got["proven"])
|
|
self.assertEqual(got["reason_code"], crr.DENY_NO_BASE_BRANCH)
|
|
self.assertEqual(got["tracking_refs"], ())
|
|
self.assertEqual(got["cached_remote_head_branch"], "trunk")
|
|
# The refusal names the misleading signal so an operator is not sent
|
|
# chasing a ref that looks authoritative.
|
|
self.assertIn("trunk", " ".join(got["reasons"]))
|
|
|
|
def test_cached_remote_head_never_breaks_a_tie(self):
|
|
"""Two candidates, no upstream: the cache must not decide."""
|
|
head = _make_repo(self.root, remote="MDCPS", url=MDCPS_URL)
|
|
_set_remote_branch(self.root, "MDCPS", "dev", head)
|
|
_set_remote_branch(self.root, "MDCPS", "main", head)
|
|
_set_remote_head(self.root, "MDCPS", "dev")
|
|
|
|
got = crr.resolve_target_base_ref(self.root)
|
|
self.assertFalse(got["proven"])
|
|
self.assertEqual(got["reason_code"], crr.DENY_AMBIGUOUS_BASE_BRANCH)
|
|
self.assertIsNone(root_checkout_guard.resolve_remote_master_sha(self.root))
|
|
|
|
def test_no_proven_source_is_the_remote_head_cache(self):
|
|
"""Structural guard: the cache is not in the set of proving sources."""
|
|
sources = {
|
|
crr.BASE_REF_SOURCE_CONFIGURED_UPSTREAM,
|
|
crr.BASE_REF_SOURCE_UNIQUE_CANDIDATE,
|
|
}
|
|
self.assertNotIn("remote_head_symref", sources)
|
|
self.assertFalse(hasattr(crr, "BASE_REF_SOURCE_REMOTE_HEAD"))
|
|
|
|
|
|
class TestCrossRepositoryTarget(_RepoCase):
|
|
"""Required coverage 2/4/5: MDCPS/dev, no origin remote, exact case."""
|
|
|
|
def test_mdcps_dev_resolves(self):
|
|
head = self._weekly_briefings_shape()
|
|
got = crr.resolve_target_base_ref(self.root)
|
|
self.assertTrue(got["proven"], got["reasons"])
|
|
self.assertEqual(got["remote"], "MDCPS")
|
|
self.assertEqual(got["branch"], "dev")
|
|
self.assertEqual(got["tracking_ref"], "refs/remotes/MDCPS/dev")
|
|
self.assertEqual(got["repository_slug"], MDCPS_SLUG)
|
|
self.assertEqual(root_checkout_guard.resolve_remote_master_sha(self.root), head)
|
|
|
|
def test_no_remote_named_origin(self):
|
|
self._weekly_briefings_shape()
|
|
self.assertEqual(_git(self.root, "remote"), "MDCPS")
|
|
got = crr.resolve_target_base_ref(self.root)
|
|
self.assertTrue(got["proven"], got["reasons"])
|
|
self.assertNotIn("origin", got["tracking_ref"])
|
|
|
|
def test_remote_name_case_is_preserved_exactly(self):
|
|
self._weekly_briefings_shape()
|
|
got = crr.resolve_target_base_ref(self.root)
|
|
self.assertEqual(got["remote"], "MDCPS")
|
|
self.assertNotEqual(got["remote"], "mdcps")
|
|
# The tracking ref must address the real ref, which is case-sensitive.
|
|
self.assertEqual(got["tracking_ref"], "refs/remotes/MDCPS/dev")
|
|
self.assertTrue(
|
|
_git(self.root, "rev-parse", "--verify", got["tracking_ref"]),
|
|
"case-preserved tracking ref must resolve",
|
|
)
|
|
|
|
def test_lowercase_candidate_never_supplies_the_remote_name(self):
|
|
"""Guards against silently case-folding MDCPS to the candidate 'mdcps'.
|
|
|
|
``_IDENTITY_REMOTE_CANDIDATES`` contains a lowercase ``mdcps`` entry and
|
|
is probed *before* the repository's own remote listing. Git remote names
|
|
live in case-sensitive config subsections on every platform, so the
|
|
lowercase probe cannot resolve and the exact-case name must arrive from
|
|
``git remote``. Asserted through config rather than ref lookup because a
|
|
case-insensitive filesystem (macOS) resolves loose refs either way, which
|
|
would make a ref-based assertion test the filesystem instead of the code.
|
|
"""
|
|
self._weekly_briefings_shape()
|
|
res = subprocess.run(
|
|
["git", "-C", self.root, "remote", "get-url", "mdcps"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
self.assertNotEqual(res.returncode, 0, "git remote names are case-sensitive")
|
|
|
|
# A caller naming the wrong case cannot disambiguate, but the target is
|
|
# unambiguous anyway, so the exact-case name is still resolved.
|
|
got = crr.resolve_target_base_ref(self.root, explicit_remote="mdcps")
|
|
self.assertTrue(got["proven"], got["reasons"])
|
|
self.assertEqual(got["remote"], "MDCPS")
|
|
self.assertEqual(got["tracking_ref"], "refs/remotes/MDCPS/dev")
|
|
self.assertFalse(got["identity_explicit"], "a non-matching name is not explicit intent")
|
|
|
|
name, slug = crr.resolve_identity_remote(self.root)
|
|
self.assertEqual(name, "MDCPS")
|
|
self.assertEqual(slug, MDCPS_SLUG)
|
|
|
|
|
|
class TestTargetStaleness(_RepoCase):
|
|
"""Required coverage 6/7: matching tip, and behind or divergent checkout."""
|
|
|
|
def test_local_equal_to_resolved_tip_is_not_stale(self):
|
|
self._weekly_briefings_shape()
|
|
got = master_parity_gate.assess_target_repository_parity(
|
|
canonical_root=self.root, source="test"
|
|
)
|
|
self.assertTrue(got["determinable"])
|
|
self.assertFalse(got["stale"])
|
|
self.assertEqual(got["tracking_ref"], "refs/remotes/MDCPS/dev")
|
|
self.assertEqual(got["base_remote"], "MDCPS")
|
|
self.assertEqual(got["base_branch"], "dev")
|
|
self.assertIsNone(got["reason_code"])
|
|
|
|
def test_local_behind_resolved_tip_is_stale(self):
|
|
head = self._weekly_briefings_shape()
|
|
advanced = _advance(self.root, "remote moved on")
|
|
_set_remote_branch(self.root, "MDCPS", "dev", advanced)
|
|
_git(self.root, "reset", "--hard", "--quiet", head)
|
|
|
|
got = master_parity_gate.assess_target_repository_parity(
|
|
canonical_root=self.root, source="test"
|
|
)
|
|
self.assertTrue(got["determinable"])
|
|
self.assertTrue(got["stale"])
|
|
self.assertEqual(got["checkout_head"], head)
|
|
self.assertEqual(got["remote_tracking_head"], advanced)
|
|
|
|
def test_local_divergent_from_resolved_tip_is_stale(self):
|
|
head = self._weekly_briefings_shape()
|
|
remote_side = _advance(self.root, "remote side")
|
|
_set_remote_branch(self.root, "MDCPS", "dev", remote_side)
|
|
_git(self.root, "reset", "--hard", "--quiet", head)
|
|
local_side = _advance(self.root, "local side")
|
|
|
|
got = master_parity_gate.assess_target_repository_parity(
|
|
canonical_root=self.root, source="test"
|
|
)
|
|
self.assertTrue(got["stale"])
|
|
self.assertEqual(got["checkout_head"], local_side)
|
|
self.assertNotEqual(local_side, remote_side)
|
|
|
|
|
|
class TestFailClosed(_RepoCase):
|
|
"""Required coverage 8/9: missing remote or ref, and ambiguous targets."""
|
|
|
|
def test_missing_remote_fails_closed(self):
|
|
_make_repo(self.root, remote=None, url=None)
|
|
got = crr.resolve_target_base_ref(self.root)
|
|
self.assertFalse(got["proven"])
|
|
self.assertEqual(got["reason_code"], crr.DENY_NO_IDENTITY_REMOTE)
|
|
self.assertEqual(got["tracking_refs"], ())
|
|
self.assertIsNone(root_checkout_guard.resolve_remote_master_sha(self.root))
|
|
|
|
def test_missing_tracking_ref_fails_closed(self):
|
|
_make_repo(self.root, remote="MDCPS", url=MDCPS_URL)
|
|
# Remote configured, but nothing has ever been fetched.
|
|
got = crr.resolve_target_base_ref(self.root)
|
|
self.assertFalse(got["proven"])
|
|
self.assertEqual(got["reason_code"], crr.DENY_NO_BASE_BRANCH)
|
|
self.assertIsNone(root_checkout_guard.resolve_remote_master_sha(self.root))
|
|
|
|
def test_configured_upstream_without_a_tracking_ref_fails_closed(self):
|
|
"""A proven target always names a ref that resolves."""
|
|
_make_repo(self.root, remote="MDCPS", url=MDCPS_URL)
|
|
_set_upstream(self.root, "MDCPS", "dev") # declared, never fetched
|
|
|
|
got = crr.resolve_target_base_ref(self.root)
|
|
self.assertFalse(got["proven"])
|
|
self.assertEqual(got["reason_code"], crr.DENY_NO_BASE_BRANCH)
|
|
self.assertIsNone(got["tracking_ref"])
|
|
|
|
def test_every_proven_target_resolves(self):
|
|
"""No 'proven' result may name an unresolvable tracking ref."""
|
|
head = self._weekly_briefings_shape()
|
|
got = crr.resolve_target_base_ref(self.root)
|
|
self.assertTrue(got["proven"])
|
|
self.assertEqual(_git(self.root, "rev-parse", got["tracking_ref"]), head)
|
|
|
|
def test_ambiguous_integration_branch_fails_closed(self):
|
|
head = _make_repo(self.root, remote="MDCPS", url=MDCPS_URL)
|
|
# Two candidate integration branches and no configured upstream.
|
|
_set_remote_branch(self.root, "MDCPS", "dev", head)
|
|
_set_remote_branch(self.root, "MDCPS", "main", head)
|
|
|
|
got = crr.resolve_target_base_ref(self.root)
|
|
self.assertFalse(got["proven"])
|
|
self.assertEqual(got["reason_code"], crr.DENY_AMBIGUOUS_BASE_BRANCH)
|
|
self.assertIsNone(root_checkout_guard.resolve_remote_master_sha(self.root))
|
|
|
|
def test_ambiguous_identity_remote_fails_closed(self):
|
|
head = _make_repo(self.root, remote="MDCPS", url=MDCPS_URL)
|
|
_git(self.root, "remote", "add", "prgs", PRGS_URL)
|
|
_set_remote_branch(self.root, "MDCPS", "dev", head)
|
|
_set_remote_branch(self.root, "prgs", "master", head)
|
|
|
|
got = crr.resolve_target_base_ref(self.root)
|
|
self.assertFalse(got["proven"])
|
|
self.assertEqual(got["reason_code"], crr.DENY_AMBIGUOUS_REMOTE)
|
|
self.assertEqual(got["tracking_refs"], ())
|
|
|
|
def test_orphan_tracking_ref_from_removed_remote_is_ignored(self):
|
|
"""The live Gitea-Tools symptom: refs/remotes/origin/* outlives its remote."""
|
|
head = _make_repo(self.root, remote="prgs", url=PRGS_URL)
|
|
_set_remote_branch(self.root, "prgs", "master", head)
|
|
_set_upstream(self.root, "prgs", "master")
|
|
# An abandoned ref left behind by a remote that no longer exists.
|
|
_set_remote_branch(self.root, "origin", "master", head)
|
|
_advance(self.root, "orphan must not be consulted")
|
|
|
|
got = master_parity_gate.assess_target_repository_parity(
|
|
canonical_root=self.root, source="test"
|
|
)
|
|
self.assertEqual(got["tracking_ref"], "refs/remotes/prgs/master")
|
|
self.assertEqual(got["repository_slug"], PRGS_SLUG)
|
|
self.assertNotIn(
|
|
"target repository identity could not be derived from its git remote",
|
|
got["reasons"],
|
|
)
|
|
|
|
|
|
class TestExplicitVersusInferredRemote(_RepoCase):
|
|
"""Required coverage 11: explicit disambiguation stays distinct from inference.
|
|
|
|
This is B2. A remote the module inferred while probing must never re-enter
|
|
the resolver as though an operator had named it.
|
|
"""
|
|
|
|
def _two_remotes(self) -> str:
|
|
head = _make_repo(self.root, remote="MDCPS", url=MDCPS_URL)
|
|
_git(self.root, "remote", "add", "prgs", PRGS_URL)
|
|
_set_remote_branch(self.root, "MDCPS", "dev", head)
|
|
_set_remote_branch(self.root, "prgs", "master", head)
|
|
return head
|
|
|
|
def test_explicit_remote_disambiguates(self):
|
|
self._two_remotes()
|
|
got = crr.resolve_target_base_ref(self.root, explicit_remote="MDCPS")
|
|
self.assertTrue(got["proven"], got["reasons"])
|
|
self.assertEqual(got["remote"], "MDCPS")
|
|
self.assertEqual(got["branch"], "dev")
|
|
self.assertTrue(got["identity_explicit"])
|
|
|
|
def test_inferred_remote_does_not_disambiguate(self):
|
|
"""Feeding the inferred remote back in must not unlock the target."""
|
|
self._two_remotes()
|
|
inferred, _ = crr.resolve_identity_remote(self.root)
|
|
self.assertIsNotNone(inferred, "the first-wins probe still returns a name")
|
|
|
|
# The report infers internally and must still refuse.
|
|
report = master_parity_gate.assess_target_repository_parity(
|
|
canonical_root=self.root, source="test"
|
|
)
|
|
self.assertEqual(report["reason_code"], crr.DENY_AMBIGUOUS_REMOTE)
|
|
self.assertIsNone(report["repository_slug"])
|
|
self.assertIsNone(report["tracking_ref"])
|
|
self.assertFalse(report["stale"])
|
|
self.assertTrue(report["reasons"])
|
|
|
|
def test_report_never_passes_a_remote_into_the_resolver(self):
|
|
"""Structural guard against the exact B2 regression."""
|
|
src = inspect.getsource(master_parity_gate.assess_target_repository_parity)
|
|
self.assertIn("resolve_target_base_ref(canonical_root)", src)
|
|
self.assertNotIn("resolve_target_base_ref(canonical_root, remote=", src)
|
|
self.assertNotIn("explicit_remote=identity", src)
|
|
# Reporting must use the ambiguity-aware identity resolver.
|
|
self.assertIn("assess_identity_remote(canonical_root)", src)
|
|
|
|
def test_explicit_parameter_is_named_for_its_meaning(self):
|
|
for fn in (
|
|
crr.resolve_target_base_ref,
|
|
root_checkout_guard.resolve_remote_master_sha,
|
|
root_checkout_guard.resolve_remote_master_ref_state,
|
|
):
|
|
params = inspect.signature(fn).parameters
|
|
self.assertIn("explicit_remote", params, fn.__name__)
|
|
self.assertNotIn("remote", params, fn.__name__)
|
|
|
|
def test_unmatched_explicit_remote_cannot_unlock_an_ambiguous_target(self):
|
|
self._two_remotes()
|
|
got = crr.resolve_target_base_ref(self.root, explicit_remote="nonexistent")
|
|
self.assertFalse(got["proven"])
|
|
self.assertEqual(got["reason_code"], crr.DENY_AMBIGUOUS_REMOTE)
|
|
|
|
|
|
class TestGatingAndReportingAgree(_RepoCase):
|
|
"""Required coverage 10: guard and parity report make identical decisions."""
|
|
|
|
def test_same_resolved_target_for_gate_and_report(self):
|
|
self._weekly_briefings_shape()
|
|
|
|
gate = root_checkout_guard.resolve_remote_master_ref_state(self.root)
|
|
report = master_parity_gate.assess_target_repository_parity(
|
|
canonical_root=self.root, source="test"
|
|
)
|
|
|
|
self.assertEqual(gate["remote"], report["base_remote"])
|
|
self.assertEqual(gate["branch"], report["base_branch"])
|
|
self.assertEqual(gate["sha"], report["remote_tracking_head"])
|
|
self.assertIn(
|
|
gate["ref"],
|
|
(report["tracking_ref"], f"{gate['remote']}/{gate['branch']}"),
|
|
)
|
|
|
|
def test_both_sides_refuse_the_same_unresolvable_target(self):
|
|
_make_repo(self.root, remote=None, url=None)
|
|
|
|
gate = root_checkout_guard.resolve_remote_master_ref_state(self.root)
|
|
report = master_parity_gate.assess_target_repository_parity(
|
|
canonical_root=self.root, source="test"
|
|
)
|
|
self.assertIsNone(gate["sha"])
|
|
self.assertIsNone(report["remote_tracking_head"])
|
|
self.assertFalse(report["stale"])
|
|
self.assertTrue(report["reasons"])
|
|
self.assertEqual(gate["reason_code"], report["reason_code"])
|
|
|
|
def test_both_sides_refuse_the_same_ambiguous_target(self):
|
|
head = _make_repo(self.root, remote="MDCPS", url=MDCPS_URL)
|
|
_git(self.root, "remote", "add", "prgs", PRGS_URL)
|
|
_set_remote_branch(self.root, "MDCPS", "dev", head)
|
|
_set_remote_branch(self.root, "prgs", "master", head)
|
|
|
|
gate = root_checkout_guard.resolve_remote_master_ref_state(self.root)
|
|
report = master_parity_gate.assess_target_repository_parity(
|
|
canonical_root=self.root, source="test"
|
|
)
|
|
|
|
self.assertIsNone(gate["sha"])
|
|
self.assertEqual(gate["reason_code"], crr.DENY_AMBIGUOUS_REMOTE)
|
|
# The report must not name a repository the gate refuses to act on.
|
|
self.assertEqual(report["reason_code"], gate["reason_code"])
|
|
self.assertIsNone(report["repository_slug"])
|
|
self.assertIsNone(report["base_remote"])
|
|
self.assertIsNone(report["base_branch"])
|
|
self.assertFalse(report["stale"])
|
|
|
|
def test_both_sides_refuse_the_same_ambiguous_branch(self):
|
|
head = _make_repo(self.root, remote="MDCPS", url=MDCPS_URL)
|
|
_set_remote_branch(self.root, "MDCPS", "dev", head)
|
|
_set_remote_branch(self.root, "MDCPS", "main", head)
|
|
|
|
gate = root_checkout_guard.resolve_remote_master_ref_state(self.root)
|
|
report = master_parity_gate.assess_target_repository_parity(
|
|
canonical_root=self.root, source="test"
|
|
)
|
|
self.assertEqual(gate["reason_code"], crr.DENY_AMBIGUOUS_BASE_BRANCH)
|
|
self.assertEqual(report["reason_code"], gate["reason_code"])
|
|
self.assertIsNone(report["tracking_ref"])
|
|
|
|
|
|
class TestProductionCallers(unittest.TestCase):
|
|
"""Required coverage 13: every production caller consumes the same target."""
|
|
|
|
def test_guard_reports_the_ref_it_actually_compared(self):
|
|
assessment = root_checkout_guard.assess_root_checkout_guard(
|
|
workspace_path="/tmp/nonexistent-workspace-983",
|
|
canonical_repo_root="/tmp/nonexistent-root-983",
|
|
current_branch="dev",
|
|
head_sha="a" * 40,
|
|
porcelain_status="",
|
|
remote_master_sha="b" * 40,
|
|
remote_master_ref="refs/remotes/MDCPS/dev",
|
|
)
|
|
self.assertTrue(assessment["block"])
|
|
joined = " ".join(assessment["reasons"])
|
|
self.assertIn("refs/remotes/MDCPS/dev", joined)
|
|
self.assertNotIn("prgs/master", joined)
|
|
|
|
def test_guard_message_without_a_ref_stays_generic(self):
|
|
assessment = root_checkout_guard.assess_root_checkout_guard(
|
|
workspace_path="/tmp/nonexistent-workspace-983",
|
|
canonical_repo_root="/tmp/nonexistent-root-983",
|
|
current_branch="master",
|
|
head_sha="a" * 40,
|
|
porcelain_status="",
|
|
remote_master_sha="b" * 40,
|
|
)
|
|
joined = " ".join(assessment["reasons"])
|
|
self.assertIn("the tracking integration ref", joined)
|
|
self.assertNotIn("prgs/master", joined)
|
|
|
|
def test_anti_stomp_preflight_forwards_the_resolved_ref(self):
|
|
sig = inspect.signature(anti_stomp_preflight.assess_anti_stomp_preflight)
|
|
self.assertIn("remote_master_ref", sig.parameters)
|
|
src = inspect.getsource(anti_stomp_preflight.assess_anti_stomp_preflight)
|
|
self.assertIn("remote_master_ref=remote_master_ref", src)
|
|
|
|
def test_no_production_caller_inherits_the_prgs_default(self):
|
|
"""Every resolve site must derive, or pass remote_refs explicitly."""
|
|
import gitea_mcp_server
|
|
|
|
src = inspect.getsource(gitea_mcp_server)
|
|
# The four historical call sites now consume the resolved-target state.
|
|
self.assertGreaterEqual(src.count("resolve_remote_master_ref_state("), 4)
|
|
self.assertNotIn("resolve_remote_master_sha(canonical_root)", src)
|
|
|
|
def test_no_production_caller_supplies_an_explicit_remote(self):
|
|
"""Nothing in production may suppress the ambiguity gate (#983 B2)."""
|
|
import gitea_mcp_server
|
|
|
|
for module in (gitea_mcp_server, anti_stomp_preflight, master_parity_gate):
|
|
src = inspect.getsource(module)
|
|
self.assertNotIn("explicit_remote=", src, module.__name__)
|
|
|
|
|
|
class TestRepositoryStructureUntouched(_RepoCase):
|
|
"""Required coverage 12: resolution mutates no ref, remote, config, or checkout."""
|
|
|
|
def test_resolution_creates_no_refs_or_branches(self):
|
|
self._weekly_briefings_shape()
|
|
|
|
fmt = "--format=%(refname) %(objectname)"
|
|
before_refs = _git(self.root, "for-each-ref", fmt)
|
|
before_remotes = _git(self.root, "remote")
|
|
before_config = _git(self.root, "config", "--local", "--list")
|
|
before_head = _git(self.root, "rev-parse", "HEAD")
|
|
before_branch = _git(self.root, "symbolic-ref", "--short", "HEAD")
|
|
before_status = _git(self.root, "status", "--porcelain", "--untracked-files=all")
|
|
before_symref = _git(self.root, "symbolic-ref", "refs/remotes/MDCPS/HEAD")
|
|
|
|
crr.resolve_target_base_ref(self.root)
|
|
crr.assess_identity_remote(self.root)
|
|
root_checkout_guard.resolve_remote_master_sha(self.root)
|
|
root_checkout_guard.resolve_remote_master_ref_state(self.root)
|
|
master_parity_gate.assess_target_repository_parity(
|
|
canonical_root=self.root, source="test"
|
|
)
|
|
|
|
self.assertEqual(_git(self.root, "for-each-ref", fmt), before_refs)
|
|
self.assertEqual(_git(self.root, "remote"), before_remotes)
|
|
self.assertEqual(_git(self.root, "config", "--local", "--list"), before_config)
|
|
self.assertEqual(_git(self.root, "rev-parse", "HEAD"), before_head)
|
|
self.assertEqual(_git(self.root, "symbolic-ref", "--short", "HEAD"), before_branch)
|
|
self.assertEqual(
|
|
_git(self.root, "status", "--porcelain", "--untracked-files=all"), before_status
|
|
)
|
|
# The stale cache is specifically NOT repaired: that would be a mutation.
|
|
self.assertEqual(_git(self.root, "symbolic-ref", "refs/remotes/MDCPS/HEAD"), before_symref)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|