DimitriGilbert / DimitriGilbert/LiteChat

Test & quality alignment plan

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

Description

# LiteChat — Test & Quality Alignment Plan

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

- **Project**: LiteChat (`/home/didi/workspace/LiteChat`, origin `DimitriGilbert/LiteChat`)
- **Date**: 2026-09-05
- **Generated by the fleet test-alignment initiative** (28-repo audit + synthesis)
- **Inputs**: per-project audit [`../.test-alignment-audit/reports/LiteChat.md`](../.test-alignment-audit/reports/LiteChat.md) · fleet decisions [`../.test-alignment-audit/synthesis-decisions.md`](../.test-alignment-audit/synthesis-decisions.md) (relative links resolve inside `/home/didi/workspace`; the audit tree is not committed to this repo)
- **Repo shape**: single npm package (not pnpm, not Better-T-Stack) — React 19 + Vite 7 + TS 5.7 + Tailwind v4 + Zustand/Dexie, ~99.8k LOC client-side AI chat, 0 tests at HEAD
- **Baseline commit**: `bd3e2ee` (tree clean at plan time)

---

## 0. Reality vs synthesis (deviations, stated up front)

Per the initiative rules, where the synthesis conflicts with this repo's reality, reality wins and the deviation is recorded here:

1. **npm, not pnpm.** All dependency commands below use `npm install -D` / `npm ci`. There is no `pnpm-workspace.yaml`, no `catalog:`, no turbo — so the fleet-wide "dangling turbo `lint` task" cleanup item is **N/A** here. With CI removed fleet-wide (2026-09-05 owner decision) there is no workflow to shape-match either: the gate is local npm commands. Do not migrate LiteChat to pnpm (synthesis §5 explicitly keeps it npm).
2. **No `"latest"` deps found.** The synthesis delta for LiteChat says "pin the `latest`-style float"; a grep of `package.json` for `latest`/`git+`/`file:`/`github:` finds nothing (checked 2026-09-05). `zustand` is exact-pinned at `5.0.3`; everything else is ordinary caret ranges. DoD item 7's "no `latest` deps" is already satisfied; only the Renovate one-liner is missing. The real hygiene bug is the stray runtime dep `add` (see Phase 0).
3. **Vitest is on the v3 line (`^3.2.4`), not the fleet's v4 line.** The v3 config, scripts, `@vitest/ui`, and `jsdom` all survive and are v3-shaped. Following D1's own upgrade logic ("upgrade at the next natural touchpoint, not by decree"), this plan keeps v3 for the revival and defers a v4 bump to a later touchpoint (Renovate will propose it).
4. **Single package, so the LearnABee multi-project vitest factory pattern does not apply.** One inline `test` block in `vite.config.ts` stays (extracting a standalone `vitest.config.ts` is optional polish, not required).
5. **`jsdom` here is justified.** D1's "node by default" policy does not apply: LiteChat is a React/Radix SPA and the revived tests are hook/component tests. This is exactly D1's "jsdom only where React/DOM is actually under test" case.

---

## 1. Adopted decisions (D1–D7, adapted)

