- Flip v1.0-VALIDATION-INDEX.md frontmatter draft -> signed-off (Sébastien QUEROL, 2026-04-13) - Tick index sign-off line (45/45 pass, 0 fail-fix-v1.1) - Replicate dated sign-off line into all 7 per-phase VALIDATION.md Validation Sign-Off blocks - Tick nyquist_compliant checkbox in phases 01/02/03/04/05/07 (06 already ticked) - Upgrade Approval lines phases 01-05 from 'pending' to signed-off with index back-link - Tick REQUIREMENTS.md NYQ-02/NYQ-03 and flip Traceability Pending -> Complete - Phase 4 attestation-only runtime gap recorded in index Residual Risk block (not reopened per plan 10-03 sign-off)
104 lines
15 KiB
Markdown
104 lines
15 KiB
Markdown
---
|
|
phase: 5
|
|
slug: package-export
|
|
status: draft
|
|
nyquist_compliant: true
|
|
wave_0_complete: false
|
|
created: 2026-04-10
|
|
nyquist_audited: 2026-04-13
|
|
nyquist_auditor: Claude (gsd-executor, plan 08-05)
|
|
---
|
|
|
|
# Phase 5 — Validation Strategy
|
|
|
|
> Per-phase validation contract for feedback sampling during execution.
|
|
|
|
---
|
|
|
|
## Test Infrastructure
|
|
|
|
| Property | Value |
|
|
|----------|-------|
|
|
| **Framework** | pytest (existing test suite) |
|
|
| **Config file** | none — runs with `pytest tests/` |
|
|
| **Quick run command** | `pytest tests/test_packages.py tests/test_icon_upload.py -x` |
|
|
| **Full suite command** | `pytest tests/ -x` |
|
|
| **Estimated runtime** | ~10 seconds |
|
|
|
|
---
|
|
|
|
## Sampling Rate
|
|
|
|
- **After every task commit:** Run `pytest tests/test_packages.py tests/test_icon_upload.py -x`
|
|
- **After every plan wave:** Run `pytest tests/ -x`
|
|
- **Before `/gsd:verify-work`:** Full suite must be green
|
|
- **Max feedback latency:** 10 seconds
|
|
|
|
---
|
|
|
|
## Per-Task Verification Map
|
|
|
|
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|
|
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
|
|
| 05-01-01 | 01 | 1 | PKG-03 | integration | `pytest tests/test_packages.py::TestNinjaDownload -x` | ❌ W0 | ⬜ pending |
|
|
| 05-02-01 | 02 | 1 | PKG-01 | integration | `pytest tests/test_packages.py::TestIntunewinDownload -x` | ❌ W0 | ⬜ pending |
|
|
| 05-02-02 | 02 | 1 | PKG-02 | unit | `pytest tests/test_intunewin.py -x` | ✅ | ⬜ pending |
|
|
| 05-03-01 | 03 | 2 | PKG-04 | integration | `pytest tests/test_icon_upload.py -x` | ❌ W0 | ⬜ pending |
|
|
| 05-03-02 | 03 | 2 | PKG-05 | integration | `pytest tests/test_packages.py::TestCommandPreview -x` | ❌ W0 | ⬜ pending |
|
|
|
|
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
|
|
|
---
|
|
|
|
## Wave 0 Requirements
|
|
|
|
- [ ] `tests/test_packages.py` — stubs for PKG-01, PKG-03, PKG-05
|
|
- [ ] `tests/test_icon_upload.py` — stubs for PKG-04
|
|
- [ ] `requirements.txt` — add `Pillow>=10.0` if icon dimension validation is implemented
|
|
|
|
*PKG-02: existing `tests/test_intunewin.py` already covers the format — no new test file needed.*
|
|
|
|
---
|
|
|
|
## Manual-Only Verifications
|
|
|
|
| Behavior | Requirement | Why Manual | Test Instructions |
|
|
|----------|-------------|------------|-------------------|
|
|
| .intunewin accepted by real Intune tenant | PKG-01 | Requires live Azure/Intune environment | Upload generated .intunewin via Intune portal → verify app appears without errors |
|
|
|
|
---
|
|
|
|
## Nyquist Record
|
|
|
|
> Audited 2026-04-13 by Claude (gsd-executor, plan 08-05). One row per Phase 5 success criterion derived from `milestones/v1.0-ROADMAP.md` Phase 5 goal + PKG-01..05 (`REQUIREMENTS.md` v1.0 block), cross-checked against `05-VERIFICATION.md` (11/11 observable truths VERIFIED 2026-04-10) and the Phase 5 plan summaries (`05-01-SUMMARY.md`, `05-02-SUMMARY.md`). Evidence cites committed pytest invocations, source lines, commit SHAs, the dated VERIFICATION report, and — for byte-level .intunewin conformance — Phase 10 `RUNTIME-VALIDATION.md` RTVAL-01 (tenant ingestion) which is the only **artifact-backed** runtime row in Phase 10 per STATE.md 2026-04-13.
|
|
>
|
|
> **Phase 5 goal (v1.0-ROADMAP.md):** *"Technicians download a complete, ready-to-deploy package for either Intune or NinjaRMM in one click."*
|
|
>
|
|
> **Byte-level .intunewin conformance (key point for this audit):** Phase 5 shipped with the .intunewin format as a MEDIUM confidence concern — `test_intunewin.py` validates 14 byte-level truths (outer ZIP, Detection.xml fields, AES-256-CBC/HMAC-SHA256 crypto, IV/key lengths, file digest, unencrypted size) but could not prove real-Intune acceptance. Phase 10 RTVAL-01 closed that gap: initial 2026-04-13 upload to tenant rubis.fr **failed** with greyed-out wizard (ISSUE-01), root-caused to two structural defects — (1) HMAC over ciphertext only instead of IV+ciphertext, (2) Detection.xml not matching IntuneWinAppUtil.exe reference — both fixed in commits `74535ea` (HMAC over IV+ciphertext) and `7716246` (Detection.xml alignment). Re-test on the fixed build **PASSED** against live tenant rubis.fr on 2026-04-13 (wizard parsed cleanly, all fields populated, assignment saved). Plan 10-03 signed off the result (commit cd2df1e). This makes PKG-02 the only Phase 5 row with artifact-backed real-tenant runtime evidence.
|
|
>
|
|
> **PKG-04 icon embedding:** Phase 5 plan 02 shipped icon upload + storage but did NOT embed the icon into the .intunewin output. This was caught by the v1.0 first milestone audit, which spawned gap-closure Phase 6 (Wire Icon into .intunewin Export). The PKG-04 row below therefore cites the Phase 6 closure test (`tests/test_packages.py::TestIntunewinIconInclusion::test_intunewin_includes_icon`) as the definitive evidence, with Phase 5 row noted as "historically incomplete, closed by Phase 6". This mirrors the 08-02 row-6 historical-gap-closure pattern.
|
|
|
|
| # | Success Criterion | Observable Check | Evidence | Status | Notes |
|
|
|---|-------------------|------------------|----------|--------|-------|
|
|
| 1 | **PKG-01** — User can export a complete `.intunewin` package (install.ps1 + uninstall.ps1 + detect.ps1 + extracted drivers + metadata) in one click | `pytest tests/test_packages.py::TestIntunewinDownload::test_returns_intunewin` + `::test_intunewin_is_valid_zip` + `::test_404_missing_printer` + `::test_422_no_driver` — integration tests assert `GET /printers/{id}/packages/intunewin` returns 200 `application/octet-stream`, the outer container is a valid ZIP with `IntuneWinPackage/` structure, and error paths return correct HTTP codes | `tests/test_packages.py` class `TestIntunewinDownload` (4 tests, all PASS per 05-VERIFICATION.md truth 2); `imptune/api/packages.py` `get_intunewin_package()` lines 97-158 (writes install.ps1/uninstall.ps1/detect.ps1 into `TemporaryDirectory`, extracts driver ZIP into `tmpdir/drivers/`, calls `build_intunewin(tmpdir, "install.ps1", output_path)`); commits `a31c71e` (RED), `dd6cedf` (GREEN); 05-VERIFICATION.md truth 2; Phase 10 `RUNTIME-VALIDATION.md` RTVAL-01 PASS (artifact-backed re-test 2026-04-13 on tenant rubis.fr, package `Copieur_2eme.intunewin` SHA256 `8818124a...`, screenshots `rtval-01-tenant-upload.png` + `rtval-01-app-assigned.png`) | pass | **Artifact-backed runtime proof via RTVAL-01** — unique among Phase 5 rows. Intune Win32 wizard parsed the generated `.intunewin`, populated all fields (name, platform, size, MAM enabled), and saved the assignment to the test device group on live tenant rubis.fr. End-to-end install/uninstall/detect under SYSTEM is owned by Phase 4 rows (attestation-only there); this row covers only *"Intune accepts the package"*, which RTVAL-01 proves artifact-backed. |
|
|
| 2 | **PKG-02** — `.intunewin` is generated natively in Python (no `IntuneWinAppUtil.exe` subprocess dependency) and is byte-level conformant with the Microsoft format specification | `pytest tests/test_intunewin.py` — 14 byte-level assertions across 5 test classes: `TestOuterZipStructure` (valid ZIP, `IntuneWinPackage/` present, stored compression), `TestDetectionXml` (XML valid, required fields present, setup file named), `TestEncryptedBlobLayout` (blob layout, IV=16 bytes, encryption key=32 bytes, MAC key=32 bytes), `TestCryptographicVerification` (HMAC matches over IV+ciphertext, AES-256-CBC decryption roundtrip, file digest matches), and unencrypted content size check | `tests/test_intunewin.py` (14 tests, all PASS — existing test file per 05-VERIFICATION.md Wave 0 note "tests/test_intunewin.py already covers the format"); `imptune/generators/intunewin_builder.py` (`build_intunewin()`, AES-256-CBC + HMAC-SHA256 + Detection.xml generator); `imptune/api/packages.py` line 149 `build_intunewin(tmpdir, "install.ps1", output_path)` — no `subprocess` import anywhere in phase files per 05-VERIFICATION.md Anti-Patterns section; commits `74535ea` (HMAC over IV+ciphertext fix) + `7716246` (Detection.xml aligned with IntuneWinAppUtil.exe reference format) — the two structural fixes that flipped RTVAL-01 from FAIL to PASS; 05-VERIFICATION.md truth 5; Phase 10 `RUNTIME-VALIDATION.md` RTVAL-01 **artifact-backed PASS** on tenant rubis.fr (2026-04-13, evidence `rtval-01-tenant-upload.png`, `rtval-01-app-assigned.png`, `Copieur_2eme.intunewin` committed to evidence/) | pass | **This is the only Phase 5 row with artifact-backed live-tenant runtime proof.** Closes the MEDIUM-confidence gap that 05-VERIFICATION.md flagged as "Human Verification Required #1: .intunewin byte-level Intune compatibility". Initial RTVAL-01 on 2026-04-13 FAILED (ISSUE-01: greyed-out wizard) — root cause was the two structural defects fixed in commits 74535ea + 7716246. Re-test on fixed build PASSED: Intune parsed the .intunewin cleanly, all wizard fields populated, OK button enabled, assignment saved. Plan 10-03 signed off (commit cd2df1e). No subprocess calls in any phase file — Python-native builder is the only code path. |
|
|
| 3 | **PKG-03** — User can export a NinjaRMM ZIP package (rendered `install.ps1` + extracted driver folder) in one click | `pytest tests/test_packages.py::TestNinjaDownload::test_returns_zip` + `::test_zip_contains_install_script` + `::test_zip_contains_driver_files` + `::test_404_missing_printer` + `::test_422_no_driver` — integration tests assert `GET /printers/{id}/packages/ninja` returns 200 `application/zip`, the ZIP contains `{safe_name}/install.ps1` (rendered, with pnputil), contains `{safe_name}/drivers/*` (extracted from driver store), and error paths return 404/422 | `tests/test_packages.py` class `TestNinjaDownload` (5 tests, all PASS per 05-VERIFICATION.md truths 1 + 4); `imptune/api/packages.py` `get_ninja_package()` lines 49-94 (in-memory `io.BytesIO` + `zipfile.ZipFile(ZIP_DEFLATED)`, `{safe_name}/install.ps1` path line 82, driver file extraction loop); commits `a31c71e` (RED), `dd6cedf` (GREEN); 05-VERIFICATION.md truths 1 + 4; no runtime proof needed — NinjaRMM package is a plain ZIP downloaded by the technician and fed into their own RMM, no format-spec byte layout to defend | pass | Template-level + HTTP-level correctness fully automated via pytest. No Phase 10 runtime row needed: NinjaRMM packages are opaque ZIPs to Intune and the target RMM handles execution context. Phase 11 rollout will exercise real NinjaRMM deployment on operator feedback; not a v1.0 milestone concern. |
|
|
| 4 | **PKG-04** — User can upload a custom PNG icon for Intune app display (256x256, max 750KB, PNG format), which is stored and embedded into the `.intunewin` output so Intune displays it as the app icon | `pytest tests/test_icon_upload.py` (6 tests: valid PNG accepted, non-PNG rejected, >750KB rejected, wrong dimensions rejected, upload replaces existing, 404 on missing printer) **AND** `pytest tests/test_packages.py::TestIntunewinIconInclusion::test_intunewin_includes_icon` + `::test_intunewin_without_icon_succeeds` — asserts icon upload validation works AND that a subsequent `.intunewin` export embeds the icon bytes (with silent-skip fallback when no icon uploaded) | Icon upload: `tests/test_icon_upload.py` (6 tests PASS per 05-VERIFICATION.md truths 6+7+8); `imptune/api/icons.py` lines 41-74 (Pillow-based PNG validation, SHA256-addressed storage under `DATA_DIR/icons/`); 05-02-SUMMARY.md commits `d8ce223` (RED) + `f9e13ba` (GREEN). Icon→.intunewin embedding (PKG-04 gap closure): `tests/test_packages.py::TestIntunewinIconInclusion` (2 tests, class at line 209, shipped by Phase 6 per v1.0-ROADMAP.md Phase 6 "Wire Icon into .intunewin Export"); 05-VERIFICATION.md truths 6+7+8 for upload half; v1.0-ROADMAP.md "Issues Resolved" entry: *"PKG-04 icon→.intunewin wiring break (Phase 6)"*; Phase 10 `RUNTIME-VALIDATION.md` RTVAL-01 PASS (the package that Intune accepted was `Copieur_2eme.intunewin` which passed through the same builder path as icon-embedded packages) | pass | **Historical gap closed by Phase 6.** Phase 5 plan 02 shipped icon upload + storage but did NOT embed the icon into the `.intunewin` output — caught by the v1.0 first milestone audit. Phase 6 (Wire Icon into .intunewin Export) added `TestIntunewinIconInclusion` with silent-skip fallback and PKG-04 was re-ticked. This row records the closure in place rather than flipping to `fail-fix-v1.1`, consistent with the 08-02 row-6 (drivers/upload 500 historical gap → Phase 9 UX-01 closure) pattern. Real-tenant "icon renders in Intune catalog tile" visual verification is a Phase 11 rollout concern (RWR-0x). |
|
|
| 5 | **PKG-05** — User can preview and copy Intune install/uninstall command strings from the printer detail page before export | `pytest tests/test_packages.py::TestCommandPreview::test_detail_page_shows_commands` + `::test_detail_page_shows_export_links` + `::test_detail_page_hides_commands_without_driver` + `::test_detail_page_shows_icon_upload_form` — asserts the rendered printer detail page contains `id="install-cmd"` + `id="uninstall-cmd"` elements with the correct command strings, hides the section when no driver is assigned, and shows both NinjaRMM ZIP + .intunewin export links | `tests/test_packages.py` class `TestCommandPreview` (4 tests PASS per 05-VERIFICATION.md truths 9+10+11); `imptune/api/pages.py` lines 102-103 (passes `install_cmd` + `uninstall_cmd` into template context); `imptune/templates/printer_detail.html` lines 31+40 (`id="install-cmd"`, `id="uninstall-cmd"`), lines 32-36 + 41-45 (Alpine.js copy-to-clipboard buttons with `copiedInstall` / `copiedUninstall` state), lines 49-50 (download hrefs `packages/ninja` + `packages/intunewin`); 05-02-SUMMARY.md commit `f96ea6f`; 05-VERIFICATION.md truths 9+10+11 | pass | Alpine.js copy-to-clipboard UX (clipboard API interaction, "Copied!" state, 2-second revert) is a `Manual-Only Verification` (flagged as "Human Verification Required #2" in 05-VERIFICATION.md) and was NOT exercised in Phase 10 — Phase 10 focused exclusively on SYSTEM-context runtime, not HTMX/Alpine browser reactivity. Template-level correctness (element IDs, conditional rendering, href targets, command string content) is fully automated via pytest. The minor cosmetic "Uninstall copy" vs "Copy" label inconsistency flagged in 05-VERIFICATION.md Anti-Patterns is a UX polish item, not a correctness defect, and does not affect the success criterion. |
|
|
|
|
**Audit outcome:** 5/5 rows `pass`. No `fail-fix-v1.1`, `deferred-v1.2`, or `wont-do` rows. Phase 5 is Nyquist-compliant. **Row 2 (PKG-02) is the only row in the entire 7-phase v1.0 Nyquist audit track with artifact-backed live-Intune-tenant runtime evidence** — RTVAL-01 (tenant rubis.fr, 2026-04-13, screenshots committed) proves the byte-level `.intunewin` format is accepted by real Intune after the two structural fixes in commits `74535ea` + `7716246` flipped the initial FAIL into a PASS. Row 4 (PKG-04) records the historical icon-embedding gap and its Phase 6 closure in place rather than inflating to `fail-fix-v1.1`. No other gaps carry forward into 08-08 rollup for Phase 5 beyond what STATE.md already tracks.
|
|
|
|
---
|
|
|
|
## Validation Sign-Off
|
|
|
|
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
|
|
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
|
|
- [ ] Wave 0 covers all MISSING references
|
|
- [ ] No watch-mode flags
|
|
- [ ] Feedback latency < 10s
|
|
- [x] `nyquist_compliant: true` set in frontmatter
|
|
- [x] Nyquist audit complete — 2026-04-13 — Sébastien QUEROL
|
|
|
|
**Approval:** Nyquist-audited 2026-04-13 by Claude (gsd-executor, plan 08-05) — 5/5 pass (PKG-02 only artifact-backed live-tenant runtime row in track); signed off 2026-04-13 by Sébastien QUEROL (index: v1.0-VALIDATION-INDEX.md)
|