Skip to content

Commit a7a1209

Browse files
Require confirmation for spacetime delete (#4770)
## Summary - require an explicit confirmation before `spacetime delete` sends an irreversible delete request - preserve `--yes` as the non-interactive override - add smoketests for aborting on `n`, deleting on `y`, and skipping the prompt with `--yes` Closes #4679. ## History note `spacetime delete` has never required confirmation for ordinary deletes. The command was added in commit `44df6c6e7` (`Initial commit`, 2023-08-01) without a prompt. Later, commit `a36f7091d` (`[teams 3/5] API authorization, CLI, smoketests`, 2025-11-11) added a confirmation flow only for the special case where deleting a database would also delete its children, but plain deletes still executed immediately until this change. ## Testing Added new smoketests --------- Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com> Co-authored-by: Zeke Foppa <bfops@users.noreply.github.com>
1 parent 8801913 commit a7a1209

File tree

2 files changed

+71
-3
lines changed

2 files changed

+71
-3
lines changed

crates/cli/src/subcommands/delete.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
4545
let force = args.get_flag("force");
4646

4747
let identity = database_identity(&config, &resolved.database, server).await?;
48+
let delete_target = if resolved.database == identity.to_string() {
49+
identity.to_string()
50+
} else {
51+
format!("{} ({identity})", resolved.database)
52+
};
53+
54+
if !y_or_n(
55+
force,
56+
&format!("Are you sure you want to delete database {delete_target}? This action cannot be undone."),
57+
)? {
58+
println!("Aborting");
59+
return Ok(());
60+
}
61+
4862
let host_url = config.get_host_url(server)?;
4963
let request_path = format!("{host_url}/v1/database/{identity}");
5064
let auth_header = get_auth_header(&mut config, false, server, !force).await?;
@@ -58,7 +72,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
5872
if !force {
5973
print_database_tree_info(&confirm.database_tree).await?;
6074
}
61-
if y_or_n(force, "Do you want to proceed deleting above databases?")? {
75+
if force || y_or_n(false, "Do you want to proceed deleting above databases?")? {
6276
send_request(&client, &request_path, &auth_header, Some(confirm.confirmation_token))
6377
.await?
6478
.error_for_status()?;

crates/smoketests/tests/smoketests/delete_database.rs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,41 @@ use spacetimedb_smoketests::Smoketest;
22
use std::thread;
33
use std::time::Duration;
44

5+
fn assert_delete_prompt(output: &str, database: &str) {
6+
assert!(
7+
output.contains("Are you sure you want to delete database"),
8+
"expected confirmation prompt in output:\n{output}"
9+
);
10+
assert!(
11+
output.contains(database),
12+
"expected database name in confirmation prompt:\n{output}"
13+
);
14+
}
15+
16+
#[test]
17+
fn test_delete_database_aborts_without_confirmation() {
18+
let mut test = Smoketest::builder()
19+
.precompiled_module("delete-database")
20+
.autopublish(false)
21+
.build();
22+
23+
let name = format!("test-db-{}", std::process::id());
24+
test.publish_module_named(&name, false).unwrap();
25+
26+
let output = test
27+
.spacetime(&["delete", "--server", &test.server_url, &name])
28+
.unwrap();
29+
assert_delete_prompt(&output, &name);
30+
assert!(output.contains("Aborting"), "expected abort message:\n{output}");
31+
32+
test.spacetime(&["logs", "--server", &test.server_url, &name]).unwrap();
33+
}
34+
535
/// Test that deleting a database stops the module.
636
/// The module is considered stopped if its scheduled reducer stops
737
/// producing update events.
838
#[test]
9-
fn test_delete_database() {
39+
fn test_delete_database_with_confirmation() {
1040
let mut test = Smoketest::builder()
1141
.precompiled_module("delete-database")
1242
.autopublish(false)
@@ -23,8 +53,10 @@ fn test_delete_database() {
2353
thread::sleep(Duration::from_secs(2));
2454

2555
// Delete the database
26-
test.spacetime(&["delete", "--server", &test.server_url, &name])
56+
let output = test
57+
.spacetime_with_stdin(&["delete", "--server", &test.server_url, &name], "y\n")
2758
.unwrap();
59+
assert_delete_prompt(&output, &name);
2860

2961
// Collect whatever updates we got
3062
let updates = sub.collect().unwrap();
@@ -37,3 +69,25 @@ fn test_delete_database() {
3769
updates.len()
3870
);
3971
}
72+
73+
#[test]
74+
fn test_delete_database_yes_skips_confirmation() {
75+
let mut test = Smoketest::builder()
76+
.precompiled_module("delete-database")
77+
.autopublish(false)
78+
.build();
79+
80+
let name = format!("test-db-{}", std::process::id());
81+
test.publish_module_named(&name, false).unwrap();
82+
83+
let output = test
84+
.spacetime(&["delete", "--server", &test.server_url, "--yes", &name])
85+
.unwrap();
86+
assert!(
87+
output.contains("Skipping confirmation due to --yes"),
88+
"expected --yes skip message:\n{output}"
89+
);
90+
91+
let result = test.spacetime(&["logs", "--server", &test.server_url, &name]);
92+
assert!(result.is_err(), "expected database to be deleted");
93+
}

0 commit comments

Comments
 (0)