Skip to content
Documentation GitHub
Development Guides

Run Tests

All testing commands in one place.

Quick Reference

WhatCommandWhen to use
All Rust testscargo testBefore every commit
Single cratecargo test -p <crate>After changes to a specific crate
Integration (real SQLite)cargo test -p core-testsAfter storage or use case changes
All tests (Turbo)pnpm testFull suite across Rust + frontend
Frontend integrationpnpm test:integrationAfter UI or Tauri command changes
E2E (e2e)pnpm test:e2eEnd-to-end verification before release

Rust Tests

Unit tests

Unit tests live alongside their source code in #[cfg(test)] modules. Run them with:

Terminal window
# All Rust tests in the workspace
cargo test
# Tests for a specific crate
cargo test -p domain
cargo test -p application
cargo test -p infrastructure-sqlite
# A specific test function
cargo test test_create_page_enforces_min_block
# All tests matching a pattern
cargo test word_count

Test naming convention: test_[what]_[condition]_[expected].

Application layer tests use mock repositories from crates/application/src/test_helpers.rs:

use crate::test_helpers::MockPageRepository;
use crate::permission::test_helpers::owner_guard;
#[test]
fn test_get_page_by_slug() {
let page = Page::new("My Page");
let repo = MockPageRepository::with_pages(vec![page]);
let use_case = GetPageUseCase::new(repo);
let result = use_case.execute(&owner_guard(), "my-page").unwrap();
assert_eq!(result.title, "My Page");
}

Integration tests

Integration tests in tests/core/ wire real use cases to a real SQLite database in a temporary directory. They are the authoritative test for storage correctness.

Terminal window
# Run all integration tests
cargo test -p core-tests
# Run a specific integration test file
cargo test -p core-tests --test page_lifecycle
# Run with output visible (useful for debugging)
cargo test -p core-tests -- --nocapture

Test files cover full lifecycle scenarios:

tests/core/tests/
├── page_lifecycle.rs # Create, read, update, delete
├── page_hierarchy.rs # Parent/child relationships, move, cascade delete
├── page_search.rs # FTS5 full-text search
├── tag_tests.rs # Tag CRUD, merge, page assignment
├── attachment_lifecycle.rs # Upload, retrieve, dedup via SHA-256
├── event_log_lifecycle.rs # Event recording and timeline queries
├── sync_queue_roundtrip.rs # Sync queue enqueue/dequeue
├── loro_block_storage.rs # CRDT BLOB round-trip
├── migration_chain.rs # V001 → latest schema migration
└── ...

setup_workspace (in tests/core/tests/common/mod.rs) handles temp directory creation, SQLite initialization, and repository wiring. Use it directly in new integration tests:

use common::setup_workspace;
#[test]
fn test_my_feature() {
let ws = setup_workspace("my-feature-test");
// ws.page_repo, ws.workspace_repo, ws.db, etc. are ready to use
}

Frontend Tests

Playwright integration tests

Frontend integration tests in apps/desktop/tests/ run the React UI against a mocked Tauri backend. They test UI flows and component behavior without requiring the Rust backend or native Tauri runtime.

Terminal window
# From the project root
pnpm test:integration
# Interactive UI mode (recommended for local development)
pnpm --dir apps/desktop/tests test:ui
# Visible browser (useful for debugging)
pnpm --dir apps/desktop/tests test:headed
# Step-through debugging
pnpm --dir apps/desktop/tests test:debug
# CI mode with GitHub reporter
pnpm --dir apps/desktop/tests test:ci

First-time setup:

Terminal window
cd apps/desktop/tests
pnpm install
pnpm exec playwright install

Test specs live in apps/desktop/tests/specs/. Each spec covers a feature area:

specs/
├── workspace.spec.ts # Workspace creation, switching
├── page.spec.ts # Page CRUD, hierarchy
├── settings.spec.ts # Settings panel
├── first-launch.spec.ts # Onboarding tour
├── wiki-links.spec.ts # Wiki-link insertion and navigation
├── editor-loro.spec.ts # CRDT editor round-trips
└── ...

The mock intercepts window.__TAURI__ calls. See fixtures/tauri-mock.ts for supported commands. To add a new mock handler:

// In fixtures/tauri-mock.ts
case "my_new_command":
return { result: "data" };

Viewing failure reports

After a failed run, open the HTML report:

Terminal window
pnpm --dir apps/desktop/tests report

Trace files are saved at test-results/<test-name>/trace.zip. Open with:

Terminal window
pnpm exec playwright show-trace test-results/<path>/trace.zip

E2E Tests

E2E tests (tests/e2e/) are end-to-end tests that drive the real React UI against the real Rust backend via the HTTP bridge — no mocks, no Tauri runtime required.

Setup

pnpm test:e2e handles bridge startup automatically via Playwright webServer config — no manual infrastructure setup required.

Running tests

Terminal window
cd tests/e2e
# All tests
pnpm test
# Visible browser
pnpm test:headed
# Interactive UI mode
pnpm test:ui
# Single spec file
pnpm test workspace-page-crud
# Single test by name
pnpm test -g "Create a new page"
# Single partition
pnpm test --project=editor-content

Test partitions

Tests are isolated across 5 bridge instances to prevent data collisions:

PartitionPortScope
workspace-pages9990Workspace + page lifecycle
editor-content9991Editor + content ops
navigation-search9992Navigation, search, tags
advanced-features9993Import, attachments
first-launch9994Onboarding tour (fresh state)

Troubleshooting

All tests fail with “sidebar not visible”: The first-launch overlay (PersonaSelector) is blocking. Check that complete_first_launch ran:

