Skip to content

Commit a218759

Browse files
author
Marcos Hernandez
committed
chore: portabilidad cross-platform Windows 11 / macOS
Javi usa Windows 11 + Claude Code; yo uso macOS. La suite estaba casi portable ya (vitest, builder.mjs, todos los tests son Node puro), pero el hook PostToolUse era bash y .gitattributes faltaba. Cambios: - Hook reescrito de bash a Node (.claude/hooks/run-tests-on-edit.mjs). Identico comportamiento, corre nativo en Windows sin Git Bash ni WSL. Lee stdin, parsea tool_input.file_path, decide filtro segun area (codegen/stores/fixtures/engine), invoca npm test, reporta a stderr solo si falla. Cero ruido en exito. Nunca bloquea (siempre exit 0). Path matching agnostico al separador (/ o \). - .claude/settings.json apunta a "node .claude/hooks/run-tests-on-edit.mjs" en lugar del .sh. Una sola entrada para los dos OS. - .gitattributes nuevo. Texto en LF (* text=auto eol=lf) para evitar que core.autocrlf=true en Windows convierta a CRLF y rompa snapshots vitest. Marcadores binary para PCX/MID/WAV/DAT/OBJ/EXE/BMP/PNG/PDF para evitar que Git interprete los bytes como texto y los corrompa. Renormalizacion confirma que los textos del repo ya estaban en LF (creado en mac, sin CRLF que limpiar). - GitHub Actions matrix mac+windows en .github/workflows/test.yml. Corre npm test en macos-latest y windows-latest, con Node 20 y 22. Verifica idempotencia del fixture builder (regenerar dos veces debe producir mismos bytes) y smoke test del hook Node en cada plataforma. El motor C (Phase 3) queda fuera de CI por diseno: requiere Watcom y DOSBox-X que los runners gratis no tienen. - Documentacion (tests/README.md, CLAUDE.md) actualizada con seccion cross-platform: tabla de equivalencias macOS/Windows, snippets de pre-commit hook portables, smoke test del hook en ambos OS, explicacion del rol del .gitattributes. Phase 3 (motor C) seguira el patron existente del README.md raiz que ya usa Javi (Watcom v2 nativo en Windows, paths C:\WATCOM\). En mi mac tendre una alternativa equivalente con DOSBox-X envolviendo el mismo Watcom v2, para poder reproducir y verificar regresiones desde mi maquina sin sacar a Javi de su flujo. Eso vendra en otro PR cuando toque Phase 3. Verificacion local: 219 tests verde en mac (npm test). El smoke test manual del hook con todos los casos pasa correctamente: edit irrelevante = noop, edit en store = silencio, edit con goldens rotos = reporta FAIL a stderr y exit 0.
1 parent bb779ab commit a218759

7 files changed

