aboutsummaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/C_API.cpp442
-rw-r--r--test/CursorManager.cpp160
-rw-r--r--test/LifecycleManager.cpp185
-rw-r--r--test/MultigroupCodec.cpp145
-rw-r--r--test/NamespaceReferenceManager.cpp203
-rw-r--r--test/ObjectManager.cpp75
-rw-r--r--test/VM.cpp12
7 files changed, 1218 insertions, 4 deletions
diff --git a/test/C_API.cpp b/test/C_API.cpp
new file mode 100644
index 0000000..f186374
--- /dev/null
+++ b/test/C_API.cpp
@@ -0,0 +1,442 @@
+#include <catch2/catch_test_macros.hpp>
+
+#include "RNT_C_API.h"
+
+#include <cstring>
+#include <string>
+
+// ---------------------------------------------------------------------------
+// End-to-end C API tests covering step 2 (Branch hash-pointer), step 3+4
+// (copy-on-write writes and resolver wiring through rnt_open_handle), and
+// step 5 (session lifecycle and overrides).
+//
+// The C API holds a single process-global runtime; rnt_init is idempotent.
+// Each test uses unique branch and session identifiers so state from one
+// test does not affect another.
+// ---------------------------------------------------------------------------
+
+namespace
+{
+ struct InitGuard {
+ InitGuard() { REQUIRE(rnt_init("memory", nullptr) == 0); }
+ };
+
+ std::string take_string(char* p)
+ {
+ std::string s = p ? p : "";
+ rnt_free_string(p);
+ return s;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Step 2 — Branch hash-pointer model
+// ---------------------------------------------------------------------------
+
+TEST_CASE("rnt_register_branch with empty target creates an unborn branch",
+ "[step2][capi][branch]")
+{
+ InitGuard _;
+ const char* path = "/system/branches/test_step2_unborn";
+ REQUIRE(rnt_register_branch(path, "") == 0);
+
+ rnt_handle_t h = rnt_open_handle(path, nullptr);
+ REQUIRE(h != nullptr);
+
+ char* target = nullptr;
+ REQUIRE(rnt_branch_target(h, &target) == 0);
+ REQUIRE(take_string(target) == "");
+
+ REQUIRE(rnt_close_handle(h) == 0);
+}
+
+TEST_CASE("rnt_register_branch rejects an unregistered target hash",
+ "[step2][capi][branch]")
+{
+ InitGuard _;
+ REQUIRE(rnt_register_branch("/system/branches/test_step2_bad_init",
+ "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")
+ != 0);
+}
+
+TEST_CASE("rnt_branch_advance rejects an unregistered target hash",
+ "[step2][capi][branch]")
+{
+ InitGuard _;
+ const char* bpath = "/system/branches/test_step2_bad_advance";
+ REQUIRE(rnt_register_branch(bpath, "") == 0);
+
+ REQUIRE(rnt_branch_advance(
+ bpath,
+ "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") != 0);
+
+ // Target unchanged.
+ rnt_handle_t h = rnt_open_handle(bpath, nullptr);
+ REQUIRE(h != nullptr);
+ char* target = nullptr;
+ REQUIRE(rnt_branch_target(h, &target) == 0);
+ REQUIRE(take_string(target) == "");
+ rnt_close_handle(h);
+}
+
+TEST_CASE("Writing a tuple advances the branch to a new snapshot",
+ "[step2][step3][capi][branch]")
+{
+ InitGuard _;
+ const char* bpath = "/system/branches/test_step2_advance";
+ const char* rpath = "/system/branches/test_step2_advance/multigroups/public/relations/items";
+ REQUIRE(rnt_register_branch(bpath, "") == 0);
+ REQUIRE(rnt_register_relation(rpath) == 0);
+
+ // After register_relation, the branch is bound to the first snapshot.
+ rnt_handle_t h = rnt_open_handle(bpath, nullptr);
+ REQUIRE(h != nullptr);
+ char* first = nullptr;
+ REQUIRE(rnt_branch_target(h, &first) == 0);
+ const std::string first_hash = take_string(first);
+ REQUIRE(first_hash.size() == 64);
+ rnt_close_handle(h);
+
+ // A tuple write advances the branch again.
+ char* tuple_hash = nullptr;
+ REQUIRE(rnt_link_tuple(rpath, "id=1\n", &tuple_hash) == 0);
+ rnt_free_string(tuple_hash);
+
+ h = rnt_open_handle(bpath, nullptr);
+ REQUIRE(h != nullptr);
+ char* second = nullptr;
+ REQUIRE(rnt_branch_target(h, &second) == 0);
+ const std::string second_hash = take_string(second);
+ rnt_close_handle(h);
+
+ REQUIRE(second_hash.size() == 64);
+ REQUIRE(second_hash != first_hash);
+}
+
+// ---------------------------------------------------------------------------
+// Step 3+4 — Copy-on-write writes and resolver wiring
+// ---------------------------------------------------------------------------
+
+TEST_CASE("link_tuple commits a new snapshot and relation_root reflects it",
+ "[step3][capi][cow]")
+{
+ InitGuard _;
+ const char* bpath = "/system/branches/test_step3_link";
+ const char* rpath = "/system/branches/test_step3_link/multigroups/public/relations/items";
+ REQUIRE(rnt_register_branch(bpath, "") == 0);
+ REQUIRE(rnt_register_relation(rpath) == 0);
+
+ // Empty relation: root is "".
+ char* root = nullptr;
+ REQUIRE(rnt_relation_root(rpath, &root) == 0);
+ REQUIRE(take_string(root) == "");
+
+ char* tuple_hash = nullptr;
+ REQUIRE(rnt_link_tuple(rpath, "id=1\nname=alpha\n", &tuple_hash) == 0);
+ REQUIRE(tuple_hash != nullptr);
+ rnt_free_string(tuple_hash);
+
+ REQUIRE(rnt_relation_root(rpath, &root) == 0);
+ const std::string new_root = take_string(root);
+ REQUIRE(new_root.size() == 64); // a real Merkle root now
+}
+
+TEST_CASE("unlink_tuple and clear_relation walk back through new snapshots",
+ "[step3][capi][cow]")
+{
+ InitGuard _;
+ const char* bpath = "/system/branches/test_step3_unlink";
+ const char* rpath = "/system/branches/test_step3_unlink/multigroups/public/relations/items";
+ REQUIRE(rnt_register_branch(bpath, "") == 0);
+ REQUIRE(rnt_register_relation(rpath) == 0);
+
+ char* th = nullptr;
+ REQUIRE(rnt_link_tuple(rpath, "id=1\n", &th) == 0);
+ const std::string tuple_hash = take_string(th);
+
+ char* root = nullptr;
+ REQUIRE(rnt_relation_root(rpath, &root) == 0);
+ REQUIRE(take_string(root) != "");
+
+ REQUIRE(rnt_unlink_tuple(rpath, tuple_hash.c_str()) == 0);
+ REQUIRE(rnt_relation_root(rpath, &root) == 0);
+ REQUIRE(take_string(root) == "");
+
+ // After re-link and clear, root is empty again.
+ REQUIRE(rnt_link_tuple(rpath, "id=2\n", &th) == 0);
+ rnt_free_string(th);
+ REQUIRE(rnt_clear_relation(rpath) == 0);
+ REQUIRE(rnt_relation_root(rpath, &root) == 0);
+ REQUIRE(take_string(root) == "");
+}
+
+TEST_CASE("rnt_open_handle resolves branch-relative paths to snapshot relations",
+ "[step4][capi][resolver]")
+{
+ InitGuard _;
+ const char* bpath = "/system/branches/test_step4_resolve";
+ const char* rpath = "/system/branches/test_step4_resolve/multigroups/public/relations/items";
+ REQUIRE(rnt_register_branch(bpath, "") == 0);
+ REQUIRE(rnt_register_relation(rpath) == 0);
+
+ char* tuple_hash = nullptr;
+ REQUIRE(rnt_link_tuple(rpath, "id=1\n", &tuple_hash) == 0);
+ rnt_free_string(tuple_hash);
+
+ // Opening the branch-relative path resolves to the snapshot-bound entry,
+ // and a cursor reads the tuple just written.
+ rnt_handle_t h = rnt_open_handle(rpath, nullptr);
+ REQUIRE(h != nullptr);
+ rnt_cursor_t c = rnt_cursor_open(h);
+ REQUIRE(c != nullptr);
+
+ char* row = nullptr;
+ REQUIRE(rnt_cursor_next(c, &row) == 1);
+ REQUIRE(row != nullptr);
+ rnt_free_string(row);
+
+ // Exhausted on the next call.
+ REQUIRE(rnt_cursor_next(c, &row) == 0);
+ REQUIRE(row == nullptr);
+
+ rnt_cursor_close(c);
+ rnt_close_handle(h);
+}
+
+TEST_CASE("Reading an unborn branch's relation fails cleanly",
+ "[step4][capi][resolver]")
+{
+ InitGuard _;
+ const char* bpath = "/system/branches/test_step4_unborn";
+ REQUIRE(rnt_register_branch(bpath, "") == 0);
+
+ // No relation registered: relation_root miss returns nonzero.
+ char* root = nullptr;
+ REQUIRE(rnt_relation_root(
+ "/system/branches/test_step4_unborn/multigroups/public/relations/missing", &root) != 0);
+}
+
+// ---------------------------------------------------------------------------
+// Step 5 — Session lifecycle and overrides
+// ---------------------------------------------------------------------------
+
+TEST_CASE("Each rnt_session_open returns a distinct 64-char hex hash",
+ "[step5][capi][session]")
+{
+ InitGuard _;
+ char* a = nullptr;
+ char* b = nullptr;
+ REQUIRE(rnt_session_open(nullptr, &a) == 0);
+ REQUIRE(rnt_session_open(nullptr, &b) == 0);
+ const std::string ha = take_string(a);
+ const std::string hb = take_string(b);
+ REQUIRE(ha.size() == 64);
+ REQUIRE(hb.size() == 64);
+ REQUIRE(ha != hb);
+
+ REQUIRE(rnt_session_close(ha.c_str()) == 0);
+ REQUIRE(rnt_session_close(hb.c_str()) == 0);
+}
+
+TEST_CASE("rnt_session_close on an unknown hash fails",
+ "[step5][capi][session]")
+{
+ InitGuard _;
+ REQUIRE(rnt_session_close(
+ "0000000000000000000000000000000000000000000000000000000000000000") != 0);
+}
+
+TEST_CASE("Session override redirects branch-relative reads to a different snapshot",
+ "[step5][capi][session][resolver]")
+{
+ InitGuard _;
+ const char* bpath = "/system/branches/test_step5_override";
+ const char* rpath = "/system/branches/test_step5_override/multigroups/public/relations/items";
+ REQUIRE(rnt_register_branch(bpath, "") == 0);
+ REQUIRE(rnt_register_relation(rpath) == 0);
+
+ // Snapshot S1: one tuple.
+ char* th = nullptr;
+ REQUIRE(rnt_link_tuple(rpath, "id=1\n", &th) == 0);
+ rnt_free_string(th);
+
+ // Capture S1's hash and pin it via a session override before the next
+ // commit — without the override the next link_tuple would cascade-collect
+ // S1 as soon as the branch advances away from it.
+ rnt_handle_t bh = rnt_open_handle(bpath, nullptr);
+ REQUIRE(bh != nullptr);
+ char* target = nullptr;
+ REQUIRE(rnt_branch_target(bh, &target) == 0);
+ const std::string s1 = take_string(target);
+ rnt_close_handle(bh);
+
+ char* sess = nullptr;
+ REQUIRE(rnt_session_open(nullptr, &sess) == 0);
+ const std::string sid = take_string(sess);
+ REQUIRE(rnt_session_set_branch(sid.c_str(),
+ "test_step5_override",
+ s1.c_str()) == 0);
+
+ // Advance the branch to S2 — global HEAD moves on; the session override
+ // keeps S1 resident.
+ REQUIRE(rnt_link_tuple(rpath, "id=2\n", &th) == 0);
+ rnt_free_string(th);
+
+ // Session view: still S1 (one tuple).
+ const std::string spath =
+ "/system/sessions/" + sid +
+ "/branches/test_step5_override/multigroups/public/relations/items";
+
+ rnt_handle_t h_session = rnt_open_handle(spath.c_str(), nullptr);
+ REQUIRE(h_session != nullptr);
+ rnt_cursor_t c_session = rnt_cursor_open(h_session);
+ REQUIRE(c_session != nullptr);
+
+ int rows_under_session = 0;
+ char* row = nullptr;
+ while (rnt_cursor_next(c_session, &row) == 1)
+ {
+ ++rows_under_session;
+ rnt_free_string(row);
+ }
+ REQUIRE(rows_under_session == 1);
+
+ rnt_cursor_close(c_session);
+ rnt_close_handle(h_session);
+
+ // Global view sees both tuples.
+ rnt_handle_t h_global = rnt_open_handle(rpath, nullptr);
+ REQUIRE(h_global != nullptr);
+ rnt_cursor_t c_global = rnt_cursor_open(h_global);
+ REQUIRE(c_global != nullptr);
+ int rows_global = 0;
+ while (rnt_cursor_next(c_global, &row) == 1) { ++rows_global; rnt_free_string(row); }
+ REQUIRE(rows_global == 2);
+ rnt_cursor_close(c_global);
+ rnt_close_handle(h_global);
+
+ REQUIRE(rnt_session_close(sid.c_str()) == 0);
+}
+
+TEST_CASE("Clearing a session override falls back to the global branch HEAD",
+ "[step5][capi][session][resolver]")
+{
+ InitGuard _;
+ const char* bpath = "/system/branches/test_step5_clear";
+ const char* rpath = "/system/branches/test_step5_clear/multigroups/public/relations/items";
+ REQUIRE(rnt_register_branch(bpath, "") == 0);
+ REQUIRE(rnt_register_relation(rpath) == 0);
+
+ char* th = nullptr;
+ REQUIRE(rnt_link_tuple(rpath, "id=1\n", &th) == 0);
+ rnt_free_string(th);
+ rnt_handle_t bh = rnt_open_handle(bpath, nullptr);
+ REQUIRE(bh != nullptr);
+ char* target = nullptr;
+ REQUIRE(rnt_branch_target(bh, &target) == 0);
+ const std::string s1 = take_string(target);
+ rnt_close_handle(bh);
+
+ // Same pattern as above: install the override on S1 before advancing.
+ char* sess = nullptr;
+ REQUIRE(rnt_session_open(nullptr, &sess) == 0);
+ const std::string sid = take_string(sess);
+ REQUIRE(rnt_session_set_branch(sid.c_str(), "test_step5_clear", s1.c_str()) == 0);
+
+ REQUIRE(rnt_link_tuple(rpath, "id=2\n", &th) == 0);
+ rnt_free_string(th);
+
+ // Clear the override.
+ REQUIRE(rnt_session_set_branch(sid.c_str(), "test_step5_clear", "") == 0);
+
+ // Now the session sees the global view (two tuples).
+ const std::string spath =
+ "/system/sessions/" + sid +
+ "/branches/test_step5_clear/multigroups/public/relations/items";
+ rnt_handle_t h = rnt_open_handle(spath.c_str(), nullptr);
+ REQUIRE(h != nullptr);
+ rnt_cursor_t c = rnt_cursor_open(h);
+ REQUIRE(c != nullptr);
+ int rows = 0;
+ char* row = nullptr;
+ while (rnt_cursor_next(c, &row) == 1) { ++rows; rnt_free_string(row); }
+ REQUIRE(rows == 2);
+ rnt_cursor_close(c);
+ rnt_close_handle(h);
+
+ REQUIRE(rnt_session_close(sid.c_str()) == 0);
+}
+
+TEST_CASE("rnt_session_set_branch rejects an unregistered snapshot",
+ "[step5][capi][session]")
+{
+ InitGuard _;
+ char* sess = nullptr;
+ REQUIRE(rnt_session_open(nullptr, &sess) == 0);
+ const std::string sid = take_string(sess);
+
+ REQUIRE(rnt_session_set_branch(
+ sid.c_str(),
+ "anything",
+ "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") != 0);
+
+ REQUIRE(rnt_session_close(sid.c_str()) == 0);
+}
+
+// ---------------------------------------------------------------------------
+// Multi-mg-per-branch — sibling multigroups under one branch evolve
+// independently. Inserting into one mg must not perturb the other's hash
+// or its enumerable relation set.
+// ---------------------------------------------------------------------------
+
+TEST_CASE("Two multigroups under one branch advance independently",
+ "[capi][multi-mg]")
+{
+ InitGuard _;
+ const std::string bpath = "/system/branches/test_multi_mg";
+ REQUIRE(rnt_register_branch(bpath.c_str(), "") == 0);
+
+ // Register one relation in each of two distinct multigroups.
+ REQUIRE(rnt_register_relation(
+ (bpath + "/multigroups/warehouse/relations/orders").c_str()) == 0);
+ REQUIRE(rnt_register_relation(
+ (bpath + "/multigroups/audit/relations/events").c_str()) == 0);
+
+ char* mgs_raw = nullptr;
+ REQUIRE(rnt_list_branch_multigroups(bpath.c_str(), &mgs_raw) == 0);
+ const std::string mgs = take_string(mgs_raw);
+ REQUIRE(mgs.find("warehouse\t") != std::string::npos);
+ REQUIRE(mgs.find("audit\t") != std::string::npos);
+
+ // Insert into warehouse; record audit's root before/after.
+ char* audit_before = nullptr;
+ REQUIRE(rnt_list_relations(
+ (bpath + "/multigroups/audit").c_str(), &audit_before) == 0);
+ const std::string audit_before_s = take_string(audit_before);
+
+ char* th = nullptr;
+ REQUIRE(rnt_link_tuple((bpath + "/multigroups/warehouse/relations/orders").c_str(),
+ "id=1\n", &th) == 0);
+ rnt_free_string(th);
+
+ char* audit_after = nullptr;
+ REQUIRE(rnt_list_relations(
+ (bpath + "/multigroups/audit").c_str(), &audit_after) == 0);
+ const std::string audit_after_s = take_string(audit_after);
+
+ // audit's relation set is byte-identical: the warehouse-side mutation
+ // touched only the warehouse subtree at the multigroup level.
+ REQUIRE(audit_before_s == audit_after_s);
+
+ // warehouse/orders is now non-empty.
+ char* root = nullptr;
+ REQUIRE(rnt_relation_root(
+ (bpath + "/multigroups/warehouse/relations/orders").c_str(), &root) == 0);
+ REQUIRE(!take_string(root).empty());
+
+ // audit/events still empty.
+ char* root2 = nullptr;
+ REQUIRE(rnt_relation_root(
+ (bpath + "/multigroups/audit/relations/events").c_str(), &root2) == 0);
+ REQUIRE(take_string(root2).empty());
+}
diff --git a/test/CursorManager.cpp b/test/CursorManager.cpp
new file mode 100644
index 0000000..8c7de1a
--- /dev/null
+++ b/test/CursorManager.cpp
@@ -0,0 +1,160 @@
+#include <catch2/catch_test_macros.hpp>
+
+#include "CursorManager.h"
+#include "HandlerManager.h"
+#include "IdentityManager.h"
+#include "InMemoryBackend.h"
+#include "LifecycleManager.h"
+#include "Merkle.h"
+#include "MultigroupCodec.h"
+#include "NamespaceReferenceManager.h"
+#include "ObjectManager.h"
+#include "PermissionsManager.h"
+#include "TupleCodec.h"
+
+#include <memory>
+
+// ---------------------------------------------------------------------------
+// Step 6 / cursor-independence follow-up
+//
+// Two things to verify:
+// 1. A cursor opened on a /system/snapshots/<H>/relations/<n> relation
+// pins the parent Multigroup for its lifetime; releasing the cursor
+// releases the pin.
+// 2. Closing the handle a cursor was opened from no longer dereferences
+// a freed handle pointer inside Next() — the cursor captures the type
+// label and the ephemeral generator at Open time, so iteration after
+// the handle's death is degenerate but safe.
+// ---------------------------------------------------------------------------
+
+namespace
+{
+ std::unique_ptr<nt::ObjectManager::object_type> make_type(OBJECT_TYPE label)
+ {
+ auto t = std::make_unique<nt::ObjectManager::object_type>();
+ t->label = label;
+ t->methods = { OPEN, CLOSE };
+ return t;
+ }
+
+ std::unique_ptr<nt::ObjectManager::ephemeral_object_type>
+ make_constant_eph_type(std::vector<nt::Attribute> attrs)
+ {
+ auto t = std::make_unique<nt::ObjectManager::ephemeral_object_type>();
+ t->label = EPHEMERAL_RELATION;
+ t->methods = { OPEN, CLOSE };
+ t->cardinality = nt::ObjectManager::ephemeral_object_type::Cardinality::Finite;
+ t->generator = [attrs](const std::vector<std::string>&,
+ std::size_t offset, std::size_t limit)
+ -> std::vector<nt::Tuple>
+ {
+ if (offset > 0 || limit == 0) return {};
+ return { nt::Tuple(attrs) };
+ };
+ return t;
+ }
+
+ struct Stack {
+ nt::InMemoryBackend backend;
+ nt::ObjectManager objects;
+ nt::PermissionsManager permissions;
+ nt::IdentityManager identities;
+ nt::LifecycleManager lifecycles { objects, backend };
+ nt::NamespaceReferenceManager references { objects, backend };
+ nt::HandlerManager handler { objects, permissions, identities,
+ lifecycles, references };
+ nt::CursorManager cursors { backend, &lifecycles, &objects };
+ };
+}
+
+TEST_CASE("Cursor pins the parent snapshot and unpins on close",
+ "[step6][cursor][pin]")
+{
+ Stack s;
+
+ // Stand up a minimal snapshot with one relation.
+ auto& om = s.objects;
+ om.Register({"system", "snapshots", "Hcur"},
+ std::make_unique<nt::ObjectManager::Multigroup>(), make_type(MULTIGROUP));
+ om.Register({"system", "snapshots", "Hcur", "relations", "items"},
+ std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
+
+ auto* mg_entry = om.Find({"system", "snapshots", "Hcur"});
+ auto* rel_entry = om.Find({"system", "snapshots", "Hcur", "relations", "items"});
+
+ // Parent's pin on the child Relation (mirrors register_snapshot in the C
+ // API). An external pin on the Multigroup simulates a branch holding the
+ // snapshot — without it, closing the cursor would drop ref_count to 0 and
+ // cascade-collect mg_entry mid-test.
+ s.lifecycles.Pin(rel_entry);
+ s.lifecycles.Pin(mg_entry);
+
+ // Open a handle and a cursor on the child Relation.
+ int conn = 1;
+ auto* h = s.handler.Open({"system", "snapshots", "Hcur", "relations", "items"}, &conn);
+ REQUIRE(h != nullptr);
+ REQUIRE(mg_entry->head->reference_count == 1); // external pin only
+
+ auto* c = s.cursors.Open(h);
+ REQUIRE(c != nullptr);
+ REQUIRE(c->pinned_snapshot == mg_entry);
+ REQUIRE(mg_entry->head->reference_count == 2); // external + cursor pin
+
+ s.cursors.Close(c);
+ REQUIRE(mg_entry->head->reference_count == 1); // back to external only
+
+ s.handler.Close(h);
+ s.lifecycles.Unpin(mg_entry); // releases the external pin; cascade-collects.
+}
+
+TEST_CASE("Closing the handle mid-iteration does not crash an ephemeral cursor",
+ "[step6][cursor][handle-independence]")
+{
+ Stack s;
+ // Register an ephemeral relation; its generator yields one tuple.
+ s.objects.Register({"eph"},
+ std::make_unique<nt::ObjectManager::Relation>(),
+ make_constant_eph_type({{"k", "v"}}));
+
+ int conn = 1;
+ auto* h = s.handler.Open({"eph"}, &conn);
+ REQUIRE(h != nullptr);
+ auto* c = s.cursors.Open(h);
+ REQUIRE(c != nullptr);
+
+ // Ephemeral cursors start exhausted; reset to simulate a JOIN probe.
+ c->exhausted = false;
+ c->args = {};
+
+ // Now violate the documented contract: close the handle BEFORE Next().
+ // The cursor must still produce its tuple from the captured generator
+ // without dereferencing the freed handle.
+ s.handler.Close(h);
+
+ auto* tuple = s.cursors.Next(c);
+ REQUIRE(tuple != nullptr);
+ bool found = false;
+ for (const auto& a : tuple->attrs())
+ if (a.name == "k" && a.value == "v") { found = true; break; }
+ REQUIRE(found);
+
+ s.cursors.Close(c);
+}
+
+TEST_CASE("Cursor opened against a non-snapshot relation does not pin anything",
+ "[step6][cursor][pin]")
+{
+ Stack s;
+ s.objects.Register({"plain", "rel"},
+ std::make_unique<nt::ObjectManager::Relation>(),
+ make_type(RELATION));
+ int conn = 1;
+ auto* h = s.handler.Open({"plain", "rel"}, &conn);
+ REQUIRE(h != nullptr);
+ auto* c = s.cursors.Open(h);
+ REQUIRE(c != nullptr);
+ REQUIRE(c->pinned_snapshot == nullptr);
+
+ s.cursors.Close(c);
+ s.handler.Close(h);
+}
diff --git a/test/LifecycleManager.cpp b/test/LifecycleManager.cpp
new file mode 100644
index 0000000..aa75e14
--- /dev/null
+++ b/test/LifecycleManager.cpp
@@ -0,0 +1,185 @@
+#include <catch2/catch_test_macros.hpp>
+
+#include "InMemoryBackend.h"
+#include "LifecycleManager.h"
+#include "ObjectManager.h"
+
+#include <memory>
+
+// ---------------------------------------------------------------------------
+// Step 6 — Pin/Unpin and the cascade
+//
+// LifecycleManager owns object lifetime. Pin/Unpin adjust reference_count;
+// Monitor/Unmonitor adjust handle_count. Both decrement paths call into
+// TryCollect, which gates GC on the joint condition (both counters zero)
+// and, for MULTIGROUP entries, cascades through CascadeMultigroup to
+// unpin every Relation registered under their snapshot subtree.
+// ---------------------------------------------------------------------------
+
+namespace
+{
+ std::unique_ptr<nt::ObjectManager::object_type> make_type(OBJECT_TYPE label)
+ {
+ auto t = std::make_unique<nt::ObjectManager::object_type>();
+ t->label = label;
+ t->methods = { OPEN, CLOSE };
+ return t;
+ }
+}
+
+TEST_CASE("Pin and Unpin adjust reference_count symmetrically",
+ "[step6][lifecycle][pin]")
+{
+ nt::InMemoryBackend store;
+ nt::ObjectManager om;
+ nt::LifecycleManager lm(om, store);
+ om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
+ auto* entry = om.Find({"r1"});
+ REQUIRE(entry->head->reference_count == 0);
+
+ lm.Pin(entry);
+ lm.Pin(entry);
+ REQUIRE(entry->head->reference_count == 2);
+
+ lm.Unpin(entry);
+ REQUIRE(entry->head->reference_count == 1);
+
+ // Still alive at ref_count=1.
+ REQUIRE(om.Find({"r1"}) != nullptr);
+}
+
+TEST_CASE("Pin on nullptr is a no-op", "[step6][lifecycle][pin]")
+{
+ nt::InMemoryBackend store;
+ nt::ObjectManager om;
+ nt::LifecycleManager lm(om, store);
+ REQUIRE_NOTHROW(lm.Pin(nullptr));
+ REQUIRE_NOTHROW(lm.Unpin(nullptr));
+}
+
+TEST_CASE("Unpin below zero does not underflow", "[step6][lifecycle][pin]")
+{
+ nt::InMemoryBackend store;
+ nt::ObjectManager om;
+ nt::LifecycleManager lm(om, store);
+ om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
+ auto* entry = om.Find({"r1"});
+
+ lm.Unpin(entry);
+ REQUIRE(entry->head->reference_count == 0);
+}
+
+TEST_CASE("GC is gated on both counters reaching zero", "[step6][lifecycle][gc]")
+{
+ nt::InMemoryBackend store;
+ nt::ObjectManager om;
+ nt::LifecycleManager lm(om, store);
+ om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
+ auto* entry = om.Find({"r1"});
+
+ lm.Monitor(entry);
+ lm.Pin(entry);
+
+ // handle_count=1, ref_count=1.
+ lm.Unpin(entry); // ref_count → 0, but handle_count still 1.
+ REQUIRE(om.Find({"r1"}) != nullptr);
+
+ lm.Pin(entry);
+ lm.Unmonitor(entry); // handle_count → 0, but ref_count still 1.
+ REQUIRE(om.Find({"r1"}) != nullptr);
+
+ lm.Unpin(entry); // both zero → GC.
+ REQUIRE(om.Find({"r1"}) == nullptr);
+}
+
+TEST_CASE("Unmonitor that brings both counters to zero collects the entry",
+ "[step6][lifecycle][gc]")
+{
+ nt::InMemoryBackend store;
+ nt::ObjectManager om;
+ nt::LifecycleManager lm(om, store);
+ om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
+ auto* entry = om.Find({"r1"});
+
+ lm.Monitor(entry);
+ REQUIRE(entry->head->handle_count == 1);
+ lm.Unmonitor(entry);
+ REQUIRE(om.Find({"r1"}) == nullptr);
+}
+
+TEST_CASE("IsEligibleForGC reflects the joint-zero rule",
+ "[step6][lifecycle][gc]")
+{
+ nt::InMemoryBackend store;
+ nt::ObjectManager om;
+ nt::LifecycleManager lm(om, store);
+ om.Register({"r1"}, std::make_unique<nt::ObjectManager::Relation>(), make_type(RELATION));
+ auto* entry = om.Find({"r1"});
+
+ REQUIRE(lm.IsEligibleForGC(entry));
+ lm.Pin(entry);
+ REQUIRE_FALSE(lm.IsEligibleForGC(entry));
+}
+
+TEST_CASE("CascadeMultigroup unpins child Relations on snapshot collection",
+ "[step6][lifecycle][cascade]")
+{
+ nt::InMemoryBackend store;
+ nt::ObjectManager om;
+ nt::LifecycleManager lm(om, store);
+
+ om.Register({"system", "snapshots", "H1"},
+ std::make_unique<nt::ObjectManager::Multigroup>(), make_type(MULTIGROUP));
+ auto* mg_entry = om.Find({"system", "snapshots", "H1"});
+
+ for (const auto& n : { std::string{"foo"}, std::string{"bar"} })
+ {
+ om.Register({"system", "snapshots", "H1", "relations", n},
+ std::make_unique<nt::ObjectManager::Relation>(),
+ make_type(RELATION));
+ // The parent Multigroup's pin on each child, mirroring what
+ // register_snapshot does in the C API layer.
+ lm.Pin(om.Find({"system", "snapshots", "H1", "relations", n}));
+ }
+ REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "foo"}) != nullptr);
+ REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "bar"}) != nullptr);
+
+ // One external pin on the Multigroup (e.g. a branch). Release it: cascade.
+ lm.Pin(mg_entry);
+ lm.Unpin(mg_entry);
+
+ REQUIRE(om.Find({"system", "snapshots", "H1"}) == nullptr);
+ REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "foo"}) == nullptr);
+ REQUIRE(om.Find({"system", "snapshots", "H1", "relations", "bar"}) == nullptr);
+}
+
+TEST_CASE("Cascade survives a child with an open handle",
+ "[step6][lifecycle][cascade]")
+{
+ nt::InMemoryBackend store;
+ nt::ObjectManager om;
+ nt::LifecycleManager lm(om, store);
+
+ om.Register({"system", "snapshots", "H2"},
+ std::make_unique<nt::ObjectManager::Multigroup>(), make_type(MULTIGROUP));
+ auto* mg_entry = om.Find({"system", "snapshots", "H2"});
+
+ om.Register({"system", "snapshots", "H2", "relations", "live"},
+ std::make_unique<nt::ObjectManager::Relation>(),
+ make_type(RELATION));
+ auto* child = om.Find({"system", "snapshots", "H2", "relations", "live"});
+ lm.Pin(child); // parent's pin
+ lm.Monitor(child); // simulate an open handle on the child
+
+ // Drop the Multigroup's external ref: cascade unpins child (ref → 0),
+ // but handle_count is still 1 — child must survive.
+ lm.Pin(mg_entry);
+ lm.Unpin(mg_entry);
+
+ REQUIRE(om.Find({"system", "snapshots", "H2"}) == nullptr);
+ REQUIRE(om.Find({"system", "snapshots", "H2", "relations", "live"}) != nullptr);
+
+ // Closing the handle now releases the child.
+ lm.Unmonitor(child);
+ REQUIRE(om.Find({"system", "snapshots", "H2", "relations", "live"}) == nullptr);
+}
diff --git a/test/MultigroupCodec.cpp b/test/MultigroupCodec.cpp
new file mode 100644
index 0000000..25c8b53
--- /dev/null
+++ b/test/MultigroupCodec.cpp
@@ -0,0 +1,145 @@
+#include <catch2/catch_test_macros.hpp>
+
+#include "InMemoryBackend.h"
+#include "MultigroupCodec.h"
+
+#include <string>
+#include <vector>
+
+// ---------------------------------------------------------------------------
+// Multigroup snapshot — Merkle<string> tree mapping relation name → relation root.
+//
+// The codec is a thin facade over Merkle<std::string>; these tests confirm
+// the tree is order-independent (it sorts intrinsically), that mutations are
+// path-localised (sibling entries are untouched), and that snapshots can be
+// re-enumerated from a cold backend by hash alone.
+// ---------------------------------------------------------------------------
+
+namespace {
+
+// Pad a short label into a deterministic 64-char hex string so the codec can
+// decode it as a 32-byte payload.
+std::string mk_root(char c, char d = '0')
+{
+ std::string h(64, '0');
+ h[0] = c;
+ h[1] = d;
+ return h;
+}
+
+} // namespace
+
+TEST_CASE("Build is order-independent — tree sorts intrinsically",
+ "[multigroup-codec]")
+{
+ nt::InMemoryBackend a, b;
+
+ std::vector<nt::MultigroupCodec::RelationEntry> in_order = {
+ {"alpha", mk_root('a')},
+ {"bravo", mk_root('b')},
+ {"charlie", mk_root('c')},
+ };
+ std::vector<nt::MultigroupCodec::RelationEntry> shuffled = {
+ {"charlie", mk_root('c')},
+ {"alpha", mk_root('a')},
+ {"bravo", mk_root('b')},
+ };
+
+ REQUIRE(nt::MultigroupCodec::Build(a, in_order)
+ == nt::MultigroupCodec::Build(b, shuffled));
+}
+
+TEST_CASE("Build distinguishes content changes", "[multigroup-codec]")
+{
+ nt::InMemoryBackend backend;
+
+ std::vector<nt::MultigroupCodec::RelationEntry> base = {{"foo", mk_root('a')}};
+ std::vector<nt::MultigroupCodec::RelationEntry> different_root = {{"foo", mk_root('b')}};
+ std::vector<nt::MultigroupCodec::RelationEntry> different_name = {{"bar", mk_root('a')}};
+ std::vector<nt::MultigroupCodec::RelationEntry> extra_member = {
+ {"foo", mk_root('a')}, {"bar", mk_root('b')}
+ };
+
+ const auto h_base = nt::MultigroupCodec::Build(backend, base);
+ REQUIRE(h_base != nt::MultigroupCodec::Build(backend, different_root));
+ REQUIRE(h_base != nt::MultigroupCodec::Build(backend, different_name));
+ REQUIRE(h_base != nt::MultigroupCodec::Build(backend, extra_member));
+}
+
+TEST_CASE("Build then List round-trips entries in key-sorted order",
+ "[multigroup-codec]")
+{
+ nt::InMemoryBackend backend;
+
+ std::vector<nt::MultigroupCodec::RelationEntry> entries = {
+ {"foo", mk_root('a', '1')},
+ {"bar", mk_root('b', '2')},
+ {"baz", mk_root('c', '3')},
+ };
+ const auto root = nt::MultigroupCodec::Build(backend, entries);
+
+ auto listed = nt::MultigroupCodec::List(backend, root);
+ REQUIRE(listed.size() == 3);
+ REQUIRE(listed[0].first == "bar");
+ REQUIRE(listed[1].first == "baz");
+ REQUIRE(listed[2].first == "foo");
+ REQUIRE(listed[0].second == mk_root('b', '2'));
+}
+
+TEST_CASE("Lookup returns payload by name", "[multigroup-codec]")
+{
+ nt::InMemoryBackend backend;
+
+ std::vector<nt::MultigroupCodec::RelationEntry> entries = {
+ {"foo", mk_root('a')},
+ {"bar", mk_root('b')},
+ };
+ const auto root = nt::MultigroupCodec::Build(backend, entries);
+
+ REQUIRE(nt::MultigroupCodec::Lookup(backend, root, "foo") == mk_root('a'));
+ REQUIRE(nt::MultigroupCodec::Lookup(backend, root, "bar") == mk_root('b'));
+ REQUIRE(nt::MultigroupCodec::Lookup(backend, root, "missing").empty());
+}
+
+TEST_CASE("InsertOne mutates one entry; sibling subtree is untouched",
+ "[multigroup-codec]")
+{
+ nt::InMemoryBackend backend;
+
+ std::vector<nt::MultigroupCodec::RelationEntry> entries = {
+ {"foo", mk_root('a')},
+ {"bar", mk_root('b')},
+ };
+ const auto root0 = nt::MultigroupCodec::Build(backend, entries);
+
+ // Mutate foo only; bar's payload must round-trip unchanged.
+ const auto root1 = nt::MultigroupCodec::InsertOne(
+ backend, root0, "foo", mk_root('d'));
+ REQUIRE(root1 != root0);
+ REQUIRE(nt::MultigroupCodec::Lookup(backend, root1, "foo") == mk_root('d'));
+ REQUIRE(nt::MultigroupCodec::Lookup(backend, root1, "bar") == mk_root('b'));
+}
+
+TEST_CASE("RemoveOne deletes one entry; sibling preserved",
+ "[multigroup-codec]")
+{
+ nt::InMemoryBackend backend;
+
+ std::vector<nt::MultigroupCodec::RelationEntry> entries = {
+ {"foo", mk_root('a')},
+ {"bar", mk_root('b')},
+ };
+ const auto root0 = nt::MultigroupCodec::Build(backend, entries);
+
+ const auto root1 = nt::MultigroupCodec::RemoveOne(backend, root0, "foo");
+ REQUIRE(nt::MultigroupCodec::Lookup(backend, root1, "foo").empty());
+ REQUIRE(nt::MultigroupCodec::Lookup(backend, root1, "bar") == mk_root('b'));
+}
+
+TEST_CASE("Empty multigroup builds to an empty root", "[multigroup-codec]")
+{
+ nt::InMemoryBackend backend;
+ std::vector<nt::MultigroupCodec::RelationEntry> empty;
+ REQUIRE(nt::MultigroupCodec::Build(backend, empty).empty());
+ REQUIRE(nt::MultigroupCodec::List(backend, "").empty());
+}
diff --git a/test/NamespaceReferenceManager.cpp b/test/NamespaceReferenceManager.cpp
new file mode 100644
index 0000000..27f0dcd
--- /dev/null
+++ b/test/NamespaceReferenceManager.cpp
@@ -0,0 +1,203 @@
+#include <catch2/catch_test_macros.hpp>
+
+#include "InMemoryBackend.h"
+#include "Merkle.h"
+#include "NamespaceReferenceManager.h"
+#include "ObjectManager.h"
+
+#include <memory>
+
+// ---------------------------------------------------------------------------
+// Resolver — two-level reparse through the branch tree.
+//
+// NamespaceReferenceManager::Resolve rewrites
+// /system/branches/<name>/multigroups/<mg>/<sub>...
+// into
+// /system/snapshots/<mg_hash>/<sub>...
+// by reading branch.target_hash (a Merkle<string> root) from storage and
+// looking up <mg>. Session paths consult the per-session override map first
+// and fall back to the global branch when no override is set.
+// ---------------------------------------------------------------------------
+
+namespace {
+
+using Path = std::vector<std::string>;
+
+// Pad a label into a deterministic 64-char hex string so the merkle codec can
+// decode it as a 32-byte payload.
+std::string mk_hash(char c, char d = '0')
+{
+ std::string h(64, '0');
+ h[0] = c;
+ h[1] = d;
+ return h;
+}
+
+std::unique_ptr<nt::ObjectManager::object_type> make_type(OBJECT_TYPE label)
+{
+ auto t = std::make_unique<nt::ObjectManager::object_type>();
+ t->label = label;
+ t->methods = { OPEN, CLOSE };
+ return t;
+}
+
+void register_branch(nt::ObjectManager& om,
+ const std::string& name,
+ const std::string& target_hash)
+{
+ auto br = std::make_unique<nt::ObjectManager::Branch>();
+ br->name = name;
+ br->target_hash = target_hash;
+ om.Register({"system", "branches", name}, std::move(br), make_type(BRANCH));
+}
+
+void register_session(nt::ObjectManager& om,
+ const std::string& id,
+ std::map<std::string, std::string> overrides = {})
+{
+ auto s = std::make_unique<nt::ObjectManager::Session>();
+ s->branch_overrides = std::move(overrides);
+ om.Register({"system", "sessions", id}, std::move(s), make_type(SESSION));
+}
+
+// Builds a branch tree with a single (mg_name → mg_hash) entry. Returns the
+// branch-tree root hex.
+std::string build_branch_tree(nt::InMemoryBackend& store,
+ const std::string& mg_name,
+ const std::string& mg_hash_hex)
+{
+ return nt::Merkle<std::string>::Insert(store, "", mg_name,
+ nt::hex_to_bin(mg_hash_hex));
+}
+
+} // namespace
+
+TEST_CASE("Branch sub-path rewrites through the branch tree to the mg snapshot",
+ "[resolve]")
+{
+ nt::ObjectManager om;
+ nt::InMemoryBackend store;
+
+ const std::string mg_hash = mk_hash('a');
+ const std::string tree = build_branch_tree(store, "warehouse", mg_hash);
+ register_branch(om, "main", tree);
+
+ nt::NamespaceReferenceManager nrm(om, store);
+ REQUIRE(nrm.Resolve({"system", "branches", "main", "multigroups", "warehouse", "relations", "items"})
+ == Path{"system", "snapshots", mg_hash, "relations", "items"});
+}
+
+TEST_CASE("Resolve on the branch object itself is left unchanged", "[resolve]")
+{
+ nt::ObjectManager om;
+ nt::InMemoryBackend store;
+ const std::string tree = build_branch_tree(store, "warehouse", mk_hash('a'));
+ register_branch(om, "main", tree);
+
+ nt::NamespaceReferenceManager nrm(om, store);
+ const Path p{"system", "branches", "main"};
+ REQUIRE(nrm.Resolve(p) == p);
+}
+
+TEST_CASE("Resolve on an unborn branch leaves the path unchanged", "[resolve]")
+{
+ nt::ObjectManager om;
+ nt::InMemoryBackend store;
+ register_branch(om, "main", ""); // unborn
+
+ nt::NamespaceReferenceManager nrm(om, store);
+ const Path p{"system", "branches", "main", "multigroups", "warehouse", "relations", "items"};
+ REQUIRE(nrm.Resolve(p) == p);
+}
+
+TEST_CASE("Resolve on an absent multigroup leaves the path unchanged", "[resolve]")
+{
+ nt::ObjectManager om;
+ nt::InMemoryBackend store;
+ const std::string tree = build_branch_tree(store, "warehouse", mk_hash('a'));
+ register_branch(om, "main", tree);
+
+ nt::NamespaceReferenceManager nrm(om, store);
+ const Path p{"system", "branches", "main", "multigroups", "audit", "relations", "events"};
+ REQUIRE(nrm.Resolve(p) == p);
+}
+
+TEST_CASE("Resolve on an unknown branch leaves the path unchanged", "[resolve]")
+{
+ nt::ObjectManager om;
+ nt::InMemoryBackend store;
+ nt::NamespaceReferenceManager nrm(om, store);
+ const Path p{"system", "branches", "ghost", "multigroups", "warehouse", "relations", "items"};
+ REQUIRE(nrm.Resolve(p) == p);
+}
+
+TEST_CASE("Non-branch paths pass through unchanged", "[resolve]")
+{
+ nt::ObjectManager om;
+ nt::InMemoryBackend store;
+ nt::NamespaceReferenceManager nrm(om, store);
+ const Path p{"relations", "anywhere"};
+ REQUIRE(nrm.Resolve(p) == p);
+}
+
+TEST_CASE("Session override takes precedence over the global branch HEAD",
+ "[resolve]")
+{
+ nt::ObjectManager om;
+ nt::InMemoryBackend store;
+
+ const std::string mg_global = mk_hash('a');
+ const std::string mg_session = mk_hash('b');
+ const std::string tree_global =
+ build_branch_tree(store, "warehouse", mg_global);
+ const std::string tree_session =
+ build_branch_tree(store, "warehouse", mg_session);
+
+ register_branch(om, "main", tree_global);
+ register_session(om, "sess123", {{"main", tree_session}});
+
+ nt::NamespaceReferenceManager nrm(om, store);
+ REQUIRE(nrm.Resolve({"system", "sessions", "sess123", "branches", "main",
+ "multigroups", "warehouse", "relations", "items"})
+ == Path{"system", "snapshots", mg_session, "relations", "items"});
+}
+
+TEST_CASE("Session resolution falls back to global branch when override is absent",
+ "[resolve]")
+{
+ nt::ObjectManager om;
+ nt::InMemoryBackend store;
+
+ const std::string mg_global = mk_hash('a');
+ const std::string tree_global =
+ build_branch_tree(store, "warehouse", mg_global);
+
+ register_branch(om, "main", tree_global);
+ register_session(om, "sess123"); // no overrides
+
+ nt::NamespaceReferenceManager nrm(om, store);
+ REQUIRE(nrm.Resolve({"system", "sessions", "sess123", "branches", "main",
+ "multigroups", "warehouse", "relations", "items"})
+ == Path{"system", "snapshots", mg_global, "relations", "items"});
+}
+
+TEST_CASE("Session resolution with neither override nor global branch is unchanged",
+ "[resolve]")
+{
+ nt::ObjectManager om;
+ nt::InMemoryBackend store;
+ register_session(om, "sess123");
+
+ nt::NamespaceReferenceManager nrm(om, store);
+ const Path p{"system", "sessions", "sess123", "branches", "main",
+ "multigroups", "warehouse", "relations", "items"};
+ REQUIRE(nrm.Resolve(p) == p);
+}
+
+TEST_CASE("Empty input path is returned unchanged", "[resolve]")
+{
+ nt::ObjectManager om;
+ nt::InMemoryBackend store;
+ nt::NamespaceReferenceManager nrm(om, store);
+ REQUIRE(nrm.Resolve({}).empty());
+}
diff --git a/test/ObjectManager.cpp b/test/ObjectManager.cpp
new file mode 100644
index 0000000..e97bcb2
--- /dev/null
+++ b/test/ObjectManager.cpp
@@ -0,0 +1,75 @@
+#include <catch2/catch_test_macros.hpp>
+
+#include "ObjectManager.h"
+
+#include <memory>
+
+// ---------------------------------------------------------------------------
+// Step 5 — Unregister
+//
+// Sessions are disposable and must be removable from the registry on close.
+// Unregister is the splice-out primitive that rnt_session_close, the
+// LifecycleManager TryCollect path, and any future explicit-delete API all
+// share.
+// ---------------------------------------------------------------------------
+
+namespace
+{
+ std::unique_ptr<nt::ObjectManager::object_type> session_type()
+ {
+ auto t = std::make_unique<nt::ObjectManager::object_type>();
+ t->label = SESSION;
+ t->disposable = true;
+ t->methods = { OPEN, CLOSE };
+ return t;
+ }
+}
+
+TEST_CASE("Unregister removes the entry and Find returns nullptr", "[step5][object-manager]")
+{
+ nt::ObjectManager om;
+ om.Register({"system", "sessions", "alpha"},
+ std::make_unique<nt::ObjectManager::Session>(),
+ session_type());
+ REQUIRE(om.Find({"system", "sessions", "alpha"}) != nullptr);
+
+ REQUIRE(om.Unregister({"system", "sessions", "alpha"}) == true);
+ REQUIRE(om.Find({"system", "sessions", "alpha"}) == nullptr);
+}
+
+TEST_CASE("Unregister returns false for missing paths", "[step5][object-manager]")
+{
+ nt::ObjectManager om;
+ REQUIRE(om.Unregister({"never", "registered"}) == false);
+}
+
+TEST_CASE("Unregister splices a middle entry without disturbing neighbours",
+ "[step5][object-manager]")
+{
+ nt::ObjectManager om;
+ om.Register({"a"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
+ om.Register({"b"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
+ om.Register({"c"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
+
+ REQUIRE(om.Unregister({"b"}) == true);
+
+ REQUIRE(om.Find({"a"}) != nullptr);
+ REQUIRE(om.Find({"b"}) == nullptr);
+ REQUIRE(om.Find({"c"}) != nullptr);
+}
+
+TEST_CASE("Unregister at the head of the list works", "[step5][object-manager]")
+{
+ nt::ObjectManager om;
+ om.Register({"first"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
+ om.Register({"second"}, std::make_unique<nt::ObjectManager::Session>(), session_type());
+
+ // The most recently registered entry is the list head (LIFO).
+ REQUIRE(om.Unregister({"second"}) == true);
+ REQUIRE(om.Find({"second"}) == nullptr);
+ REQUIRE(om.Find({"first"}) != nullptr);
+
+ // And the new head is removable too.
+ REQUIRE(om.Unregister({"first"}) == true);
+ REQUIRE(om.Find({"first"}) == nullptr);
+}
diff --git a/test/VM.cpp b/test/VM.cpp
index 00361cb..b0a602f 100644
--- a/test/VM.cpp
+++ b/test/VM.cpp
@@ -6,6 +6,7 @@
#include "InMemoryBackend.h"
#include "LifecycleManager.h"
#include "Merkle.h"
+#include "NamespaceReferenceManager.h"
#include "ObjectManager.h"
#include "PermissionsManager.h"
#include "TupleCodec.h"
@@ -32,7 +33,9 @@ static void store_tuple(nt::InMemoryBackend& backend,
if (!entry) return;
auto* rel = dynamic_cast<nt::ObjectManager::Relation*>(entry->object.get());
if (!rel) return;
- rel->merkle_root = nt::Merkle::Insert(backend, rel->merkle_root, hash);
+ auto hash_bin = nt::hex_to_bin(hash);
+ rel->merkle_root = nt::Merkle<nt::Hash32>::Insert(backend, rel->merkle_root,
+ hash_bin, hash_bin);
}
static std::unique_ptr<nt::ObjectManager::object_type> make_relation_type()
@@ -74,9 +77,10 @@ struct Fixture
nt::ObjectManager objects;
nt::PermissionsManager permissions;
nt::IdentityManager identities;
- nt::LifecycleManager lifecycles;
- nt::HandlerManager handler { objects, permissions, identities, lifecycles };
- nt::CursorManager cursors { backend };
+ nt::LifecycleManager lifecycles { objects, backend };
+ nt::NamespaceReferenceManager references { objects, backend };
+ nt::HandlerManager handler { objects, permissions, identities, lifecycles, references };
+ nt::CursorManager cursors { backend, &lifecycles, &objects };
int conn = 1;
};