Testing

Testing Strategy

Why We Tested This Way

SmolPC 2.0 spans a shared Rust inference engine, a Tauri desktop shell, a Svelte frontend, and host-application bridges for GIMP, Blender, Writer, and Slides. A regression in any layer — a renamed JSON field, a broken backend selection path, a misconfigured DLL search order — can silently break the entire platform even when the UI still appears to load. Automated testing was therefore not optional: it was the primary mechanism for catching these cross-boundary failures before they reached users.

Two-Tier Strategy

Tier 1: Unit Tests — Logic Without Hardware

Unit tests validate logic that does not require a real GPU, NPU, or host application. ChatML rendering, config parsing, backend selection state machines, thinking filters, error classification, type serialisation contracts, path validation, and connector heuristics are all testable without hardware. These tests run in CI on every push to main and on all pull requests, providing immediate feedback on regressions.

Tier 2: Live Tests — Hardware Paths

Live tests validate the hardware-dependent paths: generation quality on a specific backend, streaming behaviour, model loading on real NPU or GPU hardware, and host-application bridge behaviour with real GIMP, Blender, or LibreOffice processes. These run manually on physical machines because there is no way to simulate a real NPU or discrete GPU in CI. After changing generation config, backend selection, or model loading, the protocol is: start the engine with SMOLPC_FORCE_EP=<backend>, send a streaming chat completion via curl, and verify output quality — not just "no crash."

Scale: ~302 Test Functions

The test suite covers the full stack from low-level tensor extraction to high-level provisioning workflows. The following table summarises the distribution across all components.

Component Source Location Test Count
Engine hostengine/crates/smolpc-engine-host/src/tests.rs49
Engine core7 inline #[cfg(test)] modules40
Engine clientengine/crates/smolpc-engine-client/src/lib.rs18
Desktop appapp/src-tauri/src/ (19 modules)76
Blender connectorconnectors/blender/ (7 modules)32
GIMP connectorconnectors/gimp/ (6 modules)28
LibreOffice connectorconnectors/libreoffice/ (5 modules)26
Connector-commoncrates/smolpc-connector-common/ (5 modules)16
Securityapp/src-tauri/src/security/tests.rs7
Type contractscrates/smolpc-assistant-types/tests/contracts.rs5
Benchmark statsengine/crates/smolpc-benchmark/src/stats.rs5
Total~302

Tools and Commands

Automated Toolchain

The project uses five complementary automated verification tools, each targeting a different failure class:

Tool Command What It Catches
cargo testcargo test --workspaceLogic regressions in all Rust crates (engine, desktop app, connectors)
npm run checkTypeScript + Svelte type checkingType errors in the frontend shell before they reach the browser
npm run boundary:checkRuns check-boundaries.ps1Architectural violations: legacy module resurrection, broken process isolation
cargo auditDependency vulnerability scanKnown CVEs in Rust dependency tree
Manual testingTauri dev + packaged buildsEnd-to-end workflows with real host applications, hardware-specific paths
# Engine and connector tests
cargo test -p smolpc-engine-core -p smolpc-engine-client -p smolpc-engine-host
cargo test -p smolpc-connector-common -p smolpc-connector-blender
cargo test -p smolpc-connector-gimp -p smolpc-connector-libreoffice

# Desktop app tests
cargo test -p smolpc-desktop

# Frontend checks
npm run check
npm run boundary:check

# Security audit
cargo audit

Test Isolation Patterns

Rust's cargo test runs tests in parallel by default, and environment variables are process-global. Without isolation, env-mutating tests would produce non-deterministic failures. The project uses four isolation patterns to prevent this:

RAII Environment Guards

EnvVarGuard (engine host) and RuntimeEnvGuard (engine client) are RAII structs that save the current value of an environment variable on construction and restore it on Drop — even if the test panics. This prevents a failing test from leaking SMOLPC_FORCE_EP or SMOLPC_DML_DEVICE_ID into subsequent tests.

Global Mutex Serialisation

A global OnceLock<Mutex<()>> guard serialises all tests that mutate environment variables. Both the engine host and engine client test modules use env_lock() to acquire this mutex before modifying any SMOLPC_* variables, ensuring deterministic results despite parallel execution.

Filesystem Isolation via tempdir()

The tempfile crate's tempdir() creates a unique temporary directory per test, used extensively in runtime loading, backend store persistence, connector setup, provisioning, and ZIP extraction tests. The directory is automatically deleted when the TempDir guard drops.

Mock Implementations via #[cfg(test)]

Connector crates use conditional compilation to substitute mock implementations for bridge, runtime, and transport layers during testing. This allows provider and executor tests to run without real Blender, GIMP, or LibreOffice processes, keeping them deterministic and fast enough for CI.

CI Pipeline: 7 Jobs on Every Push and PR

Every push to main and every pull request triggers seven CI jobs on windows-latest runners. This provides fast feedback on regressions across the frontend, the engine, the desktop app, architectural boundaries, code style, and dependency security. The pipeline is defined in .github/workflows/ci.yml.

