From 5ae22ff9f8c1476261f29acba35cc7e23b579f3d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=CE=A7=CE=B3=CF=86=CF=84=20Kompanion?=
 <chigammaphitau@chelydra.at>
Date: Wed, 15 Oct 2025 01:22:29 +1300
Subject: [PATCH] Add testsystem

---
 .acf/tasks.json                   | 165 +++++++++++++++++++++++++++++-
 CMakeLists.txt                    |  23 ++---
 db/queries.sql                    |   1 -
 db/schema_branch_embeddings.sql   |   1 -
 db/schema_dev.sql                 |   0
 db/schema_runtime.sql             |   0
 src/dal/CMakeLists.txt            |   6 ++
 tasks-table.md                    |  26 ++++-
 tests/CMakeLists.txt              |  13 +++
 tests/contract/test_mcp_tools.cpp |  23 ++++-
 tests/contract_memory.cpp         |  42 ++++++++
 11 files changed, 277 insertions(+), 23 deletions(-)
 delete mode 100644 db/queries.sql
 delete mode 100644 db/schema_branch_embeddings.sql
 delete mode 100644 db/schema_dev.sql
 delete mode 100644 db/schema_runtime.sql
 create mode 100644 src/dal/CMakeLists.txt
 create mode 100644 tests/CMakeLists.txt
 create mode 100644 tests/contract_memory.cpp

