feat: H3 help <command> per-command detail + general reference
HELP node takes an optional single-word topic (BarePath);
AppCommand::Help { topic }. note_help_topic renders the help
block(s) of every command sharing that entry word (so `help
create` covers both create forms), plus `help types` and a
friendly "no help for X" pointer for unknown topics. Full help
gains a detail-hint footer. Catalogued help.detail_hint /
help.unknown_topic; parse-error matrix updated (help now takes a
topic, so the near-miss is the multi-word case). 9 integration
tests in tests/it/help_command.rs. Mark H3 [x].
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
//! 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::{parse_command, AppCommand, 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:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[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:?}",
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ mod case_insensitive_names;
|
||||
mod column_op_guards;
|
||||
mod engine_vocabulary_audit;
|
||||
mod friendly_enrichment;
|
||||
mod help_command;
|
||||
mod iteration2_persistence;
|
||||
mod iteration3_rebuild;
|
||||
mod iteration4a_rebuild_command;
|
||||
|
||||
@@ -72,7 +72,10 @@ fn near_miss_matrix_simple_mode() {
|
||||
// trailing junk with "expected end of input" + their usage
|
||||
// (audited 2026-06-05); locked here as regression insurance.
|
||||
("quit now", &["after `quit`, expected end of input", " quit"]),
|
||||
("help foo", &["after `help`, expected end of input", " help"]),
|
||||
// `help` now takes an optional single-word topic (H3), so
|
||||
// `help foo` parses (topic lookup); only a *multi-word*
|
||||
// topic is the near-miss that rejects trailing junk.
|
||||
("help foo bar", &["after `help foo`, expected end of input", "help [<command>]"]),
|
||||
("rebuild now", &["after `rebuild`, expected end of input", " rebuild"]),
|
||||
("new foo", &["after `new`, expected end of input", " new"]),
|
||||
("load foo", &["after `load`, expected end of input", " load"]),
|
||||
|
||||
Reference in New Issue
Block a user