002 — Estratégia de testes em tiers (domain / infra / e2e)
Status: Aceita (2026-06-03)
Contexto
Section titled “Contexto”Hoje todo teste é spec de cenário em scenarios/*.spec.ts — in-memory, ms por
spec, vitest watch sempre rodando. Funciona enquanto o projeto é só domínio.
A fase de infraestrutura entra agora: SQLite (drizzle + better-sqlite3), LLM (Gemini Flash via OpenRouter + Vercel AI SDK), parsers de fatura (unpdf), UI chat (Next.js) + Playwright. Cada um tem perfil de custo/latência diferente — e fingir que tudo cabe num único tier degrada o loop de feedback (e2e lento poluindo o watch, ou unit superficial demais pra pegar bug de SQL).
Precisamos decidir onde cada tipo de teste vive, contra o quê ele roda, e quando ele dispara — antes de escrever o primeiro spec de infra.
Decisão
Section titled “Decisão”Três tiers, cada um com escopo e runner explícito.
Tier 1 — domain
Section titled “Tier 1 — domain”- Path:
scenarios/*.spec.ts(sibling do.md, 1:1). - Target: tudo in-memory. Sem I/O.
- Latência: ms por spec.
- Loop:
npm run test:watch(vitest watch, default dev loop). - Estado atual: 000-006 vivem aqui.
Tier 2 — infra
Section titled “Tier 2 — infra”- Path:
src/contexts/*/infrastructure/*.spec.ts(colocated com o adapter). - Target: dependência real quando barato, mock determinístico quando não.
- SQLite:
:memory:via better-sqlite3 (síncrono, ms). - LLM:
MockLanguageModelV1do Vercel AI SDK. - HTTP externo (Pluggy, OpenRouter REST direto): mock inicial; cassette (msw/polly) só quando aparecer bug que o mock não pegar.
- SQLite:
- Loop:
npm run test:infra(on-demand, fora do watch). - Futuro:
npm run test:infra:changed→vitest --project=infra --changed origin/mainquando houver remote.
Tier 3 — e2e
Section titled “Tier 3 — e2e”- Path:
e2e/*.spec.tsno root. - Target: Playwright + Next.js dev server + mock LLM provider
(
MockLanguageModelV1+simulateReadableStream), gated porNODE_ENV=test. Padrão do vercel/chatbot starter. - Loop:
npm run test:e2e(Playwright tem config própria, fora do vitest). - Futuro: tag
@realpra subset opcional rodando contra LLM real (cassette ou live). Decisão diferida.
Runner
Section titled “Runner”Vitest workspaces/projects com três projects: domain, infra, e2e
(Playwright separado). Scripts:
npm run test:watch→vitest --project=domainnpm run test:infra→vitest --project=infranpm run test:e2e→playwright testnpm test→ tudo (check manual ou CI futuro)
Refinamento 2026-06-03: test:watch roda ambos os projects (domain + infra), não só domain como originalmente especificado. Justificativa: vitest com projects + watch rerroda só specs afetados via grafo de módulos — infra (:memory: SQLite + mocks) é rápido suficiente pra ficar no loop, e o workflow real do implementer precisa de feedback em qualquer tier. Quem quiser foco domain-only usa test:domain. E2E (test:e2e) continua fora — Playwright runner próprio. Scripts atualizados em package.json:
test → vitest run · test:watch → vitest · test:domain → vitest run --project=domain · test:infra → vitest run --project=infra · test:e2e → playwright test.
Config (vitest.config.ts com workspaces, scripts em package.json) entra
quando o primeiro spec de infra/e2e for escrito — não agora, pra não ter config
ociosa.
LLM testing strategy
Section titled “LLM testing strategy”Mock-first. MockLanguageModelV1 cobre ~95%: tool-calling, parsing de resposta,
lógica de retry, branching por tool result. Cassette (msw/polly grava+replay)
entra só quando aparecer bug que o mock não pegar — não preventivo.
Sem CI nem hooks por enquanto
Section titled “Sem CI nem hooks por enquanto”Sem remote git ainda. Validação 100% local. Pre-commit/pre-push e GitHub Actions ficam fora do escopo dessa ADR — decisão diferida pra quando o remote existir.
Consequências
Section titled “Consequências”Positivas:
- Watch loop continua ms (só domain). Infra/e2e não poluem o feedback rápido.
- Cada tier tem alvo claro —
:memory:SQLite valida SQL real sem cerimônia; mock LLM mantém infra determinística. - Cassette é opt-in por bug, não overhead por default.
- Convenções já estabelecidas (Gherkin doc-pair, happy path, sibling spec, no per-VO unit) seguem valendo no tier domain sem mudança.
Negativas / Trade-offs:
- Três runners pra manter mentalmente. Mitigado por scripts npm dedicados.
- Mock LLM não pega regressão de comportamento real do modelo — risco aceito
até cassette/tag
@realvirar necessidade. - Sem CI = nada barra regressão automaticamente. Aceito enquanto o projeto é solo + local-first.
Alternativas consideradas
Section titled “Alternativas consideradas”- Tudo no mesmo runner sem projects: simples mas e2e/infra entram no watch e quebram o loop ms. Rejeitada.
- Cassette desde o início pra LLM: setup pesado (gravar fixtures, versionar payloads, lidar com não-determinismo de streaming) sem bug concreto pra justificar. Adiada até precisar.
- GitHub Actions já: sem remote não faz sentido. Adiada.
- Pre-commit hook rodando domain tier: tentador, mas watch já cobre — hook duplicaria o sinal. Adiada pra quando houver remote + push.
Referências
Section titled “Referências”- AGENTS.md → “Testing convention” (Gherkin doc-pair, happy path, sibling spec).
- AGENTS.md → “Test loop (TDD)” (vitest watch como default).
- Decisão fonte:
tmp/grill/decisions.jsonl(2026-06-03). - Vercel AI SDK:
MockLanguageModelV1,simulateReadableStream. - vercel/chatbot starter: pattern de mock LLM gated por
NODE_ENV=test.