diff --git a/.acf/tasks.json b/.acf/tasks.json
index 21e6fc8..e02fefc 100644
--- a/.acf/tasks.json
+++ b/.acf/tasks.json
@@ -1,7 +1,7 @@
 {
   "projectName": "metal-kompanion-mcp",
   "projectDescription": "MCP backend for Kompanion: memory/context/embedding provider over MCP, built from scratch (qtmcp-based) to persist conversation state and serve embeddings + retrieval to avoid forgetting across threads.",
-  "lastTaskId": 20,
+  "lastTaskId": 23,
   "tasks": [
     {
       "id": 1,
@@ -441,6 +441,169 @@
           "message": "Task created with title: \"Claude Code integration rescue plan\""
         }
       ]
+    },
+    {
+      "id": 21,
+      "title": "DAL Phase 1: libpq/pqxx wiring + SQL calls",
+      "description": "Link pqxx, implement PgDal against Postgres+pgvector: connect/tx, ensureNamespace, upsertItem/Chunks/Embeddings, searchText (FTS/trgm), searchVector (<->). Provide DSN via env; add cmake find + link.",
+      "status": "todo",
+      "priority": 510,
+      "priorityDisplay": "P0",
+      "dependsOn": [],
+      "createdAt": "2025-10-14T00:29:55.327Z",
+      "updatedAt": "2025-10-14T00:29:55.327Z",
+      "subtasks": [
+        {
+          "id": "21.1",
+          "title": "CMake: find_package(pqxx) and link; CI env var DSN",
+          "status": "todo",
+          "createdAt": "2025-10-14T00:30:00.856Z",
+          "updatedAt": "2025-10-14T00:30:00.857Z",
+          "activityLog": [
+            {
+              "timestamp": "2025-10-14T00:30:00.856Z",
+              "type": "log",
+              "message": "Subtask created with title: \"CMake: find_package(pqxx) and link; CI env var DSN\""
+            }
+          ]
+        },
+        {
+          "id": "21.2",
+          "title": "PgDal: implement connect/tx + prepared statements",
+          "status": "todo",
+          "createdAt": "2025-10-14T00:30:06.138Z",
+          "updatedAt": "2025-10-14T00:30:06.138Z",
+          "activityLog": [
+            {
+              "timestamp": "2025-10-14T00:30:06.138Z",
+              "type": "log",
+              "message": "Subtask created with title: \"PgDal: implement connect/tx + prepared statements\""
+            }
+          ]
+        },
+        {
+          "id": "21.3",
+          "title": "SQL: ensureNamespace, upsertItem/Chunks/Embeddings",
+          "status": "todo",
+          "createdAt": "2025-10-14T00:30:11.519Z",
+          "updatedAt": "2025-10-14T00:30:11.519Z",
+          "activityLog": [
+            {
+              "timestamp": "2025-10-14T00:30:11.519Z",
+              "type": "log",
+              "message": "Subtask created with title: \"SQL: ensureNamespace, upsertItem/Chunks/Embeddings\""
+            }
+          ]
+        },
+        {
+          "id": "21.4",
+          "title": "Search: FTS/trgm + vector <-> with filters (namespace/thread/tags)",
+          "status": "todo",
+          "createdAt": "2025-10-14T00:30:17.290Z",
+          "updatedAt": "2025-10-14T00:30:17.290Z",
+          "activityLog": [
+            {
+              "timestamp": "2025-10-14T00:30:17.290Z",
+              "type": "log",
+              "message": "Subtask created with title: \"Search: FTS/trgm + vector <-> with filters (namespace/thread/tags)\""
+            }
+          ]
+        }
+      ],
+      "lastSubtaskIndex": 4,
+      "relatedFiles": [],
+      "activityLog": [
+        {
+          "timestamp": "2025-10-14T00:29:55.327Z",
+          "type": "log",
+          "message": "Task created with title: \"DAL Phase 1: libpq/pqxx wiring + SQL calls\""
+        }
+      ]
+    },
+    {
+      "id": 22,
+      "title": "Handlers → DAL integration",
+      "description": "Wire kom.memory.v1.upsert_memory/search_memory to IDatabase. Parse JSON with a real parser, validate against schemas, enforce scope and sensitivity rules.",
+      "status": "todo",
+      "priority": 490,
+      "priorityDisplay": "P0",
+      "dependsOn": [],
+      "createdAt": "2025-10-14T00:30:26.285Z",
+      "updatedAt": "2025-10-14T00:30:26.285Z",
+      "subtasks": [
+        {
+          "id": "22.1",
+          "title": "Replace ad-hoc JSON with parser (nlohmann/json or simdjson)",
+          "status": "todo",
+          "createdAt": "2025-10-14T00:30:33.761Z",
+          "updatedAt": "2025-10-14T00:30:33.761Z",
+          "activityLog": [
+            {
+              "timestamp": "2025-10-14T00:30:33.761Z",
+              "type": "log",
+              "message": "Subtask created with title: \"Replace ad-hoc JSON with parser (nlohmann/json or simdjson)\""
+            }
+          ]
+        },
+        {
+          "id": "22.2",
+          "title": "Validate request bodies against schemas before DAL calls",
+          "status": "todo",
+          "createdAt": "2025-10-14T00:30:39.868Z",
+          "updatedAt": "2025-10-14T00:30:39.868Z",
+          "activityLog": [
+            {
+              "timestamp": "2025-10-14T00:30:39.868Z",
+              "type": "log",
+              "message": "Subtask created with title: \"Validate request bodies against schemas before DAL calls\""
+            }
+          ]
+        },
+        {
+          "id": "22.3",
+          "title": "Scope & sensitivity enforcement (namespace/user + skip secret embeddings)",
+          "status": "todo",
+          "createdAt": "2025-10-14T00:30:45.261Z",
+          "updatedAt": "2025-10-14T00:30:45.261Z",
+          "activityLog": [
+            {
+              "timestamp": "2025-10-14T00:30:45.261Z",
+              "type": "log",
+              "message": "Subtask created with title: \"Scope & sensitivity enforcement (namespace/user + skip secret embeddings)\""
+            }
+          ]
+        }
+      ],
+      "lastSubtaskIndex": 3,
+      "relatedFiles": [],
+      "activityLog": [
+        {
+          "timestamp": "2025-10-14T00:30:26.285Z",
+          "type": "log",
+          "message": "Task created with title: \"Handlers → DAL integration\""
+        }
+      ]
+    },
+    {
+      "id": 23,
+      "title": "Contract tests: DAL-backed tools",
+      "description": "Expand CTest to cover DAL-backed upsert/search and backup export/import; include error cases and schema violations; run against build-komhands.",
+      "status": "todo",
+      "priority": 511,
+      "priorityDisplay": "P1",
+      "dependsOn": [],
+      "createdAt": "2025-10-14T00:30:51.716Z",
+      "updatedAt": "2025-10-14T00:30:51.716Z",
+      "subtasks": [],
+      "lastSubtaskIndex": 0,
+      "relatedFiles": [],
+      "activityLog": [
+        {
+          "timestamp": "2025-10-14T00:30:51.716Z",
+          "type": "log",
+          "message": "Task created with title: \"Contract tests: DAL-backed tools\""
+        }
+      ]
     }
   ]
 }
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 85241d9..d2d3f72 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -8,25 +8,18 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
 # find_package(Qt6 COMPONENTS Core Network REQUIRED)
 # find_package(qtmcp REQUIRED)
 
