tests/testthat/test-document.R

# Document Lifecycle Tests (Phase 2)

test_that("am_close() returns NULL invisibly", {
  doc <- am_create()
  result <- am_close(doc)
  expect_null(result)
})

test_that("am_close() can be called twice (idempotent)", {
  doc <- am_create()
  expect_no_error(am_close(doc))
  expect_no_error(am_close(doc))
})

test_that("document operations error after am_close()", {
  doc <- am_create()
  am_put(doc, AM_ROOT, "key", "value")
  am_close(doc)

  expect_error(am_get(doc, AM_ROOT, "key"))
  expect_error(am_save(doc))
  expect_error(am_fork(doc))
})

test_that("am_create() creates a valid document", {
  doc <- am_create()
  expect_s3_class(doc, "am_doc")
  expect_s3_class(doc, "automerge")
})

test_that("am_create() works with NULL actor_id", {
  doc <- am_create(NULL)
  expect_s3_class(doc, "am_doc")
  actor <- am_get_actor(doc)
  expect_type(actor, "raw")
  expect_true(length(actor) > 0)
})

test_that("am_create() works with hex string actor_id", {
  # Create with specific hex actor ID (32 hex chars = 16 bytes)
  hex_id <- paste0(rep("0", 32), collapse = "")
  doc <- am_create(hex_id)
  expect_s3_class(doc, "am_doc")
})

test_that("am_create() works with raw bytes actor_id", {
  # Create with 16 byte actor ID
  actor_bytes <- as.raw(1:16)
  doc <- am_create(actor_bytes)
  expect_s3_class(doc, "am_doc")

  # Verify actor ID was set correctly
  retrieved_actor <- am_get_actor(doc)
  expect_equal(retrieved_actor, actor_bytes)
})

test_that("am_create() errors on invalid actor_id", {
  expect_error(am_create(123), "actor_id must be NULL")
  expect_error(am_create(list()), "actor_id must be NULL")
})

test_that("am_save() returns raw bytes", {
  doc <- am_create()
  bytes <- am_save(doc)
  expect_type(bytes, "raw")
  expect_true(length(bytes) > 0)
})

test_that("am_load() restores a saved document", {
  doc1 <- am_create()
  bytes <- am_save(doc1)

  doc2 <- am_load(bytes)
  expect_s3_class(doc2, "am_doc")
})

test_that("am_load() errors on non-raw input", {
  expect_error(am_load("not raw"), "data must be a raw vector")
  expect_error(am_load(123), "data must be a raw vector")
})

test_that("am_fork() creates independent copy", {
  doc1 <- am_create()
  doc2 <- am_fork(doc1)

  expect_s3_class(doc2, "am_doc")
  # doc1 and doc2 should be different external pointers
  expect_false(identical(doc1, doc2))
})

test_that("am_merge() combines documents", {
  doc1 <- am_create()
  doc2 <- am_create()

  # Merge doc2 into doc1
  result <- am_merge(doc1, doc2)
  expect_identical(result, doc1) # Should return doc1
})

test_that("am_get_actor() returns raw bytes", {
  doc <- am_create()
  actor <- am_get_actor(doc)

  expect_type(actor, "raw")
  expect_true(length(actor) > 0)

  # Can display as hex
  hex_str <- paste(format(actor, width = 2), collapse = "")
  expect_type(hex_str, "character")
  expect_true(nchar(hex_str) > 0)
})

test_that("am_set_actor() changes actor ID", {
  doc <- am_create()
  original_actor <- am_get_actor(doc)

  # Set new random actor ID
  am_set_actor(doc, NULL)
  new_actor <- am_get_actor(doc)

  expect_type(new_actor, "raw")
  # New actor should be different (almost certainly)
  expect_false(identical(original_actor, new_actor))
})

test_that("am_set_actor() works with hex string", {
  doc <- am_create()
  hex_id <- paste0(rep("0", 32), collapse = "")

  am_set_actor(doc, hex_id)
  actor <- am_get_actor(doc)

  expect_type(actor, "raw")
  expect_equal(length(actor), 16)
})

