Development Guide¶
This guide covers the development setup, architecture, testing, and contribution guidelines for the Continuum Router project.
Table of Contents¶
- Prerequisites
- Development Setup
- Project Structure
- Architecture Overview
- Building and Running
- Release Packaging and Code Signing
- Testing
- Code Quality
- Debugging
- Performance Profiling
- Contributing
Prerequisites¶
Required Tools¶
- Rust: 1.97 or later (Install Rust); the root router package and standalone
perf/harness use Rust edition 2024, while this repository pins Rust 1.97.1 inrust-toolchain.tomlfor reproducible CI and local linting. - Git: For version control
- C Compiler: gcc, clang, or MSVC (for native dependencies)
Rust edition, MSRV, and formatting¶
The root continuum-router package and standalone perf/ harness declare Rust edition 2024 with rust-version = "1.97". The vendored crates/continuum-protocol package intentionally stays on edition 2021, with the hub workspace MSRV of Rust 1.97, because it mirrors the upstream Continuum Hub protocol crate; Cargo supports this mixed-edition graph, and router CI validates that path dependency separately, including a byte-identity check against its pinned hub revision (see Vendored continuum-protocol crate). rustfmt.toml pins style_edition = "2021" so cargo fmt and editor integrations keep the existing formatting style until a separate formatting-only migration is accepted.
Recommended Tools¶
- cargo-watch: For auto-recompilation during development
- cargo-edit: For managing dependencies
- cargo-audit: For security vulnerability checking
- cargo-flamegraph: For performance profiling
- rust-analyzer: IDE support (VS Code, IntelliJ, vim/neovim)
Install Development Tools¶
# Install essential development tools
cargo install cargo-watch cargo-edit cargo-audit cargo-flamegraph
# Install code coverage tools
cargo install cargo-tarpaulin
# Install benchmarking tools
cargo install cargo-criterion
Development Setup¶
Clone and Setup¶
# Clone the repository
git clone https://github.com/lablup/continuum-router.git
cd continuum-router
# Check that everything compiles
cargo check
# Run initial test suite
cargo test
# Build development version
cargo build
make setup installs the development tools and points Git at the checked-in hooks in .githooks/ (core.hooksPath). The pre-commit hook runs cargo fmt and Clippy and stops the commit when either one changes the tree, so stage the changes and commit again. If core.hooksPath is already set to another directory, make setup leaves it unchanged and prints the command to switch.
IDE Setup¶
VS Code¶
- Install the
rust-analyzerextension - Install the
CodeLLDBextension for debugging - Use the provided
.vscode/settings.json:
{
"rust-analyzer.cargo.features": "all",
"rust-analyzer.checkOnSave.command": "clippy",
"editor.formatOnSave": true,
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
}
}
IntelliJ IDEA / CLion¶
- Install the Rust plugin
- Open the project root
- Configure Cargo settings in Preferences → Build → Cargo
Vim/Neovim¶
- Install
rust-analyzervia Mason or manually - Configure LSP client (nvim-lspconfig, coc.nvim, etc.)
- Example nvim-lspconfig setup:
require('lspconfig').rust_analyzer.setup({
settings = {
["rust-analyzer"] = {
cargo = { features = "all" },
checkOnSave = { command = "clippy" }
}
}
})
Project Structure¶
Layer Architecture¶
The project follows a clean 4-layer architecture:
Directory Structure¶
src/
├── main.rs # Application entry point and CLI
├── lib.rs # Library exports and module declarations
│
├── core/ # Core domain layer (no external dependencies)
│ ├── mod.rs # Core module exports
│ ├── models/ # Domain models
│ │ ├── backend.rs # Backend configuration and state
│ │ ├── model.rs # LLM model representations
│ │ ├── request.rs # Request/response models
│ │ └── health.rs # Health check models
│ ├── errors.rs # Error types and handling
│ ├── traits.rs # Domain traits and interfaces
│ ├── retry/ # Retry logic and policies
│ │ ├── policy.rs # Retry policies
│ │ └── backoff.rs # Backoff strategies
│ └── config/ # Configuration models
│ ├── mod.rs # Config module exports
│ ├── models.rs # Configuration structures
│ └── validation.rs # Config validation logic
│
├── services/ # Business logic layer
│ ├── mod.rs # Module exports
│ ├── model_service.rs # Model aggregation and caching
│ ├── proxy_service.rs # Request routing and proxying
│ ├── health_service.rs # Health monitoring
│ └── streaming/ # SSE streaming support
│ ├── mod.rs # Streaming module exports
│ ├── parser.rs # SSE event parsing
│ └── handler.rs # Stream handling logic
│
├── infrastructure/ # External integrations layer
│ ├── mod.rs # Infrastructure exports
│ ├── backends/ # Backend client implementations
│ │ ├── mod.rs # Backend trait and factory
│ │ ├── openai.rs # OpenAI-compatible backend
│ │ ├── ollama.rs # Ollama backend
│ │ └── vllm.rs # vLLM backend
│ ├── cache/ # Caching implementations
│ │ ├── mod.rs # Cache traits
│ │ ├── lru.rs # LRU cache
│ │ └── ttl.rs # TTL-based cache
│ ├── config/ # Configuration loading
│ │ ├── loader.rs # Config file loading
│ │ └── watcher.rs # Hot-reload support
│ └── common/http_client.rs # HTTP client factory with pooling
│
├── http/ # HTTP layer
│ ├── mod.rs # HTTP module exports
│ ├── routes.rs # Route definitions
│ ├── handlers/ # Request handlers
│ │ ├── chat.rs # Chat completion endpoints
│ │ ├── models.rs # Model listing endpoints
│ │ ├── health.rs # Health check endpoints
│ │ └── admin.rs # Admin endpoints
│ ├── middleware/ # HTTP middleware
│ │ ├── auth.rs # Authentication
│ │ ├── logging.rs # Request logging
│ │ ├── metrics.rs # Metrics collection
│ │ └── error.rs # Error handling
│ └── dto/ # Data Transfer Objects
│ ├── request.rs # Request DTOs
│ └── response.rs # Response DTOs
│
└── metrics/ # Metrics and monitoring
├── mod.rs # Metrics module exports
├── backend.rs # Backend-specific metrics
├── routing.rs # Routing metrics
├── model.rs # Model service metrics
└── streaming.rs # Streaming metrics
tests/
├── rate_limiting_test.rs # A top-level target: CI selects it by name or it
│ # reads process-global state
├── streaming/ # A group target (issue #1620)
│ ├── main.rs # `pub mod <stem>;` per member, helpers via #[path]
│ ├── streaming_test.rs # Member: a former top-level file, unchanged
│ └── ...
├── server/, routing/, guardrails/, ... # The other group targets
├── tool_calling/ # Tool-calling suite, its own target (main.rs)
├── common/ # Shared helpers (mock servers, fixtures)
├── *_support/, *_common/ # Other shared helpers, declared by the targets
│ # that use them
└── fixtures/ # Test data
examples/
├── standalone_server.rs # Minimal server from a config file
├── axum_integration.rs # Embedding router in an existing Axum app
├── programmatic_config.rs # Configuration without YAML
└── hot_reload.rs # Hot-reload and config-change subscriptions
Running Examples¶
The examples/ directory contains runnable Rust programs that demonstrate the library API. Run any example with:
For example:
# Start a standalone server
cargo run --example standalone_server -- --config config.yaml
# Embed the router in a custom Axum app
cargo run --example axum_integration -- --config config.yaml
# Configure programmatically (no YAML file)
OPENAI_API_KEY=sk-... cargo run --example programmatic_config
# Demonstrate hot-reload
cargo run --example hot_reload -- --config config.yaml
See Library Usage for a full guide to the embeddable crate API.
Architecture Overview¶
Dependency Flow¶
HTTP Layer → Services Layer → Infrastructure Layer → Core Layer
↓ ↓ ↓ ↑
└────────────┴────────────────┴────────────────────┘
Uses Core Types
Key Design Patterns¶
- Constructor Injection: Services are passed as constructor arguments; there is no runtime DI container
- Repository Pattern: Backend implementations abstract data access
- Strategy Pattern: Load balancing strategies are pluggable
- Observer Pattern: Health monitoring uses event-driven updates
- Circuit-breaker state machine: Standalone state tracking and Admin controls
- Retry with Backoff: Handles transient failures
Building and Running¶
Development Build¶
# Quick development build
cargo build
# Run with debug output
RUST_LOG=debug ./target/debug/continuum-router
# Run with auto-reload
cargo watch -x run
# Run with specific config
cargo run -- --config dev-config.yaml
Release Build¶
# Optimized release build (all features enabled by default)
cargo build --release
# Run release build
./target/release/continuum-router
# Build with specific features only (minimal library build)
cargo build --lib --no-default-features --features "metrics,hot-reload"
The committed .cargo/config.toml sets no target-cpu, so every build above targets the portable baseline for its architecture, the same as what release binaries ship. To opt into native CPU features for a local build, set RUSTFLAGS for that invocation rather than editing the committed file: RUSTFLAGS="-C target-cpu=native" cargo build --release. A user-level ~/.cargo/config.toml works too, and neither is ever committed or shared through a CI cache: cached proc-macro dylibs built for one CPU generation crash rustc with SIGILL when restored onto a different one on GitHub's heterogeneous hosted runners.
Cargo Feature Flags¶
The crate exposes feature flags to enable optional components. The full feature set is enabled by default when building the binary.
| Feature | Description | Default |
|---|---|---|
full | Enables all features below | yes (via default) |
cli | CLI binary support via clap | yes |
admin | Admin API endpoints | yes |
metrics | Prometheus metrics endpoint | yes |
files | Files API (/v1/files) | yes |
health-check | Background health monitoring | yes |
hot-reload | Config file watching for live reload | yes |
webui | Embedded WebUI assets | yes |
gcp-auth | Google Cloud auth for Gemini Service Account | yes |
redis-cache | Redis/Valkey backend for the response cache and for distributed rate limiting (deadpool-redis). Not in default/full, but official release binaries and container images compile it; see Deployment for the supported endpoint contract. | no (shipped in release builds) |
embed | Minimal in-process serving surface for embedding the router as a library (e.g. iOS). Enables no extra component and pulls in none of the iOS-hostile optional deps (rusqlite, redis, prometheus, rust-embed, aws-*, clap, notify, rmcp, object_store, gcp_auth). | no |
When using continuum-router as a library crate, you can select only the features you need to reduce compile times and binary size:
[dependencies]
continuum-router = { git = "...", default-features = false, features = ["metrics", "hot-reload"] }
Build examples:
# Minimal core library (no optional deps)
cargo build --lib --no-default-features
# Library with metrics support
cargo build --lib --no-default-features --features "metrics"
# Library with admin and hot-reload
cargo build --lib --no-default-features --features "admin,hot-reload"
# Minimal in-process embedding surface (e.g. linking into an iOS app)
cargo check --lib --no-default-features --features "embed"
cargo check --lib --no-default-features --features "embed" --target aarch64-apple-ios
# Full binary (default)
cargo build --release
The embed feature is the supported way to link continuum-router as a Rust library and run its HTTP server in-process (for example on iOS, where the app sandbox forbids running the router as a sidecar subprocess). It keeps the dependency graph free of crates that are awkward or impossible to cross-compile for mobile targets. The entrypoint is serve_embedded; verify the excluded dependencies with:
Cross-Compilation¶
# Install cross
cargo install cross
# Build for Linux x86_64
cross build --release --target x86_64-unknown-linux-gnu
# Build for macOS ARM64
cross build --release --target aarch64-apple-darwin
# Build for Windows
cross build --release --target x86_64-pc-windows-gnu
CI runner usage and build reuse¶
Automatic self-hosted CI runs on [self-hosted, macOS], supporting both Intel and Apple Silicon machines:
- Concurrency groups: Two repository-wide concurrency groups bound simultaneous jobs across pull requests, dependency floats, and documentation builds:
continuum-router-self-hosted-1: admits the default graph, isolated graphs, and weekly dependency float.continuum-router-self-hosted-2: admits change detection, the release graph, deployment validation, IDE drift checks, and documentation builds.- Each group uses
queue: maxwithcancel-in-progress: false, preserving up to 100 waiting jobs in FIFO order instead of replacing another PR's pending check (while workflow-level concurrency still cancels older runs of the same PR on a new push). This bounds automatic jobs in this repository, not other organization repositories or manually dispatched Dynamo E2E jobs with custom runner labels; see GitHub's concurrency documentation.
- Two-build test execution: Both the CI workflow and
make ci-localrun tests from two library builds:- Default features (
cargo test --testsinci-default), retained because six tests gated onnot(feature = "control-plane")ornot(feature = "bedrock-sigv4")exist only there. - All features (
cargo test --all-features --no-fail-fast --lib --testsinci-release-graph), where--no-fail-fastruns the remaining binaries after a failure. - Different feature graphs, normal builds versus test builds, and
clippyversus executable tests still require separate compilations (a repeatedCompiling continuum-routerline can name a new integration binary rather than rebuildingsrc/lib.rs). VerboseFreshoutput is omitted, andcargo-denyreports duplicate-version warnings without long inclusion graphs.
- Default features (
- Local CI helpers:
scripts/local-ci.sh --only <test target>reruns a single exact target under every feature (exiting2if no step matches or if every selected step was skipped withNo step ran, and reporting the skipped count on the last line when steps are skipped).make ci-local-gatedskips the feature-graph steps that the hosted gate would skip for the branch (running everything when there is no merge base withorigin/main, when the work tree is dirty, or when the gate errors or widens), while ungatedmake ci-localremains the merge gate.
- Toolchains, caching, and debug symbols: Rust jobs install the numeric channel from
rust-toolchain.tomlvia.github/actions/rust-toolchain, and native self-hosted artifacts stay in$HOME/actions-cargo-target/continuum-router(outside checkout cleanup, with an 80 GB ceiling, kept separate per Intel/Apple Silicon host). CI and local mirrors default toCARGO_PROFILE_DEV_DEBUG=0andCARGO_PROFILE_TEST_DEBUG=0to reduce debug artifacts and link time while keeping assertions (set either to2locally when symbols are needed); the performance workflow retains debug symbols for crash diagnostics and uses the shipped release profile for capacity measurements.
| Active workflow or job | Runner policy | Remaining requirement |
|---|---|---|
| CI Rust graphs, change detection, IDE checks | Self-hosted macOS, two shared lanes | Native Rust toolchain and persistent artifacts |
| Deployment asset validation | Self-hosted macOS, lane 2 | Installs Go, Helm, kubectl, kubeconform, and PyYAML; no live cluster or Docker needed |
| Documentation build / deployment | Build on self-hosted macOS lane 2; Pages deployment on GitHub-hosted Ubuntu | Pages write and OIDC permissions stay on the deployment job |
| Dependency float | Self-hosted macOS, lane 1 | Serial full matrix against updated dependencies |
| CI Redis shared-state checks | GitHub-hosted Ubuntu | Linux Actions service container; migrate after the Linux VM is ready |
| Performance harness and capacity gate | GitHub-hosted Ubuntu | Existing Linux CPU baseline and gdb/core-dump diagnostics; review baselines when moving to a VM |
| KV listener container smoke / Dynamo E2E default | GitHub-hosted Ubuntu | Linux Docker/Compose; Dynamo E2E is manual and accepts explicit runner labels |
| Release and Debian packaging | Hosted Linux and Windows ARM64 builders; self-hosted macOS and Windows signer | Platform toolchains, signing/publishing credentials, Docker/Buildx; Docker and Debian jobs already reuse release binaries |
| Project automation / staging deployment | GitHub-hosted Ubuntu | App credentials or deployment credentials; migration needs a trusted isolated VM |
update_homebrew_formula.yml_ and launchpad_ppa.yml_ are inactive files, not scheduled workflows. GitHub-hosted jobs remain until their required VM capabilities are provisioned; the efficiency changes add no new hosted jobs.
Release Packaging and Code Signing¶
The release workflow treats signing as part of artifact construction, not as optional post-processing. The four Linux matrix entries remain unsigned and unchanged. The macOS and Windows paths fail before archive, checksum, or release upload when their signing contract is not satisfied.
Platform flow¶
- macOS ARM64 runs on
self-hosted-macos-26-arm64..github/actions/macos-signing-setupfinds a capable Homebrew OpenSSL through the known/opt/homebrewand/usr/localkeg andbrewpaths before falling back toPATH, passes the.p12password through the environment, extracts a Developer ID Application certificate for keychain-freercodesign, and pins SHA-256 digests for both thercodesignarchive and executable. The archive is verified before extraction, and an extracted or cached executable is verified before execution or reuse..github/actions/macos-sign-notarize-binarysigns bothcontinuum-routerandcontinuum-kv-listenerwith the hardened runtime, verifies their authority and identifiers, creates the zip, and requiresnotarytoolto reportstatus: Accepted. Any other result fetches the notarization log and fails. Analways()cleanup step removes the temporary.p12and PEM immediately afterward. Apple does not staple bare command-line executables or zip containers, so Gatekeeper retrieves the ticket online. - Windows x86_64 builds and signs on
windows-on-macmini02-x64. Both executables are copied to a clean staging directory, signed throughscripts/sign-windows.ps1, and verified again withsigntool verify /pa /all /vbefore the zip is created.LICENSEandNOTICEare included beside the executables. - Windows ARM64 builds on the GitHub-hosted
windows-11-armrunner. The lockedringandaws-lc-sysdependencies require Clang-compatible ARM64 assembly support, so.github/actions/setup-clang-windows-arm64verifiesclangandclang-clbefore Cargo runs. The builder uploads only the two unsigned PE files plusLICENSEandNOTICEas a one-day workflow artifact. The x86_64 signing runner checks both PE machine fields for ARM64, signs and fail-closed verifies both files, then createscontinuum-router-windows-aarch64.zip, its checksum, and the release upload. No unsigned ARM64 archive is created or uploaded. The signer checks out onlyscripts/sign-windows.ps1, into its ownsigning-checkout/directory: it shares this runner's workspace with the x86_64 build, and a sparse checkout at the workspace root once left the next build job without the repository's files.
Both Windows targets use the static MSVC CRT. Authenticode hashes a PE file without executing it, so the x86_64 signing host can sign ARM64 outputs. Every job that consumes the completed build, including release notifications and container publication, also depends on the ARM64 signing job and cannot report success while that artifact is unsigned or missing.
Safe signing test mode¶
A manual workflow_dispatch can set the boolean signing_test input to true to exercise the production signing path without creating or changing a release. The default is false, so existing release events and manual production dispatches retain their previous behavior. Test mode runs all seven build matrix entries, performs real macOS signing and notarization, signs and verifies Windows x86_64, and completes the hosted Windows ARM64 handoff on the restricted signer.
Completed archives and checksums are retained only as GitHub Actions artifacts for three days. The Windows ARM64 final artifact is named signing-test-final-signed-aarch64-pc-windows-msvc, which is distinct from the one-day unsigned-aarch64-pc-windows-msvc handoff. The workflow does not invoke either GitHub Release upload, create a tag or release, publish either container image, mirror to Harbor, notify Teams, or update Homebrew while signing_test is true. Its explicit Signing test (no publication) run title also makes the downstream Debian packaging workflow skip the completion event instead of modifying an existing release, while the staging workflow already accepts only release completion events.
The top-level workflow name is Release Packaging, migrated from Release together with both workflow_run consumers. GitHub evaluates a workflow_run consumer from the default branch, so a branch-only condition in debian_build.yml cannot protect the first pre-merge test. The distinct name closes that rollout gap: while this change is still on a pull request, default-branch consumers listen only for the old Release name and cannot be triggered by the branch's Release Packaging run; after merge, both consumers listen for Release Packaging, and the run-title and event-type guards provide the steady-state protection. The rename changes orchestration identity only; release events, manual production dispatches, asset uploads, containers, Debian packaging, and staging keep their normal behavior.
Dispatch the test against the revision under review and leave release_tag empty:
Packaging environment¶
Configure these names in the protected packaging environment. Never commit or print their values.
| Name | Required | Purpose |
|---|---|---|
APPLE_CERTIFICATE | yes | Base64-encoded Developer ID Application .p12 |
APPLE_CERTIFICATE_PASSWORD | yes | Password for the .p12 |
AC_API_KEY_ID | yes | App Store Connect API key ID for notarization |
AC_API_PRIVATE_KEY_P8 | yes | Base64-encoded or raw App Store Connect private key |
AC_API_ISSUER_ID | Team keys only | App Store Connect issuer ID; omit for an Individual key |
BUNDLE_ID | yes (environment variable preferred) | Stable reverse-DNS code-signing identifier |
WINDOWS_SIGN_CERT_PATH | yes | Path on the signing runner to the public code-signing certificate |
WINDOWS_SIGN_CA_CERT_PATH | yes | Path on the signing runner to the issuing CA certificate |
WINDOWS_SIGN_KEY_CONTAINER | yes | KMS-backed key container passed to the Windows CSP |
WINDOWS_SIGNTOOL_PATH | no | Override for the Windows SDK signtool.exe |
WINDOWS_SIGN_CSP | no | Override for the KMS CSP provider name |
WINDOWS_SIGN_TIMESTAMP_URL | no | Override for the RFC 3161 timestamp service; the default is Sectigo |
The Windows private key is not exported to GitHub. The self-hosted signing runner must already have the Windows SDK, the KMS CSP, access to the configured certificate chain, and authorization to use the key. The helper does not echo configured paths or the key container; missing files are reported by variable name, and every deployment-specific value must be stored as a masked environment secret.
Validation and rollout¶
Static checks that do not need signing credentials are safe to run on a development machine:
python3 scripts/check_release_signing.py
python3 scripts/test_check_release_signing.py
python3 scripts/check_action_pins.py
actionlint -shellcheck= .github/workflows/release.yml
cargo tree --locked --target aarch64-pc-windows-msvc --features control-plane,appproxy-router,hot-reload,redis-cache,otel,dynamo,dynamo-kv,dynamo-event-bus
cargo tree --locked --manifest-path crates/continuum-kv-listener/Cargo.toml --target aarch64-pc-windows-msvc
cargo tree verifies target-specific dependency resolution only. A rollout is complete only after a controlled signing_test dispatch runs on the configured runners and all of the following are checked:
- Both Windows archives contain
continuum-router.exe,continuum-kv-listener.exe,LICENSE, andNOTICE, and their.sha256files validate. - On clean x86_64 and ARM64 Windows machines, both executables report a valid expected publisher through
Get-AuthenticodeSignatureandsigntool verify /pa /all /v. - On a clean networked macOS machine, both extracted executables show a Developer ID Application authority, the hardened runtime flag, and pass Gatekeeper without an unidentified-developer warning.
- The macOS notarization log reports
Accepted, the final Windows ARM64 Actions artifact is distinct from the unsigned handoff, and every release upload, container publication, Harbor mirror, notification, and Homebrew path was skipped.
Do not remove old credentials or change runner provisioning until that signing-test evidence is retained. Secret creation, key access, and clean-machine verification are operational steps and cannot be proven by the repository's static tests.
Testing¶
Running Tests¶
# Run all tests
cargo test
# Run with output
cargo test -- --nocapture
# Run specific test
cargo test test_health_check
# Run tests in single thread (for debugging)
cargo test -- --test-threads=1
# Run integration tests only
cargo test --test '*'
# Run unit tests only
cargo test --lib
# Run doc tests
cargo test --doc
Writing Tests¶
Unit Test Example¶
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backend_selection() {
let backends = vec![
Backend::new("backend1", "http://localhost:8000"),
Backend::new("backend2", "http://localhost:8001"),
];
let selector = RoundRobinSelector::new(backends);
let selected = selector.select().unwrap();
assert_eq!(selected.name(), "backend1");
}
#[tokio::test]
async fn test_async_health_check() {
let backend = Backend::new("test", "http://localhost:8000");
let result = backend.health_check().await;
assert!(result.is_ok());
}
}
Integration Test Example¶
Integration test structure and CI discovery rules:
- Target discovery and domain groups: Cargo discovers integration test targets as top-level
tests/*.rsfiles and astests/<group>/main.rs. Most integration tests live in a domain group (such astests/streaming/ortests/guardrails/) whosemain.rsdeclares each member withpub mod <stem>;so the group links once; see "Where a new test file may live" inCLAUDE.md.- Run one member with
cargo test --test <group> <stem>::(substring match) or a single exact test withcargo test --test <group> -- --exact <stem>::<test>. - Files that CI selects by name or that read process-global state remain top-level
tests/*.rstargets.
- Run one member with
- Shared state isolation: State keyed by a test-chosen ID (an upstream response, file, or conversation ID) is shared across all members of a group binary unless the store is scoped to its own
AppState(as the Responses session store is). Prefix test-chosen IDs with the file name to prevent collisions. - Unreferenced file audit: A
.rsfile undertests/that no target declares is never compiled;tests/cargo_test_discovery_audit_test.rsfails the build if an undeclared file exists. - Feature-gated test audit: Tests gated on a non-default feature (
#![cfg(feature = ...)]files, gated#[test]functions, or gated inlinemodblocks) compile to nothing incargo test --testsand run in the all-features batch step (cargo test --all-features --no-fail-fast --lib --tests) of both.github/workflows/ci.ymlandscripts/local-ci.sh:- The discovery audit fails if neither that batch step nor an explicit
cargo test --features <F> --test <target>line covers a gate in both files. - In
ci.yml, the batch command must be the entire single-linerun:of a step with onlynameandrun(nocontinue-on-error, andif:matching the pinned condition); inscripts/local-ci.sh, it must be a singlestep "<name>" cargo test ...line with a unique name printed byscripts/local-ci.sh --list. Do not wrap, split, condition, or filter that line.
- The discovery audit fails if neither that batch step nor an explicit
- PR change detection: On a pull request, the hosted gate schedules
ci-release-graphfor a changed test target only when it contains a feature-gated test; if the scanner cannot decide (such as afeaturename passed via macro argument, an unreadable#[path]/include!module, or many unterminated quotes), it runs the full graph.
// tests/chat_test.rs
use continuum_router::test_utils::TestServer;
#[tokio::test]
async fn test_chat_completion() {
let server = TestServer::new().await;
let response = server
.post("/v1/chat/completions")
.json(&json!({
"model": "gpt-5.6-luna",
"messages": [{"role": "user", "content": "Hello"}]
}))
.send()
.await;
assert_eq!(response.status(), 200);
}
Test Coverage¶
# Generate coverage report
cargo tarpaulin --out Html
# Generate coverage with specific threshold
cargo tarpaulin --fail-under 80
# Generate Codecov report
cargo tarpaulin --out Xml
Smart routing classification accuracy¶
tests/smart_routing_accuracy_test.rs measures how often the rule-based classifier's classify_only output (src/services/smart_routing/router.rs) matches a labelled dataset at tests/data/smart_routing/dataset.json, and reports a confusion matrix per classifier mode. Run the full report:
The same command is the CI gate: accuracy_meets_the_regression_gate fails the build when either mode's domain or complexity accuracy drops below the threshold recorded next to DOMAIN_ACCURACY_GATE and COMPLEXITY_ACCURACY_GATE in the test file. On a failing run the report prints automatically, --nocapture is only needed to see it on a passing run.
Two rule-classifier deployment modes are measured (LLM and hybrid modes are out of scope, since they require a live or mocked backend round-trip rather than a fast, deterministic check): rule_default (no primary_language configured) and rule_primary_ko (primary_language: ko, a Korean-deployment classifier). These are the same two configurations tests/routing/classifier_language_parity_test.rs exercises, and they differ only in how a Korean request with no domain-intent signal reads: multilingual under rule_default, general under rule_primary_ko.
A third mode, system_one, replays the same cases through classifier.method: system_one against the System One fixture server in tests/systemone_support/:
- Fidelity check (100% gate): The fixture is scripted from the
rule_primary_kolabels (where a Korean greeting isgeneral, matching the battery'smultilingualdefinition of "translation or explicitly multilingual tasks") and keyed by the exact state the classifier sends for each case. Because this mode tests the classifier's fidelity to the decision model's answers across every case (including the Korean half) rather than the model's own accuracy, its gate is 100% on both axes: any miss means a score was rounded to the wrong level, a domain option was mapped to the wrong tag, or the state drifted from the scripted fixture. - Live model calibration: The decision model's own accuracy is measured by
make calibrate-systemone, which replays the same corpora through each consumer's state builder and question battery against a live/v1/systemoneendpoint and derives thresholds from the result; see Decision Models.
Reading the report¶
Each mode prints overall and per-language accuracy, a domain confusion matrix, an overall complexity confusion matrix, a complexity confusion matrix per language, and a line for every case whose complexity band was wrong. An aggregate matrix cannot tell you which half of the corpus a confusion cell comes from, and the two languages do not fail the same way.
A miss line reads id lang expected -> actual (direction) [signals]. The bracketed list is every signal that fed determine_complexity, including the three domain signals it reads directly (code_blocks, math_notation, tool_definitions), each rendered as name=strength. Use it to trace a wrong band to the signals that produced it rather than guessing: two cases printing the same signal vector cannot be separated by any weight or boundary change, which is how the ceiling documented on COMPLEXITY_ACCURACY_GATE was established.
When you change a weight or a bucket boundary in determine_complexity, justify it by a confusion cell that moves, not by an individual fixture that starts passing. With 82 cases a single cell can hold one or two of them, and a change tuned on that is indistinguishable from overfitting.
Adding a labelled case¶
Each entry in dataset.json looks like:
{
"id": "code-fence-en",
"language": "en",
"text": "Please implement a function to reverse a string:\n```python\ndef reverse_string(s):\n pass\n```",
"expected": {
"rule_default": { "domain": "code", "complexity": "moderate" },
"rule_primary_ko": { "domain": "code", "complexity": "moderate" }
},
"note": "optional: why this label, or what gap the case is designed to surface"
}
idis unique and stable; it is what a future report references when a case's outcome changes.languageis"en"or"ko", the language oftext(the classified turn) even whencontextis in another language; the report breaks accuracy down by this field.contextis optional: the earlier messages of a multi-turn conversation, as chat message objects ({"role": "user", "content": "..."},{"role": "assistant", "content": "..."}), placed in order beforetext. Omit it for a single-turn case. Use it when the behavior under test depends on history, such as the proof floor reading only the latest user turn;reasoning-proof-explain-ko-after-enis an example. Thesystem_onemode builds its fixture state from the same payload, so a case withcontextneeds no extra wiring.textis the latest user message, the request being classified. Use a fenced code block (```) to exercise the code-domain signal, LaTeX delimiters ($...$,\frac,\sum) for the math-notation signal, and Korean or English keywords from the tables insrc/services/smart_routing/classifier_language.rsto exercise a specific intent signal.expectedmust have one entry per mode exercised by the runner (currentlyrule_defaultandrule_primary_ko); a case missing either key fails the test with a clear panic rather than being silently skipped.- Label by what a human reviewer reading the
DomainTag/ComplexityLeveldoc comments insrc/services/smart_routing/types.rswould call the request, not by what the classifier currently outputs:- Copying the classifier's own output makes accuracy trivially 100% and renders the regression gate meaningless.
- When the classifier's output differs from your label, either document the genuine limitation in
note(seecreative-poem-koandcode-unmarked-koin the dataset) or re-check your label rather than silently matching the output.
- A case's
noteis printed under its miss line. Anyexpert-labelled case that misses must carry anote, or the gate fails naming that case: because the rule classifier cannot reachexpertfor every request the taxonomy calls expert (see the ceiling onCOMPLEXITY_ACCURACY_GATEand the Complexity Levels section of the advanced configuration guide), thenotemust state which part of that ceiling applies. - After editing the dataset, rerun the report command and update the gate constants' doc comments in
tests/smart_routing_accuracy_test.rs(measured percentages, failure margins, and coverage notes) to match the new measurements. Never raise a gate above what you actually measured.
System One fixture server and evaluation corpora¶
tests/systemone_support/ provides the shared test harness for System One decision-model seams:
- Harness components: Includes a wiremock
/v1/systemonefixture driven bytests/systemone_support/answers.yaml, fault injection (429,529,422, timeouts, and malformed bodies), calibration metrics (accuracy, Brier, 10-bin ECE, AUROC, confusion matrix), and a replay runner. - Importing in tests: Load it from a top-level integration test with
mod systemone_support;, or from a domain group member withuse crate::systemone_support;after declaring#[path = "../systemone_support/mod.rs"]once in the group'smain.rs(see "Where a new test file may live" inCLAUDE.md). Wire types are kept test-local sosrc/services/systemone/is tested against an independent fixture definition. - Evaluation corpora: Labelled corpora live under
tests/fixtures/systemone/(guardrail.jsonl,routing.jsonl,tool_calls.jsonl, andbatteries.yaml), with row schemas, sources, licences, and unsafe-intent handling documented intests/fixtures/systemone/README.md.tests/systemone_corpus_test.rsfails the build if a label is invalid for its battery, a licence is missing, or row counts fall below acceptance floors (200 rows per corpus, including 40 Korean and 20 non-Latin-script guardrail rows).tests/systemone/systemone_cookbook_regression_test.rsverifies that the runner and metric calculations reproduce the TypeSafe cookbook's published verdicts fromcookbook_cache.yaml.
For a quick single-corpus check against a real endpoint, replay it with the #[ignore]d runners, which print the JSON report (the full calibration procedure, which replays every consumer and derives thresholds, is make calibrate-systemone; see Decision Models):
SYSTEMONE_ENDPOINT=http://127.0.0.1:8000/v1/systemone \
SYSTEMONE_API_KEY_ENV=LAYA_SERVE_API_KEY \
cargo test --test systemone_corpus_test -- --ignored run_routing --nocapture
SYSTEMONE_API_KEY_ENV names the variable holding the key; SYSTEMONE_MODEL overrides the requested model (default jev-latest). deploy/docker-compose.systemone.yml brings up a self-hosted laya-serve on loopback for a local run.
Vendored continuum-protocol crate¶
crates/continuum-protocol/src and tests are a byte-identical copy of a single Continuum Hub revision, recorded on the provenance line in crates/continuum-protocol/Cargo.toml (# Vendored verbatim from lablup/continuum-hub crates/continuum-protocol at rev <sha>.):
- Manifest and drift check:
crates/continuum-protocol/UPSTREAM.sha256records the SHA-256 of every Hub file undersrc/andtests/at that revision (plus the digest of the Hub's crateCargo.toml).scripts/check-vendored-protocol.shfails (naming each offending path) when the manifest revision differs from the provenance line, when any file is modified, missing, or added, or when aLOCAL-AHEADentry is malformed or stale. - Build-input confinement: Because only
src/andtests/are compared, the script also rejects any extra crate-root entry (such asbuild.rs), any crateCargo.tomlsection outside the allowlist ([package],[features], and registry dependency tables on default targets), anycargo metadatatarget outsidesrc/andtests/, and any routerCargo.tomlthat resolvescontinuum-protocolfrom anywhere other thanpath = "crates/continuum-protocol". - Execution: Requires no network or Hub credentials, runs first in the release-graph job of CI and
make ci-local, and is self-tested across all failure modes byscripts/test-check-vendored-protocol.sh.
# Offline check
bash scripts/check-vendored-protocol.sh
# What CI and make ci-local run: also fail a manifest edit that kept its rev line
bash scripts/check-vendored-protocol.sh --base "$(git merge-base origin/main HEAD)"
# Also regenerate the manifest from a hub clone and compare it with the committed one
CONTINUUM_HUB_DIR=../continuum-hub bash scripts/check-vendored-protocol.sh
# Re-vendor from a hub revision: replaces src/ and tests/, rewrites UPSTREAM.sha256
bash scripts/refresh-vendored-protocol-manifest.sh --hub ../continuum-hub --rev <sha> --sync
--base exists because the natural reaction to "digest differs" is to update the digest: a manifest whose hashes changed while its # rev: line did not can only be a vendored file edited together with its manifest line. Export CONTINUUM_HUB_DIR for make ci-local whenever a hub clone is at hand; that run is the merge gate.
The refresh script never edits Cargo.toml, and in a git work tree it refuses to --sync over uncommitted or untracked changes under src/ or tests/ unless given --force. After a re-vendor, put the provenance line it prints into crates/continuum-protocol/Cargo.toml, carry over any dependency change in git -C ../continuum-hub show <sha>:crates/continuum-protocol/Cargo.toml (the script notes when that file changed), and run the check.
To change a vendored file ahead of the hub, file the hub issue first, then add <path> lablup/continuum-hub#<N> <one-line reason> to crates/continuum-protocol/LOCAL-AHEAD in the same PR as the change, and leave UPSTREAM.sha256 alone. Entries are file paths under src/ or tests/, never directories or globs. The check fails on an entry whose file already matches the pinned revision, so drop it when a re-vendor picks up the hub change.
Code Quality¶
Formatting¶
# Format all code
cargo fmt
# Check formatting without changes
cargo fmt -- --check
# Format specific file
cargo fmt -- src/main.rs
Linting¶
# Run clippy with warnings as errors
cargo clippy -- -D warnings
# Run clippy with pedantic lints
cargo clippy -- -W clippy::pedantic
# Run clippy with auto-fix
cargo clippy --fix
Security Audit¶
# Check for known vulnerabilities
cargo audit
# Fix vulnerable dependencies
cargo audit fix
# Generate security report
cargo audit --json > audit-report.json
Documentation¶
# Build documentation
cargo doc
# Build and open documentation
cargo doc --open
# Build documentation with private items
cargo doc --document-private-items
# Check documentation examples
cargo test --doc
Debugging¶
Debug Logging¶
# Enable debug logging
RUST_LOG=debug cargo run
# Enable trace logging for specific module
RUST_LOG=continuum_router::services=trace cargo run
# Enable structured logging
RUST_LOG=info RUST_LOG_FORMAT=json cargo run
Using Debugger¶
VS Code¶
- Create
.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug Router",
"cargo": {
"args": ["build", "--bin=continuum-router"],
"filter": {
"name": "continuum-router",
"kind": "bin"
}
},
"args": ["--config", "dev-config.yaml"],
"cwd": "${workspaceFolder}"
}
]
}
- Set breakpoints and press F5 to debug
Command Line (lldb)¶
# Build with debug symbols
cargo build
# Run with lldb
lldb target/debug/continuum-router
# Set breakpoint
(lldb) b src/main.rs:50
# Run program
(lldb) r --config config.yaml
Memory Debugging¶
# Use valgrind (Linux)
valgrind --leak-check=full ./target/debug/continuum-router
# Use Address Sanitizer
RUSTFLAGS="-Z sanitizer=address" cargo build -Z build-std --target x86_64-unknown-linux-gnu
./target/x86_64-unknown-linux-gnu/debug/continuum-router
Performance Profiling¶
CPU Profiling¶
# Generate flame graph
cargo flamegraph -- --config config.yaml
# Profile specific scenario
cargo flamegraph --bin continuum-router -- --benchmark
# Use perf (Linux)
perf record -g ./target/release/continuum-router
perf report
Benchmarking¶
# Run benchmarks
cargo bench
# Run specific benchmark
cargo bench routing
# Compare benchmark results
cargo bench -- --save-baseline main
git checkout feature-branch
cargo bench -- --baseline main
Writing Benchmarks¶
// benches/routing_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn benchmark_routing(c: &mut Criterion) {
c.bench_function("round_robin_selection", |b| {
let selector = RoundRobinSelector::new(backends);
b.iter(|| {
selector.select(black_box(&request))
});
});
}
criterion_group!(benches, benchmark_routing);
criterion_main!(benches);
Memory Profiling¶
# Use heaptrack (Linux)
heaptrack ./target/release/continuum-router
heaptrack_gui heaptrack.continuum-router.*.gz
# Use Instruments (macOS)
cargo instruments -t Allocations
Contributing¶
Development Workflow¶
-
Fork and Clone
-
Create Feature Branch
-
Make Changes
- Write code following Rust conventions
- Add tests for new functionality
-
Update documentation
-
Test Your Changes
-
Commit Changes
-
Push and Create PR
Code Standards¶
- Follow Rust naming conventions (snake_case for functions, CamelCase for types)
- Write comprehensive tests (aim for >90% coverage)
- Document public APIs with doc comments
- Use
cargo fmtfor consistent formatting - Address all
cargo clippywarnings - Keep functions small and focused
- Prefer composition over inheritance
- Use appropriate error handling (Result/Option)
Commit Message Format¶
Follow the conventional commits specification:
Types: - feat: New feature - fix: Bug fix - docs: Documentation only - style: Code style changes - refactor: Code refactoring - perf: Performance improvements - test: Adding tests - chore: Maintenance tasks
Pull Request Guidelines¶
- Title: Clear and descriptive
- Description: Explain what, why, and how
- Tests: Include tests for new features
- Documentation: Update relevant docs
- Breaking Changes: Clearly marked
- Issues: Reference related issues
Review Process¶
- Automated checks must pass
- At least one maintainer approval
- No unresolved conversations
- Up-to-date with main branch
Troubleshooting¶
Common Issues¶
Compilation Errors¶
# Clear build cache
cargo clean
# Update dependencies
cargo update
# Check for conflicting features
cargo tree --duplicates
Test Failures¶
# Run tests verbosely
cargo test -- --nocapture --test-threads=1
# Check for race conditions
cargo test -- --test-threads=1
# Verify test environment
cargo test -- --ignored
Performance Issues¶
# Profile release build
cargo build --release
perf record ./target/release/continuum-router
perf report
# Check for blocking operations
RUST_LOG=trace cargo run 2>&1 | grep "blocking"