Skip to content

test: strict CREATE TABLE, demo schema pins, grid message coverage (#50) - #51

Merged
DDecoene merged 1 commit into
release/v1.2.0from
feature/50-test-hardening
Jul 9, 2026
Merged

test: strict CREATE TABLE, demo schema pins, grid message coverage (#50)#51
DDecoene merged 1 commit into
release/v1.2.0from
feature/50-test-hardening

Conversation

@DDecoene

@DDecoene DDecoene commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Closes #50.

Why

Two bugs shipped through a 283-test suite (both found while building #45). Neither was bad luck — the suite was structurally incapable of seeing them:

  • toContain proves presence, never absence. Nearly every assertion in the repo greps rendered text for a substring. A phantom column named 2 therefore passed every LIST / LIST STRUCTURE check, because the value each test looked for was still there.
  • Four of twelve ClientMessage types had zero tests. grid-edit wrote straight to SQLite with no validation and nobody noticed, because the grid tests only opened the grid, read some text, and pressed Escape.
  • Green CI ≠ correct. The cross-database ColumnMetaStore leak shipped in TIME column type #43 with seven passing tests and two green CI jobs. All seven used a single database.

What this does

Root cause: a strict CREATE TABLE grammar

parseCreate used to call ident() — "take the next token, whatever it is" — so a stray ) became a type and a stray , became a column name. That is precisely how NUM(8,2) produced a phantom column. It now rejects a malformed column list, names the offending column, and creates nothing:

. CREATE TABLE bad (a CHAR(10) b INT)
** Parse error: CREATE TABLE: expected ')' to close the column list of table 'BAD' (at 'B', line 1)

Covers missing comma, missing type, unclosed paren, unclosed qualifier, a non-numeric qualifier, and a third type argument. Every valid form still parses (bare table, no-qualifier types, CHAR(40), NUM(6), TIME(15), NUM(8,2), trailing comma).

The coverage those bugs needed

  • tests/DemoSchemas.test.ts — golden, exact column lists for all seven tables the demos create, verified both through the parser and against the real SQLite table, plus a guard that no column name is a bare number.
  • tests/GridMessages.test.ts — drives grid-edit, grid-delete, grid-new-row, grid-refresh and asserts the database effect.
  • tests/CreateTableParse.test.ts — the strict grammar, both directions.
  • npm run coverage (vitest + v8, reporting only, no thresholds). Currently ~57% statements; the browser UI is e2e-only.

Two more bugs, found by writing those tests

  1. Index metadata leaked across databases. indexes/active_indexes were keyed by table name alone, so USE PEOPLE in one database silently activated an index defined on a different database's PEOPLE — pointing the record order at a column that need not exist there, and breaking BROWSE/LIST. (This is the same defect I fixed in ColumnMetaStore in BROWSE per-cell validation hook #45; the issue listed it as out of scope, but it blocked the demo-schema pins, so it's fixed here.) Now keyed by (db, table, tag).

    Migration: index definitions aren't re-derivable (only INDEX ON creates them), so rather than discard them, each legacy row is adopted into the one database that actually owns a table of that name. Rows whose owner is ambiguous (same table name in two databases) or gone are dropped and must be recreated with INDEX ON. The underlying SQLite index objects are untouched. Covered by tests/IndexStoreMigration.test.ts, including the ambiguous case and idempotency.

  2. A bare INPUT "…" TO var at the REPL silently discarded the value. form-submit only stored the submitted values when a continuation existed — which never happens for a single statement. It worked inside a program only because a following statement happened to create one. Values a form collects are now always stored. I verified the new e2e catches it by reverting the fix: Expected "Ada", Received "".

Also

  • Removed the input-request / input-response WS message types — declared in the protocol, never sent or handled by anything. INPUT goes through form-open / form-submit.
  • Assistant parity: NUM(p,s) is a real qualifier now, so the New table wizard accepts a width (8) or a precision,scale pair (8,2), and rejects a scale ≥ precision. Driven by a new e2e that asserts the created table has exactly one column. The strict-parser change adds no command, so it needs no sidebar action; its error path is covered in the REPL instead.

Test plan

Suites run serially (they share data/).

  • npx tsc --noEmit clean.
  • npm test358/358 vitest (was 316), 25 files.
  • npx playwright test83/83 (was 79). New tests/schema-errors.spec.ts (3): malformed CREATE TABLE errors in the REPL and creates nothing; NUM(8,2) yields exactly three columns; a bare INPUT stores its value. Plus the NUM(p,s) wizard case in assistant.spec.ts.
  • Verified the new INPUT e2e fails against the unfixed code, so it isn't decorative.

Docs

CHANGELOG.md (Added / Fixed / Changed), README.md (strict CREATE TABLE note), CLAUDE.md — including a new Test discipline section recording what the suite could not see, so the next person doesn't rebuild the same blind spot.

Two bugs shipped through a 283-test suite. Both were structural blind
spots in how the tests were written, not bad luck:

- Nearly every assertion is `toContain` on rendered text, which can
  prove a thing is present but never that something extra is absent.
  A phantom column named "2" therefore sailed through every LIST and
  LIST STRUCTURE check.
- Four of twelve ClientMessage types had no test at all. grid-edit
  wrote straight to SQLite unvalidated because the grid tests only
  opened the grid and pressed Escape.

Root cause first: parseCreate used to absorb any token it did not
understand and invent a column from it. It now rejects a malformed
column list — missing comma, missing type, unclosed paren, a third
type argument — naming the offending column and creating nothing.

Then the coverage those bugs needed:

- DemoSchemas.test.ts pins the exact column list of every table the
  demos create, and asserts no column name is a bare number.
- GridMessages.test.ts drives grid-edit, grid-delete, grid-new-row and
  grid-refresh, asserting the effect on the database.
- CreateTableParse.test.ts covers the strict grammar both ways.

Writing those tests immediately found two more bugs, fixed here:

- Index metadata was keyed by table name alone, so opening PEOPLE in
  one database silently activated an index defined on a different
  database's PEOPLE — pointing record order at a column that need not
  exist there. Now keyed by (db, table, tag). Existing definitions are
  adopted into the database that owns the table; ambiguous ones are
  dropped and must be recreated with INDEX ON.
- A bare `INPUT "..." TO var` at the REPL discarded the typed value:
  form-submit only stored values when a continuation existed, which
  never happens for a single statement.

Also removes the input-request/input-response message types (declared
but never sent or handled), teaches the New table wizard to emit
NUM(p,s), and adds `npm run coverage` so untested modules stop hiding.
@DDecoene
DDecoene merged commit 39c13e0 into release/v1.2.0 Jul 9, 2026
2 checks passed
@DDecoene
DDecoene deleted the feature/50-test-hardening branch July 9, 2026 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant