build: add no-device M1 acceptance checkpoint
This commit is contained in:
parent
7dc1abc8a7
commit
f8d3c889e3
11 changed files with 457 additions and 31 deletions
19
README.md
19
README.md
|
|
@ -41,11 +41,12 @@ screenshots, the closed-application preferences folder, machine photos, and
|
|||
operator notes directly from the actual control computer. The bundle and
|
||||
exported preferences are already preserved and analyzed.
|
||||
|
||||
The offline M1 reuse proofs are progressing: the Rayforge/MeerK40t path now
|
||||
has deterministic mock encoding, fail-closed JCZ profile and parameter gates,
|
||||
list buffering, cancellation/recovery, and a transport-free workflow preview.
|
||||
The remaining gates are a private known-working `.lbrn2` project and a reviewed
|
||||
Windows GTK runtime for the packaged no-device workflow test. See the
|
||||
The offline M1 reuse proofs now pass as one consolidated acceptance checkpoint:
|
||||
the Rayforge/MeerK40t path has deterministic mock encoding, fail-closed JCZ
|
||||
profile and parameter gates, list buffering, cancellation/recovery, layer and
|
||||
recipe semantics, and a transport-free workflow preview. The remaining gates
|
||||
are a private known-working `.lbrn2` project and a reviewed Windows GTK runtime
|
||||
for the packaged no-device workflow test. See the
|
||||
[Phase 0 status](docs/status/phase-0.md) and
|
||||
[Rayforge GUI preflight](docs/research/rayforge-windows-gui-preflight.md).
|
||||
|
||||
|
|
@ -74,10 +75,9 @@ hardware-free:
|
|||
|
||||
1. collect known-working `.lbrn2` projects and the remaining screenshots,
|
||||
photos, preferences folder and operator notes;
|
||||
2. extend the synthetic import tests with the now-observed `QPulseWidth`
|
||||
evidence, without assuming its `.lbrn2` representation;
|
||||
3. define the minimum Rayforge JCZ device-profile/import extension and finish
|
||||
controller-list buffering and safe state ordering offline;
|
||||
2. obtain or produce a reviewed Windows Rayforge build artifact without
|
||||
installing a toolchain through this repository;
|
||||
3. run and record the packaged no-device desktop interaction checklist;
|
||||
4. accept, revise, or reject the proposed reuse architecture from evidence;
|
||||
5. write and review a separate hardware protocol before any live controller
|
||||
communication.
|
||||
|
|
@ -103,6 +103,7 @@ project workspace or development destination.
|
|||
- [LightBurn-to-Rayforge UX capability map](docs/research/lightburn-rayforge-ux-capability-map.md)
|
||||
- [Rayforge layer visibility and output spike](docs/research/rayforge-layer-model-spike.md)
|
||||
- [Rayforge material-recipe model spike](docs/research/rayforge-recipe-model-spike.md)
|
||||
- [M1 no-device build and acceptance checkpoint](docs/research/m1-no-device-acceptance.md)
|
||||
- [Proposed Rayforge/BalorMK reuse decision](docs/adr/0002-reuse-rayforge-and-balormk.md)
|
||||
- [Live USB enumeration](docs/research/live-usb-enumeration-2026-10-17.md)
|
||||
- [LightBurn device-export analysis](docs/research/lightburn-device-exports-2026-10-19.md)
|
||||
|
|
|
|||
|
|
@ -118,8 +118,13 @@ Subsequent offline evidence:
|
|||
canvas visibility and job participation are already independent serialized
|
||||
states; LightBurn-style layer Output is a narrow aggregate UI concern, not a
|
||||
new pipeline or document-model requirement.
|
||||
- [M1 no-device acceptance checkpoint](../research/m1-no-device-acceptance.md):
|
||||
the public regression suite and leak guard now form one transport-free gate
|
||||
over addon lifecycle, workflow, layers, recipes, encoding, negative cases,
|
||||
cancellation/recovery and synthetic imports. The packaged Windows GUI and a
|
||||
known-working private project remain external gates.
|
||||
|
||||
These results support continuing toward complete list buffering and production
|
||||
integration, but they do not yet satisfy every proof obligation or change this
|
||||
ADR's proposed status.
|
||||
These results satisfy the current model, buffering and no-device integration
|
||||
proofs. They do not yet satisfy the packaged human-interaction or real-project
|
||||
import obligations, so this ADR remains proposed.
|
||||
|
||||
|
|
|
|||
91
docs/research/m1-no-device-acceptance.md
Normal file
91
docs/research/m1-no-device-acceptance.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# M1 no-device build and acceptance checkpoint
|
||||
|
||||
- Date: 2026-10-20
|
||||
- Rayforge commit: `c28918c87f8273dd1d244f5a057addc192807fa5`
|
||||
- Scope: packaged Windows desktop and consolidated offline acceptance
|
||||
- Hardware access: prohibited
|
||||
|
||||
## Goal
|
||||
|
||||
Close the remaining software-controlled part of M1 with one reproducible
|
||||
checkpoint rather than treating isolated spikes as a finished desktop flow.
|
||||
The checkpoint has two outputs:
|
||||
|
||||
1. a pinned Windows Rayforge bundle containing the external PirateRay
|
||||
no-device addon;
|
||||
2. one machine-readable acceptance report covering all current transport-free
|
||||
evidence.
|
||||
|
||||
Neither output authorizes USB discovery, controller commands, galvo movement,
|
||||
physical framing, arming or laser output.
|
||||
|
||||
## On-demand build
|
||||
|
||||
Run the build only when a Windows artifact is required:
|
||||
|
||||
```powershell
|
||||
scripts/build_rayforge_windows_checkpoint.ps1
|
||||
```
|
||||
|
||||
The script fetches the exact revision from `upstreams/rayforge.json`, stages
|
||||
the no-device addon into the ignored build checkout, invokes Rayforge's
|
||||
upstream MSYS2 build and creates a portable ZIP plus installer hashes under
|
||||
`.build/artifacts/`. Normal clone, documentation and test workflows do not
|
||||
download or install the build stack.
|
||||
|
||||
The checkpoint script never installs the Windows toolchain. If the reviewed
|
||||
runtime is unavailable, it stops and reports the missing prerequisite. Any
|
||||
toolchain preparation remains a separate, explicitly invoked upstream action
|
||||
or should happen on a dedicated build runner.
|
||||
|
||||
Before staging, the script rejects transport imports in the addon. The build
|
||||
manifest records the exact upstream commit and hashes every addon source file,
|
||||
the portable archive and installer. It does not claim the GUI has been tested.
|
||||
|
||||
## Automated acceptance
|
||||
|
||||
Run with the isolated Rayforge Python environment:
|
||||
|
||||
```powershell
|
||||
python scripts/run_m1_no_device_acceptance.py <rayforge-checkout> `
|
||||
--output .build/artifacts/m1-no-device-acceptance.json
|
||||
```
|
||||
|
||||
The runner executes the complete repository test suite and public-repository
|
||||
guard, then collects and hashes the regression-verified golden reports for the
|
||||
addon lifecycle, graphical workflow model, layer model, recipe model, JCZ
|
||||
equivalence, negative cases, cancellation/recovery and both LightBurn import
|
||||
spikes. It passes only when the repository checks pass, transport remains
|
||||
absent, the controller backend remains unreachable, negative cases fail
|
||||
closed, cancellation requires recovery and incomplete LightBurn import still
|
||||
blocks encoding.
|
||||
|
||||
## Manual packaged-app checklist
|
||||
|
||||
Use only the isolated no-device profile and an empty work area. Do not connect
|
||||
or configure a controller.
|
||||
|
||||
1. Verify the artifact and addon hashes against the build manifest.
|
||||
2. Launch the portable build with a fresh application-data directory.
|
||||
3. Confirm the UI identifies the state as offline/no-device.
|
||||
4. Open synthetic geometry and inspect the 110 × 110 mm synthetic field.
|
||||
5. Name, color, select and reorder layers.
|
||||
6. Verify canvas visibility and output participation are independent.
|
||||
7. Edit power, speed, frequency and pulse width and confirm Hz/ns labels.
|
||||
8. Apply the synthetic unverified recipe; confirm mismatched compatibility is
|
||||
blocked and no approved label appears.
|
||||
9. Preview enabled geometry, ordering, hatch and out-of-field diagnostics.
|
||||
10. Save and reopen; confirm geometry, order and supported settings survive.
|
||||
11. Start simulation, hold, resume, cancel and explicitly recover.
|
||||
12. Exit and preserve only the sanitized acceptance report and screenshots.
|
||||
|
||||
Any device discovery prompt, physical-device status, transport attempt or
|
||||
unlabelled framing action is a stop condition and fails the checkpoint.
|
||||
|
||||
## Remaining external gates
|
||||
|
||||
- A known-working private `.lbrn2` is still required before implementing
|
||||
project-level `QPulseWidth` mapping.
|
||||
- Human interaction results from the packaged build must be recorded before
|
||||
ADR 0002 can be accepted.
|
||||
- Physical controller behavior remains outside M1.
|
||||
|
|
@ -45,10 +45,11 @@ revision into the ignored `.build/upstreams/` cache only when explicitly
|
|||
invoked. Its default action prepares source only; it neither installs the
|
||||
Windows toolchain nor starts a build.
|
||||
|
||||
Windows dependencies and bundle creation require separate
|
||||
`-InstallWindowsDependencies` and `-BuildWindowsBundle` switches. This keeps a
|
||||
multi-gigabyte MSYS2/UCRT64 toolchain out of normal clone, test and documentation
|
||||
workflows and makes a CI-produced bundle the preferred interaction-test input.
|
||||
The repository scripts do not install Windows dependencies. Bundle creation
|
||||
requires the explicit `-BuildWindowsBundle` switch and an already reviewed,
|
||||
prepared runtime. This keeps a multi-gigabyte MSYS2/UCRT64 toolchain out of
|
||||
normal clone, test and documentation workflows and makes a CI-produced bundle
|
||||
the preferred interaction-test input.
|
||||
|
||||
## Next action
|
||||
|
||||
|
|
|
|||
|
|
@ -316,6 +316,12 @@ has no MSYS2/UCRT64 Rayforge runtime. No dependencies were installed and no
|
|||
GUI success is claimed. The packaged interaction test therefore remains
|
||||
blocked on a reviewed Windows GTK runtime, not on controller behavior.
|
||||
|
||||
The transport-free portions are now consolidated by the
|
||||
[M1 no-device acceptance checkpoint](research/m1-no-device-acceptance.md).
|
||||
It reruns the public suite and leak guard and hashes the validated regression
|
||||
reports. The on-demand build entry point fetches pinned source and stages the
|
||||
addon but deliberately refuses to install a missing Windows toolchain.
|
||||
|
||||
## Decision rules
|
||||
|
||||
- A simulator test precedes every equivalent hardware behavior.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Phase 0 status
|
||||
|
||||
Last updated: 2026-10-19
|
||||
Last updated: 2026-10-20
|
||||
|
||||
## Completed
|
||||
|
||||
|
|
@ -72,13 +72,19 @@ evidence for:
|
|||
- fail-closed profile and encoder negative cases;
|
||||
- Rayforge workflow-model serialization and preview behavior for power,
|
||||
frequency and pulse width.
|
||||
- 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 69 public
|
||||
tests and the leak guard, then hashes the regression evidence.
|
||||
|
||||
The remaining M1 gates are deliberately explicit:
|
||||
|
||||
- a private known-working `.lbrn2` project is still required before mapping
|
||||
project-level `QPulseWidth` into workflow settings;
|
||||
- packaged Windows/GTK interaction is blocked until a reviewed Rayforge
|
||||
runtime with PyGObject and MSYS2/UCRT64 is available;
|
||||
runtime with PyGObject and MSYS2/UCRT64 is available; the repository build
|
||||
entry point fetches pinned source but never installs that runtime;
|
||||
- graphical workflow, import, lens correction and controller protocol
|
||||
behavior remain unproven at the physical boundary;
|
||||
- ADR 0002 remains **Proposed** until those gaps are resolved or rejected by
|
||||
|
|
|
|||
111
scripts/build_rayforge_windows_checkpoint.ps1
Normal file
111
scripts/build_rayforge_windows_checkpoint.ps1
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[string]$CacheRoot,
|
||||
[string]$ArtifactRoot
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repositoryRoot = Split-Path -Parent $PSScriptRoot
|
||||
$upstreamManifest = Get-Content -Raw -LiteralPath (
|
||||
Join-Path $repositoryRoot "upstreams\rayforge.json"
|
||||
) | ConvertFrom-Json
|
||||
$shortCommit = $upstreamManifest.commit.Substring(0, 12)
|
||||
|
||||
if (-not $CacheRoot) {
|
||||
$CacheRoot = Join-Path $repositoryRoot ".build\upstreams"
|
||||
}
|
||||
if (-not $ArtifactRoot) {
|
||||
$ArtifactRoot = Join-Path $repositoryRoot ".build\artifacts"
|
||||
}
|
||||
|
||||
& (Join-Path $PSScriptRoot "prepare_rayforge_build.ps1") `
|
||||
-CacheRoot $CacheRoot
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Pinned Rayforge source preparation failed"
|
||||
}
|
||||
|
||||
$checkout = Join-Path $CacheRoot "rayforge-$shortCommit"
|
||||
$verifiedCommit = (& git -C $checkout rev-parse HEAD).Trim()
|
||||
if ($verifiedCommit -ne $upstreamManifest.commit) {
|
||||
throw "Rayforge pin mismatch before packaging"
|
||||
}
|
||||
|
||||
$addonSource = Join-Path $repositoryRoot (
|
||||
"spikes\rayforge-jcz-no-device-addon"
|
||||
)
|
||||
$addonText = Get-ChildItem -LiteralPath $addonSource -Recurse -File |
|
||||
Where-Object { $_.Extension -eq ".py" } |
|
||||
ForEach-Object { Get-Content -Raw -LiteralPath $_.FullName }
|
||||
$forbidden = @("import usb", "import serial", "import socket", "pyusb")
|
||||
foreach ($needle in $forbidden) {
|
||||
if (($addonText -join "`n").ToLowerInvariant().Contains($needle)) {
|
||||
throw "No-device addon contains forbidden transport token: $needle"
|
||||
}
|
||||
}
|
||||
|
||||
$addonDestination = Join-Path $checkout (
|
||||
"rayforge\builtin_addons\rayforge-addon-the-pirate-ray-jcz"
|
||||
)
|
||||
if ($PSCmdlet.ShouldProcess($addonDestination, "Stage no-device addon")) {
|
||||
New-Item -ItemType Directory -Force -Path $addonDestination | Out-Null
|
||||
Copy-Item -Path (Join-Path $addonSource "*") `
|
||||
-Destination $addonDestination -Recurse -Force
|
||||
}
|
||||
|
||||
$prepareArguments = @{
|
||||
CacheRoot = $CacheRoot
|
||||
BuildWindowsBundle = $true
|
||||
}
|
||||
& (Join-Path $PSScriptRoot "prepare_rayforge_build.ps1") @prepareArguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Pinned Rayforge Windows bundle build failed"
|
||||
}
|
||||
|
||||
$bundleName = "rayforge-vpirateray-$shortCommit"
|
||||
$bundleDirectory = Join-Path $checkout "dist\$bundleName"
|
||||
$application = Join-Path $bundleDirectory "$bundleName.exe"
|
||||
$installer = Join-Path $checkout "dist\$bundleName-installer.exe"
|
||||
if (-not (Test-Path -LiteralPath $application)) {
|
||||
throw "Expected packaged application was not produced: $application"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $installer)) {
|
||||
throw "Expected installer was not produced: $installer"
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $ArtifactRoot | Out-Null
|
||||
$archive = Join-Path $ArtifactRoot "$bundleName.zip"
|
||||
if ($PSCmdlet.ShouldProcess($archive, "Create portable checkpoint archive")) {
|
||||
Compress-Archive -LiteralPath $bundleDirectory -DestinationPath $archive -Force
|
||||
}
|
||||
|
||||
$addonFiles = Get-ChildItem -LiteralPath $addonSource -Recurse -File |
|
||||
Sort-Object FullName |
|
||||
ForEach-Object {
|
||||
[ordered]@{
|
||||
path = $_.FullName.Substring($addonSource.Length + 1).Replace("\", "/")
|
||||
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName).Hash.ToLowerInvariant()
|
||||
}
|
||||
}
|
||||
$result = [ordered]@{
|
||||
checkpoint = "Rayforge Windows no-device bundle"
|
||||
rayforge_commit = $verifiedCommit
|
||||
addon = "the_pirate_ray_jcz"
|
||||
addon_source_files = $addonFiles
|
||||
hardware_access = $false
|
||||
transport = $null
|
||||
bundle = [ordered]@{
|
||||
path = $archive
|
||||
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $archive).Hash.ToLowerInvariant()
|
||||
bytes = (Get-Item -LiteralPath $archive).Length
|
||||
}
|
||||
installer = [ordered]@{
|
||||
path = $installer
|
||||
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $installer).Hash.ToLowerInvariant()
|
||||
bytes = (Get-Item -LiteralPath $installer).Length
|
||||
}
|
||||
manual_interaction_required = $true
|
||||
}
|
||||
$manifestPath = Join-Path $ArtifactRoot "rayforge-windows-checkpoint.json"
|
||||
$result | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $manifestPath -Encoding utf8
|
||||
Write-Output "Checkpoint manifest: $manifestPath"
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[string]$CacheRoot,
|
||||
[switch]$InstallWindowsDependencies,
|
||||
[switch]$BuildWindowsBundle
|
||||
)
|
||||
|
||||
|
|
@ -58,7 +57,7 @@ if ($verifiedCommit -ne $manifest.commit) {
|
|||
Write-Output "Rayforge source ready at $checkout"
|
||||
Write-Output "Verified commit: $verifiedCommit"
|
||||
|
||||
if (-not $InstallWindowsDependencies -and -not $BuildWindowsBundle) {
|
||||
if (-not $BuildWindowsBundle) {
|
||||
Write-Output "No dependencies were installed and no build was started."
|
||||
return
|
||||
}
|
||||
|
|
@ -70,19 +69,10 @@ if (-not (Test-Path -LiteralPath $bash)) {
|
|||
|
||||
$msysCheckout = (& $bash -lc "cygpath -u '$checkout'").Trim()
|
||||
|
||||
if ($InstallWindowsDependencies) {
|
||||
if ($PSCmdlet.ShouldProcess("C:\msys64", "Install Rayforge Windows build dependencies")) {
|
||||
& $bash -lc "cd '$msysCheckout' && bash scripts/win/win_setup.sh"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Rayforge Windows dependency setup failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($BuildWindowsBundle) {
|
||||
$environmentFile = Join-Path $checkout ".msys2_env"
|
||||
if (-not (Test-Path -LiteralPath $environmentFile)) {
|
||||
throw "Windows dependencies are not prepared. Re-run with -InstallWindowsDependencies."
|
||||
throw "A reviewed Rayforge Windows build runtime is not prepared. No dependencies were installed."
|
||||
}
|
||||
|
||||
if ($PSCmdlet.ShouldProcess($checkout, "Build pinned Rayforge Windows bundle")) {
|
||||
|
|
|
|||
156
scripts/run_m1_no_device_acceptance.py
Normal file
156
scripts/run_m1_no_device_acceptance.py
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
"""Consolidate the transport-free M1 regression evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PINNED_COMMIT = "c28918c87f8273dd1d244f5a057addc192807fa5"
|
||||
|
||||
EVIDENCE = {
|
||||
"addon_lifecycle": "rayforge-addon-spike.json",
|
||||
"graphical_workflow_model": "rayforge-graphical-workflow.json",
|
||||
"layer_model": "rayforge-layer-model.json",
|
||||
"recipe_model": "rayforge-recipe-model.json",
|
||||
"jcz_equivalence": "rayforge-jcz-equivalence.json",
|
||||
"jcz_negative_cases": "rayforge-jcz-negative-cases.json",
|
||||
"cancel_recovery": "rayforge-jcz-cancel-recovery.json",
|
||||
"lightburn_mopa_import": "rayforge-lightburn-mopa-import.json",
|
||||
"lightburn_device_import": "rayforge-lightburn-device-import.json",
|
||||
}
|
||||
|
||||
|
||||
def _commit_of(source: Path) -> str:
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(source), "rev-parse", "HEAD"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def _run_check(command: list[str], environment: dict[str, str]) -> dict:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=ROOT,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
env=environment,
|
||||
)
|
||||
combined = (result.stdout + result.stderr).strip()
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(combined or f"check failed: {command}")
|
||||
return {
|
||||
"status": "passed",
|
||||
"command": command,
|
||||
"output_tail": combined.splitlines()[-4:],
|
||||
}
|
||||
|
||||
|
||||
def _load_evidence() -> dict[str, dict]:
|
||||
fixture_dir = ROOT / "tests" / "fixtures"
|
||||
results = {}
|
||||
for name, filename in EVIDENCE.items():
|
||||
path = fixture_dir / filename
|
||||
results[name] = {
|
||||
"path": path.relative_to(ROOT).as_posix(),
|
||||
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
|
||||
"report": json.loads(path.read_text(encoding="utf-8")),
|
||||
}
|
||||
return results
|
||||
|
||||
|
||||
def run(source: Path) -> dict:
|
||||
commit = _commit_of(source)
|
||||
if commit != PINNED_COMMIT:
|
||||
raise RuntimeError(f"expected Rayforge {PINNED_COMMIT}, found {commit}")
|
||||
|
||||
temp_dir = ROOT / ".build" / "acceptance-temp"
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
environment = os.environ.copy()
|
||||
environment["TEMP"] = str(temp_dir)
|
||||
environment["TMP"] = str(temp_dir)
|
||||
|
||||
repository_checks = {
|
||||
"unit_tests": _run_check(
|
||||
[sys.executable, "-m", "unittest", "discover", "-s", "tests"],
|
||||
environment,
|
||||
),
|
||||
"public_repo_guard": _run_check(
|
||||
[sys.executable, "scripts/check_public_repo.py", "."],
|
||||
environment,
|
||||
),
|
||||
}
|
||||
evidence = _load_evidence()
|
||||
|
||||
graphical = evidence["graphical_workflow_model"]["report"]
|
||||
negative = evidence["jcz_negative_cases"]["report"]
|
||||
cancellation = evidence["cancel_recovery"]["report"]
|
||||
device_import = evidence["lightburn_device_import"]["report"]
|
||||
transport_free = all(
|
||||
item["report"].get("transport") is None
|
||||
for item in evidence.values()
|
||||
if "transport" in item["report"]
|
||||
)
|
||||
controller_unreachable = (
|
||||
graphical["controller_backend_reachable"] is False
|
||||
)
|
||||
checks = {
|
||||
"repository_checks_passed": all(
|
||||
item["status"] == "passed" for item in repository_checks.values()
|
||||
),
|
||||
"transport_free": transport_free,
|
||||
"controller_backend_unreachable": controller_unreachable,
|
||||
"negative_cases_fail_closed": negative["all_failed_closed"],
|
||||
"cancel_requires_recovery": cancellation[
|
||||
"restart_without_recovery_rejected"
|
||||
],
|
||||
"incomplete_import_blocks_encoding": not device_import[
|
||||
"addon_profile_seam"
|
||||
]["encoding_allowed"],
|
||||
}
|
||||
passed = all(checks.values())
|
||||
return {
|
||||
"checkpoint": "M1 no-device acceptance",
|
||||
"status": "passed" if passed else "failed",
|
||||
"rayforge_commit": commit,
|
||||
"hardware_access": False,
|
||||
"controller_backend_reachable": not controller_unreachable,
|
||||
"checks": checks,
|
||||
"repository_checks": repository_checks,
|
||||
"evidence": evidence,
|
||||
"remaining_manual_gates": [
|
||||
"packaged Windows GUI interaction",
|
||||
"known-working private LightBurn project import",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("rayforge_checkout", type=Path)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
(ROOT / ".build").mkdir(exist_ok=True)
|
||||
result = run(args.rayforge_checkout.resolve())
|
||||
rendered = json.dumps(result, indent=2, sort_keys=True) + "\n"
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(rendered, encoding="utf-8", newline="\n")
|
||||
print(f"status={result['status']}")
|
||||
print(f"sha256={hashlib.sha256(rendered.encode()).hexdigest()}")
|
||||
return 0 if result["status"] == "passed" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
59
tests/test_m1_acceptance_pipeline.py
Normal file
59
tests/test_m1_acceptance_pipeline.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ACCEPTANCE = ROOT / "scripts" / "run_m1_no_device_acceptance.py"
|
||||
BUILD = ROOT / "scripts" / "build_rayforge_windows_checkpoint.ps1"
|
||||
UPSTREAM = ROOT / "upstreams" / "rayforge.json"
|
||||
|
||||
|
||||
class M1AcceptancePipelineTests(unittest.TestCase):
|
||||
def test_acceptance_collects_every_current_offline_proof(self):
|
||||
source = ACCEPTANCE.read_text(encoding="utf-8")
|
||||
expected = (
|
||||
"rayforge-addon-spike.json",
|
||||
"rayforge-graphical-workflow.json",
|
||||
"rayforge-layer-model.json",
|
||||
"rayforge-recipe-model.json",
|
||||
"rayforge-jcz-equivalence.json",
|
||||
"rayforge-jcz-negative-cases.json",
|
||||
"rayforge-jcz-cancel-recovery.json",
|
||||
"rayforge-lightburn-mopa-import.json",
|
||||
"rayforge-lightburn-device-import.json",
|
||||
)
|
||||
for script in expected:
|
||||
self.assertIn(script, source)
|
||||
|
||||
def test_acceptance_is_pinned_and_reports_remaining_manual_gates(self):
|
||||
manifest = json.loads(UPSTREAM.read_text(encoding="utf-8"))
|
||||
source = ACCEPTANCE.read_text(encoding="utf-8")
|
||||
self.assertIn(manifest["commit"], source)
|
||||
self.assertIn("packaged Windows GUI interaction", source)
|
||||
self.assertIn("known-working private LightBurn project import", source)
|
||||
self.assertIn('"-m", "unittest", "discover"', source)
|
||||
self.assertIn("scripts/check_public_repo.py", source)
|
||||
self.assertIn("timeout=120", source)
|
||||
|
||||
def test_build_is_explicit_pinned_and_packages_the_addon(self):
|
||||
source = BUILD.read_text(encoding="utf-8")
|
||||
self.assertNotIn("InstallWindowsDependencies", source)
|
||||
self.assertIn("prepare_rayforge_build.ps1", source)
|
||||
self.assertIn("rayforge-addon-the-pirate-ray-jcz", source)
|
||||
self.assertIn("Compress-Archive", source)
|
||||
self.assertIn("Get-FileHash -Algorithm SHA256", source)
|
||||
|
||||
def test_acceptance_has_no_hardware_commands(self):
|
||||
source = ACCEPTANCE.read_text(encoding="utf-8").lower()
|
||||
for forbidden in ("usb.core", "import usb", "open_device"):
|
||||
self.assertNotIn(forbidden, source)
|
||||
|
||||
def test_build_rejects_transport_imports_in_staged_addon(self):
|
||||
source = BUILD.read_text(encoding="utf-8").lower()
|
||||
for forbidden in ("import usb", "import serial", "import socket"):
|
||||
self.assertIn(forbidden, source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -21,7 +21,7 @@ class UpstreamBuildReferenceTests(unittest.TestCase):
|
|||
script = PREPARE.read_text(encoding="utf-8")
|
||||
self.assertIn('".build\\upstreams"', script)
|
||||
self.assertIn("SupportsShouldProcess", script)
|
||||
self.assertIn("InstallWindowsDependencies", script)
|
||||
self.assertNotIn("InstallWindowsDependencies", script)
|
||||
self.assertIn("BuildWindowsBundle", script)
|
||||
self.assertIn("No dependencies were installed", script)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue