Skip to content
Select themeSelect language

Run the architecture tests

SupaCloud enforces its layer boundaries and file-size cap with a Rust test suite in server/tests/architecture.rs. These tests run inside the normal cargo test harness — no external tools required. Run them before every push; they are the CI gate that prevents regressions from accumulating.

Terminal window
cd server
cargo test arch

Cargo filters tests by name, so this runs every test whose name contains arch — currently just the single rust_source_architecture_guards_hold function. The file also contains helper unit tests (auth_user_handler_scanner_rejects_handlers_without_scope_guard, cfg_test_module_discovery_rejects_name_only_test_files) and a Docker-context test, but their names do not contain arch, so they run only under a broader filter such as plain cargo test. It scans all .rs source files under server/src/ and all .sql migration files under server/migrations/.

To run the full test suite (which includes the architecture tests):

Terminal window
cargo test

The single entry-point test rust_source_architecture_guards_hold in server/tests/architecture.rs runs fourteen independent guards and collects every violation before failing, so you see all problems in one pass.

The fundamental rule is interface → service → persistence. Four guards enforce it:

Guard What it forbids
Interfaces bypass services src/interfaces/** importing crate::db::queries, db::queries, queries::, sqlx::PgPool, &state.db
Services call interfaces src/services/** importing crate::interfaces or any interface adapter alias (api, stripe, telegram, management, discord)
Runtime/integrations bypass services src/runtime/** and src/integrations/** importing the same persistence patterns
Auth modules bypass services src/auth/** importing the same persistence patterns

Chat adapters (telegram, discord) must communicate through the neutral crate::chat surface. Cross-adapter imports are forbidden in either direction, and src/interfaces/chat must not import either concrete adapter.

Every pub async fn handler in src/interfaces/http that receives an AuthUser extractor must call at least one authorisation or scope guard (e.g. resolve_workspace_id(, require_workspace_admin(, authorize_org(, or any other entry in the guard list). Authenticated handlers that return shared (non-tenant-scoped) metadata and therefore need no further authz/scope guard have named exceptions registered in the test file (the HTTP_AUTHZ_AUTHENTICATED_METADATA_EXCEPTIONS list).

Files under src/services/management must not construct HTTP responses (StatusCode::, IntoResponse, .into_response()). Response mapping belongs in the interface layer.

Every source file under server/src/ must stay below 500 non-blank, non-comment code lines. The test measures the count after stripping trailing // comments and blank lines, so documentation volume does not push a file over the limit.

Additionally, every source file must start with a purpose comment — either // Purpose: or //! Purpose: — and must not use include!( or allow(dead_code).

New migration files (number above 168) must not introduce a team_id tenancy column — use workspace_id — and must not recreate the renamed tables teams, team_members, or team_invites. Existing sanctioned RENAME COLUMN team_id TO workspace_id lines are exempt.

Telegram adapters must use app::i18n (Fluent) rather than inline match statements on the locale. German copy (umlauts, ß) must live in server/locales/ .ftl files, not in Rust source.

When a guard fires, the test output groups violations by rule and prints each as <path>:<line> followed by the matched pattern in backticks. Fix all of them — the test does not stop at the first group.

thread 'rust_source_architecture_guards_hold' FAILED
architecture guards failed:
interfaces must call services/use-cases instead of persistence directly:
src/interfaces/http/example.rs:42 contains `sqlx::PgPool`