docs(06): create phase plan for wiring icon into intunewin export
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user