Commit initial
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
---
|
||||
phase: 04-script-generation
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["04-01"]
|
||||
files_modified:
|
||||
- imptune/generators/script_generator.py
|
||||
- imptune/templates/scripts/uninstall.ps1.j2
|
||||
- imptune/templates/scripts/detect.ps1.j2
|
||||
- imptune/api/scripts.py
|
||||
- imptune/main.py
|
||||
- tests/test_script_generator.py
|
||||
autonomous: true
|
||||
requirements:
|
||||
- SCRPT-02
|
||||
- SCRPT-03
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "render_uninstall() produces script with Remove-Printer, Remove-PrinterDriver, Remove-PrinterPort in correct order"
|
||||
- "render_detect() produces script that exits 0 with Write-Output when printer found, exits 1 when absent"
|
||||
- "GET /printers/{id}/scripts/install returns 200 with PowerShell content and attachment header"
|
||||
- "GET /printers/{id}/scripts/uninstall returns 200 with PowerShell content"
|
||||
- "GET /printers/{id}/scripts/detect returns 200 with PowerShell content"
|
||||
- "GET /printers/{id}/scripts/{type} returns 404 for nonexistent printer"
|
||||
- "GET /printers/{id}/scripts/{type} returns 422 when driver or inf_filename is missing"
|
||||
artifacts:
|
||||
- path: "imptune/templates/scripts/uninstall.ps1.j2"
|
||||
provides: "PowerShell uninstall template"
|
||||
contains: "Remove-Printer"
|
||||
- path: "imptune/templates/scripts/detect.ps1.j2"
|
||||
provides: "PowerShell detection template"
|
||||
contains: "Write-Output"
|
||||
- path: "imptune/api/scripts.py"
|
||||
provides: "Script download endpoints"
|
||||
exports: ["router"]
|
||||
- path: "imptune/generators/script_generator.py"
|
||||
provides: "render_uninstall and render_detect functions added"
|
||||
exports: ["render_install", "render_uninstall", "render_detect"]
|
||||
key_links:
|
||||
- from: "imptune/api/scripts.py"
|
||||
to: "imptune/generators/script_generator.py"
|
||||
via: "import render_install, render_uninstall, render_detect"
|
||||
pattern: "from imptune\\.generators\\.script_generator import"
|
||||
- from: "imptune/api/scripts.py"
|
||||
to: "imptune/db/models.py"
|
||||
via: "Printer.get_or_none query with Driver join"
|
||||
pattern: "Printer\\.get_or_none"
|
||||
- from: "imptune/main.py"
|
||||
to: "imptune/api/scripts.py"
|
||||
via: "app.include_router(scripts.router)"
|
||||
pattern: "include_router.*scripts"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add uninstall and detection templates, then wire all three scripts to downloadable API endpoints.
|
||||
|
||||
Purpose: Completes the script generation phase by adding the two simpler templates and exposing all scripts via GET endpoints that the printer detail page (Phase 3) can link to.
|
||||
|
||||
Output: `uninstall.ps1.j2`, `detect.ps1.j2`, `scripts.py` router, updated `main.py`, passing integration tests.
|
||||
</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/04-script-generation/04-RESEARCH.md
|
||||
@.planning/phases/04-script-generation/04-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Contracts from Plan 01 that this plan builds on -->
|
||||
|
||||
From imptune/generators/script_generator.py (created in 04-01):
|
||||
```python
|
||||
# Jinja2 Environment already configured with FileSystemLoader for templates/scripts/
|
||||
# _duplex_map already defined
|
||||
def render_install(printer_name, ip_address, port_name, driver_name, inf_filename,
|
||||
duplex_mode, color_mode, paper_size, collate) -> str: ...
|
||||
# Plan 02 adds: render_uninstall(), render_detect()
|
||||
```
|
||||
|
||||
From imptune/db/models.py:
|
||||
```python
|
||||
class Printer(BaseModel):
|
||||
name = CharField()
|
||||
ip_address = CharField()
|
||||
port_name = CharField()
|
||||
driver = ForeignKeyField(Driver, null=True, backref="printers")
|
||||
duplex_mode = CharField(default="OneSided")
|
||||
color_mode = BooleanField(default=True)
|
||||
paper_size = CharField(default="A4")
|
||||
collate = BooleanField(default=True)
|
||||
|
||||
class Driver(BaseModel):
|
||||
driver_desc = CharField(null=True) # JSON list
|
||||
inf_filename = CharField(null=True)
|
||||
```
|
||||
|
||||
From imptune/main.py (router registration pattern):
|
||||
```python
|
||||
from imptune.api import clients, drivers, health, pages, printers
|
||||
app.include_router(health.router)
|
||||
app.include_router(pages.router)
|
||||
app.include_router(drivers.router)
|
||||
app.include_router(printers.router)
|
||||
app.include_router(clients.router)
|
||||
```
|
||||
|
||||
From tests/conftest.py:
|
||||
```python
|
||||
@pytest.fixture
|
||||
def client(tmp_data_dir):
|
||||
from imptune.main import app
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_data_dir(tmp_path, monkeypatch): ...
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Uninstall and detection templates + render functions</name>
|
||||
<files>imptune/generators/script_generator.py, imptune/templates/scripts/uninstall.ps1.j2, imptune/templates/scripts/detect.ps1.j2, tests/test_script_generator.py</files>
|
||||
<behavior>
|
||||
- render_uninstall(printer_name, driver_name, port_name) returns PS script with Remove-Printer BEFORE Remove-PrinterDriver BEFORE Remove-PrinterPort, all with -ErrorAction SilentlyContinue
|
||||
- render_detect(printer_name) returns PS script with Get-Printer check, Write-Output + exit 0 when found, exit 1 when absent
|
||||
</behavior>
|
||||
<action>
|
||||
**Tests first (add to existing test_script_generator.py):**
|
||||
|
||||
- `test_render_uninstall`: call render_uninstall("Test Printer", "HP Driver", "IP_10.0.0.1"), assert output contains `Remove-Printer -Name "Test Printer"`, `Remove-PrinterDriver -Name "HP Driver"`, `Remove-PrinterPort -Name "IP_10.0.0.1"`, and `-ErrorAction SilentlyContinue` on all three. Assert Remove-Printer appears BEFORE Remove-PrinterDriver (order matters — driver removal fails if printer still references it).
|
||||
- `test_render_detect`: call render_detect("Test Printer"), assert output contains `Get-Printer -Name "Test Printer"`, `Write-Output`, `exit 0`, `exit 1`.
|
||||
|
||||
Run tests — both MUST fail.
|
||||
|
||||
**Implement:**
|
||||
|
||||
Add `render_uninstall(printer_name, driver_name, port_name) -> str` to `script_generator.py`:
|
||||
- Gets `uninstall.ps1.j2` template, renders with the three names.
|
||||
|
||||
Add `render_detect(printer_name) -> str` to `script_generator.py`:
|
||||
- Gets `detect.ps1.j2` template, renders with printer_name.
|
||||
|
||||
Create `imptune/templates/scripts/uninstall.ps1.j2`:
|
||||
```
|
||||
# Header: Generated by ImpTune — Uninstall script for {{ printer_name }}
|
||||
Remove-Printer -Name "{{ printer_name }}" -ErrorAction SilentlyContinue
|
||||
Remove-PrinterDriver -Name "{{ driver_name }}" -ErrorAction SilentlyContinue
|
||||
Remove-PrinterPort -Name "{{ port_name }}" -ErrorAction SilentlyContinue
|
||||
```
|
||||
|
||||
Create `imptune/templates/scripts/detect.ps1.j2`:
|
||||
```
|
||||
# Header: Generated by ImpTune — Detection script for {{ printer_name }}
|
||||
$printer = Get-Printer -Name "{{ printer_name }}" -ErrorAction SilentlyContinue
|
||||
if ($printer) {
|
||||
Write-Output "Installed: {{ printer_name }}"
|
||||
exit 0
|
||||
} else {
|
||||
exit 1
|
||||
}
|
||||
```
|
||||
|
||||
Run tests — both MUST pass.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>python -m pytest tests/test_script_generator.py::test_render_uninstall tests/test_script_generator.py::test_render_detect -x -q</automated>
|
||||
</verify>
|
||||
<done>render_uninstall and render_detect produce correct PowerShell scripts; removal order is correct; detection uses Write-Output + exit codes per Intune contract.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Script download API endpoints and router registration</name>
|
||||
<files>imptune/api/scripts.py, imptune/main.py, tests/test_script_generator.py</files>
|
||||
<action>
|
||||
Create `imptune/api/scripts.py`:
|
||||
- `router = APIRouter(prefix="/printers")`
|
||||
- Three GET endpoints: `/{printer_id}/scripts/install`, `/{printer_id}/scripts/uninstall`, `/{printer_id}/scripts/detect`
|
||||
- Each endpoint:
|
||||
1. `Printer.get_or_none(Printer.id == printer_id)` — return `PlainTextResponse("Printer not found", status_code=404)` if None
|
||||
2. Access `printer.driver` — return `PlainTextResponse("No driver assigned", status_code=422)` if driver is None
|
||||
3. Check `driver.inf_filename` — return `PlainTextResponse("Driver has no INF file", status_code=422)` if None/empty
|
||||
4. Parse `driver_name = json.loads(driver.driver_desc)[0]` — return 422 if driver_desc is empty/null
|
||||
5. Call the appropriate render function with plain values extracted from ORM objects
|
||||
6. Return `PlainTextResponse(content=rendered, headers={"Content-Disposition": 'attachment; filename="{type}.ps1"'})`
|
||||
- For install endpoint: extract all printer fields + driver fields, call `render_install(printer.name, printer.ip_address, printer.port_name, driver_name, driver.inf_filename, printer.duplex_mode, printer.color_mode, printer.paper_size, printer.collate)`
|
||||
- For uninstall: call `render_uninstall(printer.name, driver_name, printer.port_name)`
|
||||
- For detect: call `render_detect(printer.name)`
|
||||
|
||||
Update `imptune/main.py`:
|
||||
- Add `scripts` to import: `from imptune.api import clients, drivers, health, pages, printers, scripts`
|
||||
- Add `app.include_router(scripts.router)` after existing router registrations
|
||||
|
||||
Add integration tests to `tests/test_script_generator.py`:
|
||||
- `test_install_endpoint`: create Driver + Printer via ORM in test, GET `/printers/{id}/scripts/install`, assert 200 + content contains `pnputil`
|
||||
- `test_uninstall_endpoint`: same setup, GET `/printers/{id}/scripts/uninstall`, assert 200 + `Remove-Printer`
|
||||
- `test_detect_endpoint`: same setup, GET `/printers/{id}/scripts/detect`, assert 200 + `Write-Output`
|
||||
- `test_script_endpoint_missing_printer`: GET `/printers/9999/scripts/install`, assert 404
|
||||
- `test_script_endpoint_no_driver`: create Printer without driver FK, GET install, assert 422
|
||||
|
||||
For integration tests, use the `client` fixture from conftest.py. Create test data via ORM:
|
||||
```python
|
||||
from imptune.db.models import Driver, Printer
|
||||
driver = Driver.create(sha256="abc123", original_filename="test.zip", size_bytes=100,
|
||||
driver_desc='["Test Driver"]', inf_filename="test.inf")
|
||||
printer = Printer.create(name="Test Printer", ip_address="10.0.0.1", port_name="IP_10.0.0.1",
|
||||
driver=driver, duplex_mode="LongEdge", color_mode=True,
|
||||
paper_size="A4", collate=True)
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>python -m pytest tests/test_script_generator.py -x -q</automated>
|
||||
</verify>
|
||||
<done>All script endpoints return 200 with correct PS content; 404 for missing printer; 422 for missing driver/INF; router registered in main.py; full test suite passes.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
```bash
|
||||
python -m pytest tests/ -x -q
|
||||
```
|
||||
Full test suite passes (existing + new script tests). No regressions.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- render_uninstall produces script with correct removal order
|
||||
- render_detect follows Intune detection contract (Write-Output + exit 0/1)
|
||||
- All three script types downloadable via GET /printers/{id}/scripts/{type}
|
||||
- Error handling: 404 for missing printer, 422 for missing driver/INF
|
||||
- Scripts router registered in main.py
|
||||
- Full test suite green
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-script-generation/04-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user