-find_package(PkgConfig REQUIRED)
-pkg_check_modules(PQXX REQUIRED libpqxx)
+add_subdirectory(src/dal)
 
 add_executable(kom_mcp
     src/main.cpp
-    src/dal/PgDal.cpp
 )
-# target_link_libraries(kom_mcp PRIVATE Qt6::Core Qt6::Network qtmcp)
-target_include_directories(kom_mcp PRIVATE src ${PQXX_INCLUDE_DIRS})
-target_link_libraries(kom_mcp PRIVATE ${PQXX_LIBRARIES})
+target_include_directories(kom_mcp PRIVATE src)
+target_link_libraries(kom_mcp PRIVATE kom_dal)
 
 install(TARGETS kom_mcp RUNTIME DESTINATION bin)
 
-add_executable(test_mcp_tools
-    tests/contract/test_mcp_tools.cpp
-    src/dal/PgDal.cpp
-)
-target_include_directories(test_mcp_tools PRIVATE src ${PQXX_INCLUDE_DIRS})
-target_link_libraries(test_mcp_tools PRIVATE ${PQXX_LIBRARIES})
-
-enable_testing()
-add_test(NAME contract_mcp_tools COMMAND test_mcp_tools)
+option(BUILD_TESTS "Build tests" ON)
+if (BUILD_TESTS)
+  enable_testing()
+  add_subdirectory(tests)
+endif()
diff --git a/db/queries.sql b/db/queries.sql
deleted file mode 100644
index 083e46a..0000000
--- a/db/queries.sql
+++ /dev/null
@@ -1 +0,0 @@
--- Kompanion quick checks
diff --git a/db/schema_branch_embeddings.sql b/db/schema_branch_embeddings.sql
deleted file mode 100644
index d4df567..0000000
--- a/db/schema_branch_embeddings.sql
+++ /dev/null
@@ -1 +0,0 @@
--- placeholder: see chat for full Branch Embeddings schema
diff --git a/db/schema_dev.sql b/db/schema_dev.sql
deleted file mode 100644
index e69de29..0000000
diff --git a/db/schema_runtime.sql b/db/schema_runtime.sql
deleted file mode 100644
index e69de29..0000000
diff --git a/src/dal/CMakeLists.txt b/src/dal/CMakeLists.txt
new file mode 100644
index 0000000..1bda8ce
--- /dev/null
+++ b/src/dal/CMakeLists.txt
@@ -0,0 +1,6 @@
+add_library(kom_dal STATIC
+  PgDal.cpp
+)
+
+target_compile_features(kom_dal PUBLIC cxx_std_20)
+target_include_directories(kom_dal PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
diff --git a/tasks-table.md b/tasks-table.md
index 0844c78..b723622 100644
--- a/tasks-table.md
+++ b/tasks-table.md
@@ -3,11 +3,11 @@ MCP backend for Kompanion: memory/context/embedding provider over MCP, built fro
 
 > ## 📈 Project Summary
 > 
-> **✅ Done**: 0 | **🔄 In Progress**: 0 | **⬜ Todo**: 19 | **❌ Blocked**: 0
+> **✅ Done**: 0 | **🔄 In Progress**: 0 | **⬜ Todo**: 29 | **❌ Blocked**: 0
 > 
-> **Progress**: 0% `░░░░░░░░░░░░░░░░░░░░` 0/19 tasks
+> **Progress**: 0% `░░░░░░░░░░░░░░░░░░░░` 0/29 tasks
 > 
-> **Priorities**: 🚨 **Critical**: 0 | 🔴 **High**: 1 | 🟡 **Medium**: 20 | 🟢 **Low**: 0
+> **Priorities**: 🚨 **Critical**: 0 | 🔴 **High**: 1 | 🟡 **Medium**: 30 | 🟢 **Low**: 0
 
 ## Tasks
 
@@ -33,6 +33,9 @@ MCP backend for Kompanion: memory/context/embedding provider over MCP, built fro
 | #18 | ⬜ todo | 492 | **Expose Agentic-Control-Framework as a tool** | Wrap ACF endpoints into a too... |
 | #19 | ⬜ todo | 509 | **DAL skeleton + SQL calls (pgvector)** | Create DAL interfaces and pgv... |
 | #20 | ⬜ todo | 491 | **Claude Code integration rescue plan** | Stabilize Qwen-2.5-Coder insi... |
+| #21 | ⬜ todo | 510 | **DAL Phase 1: libpq/pqxx wiring + SQL calls** | Link pqxx, implement PgDal ag... |
+| #22 | ⬜ todo | 490 | **Handlers → DAL integration** | Wire kom.memory.v1.upsert_mem... |
+| #23 | ⬜ todo | 511 | **Contract tests: DAL-backed tools** | Expand CTest to cover DAL-bac... |
 
 
 ### Task #2: Design MCP memory/context API - Subtasks
@@ -41,3 +44,20 @@ MCP backend for Kompanion: memory/context/embedding provider over MCP, built fro
 |:--:|:------:|:------|
 | #2.1 | ⬜ todo | Write JSON Schemas for tools (done) |
 
+### Task #21: DAL Phase 1: libpq/pqxx wiring + SQL calls - Subtasks
+
+| ID | Status | Title |
+|:--:|:------:|:------|
+| #21.1 | ⬜ todo | CMake: find_package(pqxx) and link; CI env var DSN |
+| #21.2 | ⬜ todo | PgDal: implement connect/tx + prepared statements |
+| #21.3 | ⬜ todo | SQL: ensureNamespace, upsertItem/Chunks/Embeddings |
+| #21.4 | ⬜ todo | Search: FTS/trgm + vector <-> with filters (namespace/thread/tags) |
+
+### Task #22: Handlers → DAL integration - Subtasks
+
+| ID | Status | Title |
+|:--:|:------:|:------|
+| #22.1 | ⬜ todo | Replace ad-hoc JSON with parser (nlohmann/json or simdjson) |
+| #22.2 | ⬜ todo | Validate request bodies against schemas before DAL calls |
+| #22.3 | ⬜ todo | Scope & sensitivity enforcement (namespace/user + skip secret embeddings) |
+
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
new file mode 100644
index 0000000..eb11e0e
--- /dev/null
+++ b/tests/CMakeLists.txt
@@ -0,0 +1,13 @@
+add_executable(test_mcp_tools
+  contract/test_mcp_tools.cpp
+)
+target_include_directories(test_mcp_tools PRIVATE ${PROJECT_SOURCE_DIR}/src)
+target_link_libraries(test_mcp_tools PRIVATE kom_dal)
+
+add_test(NAME contract_mcp_tools COMMAND test_mcp_tools)
+
+add_library(contract_memory STATIC
+  contract_memory.cpp
+)
+target_include_directories(contract_memory PRIVATE ${PROJECT_SOURCE_DIR}/src)
+target_link_libraries(contract_memory PRIVATE kom_dal)
diff --git a/tests/contract/test_mcp_tools.cpp b/tests/contract/test_mcp_tools.cpp
index 0fbbbec..11a3ca4 100644
--- a/tests/contract/test_mcp_tools.cpp
+++ b/tests/contract/test_mcp_tools.cpp
@@ -20,15 +20,34 @@ int main() {
     KomMcpServer server;
     register_default_tools(server);
 
-    const std::string upsertReq = R"({"namespace":"tests","items":[{"text":"hello world"},{"text":"hola mundo"}]})";
+    const std::string upsertReq = R"({"namespace":"tests","items":[{"text":"hello world","tags":["greeting"],"embedding":[0.1,0.2,0.3]},{"text":"hola mundo","embedding":[0.05,0.1,0.15]}]})";
     std::string upsertResp = server.dispatch("kom.memory.v1.upsert_memory", upsertReq);
     if (!expect_contains(upsertResp, "\"upserted\":2", "upsert_memory count")) return 1;
     if (!expect_contains(upsertResp, "\"status\":\"ok\"", "upsert_memory status")) return 1;
 
+    std::string firstId;
+    const std::string idsAnchor = "\"ids\":[\"";
+    auto idsPos = upsertResp.find(idsAnchor);
+    if (idsPos != std::string::npos) {
+        idsPos += idsAnchor.size();
+        auto endPos = upsertResp.find("\"", idsPos);
+        if (endPos != std::string::npos) {
+            firstId = upsertResp.substr(idsPos, endPos - idsPos);
+        }
+    }
+    if (firstId.empty()) {
+        std::cerr << "Failed to extract first item id from upsert response: " << upsertResp << "\n";
+        return 1;
+    }
+
     const std::string searchReq = R"({"namespace":"tests","query":{"text":"hello","k":3}})";
     std::string searchResp = server.dispatch("kom.memory.v1.search_memory", searchReq);
     if (!expect_contains(searchResp, "\"matches\"", "search_memory matches key")) return 1;
-    if (!expect_contains(searchResp, "\"text\":\"hello\"", "search_memory echo text")) return 1;
+    if (!expect_contains(searchResp, "\"text\":\"hello world\"", "search_memory returns stored text")) return 1;
+
+    const std::string vectorReq = R"({"namespace":"tests","query":{"embedding":[0.1,0.2,0.3],"k":1}})";
+    std::string vectorResp = server.dispatch("kom.memory.v1.search_memory", vectorReq);
+    if (!expect_contains(vectorResp, "\"id\":\""+firstId+"\"", "vector search returns stored id")) return 1;
 
     const std::string exportReq = R"({"namespace":"tests","destination":"/tmp/example.enc"})";
     std::string exportResp = server.dispatch("kom.local.v1.backup.export_encrypted", exportReq);
diff --git a/tests/contract_memory.cpp b/tests/contract_memory.cpp
new file mode 100644
index 0000000..65a4025
--- /dev/null
+++ b/tests/contract_memory.cpp
@@ -0,0 +1,42 @@
+#include "dal/PgDal.hpp"
+
+#include <string>
+#include <vector>
+
+static void contract_pgdal_basic() {
+  kom::PgDal dal;
+  dal.connect("stub://memory");
+  auto ns = dal.ensureNamespace("tests");
+  static_cast<void>(ns);
+
+  kom::ItemRow item;
+  item.namespace_id = "tests";
+  item.text = std::string("example");
+  item.tags = {"alpha", "beta"};
+  item.id = dal.upsertItem(item);
+
+  kom::ChunkRow chunk;
+  chunk.item_id = item.id;
+  chunk.text = "chunk-text";
+  auto chunkIds = dal.upsertChunks(std::vector<kom::ChunkRow>{chunk});
+  if (!chunkIds.empty()) {
+    chunk.id = chunkIds.front();
+  }
+
+  kom::EmbeddingRow embedding;
+  embedding.chunk_id = chunk.id;
+  embedding.model = "stub-model";
+  embedding.dim = 3;
+  embedding.vector = {0.1f, 0.2f, 0.3f};
+  dal.upsertEmbeddings(std::vector<kom::EmbeddingRow>{embedding});
+
+  static_cast<void>(dal.searchText("tests", "chunk", 5));
+  static_cast<void>(dal.searchVector("tests", embedding.vector, 5));
+  static_cast<void>(dal.getItemById(item.id));
+  static_cast<void>(dal.hybridSearch(embedding.vector, "stub-model", "tests", "chunk", 5));
+}
+
+static const bool contract_pgdal_compiles = [] {
+  contract_pgdal_basic();
+  return true;
+}();
