Commit initial
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
---
|
||||
phase: 04-script-generation
|
||||
plan: 01
|
||||
type: tdd
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- imptune/generators/script_generator.py
|
||||
- imptune/templates/scripts/install.ps1.j2
|
||||
- tests/test_script_generator.py
|
||||
autonomous: true
|
||||
requirements:
|
||||
- SCRPT-01
|
||||
- SCRPT-04
|
||||
- SCRPT-05
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "render_install() produces a complete PowerShell script containing pnputil /add-driver, Add-PrinterPort, Add-PrinterDriver, Add-Printer, Set-PrintConfiguration"
|
||||
- "Generated install script contains WOW64 relaunch guard as the first executable block"
|
||||
- "Generated install script contains SYSTEM vs user detection with UAC self-elevation"
|
||||
- "Set-PrintConfiguration receives translated duplex values (TwoSidedLongEdge, TwoSidedShortEdge)"
|
||||
- "All add operations are wrapped in idempotency checks (Get-PrinterPort, Get-Printer)"
|
||||
artifacts:
|
||||
- path: "imptune/generators/script_generator.py"
|
||||
provides: "Jinja2 Environment + render_install function with duplex_map"
|
||||
exports: ["render_install"]
|
||||
- path: "imptune/templates/scripts/install.ps1.j2"
|
||||
provides: "PowerShell install template with WOW64, UAC, pnputil, idempotency"
|
||||
min_lines: 30
|
||||
- path: "tests/test_script_generator.py"
|
||||
provides: "Unit tests for SCRPT-01, SCRPT-04, SCRPT-05"
|
||||
min_lines: 40
|
||||
key_links:
|
||||
- from: "imptune/generators/script_generator.py"
|
||||
to: "imptune/templates/scripts/install.ps1.j2"
|
||||
via: "Jinja2 FileSystemLoader"
|
||||
pattern: "_env\\.get_template.*install"
|
||||
- from: "imptune/generators/script_generator.py"
|
||||
to: "imptune/db/models.py"
|
||||
via: "Printer model fields used as template vars"
|
||||
pattern: "printer\\.name|printer\\.ip_address|printer\\.port_name"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the script generator module and install.ps1 Jinja2 template with full correctness guards.
|
||||
|
||||
Purpose: The install script is the most complex of the three scripts (WOW64, UAC, pnputil two-step, idempotency, duplex mapping). Building it first with TDD ensures all edge cases are covered before the simpler templates.
|
||||
|
||||
Output: `script_generator.py` with `render_install()`, `install.ps1.j2` template, and passing unit 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
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts from existing codebase -->
|
||||
|
||||
From imptune/db/models.py:
|
||||
```python
|
||||
class Printer(BaseModel):
|
||||
name = CharField()
|
||||
ip_address = CharField()
|
||||
port_name = CharField()
|
||||
client = ForeignKeyField(Client, null=True, backref="printers")
|
||||
driver = ForeignKeyField(Driver, null=True, backref="printers")
|
||||
duplex_mode = CharField(default="OneSided") # "OneSided" | "LongEdge" | "ShortEdge"
|
||||
color_mode = BooleanField(default=True)
|
||||
paper_size = CharField(default="A4") # "A4" | "Letter" | "Legal"
|
||||
collate = BooleanField(default=True)
|
||||
|
||||
class Driver(BaseModel):
|
||||
sha256 = CharField(unique=True, index=True)
|
||||
original_filename = CharField()
|
||||
driver_desc = CharField(null=True) # JSON list: '["HP Universal Printing PCL 6"]'
|
||||
inf_filename = CharField(null=True) # e.g. "hpcu270u.inf"
|
||||
```
|
||||
|
||||
From imptune/generators/intunewin_builder.py (pattern reference):
|
||||
```python
|
||||
# Existing generator module pattern — script_generator.py follows same structure
|
||||
# Module-level setup, public render functions
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<feature>
|
||||
<name>Install script generator with TDD</name>
|
||||
<files>imptune/generators/script_generator.py, imptune/templates/scripts/install.ps1.j2, tests/test_script_generator.py</files>
|
||||
<behavior>
|
||||
- render_install(printer_name, ip_address, port_name, driver_name, inf_filename, duplex_mode, color_mode, paper_size, collate) returns a string containing valid PowerShell
|
||||
- Output contains WOW64 guard: `$env:PROCESSOR_ARCHITECTURE -eq "x86"` and `SysNative` relaunch as the FIRST executable block
|
||||
- Output contains SYSTEM/admin detection: `WindowsIdentity::GetCurrent()`, `IsSystem`, `IsInRole(Administrator)`, `Start-Process -Verb Runas`
|
||||
- Output contains pnputil two-step: `pnputil.exe /add-driver "$PSScriptRoot\drivers\{inf_filename}" /install` then `Add-PrinterDriver -Name "{driver_name}"`
|
||||
- Output contains idempotent port creation: `Get-PrinterPort` check before `Add-PrinterPort`
|
||||
- Output contains idempotent printer creation: `Get-Printer` check before `Add-Printer`
|
||||
- Output contains `Set-PrintConfiguration` with translated duplex: "LongEdge" -> "TwoSidedLongEdge", "ShortEdge" -> "TwoSidedShortEdge", "OneSided" -> "OneSided"
|
||||
- Output contains correct boolean rendering: color_mode=True -> `$true`, color_mode=False -> `$false`
|
||||
- Output contains paper size and collate values
|
||||
- Script uses proper quoting for printer name, port name, driver name (double-quoted in PS)
|
||||
</behavior>
|
||||
<implementation>
|
||||
**RED phase — write tests first in tests/test_script_generator.py:**
|
||||
|
||||
1. Create `imptune/templates/scripts/` directory (empty, needed for Jinja2 loader)
|
||||
2. Write tests that import `render_install` from `imptune.generators.script_generator` and assert on rendered output:
|
||||
- `test_render_install_contains_pnputil`: assert `pnputil.exe /add-driver` and `Add-PrinterDriver` in output
|
||||
- `test_render_install_print_config`: assert `Set-PrintConfiguration` with `-DuplexingMode TwoSidedLongEdge` when duplex_mode="LongEdge"
|
||||
- `test_render_install_wow64_guard`: assert `PROCESSOR_ARCHITECTURE` and `SysNative` in output
|
||||
- `test_render_install_uac_guard`: assert `IsSystem` and `Start-Process` and `-Verb Runas` in output
|
||||
- `test_render_install_idempotency`: assert `Get-PrinterPort` and `Get-Printer` checks before add operations
|
||||
- `test_render_install_booleans`: assert `$true` / `$false` for color and collate
|
||||
3. Run tests — all MUST fail (RED)
|
||||
|
||||
**GREEN phase — implement:**
|
||||
|
||||
4. Create `imptune/generators/script_generator.py`:
|
||||
- Module-level Jinja2 Environment with `FileSystemLoader` pointing to `imptune/templates/scripts/`
|
||||
- `trim_blocks=True`, `lstrip_blocks=True`, `keep_trailing_newline=True`
|
||||
- `_duplex_map` dict: `{"OneSided": "OneSided", "LongEdge": "TwoSidedLongEdge", "ShortEdge": "TwoSidedShortEdge"}`
|
||||
- `render_install(printer_name, ip_address, port_name, driver_name, inf_filename, duplex_mode, color_mode, paper_size, collate) -> str`
|
||||
- Translates duplex_mode via `_duplex_map`
|
||||
- Converts color_mode/collate bools to `"true"` / `"false"` (lowercase, template adds `$` prefix)
|
||||
- Calls `_env.get_template("install.ps1.j2").render(...)` with all variables
|
||||
- Use plain string parameters (not ORM objects) so the function is testable without DB
|
||||
|
||||
5. Create `imptune/templates/scripts/install.ps1.j2`:
|
||||
Template structure (in this exact order):
|
||||
```
|
||||
# Header comment: Generated by ImpTune, printer name, install command hint
|
||||
# WOW64 Guard (FIRST executable block)
|
||||
if ($env:PROCESSOR_ARCHITECTURE -eq "x86" -and $env:PROCESSOR_ARCHITEW6432) { ... relaunch 64-bit ... exit }
|
||||
# SYSTEM/Admin check + UAC elevation
|
||||
$id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$isSystem = $id.IsSystem
|
||||
$isAdmin = ... IsInRole(Administrator)
|
||||
if (-not $isSystem -and -not $isAdmin) { Start-Process -Verb Runas ... exit }
|
||||
# pnputil driver staging
|
||||
pnputil.exe /add-driver "$PSScriptRoot\drivers\{{ inf_filename }}" /install
|
||||
# Add-PrinterDriver
|
||||
Add-PrinterDriver -Name "{{ driver_name }}"
|
||||
# Idempotent port creation
|
||||
if (-not (Get-PrinterPort -Name "{{ port_name }}" -ErrorAction SilentlyContinue)) { Add-PrinterPort ... }
|
||||
# Idempotent printer creation
|
||||
if (-not (Get-Printer -Name "{{ printer_name }}" -ErrorAction SilentlyContinue)) { Add-Printer ... }
|
||||
# Set-PrintConfiguration
|
||||
Set-PrintConfiguration -PrinterName "{{ printer_name }}" -DuplexingMode {{ duplex_mode }} -Color ${{ color }} -PaperSize {{ paper_size }} -Collate ${{ collate }}
|
||||
```
|
||||
|
||||
6. Run tests — all MUST pass (GREEN)
|
||||
|
||||
**Important notes:**
|
||||
- Use plain string args for render_install, NOT Printer ORM object — keeps tests DB-free
|
||||
- The `_duplex_map` must translate BEFORE passing to template (template receives already-mapped value)
|
||||
- Boolean values: pass as lowercase string `"true"` / `"false"` so template renders `$true` / `$false` with `${{ color }}`
|
||||
- Template must use `{{ }}` for all variable interpolation — no `{% set %}` for simple values
|
||||
- All PowerShell string parameters (printer_name, port_name, driver_name) must be double-quoted in the template
|
||||
</implementation>
|
||||
</feature>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
```bash
|
||||
python -m pytest tests/test_script_generator.py -x -q
|
||||
```
|
||||
All 6+ tests pass. Rendered install script contains all required blocks in correct order.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- render_install() produces complete PowerShell install script
|
||||
- WOW64 guard appears before any other logic
|
||||
- UAC self-elevation skips when SYSTEM
|
||||
- Duplex mode values correctly translated (LongEdge -> TwoSidedLongEdge)
|
||||
- All add operations wrapped in idempotency checks
|
||||
- All unit tests pass
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-script-generation/04-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
phase: 04-script-generation
|
||||
plan: "01"
|
||||
subsystem: script-generator
|
||||
tags: [jinja2, powershell, tdd, wow64, uac, pnputil, idempotency]
|
||||
one_liner: "Jinja2-based install.ps1 generator with WOW64 guard, UAC elevation, pnputil two-step, and duplex mapping"
|
||||
dependency_graph:
|
||||
requires: []
|
||||
provides: [render_install, install.ps1.j2]
|
||||
affects: [imptune.generators.script_generator, imptune.templates.scripts]
|
||||
tech_stack:
|
||||
added: []
|
||||
patterns:
|
||||
- Jinja2 FileSystemLoader with trim_blocks + lstrip_blocks for PowerShell templates
|
||||
- Plain-string function parameters for DB-free unit testability
|
||||
- _duplex_map translation dict (model values -> PowerShell cmdlet values)
|
||||
key_files:
|
||||
created:
|
||||
- imptune/generators/script_generator.py
|
||||
- imptune/templates/scripts/install.ps1.j2
|
||||
- tests/test_script_generator.py
|
||||
modified: []
|
||||
decisions:
|
||||
- "render_install() takes plain string args (not ORM Printer object) — keeps tests DB-free"
|
||||
- "Boolean color_mode/collate converted to lowercase 'true'/'false' strings; template adds $ prefix"
|
||||
- "_duplex_map translates before rendering: LongEdge->TwoSidedLongEdge, ShortEdge->TwoSidedShortEdge"
|
||||
- "WOW64 guard comment avoids 'pnputil.exe' text to preserve ordering assertion in test"
|
||||
metrics:
|
||||
duration: "~2 min"
|
||||
completed_date: "2026-04-10"
|
||||
tasks_completed: 1
|
||||
files_created: 3
|
||||
files_modified: 0
|
||||
tests_added: 7
|
||||
tests_passing: 68
|
||||
requirements-completed: [SCRPT-01, SCRPT-04, SCRPT-05]
|
||||
---
|
||||
|
||||
# Phase 4 Plan 01: Script Generator (Install) Summary
|
||||
|
||||
**One-liner:** Jinja2-based install.ps1 generator with WOW64 guard, UAC elevation, pnputil two-step, and duplex mapping
|
||||
|
||||
## What Was Built
|
||||
|
||||
A TDD-developed module `imptune/generators/script_generator.py` with a single public function `render_install()` that renders the `install.ps1.j2` Jinja2 template into a complete, production-ready PowerShell printer install script.
|
||||
|
||||
### render_install() function
|
||||
|
||||
- Takes plain string arguments (no ORM dependency) for easy unit testing
|
||||
- Translates `duplex_mode` via `_duplex_map` before passing to template
|
||||
- Converts Python booleans to lowercase strings (`"true"`/`"false"`) for PowerShell `$true`/`$false` rendering
|
||||
|
||||
### install.ps1.j2 template structure (in order)
|
||||
|
||||
1. Header comment with printer name and required Intune install command
|
||||
2. WOW64 guard (`$env:PROCESSOR_ARCHITECTURE` + `SysNative` relaunch) — FIRST executable block
|
||||
3. SYSTEM vs admin detection (`[WindowsIdentity]::GetCurrent()`, `IsSystem`, `IsInRole(Administrator)`) + UAC self-elevation via `Start-Process -Verb Runas`
|
||||
4. pnputil two-step: `/add-driver` to stage INF, then `Add-PrinterDriver` to register
|
||||
5. Idempotent port creation: `Get-PrinterPort` check before `Add-PrinterPort`
|
||||
6. Idempotent printer creation: `Get-Printer` check before `Add-Printer`
|
||||
7. `Set-PrintConfiguration` with translated duplex, color, paper size, collate
|
||||
|
||||
## TDD Execution
|
||||
|
||||
### RED Phase (commit b4f2c64)
|
||||
|
||||
7 tests written in `tests/test_script_generator.py` covering SCRPT-01, SCRPT-04, SCRPT-05. All failed with `ModuleNotFoundError` (confirmed RED).
|
||||
|
||||
### GREEN Phase (commit 8193e9d)
|
||||
|
||||
- `imptune/generators/script_generator.py` created
|
||||
- `imptune/templates/scripts/install.ps1.j2` created
|
||||
|
||||
One auto-fix required during GREEN: template comment contained `"pnputil.exe"` before the WOW64 `PROCESSOR_ARCHITECTURE` check text, causing the ordering assertion in `test_render_install_wow64_guard` to fail. Fixed by removing `.exe` from the comment text. Not a logic error — purely a textual ordering issue in the rendered output.
|
||||
|
||||
All 7 new tests pass. Full suite: 68/68 passing.
|
||||
|
||||
## Commits
|
||||
|
||||
| Hash | Type | Description |
|
||||
|------|------|-------------|
|
||||
| b4f2c64 | test | RED phase — 7 failing tests for script_generator |
|
||||
| 8193e9d | feat | GREEN phase — script_generator.py + install.ps1.j2 |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] Template comment contained 'pnputil.exe' before WOW64 guard text**
|
||||
- **Found during:** GREEN phase test run
|
||||
- **Issue:** Comment in the WOW64 guard block header said "pnputil.exe is 64-bit only", so the string "pnputil.exe" appeared in the rendered output before "PROCESSOR_ARCHITECTURE", breaking the ordering assertion in `test_render_install_wow64_guard`
|
||||
- **Fix:** Changed "pnputil.exe is 64-bit only" to "pnputil is 64-bit only" in the template comment
|
||||
- **Files modified:** `imptune/templates/scripts/install.ps1.j2`
|
||||
- **Commit:** 8193e9d (included in same GREEN commit)
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
All created files verified on disk. All commits verified in git log.
|
||||
|
||||
| Item | Status |
|
||||
|------|--------|
|
||||
| imptune/generators/script_generator.py | FOUND |
|
||||
| imptune/templates/scripts/install.ps1.j2 | FOUND |
|
||||
| tests/test_script_generator.py | FOUND |
|
||||
| Commit b4f2c64 (RED) | FOUND |
|
||||
| Commit 8193e9d (GREEN) | FOUND |
|
||||
@@ -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>
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
phase: 04-script-generation
|
||||
plan: "02"
|
||||
subsystem: api
|
||||
tags: [powershell, jinja2, fastapi, intune, tdd]
|
||||
|
||||
requires:
|
||||
- phase: 04-01
|
||||
provides: [render_install, install.ps1.j2, script_generator module with Jinja2 env]
|
||||
provides:
|
||||
- render_uninstall function (Remove-Printer/Driver/Port in safe order)
|
||||
- render_detect function (Intune detection contract)
|
||||
- uninstall.ps1.j2 template
|
||||
- detect.ps1.j2 template
|
||||
- GET /printers/{id}/scripts/install endpoint
|
||||
- GET /printers/{id}/scripts/uninstall endpoint
|
||||
- GET /printers/{id}/scripts/detect endpoint
|
||||
- scripts.py APIRouter registered in main.py
|
||||
affects: [phase-05-packaging]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- Shared _get_printer_and_driver() helper extracts ORM validation to avoid duplication across 3 endpoints
|
||||
- All script endpoints return PlainTextResponse with Content-Disposition attachment header
|
||||
- Integration tests use ORM directly (Driver.create/Printer.create) — no HTTP fixture for setup
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- imptune/templates/scripts/uninstall.ps1.j2
|
||||
- imptune/templates/scripts/detect.ps1.j2
|
||||
- imptune/api/scripts.py
|
||||
modified:
|
||||
- imptune/generators/script_generator.py
|
||||
- imptune/main.py
|
||||
- tests/test_script_generator.py
|
||||
|
||||
key-decisions:
|
||||
- "_get_printer_and_driver() private helper centralises 404/422 validation for all 3 script endpoints"
|
||||
- "PlainTextResponse with Content-Disposition attachment; filename='{type}.ps1' on all script endpoints"
|
||||
- "Integration tests create ORM records directly (Driver.create/Printer.create) — same pattern as printer CRUD tests"
|
||||
|
||||
patterns-established:
|
||||
- "Script endpoint pattern: validate printer -> validate driver -> validate inf -> parse driver_desc -> render -> return attachment"
|
||||
|
||||
requirements-completed: [SCRPT-02, SCRPT-03]
|
||||
|
||||
duration: ~2min
|
||||
completed: 2026-04-10
|
||||
---
|
||||
|
||||
# Phase 4 Plan 02: Script Generator (Uninstall + Detect + API) Summary
|
||||
|
||||
**Jinja2 uninstall/detect templates, render_uninstall/render_detect functions, and three downloadable PS1 script endpoints wired to the scripts router**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~2 min
|
||||
- **Started:** 2026-04-10T11:33:58Z
|
||||
- **Completed:** 2026-04-10T11:36:13Z
|
||||
- **Tasks:** 2
|
||||
- **Files modified:** 6
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- render_uninstall() produces Remove-Printer > Remove-PrinterDriver > Remove-PrinterPort with -ErrorAction SilentlyContinue (safe ordering)
|
||||
- render_detect() follows Intune detection contract: Get-Printer check, Write-Output + exit 0 when found, exit 1 when absent
|
||||
- Three GET endpoints /printers/{id}/scripts/{install,uninstall,detect} return PS1 scripts as file downloads
|
||||
- Full error handling: 404 for missing printer, 422 for missing driver/INF/driver_desc
|
||||
- Full test suite green: 75 tests (7 new tests added)
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1 RED: Failing tests for render_uninstall/detect** - `0f213df` (test)
|
||||
2. **Task 1 GREEN: render_uninstall + render_detect + templates** - `6bff8f3` (feat)
|
||||
3. **Task 2: Script API endpoints + router registration** - `b7b0d1b` (feat)
|
||||
|
||||
_Note: TDD task split into RED + GREEN commits per TDD protocol_
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `imptune/templates/scripts/uninstall.ps1.j2` - PowerShell uninstall template (Remove-Printer/Driver/Port in order)
|
||||
- `imptune/templates/scripts/detect.ps1.j2` - PowerShell Intune detection template (Get-Printer + exit 0/1)
|
||||
- `imptune/generators/script_generator.py` - Added render_uninstall() and render_detect() functions
|
||||
- `imptune/api/scripts.py` - APIRouter with 3 script download endpoints, shared validation helper
|
||||
- `imptune/main.py` - Registered scripts.router
|
||||
- `tests/test_script_generator.py` - Added 2 unit tests + 5 integration tests
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- `_get_printer_and_driver()` private helper centralises 404/422 validation logic for all three endpoints — avoids repeating identical ORM+validation code 3 times
|
||||
- `PlainTextResponse` with `Content-Disposition: attachment; filename="{type}.ps1"` on all endpoints so browsers download the file rather than rendering it
|
||||
- Integration tests create ORM records directly via `Driver.create()`/`Printer.create()` — same established pattern as printer CRUD tests, no HTTP API calls for setup
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None - all tests passed on first run after implementation.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- All three script types downloadable via API — ready for Phase 5 packaging
|
||||
- render_install, render_uninstall, render_detect all available in script_generator module
|
||||
- scripts.py router registered and functional
|
||||
|
||||
---
|
||||
*Phase: 04-script-generation*
|
||||
*Completed: 2026-04-10*
|
||||
@@ -0,0 +1,584 @@
|
||||
# Phase 4: Script Generation - Research
|
||||
|
||||
**Researched:** 2026-04-10
|
||||
**Domain:** Jinja2 template-based PowerShell script generation; Intune/RMM printer deployment patterns
|
||||
**Confidence:** HIGH
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 4 produces three PowerShell scripts — install, uninstall, and detection — rendered from
|
||||
Jinja2 templates stored in `imptune/templates/scripts/`. The project already uses Jinja2 3.1.*
|
||||
(it is in `requirements.txt` and drives all HTML pages), so no new dependency is required.
|
||||
Script generation fits naturally as a new module `imptune/generators/script_generator.py` plus a
|
||||
FastAPI router `imptune/api/scripts.py`, following the exact same patterns as `intunewin_builder.py`
|
||||
and the existing API routers.
|
||||
|
||||
The most important correctness risk is a **duplex mode name mismatch**: the `Printer` model stores
|
||||
`OneSided | LongEdge | ShortEdge`, but `Set-PrintConfiguration -DuplexingMode` accepts
|
||||
`OneSided | TwoSidedLongEdge | TwoSidedShortEdge`. The templates must translate these values.
|
||||
|
||||
The second highest risk is the **WOW64 / 32-bit Intune execution context**: Intune's Win32 app
|
||||
installer runs in a 32-bit PowerShell process. `pnputil.exe` does not exist under SysWOW64, so the
|
||||
install script must detect the 32-bit environment and relaunch itself under 64-bit PowerShell before
|
||||
any driver operations occur.
|
||||
|
||||
**Primary recommendation:** Render scripts from Jinja2 `.ps1.j2` templates with
|
||||
`trim_blocks=True, lstrip_blocks=True`. Place templates at
|
||||
`imptune/templates/scripts/{install,uninstall,detect}.ps1.j2`. Return rendered content as
|
||||
`PlainTextResponse` with `Content-Disposition: attachment` from a GET endpoint.
|
||||
|
||||
---
|
||||
|
||||
<phase_requirements>
|
||||
## Phase Requirements
|
||||
|
||||
| ID | Description | Research Support |
|
||||
|----|-------------|-----------------|
|
||||
| SCRPT-01 | Generate PowerShell install script (pnputil staging + Add-PrinterPort + Add-PrinterDriver + Add-Printer + Set-PrintConfiguration) | Verified PowerShell cmdlets; pnputil two-step pattern documented below |
|
||||
| SCRPT-02 | Generate PowerShell uninstall script (Remove-Printer + Remove-PrinterDriver + Remove-PrinterPort) | Standard cmdlets; idempotency via -ErrorAction SilentlyContinue |
|
||||
| SCRPT-03 | Generate Intune detection script (printer-name registry check → exit 0 / exit 1) | Intune detection contract verified: Write-Output + exit 0 for present, exit 1 for absent |
|
||||
| SCRPT-04 | Install script detects SYSTEM vs user context and self-elevates via UAC when run by user | Pattern verified: [Environment]::UserName check + Start-Process -Verb Runas |
|
||||
| SCRPT-05 | Install script includes 64-bit WOW64 relaunch guard for Intune's 32-bit execution context | Pattern verified: $env:PROCESSOR_ARCHITECTURE + SysNative path |
|
||||
</phase_requirements>
|
||||
|
||||
---
|
||||
|
||||
## Standard Stack
|
||||
|
||||
### Core (already installed — no new packages needed)
|
||||
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| Jinja2 | 3.1.* | Template rendering engine | Already in requirements.txt; used for all HTML pages |
|
||||
| FastAPI | 0.115.* | HTTP routing + response types | Already in requirements.txt; existing router pattern |
|
||||
| Peewee | 3.17.* | ORM for reading Printer + Driver records | Already in requirements.txt |
|
||||
|
||||
### No New Dependencies
|
||||
|
||||
Script generation requires **zero new packages**. Jinja2 is the template engine; FastAPI returns
|
||||
`PlainTextResponse` with an attachment header; the Printer/Driver ORM models provide all data.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Nothing to install — all dependencies already present in requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Recommended File Layout
|
||||
|
||||
```
|
||||
imptune/
|
||||
├── generators/
|
||||
│ ├── __init__.py
|
||||
│ ├── intunewin_builder.py # existing
|
||||
│ └── script_generator.py # NEW — renders templates to strings
|
||||
├── templates/
|
||||
│ ├── scripts/ # NEW directory
|
||||
│ │ ├── install.ps1.j2
|
||||
│ │ ├── uninstall.ps1.j2
|
||||
│ │ └── detect.ps1.j2
|
||||
│ └── ... (existing HTML templates)
|
||||
├── api/
|
||||
│ ├── scripts.py # NEW — GET /printers/{id}/scripts/{type}
|
||||
│ └── ... (existing routers)
|
||||
tests/
|
||||
└── test_script_generator.py # NEW
|
||||
```
|
||||
|
||||
### Pattern 1: Jinja2 Environment for Script Templates
|
||||
|
||||
Use a separate `Environment` with `trim_blocks=True` and `lstrip_blocks=True` to prevent Jinja
|
||||
control-block lines from producing blank lines in rendered scripts.
|
||||
|
||||
```python
|
||||
# imptune/generators/script_generator.py
|
||||
from pathlib import Path
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
_SCRIPTS_DIR = Path(__file__).parent.parent / "templates" / "scripts"
|
||||
|
||||
_env = Environment(
|
||||
loader=FileSystemLoader(str(_SCRIPTS_DIR)),
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True,
|
||||
keep_trailing_newline=True,
|
||||
)
|
||||
|
||||
def render_install(printer, inf_filename: str, driver_name: str) -> str:
|
||||
"""Render install.ps1.j2 with the given printer config."""
|
||||
tpl = _env.get_template("install.ps1.j2")
|
||||
return tpl.render(
|
||||
printer_name=printer.name,
|
||||
ip_address=printer.ip_address,
|
||||
port_name=printer.port_name,
|
||||
driver_name=driver_name,
|
||||
inf_filename=inf_filename,
|
||||
duplex_mode=_duplex_map[printer.duplex_mode], # translate enum
|
||||
color=str(printer.color_mode).lower(), # "$true" / "$false"
|
||||
paper_size=printer.paper_size,
|
||||
collate=str(printer.collate).lower(),
|
||||
)
|
||||
|
||||
_duplex_map = {
|
||||
"OneSided": "OneSided",
|
||||
"LongEdge": "TwoSidedLongEdge",
|
||||
"ShortEdge": "TwoSidedShortEdge",
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: FastAPI Script Download Endpoint
|
||||
|
||||
```python
|
||||
# imptune/api/scripts.py
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from imptune.db.models import Printer, Driver
|
||||
from imptune.generators.script_generator import render_install, render_uninstall, render_detect
|
||||
import json
|
||||
|
||||
router = APIRouter(prefix="/printers")
|
||||
|
||||
@router.get("/{printer_id}/scripts/install", response_class=PlainTextResponse)
|
||||
def download_install_script(printer_id: int) -> PlainTextResponse:
|
||||
printer = Printer.get_or_none(Printer.id == printer_id)
|
||||
if printer is None:
|
||||
return PlainTextResponse("Not found", status_code=404)
|
||||
driver = printer.driver
|
||||
driver_names = json.loads(driver.driver_desc) if driver and driver.driver_desc else []
|
||||
driver_name = driver_names[0] if driver_names else ""
|
||||
inf_filename = driver.inf_filename if driver else ""
|
||||
content = render_install(printer, inf_filename, driver_name)
|
||||
return PlainTextResponse(
|
||||
content=content,
|
||||
headers={"Content-Disposition": 'attachment; filename="install.ps1"'},
|
||||
)
|
||||
```
|
||||
|
||||
Register in `main.py` alongside existing routers:
|
||||
```python
|
||||
from imptune.api import scripts
|
||||
app.include_router(scripts.router)
|
||||
```
|
||||
|
||||
### Pattern 3: Install Script Structure (Jinja2 Template Logic)
|
||||
|
||||
The `.ps1.j2` template must implement the following blocks in order:
|
||||
|
||||
```
|
||||
1. WOW64 guard (detect 32-bit, relaunch self in 64-bit, exit 32-bit process)
|
||||
2. SYSTEM vs user context check (self-elevate via UAC if running as user)
|
||||
3. pnputil step 1: /add-driver <inf> /install (uses $PSScriptRoot)
|
||||
4. Idempotency check: Add-PrinterPort only if port does not exist
|
||||
5. Add-PrinterDriver -Name <driver_name>
|
||||
6. Idempotency check: Add-Printer only if printer does not exist
|
||||
7. Set-PrintConfiguration for duplex, color, paper size, collate
|
||||
```
|
||||
|
||||
### Pattern 4: Idempotency Guards
|
||||
|
||||
All install operations must be idempotent to avoid Intune re-run failures:
|
||||
|
||||
```powershell
|
||||
# Port idempotency
|
||||
if (-not (Get-PrinterPort -Name "{{ port_name }}" -ErrorAction SilentlyContinue)) {
|
||||
Add-PrinterPort -Name "{{ port_name }}" -PrinterHostAddress "{{ ip_address }}"
|
||||
}
|
||||
|
||||
# Printer idempotency
|
||||
if (-not (Get-Printer -Name "{{ printer_name }}" -ErrorAction SilentlyContinue)) {
|
||||
Add-Printer -Name "{{ printer_name }}" -PortName "{{ port_name }}" -DriverName "{{ driver_name }}"
|
||||
}
|
||||
```
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
|
||||
- **Hardcoding System32 paths:** `C:\Windows\System32\pnputil.exe` fails from 32-bit context. Use `"$env:WINDIR\SysNative\pnputil.exe"` OR the WOW64 guard ensures the script already runs 64-bit by the time pnputil is called (then `pnputil.exe` resolves correctly from PATH).
|
||||
- **UAC elevation when already SYSTEM:** SYSTEM account does not need UAC and `Start-Process -Verb Runas` fails silently. The install script must detect the current identity and skip elevation when running as SYSTEM.
|
||||
- **Skipping idempotency checks:** Running `Add-Printer` twice throws a non-terminating error that Intune logs as a warning. Wrap all add operations with existence checks.
|
||||
- **Using bare jinja2.Environment without trim_blocks:** Produces extra blank lines from `{% if %}` blocks that make scripts harder to read and diff.
|
||||
- **Returning scripts as `application/octet-stream`:** Use `text/plain` so browsers open them without a save dialog, or use `Content-Disposition: attachment` explicitly.
|
||||
|
||||
---
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| Script whitespace | Manual string concatenation | Jinja2 with trim_blocks | Edge cases: trailing newlines, empty blocks, nested conditionals |
|
||||
| Template file loading | Inline heredocs in Python | Jinja2 FileSystemLoader | Testable in isolation; editor syntax highlighting; version-controlled separately |
|
||||
| Printer existence check | Registry query in Python | `Get-Printer` / `Get-PrinterPort` in the script itself | The check must run on the endpoint, not on the server |
|
||||
| Detection logic | Custom WMI query | Get-Printer + registry path pattern | Intune's detection contract is well-defined: exit 0 + STDOUT for present |
|
||||
|
||||
**Key insight:** Script logic (WOW64 guard, UAC self-elevation, idempotency) lives in the `.ps1.j2`
|
||||
template file, not in Python. Python only provides data variables. This keeps scripts readable and
|
||||
testable as real PowerShell without a Python interpreter on the endpoint.
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Duplex Mode Name Mismatch
|
||||
|
||||
**What goes wrong:** `Set-PrintConfiguration -DuplexingMode LongEdge` throws an error — the
|
||||
accepted values are `OneSided | TwoSidedLongEdge | TwoSidedShortEdge`.
|
||||
|
||||
**Why it happens:** The Printer model was designed with short names (`LongEdge`, `ShortEdge`) for
|
||||
UI simplicity. The PowerShell cmdlet uses the full names.
|
||||
|
||||
**How to avoid:** Use the `_duplex_map` dict in `script_generator.py` to translate before rendering.
|
||||
|
||||
**Warning signs:** `Set-PrintConfiguration` throws `"Cannot bind parameter 'DuplexingMode'"`.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 2: WOW64 — pnputil Not Found
|
||||
|
||||
**What goes wrong:** `pnputil.exe /add-driver` fails with "The system cannot find the file specified"
|
||||
when Intune's 32-bit PowerShell runs the script.
|
||||
|
||||
**Why it happens:** Under WOW64, `C:\Windows\System32` is redirected to `SysWOW64`. `pnputil.exe`
|
||||
does not exist in `SysWOW64`.
|
||||
|
||||
**How to avoid:** Place the WOW64 relaunch guard as the VERY FIRST executable block in the install
|
||||
script (before any function definitions or logic):
|
||||
|
||||
```powershell
|
||||
# WOW64 Guard — must be first
|
||||
if ($env:PROCESSOR_ARCHITECTURE -eq "x86" -and $env:PROCESSOR_ARCHITEW6432) {
|
||||
$64bit = "$env:WINDIR\SysNative\WindowsPowerShell\v1.0\powershell.exe"
|
||||
& $64bit -NoProfile -ExecutionPolicy Bypass -File $PSCommandPath @args
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
```
|
||||
|
||||
**Warning signs:** Error in Intune management console about `pnputil.exe` not found; install fails
|
||||
only on 64-bit machines through Intune but succeeds when run manually.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 3: UAC Elevation When Running as SYSTEM
|
||||
|
||||
**What goes wrong:** Calling `Start-Process powershell -Verb Runas` when already running as the
|
||||
SYSTEM account causes the elevation attempt to fail or prompt unexpectedly.
|
||||
|
||||
**Why it happens:** SYSTEM is already the highest privilege. `-Verb Runas` triggers UAC which does
|
||||
not make sense for a service account.
|
||||
|
||||
**How to avoid:** Check the current user identity before attempting elevation:
|
||||
|
||||
```powershell
|
||||
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$isSystem = $currentUser.IsSystem
|
||||
$isAdmin = ([System.Security.Principal.WindowsPrincipal]$currentUser).IsInRole(
|
||||
[System.Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
|
||||
if (-not $isAdmin -and -not $isSystem) {
|
||||
# Re-launch with elevation
|
||||
Start-Process powershell.exe -Verb Runas `
|
||||
-ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" `
|
||||
-Wait
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
# If SYSTEM or already admin, continue directly
|
||||
```
|
||||
|
||||
**Warning signs:** UAC dialog appears when Intune runs the script; script hangs waiting for user input.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 4: Detection Script STDOUT Requirement
|
||||
|
||||
**What goes wrong:** Detection script exits 0 but Intune still marks app as "Not installed".
|
||||
|
||||
**Why it happens:** Intune's detection contract requires both exit 0 AND a non-empty STDOUT string.
|
||||
Exit 0 alone is insufficient.
|
||||
|
||||
**How to avoid:**
|
||||
```powershell
|
||||
$printer = Get-Printer -Name "{{ printer_name }}" -ErrorAction SilentlyContinue
|
||||
if ($printer) {
|
||||
Write-Output "Installed"
|
||||
exit 0
|
||||
} else {
|
||||
exit 1
|
||||
}
|
||||
```
|
||||
|
||||
**Warning signs:** Script returns 0 in testing but Intune keeps re-installing.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 5: $PSScriptRoot Empty in Intune Context
|
||||
|
||||
**What goes wrong:** `$PSScriptRoot` is empty when PowerShell executes a script via
|
||||
`-Command` flag rather than `-File` flag.
|
||||
|
||||
**Why it happens:** `$PSScriptRoot` is only populated when the script is launched with `-File`.
|
||||
|
||||
**How to avoid:** Always configure Intune install command as:
|
||||
```
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "install.ps1"
|
||||
```
|
||||
Not as `-Command ".\install.ps1"`. Document the required Intune install command string in the
|
||||
generated script header comment.
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 6: Set-PrintConfiguration Requires Printer Already Exist
|
||||
|
||||
**What goes wrong:** `Set-PrintConfiguration` throws if called before `Add-Printer` completes.
|
||||
|
||||
**Why it happens:** The Print Spooler service may need a moment to register the printer.
|
||||
|
||||
**How to avoid:** Call `Set-PrintConfiguration` immediately after `Add-Printer` in the same script
|
||||
block. No sleep is required if the same PowerShell session registers the printer synchronously.
|
||||
|
||||
---
|
||||
|
||||
## Code Examples
|
||||
|
||||
Verified patterns from official sources:
|
||||
|
||||
### WOW64 Relaunch Guard (SCRPT-05)
|
||||
|
||||
```powershell
|
||||
# Source: community-verified pattern, consistent with call4cloud.nl + patchmypc.com research
|
||||
# Must appear BEFORE any other logic in the script
|
||||
if ($env:PROCESSOR_ARCHITECTURE -eq "x86" -and $env:PROCESSOR_ARCHITEW6432) {
|
||||
$ps64 = "$env:WINDIR\SysNative\WindowsPowerShell\v1.0\powershell.exe"
|
||||
& $ps64 -NoProfile -ExecutionPolicy Bypass -File "$PSCommandPath" @args
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
```
|
||||
|
||||
### SYSTEM vs User Context Detection + UAC Self-Elevation (SCRPT-04)
|
||||
|
||||
```powershell
|
||||
# Source: [System.Security.Principal.WindowsIdentity] — .NET BCL, available in all PS versions
|
||||
$id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$isSystem = $id.IsSystem
|
||||
$isAdmin = ([System.Security.Principal.WindowsPrincipal]$id).IsInRole(
|
||||
[System.Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
|
||||
if (-not $isSystem -and -not $isAdmin) {
|
||||
Start-Process powershell.exe `
|
||||
-Verb Runas `
|
||||
-ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" `
|
||||
-Wait
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
```
|
||||
|
||||
### pnputil Two-Step Driver Staging (SCRPT-01)
|
||||
|
||||
```powershell
|
||||
# Source: msendpointmgr.com + call4cloud.nl verified
|
||||
# Step 1: Stage INF into Windows Driver Store
|
||||
pnputil.exe /add-driver "$PSScriptRoot\drivers\{{ inf_filename }}" /install
|
||||
|
||||
# Step 2: Install named driver from Driver Store
|
||||
Add-PrinterDriver -Name "{{ driver_name }}"
|
||||
```
|
||||
|
||||
> Note: By the time this runs, the WOW64 guard has already relaunched in 64-bit PowerShell,
|
||||
> so `pnputil.exe` resolves to `System32\pnputil.exe` without needing an explicit path.
|
||||
|
||||
### Port + Printer Creation with Idempotency
|
||||
|
||||
```powershell
|
||||
# Source: call4cloud.nl pattern, verified against Microsoft PrintManagement module docs
|
||||
if (-not (Get-PrinterPort -Name "{{ port_name }}" -ErrorAction SilentlyContinue)) {
|
||||
Add-PrinterPort -Name "{{ port_name }}" -PrinterHostAddress "{{ ip_address }}"
|
||||
}
|
||||
|
||||
if (-not (Get-Printer -Name "{{ printer_name }}" -ErrorAction SilentlyContinue)) {
|
||||
Add-Printer -Name "{{ printer_name }}" `
|
||||
-PortName "{{ port_name }}" `
|
||||
-DriverName "{{ driver_name }}"
|
||||
}
|
||||
```
|
||||
|
||||
### Set-PrintConfiguration (SCRPT-01)
|
||||
|
||||
```powershell
|
||||
# Source: Microsoft Learn — Set-PrintConfiguration (windowsserver2025-ps)
|
||||
# DuplexingMode accepted values: OneSided, TwoSidedLongEdge, TwoSidedShortEdge
|
||||
# PaperSize accepted values include: A4, Letter, Legal (and many others)
|
||||
# Color: Boolean ($true / $false)
|
||||
# Collate: Boolean ($true / $false)
|
||||
Set-PrintConfiguration -PrinterName "{{ printer_name }}" `
|
||||
-DuplexingMode {{ duplex_mode }} `
|
||||
-Color ${{ color }} `
|
||||
-PaperSize {{ paper_size }} `
|
||||
-Collate ${{ collate }}
|
||||
```
|
||||
|
||||
### Uninstall Script (SCRPT-02)
|
||||
|
||||
```powershell
|
||||
# Source: call4cloud.nl verified; -ErrorAction SilentlyContinue for idempotency
|
||||
Remove-Printer -Name "{{ printer_name }}" -ErrorAction SilentlyContinue
|
||||
Remove-PrinterDriver -Name "{{ driver_name }}" -ErrorAction SilentlyContinue
|
||||
Remove-PrinterPort -Name "{{ port_name }}" -ErrorAction SilentlyContinue
|
||||
```
|
||||
|
||||
> Note on driver removal order: Remove-Printer BEFORE Remove-PrinterDriver. Removing the driver
|
||||
> while a printer still references it produces an error.
|
||||
|
||||
### Detection Script (SCRPT-03)
|
||||
|
||||
```powershell
|
||||
# Source: Intune detection script contract (powershellisfun.com + andrewstaylor.com verified)
|
||||
# Intune requires: exit 0 + non-empty STDOUT = installed; any other exit = not installed
|
||||
$printer = Get-Printer -Name "{{ printer_name }}" -ErrorAction SilentlyContinue
|
||||
if ($printer) {
|
||||
Write-Output "Installed: {{ printer_name }}"
|
||||
exit 0
|
||||
} else {
|
||||
exit 1
|
||||
}
|
||||
```
|
||||
|
||||
### Jinja2 Environment Setup for Script Templates
|
||||
|
||||
```python
|
||||
# Source: Jinja2 3.1.x official docs — trim_blocks + lstrip_blocks for non-HTML rendering
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from pathlib import Path
|
||||
|
||||
_env = Environment(
|
||||
loader=FileSystemLoader(str(Path(__file__).parent.parent / "templates" / "scripts")),
|
||||
trim_blocks=True, # removes newline after block tags ({% %})
|
||||
lstrip_blocks=True, # strips leading spaces/tabs before block tags
|
||||
keep_trailing_newline=True, # preserves final newline (important for scripts)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | Notes |
|
||||
|--------------|------------------|-------|
|
||||
| IntuneWinAppUtil.exe for packaging | Python-native (Phase 1 decision) | Locked decision |
|
||||
| Hardcoded scripts per printer | Jinja2 template rendering | Enables regeneration (PRNT-10) |
|
||||
| printui.exe for settings export | Set-PrintConfiguration cmdlet | Cmdlet is the current standard |
|
||||
| HKLM\...\Print\Printers registry check | Get-Printer cmdlet check | Both work; Get-Printer is more reliable |
|
||||
|
||||
**Deprecated/outdated:**
|
||||
- `wmic printer` queries: deprecated in Windows 11, use `Get-Printer`
|
||||
- `Set-WmiInstance Win32_PrinterConfiguration`: superseded by `Set-PrintConfiguration`
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Multiple driver names per INF**
|
||||
- What we know: The model stores `driver_desc` as a JSON list of names (e.g., `["HP Universal", "HP Universal PCL6"]`)
|
||||
- What's unclear: Which name should be used in the script when multiple are present?
|
||||
- Recommendation: Use the **first** name from the list (index 0). The driver selection UI (Phase 2, DRV-03) enforced a single selection — that selected name should be stored separately, or the template receives `driver_name` as the first list element. The planner should decide whether to store the selected driver name on the Printer record or derive it at render time.
|
||||
|
||||
2. **driver_name field on Printer model**
|
||||
- What we know: The `Printer` model has a `driver` FK to `Driver`, but no `driver_name` field storing the specific selected name.
|
||||
- What's unclear: Phase 2 let users pick a driver name from a dropdown, but this selection is not persisted on the Printer record.
|
||||
- Recommendation: Either (a) add a `selected_driver_name` CharField to the Printer model in this phase, or (b) derive it as `json.loads(printer.driver.driver_desc)[0]` at render time. Option (b) avoids a schema change and is simpler for v1.
|
||||
|
||||
3. **inf_filename on Driver record**
|
||||
- What we know: `Driver.inf_filename` is nullable. If null, pnputil staging cannot proceed.
|
||||
- Recommendation: The generate endpoint should return a 400/422 with a clear message if `inf_filename` is null or driver is unassigned.
|
||||
|
||||
---
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
### Test Framework
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Framework | pytest >= 8.0 |
|
||||
| Config file | none — discovered automatically |
|
||||
| Quick run command | `python -m pytest tests/test_script_generator.py -x -q` |
|
||||
| Full suite command | `python -m pytest tests/ -x -q` |
|
||||
|
||||
### Phase Requirements → Test Map
|
||||
|
||||
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
||||
|--------|----------|-----------|-------------------|-------------|
|
||||
| SCRPT-01 | render_install() produces script containing pnputil, Add-PrinterPort, Add-PrinterDriver, Add-Printer, Set-PrintConfiguration | unit | `python -m pytest tests/test_script_generator.py::test_render_install_contains_pnputil -x` | ❌ Wave 0 |
|
||||
| SCRPT-01 | Set-PrintConfiguration receives correct duplex/color/paper/collate values | unit | `python -m pytest tests/test_script_generator.py::test_render_install_print_config -x` | ❌ Wave 0 |
|
||||
| SCRPT-02 | render_uninstall() produces script with Remove-Printer, Remove-PrinterDriver, Remove-PrinterPort | unit | `python -m pytest tests/test_script_generator.py::test_render_uninstall -x` | ❌ Wave 0 |
|
||||
| SCRPT-03 | render_detect() exits 0 with Write-Output when printer present; exits 1 when absent | unit | `python -m pytest tests/test_script_generator.py::test_render_detect -x` | ❌ Wave 0 |
|
||||
| SCRPT-04 | Install script contains IsSystem + IsInRole check + Start-Process Runas | unit | `python -m pytest tests/test_script_generator.py::test_render_install_uac_guard -x` | ❌ Wave 0 |
|
||||
| SCRPT-05 | Install script contains PROCESSOR_ARCHITECTURE check + SysNative relaunch | unit | `python -m pytest tests/test_script_generator.py::test_render_install_wow64_guard -x` | ❌ Wave 0 |
|
||||
| SCRPT-01 | GET /printers/{id}/scripts/install returns 200 PlainTextResponse with .ps1 content | integration | `python -m pytest tests/test_script_generator.py::test_install_endpoint -x` | ❌ Wave 0 |
|
||||
| SCRPT-02 | GET /printers/{id}/scripts/uninstall returns 200 | integration | `python -m pytest tests/test_script_generator.py::test_uninstall_endpoint -x` | ❌ Wave 0 |
|
||||
| SCRPT-03 | GET /printers/{id}/scripts/detect returns 200 | integration | `python -m pytest tests/test_script_generator.py::test_detect_endpoint -x` | ❌ Wave 0 |
|
||||
|
||||
### Sampling Rate
|
||||
|
||||
- **Per task commit:** `python -m pytest tests/test_script_generator.py -x -q`
|
||||
- **Per wave merge:** `python -m pytest tests/ -x -q`
|
||||
- **Phase gate:** Full suite green before `/gsd:verify-work`
|
||||
|
||||
### Wave 0 Gaps
|
||||
|
||||
- [ ] `tests/test_script_generator.py` — all SCRPT-01 through SCRPT-05 unit + integration tests
|
||||
- [ ] `imptune/templates/scripts/install.ps1.j2` — template file
|
||||
- [ ] `imptune/templates/scripts/uninstall.ps1.j2` — template file
|
||||
- [ ] `imptune/templates/scripts/detect.ps1.j2` — template file
|
||||
- [ ] `imptune/generators/script_generator.py` — render functions
|
||||
- [ ] `imptune/api/scripts.py` — FastAPI router
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
|
||||
- Microsoft Learn — `Set-PrintConfiguration` (windowsserver2025-ps, updated 2025-05-14):
|
||||
https://learn.microsoft.com/en-us/powershell/module/printmanagement/set-printconfiguration?view=windowsserver2025-ps
|
||||
— confirmed parameter names and accepted enum values for DuplexingMode, PaperSize, Color, Collate
|
||||
- Jinja2 3.1.x official docs — Environment trim_blocks/lstrip_blocks:
|
||||
https://jinja.palletsprojects.com/en/stable/templates/
|
||||
- FastAPI docs — PlainTextResponse / Custom Response:
|
||||
https://fastapi.tiangolo.com/advanced/custom-response/
|
||||
- Project codebase — `imptune/db/models.py`, `imptune/generators/intunewin_builder.py`,
|
||||
`imptune/api/printers.py`, `requirements.txt` — all read directly
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
|
||||
- msendpointmgr.com — pnputil two-step staging + Add-PrinterPort/Add-PrinterDriver/Add-Printer
|
||||
sequence: https://msendpointmgr.com/2022/01/03/install-network-printers-intune-win32apps-powershell/
|
||||
- call4cloud.nl — pnputil SysNative path, idempotency patterns, detection registry path:
|
||||
https://call4cloud.nl/deploy-printer-drivers-intune-win32app/
|
||||
- powershellisfun.com — Intune detection script contract (exit 0 + STDOUT):
|
||||
https://powershellisfun.com/2023/11/30/microsoft-intune-powershell-detection-scripts/
|
||||
- andrewstaylor.com — detection script demystified:
|
||||
https://andrewstaylor.com/2022/04/19/demystifying-intune-custom-app-detection-scripts/
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
|
||||
- WOW64 relaunch guard gist (community pattern, not official MS docs):
|
||||
https://gist.github.com/talatham/ad406d5428ccec641f075a7019cd29a8
|
||||
— Cross-verified with patchmypc.com and call4cloud.nl articles describing the same pattern.
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Standard stack: HIGH — zero new dependencies; all libraries already in requirements.txt
|
||||
- Architecture patterns: HIGH — follows existing project conventions (generators/ + api/ + templates/)
|
||||
- PowerShell cmdlet parameters: HIGH — verified against Microsoft Learn official docs
|
||||
- WOW64 guard pattern: MEDIUM — community-verified, consistent across multiple sources, not in official MS docs
|
||||
- UAC self-elevation pattern: MEDIUM — community-verified, stable pattern since PS 3.0
|
||||
- Pitfalls: HIGH — duplex mismatch verified against official docs; others verified against multiple community sources
|
||||
|
||||
**Research date:** 2026-04-10
|
||||
**Valid until:** 2026-07-10 (stable domain; PowerShell PrintManagement module rarely changes)
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
phase: 4
|
||||
slug: script-generation
|
||||
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-04)
|
||||
---
|
||||
|
||||
# Phase 4 — Validation Strategy
|
||||
|
||||
> Per-phase validation contract for feedback sampling during execution.
|
||||
|
||||
---
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Framework** | pytest >= 8.0 |
|
||||
| **Config file** | none — discovered automatically |
|
||||
| **Quick run command** | `python -m pytest tests/test_script_generator.py -x -q` |
|
||||
| **Full suite command** | `python -m pytest tests/ -x -q` |
|
||||
| **Estimated runtime** | ~10 seconds |
|
||||
|
||||
---
|
||||
|
||||
## Sampling Rate
|
||||
|
||||
- **After every task commit:** Run `python -m pytest tests/test_script_generator.py -x -q`
|
||||
- **After every plan wave:** Run `python -m pytest tests/ -x -q`
|
||||
- **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 |
|
||||
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
|
||||
| 4-01-01 | 01 | 1 | SCRPT-01 | unit | `python -m pytest tests/test_script_generator.py::test_render_install_contains_pnputil -x` | ❌ W0 | ⬜ pending |
|
||||
| 4-01-02 | 01 | 1 | SCRPT-01 | unit | `python -m pytest tests/test_script_generator.py::test_render_install_print_config -x` | ❌ W0 | ⬜ pending |
|
||||
| 4-02-01 | 02 | 1 | SCRPT-02 | unit | `python -m pytest tests/test_script_generator.py::test_render_uninstall -x` | ❌ W0 | ⬜ pending |
|
||||
| 4-02-02 | 02 | 1 | SCRPT-03 | unit | `python -m pytest tests/test_script_generator.py::test_render_detect -x` | ❌ W0 | ⬜ pending |
|
||||
| 4-01-03 | 01 | 1 | SCRPT-04 | unit | `python -m pytest tests/test_script_generator.py::test_render_install_uac_guard -x` | ❌ W0 | ⬜ pending |
|
||||
| 4-01-04 | 01 | 1 | SCRPT-05 | unit | `python -m pytest tests/test_script_generator.py::test_render_install_wow64_guard -x` | ❌ W0 | ⬜ pending |
|
||||
| 4-03-01 | 03 | 2 | SCRPT-01 | integration | `python -m pytest tests/test_script_generator.py::test_install_endpoint -x` | ❌ W0 | ⬜ pending |
|
||||
| 4-03-02 | 03 | 2 | SCRPT-02 | integration | `python -m pytest tests/test_script_generator.py::test_uninstall_endpoint -x` | ❌ W0 | ⬜ pending |
|
||||
| 4-03-03 | 03 | 2 | SCRPT-03 | integration | `python -m pytest tests/test_script_generator.py::test_detect_endpoint -x` | ❌ W0 | ⬜ pending |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
- [ ] `tests/test_script_generator.py` — stubs for SCRPT-01 through SCRPT-05 (unit + integration)
|
||||
- [ ] `imptune/templates/scripts/install.ps1.j2` — Jinja2 template file
|
||||
- [ ] `imptune/templates/scripts/uninstall.ps1.j2` — Jinja2 template file
|
||||
- [ ] `imptune/templates/scripts/detect.ps1.j2` — Jinja2 template file
|
||||
- [ ] `imptune/generators/script_generator.py` — render functions
|
||||
- [ ] `imptune/api/scripts.py` — FastAPI router
|
||||
|
||||
---
|
||||
|
||||
## Manual-Only Verifications
|
||||
|
||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||
|----------|-------------|------------|-------------------|
|
||||
| Install script runs on real Windows endpoint via Intune | SCRPT-01 | Requires real Intune + endpoint | Deploy .intunewin package to test device, verify printer appears |
|
||||
| UAC elevation prompt appears for standard user | SCRPT-04 | Requires interactive desktop session | Run install.ps1 as standard user, verify UAC dialog |
|
||||
| WOW64 relaunch works in 32-bit PS | SCRPT-05 | Requires 32-bit PowerShell host | Launch powershell.exe (x86), run install.ps1, verify relaunch |
|
||||
|
||||
---
|
||||
|
||||
## Nyquist Record
|
||||
|
||||
> Audited 2026-04-13 by Claude (gsd-executor, plan 08-04). One row per Phase 4 success criterion derived from `milestones/v1.0-ROADMAP.md` Phase 4 goal + plan outcomes (SCRPT-01..05), cross-checked against `04-VERIFICATION.md` (12/12 observable truths verified 2026-04-10) and `REQUIREMENTS.md` v1.0 SCRPT-0x block. Evidence cites committed tests, source lines, the dated VERIFICATION report, and — for rows whose proof requires real-device SYSTEM-context execution — the Phase 10 `RUNTIME-VALIDATION.md` report with explicit attestation-only caveats per STATE.md 2026-04-13.
|
||||
>
|
||||
> **Phase 4 goal (v1.0-ROADMAP.md):** *"System produces correct, production-ready PowerShell scripts handling all Intune and RMM execution contexts."*
|
||||
>
|
||||
> **Attestation-only caveat (STATE.md 2026-04-13):** Phase 10 RTVAL-02 (install on real endpoint), RTVAL-03 (detection script on real endpoint), and RTVAL-04 (uninstall on real endpoint) were accepted as **attestation-only PASSes** — the technician verbally confirmed success but did not produce IntuneManagementExtension.log excerpts, portal screenshots, or status captures. The user was warned twice about cumulative audit-trail damage and explicitly approved proceeding. Plan 10-03 closed the phase with this gap acknowledged in writing. Rows below that depend on SYSTEM-context runtime proof therefore record `pass` (Phase 10 signed off) but the Notes column states the weakened audit trail faithfully — this audit does not hide it.
|
||||
|
||||
| # | Success Criterion | Observable Check | Evidence | Status | Notes |
|
||||
|---|-------------------|------------------|----------|--------|-------|
|
||||
| 1 | **SCRPT-01** — Generate PowerShell install script (pnputil staging + Add-PrinterPort + Add-PrinterDriver + Add-Printer + Set-PrintConfiguration) | `pytest tests/test_script_generator.py::test_render_install_contains_pnputil` + `::test_render_install_print_config` + `::test_install_endpoint` — unit tests assert all 5 cmdlets appear in rendered template; integration test asserts `GET /printers/{id}/scripts/install` returns 200 PowerShell content with pnputil present | `tests/test_script_generator.py::test_render_install_contains_pnputil`, `::test_render_install_print_config`, `::test_install_endpoint`; `imptune/templates/scripts/install.ps1.j2` lines 40-67 (pnputil `/add-driver` + Add-PrinterPort + Add-PrinterDriver + Add-Printer + Set-PrintConfiguration); `imptune/generators/script_generator.py` `_duplex_map` + `render_install` (commits b4f2c64 RED, 8193e9d GREEN); `imptune/api/scripts.py` lines 38-59; 04-VERIFICATION.md truths 1 + 4 + 5 + 8; Phase 10 `RUNTIME-VALIDATION.md` RTVAL-02 (install succeeded on ARES-5CG5220YTM) | pass | **SYSTEM-context runtime proof is attestation-only per STATE.md 2026-04-13.** pnputil staging + $PSScriptRoot resolution under the real Intune SYSTEM context were confirmed verbally by the technician for RTVAL-02 but no IntuneManagementExtension.log excerpt or portal screenshot was captured. Phase 10 signed off the gap; rollout Phase 11 owns re-capture of full artifacts. Template-level correctness (cmdlet presence, positional ordering, duplex mapping) is fully automated via pytest. |
|
||||
| 2 | **SCRPT-02** — Generate PowerShell uninstall script (Remove-Printer + Remove-PrinterDriver + Remove-PrinterPort in correct order) | `pytest tests/test_script_generator.py::test_render_uninstall` + `::test_uninstall_endpoint` — asserts all 3 Remove-* cmdlets appear in correct order (Printer → Driver → Port) with `-ErrorAction SilentlyContinue` on each; integration test asserts endpoint returns 200 | `tests/test_script_generator.py::test_render_uninstall`, `::test_uninstall_endpoint`; `imptune/templates/scripts/uninstall.ps1.j2` lines 2-4; `imptune/generators/script_generator.py::render_uninstall` line 70 (commit 6bff8f3); `imptune/api/scripts.py` lines 63-78 (commit b7b0d1b); 04-VERIFICATION.md truth 6 + truth 9; Phase 10 `RUNTIME-VALIDATION.md` RTVAL-04 (uninstall succeeded on real endpoint) | pass | **SYSTEM-context runtime proof is attestation-only per STATE.md 2026-04-13.** RTVAL-04 is the **third consecutive attestation-only** Phase 10 check — no `rtval-04-uninstall-log.txt` and no `rtval-04-uninstall-status.png` were captured. Template-level ordering and `-ErrorAction SilentlyContinue` safety are fully automated via pytest; real-device Remove-Printer behavior under SYSTEM rests on verbal technician confirmation only. |
|
||||
| 3 | **SCRPT-03** — Generate Intune detection script (exit 0 when printer present, exit 1 when absent, with Write-Output on success) | `pytest tests/test_script_generator.py::test_render_detect` + `::test_detect_endpoint` — asserts `Get-Printer` check + `Write-Output` + `exit 0` on found branch + `exit 1` on absent branch; integration test asserts endpoint returns 200 | `tests/test_script_generator.py::test_render_detect`, `::test_detect_endpoint`; `imptune/templates/scripts/detect.ps1.j2` lines 2-8; `imptune/generators/script_generator.py::render_detect` line 92 (commit 6bff8f3); `imptune/api/scripts.py` lines 82-94; 04-VERIFICATION.md truth 7 + truth 10; `.planning/phases/04-script-generation/04-RESEARCH.md` State-of-the-Art table (Get-Printer cmdlet chosen over HKLM registry path as more reliable); Phase 10 `RUNTIME-VALIDATION.md` RTVAL-03 (Intune detection script evaluated as installed) | pass | **Documented deviation from REQUIREMENTS.md wording.** REQUIREMENTS.md says "registry check" but 04-RESEARCH.md supersedes with `Get-Printer` cmdlet — explicitly documented as more reliable before implementation. The functional Intune contract (Write-Output + exit 0 when present, exit 1 when absent) is correctly satisfied. **SYSTEM-context runtime proof is attestation-only per STATE.md 2026-04-13** — RTVAL-03 is the second consecutive attestation-only Phase 10 check; no `rtval-03-detection.png` or `rtval-03-detect-manual.txt` was captured. Real Intune evaluator behavior confirmed verbally only. |
|
||||
| 4 | **SCRPT-04** — Install script detects SYSTEM vs user context and self-elevates via UAC when run by user | `pytest tests/test_script_generator.py::test_render_install_uac_guard` — asserts `WindowsIdentity::GetCurrent()`, `IsSystem` check, `IsInRole(Administrator)` check, and `Start-Process -Verb Runas` all present in rendered install template | `tests/test_script_generator.py::test_render_install_uac_guard`; `imptune/templates/scripts/install.ps1.j2` lines 22-33 (SYSTEM identity check + admin role check + self-elevation branch); 04-VERIFICATION.md truth 3; Phase 10 `RUNTIME-VALIDATION.md` RTVAL-02 (install succeeded under Intune SYSTEM context on ARES-5CG5220YTM — UAC guard correctly skipped elevation) | pass | **SYSTEM-context runtime proof is attestation-only per STATE.md 2026-04-13.** The `IsSystem` branch (skip elevation when run by Intune Management Extension as SYSTEM) was exercised in the attestation-only RTVAL-02 run. The user-interactive self-elevation branch (Start-Process -Verb Runas triggering a real UAC dialog for a standard user) is flagged as a `Manual-Only Verification` above and **was not exercised in Phase 10** (RTVAL only covered the Intune SYSTEM path, not standalone standard-user execution). Template-level correctness (both branches present, identity check first) is automated via pytest. |
|
||||
| 5 | **SCRPT-05** — Install script includes 64-bit WOW64 relaunch guard for Intune's 32-bit execution context | `pytest tests/test_script_generator.py::test_render_install_wow64_guard` — positional assertion: `PROCESSOR_ARCHITECTURE` + `PROCESSOR_ARCHITEW6432` + `SysNative` relaunch block appears **before** the pnputil block in rendered install template (guard must be first executable block) | `tests/test_script_generator.py::test_render_install_wow64_guard`; `imptune/templates/scripts/install.ps1.j2` lines 12-16 (WOW64 guard) preceding lines 40+ (pnputil); 04-VERIFICATION.md truth 2; Phase 10 `RUNTIME-VALIDATION.md` RTVAL-02 (install succeeded end-to-end under Intune on 64-bit Windows) | pass | **SYSTEM-context runtime proof is attestation-only per STATE.md 2026-04-13.** The WOW64 relaunch path (Intune's 32-bit PS host → SysNative 64-bit relaunch → continue execution) is **not directly observable** from RTVAL-02's attestation-only confirmation — the technician only attested the printer installed, not that the WOW64 branch was taken. This check remains a `Manual-Only Verification` pending a real 32-bit PowerShell host trace. Template-level positional correctness (guard before pnputil) is fully automated via pytest. Row recorded as `pass` because Phase 10 signed off end-to-end install; full WOW64 trace is a Phase 11 rollout concern. |
|
||||
|
||||
**Audit outcome:** 5/5 rows `pass`. No `fail-fix-v1.1`, `deferred-v1.2`, or `wont-do` rows. Phase 4 is Nyquist-compliant *at the template level* — every SCRPT-0x success criterion has exactly one observable check with cited, committed evidence. **However**, SYSTEM-context runtime behavior (pnputil staging under SYSTEM, `$PSScriptRoot` resolution under SYSTEM, detect/uninstall under SYSTEM, WOW64 relaunch in real 32-bit Intune host) rests on attestation-only Phase 10 PASSes per STATE.md 2026-04-13. This audit records the weakened runtime audit trail faithfully in the Notes column rather than flipping rows to `fail-fix-v1.1` — Phase 10 signed off with explicit written acknowledgement of the attestation gap, and Phase 11 (Real-World Rollout) owns artifact re-capture before broad rollout. Zero gaps carry forward into 08-08 (rollup) 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-04) — 5/5 pass (runtime rows attestation-only per STATE.md 2026-04-13, acknowledged in Phase 10 plan 10-03 sign-off); signed off 2026-04-13 by Sébastien QUEROL (index: v1.0-VALIDATION-INDEX.md)
|
||||
@@ -0,0 +1,159 @@
|
||||
---
|
||||
phase: 04-script-generation
|
||||
verified: 2026-04-10T12:00:00Z
|
||||
status: passed
|
||||
score: 12/12 must-haves verified
|
||||
re_verification: false
|
||||
---
|
||||
|
||||
# Phase 4: Script Generation Verification Report
|
||||
|
||||
**Phase Goal:** The system produces correct, production-ready PowerShell scripts that handle all Intune and RMM execution contexts
|
||||
**Verified:** 2026-04-10
|
||||
**Status:** passed
|
||||
**Re-verification:** No — initial verification
|
||||
|
||||
---
|
||||
|
||||
## Goal Achievement
|
||||
|
||||
### Observable Truths
|
||||
|
||||
| # | Truth | Status | Evidence |
|
||||
|---|-------|--------|----------|
|
||||
| 1 | render_install() produces a complete PowerShell script containing pnputil /add-driver, Add-PrinterPort, Add-PrinterDriver, Add-Printer, Set-PrintConfiguration | VERIFIED | install.ps1.j2 lines 40-67; test_render_install_contains_pnputil + test_render_install_print_config both pass |
|
||||
| 2 | Generated install script contains WOW64 relaunch guard as the first executable block | VERIFIED | install.ps1.j2 lines 12-16; PROCESSOR_ARCHITECTURE check at line 12 precedes pnputil at line 40; test_render_install_wow64_guard passes with positional assertion |
|
||||
| 3 | Generated install script contains SYSTEM vs user detection with UAC self-elevation | VERIFIED | install.ps1.j2 lines 22-33; IsSystem, IsInRole(Administrator), Start-Process -Verb Runas present; test_render_install_uac_guard passes |
|
||||
| 4 | Set-PrintConfiguration receives translated duplex values (TwoSidedLongEdge, TwoSidedShortEdge) | VERIFIED | _duplex_map in script_generator.py lines 22-26; OneSided/LongEdge/ShortEdge all three variants tested; test_render_install_print_config passes |
|
||||
| 5 | All add operations are wrapped in idempotency checks (Get-PrinterPort, Get-Printer) | VERIFIED | install.ps1.j2 lines 47-57; Get-PrinterPort check before Add-PrinterPort, Get-Printer check before Add-Printer; test_render_install_idempotency passes with positional assertions |
|
||||
| 6 | render_uninstall() produces script with Remove-Printer, Remove-PrinterDriver, Remove-PrinterPort in correct order | VERIFIED | uninstall.ps1.j2 lines 2-4; Remove-Printer before Remove-PrinterDriver before Remove-PrinterPort, all with -ErrorAction SilentlyContinue; test_render_uninstall passes |
|
||||
| 7 | render_detect() produces script that exits 0 with Write-Output when printer found, exits 1 when absent | VERIFIED | detect.ps1.j2 lines 2-8; Get-Printer check, Write-Output + exit 0 on found, exit 1 on absent; test_render_detect passes |
|
||||
| 8 | GET /printers/{id}/scripts/install returns 200 with PowerShell content and attachment header | VERIFIED | scripts.py lines 38-59; PlainTextResponse with Content-Disposition attachment; test_install_endpoint passes |
|
||||
| 9 | GET /printers/{id}/scripts/uninstall returns 200 with PowerShell content | VERIFIED | scripts.py lines 63-78; test_uninstall_endpoint passes |
|
||||
| 10 | GET /printers/{id}/scripts/detect returns 200 with PowerShell content | VERIFIED | scripts.py lines 82-94; test_detect_endpoint passes |
|
||||
| 11 | GET /printers/{id}/scripts/{type} returns 404 for nonexistent printer | VERIFIED | scripts.py _get_printer_and_driver() line 17; test_script_endpoint_missing_printer passes |
|
||||
| 12 | GET /printers/{id}/scripts/{type} returns 422 when driver or inf_filename is missing | VERIFIED | scripts.py _get_printer_and_driver() lines 21-33; test_script_endpoint_no_driver passes |
|
||||
|
||||
**Score:** 12/12 truths verified
|
||||
|
||||
---
|
||||
|
||||
## Required Artifacts
|
||||
|
||||
### Plan 04-01
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `imptune/generators/script_generator.py` | Jinja2 Environment + render_install with duplex_map | VERIFIED | 106 lines; _env, _duplex_map, render_install all present; exports render_uninstall and render_detect too |
|
||||
| `imptune/templates/scripts/install.ps1.j2` | PowerShell install template with WOW64, UAC, pnputil, idempotency | VERIFIED | 68 lines; all required blocks present in correct order |
|
||||
| `tests/test_script_generator.py` | Unit tests for SCRPT-01, SCRPT-04, SCRPT-05 | VERIFIED | 198 lines; 14 tests (7 unit + 5 integration + 2 unit for uninstall/detect) |
|
||||
|
||||
### Plan 04-02
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `imptune/templates/scripts/uninstall.ps1.j2` | PowerShell uninstall template containing Remove-Printer | VERIFIED | 4 lines; Remove-Printer present |
|
||||
| `imptune/templates/scripts/detect.ps1.j2` | PowerShell detection template containing Write-Output | VERIFIED | 8 lines; Write-Output present |
|
||||
| `imptune/api/scripts.py` | Script download endpoints exporting router | VERIFIED | 95 lines; router exported, 3 endpoints + shared validation helper |
|
||||
| `imptune/generators/script_generator.py` | render_uninstall and render_detect added | VERIFIED | render_uninstall (line 70) and render_detect (line 92) present |
|
||||
|
||||
---
|
||||
|
||||
## Key Link Verification
|
||||
|
||||
### Plan 04-01
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
|------|----|-----|--------|---------|
|
||||
| `imptune/generators/script_generator.py` | `imptune/templates/scripts/install.ps1.j2` | Jinja2 FileSystemLoader | WIRED | `_env.get_template("install.ps1.j2")` at line 56; FileSystemLoader points to templates/scripts/ |
|
||||
| `imptune/generators/script_generator.py` | `imptune/db/models.py` | Printer model fields as template vars | WIRED | render_install takes printer_name, ip_address, port_name as plain string args mirroring model fields; scripts.py passes printer.name, printer.ip_address, printer.port_name |
|
||||
|
||||
### Plan 04-02
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
|------|----|-----|--------|---------|
|
||||
| `imptune/api/scripts.py` | `imptune/generators/script_generator.py` | import render_install, render_uninstall, render_detect | WIRED | Line 8: `from imptune.generators.script_generator import render_detect, render_install, render_uninstall` |
|
||||
| `imptune/api/scripts.py` | `imptune/db/models.py` | Printer.get_or_none query with Driver join | WIRED | `Printer.get_or_none(Printer.id == printer_id)` at line 15; `printer.driver` access at line 19 |
|
||||
| `imptune/main.py` | `imptune/api/scripts.py` | app.include_router(scripts.router) | WIRED | Line 8: scripts in import; line 36: `app.include_router(scripts.router)` |
|
||||
|
||||
---
|
||||
|
||||
## Requirements Coverage
|
||||
|
||||
| Requirement | Source Plan | Description | Status | Evidence |
|
||||
|-------------|------------|-------------|--------|----------|
|
||||
| SCRPT-01 | 04-01 | Generate PowerShell install script (pnputil staging + Add-PrinterPort + Add-PrinterDriver + Add-Printer + Set-PrintConfiguration) | SATISFIED | install.ps1.j2 contains all 5 cmdlets; 4 unit tests cover pnputil, idempotency, duplex, booleans; integration test confirms endpoint returns 200 with pnputil in content |
|
||||
| SCRPT-02 | 04-02 | Generate PowerShell uninstall script (Remove-Printer + Remove-PrinterDriver + Remove-PrinterPort) | SATISFIED | uninstall.ps1.j2 contains all 3 Remove-* cmdlets in safe order; test_render_uninstall asserts ordering and -ErrorAction SilentlyContinue on all three |
|
||||
| SCRPT-03 | 04-02 | Generate Intune detection script (registry check for printer name) | SATISFIED — with documented deviation | REQUIREMENTS.md says "registry check" but implementation uses Get-Printer cmdlet. 04-RESEARCH.md State of the Art table explicitly documents this decision: "HKLM registry check → Get-Printer cmdlet check — Both work; Get-Printer is more reliable". Intune detection contract (Write-Output + exit 0/1) is correctly implemented. |
|
||||
| SCRPT-04 | 04-01 | Install script detects SYSTEM vs user context and self-elevates via UAC when run by user | SATISFIED | install.ps1.j2 lines 22-33; WindowsIdentity::GetCurrent(), IsSystem, IsInRole(Administrator), Start-Process -Verb Runas; UAC guard skips elevation when running as SYSTEM |
|
||||
| SCRPT-05 | 04-01 | Install script includes 64-bit WOW64 relaunch guard for Intune's 32-bit execution context | SATISFIED | install.ps1.j2 lines 12-16; PROCESSOR_ARCHITECTURE + PROCESSOR_ARCHITEW6432 check + SysNative relaunch; positional test confirms guard appears before pnputil |
|
||||
|
||||
**Note on SCRPT-03:** The requirement description says "registry check" but the research document (04-RESEARCH.md) explicitly supersedes this with Get-Printer cmdlet approach, noting it is more reliable than the HKLM registry path approach. This is a planned deviation documented before implementation. The functional contract (Intune detection: Write-Output + exit 0 when present, exit 1 when absent) is correctly satisfied.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns Found
|
||||
|
||||
No anti-patterns found in any phase 04 files.
|
||||
|
||||
Scanned: `imptune/generators/script_generator.py`, `imptune/api/scripts.py`, `imptune/templates/scripts/install.ps1.j2`, `imptune/templates/scripts/uninstall.ps1.j2`, `imptune/templates/scripts/detect.ps1.j2`
|
||||
|
||||
No TODO/FIXME/PLACEHOLDER comments, no empty implementations, no stub returns, no console.log equivalents.
|
||||
|
||||
---
|
||||
|
||||
## Commit Verification
|
||||
|
||||
| Commit | Description | Status |
|
||||
|--------|-------------|--------|
|
||||
| b4f2c64 | test(04-01): RED phase — 7 failing tests | FOUND in git log |
|
||||
| 8193e9d | feat(04-01): script_generator.py + install.ps1.j2 | FOUND in git log |
|
||||
| 0f213df | test(04-02): failing tests for render_uninstall/detect | FOUND in git log |
|
||||
| 6bff8f3 | feat(04-02): render_uninstall + render_detect + templates | FOUND in git log |
|
||||
| b7b0d1b | feat(04-02): script API endpoints + router registration | FOUND in git log |
|
||||
|
||||
---
|
||||
|
||||
## Test Suite Results
|
||||
|
||||
```
|
||||
tests/test_script_generator.py — 14/14 passed
|
||||
Full suite — 75/75 passed (no regressions)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Human Verification Required
|
||||
|
||||
### 1. WOW64 Relaunch — Live 32-bit Context
|
||||
|
||||
**Test:** Launch `powershell.exe (x86)` on a Windows endpoint and run the generated install.ps1
|
||||
**Expected:** Script detects 32-bit process, relaunches under SysNative 64-bit PowerShell, driver staging succeeds
|
||||
**Why human:** Requires a physical 32-bit PowerShell host; cannot emulate WOW64 in unit tests
|
||||
|
||||
### 2. UAC Elevation Prompt — Standard User
|
||||
|
||||
**Test:** Run install.ps1 as a non-admin standard user on a real Windows desktop
|
||||
**Expected:** UAC elevation dialog appears; after approval, printer installs successfully
|
||||
**Why human:** Requires interactive desktop session with a standard user account
|
||||
|
||||
### 3. Intune Detection Contract — Real Intune Enrollment
|
||||
|
||||
**Test:** Deploy a printer as an Intune Win32 app using the detect.ps1 as the detection script
|
||||
**Expected:** Intune marks the app as "Installed" after seeing Write-Output + exit 0
|
||||
**Why human:** Requires Intune tenant, enrolled device, and deployed Win32 app — not automatable
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 4 goal is fully achieved. All 12 observable truths are verified against actual code, not just SUMMARY claims. The implementation is substantive: templates are real PowerShell (not stubs), render functions use actual Jinja2 template rendering with duplex translation and boolean conversion, and all three API endpoints have complete ORM validation with proper 404/422 error paths.
|
||||
|
||||
The three human verification items are real-world deployment concerns that cannot be automated (WOW64 live context, interactive UAC, Intune tenant). These are flagged in the validation strategy document and are expected at this phase.
|
||||
|
||||
The SCRPT-03 "registry check" wording in REQUIREMENTS.md is a minor description inaccuracy — the implementation correctly uses Get-Printer per the research document's recommendation, which explicitly documents this as the preferred approach over the registry path. The functional Intune contract is satisfied.
|
||||
|
||||
---
|
||||
|
||||
_Verified: 2026-04-10_
|
||||
_Verifier: Claude (gsd-verifier)_
|
||||
Reference in New Issue
Block a user