* fix(graph): handle qualified C++ symbols Signed-off-by: trvon <me@trevon.dev> * refactor(vector)!: remove dead transition APIs Split Simeon vector and retrieval linkage so consumers inherit only the runtime surface they use. BREAKING CHANGE: remove unused async/model/config helpers and the undefined vector-backend factory from public headers. Signed-off-by: trvon <me@trevon.dev> * fix(vector): harden PQ snapshot lifecycle Validate and atomically install persisted generations, revalidate after races, and fall back to exact search when a snapshot is stale. Add the exact-scan reference lane and distinguish observed work counters from unavailable backend metrics. Signed-off-by: trvon <me@trevon.dev> * feat(topology): expose route coordinates Persist chart distortion and local-dimension evidence, retain pre-scalarization route axes, and emit detailed trace rows without treating unavailable vector work as zero. Signed-off-by: trvon <me@trevon.dev> * feat(xplan): add vector decision gates Track binary and corpus identity, preserve missing-work semantics, and add repeated PQ routing, crossover, and rerank plans with quality and cost summaries. Signed-off-by: trvon <me@trevon.dev> * feat(topology): formalize coordinate routing Separate chart geometry from route-risk admission and prove candidate-stage recall and strict-focus boundaries. Record the measured atlas limitations and literature basis for the next construction experiment. Signed-off-by: trvon <me@trevon.dev> * fix(search): serialize SymSpell readers SQLite-backed lookups reuse mutable prepared statements, so shared-reader locking is unsafe. Advance the owned SymSpell revision for batched delete insertion and cover concurrent root lookups under TSAN. Signed-off-by: trvon <me@trevon.dev> * fix(vector): make mutations atomic Preserve stored vectors on replacement and commit failures, reject cross-thread writes during explicit transactions, and restore exact reranking for quantized-primary PQ rows. * fix(search)!: surface SymSpell write errors Propagate persistent dictionary failures through metadata ingestion, roll back failed batches, and make clear remove stored terms. BREAKING CHANGE: SymSpellSearch mutations now return Result values. * feat(topology): formalize candidate expansion * feat(vector): add coordinate-aware embeddings * feat(topology): preserve protected relation cover * feat(search): add graph candidate rescue * feat(daemon): expose retrieval hydration telemetry * feat(xplan): add rescue and hydration gates * refactor(content): dedupe media handlers Reuse shared graph presenter helpers and centralize repeated audio/video/archive buffer processing, validation, lifecycle, and stats helper logic. * feat(lifecycle): prune stale corpus data Keep cleanup preview-first while retention stays opt-in and long prune work no longer holds IPC responses open. * feat(search): add topology candidate rescue Preserve high-value routed candidates across vector backends while keeping exact rerank diagnostics explicit. * feat(search): expose evidence paths * docs: define mobile host contract * formal: model bounded candidate rescue * fix(prune): inspect extensionless signatures * fix(prune): tolerate stale extensions --------- Signed-off-by: trvon <me@trevon.dev>
411 lines
15 KiB
C++
411 lines
15 KiB
C++
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <random>
|
|
#include <tuple>
|
|
|
|
#include <yams/compat/unistd.h>
|
|
#include <yams/integrity/repair_manager.h>
|
|
#include <yams/metadata/connection_pool.h>
|
|
#include <yams/metadata/document_metadata.h>
|
|
#include <yams/metadata/metadata_repository.h>
|
|
#include <yams/metadata/path_utils.h>
|
|
#include <yams/metadata/query_helpers.h>
|
|
#include <yams/metadata/versioning_util.h>
|
|
|
|
using namespace yams;
|
|
using namespace yams::metadata;
|
|
|
|
namespace {
|
|
|
|
std::filesystem::path make_temp_dir(std::string_view prefix = "yams_version_test_") {
|
|
namespace fs = std::filesystem;
|
|
const auto base = fs::temp_directory_path();
|
|
std::uniform_int_distribution<int> dist(0, 9999);
|
|
thread_local std::mt19937_64 rng{std::random_device{}()};
|
|
for (int attempt = 0; attempt < 512; ++attempt) {
|
|
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
|
|
auto candidate =
|
|
base / (std::string(prefix) + std::to_string(stamp) + "_" + std::to_string(dist(rng)));
|
|
std::error_code ec;
|
|
if (fs::create_directories(candidate, ec)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return base;
|
|
}
|
|
|
|
DocumentInfo makeDocInfo(const std::string& filePath, const std::string& hash,
|
|
const std::string& fileName) {
|
|
DocumentInfo info;
|
|
info.filePath = filePath;
|
|
info.fileName = fileName;
|
|
info.fileExtension = ".txt";
|
|
info.fileSize = 100;
|
|
info.sha256Hash = hash;
|
|
info.mimeType = "text/plain";
|
|
auto now = std::chrono::floor<std::chrono::seconds>(std::chrono::system_clock::now());
|
|
info.createdTime = now;
|
|
info.modifiedTime = now;
|
|
info.indexedTime = now;
|
|
info.contentExtracted = true;
|
|
info.extractionStatus = ExtractionStatus::Success;
|
|
auto derived = computePathDerivedValues(filePath);
|
|
info.filePath = derived.normalizedPath;
|
|
info.pathPrefix = derived.pathPrefix;
|
|
info.reversePath = derived.reversePath;
|
|
info.pathHash = derived.pathHash;
|
|
info.parentHash = derived.parentHash;
|
|
info.pathDepth = derived.pathDepth;
|
|
return info;
|
|
}
|
|
|
|
class BatchFailingMetadataRepository : public MetadataRepository {
|
|
public:
|
|
explicit BatchFailingMetadataRepository(ConnectionPool& pool) : MetadataRepository(pool) {}
|
|
|
|
Result<void> setMetadataBatch(
|
|
const std::vector<std::tuple<int64_t, std::string, MetadataValue>>& entries) override {
|
|
++batchAttempts;
|
|
lastBatchSize = entries.size();
|
|
if (failNextBatch) {
|
|
failNextBatch = false;
|
|
return Error{ErrorCode::DatabaseError, "injected metadata batch failure"};
|
|
}
|
|
return MetadataRepository::setMetadataBatch(entries);
|
|
}
|
|
|
|
bool failNextBatch{true};
|
|
std::size_t batchAttempts{0};
|
|
std::size_t lastBatchSize{0};
|
|
};
|
|
|
|
struct VersioningFixture {
|
|
VersioningFixture() { testDir = make_temp_dir(); }
|
|
|
|
~VersioningFixture() {
|
|
repo_.reset();
|
|
pool_.reset();
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(testDir, ec);
|
|
}
|
|
|
|
std::shared_ptr<MetadataRepository> makeRepo() {
|
|
auto dbPath = (testDir / "meta.db").string();
|
|
pool_ = std::make_shared<ConnectionPool>(dbPath);
|
|
REQUIRE(pool_->initialize());
|
|
repo_ = std::make_shared<MetadataRepository>(*pool_);
|
|
return repo_;
|
|
}
|
|
|
|
std::shared_ptr<BatchFailingMetadataRepository> makeBatchFailingRepo() {
|
|
auto dbPath = (testDir / "meta.db").string();
|
|
pool_ = std::make_shared<ConnectionPool>(dbPath);
|
|
REQUIRE(pool_->initialize());
|
|
auto repo = std::make_shared<BatchFailingMetadataRepository>(*pool_);
|
|
repo_ = repo;
|
|
return repo;
|
|
}
|
|
|
|
std::filesystem::path testDir;
|
|
std::shared_ptr<ConnectionPool> pool_;
|
|
std::shared_ptr<MetadataRepository> repo_;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE_METHOD(VersioningFixture, "Versioning: first document gets version 1 and is_latest",
|
|
"[versioning][metadata][catch2]") {
|
|
#ifdef _WIN32
|
|
_putenv_s("YAMS_ENABLE_VERSIONING", "1");
|
|
#else
|
|
setenv("YAMS_ENABLE_VERSIONING", "1", 1);
|
|
#endif
|
|
|
|
auto repo = makeRepo();
|
|
auto docInfo = makeDocInfo("/path/doc.txt", "hash_v1", "doc.txt");
|
|
auto ins = repo->insertDocument(docInfo);
|
|
REQUIRE(ins);
|
|
auto docId = ins.value();
|
|
|
|
int64_t version = applyPathSeriesVersioning(*repo, docInfo.filePath, docId, std::nullopt);
|
|
CHECK((version == 1));
|
|
|
|
auto verMeta = repo->getMetadata(docId, "version");
|
|
REQUIRE(verMeta);
|
|
REQUIRE(verMeta.value().has_value());
|
|
CHECK((verMeta.value()->asInteger() == 1));
|
|
|
|
auto latestMeta = repo->getMetadata(docId, "is_latest");
|
|
REQUIRE(latestMeta);
|
|
REQUIRE(latestMeta.value().has_value());
|
|
CHECK((latestMeta.value()->asBoolean() == true));
|
|
|
|
auto seriesMeta = repo->getMetadata(docId, "series_key");
|
|
REQUIRE(seriesMeta);
|
|
REQUIRE(seriesMeta.value().has_value());
|
|
CHECK((seriesMeta.value()->asString() == docInfo.filePath));
|
|
}
|
|
|
|
TEST_CASE_METHOD(VersioningFixture, "Versioning: re-index with different hash creates version edge",
|
|
"[versioning][metadata][catch2]") {
|
|
#ifdef _WIN32
|
|
_putenv_s("YAMS_ENABLE_VERSIONING", "1");
|
|
#else
|
|
setenv("YAMS_ENABLE_VERSIONING", "1", 1);
|
|
#endif
|
|
|
|
auto repo = makeRepo();
|
|
const std::string path = "/path/evolving.txt";
|
|
|
|
auto v1 = makeDocInfo(path, "hash_v1", "evolving.txt");
|
|
auto ins1 = repo->insertDocument(v1);
|
|
REQUIRE(ins1);
|
|
auto id1 = ins1.value();
|
|
int64_t ver1 = applyPathSeriesVersioning(*repo, path, id1, std::nullopt);
|
|
CHECK((ver1 == 1));
|
|
|
|
auto v2 = makeDocInfo(path, "hash_v2", "evolving.txt");
|
|
auto ins2 = repo->insertDocument(v2);
|
|
REQUIRE(ins2);
|
|
auto id2 = ins2.value();
|
|
REQUIRE((id1 != id2));
|
|
|
|
auto priorDocResult = repo->findDocumentByExactPath(path);
|
|
REQUIRE(priorDocResult);
|
|
auto priorDoc = priorDocResult.value();
|
|
REQUIRE(priorDoc.has_value());
|
|
int64_t ver2 = applyPathSeriesVersioning(*repo, path, id2, priorDoc);
|
|
CHECK((ver2 == 2));
|
|
|
|
// v1 is no longer latest
|
|
auto latest1 = repo->getMetadata(id1, "is_latest");
|
|
REQUIRE(latest1);
|
|
REQUIRE(latest1.value().has_value());
|
|
CHECK((latest1.value()->asBoolean() == false));
|
|
|
|
// v2 is latest
|
|
auto latest2 = repo->getMetadata(id2, "is_latest");
|
|
REQUIRE(latest2);
|
|
REQUIRE(latest2.value().has_value());
|
|
CHECK((latest2.value()->asBoolean() == true));
|
|
|
|
auto verMeta2 = repo->getMetadata(id2, "version");
|
|
REQUIRE(verMeta2);
|
|
REQUIRE(verMeta2.value().has_value());
|
|
CHECK((verMeta2.value()->asInteger() == 2));
|
|
|
|
// VersionOf relationship exists
|
|
auto relResult = repo->getRelationships(id2);
|
|
REQUIRE(relResult);
|
|
bool foundVersionOf = false;
|
|
for (const auto& rel : relResult.value()) {
|
|
if (rel.relationshipType == RelationshipType::VersionOf && rel.parentId == id1) {
|
|
foundVersionOf = true;
|
|
break;
|
|
}
|
|
}
|
|
CHECK(foundVersionOf);
|
|
}
|
|
|
|
TEST_CASE_METHOD(VersioningFixture, "Versioning: same hash re-index does not create version edge",
|
|
"[versioning][metadata][catch2]") {
|
|
#ifdef _WIN32
|
|
_putenv_s("YAMS_ENABLE_VERSIONING", "1");
|
|
#else
|
|
setenv("YAMS_ENABLE_VERSIONING", "1", 1);
|
|
#endif
|
|
|
|
auto repo = makeRepo();
|
|
const std::string path = "/path/identical.txt";
|
|
|
|
auto v1 = makeDocInfo(path, "hash_same", "identical.txt");
|
|
auto ins1 = repo->insertDocument(v1);
|
|
REQUIRE(ins1);
|
|
auto id1 = ins1.value();
|
|
applyPathSeriesVersioning(*repo, path, id1, std::nullopt);
|
|
|
|
// When caller detects same hash, versioning is not called (handled by caller).
|
|
// This test verifies the guard logic: if we accidentally called it, it still
|
|
// wouldn't create a second version because the path lookup finds the same doc.
|
|
auto priorDoc = repo->findDocumentByExactPath(path);
|
|
REQUIRE(priorDoc);
|
|
REQUIRE(priorDoc.value().has_value());
|
|
CHECK((priorDoc.value()->sha256Hash == "hash_same"));
|
|
}
|
|
|
|
TEST_CASE_METHOD(VersioningFixture,
|
|
"Versioning: metadata batch failure falls back to best-effort individual writes",
|
|
"[versioning][metadata][catch2]") {
|
|
#ifdef _WIN32
|
|
_putenv_s("YAMS_ENABLE_VERSIONING", "1");
|
|
#else
|
|
setenv("YAMS_ENABLE_VERSIONING", "1", 1);
|
|
#endif
|
|
|
|
auto repo = makeBatchFailingRepo();
|
|
const std::string path = "/path/fallback.txt";
|
|
|
|
auto v1 = makeDocInfo(path, "hash_v1_fallback", "fallback.txt");
|
|
auto ins1 = repo->insertDocument(v1);
|
|
REQUIRE(ins1);
|
|
const auto id1 = ins1.value();
|
|
REQUIRE((applyPathSeriesVersioning(*repo, path, id1, std::nullopt) == 1));
|
|
REQUIRE((repo->batchAttempts == 1));
|
|
REQUIRE((repo->lastBatchSize == 3));
|
|
|
|
repo->failNextBatch = true;
|
|
auto v2 = makeDocInfo(path, "hash_v2_fallback", "fallback.txt");
|
|
auto ins2 = repo->insertDocument(v2);
|
|
REQUIRE(ins2);
|
|
const auto id2 = ins2.value();
|
|
|
|
auto priorDoc = repo->findDocumentByExactPath(path);
|
|
REQUIRE(priorDoc);
|
|
REQUIRE(priorDoc.value().has_value());
|
|
|
|
const auto version = applyPathSeriesVersioning(*repo, path, id2, priorDoc.value());
|
|
CHECK((version == 2));
|
|
CHECK((repo->batchAttempts == 2));
|
|
CHECK((repo->lastBatchSize == 4));
|
|
|
|
auto latest1 = repo->getMetadata(id1, "is_latest");
|
|
REQUIRE(latest1);
|
|
REQUIRE(latest1.value().has_value());
|
|
CHECK((latest1.value()->asBoolean() == false));
|
|
|
|
auto version2 = repo->getMetadata(id2, "version");
|
|
REQUIRE(version2);
|
|
REQUIRE(version2.value().has_value());
|
|
CHECK((version2.value()->asInteger() == 2));
|
|
|
|
auto latest2 = repo->getMetadata(id2, "is_latest");
|
|
REQUIRE(latest2);
|
|
REQUIRE(latest2.value().has_value());
|
|
CHECK((latest2.value()->asBoolean() == true));
|
|
|
|
auto series2 = repo->getMetadata(id2, "series_key");
|
|
REQUIRE(series2);
|
|
REQUIRE(series2.value().has_value());
|
|
CHECK((series2.value()->asString() == path));
|
|
|
|
auto relResult = repo->getRelationships(id2);
|
|
REQUIRE(relResult);
|
|
bool foundVersionOf = false;
|
|
for (const auto& rel : relResult.value()) {
|
|
if (rel.relationshipType == RelationshipType::VersionOf && rel.parentId == id1) {
|
|
foundVersionOf = true;
|
|
break;
|
|
}
|
|
}
|
|
CHECK(foundVersionOf);
|
|
}
|
|
|
|
TEST_CASE_METHOD(VersioningFixture, "Versioning respects YAMS_ENABLE_VERSIONING=0",
|
|
"[versioning][metadata][catch2]") {
|
|
#ifdef _WIN32
|
|
_putenv_s("YAMS_ENABLE_VERSIONING", "0");
|
|
#else
|
|
setenv("YAMS_ENABLE_VERSIONING", "0", 1);
|
|
#endif
|
|
|
|
auto repo = makeRepo();
|
|
auto docInfo = makeDocInfo("/path/disabled.txt", "hash_disabled", "disabled.txt");
|
|
auto ins = repo->insertDocument(docInfo);
|
|
REQUIRE(ins);
|
|
auto docId = ins.value();
|
|
|
|
int64_t version = applyPathSeriesVersioning(*repo, docInfo.filePath, docId, std::nullopt);
|
|
CHECK((version == 0));
|
|
|
|
auto verMeta = repo->getMetadata(docId, "version");
|
|
REQUIRE(verMeta);
|
|
CHECK(!verMeta.value().has_value());
|
|
}
|
|
|
|
TEST_CASE_METHOD(VersioningFixture, "Retention selects only superseded versions",
|
|
"[versioning][metadata][catch2]") {
|
|
auto repo = makeRepo();
|
|
const std::string path = "/path/retained.txt";
|
|
std::vector<int64_t> ids;
|
|
for (int version = 1; version <= 3; ++version) {
|
|
auto doc = makeDocInfo(path, "retention_hash_" + std::to_string(version), "retained.txt");
|
|
auto inserted = repo->insertDocument(doc);
|
|
REQUIRE(inserted);
|
|
ids.push_back(inserted.value());
|
|
REQUIRE(repo->setMetadata(ids.back(), "series_key", MetadataValue(path)));
|
|
REQUIRE(
|
|
repo->setMetadata(ids.back(), "version", MetadataValue(static_cast<int64_t>(version))));
|
|
REQUIRE(repo->setMetadata(ids.back(), "is_latest", MetadataValue(version == 3)));
|
|
}
|
|
|
|
auto candidates = selectVersionRetentionCandidates(*repo, {.keepLatest = 2});
|
|
REQUIRE(candidates);
|
|
REQUIRE(candidates.value().size() == 1);
|
|
CHECK(candidates.value().front().document.id == ids.front());
|
|
CHECK(candidates.value().front().version == 1);
|
|
}
|
|
|
|
TEST_CASE_METHOD(VersioningFixture, "Retention never selects the marked latest version",
|
|
"[versioning][metadata][catch2]") {
|
|
auto repo = makeRepo();
|
|
const std::string path = "/path/latest-protected.txt";
|
|
for (int version = 1; version <= 2; ++version) {
|
|
auto doc =
|
|
makeDocInfo(path, "protected_hash_" + std::to_string(version), "latest-protected.txt");
|
|
auto inserted = repo->insertDocument(doc);
|
|
REQUIRE(inserted);
|
|
REQUIRE(repo->setMetadata(inserted.value(), "series_key", MetadataValue(path)));
|
|
REQUIRE(repo->setMetadata(inserted.value(), "version",
|
|
MetadataValue(static_cast<int64_t>(version))));
|
|
// A stale version number must not cause a document explicitly marked
|
|
// latest to be pruned.
|
|
REQUIRE(repo->setMetadata(inserted.value(), "is_latest", MetadataValue(version == 1)));
|
|
}
|
|
|
|
auto candidates = selectVersionRetentionCandidates(*repo, {.keepLatest = 1});
|
|
REQUIRE(candidates);
|
|
REQUIRE(candidates.value().size() == 1);
|
|
CHECK(candidates.value().front().version == 2);
|
|
}
|
|
|
|
TEST_CASE_METHOD(VersioningFixture, "Prune candidates include stored native build artifacts",
|
|
"[prune][metadata][catch2]") {
|
|
auto repo = makeRepo();
|
|
const auto insert = [&](const std::string& path, const std::string& hash) {
|
|
auto result = repo->insertDocument(
|
|
makeDocInfo(path, hash, std::filesystem::path(path).filename().string()));
|
|
REQUIRE(result);
|
|
};
|
|
insert("/workspace/x64/Release/tool.ilk", "build-ilk");
|
|
insert("/workspace/build/Debug/tool.dSYM/Contents/Resources/DWARF/tool", "build-dsym");
|
|
insert("/workspace/src/tool.cpp", "source-cpp");
|
|
|
|
integrity::PruneConfig config;
|
|
config.categories = {"build-artifacts"};
|
|
const auto candidates = integrity::RepairManager::queryCandidatesForPrune(*repo, config);
|
|
|
|
REQUIRE(candidates.size() == 2);
|
|
CHECK(std::ranges::any_of(
|
|
candidates, [](const auto& candidate) { return candidate.path.ends_with("tool.ilk"); }));
|
|
CHECK(std::ranges::any_of(
|
|
candidates, [](const auto& candidate) { return candidate.path.ends_with("/tool"); }));
|
|
}
|
|
|
|
TEST_CASE_METHOD(VersioningFixture, "Prune candidates match dotted metadata extensions",
|
|
"[prune][metadata][catch2]") {
|
|
auto repo = makeRepo();
|
|
auto log = makeDocInfo("/workspace/logs/daemon.log", "dotted-log", "daemon.log");
|
|
log.fileExtension = ".log";
|
|
REQUIRE(repo->insertDocument(log));
|
|
|
|
integrity::PruneConfig config;
|
|
config.categories = {"all"};
|
|
const auto candidates = integrity::RepairManager::queryCandidatesForPrune(*repo, config);
|
|
|
|
REQUIRE(candidates.size() == 1);
|
|
CHECK(candidates.front().category == "logs");
|
|
}
|