mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
test(core): add atomic write coverage structural regression guard (#1972)
Per CONTRIBUTING.md, enhancements require tests covering the enhanced behavior. This test structurally verifies that milestone.cjs, phase.cjs, and frontmatter.cjs do not contain bare fs.writeFileSync calls targeting .planning/ files. All such writes must route through atomicWriteFileSync. Allowed exceptions: .gitkeep writes (empty files) and archive directory writes (new files, not read-modify-write). This complements atomic-write.test.cjs which tests the helper itself. If someone later adds a bare writeFileSync to these files without using the atomic helper, this test will catch it. Review feedback on #2056 from @trek-e.
This commit is contained in:
95
tests/atomic-write-coverage.test.cjs
Normal file
95
tests/atomic-write-coverage.test.cjs
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Structural regression guard for atomic write usage (#1972).
|
||||
*
|
||||
* Ensures that milestone.cjs, phase.cjs, and frontmatter.cjs do NOT
|
||||
* contain bare fs.writeFileSync calls targeting .planning/ files. All
|
||||
* such writes must go through atomicWriteFileSync to prevent partial
|
||||
* writes from corrupting planning artifacts on crash.
|
||||
*
|
||||
* Allowed exceptions:
|
||||
* - Writes to .gitkeep (empty files, no corruption risk)
|
||||
* - Writes to archive directories (new files, not read-modify-write)
|
||||
*
|
||||
* This test is structural — it reads the source files and parses for
|
||||
* bare writeFileSync patterns. It complements functional tests in
|
||||
* atomic-write.test.cjs which verify the helper itself.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const libDir = path.resolve(__dirname, '..', 'get-shit-done', 'bin', 'lib');
|
||||
|
||||
/**
|
||||
* Find all fs.writeFileSync(...) call sites in a file.
|
||||
* Returns array of { line: number, text: string }.
|
||||
*/
|
||||
function findBareWrites(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
const hits = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (/\bfs\.writeFileSync\s*\(/.test(lines[i])) {
|
||||
hits.push({ line: i + 1, text: lines[i].trim() });
|
||||
}
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify a bare write as allowed (archive, .gitkeep) or disallowed.
|
||||
*/
|
||||
function isAllowedException(lineText) {
|
||||
// .gitkeep writes (empty file, no corruption risk)
|
||||
if (/\.gitkeep/.test(lineText)) return true;
|
||||
// Archive directory writes (new files, not read-modify-write)
|
||||
if (/archiveDir/.test(lineText)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
describe('atomic write coverage (#1972)', () => {
|
||||
const targetFiles = ['milestone.cjs', 'phase.cjs', 'frontmatter.cjs'];
|
||||
|
||||
for (const file of targetFiles) {
|
||||
test(`${file}: all fs.writeFileSync calls target allowed exceptions`, () => {
|
||||
const filePath = path.join(libDir, file);
|
||||
assert.ok(fs.existsSync(filePath), `${file} must exist at ${filePath}`);
|
||||
|
||||
const hits = findBareWrites(filePath);
|
||||
const violations = hits.filter(h => !isAllowedException(h.text));
|
||||
|
||||
if (violations.length > 0) {
|
||||
const report = violations.map(v => ` line ${v.line}: ${v.text}`).join('\n');
|
||||
assert.fail(
|
||||
`${file} contains ${violations.length} bare fs.writeFileSync call(s) targeting planning files.\n` +
|
||||
`These should use atomicWriteFileSync instead:\n${report}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test(`${file}: imports atomicWriteFileSync from core.cjs`, () => {
|
||||
const filePath = path.join(libDir, file);
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
assert.match(
|
||||
content,
|
||||
/atomicWriteFileSync.*require\(['"]\.\/core\.cjs['"]\)|atomicWriteFileSync[^)]*\}\s*=\s*require\(['"]\.\/core\.cjs['"]\)/s,
|
||||
`${file} must import atomicWriteFileSync from core.cjs`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test('all three files use atomicWriteFileSync at least once', () => {
|
||||
for (const file of targetFiles) {
|
||||
const content = fs.readFileSync(path.join(libDir, file), 'utf-8');
|
||||
assert.match(
|
||||
content,
|
||||
/atomicWriteFileSync\s*\(/,
|
||||
`${file} must contain at least one atomicWriteFileSync call`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user