From 3ab2888b1b18893ba64359e17dc0a52e9a08b656 Mon Sep 17 00:00:00 2001 From: Kris Jenkins Date: Thu, 29 Oct 2026 09:31:53 +0000 Subject: [PATCH 1/2] Point agents and new projects at the module test harness Since 2.12, TypeScript and Rust modules can be unit tested in-process: the harness runs reducers, views and procedures against the real datastore in memory, with no server and no publish. The docs cover it at https://spacetimedb.com/docs/how-to/unit-testing, but nothing a new project is born with mentions it. That matters most for coding agents. `spacetime init` writes CLAUDE.md, AGENTS.md, .windsurfrules, .github/copilot-instructions.md and .cursor/rules/ by concatenating skills/*/SKILL.md, and none of those skills say a word about testing. Ask an agent working in a fresh template how to test the module and it has to go digging through node_modules to discover that `spacetimedb/server/test-utils` exists at all, or more likely it recommends publishing to a local server and poking it with `spacetime call`. The template itself is no help either: of the TypeScript templates only chat-react-ts ships a test, and basic-ts, whose module 13 other templates copy verbatim, ships no test runner, so the first test anyone writes starts with working out the Vitest plugin incantation. This adds a Testing section to the typescript-server and rust-server skills (setup, a minimal test, and the points that trip people up, such as withReducerTx committing if you swallow an error), a unit-test step to the concepts skill's feature checklist, and "unit-testing" to both skill descriptions so the skills trigger on testing questions. It also gives the basic templates a passing test suite that new projects inherit. - basic-ts/spacetimedb and the 13 templates sharing its module (angular, browser, bun, deno, nextjs, nodejs, nuxt, react, remix, solid, svelte, tanstack, vue) gain vitest.config.ts, src/index.test.ts, a `test` script, a vitest dev dependency and "type": "module". The dependency is vitest 4, which @angular/build requires as a peer, except in remix-ts, which stays on vitest 3 because vitest 4 needs Vite 6 and Remix is on Vite 5. - Each of those templates' top-level package.json, and chat-react-ts's, gets a `test` script that runs the module's tests with `vitest run --root spacetimedb`, plus vitest as a dev dependency, so `npm test` or `pnpm test` works from the project root after the single top-level install a new project already does. Nothing installs spacetimedb/'s own dependencies in a generated project, and Node resolves vitest and spacetimedb from the module's files by walking up to the top-level node_modules. chat-react-ts runs the module's unit tests before its client suite, whose integration test needs a server. - chat-react-ts's client vitest config now excludes spacetimedb/**. It was already picking up the module's tests without the SpacetimeDB plugin, and they failed. - The lockfile churn beyond the new vitest entries is pnpm re-resolving peers (@types/node) and bun-ts's `"@types/bun": "latest"`. - `spacetime init --lang typescript` embeds the two new files. Because `spacetime publish` type-checks the whole module, a project that replaces src/index.ts must also delete or rewrite src/index.test.ts. The TypeScript chat tutorial, which has the reader replace index.ts, now has them delete the test first, and the smoketest helpers that scaffold a module with `init` and overwrite index.ts (the shared TypeScript helper and the quickstart test) do the same. - chat-react-ts's and money-exchange-react-ts's vitest configs cast the React plugin to vitest's own Plugin type. Their vite and vitest's can resolve to different copies, whose plugin types don't match. - basic-rs gains a #[cfg(test)] module and a test-utils dev dependency, with the icu_calendar 2.1.x pin from the guide (#6317), without default features so it constrains the version without adding ixdtf. - nextjs-ts and remix-ts client tsconfigs glob **/*.ts, so they now exclude spacetimedb/ (a separate project whose tests import vitest from its own node_modules), and their module tsconfigs include vitest.config.ts so eslint's project service finds it. - The codex-plugin copy of skills/ is resynced. --- Cargo.lock | 1 + .../spacetimedb/skills/concepts/SKILL.md | 7 +- .../spacetimedb/skills/rust-server/SKILL.md | 46 +- .../skills/typescript-server/SKILL.md | 46 +- crates/cli/src/subcommands/init.rs | 8 + crates/smoketests/src/lib.rs | 3 + crates/smoketests/tests/cluster/quickstart.rs | 4 + .../00300-tutorials/00100-chat-app.md | 8 +- pnpm-lock.yaml | 477 ++++++++++++++++-- skills/concepts/SKILL.md | 7 +- skills/rust-server/SKILL.md | 46 +- skills/typescript-server/SKILL.md | 46 +- templates/angular-ts/package.json | 6 +- templates/angular-ts/spacetimedb/package.json | 7 +- .../angular-ts/spacetimedb/src/index.test.ts | 41 ++ .../angular-ts/spacetimedb/vitest.config.ts | 9 + templates/basic-rs/spacetimedb/Cargo.toml | 7 + templates/basic-rs/spacetimedb/src/lib.rs | 41 ++ templates/basic-ts/package.json | 6 +- templates/basic-ts/spacetimedb/package.json | 7 +- .../basic-ts/spacetimedb/src/index.test.ts | 41 ++ .../basic-ts/spacetimedb/vitest.config.ts | 9 + templates/browser-ts/package.json | 6 +- templates/browser-ts/spacetimedb/package.json | 7 +- .../browser-ts/spacetimedb/src/index.test.ts | 41 ++ .../browser-ts/spacetimedb/vitest.config.ts | 9 + templates/bun-ts/package.json | 6 +- templates/bun-ts/spacetimedb/package.json | 7 +- .../bun-ts/spacetimedb/src/index.test.ts | 41 ++ templates/bun-ts/spacetimedb/vitest.config.ts | 9 + templates/chat-react-ts/package.json | 2 +- templates/chat-react-ts/vitest.config.ts | 7 +- templates/deno-ts/package.json | 6 +- templates/deno-ts/spacetimedb/package.json | 7 +- .../deno-ts/spacetimedb/src/index.test.ts | 41 ++ .../deno-ts/spacetimedb/vitest.config.ts | 9 + .../money-exchange-react-ts/vitest.config.ts | 4 +- templates/nextjs-ts/package.json | 6 +- templates/nextjs-ts/spacetimedb/package.json | 7 +- .../nextjs-ts/spacetimedb/src/index.test.ts | 41 ++ templates/nextjs-ts/spacetimedb/tsconfig.json | 2 +- .../nextjs-ts/spacetimedb/vitest.config.ts | 9 + templates/nextjs-ts/tsconfig.json | 2 +- templates/nodejs-ts/package.json | 6 +- templates/nodejs-ts/spacetimedb/package.json | 7 +- .../nodejs-ts/spacetimedb/src/index.test.ts | 41 ++ .../nodejs-ts/spacetimedb/vitest.config.ts | 9 + templates/nuxt-ts/package.json | 6 +- templates/nuxt-ts/spacetimedb/package.json | 7 +- .../nuxt-ts/spacetimedb/src/index.test.ts | 41 ++ .../nuxt-ts/spacetimedb/vitest.config.ts | 9 + templates/react-ts/package.json | 6 +- templates/react-ts/spacetimedb/package.json | 7 +- .../react-ts/spacetimedb/src/index.test.ts | 41 ++ .../react-ts/spacetimedb/vitest.config.ts | 9 + templates/remix-ts/package.json | 6 +- templates/remix-ts/spacetimedb/package.json | 7 +- .../remix-ts/spacetimedb/src/index.test.ts | 41 ++ templates/remix-ts/spacetimedb/tsconfig.json | 2 +- .../remix-ts/spacetimedb/vitest.config.ts | 9 + templates/remix-ts/tsconfig.json | 2 +- templates/solid-ts/package.json | 6 +- templates/solid-ts/spacetimedb/package.json | 7 +- .../solid-ts/spacetimedb/src/index.test.ts | 41 ++ .../solid-ts/spacetimedb/vitest.config.ts | 9 + templates/svelte-ts/package.json | 6 +- templates/svelte-ts/spacetimedb/package.json | 7 +- .../svelte-ts/spacetimedb/src/index.test.ts | 41 ++ .../svelte-ts/spacetimedb/vitest.config.ts | 9 + templates/tanstack-ts/package.json | 6 +- .../tanstack-ts/spacetimedb/package.json | 7 +- .../tanstack-ts/spacetimedb/src/index.test.ts | 41 ++ .../tanstack-ts/spacetimedb/vitest.config.ts | 9 + templates/vue-ts/package.json | 6 +- templates/vue-ts/spacetimedb/package.json | 7 +- .../vue-ts/spacetimedb/src/index.test.ts | 41 ++ templates/vue-ts/spacetimedb/vitest.config.ts | 9 + 77 files changed, 1540 insertions(+), 110 deletions(-) create mode 100644 templates/angular-ts/spacetimedb/src/index.test.ts create mode 100644 templates/angular-ts/spacetimedb/vitest.config.ts create mode 100644 templates/basic-ts/spacetimedb/src/index.test.ts create mode 100644 templates/basic-ts/spacetimedb/vitest.config.ts create mode 100644 templates/browser-ts/spacetimedb/src/index.test.ts create mode 100644 templates/browser-ts/spacetimedb/vitest.config.ts create mode 100644 templates/bun-ts/spacetimedb/src/index.test.ts create mode 100644 templates/bun-ts/spacetimedb/vitest.config.ts create mode 100644 templates/deno-ts/spacetimedb/src/index.test.ts create mode 100644 templates/deno-ts/spacetimedb/vitest.config.ts create mode 100644 templates/nextjs-ts/spacetimedb/src/index.test.ts create mode 100644 templates/nextjs-ts/spacetimedb/vitest.config.ts create mode 100644 templates/nodejs-ts/spacetimedb/src/index.test.ts create mode 100644 templates/nodejs-ts/spacetimedb/vitest.config.ts create mode 100644 templates/nuxt-ts/spacetimedb/src/index.test.ts create mode 100644 templates/nuxt-ts/spacetimedb/vitest.config.ts create mode 100644 templates/react-ts/spacetimedb/src/index.test.ts create mode 100644 templates/react-ts/spacetimedb/vitest.config.ts create mode 100644 templates/remix-ts/spacetimedb/src/index.test.ts create mode 100644 templates/remix-ts/spacetimedb/vitest.config.ts create mode 100644 templates/solid-ts/spacetimedb/src/index.test.ts create mode 100644 templates/solid-ts/spacetimedb/vitest.config.ts create mode 100644 templates/svelte-ts/spacetimedb/src/index.test.ts create mode 100644 templates/svelte-ts/spacetimedb/vitest.config.ts create mode 100644 templates/tanstack-ts/spacetimedb/src/index.test.ts create mode 100644 templates/tanstack-ts/spacetimedb/vitest.config.ts create mode 100644 templates/vue-ts/spacetimedb/src/index.test.ts create mode 100644 templates/vue-ts/spacetimedb/vitest.config.ts 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..5b1dc6868f3 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -1294,6 +1294,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/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/vitest.config.ts b/templates/chat-react-ts/vitest.config.ts index 5291d9f29fd..d0c4fffc468 100644 --- a/templates/chat-react-ts/vitest.config.ts +++ b/templates/chat-react-ts/vitest.config.ts @@ -1,14 +1,15 @@ -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], 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', + }, +}); From d201f03af6955501a0aaed779b6f4f79cf82c8b7 Mon Sep 17 00:00:00 2001 From: Kris Jenkins Date: Thu, 29 Oct 2026 11:26:31 +0000 Subject: [PATCH 2/2] Run the templates' tests in the template smoketest The templates now ship tests, but nothing in CI runs them. The template smoketest initialises each template, publishes its module and installs its client, and stops there. Root `pnpm test` would reach the in-repo copies, but that CI step is commented out. So a template whose tests had broken would ship that way, and nobody would know until a user ran `npm test` in a fresh project. That has already happened: chat-react-ts's integration test hardcodes ws://localhost:3000 and a database called quickstart-chat, so it can only pass on someone's laptop, and it has never run anywhere that could tell us it doesn't. After publishing a TypeScript template's module, the smoketest now installs the generated project and runs its `test` script, preferring the project's own over the module's. It sets VITE_SPACETIMEDB_HOST and VITE_SPACETIMEDB_DB_NAME to the database it just published, and deletes the database after the tests rather than straight after publishing. - The smoketest jobs install wasm-bindgen to build the SDK's test datastore. They already run Node 22, which the module test harness needs (Symbol.dispose, Set.prototype.isSubsetOf). - build_typescript_sdk runs once per test run behind a file lock. Every TypeScript smoketest used to rebuild the SDK, and tsup's clean build wipes dist/ while other tests, running in parallel, are still using it. - A new build_typescript_sdk_test_utils also generates the test datastore, once per run in the same way. Only the template smoketest calls it, so the other TypeScript smoketests don't need wasm-bindgen. - chat-react-ts's integration test reads the same environment variables as main.tsx. It no longer reads a token from localStorage: jsdom's storage always starts empty, and on Node 25+ a built-in global that is undefined unless configured shadows it. It allows its name to appear more than once, since it shows in both the profile and the user list. - chat-react-ts's client vitest config dedupes React, which otherwise loads twice when the SDK is linked from a local checkout. - money-exchange-react-ts's existing client tests now run too. --- .github/workflows/ci.yml | 30 +++++++++- crates/smoketests/src/lib.rs | 56 +++++++++++++++-- crates/smoketests/tests/cluster/templates.rs | 60 ++++++++++++++----- .../src/App.integration.test.tsx | 20 ++++--- templates/chat-react-ts/vitest.config.ts | 4 ++ 5 files changed, 142 insertions(+), 28 deletions(-) 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/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index 5b1dc6868f3..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(()) } 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/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 d0c4fffc468..4ad674aa8ba 100644 --- a/templates/chat-react-ts/vitest.config.ts +++ b/templates/chat-react-ts/vitest.config.ts @@ -3,6 +3,10 @@ import react from '@vitejs/plugin-react'; export default defineConfig({ 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