Terminal window
curl -s -X POST http://localhost:9990/invoke/complete_first_launch \
-H 'Content-Type: application/json' -d '{}'

Port conflict on startup:

Terminal window
lsof -i :1420 -i :9990 -i :9991 -i :9992 -i :9993 -i :9994
tools/qa/stop-infrastructure.sh && pnpm test:e2e

Writing New Tests

Which layer to test at

ChangeWhere to test
Domain entity logicUnit test in crates/domain/
Use case behaviorUnit test with mocks in crates/application/
Storage query correctnessIntegration test in tests/core/
Tauri command + UI wiringFrontend integration test in apps/desktop/tests/
Full user flowE2E spec in tests/e2e/

Rust test naming

test_[what]_[condition]_[expected]

Examples:

  • test_create_page_without_title_returns_error
  • test_get_page_not_found_returns_not_found_error
  • test_word_count_empty_page_returns_zero

Mock patterns

Application layer tests use constructors from crates/application/src/test_helpers.rs:

// Repository with pre-populated data
let repo = MockPageRepository::with_pages(vec![page_a, page_b]);
// Empty repository
let repo = MockPageRepository::new();
// Permission guards
let guard = owner_guard(); // all capabilities
let guard = guard_with(&[Capability::PagesRead]); // specific capabilities

For reference-aware tests, use MockReferenceRepository::with_refs(refs).

Integration test patterns

Use setup_workspace for real SQLite:

use common::{owner_guard, setup_workspace};
#[test]
fn test_my_feature_persists() {
let ws = setup_workspace("my-feature-persists");
let guard = owner_guard();
// Create something
let use_case = CreatePageUseCase::new(ws.page_repo.clone());
use_case.execute(&guard, CreatePageRequest {
title: "Test".to_string(),
parent_slug: None,
content: None,
}).unwrap();
// Verify it persists via a second use case
let get_uc = GetPageUseCase::new(ws.page_repo.clone());
let page = get_uc.execute(&guard, "test").unwrap();
assert_eq!(page.title, "Test");
}

LLM Integration Tests

LLM integration tests exercise the infrastructure-llm crate against a real Ollama server running locally. They are skipped in CI — all tests carry #[ignore] and must be opted-in explicitly.

Setup

Terminal window
brew install ollama # macOS
ollama pull qwen3:4b # default test model
ollama serve # start the server on localhost:11434

Running

Terminal window
# All LLM integration tests
cargo test -p core-tests --test llm_integration -- --ignored
# A specific test
cargo test -p core-tests --test llm_integration test_ollama_completion -- --ignored

Available tests

TestWhat it exercises
test_ollama_completionBasic prompt → text response round-trip
test_ollama_streamingStreaming API — verifies at least one chunk arrives
test_ollama_tool_callsTool definition in request — accepts text or tool-call response
test_byok_key_roundtripInMemoryKeyStore set/get/delete lifecycle (no Ollama needed)
test_registry_provider_selectionProviderRegistry default provider routing
test_graceful_degradationUnreachable URL returns Err, does not panic

Skip behaviour

Each test calls OllamaTestProvider::try_new() at the start. If Ollama is not reachable (connection refused or 2 s timeout), the test prints a SKIP message and returns early. This means you can run -- --ignored at any time without risk of spurious failures on machines without Ollama.

Adding new LLM tests

Use OllamaTestProvider from the test-support feature:

use infrastructure_llm::test_support::OllamaTestProvider;
#[tokio::test]
#[ignore] // Requires local Ollama
async fn test_my_llm_scenario() {
let Some(provider) = OllamaTestProvider::try_new().await else {
return; // skip
};
let registry = provider.registry(OllamaTestProvider::DEFAULT_MODEL);
// ...
}

Python Sidecar Tests

The Python sidecar (apps/python-sidecar/) has its own test suite managed by pytest via uv.

Setup

Terminal window
cd apps/python-sidecar
uv sync --group dev

Running tests

Terminal window
# All tests (with coverage enabled by default via pyproject.toml addopts)
uv run pytest
# Specific test file
uv run pytest tests/test_protocol.py
# Unit tests only (no external dependencies)
uv run pytest -m unit
# Integration tests (may need LLM/sidecar running)
uv run pytest -m integration
# With verbose coverage report
uv run pytest --cov --cov-report=term-missing

Test files

apps/python-sidecar/tests/
├── conftest.py # Shared fixtures (mock_prediction, fake_save_factory)
├── test_protocol.py # IPC JSON-RPC protocol validation
├── test_all_templates.py # Full template suite (all 10 templates)
├── test_templates.py # Individual template behavior
├── test_errors.py # Structured error responses
├── test_execute.py # Execute service mode
├── test_manifest.py # Manifest service mode
├── test_dispatcher.py # Request routing
└── test_main.py # Entry point and async loop

Coverage requirements

Coverage is enforced at 80% minimum (pyproject.toml [tool.coverage.report] fail_under = 80). The --cov flag is included in addopts so coverage runs on every uv run pytest invocation.

Linting and type checking

Terminal window
uv run ruff check . # Lint (pycodestyle, pyflakes, bugbear, etc.)
uv run ruff format --check . # Format check
uv run mypy src/ # Strict type checking (Python 3.14)

See Also

  • tests/core/tests/common/mod.rssetup_workspace and TestWorkspace helpers
  • apps/desktop/tests/README.md — full frontend test documentation
  • tests/e2e/README.md — full E2E documentation
  • apps/python-sidecar/README.md — full sidecar documentation
  • Development Guide — testing strategy and coverage goals

Was this page helpful?