diff --git a/README.md b/README.md index 79c3c4b..8757ab2 100644 --- a/README.md +++ b/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) diff --git a/docs/adr/0002-reuse-rayforge-and-balormk.md b/docs/adr/0002-reuse-rayforge-and-balormk.md index 9851a16..83ac32d 100644 --- a/docs/adr/0002-reuse-rayforge-and-balormk.md +++ b/docs/adr/0002-reuse-rayforge-and-balormk.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. diff --git a/docs/research/m1-no-device-acceptance.md b/docs/research/m1-no-device-acceptance.md new file mode 100644 index 0000000..076ead2 --- /dev/null +++ b/docs/research/m1-no-device-acceptance.md @@ -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 ` + --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. diff --git a/docs/research/rayforge-windows-gui-preflight.md b/docs/research/rayforge-windows-gui-preflight.md index f6a4f7d..f8b9896 100644 --- a/docs/research/rayforge-windows-gui-preflight.md +++ b/docs/research/rayforge-windows-gui-preflight.md @@ -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 diff --git a/docs/roadmap.md b/docs/roadmap.md index e65d3c1..b5297e1 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -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. diff --git a/docs/status/phase-0.md b/docs/status/phase-0.md index d3b6e8e..6e97624 100644 --- a/docs/status/phase-0.md +++ b/docs/status/phase-0.md @@ -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 diff --git a/scripts/build_rayforge_windows_checkpoint.ps1 b/scripts/build_rayforge_windows_checkpoint.ps1 new file mode 100644 index 0000000..6c19671 --- /dev/null +++ b/scripts/build_rayforge_windows_checkpoint.ps1 @@ -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" diff --git a/scripts/prepare_rayforge_build.ps1 b/scripts/prepare_rayforge_build.ps1 index 28a07db..fd0a42c 100644 --- a/scripts/prepare_rayforge_build.ps1 +++ b/scripts/prepare_rayforge_build.ps1 @@ -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")) { diff --git a/scripts/run_m1_no_device_acceptance.py b/scripts/run_m1_no_device_acceptance.py new file mode 100644 index 0000000..9d3d1ed --- /dev/null +++ b/scripts/run_m1_no_device_acceptance.py @@ -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()) diff --git a/tests/test_m1_acceptance_pipeline.py b/tests/test_m1_acceptance_pipeline.py new file mode 100644 index 0000000..2ec1c70 --- /dev/null +++ b/tests/test_m1_acceptance_pipeline.py @@ -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() diff --git a/tests/test_upstream_build_reference.py b/tests/test_upstream_build_reference.py index 9b89d37..3d6ad8d 100644 --- a/tests/test_upstream_build_reference.py +++ b/tests/test_upstream_build_reference.py @@ -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)