DimitriGilbert / DimitriGilbert/speaches-ui

Test & quality alignment plan

Open
#1 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
0
Forks
0
PR merge metrics
No merged PRs in 30d

Description

# Test & Quality Alignment Plan — speaches-ui

_Revised 2026-09-05: CI removed by owner decision — quality gates are local commands run by the owner/agents; no GitHub Actions._

- **Project**: `/home/didi/workspace/speaches-ui` (origin: `DimitriGilbert/speaches-ui`)
- **Date**: 2026-09-05
- **Generated by the fleet test-alignment initiative** (28-repo audit + synthesis)
- **Inputs**: [audit report](../.test-alignment-audit/reports/speaches-ui.md) · [fleet synthesis & decisions](../.test-alignment-audit/synthesis-decisions.md)
- **Execution model**: phases of implementer/validator subagent tasks per the `subagent-orchestration` skill (strict role separation, NO-SLOP policy, fix loop max 3 attempts, validator must actually read the code — running commands alone is not validation). Plan-only document: nothing here is implemented yet.

---

## 1. Project snapshot (reality check)

- Single-package pnpm app bootstrapped with **create-tanstack-app v1** (`.cta.json`) — TanStack Start/Query/Router, React 19, Tailwind v4, zod v4, shadcn vendored under `src/components/ui/`. **Not** Better-T-Stack; `pnpm-workspace.yaml` has only `allowBuilds` (no `packages:` field).
- ~619 LOC of hand-written logic across `src/lib/` (30 + 184 + 145 + 7), `src/routes/api/` (6 proxy routes, 209 LOC), `src/hooks/use-speaches.ts` (44 LOC). **0 test files, 0 test-bearing commits, no `test` script, no `.github/`, no Renovate/Dependabot, no hooks.**
- Testable seams already designed for it: `src/lib/models.ts` (pure env-free classifiers, docstring states the intent), `src/lib/speaches.ts` (server fetch wrapper, 1:1 with backend endpoints), `src/lib/client.ts` (browser fetch helpers, relative `/api/*` URLs).
- Existing quality discipline worth keeping: ESLint 9 flat + Prettier 3, strict tsconfig, and the **mandatory Docker sandbox** (`AGENTS.md`: every install/build/test/run via `docker compose run --rm app …`, node:22-slim, corepack pnpm, persistent pnpm store volume).
- Verification today = README curl smoke tests + `pnpm check-types` / `pnpm build` inside the sandbox. Nothing gates a push to `main`.

## 2. Adopted decisions (synthesis D1–D7), adapted to the single-package shape

