mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-29 13:05:18 +02:00
fix(config-protection): protect ignore files and current config spellings
The hook blocks edits to linter/formatter configs so an agent fixes the code instead of weakening the checks. It did not cover the cheapest way to weaken them: adding one path to an ignore file. Measured against the hook, with the file already on disk: .eslintignore -> allow .prettierignore -> allow .stylelintignore -> allow .markdownlintignore -> allow Nor did it cover the current config names — only the legacy `.stylelintrc*` and `.markdownlint.json` spellings were listed, so a project using the documented `stylelint.config.js`, `.markdownlint.jsonc` or markdownlint-cli2 had no protection at all. First-time creation stays allowed by the existing existence check, so scaffolding a fresh ignore file is unaffected.
This commit is contained in:
@@ -57,9 +57,35 @@ const PROTECTED_FILES = new Set([
|
||||
'.stylelintrc',
|
||||
'.stylelintrc.json',
|
||||
'.stylelintrc.yml',
|
||||
'.stylelintrc.yaml',
|
||||
'.stylelintrc.js',
|
||||
'.stylelintrc.cjs',
|
||||
'.stylelintrc.mjs',
|
||||
// Stylelint's current spelling; only the legacy `.stylelintrc*` forms were
|
||||
// listed, so a project using the documented `stylelint.config.js` had no
|
||||
// protection at all.
|
||||
'stylelint.config.js',
|
||||
'stylelint.config.cjs',
|
||||
'stylelint.config.mjs',
|
||||
'.markdownlint.json',
|
||||
'.markdownlint.jsonc',
|
||||
'.markdownlint.yaml',
|
||||
'.markdownlintrc'
|
||||
'.markdownlint.yml',
|
||||
'.markdownlintrc',
|
||||
// markdownlint-cli2 reads its own config names, not `.markdownlint.*`.
|
||||
'.markdownlint-cli2.jsonc',
|
||||
'.markdownlint-cli2.yaml',
|
||||
'.markdownlint-cli2.cjs',
|
||||
'.markdownlint-cli2.mjs',
|
||||
// Ignore files are the cheapest way to make a check pass without touching
|
||||
// the code OR the config: adding one path to .eslintignore silences the
|
||||
// failing file outright. Blocking the config while leaving its ignore list
|
||||
// open left the hook's whole purpose one line away from being defeated.
|
||||
// First-time creation stays allowed by the same existence check below.
|
||||
'.eslintignore',
|
||||
'.prettierignore',
|
||||
'.stylelintignore',
|
||||
'.markdownlintignore'
|
||||
]);
|
||||
|
||||
function parseInput(inputOrRaw) {
|
||||
|
||||
@@ -323,6 +323,115 @@ function runTests() {
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
test('blocks edits to an existing linter ignore file', () => {
|
||||
// Adding one path to .eslintignore silences a failing file without
|
||||
// touching the code or the config — the exact move this hook exists to
|
||||
// stop, and it was allowed. Measured before the fix: .eslintignore,
|
||||
// .prettierignore, .stylelintignore and .markdownlintignore all
|
||||
// returned exit 0.
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-config-protect-'));
|
||||
try {
|
||||
for (const name of [
|
||||
'.eslintignore',
|
||||
'.prettierignore',
|
||||
'.stylelintignore',
|
||||
'.markdownlintignore'
|
||||
]) {
|
||||
const absPath = path.join(tmpDir, name);
|
||||
fs.writeFileSync(absPath, 'dist/\n');
|
||||
|
||||
const result = runHook({
|
||||
tool_name: 'Edit',
|
||||
tool_input: { file_path: absPath, content: 'dist/\nsrc/failing-file.ts\n' }
|
||||
});
|
||||
|
||||
assert.strictEqual(result.code, 2, `Expected exit 2 for ${name}, got ${result.code}`);
|
||||
assert.ok(
|
||||
result.stderr.includes(`BLOCKED: Modifying ${name} is not allowed.`),
|
||||
`Expected block message for ${name}, got: ${result.stderr}`
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
test('blocks the current stylelint and markdownlint config spellings', () => {
|
||||
// Only the legacy `.stylelintrc*` / `.markdownlint.json` names were
|
||||
// listed, so a project on the documented `stylelint.config.js` or
|
||||
// markdownlint-cli2 had no protection at all.
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-config-protect-'));
|
||||
try {
|
||||
for (const name of [
|
||||
'stylelint.config.js',
|
||||
'stylelint.config.mjs',
|
||||
'.stylelintrc.js',
|
||||
'.markdownlint.jsonc',
|
||||
'.markdownlint-cli2.jsonc'
|
||||
]) {
|
||||
const absPath = path.join(tmpDir, name);
|
||||
fs.writeFileSync(absPath, '{}');
|
||||
|
||||
const result = runHook({
|
||||
tool_name: 'Edit',
|
||||
tool_input: { file_path: absPath, content: '{"rules": {}}' }
|
||||
});
|
||||
|
||||
assert.strictEqual(result.code, 2, `Expected exit 2 for ${name}, got ${result.code}`);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
test('a first-time ignore file and a lookalike name are still allowed', () => {
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-config-protect-'));
|
||||
try {
|
||||
// Scaffolding a brand-new ignore file is the same legitimate bootstrap
|
||||
// path the hook already allows for configs.
|
||||
const fresh = runHook({
|
||||
tool_name: 'Write',
|
||||
tool_input: { file_path: path.join(tmpDir, '.prettierignore'), content: 'dist/\n' }
|
||||
});
|
||||
assert.strictEqual(fresh.code, 0, `Expected exit 0 for a new ignore file, got ${fresh.code}`);
|
||||
|
||||
// A file that merely looks like one must not be swept up.
|
||||
const lookalike = path.join(tmpDir, '.eslintignore.bak');
|
||||
fs.writeFileSync(lookalike, 'dist/\n');
|
||||
const result = runHook({
|
||||
tool_name: 'Edit',
|
||||
tool_input: { file_path: lookalike, content: 'dist/\nsrc/\n' }
|
||||
});
|
||||
assert.strictEqual(result.code, 0, `Expected exit 0 for ${path.basename(lookalike)}`);
|
||||
} finally {
|
||||
try {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
passed++;
|
||||
else failed++;
|
||||
|
||||
if (
|
||||
test('legacy hooks do not echo raw input when they fail without stdout', () => {
|
||||
const pluginRoot = path.join(__dirname, '..', `tmp-runner-plugin-${Date.now()}`);
|
||||
|
||||
Reference in New Issue
Block a user