From 7799d3aa96b18f584daf7f8f2d7a85deeb9b9b15 Mon Sep 17 00:00:00 2001 From: Alex Kroman Date: Tue, 9 Jun 2026 10:52:22 -0700 Subject: [PATCH] Use static/ dir (not public/) in init templates for Vercel Vercel auto-serves a `public/**` directory from its CDN and omits it from the Python function bundle, so a deployed template's FastAPI `FileResponse` / `StaticFiles` reads could fail. Move each template's front-end assets into a flat `static/` dir that ships inside the function bundle, letting FastAPI own all routing with zero `vercel.json`. - public/index.html -> static/index.html; public/static/{app.js,styles.css, audio.js} -> static/{...} across all three templates. - api/index.py: PUBLIC -> STATIC, mount /static on the dir itself, with a comment explaining why not public/ so it isn't reverted. - README/AGENTS docs and the contract gate + init tests updated; the /static/* URL now maps directly to disk static/*. Verified: contract gate passes, 86 init tests pass, ruff/prettier/markdownlint clean, and a TestClient run confirms all three apps serve / and /static/*. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../templates/audio-transcription/AGENTS.md | 14 ++++----- .../templates/audio-transcription/README.md | 6 ++-- .../audio-transcription/api/index.py | 11 ++++--- .../{public => }/static/app.js | 0 .../{public => static}/index.html | 0 .../{public => }/static/styles.css | 0 .../init/templates/live-captions/AGENTS.md | 18 +++++------ .../init/templates/live-captions/api/index.py | 9 ++++-- .../live-captions/{public => }/static/app.js | 0 .../{public => }/static/audio.js | 0 .../{public => static}/index.html | 0 .../{public => }/static/styles.css | 0 aai_cli/init/templates/voice-agent/AGENTS.md | 18 +++++------ aai_cli/init/templates/voice-agent/README.md | 2 +- .../init/templates/voice-agent/api/index.py | 9 ++++-- .../voice-agent/{public => }/static/app.js | 0 .../voice-agent/{public => }/static/audio.js | 0 .../voice-agent/{public => static}/index.html | 0 .../{public => }/static/styles.css | 0 scripts/template_contract_gate.py | 27 ++++++++--------- tests/test_init_scaffold.py | 2 +- tests/test_init_template_agent.py | 2 +- tests/test_init_template_contract.py | 30 +++++++++---------- tests/test_init_template_transcribe.py | 10 +++---- 24 files changed, 83 insertions(+), 75 deletions(-) rename aai_cli/init/templates/audio-transcription/{public => }/static/app.js (100%) rename aai_cli/init/templates/audio-transcription/{public => static}/index.html (100%) rename aai_cli/init/templates/audio-transcription/{public => }/static/styles.css (100%) rename aai_cli/init/templates/live-captions/{public => }/static/app.js (100%) rename aai_cli/init/templates/live-captions/{public => }/static/audio.js (100%) rename aai_cli/init/templates/live-captions/{public => static}/index.html (100%) rename aai_cli/init/templates/live-captions/{public => }/static/styles.css (100%) rename aai_cli/init/templates/voice-agent/{public => }/static/app.js (100%) rename aai_cli/init/templates/voice-agent/{public => }/static/audio.js (100%) rename aai_cli/init/templates/voice-agent/{public => static}/index.html (100%) rename aai_cli/init/templates/voice-agent/{public => }/static/styles.css (100%) diff --git a/aai_cli/init/templates/audio-transcription/AGENTS.md b/aai_cli/init/templates/audio-transcription/AGENTS.md index f9568c10..492824cc 100644 --- a/aai_cli/init/templates/audio-transcription/AGENTS.md +++ b/aai_cli/init/templates/audio-transcription/AGENTS.md @@ -10,22 +10,22 @@ uvicorn api.index:app --reload --port 3000 - `api/settings.py`: backend customization for AssemblyAI config, sample URL, and LLM Gateway model. - `api/index.py`: server routes. Keep `ASSEMBLYAI_API_KEY` here on the server. -- `public/static/app.js`: browser workflow, polling, tab rendering, and transcript Q&A UI. -- `public/static/styles.css`: visual styling only; the top `:root` block is the primary theme/layout edit point. -- `public/index.html`: page structure and static asset links. IDs are JavaScript hooks; classes are styling hooks. +- `static/app.js`: browser workflow, polling, tab rendering, and transcript Q&A UI. +- `static/styles.css`: visual styling only; the top `:root` block is the primary theme/layout edit point. +- `static/index.html`: page structure and static asset links. IDs are JavaScript hooks; classes are styling hooks. ## Change Points - Transcription features: edit `TRANSCRIPTION_CONFIG_KWARGS` in `api/settings.py`. -- Sample audio URL: edit `SAMPLE_URL` in `api/settings.py` and the matching input value in `public/index.html`. +- Sample audio URL: edit `SAMPLE_URL` in `api/settings.py` and the matching input value in `static/index.html`. - LLM answer behavior: edit `LLM_MODEL` in `api/settings.py` or the `/api/ask` prompt in `api/index.py`. -- Transcript display: edit renderer functions in `public/static/app.js`. -- Visual theme/layout: edit the monotone Vercel-style tokens in `public/static/styles.css` before changing component rules. +- Transcript display: edit renderer functions in `static/app.js`. +- Visual theme/layout: edit the monotone Vercel-style tokens in `static/styles.css` before changing component rules. - UI state styling: status, tabs, and sentiment use `data-state`, `.is-active`, or `data-sentiment`; prefer CSS changes over JS class rewrites. ## Invariants -- Never expose `ASSEMBLYAI_API_KEY` or any server secret in `public/index.html` or `public/static/`. +- Never expose `ASSEMBLYAI_API_KEY` or any server secret in `static/index.html` or `static/`. - Keep every browser `fetch("/api/...")` route registered in `api/index.py`. - Keep `/api/status/{transcript_id}` non-blocking; do not use SDK helpers that wait for completion in that polling route. - Keep the app buildless unless the user explicitly asks for a frontend toolchain. diff --git a/aai_cli/init/templates/audio-transcription/README.md b/aai_cli/init/templates/audio-transcription/README.md index 298a6e3a..8de442d0 100644 --- a/aai_cli/init/templates/audio-transcription/README.md +++ b/aai_cli/init/templates/audio-transcription/README.md @@ -17,12 +17,12 @@ uvicorn api.index:app --reload --port 3000 Push this folder to a Git repo and import it on Vercel. Set `ASSEMBLYAI_API_KEY` as a Vercel environment variable (the local `.env` is git-ignored and not deployed). -No extra config is needed: Vercel serves the static page and discovers the -FastAPI app in `api/index.py`. +No extra config is needed: Vercel discovers the FastAPI app in `api/index.py`, +which serves the page and its `static/` assets itself. ## Ideas to extend - Show chapter summaries and highlight timestamps. - Add a waveform / audio player synced to the transcript. - Swap the analysis features in `TRANSCRIPTION_CONFIG_KWARGS` (`api/settings.py`). -- Change transcript rendering in `public/static/app.js`. +- Change transcript rendering in `static/app.js`. diff --git a/aai_cli/init/templates/audio-transcription/api/index.py b/aai_cli/init/templates/audio-transcription/api/index.py index fbe7389a..c7a498ef 100644 --- a/aai_cli/init/templates/audio-transcription/api/index.py +++ b/aai_cli/init/templates/audio-transcription/api/index.py @@ -6,7 +6,7 @@ GET /api/status/{id} -> poll; returns the full transcript JSON when complete POST /api/ask -> ask a question about a transcript via the LLM Gateway -The browser (public/index.html + public/static/app.js) submits a URL or file, then +The browser (static/index.html + static/app.js) submits a URL or file, then polls status. Your API key stays on the server — the browser never sees it. """ @@ -36,14 +36,17 @@ CONFIG = aai.TranscriptionConfig(**settings.TRANSCRIPTION_CONFIG_KWARGS) ROOT = Path(__file__).resolve().parent.parent -PUBLIC = ROOT / "public" +# Front-end assets live in static/, NOT public/: Vercel auto-serves a public/ dir +# from its CDN and omits it from the function bundle, so FastAPI couldn't read these +# files. A plain static/ dir ships with the function and lets FastAPI own all routing. +STATIC = ROOT / "static" app = FastAPI() -app.mount("/static", StaticFiles(directory=PUBLIC / "static"), name="static") +app.mount("/static", StaticFiles(directory=STATIC), name="static") @app.get("/") def index() -> FileResponse: - return FileResponse(PUBLIC / "index.html") + return FileResponse(STATIC / "index.html") def _submit(audio: str) -> dict[str, str]: diff --git a/aai_cli/init/templates/audio-transcription/public/static/app.js b/aai_cli/init/templates/audio-transcription/static/app.js similarity index 100% rename from aai_cli/init/templates/audio-transcription/public/static/app.js rename to aai_cli/init/templates/audio-transcription/static/app.js diff --git a/aai_cli/init/templates/audio-transcription/public/index.html b/aai_cli/init/templates/audio-transcription/static/index.html similarity index 100% rename from aai_cli/init/templates/audio-transcription/public/index.html rename to aai_cli/init/templates/audio-transcription/static/index.html diff --git a/aai_cli/init/templates/audio-transcription/public/static/styles.css b/aai_cli/init/templates/audio-transcription/static/styles.css similarity index 100% rename from aai_cli/init/templates/audio-transcription/public/static/styles.css rename to aai_cli/init/templates/audio-transcription/static/styles.css diff --git a/aai_cli/init/templates/live-captions/AGENTS.md b/aai_cli/init/templates/live-captions/AGENTS.md index 534c2462..e2122f77 100644 --- a/aai_cli/init/templates/live-captions/AGENTS.md +++ b/aai_cli/init/templates/live-captions/AGENTS.md @@ -10,23 +10,23 @@ uvicorn api.index:app --reload --port 3000 - `api/settings.py`: backend token host, token path, WebSocket path, and token expiry. - `api/index.py`: `/api/token` route. Keep `ASSEMBLYAI_API_KEY` here on the server. -- `public/static/app.js`: browser state, WebSocket lifecycle, and Streaming API params. -- `public/static/audio.js`: microphone pipeline and PCM downsampling helpers. -- `public/static/styles.css`: visual styling only; the top `:root` block is the primary theme/layout edit point. -- `public/index.html`: page structure and static asset links. IDs are JavaScript hooks; classes are styling hooks. +- `static/app.js`: browser state, WebSocket lifecycle, and Streaming API params. +- `static/audio.js`: microphone pipeline and PCM downsampling helpers. +- `static/styles.css`: visual styling only; the top `:root` block is the primary theme/layout edit point. +- `static/index.html`: page structure and static asset links. IDs are JavaScript hooks; classes are styling hooks. ## Change Points -- Streaming model, sample rate, encoding, and turn formatting: edit `STREAMING_CONFIG` in `public/static/app.js`. +- Streaming model, sample rate, encoding, and turn formatting: edit `STREAMING_CONFIG` in `static/app.js`. - Backend token lifetime or non-production hosts: edit `api/settings.py`. -- Caption rendering: edit `onMessage` in `public/static/app.js`. -- Microphone/downsampling behavior: edit `public/static/audio.js`. -- Visual theme/layout: edit the monotone Vercel-style tokens in `public/static/styles.css` before changing component rules. +- Caption rendering: edit `onMessage` in `static/app.js`. +- Microphone/downsampling behavior: edit `static/audio.js`. +- Visual theme/layout: edit the monotone Vercel-style tokens in `static/styles.css` before changing component rules. - UI state styling: record and status state use `data-state`; prefer CSS changes over JS class rewrites. ## Invariants -- Never expose `ASSEMBLYAI_API_KEY` or any server secret in `public/index.html` or `public/static/`. +- Never expose `ASSEMBLYAI_API_KEY` or any server secret in `static/index.html` or `static/`. - Streaming token auth uses the raw API key in the backend `Authorization` header, not `Bearer`. - Keep the browser connected directly to AssemblyAI; do not proxy the audio stream through FastAPI unless the user asks. - Keep the app buildless unless the user explicitly asks for a frontend toolchain. diff --git a/aai_cli/init/templates/live-captions/api/index.py b/aai_cli/init/templates/live-captions/api/index.py index 22d7b634..693448e2 100644 --- a/aai_cli/init/templates/live-captions/api/index.py +++ b/aai_cli/init/templates/live-captions/api/index.py @@ -21,14 +21,17 @@ from api import settings ROOT = Path(__file__).resolve().parent.parent -PUBLIC = ROOT / "public" +# Front-end assets live in static/, NOT public/: Vercel auto-serves a public/ dir +# from its CDN and omits it from the function bundle, so FastAPI couldn't read these +# files. A plain static/ dir ships with the function and lets FastAPI own all routing. +STATIC = ROOT / "static" app = FastAPI() -app.mount("/static", StaticFiles(directory=PUBLIC / "static"), name="static") +app.mount("/static", StaticFiles(directory=STATIC), name="static") @app.get("/") def index() -> FileResponse: - return FileResponse(PUBLIC / "index.html") + return FileResponse(STATIC / "index.html") @app.post("/api/token") diff --git a/aai_cli/init/templates/live-captions/public/static/app.js b/aai_cli/init/templates/live-captions/static/app.js similarity index 100% rename from aai_cli/init/templates/live-captions/public/static/app.js rename to aai_cli/init/templates/live-captions/static/app.js diff --git a/aai_cli/init/templates/live-captions/public/static/audio.js b/aai_cli/init/templates/live-captions/static/audio.js similarity index 100% rename from aai_cli/init/templates/live-captions/public/static/audio.js rename to aai_cli/init/templates/live-captions/static/audio.js diff --git a/aai_cli/init/templates/live-captions/public/index.html b/aai_cli/init/templates/live-captions/static/index.html similarity index 100% rename from aai_cli/init/templates/live-captions/public/index.html rename to aai_cli/init/templates/live-captions/static/index.html diff --git a/aai_cli/init/templates/live-captions/public/static/styles.css b/aai_cli/init/templates/live-captions/static/styles.css similarity index 100% rename from aai_cli/init/templates/live-captions/public/static/styles.css rename to aai_cli/init/templates/live-captions/static/styles.css diff --git a/aai_cli/init/templates/voice-agent/AGENTS.md b/aai_cli/init/templates/voice-agent/AGENTS.md index 89302ce4..5d0a4fc8 100644 --- a/aai_cli/init/templates/voice-agent/AGENTS.md +++ b/aai_cli/init/templates/voice-agent/AGENTS.md @@ -10,23 +10,23 @@ uvicorn api.index:app --reload --port 3000 - `api/settings.py`: backend token host, token path, WebSocket path, and token expiry. - `api/index.py`: `/api/token` route. Keep `ASSEMBLYAI_API_KEY` here on the server. -- `public/static/app.js`: Voice Agent session config, WebSocket lifecycle, UI state, and event handling. -- `public/static/audio.js`: microphone pipeline, PCM conversion, playback queue, and barge-in helpers. -- `public/static/styles.css`: visual styling only; the top `:root` block is the primary theme/layout edit point. -- `public/index.html`: page structure and static asset links. IDs are JavaScript hooks; classes are styling hooks. +- `static/app.js`: Voice Agent session config, WebSocket lifecycle, UI state, and event handling. +- `static/audio.js`: microphone pipeline, PCM conversion, playback queue, and barge-in helpers. +- `static/styles.css`: visual styling only; the top `:root` block is the primary theme/layout edit point. +- `static/index.html`: page structure and static asset links. IDs are JavaScript hooks; classes are styling hooks. ## Change Points -- Agent prompt, greeting, voice, audio formats, and microphone constraints: edit `SESSION_CONFIG` in `public/static/app.js`. +- Agent prompt, greeting, voice, audio formats, and microphone constraints: edit `SESSION_CONFIG` in `static/app.js`. - Backend token lifetime or non-production hosts: edit `api/settings.py`. -- Transcript log rendering: edit `addTurn` in `public/static/app.js`. -- Playback, barge-in, or PCM conversion: edit `public/static/audio.js`. -- Visual theme/layout: edit the monotone Vercel-style tokens in `public/static/styles.css` before changing component rules. +- Transcript log rendering: edit `addTurn` in `static/app.js`. +- Playback, barge-in, or PCM conversion: edit `static/audio.js`. +- Visual theme/layout: edit the monotone Vercel-style tokens in `static/styles.css` before changing component rules. - UI state styling: connection, status, and speaker state use `data-state` or `data-speaker`; prefer CSS changes over JS class rewrites. ## Invariants -- Never expose `ASSEMBLYAI_API_KEY` or any server secret in `public/index.html` or `public/static/`. +- Never expose `ASSEMBLYAI_API_KEY` or any server secret in `static/index.html` or `static/`. - Voice Agent token auth uses `Authorization: Bearer ...` in the backend. This differs from Streaming token auth. - Voice Agent `greeting` is spoken literally by TTS; write the exact words the user should hear. - `reply.audio` carries base64 PCM on the `data` field. diff --git a/aai_cli/init/templates/voice-agent/README.md b/aai_cli/init/templates/voice-agent/README.md index 3d4b4709..fe1a477d 100644 --- a/aai_cli/init/templates/voice-agent/README.md +++ b/aai_cli/init/templates/voice-agent/README.md @@ -25,6 +25,6 @@ is needed. ## Ideas to extend -- Change the `greeting`, `systemPrompt`, or `voice` in `SESSION_CONFIG` (`public/static/app.js`). +- Change the `greeting`, `systemPrompt`, or `voice` in `SESSION_CONFIG` (`static/app.js`). - Add tools (function calling) so the agent can look things up or take actions. - Tune `input.turn_detection` (`min_silence`/`max_silence`) inside `SESSION_CONFIG`. diff --git a/aai_cli/init/templates/voice-agent/api/index.py b/aai_cli/init/templates/voice-agent/api/index.py index 26e6473d..41a00135 100644 --- a/aai_cli/init/templates/voice-agent/api/index.py +++ b/aai_cli/init/templates/voice-agent/api/index.py @@ -21,14 +21,17 @@ from api import settings ROOT = Path(__file__).resolve().parent.parent -PUBLIC = ROOT / "public" +# Front-end assets live in static/, NOT public/: Vercel auto-serves a public/ dir +# from its CDN and omits it from the function bundle, so FastAPI couldn't read these +# files. A plain static/ dir ships with the function and lets FastAPI own all routing. +STATIC = ROOT / "static" app = FastAPI() -app.mount("/static", StaticFiles(directory=PUBLIC / "static"), name="static") +app.mount("/static", StaticFiles(directory=STATIC), name="static") @app.get("/") def index() -> FileResponse: - return FileResponse(PUBLIC / "index.html") + return FileResponse(STATIC / "index.html") @app.post("/api/token") diff --git a/aai_cli/init/templates/voice-agent/public/static/app.js b/aai_cli/init/templates/voice-agent/static/app.js similarity index 100% rename from aai_cli/init/templates/voice-agent/public/static/app.js rename to aai_cli/init/templates/voice-agent/static/app.js diff --git a/aai_cli/init/templates/voice-agent/public/static/audio.js b/aai_cli/init/templates/voice-agent/static/audio.js similarity index 100% rename from aai_cli/init/templates/voice-agent/public/static/audio.js rename to aai_cli/init/templates/voice-agent/static/audio.js diff --git a/aai_cli/init/templates/voice-agent/public/index.html b/aai_cli/init/templates/voice-agent/static/index.html similarity index 100% rename from aai_cli/init/templates/voice-agent/public/index.html rename to aai_cli/init/templates/voice-agent/static/index.html diff --git a/aai_cli/init/templates/voice-agent/public/static/styles.css b/aai_cli/init/templates/voice-agent/static/styles.css similarity index 100% rename from aai_cli/init/templates/voice-agent/public/static/styles.css rename to aai_cli/init/templates/voice-agent/static/styles.css diff --git a/scripts/template_contract_gate.py b/scripts/template_contract_gate.py index bdec3940..f2395878 100644 --- a/scripts/template_contract_gate.py +++ b/scripts/template_contract_gate.py @@ -15,9 +15,9 @@ "api/index.py", "api/__init__.py", "api/settings.py", - "public/index.html", - "public/static/app.js", - "public/static/styles.css", + "static/index.html", + "static/app.js", + "static/styles.css", "requirements.txt", "README.md", "AGENTS.md", @@ -50,27 +50,26 @@ def _required_files(template: str, path: Path) -> None: for rel in _REQUIRED_FILES: if not (path / rel).exists(): _fail(f"{template}: missing {rel}") - if ( - template in {"live-captions", "voice-agent"} - and not (path / "public/static/audio.js").exists() - ): - _fail(f"{template}: missing public/static/audio.js") + if template in {"live-captions", "voice-agent"} and not (path / "static/audio.js").exists(): + _fail(f"{template}: missing static/audio.js") def _html_static_refs(template: str, path: Path) -> None: - html = (path / "public/index.html").read_text(encoding="utf-8") + html = (path / "static/index.html").read_text(encoding="utf-8") refs = set(re.findall(r'(?:href|src)=["\'](/static/[^"\']+)', html)) if not refs: - _fail(f"{template}: public/index.html should load static assets") + _fail(f"{template}: static/index.html should load static assets") for ref in refs: - if not (path / "public" / ref.lstrip("/")).exists(): - _fail(f"{template}: public/index.html references missing asset {ref!r}") + # /static/* is mounted on the static/ dir itself, so the URL path maps + # directly to a file under the template root (e.g. /static/app.js -> static/app.js). + if not (path / ref.lstrip("/")).exists(): + _fail(f"{template}: static/index.html references missing asset {ref!r}") def _frontend_routes(template: str, path: Path) -> None: - frontend = (path / "public/index.html").read_text(encoding="utf-8") + frontend = (path / "static/index.html").read_text(encoding="utf-8") frontend += "\n".join( - asset.read_text(encoding="utf-8") for asset in (path / "public/static").glob("*.js") + asset.read_text(encoding="utf-8") for asset in (path / "static").glob("*.js") ) fetched = set(re.findall(r'fetch\(\s*["\'`](/api/[^"\'`?]+)', frontend)) fetched |= set(re.findall(r'["\'`](/api/[A-Za-z0-9_\-/]+?)(?:/?\$\{|/?["\'`]\s*\+)', frontend)) diff --git a/tests/test_init_scaffold.py b/tests/test_init_scaffold.py index 8678684b..d8b1ab72 100644 --- a/tests/test_init_scaffold.py +++ b/tests/test_init_scaffold.py @@ -32,7 +32,7 @@ def test_scaffold_copies_files_and_renames_dotfiles(tmp_path): target = tmp_path / "app" scaffold.scaffold("audio-transcription", target, api_key="sk-real-key") assert (target / "api" / "index.py").exists() - assert (target / "public" / "index.html").exists() + assert (target / "static" / "index.html").exists() assert not (target / "vercel.json").exists() # dotfile templates are renamed to their dotted names assert (target / ".gitignore").exists() diff --git a/tests/test_init_template_agent.py b/tests/test_init_template_agent.py index ce866b21..7fc0146c 100644 --- a/tests/test_init_template_agent.py +++ b/tests/test_init_template_agent.py @@ -49,7 +49,7 @@ def fake_get(url, params=None, headers=None): def test_page_reads_reply_audio_from_data_field(): # reply.audio carries the base64 PCM in `data` (not `audio`); guard the regression. - app_js = (TEMPLATE_DIR / "public" / "static" / "app.js").read_text() + app_js = (TEMPLATE_DIR / "static" / "app.js").read_text() assert "reply.audio" in app_js assert "event.data" in app_js diff --git a/tests/test_init_template_contract.py b/tests/test_init_template_contract.py index 850317f5..1acdcee4 100644 --- a/tests/test_init_template_contract.py +++ b/tests/test_init_template_contract.py @@ -24,9 +24,9 @@ def test_required_files_present(template_dir): "api/index.py", "api/__init__.py", "api/settings.py", - "public/index.html", - "public/static/app.js", - "public/static/styles.css", + "static/index.html", + "static/app.js", + "static/styles.css", "requirements.txt", "README.md", "AGENTS.md", @@ -38,25 +38,27 @@ def test_required_files_present(template_dir): def test_realtime_templates_have_audio_helpers(template_dir): if template_dir.name in {"live-captions", "voice-agent"}: - assert (template_dir / "public" / "static" / "audio.js").exists() + assert (template_dir / "static" / "audio.js").exists() def test_static_assets_referenced_by_html_exist(template_dir): - html = (template_dir / "public" / "index.html").read_text() + html = (template_dir / "static" / "index.html").read_text() refs = set(re.findall(r'(?:href|src)=["\'](/static/[^"\']+)', html)) - assert refs, f"{template_dir.name}: public/index.html should load static assets" + assert refs, f"{template_dir.name}: static/index.html should load static assets" for ref in refs: - assert (template_dir / "public" / ref.lstrip("/")).exists(), ( - f"{template_dir.name}: public/index.html references missing asset {ref!r}" + # /static/* is mounted on the static/ dir itself, so the URL path maps + # directly to a file under the template root (/static/app.js -> static/app.js). + assert (template_dir / ref.lstrip("/")).exists(), ( + f"{template_dir.name}: static/index.html references missing asset {ref!r}" ) def test_codex_edit_points_are_explicit(template_dir): notes = (template_dir / "AGENTS.md").read_text() - app_js = (template_dir / "public" / "static" / "app.js").read_text() + app_js = (template_dir / "static" / "app.js").read_text() assert "ASSEMBLYAI_API_KEY" in notes assert "buildless" in notes - assert "public/static/app.js" in notes + assert "static/app.js" in notes assert "_CONFIG" in app_js @@ -67,10 +69,8 @@ def test_no_committed_dotenv_or_real_key(template_dir): def test_frontend_routes_exist_in_backend(template_dir): """Every /api path the page fetches must be a route the backend registers.""" - frontend = (template_dir / "public" / "index.html").read_text() - frontend += "\n".join( - path.read_text() for path in (template_dir / "public" / "static").glob("*.js") - ) + frontend = (template_dir / "static" / "index.html").read_text() + frontend += "\n".join(path.read_text() for path in (template_dir / "static").glob("*.js")) fetched = set(re.findall(r'fetch\(\s*["\'`](/api/[^"\'`?]+)', frontend)) # Also catch template-literal paths like fetch(`/api/status/${id}`) and "/api/x/" + id fetched |= set(re.findall(r'["\'`](/api/[A-Za-z0-9_\-/]+?)(?:/?\$\{|/?["\'`]\s*\+)', frontend)) @@ -80,7 +80,7 @@ def test_frontend_routes_exist_in_backend(template_dir): for path in fetched: base = path.rstrip("/") assert any(base == r or base.startswith(r + "/") for r in registered_bases), ( - f"{template_dir.name}: public/index.html fetches {path!r}, " + f"{template_dir.name}: static/index.html fetches {path!r}, " f"not registered in api/index.py (routes: {sorted(registered_bases)})" ) diff --git a/tests/test_init_template_transcribe.py b/tests/test_init_template_transcribe.py index ce8937e8..7259c58c 100644 --- a/tests/test_init_template_transcribe.py +++ b/tests/test_init_template_transcribe.py @@ -42,9 +42,9 @@ def test_required_files_exist(): for rel in ( "api/index.py", "api/settings.py", - "public/index.html", - "public/static/app.js", - "public/static/styles.css", + "static/index.html", + "static/app.js", + "static/styles.css", "requirements.txt", "README.md", "AGENTS.md", @@ -68,8 +68,8 @@ def test_base_url_env_is_applied(monkeypatch, mocker): def test_page_explores_all_features_and_speakers(): # Guard the UI surface: each audio-intelligence view + per-speaker coloring stay wired. - html = (TEMPLATE_DIR / "public" / "index.html").read_text() - app_js = (TEMPLATE_DIR / "public" / "static" / "app.js").read_text() + html = (TEMPLATE_DIR / "static" / "index.html").read_text() + app_js = (TEMPLATE_DIR / "static" / "app.js").read_text() ui_src = html + app_js for token in ( "chapters",