yams/tests/unit/metadata/metadata_repository_cache_catch2_test.cpp
Trevon 9cb6421593
feat: daemon stability fixes, vector search optimizations, and docs refresh (#46)
* fix(daemon): resolve sign-conversion warnings

* fix(daemon): add IO thread startup barrier

* refactor: consolidate toLowerCopy clones

* fix(daemon): add atexit handler for static host map cleanup

* fix(build): add missing include, fix daemon_helpers syntax, use atomic barrier

* feat(docs): docs cleanup, and metadata refactor updates

* fix(daemon): resolve Windows-only PostIngestQueue shutdown SIGSEGV (#45)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* feat: submodule bump, multiclient optimizations and benchmark optimizations

* feat(perf): reduced contention with update to placement of flushPathCacheBuffer()

* fix(metadata): invalidate path cache on document mutation

* fix(metadata): hydrate cached counters before first write

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 10:30:38 -04:00

250 lines
9 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright 2025 YAMS Contributors
// Smoke test for path cache snapshot behavior via findDocumentByExactPath
#include <atomic>
#include <chrono>
#include <filesystem>
#include <thread>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include <yams/metadata/connection_pool.h>
#include <yams/metadata/metadata_repository.h>
#include <yams/metadata/path_utils.h>
#include <yams/storage/corpus_stats.h>
using namespace std::chrono;
using namespace yams::metadata;
namespace {
std::filesystem::path tempDbPath(const char* prefix) {
const char* t = std::getenv("YAMS_TEST_TMPDIR");
auto base = (t && *t) ? std::filesystem::path(t) : std::filesystem::temp_directory_path();
std::error_code ec;
std::filesystem::create_directories(base, ec);
auto ts = std::chrono::steady_clock::now().time_since_epoch().count();
auto p = base / (std::string(prefix) + std::to_string(ts) + ".db");
std::filesystem::remove(p, ec);
return p;
}
DocumentInfo mk(const std::string& path, const std::string& hash) {
DocumentInfo d;
d.filePath = path;
auto derived = computePathDerivedValues(path);
d.fileName = std::filesystem::path(derived.normalizedPath).filename().string();
d.fileExtension = std::filesystem::path(d.fileName).extension().string();
d.sha256Hash = hash;
d.pathPrefix = derived.pathPrefix;
d.reversePath = derived.reversePath;
d.pathHash = derived.pathHash;
d.parentHash = derived.parentHash;
d.pathDepth = derived.pathDepth;
auto now = floor<seconds>(system_clock::now());
d.createdTime = d.modifiedTime = d.indexedTime = now;
return d;
}
} // namespace
TEST_CASE("MetadataRepositoryCache: repeated exact path hits cache", "[unit][metadata][cache]") {
auto dbPath = tempDbPath("repo_cache_catch2_");
ConnectionPool pool(dbPath.string());
REQUIRE(pool.initialize().has_value());
MetadataRepository repo(pool);
auto id = repo.insertDocument(mk("/cache/hit.md", "H1"));
REQUIRE(id.has_value());
// First call should populate cache
auto first = repo.findDocumentByExactPath("/cache/hit.md");
REQUIRE(first.has_value());
REQUIRE(first.value().has_value());
auto d1 = first.value().value();
CHECK((d1.filePath == "/cache/hit.md"));
// Second call should return same doc (served from cache path in repo)
auto second = repo.findDocumentByExactPath("/cache/hit.md");
REQUIRE(second.has_value());
REQUIRE(second.value().has_value());
auto d2 = second.value().value();
CHECK((d2.filePath == d1.filePath));
CHECK((d2.sha256Hash == d1.sha256Hash));
// Cleanup
pool.shutdown();
std::error_code ec;
std::filesystem::remove(dbPath, ec);
}
TEST_CASE("MetadataRepositoryCache: mutations invalidate cached path entries",
"[unit][metadata][cache]") {
auto dbPath = tempDbPath("repo_cache_invalidate_catch2_");
ConnectionPool pool(dbPath.string());
REQUIRE(pool.initialize().has_value());
MetadataRepository repo(pool);
constexpr int kDocs = 40; // > flush threshold (32) so entries land in the snapshot
std::vector<int64_t> ids;
for (int i = 0; i < kDocs; ++i) {
auto path = "/cache/inv_" + std::to_string(i) + ".md";
auto id = repo.insertDocument(mk(path, "H" + std::to_string(i)));
REQUIRE(id.has_value());
ids.push_back(id.value());
}
for (int i = 0; i < kDocs; ++i) {
auto path = "/cache/inv_" + std::to_string(i) + ".md";
auto found = repo.findDocumentByExactPath(path);
REQUIRE(found.has_value());
REQUIRE(found.value().has_value());
}
SECTION("deleteDocument removes the cached entry") {
REQUIRE(repo.deleteDocument(ids[5]).has_value());
auto after = repo.findDocumentByExactPath("/cache/inv_5.md");
REQUIRE(after.has_value());
CHECK_FALSE(after.value().has_value());
}
SECTION("updateDocument refreshes the cached entry") {
auto current = repo.findDocumentByExactPath("/cache/inv_6.md");
REQUIRE(current.has_value());
REQUIRE(current.value().has_value());
auto doc = current.value().value();
doc.sha256Hash = "H6_updated";
REQUIRE(repo.updateDocument(doc).has_value());
auto after = repo.findDocumentByExactPath("/cache/inv_6.md");
REQUIRE(after.has_value());
REQUIRE(after.value().has_value());
CHECK((after.value()->sha256Hash == "H6_updated"));
}
SECTION("rename invalidates the old path") {
auto current = repo.findDocumentByExactPath("/cache/inv_7.md");
REQUIRE(current.has_value());
REQUIRE(current.value().has_value());
auto doc = current.value().value();
auto derived = computePathDerivedValues("/cache/renamed_7.md");
doc.filePath = "/cache/renamed_7.md";
doc.fileName = "renamed_7.md";
doc.pathPrefix = derived.pathPrefix;
doc.reversePath = derived.reversePath;
doc.pathHash = derived.pathHash;
doc.parentHash = derived.parentHash;
doc.pathDepth = derived.pathDepth;
REQUIRE(repo.updateDocument(doc).has_value());
auto oldPath = repo.findDocumentByExactPath("/cache/inv_7.md");
REQUIRE(oldPath.has_value());
CHECK_FALSE(oldPath.value().has_value());
auto newPath = repo.findDocumentByExactPath("/cache/renamed_7.md");
REQUIRE(newPath.has_value());
CHECK(newPath.value().has_value());
}
SECTION("deleteDocumentsBatch removes cached entries") {
REQUIRE(repo.deleteDocumentsBatch({ids[8], ids[9]}).has_value());
auto a = repo.findDocumentByExactPath("/cache/inv_8.md");
REQUIRE(a.has_value());
CHECK_FALSE(a.value().has_value());
auto b = repo.findDocumentByExactPath("/cache/inv_9.md");
REQUIRE(b.has_value());
CHECK_FALSE(b.value().has_value());
}
pool.shutdown();
std::error_code ec;
std::filesystem::remove(dbPath, ec);
}
TEST_CASE("MetadataRepository: fresh instance over existing DB hydrates counters on first write",
"[unit][metadata][counters]") {
auto dbPath = tempDbPath("repo_counter_hydrate_catch2_");
ConnectionPool pool(dbPath.string());
REQUIRE(pool.initialize().has_value());
std::vector<int64_t> ids;
{
MetadataRepository primary(pool);
primary.initializeCounters();
for (int i = 0; i < 3; ++i) {
auto id = primary.insertDocument(
mk("/counters/doc_" + std::to_string(i) + ".md", "CH" + std::to_string(i)));
REQUIRE(id.has_value());
ids.push_back(id.value());
}
REQUIRE(primary
.setMetadata(ids[0], "tag:primary", MetadataValue(std::string("true")))
.has_value());
}
// Second instance over the same DB, attached without initializeCounters()
// — the stateless-CLI case. Tagging a previously untagged document bumps
// docsWithTags by +1; the counters must be hydrated from the DB first or
// the kv_ops invariant (docsWithTags <= docCount) fires against a
// still-zero document count (previous behavior: SIGABRT).
MetadataRepository fresh(pool);
auto setRes = fresh.setMetadata(ids[1], "tag:fresh", MetadataValue(std::string("true")));
REQUIRE(setRes.has_value());
auto count = fresh.getDocumentCount();
REQUIRE(count.has_value());
CHECK(count.value() == 3);
auto stats = fresh.getCorpusStats();
REQUIRE(stats.has_value());
CHECK(stats.value().docCount == 3);
CHECK(stats.value().docsWithTags >= 1);
CHECK(stats.value().docsWithTags <= stats.value().docCount);
pool.shutdown();
std::error_code ec;
std::filesystem::remove(dbPath, ec);
}
TEST_CASE("MetadataRepositoryCache: concurrent exact path lookups stay coherent",
"[unit][metadata][cache][concurrency]") {
auto dbPath = tempDbPath("repo_cache_concurrent_catch2_");
ConnectionPool pool(dbPath.string());
REQUIRE(pool.initialize().has_value());
MetadataRepository repo(pool);
auto id = repo.insertDocument(mk("/cache/concurrent.md", "HC"));
REQUIRE(id.has_value());
std::atomic<bool> start{false};
std::atomic<int> failures{0};
std::vector<std::thread> threads;
threads.reserve(8);
for (int i = 0; i < 8; ++i) {
threads.emplace_back([&]() {
while (!start.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
for (int iter = 0; iter < 200; ++iter) {
auto result = repo.findDocumentByExactPath("/cache/concurrent.md");
if (!result || !result.value().has_value() || result.value()->sha256Hash != "HC") {
failures.fetch_add(1, std::memory_order_relaxed);
}
}
});
}
start.store(true, std::memory_order_release);
for (auto& thread : threads) {
thread.join();
}
CHECK((failures.load(std::memory_order_relaxed) == 0));
pool.shutdown();
std::error_code ec;
std::filesystem::remove(dbPath, ec);
}