What's wrong
SaveBoundary.Timestamp is documented as "When the save boundary was created" (docs/api-reference.md). docs/serialization.md even shows a custom serializer writing it out. But the timestamp is overwritten with the current time in three places:
- Deserialization.
UndoRedo/Models/SaveBoundary.cs: Timestamp { get; } = DateTimeOffset.Now is get-only and isn't a constructor parameter. System.Text.Json deserializes through the (position, description) primary constructor and discards the serialized timestamp. So JsonUndoRedoSerializer.DeserializeAsync returns boundaries stamped with the load time.
- Restore.
UndoRedo/Services/UndoRedoService.cs (RestoreFromState, around line 383) rebuilds each boundary with _saveBoundaryManager.CreateSaveBoundary(boundary.Position, boundary.Description), which stamps it again. So RestoreFromState(GetCurrentState()) loses timestamps even without a serializer.
- Trim.
UndoRedo/Services/SaveBoundaryManager.cs (AdjustPositions, around line 85) replaces each boundary with new SaveBoundary(newPosition, boundary.Description), which resets the timestamp every time the stack is trimmed.
This is the same kind of bug as #77 (command metadata lost on load), but save boundaries weren't covered by that fix.
Failure scenario (reproduced)
A history panel shows "Saved 3 hours ago". The user closes and reopens the document, and the persisted history is loaded. Every save point now shows the time the history was loaded. The same thing happens during a long session once MaxStackSize trimming kicks in.
[TestMethod]
public async Task SaveBoundaryTimestampSurvivesSaveAndLoad()
{
UndoRedoService service = new(new StackManager(), new SaveBoundaryManager(), new CommandMerger());
service.SetSerializer(new JsonUndoRedoSerializer());
service.Execute(new DelegateCommand("Edit", () => { }, () => { }));
service.MarkAsSaved("Saved");
DateTimeOffset savedAt = service.SaveBoundaries[0].Timestamp;
byte[] data = await service.SaveStateAsync();
await Task.Delay(1500);
UndoRedoService reloaded = new(new StackManager(), new SaveBoundaryManager(), new CommandMerger());
reloaded.SetSerializer(new JsonUndoRedoSerializer());
Assert.IsTrue(await reloaded.LoadStateAsync(data));
Assert.AreEqual(savedAt, reloaded.SaveBoundaries[0].Timestamp);
}
Observed: expected …12:10:36.678…, actual …12:10:38.243… (the load time). A serializer-only round trip, SerializeAsync → DeserializeAsync, fails the same way.
Suggested fix
- Give
SaveBoundary an optional timestamp parameter: SaveBoundary(int position, string? description = null, DateTimeOffset? timestamp = null) with Timestamp = timestamp ?? DateTimeOffset.Now. System.Text.Json then binds it by name.
- In
RestoreFromState, add the restored boundaries as they are (for example through a new ISaveBoundaryManager method that accepts an existing SaveBoundary, or a CreateSaveBoundary overload that takes a timestamp) instead of re-creating them.
- In
AdjustPositions, carry boundary.Timestamp into the replacement boundary.
Acceptance: a boundary's Timestamp is unchanged after a JSON round trip, after RestoreFromState(GetCurrentState()), and after the stack is trimmed.
What's wrong
SaveBoundary.Timestampis documented as "When the save boundary was created" (docs/api-reference.md).docs/serialization.mdeven shows a custom serializer writing it out. But the timestamp is overwritten with the current time in three places:UndoRedo/Models/SaveBoundary.cs:Timestamp { get; } = DateTimeOffset.Nowis get-only and isn't a constructor parameter. System.Text.Json deserializes through the(position, description)primary constructor and discards the serialized timestamp. SoJsonUndoRedoSerializer.DeserializeAsyncreturns boundaries stamped with the load time.UndoRedo/Services/UndoRedoService.cs(RestoreFromState, around line 383) rebuilds each boundary with_saveBoundaryManager.CreateSaveBoundary(boundary.Position, boundary.Description), which stamps it again. SoRestoreFromState(GetCurrentState())loses timestamps even without a serializer.UndoRedo/Services/SaveBoundaryManager.cs(AdjustPositions, around line 85) replaces each boundary withnew SaveBoundary(newPosition, boundary.Description), which resets the timestamp every time the stack is trimmed.This is the same kind of bug as #77 (command metadata lost on load), but save boundaries weren't covered by that fix.
Failure scenario (reproduced)
A history panel shows "Saved 3 hours ago". The user closes and reopens the document, and the persisted history is loaded. Every save point now shows the time the history was loaded. The same thing happens during a long session once
MaxStackSizetrimming kicks in.Observed: expected
…12:10:36.678…, actual…12:10:38.243…(the load time). A serializer-only round trip,SerializeAsync→DeserializeAsync, fails the same way.Suggested fix
SaveBoundaryan optional timestamp parameter:SaveBoundary(int position, string? description = null, DateTimeOffset? timestamp = null)withTimestamp = timestamp ?? DateTimeOffset.Now. System.Text.Json then binds it by name.RestoreFromState, add the restored boundaries as they are (for example through a newISaveBoundaryManagermethod that accepts an existingSaveBoundary, or aCreateSaveBoundaryoverload that takes a timestamp) instead of re-creating them.AdjustPositions, carryboundary.Timestampinto the replacement boundary.Acceptance: a boundary's
Timestampis unchanged after a JSON round trip, afterRestoreFromState(GetCurrentState()), and after the stack is trimmed.