* feat(#1733): normalize-path-in-content production AST rule (Phase 5) ADR-1703 Phase 5 — the first production-code rule. local/normalize-path-in-content (src/**/*.cts, @typescript-eslint/parser): flags a path-returning fn result (path.basename excluded — returns a separator-less filename) interpolated into an @-reference / config-dir markdown body without .replace(/\\/g,'/') normalization, per RULESET.CONTENT-PATH-NORMALIZATION / DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT. Build-and-assess found the canonical defect site (computePathPrefix) already compliant and only 1 src/ hit — a false positive (path.basename in a status message) — eliminated by narrowing (exclude basename; require a real @-ref/ config-dir marker, not bare .md). 0 src/ violations: clean forward-prevention. The out-of-band disable-ban now scans src/**/*.cts too (typescript-estree) so the production rule also cannot be eslint-disabled. Registered (error) + PROTECTED_RULES; CONTEXT.md predicates + how-to doc updated. - RuleTester suite (26 cases) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1733): add changeset for Windows agent-skills path-leak fix Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: harden mutation-matrix.cjs stdin read against EAGAIN on non-blocking pipe scripts/mutation-matrix.cjs read piped stdin via readFileSync(process.stdin.fd). On macOS libuv marks the stdin pipe fd non-blocking, so a synchronous read can throw EAGAIN before the writer fills the pipe — intermittently, under heavy CI shard load — aborting the script (status 2) and flaking mutation-matrix-ratchet. Replace with readStdinSync(): an fs.readSync loop that retries on EAGAIN (1ms synchronous Atomics.wait yield), stops on 0-byte/EOF, and rethrows other errors. Deterministic regression test injects EAGAIN via an fs.readSync monkeypatch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ci: re-run golden-install-parity on src/lib + installer changes (close drift guard) golden-install-parity hashes every installed bin/lib/*.cjs per runtime, so it must re-run whenever the built lib could change. ci-test-scope selected it for neither src/** nor installer changes, so a source-only edit (e.g. #1691's milestone.cts/roadmap.cts) recompiled bin/lib and silently drifted the golden fixtures past the scoped lane. Add golden-install-parity.test.cjs to both the 'TS runtime sources' and 'installer and package layout' selection rules, with behavioral regression tests for each. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: review-bot <review-bot@gsd>
94 lines
3.0 KiB
JavaScript
94 lines
3.0 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* tests/mutation-matrix-stdin-eagain.test.cjs
|
|
*
|
|
* Regression tests for the EAGAIN-resilient readStdinSync() helper added to
|
|
* scripts/mutation-matrix.cjs (issue #1733).
|
|
*
|
|
* Background: On macOS, libuv sets a piped stdin fd to non-blocking mode.
|
|
* Under heavy CI shard load a synchronous fs.readFileSync(process.stdin.fd)
|
|
* can throw EAGAIN before the writer has filled the pipe, aborting the script
|
|
* with status 2. readStdinSync() retries on EAGAIN; these tests verify that
|
|
* contract deterministically by monkeypatching fs.readSync (never via chmod /
|
|
* permission tricks — see cross-platform IO-failure-injection convention).
|
|
*/
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const matrix = require(path.resolve(__dirname, '../scripts/mutation-matrix.cjs'));
|
|
|
|
describe('readStdinSync: EAGAIN resilience', () => {
|
|
test('exports readStdinSync as a function', () => {
|
|
assert.strictEqual(
|
|
typeof matrix.readStdinSync,
|
|
'function',
|
|
'mutation-matrix.cjs must export readStdinSync'
|
|
);
|
|
});
|
|
|
|
test('retries on EAGAIN then returns the full payload', () => {
|
|
// Arrange: stub fs.readSync driven by a closure counter.
|
|
// Call 1 → throw EAGAIN (simulates non-blocking pipe not ready)
|
|
// Call 2 → write payload into buffer, return byte length
|
|
// Call 3+ → return 0 (clean EOF)
|
|
const payload = 'src/core-utils.cts\nsrc/adr-parser.cts\n';
|
|
const payloadBuf = Buffer.from(payload, 'utf8');
|
|
let callCount = 0;
|
|
|
|
const origReadSync = fs.readSync;
|
|
try {
|
|
fs.readSync = (fd, buf, offset, _length, _position) => {
|
|
callCount++;
|
|
if (callCount === 1) {
|
|
throw Object.assign(new Error('EAGAIN: resource temporarily unavailable'), { code: 'EAGAIN' });
|
|
}
|
|
if (callCount === 2) {
|
|
payloadBuf.copy(buf, offset, 0, payloadBuf.length);
|
|
return payloadBuf.length;
|
|
}
|
|
// Call 3+: EOF
|
|
return 0;
|
|
};
|
|
|
|
const result = matrix.readStdinSync();
|
|
|
|
assert.strictEqual(
|
|
result,
|
|
payload,
|
|
'readStdinSync must return the full payload after retrying the EAGAIN'
|
|
);
|
|
assert.ok(
|
|
callCount >= 3,
|
|
`expected at least 3 fs.readSync calls (EAGAIN + data + EOF), got ${callCount}`
|
|
);
|
|
} finally {
|
|
fs.readSync = origReadSync;
|
|
}
|
|
});
|
|
|
|
test('non-EAGAIN errors propagate (are not swallowed)', () => {
|
|
// Arrange: stub fs.readSync to throw a non-retryable error.
|
|
const origReadSync = fs.readSync;
|
|
try {
|
|
fs.readSync = () => {
|
|
throw Object.assign(new Error('EACCES: permission denied'), { code: 'EACCES' });
|
|
};
|
|
|
|
assert.throws(
|
|
() => matrix.readStdinSync(),
|
|
(err) => {
|
|
assert.strictEqual(err.code, 'EACCES');
|
|
return true;
|
|
},
|
|
'readStdinSync must rethrow non-EAGAIN errors'
|
|
);
|
|
} finally {
|
|
fs.readSync = origReadSync;
|
|
}
|
|
});
|
|
});
|