Job What It Does Why It Matters
1. Frontend Quality TypeScript + Svelte type checking (npm run check), NPM vulnerability audit (high+ severity) Catches type errors and known frontend vulnerabilities before merge
2. Boundary Enforcement Runs check-boundaries.ps1 with 8 architectural rules Prevents resurrection of legacy modules, broken process isolation, and Ollama remnants
3. Engine Tests (MSRV) Tests 7 crates on Rust 1.88.0 (minimum supported version) Ensures the engine builds and passes on the oldest supported compiler
4. Engine Tests (stable) Same 7 crates on latest stable Rust Catches regressions introduced by newer compiler versions
5. Tauri Build Check cargo check -p smolpc-desktop Verifies the desktop app compiles (does not produce a binary — that requires runtime DLLs)
6. Incremental Style Gates Prettier, ESLint, and rustfmt on changed files only Enforces formatting and lint rules without slowing CI on the entire codebase
7. Rust Security Audit cargo audit for known CVEs in Rust dependencies Flags vulnerable crates before they ship in a packaged build

Boundary Enforcement: 8 Rules

The boundary enforcement script enforces eight rules that prevent architectural regression. Five are file-absence rules (certain legacy paths must not exist); three are content rules (certain patterns must not appear in specific files). These rules exist because SmolPC migrated from an Ollama-backed architecture to a self-contained engine, and the engine-host boundary is the most safety-critical: if the desktop app imports smolpc-engine-host directly instead of going through smolpc-engine-client over HTTP, it defeats process isolation and means a C FFI crash in the engine takes down the entire UI.

Rule Type Target What It Prevents
File absent app/src-tauri/src/inference Legacy app-owned inference module — engine owns all inference
File absent app/src-tauri/src/models Legacy app-owned model module — engine owns model management
File absent app/src-tauri/src/commands/ollama.rs Legacy Ollama integration — fully removed
File absent app/src/lib/stores/ollama.svelte.ts Legacy Ollama frontend store — fully removed
File absent app/src/lib/types/ollama.ts Legacy Ollama frontend types — fully removed
Content rule commands/mod.rs must not contain \bollama\b No Ollama references in command routing
Content rule Cargo.toml must not contain smolpc-engine-host Desktop app must depend on engine via engine-client only
Content rule src/**/*.rs must not contain use smolpc_engine_host No direct imports of engine-host in the app — defeats process isolation

Unit Testing

Unit tests lock down the most failure-prone logic across every component. SmolPC is particularly vulnerable to subtle regressions at internal boundaries: a renamed JSON field breaks the Svelte frontend silently, a changed default in backend selection sends every user to the wrong inference path, and a broken DLL search order prevents startup on machines without a GPU. The unit test suite covers all of these failure modes automatically and runs on every CI push.

Engine Host Tests (49 tests)

The largest single test file, covering the engine's core server-side logic. These tests verify that the engine starts, selects backends, parses requests, classifies errors, and handles audio correctly — all without requiring real hardware.

Group Count What It Tests Why It Matters
ChatML parsing 5 Tag formatting with <|im_start|>/<|im_end|>, preformatted pass-through, /nothink injection and suppression for Qwen3 Malformed ChatML produces garbage output from the model. Pre-formatted pass-through prevents double-encoding. The /nothink directive controls Qwen3's reasoning mode on NPU.
Thinking filter 4 Stripping <think>...</think> tags from streaming tokens, handling tags split across chunk boundaries, passing non-thinking text unchanged Qwen3 emits internal reasoning in think tags. If these leak to the UI, users see confusing internal monologue instead of clean answers.
Request config 5 Zero max_tokens rejection, hard cap enforcement (OPENVINO_MAX_TOKENS_HARD_CAP), Qwen3 upstream defaults (temperature=0.7, top_p=0.8, top_k=20), structured message routing based on loaded runtime Uncapped token limits cause runaway generation on constrained devices. Wrong defaults produce incoherent output from quantised models.
Authentication 1 Constant-time token comparison correctness The engine runs on localhost with bearer auth. Timing-variable comparison would leak token bytes to a local attacker.
Startup degradation 4 Engine startup with missing ORT DLLs, missing OpenVINO DLLs, missing NPU plugin — all using tempdir() with mock files The engine must start on any machine, including those without a GPU or NPU. These tests verify graceful degradation rather than crash on missing runtime bundles.
Backend selection 6 DirectML preference when both candidates ready, fallback chain when OpenVINO unavailable, adapter release before rebuild, persisted CPU/OpenVINO choice survival, fallback from stale persistence Backend selection determines which hardware path every user hits. A wrong default or broken persistence sends users to the slowest path without explanation.
Model readiness 5 Lane-based readiness reporting (CPU/DirectML/OpenVINO reported separately), probe interaction with DML lane, shared artifact readiness propagation The UI shows per-lane status to users. If readiness reporting is wrong, users see a "ready" badge for a backend that will fail on first request.
Error classification 5 Unknown model and missing assets flagged non-retryable, memory pressure flagged retryable with hint injection, false-positive prevention, idempotent hint application Retrying a non-retryable error wastes time and confuses users. Missing the memory-pressure signal prevents the UI from suggesting a model switch.
RAM model selection 4 32GB picks largest model, 16GB picks largest at threshold, 12GB picks smaller model, 4GB returns None Selecting a model too large for available RAM causes the engine to crash or the system to swap heavily. Selecting too small wastes capable hardware.
Audio and TTS 7 F32 alignment for Whisper input, serialisation round-trip, Whisper model directory layout, TTS sidecar discovery (binary vs resource dir), TTS request defaults and overrides Misaligned audio data causes silent Whisper failures. Wrong sidecar discovery prevents TTS from working in packaged builds.
Configuration 3 Process idle exit disabled by default, zero timeout disables timer, model idle unload default preserved when unset Wrong idle defaults cause the engine to exit during active use or prevent it from ever releasing memory.
RAM-based model selection tests from engine/crates/smolpc-engine-host/src/tests.rs
Figure 1. RAM-based model selection tests from engine/crates/smolpc-engine-host/src/tests.rs — verifies the engine picks the right model size for the machine's available memory, including the "no suitable model" case for very low RAM.

