#include #include "InMemoryBackend.h" #include "MultigroupCodec.h" #include #include // --------------------------------------------------------------------------- // Multigroup snapshot — Merkle tree mapping relation name → relation root. // // The codec is a thin facade over Merkle; 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 in_order = { {"alpha", mk_root('a')}, {"bravo", mk_root('b')}, {"charlie", mk_root('c')}, }; std::vector 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 base = {{"foo", mk_root('a')}}; std::vector different_root = {{"foo", mk_root('b')}}; std::vector different_name = {{"bar", mk_root('a')}}; std::vector 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 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 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 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 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 empty; REQUIRE(nt::MultigroupCodec::Build(backend, empty).empty()); REQUIRE(nt::MultigroupCodec::List(backend, "").empty()); }