Skip to content

Commit 8ef4078

Browse files
elberrdclaude
andcommitted
fix: CI verde no repo público — skips de harness/ ausente + Windows (npm .cmd, CRLF, paths nativos)
- fda-lock.test: os 3 testes que copiam shims de harness/ (checkout gitignorado, ausente no CI) ganham skip com motivo — padrão do consistency.test.js; teste EPERM vira skip em win32 (kill(1,0) é POSIX) - quality.mjs: spawn com shell no win32 — npm é shim .cmd e o Node >=20.12 recusa spawná-lo sem shell; TODOS os gates de qualidade quebravam em Windows nativo - fia-launch-check: helper rel() com barras normalizadas nos relatórios (theme_tokens, raw_functions, dangerous_html) - fia-permissions.test: core.autocrlf false nos repos temporários (o global do runner Windows restaurava CRLF no revert) - fia-pi-viewer.test: assert do slug normaliza separador do join() - .gitattributes: eol=lf repo-wide (templates são stampados byte a byte) Dívida registrada em lessons.md: agent-*.mjs spawnam claude/pi/cursor-agent (também shims .cmd) — FIA em Windows nativo segue não validada. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent af4e194 commit 8ef4078

6 files changed

Lines changed: 42 additions & 15 deletions

File tree

.gitattributes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# LF everywhere, on every platform. The repo ships a Node CLI plus templates
2+
# that are stamped byte-for-byte into student projects — a CRLF checkout on
3+
# Windows (git autocrlf) would change what gets stamped AND break tests that
4+
# compare file content after git operations.
5+
* text=auto eol=lf

fia-templates/modules/quality.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ async function runSpec(spec, run) {
3131
cwd: run.repoRoot,
3232
env: run.env,
3333
stdio: ['ignore', 'pipe', 'pipe'],
34+
// npm/npx are .cmd shims on Windows and Node (>= 20.12) refuses to
35+
// spawn those without a shell. POSIX keeps the direct, quoting-safe
36+
// spawn.
37+
shell: process.platform === 'win32',
3438
});
3539
child.stdout?.on('data', (d) => (stdout += d));
3640
child.stderr?.on('data', (d) => (stderr += d));