Engine Core Tests (40 tests across 7 files)

Engine core tests live as inline #[cfg(test)] modules inside the source files they verify. This keeps tests close to the code they protect and makes it obvious when a logic change needs a corresponding test update.

Group (source file) Count What It Tests Why It Matters
backend.rs 12 Decision key fingerprint sensitivity (GPU driver, OpenVINO bundle, NPU tuning changes), benchmark gate enforcement (speedup ratio + TTFT regression limit), DirectML demotion after 3 consecutive failures, TTFT ratio edge cases, serde wire format stability, lane-based JSON structure, readiness vs artifact availability distinction Fingerprint changes trigger re-evaluation of backend choice. If fingerprints are too coarse, driver updates are ignored. If demotion is too aggressive, a transient failure permanently disables a backend.
loader.rs 6 Default model directory path contract, env override resolution (non-empty and empty), backend directory name stability (DirectML.as_dir() == "dml"), OpenVINO manifest path structure, models directory suffix stability Model paths are baked into the packaged build. If the directory naming contract breaks, the engine cannot find models on any machine.
directml.rs 6 Logits tensor row extraction for rank-1 and rank-3 tensors, error handling for invalid dimensions, partial bundle degradation (missing GenAI or DirectML DLL disables only the DML lane, ORT core stays valid) Logits extraction is the hot path for token sampling. Wrong bounds produce garbage tokens. Partial degradation ensures that a missing DirectML DLL does not also disable CPU inference.
openvino.rs 5 Metric millisecond rounding, bundle validation rejection, /nothink injection (prepend to existing system message, create if absent, idempotent on repeated application) The NPU backend requires /nothink to suppress Qwen3's reasoning mode. Non-idempotent injection would corrupt the prompt on retry.
runtime_loading.rs 4 Fingerprint determinism (changes when bundle root or versions change), path validation (relative paths rejected), centralisation scan — a source-invariant test that fails CI if any file outside runtime_loading.rs calls Library::new() or load_with_flags() DLL loading is the most dangerous operation in the engine — a wrong path or a load from an untrusted directory is a security and stability risk. The centralisation scan enforces this at the source level.
backend_store.rs 4 Serialise/deserialise round-trip for backend decisions, fingerprint-keyed storage (multiple records per model when fingerprints differ), failure counter persistence without a decision, corrupt JSON file recovery Backend decisions persist across restarts. Corrupt persistence must not crash the engine — it must reset cleanly and re-evaluate.
registry.rs 3 Model list ordering, supported model IDs present, unknown model IDs return None The model list drives the UI dropdown. Wrong ordering confuses users; accepting unknown IDs could trigger undefined behaviour in the loader.
CI centralisation scan from engine/crates/smolpc-engine-core/src/inference/runtime_loading.rs
Figure 2. CI centralisation scan from runtime_loading.rs — this test walks every .rs file in the engine crate tree and fails if any file other than runtime_loading.rs calls Library::new() or load_with_flags(). This enforces that all native DLL loading goes through one controlled module.

Engine Client Tests (18 tests)

The engine client is the typed interface between the desktop app and the engine host. Its tests verify that the contract between these two processes is stable, that environment overrides parse correctly, and that version compatibility gates work as expected.

Group Count What It Tests Why It Matters
Runtime env overrides 3 Default Auto mode when unset, parsing of cpu/DIRECTML/dml tokens, numeric device ID parsing Developers and CI use SMOLPC_FORCE_EP to pin a backend. If parsing is wrong, the override silently does nothing.
Error and metrics 2 Nested error.message extraction from JSON, fallback stream metrics with token count and TTFT tracking Clear error messages let the UI show actionable feedback instead of "unknown error."
Model response parsing 3 Missing data array rejection, unknown model ID rejection, known model acceptance A malformed or unexpected model list from the engine could populate the UI with invalid entries.
Running host policy 2 Restart when protocol is incompatible and engine is idle, rejection of forced override when engine is busy The client must handle version mismatches gracefully — restarting a busy engine would kill an active generation.
API version compatibility 2 Major version extraction from semver strings, major version gate (equal or higher required) Client and host run in separate processes and may be updated independently. The major version gate prevents silent protocol incompatibilities.
Token and serialisation 2 Private token file creation (48-char alphanumeric, idempotent reload), startup mode serialisation contract The auth token secures localhost communication. If creation is not idempotent, every restart generates a new token and invalidates existing connections.
Engine status parsing 4 Canonical readiness field deserialisation, legacy payload compatibility, is_ready() logic, is_failed() and failure message extraction Status parsing drives the startup loading screen and readiness indicators. A parsing failure makes the app think the engine is down when it is actually running.

Desktop App Tests (76 tests across 19 modules)

The Tauri desktop app tests cover the boundary between the frontend and the Rust backend: supervisor handle operations, engine client adaptation, inference commands, lifecycle state machines, path resolution, audio processing, provisioning, and setup flows.

