// Task 1 — RelationManager: the declaration registry for referential // integrity relations. No enforcement, no LMDB index yet (later tasks). // // Modeled on tests/test_view_manager_paging.cpp: relations are keyed by // their project-qualified name in a `_relations` system collection, and // loadFromStore() must page explicitly since Query::limit defaults to 100 // and limit=0 returns nothing (not everything) — the same trap that has // already shipped as a bug in ViewManager, PolicyManager and // CollectionConfigManager. #include #include #include #include "document.hpp" #include "memory_store.hpp" #include "relations/relation_manager.hpp" using namespace smartbotic::database; namespace { int g_pass = 0; int g_fail = 0; void check(bool cond, const std::string& msg) { if (cond) { ++g_pass; } else { ++g_fail; std::cerr << "FAIL: " << msg << "\n"; } } struct Fixture { MemoryStore store; Fixture() : store(MemoryStore::Config{}) { store.start(); } ~Fixture() { store.stop(); } }; void test_relations_are_project_scoped_and_survive_reload() { Fixture f; // MemoryStore, started RelationManager rm(f.store); rm.loadFromStore(); RelationInfo a; a.name = "default:exec_wf"; a.child = "default:executions"; a.childField = "workflowId"; a.parent = "default:workflows"; std::string err; check(rm.createRelation(a, err), "created in default"); RelationInfo b = a; // SAME bare name, different project b.name = "acme:exec_wf"; b.child = "acme:executions"; b.parent = "acme:workflows"; check(rm.createRelation(b, err), "the same name in another project is allowed"); check(rm.listRelations("default").size() == 1, "listing is project-filtered"); check(rm.listRelations().size() == 2, "empty project lists everything"); RelationManager fresh(f.store); // restart fresh.loadFromStore(); check(fresh.getRelation("default:exec_wf").has_value(), "survives reload"); check(fresh.getRelation("acme:exec_wf").has_value(), "both survive"); } void test_cross_project_relation_is_refused() { Fixture f; RelationManager rm(f.store); RelationInfo r; r.name = "default:bad"; r.child = "default:executions"; r.childField = "workflowId"; r.parent = "acme:workflows"; // different env - no txn spans two std::string err; check(!rm.createRelation(r, err), "a cross-project relation is refused"); check(err.find("project") != std::string::npos, "and says why"); } void test_more_than_one_page_of_relations_loads() { Fixture f; RelationManager rm(f.store); for (int i = 0; i < 250; ++i) { // Query::limit defaults to 100 RelationInfo r; char buf[32]; std::snprintf(buf, sizeof(buf), "default:r%03d", i); r.name = buf; r.child = "default:c"; r.childField = "p"; r.parent = "default:p"; std::string err; rm.createRelation(r, err); } RelationManager fresh(f.store); fresh.loadFromStore(); check(fresh.listRelations().size() == 250, "all 250 load - a bare Query would stop at 100, as it did for views, " "policies and collection configs"); } // v2.11.0 T13 round 2 (review finding 3) — createRelation() refuses a // cross-project declaration (test_cross_project_relation_is_refused, // above), but loadFromStore() is the OTHER way a RelationInfo enters the // cache and did not re-check it. A legacy or hand-written `_relations` // document naming a cross-project parent must be skipped at load time too - // arming it would resolve the bare parent name inside the CHILD's own // project env (validate_on_write/RelationRef only ever resolve `parent` // against the child's project), silently checking the wrong collection. void test_load_skips_a_cross_project_relation_written_by_hand() { Fixture f; // Bypass createRelation()'s own guard entirely - write the raw document // straight into `_relations`, the way a legacy record or a hand-edited // one would exist on disk. `parent` names a DIFFERENT project than // `child`, which createRelation() would refuse today. nlohmann::json bad = { {"name", "default:bad_cross"}, {"child", "default:executions"}, {"child_field", "workflowId"}, {"parent", "acme:workflows"}, {"on_delete", "restrict"}, {"validate_on_write", true}, {"created_at", 0}, {"updated_at", 0}, }; Document d; d.id = "default:bad_cross"; d.collection = RelationManager::SYSTEM_COLLECTION; d.set_data(bad); f.store.insert(RelationManager::SYSTEM_COLLECTION, d); // A well-formed, same-project relation alongside it, to confirm one bad // record does not stop the rest of the load. RelationInfo good; good.name = "default:exec_wf"; good.child = "default:executions"; good.childField = "ownerId"; good.parent = "default:users"; { RelationManager rm(f.store); rm.loadFromStore(); std::string err; check(rm.createRelation(good, err), "the well-formed sibling declares fine"); } RelationManager fresh(f.store); fresh.loadFromStore(); check(!fresh.getRelation("default:bad_cross").has_value(), "the cross-project relation was skipped, not armed with the wrong parent"); check(fresh.getRelation("default:exec_wf").has_value(), "the well-formed sibling still loaded - one bad record did not stop the rest"); } } // namespace int main() { std::cout << "=== test_relation_manager ===\n"; test_relations_are_project_scoped_and_survive_reload(); test_cross_project_relation_is_refused(); test_more_than_one_page_of_relations_loads(); test_load_skips_a_cross_project_relation_written_by_hand(); std::cout << "passed: " << g_pass << ", failed: " << g_fail << "\n"; return g_fail == 0 ? 0 : 1; }