fia-templates/scripts/fia-launch-check.mjs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ function listFiles(dir, exts, acc = [], depth = 0) {
191191
export function runLaunchChecks(root = process.cwd(), opts = {}) {
192192
root = resolve(root);
193193
const aiDocsDir = opts.aiDocsDir || process.env.FIA_AI_DOCS || join(root, 'ai-docs');
194+
// Repo-relative path for reports — always forward slashes, so a finding
195+
// reads the same (and diffs the same) on Windows and POSIX.
196+
const rel = (f) => f.slice(root.length + 1).replaceAll('\\', '/');
194197
const checks = [];
195198
const add = (section, id, level, status, label, detail = null, fix = null) =>
196199
checks.push({ section, id, level, status, label, ...(detail ? { detail } : {}), ...(fix ? { fix } : {}) });
@@ -360,7 +363,7 @@ export function runLaunchChecks(root = process.cwd(), opts = {}) {
360363
for (const dir of uiDirs) {
361364
for (const f of listFiles(dir, ['.tsx', '.jsx'])) {
362365
if (/[/\\]emails?[/\\]/.test(f)) continue; // react-email inlines styles by design
363-
if (hex.test(readIf(f) || '')) offenders.push(f.slice(root.length + 1));
366+
if (hex.test(readIf(f) || '')) offenders.push(rel(f));
364367
}
365368
}
366369
add('Work', 'theme_tokens', 'warn', offenders.length === 0 ? 'pass' : 'fail', 'No hardcoded hex colors in UI components', offenders.length ? offenders.slice(0, 5).join(', ') + (offenders.length > 5 ? ` (+${offenders.length - 5})` : '') : null, offenders.length ? 'use the theme CSS variables — raw hex is invisible to /theme (design-system skill, rule 8)' : null);
@@ -417,15 +420,15 @@ export function runLaunchChecks(root = process.cwd(), opts = {}) {
417420
for (const f of convexFiles) {
418421
if (/[/\\]lib[/\\]/.test(f)) continue;
419422
const src = readIf(f) || '';
420-
if (/=\s*(query|mutation)\s*\(/.test(src)) rawFns.push(f.slice(root.length + 1));
423+
if (/=\s*(query|mutation)\s*\(/.test(src)) rawFns.push(rel(f));
421424
}
422425
add('Security', 'raw_functions', 'warn', rawFns.length === 0 ? 'pass' : 'fail', 'No raw query/mutation outside convex/lib', rawFns.length ? rawFns.slice(0, 5).join(', ') : null, rawFns.length ? 'use authedQuery/authedMutation (security skill, pillar 1)' : null);
423426
} else {
424427
add('Security', 'raw_functions', 'info', 'skip', 'No raw query/mutation outside convex/lib', 'stack does not use Convex', null);
425428
}
426429

427430
const uiFiles = [...listFiles(join(root, 'app'), ['.tsx', '.ts']), ...listFiles(join(root, 'components'), ['.tsx'])];
428-
const danger = uiFiles.filter((f) => /dangerouslySetInnerHTML/.test(readIf(f) || '')).map((f) => f.slice(root.length + 1));
431+
const danger = uiFiles.filter((f) => /dangerouslySetInnerHTML/.test(readIf(f) || '')).map(rel);
429432
add('Security', 'dangerous_html', 'warn', danger.length === 0 ? 'pass' : 'fail', 'No dangerouslySetInnerHTML', danger.slice(0, 5).join(', ') || null, danger.length ? 'never with user content — React escapes by default' : null);
430433

431434
const httpTs = readIf(join(root, 'convex', 'http.ts'));

test/fda-lock.test.js

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { test } from 'node:test';
22
import assert from 'node:assert/strict';
33
import { spawnSync } from 'node:child_process';
4-
import { mkdtempSync, mkdirSync, writeFileSync, cpSync, readFileSync } from 'node:fs';
4+
import { existsSync, mkdtempSync, mkdirSync, writeFileSync, cpSync, readFileSync } from 'node:fs';
55
import { join } from 'node:path';
66
import { tmpdir } from 'node:os';
77
import {
@@ -14,6 +14,10 @@ import { acquireLock } from '../fia-templates/modules/session.mjs';
1414

1515
const SCRIPT = join(import.meta.dirname, '..', 'fia-templates', 'scripts', 'fda-lock.mjs');
1616
const SHIM = join(import.meta.dirname, '..', 'harness', '.claude', 'hooks', 'fda-lock.mjs');
17+
// The shims live in the harness — a SEPARATE repo checked out here only in
18+
// dev (gitignored, absent on a fresh clone/CI). Same pattern as
19+
// consistency.test.js: skip, never fail, when the checkout is missing.
20+
const NO_HARNESS = 'harness/ not present (nested repo, absent on fresh checkout)';
1721

1822
/** A temp project root, optionally holding imp/data/.fda.lock with `lock`. */
1923
function projectWith(lock) {
@@ -131,7 +135,7 @@ test('renderWarn shows fda_id, runner and start time', () => {
131135
assert.match(text, /2026-08-13T12:00:00Z/);
132136
});
133137

134-
test('harness shim: silent without FIA, delegates when imp/ exists', () => {
138+
test('harness shim: silent without FIA, delegates when imp/ exists', { skip: !existsSync(SHIM) && NO_HARNESS }, () => {
135139
// No imp/scripts → import fails → exit 0, no output, nothing blocked.
136140
const bare = projectWith(LIVE);
137141
mkdirSync(join(bare, '.claude', 'hooks'), { recursive: true });
@@ -186,7 +190,7 @@ function runCursorShim(root, payload, env = {}) {
186190
return JSON.parse(r.stdout);
187191
}
188192

189-
test('cursor hook: denies a repo write while a run is live, allows reads', () => {
193+
test('cursor hook: denies a repo write while a run is live, allows reads', { skip: !existsSync(CURSOR_SHIM) && NO_HARNESS }, () => {
190194
const root = cursorProject(LIVE);
191195
const deny = runCursorShim(root, { command: 'rm src/app.ts', workspace_roots: [root], cwd: root });
192196
assert.equal(deny.permission, 'deny');
@@ -197,7 +201,7 @@ test('cursor hook: denies a repo write while a run is live, allows reads', () =>
197201
assert.deepEqual(read, { permission: 'allow' });
198202
});
199203

200-
test('cursor hook: dead lock, missing runtime, own run tree and bad stdin all allow', () => {
204+
test('cursor hook: dead lock, missing runtime, own run tree and bad stdin all allow', { skip: !existsSync(CURSOR_SHIM) && NO_HARNESS }, () => {
201205
const dead = cursorProject(DEAD);
202206
assert.deepEqual(runCursorShim(dead, { command: 'rm src/x.ts', workspace_roots: [dead] }), {
203207
permission: 'allow',
@@ -249,10 +253,16 @@ test('acquireLock: refuses a live holder, replaces a dead one, atomic create', (
249253
assert.equal(JSON.parse(readFileSync(join(garbage, 'imp', 'data', '.fda.lock'), 'utf8')).fda_id, 'stolen');
250254
});
251255

252-
test('acquireLock: EPERM means ALIVE — a lock held by another user is never stolen', () => {
253-
// pid 1 (launchd/init) exists and belongs to root: kill(1, 0) from an
254-
// unprivileged test process raises EPERM. The writer must treat that as a
255-
// live run — exactly like the reader (activeFdaLock) — and refuse.
256-
const root = projectWith({ pid: 1, fda_id: 'rooted', runner: 'fda_build', started_at: '2026-08-13T12:00:00Z' });
257-
assert.throws(() => acquireLock(join(root, 'imp', 'data'), 'newrun'), /already active/);
258-
});
256+
test(
257+
'acquireLock: EPERM means ALIVE — a lock held by another user is never stolen',
258+
// kill(1, 0) → EPERM is POSIX (pid 1 = launchd/init, owned by root). On
259+
// Windows there is no deterministic pid that raises EPERM for a test.
260+
{ skip: process.platform === 'win32' && 'POSIX-only: no deterministic EPERM pid on Windows' },
261+
() => {
262+
// pid 1 (launchd/init) exists and belongs to root: kill(1, 0) from an
263+
// unprivileged test process raises EPERM. The writer must treat that as a
264+
// live run — exactly like the reader (activeFdaLock) — and refuse.
265+
const root = projectWith({ pid: 1, fda_id: 'rooted', runner: 'fda_build', started_at: '2026-08-13T12:00:00Z' });
266+
assert.throws(() => acquireLock(join(root, 'imp', 'data'), 'newrun'), /already active/);
267+
},
268+
);

test/fia-permissions.test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ function initGitRepo(root) {
1010
execFileSync('git', ['init'], { cwd: root, stdio: 'ignore' });
1111
execFileSync('git', ['config', 'user.email', 'fia@test.dev'], { cwd: root, stdio: 'ignore' });
1212
execFileSync('git', ['config', 'user.name', 'FIA Test'], { cwd: root, stdio: 'ignore' });
13+
// Windows runners default autocrlf=true globally — a revert would restore
14+
// CRLF bytes and break the literal content comparisons below.
15+
execFileSync('git', ['config', 'core.autocrlf', 'false'], { cwd: root, stdio: 'ignore' });
1316
writeFileSync(join(root, 'README.md'), '# test\n');
1417
execFileSync('git', ['add', 'README.md'], { cwd: root, stdio: 'ignore' });
1518
execFileSync('git', ['commit', '-m', 'init'], { cwd: root, stdio: 'ignore' });

test/fia-pi-viewer.test.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,9 @@ test('pi sessions: quiet-but-recent is idle, only real silence is finished', ()
133133

134134
test('pi helpers: project slug dir and subagent name parsing', () => {
135135
assert.equal(
136-
piSessionsDirFor('/Users/x/proj', '/home/u'),
136+
// join() is platform-native (backslashes on Windows); the slug logic is
137+
// what this asserts, so normalize the separators before comparing.
138+
piSessionsDirFor('/Users/x/proj', '/home/u').replaceAll('\\', '/'),
137139
'/home/u/.pi/agent/sessions/--Users-x-proj--',
138140
);
139141
assert.equal(agentFromSessionName('subagent-start-mapper-5e8a33b2-02c5-44fa-8ed8-f8fccd67bdd1'), 'start-mapper');

0 commit comments

Comments
 (0)