Module Count What It Tests Why It Matters
engine/handle.rs 10 Clone behaviour, synchronous and async client acquisition, timeout on non-running state, terminal state detection, command dispatch (ensure-started, shutdown), fire-and-forget operations The supervisor handle is the single point of contact between Tauri commands and the engine. A broken handle silently drops commands.
engine_client_adapter.rs 8 Startup mode to runtime mode mapping, runtime env override reading, unknown state rejection, DTO mapping from engine status (canonical fields and fallback), input sanitisation, wire format serialisation The adapter translates between the engine's internal status format and the frontend's expected DTO shape. A mapping error produces wrong status in the UI.
commands/inference.rs 8 Memory detection heuristics, unload-in-progress error detection, memory level classification (warning/critical thresholds), model recommendation logic, heavy mode detection (Blender, GIMP), auto-unload messaging Memory pressure handling protects low-RAM machines from becoming unstable during inference. Wrong thresholds either crash the system or unnecessarily restrict capable hardware.
lib.rs 6 App local data directory resolution chain (Tauri path, dirs fallback, debug fallback, last resort), directory creation on disk, managed state wiring for setup and providers The data directory is the root for all persistent state. If resolution fails, the app cannot store settings, tokens, or provisioned resources.
engine/mod.rs 6 Lifecycle state queries (running, terminal), state machine transition rules (valid and invalid transitions), serialisation format The lifecycle state machine drives startup, crash recovery, and shutdown. An invalid transition would put the engine in an undefined state.
app_paths.rs 5 Tauri resource path normalisation, nested resources root handling, debug and release fallback behaviour, graceful degradation for unusable candidates Tauri injects resource paths at build time. If normalisation is wrong, the packaged build cannot find bundled assets.
commands/audio.rs 5 Stereo-to-mono channel mixing, sample rate conversion (passthrough at 16kHz, 48kHz and 44.1kHz down to 16kHz for Whisper), recording buffer size bounds Whisper requires 16kHz mono PCM. Wrong resampling produces garbled transcription; an unbounded buffer could exhaust memory during long recordings.
Provisioning (extractor + manifest + source) 11 ZIP extraction with prefix normalisation, cancellation during extraction, recursive directory copy, manifest parsing and version validation, SHA256 integrity verification, model detection, source detection modes Provisioning installs models, runtimes, and host-app plugins. If extraction silently corrupts files or ignores cancellation, the user is left with a broken installation and no clear error.
setup/provision.rs 4 Directory and Python runtime preparation, isolation from user profile roots, conditional Blender addon and GIMP plugin provisioning Setup must never touch files outside its designated directories. Conditional provisioning avoids wasting time staging resources for host apps that are not installed.
Remaining (13 modules) 13 Hardware memory heuristics, mode config ordering, setup status collection, model manifest checks, setup cache persistence and corrupt recovery, code provider idle state, registry sharing, DTO casing Each covers a specific boundary or contract that, if broken, produces a subtle but user-visible failure.

Connector Tests (102 tests across 3 connectors + 16 shared)

Connector crates bridge the unified shell to external desktop applications. Their tests use #[cfg(test)] mock implementations for bridge, runtime, and transport layers so that they run without real Blender, GIMP, or LibreOffice processes. This keeps them deterministic and fast enough for CI while still exercising the full provider and executor logic.

Blender Connector (32 tests across 7 modules)

Module Count What It Tests Why It Matters
bridge.rs 9 Token exact-match comparison, token round-trip persistence, public health route (no auth), bearer token validation on scene_current, snapshot payload return, scene cache mutation, oversized body rejection, port conflict error, bridge shutdown The bridge runs as a local HTTP server. Auth bypass, body limit failures, or port conflicts would break the Blender workflow or expose scene data.
provider.rs 9 Tool definitions after bridge start, port-occupied error, scene snapshot via tool, RAG context retrieval with metadata, retrieval failure degradation, live scene status, reconnect tool restoration, missing host status, bridge start before addon provisioning The provider orchestrates bridge, RAG, and tool execution. A failure in any step must degrade gracefully rather than crash the assistant.
setup.rs 5 Addon provisioning, marker short-circuit, probe/enable failure messages, not-prepared status when Blender detected Setup provisioning installs the Blender addon. If the marker check is wrong, provisioning reruns on every launch.
executor.rs 3 Scene query skips RAG, workflow question uses RAG and emits tokens, cancellation returns cancelled result The executor must respect cancellation — a hung generation would block the user until timeout.
state.rs / rag.rs / response.rs 6 Default cache returns no-scene message, fresh data returns connected scene, top-match retrieval, disabled index returns empty, messages marked non-undoable, RAG context parsing State and RAG correctness determines whether the Blender assistant gives scene-aware answers or generic ones.

GIMP Connector (28 tests across 6 modules)

Module Count What It Tests Why It Matters
setup.rs 7 GIMP 2 path rejection, major version CLI parsing, leading number handling, ambiguous path validation, GIMP 3 acceptance, not-prepared status, plugin provisioning with marker write GIMP 2 is not supported. If version detection is wrong, the plugin is installed into an incompatible host, producing cryptic Script-Fu errors.
planner.rs 5 JSON selector parsing, code fence extraction, invalid tool rejection, non-JSON fallback, retry on first non-JSON output The planner parses structured LLM output. Offline models produce inconsistent formatting more often than cloud models, making robust parsing essential.
executor.rs 5 Info query success, fast-path edit success, planned call-api edit, answer without connection when selector chooses none, user-friendly error when edit requires connection The executor must handle both the fast path (heuristic-matched edits) and the planned path (LLM-guided tool calls), with clear errors when the bridge is not connected.
runtime.rs 4 Python resolution priority (prepared > venv > system), strict mode enforcement when dev fallback disabled GIMP's bridge depends on Python. Wrong resolution order could pick the wrong interpreter or fail on machines without system Python.
provider.rs 4 Disconnected state error, connected state tool definitions, missing GIMP status, GIMP 2 rejection The provider must report honest status — claiming "connected" when the bridge is down leads to silent tool execution failures.
heuristics.rs 3 Metadata query detection, region blur fast path, rotate fast path Heuristics route common prompts directly to tools without needing a full LLM planning step, making simple edits faster and more reliable.

