Files
MindSift/internal/collab/orderkey_test.go
T
sandrodachandClaude Opus 5.5 1ea0c78b41
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 50s
feat(notes): operation log, block leases and sync API for collaborative editing
Phase 1 of the collaboration plan (server side):
- internal/collab: block tree with parent IDs and fractional order keys,
  operations with conflict rules (version check, block leases, conflict
  copies instead of lost text, restore on late edits, cycle checks),
  in-memory leases/sessions and long-poll notifier.
- Storage: per-note lock + transaction, append-only effect log (doc_ops),
  idempotent results per opId (doc_op_results), snapshot, log paging,
  central DocRole. Old flat block data is converted to the tree.
- The existing whole-document save (PUT/PATCH) runs as an adapter through
  the same engine, so the current editor keeps working and writes to the log.
- API: GET /docs/{id}/snapshot, POST /docs/{id}/sync, GET /docs/{id}/changes
  (long poll), POST /docs/{id}/leave; DOC_SEQ poke for WebSocket/SSE rooms.
- Backend accepts editor (write) shares for notes and folders; the forced
  read-only migration is gone. The share dialog still offers read only.
- Tests: conflict matrix, order keys, leases, storage adapter round trip,
  idempotency, API protocol incl. long poll, and a randomized simulation
  (4 clients, lost/duplicated requests and responses, offline phases,
  lease expiry) checking convergence, no lost text and exactly-once results.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:47:56 +01:00

91 lines
2.3 KiB
Go

package collab
import (
"math/rand"
"sort"
"testing"
)
func TestKeyBetweenBasics(t *testing.T) {
cases := [][2]string{{"", ""}, {"", "a0"}, {"a0", ""}, {"a0", "a1"}, {"a0", "a0V"}, {"a1", "a2"}, {"a0V", "a1"}, {"Zz", "a0"}, {"az", ""}, {"", "Zz"}}
for _, c := range cases {
k, err := KeyBetween(c[0], c[1])
if err != nil {
t.Fatalf("KeyBetween(%q,%q): %v", c[0], c[1], err)
}
if !ValidOrderKey(k) {
t.Fatalf("KeyBetween(%q,%q) = %q is not a valid key", c[0], c[1], k)
}
if (c[0] != "" && k <= c[0]) || (c[1] != "" && k >= c[1]) {
t.Fatalf("KeyBetween(%q,%q) = %q is not between", c[0], c[1], k)
}
}
if _, err := KeyBetween("a1", "a0"); err == nil {
t.Fatal("reversed range must fail")
}
if _, err := KeyBetween("a0V0", ""); err == nil {
t.Fatal("trailing zero key must be rejected")
}
}
// Random inserts between neighbours must always produce a strictly increasing sequence.
func TestKeyBetweenRandomInserts(t *testing.T) {
r := rand.New(rand.NewSource(42))
keys := []string{}
for i := 0; i < 3000; i++ {
pos := r.Intn(len(keys) + 1)
a, b := "", ""
if pos > 0 {
a = keys[pos-1]
}
if pos < len(keys) {
b = keys[pos]
}
k, err := KeyBetween(a, b)
if err != nil {
t.Fatalf("step %d: %v", i, err)
}
keys = append(keys[:pos], append([]string{k}, keys[pos:]...)...)
}
if !sort.StringsAreSorted(keys) {
t.Fatal("keys are not sorted")
}
for i := 1; i < len(keys); i++ {
if keys[i] == keys[i-1] {
t.Fatalf("duplicate key %q", keys[i])
}
}
}
// Appending at the end, the most common case, must keep keys short.
func TestKeyBetweenAppendStaysShort(t *testing.T) {
last := ""
for i := 0; i < 1000; i++ {
k, _ := KeyBetween(last, "")
last = k
}
if len(last) > 4 {
t.Fatalf("key after 1000 appends too long: %d chars", len(last))
}
}
func TestKeysBetweenBulk(t *testing.T) {
for _, r := range [][2]string{{"", ""}, {"a0", ""}, {"", "a0"}, {"a0", "a1"}} {
keys, err := KeysBetween(r[0], r[1], 500)
if err != nil {
t.Fatalf("%v: %v", r, err)
}
if len(keys) != 500 || !sort.StringsAreSorted(keys) {
t.Fatalf("%v: wrong or unsorted result", r)
}
for i, k := range keys {
if !ValidOrderKey(k) || (i > 0 && keys[i-1] == k) {
t.Fatalf("%v: bad key %q", r, k)
}
if (r[0] != "" && k <= r[0]) || (r[1] != "" && k >= r[1]) {
t.Fatalf("%v: key %q out of range", r, k)
}
}
}
}