From 4143a01ff46e4aec44e0d85041e7124be201b513 Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Thu, 15 Oct 2026 07:06:04 +0100 Subject: [PATCH] fix(commands): deny list during BEDIT Refuse LAYOUT, VPORTS, MVIEW, WBLOCK, PURGE, RENAME, REFEDIT, REFEDIT_BEGIN and PLOT from the command line while a block editor session is open, matching the reference product's " command not allowed in block editor" behaviour (spec section 8, based on the V7 hands-on check). XREF, NEW, OPEN and SAVEAS stay allowed. The service-level RENAME guard stays as defense in depth. --- src/app/command_driver/tests.rs | 109 ++++++++++++++++++++++++++++++ src/app/commands/mod.rs | 24 +++++++ src/app/update/block_ops_tests.rs | 9 ++- 3 files changed, 139 insertions(+), 3 deletions(-) diff --git a/src/app/command_driver/tests.rs b/src/app/command_driver/tests.rs index b2ec4cb83..31295729d 100644 --- a/src/app/command_driver/tests.rs +++ b/src/app/command_driver/tests.rs @@ -1318,6 +1318,115 @@ mod bedit_characterization_tests { assert!(app.tabs[tab].block_edits.is_empty()); } + /// V7 (reference check): while a block editor session is open this + /// command set is refused outright with "not allowed in block editor"; + /// the refusal pushes no undo entry and starts no command. + #[test] + fn locked_deny_list_refuses_while_session_open() { + let mut app = OpenCADStudio::new_for_test(); + let (insert, _) = setup_block_and_insert(&mut app); + let tab = app.active_tab; + let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", insert.value())); + assert!( + app.tabs[tab].active_block_edit.is_some(), + "setup: BEDIT_BEGIN should open a session" + ); + let undo_depth = app.tabs[tab].history.undo_stack.len(); + for verb in [ + "LAYOUT", + "VPORTS", + "MVIEW", + "WBLOCK", + "PURGE", + "RENAME", + "REFEDIT", + "REFEDIT_BEGIN", + "PLOT", + ] { + let revision = app.command_line.error_revision; + let _ = app.dispatch_command(verb); + assert_eq!( + app.command_line.error_revision, + revision.wrapping_add(1), + "{verb} must refuse with a command-line error" + ); + assert!( + app.command_line + .last_error + .as_deref() + .unwrap_or_default() + .contains("command not allowed in block editor"), + "{verb}: got {:?}", + app.command_line.last_error + ); + assert!( + app.tabs[tab].active_block_edit.is_some(), + "{verb} must keep the session open" + ); + assert!( + app.tabs[tab].active_cmd.is_none(), + "{verb} must not install a command" + ); + assert_eq!( + app.tabs[tab].history.undo_stack.len(), + undo_depth, + "{verb} must push no undo entry" + ); + } + } + + /// Spec 8: a rejected command must not cancel a running command — the + /// deny hook sits before the running-command teardown. + #[test] + fn locked_deny_list_does_not_cancel_a_running_command() { + let mut app = OpenCADStudio::new_for_test(); + let (insert, _) = setup_block_and_insert(&mut app); + let tab = app.active_tab; + let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", insert.value())); + assert!(app.tabs[tab].active_block_edit.is_some()); + let _ = app.dispatch_command("LINE"); + assert_eq!( + app.tabs[tab].active_cmd.as_deref().map(|cmd| cmd.name()), + Some("LINE"), + "setup: LINE should be running inside the session" + ); + let _ = app.dispatch_command("LAYOUT"); + assert!(app + .command_line + .last_error + .as_deref() + .unwrap_or_default() + .contains("command not allowed in block editor")); + assert_eq!( + app.tabs[tab].active_cmd.as_deref().map(|cmd| cmd.name()), + Some("LINE"), + "the refused command must leave the running LINE untouched" + ); + } + + /// V7: the reference keeps XREF, NEW, OPEN, SAVEAS and BEDIT available + /// inside the editor — the deny list must not touch them. + #[test] + fn locked_deny_list_leaves_v7_allowed_commands_alone() { + for verb in ["XREF", "NEW", "OPEN", "SAVEAS", "BEDIT"] { + let mut app = OpenCADStudio::new_for_test(); + let (insert, _) = setup_block_and_insert(&mut app); + let tab = app.active_tab; + let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", insert.value())); + assert!(app.tabs[tab].active_block_edit.is_some()); + let _ = app.dispatch_command(verb); + assert!( + !app.command_line + .last_error + .as_deref() + .unwrap_or_default() + .contains("not allowed in block editor"), + "{verb} is available in the reference session and must not be denied: {:?}", + app.command_line.last_error + ); + } + } + // ---- documenting: today's behavior, expected to flip in Phase 1 ---- /// Discard removes local before-images before drawing undo resumes. diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index 1192c8e72..a1af0cdcd 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -209,6 +209,30 @@ impl OpenCADStudio { return Task::none(); } } + // V7 (reference check): inside a block editor session a set of + // commands is refused outright. The hook sits after alias resolution + // and before the transparent split and the running-command teardown, + // so a rejected command leaves whatever is running untouched. + if self.tabs[i].active_block_edit.is_some() { + let name = cmd.split([' ', ':']).next().unwrap_or(cmd); + if matches!( + name, + "LAYOUT" + | "VPORTS" + | "MVIEW" + | "WBLOCK" + | "PURGE" + | "RENAME" + | "REFEDIT" + | "REFEDIT_BEGIN" + | "PLOT" + ) { + self.command_line.push_error( + crate::tf!("{} command not allowed in block editor", name).as_ref(), + ); + return Task::none(); + } + } if is_spacemouse_command(cmd) { return self.run_action(cmd); } diff --git a/src/app/update/block_ops_tests.rs b/src/app/update/block_ops_tests.rs index c8b070667..8afcf5910 100644 --- a/src/app/update/block_ops_tests.rs +++ b/src/app/update/block_ops_tests.rs @@ -626,6 +626,9 @@ fn locked_rename_refuses_anonymous_and_layout_records_with_the_shared_text() { } /// A rename refuses a block with an open BEDIT session, whatever the new name. +/// V7: the RENAME verb itself is refused at the command choke point while a +/// session is open; the block service keeps its own refusal as defense in +/// depth for the non-command paths (control op, palette). #[test] fn locked_rename_refuses_a_block_with_an_open_bedit_session() { let mut app = app(); @@ -639,9 +642,9 @@ fn locked_rename_refuses_a_block_with_an_open_bedit_session() { assert_eq!(undo_entry_count(&app), before); assert_eq!( errors(&app), - vec![command_line_error(&format!( - "block \"{name}\" is open in the block editor; close the editor first" - ))] + vec![command_line_error( + "RENAME command not allowed in block editor" + )] ); let _ = app.dispatch_command("BEDIT_DISCARD"); }