LibreOffice Connector (26 tests across 5 modules)

Module Count What It Tests Why It Matters
executor.rs 9 Writer tool execution with streaming, Impress JSON fallback parsing, stringified JSON arguments, trailing brace/whitespace garbage recovery, summary failure fallback, cancellation fallback, timeout fallback Offline LLMs produce malformed JSON more often than cloud models. The garbage recovery tests verify that common format errors (trailing braces, extra whitespace, stringified args) are handled rather than crashing the executor.
provider.rs 7 Writer and Impress status with filtered tools, unsupported mode error, runtime start failure error, missing resources error, concurrent status refresh survival, retryable runtime failure detection Writer and Slides share one provider family but expose different tool sets. If filtering is wrong, Writer users see slide-creation tools and vice versa.
resources.rs 4 Nested path fallback, direct path preference, missing asset error context, dev mode resolution Resource resolution differs between development and packaged builds. Missing asset detection prevents cryptic runtime errors.
response.rs 3 Fallback summary using document count/names, error payload marking, messages marked non-undoable When the LLM summary fails, the fallback summary must still describe what changed. Marking responses non-undoable prevents users from trying to undo actions that cannot be reversed.
runtime.rs 3 Config from layout sets entrypoint, stdio transport log dir, summary description The runtime config drives the Python MCP transport. Wrong entrypoint or log directory makes debugging failures impossible.

Connector-Common (16 tests across 5 modules)

Module Count What It Tests Why It Matters
launch.rs 5 Process detection (skip spawn when running, spawn when absent), executable path exact-match, GIMP and LibreOffice launch error messages Double-launching a host app creates confusion. Missing executable detection must produce a clear message, not a crash.
python.rs 4 Bundled Python not-prepared when payload missing, staged payload can-prepare, prepare copies payload, resolve prepared Python command The bundled Python runtime is required for GIMP and LibreOffice bridges. If preparation fails, both connectors are unavailable.
host_apps.rs 3 Cached path preference when it still exists, standard path fallback, GIMP 3 executable name acceptance Host app discovery caches results for performance. If the cache is stale (app uninstalled or moved), fallback must work.
manifests.rs 3 Manifest validation (empty values rejected), missing expected paths reported, camelCase manifest parsing Manifests drive provisioning. An invalid manifest must be rejected, not silently accepted and used to stage corrupt files.
text_generation.rs 1 TextStreamer trait propagates cancelled result Cancellation propagation ensures that a cancelled generation does not continue streaming tokens after the user stops it.

Type Contract, Security, and Benchmark Tests (17 tests)

Group Count What It Tests Why It Matters
Type contracts 5 JSON wire format for AppMode, camelCase keys in ModeConfig, kind tag in stream events, toolResults key in response, SetupStatus wire keys and enum values The Svelte frontend consumes these JSON payloads directly. A renamed field breaks the UI silently — the TypeScript types match but the runtime values do not.
Security 7 Content validation (valid, empty, at limit, over limit), large content error formatting, file size validation, file size error message Content and file size limits protect the engine from denial-of-service via oversized payloads.
Benchmark stats 5 Empty input produces no stats, single value edge case, known values (mean, std_dev), percentile interpolation, two-value case The benchmark infrastructure computes statistics across runs. Wrong percentile interpolation or edge-case handling would produce misleading performance reports.

Analysis: What Passing Unit Tests Guarantee

When all ~302 unit tests pass, the following properties are verified automatically: the engine starts on any machine regardless of which DLLs are present, backend selection follows the correct preference and fallback chain, backend decisions persist and recover from corruption, all JSON wire formats match the frontend's expectations, ChatML is rendered correctly for each model family, memory pressure thresholds and model selection heuristics are correct, the engine-host process isolation boundary is not violated by any source file, and all connector providers degrade gracefully when their host application is unavailable. These guarantees are enforced on every push without requiring hardware or manual intervention.

Integration Testing

Integration testing was essential because the platform is only useful when its parts work together: the shared engine must start correctly, the unified app must display the right state, each mode must route to the correct provider, and host-application bridges must behave predictably. Integration tests require real hardware and are not automated in CI because there is no way to simulate a real NPU, discrete GPU, or running GIMP/Blender process in a CI container.

Hardware Coverage

Three physical test machines cover the backend matrix, ensuring that every supported inference path is exercised on real hardware:

Machine CPU GPU NPU RAM Backends Tested
Core Ultra laptop Intel Core Ultra Intel Arc (integrated) Intel AI Boost 16 GB CPU, OpenVINO NPU
Intel CPU desktop Intel i-series None None 8 GB CPU only (fallback path)
RTX workstation Intel i5 NVIDIA RTX 2000 None 16 GB CPU, DirectML

The Core Ultra machine is the primary NPU test target. The RTX workstation validates DirectML with a discrete NVIDIA GPU. The Intel CPU machine validates the pure fallback path — the lowest-capability configuration the platform supports.

Integration Test Protocol

After any change to generation config, backend selection, or model loading, the following protocol is executed manually on each relevant machine:

  1. Start the engine with a forced backend: $env:SMOLPC_FORCE_EP = "cpu" (or "directml", "openvino_npu")
  2. Send a streaming chat completion via curl to localhost:19432/v1/chat/completions
  3. Verify: response streams tokens (not a single dump), output is coherent code (not garbage or repetition), generation stops at a reasonable length, TTFT and tokens/sec are within expected range, no crash or "unknown exception" errors