| Decision | Adoption in LiteChat |
|---|---|
| **D1 — runner & layout** | Vitest stays (v3 for now, see deviation 3). Layout: new tests **colocated** with source as `*.test.ts(x)` (the deleted suite's `src/test/` dir is *not* recreated except `src/test/setup.ts`, which survives as the setup file — `tsconfig.app.json:30–33` already excludes `src/test` and `**/*.test.*` from the app build). `test` script becomes headless `vitest run`; watch mode moves to `test:watch`. No `passWithNoTests` anywhere (it does not exist today; keep it that way — DoD 9). |
| **D2 — coverage** | Un-comment the coverage block at `vite.config.ts:177–183`, install `@vitest/coverage-v8`, reporters `text + json + html`, `include: ['src/**/*.{ts,tsx}']`, keep the existing excludes (`src/main.tsx`, `src/vite-env.d.ts`, `src/test/setup.ts`, `src/lib/db.ts`, `src/**/*.d.ts`, `src/components/ui/**`). **State 3 (zero-test repo): report-only, no thresholds** until seeds land; after Phase 3 record the measured baseline in AGENTS.md and set `thresholds.lines` at baseline − 5, ratcheting +5 whenever green. Never lower a threshold. |
| **D3 — lint & format** | ESLint 9 flat config stays; upgrade `eslint.config.js` from `tseslint.configs.recommended` to **`recommendedTypeChecked`** (with `parserOptions.project` / project-service against `tsconfig.app.json` + `tsconfig.node.json`) and **remove `"@typescript-eslint/no-explicit-any": "off"`** (line 26) — staged with a ratchet (Phase 4) because this is a 99.8k-LOC codebase where the rule has been off since inception. Add **Prettier 3** (no formatter exists today). Keep `eslint-plugin-react-hooks`/`react-refresh`. Ignores updated for `dist`, coverage output, `src/components/formedible/**` (generated/externalized per `tsconfig.app.json`). |
| **D4 — quality metrics** | knip + jscpd, **report-only** (`exitCode`-safe scripts, no gates), added in Phase 5. CRAP is deferred: it consumes `coverage-final.json`, so it becomes possible only after Phase 3's seeds and can ride the local quality scripts later. No mutation testing (D4 scopes Stryker to five other cores; LiteChat is not one). |
| **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 an npm-input coordination)" plan is dropped entirely. Enforcement = the root-level headless verification entrypoint (`npm run verify`: typecheck → lint → test:run → build, composed in Phase 2 with typecheck-first ordering) run by the owner and by agents; the plan's phase validation gates are run by implementer/validator agents before any phase is declared done. Coverage output stays a local report — no artifact uploads. A `typecheck` script is added to `package.json` (today `tsc --noEmit` is buried inside `build`, line 8). |
| **D6 — shared config** | Opportunistic only: this plan touches lint/vitest/tsconfig anyway, so if `@dg/config` is published by the time Phase 1/4 runs, the ESLint preset and vitest factory are consumed from `@dg/config` instead of hand-rolled; otherwise repo-local configs are written to converge later. Never a config-churn PR just for its own sake. |
| **D7 — hooks** | No hooks. The local gate commands run by the owner/agents are the only authoritative gate. AGENT.md/CLAUDE.md will state: "the headless local verification command (`npm run verify`) is the quality gate, run by the owner/agents; hooks are local convenience." A repo with no hooks and a green local `npm run verify` is compliant. No lefthook. |

---

## 2. Alignment Definition of Done — per-item status for this repo

Status as of 2026-09-05 at `bd3e2ee`:

| # | DoD item | Status today | Closed by |
|---|---|---|---|
| 1 | Root-level headless test command, documented in AGENTS.md | **Missing** — `test` runs `vitest` in watch mode (`package.json:14`); AGENT.md has one line (`AGENT.md:183`) | Phase 0 + 5 |
| 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 exists | Phase 2 |
| 3 | Coverage measured & reported; baseline recorded; thresholds only per D2; never lowered | **Missing** — coverage block fully commented out (`vite.config.ts:177–183`); `@vitest/coverage-v8` not installed | Phase 1 (wiring), Phase 3 (baseline) |
| 4 | Lint installed, zero errors, wired into the headless entrypoint; dead lint task implemented or deleted | **Partial** — ESLint 9 flat config exists and passes, but recommended-only, `no-explicit-any: "off"` (`eslint.config.js:26`), not in any aggregate gate. (No turbo dangling-lint task here — N/A.) | Phase 2 (wiring), Phase 4 (harden) |
| 5 | AGENTS.md "Testing" section matches reality — no phantom docs | **Partial** — `AGENT.md:183` one line ("Use Vitest with jsdom"); `CLAUDE.md` has zero test mentions; `docs/llm/llm.testing-debugging.txt` documents real event-driven seams and is accurate | Phase 5 |
| 6 | Dead scaffold test deps removed **or actually used** | **Partial** — `jsdom` + `@vitest/ui` survive unused (they will be used again by the revival); `@testing-library/react`/`jest-dom` were deleted and come back *with tests*; stray runtime dep `add ^2.0.6` (`package.json:65`) must be removed | Phase 0, Phase 1 |
| 7 | Renovate one-liner; no `"latest"` deps | **Partial** — no `renovate.json`; **no `latest` deps exist** (deviation 2) | Phase 2 |
| 8 | Quality-report script (knip + jscpd, + CRAP where coverage exists), runnable locally, report-only | **Missing** | Phase 5 |
| 9 | No fake pass (no `passWithNoTests`, no echo-test, no tautologies) | **OK** — no fake mechanisms present; the empty-suite state means `vitest run` currently exits non-zero, which is honest. Guard: never add `passWithNoTests`. | Ongoing guard |