test_that("am_set_actor() works with raw bytes", {
  doc <- am_create()
  new_actor_bytes <- as.raw(seq(16, 1, -1)) # 16 bytes in reverse

  am_set_actor(doc, new_actor_bytes)
  retrieved_actor <- am_get_actor(doc)

  expect_equal(retrieved_actor, new_actor_bytes)
})

test_that("am_get_actor_hex() returns hex string", {
  doc <- am_create()
  actor_hex <- am_get_actor_hex(doc)

  expect_type(actor_hex, "character")
  expect_equal(length(actor_hex), 1)
  expect_true(nchar(actor_hex) > 0)
  expect_match(actor_hex, "^[0-9a-f]+$")
})

test_that("am_get_actor_hex() matches am_get_actor() conversion", {
  doc <- am_create()
  actor_raw <- am_get_actor(doc)
  actor_hex <- am_get_actor_hex(doc)

  manual_hex <- paste(format(actor_raw, width = 2), collapse = "")
  expect_equal(actor_hex, manual_hex)
})

test_that("am_get_actor_hex() works with custom actor ID", {
  doc <- am_create()
  custom_hex <- "0123456789abcdef0123456789abcdef"
  am_set_actor(doc, custom_hex)

  retrieved_hex <- am_get_actor_hex(doc)
  expect_equal(retrieved_hex, custom_hex)
})

test_that("am_commit() works with no arguments", {
  doc <- am_create()
  result <- am_commit(doc)
  expect_identical(result, doc) # Returns doc invisibly
})

test_that("am_commit() works with message", {
  doc <- am_create()
  result <- am_commit(doc, "Test commit message")
  expect_identical(result, doc)
})

test_that("am_commit() works with message and time", {
  doc <- am_create()
  timestamp <- Sys.time()
  result <- am_commit(doc, "Commit with timestamp", timestamp)
  expect_identical(result, doc)
})

test_that("am_commit() errors on invalid message", {
  doc <- am_create()
  expect_error(am_commit(doc, 123), "message must be NULL")
  expect_error(am_commit(doc, c("a", "b")), "message must be NULL")
})

test_that("am_commit() errors on invalid time", {
  doc <- am_create()
  expect_error(am_commit(doc, NULL, "not a time"), "time must be NULL")
  expect_error(am_commit(doc, NULL, 12345), "time must be NULL")
})

test_that("am_rollback() works", {
  doc <- am_create()
  result <- am_rollback(doc)
  expect_identical(result, doc) # Returns doc invisibly
})

test_that("Document lifecycle integration test", {
  # Create document
  doc1 <- am_create()

  # Commit some changes (even though we haven't added data yet)
  am_commit(doc1, "Initial commit")

  # Save to bytes
  bytes <- am_save(doc1)
  expect_type(bytes, "raw")

  # Load from bytes
  doc2 <- am_load(bytes)
  expect_s3_class(doc2, "am_doc")

  # Fork the document
  doc3 <- am_fork(doc2)
  expect_s3_class(doc3, "am_doc")

  # Merge (even though they're identical)
  am_merge(doc1, doc3)

  # All operations succeeded without error
  expect_true(TRUE)
})

test_that("Actor ID round-trip works", {
  doc1 <- am_create()
  actor1 <- am_get_actor(doc1)

  # Create new document with same actor
  doc2 <- am_create(actor1)
  actor2 <- am_get_actor(doc2)

  expect_equal(actor1, actor2)
})

# Edge Cases ------------------------------------------------------------------

test_that("am_commit without changes succeeds", {
  doc <- am_create()
  am_put(doc, AM_ROOT, "key", "value")
  am_commit(doc, "First commit")

  expect_no_error(am_commit(doc, "Second commit without changes"))
})

