# Testing & Reliability **Project:** Actual MCP Server **Purpose:** Define testing philosophy, frameworks, and enforcement policies **Last Updated:** 2026-06-07 --- ## ๐ŸŽฏ Testing Philosophy ### Core Principles 1. **Test Before Commit**: No code is committed without passing tests 2. **Test Pyramid**: Unit tests (most) โ†’ Integration tests โ†’ E2E tests (least) 3. **Fail Fast**: Tests catch issues early in development 4. **Continuous Testing**: CI/CD runs full test suite on every push 5. **Real-World Scenarios**: Tests reflect actual usage patterns ### Testing Goals - **Prevent Regressions**: Catch breaking changes before they reach production - **Document Behavior**: Tests serve as living documentation - **Enable Refactoring**: High coverage enables confident code changes - **Ensure Reliability**: Production code works as expected --- ## ๐Ÿงช Testing Frameworks & Tools ### Testing Stack | Tool | Version | Purpose | |------|---------|---------| | **Node.js Built-in** | Native | Unit test runner | | **TypeScript** | ^6.0.3 | Type checking (compile-time testing) | | **Playwright** | ^1.64.1 | End-to-end testing | | **Custom Adapter Tests** | N/A | Smoke tests for Actual API | | **npm audit** | Native | Security vulnerability scanning | ### Test Categories ``` tests/ โ”œโ”€โ”€ unit/ # Unit tests (fast, isolated, offline) โ”‚ โ”œโ”€โ”€ transactions_create.test.js # Zod schema validation (transactions_create) โ”‚ โ”œโ”€โ”€ generated_tools.smoke.test.js # All 84 tools: stub adapter + correctness assertions โ”‚ โ”œโ”€โ”€ schema_validation.test.js # Negative-path schema tests (11+ tool schemas) โ”‚ โ”œโ”€โ”€ schema_json_openai_compat.test.js # Published schemas OpenAI/ECMA-262 regex-compatible (#293) โ”‚ โ””โ”€โ”€ schema_published_parity.test.js # Zod and the SDK Ajv validator accept the same inputs; no regex flags (#588) โ”œโ”€โ”€ e2e/ # End-to-end tests โ”‚ โ”œโ”€โ”€ mcp-client.playwright.spec.ts # Protocol tests (fast, no Docker) โ”‚ โ”œโ”€โ”€ docker.e2e.spec.ts # Docker smoke integration (full stack) โ”‚ โ”œโ”€โ”€ docker-all-tools.e2e.spec.ts # All-tools Docker E2E (84 tools) โ”‚ โ””โ”€โ”€ run-docker-e2e.sh # Docker test orchestrator โ””โ”€โ”€ manual/ # Live integration tests (real Actual Budget) โ”œโ”€โ”€ index.js # Entry point, level-gated execution โ”œโ”€โ”€ cleanup.js # Standalone MCP-* data cleanup โ””โ”€โ”€ tests/ # Per-domain test modules (14 files) ``` **Docker-based E2E Tests**: Full stack integration testing with real Actual Budget server in Docker. --- ## ๐Ÿƒ Running Tests ### Quick Start ```bash # Run all tests (recommended) npm run test:all # Run protocol tests (fast, ~10s) npm run test:e2e # Run Docker integration tests (thorough, ~60s) npm run test:e2e:docker ``` ### Test Types | Command | Type | Speed | Scope | When to Use | |---------|------|-------|-------|-------------| | `npm run test:adapter` | Smoke | โšก 30s | Adapter layer | Pre-commit | | `npm run test:unit-js` | Unit | โšก 5s | Single unit | Development | | `npm run test:e2e` | Protocol | โšก 10s | MCP protocol | Pre-commit | | `npm run test:e2e:docker` | Integration | ๐Ÿข 60s | Full stack | Pre-merge | | `npm run test:all` | All | ๐Ÿข 90s | Everything | Before release | ### Pre-Commit Tests (Essential) ```bash # Essential tests before commit npm run build # TypeScript compilation & type checking npm run test:adapter # Adapter smoke tests (30s) npm run test:unit-js # Unit + schema tests (3s) npm audit --audit-level=moderate # Security check ``` ### Full Test Suite ```bash # Complete test suite npm run test:all # Runs: adapter + unit + Docker E2E (smoke) # Or run individually: npm run build # Build TypeScript npm run test:adapter # Adapter tests npm run test:unit-js # Unit tests: schema, smoke, negative-path npm run test:e2e # Protocol tests (fast, no Docker) npm run test:e2e:docker # Docker integration smoke (thorough) npm audit # Security audit ``` ### Docker E2E Tests **Full stack integration testing with real Actual Budget server:** ```bash # Run Docker-based E2E tests (quick smoke tests) npm run test:e2e:docker # Run comprehensive ALL TOOLS test (50+ tests) npx playwright test tests/e2e/docker-all-tools.e2e.spec.ts # Advanced options ./tests/e2e/run-docker-e2e.sh --no-cleanup # Leave containers for debugging ./tests/e2e/run-docker-e2e.sh --verbose # Detailed output ./tests/e2e/run-docker-e2e.sh --build-only # Just build, don't test ``` **What Docker E2E tests verify:** - โœ… Docker build works correctly - โœ… Container networking (MCP โ†” Actual Budget) - โœ… Real tool execution (**all 84 tools at 100% coverage**) - โœ… Session management and persistence - โœ… Production-like deployment - โœ… Error handling and validation (15+ error scenarios) - โœ… Regression tests (strict validation, batch operations) **Test Suites:** - **docker.e2e.spec.ts**: Basic smoke tests (11 tests) - **docker-all-tools.e2e.spec.ts**: Comprehensive all-tools test (84 tools, 80+ test cases) ### Multi-user ACL E2E (`scripts/acl-e2e.sh`) Not part of `test:e2e:docker`. It stands up its own stack (an Actual server in multi-user OpenID mode plus `navikt/mock-oauth2-server`) because the budget ACL is the one behaviour whose correctness depends on an identifier produced by ANOTHER system. ```bash bash scripts/acl-e2e.sh # build the environment + fixture, then verify bash scripts/acl-e2e.sh --keep # leave the containers up for debugging node scripts/acl-e2e-verify.mjs # re-run the scenarios against a kept environment ``` **Why it cannot be a Playwright suite.** It needs a password bootstrap FIRST and OpenID enabled second (`@actual-app/api` authenticates with a password, and an OpenID-only server rejects it), an IdP that is up before Actual reads discovery at startup, and an IdP reconfiguration after the users exist. Compose cannot express that ordering. **The property it exists to protect (#344).** Alice and bob are created by driving a REAL authorization-code login, so Actual runs its own precedence over the UserInfo response and stores the result. The harness then READS BACK the `user_name` Actual derived and writes it to `.release/acl-e2e-fixture.json` as `actualUserNames`; every assertion uses that value. It never asserts what the name should be. The login supplies `preferred_username` and `email` as deliberately different strings, so the stored value also reveals which claim the precedence chose. This matters because the harness previously created users with `POST /admin/users` passing `userName: "alice"` and then configured the IdP to mint `preferred_username: "alice"`: both sides of a cross-system join chosen twelve lines apart in one file, which is internally consistent by construction and cannot fail for the reason that matters. That shape hid #343 for two releases. **What it covers for #346.** The mock IdP serves a genuine `/userinfo`, and it is the same endpoint Actual itself calls to derive `user_name`, so the UserInfo identity source is exercised end to end against a live IdP: a real token, a real discovery document, a real response. That block deliberately passes NO token claims, so an implementation that quietly read the access token instead would resolve to null and fail. What it does NOT cover is the divergence case (a claim present in UserInfo but absent from the access token), because mock-oauth2-server derives the UserInfo body from the token's own claims and the two cannot disagree. That case needs the shim described in #346, so this proves the mechanism rather than the motivating bug. **Proving it is still not vacuous.** After a green run, rename the user inside Actual so its `user_name` no longer matches the token identity, and re-run the verifier. It must go red (3 assertions, verified 2026-08-10). The exact commands are in the header of `scripts/acl-e2e.sh`. `tests/unit/acl_e2e_fixture_contract.test.js` guards the same properties statically in CI, since a vacuous harness passes and so running it proves nothing on its own. ### Individual Test Commands ```bash # TypeScript compilation (includes type checking) npm run build # Adapter smoke tests npm run test:adapter # Unit tests npm run test:unit-js # Protocol E2E tests (no Docker required) npm run test:e2e # Docker integration tests (requires Docker) npm run test:e2e:docker # Test Actual connection npm run dev -- --test-actual-connection # Test MCP client interaction (needs a live Actual server past initialize, and a free MCP_BRIDGE_PORT) npm run test:mcp-client ``` --- ## โœ… Testing Policy ### Mandatory Testing Policy > โš ๏ธ **CRITICAL**: No code may be committed or pushed until **all** local tests pass. ### Pre-Commit Checklist Before running `git commit`: - [ ] `npm run build`: โœ… No TypeScript errors - [ ] `npm run test:adapter`: โœ… All adapter tests pass - [ ] `npm run test:unit-js`: โœ… All unit + schema tests pass (~52 files) - [ ] `npm audit --audit-level=moderate`: โœ… No moderate/high/critical vulnerabilities ### CI/CD Enforcement GitHub Actions automatically runs: - TypeScript compilation - All test suites - Security audit - Docker build test - Tool coverage verification - Documentation checks (`Docs check` job, #602): `npm run docs:check` with lychee v0.24.2 installed, then the negative link fixture with `DOCS_CHECK_REQUIRE_LYCHEE=1` **If CI/CD fails:** 1. โŒ Pull request cannot be merged 2. โŒ No Docker images published 3. โŒ No GitHub releases created ### Release gate: dual-transport evidence (#280) **A promotion to `main` REQUIRES a passing full integration run over BOTH transports (`MCP_TEST_TRANSPORT=http` and `MCP_TEST_TRANSPORT=stdio`) with a green zero-residue assertion.** Run `bash scripts/deploy-and-test.sh full`; it writes `.release/dual-transport-report.json`, and the release procedure verifies that dual-transport evidence against the `develop` HEAD sha before anything is fast-forwarded. HTTP-only evidence is not sufficient: stdio is the transport half our Claude Desktop users run on, and it had no write-path coverage until #280. This paragraph is load bearing: `tests/unit/dual_transport_gate.test.js` anchors on its first sentence and on the phrase "dual-transport evidence" plus the artifact path, and fails the build if either is weakened or removed. It lives here, in a tracked document, because the assistant instruction files that used to carry it (`CLAUDE.md`, `.claude/skills/release/SKILL.md`) are kept out of the repository by the leak guard and so are absent on a fresh checkout; the test still checks those copies wherever they are present locally. **Step 9 cleans up after itself (#576).** `scripts/regression-270-stall.mjs` (and the `scripts/diag-270-http.mjs` diagnostic) run AFTER the runner's zero-residue assertion, so whatever they create was invisible to it. Each run names its objects `MCP-REG270-