Skip to content
Documentation GitHub
Workspace

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_workspace before each scenario
  • Bridge shim injected via playwright.config.ts
  • The HTTP bridge exposes bookmark routes covering create_bookmark, get_bookmark, list_bookmarks, and delete_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:

  1. Call create_bookmark with name = "Before major refactor" and description = "Save point before architectural changes".
  2. 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:

  1. Call create_bookmark with name = "Milestone A" and no description (omit the field or pass null).
  2. 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:

  1. Call create_bookmark with name = "ID Test Bookmark".
  2. Capture the returned id from the response.
  3. Call get_bookmark with that id.

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:

  1. Create two bookmarks: “First Waypoint” and “Second Waypoint”.
  2. Capture both IDs from the creation responses.
  3. Call get_bookmark with 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:

  1. Generate a random UUID (do not create any bookmark with that ID).
  2. Call get_bookmark with 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:

  1. Call get_bookmark with id = "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:

  1. Create “Alpha Bookmark” and record its timestamp.
  2. Create “Beta Bookmark” and record its timestamp.
  3. Create “Gamma Bookmark” and record its timestamp.
  4. 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:

  1. Initialize a fresh workspace (no bookmark operations performed).
  2. 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:

  1. Create “To Be Deleted” and capture its id.
  2. Call delete_bookmark with that id.
  3. Call list_bookmarks and inspect the result.
  4. Attempt to call get_bookmark with the same id.

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:

  1. Generate a random UUID (do not create any bookmark with that ID).
  2. Call delete_bookmark with 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:

  1. Create 5 bookmarks in sequence: “Waypoint 1” through “Waypoint 5”.
  2. 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:

  1. Attempt create_bookmark with name = "".
  2. Observe the error.
  3. Attempt create_bookmark with name = " " (spaces only).
  4. 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:

  1. Construct a string of 201 characters (e.g., "a".repeat(201)).
  2. Attempt create_bookmark with that string as name.
  3. 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:

  1. Construct a string of exactly 200 characters (e.g., "b".repeat(200)).
  2. Call create_bookmark with 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:

  1. Call create_bookmark with name = "Unique Name". Expect success.
  2. Call create_bookmark again with name = "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:

  1. Create “Keep Me 1”, “Delete Me”, and “Keep Me 2” in sequence.
  2. Capture the ID of “Delete Me”.
  3. Call delete_bookmark with “Delete Me“‘s ID.
  4. 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

KeyValueNotes
max_bookmark_name_length200MAX_BOOKMARK_NAME_LENGTH constant in domain/src/bookmark.rs
list_sort_ordertimestamp ascendinglist_bookmarks returns oldest-first (ORDER BY timestamp ASC)
description_requiredfalsedescription is Option<String>; null is valid
created_by_fieldnot set by Tauri commandcreate_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_errordatabase 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_typeunit / nulldelete_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 (not InvalidData) — the Tauri command translates this to CommandError::Validation.
  • The created_by field exists on the Bookmark domain entity and in the SQL schema but is never set by the Tauri command layer (create_bookmark passes created_by: None). Tests should expect created_by = null in all command responses.
  • Bookmarks anchor to timestamp (the time the bookmark was created), not to an event_log foreign key. This design choice makes bookmarks survive history collapse operations (delete_before) on the event_log table. The timestamp and created_at fields on a fresh bookmark are set to Utc::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_bookmark returns a NotFound error when the row count after DELETE WHERE id = ?1 is 0. This means attempting to delete the same bookmark twice will produce a NotFound error on the second call.
  • The list_bookmarks command 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?