| Decision | Adoption here | Adaptation / deviation |
|---|---|---|
| **D1** runner & layout | **vitest v4 line**, tests colocated as `*.test.ts(x)`, root one-command headless entry `pnpm test` (`vitest run`), `passWithNoTests` never enabled | **Environment: `node` by default.** The per-project delta says "seed vitest (jsdom)", but the seed targets (`models.ts`, `speaches.ts`, `client.ts`) use zero DOM APIs — `fetch`, `FormData`, `Blob`, `Response`, `Request` are Node 22 globals. Following D1's own policy (jsdom only where DOM is under test), seeds run in `node`; **jsdom + RTL arrive in Phase 6** when hooks/components are actually tested. |
| **D2** coverage | `@vitest/coverage-v8`; reporters `text + json + html`; `include: src/**`; exclude `src/routeTree.gen.ts`, `src/components/ui/**`, tests | Start at state 3 (report-only, no thresholds — thresholds on an empty suite are theater). Once Phases 1–2 seeds land: record baseline in AGENTS.md, set `lines` threshold = measured − 5 in Phase 3, **ratchet +5 when green, never lower**. |
| **D3** lint/format | Keep ESLint 9 flat + `@tanstack/eslint-config` + Prettier 3 (working; not migrating). **Re-enable `import/no-cycle` and `import/order`** (both explicitly `off` today with no replacement) | `sort-imports` stays off (superseded by `import/order`) — documented with a comment, not silently. Convergence onto the shared `@dg/config` preset happens when that package publishes and this repo is touching lint anyway (D6 adoption principle) — not a big-bang config churn. |
| **D4** dep hygiene & metrics | **Renovate one-liner** (`renovate.json` extending `local>DimitriGilbert`) — this is the standing fix for the floating `"latest"` pins. knip + jscpd **report-only** in Phase 7 (launch-mommy canonical jscpd config: `minLines: 8, minTokens: 80`, exclude tests/vendored/generated; `exitCode: 0`) | Circular deps: covered by re-enabled `import/no-cycle` (no madge/dpdm). **No Stryker** — UI app, not one of the five nominated cores. No gates on any metric this repo has never measured. |
| **D5** no CI by design (owner decision 2026-09-05) | **No GitHub Actions, no reusable-workflow caller, no local workflow.** The earlier "thin caller of `DimitriGilbert/.github` `ts-ci.yml` (with a self-contained fallback)" plan is dropped entirely. Enforcement = the root-level headless verification entrypoint (`pnpm verify`: lint → check → check-types → test:coverage → build, composed in Phase 3) run by the owner and by agents — all inside the Docker sandbox; the plan's phase validation gates are run by implementer/validator agents before any phase is declared done. Optional Playwright e2e stays an on-demand local tier, never in the default gate. |
| **D6** shared config | Adopt `@dg/config` (tsconfig/eslint/vitest presets) at the natural touchpoints: vitest config in Phase 1, eslint changes in Phase 4 — only if published by then; otherwise local configs with minimal drift and a follow-up | Single package: no `packages/config` indirection to keep — direct consumption. |
| **D7** hooks | **No hooks.** The local gate commands run by the owner/agents are the only authoritative gate. AGENTS.md states: "the headless local verification command (`pnpm verify`) is the quality gate, run by the owner/agents; hooks are local convenience." A repo with no hooks and a green local `pnpm verify` is compliant. | Nothing to delete — none exist. |

### Deviations from the synthesis (reality over synthesis, per initiative rules)

1. **`"latest"` pin count is 9, not 11** — audit and synthesis both say 11; `grep -c '"latest"' package.json` says 9 (7 dependencies + 2 devDependencies, all TanStack). Reality wins; the fix is the same.
2. **Seeds run in `node`, not `jsdom`** (see D1 row) — no DOM API in the seed targets; jsdom deferred to the component tier.
3. **Exact pins from the lockfile instead of a pnpm catalog** — D1/D4 pin versions "via its `pnpm-workspace.yaml` catalog"; that pays off in monorepos. This repo has one `package.json` (the workspace file exists only for `allowBuilds`), so the plan pins caret ranges resolved from the committed lockfile and lets Renovate manage updates. No catalog indirection for a single package.
4. **`pnpm/json-enforce-catalog` stays off** — consequence of deviation 3; documented in the eslint config with a comment.
5. **`bts.jsonc` conventions do not apply** — this is a create-tanstack-app repo (`.cta.json`), not BTS; workspace instructions referencing `bts.jsonc` are out of scope here.
6. **Everything runs inside the Docker sandbox.** The AGENTS.md sandbox rule protects the *host* from untrusted postinstall scripts. Under the no-CI decision nothing runs on ephemeral runners anymore — the sandbox is mandatory for all execution of this plan's gates (see Risks).

---

## 3. Phased plan

> Every phase below is executed as: **implementer** (creates/modifies files, runs the gatekeeping commands itself before reporting done) → **validator** (different agent; reads the code line-by-line, enforces NO-SLOP, re-runs gates) → fix loop (fixer fixes ALL validator findings at once; max 3 attempts). All local commands run inside the sandbox: `docker compose run --rm app `. The standard gate chain used throughout:
>
> ```
> docker compose run --rm app pnpm lint
> docker compose run --rm app pnpm check
> docker compose run --rm app pnpm check-types
> docker compose run --rm app pnpm test # from Phase 1 on
> docker compose run --rm app pnpm build
> ```
>
> NO-SLOP (verbatim, every implementer/fixer dispatch): no `any`/`as any`/`: any`; no placeholders, `TODO`/`FIXME`; no unused imports/variables; no console.log hacks or `void` hacks; `import type` for type-only imports (`verbatimModuleSyntax` is on); external imports first, blank line, then local imports; never start the dev server; never fake a pass (`passWithNoTests` is forbidden).

