diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f592de25466..782007023a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -407,7 +407,8 @@ jobs: with: global-json-file: global.json - # Node.js and pnpm are required for the TypeScript smoketests. + # Node.js and pnpm are required for the TypeScript smoketests. The template + # smoketests run the templates' module tests, whose test harness needs Node 22. - name: Set up Node.js uses: actions/setup-node@v4 with: @@ -416,6 +417,33 @@ jobs: - uses: ./.github/actions/setup-pnpm + # The templates' module tests load the TypeScript SDK's test datastore, + # which the template smoketests build with wasm-bindgen. + - name: Install wasm-bindgen CLI + shell: bash + run: | + REQUIRED_WASM_BINDGEN_VERSION="$( + awk ' + $1 == "name" && $3 == "\"wasm-bindgen\"" { in_pkg = 1; next } + in_pkg && $1 == "version" { + gsub(/"/, "", $3); + print $3; + exit; + } + ' Cargo.lock + )" + if [ -z "${REQUIRED_WASM_BINDGEN_VERSION}" ]; then + echo "Failed to determine wasm-bindgen version from Cargo.lock" + exit 1 + fi + + INSTALLED_WASM_BINDGEN_VERSION="$(wasm-bindgen --version 2>/dev/null | awk '{print $2}' || true)" + if [ "${INSTALLED_WASM_BINDGEN_VERSION}" != "${REQUIRED_WASM_BINDGEN_VERSION}" ]; then + cargo install --locked --force wasm-bindgen-cli --version "${REQUIRED_WASM_BINDGEN_VERSION}" + fi + + wasm-bindgen --version + - name: Set up Bun for Windows compiler discovery smoketest if: runner.os == 'Windows' uses: oven-sh/setup-bun@v2 diff --git a/Cargo.lock b/Cargo.lock index 213fa1c1ad8..8d758efb463 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,6 +487,7 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" name = "basic-rs-template-module" version = "0.1.0" dependencies = [ + "icu_calendar", "log", "spacetimedb", ] diff --git a/codex-plugin/plugins/spacetimedb/skills/concepts/SKILL.md b/codex-plugin/plugins/spacetimedb/skills/concepts/SKILL.md index 41d030039d1..1d345ba69c2 100644 --- a/codex-plugin/plugins/spacetimedb/skills/concepts/SKILL.md +++ b/codex-plugin/plugins/spacetimedb/skills/concepts/SKILL.md @@ -31,9 +31,10 @@ SpacetimeDB is a relational database that is also a server. It lets you upload a 1. **Backend:** Define table(s) to store the data 2. **Backend:** Define reducer(s) to mutate the data -3. **Client:** Subscribe to the table(s) -4. **Client:** Call the reducer(s) from UI -5. **Client:** Render the data from the table(s) +3. **Backend:** Unit-test the reducer(s) with the in-process module test harness (TypeScript and Rust modules; see https://spacetimedb.com/docs/how-to/unit-testing) +4. **Client:** Subscribe to the table(s) +5. **Client:** Call the reducer(s) from UI +6. **Client:** Render the data from the table(s) --- diff --git a/codex-plugin/plugins/spacetimedb/skills/rust-server/SKILL.md b/codex-plugin/plugins/spacetimedb/skills/rust-server/SKILL.md index 58854ab3539..98f35af8653 100644 --- a/codex-plugin/plugins/spacetimedb/skills/rust-server/SKILL.md +++ b/codex-plugin/plugins/spacetimedb/skills/rust-server/SKILL.md @@ -1,6 +1,6 @@ --- name: rust-server -description: SpacetimeDB Rust server module SDK reference. Use when writing tables, reducers, or module logic in Rust. +description: SpacetimeDB Rust server module SDK reference. Use when writing or unit-testing tables, reducers, or module logic in Rust. license: Apache-2.0 metadata: author: clockworklabs @@ -381,6 +381,50 @@ pub enum Status { Online, Away, Offline } pub struct Point { x: f32, y: f32 } ``` +## Testing + +Unit-test reducers, procedures and views in-process with the module test harness (`spacetimedb` 2.12+). It runs the module against the real datastore, in memory, so constraints and rollback behave as in production, with no server and no publish. Full guide: https://spacetimedb.com/docs/how-to/unit-testing + +Add a dev-dependency with the `test-utils` feature, at the same version as the normal dependency, and run tests natively with `cargo test` (not for a WebAssembly target): + +```toml +[dev-dependencies] +spacetimedb = { version = "2.12.0", features = ["test-utils"] } +icu_calendar = ">=2.1, <2.2" # until #6317 is fixed: the test datastore needs 2.1.x +``` + +`TestContext::new()` discovers the module's tables and functions and creates a fresh database. Call reducers as ordinary functions inside `with_reducer_tx`: + +```rust +#[cfg(test)] +mod tests { + use super::*; + use spacetimedb::test_utils::{TestAuth, TestContext}; + use spacetimedb::ConnectionId; + + fn alice() -> TestAuth { + TestAuth::from_jwt_payload(r#"{"iss":"test-issuer","sub":"alice"}"#, ConnectionId::from_u128(1)) + .expect("valid test claims") + } + + #[test] + fn creates_an_entity() { + let test = TestContext::new().expect("test database"); + test.with_reducer_tx(alice(), |ctx| { + create_entity(ctx, "Alice".to_string()); + Ok::<(), String>(()) + }) + .expect("reducer succeeds"); + assert_eq!(test.db.entity().count(), 1); + } +} +``` + +- `with_reducer_tx(auth, body)` commits when `body` returns `Ok`, and rolls back on `Err` or panic. For a reducer returning `()`, call it and then return `Ok::<(), String>(())`. +- `TestAuth::from_jwt_payload(claims_json, connection_id)` calls as a client; the same `iss` and `sub` always give the same `ctx.sender()`. `TestAuth::internal()` calls as the database itself, as scheduled reducers are. +- `test.clock` controls `ctx.timestamp`; `test.rng` controls `ctx.random()` and `ctx.rng()`. +- Subscriptions, client connections and deployment still need a running server (`spacetime start`). + ## Complete Example ```rust diff --git a/codex-plugin/plugins/spacetimedb/skills/typescript-server/SKILL.md b/codex-plugin/plugins/spacetimedb/skills/typescript-server/SKILL.md index 42bc4d80ba1..dc41fcd10e5 100644 --- a/codex-plugin/plugins/spacetimedb/skills/typescript-server/SKILL.md +++ b/codex-plugin/plugins/spacetimedb/skills/typescript-server/SKILL.md @@ -1,6 +1,6 @@ --- name: typescript-server -description: SpacetimeDB TypeScript server module SDK reference. Use when writing tables, reducers, or module logic in TypeScript. +description: SpacetimeDB TypeScript server module SDK reference. Use when writing or unit-testing tables, reducers, or module logic in TypeScript. license: Apache-2.0 metadata: author: clockworklabs @@ -439,3 +439,47 @@ export const routes = spacetimedb.httpRouter(new Router().get('/health', health) ``` Handlers are synchronous: return `SyncResponse` directly rather than marking the callback `async`. Pass the exported `httpHandler(...)` value to the router, not its raw callback. The router selects the path, while a handler reads request data with APIs such as `request.text()`; `Request` has no `path` property. A handler context does not expose `ctx.db`; use `ctx.withTx(tx => ...)` when a handler needs transactional database access. + +## Testing + +Unit-test reducers, procedures and views in-process with the module test harness (`spacetimedb` 2.12+). It runs the module against the real datastore, in memory, so constraints and rollback behave as in production, with no server and no publish. Full guide: https://spacetimedb.com/docs/how-to/unit-testing + +In the module directory, install `vitest` as a dev dependency, set `"type": "module"` and add `"test": "vitest run"` to `package.json`. The Vitest plugin replaces the host's `spacetime:sys` imports: + +```typescript +// vitest.config.ts +import { defineConfig, type Plugin } from 'vitest/config'; +import { spacetimedbModuleTestPlugin } from 'spacetimedb/server/test-utils/vitest'; + +export default defineConfig({ + plugins: [spacetimedbModuleTestPlugin() as unknown as Plugin], + test: { environment: 'node' }, +}); +``` + +Pass the default schema and all module exports to `createModuleTestHarness`; each call creates a fresh database. An exported reducer is callable as `reducer(ctx, args)` (`{}` for no arguments): + +```typescript +// src/index.test.ts +import { expect, it } from 'vitest'; +import { ConnectionId } from 'spacetimedb'; +import { createModuleTestHarness, TestAuth } from 'spacetimedb/server/test-utils'; +import spacetimedb, * as moduleExports from './index'; + +const alice = TestAuth.fromJwtPayload( + JSON.stringify({ iss: 'test-issuer', sub: 'alice' }), + new ConnectionId(1n) +); + +it('adds a record', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + test.withReducerTx(alice, ctx => moduleExports.addRecord(ctx, { value: 1 })); + expect(test.db.scoreRecord.count()).toBe(1n); +}); +``` + +- `withReducerTx(auth, body)` commits when `body` returns and rolls back and rethrows when it throws. Keep `body` synchronous; catching an error inside it commits. +- `TestAuth.fromJwtPayload(claimsJson, connectionId)` calls as a client; the same `iss` and `sub` always give the same `ctx.sender`. `TestAuth.internal()` calls as the database itself, as scheduled reducers are. +- `test.clock.set(...)` / `test.clock.advance(...)` control `ctx.timestamp`; the `rngSeed` option controls `ctx.random`. +- `test.viewContext(auth)` calls views; `test.procedureContextBuilder(auth).http(responder)` stubs outbound HTTP for procedures. +- Subscriptions, client connections and deployment still need a running server (`spacetime start`). diff --git a/crates/cli/src/subcommands/init.rs b/crates/cli/src/subcommands/init.rs index 0645b8c3234..5f3bbc1b8bd 100644 --- a/crates/cli/src/subcommands/init.rs +++ b/crates/cli/src/subcommands/init.rs @@ -2044,10 +2044,18 @@ pub fn init_typescript_project(project_path: &Path) -> anyhow::Result<()> { include_str!("../../../../templates/basic-ts/spacetimedb/tsconfig.json"), "tsconfig.json", ), + ( + include_str!("../../../../templates/basic-ts/spacetimedb/vitest.config.ts"), + "vitest.config.ts", + ), ( include_str!("../../../../templates/basic-ts/spacetimedb/src/index.ts"), "src/index.ts", ), + ( + include_str!("../../../../templates/basic-ts/spacetimedb/src/index.test.ts"), + "src/index.test.ts", + ), ]; check_for_git(); diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 2b9285dcab7..ca6161c2d5a 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -385,6 +385,11 @@ fn run_cmd_inner(args: &[&str], cwd: &Path, stdin_input: Option<&str>) -> Result /// Runs a `pnpm` command and returns stdout as a string. pub fn pnpm(args: &[&str], cwd: &Path) -> Result { + pnpm_with_env(args, cwd, &[]) +} + +/// Runs a `pnpm` command with extra environment variables and returns stdout as a string. +pub fn pnpm_with_env(args: &[&str], cwd: &Path, env: &[(&str, &str)]) -> Result { let pnpm_path = pnpm_path().context("Could not locate pnpm")?; let minimum_release_age = pnpm_minimum_release_age()?; @@ -395,6 +400,7 @@ pub fn pnpm(args: &[&str], cwd: &Path) -> Result { .args(args) .current_dir(cwd) .env("npm_config_minimum_release_age", minimum_release_age) + .envs(env.iter().copied()) .output() .with_context(|| format!("Failed to spawn pnpm {}", args.join(" ")))?; @@ -410,12 +416,52 @@ pub fn pnpm(args: &[&str], cwd: &Path) -> Result { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } -/// Builds the local TypeScript bindings package. +/// Builds the local TypeScript bindings package, once per test run. +/// +/// Tests run in parallel, and nextest runs each one in its own process. A build +/// wipes `dist/` first, which would pull the SDK out from under a test that is +/// using it, so builds take a file lock, and a stamp file records the run that +/// last built the SDK so later tests in the same run skip it. pub fn build_typescript_sdk() -> Result<()> { - let workspace = workspace_root(); - let ts_bindings = workspace.join("crates/bindings-typescript"); - pnpm(&["--filter", "spacetimedb", "install"], &ts_bindings)?; - pnpm(&["build"], &ts_bindings)?; + let ts_bindings = workspace_root().join("crates/bindings-typescript"); + once_per_test_run("typescript-sdk", || { + pnpm(&["--filter", "spacetimedb", "install"], &ts_bindings)?; + pnpm(&["build"], &ts_bindings)?; + Ok(()) + }) +} + +/// Builds the local TypeScript bindings package with the test datastore that +/// modules' tests load, once per test run. +/// +/// `pnpm build` doesn't produce the datastore, and generating it needs +/// wasm-bindgen, so only tests that run module tests should call this. +pub fn build_typescript_sdk_test_utils() -> Result<()> { + build_typescript_sdk()?; + let ts_bindings = workspace_root().join("crates/bindings-typescript"); + once_per_test_run("typescript-sdk-test-utils", || { + pnpm(&["prepare:test-utils-wasm"], &ts_bindings)?; + Ok(()) + }) +} + +/// Runs `step` at most once per test run, across all of its test processes. +/// +/// Runs hold a lock on `.lock` in the shared target dir while they check +/// and run the step, and `.stamp` records the run that last completed it. +fn once_per_test_run(name: &str, step: impl FnOnce() -> Result<()>) -> Result<()> { + let stamp_path = shared_target_dir().join(format!("{name}.stamp")); + let lock = fs::File::create(shared_target_dir().join(format!("{name}.lock")))?; + lock.lock().with_context(|| format!("Failed to lock {name}"))?; + + // nextest gives every test process the same run ID; `cargo test` runs tests as threads of one process. + let run_id = std::env::var("NEXTEST_RUN_ID").unwrap_or_else(|_| std::process::id().to_string()); + if fs::read_to_string(&stamp_path).is_ok_and(|built| built == run_id) { + return Ok(()); + } + + step()?; + fs::write(&stamp_path, run_id)?; Ok(()) } @@ -1294,6 +1340,9 @@ impl Smoketest { let module_path = module_root.join("spacetimedb"); fs::write(module_path.join("src/index.ts"), module_source).context("Failed to write TypeScript module code")?; + // The scaffold's test exercises the sample module we just replaced, and publish type-checks it. + fs::remove_file(module_path.join("src/index.test.ts")) + .context("Failed to remove the scaffold's module test")?; build_typescript_sdk()?; let _ = pnpm(&["uninstall", "spacetimedb"], &module_path); diff --git a/crates/smoketests/tests/cluster/quickstart.rs b/crates/smoketests/tests/cluster/quickstart.rs index db38f5a82e8..b535dc19de5 100644 --- a/crates/smoketests/tests/cluster/quickstart.rs +++ b/crates/smoketests/tests/cluster/quickstart.rs @@ -626,6 +626,10 @@ log = "0.4" )?; } "typescript" => { + // The tutorial has the reader delete the scaffold's module test, which tests the sample + // module they replace. + fs::remove_file(server_path.join("src/index.test.ts"))?; + // Build and link the TypeScript SDK build_typescript_sdk()?; diff --git a/crates/smoketests/tests/cluster/templates.rs b/crates/smoketests/tests/cluster/templates.rs index 8bc30631734..4baca4716d6 100644 --- a/crates/smoketests/tests/cluster/templates.rs +++ b/crates/smoketests/tests/cluster/templates.rs @@ -12,7 +12,9 @@ use anyhow::{bail, Context, Result}; use regex::Regex; use serde_json::Value; -use spacetimedb_smoketests::{pnpm, random_string, workspace_root, Smoketest}; +use spacetimedb_smoketests::{ + build_typescript_sdk_test_utils, pnpm, pnpm_with_env, random_string, workspace_root, Smoketest, +}; use std::env; use std::fs; use std::path::{Path, PathBuf}; @@ -590,17 +592,6 @@ fn pin_csharp_client_sdk_package_version(project_path: &Path) -> Result<()> { Ok(()) } -/// Builds the TypeScript SDK (`crates/bindings-typescript`). -/// -/// Should be called once before testing any TypeScript templates. -fn build_typescript_sdk() -> Result<()> { - let sdk_path = workspace_root().join("crates/bindings-typescript"); - eprintln!("[TEMPLATES] Building TypeScript SDK at {:?}", sdk_path); - run_pnpm(&["--filter", "spacetimedb", "install"], &sdk_path)?; - run_pnpm(&["build"], &sdk_path)?; - Ok(()) -} - /// Points the `spacetimedb` entry in `package.json` at the local TypeScript /// SDK and removes the template's lockfile so pnpm re-resolves dependencies. fn setup_typescript_sdk_in_package_json(package_json_path: &Path) -> Result<()> { @@ -815,7 +806,7 @@ fn test_rust_template(test: &Smoketest, template: &Template, project_path: &Path Ok(()) } -/// Publishes a TypeScript server module and verifies the TypeScript client type-checks. +/// Publishes a TypeScript server module, then installs the project and runs its tests against it. fn test_typescript_template(test: &Smoketest, template: &Template, project_path: &Path) -> Result<()> { // Server let server_path = project_path.join("spacetimedb"); @@ -838,8 +829,22 @@ fn test_typescript_template(test: &Smoketest, template: &Template, project_path: template.id ) })?; + + // Keep the database until the tests are done, since integration tests connect to it. + let result = test_typescript_project(template, project_path, &server_path, &test.server_url, &domain); + // Best-effort cleanup. let _ = test.spacetime(&["delete", "--server", &test.server_url, "--yes", &domain]); + result +} +/// Installs the TypeScript client, if there is one, and runs the project's `test` script. +fn test_typescript_project( + template: &Template, + project_path: &Path, + server_path: &Path, + server_url: &str, + domain: &str, +) -> Result<()> { // Client type-check (only if there's a client package.json in the project root) let client_package_json = project_path.join("package.json"); if client_package_json.exists() { @@ -849,9 +854,36 @@ fn test_typescript_template(test: &Smoketest, template: &Template, project_path: // TODO: some templates don't pass tsc yet, re-enable once they're fixed. // run_pnpm(&["exec", "tsc", "--noEmit"], project_path)?; } + + // A project-level `test` script also runs the module's tests, so prefer it. + let Some(test_dir) = [project_path, server_path] + .into_iter() + .find(|dir| has_test_script(&dir.join("package.json"))) + else { + eprintln!("[TEMPLATES] Template {} has no test script", template.id); + return Ok(()); + }; + // Templates read these to find their database, so integration tests use the one just published. + pnpm_with_env( + &["test"], + test_dir, + &[ + ("VITE_SPACETIMEDB_HOST", server_url), + ("VITE_SPACETIMEDB_DB_NAME", domain), + ], + ) + .with_context(|| format!("pnpm test failed for template {}", template.id))?; Ok(()) } +/// Whether the `package.json` at `package_json_path` exists and defines a `test` script. +fn has_test_script(package_json_path: &Path) -> bool { + fs::read_to_string(package_json_path) + .ok() + .and_then(|content| serde_json::from_str::(&content).ok()) + .is_some_and(|package| package["scripts"]["test"].is_string()) +} + /// Publishes a C# server module and verifies the C# client builds. fn test_csharp_template(test: &Smoketest, template: &Template) -> Result<()> { for dotnet_version in ["8", "10"] { @@ -1011,7 +1043,7 @@ fn run_registered_template(template_id: &str) { match template.server_lang.as_deref() { Some("typescript") => { spacetimedb_smoketests::require_pnpm!(); - build_typescript_sdk().expect("Failed to build TypeScript SDK"); + build_typescript_sdk_test_utils().expect("Failed to build TypeScript SDK"); } Some("csharp") => { spacetimedb_smoketests::require_dotnet!(); diff --git a/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md b/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md index f67a10414cc..904d011e0de 100644 --- a/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md +++ b/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md @@ -151,7 +151,13 @@ spacetime init --lang cpp quickstart-chat -Inside the `spacetimedb/` directory will be a `src/index.ts` entrypoint (required for publishing). +Inside the `spacetimedb/` directory will be a `src/index.ts` entrypoint (required for publishing), and a `src/index.test.ts` with unit tests for the example module in it. + +We're about to replace that example module with our chat module, so from your project directory, delete its tests: + +```bash +rm spacetimedb/src/index.test.ts +``` diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 598913044e0..bd3724a1dcf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -743,7 +743,7 @@ importers: devDependencies: '@angular/build': specifier: ^21.2.12 - version: 21.2.13(@angular/compiler-cli@21.2.15(@angular/compiler@21.2.15)(typescript@5.9.3))(@angular/compiler@21.2.15)(@angular/core@21.2.15(@angular/compiler@21.2.15)(rxjs@7.8.2))(@angular/platform-browser@21.2.15(@angular/common@21.2.15(@angular/core@21.2.15(@angular/compiler@21.2.15)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.15(@angular/compiler@21.2.15)(rxjs@7.8.2)))(@types/node@24.3.0)(chokidar@5.0.0)(jiti@2.6.1)(postcss@8.5.6)(terser@5.43.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@26.1.0)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2) + version: 21.2.13(@angular/compiler-cli@21.2.15(@angular/compiler@21.2.15)(typescript@5.9.3))(@angular/compiler@21.2.15)(@angular/core@21.2.15(@angular/compiler@21.2.15)(rxjs@7.8.2))(@angular/platform-browser@21.2.15(@angular/common@21.2.15(@angular/core@21.2.15(@angular/compiler@21.2.15)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.15(@angular/compiler@21.2.15)(rxjs@7.8.2)))(@types/node@24.3.0)(chokidar@5.0.0)(jiti@2.6.1)(postcss@8.5.6)(terser@5.43.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.9.3)(vitest@4.1.12(@opentelemetry/api@1.9.0)(@types/node@24.3.0)(jsdom@26.1.0)(vite@7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)))(yaml@2.8.2) '@angular/cli': specifier: ^21.2.12 version: 21.2.13(@types/node@24.3.0)(chokidar@5.0.0) @@ -753,6 +753,9 @@ importers: typescript: specifier: ~5.9.2 version: 5.9.3 + vitest: + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.9.0)(@types/node@24.3.0)(jsdom@26.1.0)(vite@7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) templates/basic-ts: dependencies: @@ -769,6 +772,9 @@ importers: typescript: specifier: ~5.6.2 version: 5.6.3 + vitest: + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.9.0)(@types/node@20.19.33)(jsdom@26.1.0)(vite@7.3.2(@types/node@20.19.33)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) templates/browser-ts: dependencies: @@ -782,6 +788,9 @@ importers: vite: specifier: ^7.1.5 version: 7.1.5(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.9.0)(@types/node@24.3.0)(jsdom@26.1.0)(vite@7.1.5(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) templates/bun-ts: dependencies: @@ -798,6 +807,9 @@ importers: typescript: specifier: ~5.6.2 version: 5.6.3 + vitest: + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.9.0)(@types/node@24.3.0)(jsdom@26.1.0)(vite@7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) templates/chat-react-ts: dependencies: @@ -1108,6 +1120,9 @@ importers: typescript: specifier: ~5.6.2 version: 5.6.3 + vitest: + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.9.0)(@types/node@24.3.0)(jsdom@26.1.0)(vite@7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) templates/react-ts: dependencies: @@ -1136,6 +1151,9 @@ importers: vite: specifier: ^7.1.5 version: 7.1.5(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.9.0)(@types/node@24.3.0)(jsdom@26.1.0)(vite@7.1.5(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) templates/remix-ts: dependencies: @@ -1176,6 +1194,9 @@ importers: vite: specifier: ^5.4.0 version: 5.4.21(@types/node@24.3.0)(sass@1.97.3)(terser@5.43.1) + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.1)(jsdom@26.1.0)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) templates/solid-ts: dependencies: @@ -1195,6 +1216,9 @@ importers: vite-plugin-solid: specifier: ^2.11.8 version: 2.11.12(@testing-library/jest-dom@6.7.0)(solid-js@1.9.13)(vite@7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) + vitest: + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.9.0)(@types/node@24.3.0)(jsdom@26.1.0)(vite@7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) templates/svelte-ts: dependencies: @@ -1217,6 +1241,9 @@ importers: vite: specifier: ^6.4.1 version: 6.4.1(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.9.0)(@types/node@24.3.0)(jsdom@26.1.0)(vite@6.4.1(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) templates/tanstack-ts: dependencies: @@ -1269,6 +1296,9 @@ importers: vite-tsconfig-paths: specifier: ^5.1.4 version: 5.1.4(typescript@5.9.3)(vite@7.1.5(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) + vitest: + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.9.0)(@types/node@22.18.0)(jsdom@26.1.0)(vite@7.1.5(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) templates/vue-ts: dependencies: @@ -1288,6 +1318,9 @@ importers: vite: specifier: ^6.4.1 version: 6.4.1(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: + specifier: ^4.1.12 + version: 4.1.12(@opentelemetry/api@1.9.0)(@types/node@24.3.0)(jsdom@26.1.0)(vite@6.4.1(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) vue-tsc: specifier: ^2.2.0 version: 2.2.12(typescript@5.6.3) @@ -4524,42 +4557,49 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-arm64-musl@1.1.1': resolution: {integrity: sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@napi-rs/nice-linux-ppc64-gnu@1.1.1': resolution: {integrity: sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==} engines: {node: '>= 10'} cpu: [ppc64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-riscv64-gnu@1.1.1': resolution: {integrity: sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-s390x-gnu@1.1.1': resolution: {integrity: sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==} engines: {node: '>= 10'} cpu: [s390x] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-x64-gnu@1.1.1': resolution: {integrity: sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-x64-musl@1.1.1': resolution: {integrity: sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@napi-rs/nice-openharmony-arm64@1.1.1': resolution: {integrity: sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==} @@ -4915,24 +4955,28 @@ packages: engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-arm64-musl@0.56.5': resolution: {integrity: sha512-SCIqrL5apVbrtMoqOpKX/Ez+c46WmW0Tyhtu+Xby281biH+wYu70m+fux9ZsGmbHc2ojd4FxUcaUdCZtb5uTOQ==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@oxc-parser/binding-linux-x64-gnu@0.56.5': resolution: {integrity: sha512-I2mpX35NWo83hay4wrnzFLk3VuGK1BBwHaqvEdqsCode8iG8slYJRJPICVbCEWlkR3rotlTQ+608JcRU0VqZ5Q==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@oxc-parser/binding-linux-x64-musl@0.56.5': resolution: {integrity: sha512-xfzUHGYOh3PGWZdBuY5r1czvE8EGWPAmhTWHqkw3/uAfUVWN/qrrLjMojiaiWyUgl/9XIFg05m5CJH9dnngh5Q==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@oxc-parser/binding-wasm32-wasi@0.56.5': resolution: {integrity: sha512-+z3Ofmc1v5kcu8fXgG5vn7T1f52P47ceTTmTXsm5HPY7rq5EMYRUaBnxH6cesXwY1OVVCwYlIZbCiy8Pm1w8zQ==} @@ -5042,36 +5086,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-wasm@2.5.6': resolution: {integrity: sha512-byAiBZ1t3tXQvc8dMD/eoyE7lTXYorhn+6uVW5AC+JGI1KtJC/LvDche5cfUE+qiefH+Ybq0bUCJU0aB1cSHUA==} @@ -6076,121 +6126,145 @@ packages: resolution: {integrity: sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-gnueabihf@4.56.0': resolution: {integrity: sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.50.2': resolution: {integrity: sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm-musleabihf@4.56.0': resolution: {integrity: sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.50.2': resolution: {integrity: sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-gnu@4.56.0': resolution: {integrity: sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.50.2': resolution: {integrity: sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-musl@4.56.0': resolution: {integrity: sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.50.2': resolution: {integrity: sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-gnu@4.56.0': resolution: {integrity: sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.56.0': resolution: {integrity: sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.50.2': resolution: {integrity: sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.56.0': resolution: {integrity: sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.56.0': resolution: {integrity: sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.50.2': resolution: {integrity: sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.56.0': resolution: {integrity: sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.50.2': resolution: {integrity: sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-musl@4.56.0': resolution: {integrity: sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.50.2': resolution: {integrity: sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.56.0': resolution: {integrity: sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.50.2': resolution: {integrity: sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.56.0': resolution: {integrity: sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.50.2': resolution: {integrity: sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-linux-x64-musl@4.56.0': resolution: {integrity: sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.56.0': resolution: {integrity: sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==} @@ -6360,6 +6434,9 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@supabase/auth-js@2.97.0': resolution: {integrity: sha512-2Og/1lqp+AIavr8qS2X04aSl8RBY06y4LrtIAGxat06XoXYiDxKNQMQzWDAKm1EyZFZVRNH48DO5YvIZ7la5fQ==} engines: {node: '>=20.0.0'} @@ -7098,6 +7175,9 @@ packages: '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/expect@4.1.12': + resolution: {integrity: sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ==} + '@vitest/mocker@3.2.4': resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} peerDependencies: @@ -7109,21 +7189,47 @@ packages: vite: optional: true + '@vitest/mocker@4.1.12': + resolution: {integrity: sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@3.2.4': resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/pretty-format@4.1.12': + resolution: {integrity: sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA==} + '@vitest/runner@3.2.4': resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + '@vitest/runner@4.1.12': + resolution: {integrity: sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==} + '@vitest/snapshot@3.2.4': resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + '@vitest/snapshot@4.1.12': + resolution: {integrity: sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw==} + '@vitest/spy@3.2.4': resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@4.1.12': + resolution: {integrity: sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==} + '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@vitest/utils@4.1.12': + resolution: {integrity: sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==} + '@volar/language-core@2.4.15': resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} @@ -8220,6 +8326,10 @@ packages: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} + chai@6.3.0: + resolution: {integrity: sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -9236,6 +9346,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -9503,6 +9616,10 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} @@ -11804,6 +11921,10 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + ofetch@1.5.1: resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} @@ -13887,6 +14008,9 @@ packages: std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + std-env@4.3.0: + resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} + stdin-discarder@0.3.2: resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} engines: {node: '>=18'} @@ -13968,9 +14092,6 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-literal@3.0.0: - resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} - strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} @@ -14187,6 +14308,10 @@ packages: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} + tinyrainbow@3.2.0: + resolution: {integrity: sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==} + engines: {node: '>=14.0.0'} + tinyspy@4.0.3: resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} engines: {node: '>=14.0.0'} @@ -15101,6 +15226,47 @@ packages: jsdom: optional: true + vitest@4.1.12: + resolution: {integrity: sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.12 + '@vitest/browser-preview': 4.1.12 + '@vitest/browser-webdriverio': ^4.1.11 + '@vitest/coverage-istanbul': 4.1.12 + '@vitest/coverage-v8': 4.1.12 + '@vitest/ui': 4.1.12 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} @@ -15735,7 +15901,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular/build@21.2.13(@angular/compiler-cli@21.2.15(@angular/compiler@21.2.15)(typescript@5.9.3))(@angular/compiler@21.2.15)(@angular/core@21.2.15(@angular/compiler@21.2.15)(rxjs@7.8.2))(@angular/platform-browser@21.2.15(@angular/common@21.2.15(@angular/core@21.2.15(@angular/compiler@21.2.15)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.15(@angular/compiler@21.2.15)(rxjs@7.8.2)))(@types/node@24.3.0)(chokidar@5.0.0)(jiti@2.6.1)(postcss@8.5.6)(terser@5.43.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@26.1.0)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)': + '@angular/build@21.2.13(@angular/compiler-cli@21.2.15(@angular/compiler@21.2.15)(typescript@5.9.3))(@angular/compiler@21.2.15)(@angular/core@21.2.15(@angular/compiler@21.2.15)(rxjs@7.8.2))(@angular/platform-browser@21.2.15(@angular/common@21.2.15(@angular/core@21.2.15(@angular/compiler@21.2.15)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.15(@angular/compiler@21.2.15)(rxjs@7.8.2)))(@types/node@24.3.0)(chokidar@5.0.0)(jiti@2.6.1)(postcss@8.5.6)(terser@5.43.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.9.3)(vitest@4.1.12(@opentelemetry/api@1.9.0)(@types/node@24.3.0)(jsdom@26.1.0)(vite@7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)))(yaml@2.8.2)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.13(chokidar@5.0.0) @@ -15773,7 +15939,7 @@ snapshots: '@angular/platform-browser': 21.2.15(@angular/common@21.2.15(@angular/core@21.2.15(@angular/compiler@21.2.15)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.2.15(@angular/compiler@21.2.15)(rxjs@7.8.2)) lmdb: 3.5.1 postcss: 8.5.6 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@26.1.0)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.1.12(@opentelemetry/api@1.9.0)(@types/node@24.3.0)(jsdom@26.1.0)(vite@7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) transitivePeerDependencies: - '@types/node' - chokidar @@ -21592,6 +21758,8 @@ snapshots: '@standard-schema/spec@1.0.0': {} + '@standard-schema/spec@1.1.0': {} + '@supabase/auth-js@2.97.0': dependencies: tslib: 2.8.1 @@ -22712,48 +22880,121 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.5(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/expect@4.1.12': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.2 + '@vitest/spy': 4.1.12 + '@vitest/utils': 4.1.12 + chai: 6.3.0 + tinyrainbow: 3.2.0 + + '@vitest/mocker@3.2.4(vite@7.3.2(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 - magic-string: 0.30.19 + magic-string: 0.30.21 optionalDependencies: - vite: 7.1.5(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.2(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) - '@vitest/mocker@3.2.4(vite@7.1.5(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/mocker@3.2.4(vite@7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 - magic-string: 0.30.19 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + + '@vitest/mocker@4.1.12(vite@6.4.1(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.1.12 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.1(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + + '@vitest/mocker@4.1.12(vite@7.1.5(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.1.12 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.1.5(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + + '@vitest/mocker@4.1.12(vite@7.1.5(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.1.12 + estree-walker: 3.0.3 + magic-string: 0.30.21 optionalDependencies: vite: 7.1.5(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + '@vitest/mocker@4.1.12(vite@7.3.2(@types/node@20.19.33)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.1.12 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.2(@types/node@20.19.33)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + + '@vitest/mocker@4.1.12(vite@7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.1.12 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 + '@vitest/pretty-format@4.1.12': + dependencies: + tinyrainbow: 3.2.0 + '@vitest/runner@3.2.4': dependencies: '@vitest/utils': 3.2.4 pathe: 2.0.3 - strip-literal: 3.0.0 + strip-literal: 3.1.0 + + '@vitest/runner@4.1.12': + dependencies: + '@vitest/utils': 4.1.12 + pathe: 2.0.3 '@vitest/snapshot@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.19 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.12': + dependencies: + '@vitest/pretty-format': 4.1.12 + '@vitest/utils': 4.1.12 + magic-string: 0.30.21 pathe: 2.0.3 '@vitest/spy@3.2.4': dependencies: tinyspy: 4.0.3 + '@vitest/spy@4.1.12': {} + '@vitest/utils@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 loupe: 3.2.1 tinyrainbow: 2.0.0 + '@vitest/utils@4.1.12': + dependencies: + '@vitest/pretty-format': 4.1.12 + convert-source-map: 2.0.0 + tinyrainbow: 3.2.0 + '@volar/language-core@2.4.15': dependencies: '@volar/source-map': 2.4.15 @@ -24596,6 +24837,8 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 + chai@6.3.0: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -25470,6 +25713,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.3.2: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -25950,6 +26195,8 @@ snapshots: expect-type@1.2.2: {} + expect-type@1.4.0: {} + exponential-backoff@3.1.3: {} express-rate-limit@8.2.1(express@5.2.1): @@ -26161,7 +26408,7 @@ snapshots: fix-dts-default-cjs-exports@1.0.1: dependencies: - magic-string: 0.30.19 + magic-string: 0.30.21 mlly: 1.7.4 rollup: 4.56.0 @@ -29170,6 +29417,8 @@ snapshots: obuf@1.1.2: {} + obug@2.2.1: {} + ofetch@1.5.1: dependencies: destr: 2.0.5 @@ -31554,6 +31803,8 @@ snapshots: std-env@3.9.0: {} + std-env@4.3.0: {} + stdin-discarder@0.3.2: {} stream-shift@1.0.3: {} @@ -31637,10 +31888,6 @@ snapshots: strip-json-comments@3.1.1: {} - strip-literal@3.0.0: - dependencies: - js-tokens: 9.0.1 - strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 @@ -31879,8 +32126,8 @@ snapshots: tinyglobby@0.2.12: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tinyglobby@0.2.15: dependencies: @@ -31891,6 +32138,8 @@ snapshots: tinyrainbow@2.0.0: {} + tinyrainbow@3.2.0: {} + tinyspy@4.0.3: {} tldts-core@6.1.86: {} @@ -32210,7 +32459,7 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.0 pathe: 2.0.3 - picomatch: 4.0.3 + picomatch: 4.0.4 pkg-types: 2.3.0 scule: 1.3.0 strip-literal: 3.1.0 @@ -32361,7 +32610,7 @@ snapshots: dependencies: '@jridgewell/remapping': 2.3.5 acorn: 8.15.0 - picomatch: 4.0.3 + picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 unstorage@1.17.4(db0@0.3.4(better-sqlite3@12.6.2)(drizzle-orm@0.44.7(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.6.2)(bun-types@1.4.4)(pg@8.18.0)(sql.js@1.14.0)))(ioredis@5.9.2): @@ -32786,6 +33035,23 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 + vite@7.3.2(@types/node@20.19.33)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + esbuild: 0.27.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.6 + rollup: 4.56.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.33 + fsevents: 2.3.3 + jiti: 2.6.1 + sass: 1.97.3 + terser: 5.43.1 + tsx: 4.21.0 + yaml: 2.8.2 + vite@7.3.2(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.27.3 @@ -32836,7 +33102,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.5(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(vite@7.3.2(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -32845,16 +33111,16 @@ snapshots: chai: 5.3.3 debug: 4.4.3 expect-type: 1.2.2 - magic-string: 0.30.19 + magic-string: 0.30.21 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.9.0 + picomatch: 4.0.4 + std-env: 3.10.0 tinybench: 2.9.0 tinyexec: 0.3.2 tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.5(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.2(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) vite-node: 3.2.4(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: @@ -32879,7 +33145,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.5(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/mocker': 3.2.4(vite@7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -32888,16 +33154,16 @@ snapshots: chai: 5.3.3 debug: 4.4.3 expect-type: 1.2.2 - magic-string: 0.30.19 + magic-string: 0.30.21 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.9.0 + picomatch: 4.0.4 + std-env: 3.10.0 tinybench: 2.9.0 tinyexec: 0.3.2 tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.5(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + vite: 7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) vite-node: 3.2.4(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: @@ -32918,6 +33184,151 @@ snapshots: - tsx - yaml + vitest@4.1.12(@opentelemetry/api@1.9.0)(@types/node@20.19.33)(jsdom@26.1.0)(vite@7.3.2(@types/node@20.19.33)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@vitest/expect': 4.1.12 + '@vitest/mocker': 4.1.12(vite@7.3.2(@types/node@20.19.33)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.1.12 + '@vitest/runner': 4.1.12 + '@vitest/snapshot': 4.1.12 + '@vitest/spy': 4.1.12 + '@vitest/utils': 4.1.12 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.2.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.3.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.2.0 + vite: 7.3.2(@types/node@20.19.33)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 20.19.33 + jsdom: 26.1.0 + transitivePeerDependencies: + - msw + + vitest@4.1.12(@opentelemetry/api@1.9.0)(@types/node@22.18.0)(jsdom@26.1.0)(vite@7.1.5(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@vitest/expect': 4.1.12 + '@vitest/mocker': 4.1.12(vite@7.1.5(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.1.12 + '@vitest/runner': 4.1.12 + '@vitest/snapshot': 4.1.12 + '@vitest/spy': 4.1.12 + '@vitest/utils': 4.1.12 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.2.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.3.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.2.0 + vite: 7.1.5(@types/node@22.18.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 22.18.0 + jsdom: 26.1.0 + transitivePeerDependencies: + - msw + + vitest@4.1.12(@opentelemetry/api@1.9.0)(@types/node@24.3.0)(jsdom@26.1.0)(vite@6.4.1(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@vitest/expect': 4.1.12 + '@vitest/mocker': 4.1.12(vite@6.4.1(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.1.12 + '@vitest/runner': 4.1.12 + '@vitest/snapshot': 4.1.12 + '@vitest/spy': 4.1.12 + '@vitest/utils': 4.1.12 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.2.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.3.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.2.0 + vite: 6.4.1(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 24.3.0 + jsdom: 26.1.0 + transitivePeerDependencies: + - msw + + vitest@4.1.12(@opentelemetry/api@1.9.0)(@types/node@24.3.0)(jsdom@26.1.0)(vite@7.1.5(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@vitest/expect': 4.1.12 + '@vitest/mocker': 4.1.12(vite@7.1.5(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.1.12 + '@vitest/runner': 4.1.12 + '@vitest/snapshot': 4.1.12 + '@vitest/spy': 4.1.12 + '@vitest/utils': 4.1.12 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.2.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.3.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.2.0 + vite: 7.1.5(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 24.3.0 + jsdom: 26.1.0 + transitivePeerDependencies: + - msw + + vitest@4.1.12(@opentelemetry/api@1.9.0)(@types/node@24.3.0)(jsdom@26.1.0)(vite@7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@vitest/expect': 4.1.12 + '@vitest/mocker': 4.1.12(vite@7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.1.12 + '@vitest/runner': 4.1.12 + '@vitest/snapshot': 4.1.12 + '@vitest/spy': 4.1.12 + '@vitest/utils': 4.1.12 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.2.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.3.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.2.0 + vite: 7.3.2(@types/node@24.3.0)(jiti@2.6.1)(sass@1.97.3)(terser@5.43.1)(tsx@4.21.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 24.3.0 + jsdom: 26.1.0 + transitivePeerDependencies: + - msw + vscode-uri@3.1.0: {} vue-bundle-renderer@2.2.0: @@ -33118,7 +33529,7 @@ snapshots: figures: 3.2.0 markdown-table: 2.0.0 pretty-time: 1.1.0 - std-env: 3.9.0 + std-env: 3.10.0 webpack: 5.102.0 wrap-ansi: 7.0.0 diff --git a/skills/concepts/SKILL.md b/skills/concepts/SKILL.md index 41d030039d1..1d345ba69c2 100644 --- a/skills/concepts/SKILL.md +++ b/skills/concepts/SKILL.md @@ -31,9 +31,10 @@ SpacetimeDB is a relational database that is also a server. It lets you upload a 1. **Backend:** Define table(s) to store the data 2. **Backend:** Define reducer(s) to mutate the data -3. **Client:** Subscribe to the table(s) -4. **Client:** Call the reducer(s) from UI -5. **Client:** Render the data from the table(s) +3. **Backend:** Unit-test the reducer(s) with the in-process module test harness (TypeScript and Rust modules; see https://spacetimedb.com/docs/how-to/unit-testing) +4. **Client:** Subscribe to the table(s) +5. **Client:** Call the reducer(s) from UI +6. **Client:** Render the data from the table(s) --- diff --git a/skills/rust-server/SKILL.md b/skills/rust-server/SKILL.md index 58854ab3539..98f35af8653 100644 --- a/skills/rust-server/SKILL.md +++ b/skills/rust-server/SKILL.md @@ -1,6 +1,6 @@ --- name: rust-server -description: SpacetimeDB Rust server module SDK reference. Use when writing tables, reducers, or module logic in Rust. +description: SpacetimeDB Rust server module SDK reference. Use when writing or unit-testing tables, reducers, or module logic in Rust. license: Apache-2.0 metadata: author: clockworklabs @@ -381,6 +381,50 @@ pub enum Status { Online, Away, Offline } pub struct Point { x: f32, y: f32 } ``` +## Testing + +Unit-test reducers, procedures and views in-process with the module test harness (`spacetimedb` 2.12+). It runs the module against the real datastore, in memory, so constraints and rollback behave as in production, with no server and no publish. Full guide: https://spacetimedb.com/docs/how-to/unit-testing + +Add a dev-dependency with the `test-utils` feature, at the same version as the normal dependency, and run tests natively with `cargo test` (not for a WebAssembly target): + +```toml +[dev-dependencies] +spacetimedb = { version = "2.12.0", features = ["test-utils"] } +icu_calendar = ">=2.1, <2.2" # until #6317 is fixed: the test datastore needs 2.1.x +``` + +`TestContext::new()` discovers the module's tables and functions and creates a fresh database. Call reducers as ordinary functions inside `with_reducer_tx`: + +```rust +#[cfg(test)] +mod tests { + use super::*; + use spacetimedb::test_utils::{TestAuth, TestContext}; + use spacetimedb::ConnectionId; + + fn alice() -> TestAuth { + TestAuth::from_jwt_payload(r#"{"iss":"test-issuer","sub":"alice"}"#, ConnectionId::from_u128(1)) + .expect("valid test claims") + } + + #[test] + fn creates_an_entity() { + let test = TestContext::new().expect("test database"); + test.with_reducer_tx(alice(), |ctx| { + create_entity(ctx, "Alice".to_string()); + Ok::<(), String>(()) + }) + .expect("reducer succeeds"); + assert_eq!(test.db.entity().count(), 1); + } +} +``` + +- `with_reducer_tx(auth, body)` commits when `body` returns `Ok`, and rolls back on `Err` or panic. For a reducer returning `()`, call it and then return `Ok::<(), String>(())`. +- `TestAuth::from_jwt_payload(claims_json, connection_id)` calls as a client; the same `iss` and `sub` always give the same `ctx.sender()`. `TestAuth::internal()` calls as the database itself, as scheduled reducers are. +- `test.clock` controls `ctx.timestamp`; `test.rng` controls `ctx.random()` and `ctx.rng()`. +- Subscriptions, client connections and deployment still need a running server (`spacetime start`). + ## Complete Example ```rust diff --git a/skills/typescript-server/SKILL.md b/skills/typescript-server/SKILL.md index 42bc4d80ba1..dc41fcd10e5 100644 --- a/skills/typescript-server/SKILL.md +++ b/skills/typescript-server/SKILL.md @@ -1,6 +1,6 @@ --- name: typescript-server -description: SpacetimeDB TypeScript server module SDK reference. Use when writing tables, reducers, or module logic in TypeScript. +description: SpacetimeDB TypeScript server module SDK reference. Use when writing or unit-testing tables, reducers, or module logic in TypeScript. license: Apache-2.0 metadata: author: clockworklabs @@ -439,3 +439,47 @@ export const routes = spacetimedb.httpRouter(new Router().get('/health', health) ``` Handlers are synchronous: return `SyncResponse` directly rather than marking the callback `async`. Pass the exported `httpHandler(...)` value to the router, not its raw callback. The router selects the path, while a handler reads request data with APIs such as `request.text()`; `Request` has no `path` property. A handler context does not expose `ctx.db`; use `ctx.withTx(tx => ...)` when a handler needs transactional database access. + +## Testing + +Unit-test reducers, procedures and views in-process with the module test harness (`spacetimedb` 2.12+). It runs the module against the real datastore, in memory, so constraints and rollback behave as in production, with no server and no publish. Full guide: https://spacetimedb.com/docs/how-to/unit-testing + +In the module directory, install `vitest` as a dev dependency, set `"type": "module"` and add `"test": "vitest run"` to `package.json`. The Vitest plugin replaces the host's `spacetime:sys` imports: + +```typescript +// vitest.config.ts +import { defineConfig, type Plugin } from 'vitest/config'; +import { spacetimedbModuleTestPlugin } from 'spacetimedb/server/test-utils/vitest'; + +export default defineConfig({ + plugins: [spacetimedbModuleTestPlugin() as unknown as Plugin], + test: { environment: 'node' }, +}); +``` + +Pass the default schema and all module exports to `createModuleTestHarness`; each call creates a fresh database. An exported reducer is callable as `reducer(ctx, args)` (`{}` for no arguments): + +```typescript +// src/index.test.ts +import { expect, it } from 'vitest'; +import { ConnectionId } from 'spacetimedb'; +import { createModuleTestHarness, TestAuth } from 'spacetimedb/server/test-utils'; +import spacetimedb, * as moduleExports from './index'; + +const alice = TestAuth.fromJwtPayload( + JSON.stringify({ iss: 'test-issuer', sub: 'alice' }), + new ConnectionId(1n) +); + +it('adds a record', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + test.withReducerTx(alice, ctx => moduleExports.addRecord(ctx, { value: 1 })); + expect(test.db.scoreRecord.count()).toBe(1n); +}); +``` + +- `withReducerTx(auth, body)` commits when `body` returns and rolls back and rethrows when it throws. Keep `body` synchronous; catching an error inside it commits. +- `TestAuth.fromJwtPayload(claimsJson, connectionId)` calls as a client; the same `iss` and `sub` always give the same `ctx.sender`. `TestAuth.internal()` calls as the database itself, as scheduled reducers are. +- `test.clock.set(...)` / `test.clock.advance(...)` control `ctx.timestamp`; the `rngSeed` option controls `ctx.random`. +- `test.viewContext(auth)` calls views; `test.procedureContextBuilder(auth).http(responder)` stubs outbound HTTP for procedures. +- Subscriptions, client connections and deployment still need a running server (`spacetime start`). diff --git a/templates/angular-ts/package.json b/templates/angular-ts/package.json index 0091a526d6c..166a7923723 100644 --- a/templates/angular-ts/package.json +++ b/templates/angular-ts/package.json @@ -9,7 +9,8 @@ "generate": "cargo run -p gen-bindings -- --out-dir src/module_bindings --module-path spacetimedb && prettier --write src/module_bindings", "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb", "spacetime:publish:local": "spacetime publish --module-path spacetimedb --server local", - "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud" + "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud", + "test": "vitest run --root spacetimedb" }, "dependencies": { "@angular/common": "^21.2.12", @@ -24,7 +25,8 @@ "@angular/build": "^21.2.12", "@angular/cli": "^21.2.12", "@angular/compiler-cli": "^21.2.12", - "typescript": "~5.9.2" + "typescript": "~5.9.2", + "vitest": "^4.1.12" }, "engines": { "node": "^22.23.4 || ^24.22.1 || >=26.12.1" diff --git a/templates/angular-ts/spacetimedb/package.json b/templates/angular-ts/spacetimedb/package.json index 8a263dc97a6..ea73c85a710 100644 --- a/templates/angular-ts/spacetimedb/package.json +++ b/templates/angular-ts/spacetimedb/package.json @@ -2,9 +2,11 @@ "name": "spacetime-module", "version": "1.0.0", "description": "", + "type": "module", "scripts": { "build": "spacetime build", - "publish": "spacetime publish" + "publish": "spacetime publish", + "test": "vitest run" }, "keywords": [], "author": "", @@ -13,6 +15,7 @@ "spacetimedb": "workspace:*" }, "devDependencies": { - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" } } diff --git a/templates/angular-ts/spacetimedb/src/index.test.ts b/templates/angular-ts/spacetimedb/src/index.test.ts new file mode 100644 index 00000000000..440c2b21eb1 --- /dev/null +++ b/templates/angular-ts/spacetimedb/src/index.test.ts @@ -0,0 +1,41 @@ +// Unit tests run the module against an in-memory database, with no server. +// See https://spacetimedb.com/docs/how-to/unit-testing +import { expect, it } from 'vitest'; +import { ConnectionId } from 'spacetimedb'; +import { + createModuleTestHarness, + TestAuth, +} from 'spacetimedb/server/test-utils'; +import spacetimedb, * as moduleExports from './index'; + +function clientAuth() { + return TestAuth.fromJwtPayload( + JSON.stringify({ iss: 'test-issuer', sub: 'alice' }), + new ConnectionId(1n) + ); +} + +it('adds a person', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + }); + + expect([...test.db.person.iter()].map(person => person.name)).toEqual([ + 'Alice', + ]); +}); + +it('rolls back all writes when the transaction throws', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + expect(() => + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + throw new Error('Something went wrong'); + }) + ).toThrow('Something went wrong'); + + expect(test.db.person.count()).toBe(0n); +}); diff --git a/templates/angular-ts/spacetimedb/vitest.config.ts b/templates/angular-ts/spacetimedb/vitest.config.ts new file mode 100644 index 00000000000..e65ffd04d4a --- /dev/null +++ b/templates/angular-ts/spacetimedb/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, type Plugin } from 'vitest/config'; +import { spacetimedbModuleTestPlugin } from 'spacetimedb/server/test-utils/vitest'; + +export default defineConfig({ + plugins: [spacetimedbModuleTestPlugin() as unknown as Plugin], + test: { + environment: 'node', + }, +}); diff --git a/templates/basic-rs/spacetimedb/Cargo.toml b/templates/basic-rs/spacetimedb/Cargo.toml index 5aef9971709..58db7414a27 100644 --- a/templates/basic-rs/spacetimedb/Cargo.toml +++ b/templates/basic-rs/spacetimedb/Cargo.toml @@ -11,3 +11,10 @@ crate-type = ["cdylib"] [dependencies] spacetimedb = { path = "../../../crates/bindings" } log = "0.4" + +[dev-dependencies] +# Unit tests run the module against an in-memory database with `cargo test`. +# See https://spacetimedb.com/docs/how-to/unit-testing +spacetimedb = { path = "../../../crates/bindings", features = ["test-utils"] } +# The test datastore needs icu_calendar 2.1.x until #6317 is fixed. +icu_calendar = { version = ">=2.1, <2.2", default-features = false } diff --git a/templates/basic-rs/spacetimedb/src/lib.rs b/templates/basic-rs/spacetimedb/src/lib.rs index e03cee881e1..6578cee342b 100644 --- a/templates/basic-rs/spacetimedb/src/lib.rs +++ b/templates/basic-rs/spacetimedb/src/lib.rs @@ -32,3 +32,44 @@ pub fn say_hello(ctx: &ReducerContext) { } log::info!("Hello, World!"); } + +// Unit tests run the module against an in-memory database, with no server. +// See https://spacetimedb.com/docs/how-to/unit-testing +#[cfg(test)] +mod tests { + use super::*; + use spacetimedb::test_utils::{TestAuth, TestContext}; + use spacetimedb::ConnectionId; + + fn client_auth() -> TestAuth { + TestAuth::from_jwt_payload(r#"{"iss":"test-issuer","sub":"alice"}"#, ConnectionId::from_u128(1)) + .expect("valid test claims") + } + + #[test] + fn adds_a_person() { + let test = TestContext::new().expect("test database"); + + test.with_reducer_tx(client_auth(), |ctx| { + add(ctx, "Alice".to_string()); + Ok::<(), String>(()) + }) + .expect("reducer succeeds"); + + let names: Vec = test.db.person().iter().map(|person| person.name).collect(); + assert_eq!(names, ["Alice"]); + } + + #[test] + fn rolls_back_all_writes_on_error() { + let test = TestContext::new().expect("test database"); + + let result = test.with_reducer_tx(client_auth(), |ctx| { + add(ctx, "Alice".to_string()); + Err::<(), String>("Something went wrong".to_string()) + }); + + assert_eq!(result, Err("Something went wrong".to_string())); + assert_eq!(test.db.person().count(), 0); + } +} diff --git a/templates/basic-ts/package.json b/templates/basic-ts/package.json index be34b675892..beb61d07c34 100644 --- a/templates/basic-ts/package.json +++ b/templates/basic-ts/package.json @@ -9,7 +9,8 @@ "build": "esbuild src/main.ts --bundle --platform=node --outfile=dist/main.js --format=esm", "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb", "spacetime:publish:local": "spacetime publish --module-path spacetimedb --server local", - "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud" + "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud", + "test": "vitest run --root spacetimedb" }, "dependencies": { "spacetimedb": "workspace:*" @@ -17,7 +18,8 @@ "devDependencies": { "@types/node": "^20.0.0", "esbuild": "^0.24.0", - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" }, "engines": { "node": "^22.23.4 || ^24.22.1 || >=26.12.1" diff --git a/templates/basic-ts/spacetimedb/package.json b/templates/basic-ts/spacetimedb/package.json index 8a263dc97a6..ea73c85a710 100644 --- a/templates/basic-ts/spacetimedb/package.json +++ b/templates/basic-ts/spacetimedb/package.json @@ -2,9 +2,11 @@ "name": "spacetime-module", "version": "1.0.0", "description": "", + "type": "module", "scripts": { "build": "spacetime build", - "publish": "spacetime publish" + "publish": "spacetime publish", + "test": "vitest run" }, "keywords": [], "author": "", @@ -13,6 +15,7 @@ "spacetimedb": "workspace:*" }, "devDependencies": { - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" } } diff --git a/templates/basic-ts/spacetimedb/src/index.test.ts b/templates/basic-ts/spacetimedb/src/index.test.ts new file mode 100644 index 00000000000..440c2b21eb1 --- /dev/null +++ b/templates/basic-ts/spacetimedb/src/index.test.ts @@ -0,0 +1,41 @@ +// Unit tests run the module against an in-memory database, with no server. +// See https://spacetimedb.com/docs/how-to/unit-testing +import { expect, it } from 'vitest'; +import { ConnectionId } from 'spacetimedb'; +import { + createModuleTestHarness, + TestAuth, +} from 'spacetimedb/server/test-utils'; +import spacetimedb, * as moduleExports from './index'; + +function clientAuth() { + return TestAuth.fromJwtPayload( + JSON.stringify({ iss: 'test-issuer', sub: 'alice' }), + new ConnectionId(1n) + ); +} + +it('adds a person', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + }); + + expect([...test.db.person.iter()].map(person => person.name)).toEqual([ + 'Alice', + ]); +}); + +it('rolls back all writes when the transaction throws', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + expect(() => + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + throw new Error('Something went wrong'); + }) + ).toThrow('Something went wrong'); + + expect(test.db.person.count()).toBe(0n); +}); diff --git a/templates/basic-ts/spacetimedb/vitest.config.ts b/templates/basic-ts/spacetimedb/vitest.config.ts new file mode 100644 index 00000000000..e65ffd04d4a --- /dev/null +++ b/templates/basic-ts/spacetimedb/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, type Plugin } from 'vitest/config'; +import { spacetimedbModuleTestPlugin } from 'spacetimedb/server/test-utils/vitest'; + +export default defineConfig({ + plugins: [spacetimedbModuleTestPlugin() as unknown as Plugin], + test: { + environment: 'node', + }, +}); diff --git a/templates/browser-ts/package.json b/templates/browser-ts/package.json index b4815faa5a6..7facff5cf1d 100644 --- a/templates/browser-ts/package.json +++ b/templates/browser-ts/package.json @@ -8,14 +8,16 @@ "build": "vite build", "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb", "spacetime:publish:local": "spacetime publish --module-path spacetimedb --server local", - "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud" + "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud", + "test": "vitest run --root spacetimedb" }, "dependencies": { "spacetimedb": "workspace:*" }, "devDependencies": { "typescript": "~5.6.2", - "vite": "^7.1.5" + "vite": "^7.1.5", + "vitest": "^4.1.12" }, "engines": { "node": "^22.23.4 || ^24.22.1 || >=26.12.1" diff --git a/templates/browser-ts/spacetimedb/package.json b/templates/browser-ts/spacetimedb/package.json index 8a263dc97a6..ea73c85a710 100644 --- a/templates/browser-ts/spacetimedb/package.json +++ b/templates/browser-ts/spacetimedb/package.json @@ -2,9 +2,11 @@ "name": "spacetime-module", "version": "1.0.0", "description": "", + "type": "module", "scripts": { "build": "spacetime build", - "publish": "spacetime publish" + "publish": "spacetime publish", + "test": "vitest run" }, "keywords": [], "author": "", @@ -13,6 +15,7 @@ "spacetimedb": "workspace:*" }, "devDependencies": { - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" } } diff --git a/templates/browser-ts/spacetimedb/src/index.test.ts b/templates/browser-ts/spacetimedb/src/index.test.ts new file mode 100644 index 00000000000..440c2b21eb1 --- /dev/null +++ b/templates/browser-ts/spacetimedb/src/index.test.ts @@ -0,0 +1,41 @@ +// Unit tests run the module against an in-memory database, with no server. +// See https://spacetimedb.com/docs/how-to/unit-testing +import { expect, it } from 'vitest'; +import { ConnectionId } from 'spacetimedb'; +import { + createModuleTestHarness, + TestAuth, +} from 'spacetimedb/server/test-utils'; +import spacetimedb, * as moduleExports from './index'; + +function clientAuth() { + return TestAuth.fromJwtPayload( + JSON.stringify({ iss: 'test-issuer', sub: 'alice' }), + new ConnectionId(1n) + ); +} + +it('adds a person', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + }); + + expect([...test.db.person.iter()].map(person => person.name)).toEqual([ + 'Alice', + ]); +}); + +it('rolls back all writes when the transaction throws', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + expect(() => + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + throw new Error('Something went wrong'); + }) + ).toThrow('Something went wrong'); + + expect(test.db.person.count()).toBe(0n); +}); diff --git a/templates/browser-ts/spacetimedb/vitest.config.ts b/templates/browser-ts/spacetimedb/vitest.config.ts new file mode 100644 index 00000000000..e65ffd04d4a --- /dev/null +++ b/templates/browser-ts/spacetimedb/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, type Plugin } from 'vitest/config'; +import { spacetimedbModuleTestPlugin } from 'spacetimedb/server/test-utils/vitest'; + +export default defineConfig({ + plugins: [spacetimedbModuleTestPlugin() as unknown as Plugin], + test: { + environment: 'node', + }, +}); diff --git a/templates/bun-ts/package.json b/templates/bun-ts/package.json index 84008784167..099590ee6de 100644 --- a/templates/bun-ts/package.json +++ b/templates/bun-ts/package.json @@ -7,7 +7,8 @@ "dev": "bun --watch src/main.ts", "start": "bun src/main.ts", "build": "bun build src/main.ts --outdir dist", - "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb" + "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb", + "test": "vitest run --root spacetimedb" }, "dependencies": { "spacetimedb": "workspace:*" @@ -15,6 +16,7 @@ "devDependencies": { "@types/bun": "latest", "bun": "^1.4.4", - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" } } diff --git a/templates/bun-ts/spacetimedb/package.json b/templates/bun-ts/spacetimedb/package.json index 8a263dc97a6..ea73c85a710 100644 --- a/templates/bun-ts/spacetimedb/package.json +++ b/templates/bun-ts/spacetimedb/package.json @@ -2,9 +2,11 @@ "name": "spacetime-module", "version": "1.0.0", "description": "", + "type": "module", "scripts": { "build": "spacetime build", - "publish": "spacetime publish" + "publish": "spacetime publish", + "test": "vitest run" }, "keywords": [], "author": "", @@ -13,6 +15,7 @@ "spacetimedb": "workspace:*" }, "devDependencies": { - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" } } diff --git a/templates/bun-ts/spacetimedb/src/index.test.ts b/templates/bun-ts/spacetimedb/src/index.test.ts new file mode 100644 index 00000000000..440c2b21eb1 --- /dev/null +++ b/templates/bun-ts/spacetimedb/src/index.test.ts @@ -0,0 +1,41 @@ +// Unit tests run the module against an in-memory database, with no server. +// See https://spacetimedb.com/docs/how-to/unit-testing +import { expect, it } from 'vitest'; +import { ConnectionId } from 'spacetimedb'; +import { + createModuleTestHarness, + TestAuth, +} from 'spacetimedb/server/test-utils'; +import spacetimedb, * as moduleExports from './index'; + +function clientAuth() { + return TestAuth.fromJwtPayload( + JSON.stringify({ iss: 'test-issuer', sub: 'alice' }), + new ConnectionId(1n) + ); +} + +it('adds a person', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + }); + + expect([...test.db.person.iter()].map(person => person.name)).toEqual([ + 'Alice', + ]); +}); + +it('rolls back all writes when the transaction throws', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + expect(() => + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + throw new Error('Something went wrong'); + }) + ).toThrow('Something went wrong'); + + expect(test.db.person.count()).toBe(0n); +}); diff --git a/templates/bun-ts/spacetimedb/vitest.config.ts b/templates/bun-ts/spacetimedb/vitest.config.ts new file mode 100644 index 00000000000..e65ffd04d4a --- /dev/null +++ b/templates/bun-ts/spacetimedb/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, type Plugin } from 'vitest/config'; +import { spacetimedbModuleTestPlugin } from 'spacetimedb/server/test-utils/vitest'; + +export default defineConfig({ + plugins: [spacetimedbModuleTestPlugin() as unknown as Plugin], + test: { + environment: 'node', + }, +}); diff --git a/templates/chat-react-ts/package.json b/templates/chat-react-ts/package.json index bcf6b765084..6eab04b115d 100644 --- a/templates/chat-react-ts/package.json +++ b/templates/chat-react-ts/package.json @@ -9,7 +9,7 @@ "format": "prettier . --write --ignore-path ../../.prettierignore", "lint": "eslint . && prettier . --check --ignore-path ../../.prettierignore", "preview": "vite preview", - "test": "vitest run", + "test": "vitest run --root spacetimedb && vitest run", "generate": "cargo run -p gen-bindings -- --out-dir src/module_bindings --module-path spacetimedb && prettier --write src/module_bindings", "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb", "spacetime:publish:local": "spacetime publish --module-path server --server local", diff --git a/templates/chat-react-ts/src/App.integration.test.tsx b/templates/chat-react-ts/src/App.integration.test.tsx index 6e2bcd7276c..740dc5d6c84 100644 --- a/templates/chat-react-ts/src/App.integration.test.tsx +++ b/templates/chat-react-ts/src/App.integration.test.tsx @@ -5,16 +5,19 @@ import App from './App'; import { SpacetimeDBProvider } from 'spacetimedb/react'; import { DbConnection } from './module_bindings'; +// Connect to the same database as the app, unless the environment points +// elsewhere (the template smoketest publishes to its own server). +const HOST = import.meta.env.VITE_SPACETIMEDB_HOST ?? 'ws://localhost:3000'; +const DB_NAME = import.meta.env.VITE_SPACETIMEDB_DB_NAME ?? 'quickstart-chat'; + describe('App Integration Test', () => { it('connects to the DB, allows name change and message sending', async () => { + // No token: the test connects as a fresh anonymous identity. (It used to + // look one up in localStorage, but jsdom's storage always starts empty, and + // Node 25+ shadows it with a global that is undefined unless configured.) const connectionBuilder = DbConnection.builder() - .withUri('ws://localhost:3000') - .withDatabaseName('quickstart-chat') - .withToken( - localStorage.getItem( - 'ws://localhost:3000/quickstart-chat/auth_token' - ) || '' - ); + .withUri(HOST) + .withDatabaseName(DB_NAME); render( @@ -53,7 +56,8 @@ describe('App Integration Test', () => { // If your DB or UI updates instantly, we can check that the new name shows up await waitFor( () => { - expect(screen.getByText('TestUser')).toBeInTheDocument(); + // The name shows in both the profile and the list of users. + expect(screen.getAllByText('TestUser').length).toBeGreaterThan(0); }, { timeout: 10000 } ); diff --git a/templates/chat-react-ts/vitest.config.ts b/templates/chat-react-ts/vitest.config.ts index 5291d9f29fd..4ad674aa8ba 100644 --- a/templates/chat-react-ts/vitest.config.ts +++ b/templates/chat-react-ts/vitest.config.ts @@ -1,14 +1,19 @@ -import { defineConfig } from 'vitest/config'; +import { configDefaults, defineConfig, type Plugin } from 'vitest/config'; import react from '@vitejs/plugin-react'; -import type { PluginOption } from 'vite'; export default defineConfig({ - plugins: [react() as unknown as PluginOption], + plugins: [react() as unknown as Plugin], + // Use one copy of React even when the SDK is linked from a local checkout. + resolve: { + dedupe: ['react', 'react-dom'], + }, test: { globals: true, environment: 'jsdom', // or "node" if you're not testing DOM setupFiles: './src/setupTests.ts', testTimeout: 15_000, // give extra time for real connections hookTimeout: 15_000, + // The module's tests run separately, with its own config. + exclude: [...configDefaults.exclude, 'spacetimedb/**'], }, }); diff --git a/templates/deno-ts/package.json b/templates/deno-ts/package.json index 6d25e2c1d38..e7d05940f49 100644 --- a/templates/deno-ts/package.json +++ b/templates/deno-ts/package.json @@ -6,12 +6,14 @@ "scripts": { "dev": "deno run --watch --allow-net --allow-read --allow-write --allow-env --unstable-sloppy-imports src/main.ts", "start": "deno run --allow-net --allow-read --allow-write --allow-env --unstable-sloppy-imports src/main.ts", - "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb" + "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb", + "test": "vitest run --root spacetimedb" }, "dependencies": { "spacetimedb": "workspace:*" }, "devDependencies": { - "deno": "^2.6.0" + "deno": "^2.6.0", + "vitest": "^4.1.12" } } diff --git a/templates/deno-ts/spacetimedb/package.json b/templates/deno-ts/spacetimedb/package.json index 8a263dc97a6..ea73c85a710 100644 --- a/templates/deno-ts/spacetimedb/package.json +++ b/templates/deno-ts/spacetimedb/package.json @@ -2,9 +2,11 @@ "name": "spacetime-module", "version": "1.0.0", "description": "", + "type": "module", "scripts": { "build": "spacetime build", - "publish": "spacetime publish" + "publish": "spacetime publish", + "test": "vitest run" }, "keywords": [], "author": "", @@ -13,6 +15,7 @@ "spacetimedb": "workspace:*" }, "devDependencies": { - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" } } diff --git a/templates/deno-ts/spacetimedb/src/index.test.ts b/templates/deno-ts/spacetimedb/src/index.test.ts new file mode 100644 index 00000000000..440c2b21eb1 --- /dev/null +++ b/templates/deno-ts/spacetimedb/src/index.test.ts @@ -0,0 +1,41 @@ +// Unit tests run the module against an in-memory database, with no server. +// See https://spacetimedb.com/docs/how-to/unit-testing +import { expect, it } from 'vitest'; +import { ConnectionId } from 'spacetimedb'; +import { + createModuleTestHarness, + TestAuth, +} from 'spacetimedb/server/test-utils'; +import spacetimedb, * as moduleExports from './index'; + +function clientAuth() { + return TestAuth.fromJwtPayload( + JSON.stringify({ iss: 'test-issuer', sub: 'alice' }), + new ConnectionId(1n) + ); +} + +it('adds a person', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + }); + + expect([...test.db.person.iter()].map(person => person.name)).toEqual([ + 'Alice', + ]); +}); + +it('rolls back all writes when the transaction throws', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + expect(() => + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + throw new Error('Something went wrong'); + }) + ).toThrow('Something went wrong'); + + expect(test.db.person.count()).toBe(0n); +}); diff --git a/templates/deno-ts/spacetimedb/vitest.config.ts b/templates/deno-ts/spacetimedb/vitest.config.ts new file mode 100644 index 00000000000..e65ffd04d4a --- /dev/null +++ b/templates/deno-ts/spacetimedb/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, type Plugin } from 'vitest/config'; +import { spacetimedbModuleTestPlugin } from 'spacetimedb/server/test-utils/vitest'; + +export default defineConfig({ + plugins: [spacetimedbModuleTestPlugin() as unknown as Plugin], + test: { + environment: 'node', + }, +}); diff --git a/templates/money-exchange-react-ts/vitest.config.ts b/templates/money-exchange-react-ts/vitest.config.ts index 76e577fb930..02358192910 100644 --- a/templates/money-exchange-react-ts/vitest.config.ts +++ b/templates/money-exchange-react-ts/vitest.config.ts @@ -1,8 +1,8 @@ -import { defineConfig } from 'vitest/config'; +import { defineConfig, type Plugin } from 'vitest/config'; import react from '@vitejs/plugin-react'; export default defineConfig({ - plugins: [react()], + plugins: [react() as unknown as Plugin], test: { globals: true, environment: 'jsdom', diff --git a/templates/nextjs-ts/package.json b/templates/nextjs-ts/package.json index e59e6b5335b..19059dbcef6 100644 --- a/templates/nextjs-ts/package.json +++ b/templates/nextjs-ts/package.json @@ -11,7 +11,8 @@ "generate": "pnpm --dir spacetimedb install && cargo run -p gen-bindings -- --out-dir src/module_bindings --module-path spacetimedb && prettier --write src/module_bindings", "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb", "spacetime:publish:local": "spacetime publish --module-path spacetimedb --server local", - "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud" + "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud", + "test": "vitest run --root spacetimedb" }, "dependencies": { "next": "^15.0.0", @@ -23,7 +24,8 @@ "@types/node": "^20", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" }, "engines": { "node": "^22.23.4 || ^24.22.1 || >=26.12.1" diff --git a/templates/nextjs-ts/spacetimedb/package.json b/templates/nextjs-ts/spacetimedb/package.json index 8a263dc97a6..ea73c85a710 100644 --- a/templates/nextjs-ts/spacetimedb/package.json +++ b/templates/nextjs-ts/spacetimedb/package.json @@ -2,9 +2,11 @@ "name": "spacetime-module", "version": "1.0.0", "description": "", + "type": "module", "scripts": { "build": "spacetime build", - "publish": "spacetime publish" + "publish": "spacetime publish", + "test": "vitest run" }, "keywords": [], "author": "", @@ -13,6 +15,7 @@ "spacetimedb": "workspace:*" }, "devDependencies": { - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" } } diff --git a/templates/nextjs-ts/spacetimedb/src/index.test.ts b/templates/nextjs-ts/spacetimedb/src/index.test.ts new file mode 100644 index 00000000000..440c2b21eb1 --- /dev/null +++ b/templates/nextjs-ts/spacetimedb/src/index.test.ts @@ -0,0 +1,41 @@ +// Unit tests run the module against an in-memory database, with no server. +// See https://spacetimedb.com/docs/how-to/unit-testing +import { expect, it } from 'vitest'; +import { ConnectionId } from 'spacetimedb'; +import { + createModuleTestHarness, + TestAuth, +} from 'spacetimedb/server/test-utils'; +import spacetimedb, * as moduleExports from './index'; + +function clientAuth() { + return TestAuth.fromJwtPayload( + JSON.stringify({ iss: 'test-issuer', sub: 'alice' }), + new ConnectionId(1n) + ); +} + +it('adds a person', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + }); + + expect([...test.db.person.iter()].map(person => person.name)).toEqual([ + 'Alice', + ]); +}); + +it('rolls back all writes when the transaction throws', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + expect(() => + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + throw new Error('Something went wrong'); + }) + ).toThrow('Something went wrong'); + + expect(test.db.person.count()).toBe(0n); +}); diff --git a/templates/nextjs-ts/spacetimedb/tsconfig.json b/templates/nextjs-ts/spacetimedb/tsconfig.json index 0bede6a959b..c089b1b05be 100644 --- a/templates/nextjs-ts/spacetimedb/tsconfig.json +++ b/templates/nextjs-ts/spacetimedb/tsconfig.json @@ -8,5 +8,5 @@ "skipLibCheck": true, "outDir": "./dist" }, - "include": ["src/**/*"] + "include": ["src/**/*", "vitest.config.ts"] } diff --git a/templates/nextjs-ts/spacetimedb/vitest.config.ts b/templates/nextjs-ts/spacetimedb/vitest.config.ts new file mode 100644 index 00000000000..e65ffd04d4a --- /dev/null +++ b/templates/nextjs-ts/spacetimedb/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, type Plugin } from 'vitest/config'; +import { spacetimedbModuleTestPlugin } from 'spacetimedb/server/test-utils/vitest'; + +export default defineConfig({ + plugins: [spacetimedbModuleTestPlugin() as unknown as Plugin], + test: { + environment: 'node', + }, +}); diff --git a/templates/nextjs-ts/tsconfig.json b/templates/nextjs-ts/tsconfig.json index 2c145a2d166..5ed99f71a97 100644 --- a/templates/nextjs-ts/tsconfig.json +++ b/templates/nextjs-ts/tsconfig.json @@ -23,5 +23,5 @@ } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules", "spacetimedb"] } diff --git a/templates/nodejs-ts/package.json b/templates/nodejs-ts/package.json index 88aa3585e99..3ce96f342fd 100644 --- a/templates/nodejs-ts/package.json +++ b/templates/nodejs-ts/package.json @@ -8,7 +8,8 @@ "start": "node dist/main.js", "build": "esbuild src/main.ts --bundle --platform=node --outfile=dist/main.js --format=esm", "typecheck": "tsc --noEmit", - "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb" + "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb", + "test": "vitest run --root spacetimedb" }, "dependencies": { "spacetimedb": "workspace:*" @@ -17,7 +18,8 @@ "@types/node": "^20.0.0", "esbuild": "^0.24.0", "typescript": "~5.6.2", - "undici": "^6.19.2" + "undici": "^6.19.2", + "vitest": "^4.1.12" }, "engines": { "node": "^22.23.4 || ^24.22.1 || >=26.12.1" diff --git a/templates/nodejs-ts/spacetimedb/package.json b/templates/nodejs-ts/spacetimedb/package.json index 8a263dc97a6..ea73c85a710 100644 --- a/templates/nodejs-ts/spacetimedb/package.json +++ b/templates/nodejs-ts/spacetimedb/package.json @@ -2,9 +2,11 @@ "name": "spacetime-module", "version": "1.0.0", "description": "", + "type": "module", "scripts": { "build": "spacetime build", - "publish": "spacetime publish" + "publish": "spacetime publish", + "test": "vitest run" }, "keywords": [], "author": "", @@ -13,6 +15,7 @@ "spacetimedb": "workspace:*" }, "devDependencies": { - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" } } diff --git a/templates/nodejs-ts/spacetimedb/src/index.test.ts b/templates/nodejs-ts/spacetimedb/src/index.test.ts new file mode 100644 index 00000000000..440c2b21eb1 --- /dev/null +++ b/templates/nodejs-ts/spacetimedb/src/index.test.ts @@ -0,0 +1,41 @@ +// Unit tests run the module against an in-memory database, with no server. +// See https://spacetimedb.com/docs/how-to/unit-testing +import { expect, it } from 'vitest'; +import { ConnectionId } from 'spacetimedb'; +import { + createModuleTestHarness, + TestAuth, +} from 'spacetimedb/server/test-utils'; +import spacetimedb, * as moduleExports from './index'; + +function clientAuth() { + return TestAuth.fromJwtPayload( + JSON.stringify({ iss: 'test-issuer', sub: 'alice' }), + new ConnectionId(1n) + ); +} + +it('adds a person', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + }); + + expect([...test.db.person.iter()].map(person => person.name)).toEqual([ + 'Alice', + ]); +}); + +it('rolls back all writes when the transaction throws', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + expect(() => + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + throw new Error('Something went wrong'); + }) + ).toThrow('Something went wrong'); + + expect(test.db.person.count()).toBe(0n); +}); diff --git a/templates/nodejs-ts/spacetimedb/vitest.config.ts b/templates/nodejs-ts/spacetimedb/vitest.config.ts new file mode 100644 index 00000000000..e65ffd04d4a --- /dev/null +++ b/templates/nodejs-ts/spacetimedb/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, type Plugin } from 'vitest/config'; +import { spacetimedbModuleTestPlugin } from 'spacetimedb/server/test-utils/vitest'; + +export default defineConfig({ + plugins: [spacetimedbModuleTestPlugin() as unknown as Plugin], + test: { + environment: 'node', + }, +}); diff --git a/templates/nuxt-ts/package.json b/templates/nuxt-ts/package.json index 8ded945f033..84da32bf9a3 100644 --- a/templates/nuxt-ts/package.json +++ b/templates/nuxt-ts/package.json @@ -10,7 +10,8 @@ "generate": "pnpm --dir spacetimedb install && cargo run -p gen-bindings -- --out-dir module_bindings --module-path spacetimedb && prettier --write module_bindings", "spacetime:generate": "spacetime generate --lang typescript --out-dir module_bindings --module-path spacetimedb", "spacetime:publish:local": "spacetime publish --module-path spacetimedb --server local", - "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud" + "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud", + "test": "vitest run --root spacetimedb" }, "dependencies": { "spacetimedb": "workspace:*", @@ -18,7 +19,8 @@ "vue": "^3.5.13" }, "devDependencies": { - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" }, "engines": { "node": "^22.23.4 || ^24.22.1 || >=26.12.1" diff --git a/templates/nuxt-ts/spacetimedb/package.json b/templates/nuxt-ts/spacetimedb/package.json index 8a263dc97a6..ea73c85a710 100644 --- a/templates/nuxt-ts/spacetimedb/package.json +++ b/templates/nuxt-ts/spacetimedb/package.json @@ -2,9 +2,11 @@ "name": "spacetime-module", "version": "1.0.0", "description": "", + "type": "module", "scripts": { "build": "spacetime build", - "publish": "spacetime publish" + "publish": "spacetime publish", + "test": "vitest run" }, "keywords": [], "author": "", @@ -13,6 +15,7 @@ "spacetimedb": "workspace:*" }, "devDependencies": { - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" } } diff --git a/templates/nuxt-ts/spacetimedb/src/index.test.ts b/templates/nuxt-ts/spacetimedb/src/index.test.ts new file mode 100644 index 00000000000..440c2b21eb1 --- /dev/null +++ b/templates/nuxt-ts/spacetimedb/src/index.test.ts @@ -0,0 +1,41 @@ +// Unit tests run the module against an in-memory database, with no server. +// See https://spacetimedb.com/docs/how-to/unit-testing +import { expect, it } from 'vitest'; +import { ConnectionId } from 'spacetimedb'; +import { + createModuleTestHarness, + TestAuth, +} from 'spacetimedb/server/test-utils'; +import spacetimedb, * as moduleExports from './index'; + +function clientAuth() { + return TestAuth.fromJwtPayload( + JSON.stringify({ iss: 'test-issuer', sub: 'alice' }), + new ConnectionId(1n) + ); +} + +it('adds a person', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + }); + + expect([...test.db.person.iter()].map(person => person.name)).toEqual([ + 'Alice', + ]); +}); + +it('rolls back all writes when the transaction throws', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + expect(() => + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + throw new Error('Something went wrong'); + }) + ).toThrow('Something went wrong'); + + expect(test.db.person.count()).toBe(0n); +}); diff --git a/templates/nuxt-ts/spacetimedb/vitest.config.ts b/templates/nuxt-ts/spacetimedb/vitest.config.ts new file mode 100644 index 00000000000..e65ffd04d4a --- /dev/null +++ b/templates/nuxt-ts/spacetimedb/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, type Plugin } from 'vitest/config'; +import { spacetimedbModuleTestPlugin } from 'spacetimedb/server/test-utils/vitest'; + +export default defineConfig({ + plugins: [spacetimedbModuleTestPlugin() as unknown as Plugin], + test: { + environment: 'node', + }, +}); diff --git a/templates/react-ts/package.json b/templates/react-ts/package.json index ce6adc37105..ebd8cf42c1e 100644 --- a/templates/react-ts/package.json +++ b/templates/react-ts/package.json @@ -10,7 +10,8 @@ "generate": "cargo run -p gen-bindings -- --out-dir src/module_bindings --module-path spacetimedb && prettier --write src/module_bindings", "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb", "spacetime:publish:local": "spacetime publish --module-path server --server local", - "spacetime:publish": "spacetime publish --module-path server --server maincloud" + "spacetime:publish": "spacetime publish --module-path server --server maincloud", + "test": "vitest run --root spacetimedb" }, "dependencies": { "spacetimedb": "workspace:*", @@ -22,7 +23,8 @@ "@types/react-dom": "^18.3.5", "@vitejs/plugin-react": "^5.0.2", "typescript": "~5.6.2", - "vite": "^7.1.5" + "vite": "^7.1.5", + "vitest": "^4.1.12" }, "engines": { "node": "^22.23.4 || ^24.22.1 || >=26.12.1" diff --git a/templates/react-ts/spacetimedb/package.json b/templates/react-ts/spacetimedb/package.json index 8a263dc97a6..ea73c85a710 100644 --- a/templates/react-ts/spacetimedb/package.json +++ b/templates/react-ts/spacetimedb/package.json @@ -2,9 +2,11 @@ "name": "spacetime-module", "version": "1.0.0", "description": "", + "type": "module", "scripts": { "build": "spacetime build", - "publish": "spacetime publish" + "publish": "spacetime publish", + "test": "vitest run" }, "keywords": [], "author": "", @@ -13,6 +15,7 @@ "spacetimedb": "workspace:*" }, "devDependencies": { - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" } } diff --git a/templates/react-ts/spacetimedb/src/index.test.ts b/templates/react-ts/spacetimedb/src/index.test.ts new file mode 100644 index 00000000000..440c2b21eb1 --- /dev/null +++ b/templates/react-ts/spacetimedb/src/index.test.ts @@ -0,0 +1,41 @@ +// Unit tests run the module against an in-memory database, with no server. +// See https://spacetimedb.com/docs/how-to/unit-testing +import { expect, it } from 'vitest'; +import { ConnectionId } from 'spacetimedb'; +import { + createModuleTestHarness, + TestAuth, +} from 'spacetimedb/server/test-utils'; +import spacetimedb, * as moduleExports from './index'; + +function clientAuth() { + return TestAuth.fromJwtPayload( + JSON.stringify({ iss: 'test-issuer', sub: 'alice' }), + new ConnectionId(1n) + ); +} + +it('adds a person', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + }); + + expect([...test.db.person.iter()].map(person => person.name)).toEqual([ + 'Alice', + ]); +}); + +it('rolls back all writes when the transaction throws', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + expect(() => + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + throw new Error('Something went wrong'); + }) + ).toThrow('Something went wrong'); + + expect(test.db.person.count()).toBe(0n); +}); diff --git a/templates/react-ts/spacetimedb/vitest.config.ts b/templates/react-ts/spacetimedb/vitest.config.ts new file mode 100644 index 00000000000..e65ffd04d4a --- /dev/null +++ b/templates/react-ts/spacetimedb/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, type Plugin } from 'vitest/config'; +import { spacetimedbModuleTestPlugin } from 'spacetimedb/server/test-utils/vitest'; + +export default defineConfig({ + plugins: [spacetimedbModuleTestPlugin() as unknown as Plugin], + test: { + environment: 'node', + }, +}); diff --git a/templates/remix-ts/package.json b/templates/remix-ts/package.json index 53530800d24..a6225ea9b62 100644 --- a/templates/remix-ts/package.json +++ b/templates/remix-ts/package.json @@ -10,7 +10,8 @@ "generate": "pnpm --dir spacetimedb install && cargo run -p gen-bindings -- --out-dir src/module_bindings --module-path spacetimedb && prettier --write src/module_bindings", "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb", "spacetime:publish:local": "spacetime publish --module-path spacetimedb --server local", - "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud" + "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud", + "test": "vitest run --root spacetimedb" }, "dependencies": { "@remix-run/node": "^2.16.0", @@ -26,7 +27,8 @@ "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "typescript": "~5.6.2", - "vite": "^5.4.0" + "vite": "^5.4.0", + "vitest": "^3.2.4" }, "engines": { "node": "^22.23.4 || ^24.22.1 || >=26.12.1" diff --git a/templates/remix-ts/spacetimedb/package.json b/templates/remix-ts/spacetimedb/package.json index 8a263dc97a6..6b99f0413c5 100644 --- a/templates/remix-ts/spacetimedb/package.json +++ b/templates/remix-ts/spacetimedb/package.json @@ -2,9 +2,11 @@ "name": "spacetime-module", "version": "1.0.0", "description": "", + "type": "module", "scripts": { "build": "spacetime build", - "publish": "spacetime publish" + "publish": "spacetime publish", + "test": "vitest run" }, "keywords": [], "author": "", @@ -13,6 +15,7 @@ "spacetimedb": "workspace:*" }, "devDependencies": { - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^3.2.4" } } diff --git a/templates/remix-ts/spacetimedb/src/index.test.ts b/templates/remix-ts/spacetimedb/src/index.test.ts new file mode 100644 index 00000000000..440c2b21eb1 --- /dev/null +++ b/templates/remix-ts/spacetimedb/src/index.test.ts @@ -0,0 +1,41 @@ +// Unit tests run the module against an in-memory database, with no server. +// See https://spacetimedb.com/docs/how-to/unit-testing +import { expect, it } from 'vitest'; +import { ConnectionId } from 'spacetimedb'; +import { + createModuleTestHarness, + TestAuth, +} from 'spacetimedb/server/test-utils'; +import spacetimedb, * as moduleExports from './index'; + +function clientAuth() { + return TestAuth.fromJwtPayload( + JSON.stringify({ iss: 'test-issuer', sub: 'alice' }), + new ConnectionId(1n) + ); +} + +it('adds a person', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + }); + + expect([...test.db.person.iter()].map(person => person.name)).toEqual([ + 'Alice', + ]); +}); + +it('rolls back all writes when the transaction throws', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + expect(() => + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + throw new Error('Something went wrong'); + }) + ).toThrow('Something went wrong'); + + expect(test.db.person.count()).toBe(0n); +}); diff --git a/templates/remix-ts/spacetimedb/tsconfig.json b/templates/remix-ts/spacetimedb/tsconfig.json index 0bede6a959b..c089b1b05be 100644 --- a/templates/remix-ts/spacetimedb/tsconfig.json +++ b/templates/remix-ts/spacetimedb/tsconfig.json @@ -8,5 +8,5 @@ "skipLibCheck": true, "outDir": "./dist" }, - "include": ["src/**/*"] + "include": ["src/**/*", "vitest.config.ts"] } diff --git a/templates/remix-ts/spacetimedb/vitest.config.ts b/templates/remix-ts/spacetimedb/vitest.config.ts new file mode 100644 index 00000000000..e65ffd04d4a --- /dev/null +++ b/templates/remix-ts/spacetimedb/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, type Plugin } from 'vitest/config'; +import { spacetimedbModuleTestPlugin } from 'spacetimedb/server/test-utils/vitest'; + +export default defineConfig({ + plugins: [spacetimedbModuleTestPlugin() as unknown as Plugin], + test: { + environment: 'node', + }, +}); diff --git a/templates/remix-ts/tsconfig.json b/templates/remix-ts/tsconfig.json index a8e947275c9..9e24b8a48df 100644 --- a/templates/remix-ts/tsconfig.json +++ b/templates/remix-ts/tsconfig.json @@ -26,5 +26,5 @@ "**/.client/**/*.ts", "**/.client/**/*.tsx" ], - "exclude": ["node_modules", "build"] + "exclude": ["node_modules", "build", "spacetimedb"] } diff --git a/templates/solid-ts/package.json b/templates/solid-ts/package.json index f4a3ca7c872..63696bd7656 100644 --- a/templates/solid-ts/package.json +++ b/templates/solid-ts/package.json @@ -10,7 +10,8 @@ "generate": "pnpm --dir spacetimedb install && cargo run -p gen-bindings -- --out-dir src/module_bindings --module-path spacetimedb && prettier --write src/module_bindings", "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb", "spacetime:publish:local": "spacetime publish --module-path spacetimedb --server local", - "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud" + "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud", + "test": "vitest run --root spacetimedb" }, "dependencies": { "spacetimedb": "workspace:*", @@ -19,7 +20,8 @@ "devDependencies": { "typescript": "~5.6.2", "vite": "^7.1.5", - "vite-plugin-solid": "^2.11.8" + "vite-plugin-solid": "^2.11.8", + "vitest": "^4.1.12" }, "engines": { "node": "^22.23.4 || ^24.22.1 || >=26.12.1" diff --git a/templates/solid-ts/spacetimedb/package.json b/templates/solid-ts/spacetimedb/package.json index 8a263dc97a6..ea73c85a710 100644 --- a/templates/solid-ts/spacetimedb/package.json +++ b/templates/solid-ts/spacetimedb/package.json @@ -2,9 +2,11 @@ "name": "spacetime-module", "version": "1.0.0", "description": "", + "type": "module", "scripts": { "build": "spacetime build", - "publish": "spacetime publish" + "publish": "spacetime publish", + "test": "vitest run" }, "keywords": [], "author": "", @@ -13,6 +15,7 @@ "spacetimedb": "workspace:*" }, "devDependencies": { - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" } } diff --git a/templates/solid-ts/spacetimedb/src/index.test.ts b/templates/solid-ts/spacetimedb/src/index.test.ts new file mode 100644 index 00000000000..440c2b21eb1 --- /dev/null +++ b/templates/solid-ts/spacetimedb/src/index.test.ts @@ -0,0 +1,41 @@ +// Unit tests run the module against an in-memory database, with no server. +// See https://spacetimedb.com/docs/how-to/unit-testing +import { expect, it } from 'vitest'; +import { ConnectionId } from 'spacetimedb'; +import { + createModuleTestHarness, + TestAuth, +} from 'spacetimedb/server/test-utils'; +import spacetimedb, * as moduleExports from './index'; + +function clientAuth() { + return TestAuth.fromJwtPayload( + JSON.stringify({ iss: 'test-issuer', sub: 'alice' }), + new ConnectionId(1n) + ); +} + +it('adds a person', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + }); + + expect([...test.db.person.iter()].map(person => person.name)).toEqual([ + 'Alice', + ]); +}); + +it('rolls back all writes when the transaction throws', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + expect(() => + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + throw new Error('Something went wrong'); + }) + ).toThrow('Something went wrong'); + + expect(test.db.person.count()).toBe(0n); +}); diff --git a/templates/solid-ts/spacetimedb/vitest.config.ts b/templates/solid-ts/spacetimedb/vitest.config.ts new file mode 100644 index 00000000000..e65ffd04d4a --- /dev/null +++ b/templates/solid-ts/spacetimedb/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, type Plugin } from 'vitest/config'; +import { spacetimedbModuleTestPlugin } from 'spacetimedb/server/test-utils/vitest'; + +export default defineConfig({ + plugins: [spacetimedbModuleTestPlugin() as unknown as Plugin], + test: { + environment: 'node', + }, +}); diff --git a/templates/svelte-ts/package.json b/templates/svelte-ts/package.json index b7f2ad5f115..cac7d18d8fd 100644 --- a/templates/svelte-ts/package.json +++ b/templates/svelte-ts/package.json @@ -10,7 +10,8 @@ "generate": "pnpm --dir spacetimedb install && cargo run -p gen-bindings -- --out-dir src/module_bindings --module-path spacetimedb && prettier --write src/module_bindings", "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb", "spacetime:publish:local": "spacetime publish --module-path spacetimedb --server local", - "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud" + "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud", + "test": "vitest run --root spacetimedb" }, "dependencies": { "spacetimedb": "workspace:*" @@ -20,7 +21,8 @@ "svelte": "^5.0.0", "svelte-check": "^4.0.0", "typescript": "~5.6.2", - "vite": "^6.4.1" + "vite": "^6.4.1", + "vitest": "^4.1.12" }, "engines": { "node": "^22.23.4 || ^24.22.1 || >=26.12.1" diff --git a/templates/svelte-ts/spacetimedb/package.json b/templates/svelte-ts/spacetimedb/package.json index 8a263dc97a6..ea73c85a710 100644 --- a/templates/svelte-ts/spacetimedb/package.json +++ b/templates/svelte-ts/spacetimedb/package.json @@ -2,9 +2,11 @@ "name": "spacetime-module", "version": "1.0.0", "description": "", + "type": "module", "scripts": { "build": "spacetime build", - "publish": "spacetime publish" + "publish": "spacetime publish", + "test": "vitest run" }, "keywords": [], "author": "", @@ -13,6 +15,7 @@ "spacetimedb": "workspace:*" }, "devDependencies": { - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" } } diff --git a/templates/svelte-ts/spacetimedb/src/index.test.ts b/templates/svelte-ts/spacetimedb/src/index.test.ts new file mode 100644 index 00000000000..440c2b21eb1 --- /dev/null +++ b/templates/svelte-ts/spacetimedb/src/index.test.ts @@ -0,0 +1,41 @@ +// Unit tests run the module against an in-memory database, with no server. +// See https://spacetimedb.com/docs/how-to/unit-testing +import { expect, it } from 'vitest'; +import { ConnectionId } from 'spacetimedb'; +import { + createModuleTestHarness, + TestAuth, +} from 'spacetimedb/server/test-utils'; +import spacetimedb, * as moduleExports from './index'; + +function clientAuth() { + return TestAuth.fromJwtPayload( + JSON.stringify({ iss: 'test-issuer', sub: 'alice' }), + new ConnectionId(1n) + ); +} + +it('adds a person', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + }); + + expect([...test.db.person.iter()].map(person => person.name)).toEqual([ + 'Alice', + ]); +}); + +it('rolls back all writes when the transaction throws', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + expect(() => + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + throw new Error('Something went wrong'); + }) + ).toThrow('Something went wrong'); + + expect(test.db.person.count()).toBe(0n); +}); diff --git a/templates/svelte-ts/spacetimedb/vitest.config.ts b/templates/svelte-ts/spacetimedb/vitest.config.ts new file mode 100644 index 00000000000..e65ffd04d4a --- /dev/null +++ b/templates/svelte-ts/spacetimedb/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, type Plugin } from 'vitest/config'; +import { spacetimedbModuleTestPlugin } from 'spacetimedb/server/test-utils/vitest'; + +export default defineConfig({ + plugins: [spacetimedbModuleTestPlugin() as unknown as Plugin], + test: { + environment: 'node', + }, +}); diff --git a/templates/tanstack-ts/package.json b/templates/tanstack-ts/package.json index 03f3f3efc36..d40d2d26617 100644 --- a/templates/tanstack-ts/package.json +++ b/templates/tanstack-ts/package.json @@ -10,7 +10,8 @@ "start": "PORT=4000 node .output/server/index.mjs", "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb", "spacetime:publish:local": "spacetime publish --module-path spacetimedb --server local", - "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud" + "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud", + "test": "vitest run --root spacetimedb" }, "dependencies": { "@tanstack/react-query": "^5.62.0", @@ -30,7 +31,8 @@ "@types/react-dom": "^19.0.0", "typescript": "^5.7.2", "vite": "^7.1.5", - "vite-tsconfig-paths": "^5.1.4" + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^4.1.12" }, "engines": { "node": "^22.23.4 || ^24.22.1 || >=26.12.1" diff --git a/templates/tanstack-ts/spacetimedb/package.json b/templates/tanstack-ts/spacetimedb/package.json index 8a263dc97a6..ea73c85a710 100644 --- a/templates/tanstack-ts/spacetimedb/package.json +++ b/templates/tanstack-ts/spacetimedb/package.json @@ -2,9 +2,11 @@ "name": "spacetime-module", "version": "1.0.0", "description": "", + "type": "module", "scripts": { "build": "spacetime build", - "publish": "spacetime publish" + "publish": "spacetime publish", + "test": "vitest run" }, "keywords": [], "author": "", @@ -13,6 +15,7 @@ "spacetimedb": "workspace:*" }, "devDependencies": { - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" } } diff --git a/templates/tanstack-ts/spacetimedb/src/index.test.ts b/templates/tanstack-ts/spacetimedb/src/index.test.ts new file mode 100644 index 00000000000..440c2b21eb1 --- /dev/null +++ b/templates/tanstack-ts/spacetimedb/src/index.test.ts @@ -0,0 +1,41 @@ +// Unit tests run the module against an in-memory database, with no server. +// See https://spacetimedb.com/docs/how-to/unit-testing +import { expect, it } from 'vitest'; +import { ConnectionId } from 'spacetimedb'; +import { + createModuleTestHarness, + TestAuth, +} from 'spacetimedb/server/test-utils'; +import spacetimedb, * as moduleExports from './index'; + +function clientAuth() { + return TestAuth.fromJwtPayload( + JSON.stringify({ iss: 'test-issuer', sub: 'alice' }), + new ConnectionId(1n) + ); +} + +it('adds a person', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + }); + + expect([...test.db.person.iter()].map(person => person.name)).toEqual([ + 'Alice', + ]); +}); + +it('rolls back all writes when the transaction throws', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + expect(() => + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + throw new Error('Something went wrong'); + }) + ).toThrow('Something went wrong'); + + expect(test.db.person.count()).toBe(0n); +}); diff --git a/templates/tanstack-ts/spacetimedb/vitest.config.ts b/templates/tanstack-ts/spacetimedb/vitest.config.ts new file mode 100644 index 00000000000..e65ffd04d4a --- /dev/null +++ b/templates/tanstack-ts/spacetimedb/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, type Plugin } from 'vitest/config'; +import { spacetimedbModuleTestPlugin } from 'spacetimedb/server/test-utils/vitest'; + +export default defineConfig({ + plugins: [spacetimedbModuleTestPlugin() as unknown as Plugin], + test: { + environment: 'node', + }, +}); diff --git a/templates/vue-ts/package.json b/templates/vue-ts/package.json index f7213006970..6a05a537ecd 100644 --- a/templates/vue-ts/package.json +++ b/templates/vue-ts/package.json @@ -10,7 +10,8 @@ "generate": "pnpm --dir spacetimedb install && cargo run -p gen-bindings -- --out-dir src/module_bindings --module-path spacetimedb && prettier --write src/module_bindings", "spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --module-path spacetimedb", "spacetime:publish:local": "spacetime publish --module-path spacetimedb --server local", - "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud" + "spacetime:publish": "spacetime publish --module-path spacetimedb --server maincloud", + "test": "vitest run --root spacetimedb" }, "dependencies": { "spacetimedb": "workspace:*", @@ -20,7 +21,8 @@ "@vitejs/plugin-vue": "^5.2.4", "typescript": "~5.6.2", "vite": "^6.4.1", - "vue-tsc": "^2.2.0" + "vue-tsc": "^2.2.0", + "vitest": "^4.1.12" }, "engines": { "node": "^22.23.4 || ^24.22.1 || >=26.12.1" diff --git a/templates/vue-ts/spacetimedb/package.json b/templates/vue-ts/spacetimedb/package.json index 8a263dc97a6..ea73c85a710 100644 --- a/templates/vue-ts/spacetimedb/package.json +++ b/templates/vue-ts/spacetimedb/package.json @@ -2,9 +2,11 @@ "name": "spacetime-module", "version": "1.0.0", "description": "", + "type": "module", "scripts": { "build": "spacetime build", - "publish": "spacetime publish" + "publish": "spacetime publish", + "test": "vitest run" }, "keywords": [], "author": "", @@ -13,6 +15,7 @@ "spacetimedb": "workspace:*" }, "devDependencies": { - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "vitest": "^4.1.12" } } diff --git a/templates/vue-ts/spacetimedb/src/index.test.ts b/templates/vue-ts/spacetimedb/src/index.test.ts new file mode 100644 index 00000000000..440c2b21eb1 --- /dev/null +++ b/templates/vue-ts/spacetimedb/src/index.test.ts @@ -0,0 +1,41 @@ +// Unit tests run the module against an in-memory database, with no server. +// See https://spacetimedb.com/docs/how-to/unit-testing +import { expect, it } from 'vitest'; +import { ConnectionId } from 'spacetimedb'; +import { + createModuleTestHarness, + TestAuth, +} from 'spacetimedb/server/test-utils'; +import spacetimedb, * as moduleExports from './index'; + +function clientAuth() { + return TestAuth.fromJwtPayload( + JSON.stringify({ iss: 'test-issuer', sub: 'alice' }), + new ConnectionId(1n) + ); +} + +it('adds a person', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + }); + + expect([...test.db.person.iter()].map(person => person.name)).toEqual([ + 'Alice', + ]); +}); + +it('rolls back all writes when the transaction throws', () => { + const test = createModuleTestHarness(spacetimedb, moduleExports); + + expect(() => + test.withReducerTx(clientAuth(), ctx => { + moduleExports.add(ctx, { name: 'Alice' }); + throw new Error('Something went wrong'); + }) + ).toThrow('Something went wrong'); + + expect(test.db.person.count()).toBe(0n); +}); diff --git a/templates/vue-ts/spacetimedb/vitest.config.ts b/templates/vue-ts/spacetimedb/vitest.config.ts new file mode 100644 index 00000000000..e65ffd04d4a --- /dev/null +++ b/templates/vue-ts/spacetimedb/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, type Plugin } from 'vitest/config'; +import { spacetimedbModuleTestPlugin } from 'spacetimedb/server/test-utils/vitest'; + +export default defineConfig({ + plugins: [spacetimedbModuleTestPlugin() as unknown as Plugin], + test: { + environment: 'node', + }, +});