From 895077e50a53a7ca172c441d33c09d3ac739f43e Mon Sep 17 00:00:00 2001 From: Thomas Snyder <127151050+tom-snyder@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:15:37 -0400 Subject: [PATCH] test: publish ADR 0007 action snapshot fixture (#26) --- .github/workflows/ci.yml | 2 + PLAN.md | 10 + ...6-07-18-adr0007-action-snapshot-fixture.md | 57 ++++ scripts/verify_adr0007_action_fixture.py | 291 ++++++++++++++++++ .../adr0007_action_snapshot/PROVENANCE.md | 17 + .../fixture-manifest.json | 17 + .../affix_pools/action_snapshot_pool.json | 20 ++ .../barrel/action_snapshot_barrel.json | 46 +++ .../affix_pools/action_snapshot_pool.json | 20 ++ .../barrel/action_snapshot_barrel.json | 45 +++ tests/test_adr0007_action_fixture.py | 86 ++++++ 11 files changed, 611 insertions(+) create mode 100644 docs/evidence/2026-07-18-adr0007-action-snapshot-fixture.md create mode 100644 scripts/verify_adr0007_action_fixture.py create mode 100644 tests/fixtures/adr0007_action_snapshot/PROVENANCE.md create mode 100644 tests/fixtures/adr0007_action_snapshot/fixture-manifest.json create mode 100644 tests/fixtures/adr0007_action_snapshot/runtime_overlay/affix_pools/action_snapshot_pool.json create mode 100644 tests/fixtures/adr0007_action_snapshot/runtime_overlay/gun_parts/barrel/action_snapshot_barrel.json create mode 100644 tests/fixtures/adr0007_action_snapshot/source_overlay/affix_pools/action_snapshot_pool.json create mode 100644 tests/fixtures/adr0007_action_snapshot/source_overlay/gun_parts/barrel/action_snapshot_barrel.json create mode 100644 tests/test_adr0007_action_fixture.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b3669d..b4096b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,8 @@ jobs: python-version: "3.12" - name: Dependency-free v3 fixture contract run: python -I tests/fixture_contract/test_v3_fixture_contract.py + - name: Dependency-free ADR 0007 action-snapshot fixture contract + run: python -I tests/test_adr0007_action_fixture.py web: runs-on: ubuntu-latest diff --git a/PLAN.md b/PLAN.md index ce6f13f..3c647e2 100644 --- a/PLAN.md +++ b/PLAN.md @@ -809,6 +809,16 @@ not a v4 owner or release gate. See ADR 0007. legacy-oracle commit. **P4.D1 remains open:** the redistributable fixture and its owner-selected license have not yet been published, so the shipped verifier foundation alone does not complete the slice. +- **ADR 0007 action-snapshot evidence overlay IMPLEMENTED (2026-07-18; pending + merge):** DesignRNG independently authors a two-entity test overlay for the + Arms NG action-lifecycle GameTest. The public JSON-RPC surface reproduces + `load -> validate -> export -> reload -> validate`, then pins the exact + two-file canonical runtime diff and its hashes. The overlay's shotgun barrel + forces the runtime projectile path; its `while_ads` affix changes damage, + reload duration, and capacity. A dependency-free contract runs in required + CI, while the full exporter reproduction remains a local/sidecar integration + gate. This artifact selects no external-content license and explicitly does + **not** complete P4.D1. - **P2.3a handling mirror IMPLEMENTED (2026-07-16; rebased onto canonical identifiers 2026-07-18; pending independent grill):** Arms NG `dfb76e5` is the shipped/green consumer. DesignRNG now owns native part ordering, binary32 handling composition, hard diff --git a/docs/evidence/2026-07-18-adr0007-action-snapshot-fixture.md b/docs/evidence/2026-07-18-adr0007-action-snapshot-fixture.md new file mode 100644 index 0000000..e361397 --- /dev/null +++ b/docs/evidence/2026-07-18-adr0007-action-snapshot-fixture.md @@ -0,0 +1,57 @@ +# ADR 0007 action-snapshot evidence overlay + +Date: 2026-07-18 + +Status: implementation complete; merge and consumer pin pending. + +## Scope + +DesignRNG independently owns the source overlay under +`tests/fixtures/adr0007_action_snapshot/source_overlay`. It adds: + +- `gunrng:focused_cycle`, gated by `gunrng:while_ads`, with + `damage_mult=2`, `reload_time_mult=0.25`, and `magazine_size_add=4`; and +- `gunrng:action_snapshot_barrel`, compatible with the demo shotgun, with + zero authored spread multiplier and the runtime bullet-entity selector. + +The existing Arms NG demo pack is a test dependency only. It is not copied +into this repository and is not part of DesignRNG's authorship claim. + +## Reproduction + +`python scripts/verify_adr0007_action_fixture.py` stages the pinned base twice, +applies the source overlay to one copy, and drives only DesignRNG's public +JSON-RPC methods: + +`load -> validate -> export -> load exported pack -> validate` + +The canonical export diff contains exactly: + +- `affix_pools/action_snapshot_pool.json` +- `gun_parts/barrel/action_snapshot_barrel.json` + +Measured runtime-overlay hash: + +`14b1d008e0aad06500fab3b1d086e23b05432c190c790c8ef4a44a055bf1f90a` + +Both baseline and candidate validate with zero warnings. The committed +runtime bytes and manifest reproduce without `--bless`; a byte or manifest +mutation makes the verifier/contract fail. + +## Gates + +- dependency-free fixture contract: 3/3 +- root pytest fixture contract: 3/3 +- public-sidecar reproduction: PASS +- sidecar pytest selection: 39/39 +- web CI-equivalent: typecheck, lint, 607/607 Vitest, production build +- dependency-free v3 fixture contract: 20 tests, 2 platform skips + +The dependency-free overlay contract is an always-on GitHub CI step. Full +sidecar reproduction remains an integration gate because the transitional v3 +sidecar still requires sibling compatibility inputs. + +## Explicit non-claim + +This fixture chooses no license for external content and is not the licensed, +redistributable P4.D1 fixture-manifest artifact. P4.D1 remains open. diff --git a/scripts/verify_adr0007_action_fixture.py b/scripts/verify_adr0007_action_fixture.py new file mode 100644 index 0000000..a330d31 --- /dev/null +++ b/scripts/verify_adr0007_action_fixture.py @@ -0,0 +1,291 @@ +"""Reproduce the ADR 0007 action-snapshot runtime overlay via DesignRNG. + +This verifier talks only to DesignRNG's public JSON-RPC sidecar surface. It +stages the pinned Arms NG demo pack twice, applies the independently authored +source overlay to one copy, then runs load -> validate -> export -> reload -> +validate for both. The byte-diff between the two canonical exports is the +small runtime overlay consumed by the dedicated Arms NG GameTest smoke. + +Usage: + python scripts/verify_adr0007_action_fixture.py + python scripts/verify_adr0007_action_fixture.py --base-pack + python scripts/verify_adr0007_action_fixture.py --bless +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[1] +FIXTURE = ROOT / "tests" / "fixtures" / "adr0007_action_snapshot" +SOURCE_OVERLAY = FIXTURE / "source_overlay" +RUNTIME_OVERLAY = FIXTURE / "runtime_overlay" +MANIFEST = FIXTURE / "fixture-manifest.json" +EXPORT_MARKER = ".designrng-export.json" +EXPECTED_RUNTIME_FILES = ( + "affix_pools/action_snapshot_pool.json", + "gun_parts/barrel/action_snapshot_barrel.json", +) + + +def tree_files(root: Path, *, exclude: set[str] | None = None) -> dict[str, bytes]: + excluded = exclude or set() + return { + path.relative_to(root).as_posix(): path.read_bytes() + for path in sorted(root.rglob("*")) + if path.is_file() and path.relative_to(root).as_posix() not in excluded + } + + +def tree_sha256(files: dict[str, bytes]) -> str: + """DesignRNG's sorted, length-delimited regular-file content hash.""" + digest = hashlib.sha256() + for relative, content in sorted(files.items()): + name = relative.encode("utf-8") + digest.update(len(name).to_bytes(8, "big")) + digest.update(name) + digest.update(len(content).to_bytes(8, "big")) + digest.update(content) + return digest.hexdigest() + + +class Sidecar: + def __init__(self) -> None: + self._next_id = 1 + self._process = subprocess.Popen( + [sys.executable, str(ROOT / "sidecar" / "server.py")], + cwd=ROOT, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + encoding="utf-8", + ) + + def call(self, method: str, params: dict[str, Any] | None = None) -> Any: + assert self._process.stdin is not None + assert self._process.stdout is not None + request_id = self._next_id + self._next_id += 1 + self._process.stdin.write( + json.dumps({"id": request_id, "method": method, "params": params or {}}) + + "\n" + ) + self._process.stdin.flush() + line = self._process.stdout.readline() + if not line: + assert self._process.stderr is not None + stderr = self._process.stderr.read() + raise RuntimeError(f"DesignRNG sidecar exited without a response: {stderr}") + response = json.loads(line) + if response.get("id") != request_id: + raise RuntimeError(f"DesignRNG sidecar response id mismatch: {response}") + if "error" in response: + raise RuntimeError( + f"DesignRNG sidecar {method} failed: {response['error'].get('message')}" + ) + return response["result"] + + def close(self) -> None: + if self._process.stdin is not None: + self._process.stdin.close() + try: + self._process.wait(timeout=10) + except subprocess.TimeoutExpired: + self._process.kill() + self._process.wait(timeout=5) + + def __enter__(self) -> "Sidecar": + return self + + def __exit__(self, *_: object) -> None: + self.close() + + +def validate_and_export(source: Path, destination: Path) -> dict[str, Any]: + with Sidecar() as sidecar: + loaded = sidecar.call("load", {"packPath": str(source)}) + session = loaded["session"] + validation = sidecar.call("validate", {"expectedSession": session}) + if validation.get("exportBlocked") is not False: + raise RuntimeError( + f"DesignRNG validation blocked {source}: {validation.get('issues')}" + ) + exported = sidecar.call( + "export", + { + "destPath": str(destination), + "expectedSession": session, + "confirmOverwrite": False, + }, + ) + if exported.get("success") is not True: + raise RuntimeError( + f"DesignRNG export failed for {source}: {exported.get('errors')}" + ) + reloaded = sidecar.call("load", {"packPath": str(destination)}) + reload_validation = sidecar.call( + "validate", {"expectedSession": reloaded["session"]} + ) + if reload_validation.get("exportBlocked") is not False: + raise RuntimeError( + "DesignRNG rejected its own canonical export: " + f"{reload_validation.get('issues')}" + ) + return { + "sourceSession": session, + "validation": validation, + "export": exported, + "reloadValidation": reload_validation, + } + + +def copy_source_base(source: Path, destination: Path) -> None: + shutil.copytree( + source, + destination, + ignore=shutil.ignore_patterns("PROVENANCE.md", EXPORT_MARKER), + ) + + +def apply_overlay(overlay: Path, destination: Path) -> None: + for relative, content in tree_files(overlay).items(): + target = destination / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(content) + + +def git_head() -> str: + return subprocess.run( + ["git", "rev-parse", "HEAD"], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + encoding="utf-8", + ).stdout.strip() + + +def write_runtime_overlay(files: dict[str, bytes]) -> None: + if RUNTIME_OVERLAY.exists(): + shutil.rmtree(RUNTIME_OVERLAY) + for relative, content in files.items(): + target = RUNTIME_OVERLAY / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(content) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "--base-pack", + type=Path, + default=ROOT.parent / "Arms NG" / "demo_pack", + help="path to the Arms NG demo_pack dependency", + ) + parser.add_argument( + "--bless", + action="store_true", + help="replace the runtime overlay and its fixture-local hash manifest", + ) + args = parser.parse_args() + base_pack = args.base_pack.resolve() + if not (base_pack / "manifest.json").is_file(): + raise SystemExit(f"base pack is missing manifest.json: {base_pack}") + if not SOURCE_OVERLAY.is_dir(): + raise SystemExit(f"source overlay is missing: {SOURCE_OVERLAY}") + + with tempfile.TemporaryDirectory(prefix="designrng-adr0007-") as temporary: + temp = Path(temporary) + baseline_source = temp / "baseline-source" + candidate_source = temp / "candidate-source" + baseline_export = temp / "baseline-export" + candidate_export = temp / "candidate-export" + copy_source_base(base_pack, baseline_source) + copy_source_base(base_pack, candidate_source) + apply_overlay(SOURCE_OVERLAY, candidate_source) + + baseline_report = validate_and_export(baseline_source, baseline_export) + candidate_report = validate_and_export(candidate_source, candidate_export) + baseline_files = tree_files(baseline_export, exclude={EXPORT_MARKER}) + candidate_files = tree_files(candidate_export, exclude={EXPORT_MARKER}) + changed = { + relative: content + for relative, content in candidate_files.items() + if baseline_files.get(relative) != content + } + removed = sorted(set(baseline_files) - set(candidate_files)) + if removed: + raise RuntimeError(f"the source overlay unexpectedly removes runtime files: {removed}") + if tuple(sorted(changed)) != EXPECTED_RUNTIME_FILES: + raise RuntimeError( + "runtime overlay surface drifted: " + f"expected {EXPECTED_RUNTIME_FILES}, got {tuple(sorted(changed))}" + ) + + values = { + "format": "designrng-adr0007-test-fixture", + "version": 1, + "basePack": { + "id": "gunrng_demo_pack", + "treeSha256": tree_sha256(tree_files(baseline_source)), + }, + "sourceOverlaySha256": tree_sha256(tree_files(SOURCE_OVERLAY)), + "runtimeOverlaySha256": tree_sha256(changed), + "baselineExportSha256": tree_sha256(baseline_files), + "composedExportSha256": tree_sha256(candidate_files), + "exportedWithDesignRngCommit": git_head(), + "runtimeFiles": list(EXPECTED_RUNTIME_FILES), + } + + if args.bless: + print("BLESS: replacing ADR 0007 runtime overlay and fixture-local hashes") + write_runtime_overlay(changed) + MANIFEST.write_text( + json.dumps(values, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + else: + expected = json.loads(MANIFEST.read_text(encoding="utf-8")) + committed_runtime = tree_files(RUNTIME_OVERLAY) + if committed_runtime != changed: + raise RuntimeError( + "committed runtime overlay differs from the public-sidecar export; " + "review the diff and use --bless intentionally" + ) + # The exporter commit is historical evidence, not a requirement to + # stay on an old checkout. Every other field must reproduce now. + current_commit = values.pop("exportedWithDesignRngCommit") + recorded_commit = expected.pop("exportedWithDesignRngCommit") + if expected != values: + raise RuntimeError( + "fixture manifest does not match the reproduced hashes:\n" + f"expected={json.dumps(expected, indent=2)}\n" + f"actual={json.dumps(values, indent=2)}" + ) + print( + "DesignRNG exporter checkout: " + f"recorded={recorded_commit}, current={current_commit}" + ) + + print( + "ADR 0007 fixture PASS: " + f"baseline warnings={len(baseline_report['validation'].get('warnings', []))}, " + f"candidate warnings={len(candidate_report['validation'].get('warnings', []))}, " + f"runtime files={len(changed)}, " + f"runtime sha256={values['runtimeOverlaySha256']}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/fixtures/adr0007_action_snapshot/PROVENANCE.md b/tests/fixtures/adr0007_action_snapshot/PROVENANCE.md new file mode 100644 index 0000000..92fa53d --- /dev/null +++ b/tests/fixtures/adr0007_action_snapshot/PROVENANCE.md @@ -0,0 +1,17 @@ +# ADR 0007 action-snapshot test overlay provenance + +The files under `source_overlay/` are independently authored test data owned +by the DesignRNG project. They add one conditional affix and one test-only gun +part to the pinned Arms NG demo-pack fixture. The pre-existing demo pack is a +test dependency and is not included in this repository or in this authorship +claim. + +`runtime_overlay/` is generated exclusively through DesignRNG's public +sidecar load → validate → export surface by +`scripts/verify_adr0007_action_fixture.py`. The script exports both the clean +base fixture and the overlaid fixture, then retains only their two-file +canonical runtime diff. `fixture-manifest.json` uses a fixture-local hash +format; it is not the P4.D1 licensed fixture-manifest contract. + +This test data does not choose or assert a license for any external content. +P4.D1 remains open and is not claimed, implemented, or closed by this fixture. diff --git a/tests/fixtures/adr0007_action_snapshot/fixture-manifest.json b/tests/fixtures/adr0007_action_snapshot/fixture-manifest.json new file mode 100644 index 0000000..6c400ae --- /dev/null +++ b/tests/fixtures/adr0007_action_snapshot/fixture-manifest.json @@ -0,0 +1,17 @@ +{ + "format": "designrng-adr0007-test-fixture", + "version": 1, + "basePack": { + "id": "gunrng_demo_pack", + "treeSha256": "d56c67b9b2755ce0c2fb05506a00ac6adb7cbbf460f386cf073119a5b7087d85" + }, + "sourceOverlaySha256": "356862b75db698e653c0b6dc20531e0c02458976938b70eac0a6d6762546ef2b", + "runtimeOverlaySha256": "14b1d008e0aad06500fab3b1d086e23b05432c190c790c8ef4a44a055bf1f90a", + "baselineExportSha256": "bd8a5a36f49f45e10fa7a2325877821b08bc5f239b9988c184e111fafb6e65c1", + "composedExportSha256": "608388b4cb336002294e6217b6de2d570fc50e9dd741cef0756eea4ce0ea01f3", + "exportedWithDesignRngCommit": "b822089908e826b1d47ae538e0f334b3e9c1d051", + "runtimeFiles": [ + "affix_pools/action_snapshot_pool.json", + "gun_parts/barrel/action_snapshot_barrel.json" + ] +} diff --git a/tests/fixtures/adr0007_action_snapshot/runtime_overlay/affix_pools/action_snapshot_pool.json b/tests/fixtures/adr0007_action_snapshot/runtime_overlay/affix_pools/action_snapshot_pool.json new file mode 100644 index 0000000..3bf098d --- /dev/null +++ b/tests/fixtures/adr0007_action_snapshot/runtime_overlay/affix_pools/action_snapshot_pool.json @@ -0,0 +1,20 @@ +{ + "id": "gunrng:action_snapshot_pool", + "display_name": "Action Snapshot Test Pool", + "entries": [ + { + "affix_id": "gunrng:focused_cycle", + "translation_key": "Focused Cycle", + "weight": 1, + "minimum_rarity_bucket": "gunrng:common", + "modifiers": { + "damage_mult": 2.0, + "reload_time_mult": 0.25, + "magazine_size_add": 4 + }, + "condition": { + "type": "gunrng:while_ads" + } + } + ] +} diff --git a/tests/fixtures/adr0007_action_snapshot/runtime_overlay/gun_parts/barrel/action_snapshot_barrel.json b/tests/fixtures/adr0007_action_snapshot/runtime_overlay/gun_parts/barrel/action_snapshot_barrel.json new file mode 100644 index 0000000..2b9195f --- /dev/null +++ b/tests/fixtures/adr0007_action_snapshot/runtime_overlay/gun_parts/barrel/action_snapshot_barrel.json @@ -0,0 +1,46 @@ +{ + "id": "gunrng:action_snapshot_barrel", + "slot": "gunrng:barrel", + "style": "gunrng:voltspoke", + "part_group": "gunrng:voltspoke/nimble", + "base_stats": { + "base_damage": 0.0, + "fire_rate_rpm": 0.0, + "base_spread": 0.0, + "base_range": 0.0, + "base_recoil": 0.0, + "base_magazine_size": 0, + "base_reload_time": 0.0 + }, + "stat_modifiers": { + "spread_mult": 0.0 + }, + "model_ref": "gunrng:item/parts/voltspoke_barrel", + "valid_materials_tag": "", + "weight": 1.0, + "tags": [ + "barrel", + "action_snapshot_test" + ], + "anchor_offset": [ + -8.0, + -8.0, + -1.0 + ], + "bullet_entity": "gunrng:action_snapshot_runtime", + "compatible_gun_types": [ + "gunrng:shotgun" + ], + "snap_location": { + "position": [ + 8.0, + 8.0, + 1.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0 + ] + } +} diff --git a/tests/fixtures/adr0007_action_snapshot/source_overlay/affix_pools/action_snapshot_pool.json b/tests/fixtures/adr0007_action_snapshot/source_overlay/affix_pools/action_snapshot_pool.json new file mode 100644 index 0000000..11d4f3a --- /dev/null +++ b/tests/fixtures/adr0007_action_snapshot/source_overlay/affix_pools/action_snapshot_pool.json @@ -0,0 +1,20 @@ +{ + "id": "gunrng:action_snapshot_pool", + "display_name": "Action Snapshot Test Pool", + "entries": [ + { + "affix_id": "gunrng:focused_cycle", + "translation_key": "Focused Cycle", + "weight": 1.0, + "minimum_rarity_bucket": "common", + "modifiers": { + "damage_mult": 2.0, + "magazine_size_add": 4, + "reload_time_mult": 0.25 + }, + "condition": { + "type": "gunrng:while_ads" + } + } + ] +} diff --git a/tests/fixtures/adr0007_action_snapshot/source_overlay/gun_parts/barrel/action_snapshot_barrel.json b/tests/fixtures/adr0007_action_snapshot/source_overlay/gun_parts/barrel/action_snapshot_barrel.json new file mode 100644 index 0000000..30bb701 --- /dev/null +++ b/tests/fixtures/adr0007_action_snapshot/source_overlay/gun_parts/barrel/action_snapshot_barrel.json @@ -0,0 +1,45 @@ +{ + "id": "gunrng:action_snapshot_barrel", + "slot": "gunrng:barrel", + "style": "gunrng:voltspoke", + "part_group": "gunrng:voltspoke/nimble", + "base_stats": { + "base_damage": 0.0, + "fire_rate_rpm": 0.0, + "base_spread": 0.0, + "base_range": 0.0, + "base_recoil": 0.0, + "base_magazine_size": 0, + "base_reload_time": 0.0 + }, + "stat_modifiers": { + "spread_mult": 0.0 + }, + "model_ref": "gunrng:item/parts/voltspoke_barrel", + "weight": 1.0, + "tags": [ + "barrel", + "action_snapshot_test" + ], + "anchor_offset": [ + 0.0, + 0.0, + 0.0 + ], + "bullet_entity": "gunrng:action_snapshot_runtime", + "compatible_gun_types": [ + "gunrng:shotgun" + ], + "snap_location": { + "position": [ + 8.0, + 8.0, + 1.0 + ], + "rotation": [ + 0.0, + 0.0, + 0.0 + ] + } +} diff --git a/tests/test_adr0007_action_fixture.py b/tests/test_adr0007_action_fixture.py new file mode 100644 index 0000000..6230383 --- /dev/null +++ b/tests/test_adr0007_action_fixture.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import hashlib +import json +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +FIXTURE = ROOT / "tests" / "fixtures" / "adr0007_action_snapshot" + + +def _files(root: Path) -> dict[str, bytes]: + return { + path.relative_to(root).as_posix(): path.read_bytes() + for path in sorted(root.rglob("*")) + if path.is_file() + } + + +def _tree_sha256(files: dict[str, bytes]) -> str: + digest = hashlib.sha256() + for relative, content in sorted(files.items()): + name = relative.encode("utf-8") + digest.update(len(name).to_bytes(8, "big")) + digest.update(name) + digest.update(len(content).to_bytes(8, "big")) + digest.update(content) + return digest.hexdigest() + + +class ActionSnapshotFixtureTest(unittest.TestCase): + def test_hashes_and_runtime_surface_are_pinned(self) -> None: + manifest = json.loads( + (FIXTURE / "fixture-manifest.json").read_text(encoding="utf-8") + ) + source = _files(FIXTURE / "source_overlay") + runtime = _files(FIXTURE / "runtime_overlay") + + self.assertEqual("designrng-adr0007-test-fixture", manifest["format"]) + self.assertEqual(1, manifest["version"]) + self.assertEqual(manifest["sourceOverlaySha256"], _tree_sha256(source)) + self.assertEqual(manifest["runtimeOverlaySha256"], _tree_sha256(runtime)) + self.assertEqual( + [ + "affix_pools/action_snapshot_pool.json", + "gun_parts/barrel/action_snapshot_barrel.json", + ], + sorted(runtime), + ) + self.assertEqual(sorted(runtime), manifest["runtimeFiles"]) + + def test_required_test_semantics(self) -> None: + affix = json.loads( + (FIXTURE / "source_overlay/affix_pools/action_snapshot_pool.json") + .read_text(encoding="utf-8") + )["entries"][0] + modifiers = affix["modifiers"] + part = json.loads( + (FIXTURE / "source_overlay/gun_parts/barrel/action_snapshot_barrel.json") + .read_text(encoding="utf-8") + ) + + self.assertEqual({"type": "gunrng:while_ads"}, affix["condition"]) + self.assertEqual( + { + "damage_mult": 2.0, + "magazine_size_add": 4, + "reload_time_mult": 0.25, + }, + modifiers, + ) + self.assertEqual(0.0, part["stat_modifiers"]["spread_mult"]) + self.assertEqual("gunrng:action_snapshot_runtime", part["bullet_entity"]) + self.assertEqual(["gunrng:shotgun"], part["compatible_gun_types"]) + + def test_provenance_keeps_p4d1_open(self) -> None: + provenance = (FIXTURE / "PROVENANCE.md").read_text(encoding="utf-8") + self.assertIn("P4.D1 remains open", provenance) + self.assertIn( + "not the P4.D1 licensed fixture-manifest contract", provenance + ) + + +if __name__ == "__main__": + unittest.main()