The six advanced SQL DML/query forms (SELECT, WITH, SQL_INSERT, SQL_UPDATE, SQL_DELETE, EXPLAIN_SQL) carried help_id: None, so `help select`/`help with` resolved to nothing and `help insert` showed only the simple form. Give each its own distinct help_id (data.select, data.sql_insert, …) with a hand-curated help.data.* page. Distinct strings keep the dedup invariant intact, and note_help_topic needs no change — `help insert` now shows the simple block and the sql_insert block (like `help create` already did), and the advanced-only forms resolve. note_help now groups the list by CommandCategory: app-lifecycle commands first (unlabelled), then "Simple-mode commands:" and "Advanced-mode (SQL) commands:" sections — replacing the old single "DSL data commands (in simple mode):" header, which used the banned "DSL" term and mis-labelled the advanced SQL forms it already contained. Four new help_command tests (red→green). Recorded as ADR-0024 Amendment 1; CHANGELOG updated.
216 lines
6.6 KiB
Rust
216 lines
6.6 KiB
Rust
//! Integration tests for `help` and `help <command>` (H3).
|
|
//!
|
|
//! Covers:
|
|
//! - Parse layer: `help` → `Help { topic: None }`; `help insert`
|
|
//! → `Help { topic: Some("insert") }`.
|
|
//! - App behaviour: the full `help` ends with the detail hint;
|
|
//! `help <command>` renders that command's block (and every
|
|
//! form sharing the entry word); `help types` renders the type
|
|
//! reference; an unknown topic gets a friendly pointer back.
|
|
|
|
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
|
|
|
use rdbms_playground::app::App;
|
|
use rdbms_playground::dsl::{AppCommand, Command, parse_command};
|
|
use rdbms_playground::event::AppEvent;
|
|
|
|
const fn key(code: KeyCode) -> AppEvent {
|
|
AppEvent::Key(KeyEvent {
|
|
code,
|
|
modifiers: KeyModifiers::NONE,
|
|
kind: KeyEventKind::Press,
|
|
state: crossterm::event::KeyEventState::NONE,
|
|
})
|
|
}
|
|
|
|
fn type_str(app: &mut App, s: &str) {
|
|
for c in s.chars() {
|
|
app.update(key(KeyCode::Char(c)));
|
|
}
|
|
}
|
|
|
|
/// Submit `input` to a fresh app and collect all output text.
|
|
fn output_for(input: &str) -> Vec<String> {
|
|
let mut app = App::new();
|
|
type_str(&mut app, input);
|
|
app.update(key(KeyCode::Enter));
|
|
app.output.iter().map(|l| l.text.clone()).collect()
|
|
}
|
|
|
|
// =================================================================
|
|
// Parse layer
|
|
// =================================================================
|
|
|
|
#[test]
|
|
fn bare_help_parses_with_no_topic() {
|
|
assert_eq!(
|
|
parse_command("help").expect("parses"),
|
|
Command::App(AppCommand::Help { topic: None }),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn help_with_topic_captures_the_word() {
|
|
assert_eq!(
|
|
parse_command("help insert").expect("parses"),
|
|
Command::App(AppCommand::Help {
|
|
topic: Some("insert".to_string())
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn help_topic_is_a_single_word_multi_word_is_a_parse_error() {
|
|
// Entry-word topics cover multi-word commands (`help create`),
|
|
// so a second word is trailing junk, not a longer topic.
|
|
assert!(parse_command("help foo bar").is_err());
|
|
}
|
|
|
|
// =================================================================
|
|
// App behaviour
|
|
// =================================================================
|
|
|
|
#[test]
|
|
fn full_help_lists_commands_and_ends_with_the_detail_hint() {
|
|
let out = output_for("help");
|
|
assert!(
|
|
out.iter().any(|l| l == "Supported commands:"),
|
|
"intro present: {out:?}",
|
|
);
|
|
assert!(
|
|
out.iter().any(|l| l.contains("help <command>")),
|
|
"detail-hint footer present: {out:?}",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn help_insert_renders_the_insert_block() {
|
|
let out = output_for("help insert");
|
|
assert!(
|
|
out.iter().any(|l| l.contains("insert into")),
|
|
"insert help shown: {out:?}",
|
|
);
|
|
// Focused: it must NOT dump the whole list — the intro header
|
|
// belongs to the full `help` only.
|
|
assert!(
|
|
!out.iter().any(|l| l == "Supported commands:"),
|
|
"focused help omits the full-list intro: {out:?}",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn help_create_covers_every_form_sharing_the_entry_word() {
|
|
// `create` is the entry word for both the DSL `create table`
|
|
// and the advanced SQL `CREATE TABLE` — `help create` shows
|
|
// both blocks.
|
|
let out = output_for("help create");
|
|
let joined = out.join("\n");
|
|
assert!(
|
|
joined.contains("create table"),
|
|
"DSL create form shown: {out:?}",
|
|
);
|
|
assert!(
|
|
joined.to_lowercase().matches("create").count() >= 2,
|
|
"more than one create form shown: {out:?}",
|
|
);
|
|
}
|
|
|
|
// ----- issue #36: advanced-mode SQL forms get distinct help content -----
|
|
|
|
#[test]
|
|
fn help_select_renders_the_sql_select_block() {
|
|
// `select` is advanced-only (no simple sibling) and used to have
|
|
// `help_id: None`, so `help select` produced the unknown-topic note.
|
|
// It now carries its own help page.
|
|
let out = output_for("help select");
|
|
let joined = out.join("\n").to_lowercase();
|
|
assert!(
|
|
joined.contains("select") && joined.contains("from"),
|
|
"help select shows the SQL select form: {out:?}",
|
|
);
|
|
assert!(
|
|
!out.iter().any(|l| l.contains("No help for")),
|
|
"help select resolves to content, not the unknown-topic note: {out:?}",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn help_with_renders_the_cte_block() {
|
|
let out = output_for("help with");
|
|
let joined = out.join("\n").to_lowercase();
|
|
assert!(
|
|
joined.contains("with") && joined.contains("as ("),
|
|
"help with shows the CTE form: {out:?}",
|
|
);
|
|
assert!(
|
|
!out.iter().any(|l| l.contains("No help for")),
|
|
"help with resolves to content: {out:?}",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn help_insert_shows_both_simple_and_sql_forms() {
|
|
// `help insert` now covers the simple form AND the advanced SQL form
|
|
// (two clearly-labelled blocks, like `help create` already does).
|
|
let out = output_for("help insert");
|
|
let joined = out.join("\n").to_lowercase();
|
|
assert!(
|
|
joined.contains("insert into"),
|
|
"simple insert form shown: {out:?}",
|
|
);
|
|
assert!(
|
|
joined.contains("advanced"),
|
|
"advanced SQL insert form shown alongside the simple one: {out:?}",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn help_list_splits_simple_and_advanced_sections() {
|
|
let out = output_for("help");
|
|
let joined = out.join("\n");
|
|
assert!(
|
|
out.iter().any(|l| l.contains("Simple-mode commands")),
|
|
"simple-mode section header present: {out:?}",
|
|
);
|
|
assert!(
|
|
out.iter()
|
|
.any(|l| l.contains("Advanced-mode") && l.contains("SQL")),
|
|
"advanced-mode (SQL) section header present: {out:?}",
|
|
);
|
|
// Copy rule: never say "DSL" in user-facing text (the old header did).
|
|
assert!(
|
|
!joined.contains("DSL"),
|
|
"help output must not contain 'DSL': {out:?}",
|
|
);
|
|
// The advanced query commands are now discoverable in the list.
|
|
assert!(
|
|
joined.to_lowercase().contains("select"),
|
|
"select is listed in help: {out:?}",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn help_types_renders_the_type_reference() {
|
|
let out = output_for("help types");
|
|
let joined = out.join("\n").to_lowercase();
|
|
// The type reference names the playground types.
|
|
assert!(
|
|
joined.contains("serial") || joined.contains("shortid"),
|
|
"type reference shown: {out:?}",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn help_unknown_topic_points_back_to_the_full_list() {
|
|
let out = output_for("help wibble");
|
|
assert!(
|
|
out.iter()
|
|
.any(|l| l.contains("No help for") && l.contains("wibble")),
|
|
"names the unknown topic: {out:?}",
|
|
);
|
|
assert!(
|
|
out.iter().any(|l| l.contains("Type `help`")),
|
|
"points back at the full list: {out:?}",
|
|
);
|
|
}
|