Lines changed: 303 additions & 90 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Hook PostToolUse cross-platform (macOS / Windows / Linux).
4+
*
5+
* Ejecuta los tests del área editada cuando Claude Code modifica un fichero.
6+
* Reporta a stderr solo si los tests fallan (cero ruido en éxito).
7+
*
8+
* Reglas (path matching es agnóstico al separador de OS):
9+
* - src/main/(datGenerator|sfxGenerator|fontGenerator|index).js
10+
* → npm test -- tests/golden/
11+
* - src/renderer/src/store/*.js
12+
* → npm test -- tests/unit/stores/
13+
* - tests/fixtures/* o tests/helpers/*
14+
* → npm test (toda la suite, los helpers afectan a varias áreas)
15+
* - resources/engine/*.c|*.h
16+
* → reservado para Phase 3a (motor host con clang)
17+
* - Cualquier otro fichero → noop, exit 0.
18+
*
19+
* Diseño:
20+
* - Nunca bloquea el edit (siempre exit 0).
21+
* - Lee el JSON del hook por stdin: { tool_name, tool_input: { file_path } }.
22+
* - Detecta plataforma vía process.platform si hace falta (no hace falta
23+
* en este hook: npm y node funcionan idénticos en mac/win/linux).
24+
*
25+
* Por qué Node y no bash: para que funcione en Windows sin Git Bash. Node es
26+
* dependencia obligatoria del proyecto (engines.node en package.json), así que
27+
* no añade requisito nuevo.
28+
*/
29+
import { execSync } from 'node:child_process'
30+
import { existsSync, readFileSync } from 'node:fs'
31+
import { dirname, join, resolve } from 'node:path'
32+
import { fileURLToPath } from 'node:url'
33+
34+
// ── Leer stdin (JSON del hook) ────────────────────────────────────────────────
35+
let raw = ''
36+
try {
37+
// readFileSync('/dev/stdin') no funciona en Windows. Usamos lectura síncrona
38+
// del fd 0 con buffer fijo (suficiente: el JSON del hook nunca pasa de unos KB).
39+
const chunk = Buffer.alloc(64 * 1024)
40+
let total = 0
41+
// eslint-disable-next-line no-constant-condition
42+
while (true) {
43+
let read
44+
try { read = readFileSync(0, { length: chunk.length - total, encoding: null }) }
45+
catch { break }
46+
if (!read || read.length === 0) break
47+
raw = read.toString('utf8')
48+
break // readFileSync(0) lee todo de golpe en una sola llamada en práctica
49+
}
50+
} catch { /* sin stdin → noop */ }
51+
52+
if (!raw.trim()) process.exit(0)
53+
54+
let payload
55+
try { payload = JSON.parse(raw) } catch { process.exit(0) }
56+
57+
const filePath = payload?.tool_input?.file_path || ''
58+
if (!filePath) process.exit(0)
59+
60+
// ── Localizar la raíz del repo (relativo al hook) ─────────────────────────────
61+
const __dirname = dirname(fileURLToPath(import.meta.url))
62+
const REPO_ROOT = resolve(__dirname, '..', '..')
63+
64+
// ── Normalizar el path para que el matcher sea OS-agnóstico ───────────────────
65+
// En Windows los paths llegan con \. Convertimos a / para usar regex sencillas.
66+
const norm = filePath.replace(/\\/g, '/')
67+
68+
// ── Decidir filtro por área ───────────────────────────────────────────────────
69+
let area = null
70+
let filter = null
71+
72+
if (/\/src\/main\/(datGenerator|sfxGenerator|fontGenerator|index)\.js$/.test(norm)) {
73+
area = 'codegen'
74+
filter = 'tests/golden/'
75+
} else if (/\/src\/renderer\/src\/store\/[^/]+\.js$/.test(norm)) {
76+
area = 'stores'
77+
filter = 'tests/unit/stores/'
78+
} else if (/\/tests\/(fixtures|helpers)\//.test(norm)) {
79+
area = 'fixtures+helpers'
80+
filter = 'tests/'
81+
} else if (/\/resources\/engine\/[^/]+\.[ch]$/.test(norm)) {
82+
// Phase 3a: motor host. No implementado todavía.
83+
const makefile = join(REPO_ROOT, 'tests', 'engine_host', 'Makefile')
84+
if (existsSync(makefile)) {
85+
runEngineHost()
86+
}
87+
process.exit(0)
88+
} else {
89+
// Path no relevante para tests
90+
process.exit(0)
91+
}
92+
93+
// ── Ejecutar npm test con el filtro ───────────────────────────────────────────
94+
// `npm test --` pasa los argumentos siguientes a vitest. El comportamiento es
95+
// idéntico en Windows y Unix (npm normaliza el shell).
96+
try {
97+
execSync(`npm test -- ${filter}`, {
98+
cwd: REPO_ROOT,
99+
stdio: ['ignore', 'pipe', 'pipe'],
100+
encoding: 'utf8',
101+
})
102+
// Tests pasaron. No emitimos nada (cero ruido en éxito).
103+
} catch (err) {
104+
const stdout = err.stdout?.toString('utf8') || ''
105+
const stderr = err.stderr?.toString('utf8') || ''
106+
const combined = (stdout + stderr).split('\n').slice(-25).join('\n')
107+
process.stderr.write(`[hook] tests ${area} FAIL — ${filePath}\n`)
108+
process.stderr.write(combined + '\n')
109+
}
110+
111+
process.exit(0)
112+
113+
// ── Phase 3a placeholder ──────────────────────────────────────────────────────
114+
function runEngineHost() {
115+
try {
116+
execSync('make -C tests/engine_host run', {
117+
cwd: REPO_ROOT,
118+
stdio: ['ignore', 'pipe', 'pipe'],
119+
encoding: 'utf8',
120+
})
121+
} catch (err) {
122+
const stdout = err.stdout?.toString('utf8') || ''
123+
const stderr = err.stderr?.toString('utf8') || ''
124+
const combined = (stdout + stderr).split('\n').slice(-20).join('\n')
125+
process.stderr.write('[hook] engine host tests FAIL:\n')
126+
process.stderr.write(combined + '\n')
127+
}
128+
}

.claude/hooks/run-tests-on-edit.sh

Lines changed: 0 additions & 83 deletions
This file was deleted.

.claude/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"hooks": [
1818
{
1919
"type": "command",
20-
"command": ".claude/hooks/run-tests-on-edit.sh"
20+
"command": "node .claude/hooks/run-tests-on-edit.mjs"
2121
}
2222
]
2323
}

