Files
pad/web
xarmian b3ca3a9691 fix(web): vitest refuses a project it cannot run instead of dropping it (BUG-3045) (#1346)
* fix(web): vitest refuses a project it cannot run instead of dropping it (BUG-3045)

The web suite is three vitest projects. Two of them — `jsdom`, which owns every
`*.svelte.test.ts`, and `idb` — were registered only if their devDependencies
resolved, and simply OMITTED otherwise. So an incomplete install narrowed the
run instead of failing it: `npm test` exited 0 having executed none of the
rendered-component suites, and nothing in the output said so.

That is a gate that passes when it cannot run — the shape scripts/ci-audit.mjs
was taught to refuse in BUG-2881, where an unreachable advisory service was
indistinguishable from a clean audit.

Two halves, because narrowing has two causes and they deserve opposite answers:

- An unresolvable dependency now REFUSES the run. The check sits at module
  scope in vitest.config.ts, so it lands while the config is still loading:
  vitest reports a startup error and exits 1 rather than falling back to a
  default config. The message names every unrunnable project and every missing
  package, so one `npm ci` fixes the whole set.

- DELIBERATE narrowing is not refused — `--project` and file filters are
  legitimate — so a reporter prints how many of the registered projects
  actually ran files. A full run says `3/3 ran test files`; `--project node`
  says `1/1`; a single-file run says `1/3` and names the projects that ran
  nothing. A narrowed run no longer looks identical to a complete one.

No escape hatch. All five packages are declared devDependencies, and CI does a
full `npm ci`, so there is no legitimate call this refuses — only the state a
green run must not be reported from.

The decision and both messages live in src/test/vitestProjects.ts with direct
tests. Fifteen mutants, all killed: thirteen by their own test leg, two by the
config refusal itself (a mutant that breaks the decision for the REAL dependency
set stops the suite before any test runs). There is deliberately no test that the
package names resolve — it could never run, for exactly that reason.

Claude-Session: https://claude.ai/code/session_019EmDd3Wy8b3cPG1TZBSzBu

* fix(web): close the fall-through hole and prove the config uses the table (BUG-3045)

Codex round 1, enumeration-prompted. Three findings acted on, one refuted.

**A test file no project runs (finding 3).** The refusal covers a project that
cannot run; it did nothing about a file that no project's glob matches. A
`*.spec.ts`, `*.test.tsx` or `*.test.js` under src/ is never collected, never
fails, and the run exits 0 — this bug's own class, one layer down. Codex checked
the tree and found no member today, which is exactly when the guard is cheap:
the first one to appear would otherwise arrive silently. `projectForTestFile`
now answers which project owns a path, the three globs moved beside it so the
config and the guard read ONE declaration rather than two copies, and a test
walks src/ and names any orphan. It asserts its own precondition first — an
empty file list would make every later assertion vacuous, which is the failure
mode this whole bug is about.

**The tests vouched for functions, not for the wiring (finding 8, CONVE-19).**
Nothing would have noticed if vitest.config.ts had stopped calling any of this,
gone back to probing dependencies inline, or dropped a project. Four tests now
read the real config: it registers exactly node/idb/jsdom, that set equals
`node` plus the requirements table, it keeps vitest's default reporter beside
the project-count one, and each project routes with the shared globs.

**Prose that overstated the refusal's reach (findings 1, 2, 4, 6).** The
refusal is unconditional and precedes `--project`, so `vitest --project node` is
refused too when the jsdom deps are missing, even though node alone would have
run. Kept — an incomplete install is a broken state and stopping on it whatever
subset was asked for is the honest answer — but the comment claiming deliberate
narrowing is never refused was false in exactly the case the refusal fires, and
now says so. Also written down: the line counts FILES, so it does not prove an
assertion executed; an idle project is printed rather than failed; and
`--reporter=<name>` replaces configured reporters, so `npm test -- --reporter=dot`
prints no summary at all.

**Refuted by measurement (finding 5).** Codex read the CLI source and concluded
`context.projects` survives `--project` filtering, so a narrowed run would print
`1/3` rather than the documented `1/1`. Running it says otherwise, twice:
`--project node` prints `1/1 ran test files — node (70 files)` with no idle
line, and `--project jsdom` prints `0/1` naming only jsdom. Under the claimed
behaviour both would have carried the other two projects. No change made.

Seven more mutants on the new legs, all killed, for 22/22 across the unit. N5
(drop the default reporter) is worth its own note: it exited 1 but my runner
called it a runner error, because the mutation removes the very output lines the
classifier reads. Re-run through `--reporter=default` on the CLI, it dies by its
named test. Third instrument in this unit whose question depended on something
the mutant controlled — the tsc gate and the kill-door grep were the first two.

Claude-Session: https://claude.ai/code/session_019EmDd3Wy8b3cPG1TZBSzBu
2026-09-13 18:36:35 -04:00
..
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00
2026-03-26 01:52:36 +00:00

Pad Web UI

SvelteKit 2 + Svelte 5 frontend for Pad, compiled to static files and embedded into the Go binary.

Development

npm install
npm run dev          # Dev server at localhost:5173 (proxies API to localhost:7777)
npm run build        # Production build to build/
npm run check        # Type checking with svelte-check

When developing, run the Go backend separately with make dev from the project root.

Building for Production

Do not build in isolation. Always use make build from the project root — this builds the web frontend, then compiles the Go binary with the build output embedded via //go:embed.

Stack

  • Svelte 5 with runes ($state, $derived, $effect)
  • SvelteKit 2 with adapter-static (SPA mode)
  • Tiptap block editor with markdown round-trip
  • svelte-dnd-action for drag-and-drop in board/list views
  • SSE for real-time updates
  • TypeScript throughout

Structure

src/
  routes/                    SvelteKit pages
    +layout.svelte           App shell (sidebar + main)
    +page.svelte             Landing/redirect
    [workspace]/
      +page.svelte           Dashboard (collections, phases, activity)
      +layout.svelte         SSE connection per workspace
      [collection]/
        +page.svelte         Collection view (board/list)
      [collection]/[item]/
        +page.svelte         Item detail + editor
      conventions/            Purpose-built conventions page
      playbooks/              Purpose-built playbooks page
      settings/               Workspace settings
  lib/
    api/client.ts            HTTP API client
    components/
      layout/                Sidebar, navigation
      editor/                Tiptap editor, raw markdown editor
      fields/                FieldEditor, relation picker
      items/                 ItemCard, ItemDetail
      collections/           BoardView, ListView
      common/                StatusBadge, badges, modals
      search/                CommandPalette
    stores/                  Svelte 5 reactive stores
      workspace.svelte.ts    Workspace state
      collections.svelte.ts  Collection + item state
      ui.svelte.ts           Sidebar, mobile state
    types/index.ts           TypeScript types and constants
  app.css                    Global styles and design tokens