Estratégia De Testes
Este monorepo usa uma arquitetura de testes orientada a workspaces em apps/*/tests. O código de produção permanece em src/, e os testes são agrupados por intenção como unit ou integration.
Estrutura De Diretórios
apps/
api/
tests/
unit/
integration/
support/
harness/ # create-api-test-app.ts
mocks/ # colaboradores fake
frontend/
tests/
unit/
integration/
support/
setup/ # vitest.setup.ts
harness/ # render.tsx
mocks/ # create-fetch-scenario.ts (apenas hooks em jsdom)
shared/
tests/
unit/
support/ é reservado para helpers que removem duplicação real dentro de um workspace. Só crie subpastas que sejam usadas imediatamente.
Taxonomia De Testes
unit: isola um hook, componente, serviço, módulo de rota ou utilitário.integration: cobre um fluxo que atravessa fronteiras de módulos dentro do mesmo workspace.browser-smoke: suíte mínima em Playwright Chromium cobrindo o fluxo end-to-end de auth.
Runners Por Workspace
| Workspace | Runner | Ambiente |
|---|---|---|
apps/api |
bun test |
Bun native |
apps/frontend |
bun:test + vitest |
Bun native para lógica pura, jsdom para React/Vite |
apps/shared |
bun:test |
Bun native |
Scripts Da Raiz
bun run check # format + lint + typecheck em todos os workspaces
bun run test # unit + integration (e2e é opt-in)
bun run test --unit # suítes unit da API, shared e frontend
bun run test --integration # suítes de integração da API e frontend
bun run test:e2e:setup # instala Playwright Chromium + dependências do SO
bun run test --e2e # auth smoke suite em Playwright Chromium (opt-in)
bun run test --coverage # coleta de cobertura nos workspaces aplicáveis
bun run test --all # todas as lanes, incluindo e2e
bun run verify # gate CI local completo: check → test --coverage → build --all
Taxonomia de lanes
| Lane | Nome da task | Workspaces | Runner | Cache Turbo |
|---|---|---|---|---|
| unit | test:unit |
api, frontend, shared | bun test / vitest | sim |
| integration | test:integration |
api, frontend | bun test / vitest | não |
| coverage | test:coverage |
api, frontend, shared | bun test / vitest | não |
| e2e | — | root (browser-smoke) | Playwright Chromium | — |
| scripts | //#test:scripts |
root | bun test | sim |
Browser Smoke
Suíte Playwright Chromium em tests/browser-smoke/. Cobre o fluxo completo de auth contra uma stack real rodando com API em :3001 e frontend em :5173.
bun run test:e2e:setup
bun run test --e2e
Rode bun run test:e2e:setup uma vez em uma máquina nova ou quando o Playwright avisar que o binário do Chromium está ausente. O script encapsula bunx playwright install --with-deps chromium, então instala apenas o navegador usado por esta suíte e evita comandos npx.
O job de browser-smoke em CI roda em ubuntu-24.04 porque o Playwright 1.60 ainda não consegue instalar dependências do Chromium no Ubuntu 26.04. Se você usa Ubuntu 26.04 localmente, rode a lane e2e em um container ou VM Ubuntu 24.04/22.04 até o suporte upstream chegar.
playwright.config.ts na raiz do repositório inicia ambos os servidores via webServer antes de rodar os testes. Em CI, força workers: 1 e faz upload de traces e screenshots em caso de falha.
Cenários em tests/browser-smoke/auth-flow.spec.ts:
- A página
/loginrenderiza - A página
/signuprenderiza - Signup com email aleatório único → entra na área autenticada
- Logout → redireciona para login
- Login novamente com as mesmas credenciais → retorna à área autenticada
| Lane | Runner | Ambiente |
|---|---|---|
browser-smoke |
playwright (Chromium) |
browser real + stack |
Scripts Por Workspace
API
bun run --filter @mangostudio/api test:unit
bun run --filter @mangostudio/api test:integration
O suporte da API vive em apps/api/tests/support/:
harness/create-api-test-app.ts— envolve plugins de rota em uma app Elysia mínima para testes viaapp.handle()mocks/— colaboradores fake, como secret stores
Frontend
bun run --filter @mangostudio/frontend test:unit
bun run --filter @mangostudio/frontend test:integration
bun run --filter @mangostudio/frontend test:coverage
O suporte do frontend vive em apps/frontend/tests/support/:
setup/vitest.setup.ts— bootstrap de runtime apenasharness/render.tsx— superfície mínima de render com providersmocks/create-fetch-scenario.ts— registry de fetch por método/path apenas para testes de hooks React
Shared
bun run --filter @mangostudio/shared test:unit
shared mantém utilitários de teste de runtime em src/test-utils/, mas os testes do workspace vivem em apps/shared/tests/unit/.
Escrevendo Testes
Integração da API — com validação de schema Typebox
import { describe, expect, it } from 'bun:test';
import { Value } from '@sinclair/typebox/value';
import { Type } from '@sinclair/typebox';
import { settingsRoutes } from '../../../src/routes/settings';
import { createApiTestApp } from '../../support/harness/create-api-test-app';
// O plugin de rota usa .group('/settings', ...) — sem prefixo /api nos testes
const app = createApiTestApp(settingsRoutes);
const ResponseSchema = Type.Object({
configured: Type.Boolean(),
status: Type.Union([Type.Literal('idle'), Type.Literal('ready'), Type.Literal('error')]),
allModels: Type.Array(Type.Any()),
});
describe('settingsRoutes', () => {
it('validates response shape with Typebox', async () => {
const response = await app.handle(new Request('http://localhost/settings/models/gemini'));
expect(response.status).toBe(200);
const payload = await response.json();
expect(Value.Check(ResponseSchema, payload)).toBe(true);
});
});
Importante: plugins de rota usam
.group('/path', ...)sem o prefixo/api. Esse prefixo é adicionado emapp.tspornew Elysia({ prefix: '/api' }). URLs de teste devem usar apenas o group path do plugin.
Exemplo Unitário Da API
import { describe, expect, it } from 'bun:test';
import { createGeminiSecretService } from '../../../src/services/gemini-secret';
import { InMemorySecretStore } from '../../support/mocks/mock-secret-store';
describe('createGeminiSecretService', () => {
it('returns environment fallback when no stored key exists', async () => {
const service = createGeminiSecretService({
secretStore: new InMemorySecretStore(),
getEnvironmentKey: () => 'env-key-5678',
});
const status = await service.getGeminiSecretStatus();
expect(status.source).toBe('environment');
});
});
Integração No Frontend — testes de hooks React com fetch mock
create-fetch-scenario.ts é limitado a testes de hooks React em jsdom. Não use isso para testes de contrato da API.
import { render, screen } from '../../support/harness/render';
import { createFetchScenario } from '../../support/mocks/create-fetch-scenario';
const fetchScenario = createFetchScenario();
fetchScenario.install().respondWithJson('GET', '/api/settings/secrets/gemini', {
body: { configured: false, source: 'none' },
});
render(<SettingsPage {...props} />);
await screen.findByText('Not Configured');
fetchScenario.restore();
Regras De Suporte
- Não adicione subpastas
supportvazias por simetria. - Mantenha helpers locais ao arquivo de teste, a menos que removam duplicação real entre múltiplos arquivos.
- Prefira um harness explícito em vez de múltiplas camadas de abstração.
- Mantenha mocks focados em seams reais de request ou dependência.
- Para validação de contratos da API, use
Value.Checkcom schema TypeBox inline.
Testes De Módulo (API)
Módulos de domínio da API colocam testes no diretório de workspace tests/:
apps/api/tests/
unit/modules/<module-name>/ # testes unitários para serviços de aplicação
integration/modules/<module-name>/ # testes de integração usando createApiTestApp
Testes de integração de módulo usam createApiTestApp com o plugin HTTP do módulo. URLs de teste devem usar o group path do plugin, sem o prefixo /api.
Matriz De Testes De Continuação / Provedores
Consulte ../architecture/continuation.md e ../providers/development.md para a arquitetura e o guia de desenvolvimento. A matriz cobre três camadas:
Motor de decisão (continuation.test.ts)
Testes de função pura em apps/api/tests/unit/services/providers/continuation.test.ts:
| Teste | O que valida |
|---|---|
| Parse/serialização do envelope | Identidade de round-trip, null/undefined/inválido, versão do schema e requisitos de cursor |
| Validação do envelope | Detecção de mismatch entre provedor, modelo, system prompt e toolset |
decideContinuation |
Decisões continue_with_cursor, degrade_to_replay e start_replay |
decideTurnPersistence |
Persistência de cursor durável e filtro de stateless-loop |
| Troca de provedor | OpenAI→Gemini degrada no primeiro turno e usa cursor Gemini no segundo |
Replay builders (replay-builder.test.ts)
Testes em apps/api/tests/unit/services/providers/replay-builder.test.ts:
- formato de replay de cada provedor
- conteúdo apenas de texto, apenas tool call ou misto
- histórico vazio e compatibilidade retroativa com texto simples
Tratamento específico de perda de cursor
Cada teste de stream por provedor deve cobrir:
- primeiro turno sem cursor → replay completo
- continuação por cursor → input mínimo
- perda de cursor sem tool results → retry com replay
- perda de cursor com tool results → abort com
tool_result_cursor_loss
Cobertura
Os relatórios de cobertura são escritos em .mango/artifacts/coverage/. O Vitest do frontend
escreve o relatório React/Vite em .mango/artifacts/coverage/frontend/vitest/, e bun:test
escreve LCOV da lógica pura em .mango/artifacts/coverage/frontend/bun/:
bun run --filter @mangostudio/frontend test:coverage
Checklist De Verificação
Antes de fazer merge, rode:
bun run check
bun run test
# ou use o atalho do gate completo de CI:
bun run verify
bun run verify corresponde ao pipeline de CI menos os jobs de smoke (browser e
binary), que exigem runners de plataforma nem sempre disponíveis localmente.
Rode-os separadamente com bun run test --e2e e bun scripts/test-build.ts.