Skip to content

Keep save-boundary timestamps through load and restore [minor] - #117

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/undoredo-91-preserve-boundary-timestamps
Sep 28, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
claude/undoredo-91-preserve-boundary-timestamps

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #91

What was wrong

SaveBoundary.Timestamp ("when the save boundary was created") was replaced with the current time in two of the three places the issue lists:

  1. Deserialization. Timestamp was get-only and not a constructor parameter, so System.Text.Json discarded the saved value.
  2. RestoreFromState. Each boundary was re-created through CreateSaveBoundary(position, description), which stamped it again.
  3. Trim. This one is already fixed on main: since UndoToSaveBoundaryAsync undoes to the wrong position when given a SaveBoundary obtained before the stack was trimmed #88, AdjustPositions uses the internal copy constructor, which carries Timestamp over. This PR adds a regression test for it.

Change

  • SaveBoundary: new public constructor SaveBoundary(int position, string? description, DateTimeOffset timestamp), marked [JsonConstructor]. A default timestamp means "now". That is also what STJ passes when saved data has no timestamp, so older data still loads.
  • SaveBoundaryManager.RestoreSaveBoundary (internal): adds a boundary with its original timestamp and marks the initial state as not clean, as CreateSaveBoundary does.
  • UndoRedoService.RestoreFromState: uses RestoreSaveBoundary when the manager is the built-in SaveBoundaryManager. Otherwise it falls back to CreateSaveBoundary.
  • Docs: api-reference.md showed SaveBoundary as a record with a DateTime Timestamp, which it isn't. serialization.md's binary serializer example called a constructor that didn't exist. Both now match the real type.

Design note: why ISaveBoundaryManager is unchanged

A clean fix for (2) would add a member to ISaveBoundaryManager. The library targets netstandard2.0, which has no default interface methods, so a new member would break every external implementer. That calls for a major version bump. This PR keeps the interface as it is and special-cases the built-in manager instead. A custom manager keeps working, but its restored boundaries still get fresh timestamps. If you'd rather add something like ISaveBoundaryManager.RestoreSaveBoundary(SaveBoundary) in a breaking release, the type check in RestoreFromState can go.

The commit is tagged [minor] because it adds a public constructor.

Testing

New tests:

  • JsonSerializer_SerializeDeserialize_PreservesSaveBoundaryTimestamp: a serializer-only round trip
  • UndoRedoService_SaveLoadState_PreservesSaveBoundaryTimestamp: SaveStateAsync → LoadStateAsync. The saved timestamp is rewritten to a fixed past time, so a restamp cannot match by chance
  • UndoRedoService_RestoreFromState_PreservesSaveBoundaryTimestamp
  • JsonSerializer_DeserializeSaveBoundaryWithoutTimestamp_UsesLoadTime: older data without a timestamp still loads
  • SaveBoundary_StackTrimmed_KeepsTimestamp: guards the existing trim behavior

To check that the tests catch the bug, I temporarily reinstated the old behavior (the constructor ignored the timestamp, and restore went back through CreateSaveBoundary). The three load/restore tests failed. With the fix, dotnet test UndoRedo.Test passes 105/105, and dotnet build UndoRedo builds all 7 target frameworks.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TXZMdofSRgS8oXS9RgTRhM


Generated by Claude Code

SaveBoundary.Timestamp was get-only and not a constructor parameter, so
System.Text.Json discarded the saved value and stamped every loaded
boundary with the load time, and RestoreFromState re-created each
boundary through CreateSaveBoundary, which stamped it again.

Add a public SaveBoundary(position, description, timestamp) constructor
marked [JsonConstructor]; a missing timestamp still means now, so older
data loads as before. RestoreFromState adds boundaries with their
original timestamp when the manager is the built-in SaveBoundaryManager;
ISaveBoundaryManager is unchanged, so custom managers keep working and
create boundaries afresh. Trimming already carried the timestamp over;
a test now guards that too. Correct the SaveBoundary shape in the API
reference and the binary serializer example.

Fixes #91

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TXZMdofSRgS8oXS9RgTRhM
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Save-boundary timestamps are reset to "now" on load, restore and stack trim

2 participants