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
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
||||||
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