test: generate checksummed M1 acceptance kit
This commit is contained in:
parent
6cd59ea624
commit
d1eca70252
5 changed files with 172 additions and 8 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
92
scripts/prepare_m1_acceptance_kit.py
Normal file
92
scripts/prepare_m1_acceptance_kit.py
Normal 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())
|
||||
55
tests/test_m1_acceptance_kit.py
Normal file
55
tests/test_m1_acceptance_kit.py
Normal 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()
|
||||
Loading…
Add table
Reference in a new issue