S7a-3 pkt. 1. Til i dag NEKTET reconcile_bundle_id en base som erklaerte en id katalogen ikke bar. Maalt mot K2 - den foerste leverte basen som erklaerer sin egen id (618 av 630 konseptfiler + rot-index, alle "k2-trinn1-20260903", levert som "K2-bundle-20260903") - betydde det at basen ikke kunne aapnes slik den var levert, og at eneste botemiddel var aa montere den paa nytt for haand, en gang per leveranse. PM-beslutning: konsumenten slakker. - Erklaert vinner (B1s rekkefoelge uroert), avviket REGISTRERES: ResolvedBundleId.mount + ProvenanceStamp.bundle_id_source + DryRunReport.bundle_id_source + run.bundle_id_notice (None ved enighet). Stempel-feltet er PAAKREVD uten default: None er en VERDI (veg-stien). - Det som fortsatt nekter er den EKTE kollisjonen: to KONSEPTER i en base som erklaerer ULIKE id-er (okf.assert_declared_ids_agree, kalt ved hver doer som aapner en base). Rot-index er IKKE med i enighets-settet - konsept-slaar-index er en presedens-regel, saa en index i utakt er fallbacken som taper. - KONSEKVENS, ikke scope-krype: explore._bundle_index loeser naa den erklaerte id-en. Den brukte Path(raw).name mens dispatcheren brukte reconcile...id; med erklaert-vinner ville explore() myntet approaches som navngir MOUNTET mens dispatcheren ruter paa ERKLAERINGEN - en utforskning med uruterbart mandat. Load-bearing MAALT: 10 mutasjoner alle roede mot HELE suiten, groenn kontroll 1243 passed / 5 skipped og golden demo-transcript.stdout byte-uendret (shasum -a 1 = ea8c534773acdbe41ae68f2c55724d69aaf8be4f). M1 1 / M2 1 / M3 1 / M4 9 / M5 2 / M6 1 / M7 1 / M8 2 / M9 2 / M11 1. Tre armer i test_bundle_id_reconciliation_loadbearing er SKREVET OM (ikke slettet) - de pinnet nekten beslutningen fjernet. (j) ble skarpere enn den den erstattet: erklaert id ruter, mountet nektes. Kontrakt: docs/okf-konsum-kontrakter.md § 3.1. Invariantrad i CLAUDE.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
434 lines
21 KiB
Python
434 lines
21 KiB
Python
"""Step 10-11 (Session 4) - ONE bundle-id rule, and a cross-base collision that stops being silent.
|
|
|
|
**The defect, measured 2026-09-02.** Two modules derived a base's id by hand and identically:
|
|
``explore._bundle_index`` (``Path(raw).name``) and ``run_mandate_across_bundles`` (``run.py:1514``,
|
|
a second copy of the same line with its own refusal beside it). Neither consulted what the base
|
|
itself says. A corpus that declares its own id in ``index.md`` -- the form OKF SPEC leaves open --
|
|
would have been mounted under a directory name that disagreed with it, and every artefact the run
|
|
stamped would have named the mount while the base named itself something else. Two copies of one
|
|
derivation rule is the ko-(p) drift class; a derivation that ignores the artefact is worse, because
|
|
nothing ever disagrees out loud.
|
|
|
|
**Operator decision B1 (D6 ratified), and why ``origin`` has THREE values.** Identity is the pair
|
|
``(bundle_id, concept_id)``, and ``bundle_id`` is read from the CONCEPT's own frontmatter first,
|
|
with the root ``index.md`` as fallback, and the mount's basename as the last resort. ``origin`` is
|
|
REQUIRED WITHOUT DEFAULT for ``cost_baseline_anchored``'s reason -- both defaults would lie about an
|
|
event -- and it is not a boolean, because a caller that cannot tell "the concept said so" from "we
|
|
fell back twice" has been handed a stamp it cannot audit.
|
|
|
|
**MEASURED before building, and it decides the shape of this file (nevner stated).** No file
|
|
anywhere under ``shared/``, ``src/`` or ``tests/`` declares ``bundle_id`` in frontmatter: zero hits
|
|
for ``^bundle_id`` against a known-positive control of 31 files carrying ``^type:`` under
|
|
``shared/examples/``. Every base in the repo therefore resolves ``mount-derived`` today, and the 27
|
|
``bundle_dirs=`` call sites stay green. That makes ``declared-concept`` and ``declared-index``
|
|
DEFENSIVE branches (the ``budget_stop`` precedent): they are driven from CRAFTED bases here,
|
|
because nothing else in the suite would keep them alive.
|
|
|
|
**``explore._bundle_index`` stays PURE, and that is a correction from review, measured.**
|
|
``tests/test_explore_loadbearing.py:750`` passes ``/tmp/base-a`` and ``:764`` passes
|
|
``/tmp/one/shared-name`` -- directories that do not exist -- and both expect an ``ExplorationError``
|
|
about ids. Reading ``index.md`` there would raise on I/O before any id logic ran. Reconciliation
|
|
therefore happens where a base is actually OPENED: ``explore.read_bundle``, ``run_project``'s
|
|
bundle arm, and the dispatcher.
|
|
|
|
**A base with no readable ``index.md`` is NOT "undeclared".** ``navigate_bundle``'s fail-fast
|
|
propagates unchanged; reading an unreadable base as "it declares nothing" is the tolerant-read-
|
|
widens-the-answer defect research topic 2 measured in SPARQL's ``SILENT``.
|
|
|
|
**REWRITTEN IN PART, 2026-09-03 (S7a-3 pkt. 1).** Arms (e), (f) and (j) pinned a refusal an
|
|
operator decision removed: a declared id that disagrees with its MOUNT is now the declaration
|
|
winning, not an error -- measured against K2, the first delivered corpus that declares its own id,
|
|
which this refusal made unopenable as delivered. They were rewritten rather than deleted, each onto
|
|
what replaced it: (e) the declaration wins and the mount survives on the resolution; (f) the CLI's
|
|
early refusal now guards the collision that IS still refused (two concepts, two corpora); (j) the
|
|
dispatcher routes by the DECLARED id and refuses the mount -- a sharper discriminator than the one
|
|
it replaced. The slacken's own gate is ``tests/test_bundle_id_slack_loadbearing.py``.
|
|
|
|
**The "DEFENSIVE branch" note above is now historical.** It was true when written and measured
|
|
again on 2026-09-03: K2 declares the key in 619 files, so ``declared-index`` fires in the wild.
|
|
Nothing in THIS repo declares it, which is why the arms below still craft their bases.
|
|
|
|
Arms: (a) the CONTROL, a shipped base resolving ``mount-derived`` * (b) a declared, agreeing root
|
|
index * (c) a declaring CONCEPT beating a declaring index -- the ordering discriminator * (d) the
|
|
three origins are pairwise distinct, read off the three arms above rather than off a literal *
|
|
(e) a declared id BEATS the mount it disagrees with * (f) a base that cannot say what it is refused
|
|
through the CLI with ZERO model calls * (g) an unreadable index refuses instead of falling back *
|
|
(h)-(j) the same helper is reached from all three doors that open a base.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from portfolio_optimiser import explore, okf, run
|
|
from portfolio_optimiser.mandate import Approach, Mandate, MandateRoutingError
|
|
from portfolio_optimiser.simulation import ScriptedChatClient
|
|
from portfolio_optimiser.verdicts import VerdictCollision
|
|
|
|
_EXAMPLES = Path(__file__).resolve().parents[1] / "shared" / "examples"
|
|
_BYGG = _EXAMPLES / "bygg-energi-mikro"
|
|
_PID = "BYGG-KONTOR-NORD"
|
|
|
|
|
|
class _Dispatched(Exception):
|
|
"""Raised from the stand-in ``run_project`` so an arm can prove a run WAS dispatched without
|
|
letting it proceed. An ``AssertionError`` would be indistinguishable from a failing assert."""
|
|
|
|
|
|
def _base_copy(tmp_path: Path, *, name: str = "bygg-energi-mikro") -> Path:
|
|
"""A throwaway copy of a shipped base. Mutations of fixture content NEVER touch the git-tracked
|
|
tree (the repo's ``shutil.copytree`` discipline)."""
|
|
dst = tmp_path / name
|
|
shutil.copytree(_BYGG, dst)
|
|
return dst
|
|
|
|
|
|
def _declare(path: Path, value: str) -> None:
|
|
"""Insert a ``bundle_id`` line into an existing frontmatter block, after the opening ``---``."""
|
|
lines = path.read_text(encoding="utf-8").splitlines(keepends=True)
|
|
assert lines[0].startswith("---"), f"{path} has no frontmatter block to declare into"
|
|
lines.insert(1, f"bundle_id: {value}\n")
|
|
path.write_text("".join(lines), encoding="utf-8")
|
|
|
|
|
|
# --- (a) the CONTROL: nothing declares, so the mount answers ------------------------------------
|
|
|
|
|
|
def test_a_base_that_declares_nothing_resolves_from_the_mount() -> None:
|
|
"""(a) CONTROL. Every base shipped today takes this branch -- measured: zero ``^bundle_id``
|
|
declarations against a known-positive control of 31 files carrying ``^type:``. Without this arm
|
|
the two declared arms below could not be read as the exceptions they are."""
|
|
resolved = okf.reconcile_bundle_id(str(_BYGG))
|
|
assert resolved.id == "bygg-energi-mikro"
|
|
assert resolved.origin == "mount-derived"
|
|
|
|
|
|
# --- (b)/(c) the two declared origins ------------------------------------------------------------
|
|
|
|
|
|
def test_a_declaring_root_index_is_reconciled_and_marked_declared_index(tmp_path: Path) -> None:
|
|
"""(b) DEFENSIVE (``budget_stop`` precedent): no shipped index declares the key, so this branch
|
|
is crafted or it is untested."""
|
|
base = _base_copy(tmp_path)
|
|
_declare(base / "index.md", base.name)
|
|
|
|
resolved = okf.reconcile_bundle_id(str(base))
|
|
assert resolved.id == base.name
|
|
assert resolved.origin == "declared-index"
|
|
|
|
|
|
def test_a_declaring_concept_beats_a_declaring_index(tmp_path: Path) -> None:
|
|
"""(c) The ORDERING discriminator, and the reason both files declare the SAME (agreeing) value:
|
|
with only the concept declaring, an index-first implementation would fall through to
|
|
``mount-derived`` and could be mistaken for a bug elsewhere. With BOTH declaring, index-first
|
|
answers ``declared-index`` and concept-first answers ``declared-concept`` -- two implementations
|
|
that differ in exactly the field under test, which is what makes the arm falsifiable."""
|
|
base = _base_copy(tmp_path)
|
|
concept = next(p for p in sorted(base.rglob("*.md")) if p.name != "index.md")
|
|
_declare(base / "index.md", base.name)
|
|
_declare(concept, base.name)
|
|
|
|
resolved = okf.reconcile_bundle_id(str(base), concept_name=concept.relative_to(base).as_posix())
|
|
assert resolved.id == base.name
|
|
assert resolved.origin == "declared-concept"
|
|
|
|
|
|
def test_the_three_origins_are_pairwise_distinct(tmp_path: Path) -> None:
|
|
"""(d) B1's own reason for three values rather than a boolean: a caller must be able to tell
|
|
"the concept said so" from "the index said so" from "we fell back twice". Read off the three
|
|
RESOLUTIONS, never off a literal list -- an assert on a ``Literal``'s members is a static
|
|
property of the type that no runtime mutation can redden (the M8 correction)."""
|
|
declared_index = _base_copy(tmp_path, name="only-index")
|
|
_declare(declared_index / "index.md", "only-index")
|
|
|
|
declared_concept = _base_copy(tmp_path, name="with-concept")
|
|
target = next(p for p in sorted(declared_concept.rglob("*.md")) if p.name != "index.md")
|
|
_declare(target, "with-concept")
|
|
|
|
origins = {
|
|
okf.reconcile_bundle_id(str(_BYGG)).origin,
|
|
okf.reconcile_bundle_id(str(declared_index)).origin,
|
|
okf.reconcile_bundle_id(
|
|
str(declared_concept),
|
|
concept_name=target.relative_to(declared_concept).as_posix(),
|
|
).origin,
|
|
}
|
|
assert len(origins) == 3, f"two sources collapsed onto one origin: {sorted(origins)}"
|
|
|
|
|
|
# --- (e)/(f) disagreement is refused, and refused EARLY -------------------------------------------
|
|
|
|
|
|
def test_a_base_that_disagrees_with_its_mount_wins_over_it(tmp_path: Path) -> None:
|
|
"""(e) REWRITTEN 2026-09-03 (S7a-3 pkt. 1), not deleted. This arm used to pin the opposite
|
|
behaviour: a declared id the mount did not carry was refused by name. The operator decision
|
|
slackened it -- the declaration IS the identity, the mount is a filesystem accident -- so the
|
|
arm now pins what replaced it, and the refusal it used to pin lives in
|
|
``test_bundle_id_slack_loadbearing.py`` in its new form (two CONCEPTS disagreeing)."""
|
|
base = _base_copy(tmp_path)
|
|
_declare(base / "index.md", "a-name-the-mount-does-not-carry")
|
|
|
|
resolved = okf.reconcile_bundle_id(str(base))
|
|
|
|
assert resolved.id == "a-name-the-mount-does-not-carry"
|
|
assert resolved.origin == "declared-index"
|
|
assert resolved.mount == base.name, "the overridden mount must survive on the resolution"
|
|
|
|
|
|
def test_the_cli_refuses_a_base_that_cannot_say_what_it_is_before_it_spends_a_call(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""(f) REWRITTEN 2026-09-03 with the refusal it now guards: this used to drive a base whose
|
|
declared id disagreed with its MOUNT, which no longer refuses. The EARLY half is what the arm
|
|
was always for and is unchanged -- the assert is on ZERO model calls, never on the exit code
|
|
alone, because a refusal that arrives AFTER the spend looks identical at rc 1 (the M12
|
|
signature, and the hoist idiom ``test_explore_callsites_loadbearing.py`` already uses twice)."""
|
|
monkeypatch.delenv("PORTFOLIO_MODEL_MAP", raising=False)
|
|
monkeypatch.delenv("PORTFOLIO_FOUNDRY_PROJECT_ENDPOINT", raising=False)
|
|
base = _base_copy(tmp_path)
|
|
concepts = [p for p in sorted(base.rglob("*.md")) if p.name != "index.md"]
|
|
_declare(concepts[0], "corpus-alfa")
|
|
_declare(concepts[1], "corpus-beta")
|
|
|
|
sink: list[str] = []
|
|
|
|
def counting_factory(profile: Any) -> Any:
|
|
return lambda role: ScriptedChatClient(sink=sink, role=role, default_reply="ok")
|
|
|
|
monkeypatch.setattr("portfolio_optimiser.run._default_factory", counting_factory)
|
|
|
|
rc = run.main([_PID, "--docs-dir", str(base), "--bundle-dir", str(base)])
|
|
assert rc == 1
|
|
assert sink == [], (
|
|
"the run reached the model before the base was asked what it is -- the base was paid for "
|
|
f"before it was refused ({len(sink)} calls)"
|
|
)
|
|
|
|
|
|
# --- (g) absence is not a declaration ------------------------------------------------------------
|
|
|
|
|
|
def test_an_unreadable_index_refuses_instead_of_falling_back_to_the_mount(tmp_path: Path) -> None:
|
|
"""(g) The tolerant-read trap, refused. A base whose index cannot be read is UNKNOWN, not
|
|
undeclared; answering ``mount-derived`` there would widen the answer on missing evidence."""
|
|
empty = tmp_path / "no-index-here"
|
|
empty.mkdir()
|
|
with pytest.raises(ValueError, match="index.md"):
|
|
okf.reconcile_bundle_id(str(empty))
|
|
|
|
|
|
# --- (h)-(j) one helper, reached from all three doors ---------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def _spy(monkeypatch: pytest.MonkeyPatch) -> list[str]:
|
|
"""Record every reconciliation while delegating to the real one. A private basename copy at a
|
|
call site leaves this list empty, which is the only witness that distinguishes "reconciled" from
|
|
"still derived by hand" (mutation M11)."""
|
|
seen: list[str] = []
|
|
real = okf.reconcile_bundle_id
|
|
|
|
def spy(bundle_dir: Any, **kwargs: Any) -> Any:
|
|
seen.append(str(bundle_dir))
|
|
return real(bundle_dir, **kwargs)
|
|
|
|
monkeypatch.setattr(okf, "reconcile_bundle_id", spy)
|
|
return seen
|
|
|
|
|
|
def test_explore_read_bundle_reconciles_the_base_it_opens(_spy: list[str]) -> None:
|
|
"""(h) The tool is driven DIRECTLY: measured in okt 56, a scripted client returns TEXT and never
|
|
emits a tool call, so a gate that only drove ``explore()`` would never enter this body."""
|
|
tools = {t.name: t for t in explore.navigator_tools((str(_BYGG),))}
|
|
# See test_read_bundle_cost_loadbearing: the tool returns a LISTING since S2c, and ``!= ""``
|
|
# holds for every list ever built — this arm is about the reconciliation, so the call must
|
|
# still be a call that DID something.
|
|
assert [e["name"] for e in tools["read_bundle"].func(bundle_id=_BYGG.name)]
|
|
assert str(_BYGG) in _spy
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_project_reconciles_the_base_its_bundle_arm_opens(_spy: list[str]) -> None:
|
|
"""(i) The pipeline door. ``run_project``'s bundle arm is where the project, the stage-0
|
|
baseline and the read context are all derived from one base -- the id must come from the same
|
|
reconciliation, not from a fourth private copy."""
|
|
await run.run_project(
|
|
_PID,
|
|
docs_dir=str(_BYGG),
|
|
bundle_dir=str(_BYGG),
|
|
client_factory=lambda role: ScriptedChatClient(role=role, default_reply="ok"),
|
|
live_dry_run=True,
|
|
)
|
|
assert str(_BYGG) in _spy
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_the_dispatcher_routes_by_the_declared_id_not_the_mount(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""(j) REWRITTEN 2026-09-03 (S7a-3 pkt. 1) and now SHARPER than what it replaced.
|
|
|
|
It used to pin that a base disagreeing with its mount was refused while assembling the routing
|
|
table. That refusal is gone; what took its place is a discriminator the old arm could not have:
|
|
on a base whose declaration disagrees with its directory, the DECLARED id must route and the
|
|
MOUNT must not. A dispatcher left on ``Path(raw).name`` answers exactly the other way round, so
|
|
the two implementations differ in both halves rather than in an exit code.
|
|
|
|
The mount half is also the arm that keeps ``explore()`` and this door on ONE naming scheme: an
|
|
exploration mints approaches from ``_bundle_index``, and if the two resolved ids differently an
|
|
exploration's own mandate would be unroutable here.
|
|
"""
|
|
base = _base_copy(tmp_path)
|
|
_declare(base / "index.md", "a-name-the-mount-does-not-carry")
|
|
|
|
started: list[str] = []
|
|
|
|
async def counting_run_project(*args: Any, **kwargs: Any) -> Any:
|
|
started.append(str(kwargs.get("bundle_dir")))
|
|
raise _Dispatched
|
|
|
|
monkeypatch.setattr(run, "run_project", counting_run_project)
|
|
|
|
def _mandate(named: str) -> Mandate:
|
|
return Mandate(
|
|
objective="o",
|
|
approaches=(Approach(id="a", label="A", bundle_id=named),),
|
|
allow_own_proposals=False,
|
|
)
|
|
|
|
with pytest.raises(_Dispatched):
|
|
await run.run_mandate_across_bundles(
|
|
_mandate("a-name-the-mount-does-not-carry"), (str(base),)
|
|
)
|
|
assert started == [str(base)], "the declared id must route to the base that declared it"
|
|
|
|
with pytest.raises(MandateRoutingError, match=base.name):
|
|
await run.run_mandate_across_bundles(_mandate(base.name), (str(base),))
|
|
assert started == [str(base)], "the mount name is not an id once the base declares one"
|
|
|
|
|
|
# ==================================================================================================
|
|
# Step 11 - a cross-base candidate collision stops being silent
|
|
# ==================================================================================================
|
|
#
|
|
# **The defect (operator decision D2).** ``VerdictStore.add`` is first-write-wins per id and
|
|
# ``_mint_id`` excludes the corpus BY CONSTRUCTION (it hashes ``affected_codes`` / ``measure_type``
|
|
# / ``claimed_saving_nok``, and nothing else). Two independently produced bases that describe the
|
|
# same candidate therefore collapse onto ONE verdict and the second is dropped in silence. No test
|
|
# covered it.
|
|
#
|
|
# **``_mint_id`` is NOT changed** -- its form is normative in ``shared/method-spec.md`` and
|
|
# commons-owned, so altering it is an amendment, not a local edit. **``add`` is not changed either:**
|
|
# it is called from inside ``run_project`` and ``run_portfolio``, never from
|
|
# ``run_mandate_across_bundles``, so a return value there would reach no code in this step's scope,
|
|
# and a drop there can equally be against a Step-7 inbox verdict or a bundle seed rather than a
|
|
# second base. The mechanism is dispatcher-side accounting, because the dispatcher is the one place
|
|
# that holds the id -> base map the store does not keep.
|
|
|
|
|
|
_COLLIDING_PROPOSAL = (
|
|
'{"measure":"LED-retrofit av kontorbelysning","affected_items":'
|
|
'[{"code":"ENERGI-TOTAL-EL","quantity":300000,"unit_cost":1.0}],"claimed_saving_nok":30000}'
|
|
)
|
|
_VERDICT_INPUT = {"decision": "approved", "rationale": "expert reviewed (fixture)"}
|
|
|
|
|
|
def _colliding_factory() -> Any:
|
|
"""Both bases answer with the SAME proposal, which is what makes the two ids collide -- the
|
|
corrected fixture note: ``_mint_id`` hashes the PROPOSAL's features, not the base's IR
|
|
projection, so matching projections alone would mint nothing and the arm would pass for the
|
|
wrong reason.
|
|
|
|
The role names are ``workflow._MAKER_CHECKER_ROLES`` verbatim (``proposer`` / ``checker``); a
|
|
typo there would feed the proposal JSON to the checker and the arm would still pass, since F2
|
|
mints a verdict on either outcome -- green for a different reason than the docstring claims.
|
|
The outcome itself is deliberately NOT asserted: this arm is about candidate IDENTITY, and
|
|
``_mint_id`` reads the same three structural fields whether the gate validated or rejected."""
|
|
return lambda role: ScriptedChatClient(
|
|
role=role,
|
|
default_reply=(_COLLIDING_PROPOSAL if role != "checker" else "Holder. VERDICT: APPROVE"),
|
|
)
|
|
|
|
|
|
def _two_bases(tmp_path: Path) -> tuple[Path, Path]:
|
|
return _base_copy(tmp_path, name="base-one"), _base_copy(tmp_path, name="base-two")
|
|
|
|
|
|
def _mandate_over(*bundle_ids: str) -> Any:
|
|
return Mandate(
|
|
objective="find the same saving twice",
|
|
approaches=tuple(
|
|
Approach(id=f"a{i}", label=f"A{i}", bundle_id=bid) for i, bid in enumerate(bundle_ids)
|
|
),
|
|
allow_own_proposals=False,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_two_bases_describing_one_candidate_are_reported_not_dropped(tmp_path: Path) -> None:
|
|
"""(k) The measured silence, closed. Two bases, one candidate: the collision names BOTH bases
|
|
and the shared id, in dispatch order."""
|
|
one, two = _two_bases(tmp_path)
|
|
result = await run.run_mandate_across_bundles(
|
|
_mandate_over(one.name, two.name),
|
|
(str(one), str(two)),
|
|
verdict_input=_VERDICT_INPUT,
|
|
client_factory=_colliding_factory(),
|
|
)
|
|
|
|
assert len(result.collisions) == 1, f"expected one collision, got {result.collisions}"
|
|
collision = result.collisions[0]
|
|
assert collision.first_bundle_id == one.name
|
|
assert collision.second_bundle_id == two.name
|
|
assert collision.verdict_id == result.runs[0].result.verdict_key
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_a_single_base_reports_no_collision(tmp_path: Path) -> None:
|
|
"""(l) CONTROL. An empty trace is an honest POSITIVE statement ("no candidate was described by
|
|
two bases") -- the ``skipped_links`` half of the required-vs-default rule, not the
|
|
``cost_baseline_anchored`` half. Without this arm, an implementation that always appended would
|
|
pass (k)."""
|
|
one = _base_copy(tmp_path, name="base-one")
|
|
result = await run.run_mandate_across_bundles(
|
|
_mandate_over(one.name),
|
|
(str(one),),
|
|
verdict_input=_VERDICT_INPUT,
|
|
client_factory=_colliding_factory(),
|
|
)
|
|
assert result.collisions == ()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_the_drop_itself_is_unchanged_only_its_visibility(tmp_path: Path) -> None:
|
|
"""(m) This step changes VISIBILITY, never semantics. The store still holds exactly one verdict
|
|
for the shared id: ``add`` is untouched and first-write-wins still wins."""
|
|
one, two = _two_bases(tmp_path)
|
|
result = await run.run_mandate_across_bundles(
|
|
_mandate_over(one.name, two.name),
|
|
(str(one), str(two)),
|
|
verdict_input=_VERDICT_INPUT,
|
|
client_factory=_colliding_factory(),
|
|
)
|
|
shared = result.collisions[0].verdict_id
|
|
assert [v.id for v in result.store.verdicts].count(shared) == 1
|
|
|
|
|
|
def test_the_collision_notice_is_omitted_when_nothing_collided() -> None:
|
|
"""(n) Omission, never an empty row (``mandate.announce``'s rule, as ``skipped_links_notice``
|
|
and ``cost_baseline_notice`` already follow). Both directions are asserted: a renderer that
|
|
always returns a line is as wrong as one that never does."""
|
|
assert run.collision_notice(()) is None
|
|
line = run.collision_notice(
|
|
(
|
|
VerdictCollision(
|
|
verdict_id="abc123", first_bundle_id="base-one", second_bundle_id="base-two"
|
|
),
|
|
)
|
|
)
|
|
assert line is not None
|
|
assert "abc123" in line and "base-one" in line and "base-two" in line
|