From 24e641bc21543fc9e84ced1a171b0be253e688d5 Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Fri, 15 May 2026 20:06:58 +0000 Subject: [PATCH] Matrix: typing-surface infrastructure + insert Form A coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per docs/handoff/20260515-handoff-12.md §1. Systematic per-position coverage of (state, hint, completion, parse_result) across canonical schema shapes; submodule per command family. Insert Form A covers 23 cursor positions across serial-PK, text-PK, multi-table, and every-Type schemas. Both bugs fixed in the previous commit were surfaced by these tests. Shared helpers under tests/typing_surface/mod.rs: 5 canonical schema shapes, assess() helper, property-assertion shortcuts, and a snap! macro that wraps insta with a stable per-cell suffix. 859 -> 885 tests passing; 1 ignored (pre-existing doc-test). --- tests/typing_surface/add_relationship.rs | 1 + tests/typing_surface/app_commands.rs | 1 + tests/typing_surface/create_table.rs | 1 + tests/typing_surface/delete_all_rows.rs | 1 + tests/typing_surface/delete_with_where.rs | 1 + tests/typing_surface/drop_column.rs | 1 + tests/typing_surface/drop_relationship.rs | 1 + tests/typing_surface/insert_form_a.rs | 348 +++++++++++++++ tests/typing_surface/insert_form_b.rs | 1 + tests/typing_surface/insert_form_c.rs | 1 + tests/typing_surface/mod.rs | 401 ++++++++++++++++++ tests/typing_surface/rename_change_column.rs | 1 + ...ects_values_keyword@after_close_paren.snap | 39 ++ ...ord_expects_into@after_insert_keyword.snap | 39 ++ ...space_expects_into@after_insert_space.snap | 39 ++ ...table_candidates@after_into_serial_pk.snap | 39 ++ ...le_offers_both@after_into_multi_table.snap | 47 ++ ...mns_only@after_open_paren_multi_table.snap | 49 +++ ...ing_serial@after_open_paren_serial_pk.snap | 53 +++ ...t_pk_columns@after_open_paren_text_pk.snap | 49 +++ ...alues_and_open_paren@after_table_name.snap | 47 ++ ...pects_open_paren@after_values_keyword.snap | 39 ++ ...te@after_values_open_paren_serial_col.snap | 33 ++ ...lumn@after_values_open_paren_text_col.snap | 33 ++ ..._mentions_true_false@form_a_bool_slot.snap | 41 ++ ..._to_insert@form_a_complete_two_values.snap | 19 + ...st_parses@form_a_complete_with_serial.snap | 19 + ...rose_says_yyyy_mm_dd@form_a_date_slot.snap | 33 ++ ...inite_error@form_a_in_progress_no_red.snap | 19 + ...complete@form_a_in_progress_one_value.snap | 19 + ...ot_prose_says_integer@form_a_int_slot.snap | 33 ++ ...other_tables@form_a_open_paren_orders.snap | 53 +++ ..._to_auto_generate@form_a_shortid_slot.snap | 33 ++ ...ng_columns@after_comma_in_column_list.snap | 53 +++ ...se@after_first_value_advance_to_email.snap | 33 ++ tests/typing_surface/update_all_rows.rs | 1 + tests/typing_surface/update_with_where.rs | 1 + tests/typing_surface_matrix.rs | 19 + 38 files changed, 1641 insertions(+) create mode 100644 tests/typing_surface/add_relationship.rs create mode 100644 tests/typing_surface/app_commands.rs create mode 100644 tests/typing_surface/create_table.rs create mode 100644 tests/typing_surface/delete_all_rows.rs create mode 100644 tests/typing_surface/delete_with_where.rs create mode 100644 tests/typing_surface/drop_column.rs create mode 100644 tests/typing_surface/drop_relationship.rs create mode 100644 tests/typing_surface/insert_form_a.rs create mode 100644 tests/typing_surface/insert_form_b.rs create mode 100644 tests/typing_surface/insert_form_c.rs create mode 100644 tests/typing_surface/mod.rs create mode 100644 tests/typing_surface/rename_change_column.rs create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_close_paren_expects_values_keyword@after_close_paren.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_insert_keyword_expects_into@after_insert_keyword.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_insert_space_expects_into@after_insert_space.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_into_offers_table_candidates@after_into_serial_pk.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_into_with_multi_table_offers_both@after_into_multi_table.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_offers_active_table_columns_only@after_open_paren_multi_table.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_serial_pk_offers_all_columns_including_serial@after_open_paren_serial_pk.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_text_pk_offers_text_pk_columns@after_open_paren_text_pk.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_table_name_offers_values_and_open_paren@after_table_name.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_values_keyword_expects_open_paren@after_values_keyword.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_values_open_paren_form_a_serial_column_offers_null_to_auto_generate@after_values_open_paren_serial_col.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_values_open_paren_form_a_text_column_prose_names_column@after_values_open_paren_text_col.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_bool_slot_prose_mentions_true_false@form_a_bool_slot.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_complete_parses_to_insert@form_a_complete_two_values.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_complete_with_serial_in_list_parses@form_a_complete_with_serial.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_date_slot_prose_says_yyyy_mm_dd@form_a_date_slot.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_in_progress_values_list_is_incomplete_not_definite_error@form_a_in_progress_no_red.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_in_progress_with_one_value_is_incomplete@form_a_in_progress_one_value.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_int_slot_prose_says_integer@form_a_int_slot.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_open_paren_no_leakage_from_other_tables@form_a_open_paren_orders.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_shortid_slot_prose_mentions_null_to_auto_generate@form_a_shortid_slot.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__mid_column_list_after_comma_offers_remaining_columns@after_comma_in_column_list.snap create mode 100644 tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__mid_value_list_after_comma_advances_to_next_column_prose@after_first_value_advance_to_email.snap create mode 100644 tests/typing_surface/update_all_rows.rs create mode 100644 tests/typing_surface/update_with_where.rs create mode 100644 tests/typing_surface_matrix.rs diff --git a/tests/typing_surface/add_relationship.rs b/tests/typing_surface/add_relationship.rs new file mode 100644 index 0000000..b2d5ff4 --- /dev/null +++ b/tests/typing_surface/add_relationship.rs @@ -0,0 +1 @@ +//! Submodule stub — populated in subsequent tasks. diff --git a/tests/typing_surface/app_commands.rs b/tests/typing_surface/app_commands.rs new file mode 100644 index 0000000..b2d5ff4 --- /dev/null +++ b/tests/typing_surface/app_commands.rs @@ -0,0 +1 @@ +//! Submodule stub — populated in subsequent tasks. diff --git a/tests/typing_surface/create_table.rs b/tests/typing_surface/create_table.rs new file mode 100644 index 0000000..b2d5ff4 --- /dev/null +++ b/tests/typing_surface/create_table.rs @@ -0,0 +1 @@ +//! Submodule stub — populated in subsequent tasks. diff --git a/tests/typing_surface/delete_all_rows.rs b/tests/typing_surface/delete_all_rows.rs new file mode 100644 index 0000000..b2d5ff4 --- /dev/null +++ b/tests/typing_surface/delete_all_rows.rs @@ -0,0 +1 @@ +//! Submodule stub — populated in subsequent tasks. diff --git a/tests/typing_surface/delete_with_where.rs b/tests/typing_surface/delete_with_where.rs new file mode 100644 index 0000000..b2d5ff4 --- /dev/null +++ b/tests/typing_surface/delete_with_where.rs @@ -0,0 +1 @@ +//! Submodule stub — populated in subsequent tasks. diff --git a/tests/typing_surface/drop_column.rs b/tests/typing_surface/drop_column.rs new file mode 100644 index 0000000..b2d5ff4 --- /dev/null +++ b/tests/typing_surface/drop_column.rs @@ -0,0 +1 @@ +//! Submodule stub — populated in subsequent tasks. diff --git a/tests/typing_surface/drop_relationship.rs b/tests/typing_surface/drop_relationship.rs new file mode 100644 index 0000000..b2d5ff4 --- /dev/null +++ b/tests/typing_surface/drop_relationship.rs @@ -0,0 +1 @@ +//! Submodule stub — populated in subsequent tasks. diff --git a/tests/typing_surface/insert_form_a.rs b/tests/typing_surface/insert_form_a.rs new file mode 100644 index 0000000..ce00ef2 --- /dev/null +++ b/tests/typing_surface/insert_form_a.rs @@ -0,0 +1,348 @@ +//! Matrix coverage for `insert into T (cols) values (vals)` +//! (Form A — explicit column list). +//! +//! Form A is the most complex insert form: the column list +//! `(cols)` constrains which values are required and in what +//! order, and the dispatch must mirror `do_insert`'s `user_cols` +//! contract (ADR-0018 §3, handoff-12 §B). Bugs unique to Form A +//! from handoff-12: +//! +//! - **E2**: `insert into T (` showed no column candidates +//! because the value-literal suppression masked the +//! `Ident{Columns}` expectation. Fixed in commit 619a8bd. +//! - **D2**: `insert into T (a, b, c) values (1, 2, 3` (no +//! closing paren) classified as `DefiniteErrorAt()` +//! because `walk_optional` was rolling back the partial value +//! list. Fixed in commit 5815918. +//! +//! Every meaningful cursor position is exercised against the +//! relevant schema shapes; per-cell snapshots pin behaviour +//! and explicit property assertions document the invariants +//! that matter most. + +use crate::typing_surface::*; +use rdbms_playground::input_render::InputState; + +// ========================================================= +// Cursor at the entry-word boundary. +// ========================================================= + +#[test] +fn after_insert_keyword_expects_into() { + let schema = schema_serial_pk(); + let a = assess_at_end("insert", &schema); + assert!(matches!(a.state, InputState::IncompleteAtEof)); + crate::snap!("after_insert_keyword", a); +} + +#[test] +fn after_insert_space_expects_into() { + let schema = schema_serial_pk(); + let a = assess_at_end("insert ", &schema); + assert!(matches!(a.state, InputState::IncompleteAtEof)); + assert_candidate_present(&a, &["into"]); + crate::snap!("after_insert_space", a); +} + +#[test] +fn after_into_offers_table_candidates() { + let schema = schema_serial_pk(); + let a = assess_at_end("insert into ", &schema); + assert!(matches!(a.state, InputState::IncompleteAtEof)); + assert_candidate_present(&a, &["Customers"]); + crate::snap!("after_into_serial_pk", a); +} + +#[test] +fn after_into_with_multi_table_offers_both() { + let schema = schema_multi_table(); + let a = assess_at_end("insert into ", &schema); + assert_candidate_present(&a, &["Customers", "Orders"]); + crate::snap!("after_into_multi_table", a); +} + +// ========================================================= +// Cursor immediately after the table-name boundary. +// +// At `insert into Customers ` the walker must offer two +// branches: `values` (Form B/A pivot) and `(` (Form A/C pivot). +// Handoff §D1 fixed this: the `(` punctuation now surfaces as +// a `CandidateKind::Punct` candidate. +// ========================================================= + +#[test] +fn after_table_name_offers_values_and_open_paren() { + let schema = schema_serial_pk(); + let a = assess_at_end("insert into Customers ", &schema); + assert!(matches!(a.state, InputState::IncompleteAtEof)); + assert_candidate_present(&a, &["values", "("]); + crate::snap!("after_table_name", a); +} + +// ========================================================= +// Form A path enters at the `(`. +// +// The §1 bug E2 was that `insert into T (` showed nothing — +// no column candidates surfaced. Reproduces here as a property +// assertion (must include at least one column from the table) +// and the inverse leakage check (must NOT include columns +// from sibling tables). +// ========================================================= + +#[test] +fn after_open_paren_offers_active_table_columns_only() { + let schema = schema_multi_table(); + let a = assess_at_end("insert into Customers (", &schema); + assert!(matches!(a.state, InputState::IncompleteAtEof)); + assert_candidate_present(&a, &["id", "Name"]); + assert_no_candidate_named(&a, &["OrderId", "CustId", "Total"]); + crate::snap!("after_open_paren_multi_table", a); +} + +#[test] +fn after_open_paren_serial_pk_offers_all_columns_including_serial() { + let schema = schema_serial_pk(); + let a = assess_at_end("insert into Customers (", &schema); + assert_candidate_present(&a, &["id", "Name", "Email"]); + crate::snap!("after_open_paren_serial_pk", a); +} + +#[test] +fn after_open_paren_text_pk_offers_text_pk_columns() { + let schema = schema_text_pk(); + let a = assess_at_end("insert into Items (", &schema); + assert_candidate_present(&a, &["Code", "Title"]); + crate::snap!("after_open_paren_text_pk", a); +} + +// ========================================================= +// Mid-column-list positions. +// ========================================================= + +#[test] +fn mid_column_list_after_comma_offers_remaining_columns() { + let schema = schema_serial_pk(); + let a = assess_at_end("insert into Customers (id, ", &schema); + assert!(matches!(a.state, InputState::IncompleteAtEof)); + assert_candidate_present(&a, &["Name", "Email"]); + crate::snap!("after_comma_in_column_list", a); +} + +#[test] +fn after_close_paren_expects_values_keyword() { + let schema = schema_serial_pk(); + let a = assess_at_end("insert into Customers (Name)", &schema); + assert!(matches!(a.state, InputState::IncompleteAtEof)); + assert_candidate_present(&a, &["values"]); + crate::snap!("after_close_paren", a); +} + +#[test] +fn after_values_keyword_expects_open_paren() { + let schema = schema_serial_pk(); + // Trailing space so we're past the `values` word boundary + // — without it the partial-prefix logic re-offers `values` + // itself as the candidate that matches the typed prefix. + let a = assess_at_end( + "insert into Customers (Name) values ", + &schema, + ); + assert!(matches!(a.state, InputState::IncompleteAtEof)); + assert_candidate_present(&a, &["("]); + crate::snap!("after_values_keyword", a); +} + +// ========================================================= +// Value-list positions — typed-slot prose. +// ========================================================= + +#[test] +fn after_values_open_paren_form_a_text_column_prose_names_column() { + let schema = schema_serial_pk(); + let a = assess_at_end( + "insert into Customers (Name) values (", + &schema, + ); + assert!( + hint_prose_contains(&a, "Name"), + "expected column name in prose, got {:?}", + a.hint, + ); + assert!( + hint_prose_contains(&a, "quoted string"), + "expected text-slot prose, got {:?}", + a.hint, + ); + crate::snap!("after_values_open_paren_text_col", a); +} + +#[test] +fn after_values_open_paren_form_a_serial_column_offers_null_to_auto_generate() { + let schema = schema_serial_pk(); + let a = assess_at_end( + "insert into Customers (id, Name) values (", + &schema, + ); + let prose = hint_prose(&a) + .unwrap_or_else(|| panic!("expected Prose hint, got {:?}", a.hint)); + assert!(prose.contains("id"), "prose should name `id`, got {prose:?}"); + assert!( + prose.contains("null") && prose.contains("auto-generate"), + "prose should mention `null` to auto-generate, got {prose:?}", + ); + crate::snap!("after_values_open_paren_serial_col", a); +} + +#[test] +fn mid_value_list_after_comma_advances_to_next_column_prose() { + let schema = schema_serial_pk(); + let a = assess_at_end( + "insert into Customers (Name, Email) values ('Alice', ", + &schema, + ); + let prose = hint_prose(&a) + .unwrap_or_else(|| panic!("expected Prose hint, got {:?}", a.hint)); + assert!( + prose.contains("Email"), + "prose should name `Email`, got {prose:?}", + ); + crate::snap!("after_first_value_advance_to_email", a); +} + +// ========================================================= +// In-progress Form A — regression for handoff §1 bug D2. +// +// `insert into Orders (...) values (..., ...` (no closing +// paren) must classify as IncompleteAtEof, not +// DefiniteErrorAt. +// ========================================================= + +#[test] +fn form_a_in_progress_values_list_is_incomplete_not_definite_error() { + let schema = schema_multi_table(); + let a = assess_at_end( + "insert into Orders (OrderId, CustId, Total) values (42, 89, 17.59", + &schema, + ); + assert!( + matches!(a.state, InputState::IncompleteAtEof), + "expected IncompleteAtEof (bug §1-D2 regression), got {:?}", + a.state, + ); + crate::snap!("form_a_in_progress_no_red", a); +} + +#[test] +fn form_a_in_progress_with_one_value_is_incomplete() { + let schema = schema_multi_table(); + let a = assess_at_end( + "insert into Orders (OrderId, CustId, Total) values (42", + &schema, + ); + assert!(matches!(a.state, InputState::IncompleteAtEof)); + crate::snap!("form_a_in_progress_one_value", a); +} + +// ========================================================= +// Form A: complete inputs parse to Insert. +// ========================================================= + +#[test] +fn form_a_complete_parses_to_insert() { + let schema = schema_serial_pk(); + let a = assess_at_end( + "insert into Customers (Name, Email) values ('Alice', 'a@b.c')", + &schema, + ); + assert!(matches!(a.state, InputState::Valid)); + assert_eq!(a.parse_result.as_deref(), Ok("Insert")); + crate::snap!("form_a_complete_two_values", a); +} + +#[test] +fn form_a_complete_with_serial_in_list_parses() { + let schema = schema_serial_pk(); + let a = assess_at_end( + "insert into Customers (id, Name, Email) values (1, 'Alice', 'a@b.c')", + &schema, + ); + assert!(matches!(a.state, InputState::Valid)); + assert_eq!(a.parse_result.as_deref(), Ok("Insert")); + crate::snap!("form_a_complete_with_serial", a); +} + +// ========================================================= +// Per-column-type prose across the Type axis (every_type +// schema). Each call to `Things.` value position +// exercises one Type variant's catalog prose. +// ========================================================= + +#[test] +fn form_a_int_slot_prose_says_integer() { + let schema = schema_every_type(); + let a = assess_at_end("insert into Things (k) values (", &schema); + let prose = hint_prose(&a).unwrap_or_else(|| { + panic!("expected Prose for int slot, got {:?}", a.hint) + }); + assert!( + prose.contains("integer"), + "int-slot prose should say `integer`, got {prose:?}", + ); + crate::snap!("form_a_int_slot", a); +} + +#[test] +fn form_a_date_slot_prose_says_yyyy_mm_dd() { + let schema = schema_every_type(); + let a = assess_at_end("insert into Things (dt) values (", &schema); + let prose = hint_prose(&a).unwrap_or_else(|| { + panic!("expected Prose for date slot, got {:?}", a.hint) + }); + assert!( + prose.contains("YYYY-MM-DD"), + "date-slot prose should reference YYYY-MM-DD format, got {prose:?}", + ); + crate::snap!("form_a_date_slot", a); +} + +#[test] +fn form_a_bool_slot_prose_mentions_true_false() { + let schema = schema_every_type(); + let a = assess_at_end("insert into Things (b) values (", &schema); + let prose = hint_prose(&a).unwrap_or_else(|| { + panic!("expected Prose for bool slot, got {:?}", a.hint) + }); + assert!( + prose.contains("true") && prose.contains("false"), + "bool-slot prose should mention `true`/`false`, got {prose:?}", + ); + crate::snap!("form_a_bool_slot", a); +} + +#[test] +fn form_a_shortid_slot_prose_mentions_null_to_auto_generate() { + let schema = schema_every_type(); + let a = assess_at_end("insert into Things (sid) values (", &schema); + let prose = hint_prose(&a).unwrap_or_else(|| { + panic!("expected Prose for shortid slot, got {:?}", a.hint) + }); + assert!( + prose.contains("null") && prose.contains("auto-generate"), + "shortid-slot prose should mention `null` to auto-generate, got {prose:?}", + ); + crate::snap!("form_a_shortid_slot", a); +} + +// ========================================================= +// Column-name leakage: Form A's column list must not offer +// columns from sibling tables in a multi-table schema. +// ========================================================= + +#[test] +fn form_a_open_paren_no_leakage_from_other_tables() { + let schema = schema_multi_table(); + let a = assess_at_end("insert into Orders (", &schema); + assert_no_candidate_named(&a, &["Name"]); + assert_candidate_present(&a, &["OrderId", "CustId", "Total"]); + crate::snap!("form_a_open_paren_orders", a); +} diff --git a/tests/typing_surface/insert_form_b.rs b/tests/typing_surface/insert_form_b.rs new file mode 100644 index 0000000..b2d5ff4 --- /dev/null +++ b/tests/typing_surface/insert_form_b.rs @@ -0,0 +1 @@ +//! Submodule stub — populated in subsequent tasks. diff --git a/tests/typing_surface/insert_form_c.rs b/tests/typing_surface/insert_form_c.rs new file mode 100644 index 0000000..b2d5ff4 --- /dev/null +++ b/tests/typing_surface/insert_form_c.rs @@ -0,0 +1 @@ +//! Submodule stub — populated in subsequent tasks. diff --git a/tests/typing_surface/mod.rs b/tests/typing_surface/mod.rs new file mode 100644 index 0000000..109b70f --- /dev/null +++ b/tests/typing_surface/mod.rs @@ -0,0 +1,401 @@ +//! Shared helpers + canonical schema shapes for the +//! typing-surface matrix. +//! +//! See `tests/typing_surface.rs` for the entry point and +//! `docs/handoff/20260515-handoff-12.md` §1 for the design +//! rationale. + +// Helpers below are `pub` so submodules can use them. The +// `unreachable_pub` lint trips because Cargo sees the test +// binary's crate root as the only consumer, but submodule +// access requires `pub`. `dead_code` similarly trips during +// incremental work when not every helper has callers yet. +#![allow(dead_code, unreachable_pub)] + +use rdbms_playground::completion::{ + Completion, SchemaCache, TableColumn, candidates_at_cursor, +}; +use rdbms_playground::dsl::parser::parse_command_with_schema; +use rdbms_playground::dsl::types::Type; +use rdbms_playground::input_render::{ + AmbientHint, InputState, ambient_hint, classify_input, +}; + +pub mod insert_form_a; +pub mod insert_form_b; +pub mod insert_form_c; +pub mod update_with_where; +pub mod update_all_rows; +pub mod delete_with_where; +pub mod delete_all_rows; +pub mod create_table; +pub mod drop_column; +pub mod drop_relationship; +pub mod add_relationship; +pub mod rename_change_column; +pub mod app_commands; + +// ========================================================= +// Canonical schema shapes (handoff §1 — CANONICAL_SCHEMA_SHAPES) +// ========================================================= +// +// Used in snapshot names so failing cases identify the schema. + +pub const SHAPE_EMPTY: &str = "empty"; +pub const SHAPE_SERIAL_PK: &str = "serial_pk"; +pub const SHAPE_TEXT_PK: &str = "text_pk"; +pub const SHAPE_MULTI_TABLE: &str = "multi_table"; +pub const SHAPE_EVERY_TYPE: &str = "every_type"; + +/// Empty schema — no tables at all. +pub fn schema_empty() -> SchemaCache { + SchemaCache::default() +} + +/// Single table with a `serial` PK + two text columns. +/// `Customers(id:serial, Name:text, Email:text)`. +/// +/// Exercises auto-gen-skip behaviour: in Form B `id` is omitted +/// from the value list; in Form A the user can list it explicitly. +pub fn schema_serial_pk() -> SchemaCache { + build_schema(&[( + "Customers", + &[ + ("id", Type::Serial), + ("Name", Type::Text), + ("Email", Type::Text), + ], + )]) +} + +/// Single table with a text PK and no auto-gen columns. +/// `Items(Code:text, Title:text)`. +/// +/// Exercises the no-auto-gen path: Form A and Form B both +/// require values for every column. +pub fn schema_text_pk() -> SchemaCache { + build_schema(&[( + "Items", + &[("Code", Type::Text), ("Title", Type::Text)], + )]) +} + +/// Two tables sharing no column names. +/// `Customers(id:serial, Name:text)` + +/// `Orders(OrderId:serial, CustId:int, Total:real)`. +/// +/// Exercises column-name-leakage detection: column candidates at +/// a position scoped to one table must not include columns from +/// the other table. +pub fn schema_multi_table() -> SchemaCache { + build_schema(&[ + ( + "Customers", + &[("id", Type::Serial), ("Name", Type::Text)], + ), + ( + "Orders", + &[ + ("OrderId", Type::Serial), + ("CustId", Type::Int), + ("Total", Type::Real), + ], + ), + ]) +} + +/// One table with one column of every `Type` variant. +/// Exercises per-type slot prose at every value position. +pub fn schema_every_type() -> SchemaCache { + build_schema(&[( + "Things", + &[ + ("k", Type::Int), + ("r", Type::Real), + ("d", Type::Decimal), + ("b", Type::Bool), + ("dt", Type::Date), + ("ts", Type::DateTime), + ("data", Type::Blob), + ("sid", Type::ShortId), + ("auto", Type::Serial), + ("note", Type::Text), + ], + )]) +} + +fn build_schema(tables: &[(&str, &[(&str, Type)])]) -> SchemaCache { + let mut cache = SchemaCache::default(); + for (table, cols) in tables { + let table_cols: Vec = cols + .iter() + .map(|(n, t)| TableColumn { + name: (*n).to_string(), + user_type: *t, + }) + .collect(); + cache.tables.push((*table).to_string()); + for c in &table_cols { + if !cache.columns.contains(&c.name) { + cache.columns.push(c.name.clone()); + } + } + cache.table_columns.insert((*table).to_string(), table_cols); + } + cache +} + +// ========================================================= +// Assessment helper — runs the four typing-surface APIs at a +// (input, cursor, schema) cell and packages the results for +// snapshot + property assertions. +// ========================================================= + +/// Compact, Debug-printable assessment of the typing surface +/// at one cell. +#[derive(Debug)] +pub struct Assessment { + pub input: String, + pub cursor: usize, + pub state: InputState, + pub hint: Option, + pub completion: Option, + /// Parse result rendered as `Ok()` or + /// `Err()` to keep snapshots stable as + /// the `Command` AST evolves. + pub parse_result: Result, +} + +/// Assess the typing surface at the given cell. +pub fn assess(input: &str, cursor: usize, schema: &SchemaCache) -> Assessment { + let state = classify_input(input); + let hint = ambient_hint(input, cursor, None, schema); + let completion = candidates_at_cursor(input, cursor, schema); + let parse_result = match parse_command_with_schema(input, schema) { + Ok(cmd) => Ok(command_kind_label(&cmd)), + Err(e) => Err(parse_error_label(&e)), + }; + Assessment { + input: input.into(), + cursor, + state, + hint, + completion, + parse_result, + } +} + +/// Convenience: assess at the end of input. +pub fn assess_at_end(input: &str, schema: &SchemaCache) -> Assessment { + assess(input, input.len(), schema) +} + +fn command_kind_label(cmd: &rdbms_playground::dsl::Command) -> String { + use rdbms_playground::dsl::AppCommand; + use rdbms_playground::dsl::Command::*; + match cmd { + CreateTable { .. } => "CreateTable".into(), + DropTable { .. } => "DropTable".into(), + AddColumn { .. } => "AddColumn".into(), + DropColumn { .. } => "DropColumn".into(), + RenameColumn { .. } => "RenameColumn".into(), + ChangeColumnType { .. } => "ChangeColumnType".into(), + AddRelationship { .. } => "AddRelationship".into(), + DropRelationship { .. } => "DropRelationship".into(), + ShowTable { .. } => "ShowTable".into(), + Insert { .. } => "Insert".into(), + Update { .. } => "Update".into(), + Delete { .. } => "Delete".into(), + ShowData { .. } => "ShowData".into(), + Replay { .. } => "Replay".into(), + App(app) => match app { + AppCommand::Quit => "App(Quit)".into(), + AppCommand::Help => "App(Help)".into(), + AppCommand::Rebuild => "App(Rebuild)".into(), + AppCommand::Save => "App(Save)".into(), + AppCommand::SaveAs => "App(SaveAs)".into(), + AppCommand::New => "App(New)".into(), + AppCommand::Load => "App(Load)".into(), + AppCommand::Export { .. } => "App(Export)".into(), + AppCommand::Import { .. } => "App(Import)".into(), + AppCommand::Mode { .. } => "App(Mode)".into(), + AppCommand::Messages { .. } => "App(Messages)".into(), + }, + } +} + +fn parse_error_label(err: &rdbms_playground::dsl::ParseError) -> String { + use rdbms_playground::dsl::ParseError::*; + match err { + Empty => "Empty".into(), + Invalid { at_eof, .. } => { + if *at_eof { + "Invalid(at_eof)".into() + } else { + "Invalid(definite)".into() + } + } + } +} + +// ========================================================= +// Property-assertion helpers. +// +// Inline tests in submodules combine these with insta +// snapshots — the snapshot detects drift; the assertions +// detect bug-class regressions (no column leakage, hint +// surfaces the right column, etc.). +// ========================================================= + +/// Returns the list of candidate texts from the ambient hint, +/// or an empty Vec if the hint isn't in Candidates mode. +pub fn hint_candidate_texts(a: &Assessment) -> Vec { + match &a.hint { + Some(AmbientHint::Candidates { items, .. }) => { + items.iter().map(|c| c.text.clone()).collect() + } + _ => Vec::new(), + } +} + +/// Returns the list of completion candidate texts (from the +/// `candidates_at_cursor` surface), or an empty Vec if there +/// is no completion. +pub fn completion_candidate_texts(a: &Assessment) -> Vec { + a.completion.as_ref().map_or_else(Vec::new, |c| { + c.candidates.iter().map(|c| c.text.clone()).collect() + }) +} + +/// True if the hint is Prose containing the given substring. +pub fn hint_prose_contains(a: &Assessment, needle: &str) -> bool { + matches!(&a.hint, Some(AmbientHint::Prose(p)) if p.contains(needle)) +} + +/// True if the hint is Prose; returns the prose string. +pub const fn hint_prose(a: &Assessment) -> Option<&str> { + match &a.hint { + Some(AmbientHint::Prose(p)) => Some(p.as_str()), + _ => None, + } +} + +/// Assert state matches the given variant (by Debug name). +pub fn assert_state(a: &Assessment, expected: &str) { + let actual = format!("{:?}", a.state); + // DefiniteErrorAt carries a position — compare prefix. + assert!( + actual.starts_with(expected), + "state: expected {expected}, got {actual} (input: {:?}, cursor: {})", + a.input, + a.cursor, + ); +} + +/// Assert that no candidate (either hint Candidates or +/// completion) contains any of the given names. Used to detect +/// column-name leakage from foreign tables. +pub fn assert_no_candidate_named(a: &Assessment, forbidden: &[&str]) { + let hint_cands = hint_candidate_texts(a); + let comp_cands = completion_candidate_texts(a); + for name in forbidden { + assert!( + !hint_cands.iter().any(|c| c == name), + "hint candidates contain forbidden {name:?}: {hint_cands:?} (input: {:?})", + a.input, + ); + assert!( + !comp_cands.iter().any(|c| c == name), + "completion candidates contain forbidden {name:?}: {comp_cands:?} (input: {:?})", + a.input, + ); + } +} + +/// Assert that at least one candidate matches each expected name. +pub fn assert_candidate_present(a: &Assessment, expected: &[&str]) { + let hint_cands = hint_candidate_texts(a); + let comp_cands = completion_candidate_texts(a); + let all: Vec<&String> = hint_cands.iter().chain(comp_cands.iter()).collect(); + for name in expected { + assert!( + all.iter().any(|c| *c == name), + "expected candidate {name:?} not present. hint: {hint_cands:?}, completion: {comp_cands:?} (input: {:?})", + a.input, + ); + } +} + +/// Snapshot helper — takes a stable name + the Assessment. +/// Wraps `insta::assert_debug_snapshot!` with a deterministic +/// snapshot suffix so each cell has its own file. +#[macro_export] +macro_rules! snap { + ($name:expr, $assessment:expr) => { + ::insta::with_settings!({ + snapshot_suffix => $name, + description => format!("input={:?} cursor={}", $assessment.input, $assessment.cursor), + }, { + ::insta::assert_debug_snapshot!(&$assessment); + }); + }; +} + +// ========================================================= +// Infrastructure smoke tests — verify the helper itself +// behaves before submodules rely on it. +// ========================================================= + +#[test] +fn smoke_each_schema_shape_builds() { + // Each canonical schema must produce a SchemaCache the + // walker can probe without panicking. Run a trivial + // assess() against each. + for (label, schema) in [ + (SHAPE_EMPTY, schema_empty()), + (SHAPE_SERIAL_PK, schema_serial_pk()), + (SHAPE_TEXT_PK, schema_text_pk()), + (SHAPE_MULTI_TABLE, schema_multi_table()), + (SHAPE_EVERY_TYPE, schema_every_type()), + ] { + let a = assess("insert", 6, &schema); + // `insert` alone is incomplete regardless of schema — + // a useful invariant to pin so any regression in the + // entry-word path surfaces immediately. + assert!( + matches!(a.state, InputState::IncompleteAtEof), + "{label}: expected IncompleteAtEof for 'insert', got {:?}", + a.state, + ); + } +} + +#[test] +fn smoke_assess_at_end_returns_each_field() { + let schema = schema_serial_pk(); + let a = assess_at_end("insert into Customers values (", &schema); + // The four surfaces are populated: + assert_eq!(a.input, "insert into Customers values ("); + assert_eq!(a.cursor, "insert into Customers values (".len()); + // The walker recognises this as incomplete (more tokens + // wanted), not a definite error. + assert!(matches!(a.state, InputState::IncompleteAtEof)); + // The hint surfaces per-column prose for the first + // non-auto-gen column (Name). + assert!( + hint_prose_contains(&a, "Name"), + "expected hint prose to mention `Name`, got {:?}", + a.hint, + ); +} + +#[test] +fn smoke_assess_parse_label_round_trips() { + let schema = schema_serial_pk(); + let a = assess_at_end( + "insert into Customers values ('Alice', 'a@b.c')", + &schema, + ); + assert_eq!(a.parse_result.as_deref(), Ok("Insert")); + assert!(matches!(a.state, InputState::Valid)); +} diff --git a/tests/typing_surface/rename_change_column.rs b/tests/typing_surface/rename_change_column.rs new file mode 100644 index 0000000..b2d5ff4 --- /dev/null +++ b/tests/typing_surface/rename_change_column.rs @@ -0,0 +1 @@ +//! Submodule stub — populated in subsequent tasks. diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_close_paren_expects_values_keyword@after_close_paren.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_close_paren_expects_values_keyword@after_close_paren.snap new file mode 100644 index 0000000..8198655 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_close_paren_expects_values_keyword@after_close_paren.snap @@ -0,0 +1,39 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Customers (Name)\" cursor=28" +expression: "& a" +--- +Assessment { + input: "insert into Customers (Name)", + cursor: 28, + state: IncompleteAtEof, + hint: Some( + Candidates { + items: [ + Candidate { + text: "values", + kind: Keyword, + }, + ], + selected: None, + }, + ), + completion: Some( + Completion { + replaced_range: ( + 28, + 28, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "values", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_insert_keyword_expects_into@after_insert_keyword.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_insert_keyword_expects_into@after_insert_keyword.snap new file mode 100644 index 0000000..e7d5e57 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_insert_keyword_expects_into@after_insert_keyword.snap @@ -0,0 +1,39 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert\" cursor=6" +expression: "& a" +--- +Assessment { + input: "insert", + cursor: 6, + state: IncompleteAtEof, + hint: Some( + Candidates { + items: [ + Candidate { + text: "insert", + kind: Keyword, + }, + ], + selected: None, + }, + ), + completion: Some( + Completion { + replaced_range: ( + 0, + 6, + ), + partial_prefix: "insert", + candidates: [ + Candidate { + text: "insert", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_insert_space_expects_into@after_insert_space.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_insert_space_expects_into@after_insert_space.snap new file mode 100644 index 0000000..90f257c --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_insert_space_expects_into@after_insert_space.snap @@ -0,0 +1,39 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert \" cursor=7" +expression: "& a" +--- +Assessment { + input: "insert ", + cursor: 7, + state: IncompleteAtEof, + hint: Some( + Candidates { + items: [ + Candidate { + text: "into", + kind: Keyword, + }, + ], + selected: None, + }, + ), + completion: Some( + Completion { + replaced_range: ( + 7, + 7, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "into", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_into_offers_table_candidates@after_into_serial_pk.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_into_offers_table_candidates@after_into_serial_pk.snap new file mode 100644 index 0000000..a73bfbb --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_into_offers_table_candidates@after_into_serial_pk.snap @@ -0,0 +1,39 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into \" cursor=12" +expression: "& a" +--- +Assessment { + input: "insert into ", + cursor: 12, + state: IncompleteAtEof, + hint: Some( + Candidates { + items: [ + Candidate { + text: "Customers", + kind: Identifier, + }, + ], + selected: None, + }, + ), + completion: Some( + Completion { + replaced_range: ( + 12, + 12, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "Customers", + kind: Identifier, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_into_with_multi_table_offers_both@after_into_multi_table.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_into_with_multi_table_offers_both@after_into_multi_table.snap new file mode 100644 index 0000000..fe49905 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_into_with_multi_table_offers_both@after_into_multi_table.snap @@ -0,0 +1,47 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into \" cursor=12" +expression: "& a" +--- +Assessment { + input: "insert into ", + cursor: 12, + state: IncompleteAtEof, + hint: Some( + Candidates { + items: [ + Candidate { + text: "Customers", + kind: Identifier, + }, + Candidate { + text: "Orders", + kind: Identifier, + }, + ], + selected: None, + }, + ), + completion: Some( + Completion { + replaced_range: ( + 12, + 12, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "Customers", + kind: Identifier, + }, + Candidate { + text: "Orders", + kind: Identifier, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_offers_active_table_columns_only@after_open_paren_multi_table.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_offers_active_table_columns_only@after_open_paren_multi_table.snap new file mode 100644 index 0000000..a94feff --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_offers_active_table_columns_only@after_open_paren_multi_table.snap @@ -0,0 +1,49 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Customers (\" cursor=23" +expression: "& a" +--- +Assessment { + input: "insert into Customers (", + cursor: 23, + state: IncompleteAtEof, + hint: Some( + Prose( + "Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 23, + 23, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + Candidate { + text: "true", + kind: Keyword, + }, + Candidate { + text: "false", + kind: Keyword, + }, + Candidate { + text: "Name", + kind: Identifier, + }, + Candidate { + text: "id", + kind: Identifier, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_serial_pk_offers_all_columns_including_serial@after_open_paren_serial_pk.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_serial_pk_offers_all_columns_including_serial@after_open_paren_serial_pk.snap new file mode 100644 index 0000000..595b5f9 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_serial_pk_offers_all_columns_including_serial@after_open_paren_serial_pk.snap @@ -0,0 +1,53 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Customers (\" cursor=23" +expression: "& a" +--- +Assessment { + input: "insert into Customers (", + cursor: 23, + state: IncompleteAtEof, + hint: Some( + Prose( + "Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 23, + 23, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + Candidate { + text: "true", + kind: Keyword, + }, + Candidate { + text: "false", + kind: Keyword, + }, + Candidate { + text: "Email", + kind: Identifier, + }, + Candidate { + text: "Name", + kind: Identifier, + }, + Candidate { + text: "id", + kind: Identifier, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_text_pk_offers_text_pk_columns@after_open_paren_text_pk.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_text_pk_offers_text_pk_columns@after_open_paren_text_pk.snap new file mode 100644 index 0000000..08402a4 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_open_paren_text_pk_offers_text_pk_columns@after_open_paren_text_pk.snap @@ -0,0 +1,49 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Items (\" cursor=19" +expression: "& a" +--- +Assessment { + input: "insert into Items (", + cursor: 19, + state: IncompleteAtEof, + hint: Some( + Prose( + "Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 19, + 19, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + Candidate { + text: "true", + kind: Keyword, + }, + Candidate { + text: "false", + kind: Keyword, + }, + Candidate { + text: "Code", + kind: Identifier, + }, + Candidate { + text: "Title", + kind: Identifier, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_table_name_offers_values_and_open_paren@after_table_name.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_table_name_offers_values_and_open_paren@after_table_name.snap new file mode 100644 index 0000000..00e8567 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_table_name_offers_values_and_open_paren@after_table_name.snap @@ -0,0 +1,47 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Customers \" cursor=22" +expression: "& a" +--- +Assessment { + input: "insert into Customers ", + cursor: 22, + state: IncompleteAtEof, + hint: Some( + Candidates { + items: [ + Candidate { + text: "values", + kind: Keyword, + }, + Candidate { + text: "(", + kind: Punct, + }, + ], + selected: None, + }, + ), + completion: Some( + Completion { + replaced_range: ( + 22, + 22, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "values", + kind: Keyword, + }, + Candidate { + text: "(", + kind: Punct, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_values_keyword_expects_open_paren@after_values_keyword.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_values_keyword_expects_open_paren@after_values_keyword.snap new file mode 100644 index 0000000..db28184 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_values_keyword_expects_open_paren@after_values_keyword.snap @@ -0,0 +1,39 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Customers (Name) values \" cursor=36" +expression: "& a" +--- +Assessment { + input: "insert into Customers (Name) values ", + cursor: 36, + state: IncompleteAtEof, + hint: Some( + Candidates { + items: [ + Candidate { + text: "(", + kind: Punct, + }, + ], + selected: None, + }, + ), + completion: Some( + Completion { + replaced_range: ( + 36, + 36, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "(", + kind: Punct, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_values_open_paren_form_a_serial_column_offers_null_to_auto_generate@after_values_open_paren_serial_col.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_values_open_paren_form_a_serial_column_offers_null_to_auto_generate@after_values_open_paren_serial_col.snap new file mode 100644 index 0000000..93ece8a --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_values_open_paren_form_a_serial_column_offers_null_to_auto_generate@after_values_open_paren_serial_col.snap @@ -0,0 +1,33 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Customers (id, Name) values (\" cursor=41" +expression: "& a" +--- +Assessment { + input: "insert into Customers (id, Name) values (", + cursor: 41, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `id`: Type null to auto-generate, or an explicit integer", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 41, + 41, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_values_open_paren_form_a_text_column_prose_names_column@after_values_open_paren_text_col.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_values_open_paren_form_a_text_column_prose_names_column@after_values_open_paren_text_col.snap new file mode 100644 index 0000000..7ba9b03 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__after_values_open_paren_form_a_text_column_prose_names_column@after_values_open_paren_text_col.snap @@ -0,0 +1,33 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Customers (Name) values (\" cursor=37" +expression: "& a" +--- +Assessment { + input: "insert into Customers (Name) values (", + cursor: 37, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `Name`: Type a quoted string (e.g. 'Alice') or null", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 37, + 37, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_bool_slot_prose_mentions_true_false@form_a_bool_slot.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_bool_slot_prose_mentions_true_false@form_a_bool_slot.snap new file mode 100644 index 0000000..5365087 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_bool_slot_prose_mentions_true_false@form_a_bool_slot.snap @@ -0,0 +1,41 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Things (b) values (\" cursor=31" +expression: "& a" +--- +Assessment { + input: "insert into Things (b) values (", + cursor: 31, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `b`: Type true, false, or null", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 31, + 31, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "true", + kind: Keyword, + }, + Candidate { + text: "false", + kind: Keyword, + }, + Candidate { + text: "null", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_complete_parses_to_insert@form_a_complete_two_values.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_complete_parses_to_insert@form_a_complete_two_values.snap new file mode 100644 index 0000000..fb76206 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_complete_parses_to_insert@form_a_complete_two_values.snap @@ -0,0 +1,19 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Customers (Name, Email) values ('Alice', 'a@b.c')\" cursor=61" +expression: "& a" +--- +Assessment { + input: "insert into Customers (Name, Email) values ('Alice', 'a@b.c')", + cursor: 61, + state: Valid, + hint: Some( + Prose( + "Submit with Enter", + ), + ), + completion: None, + parse_result: Ok( + "Insert", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_complete_with_serial_in_list_parses@form_a_complete_with_serial.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_complete_with_serial_in_list_parses@form_a_complete_with_serial.snap new file mode 100644 index 0000000..8d68dd9 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_complete_with_serial_in_list_parses@form_a_complete_with_serial.snap @@ -0,0 +1,19 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Customers (id, Name, Email) values (1, 'Alice', 'a@b.c')\" cursor=68" +expression: "& a" +--- +Assessment { + input: "insert into Customers (id, Name, Email) values (1, 'Alice', 'a@b.c')", + cursor: 68, + state: Valid, + hint: Some( + Prose( + "Submit with Enter", + ), + ), + completion: None, + parse_result: Ok( + "Insert", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_date_slot_prose_says_yyyy_mm_dd@form_a_date_slot.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_date_slot_prose_says_yyyy_mm_dd@form_a_date_slot.snap new file mode 100644 index 0000000..1f1015b --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_date_slot_prose_says_yyyy_mm_dd@form_a_date_slot.snap @@ -0,0 +1,33 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Things (dt) values (\" cursor=32" +expression: "& a" +--- +Assessment { + input: "insert into Things (dt) values (", + cursor: 32, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `dt`: Type a quoted date as 'YYYY-MM-DD' or null", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 32, + 32, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_in_progress_values_list_is_incomplete_not_definite_error@form_a_in_progress_no_red.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_in_progress_values_list_is_incomplete_not_definite_error@form_a_in_progress_no_red.snap new file mode 100644 index 0000000..0aa9658 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_in_progress_values_list_is_incomplete_not_definite_error@form_a_in_progress_no_red.snap @@ -0,0 +1,19 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Orders (OrderId, CustId, Total) values (42, 89, 17.59\" cursor=65" +expression: "& a" +--- +Assessment { + input: "insert into Orders (OrderId, CustId, Total) values (42, 89, 17.59", + cursor: 65, + state: IncompleteAtEof, + hint: Some( + Prose( + "Next: `)`", + ), + ), + completion: None, + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_in_progress_with_one_value_is_incomplete@form_a_in_progress_one_value.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_in_progress_with_one_value_is_incomplete@form_a_in_progress_one_value.snap new file mode 100644 index 0000000..37a7a22 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_in_progress_with_one_value_is_incomplete@form_a_in_progress_one_value.snap @@ -0,0 +1,19 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Orders (OrderId, CustId, Total) values (42\" cursor=54" +expression: "& a" +--- +Assessment { + input: "insert into Orders (OrderId, CustId, Total) values (42", + cursor: 54, + state: IncompleteAtEof, + hint: Some( + Prose( + "Next: `)`", + ), + ), + completion: None, + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_int_slot_prose_says_integer@form_a_int_slot.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_int_slot_prose_says_integer@form_a_int_slot.snap new file mode 100644 index 0000000..d6f74a7 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_int_slot_prose_says_integer@form_a_int_slot.snap @@ -0,0 +1,33 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Things (k) values (\" cursor=31" +expression: "& a" +--- +Assessment { + input: "insert into Things (k) values (", + cursor: 31, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `k`: Type an integer (e.g. 42, -7) or null", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 31, + 31, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_open_paren_no_leakage_from_other_tables@form_a_open_paren_orders.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_open_paren_no_leakage_from_other_tables@form_a_open_paren_orders.snap new file mode 100644 index 0000000..1762cd6 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_open_paren_no_leakage_from_other_tables@form_a_open_paren_orders.snap @@ -0,0 +1,53 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Orders (\" cursor=20" +expression: "& a" +--- +Assessment { + input: "insert into Orders (", + cursor: 20, + state: IncompleteAtEof, + hint: Some( + Prose( + "Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 20, + 20, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + Candidate { + text: "true", + kind: Keyword, + }, + Candidate { + text: "false", + kind: Keyword, + }, + Candidate { + text: "CustId", + kind: Identifier, + }, + Candidate { + text: "OrderId", + kind: Identifier, + }, + Candidate { + text: "Total", + kind: Identifier, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_shortid_slot_prose_mentions_null_to_auto_generate@form_a_shortid_slot.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_shortid_slot_prose_mentions_null_to_auto_generate@form_a_shortid_slot.snap new file mode 100644 index 0000000..5918c86 --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__form_a_shortid_slot_prose_mentions_null_to_auto_generate@form_a_shortid_slot.snap @@ -0,0 +1,33 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Things (sid) values (\" cursor=33" +expression: "& a" +--- +Assessment { + input: "insert into Things (sid) values (", + cursor: 33, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `sid`: Type null to auto-generate, or a quoted shortid", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 33, + 33, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__mid_column_list_after_comma_offers_remaining_columns@after_comma_in_column_list.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__mid_column_list_after_comma_offers_remaining_columns@after_comma_in_column_list.snap new file mode 100644 index 0000000..9dd870b --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__mid_column_list_after_comma_offers_remaining_columns@after_comma_in_column_list.snap @@ -0,0 +1,53 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Customers (id, \" cursor=27" +expression: "& a" +--- +Assessment { + input: "insert into Customers (id, ", + cursor: 27, + state: IncompleteAtEof, + hint: Some( + Prose( + "Type a value: number, 'text', true/false, null (dates as 'YYYY-MM-DD', datetimes as 'YYYY-MM-DDTHH:MM:SS')", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 27, + 27, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + Candidate { + text: "true", + kind: Keyword, + }, + Candidate { + text: "false", + kind: Keyword, + }, + Candidate { + text: "Email", + kind: Identifier, + }, + Candidate { + text: "Name", + kind: Identifier, + }, + Candidate { + text: "id", + kind: Identifier, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__mid_value_list_after_comma_advances_to_next_column_prose@after_first_value_advance_to_email.snap b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__mid_value_list_after_comma_advances_to_next_column_prose@after_first_value_advance_to_email.snap new file mode 100644 index 0000000..089c4da --- /dev/null +++ b/tests/typing_surface/snapshots/typing_surface_matrix__typing_surface__insert_form_a__mid_value_list_after_comma_advances_to_next_column_prose@after_first_value_advance_to_email.snap @@ -0,0 +1,33 @@ +--- +source: tests/typing_surface/insert_form_a.rs +description: "input=\"insert into Customers (Name, Email) values ('Alice', \" cursor=53" +expression: "& a" +--- +Assessment { + input: "insert into Customers (Name, Email) values ('Alice', ", + cursor: 53, + state: IncompleteAtEof, + hint: Some( + Prose( + "for `Email`: Type a quoted string (e.g. 'Alice') or null", + ), + ), + completion: Some( + Completion { + replaced_range: ( + 53, + 53, + ), + partial_prefix: "", + candidates: [ + Candidate { + text: "null", + kind: Keyword, + }, + ], + }, + ), + parse_result: Err( + "Invalid(at_eof)", + ), +} diff --git a/tests/typing_surface/update_all_rows.rs b/tests/typing_surface/update_all_rows.rs new file mode 100644 index 0000000..b2d5ff4 --- /dev/null +++ b/tests/typing_surface/update_all_rows.rs @@ -0,0 +1 @@ +//! Submodule stub — populated in subsequent tasks. diff --git a/tests/typing_surface/update_with_where.rs b/tests/typing_surface/update_with_where.rs new file mode 100644 index 0000000..b2d5ff4 --- /dev/null +++ b/tests/typing_surface/update_with_where.rs @@ -0,0 +1 @@ +//! Submodule stub — populated in subsequent tasks. diff --git a/tests/typing_surface_matrix.rs b/tests/typing_surface_matrix.rs new file mode 100644 index 0000000..fc55076 --- /dev/null +++ b/tests/typing_surface_matrix.rs @@ -0,0 +1,19 @@ +//! Typing-surface matrix — systematic per-position coverage of +//! what the user sees while typing a command (state, hint, +//! completion candidates, parse outcome), against canonical +//! schema shapes. +//! +//! Per `docs/handoff/20260515-handoff-12.md` §1. The matrix exists +//! to catch the class of bug where automated tests pass but real +//! users hit issues at specific typing positions — every commit +//! in handoff-12's session fixed a bug that the 800+ existing +//! tests had missed. Each row in this matrix is one (input, +//! cursor, schema) cell; failures here mean the typing surface +//! drifted or regressed at that exact cell. +//! +//! Layout: helpers and per-command submodules live under +//! `tests/typing_surface/`. Each submodule exercises one command +//! family across the canonical schema shapes defined in +//! `mod.rs`. + +mod typing_surface;