### Phase 0 — Dependency hygiene: kill the floating pins (effort: S)

**Goal**: reproducibility no longer rests on the lockfile alone; Renovate owns updates from here on.

**Tasks (implementer)**
1. Replace all 9 `"latest"` specifiers in `package.json` with caret ranges matching the lockfile-resolved versions (as of 2026-09-05 — re-read `pnpm-lock.yaml` first if it moved):
- deps: `@tanstack/react-devtools ^0.10.9`, `@tanstack/react-query ^5.101.4`, `@tanstack/react-query-devtools ^5.101.4`, `@tanstack/react-router ^1.170.18`, `@tanstack/react-router-devtools ^1.167.0`, `@tanstack/react-router-ssr-query ^1.167.1`, `@tanstack/react-start ^1.168.33`
- devDeps: `@tanstack/devtools-vite ^0.8.3`, `@tanstack/eslint-config ^0.4.0`
2. Sync the lockfile to the new specifiers (same resolved versions): `docker compose run --rm app pnpm install`.
3. Pin the package manager: add `"packageManager": "pnpm@"` to `package.json` (version = what `docker compose run --rm app pnpm --version` reports) and align `Dockerfile`'s `corepack prepare pnpm@latest --activate` to that same version — today the Dockerfile carries its own floating pin.
4. Add `renovate.json`: `{ "extends": ["local>DimitriGilbert"] }` (+ `$schema`). If the org preset doesn't exist yet, use a minimal self-contained config (timezone, `schedule: ["before 6am on monday"]`, lockFileMaintenance) with a comment to converge onto the preset — note this in the PR description.
5. Cosmetic P3 while touching configs: fix `tsconfig.json` `include` entry `"vite.config.js"` → `"vite.config.ts"` (stale reference).

**Validation gate (validator)**
- `grep -c '"latest"' package.json` → `0`.
- `docker compose run --rm app pnpm install --frozen-lockfile` succeeds; `pnpm-lock.yaml` diff shows only specifier changes (no version jumps).
- Standard gate chain green. Validator reads the `package.json`/`renovate.json`/`Dockerfile` diffs and confirms NO-SLOP and no unrelated changes.

### Phase 1 — Vitest skeleton + pure-logic seeds (effort: M)

**Goal**: a one-command, hermetic test entry point exists, and the deliberately-pure modules (`models.ts`, `utils.ts`) are covered — the first tests ever committed here.

**Tasks (implementer)**
1. `docker compose run --rm app pnpm add -D vitest@^4 @vitest/coverage-v8@^4` (esbuild is already in `allowBuilds` in `pnpm-workspace.yaml`).
2. Create `vitest.config.ts`: `environment: 'node'`, `globals: false` (explicit imports from `vitest`), `include: ['src/**/*.test.{ts,tsx}']`, **no `passWithNoTests`**; `coverage` block: provider `v8`, reporters `['text', 'json', 'html']`, `include: ['src/**']`, `exclude: ['src/routeTree.gen.ts', 'src/components/ui/**', '**/*.test.*']` (bake generated/vendored exclusions in now — fleet skew risk, D2).
3. Add scripts to `package.json`: `"test": "vitest run"`, `"test:watch": "vitest"`, `"test:coverage": "vitest run --coverage"`.
4. Create `src/lib/models.test.ts` — full behavior of `isTaskModel`/`isCloningModel`: `task`-field match for both STT and TTS; fallback regex heuristics (`whisper|parakeet|distil|canary|voxrex` vs `kokoro|piper|xtts|speech|chatterbox`) when `task` is absent; case-insensitivity; negative cases.
5. Create `src/lib/utils.test.ts` — `cn()` conditional classes + tailwind-merge conflict resolution.
6. Add an **AGENTS.md "Testing" section** matching reality: runner (vitest), commands in sandbox form, colocated `*.test.ts(x)` locations, coverage status + measured baseline (from `pnpm test:coverage` text report), and the D7 statement "the headless local verification command (`pnpm verify`) is the quality gate, run by the owner/agents; hooks are local convenience."