End-to-End Scenarios

Test ID Components Tested Description Expected Outcome Status
IT1 Startup loading + setup state + engine readiness Launch the unified app, let the startup loading screen complete, refresh setup state, and confirm that engine readiness and mode availability are reflected in the main shell. The app moves from startup into the workspace only when engine and setup state are coherent. Unavailable modes are clearly flagged. Covered
IT2 Mode registry + provider dispatch Switch between Code, GIMP, Blender, and LibreOffice modes and confirm routing to the correct backend provider. Each mode invokes the correct provider family without leaking behaviour across assistants. Covered
IT3 GIMP bridge + tool execution Prepare the local GIMP runtime, refresh available tools, execute an editing command, and test undo behaviour. The assistant connects to the bridge, executes supported tools, and returns a clear summary. Covered with live scenarios
IT4 Blender bridge + local RAG Start the Blender bridge, read scene context, retrieve local documentation, and answer a scene-specific request. The assistant combines bridge state and retrieved context without requiring cloud inference. Covered with live scenarios
IT5 LibreOffice runtime + planner/tool execution Run Writer or Slides requests that require structured tool calls and verify graceful handling of unsupported flows. Valid requests execute through the office runtime. Unsupported routes fail clearly and safely. Covered
IT6 Voice input + audio commands + engine endpoints Record a short prompt, transcribe it locally into the composer, send the prompt, and request read-aloud on the response. Dictated text appears in the chat input. Local playback works or fails cleanly without breaking the text workflow. Covered in development builds
IT7 Backend forced path on each machine Force CPU, DirectML, and OpenVINO NPU backends on the appropriate machine and run the streaming completion protocol. Each backend produces coherent streaming output with TTFT in expected range. No crashes or "unknown exception" errors. Covered across 3 machines

Automated Integration Checks

In addition to the live scenarios above, the repository includes automated checks around engine-status mapping, runtime-mode restoration, setup preparation, memory-pressure classification, connector execution, and audio request contracts. These tests cover the boundaries between the engine, the Tauri commands, and the mode providers where regression risk is highest.

# Commands exercised during integration verification
cargo test -p smolpc-engine-client
cargo test -p smolpc-engine-host
cargo test -p smolpc-desktop
cargo test -p smolpc-connector-gimp
cargo test -p smolpc-connector-blender
cargo test -p smolpc-connector-libreoffice

Prompt Testing

Prompt testing was necessary because SmolPC runs offline quantised models, and their behaviour varies more across quantisation levels and inference backends than cloud models. A prompt that works reliably with Qwen2.5-1.5B via CPU might produce a different tool-call format when the same model runs on the NPU via OpenVINO, or might generate malformed JSON that the executor must recover from. Prompt testing checks that the assistants remain understandable, reliable, and correctly routed in realistic user-facing scenarios across these conditions.

Test ID Mode Representative Prompt Expected Behaviour Status
PT-CH1 Code Mode "Explain this error and suggest how to fix it." The assistant provides a clear coding explanation and next steps in a local chat workflow. Scenario defined and used during development
PT-VO1 Code Mode Voice Dictate a short coding question, then ask the app to read the answer aloud. Voice input transcribes into the composer correctly. The response remains understandable when played back through local read-aloud. Scenario aligned with implemented UI flow
PT-GI1 GIMP "Blur the top half of the image." The assistant chooses an appropriate direct tool or fast path and reports the edit clearly. Scenario aligned with automated heuristics
PT-BL1 Blender "How do I add a bevel modifier?" The assistant retrieves relevant Blender context and gives a scene-aware explanation. Scenario aligned with local RAG tests
PT-LO1 Writer / Slides "Add a heading called Hello." / "Add a slide titled Intro." The assistant produces a valid structured tool call and returns a useful summary of what changed. Scenario aligned with executor tests
PT-FB1 Cross-mode fallback Ambiguous or malformed tool-call output The system falls back safely, summarises locally where possible, or asks for clarification instead of failing silently. Covered

Compatibility Testing

Compatibility testing focused on the environments and conditions that matter most for local deployment: packaged desktop behaviour on Windows, hardware-specific runtime paths, and graceful fallback when a preferred dependency or host application is missing. Because the project bundles its own inference engine and runtime DLLs, compatibility is as much about setup and dependency handling as it is about raw operating-system support.

Hardware and OS Coverage

Testing was conducted on three physical machines running Windows 11 (see the Integration Testing section for the full hardware matrix). Degradation was verified by removing specific DLLs from the resource directory and confirming that the engine starts with reduced capability rather than crashing. The startup degradation unit tests (4 tests using tempdir()) automate this verification for ORT, OpenVINO, and NPU plugin bundles.

