From 67aeec14556d56c8ccc3b5abb672b0ec4c5198c0 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 16:18:59 -0400 Subject: [PATCH 01/34] Support SpacetimeAuth smoketest runs --- crates/smoketests/Cargo.toml | 2 +- crates/smoketests/DEVELOP.md | 20 +++ crates/smoketests/src/lib.rs | 109 ++++++++-------- .../tests/smoketests/database_lock.rs | 4 +- .../tests/smoketests/new_user_flow.rs | 4 +- .../tests/smoketests/permissions.rs | 12 +- crates/smoketests/tests/smoketests/rls.rs | 4 +- .../tests/smoketests/timestamp_route.rs | 4 +- crates/smoketests/tests/smoketests/views.rs | 9 +- tools/ci/src/smoketest.rs | 123 +++++++++++++++++- 10 files changed, 227 insertions(+), 64 deletions(-) diff --git a/crates/smoketests/Cargo.toml b/crates/smoketests/Cargo.toml index 1ee47e33cf0..473be886c55 100644 --- a/crates/smoketests/Cargo.toml +++ b/crates/smoketests/Cargo.toml @@ -13,6 +13,7 @@ toml.workspace = true regex.workspace = true anyhow.workspace = true fs_extra.workspace = true +reqwest = { workspace = true, features = ["blocking"] } which = "8.0.0" [dev-dependencies] @@ -22,7 +23,6 @@ assert_cmd = "2" predicates = "3" tokio.workspace = true tokio-postgres.workspace = true -reqwest = { workspace = true, features = ["blocking"] } xmltree.workspace = true [lints] diff --git a/crates/smoketests/DEVELOP.md b/crates/smoketests/DEVELOP.md index 423c038f6ec..dbf9a46e9ae 100644 --- a/crates/smoketests/DEVELOP.md +++ b/crates/smoketests/DEVELOP.md @@ -18,6 +18,26 @@ cargo smoketest test_sql_format cargo smoketest "cli::" # Run all CLI tests ``` +### Remote Servers + +Run against a standalone-compatible remote server with: + +```bash +cargo ci smoketests --server https://example.spacetimedb.com +``` + +Maincloud and maincloud staging require SpacetimeAuth-issued tokens rather than +server-issued tokens. Use `--spacetime-login` for those: + +```bash +SPACETIME_SMOKETEST_TOKEN= \ + cargo ci smoketests --server https://maincloud.staging.spacetimedb.com --spacetime-login +``` + +If `SPACETIME_SMOKETEST_TOKEN` is omitted, the runner invokes `spacetime login`. +Tests that need throwaway server-issued identities should call +`require_server_issued_login!()` so they skip in SpacetimeAuth mode. + ### WARNING: Stale Binary Risk **Smoketests use pre-built binaries and DO NOT automatically rebuild them.** diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 4d918a46321..c0c896efcd9 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -78,6 +78,11 @@ pub fn is_remote_server() -> bool { remote_server_url().is_some() } +/// Returns true if remote smoketests are using a SpacetimeAuth-issued token. +pub fn is_spacetime_login() -> bool { + std::env::var("SPACETIME_SMOKETEST_SPACETIME_LOGIN").ok().as_deref() == Some("1") +} + /// Skip this test if running against a remote server. /// /// Use this macro at the start of tests that require a local server, @@ -107,6 +112,19 @@ macro_rules! require_local_server { }; } +#[macro_export] +macro_rules! require_server_issued_login { + () => { + if $crate::is_spacetime_login() { + #[allow(clippy::disallowed_macros)] + { + eprintln!("Skipping test: requires server-issued throwaway identities"); + } + return; + } + }; +} + #[macro_export] macro_rules! require_dotnet { () => { @@ -912,6 +930,9 @@ impl SmoketestBuilder { let module_name = format!("smoketest_module_{}", random_string()); let config_path = project_dir.path().join("config.toml"); + if let Ok(base_config) = std::env::var("SPACETIME_SMOKETEST_BASE_CONFIG") { + fs::write(&config_path, base_config).expect("Failed to write base smoketest config"); + } let mut smoketest = Smoketest { guard, _data_dir_fixture: data_dir_fixture, @@ -1005,10 +1026,10 @@ impl Smoketest { } pub fn login_with_token(&self, token: &str) -> Result<()> { - let host = self.server_host(); + let (protocol, host) = split_server_url(&self.server_url)?; let config_str = format!( - "default_server = \"localhost\"\n\nspacetimedb_token = \"{}\"\n\n[[server_configs]]\nnickname = \"localhost\"\nhost = \"{}\"\nprotocol = \"http\"\n", - token, host + "default_server = \"localhost\"\n\nspacetimedb_token = \"{}\"\n\n[[server_configs]]\nnickname = \"localhost\"\nhost = \"{}\"\nprotocol = \"{}\"\n", + token, host, protocol ); fs::write(&self.config_path, config_str).context("Failed to write config.toml")?; Ok(()) @@ -1639,6 +1660,10 @@ log = "0.4" /// /// This is useful for tests that need to test with multiple identities. pub fn new_identity(&self) -> Result<()> { + if is_spacetime_login() { + bail!("new_identity requires server-issued login mode"); + } + let cli_path = ensure_binaries_built(); let config_path_str = self.config_path.to_str().unwrap(); @@ -1701,58 +1726,25 @@ log = "0.4" body: Option<&[u8]>, extra_headers: &str, ) -> Result { - use std::io::{Read, Write}; - use std::net::TcpStream; - - // Parse server URL to get host and port - let url = &self.server_url; - let host_port = url - .strip_prefix("http://") - .or_else(|| url.strip_prefix("https://")) - .unwrap_or(url); - - let mut stream = TcpStream::connect(host_port).context("Failed to connect to server")?; - stream.set_read_timeout(Some(std::time::Duration::from_secs(30))).ok(); - - // Get auth token let token = self.read_token()?; - - // Build HTTP request - let content_length = body.map(|b| b.len()).unwrap_or(0); - let request = format!( - "{} {} HTTP/1.1\r\nHost: {}\r\nContent-Length: {}\r\nAuthorization: Bearer {}\r\n{}Connection: close\r\n\r\n", - method, path, host_port, content_length, token, extra_headers - ); - - stream.write_all(request.as_bytes())?; + let method = reqwest::Method::from_bytes(method.as_bytes()).context("invalid HTTP method")?; + let url = format!("{}{}", self.server_url.trim_end_matches('/'), path); + + let client = reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .context("failed to build HTTP client")?; + let mut request = client.request(method, url).bearer_auth(token); + if extra_headers.contains("Content-Type: application/json") { + request = request.header(reqwest::header::CONTENT_TYPE, "application/json"); + } if let Some(body) = body { - stream.write_all(body)?; + request = request.body(body.to_vec()); } - // Read response - let mut response = Vec::new(); - stream.read_to_end(&mut response)?; - - // Parse HTTP response - let response_str = String::from_utf8_lossy(&response); - let mut lines = response_str.lines(); - - // Parse status line - let status_line = lines.next().context("Empty response")?; - let status_code: u16 = status_line - .split_whitespace() - .nth(1) - .and_then(|s| s.parse().ok()) - .context("Failed to parse status code")?; - - // Find body (after empty line) - let header_end = response_str.find("\r\n\r\n").unwrap_or(response_str.len()); - let body_start = header_end + 4; - let body = if body_start < response.len() { - response[body_start..].to_vec() - } else { - Vec::new() - }; + let response = request.send().context("HTTP request failed")?; + let status_code = response.status().as_u16(); + let body = response.bytes().context("failed to read HTTP response body")?.to_vec(); Ok(ApiResponse { status_code, body }) } @@ -1928,6 +1920,21 @@ impl Drop for SubscriptionHandle { } } +fn split_server_url(server_url: &str) -> Result<(String, String)> { + match reqwest::Url::parse(server_url) { + Ok(url) => { + let protocol = url.scheme().to_string(); + let host = url.host_str().context("server URL missing host")?; + let host = match url.port() { + Some(port) => format!("{host}:{port}"), + None => host.to_string(), + }; + Ok((protocol, host)) + } + Err(_) => Ok(("http".to_string(), server_url.to_string())), + } +} + /// Normalizes whitespace by trimming trailing whitespace from each line. fn normalize_whitespace(s: &str) -> String { s.lines().map(|line| line.trim_end()).collect::>().join("\n") diff --git a/crates/smoketests/tests/smoketests/database_lock.rs b/crates/smoketests/tests/smoketests/database_lock.rs index ca106b34558..c1598be5223 100644 --- a/crates/smoketests/tests/smoketests/database_lock.rs +++ b/crates/smoketests/tests/smoketests/database_lock.rs @@ -1,4 +1,4 @@ -use spacetimedb_smoketests::Smoketest; +use spacetimedb_smoketests::{require_server_issued_login, Smoketest}; /// Test that a locked database cannot be deleted. #[test] @@ -97,6 +97,8 @@ fn test_unlock_is_idempotent() { /// Test that a non-owner cannot lock or unlock a database. #[test] fn test_non_owner_cannot_lock_or_unlock() { + require_server_issued_login!(); + let test = Smoketest::builder().precompiled_module("modules-basic").build(); let identity = test.database_identity.as_ref().unwrap().clone(); diff --git a/crates/smoketests/tests/smoketests/new_user_flow.rs b/crates/smoketests/tests/smoketests/new_user_flow.rs index 21d7a7abdd8..fb8b2e4ec25 100644 --- a/crates/smoketests/tests/smoketests/new_user_flow.rs +++ b/crates/smoketests/tests/smoketests/new_user_flow.rs @@ -1,9 +1,11 @@ -use spacetimedb_smoketests::Smoketest; +use spacetimedb_smoketests::{require_server_issued_login, Smoketest}; // TODO: This test originally was testing to make sure that our tutorial isn't broken. Since our onboarding has changed we should probably update this test in the future. /// Test the entirety of the new user flow. #[test] fn test_new_user_flow() { + require_server_issued_login!(); + let mut test = Smoketest::builder() .precompiled_module("new-user-flow") .autopublish(false) diff --git a/crates/smoketests/tests/smoketests/permissions.rs b/crates/smoketests/tests/smoketests/permissions.rs index d1c9c92b19f..03ab42d25dc 100644 --- a/crates/smoketests/tests/smoketests/permissions.rs +++ b/crates/smoketests/tests/smoketests/permissions.rs @@ -1,4 +1,4 @@ -use spacetimedb_smoketests::Smoketest; +use spacetimedb_smoketests::{require_server_issued_login, Smoketest}; use std::path::PathBuf; fn workspace_root() -> PathBuf { @@ -34,6 +34,8 @@ fn test_describe() { /// Ensure that we are not able to view the logs of a module that we don't have permission to view #[test] fn test_logs() { + require_server_issued_login!(); + let test = Smoketest::builder().precompiled_module("modules-basic").build(); // Call say_hello as owner @@ -57,6 +59,8 @@ fn test_logs() { /// Ensure that you cannot publish to an identity that you do not own #[test] fn test_publish() { + require_server_issued_login!(); + let test = Smoketest::builder().precompiled_module("modules-basic").build(); let identity = test.database_identity.as_ref().unwrap().clone(); @@ -97,6 +101,8 @@ fn test_publish() { /// Test that you can't replace names of a database you don't own #[test] fn test_replace_names() { + require_server_issued_login!(); + let mut test = Smoketest::builder() .precompiled_module("modules-basic") .autopublish(false) @@ -123,6 +129,8 @@ fn test_replace_names() { /// Ensure that a private table can only be queried by the database owner #[test] fn test_private_table() { + require_server_issued_login!(); + let test = Smoketest::builder().precompiled_module("permissions-private").build(); // Owner can query private table @@ -189,6 +197,8 @@ fn test_private_table() { /// Ensure that you cannot delete a database that you do not own #[test] fn test_cannot_delete_others_database() { + require_server_issued_login!(); + let test = Smoketest::builder().build(); let identity = test.database_identity.as_ref().unwrap().clone(); diff --git a/crates/smoketests/tests/smoketests/rls.rs b/crates/smoketests/tests/smoketests/rls.rs index 1e309c26700..5d5ffa33997 100644 --- a/crates/smoketests/tests/smoketests/rls.rs +++ b/crates/smoketests/tests/smoketests/rls.rs @@ -1,8 +1,10 @@ -use spacetimedb_smoketests::Smoketest; +use spacetimedb_smoketests::{require_server_issued_login, Smoketest}; /// Tests for querying tables with RLS rules #[test] fn test_rls_rules() { + require_server_issued_login!(); + let test = Smoketest::builder().precompiled_module("rls").build(); // Insert a user for Alice (current identity) diff --git a/crates/smoketests/tests/smoketests/timestamp_route.rs b/crates/smoketests/tests/smoketests/timestamp_route.rs index 1d337bed1f0..525af584301 100644 --- a/crates/smoketests/tests/smoketests/timestamp_route.rs +++ b/crates/smoketests/tests/smoketests/timestamp_route.rs @@ -1,10 +1,12 @@ -use spacetimedb_smoketests::{random_string, Smoketest}; +use spacetimedb_smoketests::{random_string, require_server_issued_login, Smoketest}; const TIMESTAMP_TAG: &str = "__timestamp_micros_since_unix_epoch__"; /// Test the /v1/database/{name}/unstable/timestamp endpoint #[test] fn test_timestamp_route() { + require_server_issued_login!(); + let mut test = Smoketest::builder().autopublish(false).build(); let name = random_string(); diff --git a/crates/smoketests/tests/smoketests/views.rs b/crates/smoketests/tests/smoketests/views.rs index c3fd230a8ee..4e2f9474aaf 100644 --- a/crates/smoketests/tests/smoketests/views.rs +++ b/crates/smoketests/tests/smoketests/views.rs @@ -2,7 +2,8 @@ use std::path::PathBuf; use serde_json::{json, Value}; use spacetimedb_smoketests::{ - require_dotnet, require_local_server, require_pnpm, workspace_root, ModuleLanguage, Smoketest, + require_dotnet, require_local_server, require_pnpm, require_server_issued_login, workspace_root, ModuleLanguage, + Smoketest, }; const STALE_VIEW_BACKING_TABLE_FIXTURE_IDENTITY: &str = @@ -723,6 +724,8 @@ fn test_repair_stale_anonymous_view_backing_table_on_startup() { #[test] fn test_view_accessibility() { + require_server_issued_login!(); + let test = Smoketest::builder().precompiled_module("views-callable").build(); test.new_identity().unwrap(); @@ -772,6 +775,8 @@ fn test_recovery_from_trapped_views_auto_migration() { #[test] fn test_subscribing_with_different_identities() { + require_server_issued_login!(); + let test = Smoketest::builder().precompiled_module("views-subscribe").build(); test.call("insert_player", &["Alice"]).unwrap(); @@ -1118,6 +1123,8 @@ fn expected_pk_join_projection(view_name: &str) -> Value { } fn run_pk_join_subscription_test(query: &str, projected_view_name: &str, mutations: &[PkJoinMutation]) { + require_server_issued_login!(); + let test = Smoketest::builder().precompiled_module("views-query").build(); // Seed rows for identity A in both underlying tables. diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index 456c13caa32..c9dd7899af5 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -1,7 +1,8 @@ #![allow(clippy::disallowed_macros)] -use anyhow::{bail, ensure, Result}; +use anyhow::{bail, ensure, Context, Result}; use clap::{Args, Subcommand}; use duct::cmd; +use reqwest::Url; use std::ffi::OsStr; use std::path::Path; use std::process::{Command, Stdio}; @@ -26,6 +27,24 @@ pub struct SmoketestsArgs { #[arg(long)] server: Option, + /// Use a SpacetimeAuth-issued login for remote-server tests. + /// + /// This is required for maincloud and maincloud staging, which reject + /// direct server-issued logins for privileged operations. + #[arg(long)] + spacetime_login: bool, + + /// Auth host to use with --spacetime-login. + #[arg(long)] + auth_host: Option, + + /// SpacetimeDB token to seed into each smoketest config. + /// + /// If omitted with --spacetime-login, the runner invokes `spacetime login`. + /// The SPACETIME_SMOKETEST_TOKEN environment variable is also accepted. + #[arg(long)] + token: Option, + #[arg(long, default_value_t = true, action = clap::ArgAction::Set)] dotnet: bool, @@ -55,7 +74,14 @@ pub fn run(args: SmoketestsArgs) -> Result<()> { eprintln!("smoketests/mod.rs is up to date."); Ok(()) } - None => run_smoketest(args.server, args.dotnet, args.args), + None => run_smoketest( + args.server, + args.dotnet, + args.spacetime_login, + args.auth_host, + args.token, + args.args, + ), } } @@ -126,13 +152,22 @@ fn build_precompiled_modules() -> Result<()> { /// 16 was found to be optimal - higher values cause OS scheduler overhead. const DEFAULT_PARALLELISM: &str = "16"; -fn run_smoketest(server: Option, dotnet: bool, args: Vec) -> Result<()> { +fn run_smoketest( + server: Option, + dotnet: bool, + spacetime_login: bool, + auth_host: Option, + token: Option, + args: Vec, +) -> Result<()> { // 1. Build binaries first (single process, no race) build_binaries()?; // 2. Build pre-compiled modules (this also warms the WASM dependency cache) build_precompiled_modules()?; + let base_config = prepare_spacetime_login_config(server.as_deref(), spacetime_login, auth_host, token)?; + // 4. Detect whether to use nextest or cargo test let use_nextest = Command::new("cargo") .args(["nextest", "--version"]) @@ -150,7 +185,7 @@ fn run_smoketest(server: Option, dotnet: bool, args: Vec) -> Res let mut cmd = if use_nextest { eprintln!("Running smoketests with cargo nextest...\n"); let mut cmd = Command::new("cargo"); - set_env(&mut cmd, server, dotnet); + set_env(&mut cmd, server, dotnet, base_config.as_deref()); cmd.args([ "nextest", "run", @@ -172,7 +207,7 @@ fn run_smoketest(server: Option, dotnet: bool, args: Vec) -> Res } else { eprintln!("Running smoketests with cargo test...\n"); let mut cmd = Command::new("cargo"); - set_env(&mut cmd, server, dotnet); + set_env(&mut cmd, server, dotnet, base_config.as_deref()); cmd.args(["test", "--release", "-p", "spacetimedb-smoketests"]); cmd }; @@ -187,10 +222,86 @@ fn run_smoketest(server: Option, dotnet: bool, args: Vec) -> Res Ok(()) } -fn set_env(cmd: &mut Command, server: Option, dotnet: bool) { +fn prepare_spacetime_login_config( + server: Option<&str>, + spacetime_login: bool, + auth_host: Option, + token: Option, +) -> Result> { + if !spacetime_login { + return Ok(None); + } + + let server = server.context("--spacetime-login requires --server")?; + let token = match token.or_else(|| env::var("SPACETIME_SMOKETEST_TOKEN").ok()) { + Some(token) => token, + None => login_and_read_token(auth_host.as_deref())?, + }; + + Ok(Some(smoketest_config(server, &token)?)) +} + +fn login_and_read_token(auth_host: Option<&str>) -> Result { + let temp_dir = tempfile::tempdir()?; + let config_path = temp_dir.path().join("config.toml"); + let config_path_str = config_path.to_str().context("invalid temp config path")?; + + let mut login = Command::new("target/release/spacetime"); + login.args(["--config-path", config_path_str, "login"]); + if let Some(auth_host) = auth_host { + login.args(["--auth-host", auth_host]); + } + + let status = login.status().context("failed to run spacetime login")?; + ensure!(status.success(), "spacetime login failed"); + + let output = Command::new("target/release/spacetime") + .args(["--config-path", config_path_str, "login", "show", "--token"]) + .output() + .context("failed to read spacetime login token")?; + ensure!( + output.status.success(), + "spacetime login show --token failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + stdout + .split_whitespace() + .last() + .map(str::to_string) + .context("failed to parse token from `spacetime login show --token` output") +} + +fn smoketest_config(server: &str, token: &str) -> Result { + let url = + Url::parse(server).with_context(|| format!("--server must be a URL when using --spacetime-login: {server}"))?; + let scheme = url.scheme(); + let host = url.host_str().context("--server URL must include a host")?; + let host = match url.port() { + Some(port) => format!("{host}:{port}"), + None => host.to_string(), + }; + + Ok(format!( + "default_server = \"smoketest\"\n\ + spacetimedb_token = \"{token}\"\n\n\ + [[server_configs]]\n\ + nickname = \"smoketest\"\n\ + host = \"{host}\"\n\ + protocol = \"{scheme}\"\n" + )) +} + +fn set_env(cmd: &mut Command, server: Option, dotnet: bool, base_config: Option<&str>) { if let Some(ref server_url) = server { cmd.env("SPACETIME_REMOTE_SERVER", server_url); } + if let Some(base_config) = base_config { + cmd.env("SPACETIME_SMOKETEST_BASE_CONFIG", base_config); + cmd.env("SPACETIME_SMOKETEST_SPACETIME_LOGIN", "1"); + } cmd.env("SMOKETESTS_DOTNET", if dotnet { "1" } else { "0" }); } From 95ea60b35a4df7a30858ef9398f6e64b9f89c23f Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 19:56:45 -0400 Subject: [PATCH 02/34] Use CLI login config for SpacetimeAuth smoketests --- crates/smoketests/DEVELOP.md | 10 ++--- crates/smoketests/src/lib.rs | 5 ++- tools/ci/src/smoketest.rs | 80 ++++++++---------------------------- 3 files changed, 24 insertions(+), 71 deletions(-) diff --git a/crates/smoketests/DEVELOP.md b/crates/smoketests/DEVELOP.md index dbf9a46e9ae..d89f2fe59b4 100644 --- a/crates/smoketests/DEVELOP.md +++ b/crates/smoketests/DEVELOP.md @@ -30,13 +30,13 @@ Maincloud and maincloud staging require SpacetimeAuth-issued tokens rather than server-issued tokens. Use `--spacetime-login` for those: ```bash -SPACETIME_SMOKETEST_TOKEN= \ - cargo ci smoketests --server https://maincloud.staging.spacetimedb.com --spacetime-login +cargo ci smoketests --server https://maincloud.staging.spacetimedb.com --spacetime-login ``` -If `SPACETIME_SMOKETEST_TOKEN` is omitted, the runner invokes `spacetime login`. -Tests that need throwaway server-issued identities should call -`require_server_issued_login!()` so they skip in SpacetimeAuth mode. +The runner invokes `spacetime login` once, then copies that logged-in config +into each isolated smoketest config. Tests that need throwaway server-issued +identities should call `require_server_issued_login!()` so they skip in +SpacetimeAuth mode. ### WARNING: Stale Binary Risk diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index c0c896efcd9..ac02d798db4 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -930,8 +930,9 @@ impl SmoketestBuilder { let module_name = format!("smoketest_module_{}", random_string()); let config_path = project_dir.path().join("config.toml"); - if let Ok(base_config) = std::env::var("SPACETIME_SMOKETEST_BASE_CONFIG") { - fs::write(&config_path, base_config).expect("Failed to write base smoketest config"); + if let Ok(base_config_path) = std::env::var("SPACETIME_SMOKETEST_BASE_CONFIG_PATH") { + fs::copy(&base_config_path, &config_path) + .unwrap_or_else(|err| panic!("failed to copy base smoketest config from {base_config_path}: {err:#}")); } let mut smoketest = Smoketest { guard, diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index c9dd7899af5..989a2fdacdb 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -2,11 +2,11 @@ use anyhow::{bail, ensure, Context, Result}; use clap::{Args, Subcommand}; use duct::cmd; -use reqwest::Url; use std::ffi::OsStr; use std::path::Path; use std::process::{Command, Stdio}; use std::{env, fs}; +use tempfile::TempDir; use crate::util; @@ -38,13 +38,6 @@ pub struct SmoketestsArgs { #[arg(long)] auth_host: Option, - /// SpacetimeDB token to seed into each smoketest config. - /// - /// If omitted with --spacetime-login, the runner invokes `spacetime login`. - /// The SPACETIME_SMOKETEST_TOKEN environment variable is also accepted. - #[arg(long)] - token: Option, - #[arg(long, default_value_t = true, action = clap::ArgAction::Set)] dotnet: bool, @@ -79,7 +72,6 @@ pub fn run(args: SmoketestsArgs) -> Result<()> { args.dotnet, args.spacetime_login, args.auth_host, - args.token, args.args, ), } @@ -157,7 +149,6 @@ fn run_smoketest( dotnet: bool, spacetime_login: bool, auth_host: Option, - token: Option, args: Vec, ) -> Result<()> { // 1. Build binaries first (single process, no race) @@ -166,7 +157,8 @@ fn run_smoketest( // 2. Build pre-compiled modules (this also warms the WASM dependency cache) build_precompiled_modules()?; - let base_config = prepare_spacetime_login_config(server.as_deref(), spacetime_login, auth_host, token)?; + let base_config_dir = prepare_spacetime_login_config(server.as_deref(), spacetime_login, auth_host)?; + let base_config_path = base_config_dir.as_ref().map(|dir| dir.path().join("config.toml")); // 4. Detect whether to use nextest or cargo test let use_nextest = Command::new("cargo") @@ -185,7 +177,7 @@ fn run_smoketest( let mut cmd = if use_nextest { eprintln!("Running smoketests with cargo nextest...\n"); let mut cmd = Command::new("cargo"); - set_env(&mut cmd, server, dotnet, base_config.as_deref()); + set_env(&mut cmd, server, dotnet, base_config_path.as_deref()); cmd.args([ "nextest", "run", @@ -207,7 +199,7 @@ fn run_smoketest( } else { eprintln!("Running smoketests with cargo test...\n"); let mut cmd = Command::new("cargo"); - set_env(&mut cmd, server, dotnet, base_config.as_deref()); + set_env(&mut cmd, server, dotnet, base_config_path.as_deref()); cmd.args(["test", "--release", "-p", "spacetimedb-smoketests"]); cmd }; @@ -226,80 +218,40 @@ fn prepare_spacetime_login_config( server: Option<&str>, spacetime_login: bool, auth_host: Option, - token: Option, -) -> Result> { +) -> Result> { if !spacetime_login { return Ok(None); } - let server = server.context("--spacetime-login requires --server")?; - let token = match token.or_else(|| env::var("SPACETIME_SMOKETEST_TOKEN").ok()) { - Some(token) => token, - None => login_and_read_token(auth_host.as_deref())?, - }; - - Ok(Some(smoketest_config(server, &token)?)) -} - -fn login_and_read_token(auth_host: Option<&str>) -> Result { + server.context("--spacetime-login requires --server")?; let temp_dir = tempfile::tempdir()?; let config_path = temp_dir.path().join("config.toml"); let config_path_str = config_path.to_str().context("invalid temp config path")?; + eprintln!("Logging in with SpacetimeAuth for remote smoketests..."); let mut login = Command::new("target/release/spacetime"); login.args(["--config-path", config_path_str, "login"]); - if let Some(auth_host) = auth_host { + if let Some(auth_host) = &auth_host { login.args(["--auth-host", auth_host]); } let status = login.status().context("failed to run spacetime login")?; ensure!(status.success(), "spacetime login failed"); - - let output = Command::new("target/release/spacetime") - .args(["--config-path", config_path_str, "login", "show", "--token"]) - .output() - .context("failed to read spacetime login token")?; ensure!( - output.status.success(), - "spacetime login show --token failed:\nstdout: {}\nstderr: {}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), + config_path.exists(), + "spacetime login succeeded but did not create {}", + config_path.display() ); - let stdout = String::from_utf8_lossy(&output.stdout); - stdout - .split_whitespace() - .last() - .map(str::to_string) - .context("failed to parse token from `spacetime login show --token` output") -} - -fn smoketest_config(server: &str, token: &str) -> Result { - let url = - Url::parse(server).with_context(|| format!("--server must be a URL when using --spacetime-login: {server}"))?; - let scheme = url.scheme(); - let host = url.host_str().context("--server URL must include a host")?; - let host = match url.port() { - Some(port) => format!("{host}:{port}"), - None => host.to_string(), - }; - - Ok(format!( - "default_server = \"smoketest\"\n\ - spacetimedb_token = \"{token}\"\n\n\ - [[server_configs]]\n\ - nickname = \"smoketest\"\n\ - host = \"{host}\"\n\ - protocol = \"{scheme}\"\n" - )) + Ok(Some(temp_dir)) } -fn set_env(cmd: &mut Command, server: Option, dotnet: bool, base_config: Option<&str>) { +fn set_env(cmd: &mut Command, server: Option, dotnet: bool, base_config_path: Option<&Path>) { if let Some(ref server_url) = server { cmd.env("SPACETIME_REMOTE_SERVER", server_url); } - if let Some(base_config) = base_config { - cmd.env("SPACETIME_SMOKETEST_BASE_CONFIG", base_config); + if let Some(base_config_path) = base_config_path { + cmd.env("SPACETIME_SMOKETEST_BASE_CONFIG_PATH", base_config_path); cmd.env("SPACETIME_SMOKETEST_SPACETIME_LOGIN", "1"); } cmd.env("SMOKETESTS_DOTNET", if dotnet { "1" } else { "0" }); From b905907308953b4c7ba4a431b809aad057f741f8 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 19:58:52 -0400 Subject: [PATCH 03/34] Narrow SpacetimeAuth smoketest skips --- crates/smoketests/src/lib.rs | 4 ---- crates/smoketests/tests/smoketests/database_lock.rs | 4 +--- crates/smoketests/tests/smoketests/permissions.rs | 12 +----------- crates/smoketests/tests/smoketests/rls.rs | 4 +--- .../smoketests/tests/smoketests/timestamp_route.rs | 4 +--- crates/smoketests/tests/smoketests/views.rs | 9 +-------- 6 files changed, 5 insertions(+), 32 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index ac02d798db4..819dabf3ca5 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -1661,10 +1661,6 @@ log = "0.4" /// /// This is useful for tests that need to test with multiple identities. pub fn new_identity(&self) -> Result<()> { - if is_spacetime_login() { - bail!("new_identity requires server-issued login mode"); - } - let cli_path = ensure_binaries_built(); let config_path_str = self.config_path.to_str().unwrap(); diff --git a/crates/smoketests/tests/smoketests/database_lock.rs b/crates/smoketests/tests/smoketests/database_lock.rs index c1598be5223..ca106b34558 100644 --- a/crates/smoketests/tests/smoketests/database_lock.rs +++ b/crates/smoketests/tests/smoketests/database_lock.rs @@ -1,4 +1,4 @@ -use spacetimedb_smoketests::{require_server_issued_login, Smoketest}; +use spacetimedb_smoketests::Smoketest; /// Test that a locked database cannot be deleted. #[test] @@ -97,8 +97,6 @@ fn test_unlock_is_idempotent() { /// Test that a non-owner cannot lock or unlock a database. #[test] fn test_non_owner_cannot_lock_or_unlock() { - require_server_issued_login!(); - let test = Smoketest::builder().precompiled_module("modules-basic").build(); let identity = test.database_identity.as_ref().unwrap().clone(); diff --git a/crates/smoketests/tests/smoketests/permissions.rs b/crates/smoketests/tests/smoketests/permissions.rs index 03ab42d25dc..d1c9c92b19f 100644 --- a/crates/smoketests/tests/smoketests/permissions.rs +++ b/crates/smoketests/tests/smoketests/permissions.rs @@ -1,4 +1,4 @@ -use spacetimedb_smoketests::{require_server_issued_login, Smoketest}; +use spacetimedb_smoketests::Smoketest; use std::path::PathBuf; fn workspace_root() -> PathBuf { @@ -34,8 +34,6 @@ fn test_describe() { /// Ensure that we are not able to view the logs of a module that we don't have permission to view #[test] fn test_logs() { - require_server_issued_login!(); - let test = Smoketest::builder().precompiled_module("modules-basic").build(); // Call say_hello as owner @@ -59,8 +57,6 @@ fn test_logs() { /// Ensure that you cannot publish to an identity that you do not own #[test] fn test_publish() { - require_server_issued_login!(); - let test = Smoketest::builder().precompiled_module("modules-basic").build(); let identity = test.database_identity.as_ref().unwrap().clone(); @@ -101,8 +97,6 @@ fn test_publish() { /// Test that you can't replace names of a database you don't own #[test] fn test_replace_names() { - require_server_issued_login!(); - let mut test = Smoketest::builder() .precompiled_module("modules-basic") .autopublish(false) @@ -129,8 +123,6 @@ fn test_replace_names() { /// Ensure that a private table can only be queried by the database owner #[test] fn test_private_table() { - require_server_issued_login!(); - let test = Smoketest::builder().precompiled_module("permissions-private").build(); // Owner can query private table @@ -197,8 +189,6 @@ fn test_private_table() { /// Ensure that you cannot delete a database that you do not own #[test] fn test_cannot_delete_others_database() { - require_server_issued_login!(); - let test = Smoketest::builder().build(); let identity = test.database_identity.as_ref().unwrap().clone(); diff --git a/crates/smoketests/tests/smoketests/rls.rs b/crates/smoketests/tests/smoketests/rls.rs index 5d5ffa33997..1e309c26700 100644 --- a/crates/smoketests/tests/smoketests/rls.rs +++ b/crates/smoketests/tests/smoketests/rls.rs @@ -1,10 +1,8 @@ -use spacetimedb_smoketests::{require_server_issued_login, Smoketest}; +use spacetimedb_smoketests::Smoketest; /// Tests for querying tables with RLS rules #[test] fn test_rls_rules() { - require_server_issued_login!(); - let test = Smoketest::builder().precompiled_module("rls").build(); // Insert a user for Alice (current identity) diff --git a/crates/smoketests/tests/smoketests/timestamp_route.rs b/crates/smoketests/tests/smoketests/timestamp_route.rs index 525af584301..1d337bed1f0 100644 --- a/crates/smoketests/tests/smoketests/timestamp_route.rs +++ b/crates/smoketests/tests/smoketests/timestamp_route.rs @@ -1,12 +1,10 @@ -use spacetimedb_smoketests::{random_string, require_server_issued_login, Smoketest}; +use spacetimedb_smoketests::{random_string, Smoketest}; const TIMESTAMP_TAG: &str = "__timestamp_micros_since_unix_epoch__"; /// Test the /v1/database/{name}/unstable/timestamp endpoint #[test] fn test_timestamp_route() { - require_server_issued_login!(); - let mut test = Smoketest::builder().autopublish(false).build(); let name = random_string(); diff --git a/crates/smoketests/tests/smoketests/views.rs b/crates/smoketests/tests/smoketests/views.rs index 4e2f9474aaf..c3fd230a8ee 100644 --- a/crates/smoketests/tests/smoketests/views.rs +++ b/crates/smoketests/tests/smoketests/views.rs @@ -2,8 +2,7 @@ use std::path::PathBuf; use serde_json::{json, Value}; use spacetimedb_smoketests::{ - require_dotnet, require_local_server, require_pnpm, require_server_issued_login, workspace_root, ModuleLanguage, - Smoketest, + require_dotnet, require_local_server, require_pnpm, workspace_root, ModuleLanguage, Smoketest, }; const STALE_VIEW_BACKING_TABLE_FIXTURE_IDENTITY: &str = @@ -724,8 +723,6 @@ fn test_repair_stale_anonymous_view_backing_table_on_startup() { #[test] fn test_view_accessibility() { - require_server_issued_login!(); - let test = Smoketest::builder().precompiled_module("views-callable").build(); test.new_identity().unwrap(); @@ -775,8 +772,6 @@ fn test_recovery_from_trapped_views_auto_migration() { #[test] fn test_subscribing_with_different_identities() { - require_server_issued_login!(); - let test = Smoketest::builder().precompiled_module("views-subscribe").build(); test.call("insert_player", &["Alice"]).unwrap(); @@ -1123,8 +1118,6 @@ fn expected_pk_join_projection(view_name: &str) -> Value { } fn run_pk_join_subscription_test(query: &str, projected_view_name: &str, mutations: &[PkJoinMutation]) { - require_server_issued_login!(); - let test = Smoketest::builder().precompiled_module("views-query").build(); // Seed rows for identity A in both underlying tables. From 0e22d7e29f224cdace781c1b0a900378e8a003bc Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 20:08:51 -0400 Subject: [PATCH 04/34] Match Python remote smoketest login setup --- crates/smoketests/src/lib.rs | 10 +++- tools/ci/src/smoketest.rs | 106 ++++++++++++++++++++++------------- 2 files changed, 76 insertions(+), 40 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 819dabf3ca5..7d54e3acfc3 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -1670,14 +1670,20 @@ log = "0.4" .output(); // Login with server-issued identity - // Format: login --server-issued-login + // Remote smoketest configs edit the "localhost" server alias to point at the + // remote URL, matching the old Python smoketest runner. + let login_server = if std::env::var_os("SPACETIME_SMOKETEST_BASE_CONFIG_PATH").is_some() { + "localhost" + } else { + &self.server_url + }; let output = Command::new(&cli_path) .args([ "--config-path", config_path_str, "login", "--server-issued-login", - &self.server_url, + login_server, ]) .output() .context("Failed to login with new identity")?; diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index 989a2fdacdb..3c7707d4741 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -34,10 +34,6 @@ pub struct SmoketestsArgs { #[arg(long)] spacetime_login: bool, - /// Auth host to use with --spacetime-login. - #[arg(long)] - auth_host: Option, - #[arg(long, default_value_t = true, action = clap::ArgAction::Set)] dotnet: bool, @@ -67,13 +63,7 @@ pub fn run(args: SmoketestsArgs) -> Result<()> { eprintln!("smoketests/mod.rs is up to date."); Ok(()) } - None => run_smoketest( - args.server, - args.dotnet, - args.spacetime_login, - args.auth_host, - args.args, - ), + None => run_smoketest(args.server, args.dotnet, args.spacetime_login, args.args), } } @@ -144,20 +134,14 @@ fn build_precompiled_modules() -> Result<()> { /// 16 was found to be optimal - higher values cause OS scheduler overhead. const DEFAULT_PARALLELISM: &str = "16"; -fn run_smoketest( - server: Option, - dotnet: bool, - spacetime_login: bool, - auth_host: Option, - args: Vec, -) -> Result<()> { +fn run_smoketest(server: Option, dotnet: bool, spacetime_login: bool, args: Vec) -> Result<()> { // 1. Build binaries first (single process, no race) build_binaries()?; // 2. Build pre-compiled modules (this also warms the WASM dependency cache) build_precompiled_modules()?; - let base_config_dir = prepare_spacetime_login_config(server.as_deref(), spacetime_login, auth_host)?; + let base_config_dir = prepare_remote_base_config(server.as_deref(), spacetime_login)?; let base_config_path = base_config_dir.as_ref().map(|dir| dir.path().join("config.toml")); // 4. Detect whether to use nextest or cargo test @@ -177,7 +161,7 @@ fn run_smoketest( let mut cmd = if use_nextest { eprintln!("Running smoketests with cargo nextest...\n"); let mut cmd = Command::new("cargo"); - set_env(&mut cmd, server, dotnet, base_config_path.as_deref()); + set_env(&mut cmd, server, dotnet, spacetime_login, base_config_path.as_deref()); cmd.args([ "nextest", "run", @@ -199,7 +183,7 @@ fn run_smoketest( } else { eprintln!("Running smoketests with cargo test...\n"); let mut cmd = Command::new("cargo"); - set_env(&mut cmd, server, dotnet, base_config_path.as_deref()); + set_env(&mut cmd, server, dotnet, spacetime_login, base_config_path.as_deref()); cmd.args(["test", "--release", "-p", "spacetimedb-smoketests"]); cmd }; @@ -214,44 +198,90 @@ fn run_smoketest( Ok(()) } -fn prepare_spacetime_login_config( - server: Option<&str>, - spacetime_login: bool, - auth_host: Option, -) -> Result> { - if !spacetime_login { - return Ok(None); +const DEFAULT_REMOTE_CONFIG: &str = r#"default_server = "localhost" + +[[server_configs]] +nickname = "localhost" +host = "127.0.0.1:3000" +protocol = "http" +"#; + +fn prepare_remote_base_config(server: Option<&str>, spacetime_login: bool) -> Result> { + if server.is_none() && spacetime_login { + bail!("--spacetime-login requires --server"); } + let Some(server) = server else { + return Ok(None); + }; - server.context("--spacetime-login requires --server")?; let temp_dir = tempfile::tempdir()?; let config_path = temp_dir.path().join("config.toml"); let config_path_str = config_path.to_str().context("invalid temp config path")?; + fs::write(&config_path, DEFAULT_REMOTE_CONFIG).context("failed to write base smoketest config")?; - eprintln!("Logging in with SpacetimeAuth for remote smoketests..."); - let mut login = Command::new("target/release/spacetime"); - login.args(["--config-path", config_path_str, "login"]); - if let Some(auth_host) = &auth_host { - login.args(["--auth-host", auth_host]); + let status = Command::new("target/release/spacetime") + .args([ + "--config-path", + config_path_str, + "server", + "edit", + "localhost", + "--url", + server, + "--yes", + ]) + .status() + .context("failed to edit smoketest server config")?; + ensure!(status.success(), "spacetime server edit failed"); + + if spacetime_login { + let _ = Command::new("target/release/spacetime") + .args(["--config-path", config_path_str, "logout"]) + .status(); + + eprintln!("Logging in with SpacetimeAuth for remote smoketests..."); + let status = Command::new("target/release/spacetime") + .args(["--config-path", config_path_str, "login"]) + .status() + .context("failed to run spacetime login")?; + ensure!(status.success(), "spacetime login failed"); + } else { + let status = Command::new("target/release/spacetime") + .args([ + "--config-path", + config_path_str, + "login", + "--server-issued-login", + "localhost", + ]) + .status() + .context("failed to create server-issued smoketest identity")?; + ensure!(status.success(), "spacetime login --server-issued-login failed"); } - let status = login.status().context("failed to run spacetime login")?; - ensure!(status.success(), "spacetime login failed"); ensure!( config_path.exists(), - "spacetime login succeeded but did not create {}", + "smoketest config setup succeeded but did not create {}", config_path.display() ); Ok(Some(temp_dir)) } -fn set_env(cmd: &mut Command, server: Option, dotnet: bool, base_config_path: Option<&Path>) { +fn set_env( + cmd: &mut Command, + server: Option, + dotnet: bool, + spacetime_login: bool, + base_config_path: Option<&Path>, +) { if let Some(ref server_url) = server { cmd.env("SPACETIME_REMOTE_SERVER", server_url); } if let Some(base_config_path) = base_config_path { cmd.env("SPACETIME_SMOKETEST_BASE_CONFIG_PATH", base_config_path); + } + if spacetime_login { cmd.env("SPACETIME_SMOKETEST_SPACETIME_LOGIN", "1"); } cmd.env("SMOKETESTS_DOTNET", if dotnet { "1" } else { "0" }); From 69c91d983c2a42a0124cf5c0a3359320028f1f79 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Sun, 28 Jun 2026 17:15:16 -0700 Subject: [PATCH 05/34] Apply suggestion from @bfops Signed-off-by: Zeke Foppa <196249+bfops@users.noreply.github.com> --- tools/ci/src/smoketest.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index 3c7707d4741..c74992404d4 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -235,10 +235,6 @@ fn prepare_remote_base_config(server: Option<&str>, spacetime_login: bool) -> Re ensure!(status.success(), "spacetime server edit failed"); if spacetime_login { - let _ = Command::new("target/release/spacetime") - .args(["--config-path", config_path_str, "logout"]) - .status(); - eprintln!("Logging in with SpacetimeAuth for remote smoketests..."); let status = Command::new("target/release/spacetime") .args(["--config-path", config_path_str, "login"]) From c39053a9abe4b50ef30f2c0d703b3bfeb68f52b4 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 20:32:02 -0400 Subject: [PATCH 06/34] Use reqwest for smoketest HTTP API calls --- crates/smoketests/Cargo.toml | 2 +- crates/smoketests/src/lib.rs | 84 ++++++++++++++---------------------- 2 files changed, 34 insertions(+), 52 deletions(-) diff --git a/crates/smoketests/Cargo.toml b/crates/smoketests/Cargo.toml index 1ee47e33cf0..473be886c55 100644 --- a/crates/smoketests/Cargo.toml +++ b/crates/smoketests/Cargo.toml @@ -13,6 +13,7 @@ toml.workspace = true regex.workspace = true anyhow.workspace = true fs_extra.workspace = true +reqwest = { workspace = true, features = ["blocking"] } which = "8.0.0" [dev-dependencies] @@ -22,7 +23,6 @@ assert_cmd = "2" predicates = "3" tokio.workspace = true tokio-postgres.workspace = true -reqwest = { workspace = true, features = ["blocking"] } xmltree.workspace = true [lints] diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 4d918a46321..d96aff6ec68 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -1005,10 +1005,10 @@ impl Smoketest { } pub fn login_with_token(&self, token: &str) -> Result<()> { - let host = self.server_host(); + let (protocol, host) = split_server_url(&self.server_url)?; let config_str = format!( - "default_server = \"localhost\"\n\nspacetimedb_token = \"{}\"\n\n[[server_configs]]\nnickname = \"localhost\"\nhost = \"{}\"\nprotocol = \"http\"\n", - token, host + "default_server = \"localhost\"\n\nspacetimedb_token = \"{}\"\n\n[[server_configs]]\nnickname = \"localhost\"\nhost = \"{}\"\nprotocol = \"{}\"\n", + token, host, protocol ); fs::write(&self.config_path, config_str).context("Failed to write config.toml")?; Ok(()) @@ -1701,58 +1701,25 @@ log = "0.4" body: Option<&[u8]>, extra_headers: &str, ) -> Result { - use std::io::{Read, Write}; - use std::net::TcpStream; - - // Parse server URL to get host and port - let url = &self.server_url; - let host_port = url - .strip_prefix("http://") - .or_else(|| url.strip_prefix("https://")) - .unwrap_or(url); - - let mut stream = TcpStream::connect(host_port).context("Failed to connect to server")?; - stream.set_read_timeout(Some(std::time::Duration::from_secs(30))).ok(); - - // Get auth token let token = self.read_token()?; - - // Build HTTP request - let content_length = body.map(|b| b.len()).unwrap_or(0); - let request = format!( - "{} {} HTTP/1.1\r\nHost: {}\r\nContent-Length: {}\r\nAuthorization: Bearer {}\r\n{}Connection: close\r\n\r\n", - method, path, host_port, content_length, token, extra_headers - ); - - stream.write_all(request.as_bytes())?; + let method = reqwest::Method::from_bytes(method.as_bytes()).context("invalid HTTP method")?; + let url = format!("{}{}", self.server_url.trim_end_matches('/'), path); + + let client = reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .context("failed to build HTTP client")?; + let mut request = client.request(method, url).bearer_auth(token); + if extra_headers.contains("Content-Type: application/json") { + request = request.header(reqwest::header::CONTENT_TYPE, "application/json"); + } if let Some(body) = body { - stream.write_all(body)?; + request = request.body(body.to_vec()); } - // Read response - let mut response = Vec::new(); - stream.read_to_end(&mut response)?; - - // Parse HTTP response - let response_str = String::from_utf8_lossy(&response); - let mut lines = response_str.lines(); - - // Parse status line - let status_line = lines.next().context("Empty response")?; - let status_code: u16 = status_line - .split_whitespace() - .nth(1) - .and_then(|s| s.parse().ok()) - .context("Failed to parse status code")?; - - // Find body (after empty line) - let header_end = response_str.find("\r\n\r\n").unwrap_or(response_str.len()); - let body_start = header_end + 4; - let body = if body_start < response.len() { - response[body_start..].to_vec() - } else { - Vec::new() - }; + let response = request.send().context("HTTP request failed")?; + let status_code = response.status().as_u16(); + let body = response.bytes().context("failed to read HTTP response body")?.to_vec(); Ok(ApiResponse { status_code, body }) } @@ -1928,6 +1895,21 @@ impl Drop for SubscriptionHandle { } } +fn split_server_url(server_url: &str) -> Result<(String, String)> { + match reqwest::Url::parse(server_url) { + Ok(url) => { + let protocol = url.scheme().to_string(); + let host = url.host_str().context("server URL missing host")?; + let host = match url.port() { + Some(port) => format!("{host}:{port}"), + None => host.to_string(), + }; + Ok((protocol, host)) + } + Err(_) => Ok(("http".to_string(), server_url.to_string())), + } +} + /// Normalizes whitespace by trimming trailing whitespace from each line. fn normalize_whitespace(s: &str) -> String { s.lines().map(|line| line.trim_end()).collect::>().join("\n") From ed2e99b157474ff09b2e6d79960419e0e4ebbaa9 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 16:18:59 -0400 Subject: [PATCH 07/34] Support SpacetimeAuth smoketest runs --- crates/smoketests/DEVELOP.md | 20 +++ crates/smoketests/src/lib.rs | 25 ++++ .../tests/smoketests/database_lock.rs | 4 +- .../tests/smoketests/new_user_flow.rs | 4 +- .../tests/smoketests/permissions.rs | 12 +- crates/smoketests/tests/smoketests/rls.rs | 4 +- .../tests/smoketests/timestamp_route.rs | 4 +- crates/smoketests/tests/smoketests/views.rs | 9 +- tools/ci/src/smoketest.rs | 123 +++++++++++++++++- 9 files changed, 193 insertions(+), 12 deletions(-) diff --git a/crates/smoketests/DEVELOP.md b/crates/smoketests/DEVELOP.md index 423c038f6ec..dbf9a46e9ae 100644 --- a/crates/smoketests/DEVELOP.md +++ b/crates/smoketests/DEVELOP.md @@ -18,6 +18,26 @@ cargo smoketest test_sql_format cargo smoketest "cli::" # Run all CLI tests ``` +### Remote Servers + +Run against a standalone-compatible remote server with: + +```bash +cargo ci smoketests --server https://example.spacetimedb.com +``` + +Maincloud and maincloud staging require SpacetimeAuth-issued tokens rather than +server-issued tokens. Use `--spacetime-login` for those: + +```bash +SPACETIME_SMOKETEST_TOKEN= \ + cargo ci smoketests --server https://maincloud.staging.spacetimedb.com --spacetime-login +``` + +If `SPACETIME_SMOKETEST_TOKEN` is omitted, the runner invokes `spacetime login`. +Tests that need throwaway server-issued identities should call +`require_server_issued_login!()` so they skip in SpacetimeAuth mode. + ### WARNING: Stale Binary Risk **Smoketests use pre-built binaries and DO NOT automatically rebuild them.** diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index d96aff6ec68..c0c896efcd9 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -78,6 +78,11 @@ pub fn is_remote_server() -> bool { remote_server_url().is_some() } +/// Returns true if remote smoketests are using a SpacetimeAuth-issued token. +pub fn is_spacetime_login() -> bool { + std::env::var("SPACETIME_SMOKETEST_SPACETIME_LOGIN").ok().as_deref() == Some("1") +} + /// Skip this test if running against a remote server. /// /// Use this macro at the start of tests that require a local server, @@ -107,6 +112,19 @@ macro_rules! require_local_server { }; } +#[macro_export] +macro_rules! require_server_issued_login { + () => { + if $crate::is_spacetime_login() { + #[allow(clippy::disallowed_macros)] + { + eprintln!("Skipping test: requires server-issued throwaway identities"); + } + return; + } + }; +} + #[macro_export] macro_rules! require_dotnet { () => { @@ -912,6 +930,9 @@ impl SmoketestBuilder { let module_name = format!("smoketest_module_{}", random_string()); let config_path = project_dir.path().join("config.toml"); + if let Ok(base_config) = std::env::var("SPACETIME_SMOKETEST_BASE_CONFIG") { + fs::write(&config_path, base_config).expect("Failed to write base smoketest config"); + } let mut smoketest = Smoketest { guard, _data_dir_fixture: data_dir_fixture, @@ -1639,6 +1660,10 @@ log = "0.4" /// /// This is useful for tests that need to test with multiple identities. pub fn new_identity(&self) -> Result<()> { + if is_spacetime_login() { + bail!("new_identity requires server-issued login mode"); + } + let cli_path = ensure_binaries_built(); let config_path_str = self.config_path.to_str().unwrap(); diff --git a/crates/smoketests/tests/smoketests/database_lock.rs b/crates/smoketests/tests/smoketests/database_lock.rs index ca106b34558..c1598be5223 100644 --- a/crates/smoketests/tests/smoketests/database_lock.rs +++ b/crates/smoketests/tests/smoketests/database_lock.rs @@ -1,4 +1,4 @@ -use spacetimedb_smoketests::Smoketest; +use spacetimedb_smoketests::{require_server_issued_login, Smoketest}; /// Test that a locked database cannot be deleted. #[test] @@ -97,6 +97,8 @@ fn test_unlock_is_idempotent() { /// Test that a non-owner cannot lock or unlock a database. #[test] fn test_non_owner_cannot_lock_or_unlock() { + require_server_issued_login!(); + let test = Smoketest::builder().precompiled_module("modules-basic").build(); let identity = test.database_identity.as_ref().unwrap().clone(); diff --git a/crates/smoketests/tests/smoketests/new_user_flow.rs b/crates/smoketests/tests/smoketests/new_user_flow.rs index 21d7a7abdd8..fb8b2e4ec25 100644 --- a/crates/smoketests/tests/smoketests/new_user_flow.rs +++ b/crates/smoketests/tests/smoketests/new_user_flow.rs @@ -1,9 +1,11 @@ -use spacetimedb_smoketests::Smoketest; +use spacetimedb_smoketests::{require_server_issued_login, Smoketest}; // TODO: This test originally was testing to make sure that our tutorial isn't broken. Since our onboarding has changed we should probably update this test in the future. /// Test the entirety of the new user flow. #[test] fn test_new_user_flow() { + require_server_issued_login!(); + let mut test = Smoketest::builder() .precompiled_module("new-user-flow") .autopublish(false) diff --git a/crates/smoketests/tests/smoketests/permissions.rs b/crates/smoketests/tests/smoketests/permissions.rs index d1c9c92b19f..03ab42d25dc 100644 --- a/crates/smoketests/tests/smoketests/permissions.rs +++ b/crates/smoketests/tests/smoketests/permissions.rs @@ -1,4 +1,4 @@ -use spacetimedb_smoketests::Smoketest; +use spacetimedb_smoketests::{require_server_issued_login, Smoketest}; use std::path::PathBuf; fn workspace_root() -> PathBuf { @@ -34,6 +34,8 @@ fn test_describe() { /// Ensure that we are not able to view the logs of a module that we don't have permission to view #[test] fn test_logs() { + require_server_issued_login!(); + let test = Smoketest::builder().precompiled_module("modules-basic").build(); // Call say_hello as owner @@ -57,6 +59,8 @@ fn test_logs() { /// Ensure that you cannot publish to an identity that you do not own #[test] fn test_publish() { + require_server_issued_login!(); + let test = Smoketest::builder().precompiled_module("modules-basic").build(); let identity = test.database_identity.as_ref().unwrap().clone(); @@ -97,6 +101,8 @@ fn test_publish() { /// Test that you can't replace names of a database you don't own #[test] fn test_replace_names() { + require_server_issued_login!(); + let mut test = Smoketest::builder() .precompiled_module("modules-basic") .autopublish(false) @@ -123,6 +129,8 @@ fn test_replace_names() { /// Ensure that a private table can only be queried by the database owner #[test] fn test_private_table() { + require_server_issued_login!(); + let test = Smoketest::builder().precompiled_module("permissions-private").build(); // Owner can query private table @@ -189,6 +197,8 @@ fn test_private_table() { /// Ensure that you cannot delete a database that you do not own #[test] fn test_cannot_delete_others_database() { + require_server_issued_login!(); + let test = Smoketest::builder().build(); let identity = test.database_identity.as_ref().unwrap().clone(); diff --git a/crates/smoketests/tests/smoketests/rls.rs b/crates/smoketests/tests/smoketests/rls.rs index 1e309c26700..5d5ffa33997 100644 --- a/crates/smoketests/tests/smoketests/rls.rs +++ b/crates/smoketests/tests/smoketests/rls.rs @@ -1,8 +1,10 @@ -use spacetimedb_smoketests::Smoketest; +use spacetimedb_smoketests::{require_server_issued_login, Smoketest}; /// Tests for querying tables with RLS rules #[test] fn test_rls_rules() { + require_server_issued_login!(); + let test = Smoketest::builder().precompiled_module("rls").build(); // Insert a user for Alice (current identity) diff --git a/crates/smoketests/tests/smoketests/timestamp_route.rs b/crates/smoketests/tests/smoketests/timestamp_route.rs index 1d337bed1f0..525af584301 100644 --- a/crates/smoketests/tests/smoketests/timestamp_route.rs +++ b/crates/smoketests/tests/smoketests/timestamp_route.rs @@ -1,10 +1,12 @@ -use spacetimedb_smoketests::{random_string, Smoketest}; +use spacetimedb_smoketests::{random_string, require_server_issued_login, Smoketest}; const TIMESTAMP_TAG: &str = "__timestamp_micros_since_unix_epoch__"; /// Test the /v1/database/{name}/unstable/timestamp endpoint #[test] fn test_timestamp_route() { + require_server_issued_login!(); + let mut test = Smoketest::builder().autopublish(false).build(); let name = random_string(); diff --git a/crates/smoketests/tests/smoketests/views.rs b/crates/smoketests/tests/smoketests/views.rs index c3fd230a8ee..4e2f9474aaf 100644 --- a/crates/smoketests/tests/smoketests/views.rs +++ b/crates/smoketests/tests/smoketests/views.rs @@ -2,7 +2,8 @@ use std::path::PathBuf; use serde_json::{json, Value}; use spacetimedb_smoketests::{ - require_dotnet, require_local_server, require_pnpm, workspace_root, ModuleLanguage, Smoketest, + require_dotnet, require_local_server, require_pnpm, require_server_issued_login, workspace_root, ModuleLanguage, + Smoketest, }; const STALE_VIEW_BACKING_TABLE_FIXTURE_IDENTITY: &str = @@ -723,6 +724,8 @@ fn test_repair_stale_anonymous_view_backing_table_on_startup() { #[test] fn test_view_accessibility() { + require_server_issued_login!(); + let test = Smoketest::builder().precompiled_module("views-callable").build(); test.new_identity().unwrap(); @@ -772,6 +775,8 @@ fn test_recovery_from_trapped_views_auto_migration() { #[test] fn test_subscribing_with_different_identities() { + require_server_issued_login!(); + let test = Smoketest::builder().precompiled_module("views-subscribe").build(); test.call("insert_player", &["Alice"]).unwrap(); @@ -1118,6 +1123,8 @@ fn expected_pk_join_projection(view_name: &str) -> Value { } fn run_pk_join_subscription_test(query: &str, projected_view_name: &str, mutations: &[PkJoinMutation]) { + require_server_issued_login!(); + let test = Smoketest::builder().precompiled_module("views-query").build(); // Seed rows for identity A in both underlying tables. diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index 456c13caa32..c9dd7899af5 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -1,7 +1,8 @@ #![allow(clippy::disallowed_macros)] -use anyhow::{bail, ensure, Result}; +use anyhow::{bail, ensure, Context, Result}; use clap::{Args, Subcommand}; use duct::cmd; +use reqwest::Url; use std::ffi::OsStr; use std::path::Path; use std::process::{Command, Stdio}; @@ -26,6 +27,24 @@ pub struct SmoketestsArgs { #[arg(long)] server: Option, + /// Use a SpacetimeAuth-issued login for remote-server tests. + /// + /// This is required for maincloud and maincloud staging, which reject + /// direct server-issued logins for privileged operations. + #[arg(long)] + spacetime_login: bool, + + /// Auth host to use with --spacetime-login. + #[arg(long)] + auth_host: Option, + + /// SpacetimeDB token to seed into each smoketest config. + /// + /// If omitted with --spacetime-login, the runner invokes `spacetime login`. + /// The SPACETIME_SMOKETEST_TOKEN environment variable is also accepted. + #[arg(long)] + token: Option, + #[arg(long, default_value_t = true, action = clap::ArgAction::Set)] dotnet: bool, @@ -55,7 +74,14 @@ pub fn run(args: SmoketestsArgs) -> Result<()> { eprintln!("smoketests/mod.rs is up to date."); Ok(()) } - None => run_smoketest(args.server, args.dotnet, args.args), + None => run_smoketest( + args.server, + args.dotnet, + args.spacetime_login, + args.auth_host, + args.token, + args.args, + ), } } @@ -126,13 +152,22 @@ fn build_precompiled_modules() -> Result<()> { /// 16 was found to be optimal - higher values cause OS scheduler overhead. const DEFAULT_PARALLELISM: &str = "16"; -fn run_smoketest(server: Option, dotnet: bool, args: Vec) -> Result<()> { +fn run_smoketest( + server: Option, + dotnet: bool, + spacetime_login: bool, + auth_host: Option, + token: Option, + args: Vec, +) -> Result<()> { // 1. Build binaries first (single process, no race) build_binaries()?; // 2. Build pre-compiled modules (this also warms the WASM dependency cache) build_precompiled_modules()?; + let base_config = prepare_spacetime_login_config(server.as_deref(), spacetime_login, auth_host, token)?; + // 4. Detect whether to use nextest or cargo test let use_nextest = Command::new("cargo") .args(["nextest", "--version"]) @@ -150,7 +185,7 @@ fn run_smoketest(server: Option, dotnet: bool, args: Vec) -> Res let mut cmd = if use_nextest { eprintln!("Running smoketests with cargo nextest...\n"); let mut cmd = Command::new("cargo"); - set_env(&mut cmd, server, dotnet); + set_env(&mut cmd, server, dotnet, base_config.as_deref()); cmd.args([ "nextest", "run", @@ -172,7 +207,7 @@ fn run_smoketest(server: Option, dotnet: bool, args: Vec) -> Res } else { eprintln!("Running smoketests with cargo test...\n"); let mut cmd = Command::new("cargo"); - set_env(&mut cmd, server, dotnet); + set_env(&mut cmd, server, dotnet, base_config.as_deref()); cmd.args(["test", "--release", "-p", "spacetimedb-smoketests"]); cmd }; @@ -187,10 +222,86 @@ fn run_smoketest(server: Option, dotnet: bool, args: Vec) -> Res Ok(()) } -fn set_env(cmd: &mut Command, server: Option, dotnet: bool) { +fn prepare_spacetime_login_config( + server: Option<&str>, + spacetime_login: bool, + auth_host: Option, + token: Option, +) -> Result> { + if !spacetime_login { + return Ok(None); + } + + let server = server.context("--spacetime-login requires --server")?; + let token = match token.or_else(|| env::var("SPACETIME_SMOKETEST_TOKEN").ok()) { + Some(token) => token, + None => login_and_read_token(auth_host.as_deref())?, + }; + + Ok(Some(smoketest_config(server, &token)?)) +} + +fn login_and_read_token(auth_host: Option<&str>) -> Result { + let temp_dir = tempfile::tempdir()?; + let config_path = temp_dir.path().join("config.toml"); + let config_path_str = config_path.to_str().context("invalid temp config path")?; + + let mut login = Command::new("target/release/spacetime"); + login.args(["--config-path", config_path_str, "login"]); + if let Some(auth_host) = auth_host { + login.args(["--auth-host", auth_host]); + } + + let status = login.status().context("failed to run spacetime login")?; + ensure!(status.success(), "spacetime login failed"); + + let output = Command::new("target/release/spacetime") + .args(["--config-path", config_path_str, "login", "show", "--token"]) + .output() + .context("failed to read spacetime login token")?; + ensure!( + output.status.success(), + "spacetime login show --token failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + + let stdout = String::from_utf8_lossy(&output.stdout); + stdout + .split_whitespace() + .last() + .map(str::to_string) + .context("failed to parse token from `spacetime login show --token` output") +} + +fn smoketest_config(server: &str, token: &str) -> Result { + let url = + Url::parse(server).with_context(|| format!("--server must be a URL when using --spacetime-login: {server}"))?; + let scheme = url.scheme(); + let host = url.host_str().context("--server URL must include a host")?; + let host = match url.port() { + Some(port) => format!("{host}:{port}"), + None => host.to_string(), + }; + + Ok(format!( + "default_server = \"smoketest\"\n\ + spacetimedb_token = \"{token}\"\n\n\ + [[server_configs]]\n\ + nickname = \"smoketest\"\n\ + host = \"{host}\"\n\ + protocol = \"{scheme}\"\n" + )) +} + +fn set_env(cmd: &mut Command, server: Option, dotnet: bool, base_config: Option<&str>) { if let Some(ref server_url) = server { cmd.env("SPACETIME_REMOTE_SERVER", server_url); } + if let Some(base_config) = base_config { + cmd.env("SPACETIME_SMOKETEST_BASE_CONFIG", base_config); + cmd.env("SPACETIME_SMOKETEST_SPACETIME_LOGIN", "1"); + } cmd.env("SMOKETESTS_DOTNET", if dotnet { "1" } else { "0" }); } From a1cf45437e867cfe1de1b28fe689016c8630698e Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 19:56:45 -0400 Subject: [PATCH 08/34] Use CLI login config for SpacetimeAuth smoketests --- crates/smoketests/DEVELOP.md | 10 ++--- crates/smoketests/src/lib.rs | 5 ++- tools/ci/src/smoketest.rs | 80 ++++++++---------------------------- 3 files changed, 24 insertions(+), 71 deletions(-) diff --git a/crates/smoketests/DEVELOP.md b/crates/smoketests/DEVELOP.md index dbf9a46e9ae..d89f2fe59b4 100644 --- a/crates/smoketests/DEVELOP.md +++ b/crates/smoketests/DEVELOP.md @@ -30,13 +30,13 @@ Maincloud and maincloud staging require SpacetimeAuth-issued tokens rather than server-issued tokens. Use `--spacetime-login` for those: ```bash -SPACETIME_SMOKETEST_TOKEN= \ - cargo ci smoketests --server https://maincloud.staging.spacetimedb.com --spacetime-login +cargo ci smoketests --server https://maincloud.staging.spacetimedb.com --spacetime-login ``` -If `SPACETIME_SMOKETEST_TOKEN` is omitted, the runner invokes `spacetime login`. -Tests that need throwaway server-issued identities should call -`require_server_issued_login!()` so they skip in SpacetimeAuth mode. +The runner invokes `spacetime login` once, then copies that logged-in config +into each isolated smoketest config. Tests that need throwaway server-issued +identities should call `require_server_issued_login!()` so they skip in +SpacetimeAuth mode. ### WARNING: Stale Binary Risk diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index c0c896efcd9..ac02d798db4 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -930,8 +930,9 @@ impl SmoketestBuilder { let module_name = format!("smoketest_module_{}", random_string()); let config_path = project_dir.path().join("config.toml"); - if let Ok(base_config) = std::env::var("SPACETIME_SMOKETEST_BASE_CONFIG") { - fs::write(&config_path, base_config).expect("Failed to write base smoketest config"); + if let Ok(base_config_path) = std::env::var("SPACETIME_SMOKETEST_BASE_CONFIG_PATH") { + fs::copy(&base_config_path, &config_path) + .unwrap_or_else(|err| panic!("failed to copy base smoketest config from {base_config_path}: {err:#}")); } let mut smoketest = Smoketest { guard, diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index c9dd7899af5..989a2fdacdb 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -2,11 +2,11 @@ use anyhow::{bail, ensure, Context, Result}; use clap::{Args, Subcommand}; use duct::cmd; -use reqwest::Url; use std::ffi::OsStr; use std::path::Path; use std::process::{Command, Stdio}; use std::{env, fs}; +use tempfile::TempDir; use crate::util; @@ -38,13 +38,6 @@ pub struct SmoketestsArgs { #[arg(long)] auth_host: Option, - /// SpacetimeDB token to seed into each smoketest config. - /// - /// If omitted with --spacetime-login, the runner invokes `spacetime login`. - /// The SPACETIME_SMOKETEST_TOKEN environment variable is also accepted. - #[arg(long)] - token: Option, - #[arg(long, default_value_t = true, action = clap::ArgAction::Set)] dotnet: bool, @@ -79,7 +72,6 @@ pub fn run(args: SmoketestsArgs) -> Result<()> { args.dotnet, args.spacetime_login, args.auth_host, - args.token, args.args, ), } @@ -157,7 +149,6 @@ fn run_smoketest( dotnet: bool, spacetime_login: bool, auth_host: Option, - token: Option, args: Vec, ) -> Result<()> { // 1. Build binaries first (single process, no race) @@ -166,7 +157,8 @@ fn run_smoketest( // 2. Build pre-compiled modules (this also warms the WASM dependency cache) build_precompiled_modules()?; - let base_config = prepare_spacetime_login_config(server.as_deref(), spacetime_login, auth_host, token)?; + let base_config_dir = prepare_spacetime_login_config(server.as_deref(), spacetime_login, auth_host)?; + let base_config_path = base_config_dir.as_ref().map(|dir| dir.path().join("config.toml")); // 4. Detect whether to use nextest or cargo test let use_nextest = Command::new("cargo") @@ -185,7 +177,7 @@ fn run_smoketest( let mut cmd = if use_nextest { eprintln!("Running smoketests with cargo nextest...\n"); let mut cmd = Command::new("cargo"); - set_env(&mut cmd, server, dotnet, base_config.as_deref()); + set_env(&mut cmd, server, dotnet, base_config_path.as_deref()); cmd.args([ "nextest", "run", @@ -207,7 +199,7 @@ fn run_smoketest( } else { eprintln!("Running smoketests with cargo test...\n"); let mut cmd = Command::new("cargo"); - set_env(&mut cmd, server, dotnet, base_config.as_deref()); + set_env(&mut cmd, server, dotnet, base_config_path.as_deref()); cmd.args(["test", "--release", "-p", "spacetimedb-smoketests"]); cmd }; @@ -226,80 +218,40 @@ fn prepare_spacetime_login_config( server: Option<&str>, spacetime_login: bool, auth_host: Option, - token: Option, -) -> Result> { +) -> Result> { if !spacetime_login { return Ok(None); } - let server = server.context("--spacetime-login requires --server")?; - let token = match token.or_else(|| env::var("SPACETIME_SMOKETEST_TOKEN").ok()) { - Some(token) => token, - None => login_and_read_token(auth_host.as_deref())?, - }; - - Ok(Some(smoketest_config(server, &token)?)) -} - -fn login_and_read_token(auth_host: Option<&str>) -> Result { + server.context("--spacetime-login requires --server")?; let temp_dir = tempfile::tempdir()?; let config_path = temp_dir.path().join("config.toml"); let config_path_str = config_path.to_str().context("invalid temp config path")?; + eprintln!("Logging in with SpacetimeAuth for remote smoketests..."); let mut login = Command::new("target/release/spacetime"); login.args(["--config-path", config_path_str, "login"]); - if let Some(auth_host) = auth_host { + if let Some(auth_host) = &auth_host { login.args(["--auth-host", auth_host]); } let status = login.status().context("failed to run spacetime login")?; ensure!(status.success(), "spacetime login failed"); - - let output = Command::new("target/release/spacetime") - .args(["--config-path", config_path_str, "login", "show", "--token"]) - .output() - .context("failed to read spacetime login token")?; ensure!( - output.status.success(), - "spacetime login show --token failed:\nstdout: {}\nstderr: {}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), + config_path.exists(), + "spacetime login succeeded but did not create {}", + config_path.display() ); - let stdout = String::from_utf8_lossy(&output.stdout); - stdout - .split_whitespace() - .last() - .map(str::to_string) - .context("failed to parse token from `spacetime login show --token` output") -} - -fn smoketest_config(server: &str, token: &str) -> Result { - let url = - Url::parse(server).with_context(|| format!("--server must be a URL when using --spacetime-login: {server}"))?; - let scheme = url.scheme(); - let host = url.host_str().context("--server URL must include a host")?; - let host = match url.port() { - Some(port) => format!("{host}:{port}"), - None => host.to_string(), - }; - - Ok(format!( - "default_server = \"smoketest\"\n\ - spacetimedb_token = \"{token}\"\n\n\ - [[server_configs]]\n\ - nickname = \"smoketest\"\n\ - host = \"{host}\"\n\ - protocol = \"{scheme}\"\n" - )) + Ok(Some(temp_dir)) } -fn set_env(cmd: &mut Command, server: Option, dotnet: bool, base_config: Option<&str>) { +fn set_env(cmd: &mut Command, server: Option, dotnet: bool, base_config_path: Option<&Path>) { if let Some(ref server_url) = server { cmd.env("SPACETIME_REMOTE_SERVER", server_url); } - if let Some(base_config) = base_config { - cmd.env("SPACETIME_SMOKETEST_BASE_CONFIG", base_config); + if let Some(base_config_path) = base_config_path { + cmd.env("SPACETIME_SMOKETEST_BASE_CONFIG_PATH", base_config_path); cmd.env("SPACETIME_SMOKETEST_SPACETIME_LOGIN", "1"); } cmd.env("SMOKETESTS_DOTNET", if dotnet { "1" } else { "0" }); From 21e8e350eaca5e7204f9b35b05a89265b2115a34 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 19:58:52 -0400 Subject: [PATCH 09/34] Narrow SpacetimeAuth smoketest skips --- crates/smoketests/src/lib.rs | 4 ---- crates/smoketests/tests/smoketests/database_lock.rs | 4 +--- crates/smoketests/tests/smoketests/permissions.rs | 12 +----------- crates/smoketests/tests/smoketests/rls.rs | 4 +--- .../smoketests/tests/smoketests/timestamp_route.rs | 4 +--- crates/smoketests/tests/smoketests/views.rs | 9 +-------- 6 files changed, 5 insertions(+), 32 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index ac02d798db4..819dabf3ca5 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -1661,10 +1661,6 @@ log = "0.4" /// /// This is useful for tests that need to test with multiple identities. pub fn new_identity(&self) -> Result<()> { - if is_spacetime_login() { - bail!("new_identity requires server-issued login mode"); - } - let cli_path = ensure_binaries_built(); let config_path_str = self.config_path.to_str().unwrap(); diff --git a/crates/smoketests/tests/smoketests/database_lock.rs b/crates/smoketests/tests/smoketests/database_lock.rs index c1598be5223..ca106b34558 100644 --- a/crates/smoketests/tests/smoketests/database_lock.rs +++ b/crates/smoketests/tests/smoketests/database_lock.rs @@ -1,4 +1,4 @@ -use spacetimedb_smoketests::{require_server_issued_login, Smoketest}; +use spacetimedb_smoketests::Smoketest; /// Test that a locked database cannot be deleted. #[test] @@ -97,8 +97,6 @@ fn test_unlock_is_idempotent() { /// Test that a non-owner cannot lock or unlock a database. #[test] fn test_non_owner_cannot_lock_or_unlock() { - require_server_issued_login!(); - let test = Smoketest::builder().precompiled_module("modules-basic").build(); let identity = test.database_identity.as_ref().unwrap().clone(); diff --git a/crates/smoketests/tests/smoketests/permissions.rs b/crates/smoketests/tests/smoketests/permissions.rs index 03ab42d25dc..d1c9c92b19f 100644 --- a/crates/smoketests/tests/smoketests/permissions.rs +++ b/crates/smoketests/tests/smoketests/permissions.rs @@ -1,4 +1,4 @@ -use spacetimedb_smoketests::{require_server_issued_login, Smoketest}; +use spacetimedb_smoketests::Smoketest; use std::path::PathBuf; fn workspace_root() -> PathBuf { @@ -34,8 +34,6 @@ fn test_describe() { /// Ensure that we are not able to view the logs of a module that we don't have permission to view #[test] fn test_logs() { - require_server_issued_login!(); - let test = Smoketest::builder().precompiled_module("modules-basic").build(); // Call say_hello as owner @@ -59,8 +57,6 @@ fn test_logs() { /// Ensure that you cannot publish to an identity that you do not own #[test] fn test_publish() { - require_server_issued_login!(); - let test = Smoketest::builder().precompiled_module("modules-basic").build(); let identity = test.database_identity.as_ref().unwrap().clone(); @@ -101,8 +97,6 @@ fn test_publish() { /// Test that you can't replace names of a database you don't own #[test] fn test_replace_names() { - require_server_issued_login!(); - let mut test = Smoketest::builder() .precompiled_module("modules-basic") .autopublish(false) @@ -129,8 +123,6 @@ fn test_replace_names() { /// Ensure that a private table can only be queried by the database owner #[test] fn test_private_table() { - require_server_issued_login!(); - let test = Smoketest::builder().precompiled_module("permissions-private").build(); // Owner can query private table @@ -197,8 +189,6 @@ fn test_private_table() { /// Ensure that you cannot delete a database that you do not own #[test] fn test_cannot_delete_others_database() { - require_server_issued_login!(); - let test = Smoketest::builder().build(); let identity = test.database_identity.as_ref().unwrap().clone(); diff --git a/crates/smoketests/tests/smoketests/rls.rs b/crates/smoketests/tests/smoketests/rls.rs index 5d5ffa33997..1e309c26700 100644 --- a/crates/smoketests/tests/smoketests/rls.rs +++ b/crates/smoketests/tests/smoketests/rls.rs @@ -1,10 +1,8 @@ -use spacetimedb_smoketests::{require_server_issued_login, Smoketest}; +use spacetimedb_smoketests::Smoketest; /// Tests for querying tables with RLS rules #[test] fn test_rls_rules() { - require_server_issued_login!(); - let test = Smoketest::builder().precompiled_module("rls").build(); // Insert a user for Alice (current identity) diff --git a/crates/smoketests/tests/smoketests/timestamp_route.rs b/crates/smoketests/tests/smoketests/timestamp_route.rs index 525af584301..1d337bed1f0 100644 --- a/crates/smoketests/tests/smoketests/timestamp_route.rs +++ b/crates/smoketests/tests/smoketests/timestamp_route.rs @@ -1,12 +1,10 @@ -use spacetimedb_smoketests::{random_string, require_server_issued_login, Smoketest}; +use spacetimedb_smoketests::{random_string, Smoketest}; const TIMESTAMP_TAG: &str = "__timestamp_micros_since_unix_epoch__"; /// Test the /v1/database/{name}/unstable/timestamp endpoint #[test] fn test_timestamp_route() { - require_server_issued_login!(); - let mut test = Smoketest::builder().autopublish(false).build(); let name = random_string(); diff --git a/crates/smoketests/tests/smoketests/views.rs b/crates/smoketests/tests/smoketests/views.rs index 4e2f9474aaf..c3fd230a8ee 100644 --- a/crates/smoketests/tests/smoketests/views.rs +++ b/crates/smoketests/tests/smoketests/views.rs @@ -2,8 +2,7 @@ use std::path::PathBuf; use serde_json::{json, Value}; use spacetimedb_smoketests::{ - require_dotnet, require_local_server, require_pnpm, require_server_issued_login, workspace_root, ModuleLanguage, - Smoketest, + require_dotnet, require_local_server, require_pnpm, workspace_root, ModuleLanguage, Smoketest, }; const STALE_VIEW_BACKING_TABLE_FIXTURE_IDENTITY: &str = @@ -724,8 +723,6 @@ fn test_repair_stale_anonymous_view_backing_table_on_startup() { #[test] fn test_view_accessibility() { - require_server_issued_login!(); - let test = Smoketest::builder().precompiled_module("views-callable").build(); test.new_identity().unwrap(); @@ -775,8 +772,6 @@ fn test_recovery_from_trapped_views_auto_migration() { #[test] fn test_subscribing_with_different_identities() { - require_server_issued_login!(); - let test = Smoketest::builder().precompiled_module("views-subscribe").build(); test.call("insert_player", &["Alice"]).unwrap(); @@ -1123,8 +1118,6 @@ fn expected_pk_join_projection(view_name: &str) -> Value { } fn run_pk_join_subscription_test(query: &str, projected_view_name: &str, mutations: &[PkJoinMutation]) { - require_server_issued_login!(); - let test = Smoketest::builder().precompiled_module("views-query").build(); // Seed rows for identity A in both underlying tables. From 5b832a4aa013bb0edd2655ba3e110b61e860f0d9 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 20:08:51 -0400 Subject: [PATCH 10/34] Match Python remote smoketest login setup --- crates/smoketests/src/lib.rs | 10 +++- tools/ci/src/smoketest.rs | 106 ++++++++++++++++++++++------------- 2 files changed, 76 insertions(+), 40 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 819dabf3ca5..7d54e3acfc3 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -1670,14 +1670,20 @@ log = "0.4" .output(); // Login with server-issued identity - // Format: login --server-issued-login + // Remote smoketest configs edit the "localhost" server alias to point at the + // remote URL, matching the old Python smoketest runner. + let login_server = if std::env::var_os("SPACETIME_SMOKETEST_BASE_CONFIG_PATH").is_some() { + "localhost" + } else { + &self.server_url + }; let output = Command::new(&cli_path) .args([ "--config-path", config_path_str, "login", "--server-issued-login", - &self.server_url, + login_server, ]) .output() .context("Failed to login with new identity")?; diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index 989a2fdacdb..3c7707d4741 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -34,10 +34,6 @@ pub struct SmoketestsArgs { #[arg(long)] spacetime_login: bool, - /// Auth host to use with --spacetime-login. - #[arg(long)] - auth_host: Option, - #[arg(long, default_value_t = true, action = clap::ArgAction::Set)] dotnet: bool, @@ -67,13 +63,7 @@ pub fn run(args: SmoketestsArgs) -> Result<()> { eprintln!("smoketests/mod.rs is up to date."); Ok(()) } - None => run_smoketest( - args.server, - args.dotnet, - args.spacetime_login, - args.auth_host, - args.args, - ), + None => run_smoketest(args.server, args.dotnet, args.spacetime_login, args.args), } } @@ -144,20 +134,14 @@ fn build_precompiled_modules() -> Result<()> { /// 16 was found to be optimal - higher values cause OS scheduler overhead. const DEFAULT_PARALLELISM: &str = "16"; -fn run_smoketest( - server: Option, - dotnet: bool, - spacetime_login: bool, - auth_host: Option, - args: Vec, -) -> Result<()> { +fn run_smoketest(server: Option, dotnet: bool, spacetime_login: bool, args: Vec) -> Result<()> { // 1. Build binaries first (single process, no race) build_binaries()?; // 2. Build pre-compiled modules (this also warms the WASM dependency cache) build_precompiled_modules()?; - let base_config_dir = prepare_spacetime_login_config(server.as_deref(), spacetime_login, auth_host)?; + let base_config_dir = prepare_remote_base_config(server.as_deref(), spacetime_login)?; let base_config_path = base_config_dir.as_ref().map(|dir| dir.path().join("config.toml")); // 4. Detect whether to use nextest or cargo test @@ -177,7 +161,7 @@ fn run_smoketest( let mut cmd = if use_nextest { eprintln!("Running smoketests with cargo nextest...\n"); let mut cmd = Command::new("cargo"); - set_env(&mut cmd, server, dotnet, base_config_path.as_deref()); + set_env(&mut cmd, server, dotnet, spacetime_login, base_config_path.as_deref()); cmd.args([ "nextest", "run", @@ -199,7 +183,7 @@ fn run_smoketest( } else { eprintln!("Running smoketests with cargo test...\n"); let mut cmd = Command::new("cargo"); - set_env(&mut cmd, server, dotnet, base_config_path.as_deref()); + set_env(&mut cmd, server, dotnet, spacetime_login, base_config_path.as_deref()); cmd.args(["test", "--release", "-p", "spacetimedb-smoketests"]); cmd }; @@ -214,44 +198,90 @@ fn run_smoketest( Ok(()) } -fn prepare_spacetime_login_config( - server: Option<&str>, - spacetime_login: bool, - auth_host: Option, -) -> Result> { - if !spacetime_login { - return Ok(None); +const DEFAULT_REMOTE_CONFIG: &str = r#"default_server = "localhost" + +[[server_configs]] +nickname = "localhost" +host = "127.0.0.1:3000" +protocol = "http" +"#; + +fn prepare_remote_base_config(server: Option<&str>, spacetime_login: bool) -> Result> { + if server.is_none() && spacetime_login { + bail!("--spacetime-login requires --server"); } + let Some(server) = server else { + return Ok(None); + }; - server.context("--spacetime-login requires --server")?; let temp_dir = tempfile::tempdir()?; let config_path = temp_dir.path().join("config.toml"); let config_path_str = config_path.to_str().context("invalid temp config path")?; + fs::write(&config_path, DEFAULT_REMOTE_CONFIG).context("failed to write base smoketest config")?; - eprintln!("Logging in with SpacetimeAuth for remote smoketests..."); - let mut login = Command::new("target/release/spacetime"); - login.args(["--config-path", config_path_str, "login"]); - if let Some(auth_host) = &auth_host { - login.args(["--auth-host", auth_host]); + let status = Command::new("target/release/spacetime") + .args([ + "--config-path", + config_path_str, + "server", + "edit", + "localhost", + "--url", + server, + "--yes", + ]) + .status() + .context("failed to edit smoketest server config")?; + ensure!(status.success(), "spacetime server edit failed"); + + if spacetime_login { + let _ = Command::new("target/release/spacetime") + .args(["--config-path", config_path_str, "logout"]) + .status(); + + eprintln!("Logging in with SpacetimeAuth for remote smoketests..."); + let status = Command::new("target/release/spacetime") + .args(["--config-path", config_path_str, "login"]) + .status() + .context("failed to run spacetime login")?; + ensure!(status.success(), "spacetime login failed"); + } else { + let status = Command::new("target/release/spacetime") + .args([ + "--config-path", + config_path_str, + "login", + "--server-issued-login", + "localhost", + ]) + .status() + .context("failed to create server-issued smoketest identity")?; + ensure!(status.success(), "spacetime login --server-issued-login failed"); } - let status = login.status().context("failed to run spacetime login")?; - ensure!(status.success(), "spacetime login failed"); ensure!( config_path.exists(), - "spacetime login succeeded but did not create {}", + "smoketest config setup succeeded but did not create {}", config_path.display() ); Ok(Some(temp_dir)) } -fn set_env(cmd: &mut Command, server: Option, dotnet: bool, base_config_path: Option<&Path>) { +fn set_env( + cmd: &mut Command, + server: Option, + dotnet: bool, + spacetime_login: bool, + base_config_path: Option<&Path>, +) { if let Some(ref server_url) = server { cmd.env("SPACETIME_REMOTE_SERVER", server_url); } if let Some(base_config_path) = base_config_path { cmd.env("SPACETIME_SMOKETEST_BASE_CONFIG_PATH", base_config_path); + } + if spacetime_login { cmd.env("SPACETIME_SMOKETEST_SPACETIME_LOGIN", "1"); } cmd.env("SMOKETESTS_DOTNET", if dotnet { "1" } else { "0" }); From bf7af0ed2320d068209dd1800bcce50a7d9b848e Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Sun, 28 Jun 2026 17:15:16 -0700 Subject: [PATCH 11/34] Apply suggestion from @bfops Signed-off-by: Zeke Foppa <196249+bfops@users.noreply.github.com> --- tools/ci/src/smoketest.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index 3c7707d4741..c74992404d4 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -235,10 +235,6 @@ fn prepare_remote_base_config(server: Option<&str>, spacetime_login: bool) -> Re ensure!(status.success(), "spacetime server edit failed"); if spacetime_login { - let _ = Command::new("target/release/spacetime") - .args(["--config-path", config_path_str, "logout"]) - .status(); - eprintln!("Logging in with SpacetimeAuth for remote smoketests..."); let status = Command::new("target/release/spacetime") .args(["--config-path", config_path_str, "login"]) From b54aa9c9c3b07a3989a75f826d4459a2c55e38b0 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 20:34:07 -0400 Subject: [PATCH 12/34] Generate smoketest base config via CLI --- crates/smoketests/src/lib.rs | 2 +- tools/ci/README.md | 4 +++ tools/ci/src/smoketest.rs | 68 +++++++++++++++++------------------- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 7d54e3acfc3..55b97478168 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -1672,7 +1672,7 @@ log = "0.4" // Login with server-issued identity // Remote smoketest configs edit the "localhost" server alias to point at the // remote URL, matching the old Python smoketest runner. - let login_server = if std::env::var_os("SPACETIME_SMOKETEST_BASE_CONFIG_PATH").is_some() { + let login_server = if is_remote_server() { "localhost" } else { &self.server_url diff --git a/tools/ci/README.md b/tools/ci/README.md index 980f290c572..734d592fa5a 100644 --- a/tools/ci/README.md +++ b/tools/ci/README.md @@ -102,6 +102,10 @@ Usage: smoketests [OPTIONS] [ARGS]... [COMMAND] When specified, tests will connect to the given URL instead of starting local server instances. Tests that require local server control (like restart tests) will be skipped. +- `--spacetime-login`: Use a SpacetimeAuth-issued login for remote-server tests. + +This is required for maincloud and maincloud staging, which reject direct server-issued logins for privileged operations. + - `--dotnet `: - `args `: Additional arguments to pass to the test runner - `--help`: Print help (see a summary with '-h') diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index c74992404d4..bd4125fb58f 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -141,8 +141,8 @@ fn run_smoketest(server: Option, dotnet: bool, spacetime_login: bool, ar // 2. Build pre-compiled modules (this also warms the WASM dependency cache) build_precompiled_modules()?; - let base_config_dir = prepare_remote_base_config(server.as_deref(), spacetime_login)?; - let base_config_path = base_config_dir.as_ref().map(|dir| dir.path().join("config.toml")); + let base_config_dir = prepare_base_config(server.as_deref(), spacetime_login)?; + let base_config_path = base_config_dir.path().join("config.toml"); // 4. Detect whether to use nextest or cargo test let use_nextest = Command::new("cargo") @@ -161,7 +161,7 @@ fn run_smoketest(server: Option, dotnet: bool, spacetime_login: bool, ar let mut cmd = if use_nextest { eprintln!("Running smoketests with cargo nextest...\n"); let mut cmd = Command::new("cargo"); - set_env(&mut cmd, server, dotnet, spacetime_login, base_config_path.as_deref()); + set_env(&mut cmd, server, dotnet, spacetime_login, &base_config_path); cmd.args([ "nextest", "run", @@ -183,7 +183,7 @@ fn run_smoketest(server: Option, dotnet: bool, spacetime_login: bool, ar } else { eprintln!("Running smoketests with cargo test...\n"); let mut cmd = Command::new("cargo"); - set_env(&mut cmd, server, dotnet, spacetime_login, base_config_path.as_deref()); + set_env(&mut cmd, server, dotnet, spacetime_login, &base_config_path); cmd.args(["test", "--release", "-p", "spacetimedb-smoketests"]); cmd }; @@ -198,41 +198,47 @@ fn run_smoketest(server: Option, dotnet: bool, spacetime_login: bool, ar Ok(()) } -const DEFAULT_REMOTE_CONFIG: &str = r#"default_server = "localhost" - -[[server_configs]] -nickname = "localhost" -host = "127.0.0.1:3000" -protocol = "http" -"#; - -fn prepare_remote_base_config(server: Option<&str>, spacetime_login: bool) -> Result> { +fn prepare_base_config(server: Option<&str>, spacetime_login: bool) -> Result { if server.is_none() && spacetime_login { bail!("--spacetime-login requires --server"); } - let Some(server) = server else { - return Ok(None); - }; let temp_dir = tempfile::tempdir()?; let config_path = temp_dir.path().join("config.toml"); let config_path_str = config_path.to_str().context("invalid temp config path")?; - fs::write(&config_path, DEFAULT_REMOTE_CONFIG).context("failed to write base smoketest config")?; let status = Command::new("target/release/spacetime") .args([ "--config-path", config_path_str, "server", - "edit", - "localhost", + "add", "--url", - server, - "--yes", + "http://127.0.0.1:3000", + "--no-fingerprint", + "--default", + "localhost", ]) .status() - .context("failed to edit smoketest server config")?; - ensure!(status.success(), "spacetime server edit failed"); + .context("failed to initialize smoketest server config")?; + ensure!(status.success(), "spacetime server add failed"); + + if let Some(server) = server { + let status = Command::new("target/release/spacetime") + .args([ + "--config-path", + config_path_str, + "server", + "edit", + "localhost", + "--url", + server, + "--yes", + ]) + .status() + .context("failed to edit smoketest server config")?; + ensure!(status.success(), "spacetime server edit failed"); + } if spacetime_login { eprintln!("Logging in with SpacetimeAuth for remote smoketests..."); @@ -241,7 +247,7 @@ fn prepare_remote_base_config(server: Option<&str>, spacetime_login: bool) -> Re .status() .context("failed to run spacetime login")?; ensure!(status.success(), "spacetime login failed"); - } else { + } else if server.is_some() { let status = Command::new("target/release/spacetime") .args([ "--config-path", @@ -261,22 +267,14 @@ fn prepare_remote_base_config(server: Option<&str>, spacetime_login: bool) -> Re config_path.display() ); - Ok(Some(temp_dir)) + Ok(temp_dir) } -fn set_env( - cmd: &mut Command, - server: Option, - dotnet: bool, - spacetime_login: bool, - base_config_path: Option<&Path>, -) { +fn set_env(cmd: &mut Command, server: Option, dotnet: bool, spacetime_login: bool, base_config_path: &Path) { if let Some(ref server_url) = server { cmd.env("SPACETIME_REMOTE_SERVER", server_url); } - if let Some(base_config_path) = base_config_path { - cmd.env("SPACETIME_SMOKETEST_BASE_CONFIG_PATH", base_config_path); - } + cmd.env("SPACETIME_SMOKETEST_BASE_CONFIG_PATH", base_config_path); if spacetime_login { cmd.env("SPACETIME_SMOKETEST_SPACETIME_LOGIN", "1"); } From 473c01ba8e0c4310bdebff837a9b17680a85401d Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Sun, 28 Jun 2026 17:36:39 -0700 Subject: [PATCH 13/34] Apply suggestion from @bfops Signed-off-by: Zeke Foppa <196249+bfops@users.noreply.github.com> --- tools/ci/src/smoketest.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index bd4125fb58f..d59404b1608 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -275,9 +275,7 @@ fn set_env(cmd: &mut Command, server: Option, dotnet: bool, spacetime_lo cmd.env("SPACETIME_REMOTE_SERVER", server_url); } cmd.env("SPACETIME_SMOKETEST_BASE_CONFIG_PATH", base_config_path); - if spacetime_login { - cmd.env("SPACETIME_SMOKETEST_SPACETIME_LOGIN", "1"); - } + cmd.env("SPACETIME_SMOKETEST_SPACETIME_LOGIN", if spacetime_login { "1" } else { "0" }); cmd.env("SMOKETESTS_DOTNET", if dotnet { "1" } else { "0" }); } From dc9c0156cca00d23ae098ac12d39cc47df3b8e71 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 20:43:25 -0400 Subject: [PATCH 14/34] Reuse server URL parser for smoketest host --- crates/smoketests/src/lib.rs | 10 ++++------ crates/smoketests/tests/smoketests/pg_wire.rs | 3 ++- crates/smoketests/tests/smoketests/quickstart.rs | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index d96aff6ec68..bc8499ed7ab 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -975,11 +975,8 @@ impl Smoketest { } /// Returns the server host (without protocol), e.g., "127.0.0.1:3000". - pub fn server_host(&self) -> &str { - self.server_url - .strip_prefix("http://") - .or_else(|| self.server_url.strip_prefix("https://")) - .unwrap_or(&self.server_url) + pub fn server_host(&self) -> Result { + split_server_url(&self.server_url).map(|(_, host)| host) } /// Returns the PostgreSQL wire protocol port, if enabled. @@ -1026,7 +1023,8 @@ impl Smoketest { let pg_port = self.pg_port().context("PostgreSQL wire protocol not enabled")?; // Extract just the host part (without port) - let host = self.server_host().split(':').next().unwrap_or("127.0.0.1"); + let server_host = self.server_host()?; + let host = server_host.split(':').next().unwrap_or("127.0.0.1"); let output = Command::new("psql") .args([ diff --git a/crates/smoketests/tests/smoketests/pg_wire.rs b/crates/smoketests/tests/smoketests/pg_wire.rs index 4c69b475aee..b9c00b5409f 100644 --- a/crates/smoketests/tests/smoketests/pg_wire.rs +++ b/crates/smoketests/tests/smoketests/pg_wire.rs @@ -91,7 +91,8 @@ fn test_sql_conn() { let token = test.read_token().unwrap(); let pg_port = test.pg_port().expect("PostgreSQL wire protocol not enabled"); - let host = test.server_host().split(':').next().unwrap_or("127.0.0.1"); + let server_host = test.server_host().unwrap(); + let host = server_host.split(':').next().unwrap_or("127.0.0.1"); let mut cfg = tokio_postgres::Config::new(); cfg.host(host); diff --git a/crates/smoketests/tests/smoketests/quickstart.rs b/crates/smoketests/tests/smoketests/quickstart.rs index cfa4f88400e..efcbc01345e 100644 --- a/crates/smoketests/tests/smoketests/quickstart.rs +++ b/crates/smoketests/tests/smoketests/quickstart.rs @@ -765,7 +765,7 @@ log = "0.4" main_code.push_str(self.config.extra_code); // Replace server address - let host = self.test.server_host(); + let host = self.test.server_host()?; let protocol = "http"; // The smoketest server uses http main_code = main_code.replace("http://localhost:3000", &format!("{}://{}", protocol, host)); From 021ad62a31922203fe1b12248d3ada31dedfce83 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 20:45:45 -0400 Subject: [PATCH 15/34] Make smoketest URL splitting infallible --- crates/smoketests/src/lib.rs | 33 ++++++++++--------- crates/smoketests/tests/smoketests/pg_wire.rs | 2 +- .../smoketests/tests/smoketests/quickstart.rs | 2 +- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index bc8499ed7ab..9794624d40b 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -975,8 +975,9 @@ impl Smoketest { } /// Returns the server host (without protocol), e.g., "127.0.0.1:3000". - pub fn server_host(&self) -> Result { - split_server_url(&self.server_url).map(|(_, host)| host) + pub fn server_host(&self) -> String { + let (_, host) = split_server_url(&self.server_url); + host } /// Returns the PostgreSQL wire protocol port, if enabled. @@ -1002,7 +1003,7 @@ impl Smoketest { } pub fn login_with_token(&self, token: &str) -> Result<()> { - let (protocol, host) = split_server_url(&self.server_url)?; + let (protocol, host) = split_server_url(&self.server_url); let config_str = format!( "default_server = \"localhost\"\n\nspacetimedb_token = \"{}\"\n\n[[server_configs]]\nnickname = \"localhost\"\nhost = \"{}\"\nprotocol = \"{}\"\n", token, host, protocol @@ -1023,7 +1024,7 @@ impl Smoketest { let pg_port = self.pg_port().context("PostgreSQL wire protocol not enabled")?; // Extract just the host part (without port) - let server_host = self.server_host()?; + let server_host = self.server_host(); let host = server_host.split(':').next().unwrap_or("127.0.0.1"); let output = Command::new("psql") @@ -1893,18 +1894,18 @@ impl Drop for SubscriptionHandle { } } -fn split_server_url(server_url: &str) -> Result<(String, String)> { - match reqwest::Url::parse(server_url) { - Ok(url) => { - let protocol = url.scheme().to_string(); - let host = url.host_str().context("server URL missing host")?; - let host = match url.port() { - Some(port) => format!("{host}:{port}"), - None => host.to_string(), - }; - Ok((protocol, host)) - } - Err(_) => Ok(("http".to_string(), server_url.to_string())), +fn split_server_url(server_url: &str) -> (String, String) { + if let Ok(url) = reqwest::Url::parse(server_url) + && let Some(host) = url.host_str() + { + let protocol = url.scheme().to_string(); + let host = match url.port() { + Some(port) => format!("{host}:{port}"), + None => host.to_string(), + }; + (protocol, host) + } else { + ("http".to_string(), server_url.to_string()) } } diff --git a/crates/smoketests/tests/smoketests/pg_wire.rs b/crates/smoketests/tests/smoketests/pg_wire.rs index b9c00b5409f..c80ab85b03f 100644 --- a/crates/smoketests/tests/smoketests/pg_wire.rs +++ b/crates/smoketests/tests/smoketests/pg_wire.rs @@ -91,7 +91,7 @@ fn test_sql_conn() { let token = test.read_token().unwrap(); let pg_port = test.pg_port().expect("PostgreSQL wire protocol not enabled"); - let server_host = test.server_host().unwrap(); + let server_host = test.server_host(); let host = server_host.split(':').next().unwrap_or("127.0.0.1"); let mut cfg = tokio_postgres::Config::new(); diff --git a/crates/smoketests/tests/smoketests/quickstart.rs b/crates/smoketests/tests/smoketests/quickstart.rs index efcbc01345e..cfa4f88400e 100644 --- a/crates/smoketests/tests/smoketests/quickstart.rs +++ b/crates/smoketests/tests/smoketests/quickstart.rs @@ -765,7 +765,7 @@ log = "0.4" main_code.push_str(self.config.extra_code); // Replace server address - let host = self.test.server_host()?; + let host = self.test.server_host(); let protocol = "http"; // The smoketest server uses http main_code = main_code.replace("http://localhost:3000", &format!("{}://{}", protocol, host)); From f80704f47cc573d84b47167182366de6451056f2 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 20:49:23 -0400 Subject: [PATCH 16/34] Keep smoketest server host borrowed --- crates/smoketests/src/lib.rs | 22 +++++++------------ crates/smoketests/tests/smoketests/pg_wire.rs | 3 +-- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 9794624d40b..aaca9c59df8 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -975,7 +975,7 @@ impl Smoketest { } /// Returns the server host (without protocol), e.g., "127.0.0.1:3000". - pub fn server_host(&self) -> String { + pub fn server_host(&self) -> &str { let (_, host) = split_server_url(&self.server_url); host } @@ -1024,8 +1024,7 @@ impl Smoketest { let pg_port = self.pg_port().context("PostgreSQL wire protocol not enabled")?; // Extract just the host part (without port) - let server_host = self.server_host(); - let host = server_host.split(':').next().unwrap_or("127.0.0.1"); + let host = self.server_host().split(':').next().unwrap_or("127.0.0.1"); let output = Command::new("psql") .args([ @@ -1894,18 +1893,13 @@ impl Drop for SubscriptionHandle { } } -fn split_server_url(server_url: &str) -> (String, String) { - if let Ok(url) = reqwest::Url::parse(server_url) - && let Some(host) = url.host_str() - { - let protocol = url.scheme().to_string(); - let host = match url.port() { - Some(port) => format!("{host}:{port}"), - None => host.to_string(), - }; - (protocol, host) +fn split_server_url(server_url: &str) -> (&str, &str) { + if let Some(host) = server_url.strip_prefix("http://") { + ("http", host) + } else if let Some(host) = server_url.strip_prefix("https://") { + ("https", host) } else { - ("http".to_string(), server_url.to_string()) + ("http", server_url) } } diff --git a/crates/smoketests/tests/smoketests/pg_wire.rs b/crates/smoketests/tests/smoketests/pg_wire.rs index c80ab85b03f..4c69b475aee 100644 --- a/crates/smoketests/tests/smoketests/pg_wire.rs +++ b/crates/smoketests/tests/smoketests/pg_wire.rs @@ -91,8 +91,7 @@ fn test_sql_conn() { let token = test.read_token().unwrap(); let pg_port = test.pg_port().expect("PostgreSQL wire protocol not enabled"); - let server_host = test.server_host(); - let host = server_host.split(':').next().unwrap_or("127.0.0.1"); + let host = test.server_host().split(':').next().unwrap_or("127.0.0.1"); let mut cfg = tokio_postgres::Config::new(); cfg.host(host); From baeaff151f26383e285163e3690240b92a379d09 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 20:55:17 -0400 Subject: [PATCH 17/34] Use default CLI server config for smoketests --- crates/smoketests/src/lib.rs | 10 +++------- tools/ci/src/smoketest.rs | 23 ++++++++--------------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 4e61d0972d2..87c1240beeb 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -1668,13 +1668,9 @@ log = "0.4" .output(); // Login with server-issued identity - // Remote smoketest configs edit the "localhost" server alias to point at the - // remote URL, matching the old Python smoketest runner. - let login_server = if is_remote_server() { - "localhost" - } else { - &self.server_url - }; + // Remote smoketest configs edit the default "local" server alias to point at the + // remote URL, matching the old Python smoketest runner's named-server flow. + let login_server = if is_remote_server() { "local" } else { &self.server_url }; let output = Command::new(&cli_path) .args([ "--config-path", diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index d59404b1608..e30fe050079 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -208,20 +208,10 @@ fn prepare_base_config(server: Option<&str>, spacetime_login: bool) -> Result, spacetime_login: bool) -> Result, spacetime_login: bool) -> Result, dotnet: bool, spacetime_lo cmd.env("SPACETIME_REMOTE_SERVER", server_url); } cmd.env("SPACETIME_SMOKETEST_BASE_CONFIG_PATH", base_config_path); - cmd.env("SPACETIME_SMOKETEST_SPACETIME_LOGIN", if spacetime_login { "1" } else { "0" }); + cmd.env( + "SPACETIME_SMOKETEST_SPACETIME_LOGIN", + if spacetime_login { "1" } else { "0" }, + ); cmd.env("SMOKETESTS_DOTNET", if dotnet { "1" } else { "0" }); } From be17627fc4a315c284d02c301d946eb5be8ae23c Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 20:58:59 -0400 Subject: [PATCH 18/34] Use server URL for new smoketest identities --- crates/smoketests/src/lib.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 87c1240beeb..2df4c8c6211 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -1668,16 +1668,13 @@ log = "0.4" .output(); // Login with server-issued identity - // Remote smoketest configs edit the default "local" server alias to point at the - // remote URL, matching the old Python smoketest runner's named-server flow. - let login_server = if is_remote_server() { "local" } else { &self.server_url }; let output = Command::new(&cli_path) .args([ "--config-path", config_path_str, "login", "--server-issued-login", - login_server, + &self.server_url, ]) .output() .context("Failed to login with new identity")?; From 3bcdaf2b01d35daf2d98c11bda413816e859d219 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Sun, 28 Jun 2026 18:00:44 -0700 Subject: [PATCH 19/34] Apply suggestion from @bfops Signed-off-by: Zeke Foppa <196249+bfops@users.noreply.github.com> --- tools/ci/src/smoketest.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index e30fe050079..7e853373765 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -207,6 +207,7 @@ fn prepare_base_config(server: Option<&str>, spacetime_login: bool) -> Result Date: Sun, 28 Jun 2026 21:03:11 -0400 Subject: [PATCH 20/34] Restore server-issued login format note --- crates/smoketests/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 2df4c8c6211..461d52bbc0d 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -1668,6 +1668,7 @@ log = "0.4" .output(); // Login with server-issued identity + // Format: login --server-issued-login let output = Command::new(&cli_path) .args([ "--config-path", From fcc55298b918f977a0969b0c02d0980e6c83969b Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Sun, 28 Jun 2026 18:09:12 -0700 Subject: [PATCH 21/34] [bot/smoketest-spacetime-login]: small tweak --- tools/ci/src/smoketest.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index 7e853373765..ce5f9eecfee 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -2,6 +2,7 @@ use anyhow::{bail, ensure, Context, Result}; use clap::{Args, Subcommand}; use duct::cmd; +use spacetimedb_guard::ensure_binaries_built; use std::ffi::OsStr; use std::path::Path; use std::process::{Command, Stdio}; @@ -141,7 +142,8 @@ fn run_smoketest(server: Option, dotnet: bool, spacetime_login: bool, ar // 2. Build pre-compiled modules (this also warms the WASM dependency cache) build_precompiled_modules()?; - let base_config_dir = prepare_base_config(server.as_deref(), spacetime_login)?; + let cli_path = ensure_binaries_built(); + let base_config_dir = prepare_base_config(&cli_path, server.as_deref(), spacetime_login)?; let base_config_path = base_config_dir.path().join("config.toml"); // 4. Detect whether to use nextest or cargo test @@ -198,7 +200,7 @@ fn run_smoketest(server: Option, dotnet: bool, spacetime_login: bool, ar Ok(()) } -fn prepare_base_config(server: Option<&str>, spacetime_login: bool) -> Result { +fn prepare_base_config(cli_path: &Path, server: Option<&str>, spacetime_login: bool) -> Result { if server.is_none() && spacetime_login { bail!("--spacetime-login requires --server"); } @@ -208,14 +210,14 @@ fn prepare_base_config(server: Option<&str>, spacetime_login: bool) -> Result, spacetime_login: bool) -> Result Date: Sun, 28 Jun 2026 18:19:09 -0700 Subject: [PATCH 22/34] [bot/smoketest-spacetime-login]: some extra --yes --- .../tests/smoketests/cli/publish.rs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/smoketests/tests/smoketests/cli/publish.rs b/crates/smoketests/tests/smoketests/cli/publish.rs index 50b32ce3e80..468a4fcb7ac 100644 --- a/crates/smoketests/tests/smoketests/cli/publish.rs +++ b/crates/smoketests/tests/smoketests/cli/publish.rs @@ -15,13 +15,31 @@ fn cli_can_publish_spacetimedb_on_disk() { .join("spacetimedb"); let dir = dir.to_string(); + // Needed when running smoketests against a remote server. let _ = test - .spacetime(&["publish", "--module-path", &dir, "--server", &test.server_url, "foobar"]) + .spacetime(&[ + "publish", + "--module-path", + &dir, + "--server", + &test.server_url, + "--yes=remote", + "foobar", + ]) .unwrap(); // Can republish without error to the same name + // Needed when running smoketests against a remote server. let _ = test - .spacetime(&["publish", "--module-path", &dir, "--server", &test.server_url, "foobar"]) + .spacetime(&[ + "publish", + "--module-path", + &dir, + "--server", + &test.server_url, + "--yes=remote", + "foobar", + ]) .unwrap(); } @@ -228,10 +246,12 @@ fn cli_publish_with_config_but_no_match_uses_cli_args() { std::fs::write(module_dir.join("spacetime.json"), config_content).expect("failed to write config"); // Publish with a different database name from CLI - should use CLI args, not config + // Needed when running smoketests against a remote server. test.spacetime(&[ "publish", "--server", &test.server_url, + "--yes=remote", "cli-db-name", "--module-path", module_dir.to_str().unwrap(), From a89bfce231d96e86346382545b19f50d225e14a1 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Sun, 28 Jun 2026 18:32:26 -0700 Subject: [PATCH 23/34] [bot/smoketest-spacetime-login]: fix auth host --- tools/ci/README.md | 4 ++- tools/ci/src/smoketest.rs | 54 +++++++++++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/tools/ci/README.md b/tools/ci/README.md index 734d592fa5a..08dd7fa6338 100644 --- a/tools/ci/README.md +++ b/tools/ci/README.md @@ -102,10 +102,12 @@ Usage: smoketests [OPTIONS] [ARGS]... [COMMAND] When specified, tests will connect to the given URL instead of starting local server instances. Tests that require local server control (like restart tests) will be skipped. -- `--spacetime-login`: Use a SpacetimeAuth-issued login for remote-server tests. +- `--spacetime-login `: Use a SpacetimeAuth-issued login for remote-server tests. This is required for maincloud and maincloud staging, which reject direct server-issued logins for privileged operations. +Optionally accepts an auth host to pass through to `spacetime login`, for example `--spacetime-login=https://maincloud.staging.spacetimedb.com`. + - `--dotnet `: - `args `: Additional arguments to pass to the test runner - `--help`: Print help (see a summary with '-h') diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index ce5f9eecfee..c2a2dfaa394 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -32,8 +32,11 @@ pub struct SmoketestsArgs { /// /// This is required for maincloud and maincloud staging, which reject /// direct server-issued logins for privileged operations. - #[arg(long)] - spacetime_login: bool, + /// + /// Optionally accepts an auth host to pass through to `spacetime login`, + /// for example `--spacetime-login=https://maincloud.staging.spacetimedb.com`. + #[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "")] + spacetime_login: Option, #[arg(long, default_value_t = true, action = clap::ArgAction::Set)] dotnet: bool, @@ -64,7 +67,7 @@ pub fn run(args: SmoketestsArgs) -> Result<()> { eprintln!("smoketests/mod.rs is up to date."); Ok(()) } - None => run_smoketest(args.server, args.dotnet, args.spacetime_login, args.args), + None => run_smoketest(args.server, args.dotnet, args.spacetime_login.as_deref(), args.args), } } @@ -135,7 +138,12 @@ fn build_precompiled_modules() -> Result<()> { /// 16 was found to be optimal - higher values cause OS scheduler overhead. const DEFAULT_PARALLELISM: &str = "16"; -fn run_smoketest(server: Option, dotnet: bool, spacetime_login: bool, args: Vec) -> Result<()> { +fn run_smoketest( + server: Option, + dotnet: bool, + spacetime_login_auth_host: Option<&str>, + args: Vec, +) -> Result<()> { // 1. Build binaries first (single process, no race) build_binaries()?; @@ -143,7 +151,7 @@ fn run_smoketest(server: Option, dotnet: bool, spacetime_login: bool, ar build_precompiled_modules()?; let cli_path = ensure_binaries_built(); - let base_config_dir = prepare_base_config(&cli_path, server.as_deref(), spacetime_login)?; + let base_config_dir = prepare_base_config(&cli_path, server.as_deref(), spacetime_login_auth_host)?; let base_config_path = base_config_dir.path().join("config.toml"); // 4. Detect whether to use nextest or cargo test @@ -163,7 +171,13 @@ fn run_smoketest(server: Option, dotnet: bool, spacetime_login: bool, ar let mut cmd = if use_nextest { eprintln!("Running smoketests with cargo nextest...\n"); let mut cmd = Command::new("cargo"); - set_env(&mut cmd, server, dotnet, spacetime_login, &base_config_path); + set_env( + &mut cmd, + server, + dotnet, + spacetime_login_auth_host.is_some(), + &base_config_path, + ); cmd.args([ "nextest", "run", @@ -185,7 +199,13 @@ fn run_smoketest(server: Option, dotnet: bool, spacetime_login: bool, ar } else { eprintln!("Running smoketests with cargo test...\n"); let mut cmd = Command::new("cargo"); - set_env(&mut cmd, server, dotnet, spacetime_login, &base_config_path); + set_env( + &mut cmd, + server, + dotnet, + spacetime_login_auth_host.is_some(), + &base_config_path, + ); cmd.args(["test", "--release", "-p", "spacetimedb-smoketests"]); cmd }; @@ -200,8 +220,12 @@ fn run_smoketest(server: Option, dotnet: bool, spacetime_login: bool, ar Ok(()) } -fn prepare_base_config(cli_path: &Path, server: Option<&str>, spacetime_login: bool) -> Result { - if server.is_none() && spacetime_login { +fn prepare_base_config( + cli_path: &Path, + server: Option<&str>, + spacetime_login_auth_host: Option<&str>, +) -> Result { + if server.is_none() && spacetime_login_auth_host.is_some() { bail!("--spacetime-login requires --server"); } @@ -233,12 +257,14 @@ fn prepare_base_config(cli_path: &Path, server: Option<&str>, spacetime_login: b ensure!(status.success(), "spacetime server edit failed"); } - if spacetime_login { + if let Some(auth_host) = spacetime_login_auth_host { eprintln!("Logging in with SpacetimeAuth for remote smoketests..."); - let status = Command::new(cli_path) - .args(["--config-path", config_path_str, "login"]) - .status() - .context("failed to run spacetime login")?; + let mut login = Command::new(cli_path); + login.args(["--config-path", config_path_str, "login"]); + if !auth_host.is_empty() { + login.args(["--auth-host", auth_host]); + } + let status = login.status().context("failed to run spacetime login")?; ensure!(status.success(), "spacetime login failed"); } else if server.is_some() { let status = Command::new(cli_path) From 2119217e883b66d5bcd7d8707379a35899197ef4 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Sun, 28 Jun 2026 18:44:42 -0700 Subject: [PATCH 24/34] [bot/smoketest-spacetime-login]: fix some tests --- crates/smoketests/tests/smoketests/quickstart.rs | 6 +++++- crates/smoketests/tests/smoketests/timestamp_route.rs | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/smoketests/tests/smoketests/quickstart.rs b/crates/smoketests/tests/smoketests/quickstart.rs index cfa4f88400e..a6700f5ff06 100644 --- a/crates/smoketests/tests/smoketests/quickstart.rs +++ b/crates/smoketests/tests/smoketests/quickstart.rs @@ -766,7 +766,11 @@ log = "0.4" // Replace server address let host = self.test.server_host(); - let protocol = "http"; // The smoketest server uses http + let protocol = if self.test.server_url.starts_with("https://") { + "https" + } else { + "http" + }; main_code = main_code.replace("http://localhost:3000", &format!("{}://{}", protocol, host)); // Write the client code diff --git a/crates/smoketests/tests/smoketests/timestamp_route.rs b/crates/smoketests/tests/smoketests/timestamp_route.rs index 1d337bed1f0..525af584301 100644 --- a/crates/smoketests/tests/smoketests/timestamp_route.rs +++ b/crates/smoketests/tests/smoketests/timestamp_route.rs @@ -1,10 +1,12 @@ -use spacetimedb_smoketests::{random_string, Smoketest}; +use spacetimedb_smoketests::{random_string, require_server_issued_login, Smoketest}; const TIMESTAMP_TAG: &str = "__timestamp_micros_since_unix_epoch__"; /// Test the /v1/database/{name}/unstable/timestamp endpoint #[test] fn test_timestamp_route() { + require_server_issued_login!(); + let mut test = Smoketest::builder().autopublish(false).build(); let name = random_string(); From f746e527cead1c6807a2464b31beb4223da3fd36 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Sun, 28 Jun 2026 19:05:04 -0700 Subject: [PATCH 25/34] [bot/smoketest-spacetime-login]: fix several broken tests --- crates/smoketests/src/lib.rs | 17 +++++++++-------- .../tests/smoketests/new_user_flow.rs | 2 ++ .../tests/smoketests/publish_upgrade_prompt.rs | 12 ++++++++++-- crates/smoketests/tests/smoketests/servers.rs | 6 +++++- .../tests/smoketests/timestamp_route.rs | 2 ++ 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 461d52bbc0d..bb17dfae108 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -519,7 +519,7 @@ pub struct PublishBuilder<'a> { break_clients: bool, num_replicas: Option, organization: Option, - force: bool, + force: Option<&'static str>, stdin_input: Option, source: Option, } @@ -546,7 +546,7 @@ impl<'a> PublishBuilder<'a> { break_clients: false, num_replicas: None, organization: None, - force: true, + force: Some("all"), stdin_input: None, source: None, } @@ -577,13 +577,13 @@ impl<'a> PublishBuilder<'a> { self } - pub fn force(mut self, force: bool) -> Self { + pub fn force(mut self, force: Option<&'static str>) -> Self { self.force = force; self } pub fn stdin(mut self, stdin_input: impl Into) -> Self { - self.force = false; + self.force = None; self.stdin_input = Some(stdin_input.into()); self } @@ -1438,7 +1438,7 @@ log = "0.4" break_clients: bool, num_replicas: Option, organization: Option<&str>, - force: bool, + force: Option<&str>, stdin_input: Option<&str>, ) -> Result { let start = Instant::now(); @@ -1481,9 +1481,10 @@ log = "0.4" // Now publish with --bin-path to skip rebuild let publish_start = Instant::now(); let mut args = vec!["publish", "--server", &self.server_url, "--bin-path", &wasm_path_str]; - - if force { - args.push("--yes"); + let force_arg; + if let Some(force) = force { + force_arg = format!("--yes={force}"); + args.push(&force_arg); } if clear { diff --git a/crates/smoketests/tests/smoketests/new_user_flow.rs b/crates/smoketests/tests/smoketests/new_user_flow.rs index fb8b2e4ec25..99613e527a5 100644 --- a/crates/smoketests/tests/smoketests/new_user_flow.rs +++ b/crates/smoketests/tests/smoketests/new_user_flow.rs @@ -4,6 +4,8 @@ use spacetimedb_smoketests::{require_server_issued_login, Smoketest}; /// Test the entirety of the new user flow. #[test] fn test_new_user_flow() { + // This flow creates a throwaway server-issued identity with `new_identity`, + // which is not available when smoketests use SpacetimeAuth login. require_server_issued_login!(); let mut test = Smoketest::builder() diff --git a/crates/smoketests/tests/smoketests/publish_upgrade_prompt.rs b/crates/smoketests/tests/smoketests/publish_upgrade_prompt.rs index 4e39555b91d..127860003a8 100644 --- a/crates/smoketests/tests/smoketests/publish_upgrade_prompt.rs +++ b/crates/smoketests/tests/smoketests/publish_upgrade_prompt.rs @@ -36,14 +36,22 @@ fn upgrade_prompt_on_publish() { let deny_err = test .publish() .name(&db_name) - .force(false) + // Needed when running smoketests against a remote server. + .force(Some("remote")) .run() .unwrap_err() .to_string(); assert!(deny_err.contains("major version upgrade from 1.0 to 2.0")); assert!(deny_err.contains("Please type 'upgrade' to accept this change:")); - let accepted_identity = test.publish().name(&db_name).stdin("upgrade\n").run().unwrap(); + let accepted_identity = test + .publish() + .name(&db_name) + .stdin("upgrade\n") + // Needed when running smoketests against a remote server. + .force(Some("remote")) + .run() + .unwrap(); assert_eq!(accepted_identity, initial_identity); } diff --git a/crates/smoketests/tests/smoketests/servers.rs b/crates/smoketests/tests/smoketests/servers.rs index 6fa3946da2f..c275721103f 100644 --- a/crates/smoketests/tests/smoketests/servers.rs +++ b/crates/smoketests/tests/smoketests/servers.rs @@ -1,9 +1,13 @@ use regex::Regex; -use spacetimedb_smoketests::Smoketest; +use spacetimedb_smoketests::{require_local_server, Smoketest}; /// Verify that we can add and list server configurations #[test] fn test_servers() { + // This only covers local CLI config behavior, so it is not valuable to run + // against remote servers. + require_local_server!(); + let test = Smoketest::builder().autopublish(false).build(); // Add a test server (local-only command, no --server flag needed) diff --git a/crates/smoketests/tests/smoketests/timestamp_route.rs b/crates/smoketests/tests/smoketests/timestamp_route.rs index 525af584301..d106e1a4cbb 100644 --- a/crates/smoketests/tests/smoketests/timestamp_route.rs +++ b/crates/smoketests/tests/smoketests/timestamp_route.rs @@ -5,6 +5,8 @@ const TIMESTAMP_TAG: &str = "__timestamp_micros_since_unix_epoch__"; /// Test the /v1/database/{name}/unstable/timestamp endpoint #[test] fn test_timestamp_route() { + // This test creates a throwaway server-issued identity before publishing, + // which is not available when smoketests use SpacetimeAuth login. require_server_issued_login!(); let mut test = Smoketest::builder().autopublish(false).build(); From cff0752567ca9094177476607b563f2e21aefd55 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Sun, 28 Jun 2026 19:08:10 -0700 Subject: [PATCH 26/34] Apply suggestion from @bfops Signed-off-by: Zeke Foppa <196249+bfops@users.noreply.github.com> --- tools/ci/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ci/README.md b/tools/ci/README.md index 08dd7fa6338..b9bcf7469a3 100644 --- a/tools/ci/README.md +++ b/tools/ci/README.md @@ -104,7 +104,7 @@ When specified, tests will connect to the given URL instead of starting local se - `--spacetime-login `: Use a SpacetimeAuth-issued login for remote-server tests. -This is required for maincloud and maincloud staging, which reject direct server-issued logins for privileged operations. +This is required for servers that reject direct server-issued logins for privileged operations. Optionally accepts an auth host to pass through to `spacetime login`, for example `--spacetime-login=https://maincloud.staging.spacetimedb.com`. From 7fa71c99af4d43641fedd8e9866439616c455e1e Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Sun, 28 Jun 2026 19:08:20 -0700 Subject: [PATCH 27/34] Apply suggestion from @bfops Signed-off-by: Zeke Foppa <196249+bfops@users.noreply.github.com> --- tools/ci/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ci/README.md b/tools/ci/README.md index b9bcf7469a3..a3d6f193ea0 100644 --- a/tools/ci/README.md +++ b/tools/ci/README.md @@ -106,7 +106,7 @@ When specified, tests will connect to the given URL instead of starting local se This is required for servers that reject direct server-issued logins for privileged operations. -Optionally accepts an auth host to pass through to `spacetime login`, for example `--spacetime-login=https://maincloud.staging.spacetimedb.com`. +Optionally accepts an auth host to pass through to `spacetime login`, for example `--spacetime-login=https://spacetimedb.com`. - `--dotnet `: - `args `: Additional arguments to pass to the test runner From ad177da567a582d6cabbda62210b0a64f710d189 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Sun, 28 Jun 2026 19:08:57 -0700 Subject: [PATCH 28/34] Apply suggestion from @bfops Signed-off-by: Zeke Foppa <196249+bfops@users.noreply.github.com> --- tools/ci/src/smoketest.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index c2a2dfaa394..4e9715842eb 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -30,8 +30,7 @@ pub struct SmoketestsArgs { /// Use a SpacetimeAuth-issued login for remote-server tests. /// - /// This is required for maincloud and maincloud staging, which reject - /// direct server-issued logins for privileged operations. + /// This is required for servers that reject direct server-issued logins for privileged operations. /// /// Optionally accepts an auth host to pass through to `spacetime login`, /// for example `--spacetime-login=https://maincloud.staging.spacetimedb.com`. From e318488ef83e8f71d212181d7d8a6146abeea312 Mon Sep 17 00:00:00 2001 From: Zeke Foppa <196249+bfops@users.noreply.github.com> Date: Sun, 28 Jun 2026 19:10:06 -0700 Subject: [PATCH 29/34] Apply suggestion from @bfops Signed-off-by: Zeke Foppa <196249+bfops@users.noreply.github.com> --- tools/ci/src/smoketest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index 4e9715842eb..fc308ef311e 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -33,7 +33,7 @@ pub struct SmoketestsArgs { /// This is required for servers that reject direct server-issued logins for privileged operations. /// /// Optionally accepts an auth host to pass through to `spacetime login`, - /// for example `--spacetime-login=https://maincloud.staging.spacetimedb.com`. + /// for example `--spacetime-login=https://spacetimedb.com`. #[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "")] spacetime_login: Option, From 8bb7756d2c92034e3e22ca6e0720a6133d120472 Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 22:11:47 -0400 Subject: [PATCH 30/34] Move remote publish comments to yes flags --- crates/smoketests/tests/smoketests/cli/publish.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/smoketests/tests/smoketests/cli/publish.rs b/crates/smoketests/tests/smoketests/cli/publish.rs index 468a4fcb7ac..7fe5fab5fc0 100644 --- a/crates/smoketests/tests/smoketests/cli/publish.rs +++ b/crates/smoketests/tests/smoketests/cli/publish.rs @@ -15,7 +15,6 @@ fn cli_can_publish_spacetimedb_on_disk() { .join("spacetimedb"); let dir = dir.to_string(); - // Needed when running smoketests against a remote server. let _ = test .spacetime(&[ "publish", @@ -23,13 +22,13 @@ fn cli_can_publish_spacetimedb_on_disk() { &dir, "--server", &test.server_url, + // Needed when running smoketests against a remote server. "--yes=remote", "foobar", ]) .unwrap(); // Can republish without error to the same name - // Needed when running smoketests against a remote server. let _ = test .spacetime(&[ "publish", @@ -37,6 +36,7 @@ fn cli_can_publish_spacetimedb_on_disk() { &dir, "--server", &test.server_url, + // Needed when running smoketests against a remote server. "--yes=remote", "foobar", ]) @@ -246,11 +246,11 @@ fn cli_publish_with_config_but_no_match_uses_cli_args() { std::fs::write(module_dir.join("spacetime.json"), config_content).expect("failed to write config"); // Publish with a different database name from CLI - should use CLI args, not config - // Needed when running smoketests against a remote server. test.spacetime(&[ "publish", "--server", &test.server_url, + // Needed when running smoketests against a remote server. "--yes=remote", "cli-db-name", "--module-path", From fb22aa64fe74955fb6e1cc4e984771bb128b7a57 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Sun, 28 Jun 2026 19:14:34 -0700 Subject: [PATCH 31/34] [bot/smoketest-spacetime-login]: fix remaining tests --- crates/smoketests/tests/smoketests/http_routes.rs | 5 +++-- crates/smoketests/tests/smoketests/views.rs | 11 +++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/smoketests/tests/smoketests/http_routes.rs b/crates/smoketests/tests/smoketests/http_routes.rs index 2d8c28b2f1f..c51ae66b0a7 100644 --- a/crates/smoketests/tests/smoketests/http_routes.rs +++ b/crates/smoketests/tests/smoketests/http_routes.rs @@ -1,6 +1,6 @@ use regex::Regex; use spacetimedb_smoketests::{ - require_dotnet, require_emscripten, require_pnpm, workspace_root, ModuleLanguage, Smoketest, + random_string, require_dotnet, require_emscripten, require_pnpm, workspace_root, ModuleLanguage, Smoketest, }; use std::{fs, path::Path}; @@ -1109,9 +1109,10 @@ fn cpp_http_test(name: &str, module_code: &str) -> (Smoketest, String) { fn typescript_http_test(name: &str, module_code: &str) -> (Smoketest, String) { require_pnpm!(); let mut test = Smoketest::builder().autopublish(false).build(); + let database_name = format!("{name}-{}", random_string()); let identity = test .publish() - .name(name) + .name(&database_name) .source(ModuleLanguage::TypeScript, name, module_code) .run() .unwrap(); diff --git a/crates/smoketests/tests/smoketests/views.rs b/crates/smoketests/tests/smoketests/views.rs index c3fd230a8ee..517242edba6 100644 --- a/crates/smoketests/tests/smoketests/views.rs +++ b/crates/smoketests/tests/smoketests/views.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use serde_json::{json, Value}; use spacetimedb_smoketests::{ - require_dotnet, require_local_server, require_pnpm, workspace_root, ModuleLanguage, Smoketest, + random_string, require_dotnet, require_local_server, require_pnpm, workspace_root, ModuleLanguage, Smoketest, }; const STALE_VIEW_BACKING_TABLE_FIXTURE_IDENTITY: &str = @@ -881,8 +881,9 @@ fn test_procedure_triggers_subscription_updates() { fn test_typescript_procedure_triggers_subscription_updates() { require_pnpm!(); let mut test = Smoketest::builder().autopublish(false).build(); + let database_name = format!("views-subscribe-typescript-{}", random_string()); test.publish() - .name("views-subscribe-typescript") + .name(&database_name) .source( ModuleLanguage::TypeScript, "views-subscribe-typescript", @@ -933,8 +934,9 @@ fn test_typescript_count_view_subscription_refreshes() { require_pnpm!(); let mut test = Smoketest::builder().autopublish(false).build(); + let database_name = format!("views-count-typescript-{}", random_string()); test.publish() - .name("views-count-typescript") + .name(&database_name) .source( ModuleLanguage::TypeScript, "views-count-typescript", @@ -1031,8 +1033,9 @@ fn test_disconnect_does_not_break_anonymous_view() { fn test_typescript_query_builder_view_query() { require_pnpm!(); let mut test = Smoketest::builder().autopublish(false).build(); + let database_name = format!("views-query-builder-typescript-{}", random_string()); test.publish() - .name("views-query-builder-typescript") + .name(&database_name) .source( ModuleLanguage::TypeScript, "views-query-builder-typescript", From 7a8697bba3fa3482300cc73b7c8754ecd142886b Mon Sep 17 00:00:00 2001 From: clockwork-labs-bot Date: Sun, 28 Jun 2026 22:19:36 -0400 Subject: [PATCH 32/34] Rename smoketest auth flag --- crates/smoketests/DEVELOP.md | 4 +-- crates/smoketests/src/lib.rs | 6 ++--- tools/ci/README.md | 4 +-- tools/ci/src/smoketest.rs | 50 ++++++++++-------------------------- 4 files changed, 20 insertions(+), 44 deletions(-) diff --git a/crates/smoketests/DEVELOP.md b/crates/smoketests/DEVELOP.md index d89f2fe59b4..6acba49aea2 100644 --- a/crates/smoketests/DEVELOP.md +++ b/crates/smoketests/DEVELOP.md @@ -27,10 +27,10 @@ cargo ci smoketests --server https://example.spacetimedb.com ``` Maincloud and maincloud staging require SpacetimeAuth-issued tokens rather than -server-issued tokens. Use `--spacetime-login` for those: +server-issued tokens. Use `--auth-host` for those: ```bash -cargo ci smoketests --server https://maincloud.staging.spacetimedb.com --spacetime-login +cargo ci smoketests --server https://maincloud.staging.spacetimedb.com --auth-host ``` The runner invokes `spacetime login` once, then copies that logged-in config diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index bb17dfae108..7c7ab10345d 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -79,8 +79,8 @@ pub fn is_remote_server() -> bool { } /// Returns true if remote smoketests are using a SpacetimeAuth-issued token. -pub fn is_spacetime_login() -> bool { - std::env::var("SPACETIME_SMOKETEST_SPACETIME_LOGIN").ok().as_deref() == Some("1") +pub fn is_auth_host() -> bool { + std::env::var("SPACETIME_SMOKETEST_AUTH_HOST").ok().as_deref() == Some("1") } /// Skip this test if running against a remote server. @@ -115,7 +115,7 @@ macro_rules! require_local_server { #[macro_export] macro_rules! require_server_issued_login { () => { - if $crate::is_spacetime_login() { + if $crate::is_auth_host() { #[allow(clippy::disallowed_macros)] { eprintln!("Skipping test: requires server-issued throwaway identities"); diff --git a/tools/ci/README.md b/tools/ci/README.md index a3d6f193ea0..78147590f46 100644 --- a/tools/ci/README.md +++ b/tools/ci/README.md @@ -102,11 +102,11 @@ Usage: smoketests [OPTIONS] [ARGS]... [COMMAND] When specified, tests will connect to the given URL instead of starting local server instances. Tests that require local server control (like restart tests) will be skipped. -- `--spacetime-login `: Use a SpacetimeAuth-issued login for remote-server tests. +- `--auth-host `: Use a SpacetimeAuth-issued login for remote-server tests. This is required for servers that reject direct server-issued logins for privileged operations. -Optionally accepts an auth host to pass through to `spacetime login`, for example `--spacetime-login=https://spacetimedb.com`. +Optionally accepts an auth host to pass through to `spacetime login`, for example `--auth-host=https://spacetimedb.com`. - `--dotnet `: - `args `: Additional arguments to pass to the test runner diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index fc308ef311e..7a5540088df 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -33,9 +33,9 @@ pub struct SmoketestsArgs { /// This is required for servers that reject direct server-issued logins for privileged operations. /// /// Optionally accepts an auth host to pass through to `spacetime login`, - /// for example `--spacetime-login=https://spacetimedb.com`. + /// for example `--auth-host=https://spacetimedb.com`. #[arg(long, num_args = 0..=1, require_equals = true, default_missing_value = "")] - spacetime_login: Option, + auth_host: Option, #[arg(long, default_value_t = true, action = clap::ArgAction::Set)] dotnet: bool, @@ -66,7 +66,7 @@ pub fn run(args: SmoketestsArgs) -> Result<()> { eprintln!("smoketests/mod.rs is up to date."); Ok(()) } - None => run_smoketest(args.server, args.dotnet, args.spacetime_login.as_deref(), args.args), + None => run_smoketest(args.server, args.dotnet, args.auth_host.as_deref(), args.args), } } @@ -137,12 +137,7 @@ fn build_precompiled_modules() -> Result<()> { /// 16 was found to be optimal - higher values cause OS scheduler overhead. const DEFAULT_PARALLELISM: &str = "16"; -fn run_smoketest( - server: Option, - dotnet: bool, - spacetime_login_auth_host: Option<&str>, - args: Vec, -) -> Result<()> { +fn run_smoketest(server: Option, dotnet: bool, auth_host: Option<&str>, args: Vec) -> Result<()> { // 1. Build binaries first (single process, no race) build_binaries()?; @@ -150,7 +145,7 @@ fn run_smoketest( build_precompiled_modules()?; let cli_path = ensure_binaries_built(); - let base_config_dir = prepare_base_config(&cli_path, server.as_deref(), spacetime_login_auth_host)?; + let base_config_dir = prepare_base_config(&cli_path, server.as_deref(), auth_host)?; let base_config_path = base_config_dir.path().join("config.toml"); // 4. Detect whether to use nextest or cargo test @@ -170,13 +165,7 @@ fn run_smoketest( let mut cmd = if use_nextest { eprintln!("Running smoketests with cargo nextest...\n"); let mut cmd = Command::new("cargo"); - set_env( - &mut cmd, - server, - dotnet, - spacetime_login_auth_host.is_some(), - &base_config_path, - ); + set_env(&mut cmd, server, dotnet, auth_host.is_some(), &base_config_path); cmd.args([ "nextest", "run", @@ -198,13 +187,7 @@ fn run_smoketest( } else { eprintln!("Running smoketests with cargo test...\n"); let mut cmd = Command::new("cargo"); - set_env( - &mut cmd, - server, - dotnet, - spacetime_login_auth_host.is_some(), - &base_config_path, - ); + set_env(&mut cmd, server, dotnet, auth_host.is_some(), &base_config_path); cmd.args(["test", "--release", "-p", "spacetimedb-smoketests"]); cmd }; @@ -219,13 +202,9 @@ fn run_smoketest( Ok(()) } -fn prepare_base_config( - cli_path: &Path, - server: Option<&str>, - spacetime_login_auth_host: Option<&str>, -) -> Result { - if server.is_none() && spacetime_login_auth_host.is_some() { - bail!("--spacetime-login requires --server"); +fn prepare_base_config(cli_path: &Path, server: Option<&str>, auth_host: Option<&str>) -> Result { + if server.is_none() && auth_host.is_some() { + bail!("--auth-host requires --server"); } let temp_dir = tempfile::tempdir()?; @@ -256,7 +235,7 @@ fn prepare_base_config( ensure!(status.success(), "spacetime server edit failed"); } - if let Some(auth_host) = spacetime_login_auth_host { + if let Some(auth_host) = auth_host { eprintln!("Logging in with SpacetimeAuth for remote smoketests..."); let mut login = Command::new(cli_path); login.args(["--config-path", config_path_str, "login"]); @@ -288,15 +267,12 @@ fn prepare_base_config( Ok(temp_dir) } -fn set_env(cmd: &mut Command, server: Option, dotnet: bool, spacetime_login: bool, base_config_path: &Path) { +fn set_env(cmd: &mut Command, server: Option, dotnet: bool, auth_host: bool, base_config_path: &Path) { if let Some(ref server_url) = server { cmd.env("SPACETIME_REMOTE_SERVER", server_url); } cmd.env("SPACETIME_SMOKETEST_BASE_CONFIG_PATH", base_config_path); - cmd.env( - "SPACETIME_SMOKETEST_SPACETIME_LOGIN", - if spacetime_login { "1" } else { "0" }, - ); + cmd.env("SPACETIME_SMOKETEST_AUTH_HOST", if auth_host { "1" } else { "0" }); cmd.env("SMOKETESTS_DOTNET", if dotnet { "1" } else { "0" }); } From b79c8862aa7edce406853069561e82ecf1bf5ff9 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Sun, 28 Jun 2026 19:21:38 -0700 Subject: [PATCH 33/34] [bot/smoketest-spacetime-login]: rename --- crates/smoketests/src/lib.rs | 2 +- tools/ci/src/smoketest.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 7c7ab10345d..af5a6a08360 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -80,7 +80,7 @@ pub fn is_remote_server() -> bool { /// Returns true if remote smoketests are using a SpacetimeAuth-issued token. pub fn is_auth_host() -> bool { - std::env::var("SPACETIME_SMOKETEST_AUTH_HOST").ok().as_deref() == Some("1") + std::env::var("SPACETIME_USE_AUTH_HOST").ok().as_deref() == Some("1") } /// Skip this test if running against a remote server. diff --git a/tools/ci/src/smoketest.rs b/tools/ci/src/smoketest.rs index 7a5540088df..41bc8619841 100644 --- a/tools/ci/src/smoketest.rs +++ b/tools/ci/src/smoketest.rs @@ -272,7 +272,7 @@ fn set_env(cmd: &mut Command, server: Option, dotnet: bool, auth_host: b cmd.env("SPACETIME_REMOTE_SERVER", server_url); } cmd.env("SPACETIME_SMOKETEST_BASE_CONFIG_PATH", base_config_path); - cmd.env("SPACETIME_SMOKETEST_AUTH_HOST", if auth_host { "1" } else { "0" }); + cmd.env("SPACETIME_USE_AUTH_HOST", if auth_host { "1" } else { "0" }); cmd.env("SMOKETESTS_DOTNET", if dotnet { "1" } else { "0" }); } From aae7a2a40652814da5001dd44dad7bed8c2eab11 Mon Sep 17 00:00:00 2001 From: Zeke Foppa Date: Sun, 28 Jun 2026 19:22:10 -0700 Subject: [PATCH 34/34] [bot/smoketest-spacetime-login]: rename --- crates/smoketests/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index af5a6a08360..225c2864b3f 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -79,7 +79,7 @@ pub fn is_remote_server() -> bool { } /// Returns true if remote smoketests are using a SpacetimeAuth-issued token. -pub fn is_auth_host() -> bool { +pub fn is_using_auth_host() -> bool { std::env::var("SPACETIME_USE_AUTH_HOST").ok().as_deref() == Some("1") } @@ -115,7 +115,7 @@ macro_rules! require_local_server { #[macro_export] macro_rules! require_server_issued_login { () => { - if $crate::is_auth_host() { + if $crate::is_using_auth_host() { #[allow(clippy::disallowed_macros)] { eprintln!("Skipping test: requires server-issued throwaway identities");