From 8512e6d8a51fc4e1d1e03366ba3e9c53913806a9 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Wed, 18 Mar 2026 03:24:40 -0700 Subject: [PATCH] fix: skip identical session autosave writes (#1732) * test: cover repeated identical session saves * fix: skip identical session snapshot writes --------- Co-authored-by: Lawrence Chen --- Sources/SessionPersistence.swift | 13 +++++++++--- cmuxTests/SessionPersistenceTests.swift | 27 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Sources/SessionPersistence.swift b/Sources/SessionPersistence.swift index b0303d53..188833d4 100644 --- a/Sources/SessionPersistence.swift +++ b/Sources/SessionPersistence.swift @@ -377,9 +377,10 @@ enum SessionPersistenceStore { let directory = fileURL.deletingLastPathComponent() do { try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil) - let encoder = JSONEncoder() - encoder.outputFormatting = [.sortedKeys] - let data = try encoder.encode(snapshot) + let data = try encodedSnapshotData(snapshot) + if let existingData = try? Data(contentsOf: fileURL), existingData == data { + return true + } try data.write(to: fileURL, options: .atomic) return true } catch { @@ -387,6 +388,12 @@ enum SessionPersistenceStore { } } + private static func encodedSnapshotData(_ snapshot: AppSessionSnapshot) throws -> Data { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + return try encoder.encode(snapshot) + } + static func removeSnapshot(fileURL: URL? = nil) { guard let fileURL = fileURL ?? defaultSnapshotFileURL() else { return } try? FileManager.default.removeItem(at: fileURL) diff --git a/cmuxTests/SessionPersistenceTests.swift b/cmuxTests/SessionPersistenceTests.swift index 7d04db1d..8c00c0c1 100644 --- a/cmuxTests/SessionPersistenceTests.swift +++ b/cmuxTests/SessionPersistenceTests.swift @@ -86,6 +86,28 @@ final class SessionPersistenceTests: XCTestCase { ) } + func testSaveSkipsRewritingIdenticalSnapshotData() throws { + let tempDir = FileManager.default.temporaryDirectory + .appendingPathComponent("cmux-session-tests-\(UUID().uuidString)", isDirectory: true) + try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: tempDir) } + + let snapshotURL = tempDir.appendingPathComponent("session.json", isDirectory: false) + let snapshot = makeSnapshot(version: SessionSnapshotSchema.currentVersion) + + XCTAssertTrue(SessionPersistenceStore.save(snapshot, fileURL: snapshotURL)) + let firstFileNumber = try fileNumber(for: snapshotURL) + + XCTAssertTrue(SessionPersistenceStore.save(snapshot, fileURL: snapshotURL)) + let secondFileNumber = try fileNumber(for: snapshotURL) + + XCTAssertEqual( + secondFileNumber, + firstFileNumber, + "Saving identical session data should not replace the snapshot file" + ) + } + func testWorkspaceCustomColorDecodeSupportsMissingLegacyField() throws { var snapshot = makeSnapshot(version: SessionSnapshotSchema.currentVersion) snapshot.windows[0].tabManager.workspaces[0].customColor = nil @@ -780,6 +802,11 @@ final class SessionPersistenceTests: XCTestCase { windows: [window] ) } + + private func fileNumber(for fileURL: URL) throws -> Int { + let attributes = try FileManager.default.attributesOfItem(atPath: fileURL.path) + return try XCTUnwrap(attributes[.systemFileNumber] as? Int) + } } final class SocketListenerAcceptPolicyTests: XCTestCase {