Platform or Condition Focus Expected Behaviour How Verified Status
Windows 11 packaged build Primary deployment target for bundled local inference Installer launches correctly, prepares local resources, and exposes supported modes Tested on 3 physical machines with NSIS installer Primary validation target
CPU-only machine (8 GB, no GPU/NPU) Fallback inference path Engine detects no GPU/NPU and selects CPU backend automatically Tested on Intel CPU desktop; verified via engine status API Covered
DirectML machine (NVIDIA RTX 2000) GPU-accelerated inference via DirectML Engine selects DirectML backend, generation performance improves over CPU Tested on RTX workstation with forced and automatic selection Covered
NPU machine (Intel AI Boost) NPU-accelerated inference via OpenVINO Engine selects OpenVINO NPU backend after probe confirms candidate Tested on Core Ultra laptop Covered
Missing runtime DLLs Graceful degradation when ORT, DirectML, or OpenVINO bundles are absent Engine starts with reduced capability; affected lanes disabled, others remain functional 4 automated unit tests (tempdir-based), plus manual removal testing Covered
Missing host applications Setup gating and user messaging Unsupported or incomplete host dependencies produce a clear blocked or setup-required state Verified via setup status checks; GIMP version detection rejects GIMP 2 Covered
Missing microphone or audio output Voice feature degradation Voice controls disable cleanly; text-chat workflow remains unaffected Tested with audio devices disabled in Windows settings Covered

Performance and Stress Testing

Performance matters because SmolPC runs local models on varied hardware. A machine with 16 GB RAM and an NPU has a fundamentally different performance profile than an 8 GB CPU-only laptop, and both must feel usable. The project includes a dedicated benchmark infrastructure for systematic measurement, plus automated tests for the heuristics that drive runtime behaviour (memory pressure, model selection, streaming).

Benchmark Infrastructure: smolpc-benchmark

The smolpc-benchmark crate is a standalone CLI tool that measures inference performance across backends using fixed prompts, greedy decoding (temperature=0.0), and process-specific resource monitoring. This produces reproducible, comparable results across machines and across code changes.

Measurement Methodology

For each backend-model combination, the tool loads the model, verifies the backend, captures an idle baseline (2 seconds of RSS and CPU% with no inference), runs warmup iterations (minimum 3 for NPU to ensure compilation cache is populated), then executes 10 fixed coding prompts across 3 tiers (short, medium, long) for N measured iterations. Statistics are computed per-prompt: mean, median, P90, P95, standard deviation, min, max.

Metrics Collected

Metric Source Unit Why It Matters
TTFT (Time to First Token) Engine GenerationMetrics Milliseconds Perceived responsiveness — the most noticeable latency metric for users
Tokens per second Engine GenerationMetrics Tokens/sec Decode throughput; determines how fast the response streams in
TPOT (Time Per Output Token) Derived: (total_time - ttft) / (tokens - 1) Milliseconds Isolates decode speed from prefill latency
Peak memory (RSS) Background ResourceSampler via sysinfo MB Determines whether the engine fits within the machine's available RAM
CPU utilisation Background ResourceSampler (mean and peak) Percent High CPU% during NPU inference indicates fallback to CPU path
Truncation and EOS rates Reliability aggregation Fraction (0.0–1.0) High truncation means max_tokens is too low; low EOS rate means the model is not stopping naturally

Statistical Analysis

The Stats struct computes population statistics with linear interpolation for percentiles. The benchmark stats module is itself covered by 5 unit tests verifying empty input, single value, known values, percentile interpolation, and the two-value edge case.

pub struct Stats {
    pub mean: f64,
    pub median: f64,
    pub p90: f64,
    pub p95: f64,
    pub std_dev: f64,
    pub min: f64,
    pub max: f64,
}

Performance-Related Automated Tests

In addition to the benchmark tool, several automated tests verify the heuristics that drive runtime performance decisions:

Test ID Area What Is Tested Why It Matters Status
PF1 Engine startup Startup degradation tests (4 tests) verify the engine starts in all DLL configurations; readiness time measured during integration testing Slow or fragile startup undermines classroom usability even if generation quality is good Automated + measured
PF2 Streaming generation TTFT, streaming continuity, and response duration measured by benchmark crate; token streaming verified in integration protocol These metrics affect perceived responsiveness more than a single total latency number Instrumented
PF3 Backend selection 6 host tests + 12 core tests for selection state machine, benchmark gate enforcement (speedup ratio + TTFT regression limit), demotion after consecutive failures Wrong backend choice sends users to the slowest path without explanation Automated
PF4 Memory pressure Warning and critical thresholds, model recommendation logic, heavy mode detection, auto-unload messaging (8 inference command tests) Protects low-RAM devices from instability during generation Automated
PF5 Voice I/O Audio resampling tests (5 tests), recording buffer bounds, TTS sidecar discovery Voice must not degrade the main chat path; unbounded buffers could exhaust memory Automated

Benchmark Method

The benchmark runs used the project's dedicated benchmarking tool with a fixed prompt corpus and deterministic decoding settings. This kept the prompt inputs aligned across all three machines, but the configurations were not perfectly identical: two machines used 10 runs per prompt with 2 warmups, while the Core Ultra Vivobook used 3 runs per prompt with 1 warmup for a quicker backend comparison pass. The results below should therefore be read as representative measurements that support the hardware-aware runtime story, rather than as one fully normalised laboratory benchmark matrix.

We focused the main report on time to first token, tokens per second, and time per output token, because these most directly affect perceived responsiveness in a local assistant. Full raw JSON artefacts, including per-prompt breakdowns and skipped combinations, are linked in the Appendix benchmark section.

Test Machines

Machine Hardware Summary Benchmark Coverage Main Role in Testing
LAPTOP-4Q14H91S 11th Gen Intel Core i5-1135G7, 7.7 GB RAM, Intel Iris Xe CPU benchmark for qwen2.5-1.5b-instruct; attempted NPU and 4B paths but they were unavailable on this setup Older low-memory fallback baseline
codex-cpu-full-20260327 12th Gen Intel Core i7-1255U, 31.7 GB RAM, Intel Iris Xe CPU benchmark for both qwen2.5-1.5b-instruct and qwen3-4b Stronger CPU-only comparison point
vivobook-pinto Intel Core Ultra 9 285H, 15.4 GB RAM, NVIDIA RTX 4050 Laptop GPU, Intel Arc 140T GPU CPU, DirectML, and OpenVINO NPU benchmarks for both qwen2.5-1.5b-instruct and qwen3-4b Main backend-comparison machine

