From dc11fc2fd8acb60cd276640a74ef134e8048b231 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 13 Feb 2026 04:23:22 -0800 Subject: [PATCH] fix: make saveAliases atomic on Unix by skipping unnecessary unlink before rename On Unix/macOS, rename(2) atomically replaces the destination file. The previous code ran unlinkSync before renameSync on all platforms, creating an unnecessary non-atomic window where a crash could lose data. Now the delete-before-rename is gated behind process.platform === 'win32', where rename cannot overwrite an existing file. --- scripts/lib/session-aliases.js | 6 ++++-- tests/lib/session-aliases.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/scripts/lib/session-aliases.js b/scripts/lib/session-aliases.js index f517145..cf9acda 100644 --- a/scripts/lib/session-aliases.js +++ b/scripts/lib/session-aliases.js @@ -110,8 +110,10 @@ function saveAliases(aliases) { // Atomic write: write to temp file, then rename fs.writeFileSync(tempPath, content, 'utf8'); - // On Windows, we need to delete the target file before renaming - if (fs.existsSync(aliasesPath)) { + // On Windows, rename fails with EEXIST if destination exists, so delete first. + // On Unix/macOS, rename(2) atomically replaces the destination — skip the + // delete to avoid an unnecessary non-atomic window between unlink and rename. + if (process.platform === 'win32' && fs.existsSync(aliasesPath)) { fs.unlinkSync(aliasesPath); } fs.renameSync(tempPath, aliasesPath); diff --git a/tests/lib/session-aliases.test.js b/tests/lib/session-aliases.test.js index bc77d41..b36bef6 100644 --- a/tests/lib/session-aliases.test.js +++ b/tests/lib/session-aliases.test.js @@ -801,6 +801,31 @@ function runTests() { 'Original aliases data should be preserved after failed save'); })) passed++; else failed++; + // ── Round 39: atomic overwrite on Unix (no unlink before rename) ── + console.log('\nRound 39: atomic overwrite:'); + + if (test('saveAliases overwrites existing file atomically', () => { + // Create initial aliases + aliases.setAlias('atomic-test', '2026-01-01-abc123-session.tmp'); + const aliasesPath = aliases.getAliasesPath(); + assert.ok(fs.existsSync(aliasesPath), 'Aliases file should exist'); + const sizeBefore = fs.statSync(aliasesPath).size; + assert.ok(sizeBefore > 0, 'Aliases file should have content'); + + // Overwrite with different data + aliases.setAlias('atomic-test-2', '2026-02-01-def456-session.tmp'); + + // The file should still exist and be valid JSON + const content = fs.readFileSync(aliasesPath, 'utf8'); + const parsed = JSON.parse(content); + assert.ok(parsed.aliases['atomic-test'], 'First alias should exist'); + assert.ok(parsed.aliases['atomic-test-2'], 'Second alias should exist'); + + // Cleanup + aliases.deleteAlias('atomic-test'); + aliases.deleteAlias('atomic-test-2'); + })) passed++; else failed++; + // Cleanup — restore both HOME and USERPROFILE (Windows) process.env.HOME = origHome; if (origUserProfile !== undefined) {