6.8 KiB
Story: Containerfile Builds the Whole App Again
Phase: 23_containerfile_build · Gate: hermetic integration
tests/integration/test_containerfile_assets.py + real podman build
- image smoke (no Playwright suite — build/infrastructure phase, per the phase plan's A16 note)
Narrative
As the owner, I reported (2026-08-24, TODO.md L6) that the
Containerfile build "is not working". I want
podman build -f Containerfile . to succeed again — and, more
importantly, for the resulting image to serve the whole app: all
four pages (chat, sources, document viewer, login) with their bundled,
minified, local-only assets (No CDN rule, A11), the shared header
module evaluated exactly once per page, and a build contract that
fails loudly when a future page/script/asset outgrows the Containerfile.
- Given the 3-stage
Containerfile(esbuild frontend bundle → uv python deps → slim runtime serving/app/static) - When
podman build -f Containerfile .runs - Then the build is green and the image serves
/,/sources.html,/document.html,/login.htmlwith 200 and every referenced asset local and minified — withheader.jsinlined into each page bundle exactly once.
Bug report (verbatim, TODO.md L6)
Fix Containerfile build not working
Verified diagnosis (2026-08-24, phase 23 — not a guess)
- Root cause of the build failure: phase 19 switched the page
scripts to
import … from "/assets/header.js"(an absolute URL). esbuild resolves that as the filesystem path/assets/header.jsand the stage-1 bundle dies:✘ [ERROR] Could not resolve "/assets/header.js"(reproduced with esbuild 0.25.5, the exact pinned version, on a copy offrontend/—repro_esbuild_0.25.5.log). - Secondary gap (image would be broken even if it built): stage 1
bundled only
app.js+sources.jsand copied onlyindex.html+sources.html. Missing from the image:document.html+login.html(phases 10/16),document.js+login.js, andmarkdown.js(classic script loaded byindex.html+document.html). - Latent double-evaluation trap: all four HTML pages also load
<script type="module" src="/assets/header.js">directly while the page script imports it. In dev the browser dedupes (same module URL) — but in the image the bundled page script already contains the header code, so shipping a rawheader.jstoo would evaluate the module twice (duplicate sign-out listener, double init). The direct tags are redundant: the page script'simportis hoisted and guaranteesheader.jsevaluates before the page script's body callsinitSharedHeader(), in dev and in the bundle alike. - Second build blocker (found in task 01): a local
npm installof esbuild left the binary innode_modules/.bin, invisible to the nextRUN—esbuild: not found. The stage must install globally.
Owner-confirmed (2026-08-24, roadmap A4)
- Relative imports (
from "./header.js") over an esbuild alias — simpler, verified working, dev-server behavior unchanged (files are side-by-side). - Remove the four redundant direct
header.jsscript tags (the design above) rather than ship a rawheader.jsinto the image — single module evaluation, no duplicate listeners. - The image must cover all four pages + all local assets they reference — the integration test (task 02) enforces this coverage so the gap cannot silently reappear.
Fix
| piece | before | after |
|---|---|---|
| page-script header import | from "/assets/header.js" (absolute — esbuild fatal) |
from "./header.js" (relative, bundles cleanly) |
direct header.js <script> tags |
4 (one per page — double-eval trap) | 0 (the hoisted import covers evaluation order) |
| esbuild install | local (binary off the next RUN's PATH) | npm install -g esbuild@0.25.5 (pinned, on PATH) |
| stage-1 bundles | app.js, sources.js |
app.js, sources.js, document.js, login.js |
| stage-1 minifies | styles.css |
styles.css, markdown.js (no --bundle — classic script) |
| stage-1 copies | index.html, sources.html |
all four pages into /out |
Acceptance criteria
- Local esbuild 0.25.5 bundles all four page scripts cleanly.
podman build -f Containerfile .green (log excerpt in.agent/reports/23_containerfile_build/).- Image smoke: container runs (throwaway Postgres 17 + pgvector);
GET /,/sources.html,/document.html,/login.html→ 200;/assets/app.jsminified and contains the header code;/assets/markdown.js200; nohttp(s)://asset reference in any served page (No CDN rule). - Dev server unchanged in behavior:
test_smoke.py,test_shared_header.py,test_chat_persistence.pygreen in isolation. test_containerfile_assets.py6/6 within the full suite;uv run pytestgreen;app/coverage ≥ today's number (>90% gate); ruff + pyright clean.
Test mapping table
Test Scenario → tests/integration/test_containerfile_assets.py
(hermetic — parses Containerfile + frontend/ as text; no podman,
no network, no DB):
test_every_html_page_is_copied_into_stage1— everyfrontend/*.htmlis copied by a stage-1cpinto/out, and the copied set is exactly the pages on disk (new page without cp entry → fail; cp of a deleted page → fail).test_every_local_asset_reference_is_produced— every localassets///assets/src=/href=in the pages is produced by a stage-1 line (esbuild--outfile=/out/assets/<name>or acp) — the missing-markdown.js-style gap cannot reappear.test_page_module_scripts_are_bundled— thetype="module"page scripts referenced by the HTML (basenames) equal the esbuild--bundleinputs in stage 1 (today:app.js,sources.js,document.js,login.js).test_header_module_is_imported_not_directly_loaded— no HTML carries a direct<script … src="…/header.js">tag (single-evaluation design pin, owner-confirmed A4-2) and every page script imports it relatively (from "./header.js"— no absolute form).test_markdown_js_is_a_produced_classic_script—markdown.jshas a stage-1 minify line without--bundleand no top-levelimport/exportin the source (both sides of the classic-script assumption pinned).test_esbuild_stays_pinned— the frontend stage pins a concreteesbuild@X.Y.Z(nolatest/^/~/bare spec — the exact pinned 0.25.5 is what the diagnosis reproduced against).
Complementary source pins live in tests/unit/test_shared_header.py:: test_header_module_loads_before_the_page_script (unit) and the real
build + image smoke are recorded in
.agent/reports/23_containerfile_build/ (task 01).