---

## 3. Execution model (subagent orchestration)

Execution follows the `subagent-orchestration` skill: an orchestrator dispatches subagents and never writes code itself. Every phase below lists its role assignments. Rules baked into every dispatch:

- **Implementer** receives the COMPLETE phase requirements from this plan, the file list, boundaries, and the NO-SLOP policy verbatim; it must run the phase's gatekeeping commands and fix all errors **before** reporting done.
- **Validator** (always a different agent) must **actually read** the created/modified files, verify each requirement line-by-line, enforce NO-SLOP, and run the validation gate commands. Running commands alone is not validation.
- **Fixer** receives ALL validator findings at once and fixes them in one pass, then the validator re-validates. Up to 3 fix attempts per phase/sub-phase, then halt and report.
- **NO-SLOP policy** (verbatim in every implementer/fixer dispatch, enforced by every validator): no `any`, `as any`, or `: any`; no placeholder code, no `TODO`/`FIXME`; no unused imports or variables; no console.log hacks; `import type` for type-only imports; external imports first, blank line, then local imports; no dev server started; production quality only.
- **Phase sizing**: each phase fits one agent context (~15 files / ~500 new lines). Phase 3 and Phase 4 are split into sub-phases with per-sub-phase validation plus a phase-wide validator (Rule 3 of the skill).
- **Hard constraints for all subagents on this repo**: npm only (never pnpm/yarn); no commits, no pushes, no dev servers; tests colocated; never add `passWithNoTests`; never silence a lint/type error — fix the cause.

---

## 4. Phases

### Phase 0 — Hygiene: stray dep, orphan scripts, headless test entry

**Goal**: remove the accidental dependency and make the test entry point headless and honest, before any test code exists.
**Effort**: S · **Roles**: 1 implementer → 1 validator (fix loop ≤ 3).