test_that("am_commit with empty message", {
  doc <- am_create()
  am_put(doc, AM_ROOT, "key", "value")
  am_commit(doc, "")

  expect_equal(am_get(doc, AM_ROOT, "key"), "value")
})

test_that("am_commit with very long message", {
  doc <- am_create()
  am_put(doc, AM_ROOT, "key", "value")

  long_message <- paste(rep("Long message text. ", 500), collapse = "")
  am_commit(doc, long_message)

  expect_equal(am_get(doc, AM_ROOT, "key"), "value")
})

test_that("am_commit with special characters in message", {
  doc <- am_create()
  am_put(doc, AM_ROOT, "key", "value")

  special_message <- "Commit\nwith\ttabs\rand\nnewlines\r\n"
  am_commit(doc, special_message)

  expect_equal(am_get(doc, AM_ROOT, "key"), "value")
})

test_that("am_commit with UTF-8 message", {
  doc <- am_create()
  am_put(doc, AM_ROOT, "key", "value")

  utf8_message <- "提交消息 🎉 Mensaje de confirmación"
  am_commit(doc, utf8_message)

  expect_equal(am_get(doc, AM_ROOT, "key"), "value")
})

test_that("am_merge with same document", {
  doc <- am_create()
  am_put(doc, AM_ROOT, "key", "value")
  am_commit(doc)

  expect_no_error(am_merge(doc, doc))
  expect_equal(am_get(doc, AM_ROOT, "key"), "value")
})

test_that("am_merge with unrelated documents", {
  doc1 <- am_create()
  am_put(doc1, AM_ROOT, "key1", "value1")
  am_commit(doc1)

  doc2 <- am_create()
  am_put(doc2, AM_ROOT, "key2", "value2")
  am_commit(doc2)

  am_merge(doc1, doc2)

  expect_equal(am_get(doc1, AM_ROOT, "key1"), "value1")
  expect_equal(am_get(doc1, AM_ROOT, "key2"), "value2")
})

test_that("am_merge handles concurrent changes to same key", {
  doc <- am_create()
  am_put(doc, AM_ROOT, "key", "original")
  am_commit(doc)

  doc1 <- am_fork(doc)
  doc2 <- am_fork(doc)

  am_put(doc1, AM_ROOT, "key", "change1")
  am_commit(doc1)

  am_put(doc2, AM_ROOT, "key", "change2")
  am_commit(doc2)

  am_merge(doc1, doc2)

  result <- am_get(doc1, AM_ROOT, "key")
  expect_true(result %in% c("change1", "change2"))
})

test_that("am_fork preserves all data", {
  doc1 <- am_create()
  am_put(doc1, AM_ROOT, "key1", "value1")
  am_put(doc1, AM_ROOT, "key2", list(nested = "data"))
  am_commit(doc1, "Initial data")

  doc2 <- am_fork(doc1)

  expect_equal(am_get(doc2, AM_ROOT, "key1"), "value1")

  nested <- am_get(doc2, AM_ROOT, "key2")
  expect_s3_class(nested, "am_object")
  expect_equal(am_get(doc2, nested, "nested"), "data")
})

test_that("am_save on empty document", {
  doc <- am_create()
  bytes <- am_save(doc)

  expect_type(bytes, "raw")
  expect_true(length(bytes) > 0)

  doc2 <- am_load(bytes)
  expect_s3_class(doc2, "am_doc")
  expect_equal(am_length(doc2, AM_ROOT), 0)
})

test_that("am_save/load preserves complex structure", {
  doc1 <- am_create()
  doc1$users <- am_list(
    list(name = "Alice", age = 30L),
    list(name = "Bob", age = 25L)
  )
  doc1$metadata <- list(
    version = "1.0",
    created = Sys.time()
  )

  bytes <- am_save(doc1)
  doc2 <- am_load(bytes)

  users <- doc2$users
  expect_equal(users[[1]]$name, "Alice")
  expect_equal(users[[2]]$name, "Bob")

  metadata <- doc2$metadata
  expect_equal(metadata$version, "1.0")
})

