Skip to content

Commit 1efe59a

Browse files
committed
docs(tests): documenta el flujo del motor host (sub 2.1 + 2.2 + patron)
Tras montar Sub 2.2 (PCX decode portable) faltaba que Javi pudiera leer el README y entender: - Cómo cooperan las 4 piezas que validan el motor (motor real + copia portable + runner C + JS reference impl). - Las 3 garantías (drift, bit-exact JS↔C, goldens binarios) y qué tipo de regresión caza cada una. - Workflow concreto cuando edita el motor: el hook dispara, drift test avisa, decisión sincronizar o revertir. - Cómo añadir un módulo nuevo (patrón en 8 pasos para sub-etapas futuras: A* en sub 2.3, lightmap en 2.4, etc.). - Por qué hex en stdout (no raw): translation LF→CRLF en Windows. - Skip elegante si clang no está disponible. Cambios concretos en tests/README.md: - Layout: añade lib/pcx_decode.c y goldens/engine/pcx/. - Sección "Qué cubre" con bloque nuevo para tests/golden/engine-host-pcx.test.js (16 tests, sub 2.2): drift, bit-exact con JS, goldens SHA, sanity de dimensiones, decisión de no testear apply_pal (toca VGA), justificación del hex output. - Nueva sección "Cómo funciona el motor host (Phase 3a)" antes del hook docs: - Diagrama ASCII de las 4 piezas y sus relaciones. - Tabla de cada pieza con su rol y ubicación. - Las 3 garantías explicadas con ejemplo de qué regresión caza cada una. - Workflow del editor del motor en 4 pasos. - Patrón en 8 pasos para añadir un módulo nuevo. - Sección "por qué hex" con la alternativa _setmode descartada. - Skip elegante sin clang. - Tabla del hook actualizada: matcher dispara engine-host*.test.js (con asterisco — varios ficheros ahora). Documentación en español, mismo tono que el resto. Sin tocar contenido de Sub 2.1 (CRC32) que ya estaba documentado. Suite: 257/257 verde, sin cambios funcionales.
1 parent a25872f commit 1efe59a

1 file changed

Lines changed: 183 additions & 10 deletions

File tree

tests/README.md

