Commit initial

This commit is contained in:
2026-04-15 17:57:12 +02:00
parent 005d8e797e
commit 55516ee10f
269 changed files with 26854 additions and 0 deletions
@@ -0,0 +1,170 @@
---
phase: 06-wire-icon-intunewin
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- imptune/api/packages.py
- tests/test_packages.py
autonomous: true
requirements:
- PKG-04
must_haves:
truths:
- "Exported .intunewin includes icon.png in staging when printer has an uploaded icon"
- "Exported .intunewin succeeds without error when printer has no icon"
artifacts:
- path: "imptune/api/packages.py"
provides: "Icon lookup and copy into tmpdir staging"
contains: "Icon.get_or_none"
- path: "tests/test_packages.py"
provides: "Integration tests for icon-in-package and no-icon baseline"
contains: "test_intunewin_includes_icon"
key_links:
- from: "imptune/api/packages.py"
to: "imptune/db/models.py"
via: "Icon.get_or_none(Icon.printer == printer.id)"
pattern: "Icon\\.get_or_none"
- from: "imptune/api/packages.py"
to: "imptune/config.py"
via: "cfg.ICONS_DIR for icon source path"
pattern: "cfg\\.ICONS_DIR"
---
<objective>
Wire the uploaded PNG icon into the .intunewin export pipeline so that printers with an uploaded icon include it in the deployment package.
Purpose: Closes the PKG-04 gap — icon upload exists (Phase 5) but the .intunewin builder never receives the icon file. This is the last unsatisfied v1 requirement.
Output: Modified packages.py with icon lookup + copy, two new integration tests in test_packages.py.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/06-wire-icon-intunewin/06-RESEARCH.md
<interfaces>
<!-- Key types and contracts the executor needs. -->
From imptune/db/models.py:
```python
class Icon(BaseModel):
printer = ForeignKeyField(Printer, unique=True, backref="icons")
sha256 = CharField()
original_filename = CharField()
size_bytes = IntegerField()
uploaded_at = DateTimeField(default=datetime.utcnow)
class Meta:
table_name = "icon"
```
From imptune/config.py:
```python
ICONS_DIR = str(Path(DATA_DIR) / "icons")
```
From imptune/api/packages.py (insertion point — line 146, after driver extraction, before build_intunewin):
```python
# Line 132: with tempfile.TemporaryDirectory(prefix="imptune_") as tmpdir:
# Lines 134-139: write install.ps1, uninstall.ps1, detect.ps1
# Lines 142-145: extract driver ZIP into tmpdir/drivers/
# >>> INSERT ICON COPY HERE <<<
# Line 148: output_path = os.path.join(tmpdir, "out.intunewin")
# Line 149: build_intunewin(tmpdir, "install.ps1", output_path)
```
From tests/test_packages.py (existing fixtures):
```python
@pytest.fixture
def setup_printer_with_driver(tmp_data_dir, driver_zip_bytes):
# Creates Driver + Printer records, writes driver ZIP to cfg.DRIVERS_DIR
# Returns (printer, driver)
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Add icon-in-package tests and wire icon into packages.py</name>
<files>tests/test_packages.py, imptune/api/packages.py</files>
<behavior>
- test_intunewin_includes_icon: upload a 256x256 PNG icon for the printer, monkeypatch build_intunewin to capture staged file list, call GET /printers/{id}/packages/intunewin, assert "icon.png" is in staged files and response is 200
- test_intunewin_without_icon_succeeds: no icon uploaded, monkeypatch build_intunewin, call GET /printers/{id}/packages/intunewin, assert response is 200 (no crash from missing icon)
</behavior>
<action>
RED phase — add two tests to TestIntunewinDownload class in tests/test_packages.py:
1. `test_intunewin_includes_icon(self, client, setup_printer_with_driver, tmp_data_dir, monkeypatch)`:
- Create a 256x256 RGBA PNG using `PIL.Image.new("RGBA", (256, 256), color="red")`
- POST it to `/printers/{printer.id}/icon` as multipart file upload
- Assert upload returns 200
- Define `fake_build(source_dir, setup_file, output_path)` that captures `os.listdir(source_dir)` into a list and writes `b"FAKE"` to output_path
- Monkeypatch `imptune.api.packages.build_intunewin` with fake_build
- GET `/printers/{printer.id}/packages/intunewin`
- Assert status 200 and `"icon.png"` in captured staged files
2. `test_intunewin_without_icon_succeeds(self, client, setup_printer_with_driver, monkeypatch)`:
- Same fake_build monkeypatch (no icon upload)
- GET `/printers/{printer.id}/packages/intunewin`
- Assert status 200
Run tests — both MUST fail (icon.png not staged, and second test should actually pass since no icon code yet — if it passes, that is acceptable for the baseline).
GREEN phase — modify imptune/api/packages.py:
1. Add `import shutil` at top
2. Add `from imptune.db.models import Icon` to the existing models import (line 12 area — add Icon next to Printer)
3. Inside `get_intunewin_package()`, after the driver ZIP extraction block (after line 145) and BEFORE `build_intunewin()` (line 149), insert:
```python
# Copy icon into staging if one exists for this printer
icon_record = Icon.get_or_none(Icon.printer == printer.id)
if icon_record is not None:
icon_src = os.path.join(cfg.ICONS_DIR, icon_record.sha256)
if os.path.isfile(icon_src):
shutil.copy2(icon_src, os.path.join(tmpdir, "icon.png"))
```
This is 4 lines of production code. The icon is optional — missing DB record or missing file on disk both result in silent skip (no error, export proceeds without icon).
Run tests again — both MUST pass.
</action>
<verify>
<automated>python -m pytest tests/test_packages.py -x -q</automated>
</verify>
<done>
- test_intunewin_includes_icon passes: icon.png present in staged files when icon uploaded
- test_intunewin_without_icon_succeeds passes: export works with no icon
- All pre-existing test_packages.py tests still pass (no regressions)
- Full test suite green: python -m pytest tests/ -q
</done>
</task>
</tasks>
<verification>
- `python -m pytest tests/test_packages.py -x -q` — all tests pass including two new icon tests
- `python -m pytest tests/ -q` — full suite green, no regressions
- Manual code review: `shutil.copy2` call is BEFORE `build_intunewin()` call (not after)
- Manual code review: `Icon` import added, `cfg.ICONS_DIR` used (not hardcoded path)
</verification>
<success_criteria>
1. Printers with an uploaded icon have icon.png included in .intunewin staging directory
2. Printers without an icon export successfully with no error
3. All existing tests pass without modification
4. PKG-04 requirement satisfied
</success_criteria>
<output>
After completion, create `.planning/phases/06-wire-icon-intunewin/06-01-SUMMARY.md`
</output>
@@ -0,0 +1,73 @@
---
phase: 06-wire-icon-intunewin
plan: "01"
subsystem: package-export
tags: [icon, intunewin, tdd, PKG-04]
dependency_graph:
requires: [05-02]
provides: [icon-in-intunewin]
affects: [imptune/api/packages.py]
tech_stack:
added: []
patterns: [content-addressed-icon-lookup, optional-staging-copy]
key_files:
created: []
modified:
- imptune/api/packages.py
- tests/test_packages.py
decisions:
- "Icon copy is silent-skip on missing DB record or missing disk file — export always succeeds regardless of icon presence"
- "shutil.copy2 preserves file metadata; icon staged as icon.png (constant name) for Intune package structure"
metrics:
duration: "~5 minutes"
completed: "2026-04-10"
tasks_completed: 1
files_modified: 2
requirements-completed: [PKG-04]
---
# Phase 06 Plan 01: Wire Icon into .intunewin Export Summary
**One-liner:** Icon lookup via `Icon.get_or_none` + `shutil.copy2` into tmpdir staging before `build_intunewin()` call, satisfying PKG-04 with silent-skip for missing icons.
## What Was Built
Added 4 lines of production code to `imptune/api/packages.py` that look up an `Icon` record for the current printer and, if found and on disk, copy it as `icon.png` into the `.intunewin` staging directory before `build_intunewin()` is called.
Two new integration tests were added to `tests/test_packages.py` in a new `TestIntunewinIconInclusion` class:
- `test_intunewin_includes_icon`: uploads a 256x256 PNG, monkeypatches `build_intunewin`, asserts `icon.png` appears in staged files
- `test_intunewin_without_icon_succeeds`: no icon uploaded, asserts export returns 200 with no crash
## Decisions Made
- **Silent-skip pattern:** Missing `Icon` DB record or missing file on disk both result in the icon step being skipped silently. Export always proceeds; the icon is optional metadata.
- **Constant filename:** Icon is always staged as `icon.png` regardless of the `original_filename` stored in the DB. This gives the `.intunewin` package a predictable icon path for Intune policies that reference it.
## TDD Execution
**RED:** `test_intunewin_includes_icon` failed (icon.png not in staged files `['detect.ps1', 'drivers', 'install.ps1', 'uninstall.ps1']`). `test_intunewin_without_icon_succeeds` passed as expected baseline.
**GREEN:** After adding `shutil`/`Icon` imports and the 4-line copy block, both tests passed. Full suite: 96 passed, 0 failed.
## Deviations from Plan
None — plan executed exactly as written.
## Verification Results
- `python -m pytest tests/test_packages.py -x -q` — 15 passed
- `python -m pytest tests/ -q` — 96 passed, 0 failed
- Code review: `shutil.copy2` call is BEFORE `build_intunewin()` call ✓
- Code review: `Icon` imported from `imptune.db.models`, `cfg.ICONS_DIR` used (not hardcoded) ✓
## Self-Check
Files exist:
- `imptune/api/packages.py` — modified
- `tests/test_packages.py` — modified
Commits:
- `2723cc8` — test(06-01): add failing test for icon inclusion in .intunewin export
- `6310be5` — feat(06-01): wire icon into .intunewin staging before build
## Self-Check: PASSED
@@ -0,0 +1,356 @@
# Phase 6: Wire Icon into .intunewin Export — Research
**Researched:** 2026-04-10
**Domain:** Python file I/O, Peewee ORM query, .intunewin staging pipeline
**Confidence:** HIGH
---
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|-----------------|
| PKG-04 | User can upload a custom PNG icon for Intune app display (256x256, max 750KB) | Icon upload (icons.py + Icon model) is already complete. This phase wires the stored icon into the .intunewin build by copying it into the tmpdir before `build_intunewin()` is called. |
</phase_requirements>
---
## Summary
Phase 5 implemented icon upload and storage (`icons.py`, `Icon` model, SHA256-addressed
file under `DATA_DIR/icons/`). The `get_intunewin_package()` endpoint in `packages.py`
already builds a `.intunewin` file from a `TemporaryDirectory` staging area. The gap is
that the endpoint never queries the `Icon` model, so the PNG never lands in the tmpdir and
is never encrypted into the inner ZIP.
The work is minimal and mechanical: one `Icon.get_or_none()` lookup, one `shutil.copy2()`
(or equivalent `open/write`) into `tmpdir`, and an integration test that verifies the icon
is present inside the exported package. No schema changes, no new dependencies.
**Primary recommendation:** Query `Icon.get_or_none(Icon.printer == printer_id)` in
`get_intunewin_package()`, and if an icon exists copy it into `tmpdir` as `icon.png`
before calling `build_intunewin()`. Add one integration test that uploads an icon and
verifies `icon.png` appears inside the `.intunewin` inner ZIP.
---
## Standard Stack
### Core (already in place — no new installs)
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| peewee | 3.x | ORM query for Icon record | Already used throughout; `Icon.get_or_none()` follows established pattern |
| Python shutil / pathlib | stdlib | Copy icon file from ICONS_DIR into tmpdir | Zero-dep, already used in codebase |
| Pillow | current | Not needed here (already validated on upload) | Upload already guarantees PNG 256x256 |
| zipfile | stdlib | Read inner ZIP of .intunewin to assert icon presence in tests | Already used in test_packages.py |
**Installation:** None required. All dependencies exist.
---
## Architecture Patterns
### Existing tmpdir staging pattern (packages.py lines 132-149)
```python
with tempfile.TemporaryDirectory(prefix="imptune_") as tmpdir:
# Write scripts
with open(os.path.join(tmpdir, "install.ps1"), "w", encoding="utf-8") as f:
f.write(install_script)
# ... uninstall.ps1, detect.ps1 ...
# Extract driver ZIP into tmpdir/drivers/
drivers_subdir = os.path.join(tmpdir, "drivers")
os.makedirs(drivers_subdir, exist_ok=True)
with zipfile.ZipFile(driver_zip_path, "r") as driver_zf:
driver_zf.extractall(drivers_subdir)
# Build .intunewin
build_intunewin(tmpdir, "install.ps1", output_path)
```
The icon copy slots in immediately after driver extraction, before `build_intunewin()`.
### Pattern: icon lookup + conditional copy
```python
import shutil
from imptune.db.models import Icon
# Inside get_intunewin_package(), after driver validation:
icon_record = Icon.get_or_none(Icon.printer == printer_id)
if icon_record is not None:
icon_src = os.path.join(cfg.ICONS_DIR, icon_record.sha256)
icon_dst = os.path.join(tmpdir, "icon.png")
shutil.copy2(icon_src, icon_dst)
```
**Key decision:** Copy is conditional — printers without an uploaded icon still export
successfully. `shutil.copy2` preserves metadata and is the idiomatic stdlib copy call.
### Icon filename in tmpdir
Use `"icon.png"` as the fixed destination name regardless of `original_filename`. This is
predictable for the test assertion and matches Intune's expectation for app icons (a
well-known filename in the package root).
### Established ORM access pattern (from project decisions)
- Use `Model.get_or_none()` (not `Model.get()`) — avoids `DoesNotExist` exception
- Test assertions use `list(Model.select().where(...))` not `Model.get()` — avoids Peewee
cursor-caching issue with re-init'd DBs across tests (Phase 03 decision)
### Anti-Patterns to Avoid
- **Raising 422 when icon is absent:** Icon is optional. The export must succeed without one.
- **Storing icon in a subdirectory:** `build_intunewin` walks `source_dir` recursively; any
placement works, but root-level `icon.png` is simplest and most predictable.
- **Re-validating the PNG in packages.py:** Validation already happened at upload time. Do
not import Pillow into packages.py; just copy the bytes.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| File copy into tmpdir | Custom open/read/write loop | `shutil.copy2()` | Handles edge cases, one line |
| Icon existence check | Filesystem path probe | `Icon.get_or_none()` | Source of truth is the DB record, not filesystem |
| Verify icon in .intunewin | Manual ZIP decryption | Read inner ZIP via `zipfile` after decoding (see test pattern below) | The test can open the outer ZIP and then the inner ZIP without decryption using stored bytes |
---
## Common Pitfalls
### Pitfall 1: ICONS_DIR not `cfg.ICONS_DIR`
**What goes wrong:** Hardcoding `DATA_DIR + "/icons"` instead of reading `cfg.ICONS_DIR`.
**Why it happens:** `cfg.ICONS_DIR` was added in Phase 5 (config.py line 13); easy to miss.
**How to avoid:** Always use `cfg.ICONS_DIR` — monkeypatched by `tmp_data_dir` fixture in tests.
**Warning signs:** Integration test writes icon to `cfg.ICONS_DIR` but packages.py reads
from a hardcoded path — icon silently missing in package.
### Pitfall 2: Icon copy placed after `build_intunewin()` call
**What goes wrong:** Icon is written to tmpdir but after `build_intunewin()` already ran;
not included in the inner ZIP.
**How to avoid:** Icon copy must come BEFORE `build_intunewin(tmpdir, ...)` call.
### Pitfall 3: Test verifying icon in outer ZIP instead of inner ZIP
**What goes wrong:** The `.intunewin` outer ZIP contains only `IntuneWinPackage/Contents/
IntunePackage.intunewin` (encrypted blob) and `Detection.xml`. `icon.png` is NOT a member
of the outer ZIP; it is packed into the inner ZIP (the plaintext content ZIP). A test that
opens the outer ZIP and checks `namelist()` for `icon.png` will always fail.
**How to avoid:** The integration test must decrypt (or use a pre-decryption approach — see
Code Examples below) to verify icon presence. The simplest approach: use a test fixture
that calls `build_intunewin` directly against a tmpdir so the inner ZIP can be inspected
before encryption, OR read the Detection.xml and re-derive key material from a
test-seeded build. Given current architecture uses random keys, the cleanest test strategy
is:
a) Make a real HTTP request through the test client to get the `.intunewin` bytes
b) Use `build_intunewin`'s known structure: the inner ZIP is the AES-encrypted payload
— cannot open it directly without the key.
c) **Best approach:** Test that the icon file is present in `tmpdir` staging BEFORE
`build_intunewin()` is called by exposing a helper, OR introduce a
`build_intunewin` that accepts a pre-built inner ZIP for testability.
d) **Pragmatic approach that avoids refactoring:** In the integration test, directly
call the packages.py function with a monkeypatched `build_intunewin` that records
what was in `tmpdir` instead of encrypting. This is the pattern to use.
**Recommended test strategy:** Monkeypatch `build_intunewin` to capture the staging
directory contents, then assert `icon.png` is present among the staged files. This is
simpler than decrypting the output and avoids coupling the test to the encryption
implementation.
### Pitfall 4: `printer_id` vs ORM instance in Icon query
**What goes wrong:** `Icon.get_or_none(Icon.printer == printer)` (ORM instance) vs
`Icon.get_or_none(Icon.printer == printer_id)` (integer). Both work in Peewee, but the
integer form is more explicit and consistent with how icons.py deletes records
(`Icon.delete().where(Icon.printer == printer_id)`).
**How to avoid:** Use integer `printer_id` (the local variable from `_get_printer_and_driver`
returns the printer ORM object; extract `.id` from it or use it directly — Peewee resolves
FK equality either way, but be consistent with existing codebase style).
---
## Code Examples
### Icon lookup and conditional copy (insert inside `get_intunewin_package`)
```python
# Source: derived from icons.py pattern + packages.py TemporaryDirectory pattern
import shutil
from imptune.db.models import Icon
# After extracting driver ZIP, before build_intunewin():
icon_record = Icon.get_or_none(Icon.printer == printer.id)
if icon_record is not None:
icon_src = os.path.join(cfg.ICONS_DIR, icon_record.sha256)
if os.path.isfile(icon_src):
shutil.copy2(icon_src, os.path.join(tmpdir, "icon.png"))
```
### Integration test — icon presence in staged files (monkeypatch approach)
```python
# Source: pattern established by test_packages.py + test_icon_upload.py
def test_intunewin_includes_icon(client, setup_printer_with_driver, tmp_data_dir, monkeypatch):
"""Icon PNG is included in .intunewin staging directory."""
import io
import os
from PIL import Image
from imptune.db.models import Icon
import imptune.config as cfg
import imptune.api.packages as pkg_module
printer, _ = setup_printer_with_driver
# Create and upload icon
img = Image.new("RGBA", (256, 256), color="red")
buf = io.BytesIO()
img.save(buf, format="PNG")
png_data = buf.getvalue()
resp = client.post(
f"/printers/{printer.id}/icon",
files={"file": ("icon.png", io.BytesIO(png_data), "image/png")},
)
assert resp.status_code == 200
# Capture staging contents via monkeypatched build_intunewin
staged_files = []
def fake_build(source_dir, setup_file, output_path):
staged_files.extend(os.listdir(source_dir))
# Write a minimal valid file so endpoint can read it
with open(output_path, "wb") as f:
f.write(b"FAKE")
monkeypatch.setattr(pkg_module, "build_intunewin", fake_build)
resp = client.get(f"/printers/{printer.id}/packages/intunewin")
assert resp.status_code == 200
assert "icon.png" in staged_files
```
### Test: export succeeds with no icon (icon-free baseline)
```python
def test_intunewin_without_icon_succeeds(client, setup_printer_with_driver, monkeypatch):
"""Export succeeds even when no icon has been uploaded."""
import imptune.api.packages as pkg_module
import os
printer, _ = setup_printer_with_driver
def fake_build(source_dir, setup_file, output_path):
with open(output_path, "wb") as f:
f.write(b"FAKE")
monkeypatch.setattr(pkg_module, "build_intunewin", fake_build)
resp = client.get(f"/printers/{printer.id}/packages/intunewin")
assert resp.status_code == 200
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| icon stored as column in Printer | Separate `Icon` model with FK, SHA256-addressed on disk | Phase 5 | Clean separation; icon is optional; no schema migration needed here |
| build_intunewin reads arbitrary files | build_intunewin walks entire tmpdir | Phase 5 | Any file placed in tmpdir automatically included in inner ZIP |
---
## Open Questions
1. **Should missing icon file on disk (orphaned DB record) be silently skipped or 422?**
- What we know: `icon_record` can exist in DB but file might be absent (disk corruption,
manual cleanup). Current `icons.py` does not handle this case either.
- Recommendation: Silent skip (don't copy if `os.path.isfile(icon_src)` is False) —
consistent with the optional nature of icons and avoids breaking exports for stale records.
2. **Is there any Intune-specific requirement for the icon filename or location inside .intunewin?**
- What we know: The `.intunewin` package is a deployment container; Intune does NOT
read the icon from within the .intunewin file. The icon is set separately in the
Intune portal during app creation. The PKG-04 requirement says "included in the
.intunewin package" as a convenience/archive — not as an Intune-interpreted artifact.
- Confidence: MEDIUM — based on reverse-engineering of .intunewin format. The
requirement text "Intune displays it as the app icon" describes the end-state in
Intune, not a technical requirement for the .intunewin binary.
- Recommendation: Include the icon in the package root as `icon.png` for user
convenience (they can extract it when uploading manually to Intune portal).
---
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | pytest (detected: pytest.ini or pyproject.toml implicit) |
| Config file | none detected — runs via `python -m pytest` |
| Quick run command | `python -m pytest tests/test_packages.py -x -q` |
| Full suite command | `python -m pytest tests/ -q` |
### Phase Requirements -> Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| PKG-04 | Icon PNG is copied into .intunewin staging dir when icon exists | integration | `python -m pytest tests/test_packages.py::TestIntunewinDownload -x -q` | Partial — test_packages.py exists but no icon-in-package test yet |
| PKG-04 | Export succeeds when no icon uploaded | integration | `python -m pytest tests/test_packages.py::TestIntunewinDownload -x -q` | Partial — baseline export tests exist |
### Sampling Rate
- **Per task commit:** `python -m pytest tests/test_packages.py -x -q`
- **Per wave merge:** `python -m pytest tests/ -q`
- **Phase gate:** Full suite green before `/gsd:verify-work`
### Wave 0 Gaps
- [ ] `tests/test_packages.py` — add `test_intunewin_includes_icon` test (monkeypatch `build_intunewin`)
- [ ] `tests/test_packages.py` — add `test_intunewin_without_icon_succeeds` test
*(Existing test infrastructure covers the rest — no new files or fixtures needed)*
---
## Sources
### Primary (HIGH confidence)
- Direct code reading: `imptune/api/packages.py` — full `get_intunewin_package()` implementation
- Direct code reading: `imptune/api/icons.py` — Icon upload, SHA256 storage path, `cfg.ICONS_DIR`
- Direct code reading: `imptune/db/models.py``Icon` model, FK to `Printer`, `sha256` field
- Direct code reading: `imptune/config.py``ICONS_DIR = str(Path(DATA_DIR) / "icons")`
- Direct code reading: `imptune/generators/intunewin_builder.py``build_intunewin` walks entire `source_dir`
- Direct code reading: `tests/conftest.py``tmp_data_dir` monkeypatches `cfg.ICONS_DIR`
- Direct code reading: `tests/test_packages.py` — existing test patterns (fixtures, monkeypatch style)
- Direct code reading: `.planning/STATE.md` — Phase 5 decisions on ICONS_DIR, Icon model, shutil pattern
### Secondary (MEDIUM confidence)
- Intune .intunewin format: icon is NOT read from within the package by Intune — icons are
set in the Intune portal. Including it in the package is for user convenience only.
(Based on reverse-engineering notes in `intunewin_builder.py` comments + svrooij.io reference)
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — all libraries already in codebase, no new dependencies
- Architecture: HIGH — single code location change (packages.py), clear insertion point
- Pitfalls: HIGH — inner-ZIP test trap verified by reading intunewin_builder.py structure
- Test strategy: HIGH — monkeypatch pattern already established in existing test suite
**Research date:** 2026-04-10
**Valid until:** Stable — no moving parts (stdlib + existing codebase only)
@@ -0,0 +1,102 @@
---
phase: 6
slug: wire-icon-intunewin
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-06)
---
# Phase 6 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution.
---
## Test Infrastructure
| Property | Value |
|----------|-------|
| **Framework** | pytest |
| **Config file** | none — runs via `python -m pytest` |
| **Quick run command** | `python -m pytest tests/test_packages.py -x -q` |
| **Full suite command** | `python -m pytest tests/ -q` |
| **Estimated runtime** | ~5 seconds |
---
## Sampling Rate
- **After every task commit:** Run `python -m pytest tests/test_packages.py -x -q`
- **After every plan wave:** Run `python -m pytest tests/ -q`
- **Before `/gsd:verify-work`:** Full suite must be green
- **Max feedback latency:** 5 seconds
---
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
| 06-01-01 | 01 | 1 | PKG-04 | integration | `python -m pytest tests/test_packages.py::test_intunewin_includes_icon -x -q` | ❌ W0 | ⬜ pending |
| 06-01-02 | 01 | 1 | PKG-04 | integration | `python -m pytest tests/test_packages.py::test_intunewin_without_icon_succeeds -x -q` | ❌ W0 | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
---
## Wave 0 Requirements
- [ ] `tests/test_packages.py` — add `test_intunewin_includes_icon` (monkeypatch `build_intunewin` to capture staging dir contents)
- [ ] `tests/test_packages.py` — add `test_intunewin_without_icon_succeeds` (baseline: export without icon)
*Existing test infrastructure covers the rest — no new files or fixtures needed.*
---
## Manual-Only Verifications
*All phase behaviors have automated verification.*
---
## Nyquist Record
> Audited 2026-04-13 by Claude (gsd-executor, plan 08-06). Phase 6 is a **gap-closure phase** with a single success criterion (PKG-04 icon embedding) spawned after the first v1.0 milestone audit flagged that Phase 5 had shipped icon upload+storage but never wired the icon into `.intunewin` output. One row per Phase 6 success criterion, derived from `milestones/v1.0-ROADMAP.md` Phase 6 goal block + `REQUIREMENTS.md` PKG-04, cross-checked against `06-VERIFICATION.md` (2/2 truths VERIFIED 2026-04-10) and `06-01-SUMMARY.md`. Evidence cites committed pytest invocations, source lines, commit SHAs, and — as supporting transitive evidence — Phase 10 `RUNTIME-VALIDATION.md` RTVAL-01 (artifact-backed tenant acceptance of the exact `.intunewin` builder path on tenant rubis.fr, 2026-04-13).
>
> **Phase 6 goal (v1.0-ROADMAP.md):** *"Uploaded PNG icon is embedded in the .intunewin package so Intune displays it as the app icon."*
>
> **Single-criterion phase:** Unlike Phases 1-5 which enumerate multiple requirements, Phase 6 has exactly one requirement (PKG-04) and one plan (06-01). The Nyquist Record therefore contains exactly one row. This mirrors the plan 08-05 row-4 (PKG-04) closure citation in reverse direction: 05-VALIDATION.md rows 4 cites **this** phase's test as its closure evidence; this phase's row cites the same test as its canonical evidence.
>
> **RTVAL-01 transitive coverage:** RTVAL-01 PASS on tenant rubis.fr (screenshots `rtval-01-tenant-upload.png` + `rtval-01-app-assigned.png`, committed package `Copieur_2eme.intunewin`) exercised the exact same `build_intunewin()` staging path that Phase 6's `shutil.copy2(...'icon.png')` feeds into. The test package was built with the icon-wiring code live, so Intune's successful ingestion of the package is transitive evidence that the icon staging does not corrupt the `.intunewin` output. The PKG-04 row notes this as supporting — not primary — evidence because RTVAL-01's observable check was "tenant accepts package", not "icon appears on Intune app tile" (the latter remains a Manual-Only polish item owned by Phase 11 rollout).
| # | Criterion | Observable Check | Evidence | Status | Notes |
|---|-----------|-----------------|----------|--------|-------|
| 1 | **PKG-04:** Uploaded PNG icon is embedded in `.intunewin` output so Intune displays it as the app icon | `python -m pytest tests/test_packages.py::TestIntunewinIconInclusion -x -q` (2 tests: `test_intunewin_includes_icon` asserts `icon.png` appears in staged files via monkeypatched `build_intunewin`; `test_intunewin_without_icon_succeeds` asserts baseline export returns 200 when no icon uploaded) | Tests: `tests/test_packages.py::TestIntunewinIconInclusion::test_intunewin_includes_icon` (lines 200-231) + `::test_intunewin_without_icon_succeeds` (lines 233-245). Source: `imptune/api/packages.py` line 149 (`Icon.get_or_none(Icon.printer == printer.id)`), line 151 (`cfg.ICONS_DIR`), line 153 (`shutil.copy2` as `icon.png`), line 157 (`build_intunewin` call — staging BEFORE build confirmed). Imports: `Icon` from `imptune.db.models` (line 13), `cfg` (line 13). Commits: `2723cc8` (06-01 TDD RED — failing test) + `6310be5` (06-01 TDD GREEN — 4-line icon staging block). 06-VERIFICATION.md (2026-04-10): 2/2 truths VERIFIED, key links WIRED, PKG-04 SATISFIED, no anti-patterns, full suite 96/96 green. Supporting: Phase 10 `RUNTIME-VALIDATION.md` RTVAL-01 PASS on tenant rubis.fr (2026-04-13) — same `build_intunewin` path with icon-wiring code live, package `Copieur_2eme.intunewin` accepted by tenant after fix commits `74535ea` + `7716246`. | pass | Silent-skip pattern: missing `Icon` DB record or missing disk file both skip the copy; export always succeeds (decision in 06-01-SUMMARY.md). Icon staged as constant filename `icon.png` regardless of original filename. **Manual-Only polish item:** Visual confirmation that the icon actually appears on the Intune app tile in the portal is NOT covered by this row — RTVAL-01 proved ingestion, not icon-tile rendering. This cosmetic check is Manual-Only and owned by Phase 11 rollout visual polish (same as 05-VALIDATION.md row 4 notes). Audit trail for the embedding mechanism itself is strong (pytest + source review + TDD commits + transitive real-tenant ingestion). |
### Audit Outcome
| Status | Count |
|---------------|-------|
| pass | 1 |
| fail-fix-v1.1 | 0 |
| deferred-v1.2 | 0 |
| wont-do | 0 |
Phase 6 is Nyquist-compliant. The single gap-closure criterion is satisfied with strong test-level evidence plus transitive artifact-backed runtime coverage via RTVAL-01. The only residual item is the cosmetic "does the icon actually show on the Intune app tile" visual check, which is Manual-Only and correctly deferred to Phase 11 rollout.
---
## Validation Sign-Off
- [x] All tasks have `<automated>` verify or Wave 0 dependencies
- [x] Sampling continuity: no 3 consecutive tasks without automated verify
- [x] Wave 0 covers all MISSING references
- [x] No watch-mode flags
- [x] Feedback latency < 5s
- [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-06) — 1/1 pass; signed off 2026-04-13 by Sébastien QUEROL (index: v1.0-VALIDATION-INDEX.md)
@@ -0,0 +1,92 @@
---
phase: 06-wire-icon-intunewin
verified: 2026-04-10T00:00:00Z
status: passed
score: 2/2 must-haves verified
gaps: []
---
# Phase 06: Wire Icon into .intunewin Export — Verification Report
**Phase Goal:** Wire uploaded icon into .intunewin export pipeline so printers with an icon include it in the deployment package.
**Verified:** 2026-04-10
**Status:** PASSED
**Re-verification:** No — initial verification
---
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | Exported .intunewin includes icon.png in staging when printer has an uploaded icon | VERIFIED | `test_intunewin_includes_icon` passes (line 200-231, test_packages.py). Monkeypatched `build_intunewin` captures `os.listdir(source_dir)` and asserts `"icon.png" in staged_files`. Live run: 15/15 passed. |
| 2 | Exported .intunewin succeeds without error when printer has no icon | VERIFIED | `test_intunewin_without_icon_succeeds` passes (line 233-245, test_packages.py). No icon uploaded; export returns 200. Live run confirms. |
**Score:** 2/2 truths verified
---
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `imptune/api/packages.py` | Icon lookup and copy into tmpdir staging | VERIFIED | Contains `Icon.get_or_none` (line 149), `cfg.ICONS_DIR` (line 151), `shutil.copy2` (line 153). Substantive: 167 lines, full implementation with silent-skip guard. Wired: imported by FastAPI router and reachable from GET `/printers/{id}/packages/intunewin`. |
| `tests/test_packages.py` | Integration tests for icon-in-package and no-icon baseline | VERIFIED | Contains `test_intunewin_includes_icon` and `test_intunewin_without_icon_succeeds` in `TestIntunewinIconInclusion` class (lines 199-246). Both tests pass. |
---
### Key Link Verification
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `imptune/api/packages.py` | `imptune/db/models.py` | `Icon.get_or_none(Icon.printer == printer.id)` | WIRED | Pattern `Icon\.get_or_none` found at line 149. `Icon` imported at line 13: `from imptune.db.models import Icon, Printer`. |
| `imptune/api/packages.py` | `imptune/config.py` | `cfg.ICONS_DIR` for icon source path | WIRED | Pattern `cfg\.ICONS_DIR` found at line 151. `cfg` imported at line 13: `import imptune.config as cfg`. |
| `shutil.copy2` call | `build_intunewin` call | Icon staged BEFORE build | WIRED | `shutil.copy2` at line 153, `build_intunewin` at line 157 — ordering confirmed correct. |
---
### Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|-------------|-------------|--------|----------|
| PKG-04 | 06-01-PLAN.md | User can upload a custom PNG icon for Intune app display (256x256, max 750KB) | SATISFIED | Phase 5 delivered the upload endpoint; Phase 6 closes the PKG-04 gap by wiring the stored icon into the .intunewin staging pipeline. REQUIREMENTS.md traceability table confirms PKG-04 mapped to Phase 6, status Complete. |
No orphaned requirements: REQUIREMENTS.md maps PKG-04 exclusively to Phase 6 and no other Phase 6 IDs appear in REQUIREMENTS.md.
---
### Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| — | — | None | — | — |
No TODO/FIXME/placeholder comments, no stub returns, no empty handlers found in either modified file.
---
### Human Verification Required
None. All observable behaviors are verifiable via automated tests. The icon staging path is fully exercised by `test_intunewin_includes_icon` using a real PNG upload and a monkeypatched build step that captures the staged file list.
---
### Gaps Summary
No gaps. Both must-have truths are verified, both artifacts are substantive and wired, both key links exist, PKG-04 is satisfied, and the full test suite (96 tests) is green with zero failures.
---
## Commit Verification
| Commit | Message | Files Changed | Verified |
|--------|---------|---------------|---------|
| `2723cc8` | test(06-01): add failing test for icon inclusion in .intunewin export | tests/test_packages.py (+55 lines) | YES |
| `6310be5` | feat(06-01): wire icon into .intunewin staging before build | imptune/api/packages.py (+9/-1 lines) | YES |
---
_Verified: 2026-04-10_
_Verifier: Claude (gsd-verifier)_