**Validation gate (validator)**
- Standard gate chain green **including `pnpm test`** and `pnpm test:coverage` (coverage text report shows >0% and includes only non-excluded sources).
- Validator reads both test files: every branch of `isTaskModel` exercised, assertions are behavioral (no tautologies), NO-SLOP enforced. Validator confirms `vitest.config.ts` has no `passWithNoTests` and correct exclusions, and that the AGENTS.md Testing section matches what was actually built.

### Phase 2 — Mocked-fetch contract tests for the Speaches clients (effort: M)

**Goal**: `src/lib/speaches.ts` (server client) and `src/lib/client.ts` (browser helpers) are pinned to the OpenAI-compatible contract — these double as the tripwire against drift in the sibling Python backend.

**Tasks (implementer)**
1. Create `src/lib/speaches.test.ts`, node env, `vi.stubGlobal('fetch', …)` + `vi.unstubAllGlobals()` in `afterEach`. The env seam: `speachesBaseUrl` is computed at module load in `#/env`, so use `vi.stubEnv('SPEACHES_URL', 'http://speaches.test:8000/')` + `vi.resetModules()` + dynamic `await import('#/lib/speaches')` (also exercises the trailing-slash strip). Cases:
- `getHealth`: ok → `true`; non-ok → `false`; fetch throws → `false`.
- `listModels`: GET hits `${base}/v1/models`, parses `{object:'list', data:[…]}`; non-ok → throws `SpeachesError` with `status`/`body` and message truncated at 200 chars.
- `transcribe`: POST multipart contains `file` (with filename), `model`, `response_format=json`, extra opts appended; non-ok → `SpeachesError`.
- `createVoice`: form fields `file` + `name`.
- `synthesize`: JSON body defaults (`response_format: 'mp3'`, `speed: 1`) and overrides; `content-type` header fallback `audio/mpeg`; blob bytes round-trip.
- `realtimeWsUrl`: `http→ws` / `https→wss` rewrite, `model` + `intent=transcription` + optional `language` query params.
2. Create `src/lib/client.test.ts`: mocked fetch against relative `/api/*` URLs — `fetchHealth` parses the body even on 502; `fetchModels`/`postTranscribe`/`registerVoice` error paths prefer the server `error` field with status fallback; `postSpeak` returns `bytes`/`contentType`/object URL; `fetchRealtimeUrl` builds the query string. (Stay in `node`; only if a browser-only API is genuinely required, use a per-file `// @vitest-environment jsdom` pragma — do not flip the default environment.)
3. No production code changes expected in this phase; if a test forces a refactor, it must be behavior-preserving and flagged to the validator.

**Validation gate (validator)**
- Standard gate chain green; every exported function of both modules has at least one happy-path and one error-path assertion.
- Validator reads both test files in full: fetch stubs assert method + URL + body shape (not just status), no network access anywhere, NO-SLOP. Cross-checks asserted request shapes against `AGENTS.md` "Backend (Speaches) facts".

### Phase 3 — Verify entrypoint + coverage baseline/threshold (effort: S)

**Goal**: nothing lands untested: one headless command gates lint + format + typecheck + tests + build locally; coverage carries a committed, ratcheting threshold.

**Tasks (implementer)**
1. Add root script `"verify": "pnpm lint && pnpm check && pnpm check-types && pnpm test:coverage && pnpm build"` — the headless verification entrypoint (D5; from Phase 3 on the chain runs the coverage tier so thresholds gate).
2. Set the first threshold in `vitest.config.ts`: `coverage.thresholds.lines` (and `branches`) = **measured baseline − 5** (read the number off the Phase 2 coverage report). Update the AGENTS.md Testing section with the baseline and the ratchet rule (+5 when green, never lower).
3. Coverage outputs stay local and git-ignored; no artifact uploads, no workflow files (D5).

**Validation gate (validator)**
- `pnpm run verify` (inside the sandbox) exits 0 with every sub-command existing in `package.json`; the implementer/validator agents record the green output before the phase is declared done (D5). This is DoD item 2.
- Validator confirms the threshold equals measured−5, AGENTS.md records it, and no workflow file exists anywhere.

### Phase 4 — Lint hardening & import hygiene (effort: S)

