CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 1m12s
CI & Build / Python tests (push) Successful in 1m53s
CI & Build / Build & push image (push) Successful in 12s
shape ledger, MCP projects/search/rulebooks tools, backup, chunking, plugin context, derived mirror, rule band, shape form gate, plugin version mint and note usage tests drop issue, milestone and rule numbers, dates and one-install figures from docstrings, comments and messages. Three shape-form-gate test names that told the discovery story now name the property: a registry module's helpers are not service functions, the divergence gate silences only a family contradiction, and callable helpers beside a callable canon still prompt. The rulebook tool-count comment that logged each milestone's additions is replaced by the reason it is a count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
288 lines
12 KiB
Python
288 lines
12 KiB
Python
"""The plugin's version is MINTED, and CI is the control that it moved.
|
|
|
|
WHAT THIS IS ABOUT. `plugin/` ships straight from this
|
|
git repo — no build step, so no moment at which CI could stamp a version in.
|
|
The value is therefore minted by a script before the commit, and CI's job is
|
|
not to produce it but to prove it moved when it had to.
|
|
|
|
THE DIVERGENCE THESE GUARD. Two artifacts in one repo derive their versions
|
|
from different clocks, on purpose:
|
|
|
|
server image name from COMMIT time, ordering key from BUILD time
|
|
plugin one value, from MINT time
|
|
|
|
The server uses commit time so two lanes building one source report one
|
|
string. The plugin has one lane and no build, so that reason does not reach
|
|
it. "Let's make these consistent" is the obvious tidy-up and it breaks
|
|
whichever artifact loses — which is why the difference is pinned here rather
|
|
than only explained in a comment.
|
|
|
|
The trade mint time makes — you cannot recompute the value from history, only
|
|
verify it moved — is acceptable ONLY because the installer's refresh test
|
|
is `===` with no ordering anywhere. Where a
|
|
comparator orders, an unreproducible version would be unverifiable too.
|
|
"""
|
|
import ast
|
|
import json
|
|
import pathlib
|
|
import re
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import pytest
|
|
|
|
from scripts import check_plugin
|
|
from scripts.mint_plugin_version import VERSION_RE, mint, rewrite
|
|
|
|
MINT_SRC = pathlib.Path(check_plugin.ROOT) / "scripts" / "mint_plugin_version.py"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_failures():
|
|
"""`check_plugin.fail` appends to a module global; without this a failing
|
|
assertion in one test would be visible from the next."""
|
|
check_plugin.failures.clear()
|
|
yield
|
|
check_plugin.failures.clear()
|
|
|
|
|
|
def fake_manifest(version: str = "2026.09.01.2252") -> str:
|
|
return json.dumps(
|
|
{"name": "scribe", "description": "d", "version": version,
|
|
"userConfig": {"api_endpoint": {"type": "string"}}},
|
|
indent=2,
|
|
)
|
|
|
|
|
|
# ── The mint ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.parametrize("when,expected", [
|
|
# THE midnight case. An unpadded
|
|
# `%-H%M` renders this hour as `0` and silently shortens the string.
|
|
(datetime(2026, 1, 5, 0, 0, tzinfo=timezone.utc), "2026.01.05.0000"),
|
|
(datetime(2026, 1, 5, 0, 9, tzinfo=timezone.utc), "2026.01.05.0009"),
|
|
(datetime(2026, 12, 31, 23, 59, tzinfo=timezone.utc), "2026.12.31.2359"),
|
|
(datetime(2026, 9, 1, 22, 52, tzinfo=timezone.utc), "2026.09.01.2252"),
|
|
])
|
|
def test_the_mint_zero_pads_every_field(when, expected):
|
|
assert mint(when) == expected
|
|
assert VERSION_RE.match(mint(when))
|
|
|
|
|
|
def test_the_mint_is_UTC_not_local():
|
|
"""A local-time mint would make the value depend on who ran it — two people
|
|
minting the same minute would disagree, and the string is the artifact's
|
|
identity."""
|
|
utc = datetime(2026, 9, 1, 22, 52, tzinfo=timezone.utc)
|
|
east = utc.astimezone(timezone(timedelta(hours=9)))
|
|
assert mint(east) == mint(utc) == "2026.09.01.2252"
|
|
|
|
|
|
def test_the_mint_reads_a_CLOCK_and_never_git():
|
|
"""The clock divergence from the server image, asserted structurally.
|
|
|
|
Mint time is only meaningful if nothing consults history — the moment this
|
|
script shells out to git it has quietly become a commit-time deriver, and
|
|
the two artifacts' clocks have been "made consistent" without anyone
|
|
deciding to. That change would pass every other test in this file.
|
|
|
|
Asserted over the AST rather than the text, because the module docstring
|
|
discusses git at length explaining why it is absent. This looks for USE,
|
|
not mention.
|
|
"""
|
|
tree = ast.parse(MINT_SRC.read_text())
|
|
|
|
imported = set()
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Import):
|
|
imported |= {a.name.split(".")[0] for a in node.names}
|
|
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
imported.add(node.module.split(".")[0])
|
|
assert "subprocess" not in imported, (
|
|
"the mint script imports subprocess — a mint that can read history is "
|
|
"a commit-time deriver wearing the wrong name"
|
|
)
|
|
|
|
called = {ast.unparse(n.func) for n in ast.walk(tree) if isinstance(n, ast.Call)}
|
|
assert "datetime.now" in called, "the mint script no longer reads a clock"
|
|
|
|
|
|
# ── The rewrite ────────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_the_rewrite_touches_exactly_one_line():
|
|
"""Surgical, not a JSON round-trip. The manifest's formatting and key order
|
|
are not this script's to decide, and a whole-file reformat would make every
|
|
mint an unreadable diff."""
|
|
before = fake_manifest("0.1.48")
|
|
after = rewrite(before, "2026.09.01.2252")
|
|
|
|
b, a = before.splitlines(), after.splitlines()
|
|
assert len(b) == len(a)
|
|
differing = [i for i, (x, y) in enumerate(zip(b, a)) if x != y]
|
|
assert len(differing) == 1
|
|
assert '"version": "2026.09.01.2252"' in a[differing[0]]
|
|
|
|
|
|
def test_the_rewrite_preserves_indentation_and_key_order():
|
|
weird = '{\n\t"name": "scribe",\n\t"version": "0.1.48",\n\t"z": 1\n}\n'
|
|
out = rewrite(weird, "2026.09.01.2252")
|
|
assert out == '{\n\t"name": "scribe",\n\t"version": "2026.09.01.2252",\n\t"z": 1\n}\n'
|
|
|
|
|
|
def test_the_rewrite_refuses_a_manifest_it_cannot_match():
|
|
"""Raises rather than falling back to a JSON round-trip: a manifest this
|
|
cannot match is one whose shape changed, and quietly reformatting the file
|
|
to cope would be a far larger edit than the caller asked for."""
|
|
with pytest.raises(ValueError):
|
|
rewrite('{"name": "scribe"}', "2026.09.01.2252")
|
|
|
|
|
|
def test_the_rewrite_refuses_TWO_version_lines():
|
|
"""A capped `subn` would report one replacement and look clean while the
|
|
second `version` — possibly the real one — kept its old value."""
|
|
two = '{\n "version": "0.1.48",\n "nested": {\n "version": "9.9.9"\n }\n}\n'
|
|
with pytest.raises(ValueError):
|
|
rewrite(two, "2026.09.01.2252")
|
|
|
|
|
|
# ── The version-relevant set includes its own deriver ──────────────────────
|
|
|
|
|
|
def test_the_mint_script_is_version_relevant():
|
|
"""The deriver's asymmetry. A change to how the version is COMPUTED is
|
|
compared against nothing — leave the deriver out of the set and a format
|
|
change never forces a re-mint, so the manifest keeps a value in the old
|
|
format indefinitely and nothing says so."""
|
|
paths = check_plugin.version_relevant_paths()
|
|
assert "scripts/mint_plugin_version.py" in paths
|
|
for shipped in check_plugin.SHIPPED_PATHS:
|
|
assert shipped in paths
|
|
|
|
|
|
def test_the_checker_is_NOT_version_relevant():
|
|
"""The inverse, and it is the easy mistake. A checker decides whether the
|
|
lane goes red, not what the artifact reports — so its absence here is a
|
|
decision, not an oversight."""
|
|
assert "scripts/check_plugin.py" not in check_plugin.version_relevant_paths()
|
|
|
|
|
|
# ── The check ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
def run_check(here: str, there: str | None, changed_paths: list[str], monkeypatch):
|
|
"""Drive `check_version_is_minted` with git stubbed. Returns the failures."""
|
|
monkeypatch.setattr(
|
|
check_plugin, "_git",
|
|
lambda *a: (0, "\n".join(changed_paths)) if a[0] == "diff" else (0, ""),
|
|
)
|
|
monkeypatch.setattr(
|
|
check_plugin, "manifest_version",
|
|
lambda ref=None: here if ref is None else there,
|
|
)
|
|
monkeypatch.setattr(
|
|
check_plugin, "manifest_text",
|
|
lambda ref=None: fake_manifest(here if ref is None else (there or "0.0.0.0000")),
|
|
)
|
|
check_plugin.check_version_is_minted("origin/main")
|
|
return list(check_plugin.failures)
|
|
|
|
|
|
def test_content_changed_and_the_version_did_not_FAILS(monkeypatch):
|
|
"""The headline, and the only reason the check exists."""
|
|
failures = run_check(
|
|
"2026.09.01.2252", "2026.09.01.2252",
|
|
["plugin/hooks/scribe_session_context.sh"], monkeypatch,
|
|
)
|
|
assert len(failures) == 1
|
|
assert "still 2026.09.01.2252" in failures[0]
|
|
assert "scribe_session_context.sh" in failures[0]
|
|
|
|
|
|
def test_content_changed_and_the_version_moved_PASSES(monkeypatch):
|
|
assert run_check(
|
|
"2026.09.01.2252", "2026.08.30.1200",
|
|
["plugin/hooks/scribe_session_context.sh"], monkeypatch,
|
|
) == []
|
|
|
|
|
|
def test_a_version_that_is_not_the_canonical_shape_FAILS(monkeypatch):
|
|
"""`K4` returns the manifest string verbatim, so a malformed value is not
|
|
rejected by the installer — it either sorts as an ordinary string or, when
|
|
unreadable, forces a reinstall every session. Neither is loud."""
|
|
failures = run_check("0.1.48", "0.1.47", [], monkeypatch)
|
|
assert len(failures) == 1
|
|
assert "not YYYY.MM.DD.HHMM" in failures[0]
|
|
|
|
|
|
@pytest.mark.parametrize("bad", ["2026.9.1.2252", "2026.09.01.252", "2026.09.01"])
|
|
def test_an_UNPADDED_or_short_version_FAILS(bad, monkeypatch):
|
|
"""The padding is the contract, not cosmetics — one shape for every version
|
|
in the family."""
|
|
assert run_check(bad, "2026.08.30.1200", [], monkeypatch) != []
|
|
|
|
|
|
def test_a_version_in_the_future_FAILS(monkeypatch):
|
|
ahead = (datetime.now(timezone.utc) + timedelta(days=400)).strftime("%Y.%m.%d.%H%M")
|
|
failures = run_check(ahead, "2026.08.30.1200", [], monkeypatch)
|
|
assert len(failures) == 1
|
|
assert "in the future" in failures[0]
|
|
|
|
|
|
def test_a_version_minted_minutes_ago_is_NOT_in_the_future(monkeypatch):
|
|
"""The guard has to tolerate ordinary skew: the mint happens on a
|
|
workstation and the lane runs later, on another machine's clock."""
|
|
now = datetime.now(timezone.utc).strftime("%Y.%m.%d.%H%M")
|
|
assert run_check(now, "2026.08.30.1200", [], monkeypatch) == []
|
|
|
|
|
|
def test_nothing_changed_and_nothing_minted_PASSES(monkeypatch):
|
|
assert run_check("2026.09.01.2252", "2026.09.01.2252", [], monkeypatch) == []
|
|
|
|
|
|
def test_a_version_that_moved_with_no_content_change_is_NOT_a_failure(monkeypatch):
|
|
"""Deliberately a pass. A needless re-mint costs one cache refresh; failing
|
|
the lane over a harmless act is how a check earns a `--no-version` in
|
|
somebody's muscle memory and stops running at all. The implication that
|
|
matters is one-directional: content changed IMPLIES version moved."""
|
|
assert run_check("2026.09.01.2252", "2026.08.30.1200", [], monkeypatch) == []
|
|
|
|
|
|
def test_a_failed_diff_FAILS_rather_than_passing_quietly(monkeypatch):
|
|
"""A check that cannot run must not report the same thing as a check that
|
|
passed.
|
|
|
|
`rev-parse` is stubbed to SUCCEED so only the diff fails. Failing every git
|
|
call would trip the base-branch guard first and this would pass while
|
|
proving nothing about the diff arm.
|
|
"""
|
|
monkeypatch.setattr(
|
|
check_plugin, "_git",
|
|
lambda *a: (128, "fatal: bad object") if a[0] == "diff" else (0, ""),
|
|
)
|
|
monkeypatch.setattr(check_plugin, "manifest_version",
|
|
lambda ref=None: "2026.09.01.2252")
|
|
check_plugin.check_version_is_minted("origin/main")
|
|
assert len(check_plugin.failures) == 1
|
|
assert "git diff" in check_plugin.failures[0]
|
|
|
|
|
|
# ── The real manifest ──────────────────────────────────────────────────────
|
|
|
|
|
|
def test_the_shipped_manifest_carries_a_minted_version():
|
|
"""The end of the hand-bumped scheme, asserted on the real file. `0.1.48`
|
|
was the last of 48 numbers a person typed."""
|
|
version = json.loads(check_plugin.MANIFEST.read_text())["version"]
|
|
assert VERSION_RE.match(version), (
|
|
f"the shipped manifest says {version!r}, which is not a minted version"
|
|
)
|
|
|
|
|
|
def test_the_session_context_hook_still_reads_the_version_field():
|
|
"""The hook reads `.version` out of the manifest rather than deriving the
|
|
string some other way — if this has to move, the derivation went
|
|
somewhere it should not have."""
|
|
hook = (check_plugin.HOOKS_DIR / "scribe_session_context.sh").read_text()
|
|
assert re.search(r"scribe_json_pick[^\n]*'\.version'", hook)
|