test: generate checksummed M1 acceptance kit

This commit is contained in:
Codex 2026-10-21 08:45:03 +02:00
parent 6cd59ea624
commit d1eca70252
5 changed files with 172 additions and 8 deletions

View file

@ -80,6 +80,18 @@ blocks encoding.
Use only the isolated no-device profile and an empty work area. Do not connect Use only the isolated no-device profile and an empty work area. Do not connect
or configure a controller. or configure a controller.
Prepare a fresh, checksummed kit containing only the public synthetic inputs
and a new report template:
```powershell
python scripts/prepare_m1_acceptance_kit.py
```
The default output is `.build/acceptance-kit/`. The generator uses a fixed
public allowlist, records the repository commit and refuses to overwrite a
non-empty directory. It does not include the application bundle, private
LightBurn evidence or any machine data.
1. Pass the non-executing artifact verifier above. 1. Pass the non-executing artifact verifier above.
2. Launch the portable build with a fresh application-data directory. 2. Launch the portable build with a fresh application-data directory.
3. Confirm the UI identifies the state as offline/no-device. 3. Confirm the UI identifies the state as offline/no-device.
@ -97,15 +109,14 @@ or configure a controller.
Any device discovery prompt, physical-device status, transport attempt or Any device discovery prompt, physical-device status, transport attempt or
unlabelled framing action is a stop condition and fails the checkpoint. unlabelled framing action is a stop condition and fails the checkpoint.
Record the run by copying Record the run in the generated `manual-report.json` and place sanitized
`tests/fixtures/m1-manual-acceptance-template.json` outside the repository, evidence below the report directory. Then validate it without launching the
filling it in and placing sanitized evidence below the report directory. Then application:
validate it without launching the application:
```powershell ```powershell
python scripts/validate_m1_manual_acceptance.py ` python scripts/validate_m1_manual_acceptance.py `
.build/acceptance/manual-report.json ` .build/acceptance-kit/manual-report.json `
--output .build/acceptance/manual-validation.json --output .build/acceptance-kit/manual-validation.json
``` ```
The validator verifies evidence hashes and relative paths. It reports The validator verifies evidence hashes and relative paths. It reports

View file

@ -327,6 +327,9 @@ packaged addon source before the manual GUI checklist may begin.
The manual checklist now also has a versioned, fail-closed report template and The manual checklist now also has a versioned, fail-closed report template and
validator. Missing steps remain incomplete, stop conditions fail the run, and validator. Missing steps remain incomplete, stop conditions fail the run, and
hashed screenshots stay outside the public repository pending sanitization. hashed screenshots stay outside the public repository pending sanitization.
A reproducible kit generator copies only allowlisted synthetic inputs into the
ignored build area, records their hashes and refuses to overwrite an existing
test run.
## Decision rules ## Decision rules

View file

@ -75,13 +75,16 @@ evidence for:
- independent layer visibility/output state and ordered serialization; - independent layer visibility/output state and ordered serialization;
- material-recipe matching plus a fail-closed PirateRay provenance and exact - material-recipe matching plus a fail-closed PirateRay provenance and exact
machine/source/lens/correction compatibility gate; machine/source/lens/correction compatibility gate;
- one consolidated no-device acceptance report that reruns all 79 public - one consolidated no-device acceptance report that reruns all 83 public
tests and the leak guard, then hashes the regression evidence; tests and the leak guard, then hashes the regression evidence;
- a non-executing Windows artifact verifier that checks portable paths, - a non-executing Windows artifact verifier that checks portable paths,
archive safety, sizes, hashes, the pinned Rayforge revision and every archive safety, sizes, hashes, the pinned Rayforge revision and every
packaged addon source while retaining the manual GUI gate; packaged addon source while retaining the manual GUI gate;
- a versioned manual desktop-acceptance template and validator that cannot - a versioned manual desktop-acceptance template and validator that cannot
pass missing steps, connected hardware, changed evidence or stop conditions. pass missing steps, connected hardware, changed evidence or stop conditions;
- a deterministic acceptance-kit generator that copies only four allowlisted
public synthetic inputs, hashes them and refuses to overwrite an existing
run directory.
The remaining M1 gates are deliberately explicit: The remaining M1 gates are deliberately explicit:

View file

@ -0,0 +1,92 @@
"""Create a checksummed M1 desktop acceptance kit from public fixtures only."""
from __future__ import annotations
import argparse
import hashlib
import json
import shutil
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
KIT_FILES = {
"manual-report.json": "tests/fixtures/m1-manual-acceptance-template.json",
"inputs/synthetic-mopa-import.lbrn2": "tests/fixtures/synthetic-mopa-import.lbrn2",
"inputs/synthetic-jcz-device.lbdev": "tests/fixtures/synthetic-jcz-device.lbdev",
"inputs/synthetic-pirateray-recipe-metadata.json": (
"tests/fixtures/synthetic-pirateray-recipe-metadata.json"
),
}
def _sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _repository_commit() -> str:
result = subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=ROOT,
check=True,
capture_output=True,
text=True,
)
return result.stdout.strip()
def create_kit(output: Path, repository_commit: str | None = None) -> dict:
output = output.resolve()
if output.exists() and any(output.iterdir()):
raise ValueError(f"acceptance-kit directory is not empty: {output}")
output.mkdir(parents=True, exist_ok=True)
records = []
for destination, source_name in KIT_FILES.items():
source = ROOT / source_name
target = output / destination
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source, target)
records.append({
"path": destination,
"source": source_name,
"classification": "synthetic-public",
"bytes": target.stat().st_size,
"sha256": _sha256(target),
})
manifest = {
"schema_version": 1,
"kit": "M1 packaged no-device desktop acceptance",
"repository_commit": repository_commit or _repository_commit(),
"hardware_access_authorized": False,
"contains_private_evidence": False,
"files": records,
}
manifest_path = output / "kit-manifest.json"
manifest_path.write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
newline="\n",
)
return manifest
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--output",
type=Path,
default=ROOT / ".build" / "acceptance-kit",
)
args = parser.parse_args()
result = create_kit(args.output)
print(f"kit={args.output.resolve()}")
print(f"files={len(result['files'])}")
print(f"repository_commit={result['repository_commit']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,55 @@
import hashlib
import json
import tempfile
import unittest
from pathlib import Path
from scripts.prepare_m1_acceptance_kit import KIT_FILES, ROOT, create_kit
class M1AcceptanceKitTests(unittest.TestCase):
def test_kit_contains_only_allowlisted_public_inputs(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
output = Path(directory) / "kit"
manifest = create_kit(output, repository_commit="a" * 40)
self.assertFalse(manifest["hardware_access_authorized"])
self.assertFalse(manifest["contains_private_evidence"])
self.assertEqual(
{record["path"] for record in manifest["files"]},
set(KIT_FILES),
)
for record in manifest["files"]:
path = output / record["path"]
self.assertEqual(record["bytes"], path.stat().st_size)
self.assertEqual(record["sha256"], hashlib.sha256(path.read_bytes()).hexdigest())
self.assertEqual(record["classification"], "synthetic-public")
def test_manifest_is_reproducible_for_same_commit(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
root = Path(directory)
create_kit(root / "one", repository_commit="b" * 40)
create_kit(root / "two", repository_commit="b" * 40)
one = (root / "one" / "kit-manifest.json").read_bytes()
two = (root / "two" / "kit-manifest.json").read_bytes()
self.assertEqual(one, two)
def test_nonempty_output_is_not_overwritten(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
output = Path(directory) / "kit"
output.mkdir()
sentinel = output / "keep.txt"
sentinel.write_text("keep", encoding="utf-8")
with self.assertRaisesRegex(ValueError, "not empty"):
create_kit(output, repository_commit="c" * 40)
self.assertEqual(sentinel.read_text(encoding="utf-8"), "keep")
def test_manifest_on_disk_matches_returned_data(self):
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
output = Path(directory) / "kit"
expected = create_kit(output, repository_commit="d" * 40)
actual = json.loads((output / "kit-manifest.json").read_text(encoding="utf-8"))
self.assertEqual(actual, expected)
if __name__ == "__main__":
unittest.main()