**Goal**: the disabled hygiene rules come back on, with violations fixed rather than silenced.

**Tasks (implementer)**
1. Edit `eslint.config.js`: **delete** the `'import/no-cycle': 'off'` and `'import/order': 'off'` lines (synthesis D3 names this repo explicitly for this fix). Keep `'sort-imports': 'off'` with a comment "superseded by import/order". Keep `'pnpm/json-enforce-catalog': 'off'` with a comment "single package — no catalog (see TEST-ALIGNMENT-PLAN.md deviation 3)". Enable `'@typescript-eslint/array-type'` (currently off for no stated reason) if it doesn't open a large diff; otherwise defer with a comment.
2. Fix every violation the re-enabled rules surface (~619 LOC of hand-written code; expected small). **Never** re-disable a rule to make lint pass.
3. Optional (P3, same PR only if the diff stays small): enable `noUncheckedIndexedAccess` in `tsconfig.json` and fix the fallout.
4. If `@dg/config`'s eslint preset has published by now, converge `eslint.config.js` onto it (keeping this repo's overrides); otherwise leave a one-line TODO-free note in the plan follow-ups — no placeholder comments in code.

**Validation gate (validator)**
- `pnpm lint` green with `import/no-cycle` and `import/order` verified absent from the `off` list; standard gate chain green.
- Validator greps `eslint.config.js` to confirm no hygiene rule is silenced without an explanatory comment, and re-runs `pnpm check-types`.

### Phase 5 — Proxy route-handler tests (effort: M)

**Goal**: the 6 server routes in `src/routes/api/` (the security/confinement boundary of this app) have integration tests covering status codes, validation, and error mapping.

**Tasks (implementer)**
1. Make each handler testable with a minimal, behavior-preserving refactor: export the handler function alongside `createFileRoute` (e.g. `export async function healthHandler()` in `src/routes/api/health.ts`; same pattern for the other 5). Route declarations unchanged.
2. **Router-generator safety**: colocate tests per D1 (`src/routes/api/health.test.ts`, …), and add `"routeFileIgnorePattern": "\\.test\\.(ts|tsx)$"` to `tsr.config.json`; run `docker compose run --rm app pnpm generate-routes` and confirm `src/routeTree.gen.ts` is unchanged (no phantom routes from test files).
3. Write one test file per route, mocking at the `#/lib/speaches` boundary (`vi.mock`) so the *route* is the subject: `health` → 200 when ok / **502 when down**; `models` → list passthrough + voice attachment; `speak` → JSON body validation errors; `transcribe` → multipart handling; `voices` → **400 (not 500) on non-multipart** (regression test for commit `58e7d1b`); `realtime-url` → query-param forwarding. Node 22 `Request`/`Response` globals suffice.
4. Re-read the coverage report; if the ratchet is due (+5 when green, per AGENTS.md), raise the threshold in the same PR.

**Validation gate (validator)**
- Standard gate chain green; all 6 routes covered; `routeTree.gen.ts` diff empty after `generate-routes`.
- Validator reads all handler refactors (must be export-only, zero logic changes) and every route test (asserts status codes and error bodies, not just "does not throw").

### Phase 6 — Hook & component tests: the jsdom tier (effort: M)

**Goal**: the React layer is covered where it carries logic — the polling hook, the model/voice derivation, and the health badge — with jsdom confined to exactly this tier (D1 environment policy).

**Tasks (implementer)**
1. `docker compose run --rm app pnpm add -D jsdom @testing-library/react @testing-library/user-event @testing-library/dom` (these deps now exist *and are used* — DoD item 6).
2. Config: add a jsdom tier without flipping the default — either a `projects` split in `vitest.config.ts` (`*.test.tsx` → jsdom, `*.test.ts` → node) or per-file `// @vitest-environment jsdom` pragmas in the new `.test.tsx` files. Pick one, document it in AGENTS.md.
3. Tests (minimal, highest-value):
- `src/hooks/use-speaches.test.tsx`: `useHealth` polling/select semantics (wrapped in a fresh `QueryClientProvider`, `client.ts` mocked — `{ok:false}` is a valid state, not an error); `useModels` stt/tts/all split via `isTaskModel`, `staleTime`/`retry` config.
- `src/components/health-badge.test.tsx`: ok / down render paths.
- `src/components/model-select.test.tsx`: filtering by task and per-model `voices[]` derivation.
4. Update the AGENTS.md Testing section (component tier + environment split).

