docs(02-driver-management): create phase plan
Two plans: INF parser TDD (wave 1), upload endpoint + UI (wave 2). Covers DRV-01 through DRV-05. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,12 +45,11 @@ Plans:
|
||||
2. After upload, user sees a dropdown of driver names extracted from the INF (DriverDesc values), not a text field
|
||||
3. Uploaded driver packages survive container restarts (persisted to Docker volume)
|
||||
4. System flags files in the driver package that are not referenced by the INF, with a count or list
|
||||
**Plans**: TBD
|
||||
**Plans**: 2 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 02-01: Driver upload endpoint and volume storage (SHA256-keyed, deduplication)
|
||||
- [ ] 02-02: INF parser (DriverDesc extraction, multi-model INF support)
|
||||
- [ ] 02-03: Driver library UI (upload form, driver list, unused-file hints)
|
||||
- [ ] 02-01-PLAN.md — INF parser with TDD (encoding detection, token resolution, multi-model, unused files)
|
||||
- [ ] 02-02-PLAN.md — Driver upload endpoint, persistence, drivers page UI with HTMX
|
||||
|
||||
### Phase 3: Printer Configuration
|
||||
**Goal**: Technicians can configure all printer parameters, assign printers to clients, and retrieve saved configs without re-uploading drivers
|
||||
@@ -109,7 +108,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5
|
||||
| Phase | Plans Complete | Status | Completed |
|
||||
|-------|----------------|--------|-----------|
|
||||
| 1. Foundation | 3/3 | Complete | 2026-04-10 |
|
||||
| 2. Driver Management | 0/3 | Not started | - |
|
||||
| 2. Driver Management | 0/2 | Not started | - |
|
||||
| 3. Printer Configuration | 0/3 | Not started | - |
|
||||
| 4. Script Generation | 0/3 | Not started | - |
|
||||
| 5. Package Export | 0/3 | Not started | - |
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
---
|
||||
phase: 02-driver-management
|
||||
plan: "01"
|
||||
type: tdd
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- imptune/services/__init__.py
|
||||
- imptune/services/inf_parser.py
|
||||
- tests/test_inf_parser.py
|
||||
- tests/fixtures/sample.inf
|
||||
- tests/fixtures/sample_utf16.inf
|
||||
- tests/fixtures/sample_multi_model.inf
|
||||
autonomous: true
|
||||
requirements: [DRV-02, DRV-05]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "parse_inf extracts DriverDesc values from a simple INF with literal names"
|
||||
- "parse_inf resolves %TOKEN% references via the [Strings] section"
|
||||
- "parse_inf handles UTF-16 LE BOM, UTF-8 BOM, and ANSI (cp1252) encoded INF files"
|
||||
- "parse_inf deduplicates driver names from multi-model INFs (NTamd64 + undecorated)"
|
||||
- "parse_inf returns a list of unused files not referenced in the INF text"
|
||||
- "parse_inf detects architecture from section decorations (x64, x86, arm64)"
|
||||
- "parse_inf detects presence of .cat file in ZIP member list"
|
||||
artifacts:
|
||||
- path: "imptune/services/inf_parser.py"
|
||||
provides: "ParsedInf dataclass and parse_inf() + _detect_encoding() functions"
|
||||
exports: ["ParsedInf", "parse_inf", "_detect_encoding"]
|
||||
- path: "tests/test_inf_parser.py"
|
||||
provides: "Unit tests covering all DRV-02 and DRV-05 behaviors"
|
||||
min_lines: 80
|
||||
- path: "tests/fixtures/sample.inf"
|
||||
provides: "Minimal valid INF with %TOKEN% values and [Strings] section"
|
||||
- path: "tests/fixtures/sample_utf16.inf"
|
||||
provides: "UTF-16 LE encoded INF for encoding detection test"
|
||||
- path: "tests/fixtures/sample_multi_model.inf"
|
||||
provides: "INF with NTamd64 and undecorated Models sections"
|
||||
key_links:
|
||||
- from: "imptune/services/inf_parser.py"
|
||||
to: "configparser.RawConfigParser"
|
||||
via: "stdlib import"
|
||||
pattern: "RawConfigParser.*strict=False"
|
||||
- from: "imptune/services/inf_parser.py"
|
||||
to: "[Strings] section"
|
||||
via: "_resolve_tokens regex expansion"
|
||||
pattern: "re\\.sub.*%([^%]+)%"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the INF parser service that extracts driver names (DriverDesc) from Windows INF files, with encoding auto-detection, %TOKEN% resolution, multi-model support, and unused-file detection.
|
||||
|
||||
Purpose: This is the core novel logic of Phase 2. The INF parser is a pure function with defined I/O — ideal for TDD. All other Phase 2 work (upload endpoint, UI) consumes this parser's output.
|
||||
Output: `imptune/services/inf_parser.py` with `ParsedInf` dataclass and `parse_inf()` function, plus comprehensive unit tests and INF fixture files.
|
||||
</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/02-driver-management/02-RESEARCH.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Existing codebase interfaces this plan needs -->
|
||||
|
||||
From imptune/config.py:
|
||||
```python
|
||||
DATA_DIR = os.environ.get("DATA_DIR", "/data")
|
||||
DRIVERS_DIR = str(Path(DATA_DIR) / "drivers")
|
||||
```
|
||||
|
||||
From imptune/storage/driver_store.py:
|
||||
```python
|
||||
class DriverStore:
|
||||
def save(self, data: bytes) -> str: ... # returns SHA256 hex digest
|
||||
def get_path(self, sha256: str) -> Path: ...
|
||||
def exists(self, sha256: str) -> bool: ...
|
||||
```
|
||||
|
||||
From imptune/db/models.py:
|
||||
```python
|
||||
class Driver(BaseModel):
|
||||
sha256 = CharField(unique=True, index=True)
|
||||
original_filename = CharField()
|
||||
size_bytes = IntegerField()
|
||||
uploaded_at = DateTimeField(default=datetime.utcnow)
|
||||
driver_desc = CharField(null=True) # Store json.dumps(list) for multi-model
|
||||
inf_filename = CharField(null=True)
|
||||
architecture = CharField(null=True) # 'x64', 'x86', 'arm64', or None
|
||||
has_cat_file = BooleanField(default=False)
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: INF parser with TDD (RED then GREEN)</name>
|
||||
<files>imptune/services/__init__.py, imptune/services/inf_parser.py, tests/test_inf_parser.py, tests/fixtures/sample.inf, tests/fixtures/sample_utf16.inf, tests/fixtures/sample_multi_model.inf</files>
|
||||
<behavior>
|
||||
- test_detect_encoding_utf16le: _detect_encoding(b'\xff\xfe...') returns 'utf-16'
|
||||
- test_detect_encoding_utf8bom: _detect_encoding(b'\xef\xbb\xbf...') returns 'utf-8-sig'
|
||||
- test_detect_encoding_ansi: _detect_encoding(b'[Version]...') returns 'cp1252'
|
||||
- test_simple_driver_desc: parse_inf with literal DriverDesc in [Models] section returns those names in driver_names
|
||||
- test_token_resolution: parse_inf with %HP_DRIVER% in [Models] and HP_DRIVER="HP LaserJet" in [Strings] resolves to "HP LaserJet"
|
||||
- test_utf16_encoding: Reading a UTF-16 LE BOM fixture, decoding with _detect_encoding, and passing to parse_inf produces correct driver_names
|
||||
- test_multi_model_inf: INF with both [Mfg.NTamd64] and [Mfg] sections returns deduplicated driver_names; architecture='x64' when NTamd64 is present alone
|
||||
- test_architecture_detection: NTamd64 -> 'x64', NTarm64 -> 'arm64', undecorated only -> 'x86', mixed -> None
|
||||
- test_cat_file_detection: zip_names containing 'driver.cat' -> has_cat_file=True; without -> False
|
||||
- test_unused_files: ZIP members ['driver.inf', 'driver.dll', 'readme.txt'] where INF text mentions 'driver.inf' and 'driver.dll' but not 'readme.txt' -> unused_files=['readme.txt']
|
||||
- test_empty_models_section: INF with [Manufacturer] but empty Models section returns empty driver_names list
|
||||
</behavior>
|
||||
<action>
|
||||
**Phase: RED**
|
||||
|
||||
1. Create `tests/fixtures/` directory if it does not exist.
|
||||
|
||||
2. Create `tests/fixtures/sample.inf` — minimal valid INF with %TOKEN% values:
|
||||
```ini
|
||||
[Version]
|
||||
Signature="$Windows NT$"
|
||||
Class=Printer
|
||||
Provider=%MFG%
|
||||
|
||||
[Manufacturer]
|
||||
%MFG%=Models,NTamd64
|
||||
|
||||
[Models.NTamd64]
|
||||
%DRIVER_NAME%=Install,{GUID}
|
||||
|
||||
[Strings]
|
||||
MFG="Test Manufacturer"
|
||||
DRIVER_NAME="Test LaserJet Pro"
|
||||
```
|
||||
|
||||
3. Create `tests/fixtures/sample_utf16.inf` — same content as sample.inf but encoded as UTF-16 LE with BOM. Write using Python: `content.encode('utf-16-le')` prepended with `b'\xff\xfe'`. Actually, create this fixture programmatically within the test (or as a conftest fixture) since writing binary fixtures from plan text is fragile.
|
||||
|
||||
4. Create `tests/fixtures/sample_multi_model.inf` — INF with both decorated and undecorated sections:
|
||||
```ini
|
||||
[Version]
|
||||
Signature="$Windows NT$"
|
||||
Class=Printer
|
||||
|
||||
[Manufacturer]
|
||||
%MFG%=Models,Models.NTamd64
|
||||
|
||||
[Models]
|
||||
%DRIVER_A%=InstallA,{GUID1}
|
||||
|
||||
[Models.NTamd64]
|
||||
%DRIVER_A%=InstallA,{GUID1}
|
||||
%DRIVER_B%=InstallB,{GUID2}
|
||||
|
||||
[Strings]
|
||||
MFG="Multi Corp"
|
||||
DRIVER_A="Multi Printer 1000"
|
||||
DRIVER_B="Multi Printer 2000"
|
||||
```
|
||||
|
||||
5. Create `imptune/services/__init__.py` — empty package marker.
|
||||
|
||||
6. Create `tests/test_inf_parser.py` with all 11 test functions listed in behavior. Tests import from `imptune.services.inf_parser` and call `parse_inf()` / `_detect_encoding()`. Each test asserts specific expected outputs. For the UTF-16 test, generate the fixture bytes inline: `sample_text.encode('utf-16')`.
|
||||
|
||||
7. Run `pytest tests/test_inf_parser.py -x` — all tests MUST FAIL (ImportError or assertion errors). Commit: `test(02-01): add failing tests for INF parser`
|
||||
|
||||
**Phase: GREEN**
|
||||
|
||||
8. Create `imptune/services/inf_parser.py` implementing:
|
||||
- `ParsedInf` dataclass with fields: `driver_names: list[str]`, `inf_filename: str`, `architecture: str | None`, `has_cat_file: bool`, `unused_files: list[str]`
|
||||
- `_detect_encoding(raw: bytes) -> str` — BOM sniffing (UTF-16 BOM -> 'utf-16', UTF-8 BOM -> 'utf-8-sig', else -> 'cp1252')
|
||||
- `_resolve_tokens(value: str, strings: dict[str, str]) -> str` — regex `%TOKEN%` expansion from strings dict
|
||||
- `parse_inf(inf_text: str, inf_filename: str, zip_names: list[str]) -> ParsedInf` — uses `configparser.RawConfigParser(strict=False, comment_prefixes=(';', '#'), delimiters=('=',))`, reads [Manufacturer] to find Models section names, iterates all matching sections (decorated: .NTamd64, .NTarm64, .NTx86; undecorated), extracts left-hand keys as device-descriptions, resolves tokens, deduplicates with set(), detects architecture from section suffix, detects .cat in zip_names, computes unused files by checking if each zip member's basename appears in inf_text (case-insensitive)
|
||||
|
||||
Follow the exact code patterns from 02-RESEARCH.md "Pattern 2: INF DriverDesc Extraction". Key points:
|
||||
- Use `RawConfigParser` (NOT `ConfigParser` — avoids %(interpolation)s interference)
|
||||
- `strict=False` to handle duplicate keys in real INFs
|
||||
- Strings dict keys must be lowercased (configparser lowercases keys by default)
|
||||
- Strip surrounding double-quotes from [Strings] values
|
||||
- Architecture: if exactly one arch hint in set -> return it; multiple -> None
|
||||
- `sorted(driver_names)` for deterministic dropdown order
|
||||
|
||||
9. Run `pytest tests/test_inf_parser.py -x` — all tests MUST PASS. Commit: `feat(02-01): implement INF parser with encoding detection and token resolution`
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_inf_parser.py -v</automated>
|
||||
</verify>
|
||||
<done>All 11 tests pass. ParsedInf dataclass and parse_inf() function correctly extract driver names from simple, tokenized, UTF-16, and multi-model INF files. Unused files detected. Architecture and .cat presence detected.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
```bash
|
||||
pytest tests/test_inf_parser.py -v
|
||||
pytest tests/ -x -q # no regressions in existing tests
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- parse_inf() extracts DriverDesc from all 3 fixture types (simple, UTF-16, multi-model)
|
||||
- %TOKEN% references resolved to human-readable names
|
||||
- Unused files correctly identified
|
||||
- All 11+ unit tests green, zero regressions in existing suite
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-driver-management/02-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,311 @@
|
||||
---
|
||||
phase: 02-driver-management
|
||||
plan: "02"
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["02-01"]
|
||||
files_modified:
|
||||
- imptune/api/drivers.py
|
||||
- imptune/api/pages.py
|
||||
- imptune/main.py
|
||||
- imptune/templates/drivers.html
|
||||
- imptune/templates/partials/driver_list.html
|
||||
- tests/test_driver_upload.py
|
||||
autonomous: true
|
||||
requirements: [DRV-01, DRV-03, DRV-04, DRV-05]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "User can upload a ZIP file via the /drivers page and receive a success response"
|
||||
- "After upload, the response contains a populated select dropdown with driver names from the INF"
|
||||
- "Uploading a non-ZIP file or a ZIP with no INF returns a 400 error displayed in-page"
|
||||
- "Uploaded driver file is persisted to DRIVERS_DIR via DriverStore (survives restart)"
|
||||
- "Re-uploading the same ZIP does not create a duplicate Driver record (SHA256 dedup)"
|
||||
- "Upload response shows count of unused files not referenced by the INF"
|
||||
- "GET /drivers renders the drivers page with upload form and existing driver list"
|
||||
artifacts:
|
||||
- path: "imptune/api/drivers.py"
|
||||
provides: "POST /drivers/upload endpoint returning HTMX partial"
|
||||
exports: ["router"]
|
||||
- path: "imptune/templates/drivers.html"
|
||||
provides: "Drivers page with upload form and driver list container"
|
||||
contains: "hx-post"
|
||||
- path: "imptune/templates/partials/driver_list.html"
|
||||
provides: "HTMX partial fragment with driver table and select dropdown"
|
||||
contains: "<select"
|
||||
- path: "tests/test_driver_upload.py"
|
||||
provides: "Integration tests for upload endpoint and drivers page"
|
||||
min_lines: 80
|
||||
key_links:
|
||||
- from: "imptune/api/drivers.py"
|
||||
to: "imptune/services/inf_parser.py"
|
||||
via: "import parse_inf, _detect_encoding"
|
||||
pattern: "from imptune\\.services\\.inf_parser import"
|
||||
- from: "imptune/api/drivers.py"
|
||||
to: "imptune/storage/driver_store.py"
|
||||
via: "DriverStore(DRIVERS_DIR).save(data)"
|
||||
pattern: "DriverStore.*save"
|
||||
- from: "imptune/api/drivers.py"
|
||||
to: "imptune/db/models.py"
|
||||
via: "Driver.get_or_create(sha256=...)"
|
||||
pattern: "Driver\\.get_or_create"
|
||||
- from: "imptune/templates/drivers.html"
|
||||
to: "/drivers/upload"
|
||||
via: "hx-post with multipart/form-data"
|
||||
pattern: "hx-post.*drivers/upload"
|
||||
- from: "imptune/main.py"
|
||||
to: "imptune/api/drivers.py"
|
||||
via: "app.include_router(drivers.router)"
|
||||
pattern: "include_router.*drivers"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the driver upload endpoint, drivers page, and HTMX-driven UI that lets technicians upload driver ZIPs, see parsed driver names in a dropdown, and view unused-file hints.
|
||||
|
||||
Purpose: This wires the INF parser (from plan 02-01) into a working upload flow with persistence and UI feedback. After this plan, the full DRV-01 through DRV-05 feature set is functional.
|
||||
Output: Upload API endpoint, drivers page template, HTMX partial for driver list, 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/02-driver-management/02-RESEARCH.md
|
||||
@.planning/phases/02-driver-management/02-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- From Plan 02-01 (INF parser) -->
|
||||
From imptune/services/inf_parser.py:
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class ParsedInf:
|
||||
driver_names: list[str] # resolved DriverDesc values, deduplicated, sorted
|
||||
inf_filename: str # which .inf file inside the ZIP
|
||||
architecture: str | None # 'x64', 'x86', 'arm64', or None
|
||||
has_cat_file: bool # whether a .cat file exists in the ZIP
|
||||
unused_files: list[str] # ZIP members not referenced by the INF
|
||||
|
||||
def _detect_encoding(raw: bytes) -> str: ...
|
||||
def parse_inf(inf_text: str, inf_filename: str, zip_names: list[str]) -> ParsedInf: ...
|
||||
```
|
||||
|
||||
<!-- Existing Phase 1 interfaces -->
|
||||
From imptune/config.py:
|
||||
```python
|
||||
DATA_DIR = os.environ.get("DATA_DIR", "/data")
|
||||
DRIVERS_DIR = str(Path(DATA_DIR) / "drivers")
|
||||
```
|
||||
|
||||
From imptune/storage/driver_store.py:
|
||||
```python
|
||||
class DriverStore:
|
||||
def __init__(self, base_dir: str) -> None: ...
|
||||
def save(self, data: bytes) -> str: ... # returns SHA256 hex
|
||||
```
|
||||
|
||||
From imptune/db/models.py:
|
||||
```python
|
||||
class Driver(BaseModel):
|
||||
sha256 = CharField(unique=True, index=True)
|
||||
original_filename = CharField()
|
||||
size_bytes = IntegerField()
|
||||
uploaded_at = DateTimeField(default=datetime.utcnow)
|
||||
driver_desc = CharField(null=True) # json.dumps(list) for multi-model
|
||||
inf_filename = CharField(null=True)
|
||||
architecture = CharField(null=True)
|
||||
has_cat_file = BooleanField(default=False)
|
||||
```
|
||||
|
||||
From imptune/main.py:
|
||||
```python
|
||||
app = FastAPI(title="ImpTune", lifespan=lifespan)
|
||||
app.include_router(health.router)
|
||||
app.include_router(pages.router)
|
||||
# Add: app.include_router(drivers.router)
|
||||
```
|
||||
|
||||
From imptune/api/pages.py:
|
||||
```python
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
```
|
||||
|
||||
From imptune/templates/base.html:
|
||||
```html
|
||||
<!-- Sidebar already has /drivers link -->
|
||||
<li><a href="/drivers" ...>Drivers</a></li>
|
||||
<!-- Content block: {% block content %}{% endblock %} -->
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Upload endpoint, drivers page route, and integration tests</name>
|
||||
<files>imptune/api/drivers.py, imptune/api/pages.py, imptune/main.py, tests/test_driver_upload.py</files>
|
||||
<behavior>
|
||||
- test_drivers_page: GET /drivers returns 200 with HTML containing upload form (input type="file", hx-post="/drivers/upload")
|
||||
- test_upload_valid_zip: POST /drivers/upload with a valid ZIP containing sample.inf returns 200, HTML contains driver name from INF
|
||||
- test_upload_non_zip: POST /drivers/upload with a .txt file returns 400
|
||||
- test_upload_no_inf: POST /drivers/upload with a ZIP containing no .inf returns 400
|
||||
- test_upload_returns_select: POST /drivers/upload with valid ZIP returns HTML containing a select element with driver names as options
|
||||
- test_driver_persisted: After upload, Driver.select().where(Driver.sha256==expected).count() == 1, and DriverStore file exists on disk
|
||||
- test_dedup_upload: Uploading same ZIP twice creates only one Driver record
|
||||
- test_unused_files_in_response: Upload a ZIP with an extra file not in INF text; response HTML contains "unused" or the count
|
||||
</behavior>
|
||||
<action>
|
||||
**RED phase first:**
|
||||
|
||||
1. Create `tests/test_driver_upload.py` with all 8 integration tests. Tests use the `client` fixture from conftest.py. For test fixtures, create valid ZIP bytes in-memory using `zipfile.ZipFile(io.BytesIO(), 'w')`:
|
||||
- Build a helper `_make_driver_zip(inf_content: str, extra_files: dict[str, bytes] = None) -> bytes` that creates a ZIP with the INF and optional extra files
|
||||
- Use the same sample INF content from tests/fixtures/sample.inf (read it or inline it)
|
||||
- For `test_upload_non_zip`, send raw text bytes with filename="test.zip"
|
||||
- For `test_upload_no_inf`, create a ZIP with only a .txt file
|
||||
- For `test_unused_files_in_response`, add a "readme.txt" to the ZIP that the INF does not reference
|
||||
- All tests use `client.post("/drivers/upload", files={"file": ("driver.zip", zip_bytes, "application/zip")})`
|
||||
- Import `Driver` from `imptune.db.models` and `init_db` from `imptune.db.database` for persistence checks. Call `init_db()` in tests that check DB state (the `client` fixture triggers lifespan which calls init_db).
|
||||
|
||||
2. Run `pytest tests/test_driver_upload.py -x` — all MUST FAIL. Commit: `test(02-02): add failing integration tests for driver upload`
|
||||
|
||||
**GREEN phase:**
|
||||
|
||||
3. Create `imptune/api/drivers.py`:
|
||||
- `router = APIRouter(prefix="/drivers")`
|
||||
- `templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))`
|
||||
- `MAX_UPLOAD_BYTES = 100 * 1024 * 1024`
|
||||
- `POST /upload` endpoint (sync def, not async — Peewee is sync):
|
||||
- Read file bytes, validate size <= 100MB
|
||||
- Validate filename ends with `.zip`
|
||||
- Validate `zipfile.is_zipfile(io.BytesIO(data))`
|
||||
- Open ZIP, validate no zip-slip paths (reject `..` or absolute paths)
|
||||
- Find `.inf` files in namelist; raise 400 if none
|
||||
- Prefer INF whose path contains `amd64`/`x64` if multiple exist; else first alphabetically
|
||||
- Read INF bytes, detect encoding with `_detect_encoding()`, decode
|
||||
- Call `parse_inf(inf_text, inf_filename, zip_names)`
|
||||
- Save via `DriverStore(DRIVERS_DIR).save(data)`
|
||||
- Upsert `Driver.get_or_create(sha256=sha256, defaults={...})` — store `json.dumps(parsed.driver_names)` in `driver_desc`
|
||||
- Query all drivers: `Driver.select().order_by(Driver.uploaded_at.desc())`
|
||||
- Return `templates.TemplateResponse(request=request, name="partials/driver_list.html", context={...})`
|
||||
- On validation errors, return HTMX-friendly error: `HTMLResponse(content="<div id='driver-list' class='error'>Error message</div>", status_code=400)` — so HTMX can swap the error into the target area
|
||||
|
||||
4. Add GET /drivers route to `imptune/api/pages.py`:
|
||||
```python
|
||||
@router.get("/drivers", response_class=HTMLResponse)
|
||||
def drivers_page(request: Request):
|
||||
from imptune.db.models import Driver
|
||||
drivers = list(Driver.select().order_by(Driver.uploaded_at.desc()))
|
||||
return templates.TemplateResponse(
|
||||
request=request, name="drivers.html",
|
||||
context={"drivers": drivers}
|
||||
)
|
||||
```
|
||||
|
||||
5. Register the drivers router in `imptune/main.py`:
|
||||
- Add `from imptune.api import drivers` to imports
|
||||
- Add `app.include_router(drivers.router)` after the pages router
|
||||
|
||||
6. Run `pytest tests/test_driver_upload.py -x` — all MUST PASS. Commit: `feat(02-02): add driver upload endpoint with INF parsing and dedup`
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/test_driver_upload.py -v</automated>
|
||||
</verify>
|
||||
<done>All 8 integration tests pass. POST /drivers/upload accepts ZIPs, parses INFs, persists via DriverStore + Peewee, returns HTMX partial. GET /drivers renders the page. Error cases return 400.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Drivers page template and HTMX partial</name>
|
||||
<files>imptune/templates/drivers.html, imptune/templates/partials/driver_list.html</files>
|
||||
<action>
|
||||
1. Create `imptune/templates/partials/` directory (if not exists).
|
||||
|
||||
2. Create `imptune/templates/drivers.html` extending base.html:
|
||||
```html
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Drivers</h1>
|
||||
<section>
|
||||
<h2>Upload Driver Package</h2>
|
||||
<form
|
||||
hx-post="/drivers/upload"
|
||||
hx-encoding="multipart/form-data"
|
||||
hx-target="#driver-list"
|
||||
hx-swap="outerHTML"
|
||||
hx-indicator="#upload-spinner"
|
||||
>
|
||||
<label for="driver-file">Driver Package (ZIP containing .inf + driver files)</label>
|
||||
<input type="file" id="driver-file" name="file" accept=".zip" required>
|
||||
<button type="submit">Upload</button>
|
||||
<span id="upload-spinner" class="htmx-indicator" aria-busy="true">Uploading...</span>
|
||||
</form>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Driver Library</h2>
|
||||
<div id="driver-list">
|
||||
{% include "partials/driver_list.html" %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
3. Create `imptune/templates/partials/driver_list.html`:
|
||||
- Wrap everything in `<div id="driver-list">` (for HTMX outerHTML swap)
|
||||
- If `drivers` list is empty, show "No drivers uploaded yet."
|
||||
- If `drivers` exist, render a table with columns: Filename, Driver Name(s), Architecture, Uploaded, Unused Files
|
||||
- For each driver, parse `driver.driver_desc` as JSON to get the list of driver names. Display as a `<select>` dropdown if multiple names, or plain text if single name. Use Jinja2: `{% set names = driver.driver_desc | tojson | default('[]') %}` — actually, since driver_desc is already a JSON string, parse it in template or pass parsed data from the route.
|
||||
- Show unused files count if `parsed` context variable is available (on fresh upload): "N files may be unused" with a details/summary for the list
|
||||
- For the "new_driver" highlight (if present in context), add a CSS class to indicate success
|
||||
|
||||
The partial must work both as an include (initial page load, no `parsed` variable) and as a standalone HTMX response (after upload, `parsed` available).
|
||||
|
||||
Template approach for driver names: In the route, pass `driver_names_map` — a dict mapping driver.id to the parsed list. Or simpler: add a property/method. Simplest approach for Jinja2: use a custom filter or pass a helper. Actually, simplest: in the route handler, build a list of dicts with pre-parsed data:
|
||||
```python
|
||||
import json
|
||||
driver_data = []
|
||||
for d in drivers:
|
||||
names = json.loads(d.driver_desc) if d.driver_desc else []
|
||||
driver_data.append({"driver": d, "names": names})
|
||||
```
|
||||
Pass `driver_data` to template. Template iterates `driver_data` and renders `item.names` as select options.
|
||||
|
||||
Update both the drivers.py upload endpoint AND the pages.py GET /drivers route to pass `driver_data` in this format.
|
||||
|
||||
4. Run full test suite to verify no regressions: `pytest tests/ -x -q`
|
||||
</action>
|
||||
<verify>
|
||||
<automated>pytest tests/ -v</automated>
|
||||
</verify>
|
||||
<done>GET /drivers renders a page with upload form and driver table. After upload, HTMX swaps in updated driver list with select dropdown containing parsed driver names. Unused file count visible. All tests green.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
```bash
|
||||
pytest tests/ -v # Full suite green
|
||||
pytest tests/test_driver_upload.py -v # All upload integration tests
|
||||
pytest tests/test_inf_parser.py -v # All parser unit tests
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- POST /drivers/upload with valid driver ZIP returns 200 with HTML containing driver name dropdown
|
||||
- POST /drivers/upload with invalid input returns 400 with clear error
|
||||
- Driver records persisted in SQLite with json.dumps(driver_names) in driver_desc
|
||||
- Driver files persisted in DRIVERS_DIR via SHA256 content-addressed storage
|
||||
- Duplicate uploads produce no duplicate records
|
||||
- GET /drivers renders upload form and existing driver list
|
||||
- Unused files flagged in upload response
|
||||
- All integration + unit tests pass, zero regressions
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-driver-management/02-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user