31 KiB
Phase 9: UX Tech Debt Closure - Research
Researched: 2026-04-13 Domain: FastAPI / HTMX / Alpine.js / Playwright — frontend UX defect closure Confidence: HIGH (all findings grounded in direct codebase inspection)
<user_constraints>
User Constraints (from CONTEXT.md)
Locked Decisions
Plan structure (3 plans, sequential):
- 09-01: UX-01 + driver upload 500 fix (bundled). TDD: failing pytest first, fix root cause in
imptune/api/drivers.py, then add inline driver upload to printer form template, HTMX OOB swap refreshes<select name="driver_id">, auto-select new driver. - 09-02: UX-02 live verification via Playwright headless test. Add Playwright as dev dep, write headless test that loads printer form, types IP, asserts port auto-fill. Test IS the VALIDATION.md evidence.
- 09-03: UX-03 per-script download links. Three distinct GET routes (
/printers/{id}/scripts/install.ps1,/uninstall.ps1,/detect.ps1), reuse existing script generators,Content-Disposition: attachment, wire 3<a role="button">links intoprinter_detail.html.
UX-01 specifics:
- Inline upload control inside
printer_form.html(not a separate page flow) - HTMX OOB swap on
POST /drivers/uploadresponse —<select name="driver_id">refreshed - Auto-select newly uploaded driver (overrides any prior selection — user-confirmed behavior)
- TDD mandatory: failing test → red → fix → green → atomic commit
- Fix the specific 500 root cause only; do not add global exception swallowing
UX-02 specifics:
- Playwright headless test in
tests/(e2e subdir or similar) - pytest-playwright plugin; app started via uvicorn subprocess/thread fixture
- chromium browser (default)
- Test file path + green pytest run = VALIDATION.md evidence
UX-03 specifics:
- Three distinct GET routes (not
?kind=param) - Reuse generators from
imptune/services/(same as package export) Content-Type: text/plain; charset=utf-8,Content-Disposition: attachment; filename="<kind>.ps1"- New
<h2>Scripts</h2>block inprinter_detail.htmlabove/below existing Export section - Existing package export buttons stay untouched
Claude's Discretion
- Exact Alpine.js / HTMX OOB wiring syntax for the driver refresh
- Whether to inline the upload control inside the printer form or stack it above
- Playwright config file location and browser choice (chromium is default, fine)
- Exact wording of the 3 download link labels
- Whether the 3 new script routes live in
imptune/api/printers.pyor a newimptune/api/scripts.py - Test fixture format for the 500 repro (real driver ZIP vs. synthetic ZIP)
Deferred Ideas (OUT OF SCOPE)
- Hardening
/drivers/uploadagainst unknown future exceptions (global try/except, structured error pages) - A "Download all scripts as ZIP" button alongside the 3 individual links
- Playwright tests for anything beyond UX-02
- Refactoring
upload_driverinto a service layer - Fixing
/drivers/uploadto return JSON for programmatic clients </user_constraints>
<phase_requirements>
Phase Requirements
| ID | Description | Research Support |
|---|---|---|
| UX-01 | After a new driver is uploaded on the printer form, the DriverDesc dropdown refreshes automatically (no manual page reload) — verified live in browser | HTMX OOB swap pattern documented; existing upload_driver handler and printer form template inspected; OOB fragment wiring described |
| UX-02 | PRNT-03 Alpine.js IP→port auto-derivation is verified live in a real browser session, with the verification recorded in VALIDATION.md | Alpine.js @input handler already present in printer_form.html:14; Playwright + pytest-playwright integration pattern documented |
| UX-03 | The printer detail page exposes direct download links for each generated script (install / uninstall / detect) in addition to the package export buttons | Script routes ALREADY EXIST in imptune/api/scripts.py — only template links are missing; generator functions confirmed reusable |
| </phase_requirements> |
Summary
Phase 9 closes three UX defects carried over from v1.0, plus one HTTP 500 bug blocking UX-01. All work is surgical: two of the three requirements touch existing files in limited ways, and one (UX-03) is nearly complete — the server-side routes already exist, only the template links are missing.
UX-01 requires the most work. The printer form has no inline upload control today; the upload endpoint works but throws HTTP 500 in at least one code path. The fix requires: (1) diagnosing and TDD-fixing the 500, (2) adding an inline <form> + file input to printer_form.html, (3) extending upload_driver to emit an HTMX OOB fragment that refreshes the driver <select> in the printer form, and (4) marking the new driver as selected. The HTMX OOB swap is the key mechanism: the response must include both the existing #driver-list fragment AND a second fragment with hx-swap-oob="true" targeting a stable id on the driver <select>.
UX-02 is a verification task only: the Alpine.js handler already exists and works (line 14 of printer_form.html). The requirement is to produce permanent machine-readable evidence by writing a Playwright headless test. No application code changes needed — only a new dev dependency and a new test file.
UX-03 is almost entirely done: imptune/api/scripts.py already implements the three GET routes (/printers/{id}/scripts/install, .../uninstall, .../detect) with correct Content-Disposition headers. The only gap is the URL shape (no .ps1 extension in current routes) and the missing template links in printer_detail.html. Decision: whether to rename routes to include .ps1 extension or keep as-is is a Claude's Discretion call — the .ps1 extension in URLs was specified in CONTEXT.md decisions, so new routes or route aliases must match.
Primary recommendation: Read imptune/api/scripts.py before implementing UX-03 — the routes exist, check if renaming vs. adding aliases is cleaner. For UX-01, reproduce the 500 in the test suite first before touching template code.
Standard Stack
Core (already in use — no new prod dependencies)
| Library | Version | Purpose | Why Standard |
|---|---|---|---|
| FastAPI | 0.115.* | HTTP routing, response types | Project standard |
| HTMX | (baked into /static) |
Partial HTML swaps, OOB swap | Project standard for dynamic UI |
| Alpine.js | (baked into /static) |
Inline reactive state (x-data, @input) |
Project standard for client-side reactivity |
| Jinja2 | 3.1.* | HTML template rendering | Project standard |
| Peewee | 3.17.* | ORM for Driver/Printer queries | Project standard |
| pytest + httpx | >=8.0 / >=0.27 | API integration tests | Project standard |
New Dev Dependency (UX-02 only)
| Library | Version | Purpose | When to Use |
|---|---|---|---|
| pytest-playwright | latest stable | Playwright integration for pytest | UX-02 headless browser test |
| playwright | latest stable | Browser automation (chromium) | UX-02 headless browser test |
Installation (dev only):
pip install pytest-playwright playwright
playwright install chromium
Add to requirements-dev.txt:
pytest>=8.0
httpx>=0.27
pytest-playwright
playwright
What Already Exists (Do Not Rebuild)
| Problem | Existing Solution | Location |
|---|---|---|
| Script download routes (install/uninstall/detect) | Already implemented | imptune/api/scripts.py |
| Script generators | render_install, render_uninstall, render_detect |
imptune/generators/script_generator.py |
| Driver upload handler | upload_driver at POST /drivers/upload |
imptune/api/drivers.py |
| Alpine.js IP→port derivation | @input handler on ip_address input |
printer_form.html:14 |
| HTMX partial response pattern | _error_response() + template response |
imptune/api/drivers.py, imptune/api/printers.py |
Architecture Patterns
Pattern 1: HTMX OOB (Out-of-Band) Swap
What: HTMX allows a single response to update multiple DOM regions. The primary content targets the element specified in hx-target; additional hx-swap-oob="true" fragments in the same response are swapped into their respective id-matched DOM elements.
When to use: When a single user action (driver upload) needs to update two independent regions (the driver list on the current view AND the driver <select> in the printer form).
Key constraint: The OOB-swapped element MUST have a stable HTML id attribute in the page DOM. The printer form's <select name="driver_id"> currently has no id — it must be given one (e.g., id="printer-form-driver-select").
Response structure required:
<!-- Primary: updates hx-target="#driver-list" -->
<div id="driver-list">
... existing driver list content ...
</div>
<!-- OOB: updates #printer-form-driver-select anywhere in the page -->
<select name="driver_id" id="printer-form-driver-select" hx-swap-oob="true">
<option value="">-- No driver --</option>
{% for item in driver_data %}
<option value="{{ item.driver.id }}"
{% if item.driver.id == new_driver_id %}selected{% endif %}>
{{ item.driver.original_filename }} ({{ item.names | join(', ') }})
</option>
{% endfor %}
</select>
How the handler knows it's called from the printer form: Check for HX-Target header value, or include a hidden sentinel field in the inline upload form (e.g., <input type="hidden" name="caller" value="printer_form">). The sentinel field is simpler and more explicit.
Source: HTMX documentation — hx-swap-oob attribute (HIGH confidence, direct HTMX docs concept, also in CONTEXT.md decisions)
Pattern 2: Inline Upload Form Within an Existing Form
What: The printer form is a single <form> element. The inline driver upload must NOT be nested inside that form (invalid HTML). It must be a separate <form> element, visually grouped near the driver <select>.
Correct approach:
<!-- In printer_form.html, AFTER closing the driver <label> block but still
within the Alpine x-data div -->
<!-- Existing driver select label (with added id on the select) -->
<label>
Driver
<select name="driver_id" id="printer-form-driver-select">
...
</select>
</label>
<!-- Separate upload form — NOT nested inside the printer <form> -->
<form hx-post="/drivers/upload"
hx-target="#driver-list"
hx-encoding="multipart/form-data"
hx-swap="outerHTML">
<input type="hidden" name="caller" value="printer_form">
<input type="file" name="file" accept=".zip">
<button type="submit">Upload Driver</button>
</form>
Anti-pattern: Nesting <form> inside <form> — browsers silently ignore the inner form; the upload will never fire.
Pattern 3: Playwright Test with App Fixture
What: pytest-playwright provides page and browser fixtures. The app must be running and accessible on a URL before the test can load pages. Use a pytest fixture that starts uvicorn in a background thread.
Fixture pattern:
# tests/e2e/conftest.py
import threading
import time
import pytest
import uvicorn
from imptune.main import app
@pytest.fixture(scope="session")
def live_server(tmp_path_factory):
"""Start the FastAPI app on a random port for E2E tests."""
# Setup tmp data dir (similar to unit test conftest)
...
config = uvicorn.Config(app, host="127.0.0.1", port=8765, log_level="error")
server = uvicorn.Server(config)
thread = threading.Thread(target=server.run, daemon=True)
thread.start()
time.sleep(0.5) # Let server start
yield "http://127.0.0.1:8765"
server.should_exit = True
UX-02 test body:
def test_port_autofill(page, live_server):
page.goto(f"{live_server}/printers")
# Trigger printer form (HTMX-loaded partial or direct URL)
page.fill("input[name='ip_address']", "192.168.1.100")
# Alpine.js reacts synchronously on @input
expected = "IP_192_168_1_100"
assert page.input_value("input[name='port_name']") == expected
Source: pytest-playwright documentation (MEDIUM confidence — verified pattern from official docs concept; exact fixture API confirmed via library knowledge)
Pattern 4: Script Download Routes (UX-03 — Near Zero Work)
Existing state: imptune/api/scripts.py already provides:
GET /printers/{printer_id}/scripts/install→Content-Disposition: attachment; filename="install.ps1"GET /printers/{printer_id}/scripts/uninstall→Content-Disposition: attachment; filename="uninstall.ps1"GET /printers/{printer_id}/scripts/detect→Content-Disposition: attachment; filename="detect.ps1"
Gap vs. CONTEXT.md decision: CONTEXT.md locked URLs include .ps1 extension in the path (e.g., /printers/{id}/scripts/install.ps1). Current routes do NOT have .ps1 in the path — they use /scripts/install without extension.
Resolution (Claude's Discretion): Two options:
- Add new routes with
.ps1extension alongside existing routes (minimal risk, no breakage) - Rename existing routes (simpler, but technically a breaking change if anything already links to the old URLs — unlikely since there are no template links yet)
Option 1 (add aliases) is safer. The existing routes have no template links so breakage risk is zero either way, but aliases are unambiguous.
Template gap: printer_detail.html has NO links to any script download routes. The entire <h2>Scripts</h2> block needs to be added.
Anti-Patterns to Avoid
- Nesting forms: Never put the inline driver upload
<form>inside the printer<form>— browsers silently reject nested forms. - Using
hx-swap-oobwithout a matching DOM id: The OOB target element must exist in the current page DOM with the exact matchingid. - Returning OOB fragment on the
/driverspage route: The/driverspage does NOT contain#printer-form-driver-select. The upload handler must detect its caller context and only emit the OOB fragment when called from the printer form. - Installing Playwright in the production image: Playwright headless browsers are large. Keep in
requirements-dev.txtonly. The Docker production image must not install Playwright. - Running Playwright tests in the standard unit test suite without a live server: pytest-playwright tests require a running HTTP server — they cannot use
TestClient. Use a session-scoped live server fixture.
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| OOB DOM updates after upload | Custom JS to manually refresh the <select> |
HTMX hx-swap-oob |
Already in the project's HTMX bundle; no JS needed |
| Headless browser testing | Selenium setup, manual browser control | pytest-playwright | Built-in pytest fixtures, automatic browser management |
| Script content generation | New generator logic | render_install, render_uninstall, render_detect in script_generator.py |
Already implemented, tested, and used by package export |
| Per-script download routes | New API module | imptune/api/scripts.py (already exists!) |
Routes already implemented — only template links missing |
Common Pitfalls
Pitfall 1: The 500 — Unknown Root Cause (Must Reproduce First)
What goes wrong: POST /drivers/upload returns HTTP 500. The exact traceback is unknown at research time — it was reported during Phase 8 kickoff but not captured in CONTEXT.md.
Why it happens: Looking at drivers.py, the handler has no try/except around the critical path. Likely candidates:
parse_inf()raises an unhandled exception for certain INF contentDriverStore.save()raises a filesystem error (e.g.,DRIVERS_DIRnot created at call time in some edge case)Driver.get_or_create()raises a PeeweeIntegrityErroror similar ORM exception
How to avoid: The locked TDD process is the correct approach: reproduce first in a pytest case, then diagnose from the traceback. Do NOT guess the fix from code reading alone.
Warning signs: Test passes with synthetic INF but fails with real-world driver ZIPs — suggests parse_inf() chokes on real INF content (encoding edge cases, unusual section names).
Pitfall 2: HTMX OOB Fragment Injected on Wrong Page
What goes wrong: The upload handler emits the OOB <select> fragment even when called from the /drivers standalone page. The fragment is silently discarded by HTMX (no matching DOM id) but the presence of junk HTML in the response may cause unexpected behavior.
How to avoid: Gate the OOB fragment emission on the sentinel field: if request.form.get("caller") == "printer_form": emit_oob = True. Only include the OOB block in that branch.
Pitfall 3: Alpine.js x-data Scope and the Inline Upload Form
What goes wrong: The inline upload form is placed inside the printer form's Alpine x-data div. If upload state (e.g., uploading: false) needs to be tracked, it must be added to the x-data initialization object on the outer div — not declared in a nested Alpine component, which would create a separate reactive scope that can't interact with the parent form's ip/port variables.
How to avoid: Extend the existing x-data="{ ip: ..., port: ..., portEdited: ... }" declaration with upload state: x-data="{ ip: ..., port: ..., portEdited: ..., uploading: false }".
Pitfall 4: Playwright Test Startup Race
What goes wrong: The live server fixture starts uvicorn in a thread and immediately yields — the server may not be bound and listening before the first test navigates to a URL, causing connection refused.
How to avoid: Add a brief readiness poll after starting the server thread (e.g., retry GET /health up to 10 times with 100ms sleep). The existing GET /health endpoint is available for this purpose.
Pitfall 5: pytest-playwright Not Finding Chromium Binaries
What goes wrong: playwright install must be run separately from pip install playwright. A pip install alone does not download browser binaries.
How to avoid: Document playwright install chromium as a required setup step in the plan. In CI/CD this would be a setup step; for local dev, the implementing developer must run it once.
Pitfall 6: UX-03 — Confusing Existing Routes with CONTEXT.md URL Shape
What goes wrong: Developer reads imptune/api/scripts.py, sees the three routes exist, declares UX-03 done, forgets (a) the .ps1 extension in the URL was locked in CONTEXT.md, and (b) there are no template links yet.
How to avoid: Check both the route URL shape and the template. Two tasks: add URL aliases/renames if needed, and add template links.
Code Examples
OOB Swap in Upload Handler
# imptune/api/drivers.py — extended upload_driver response branch
# Source: HTMX OOB swap pattern + existing project codebase
from fastapi.responses import HTMLResponse
@router.post("/upload", response_class=HTMLResponse)
def upload_driver(request: Request, file: UploadFile) -> HTMLResponse:
# ... existing validation and persistence logic ...
# Build driver_data for template
drivers = list(Driver.select().order_by(Driver.uploaded_at.desc()))
driver_data = []
for d in drivers:
names = json.loads(d.driver_desc) if d.driver_desc else []
driver_data.append({"driver": d, "names": names})
# Detect caller context via form field
# (caller field included in inline upload form in printer_form.html)
called_from_printer_form = False
try:
form_data = ... # access via request if needed, or pass as Form() param
called_from_printer_form = (form_data.get("caller") == "printer_form")
except Exception:
pass
if called_from_printer_form:
# Return primary fragment + OOB select fragment
return templates.TemplateResponse(
request=request,
name="partials/driver_upload_with_oob.html",
context={
"driver_data": driver_data,
"new_driver_id": new_driver.id,
"parsed": parsed,
},
)
else:
# Existing behavior — driver list only
return templates.TemplateResponse(
request=request,
name="partials/driver_list.html",
context={"driver_data": driver_data, "parsed": parsed},
)
Adding Sentinel Field to Inline Upload Form
<!-- In printer_form.html — separate <form> outside the printer <form> -->
<form hx-post="/drivers/upload"
hx-target="#driver-list"
hx-encoding="multipart/form-data"
hx-swap="outerHTML"
hx-indicator="#upload-indicator">
<input type="hidden" name="caller" value="printer_form">
<label>
Upload Driver
<input type="file" name="file" accept=".zip" required>
</label>
<button type="submit" :disabled="uploading">Upload</button>
<span id="upload-indicator" class="htmx-indicator">Uploading…</span>
</form>
Adding id to the Existing Driver Select
<!-- printer_form.html:30 — add id attribute to the existing <select> -->
<select name="driver_id" id="printer-form-driver-select">
<option value="">-- No driver --</option>
{% for item in driver_data %}
<option value="{{ item.driver.id }}"
{% if printer and printer.driver_id == item.driver.id %}selected{% endif %}>
{{ item.driver.original_filename }} ({{ item.names | join(', ') }})
</option>
{% endfor %}
</select>
OOB Partial Template Fragment
<!-- partials/driver_upload_with_oob.html (new file) -->
<!-- Primary swap target: #driver-list (from hx-target) -->
{% include "partials/driver_list.html" %}
<!-- OOB swap: refreshes driver select in printer form -->
<select name="driver_id" id="printer-form-driver-select" hx-swap-oob="true">
<option value="">-- No driver --</option>
{% for item in driver_data %}
<option value="{{ item.driver.id }}"
{% if item.driver.id == new_driver_id %}selected{% endif %}>
{{ item.driver.original_filename }} ({{ item.names | join(', ') }})
</option>
{% endfor %}
</select>
UX-03 Template Links
<!-- printer_detail.html — new Scripts section, inside {% if has_driver %} -->
<h2>Scripts</h2>
<a href="/printers/{{ printer.id }}/scripts/install.ps1" role="button" class="secondary">
Download Install Script
</a>
<a href="/printers/{{ printer.id }}/scripts/uninstall.ps1" role="button" class="secondary">
Download Uninstall Script
</a>
<a href="/printers/{{ printer.id }}/scripts/detect.ps1" role="button" class="secondary">
Download Detect Script
</a>
TDD Pattern for 500 Repro (Follow Existing Test Structure)
# tests/test_driver_upload.py — add regression test FIRST, before fixing
def test_upload_500_regression(client: TestClient) -> None:
"""POST /drivers/upload must not return 500 for a valid driver ZIP.
This test was added to capture the repro of the HTTP 500 reported
during Phase 8 kickoff (2026-04-13). It should go RED first, then
GREEN after the fix is applied.
"""
zip_bytes = _make_driver_zip() # or use real driver ZIP that triggers the bug
resp = client.post(
"/drivers/upload",
files={"file": ("driver.zip", zip_bytes, "application/zip")},
)
assert resp.status_code != 500, f"Upload returned 500: {resp.text}"
assert resp.status_code == 200
State of the Art
| Old Approach | Current Approach | Impact on Phase 9 |
|---|---|---|
| Script routes did not exist | imptune/api/scripts.py ships in v1.0 with all three routes |
UX-03 is mostly done — only template links missing |
| No per-script download links in template | Template has Export section but no script links | Add <h2>Scripts</h2> block to printer_detail.html |
| Alpine.js port handler untested | Handler exists at printer_form.html:14, known-working |
UX-02 only needs a Playwright test as evidence |
| No inline driver upload in printer form | v1.0 had separate /drivers upload flow |
UX-01 requires both the 500 fix and the inline upload addition |
Key discovery — UX-03 near-complete: The three script download routes already exist in imptune/api/scripts.py, registered in imptune/main.py, with correct Content-Disposition headers. The only work is:
- Determine if URL shape needs
.ps1extension (CONTEXT.md says yes —/printers/{id}/scripts/install.ps1) - Add template links in
printer_detail.html
Current route URLs (/scripts/install) differ from locked decision URLs (/scripts/install.ps1). Claude's Discretion on whether to add aliases or rename.
Open Questions
-
What triggers the HTTP 500 in
POST /drivers/upload?- What we know: Reported during Phase 8 kickoff (2026-04-13); exact traceback not captured in planning docs
- What's unclear: Whether it's
parse_inf(),DriverStore.save(), orDriver.get_or_create()failing - Recommendation: The TDD plan (reproduce → diagnose → fix) is correct; do not guess the fix from code reading. The most likely candidates based on code inspection: (a)
parse_inf()with unusual INF content, (b)DriverStore.save()with a missing directory in certain startup sequences.
-
Passing
callerfield from an HTMX form — Form() parameter or body access?- What we know: FastAPI
Form()parameters work forapplication/x-www-form-urlencodedandmultipart/form-data - What's unclear: The upload endpoint uses
UploadFilewhich is already multipart; addingcaller: str = Form("")as a parameter alongsidefile: UploadFileshould work with FastAPI's multipart handling - Recommendation: Add
caller: str = Form("")parameter toupload_driversignature — FastAPI handles mixed multipart fields + files natively.
- What we know: FastAPI
-
Playwright live server fixture — thread vs. subprocess?
- What we know: uvicorn can run in a thread via
uvicorn.Server.run(); subprocess is more isolated but harder to share DB state - What's unclear: Whether the thread-based approach handles the Peewee SQLite connection properly in a test context (SQLite has per-thread connection behavior)
- Recommendation: Use thread-based fixture but initialize a fresh in-memory or tmp SQLite DB for the E2E session, same pattern as
conftest.pytmp_data_dirfixture. Alternatively, set up one driver + printer record before starting the server so the form has data to interact with.
- What we know: uvicorn can run in a thread via
Validation Architecture
workflow.nyquist_validationistruein.planning/config.json— this section is required.
Test Framework
| Property | Value |
|---|---|
| Framework | pytest >= 8.0 |
| Config file | None — no pytest.ini or pyproject.toml detected |
| Quick run command | pytest tests/ -x -q |
| Full suite command | pytest tests/ -v |
| E2E run command | pytest tests/e2e/ -v (after Playwright install) |
Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|---|---|---|---|---|
| UX-01 (500 fix) | POST /drivers/upload never returns HTTP 500 for valid input |
unit/integration | pytest tests/test_driver_upload.py::test_upload_500_regression -x |
❌ Wave 0 — add test case |
| UX-01 (OOB swap) | Upload response includes OOB <select> fragment when caller=printer_form |
integration | pytest tests/test_driver_upload.py::test_upload_returns_oob_when_called_from_form -x |
❌ Wave 0 — add test case |
| UX-01 (auto-select) | OOB fragment marks newly uploaded driver as selected |
integration | pytest tests/test_driver_upload.py::test_upload_oob_autoselects_new_driver -x |
❌ Wave 0 — add test case |
| UX-02 | input[name='port_name'] fills with IP_192_168_1_100 after typing IP |
e2e/browser | pytest tests/e2e/test_port_autofill.py -v |
❌ Wave 0 — create file |
| UX-03 (routes) | GET /printers/{id}/scripts/install.ps1 returns 200 with Content-Disposition: attachment |
integration | pytest tests/test_script_download.py -x |
❌ Wave 0 — create file |
| UX-03 (template) | printer_detail.html contains links to all 3 script download URLs |
integration | pytest tests/test_packages.py::TestCommandPreview::test_detail_page_shows_script_links -x |
❌ Wave 0 — add test case |
Sampling Rate
- Per task commit:
pytest tests/ -x -q(skip e2e unless Playwright installed) - Per wave merge:
pytest tests/ -v - Phase gate: Full suite green (including
pytest tests/e2e/ -v) before/gsd:verify-work
Wave 0 Gaps
tests/test_driver_upload.py— add:test_upload_500_regression,test_upload_returns_oob_when_called_from_form,test_upload_oob_autoselects_new_drivertests/e2e/conftest.py— live server fixture (uvicorn thread + tmp data dir)tests/e2e/test_port_autofill.py— UX-02 Playwright testtests/test_script_download.py— UX-03 route tests for.ps1URL shapetests/test_packages.py— addtest_detail_page_shows_script_linkstoTestCommandPreview- Framework install:
pip install pytest-playwright playwright && playwright install chromium
Sources
Primary (HIGH confidence)
- Direct codebase inspection —
imptune/api/drivers.py(upload handler, OOB target analysis) - Direct codebase inspection —
imptune/api/scripts.py(existing script routes confirmed) - Direct codebase inspection —
imptune/templates/partials/printer_form.html(Alpine x-data, driver select, no inline upload form) - Direct codebase inspection —
imptune/templates/printer_detail.html(Export section, no script links) - Direct codebase inspection —
imptune/generators/script_generator.py(generator function signatures) - Direct codebase inspection —
tests/test_driver_upload.py,tests/conftest.py(test patterns) - Direct codebase inspection —
requirements.txt,requirements-dev.txt(dependency baseline) .planning/phases/09-ux-tech-debt-closure/09-CONTEXT.md(locked decisions)
Secondary (MEDIUM confidence)
- HTMX
hx-swap-oobdocumentation pattern — documented mechanism; confirmed consistent with HTMX version bundled in project static assets - pytest-playwright fixture pattern — standard plugin API;
pagefixture andbrowserfixture are stable
Tertiary (LOW confidence)
- Root cause of the HTTP 500 — inferred from code reading (no traceback available); confirmed candidates but not verified against a live repro
Metadata
Confidence breakdown:
- UX-03 scope: HIGH — routes exist, confirmed in code; only template + URL shape work remains
- UX-02 scope: HIGH — Alpine handler confirmed in template; Playwright pattern is standard
- UX-01 scope: HIGH for template/HTMX work; MEDIUM for 500 root cause (traceback not available)
- Standard stack: HIGH — verified by direct file inspection
- Architecture patterns: HIGH — grounded in existing codebase conventions
- Pitfalls: HIGH — derived from actual code gaps found during inspection
Research date: 2026-04-13 Valid until: 2026-05-13 (stable codebase; only changes if v1.0 files are modified)