Lines changed: 183 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,13 @@ agemki/
186186
│ │ └── __snapshots__/ snapshots vitest (preview hex de cabeceras)
187187
│ │
188188
│ └── engine_host/ ← Phase 3a — runner C compilado con clang en host
189-
├── lib/crc32.c copia byte-exact de _sfx_crc32 del motor
190-
├── include/ag_test.h header compartido del runner
191-
├── runner.c entrypoint con dispatcher de tests
192-
├── build.mjs compila con clang (cross-platform mac/win)
193-
└── runner / runner.exe binario generado (gitignored)
189+
├── lib/
190+
│ ├── crc32.c copia byte-exact de _sfx_crc32 (sub 2.1)
191+
│ └── pcx_decode.c copia byte-exact de _pcx_decode RLE (sub 2.2)
192+
├── include/ag_test.h header compartido del runner
193+
├── runner.c entrypoint con dispatcher de tests
194+
├── build.mjs compila con clang (cross-platform mac/win)
195+
└── runner / runner.exe binario generado (gitignored)
194196
195197
├── goldens/ ← outputs esperados (entran al repo, !goldens/** en .gitignore)
196198
│ ├── dat/
@@ -200,7 +202,8 @@ agemki/
200202
│ │ ├── AUDIO.DAT
201203
│ │ ├── FONTS.DAT
202204
│ │ └── manifest.json sha256 + size + numBlocks por DAT
203-
│ ├── engine/ (futuro Phase 3a) outputs binarios de lógica pura
205+
│ ├── engine/ outputs binarios de lógica pura del motor host
206+
│ │ └── pcx/ SHA-256 de cada PCX decodificado (sub 2.2)
204207
│ └── runtime/ (futuro Phase 3c) BMP frames del motor en DOSBox-X
205208
│ capturados en puntos deterministas del game loop
206209
@@ -235,11 +238,11 @@ Smoke de los helpers de testing:
235238
- `decodeDat` parsea cabecera + index entry de un fichero AGMK construido a mano.
236239
- Constantes (`MAGIC`, `HEADER_SIZE`, `INDEX_ENTRY_SIZE`) coherentes con la spec.
237240

238-
### `tests/golden/engine-host.test.js` (22 tests, Phase 3a)
241+
### `tests/golden/engine-host.test.js` (22 tests, Phase 3a sub 2.1)
239242

240243
Tests del **subset portable del motor C** compilado con clang en host.
241-
Primer test: CRC32 (`_sfx_crc32` del motor) — algoritmo crítico porque
242-
el motor usa este hash para binary search en el TOC de SFX.DAT. Si
244+
Primer módulo: CRC32 (`_sfx_crc32` del motor) — algoritmo crítico
245+
porque el motor lo usa para binary search en el TOC de SFX.DAT. Si
243246
difiere del CRC32 del codegen JS (sfxGenerator.js / datGenerator.js),
244247
los chunks no se encuentran en runtime.
245248

@@ -258,6 +261,40 @@ linux), el bloque de tests dependientes se **skipea** automáticamente
258261
y el suite sigue verde. El drift test SÍ se ejecuta siempre (no
259262
requiere clang).
260263

264+
### `tests/golden/engine-host-pcx.test.js` (16 tests, Phase 3a sub 2.2)
265+
266+
Tests del decoder PCX del motor: la sección RLE de
267+
`_pcx_decode` en `resources/engine/agemki_engine.c:995-1034`. La
268+
parte RLE (decompresión) es 100% portable, lo que toca registros VGA
269+
(`outp(0x3C8/0x3C9)` para cargar la paleta) queda fuera del subset
270+
host por ser HW-only.
271+
272+
Cubre:
273+
- **Drift detection** (sin clang): la copia en
274+
`tests/engine_host/lib/pcx_decode.c` sigue byte-exact a la sección
275+
RLE de `_pcx_decode` del motor. Cualquier edit en el motor que altere
276+
esos bytes se caza con un test rojo y un mensaje claro.
277+
- **Bit-exact motor C ↔ JS**: para los 6 PCX del fixture minimal (BG,
278+
sprite, objeto, 3 fuentes), el buffer decodificado por el runner C
279+
coincide al byte con el de `jsPcxDecode` (implementación de
280+
referencia en JS puro).
281+
- **Goldens binarios**: el SHA-256 del buffer decodificado de cada PCX
282+
se persiste en `goldens/engine/pcx/<name>.sha256.txt`. Cualquier
283+
cambio en bytes (por edit del motor, del fixture, o de cualquier
284+
intermedio) deja el SHA distinto y el test rojo identifica qué PCX
285+
cambió.
286+
- **Sanity sobre dimensiones**: cada fixture decodifica al `WxH`
287+
esperado.
288+
289+
Por qué no testear `apply_pal`: usa `outp()` (escritura a registros
290+
DAC de VGA) y `g_pal_raw` (global del motor). En host no tiene sentido,
291+
sería testear el stub.
292+
293+
Por qué hex en stdout y no bytes raw: en Windows, stdout tiene
294+
LF→CRLF translation por defecto que corrompe bytes 0x0A. El runner
295+
C emite el buffer como hex (2 chars por byte) para evitar el issue
296+
sin parches específicos de OS.
297+
261298
### `tests/golden/dat.test.js` (26 tests)
262299

263300
Para cada fixture (`minimal`):
@@ -404,6 +441,142 @@ con cache buster para reiniciar `loadRecent()`.
404441

405442
---
406443

444+
## Cómo funciona el motor host (Phase 3a)
445+
446+
Los tests `engine-host*.test.js` validan que módulos puros del motor
447+
C (CRC32, decoder PCX, A*, lightmap, ...) producen los mismos bytes
448+
ejecutándose en host con clang que ejecutándose en DOS con Watcom.
449+
La idea es **cazar regresiones del motor sin necesidad de DOSBox-X
450+
ni de bootear el juego**.
451+
452+
### Las 4 piezas que cooperan
453+
454+
```
455+
┌─────────────────────────────────────────────────────────────────────────┐
456+
│ │
457+
│ resources/engine/<file>.c tests/engine_host/lib/<file>.c │
458+
│ ┌──────────────────────────┐ ┌────────────────────────────────┐ │
459+
│ │ función real del motor │ ←══→ │ copia byte-exact de la sección │ │
460+
│ │ (puede tener HW outp/ │ drift │ portable (sin HW) │ │
461+
│ │ globals/etc.) │ test │ │ │
462+
│ └──────────────────────────┘ └────────────────────────────────┘ │
463+
│ ↑ ↓ clang -std=c89 │
464+
│ │ ┌─────────────────────┐ │
465+
│ │ │ ./runner <test> │ │
466+
│ │ │ emite output hex │ │
467+
│ │ └─────────────────────┘ │
468+
│ │ ↓ stdout │
469+
│ │ ┌─────────────────────┐ │
470+
│ │ función JS de referencia → │ tests/golden/ │ │
471+
│ └─────────────────────────────→ │ engine-host-*.test │ │
472+
│ │ .js │ │
473+
│ └─────────────────────┘ │
474+
│ ↓ │
475+
│ SHA-256 → goldens/engine/<m>/*.txt │
476+
└─────────────────────────────────────────────────────────────────────────┘
477+
```
478+
479+
| Pieza | Función | Dónde |
480+
|---|---|---|
481+
| **Motor real** | El código que ejecuta el juego en DOS | `resources/engine/*.c` |
482+
| **Copia portable** | Subset del motor compilable con clang en host | `tests/engine_host/lib/*.c` |
483+
| **Runner C** | Binario host que invoca cada función bajo test y emite output | `tests/engine_host/runner.c``runner` (gitignored) |
484+
| **JS reference impl** | Misma lógica reescrita en JS puro, validador independiente | `tests/helpers/engine-host.js` |
485+
486+
### Las 3 garantías (cada una caza un tipo de regresión)
487+
488+
1. **Drift test** — la copia en `lib/` sigue byte-exact al motor real.
489+
Si Javi edita el motor, te avisa para sincronizar la copia.
490+
*No requiere clang* — solo lee los `.c` con `fs.readFileSync` y los
491+
compara con regex extractors.
492+
493+
2. **Bit-exact JS ↔ C** — el output del motor coincide con el de la
494+
implementación JS de referencia. Si el motor cambia su lógica
495+
(no solo su sintaxis), te avisa.
496+
497+
3. **Goldens binarios** — el SHA-256 de los outputs reales (`buffer
498+
decodificado`, `lista de waypoints`, `bitmap lightmap`, ...) se
499+
persiste en `goldens/engine/<modulo>/`. Cualquier cambio que
500+
altere bytes deja el SHA distinto y el test rojo identifica qué
501+
fixture cambió.
502+
503+
### Workflow cuando edites el motor
504+
505+
Si tocas `resources/engine/agemki_audio.c` o `agemki_engine.c`:
506+
507+
1. **Hook automático** dispara `tests/golden/engine-host*.test.js`.
508+
2. Si tu cambio NO toca lógica cubierta → todo verde, sigues.
509+
3. Si tu cambio toca una función cubierta → drift FAIL con mensaje
510+
tipo "lib/pcx_decode.c y agemki_engine.c divergen". Dos opciones:
511+
- Cambio intencional: copia el bloque actualizado del motor a
512+
`tests/engine_host/lib/<file>.c`. Ejecuta `npm run goldens:update`
513+
si los bytes producidos también cambiaron.
514+
- Cambio no intencional: revierte.
515+
4. Si tu cambio en el motor produce **bytes distintos** (no solo
516+
estilo/comments): el drift queda verde tras sincronizar la copia,
517+
pero los SHA-256 de los goldens fallan. Ahí decides si el cambio
518+
es legítimo (regenera goldens) o es un bug (revierte).
519+
520+
### Cómo añadir un nuevo módulo (el patrón)
521+
522+
Cuando arranques una sub-etapa nueva (ej: A* en sub 2.3), el patrón:
523+
524+
1. **Localizar la función pura** en el motor. Identifica qué deps HW
525+
tiene (`outp/inp/int86`, globals, etc.). Si el módulo está
526+
acoplado a HW de forma que no se puede aislar trivialmente,
527+
replantea — quizás necesite refactor.
528+
2. **Copiar byte-exact** la sección portable a
529+
`tests/engine_host/lib/<modulo>.c`. Renombra la función con
530+
prefijo `ag_test_` para exponerla.
531+
3. **Añadir typedef** si la copia usa tipos del motor (`u8`, `s16`,
532+
`Point`, ...). Mapear a `<stdint.h>` o equivalentes en host.
533+
4. **Extender el runner** (`runner.c`) con un dispatcher nuevo
534+
(`./runner <comando> <args>`). Output siempre en formato
535+
line-based + hex (no binario raw, ver "por qué hex" abajo).
536+
5. **Helper JS en `tests/helpers/engine-host.js`** con:
537+
- Función `runner<Modulo>(...)` que invoca el binario y parsea
538+
stdout.
539+
- Función `js<Modulo>(...)` que reimplementa el algoritmo en JS
540+
puro, sirve de referencia.
541+
- Función `detect<Modulo>Drift()` con la regex que extrae el
542+
bloque del motor real y lo compara con la copia local.
543+
6. **Test vitest** en `tests/golden/engine-host-<modulo>.test.js`
544+
con tres bloques:
545+
- `describe('drift detection (sin clang)', ...)` — independiente.
546+
- `describe.skipIf(noClang)('bit-exact con JS', ...)` — el grueso.
547+
- `describe.skipIf(noClang)('goldens', ...)` — SHA-256 vs
548+
`goldens/engine/<modulo>/`.
549+
7. **Hook**: el matcher actual ya cubre `resources/engine/*.c|*.h`,
550+
no hace falta tocar nada.
551+
8. **`tests/README.md`**: añadir sección sobre el nuevo test.
552+
553+
### Por qué hex y no bytes raw en el stdout del runner
554+
555+
Windows hace LF→CRLF translation por defecto en stdout. Cualquier
556+
byte 0x0A en el output binario se convierte a 0x0D 0x0A al salir,
557+
corrompiendo el SHA-256 y rompiendo el test cross-platform.
558+
559+
Solución elegida: el runner emite el buffer como cadena hex (2 chars
560+
por byte). Cero bytes 0x0A en el output, cero translation issue.
561+
Coste: ~2x bytes en stdout, despreciable para fixtures pequeños.
562+
563+
Alternativa con `_setmode(_fileno(stdout), _O_BINARY)` también
564+
funcionaría, pero requiere `#ifdef _WIN32` y headers `<io.h>` en C
565+
— el hex es más simple y portable.
566+
567+
### Skip elegante si clang no está
568+
569+
`tests/engine_host/build.mjs` invoca `clang --version` al arrancar.
570+
Si no está disponible (mac sin Xcode CLT, win sin LLVM, linux sin
571+
apt), el build sale con exit 2 y el helper `ensureRunnerBuilt()`
572+
devuelve `{ status: 'no-clang' }`.
573+
574+
`describe.skipIf(noClang)` skipea automáticamente los bloques que
575+
necesitan el binario, manteniendo el suite verde. **El drift test
576+
NO se skipea** — solo lee `.c` files con `fs`, no requiere clang.
577+
578+
---
579+
407580
## Cómo funciona el hook `PostToolUse`
408581

409582
`.claude/settings.json` registra un hook que se dispara cada vez que
@@ -437,7 +610,7 @@ El hook `.claude/hooks/run-tests-on-edit.mjs`:
437610
| `src/main/index.js` | `tests/golden/` |
438611
| `src/renderer/src/store/*.js` | `tests/unit/stores/` |
439612
| `tests/fixtures/*` o `tests/helpers/*` | `tests/` (toda la suite) |
440-
| `resources/engine/*.c` o `*.h` | `tests/engine_host/` (Phase 3a) |
613+
| `resources/engine/*.c` o `*.h` | `tests/golden/engine-host*.test.js` (drift + tests del módulo si clang está disponible) |
441614
| Cualquier otro path | noop, sale 0 |
442615

443616
Reglas del hook:

0 commit comments

Comments
 (0)