test_that("am_rollback clears uncommitted changes", {
  doc <- am_create()
  am_put(doc, AM_ROOT, "key1", "value1")
  am_commit(doc)

  am_put(doc, AM_ROOT, "key2", "value2")
  expect_equal(am_get(doc, AM_ROOT, "key2"), "value2")

  am_rollback(doc)

  expect_null(am_get(doc, AM_ROOT, "key2"))
  expect_equal(am_get(doc, AM_ROOT, "key1"), "value1")
})

test_that("am_rollback on empty transaction", {
  doc <- am_create()
  am_put(doc, AM_ROOT, "key", "value")
  am_commit(doc)

  expect_no_error(am_rollback(doc))
  expect_equal(am_get(doc, AM_ROOT, "key"), "value")
})

test_that("multiple consecutive commits", {
  doc <- am_create()

  am_put(doc, AM_ROOT, "key1", "value1")
  am_commit(doc, "Commit 1")

  am_put(doc, AM_ROOT, "key2", "value2")
  am_commit(doc, "Commit 2")

  am_put(doc, AM_ROOT, "key3", "value3")
  am_commit(doc, "Commit 3")

  expect_equal(am_get(doc, AM_ROOT, "key1"), "value1")
  expect_equal(am_get(doc, AM_ROOT, "key2"), "value2")
  expect_equal(am_get(doc, AM_ROOT, "key3"), "value3")
})

test_that("am_set_actor changes actor ID", {
  doc <- am_create()
  original_actor <- am_get_actor(doc)

  new_actor <- as.raw(rep(0xFF, 16))
  am_set_actor(doc, new_actor)

  current_actor <- am_get_actor(doc)
  expect_equal(current_actor, new_actor)
  expect_false(identical(current_actor, original_actor))
})

test_that("am_get_actor returns consistent format", {
  doc1 <- am_create()
  actor1 <- am_get_actor(doc1)

  expect_type(actor1, "raw")
  expect_equal(length(actor1), 16)

  doc2 <- am_create(actor1)
  actor2 <- am_get_actor(doc2)

  expect_identical(actor1, actor2)
})

test_that("documents with different actors can merge", {
  doc1 <- am_create()
  am_put(doc1, AM_ROOT, "from", "doc1")
  am_commit(doc1)

  doc2 <- am_create()
  am_put(doc2, AM_ROOT, "from", "doc2")
  am_commit(doc2)

  actor1 <- am_get_actor(doc1)
  actor2 <- am_get_actor(doc2)
  expect_false(identical(actor1, actor2))

  am_merge(doc1, doc2)

  expect_true(am_get(doc1, AM_ROOT, "from") %in% c("doc1", "doc2"))
})

# Historical Query Tests (Phase 6) --------------------------------------------

test_that("am_get_last_local_change() returns NULL for new document", {
  doc <- am_create()
  change <- am_get_last_local_change(doc)
  expect_null(change)
})

test_that("am_get_last_local_change() returns change after commit", {
  doc <- am_create()
  am_put(doc, AM_ROOT, "key", "value")
  am_commit(doc, "Add key")

  change <- am_get_last_local_change(doc)
  expect_type(change, "raw")
  expect_true(length(change) > 0)
})

test_that("am_get_last_local_change() returns most recent change", {
  doc <- am_create()

  am_put(doc, AM_ROOT, "key1", "value1")
  am_commit(doc, "First commit")
  change1 <- am_get_last_local_change(doc)

  am_put(doc, AM_ROOT, "key2", "value2")
  am_commit(doc, "Second commit")
  change2 <- am_get_last_local_change(doc)

  expect_false(identical(change1, change2))
  expect_type(change2, "raw")
})

test_that("am_get_change_by_hash() retrieves existing change", {
  doc <- am_create()
  doc$key <- "value"
  am_commit(doc, "Add key")

  heads <- am_get_heads(doc)
  expect_length(heads, 1)

  change <- am_get_change_by_hash(doc, heads[[1]])
  expect_type(change, "raw")
  expect_true(length(change) > 0)
})

