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:
@@ -263,6 +263,16 @@ fn format_expectation(e: &crate::dsl::walker::outcome::Expectation) -> String {
|
|||||||
use crate::dsl::grammar::IdentSource;
|
use crate::dsl::grammar::IdentSource;
|
||||||
use crate::dsl::walker::outcome::Expectation;
|
use crate::dsl::walker::outcome::Expectation;
|
||||||
match e {
|
match e {
|
||||||
|
// ADR-0042 G1: the bare `1` that opens `add 1:n
|
||||||
|
// relationship …` is the project's only `Literal("1")`
|
||||||
|
// (grammar `ddl.rs`); on its own in an expected-set it is
|
||||||
|
// cryptic — a learner cannot know it begins a
|
||||||
|
// relationship. Render it as the named construct in error
|
||||||
|
// wording. This is render-only: completion/hints read the
|
||||||
|
// raw `Expectation::Literal("1")` directly (offering the
|
||||||
|
// literal `1` to type), so the candidate surface is
|
||||||
|
// unchanged.
|
||||||
|
Expectation::Literal("1") => "`1:n relationship`".to_string(),
|
||||||
Expectation::Word(w) | Expectation::Literal(w) => format!("`{w}`"),
|
Expectation::Word(w) | Expectation::Literal(w) => format!("`{w}`"),
|
||||||
Expectation::Ident { source, .. } => match source {
|
Expectation::Ident { source, .. } => match source {
|
||||||
// Match `IdentSlot::expected_label` outputs so the
|
// Match `IdentSlot::expected_label` outputs so the
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
|||||||
use rdbms_playground::action::Action;
|
use rdbms_playground::action::Action;
|
||||||
use rdbms_playground::app::{App, OutputKind};
|
use rdbms_playground::app::{App, OutputKind};
|
||||||
use rdbms_playground::event::AppEvent;
|
use rdbms_playground::event::AppEvent;
|
||||||
|
use rdbms_playground::mode::Mode;
|
||||||
|
|
||||||
const fn key(code: KeyCode) -> AppEvent {
|
const fn key(code: KeyCode) -> AppEvent {
|
||||||
AppEvent::Key(KeyEvent {
|
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]
|
#[test]
|
||||||
fn create_alone_renders_create_table_usage() {
|
fn create_alone_renders_create_table_usage() {
|
||||||
let lines = error_lines_for("create");
|
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() {
|
fn add_alone_renders_both_add_family_usages() {
|
||||||
let lines = error_lines_for("add");
|
let lines = error_lines_for("add");
|
||||||
let dump_msg = dump("add", &lines);
|
let dump_msg = dump("add", &lines);
|
||||||
// Aggregation across `choice` (ADR-0020): the structural
|
// Aggregation across the top-level choice: the structural
|
||||||
// error line lists both add-family entries.
|
// 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!(
|
assert!(
|
||||||
lines.iter().any(|l| {
|
lines.iter().any(|l| {
|
||||||
l.starts_with("parse error")
|
l.starts_with("parse error")
|
||||||
&& l.contains("`1`")
|
&& l.contains("`1:n relationship`")
|
||||||
&& l.contains("`column`")
|
&& 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.
|
// Usage block (ADR-0021): both add-* templates surface.
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
Reference in New Issue
Block a user