**Validation gate (validator)**
- Standard gate chain green; validator confirms `node` is still the default environment and jsdom applies only to the component tier; tests use RTL queries (no `querySelector` string-bashing), cleanups run (RTL auto-cleanup intact), NO-SLOP.

### Phase 7 — Quality metrics (report-only) + close-out (effort: M; optional e2e: L)

**Goal**: the fleet-standard report-only metric pipeline exists; every DoD item is verified closed.

**Tasks (implementer)**
1. `docker compose run --rm app pnpm add -D knip jscpd`. Add `knip.json` (entry: `src/routes/**`, `src/router.tsx`; ignore `src/components/ui/**`, `src/routeTree.gen.ts`) and a jscpd block with the launch-mommy canonical config (`minLines: 8, `minTokens: 80`, exclude `**/*.test.*`, `src/components/ui/**`, `src/routeTree.gen.ts`, **`exitCode: 0`** — report-only until a baseline exists, per D4). Scripts: `"quality:knip": "knip"`, `"quality:jscpd": "jscpd"`.
2. The two local scripts are the deliverable, run on demand by the owner/agents (or during a periodic agent pass); no workflow, no schedule (D5). Nothing here may gate a merge — no repo gets gated on a number it hasn't seen.
3. **Optional e2e (explicitly deferred)**: Playwright over the three README journeys (Transcribe, Speak, Live WS) against the compose stack or a stubbed backend — an on-demand local tier only, never part of the default gate (D5 §5). Attempt only if Phases 0–6 are green and someone owns the compose-based fixture.
4. Close-out pass: walk the DoD table below item by item; update AGENTS.md Testing section to final state; confirm no `"latest"` anywhere, no `passWithNoTests`, no disabled-without-comment rules.

**Validation gate (validator)**
- `docker compose run --rm app pnpm quality:knip` and `pnpm quality:jscpd` both run and produce reports with exit 0; standard gate chain green.
- Validator re-verifies the full DoD checklist against the repo state (not against this document's claims) and reads the knip/jscpd configs for forbidden `exitCode` non-zero gates on unbaselined metrics.

**Dependency notes**: Phases are sequential (0 → 7). Phase 3 depends on Phase 1 (coverage needs tests to exist). Phase 5 depends on Phase 2's mocking conventions. The remaining org-level items (Renovate preset, `@dg/config`) are fleet tracks — each phase names its fallback so this repo never blocks on another repo.

---

## 4. Alignment Definition of Done — current status per item

| # | DoD item (synthesis §3) | Status today (2026-09-05) | Closed by |
|---|---|---|---|
| 1 | Root-level headless test command exists, documented in AGENTS.md | **Missing** — no `test` script at all | Phase 1 |
| 2 | Headless verification entrypoint running typecheck + test + lint in one local command; agent records green before a phase is done | **Missing** — no root aggregate command | Phase 3 |
| 3 | Coverage measured & reported; baseline recorded in repo; thresholds only per D2; never lowered | **Missing** — no coverage anywhere | Phases 1–3 (state 3 → measured−5 threshold) |
| 4 | Language-appropriate lint installed, zero errors, wired into the headless entrypoint; dead lint task implemented or deleted | **Partial** — ESLint 9 + Prettier 3 installed and runnable in sandbox, but not wired into any aggregate gate, and `import/no-cycle` + `import/order` explicitly off (no turbo here, so no dangling lint task) | Phases 3–4 |
| 5 | AGENTS.md "Testing" section matches reality | **Missing** — no Testing section (docs only reference the Python backend's pytest conventions) | Phase 1 |
| 6 | Dead scaffold test deps removed or actually used | **Pass** (vacuously) — no RTL/jsdom dead weight exists; Phase 6 adds RTL/jsdom *and uses them* | Phase 6 (keeps it true) |
| 7 | Renovate one-liner present; no `"latest"` deps | **Fail** — 9 `"latest"` specs (audit said 11; reality 9), no Renovate | Phase 0 |
| 8 | Quality-report script (knip + jscpd, CRAP where coverage exists) runnable locally, report-only | **Missing** | Phase 7 |
| 9 | Nothing satisfied by a fake pass (no `passWithNoTests`, no echo tests, no tautologies) | **Pass** vacuously (nothing exists); standing NO-SLOP rule keeps it true | All phases |

## 5. Risks & repo-specific notes

- **Docker sandbox is the execution boundary.** Every validation gate in this plan is expressed as `docker compose run --rm app …`; implementers/validators must never run `pnpm`/`tsc`/`vite` on the host (AGENTS.md MANDATORY rule). The persistent pnpm-store volume makes repeated sandbox runs cheap. The Dockerfile's `corepack prepare pnpm@latest` is itself a floating pin — Phase 0 fixes it via the `packageManager` field. Under the no-CI decision there is no runner exception: everything, gates included, runs inside the sandbox.
- **Floating `"latest"` pins are the top hygiene risk.** Reproducibility currently rests solely on `pnpm-lock.yaml`; with no Renovate, breakage surfaces only at the next manual build. Renovate cannot manage `"latest"` specs, so replacement (Phase 0) must precede Renovate adoption. Note the TanStack packages are version-skewed among themselves (router 1.170.18 vs router-devtools 1.167.0 vs react-start 1.168.33) — after pinning, expect Renovate's first update PRs to be non-trivial; the full sandbox gate chain is the safety net. Audit/synthesis say 11 pins; the real count is 9 (deviation 1).
- **Coupling to the Python backend** (`/home/didi/workspace/speaches`, OpenAI-compatible STT/TTS). `src/lib/speaches.ts` is a 1:1 wrapper over its endpoints; `AGENTS.md` "Backend (Speaches) facts" is the shared spec. The Phase 2 contract tests are deliberately shaped as drift tripwires for the backend fork work planned in `docs/CHATTERBOX-ORCHESTRATION.md` — if the backend changes a contract, these tests fail first, locally. All tests must be hermetic: stubbed fetch, `vi.stubEnv` for `SPEACHES_URL`, no dependency on a running Speaches server, `.env`, or the network.
- **`speachesBaseUrl` is baked at module load** (`src/env.ts` computes it from `process.env` at import time). Tests handle this via `vi.stubEnv` + `vi.resetModules()` + dynamic import rather than refactoring production code; if that proves brittle, the fallback is an optional `baseUrl` parameter with the env value as default (behavior-preserving, flagged to the validator).
- **Realtime WS (`/v1/realtime`) is only unit-testable at URL-building level** (`realtimeWsUrl`). True streaming behavior is covered solely by the optional dispatch-only Playwright tier (Phase 7) — do not pretend otherwise in test names or docs.
- **TanStack Start server-route API is evolving** (`createFileRoute(...).server.handlers`). Phase 5's direct handler tests are coupled to this shape; an upstream signature change is a legitimate tripwire, but validators should treat "test broke because the framework API changed" differently from "test broke because behavior regressed".
- **Colocated route tests vs the router generator**: files under `src/routes/` are route candidates; Phase 5 must set `routeFileIgnorePattern` in `tsr.config.json` and prove `routeTree.gen.ts` is unchanged, or phantom routes will ship.
- **No hooks, on purpose** (D7): the local gate commands run by the owner/agents are the only authoritative gate; AGENTS.md says so. A repo with no hooks and a green local `pnpm verify` is compliant.

---

*End of plan. This document is plan-only — no code, configs, or tests have been created or modified in the repository by this initiative.*

Contributor guide

No contributing guide indexed for this repository

Research direction

Read AGENTS.md and the phased plan, then inspect package.json, pnpm-lock.yaml, Dockerfile, src/lib/models.ts, and src/lib/utils.ts. Start by running the existing sandbox checks, then implement and validate the planned Vitest tests, coverage, dependency hygiene, and local verification entrypoint. Done means the documented commands and tests pass without CI.

Written by the indexing model from the issue text.

Assessment

Tech stack
docker, typescript
Domain
developer-experience, testing, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.