Skip to content

002 — Estratégia de testes em tiers (domain / infra / e2e)

Status: Aceita (2026-06-03)

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.

Três tiers, cada um com escopo e runner explícito.

  • 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.
  • 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: MockLanguageModelV1 do Vercel AI SDK.
    • HTTP externo (Pluggy, OpenRouter REST direto): mock inicial; cassette (msw/polly) só quando aparecer bug que o mock não pegar.
  • Loop: npm run test:infra (on-demand, fora do watch).
  • Futuro: npm run test:infra:changedvitest --project=infra --changed origin/main quando houver remote.
  • Path: e2e/*.spec.ts no root.
  • Target: Playwright + Next.js dev server + mock LLM provider (MockLanguageModelV1 + simulateReadableStream), gated por NODE_ENV=test. Padrão do vercel/chatbot starter.
  • Loop: npm run test:e2e (Playwright tem config própria, fora do vitest).
  • Futuro: tag @real pra subset opcional rodando contra LLM real (cassette ou live). Decisão diferida.

Vitest workspaces/projects com três projects: domain, infra, e2e (Playwright separado). Scripts:

  • npm run test:watchvitest --project=domain
  • npm run test:infravitest --project=infra
  • npm run test:e2eplaywright test
  • npm 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:

testvitest run · test:watchvitest · test:domainvitest run --project=domain · test:infravitest run --project=infra · test:e2eplaywright 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.

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 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.

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 @real virar necessidade.
  • Sem CI = nada barra regressão automaticamente. Aceito enquanto o projeto é solo + local-first.
  • 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.
  • 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.