test_that("am_get_change_by_hash() returns NULL for non-existent hash", {
  doc <- am_create()
  doc$key <- "value"
  am_commit(doc)

  fake_hash <- as.raw(rep(0xFF, 32))
  change <- am_get_change_by_hash(doc, fake_hash)
  expect_null(change)
})

test_that("am_get_change_by_hash() errors on invalid hash length", {
  doc <- am_create()
  doc$key <- "value"
  am_commit(doc)

  expect_error(
    am_get_change_by_hash(doc, as.raw(1:10)),
    "Change hash must be exactly 32 bytes"
  )
})

test_that("am_get_change_by_hash() errors on non-raw hash", {
  doc <- am_create()
  expect_error(
    am_get_change_by_hash(doc, "not a raw vector"),
    "hash must be a raw vector"
  )
})

test_that("am_get_changes_added() returns empty list for identical documents", {
  doc1 <- am_create()
  doc1$key <- "value"
  am_commit(doc1)

  doc2 <- am_fork(doc1)

  changes <- am_get_changes_added(doc1, doc2)
  expect_type(changes, "list")
  expect_length(changes, 0)
})

test_that("am_get_changes_added() finds new changes in doc2", {
  doc1 <- am_create()
  doc1$x <- 1
  am_commit(doc1, "Add x")

  doc2 <- am_create()
  doc2$y <- 2
  am_commit(doc2, "Add y")

  changes <- am_get_changes_added(doc1, doc2)
  expect_type(changes, "list")
  expect_length(changes, 1)
  expect_type(changes[[1]], "raw")
})

test_that("am_get_changes_added() can sync documents", {
  doc1 <- am_create()
  doc1$from_doc1 <- "value1"
  am_commit(doc1)

  doc2 <- am_create()
  doc2$from_doc2 <- "value2"
  am_commit(doc2)

  changes <- am_get_changes_added(doc1, doc2)
  am_apply_changes(doc1, changes)

  expect_equal(am_get(doc1, AM_ROOT, "from_doc1"), "value1")
  expect_equal(am_get(doc1, AM_ROOT, "from_doc2"), "value2")
})

test_that("am_get_changes_added() works with forked documents", {
  base <- am_create()
  base$initial <- "value"
  am_commit(base)

  fork1 <- am_fork(base)
  fork1$fork1_data <- "data1"
  am_commit(fork1)

  fork2 <- am_fork(base)
  fork2$fork2_data <- "data2"
  am_commit(fork2)

  changes <- am_get_changes_added(fork1, fork2)
  expect_length(changes, 1)

  am_apply_changes(fork1, changes)
  expect_equal(am_get(fork1, AM_ROOT, "fork2_data"), "data2")
})

test_that("am_fork() at specific head (single) works", {
  doc <- am_create()
  doc$v1 <- "first"
  am_commit(doc, "v1")

  heads_v1 <- am_get_heads(doc)

  doc$v2 <- "second"
  am_commit(doc, "v2")

  # Fork at v1 heads
  fork_at_v1 <- am_fork(doc, heads_v1)
  expect_s3_class(fork_at_v1, "am_doc")

  # Forked document should only have v1 data
  expect_equal(am_get(fork_at_v1, AM_ROOT, "v1"), "first")
  expect_null(am_get(fork_at_v1, AM_ROOT, "v2"))
})

test_that("am_fork() with empty list works like NULL", {
  doc <- am_create()
  doc$key <- "value"
  am_commit(doc)

  fork_current <- am_fork(doc, NULL)
  fork_empty <- am_fork(doc, list())

  expect_equal(
    am_get(fork_current, AM_ROOT, "key"),
    am_get(fork_empty, AM_ROOT, "key")
  )
})

Try the automerge package in your browser

Any scripts or data that you put into this service are public.

automerge documentation built on Feb. 5, 2026, 5:08 p.m.