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
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.
2. Launch the portable build with a fresh application-data directory.
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
unlabelled framing action is a stop condition and fails the checkpoint.
Record the run by copying
`tests/fixtures/m1-manual-acceptance-template.json` outside the repository,
filling it in and placing sanitized evidence below the report directory. Then
validate it without launching the application:
Record the run in the generated `manual-report.json` and place sanitized
evidence below the report directory. Then validate it without launching the
application:
```powershell
python scripts/validate_m1_manual_acceptance.py `
.build/acceptance/manual-report.json `
--output .build/acceptance/manual-validation.json
.build/acceptance-kit/manual-report.json `
--output .build/acceptance-kit/manual-validation.json
```
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
validator. Missing steps remain incomplete, stop conditions fail the run, and
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

View file

@ -75,13 +75,16 @@ evidence for:
- independent layer visibility/output state and ordered serialization;
- material-recipe matching plus a fail-closed PirateRay provenance and exact
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;
- a non-executing Windows artifact verifier that checks portable paths,
archive safety, sizes, hashes, the pinned Rayforge revision and every
packaged addon source while retaining the manual GUI gate;
- 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:

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()