refactor(persistence): canonical atomic file writer + notifications.json crash-safety (DC-099) [glm-grade=B]
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s

- src/utils/atomic-write.js: single shared tmp+fsync+rename writer
  (exclusive-create 0600, unique tmp names, cleanup-on-failure,
  best-effort parent-dir fsync after rename for swap durability)
- notification-manager: both write paths (load-time canonicalization
  write-back + saveConfig) converted from plain writeFileSync — a
  crash mid-write can no longer truncate notifications.json
- DC-097/098 test seams migrated to the atomic path; new suite pins
  syscall discipline (order, wx flags, tmp naming, error cleanup,
  dir-fsync swallow)
- 121 suites / 2768 tests green
- Judge: GLM-5.3 cold read grade B/ship (deleg_23f7abad); polish items
  folded: dir-fsync added, header copy-count corrected. Remaining:
  migrate invite/user/share-store private _atomicWriteJSON copies as
  they are touched (queued).

URN: pending (recorded post-commit)
This commit is contained in:
Hermes
2026-08-22 23:28:36 -07:00
parent 8d42eae6ac
commit 80a82c4cae
5 changed files with 341 additions and 19 deletions
@@ -12,6 +12,13 @@ jest.mock('fs', () => ({
readFileSync: jest.fn().mockReturnValue('{}'),
writeFileSync: jest.fn(),
mkdirSync: jest.fn(),
// DC-099 atomic write path (open tmp → write → fsync → close → rename).
openSync: jest.fn().mockReturnValue(3),
writeSync: jest.fn(),
fsyncSync: jest.fn(),
closeSync: jest.fn(),
renameSync: jest.fn(),
unlinkSync: jest.fn(),
}));
jest.mock('nodemailer', () => ({
@@ -86,10 +93,10 @@ describe('DC-097 notification config canonicalization write-back', () => {
expect(nm.config.events['container-down']).toBe(false);
expect(nm.config.events['deploy-success']).toBe(false);
// On-disk write-back: exactly one write of the full canonical config.
expect(fs.writeFileSync).toHaveBeenCalledTimes(1);
const [pathArg, contentsArg] = fs.writeFileSync.mock.calls[0];
expect(pathArg).toBe(NOTIF_FILE);
// On-disk write-back: exactly one atomic write (DC-099: write tmp → fsync → rename).
expect(fs.renameSync).toHaveBeenCalledTimes(1);
expect(fs.renameSync.mock.calls[0][1]).toBe(NOTIF_FILE);
const contentsArg = fs.writeSync.mock.calls[0][1];
const written = JSON.parse(contentsArg);
expect(written.providers.email.username).toBe('legacy-user');
expect(written.providers.email.password).toBe('legacy-pass');
@@ -108,14 +115,15 @@ describe('DC-097 notification config canonicalization write-back', () => {
events: { containerDown: true },
});
const first = loadWithFile(legacy, log);
expect(fs.writeFileSync).toHaveBeenCalledTimes(1);
const canonicalContents = fs.writeFileSync.mock.calls[0][1];
expect(fs.renameSync).toHaveBeenCalledTimes(1);
const canonicalContents = fs.writeSync.mock.calls[0][1];
first.stopHealthDaemon && first.stopHealthDaemon();
fs.writeFileSync.mockClear();
fs.renameSync.mockClear();
fs.writeSync.mockClear();
// Second load against the canonical bytes: no write.
const second = loadWithFile(canonicalContents, log);
expect(fs.writeFileSync).not.toHaveBeenCalled();
expect(fs.renameSync).not.toHaveBeenCalled();
expect(second.config.providers.email.username).toBe('u');
second.stopHealthDaemon && second.stopHealthDaemon();
});
@@ -125,11 +133,12 @@ describe('DC-097 notification config canonicalization write-back', () => {
// Build it by round-tripping: write-back from a minimal legacy file
// produces the canonical full shape; feed those exact bytes back.
const nm = loadWithFile(ser({ enabled: true }), log); // 1 write (defaults fill-in)
const canonicalContents = fs.writeFileSync.mock.calls[0][1];
const canonicalContents = fs.writeSync.mock.calls[0][1];
nm.stopHealthDaemon && nm.stopHealthDaemon();
fs.writeFileSync.mockClear();
fs.renameSync.mockClear();
fs.writeSync.mockClear();
const again = loadWithFile(canonicalContents, log);
expect(fs.writeFileSync).not.toHaveBeenCalled();
expect(fs.renameSync).not.toHaveBeenCalled();
again.stopHealthDaemon && again.stopHealthDaemon();
});
@@ -138,7 +147,7 @@ describe('DC-097 notification config canonicalization write-back', () => {
providers: { email: { user: 'u2', pass: 'p2' } },
events: { workflowDone: true },
});
fs.writeFileSync.mockImplementation(() => { throw new Error('EACCES: permission denied'); });
fs.openSync.mockImplementation(() => { throw new Error('EACCES: permission denied'); });
let nm;
expect(() => { nm = loadWithFile(legacy, log); }).not.toThrow();
expect(nm.config.providers.email.username).toBe('u2');
@@ -153,7 +162,7 @@ describe('DC-097 notification config canonicalization write-back', () => {
fs.existsSync.mockReturnValue(false);
const nm = new NotificationManager(makeCtx(log));
expect(fs.readFileSync).not.toHaveBeenCalled();
expect(fs.writeFileSync).not.toHaveBeenCalled();
expect(fs.renameSync).not.toHaveBeenCalled();
nm.stopHealthDaemon && nm.stopHealthDaemon();
});
});