**Tasks**
1. Remove the stray runtime dep `add` (accidental `npm add` artifact): `npm uninstall add`. Verify `package.json` dependencies and `package-lock.json` no longer reference it.
2. Rewrite the test scripts in `package.json`:
- `"test": "vitest run"` (headless, root-level, one command — DoD 1). Drop the `DEBUG_PRINT_LIMIT=10000` env prefix (it existed for the deleted suite's debug output; if kept, justify in the PR description — default is to drop).
- `"test:watch": "vitest"` (watch mode preserved for humans).
- `"test:ui"` may stay as `vitest --ui` (uses the already-present `@vitest/ui`).
- Leave `"coverage"` as is until Phase 1 un-comments the block.
3. Add `"typecheck": "tsc --noEmit --skipLibCheck"` to `package.json` scripts (mirrors the gate currently buried in `build`, line 8) so the gate can run it standalone.
4. Do **not** remove `jsdom`/`@vitest/ui` (Phase 1 uses them) and do **not** touch the `tsconfig.app.json` test excludes (they are correct for the returning colocated tests) and do **not** remove the `src/test/setup.ts` ignore in `eslint.config.js:8` (the file returns in Phase 1).

**Validation gate** (validator runs all)
- `npm run lint` → exit 0
- `npm run typecheck` → exit 0
- `npm run build` → exit 0 (proves the script edits broke nothing)
- `grep -n '"add"' package.json` → no matches; `grep -c '"add"' package-lock.json` → 0
- `package.json` has `"test": "vitest run"` and `"test:watch"`
- Note (expected, non-blocking): `npm run test:run` exits non-zero today because no test files exist — that is the honest state DoD 9 requires; it turns green in Phase 1.

---

### Phase 1 — Test infrastructure revival (restore vitest + RTL, un-comment coverage)

**Goal**: make `npm run test:run` real again: revive the setup file from git history (read-only `git show`, not a checkout), restore RTL deps, enable coverage reporting.
**Effort**: S · **Roles**: 1 implementer → 1 validator (fix loop ≤ 3).

**Context (mandatory reading for the implementer)**
- The deleted suite: commit `d443fea` "[vibe] full rebuild" removed 7 files from `src/test/` (~1,337 lines, ~24 `it()` cases: 4 hook tests, 2 component tests incl. a committed `.bak`, and `setup.ts`). The tested hooks (`use-api-keys-management`, `use-chat-input`, `use-chat-settings`, `use-provider-model-selection`) **no longer exist**, and `@/lib/types` moved to `src/types/litechat/` — the old test files must **not** be restored wholesale.
- The recoverable assets are the **idiom and the shims**: `git show d443fea^:src/test/setup.ts` yields the Radix-compatible jsdom setup (`@testing-library/jest-dom/vitest`, RTL `configure`/`cleanup`, `MockPointerEvent`, `setPointerCapture`/`releasePointerCapture`/`hasPointerCapture`/`scrollIntoView` stubs, `matchMedia` mock). Recreate that file adapted to current code (type the `window.PointerEvent` assignment without `as any` — the deleted original used one; use a proper structural type or `typeof window.PointerEvent` cast via `Object.defineProperty`).

**Tasks**
1. `npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event @vitest/coverage-v8` (this also flips DoD 6: the jsdom/RTL deps go from dead to used).
2. Recreate `src/test/setup.ts` from the git-history idiom above. Keep it shim-only: no test fixtures, no network, no clock dependence.
3. Un-comment the coverage block in `vite.config.ts:177–183` and keep its existing `include`/`exclude` globs verbatim (they already exclude `src/main.tsx`, `src/vite-env.d.ts`, `src/test/setup.ts`, `src/lib/db.ts`, `**/*.d.ts`, `src/components/ui/**`). No thresholds (D2 state 3).
4. Confirm `/// ` stays at `vite.config.ts:1`.
5. Write exactly **one real seed test** as the harness boot proof — a colocated pure-function test for `src/lib/litechat/event-emitter.ts` (the mitt wrapper the whole event-first architecture rides on) at `src/lib/litechat/event-emitter.test.ts`: subscribe → emit → assert payload, unsubscribe → no delivery. Real code, real assertions; this is not a placeholder.
6. Do not restore any other deleted test file. Do not create `vitest.config.ts` (inline block stays, deviation 4).

**Validation gate**
- `npm run test:run` → exit 0, reports the `event-emitter` test passing (≥ 1 test, ≥ 2 assertions)
- `npm run coverage` → exit 0, prints a text coverage table and writes `coverage/` HTML+JSON
- `npm run typecheck` → exit 0; `npm run lint` → exit 0; `npm run build` → exit 0 (colocated `*.test.ts` must not enter the app build — proves the tsconfig excludes hold)
- `grep -n "passWithNoTests" vite.config.ts package.json` → no matches
- Validator reads `src/test/setup.ts` and `event-emitter.test.ts` in full: shims complete (PointerEvent, pointer capture, scrollIntoView, matchMedia), no `any`, no dead imports, assertions meaningful.

---

### Phase 2 — Local gate composition (`npm run verify`) + Renovate

**Goal**: nothing lands untested again: one headless command gates typecheck → lint → test → build locally, with typecheck-first ordering. Plus the Renovate one-liner.
**Effort**: S · **Roles**: 1 implementer → 1 validator (fix loop ≤ 3).

**Tasks**
1. Add `"verify": "npm run typecheck && npm run lint && npm run test:run && npm run build"` to `package.json` — the headless verification entrypoint (D5), preserving the typecheck-first ordering.
2. Note: `npm run build` self-contains its env (`export VITE_SYSTEM_PROMPT_FILE=system-prompt.txt`, `package.json:8`) — no extra secrets/env needed to run the gate locally.
3. Add `renovate.json` at repo root: `{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["local>DimitriGilbert"] }` (DoD 7; a Renovate preset is a non-workflow artifact, unaffected by the no-CI decision).
4. No workflow file is created anywhere; nothing depends on `DimitriGilbert/.github` (D5).

**Validation gate**
- `npm run verify` → exit 0 locally (the exact gate chain, from a clean install state: `npm ci` first); every sub-command exists in `package.json` scripts.
- Fail-fast proven once: the validator introduces a failing assertion in a scratch copy and confirms `npm run verify` exits non-zero at the test step — never on the real tree.
- The implementer/validator agents record the green output before the phase is declared done (D5); no workflow file exists.
- `renovate.json` parses (`node -e "JSON.parse(require('fs').readFileSync('renovate.json','utf8'))"` → no throw).

---

### Phase 3 — Seed tests for the event-first core (multi-sub-phase)

**Goal**: build the real safety net the 24 services and 17 stores never had, prioritizing pure logic, using the seams `docs/llm/llm.testing-debugging.txt` already documents (events, stores, modular extension points). Colocated `*.test.ts(x)` only.
**Effort**: L total (two M sub-phases) · **Roles**: parallel implementers A + B, one validator per sub-phase, then a mandatory phase-wide validator (skill Rule 3).

**Sub-phase 3a — pure lib functions (implementer A)**
Targets (all pure or near-pure, zero mock cost), colocated next to each source file:
- `src/lib/litechat/event-emitter.test.ts` — extend the Phase 1 seed to wildcard (`*`) handling and multiple subscribers
- `src/lib/litechat/prompt-util.test.ts`, `provider-helpers.test.ts`, `url-helpers.test.ts`, `file-manager-utils.test.ts`, `file-extensions.test.ts`, `tree-layout.test.ts`, `flow-parser.test.ts` (and `chart-parser.test.ts` if it fits the context budget)
Each file: state transitions and edge cases (empty input, malformed input, unicode), 2–3 assertions per case, mirroring the assertion density of the deleted suite (~2–3 `expect()` per `it()`). No snapshot spam.

**Sub-phase 3b — services, store, hook (implementer B)**
- `src/services/text-trigger-parser.service.test.ts` and `src/services/prompt-compilation.service.test.ts` — mock the event bus/store seams via `vi.mock` with `importOriginal` partial-mock spread, typed with `vi.MockedFunction` (the deleted suite's idiom, which the audit rated disciplined)
- `src/store/prompt.store.test.ts` (or `settings.store.test.ts`) — direct Zustand store actions → assert state; no React needed
- `src/hooks/litechat/useAsyncOperation.test.ts` — `renderHook` from `@testing-library/react` + fake timers for async states; this revives the hook-test genre of the deleted suite against a hook that actually exists today
- One Radix component smoke test (e.g. an existing `src/components/ui/*.tsx` render + one `@testing-library/user-event` interaction) proving the Phase 1 shims hold — the genre of the deleted `api-key-selector.test.tsx`
Explicitly **out of scope for seeds**: `src/lib/db.ts` / Dexie/IndexedDB-backed services (`persistence`, `sync`, `bulk-sync`, `startup-sync`) and `workflow.service.ts` (2,613 LOC) — they need a fake-indexeddb harness; note them as Phase-3-follow-up candidates, do not fake them with mocks-that-lie.

**Validation gates**
- Per sub-phase: `npm run test:run` → exit 0 with that sub-phase's new tests passing; `npm run typecheck` + `npm run lint` exit 0; validator reads every new test file, checks assertions are meaningful (no assertion-free smoke), mocks are typed, no `any`, imports ordered.
- Phase-wide validator: reads ALL Phase 3 test files together — checks for duplicate helpers (extract a `src/test/test-utils.ts` only if duplication actually appears), consistent mock idiom across files, no cross-test pollution (stores reset between tests), suite runtime sane. Then runs: `npm run test:run` (all green), `npm run coverage` → **record the measured `lines` % in the validator report** — that number is the D2 baseline.
- Post-phase bookkeeping (implementer of 4a picks it up): write the baseline into AGENTS.md (Phase 5) and optionally set `coverage.thresholds.lines = baseline − 5` in `vite.config.ts` (D2 state 2 transition, ratchet +5 when green).

---

### Phase 4 — Lint hardening: type-checked rules, kill `no-explicit-any: off`, Prettier (multi-sub-phase)

**Goal**: close the P2 "permissive lint" gap without a week of red. Staged, ratcheted, never silenced.
**Effort**: M–L total · **Roles**: 4a implementer + validator; 4b implementer (fix-loop style, may repeat) + validator; phase-wide validator at the end.

**Sub-phase 4a — config upgrade + baseline**
1. `npm install -D prettier` (Prettier 3). Add `.prettierrc.json` (minimal baseline) and `.prettierignore` (`dist`, `coverage`, `package-lock.json`, `src/components/formedible/**`). Scripts: `"format": "prettier --write ."`, `"format:check": "prettier --check ."`.
2. Upgrade `eslint.config.js`: `tseslint.configs.recommendedTypeChecked` with `parserOptions.project` covering `tsconfig.app.json` + `tsconfig.node.json` (or `projectService: true`), keep react-hooks/react-refresh, ignore `dist`, `coverage`, `src/components/formedible/**`, `tailwind.config.ts`.
3. Flip `"@typescript-eslint/no-explicit-any"` from `"off"` to `"error"` **and measure**: run `npm run lint`, capture the error count. If the count is large (expected — the rule has been off for the project's life), keep the repo green by scoping the rule to `"warn"` **temporarily** with `--max-warnings=` enforcement in the `lint` script; 4b then ratchets the max-warnings down to 0, at which point the rule becomes `"error"` outright. **Never** add file-level `eslint-disable` sweeps or an ignore list to fake the pass; a disabled gate is worse than none. Record the measured numbers in the sub-phase report.
4. Run `npm run format` once (single formatting commit when the user approves the push) and fix any resulting type/lint fallout.
5. Add `format:check` to the verify chain after `lint` (update the `verify` script).

**Sub-phase 4b — ratchet to zero**
6. Fix `no-explicit-any` findings in batches (per skill fix-loop rules: all findings of a batch at once, gatekeeping commands before done, validator re-checks and lowers `--max-warnings`). Repeat until `npm run lint` exits 0 with the rule at `"error"`. If genuine third-party-typing holes surface, the fix is a proper type or `unknown` + narrowing — not `any`.

**Validation gates**
- `npm run lint` → exit 0 **with** `recommendedTypeChecked` active and `no-explicit-any` at `error` (validator inspects `eslint.config.js` to confirm, and greps for `eslint-disable` additions — none allowed beyond pre-existing ones)
- `npm run format:check` → exit 0
- `npm run typecheck`, `npm run test:run`, `npm run build` → all exit 0
- `verify` script includes the `format:check` step
- Phase-wide validator: confirms no type-safety regressions smuggled in via casts (`grep -rn "as any" src/` → no new hits vs. baseline `bd3e2ee`)

---

### Phase 5 — Docs truthfulness + report-only quality metrics

**Goal**: make the documentation match the revived reality and add the report-only metric layer (DoD 1, 5, 8).
**Effort**: S · **Roles**: 1 implementer → 1 validator (fix loop ≤ 3).

**Tasks**
1. `npm install -D knip jscpd`; add report-only scripts (never gates): `"quality:knip": "knip"`, `"quality:jscpd": "jscpd --min-lines 8 --min-tokens 80 --reporters consoleFull --exitCode 0 src"` with a `.jscpd.json` excluding `**/*.test.*`, `coverage/**`, `src/components/formedible/**`, `src/components/ui/**` (launch-mommy canonical config, adapted; `exitCode: 0` stays until a baseline exists — D4).
2. Add `knip.json` with `entry` patterns for `src/main.tsx`, `vite.config.ts`, `src/test/setup.ts`, and the `index.html` script entry so knip doesn't drown in false positives; report-only tolerance for first-run noise.
3. Rewrite the Testing section of `AGENT.md` (line 183 area): runner (vitest v3, jsdom), commands (`npm test` headless / `npm run test:watch` / `npm run coverage` / `npm run verify`), colocated layout, coverage baseline + current threshold, the one-line history note ("prior suite deleted in `d443fea`; revived 2026 against current hooks/services"), and the D7 gate statement ("the headless local verification command (`npm run verify`) is the quality gate, run by the owner/agents; hooks are local convenience").
4. Add the same Testing summary to `CLAUDE.md` (currently zero test mentions).
5. Update `docs/llm/llm.testing-debugging.txt` only if the seed tests revealed seams it misdescribes — otherwise leave it (the audit rated it accurate).

**Validation gate**
- `npm run quality:knip` and `npm run quality:jscpd` → exit 0 (report-only, output captured in the validator report)
- `npm run test:run`, `npm run typecheck`, `npm run lint`, `npm run format:check`, `npm run build` → all exit 0 (full local DoD sweep)
- Validator reads `AGENT.md`/`CLAUDE.md` Testing sections and cross-checks every claim against `package.json`/`vite.config.ts` — no phantom docs (DoD 5)
- If the D2 threshold was set after Phase 3: confirm `coverage.thresholds` in `vite.config.ts` equals the recorded baseline − 5 and that a deliberate `npm run coverage` run passes it

---

## 5. Risks & repo-specific notes

1. **The deleted suite is a revival, not a restoration.** `git show d443fea^:src/test/*` is reference material for idiom (typed `importOriginal` partial mocks, `vi.MockedFunction` casts, Radix shims), but every tested hook was itself deleted in the same commit and `@/lib/types` moved to `src/types/litechat/`. Restoring the old files wholesale produces a red suite on arrival. Phase 1's gate forbids it.
2. **npm shape.** No fleet CI plumbing applies anymore (D5): the gate is local npm commands and nothing depends on org-repo workflows. Likewise `@dg/config` (D6) is adopted only if/when published — the plan never blocks on an artifact that doesn't exist yet.
3. **`recommendedTypeChecked` + `no-explicit-any` on 99.8k LOC is the biggest-spread risk in this plan.** The rule has been off since inception (`eslint.config.js:26`), so the finding count is unknown and possibly large. Phase 4's max-warnings ratchet exists precisely so the gate never goes red-and-ignored. If 4b stalls past 3 fix loops, the honest stopping point is a documented, non-zero `--max-warnings` ceiling that only ever decreases — not a silenced rule.
4. **Empty-suite honesty.** Between Phase 0 and Phase 1, `npm test` fails with "no test files found". That is correct (DoD 9). Resist any temptation to add `passWithNoTests` or a placeholder passing test in the gap.
5. **Dexie/IndexedDB and PWA code stay untested in v1 seeds.** Mocking IndexedDB would produce tests that assert our mocks, not the app (the cropcircle failure mode). A `fake-indexeddb` harness for `persistence`/`sync` services is the natural Phase-3 follow-up, deliberately out of scope here.
6. **All gate proof is local (D5).** No pushes are needed or permitted; the implementer/validator agents run `npm run verify` and record the green output — that is the DoD-2 evidence.
7. **No `"latest"` deps (deviation 2)** — but the ~90 runtime deps are caret-floated with no automated updates; Renovate in Phase 2 is what keeps the range drift auditable. `zustand` is exact-pinned at `5.0.3`; leave that as-is.
8. **Build script embeds typecheck** (`package.json:8`). The new standalone `typecheck` script must stay flag-identical (`--noEmit --skipLibCheck`) to the build's inner call, or the gate and build can disagree.

## 6. Out of scope (explicit non-goals)

- Migrating to pnpm, BTS, or workspaces (synthesis §5: LiteChat stays npm).
- Vitest v3 → v4 bump (next natural touchpoint; Renovate will propose).
- E2E/Playwright (no e2e today; not a D5 default for this repo).
- Lefthook/any git hooks (D7).
- Mutation testing, CRAP gating, jscpd gating (D4: report-only; Stryker not scoped to this repo).
- Testing Dexie-backed services via fake-indexeddb (follow-up candidate, not this plan).

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with package.json, vite.config.ts, eslint.config.js, tsconfig.app.json, AGENT.md, and the existing src/test/setup.ts. Run the current npm lint, typecheck, build, and test commands to establish the baseline, then follow the phases in order. Done means the local npm run verify gate, tests, coverage and report-only quality scripts match the plan without fake passing mechanisms.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, tailwindcss, typescript, vite
Domain
build-system, 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.