.gitattributes

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# EOL normalizado a LF para todo el contenido textual del repo.
2+
# Esto evita que `core.autocrlf` en Windows convierta a CRLF y luego los tests
3+
# fallen por mismatch de línea en goldens, snapshots o JSON.
4+
* text=auto eol=lf
5+
6+
# Forzar texto explícito en ficheros que algunos detectores pueden malinterpretar
7+
*.md text eol=lf
8+
*.json text eol=lf
9+
*.js text eol=lf
10+
*.jsx text eol=lf
11+
*.mjs text eol=lf
12+
*.cjs text eol=lf
13+
*.snap text eol=lf
14+
*.sh text eol=lf
15+
*.conf text eol=lf
16+
*.yml text eol=lf
17+
*.yaml text eol=lf
18+
19+
# ── Binarios: NO tocarles los bytes nunca ────────────────────────────────────
20+
# Goldens y fixtures binarios. Si Git los procesa como texto, los corrompe.
21+
*.DAT binary
22+
*.dat binary
23+
*.PCX binary
24+
*.pcx binary
25+
*.MID binary
26+
*.mid binary
27+
*.MIDI binary
28+
*.midi binary
29+
*.WAV binary
30+
*.wav binary
31+
*.IMA binary
32+
*.ima binary
33+
*.OBJ binary
34+
*.obj binary
35+
*.EXE binary
36+
*.exe binary
37+
*.BMP binary
38+
*.bmp binary
39+
*.PNG binary
40+
*.png binary
41+
*.PDF binary
42+
*.pdf binary
43+
*.OP2 binary
44+
*.op2 binary
45+
*.PAL binary
46+
*.pal binary

.github/workflows/test.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: tests
2+
3+
# Corre la suite de tests JS en macOS y Windows en cada push y pull request.
4+
# El motor C (Phase 3) queda fuera de CI por diseño: requiere DOSBox-X / Watcom
5+
# y los runners gratis de GitHub Actions no los tienen configurados.
6+
7+
on:
8+
push:
9+
branches: [main, 'marcos/**']
10+
pull_request:
11+
branches: [main]
12+
workflow_dispatch: {}
13+
14+
jobs:
15+
test:
16+
name: tests (${{ matrix.os }} / Node ${{ matrix.node }})
17+
runs-on: ${{ matrix.os }}
18+
strategy:
19+
fail-fast: false
20+
matrix:
21+
os: [macos-latest, windows-latest]
22+
node: ['20', '22']
23+
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@v4
27+
# core.autocrlf y line endings: el .gitattributes del repo manda;
28+
# forzamos input para que Windows no convierta CRLF en archivos texto.
29+
with:
30+
fetch-depth: 1
31+
32+
- name: Setup Node ${{ matrix.node }}
33+
uses: actions/setup-node@v4
34+
with:
35+
node-version: ${{ matrix.node }}
36+
cache: npm
37+
38+
- name: Install deps
39+
run: npm ci
40+
41+
- name: Run tests
42+
run: npm test
43+
44+
- name: Verify fixture builder is idempotente
45+
# Si el builder produce bytes distintos en run-tras-run, los goldens
46+
# se desincronizarían. Verificamos que regenerar los fixtures no rompe.
47+
run: |
48+
node tests/fixtures/builder.mjs
49+
npm test
50+
51+
- name: Smoke test del hook PostToolUse
52+
# Verifica que el hook Node funciona en este OS (sin tocar bash/sh).
53+
# Editar un fichero irrelevante debe ser noop, edit en store ejecuta
54+
# tests y devuelve exit 0 si verde.
55+
shell: node {0}
56+
run: |
57+
import { execSync } from 'node:child_process'
58+
import { resolve } from 'node:path'
59+
const file = resolve('src/renderer/src/store/sceneStore.js')
60+
const payload = JSON.stringify({ tool_name: 'Edit', tool_input: { file_path: file } })
61+
execSync('node .claude/hooks/run-tests-on-edit.mjs', {
62+
input: payload, stdio: ['pipe', 'inherit', 'inherit']
63+
})
64+
console.log('hook OK en', process.platform)

CLAUDE.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,9 @@ node tests/fixtures/builder.mjs # regenera los fixtures binarios
125125
### Pre-commit hook opcional (recomendado)
126126

127127
Si quieres que git bloquee commits con tests rojos sin tener que acordarte
128-
manualmente, instala un hook local (no se commitea, vive en `.git/hooks/`):
128+
manualmente, instala un hook local (no se commitea, vive en `.git/hooks/`).
129129

130+
**macOS / Linux**:
130131
```bash
131132
cat > .git/hooks/pre-commit <<'EOF'
132133
#!/bin/sh
@@ -135,8 +136,17 @@ EOF
135136
chmod +x .git/hooks/pre-commit
136137
```
137138

138-
Saltable puntualmente con `git commit --no-verify` cuando quieras
139-
commitear WIP a medias.
139+
**Windows (Git for Windows + PowerShell)**:
140+
```powershell
141+
@"
142+
#!/bin/sh
143+
npm test --silent
144+
"@ | Set-Content -Encoding ASCII -NoNewline .git/hooks/pre-commit
145+
```
146+
147+
Git for Windows interpreta el shebang con su bash interno, no hace falta
148+
ningún `chmod`. Saltable con `git commit --no-verify` cuando quieras
149+
commitear WIP a medias, en cualquier OS.
140150

141151
### Convención de idioma (mandatorio en repo)
142152

0 commit comments

Comments
 (0)