Testing the apps
Every app under apps/ ships with a test suite that runs via pnpm test and is enforced by the pre-push hook. This runbook explains the layers so you know where to add tests, and how to debug failures.
What you get for free
A single pnpm -r test from the repo root runs every workspace’s tests in parallel:
| Workspace | Stack | Runner | What’s tested |
|---|---|---|---|
apps/api | NestJS / Fastify | jest (test:unit) + jest e2e (test:e2e) | HealthController unit + real HTTP integration via supertest |
apps/ai-worker | Python / FastAPI | pytest with TestClient | /healthz shape, 501 stubs for tier1/tier2, 404 handling |
apps/edge-app | Python / FastAPI | pytest with TestClient | /healthz shape, 404 handling |
apps/edge-supervisor | Go / net/http | go test | handleHealth + envDefault + loggingMiddleware |
apps/dashboard | React + Vite | vitest + Testing Library | App renders, brand + tagline, footer with version |
apps/fd-view | React + Vite | vitest + Testing Library | Same shape as dashboard |
apps/edge-dashboard | React + Vite | vitest + Testing Library | Same shape as dashboard |
packages/contracts | TS + Python + Go | vitest + pytest + go test | Round-trip + re-serialization stability per language |
The pre-push hook runs pnpm -r test, so a broken test blocks every push. To bypass (rare, last resort), git push --no-verify.
Three layers of testing
1. Unit / component tests (fast — in the default pnpm test)
What runs on every push.
- TypeScript apps use jest (
apps/api) or vitest (apps/dashboard,fd-view,edge-dashboard). - Python apps use pytest with
fastapi.testclient.TestClient— in-process HTTP, no real socket. - Go apps use the standard
testingpackage withhttptest.NewRecorder.
Where they live:
apps/api/src/health/health.controller.spec.ts ← unit (NestJS controller-only)
apps/api/test/health.e2e-spec.ts ← in-process HTTP via supertest
apps/ai-worker/tests/test_main.py ← pytest + TestClient
apps/edge-supervisor/cmd/supervisor/main_test.go ← go test
apps/dashboard/src/App.test.tsx ← vitest + RTL 2. Docker container tests (slow — opt-in via test:docker)
Per app, a test:docker script wraps a shared runner at scripts/test-app-docker.mjs. The runner:
docker build -f apps/<app>/Dockerfile -t <app>:test-<timestamp> .docker run -don a random localhost port- Wait up to 60s for
/healthzto respond 200 - Validate the response JSON has
ok: trueand the expectedservicefield - Always tear down (
docker rm -f) — even on failure
Run for one app:
pnpm -F @wiqaia/api test:docker Run for all apps (~5–10 minutes total, depending on Docker cache state):
pnpm -r test:docker Not in the default pnpm test because each one takes ~30–60s and the pre-push hook would become unbearable. These run in CI when Cloud Build is set up (see Gitea issue “Set up Cloud Build CI infrastructure”).
If a Docker test fails, the runner dumps the container’s logs before tearing down — that’s usually enough to see what went wrong.
3. Cross-language wire-contract tests (in packages/contracts)
These prove that a message encoded in one language decodes identically in the other two. See Working with contracts for details.
Adding a new test
TypeScript / NestJS (apps/api)
Unit test (controller-only, fast):
// apps/api/src/foo/foo.spec.ts
import { Test } from '@nestjs/testing';
import { FooController } from './foo.controller';
describe('FooController', () => {
it('does the thing', async () => {
const moduleRef = await Test.createTestingModule({
controllers: [FooController],
}).compile();
const controller = moduleRef.get(FooController);
expect(controller.bar()).toBe('baz');
});
}); HTTP integration (real Fastify, slower):
// apps/api/test/foo.e2e-spec.ts
import { Test } from '@nestjs/testing';
import { FastifyAdapter } from '@nestjs/platform-fastify';
import request from 'supertest';
import { AppModule } from '../src/app.module';
// boot via Test.createTestingModule, then await app.init().
// Use request(app.getHttpServer()) for assertions. Vitest / React SPAs
// apps/dashboard/src/Foo.test.tsx
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import Foo from './Foo';
describe('Foo', () => {
it('renders', () => {
render(<Foo />);
expect(screen.getByText('expected text')).toBeInTheDocument();
});
}); Python (apps/ai-worker, apps/edge-app)
# apps/<app>/tests/test_foo.py
from fastapi.testclient import TestClient
from wiqaia_<app>.main import app
client = TestClient(app)
def test_foo_endpoint():
res = client.get('/foo')
assert res.status_code == 200
assert res.json()['ok'] is True Go (apps/edge-supervisor)
// apps/edge-supervisor/cmd/supervisor/foo_test.go
func TestFoo(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/foo", nil)
rec := httptest.NewRecorder()
handleFoo(rec, req)
if rec.Code != http.StatusOK { t.Fatal(rec.Code) }
} Common debugging
Vitest can’t find __APP_VERSION__. Define it in vitest.config.ts:
define: { __APP_VERSION__: JSON.stringify('test-build') } pytest can’t import the app’s package. Ensure the test file pre-pends the app’s src/ to sys.path:
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'src')) Go tests reference a helper that doesn’t compile. Test-only helpers should live in a _test.go file so they’re not in the production binary.
Docker integration test fails with container not ready after 60s. The runner dumps logs on failure. Common causes: wrong --container-port flag, the app binds to 127.0.0.1 inside the container instead of 0.0.0.0, or the /healthz route isn’t actually wired.