Cómo practicar TDD dentro de una sesión agente sin caer en phase-collapse. Commits [RED]/[GREEN] como gate estructural, tests adversariales contra interfaces, prevención de tests vacíos post-hoc. Usar cuando la tarea implique escribir código nuevo de lógica de dominio (parsers, normalizadores, services, ETL mappers).
Paper GS §4.3 describe el phase-collapse: en un agente de IA, el que escribe el test y el que escribe el código viven en el mismo contexto. No hay separación temporal, entonces la presión del "test que falla" se pierde.
Este skill es el gate estructural que evita ese colapso.
No aplica a: setup de infra, ajustes de config, UI sin lógica, CSS, cambios tipográficos.
test(parser): [RED] rejects PDF with empty text
Expected: parseCv returns { parse_error: 'empty_text' }
Actual: TypeError: parseCv is not a function
Output:
FAIL src/lib/cv/parser.test.ts > rejects PDF with empty text
TypeError: parseCv is not a function
at .../parser.test.ts:12:3
Regla estructural: sin el [RED] en el subject, el hook de
commit-msg lo rechaza. El output pegado es evidencia de que el
test realmente falla.
Cómo commitear con tests fallando: el pre-commit corre los
tests y bloquea commits con tests rojos. Para un [RED] legítimo,
invocá:
TDD_RED=1 git commit -m 'test(scope): [RED] ...'
Esto bypasea SOLO la corrida de tests en pre-commit, no la gate de
[GREEN] en commit-msg.
feat(parser): [GREEN] handle empty-text PDFs
Minimal implementation: returns parse_error='empty_text' when
extracted text is shorter than MIN_USEFUL_TEXT.
Closes RED commit abc1234.
Regla estructural: el pre-commit hook valida que haya un commit
test: [RED] previo con scope coincidente (parser en este caso)
en los últimos N commits del branch. Si no, rechaza.
refactor(parser): extract normalizeWhitespace helper
El test es un cazador, no un testigo (paper §4.3 Verifiable).
Antes de escribir cada test, responder (aunque sea mentalmente):
// ✅ Buenos nombres (nombran la violación)
test_rejects_content_hash_mismatch
test_denies_cross_tenant_access
test_refuses_oversized_file
test_stops_retrying_after_max_attempts
// ❌ Malos nombres (solo documentan el happy path)
test_parser_works
test_basic_flow
test_returns_correct_value
// ❌ MAL — acoplado a internals
test('_hash calls createHash once', () => {
const spy = vi.spyOn(crypto, 'createHash');
parser._hash('foo');
expect(spy).toHaveBeenCalledOnce();
});
// ✅ BIEN — verifica comportamiento observable
test('returns same hash for equivalent input', () => {
expect(parser.hash(' foo ')).toBe(parser.hash('foo'));
});
Regla: si el test verifica cómo, no qué, probablemente lo estás haciendo mal.
El pre-commit hook rechaza:
feat: sin test: [RED] previo en el mismo scope.expect(x).toBeDefined() o
expect(true).toBe(true)..test.ts con skip / only / todo.No todo merece TDD estricto:
Si skipeás, documentarlo:
feat(ui): fix typo in empty state message [tdd-skip: trivial]
El hook conoce la flag [tdd-skip: <razon>]. Abusarla activa
review manual.
src/lib/ 90%, src/lib/auth/ 95% (ver
docs/test-architecture.md §8)./* istanbul ignore */ sin comment explicando
por qué. El hook valida el comment.test: [RED] con test que falla por razón correcta.feat: [GREEN] con implementación mínima.docs/test-architecture.md — pirámide y coverage..claude/hooks/pre-commit.sh — hook que valida..claude/hooks/commit-msg.sh — hook que valida [RED]/[GREEN].