Cleanup pass: --help, in-app help, post-rebuild message, unmodified-temp cleanup

Four post-Iteration-4 polish items surfaced by manual testing.

1. `--help` / `-h` CLI flag prints a usage banner (options +
   app-level commands + DSL grammar reference) and exits. Parse
   errors also print the banner to stderr.

2. `help` app-level command notes the same list of supported
   commands to the output panel -- a simple stand-in for the
   richer H3 help system, kept in sync with what's actually
   wired up.

3. The silent rebuild that runs when playground.db is missing
   now surfaces a system message in the output panel ("[ok]
   rebuild -- N tables, M rows reconstructed; ...") via a new
   initial_events plumbing. The user no longer wonders whether
   the .db was magically restored or whether anything happened
   on launch.

4. Unmodified empty temp projects (kind=Temp, project.yaml has
   tables: [] and relationships: []) are now auto-deleted when
   the user switches away (load / new / save as) or quits. This
   addresses the "launch app, load existing project, quit"
   pattern that was leaving an empty temp directory behind
   every time. Modified temps (with any user-created tables or
   relationships) are never auto-deleted; corrupted projects
   are also never auto-deleted (defensive default-to-false on
   yaml read/parse errors).

Tests: 338 passing (272 lib + 9 + 5 + 6 + 20 + 9 + 17),
0 failing, 0 skipped. Clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-08 06:43:49 +00:00
parent f2198275f0
commit b7addd6161
6 changed files with 302 additions and 18 deletions
+79
View File
@@ -17,6 +17,9 @@ use rdbms_playground::app::{
PathEntryPurpose,
};
use rdbms_playground::event::AppEvent;
use rdbms_playground::db::Database;
use rdbms_playground::dsl::{ColumnSpec, Type};
use rdbms_playground::persistence::Persistence;
use rdbms_playground::project::{self, Project, ProjectKind, copy_project};
const fn key(code: KeyCode) -> AppEvent {
@@ -42,6 +45,26 @@ fn tempdir() -> tempfile::TempDir {
tempfile::tempdir().expect("create tempdir")
}
#[test]
fn help_command_lists_supported_commands() {
let mut app = App::new();
type_str(&mut app, "help");
let actions = submit(&mut app);
assert!(actions.is_empty());
let body = app
.output
.iter()
.map(|l| l.text.as_str())
.collect::<Vec<_>>()
.join("\n");
for keyword in ["quit", "rebuild", "save", "load", "new", "create table"] {
assert!(
body.contains(keyword),
"help output missing `{keyword}`:\n{body}",
);
}
}
#[test]
fn save_on_temp_opens_path_entry_modal() {
let mut app = App::new();
@@ -308,6 +331,62 @@ fn project_kind_recovered_from_dirname_on_open() {
assert_eq!(opened_named.display_name(), "My Project");
}
#[test]
fn fresh_temp_is_unmodified() {
let data = tempdir();
let project = project::open_or_create(None, Some(data.path())).unwrap();
assert!(project.is_unmodified_temp());
}
#[test]
fn temp_with_a_table_is_no_longer_unmodified() {
let data = tempdir();
let project = project::open_or_create(None, Some(data.path())).unwrap();
let path = project.path().to_path_buf();
let db = Database::open_with_persistence(
project.db_path(),
Persistence::new(path.clone()),
)
.unwrap();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
db.create_table(
"T".to_string(),
vec![ColumnSpec { name: "id".to_string(), ty: Type::Serial }],
vec!["id".to_string()],
Some("create".to_string()),
)
.await
.unwrap();
});
drop(db);
drop(project);
let reopened = Project::open(&path).unwrap();
assert!(
!reopened.is_unmodified_temp(),
"a temp with a table should not be considered unmodified",
);
}
#[test]
fn named_project_is_never_unmodified_temp() {
let data = tempdir();
let temp = project::open_or_create(None, Some(data.path())).unwrap();
let temp_path = temp.path().to_path_buf();
drop(temp);
let named = data.path().join("MyOrders");
copy_project(&temp_path, &named).unwrap();
let opened = Project::open(&named).unwrap();
// Even though the schema is empty, kind is Named.
assert_eq!(opened.kind(), ProjectKind::Named);
assert!(!opened.is_unmodified_temp());
}
#[test]
fn list_projects_sorts_by_mtime() {
let data = tempdir();