Summary Results

Machine Backend Model Mean TTFT (ms) Mean Tok/s Mean TPOT (ms) Cold Start TTFT (ms) Result Note
LAPTOP-4Q14H91S CPU qwen2.5-1.5b-instruct 684.4 7.88 129.0 621 Usable fallback on older hardware; only successful path on this machine
codex-cpu-full-20260327 CPU qwen2.5-1.5b-instruct 476.8 13.93 72.8 589 Noticeably stronger CPU-only baseline than the older i5 laptop
codex-cpu-full-20260327 CPU qwen3-4b 1082.7 3.97 253.7 1364 Larger model remains usable, but much slower on CPU
vivobook-pinto CPU qwen2.5-1.5b-instruct 106.6 51.61 19.4 115 Very strong CPU fallback on newer hardware
vivobook-pinto CPU qwen3-4b 245.5 15.07 66.4 1382 4B model is practical on a stronger CPU, but not the fastest lane
vivobook-pinto DirectML qwen2.5-1.5b-instruct 154.2 108.62 8.5 614 Best sustained throughput for the smaller model
vivobook-pinto DirectML qwen3-4b 262.7 52.46 17.7 490 Best sustained throughput for the larger model
vivobook-pinto OpenVINO NPU qwen2.5-1.5b-instruct 2279.3 16.52 60.5 2298 Worked successfully, but was much slower than DirectML and CPU in these tests
vivobook-pinto OpenVINO NPU qwen3-4b 4091.5 7.88 127.0 4175 Highest latency path in this benchmark set

Peak memory is intentionally omitted from the main comparison table because the benchmark tracks engine-process RSS rather than full GPU VRAM use. Throughput and latency are therefore the more reliable headline metrics for the report body.

Key Findings

User Acceptance Testing

User acceptance testing was planned around representative target users rather than a single generic tester. This matched the product itself: Code mode targets coding support, GIMP and Blender target creative workflows, and the LibreOffice modes target classroom and document-based tasks. Feedback was gathered informally and anonymously from representative users and peers across these workflows to check whether the product felt useful, understandable, and realistic to use as an offline assistant platform.

Tester Profiles

Tester Profile Background Assistant Tested
Tester 1 Student or beginner programmer Code Mode
Tester 2 Teacher or document-focused user Writer / Slides
Tester 3 Image-editing user GIMP
Tester 4 3D learner or maker Blender

Test Cases

Test ID Assistant Description Expected Outcome Status
UAT1 Code Mode Ask for help understanding and fixing a short code issue in a chat-based workflow, optionally using voice input or read-aloud. The tester understands the answer, identifies a next step, and remains comfortable with both the offline chat flow and the optional voice controls. Confirmed with anonymous positive feedback
UAT2 Writer / Slides Request a simple structured document or slide change through natural language. The assistant makes or explains the change clearly and feels useful for classroom productivity tasks. Confirmed with anonymous positive feedback
UAT3 GIMP Perform a straightforward image-editing action using a plain-English command. The tester sees an understandable edit result and trusts the assistant's summary of what happened. Positive for simple edits
UAT4 Blender Ask for help with a common modelling action that benefits from scene or documentation context. The tester receives Blender-specific guidance that feels more useful than a generic chatbot response. Confirmed with anonymous positive feedback

Tester Feedback

Code Mode Profile

Anonymous feedback on Code mode was positive overall. Testers found the chat flow easy to follow, valued the offline setup with no cloud dependency, and understood the assistant's answers well enough to identify sensible next steps in beginner-friendly coding tasks. Voice input was noted as a useful accessibility feature, though testers generally preferred typing for code-related queries. The local read-aloud feature was appreciated for reviewing longer explanations hands-free.

Writer / Slides Profile

Anonymous feedback suggested that the office workflows made simple document and slide actions clearer and faster to access, especially for users who preferred describing an action in plain language rather than searching through menus. Testers found the structured tool calls reliable for straightforward operations (add heading, add slide), though more complex multi-step changes occasionally required rephrasing.

GIMP Profile

Anonymous feedback on GIMP was positive for straightforward edits and summaries, but it also confirmed that this remains the least consistent specialist mode. Testers noted that the heuristic fast-path routing (e.g., detecting "rotate" or "blur" prompts) worked well for common operations, while more complex multi-step edits sometimes required clearer prompts. The feedback supports continued focus on tightly scoped prompts and clearer setup guidance.

Blender Profile

Anonymous feedback indicated that the Blender workflow felt more useful than a generic chatbot response because the assistant could combine scene-aware guidance with locally retrieved help content. Testers particularly valued that the assistant knew the current scene state (objects, modifiers) rather than giving generic Blender documentation. The local RAG retrieval was noted as making responses noticeably more relevant.

Project Partner Feedback

Anonymous stakeholder and partner-facing feedback was positive overall, particularly around the value of an offline-first assistant, the unified platform direction, and the practicality of a deployment story that does not depend on cloud accounts or a separate local model server. Partners noted that the single-installer approach significantly reduced the barrier to evaluation compared to solutions requiring separate Ollama or LM Studio installations.