From c6a540116147649b3f900300a1f55583e680e05a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ClodoCap=C3=A9o?= <159788250+ClodoCapeo@users.noreply.github.com> Date: Sun, 16 Aug 2026 20:05:39 +0200 Subject: [PATCH 1/2] feat(tests): add QueryMe arm of the QueryDescriptor contract test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QueryMe owns the QueryDescriptor schema but had no bras of its own in the cross-repo contract (ADR 013 G3): the golden fixture was copied to Blue and Orion, never to the repo that defines the shape it pins. Ship QueryMe's copy of querydescriptor_golden.json with a self-computed sha256 sidecar (its only local control — QueryMe is consomme_module_go: false and has no digest-pinned authenticity channel, ADR 013 G7) and a test validating it round-trips through queryme.descriptor.QueryDescriptor. Wire pytest -v into CI so the run log names each collected test with its outcome: plain `pytest --color=yes` prints one dot per file, which cannot tell a test that ran from a test that is present but never collected (ADR 013 RC5, the exact failure mode this tranche closes). Refs #331 Agent-Role: forge Agent-Thread: ADR013-T4 Work-Unit: ADR013-T4 Issue: 331 ADR-Revision: 013 --- .github/workflows/ci.yml | 7 +- tests/fixtures/querydescriptor_golden.json | 15 ++++ .../querydescriptor_golden.json.sha256 | 1 + tests/test_descriptor_contract.py | 74 +++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/querydescriptor_golden.json create mode 100644 tests/fixtures/querydescriptor_golden.json.sha256 create mode 100644 tests/test_descriptor_contract.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 314ba77..c21e278 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,12 @@ jobs: - name: Sync dev dependencies run: uv sync --frozen - name: Run pytest - run: uv run pytest --color=yes + # -v names every collected test in the log with its outcome + # (PASSED/FAILED/SKIPPED). Plain `pytest --color=yes` only prints a + # dot per file: a test that runs and a test that is present but + # never collected are indiscernible in that output (ADR 013 §1.3.2, + # RC5) — -v is the minimum needed to tell them apart. + run: uv run pytest --color=yes -v deps-audit: name: Dependency audit (pip-audit) diff --git a/tests/fixtures/querydescriptor_golden.json b/tests/fixtures/querydescriptor_golden.json new file mode 100644 index 0000000..c7bb272 --- /dev/null +++ b/tests/fixtures/querydescriptor_golden.json @@ -0,0 +1,15 @@ +{ + "table": "players", + "where": [ + { "column": "summoner_name", "op": "=", "value": "GIDEON" }, + { "column": "deleted_at", "op": "IS NULL", "value": null } + ], + "joins": [ + { "table": "scores", "on": ["id", "player_id"], "select": ["points"] } + ], + "select": ["summoner_name", "region"], + "order": [ + { "column": "points", "direction": "desc" } + ], + "limit": 5 +} diff --git a/tests/fixtures/querydescriptor_golden.json.sha256 b/tests/fixtures/querydescriptor_golden.json.sha256 new file mode 100644 index 0000000..f4fbe7c --- /dev/null +++ b/tests/fixtures/querydescriptor_golden.json.sha256 @@ -0,0 +1 @@ +23e10e33f0ebbb01a212c038b3ec24c3b546e81f7ddc4251009856907026c478 querydescriptor_golden.json diff --git a/tests/test_descriptor_contract.py b/tests/test_descriptor_contract.py new file mode 100644 index 0000000..50b1f33 --- /dev/null +++ b/tests/test_descriptor_contract.py @@ -0,0 +1,74 @@ +"""Cross-repo QueryDescriptor contract test — the QueryMe arm (ADR 013 §6 RC5). + +QueryMe owns the ``QueryDescriptor`` schema, so this arm is the reference +point the other two representations are pinned against: + +1. **QueryMe** ``QueryDescriptor`` Pydantic model (this file) — the golden + must validate and round-trip through it. +2. **Blue's preview executor** ``core.db.*`` builder chain + (``Blue/tests/test_querydescriptor_contract.py``) — pinned against a + byte-identical copy of the same golden. +3. **Orion** ``queryDescriptor`` Go struct + (``Orion/internal/runtime/compute_db_test.go::TestQueryDescriptor_GoldenParity``) + — pinned against its own byte-identical copy. + +A field rename or reorder on any side breaks its own arm loudly, so the +three representations can never silently drift apart. + +QueryMe does not consume the ``blue-runtime-go`` module (``pyproject.toml`` +has no path into it — Python cannot import a Go artefact), so this arm has +no digest-pinned authenticity channel back to Blue's published fixture +table: it is the ADR 013 G7 case, its inventory entry is **attested**, not +verified, and this test is the whole of its local control. The one thing +it *can* check on its own is that the copy of the golden it ships has not +drifted from the digest recorded alongside it +(``tests/fixtures/querydescriptor_golden.json.sha256``) — the same +self-consistency channel §3.4.1 gives every implementer, computed here +rather than trusted from a hand-copied literal. +""" + +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +from typing import Any + +from queryme.descriptor import QueryDescriptor + +_FIXTURES_DIR = Path(__file__).parent / "fixtures" +_GOLDEN_PATH = _FIXTURES_DIR / "querydescriptor_golden.json" +_SIDECAR_PATH = _FIXTURES_DIR / "querydescriptor_golden.json.sha256" + +_GOLDEN_BYTES = _GOLDEN_PATH.read_bytes() +_GOLDEN = json.loads(_GOLDEN_BYTES) + + +def _canonical(d: dict[str, Any]) -> dict[str, Any]: + """Normalise a descriptor dict for cross-arm comparison: drop a + top-level ``offset`` that is absent/None. The golden carries no + ``offset`` key at all; Pydantic fills the field with its ``None`` + default on validation, so a direct dict comparison would fail on that + field alone without touching the contract this test actually guards. + """ + return {k: v for k, v in d.items() if not (k == "offset" and v is None)} + + +def test_golden_fixture_matches_its_sidecar_digest() -> None: + """Local self-consistency channel (§3.4.1): the copy of the golden this + repo ships has not drifted from the digest recorded next to it. This is + QueryMe's only local control — it has no module-pinned digest to check + against (G7) — so the sidecar must be produced from the real file, not + hand-copied, or this test would pass while proving nothing. + """ + want = hashlib.sha256(_GOLDEN_BYTES).hexdigest() + got_line = _SIDECAR_PATH.read_text(encoding="utf-8").strip() + got = got_line.split()[0] + assert got == want, f"sidecar records {got}, golden file actually hashes to {want}" + + +def test_queryme_model_round_trips_golden() -> None: + """The schema owner accepts the golden and re-emits the same shape.""" + desc = QueryDescriptor.model_validate(_GOLDEN) + dumped = desc.model_dump(mode="json") + assert _canonical(dumped) == _canonical(_GOLDEN) From 360bd554aea676db20fcb8011417b7715b2b0898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ClodoCap=C3=A9o?= <159788250+ClodoCapeo@users.noreply.github.com> Date: Sun, 16 Aug 2026 20:22:12 +0200 Subject: [PATCH 2/2] fix(tests): drop false reorder claim, add .gitattributes for LF golden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bastion clearance on PR #15 found two gaps: - No .gitattributes: Blue and Orion both pin LF for their golden copy; QueryMe didn't, so a Windows clone with core.autocrlf=true silently recodes the golden to CRLF and the sidecar digest check goes red for the wrong reason — the obvious "fix" would be to loosen the byte comparison, which is exactly the relaxation this ADR forbids. - The docstring's "field rename or reorder... breaks its arm loudly" repeated Orion's comment verbatim without checking the "reorder" half against this arm's own mechanism: comparison is by parsed dict, so a reordered-but-equal golden re-hashed into a fresh sidecar stays green. Say what the arm actually catches instead of inheriting an inexact claim into a new file. Refs #331 Agent-Role: forge Agent-Thread: ADR013-T4 Work-Unit: ADR013-T4 Issue: 331 ADR-Revision: 013 --- .gitattributes | 7 +++++++ tests/test_descriptor_contract.py | 9 +++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4cb122c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# Normalize line endings: LF in the repo, regardless of OS checkout. +# A Windows clone with core.autocrlf=true would otherwise silently +# recode the golden fixtures to CRLF, breaking the byte-identical +# digest this repo's contract tests pin against (ADR 013 §6 RC5, +# tests/fixtures/querydescriptor_golden.json.sha256). + +* text=auto eol=lf diff --git a/tests/test_descriptor_contract.py b/tests/test_descriptor_contract.py index 50b1f33..105d3c9 100644 --- a/tests/test_descriptor_contract.py +++ b/tests/test_descriptor_contract.py @@ -12,8 +12,13 @@ (``Orion/internal/runtime/compute_db_test.go::TestQueryDescriptor_GoldenParity``) — pinned against its own byte-identical copy. -A field rename or reorder on any side breaks its own arm loudly, so the -three representations can never silently drift apart. +A field rename on any side breaks its own arm loudly, so the three +representations can never silently drift apart on the fields they carry. +Key order is not part of that guarantee: this arm compares parsed dicts, +never raw text, so a reordered-but-equal golden re-hashed into a fresh +sidecar would still pass — the contract this test pins is field names and +values, not on-disk byte order of an object's keys (only the golden +file's own bytes are pinned byte-for-byte, by the sidecar check above). QueryMe does not consume the ``blue-runtime-go`` module (``pyproject.toml`` has no path into it — Python cannot import a Go artefact), so this arm has