test: H1a near-miss matrix + friendlier add 1:n relationship label (ADR-0042)

Start the ADR-0042 §1 near-miss matrix: a table-driven
near_miss_matrix_simple_mode locking 26 simple-mode parse-error
renderings (every DSL entry word's bare/missing-clause cases plus the
mode arg-errors and the "this is SQL" rail), and an #[ignore]
baseline_dump capturing the full rendering for ongoing triage.

G1 fix: the bare `1` cardinality literal that opens `add 1:n
relationship …` rendered cryptically in expected-sets. Render it as
`1:n relationship` in error wording only (format_expectation) —
completion/hints still read the raw Expectation::Literal("1"), so the
candidate surface is unchanged. Updated the one anchor test.

Full suite green (lib 1578 / it 382 / typing_surface_matrix 192).
This commit is contained in:
claude@clouddev1
2026-06-05 08:04:24 +00:00
parent 0e6f767848
commit 10f8c2a95c
2 changed files with 161 additions and 4 deletions
+151 -4
View File
@@ -13,6 +13,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use rdbms_playground::action::Action;
use rdbms_playground::app::{App, OutputKind};
use rdbms_playground::event::AppEvent;
use rdbms_playground::mode::Mode;
const fn key(code: KeyCode) -> AppEvent {
AppEvent::Key(KeyEvent {
@@ -59,6 +60,149 @@ fn dump(input: &str, lines: &[String]) -> String {
)
}
/// TEMP baseline-capture (ADR-0042 §1 step 1). Lenient: does not
/// assert pass/fail — just dumps every output line so we can read
/// the current rendering before writing assertions. Run with:
/// cargo test -p ... --test it baseline_dump -- --nocapture --ignored
/// Removed once the matrix assertions land.
#[test]
#[ignore = "baseline capture only; run with --ignored --nocapture"]
fn baseline_dump() {
// (input, advanced?) — salient near-misses across entry words.
let cases: &[(&str, bool)] = &[
// --- app-lifecycle (simple) ---
("quit now", false),
("import", false),
("mode sideways", false),
("messages louder", false),
("copy everything", false),
("save sideways", false),
// --- DDL bare + missing-slot (simple) ---
("create", false),
("create table", false),
("create table T", false),
("drop", false),
("drop table", false),
("add", false),
("add column", false),
("rename", false),
("rename column", false),
("change", false),
("change column", false),
// --- data bare + missing-clause (simple) ---
("show", false),
("show data", false),
("insert", false),
("insert into", false),
("insert into T", false),
("insert into T ('Oli')", false),
("update", false),
("update T", false),
("update T set x=1", false),
("delete", false),
("delete from", false),
("delete from T", false),
("replay", false),
("explain", false),
// --- advanced-only entry words ---
("select", true),
("select *", true),
("select * from", true),
("with", true),
("alter", true),
("alter table T", true),
// advanced-only word typed in SIMPLE mode → "this is SQL" hint
("alter table T add column c int", false),
("select * from T", false),
// --- advanced SQL variants + genuine gaps ---
("insert into T", true),
("update T", true),
("delete from", true),
("create", true),
("create table", true),
("create index", true),
("drop", true),
("drop index", true),
];
for (input, advanced) in cases {
let mut app = App::new();
if *advanced {
app.mode = Mode::Advanced;
}
type_str(&mut app, input);
let actions = submit(&mut app);
let mode = if *advanced { "ADV" } else { "SIM" };
eprintln!("\n=== [{mode}] {input:?} → actions: {actions:?}");
for l in &app.output {
eprintln!(" [{:?}] {}", l.kind, l.text);
}
}
}
/// The simple-mode near-miss matrix (ADR-0042 §1). Each row is a
/// near-correct input plus substrings that MUST appear across its
/// rendered error lines — the structural "name the missing
/// keyword/clause" message and the per-command usage template.
/// These are the cases triaged as already-good; the baseline dump
/// (`baseline_dump`, #[ignore]) shows the full rendering. Cases
/// flagged for wording fixes (bare `add` `1`, bare `select`
/// first-set, mode-blind usage) are deliberately NOT locked here
/// until their fixes land.
#[test]
fn near_miss_matrix_simple_mode() {
// (input, required-substrings-across-error-lines)
let matrix: &[(&str, &[&str])] = &[
// app-lifecycle arg errors
("quit now", &["after `quit`, expected end of input", " quit"]),
("save sideways", &["after `save`, expected end of input", "save | save as"]),
("mode sideways", &["unknown mode 'sideways'", "mode simple | mode advanced"]),
("messages louder", &["unknown messages mode 'louder'", "messages short"]),
("copy everything", &["unknown copy target 'everything'", "copy all"]),
// DDL bare + missing-slot
("create", &["after `create`, expected `table`", "create table <Name> with pk"]),
("create table", &["after `create table`, expected identifier", "create table <Name> with pk"]),
("create table T", &["with pk", "create table <Name> with pk"]),
// G1: relationship cardinality reads as the named construct.
("add", &["after `add`, expected `column`, `1:n relationship`", "add 1:n relationship"]),
("drop table", &["after `drop table`, expected table name", "drop table <Name>"]),
("add column", &["after `add column`, expected table name", "add column [to] [table]"]),
("rename", &["after `rename`, expected `column`", "rename column [in] [table]"]),
("rename column", &["after `rename column`, expected table name", "rename column [in] [table]"]),
("change", &["after `change`, expected `column`", "change column [in] [table]"]),
("change column", &["after `change column`, expected table name", "change column [in] [table]"]),
// data bare + missing-clause
("insert", &["after `insert`, expected `into`", "insert into <Table>"]),
("insert into", &["after `insert into`, expected table name", "insert into <Table>"]),
("insert into T", &["after `insert into T`, expected `values` or `(`", "insert into <Table>"]),
("update", &["after `update`, expected table name", "update <Table> set"]),
("update T", &["after `update T`, expected `set`", "update <Table> set"]),
("update T set x=1", &["expected `where` or `--all-rows`", "update <Table> set"]),
("delete", &["after `delete`, expected `from`", "delete from <Table>"]),
("delete from", &["after `delete from`, expected table name", "delete from <Table>"]),
("delete from T", &["expected `where` or `--all-rows`", "delete from <Table>"]),
("replay", &["after `replay`, expected string literal or path", "replay <path>"]),
("explain", &["after `explain`, expected `show`, `update`, or `delete`", "explain show data"]),
// advanced-only entry word typed in simple mode → "this is SQL" rail
("select * from T", &["`select` is SQL", "mode advanced"]),
("alter table T add column c int", &["`alter` is SQL", "mode advanced"]),
];
for (input, needles) in matrix {
let lines = error_lines_for(input);
let dump_msg = dump(input, &lines);
assert!(
lines.iter().any(|l| l.starts_with("parse error")),
"missing `parse error` line for {input:?}\n{dump_msg}",
);
let joined = lines.join("\n");
for needle in *needles {
assert!(
joined.contains(needle),
"near-miss {input:?} missing expected substring {needle:?}\n{dump_msg}",
);
}
}
}
#[test]
fn create_alone_renders_create_table_usage() {
let lines = error_lines_for("create");
@@ -81,15 +225,18 @@ fn create_alone_renders_create_table_usage() {
fn add_alone_renders_both_add_family_usages() {
let lines = error_lines_for("add");
let dump_msg = dump("add", &lines);
// Aggregation across `choice` (ADR-0020): the structural
// error line lists both add-family entries.
// Aggregation across the top-level choice: the structural
// error line lists every add-family branch. ADR-0042 G1: the
// relationship branch renders as the friendly `1:n
// relationship` rather than the cryptic bare `1` cardinality
// literal.
assert!(
lines.iter().any(|l| {
l.starts_with("parse error")
&& l.contains("`1`")
&& l.contains("`1:n relationship`")
&& l.contains("`column`")
}),
"expected aggregated `1` or `column` in structural error\n{dump_msg}",
"expected aggregated `1:n relationship` and `column` in structural error\n{dump_msg}",
);
// Usage block (ADR-0021): both add-* templates surface.
assert!(