Files
get-shit-done/tests/atomic-write.test.cjs
Tom Boucher 60fa2936dd fix(core): add atomicWriteFileSync to prevent truncated files on kill (#1943)
Replaces direct fs.writeFileSync calls for STATE.md, ROADMAP.md, and
config.json with write-to-temp-then-rename so a process killed mid-write
cannot leave an unparseable truncated file. Falls back to direct write if
rename fails (e.g. cross-device). Adds regression tests for the helper.

Closes #1915

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 17:27:20 -04:00

79 lines
2.7 KiB
JavaScript

/**
* Tests for atomicWriteFileSync helper (issue #1915)
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { createTempDir, cleanup } = require('./helpers.cjs');
const CORE_PATH = path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'core.cjs');
describe('atomicWriteFileSync', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempDir();
});
afterEach(() => {
cleanup(tmpDir);
});
test('is exported from core.cjs', () => {
const core = require(CORE_PATH);
assert.strictEqual(typeof core.atomicWriteFileSync, 'function', 'atomicWriteFileSync must be exported');
});
test('writes correct content to the target file', () => {
const { atomicWriteFileSync } = require(CORE_PATH);
const filePath = path.join(tmpDir, 'test.md');
const content = '# Hello\nworld\n';
atomicWriteFileSync(filePath, content, 'utf-8');
const written = fs.readFileSync(filePath, 'utf-8');
assert.strictEqual(written, content, 'written content must match');
});
test('does not leave .tmp.* files after successful write', () => {
const { atomicWriteFileSync } = require(CORE_PATH);
const filePath = path.join(tmpDir, 'STATE.md');
atomicWriteFileSync(filePath, '# State\n', 'utf-8');
const entries = fs.readdirSync(tmpDir);
const tmpFiles = entries.filter(e => e.includes('.tmp.'));
assert.deepStrictEqual(tmpFiles, [], 'no .tmp.* files should remain after write');
});
test('overwrites an existing file with new content', () => {
const { atomicWriteFileSync } = require(CORE_PATH);
const filePath = path.join(tmpDir, 'config.json');
atomicWriteFileSync(filePath, '{"first":true}', 'utf-8');
atomicWriteFileSync(filePath, '{"second":true}', 'utf-8');
const written = fs.readFileSync(filePath, 'utf-8');
assert.strictEqual(written, '{"second":true}', 'second write must replace first');
});
test('cleans up stale tmp file if present before write', () => {
const { atomicWriteFileSync } = require(CORE_PATH);
const filePath = path.join(tmpDir, 'ROADMAP.md');
// Place a stale tmp file matching the pattern used by atomicWriteFileSync
const staleTmp = filePath + '.tmp.' + process.pid;
fs.writeFileSync(staleTmp, 'stale content', 'utf-8');
atomicWriteFileSync(filePath, '# Roadmap\n', 'utf-8');
const entries = fs.readdirSync(tmpDir);
const tmpFiles = entries.filter(e => e.includes('.tmp.'));
assert.deepStrictEqual(tmpFiles, [], 'stale .tmp.* file must be gone after write');
const written = fs.readFileSync(filePath, 'utf-8');
assert.strictEqual(written, '# Roadmap\n', 'target file must have correct content');
});
});