Bookmarks
Bookmarks
Covers the full bookmark lifecycle: creating named anchors in the event log timeline, retrieving a bookmark by ID, listing all bookmarks in timestamp order, and deleting bookmarks. Bookmarks are the user-facing history waypoint system that replaced the git-backed checkpoint mechanism. They anchor the history collapse algorithm — events between bookmark intervals are eligible for collapse, but bookmarks themselves are never auto-pruned. This spec is P1 because bookmarks are the only user-controlled mechanism for marking significant points in workspace history; a silent failure in create, list, or delete could leave users unable to track major edit milestones.
Bookmarks are workspace-scoped, stored in the bookmarks table (SQLite per-workspace database, V002 migration). Names
must be unique within a workspace (enforced by a UNIQUE INDEX on bookmarks.name, V004 migration). The description
field is optional. Bookmarks anchor to a timestamp at creation time, not to a specific event log entry, so they survive
history collapse.
Preconditions
- HTTP bridge running on port 9990
- A workspace initialized via
initialize_workspacebefore each scenario - Bridge shim injected via
playwright.config.ts - The HTTP bridge exposes bookmark routes covering
create_bookmark,get_bookmark,list_bookmarks, anddelete_bookmark. All scenarios in this spec are exercisable via the bridge.
Scenarios
Seed: seed.spec.ts
1. Create a bookmark with name and description
A bookmark is created successfully when a valid name and optional description are provided.
Steps:
- Call
create_bookmarkwithname = "Before major refactor"anddescription = "Save point before architectural changes". - Observe the returned value.
Expected: The command succeeds and returns a Bookmark object with a valid UUID id,
name = "Before major refactor", description = "Save point before architectural changes", a non-null timestamp (RFC
3339), and a non-null created_at. The returned id is a valid UUID v4 string.
2. Create a bookmark with name only (description is optional)
A bookmark can be created without a description.
Steps:
- Call
create_bookmarkwithname = "Milestone A"and no description (omit the field or passnull). - Observe the returned value.
Expected: The command succeeds. The returned bookmark has name = "Milestone A" and description = null. All other
required fields (id, timestamp, created_at) are present and non-null.
3. Bookmark creation returns the full entity with a generated ID
The create_bookmark command returns the complete Bookmark entity (not null or unit) so callers have the ID
immediately without a follow-up get_bookmark call.
Steps:
- Call
create_bookmarkwithname = "ID Test Bookmark". - Capture the returned
idfrom the response. - Call
get_bookmarkwith thatid.
Expected: Both calls succeed. The Bookmark returned by create_bookmark and the Bookmark returned by
get_bookmark have identical id, name, description, and timestamp values.
4. Get bookmark by ID returns the correct bookmark
get_bookmark retrieves the exact bookmark matching the given ID.
Steps:
- Create two bookmarks: “First Waypoint” and “Second Waypoint”.
- Capture both IDs from the creation responses.
- Call
get_bookmarkwith the ID of “First Waypoint”.
Expected: The returned bookmark has name = "First Waypoint". The name is not “Second Waypoint”. The id in the
response matches the requested ID.
5. Get bookmark with a non-existent ID returns NotFound
Requesting a bookmark by an ID that does not exist must return a NotFound error.
Steps:
- Generate a random UUID (do not create any bookmark with that ID).
- Call
get_bookmarkwith that UUID.
Expected: A NotFound error is returned with the entity described as "Bookmark". No bookmark data is returned.
6. Get bookmark with an invalid UUID returns a validation error
Passing a non-UUID string as the bookmark ID must be rejected before any database access.
Steps:
- Call
get_bookmarkwithid = "not-a-uuid".
Expected: A validation error is returned. No database lookup is performed.
7. List all bookmarks returns them ordered by timestamp ascending
list_bookmarks returns all bookmarks in the workspace sorted by their timestamp field ascending (oldest first).
Steps:
- Create “Alpha Bookmark” and record its
timestamp. - Create “Beta Bookmark” and record its
timestamp. - Create “Gamma Bookmark” and record its
timestamp. - Call
list_bookmarks.
Expected: The returned array contains at least 3 entries. The bookmarks appear in ascending timestamp order: “Alpha Bookmark” first, “Beta Bookmark” second, “Gamma Bookmark” third. The count is at least 3.
8. List bookmarks returns an empty array in a fresh workspace
Before any bookmarks are created, list_bookmarks returns an empty array.
Steps:
- Initialize a fresh workspace (no bookmark operations performed).
- Call
list_bookmarks.
Expected: The returned array is []. The command succeeds (no error).
9. Delete a bookmark removes it from the list
Deleting a bookmark removes it from storage and it no longer appears in list_bookmarks or get_bookmark.
Steps:
- Create “To Be Deleted” and capture its
id. - Call
delete_bookmarkwith thatid. - Call
list_bookmarksand inspect the result. - Attempt to call
get_bookmarkwith the sameid.
Expected: Step 2 succeeds (returns no content / unit). Step 3 returns an array that does not contain “To Be
Deleted”. Step 4 returns a NotFound error.
10. Delete a non-existent bookmark returns NotFound
Attempting to delete a bookmark that does not exist must return a NotFound error.
Steps:
- Generate a random UUID (do not create any bookmark with that ID).
- Call
delete_bookmarkwith that UUID.
Expected: A NotFound error is returned with the entity described as "Bookmark". No bookmark in the workspace is
affected.
11. Multiple bookmarks can exist simultaneously
Creating multiple bookmarks does not interfere with one another. Each has its own unique ID.
Steps:
- Create 5 bookmarks in sequence: “Waypoint 1” through “Waypoint 5”.
- Call
list_bookmarks.
Expected: The list contains exactly 5 entries (assuming no pre-existing bookmarks). All 5 names are present. Each
bookmark has a distinct UUID id. No two entries share the same id.
12. Bookmark name validation rejects empty and whitespace-only names
An empty name or a name consisting entirely of whitespace must be rejected at the domain layer before any database write.
Steps:
- Attempt
create_bookmarkwithname = "". - Observe the error.
- Attempt
create_bookmarkwithname = " "(spaces only). - Observe the error.
Expected: Both attempts return a validation error. The error message for step 1 indicates
"Bookmark name cannot be empty" (or equivalent). The error for step 3 is the same — whitespace-only names are treated
as empty after trimming. No bookmark is written to the database in either case.
13. Bookmark name validation rejects names exceeding 200 characters
A name longer than 200 characters (after trimming) must be rejected.
Steps:
- Construct a string of 201 characters (e.g.,
"a".repeat(201)). - Attempt
create_bookmarkwith that string asname. - Observe the error.
Expected: A validation error is returned with a message indicating the name exceeds the maximum allowed length. No bookmark is written to the database.
14. Bookmark name at maximum length (200 characters) is accepted
A name of exactly 200 characters is valid.
Steps:
- Construct a string of exactly 200 characters (e.g.,
"b".repeat(200)). - Call
create_bookmarkwith that string.
Expected: The command succeeds. The returned bookmark has name equal to the 200-character string.
15. Duplicate bookmark names within the same workspace are rejected
Two bookmarks cannot share the same name within a workspace (enforced by a UNIQUE INDEX on bookmarks.name).
Steps:
- Call
create_bookmarkwithname = "Unique Name". Expect success. - Call
create_bookmarkagain withname = "Unique Name".
Expected: The second create call returns a database or validation error indicating that a bookmark with that name already exists in the workspace. The workspace still contains exactly one bookmark named “Unique Name”.
16. Deleting one bookmark does not affect others
Deleting a specific bookmark must not remove or corrupt other bookmarks.
Steps:
- Create “Keep Me 1”, “Delete Me”, and “Keep Me 2” in sequence.
- Capture the ID of “Delete Me”.
- Call
delete_bookmarkwith “Delete Me“‘s ID. - Call
list_bookmarks.
Expected: The list contains exactly “Keep Me 1” and “Keep Me 2”. “Delete Me” is absent. The timestamp and id of
“Keep Me 1” and “Keep Me 2” are unchanged from their creation values.
Test Data
| Key | Value | Notes |
|---|---|---|
| max_bookmark_name_length | 200 | MAX_BOOKMARK_NAME_LENGTH constant in domain/src/bookmark.rs |
| list_sort_order | timestamp ascending | list_bookmarks returns oldest-first (ORDER BY timestamp ASC) |
| description_required | false | description is Option<String>; null is valid |
| created_by_field | not set by Tauri command | create_bookmark Tauri command passes created_by: None |
| empty_name_error | ”Bookmark name cannot be empty” | Domain validation message for empty/whitespace name |
| max_length_error | ”Bookmark name exceeds maximum length” | Domain validation message for overly long name |
| duplicate_name_error | database error (constraint violation) | SQLite UNIQUE on bookmarks.name; mapped to a Database error variant |
| not_found_entity_label | ”Bookmark” | Used in CommandError::NotFound { entity: "Bookmark" } |
| delete_return_type | unit / null | delete_bookmark returns () on success (no entity returned) |
Notes
- The HTTP bridge exposes bookmark routes (
create_bookmark,get_bookmark,list_bookmarks,delete_bookmark). All 16 scenarios are exercisable via the bridge. - Bookmark names are unique per workspace. The UNIQUE constraint is enforced by a SQLite index added in the V004
workspace migration. Inserting a duplicate maps to
BookmarkRepositoryError::Database(notInvalidData) — the Tauri command translates this toCommandError::Validation. - The
created_byfield exists on theBookmarkdomain entity and in the SQL schema but is never set by the Tauri command layer (create_bookmarkpassescreated_by: None). Tests should expectcreated_by = nullin all command responses. - Bookmarks anchor to
timestamp(the time the bookmark was created), not to anevent_logforeign key. This design choice makes bookmarks survive history collapse operations (delete_before) on the event_log table. Thetimestampandcreated_atfields on a fresh bookmark are set toUtc::now()at creation and will be virtually identical. - The history collapse algorithm uses bookmarks as interval boundaries — but the collapse task itself is a background operation not triggered by any direct user command. Scenarios testing the interaction between bookmarks and collapse require either time-based test fixtures or direct database manipulation.
delete_bookmarkreturns aNotFounderror when the row count afterDELETE WHERE id = ?1is 0. This means attempting to delete the same bookmark twice will produce aNotFounderror on the second call.- The
list_bookmarkscommand has no pagination parameters — it returns all bookmarks. For workspaces with very large numbers of bookmarks, this could become a performance concern, but no limit is currently enforced in the use case or repository.
Was this page helpful?
Thanks for your feedback!