* fix(daemon): harden model state ownership Keep ModelPoolConfig single-defined and serialize telemetry snapshots to avoid ODR drift and torn concurrent reads. * refactor(daemon): consolidate ingest pollers Require the wake-timer contract so every post-ingest stage shares one event-driven poller configuration. * refactor(daemon): unify repair execution Share admission, operation ordering, cancellation, progress, and result aggregation between synchronous and asynchronous repair paths. * refactor(daemon): unify runtime tuning Resolve startup and reload settings through one typed path so parsing, precedence, and range validation cannot drift. * refactor(daemon)!: remove dead IPC surfaces Delete unreferenced framing, cancellation, and compatibility paths while keeping the active streaming protocol intact. BREAKING CHANGE: callers using connectAsync or legacy MessageFramer and FrameReader helpers must migrate to connect, frame_message_into, feed, and get_frame. * refactor(daemon): hide test hooks Preserve test-library linkage without exposing daemon-only probes through production headers or external symbols. * fix(daemon): avoid replaying successful adds A fresh-connection transport retry reuses the response handler. Clear its stale EOF error when the retry returns a valid add response so the outer retry loop does not enqueue the document again. * bench(ingest): enforce lossless pipeline runs Separate admission, storage, drain, enrichment, and searchability timing. Decision-grade plans now reject drops, duplicate processing, incomplete pipelines, and experiment identity drift. * bench(search): expose cold seed ingest timings Pair generalized-memory quality arms with admission, storage, drain, searchability, and enrichment timing from their shared cold seed. Keep the observation distinct from repeated throughput measurements. * perf(ingest): batch deferred content indexing Directory ingestion already schedules content work through post-ingest. Avoid duplicate inline indexing and coalesce normal-priority trickles within a bounded window. * feat(metadata): expose insert writer metrics Separate queue, batch-apply, and transaction time so ingestion runs can attribute connection pressure without changing write semantics. * fix(daemon): retry transient refused connects The daemon socket can be bound before its listener is ready during startup. Reuse the existing bounded retry budget for that refusal window. * bench(ingest): attribute submission path costs Decision-grade runs need stable coalescing identity and separate queue, apply, transaction, and estimated connection-wait costs. * perf(ingest): batch durable document storage Commit chunks, manifests, references, and metadata in bounded batches while preserving publication only after durable reference success. * perf(ingest): coalesce bulk post-processing Use a 16-item bulk cap while keeping latency-sensitive daemon ingestion on the existing four-item priority quota. * bench(ingest): record post-ingest batch size Include the effective cap in metrics and experiment identity so runs with different batching policies cannot be compared as one system. * perf(ingest): batch queued storage waves Preserve single-document fallback semantics and execute inline when the coordinator has one worker to avoid self-deadlock. * perf(ingest): route bulk enrichment batches Keep quota-sized interactive groups on the RPC lane while sending larger lossless groups through bounded coalescing. * test(daemon): initialize task fixtures * perf(ingest): batch post-ingest dispatch * perf(ingest): widen directory storage waves Borrow from a full wave when one document would otherwise fall through to single-document storage. * perf(storage): bound batch writer fanout Reuse a fixed worker set so batch size no longer creates one thread and payload copy per object. * perf(ingest): align storage batch drains Dispatch one configured batch through storage so intermediate waves do not fragment downstream metadata work. * fix(ingest): commit metadata state atomically Fold first-version, extracted-title, and embedding-completion state into existing transactions. Read committed extraction state so duplicate-content tasks cannot double-count corpus counters. * bench(ingest): measure production submission Drive pipelined files through DocumentIngestionService. Export dispatch, fingerprint, and enqueue costs through xplan. * fix(xplan): preserve native failures Report child signals and bounded stderr before readiness checks so assertions are not masked as missing traces. * fix(daemon): preserve embedding success hashes * fix(daemon): serialize pooled socket shutdown * fix(daemon): harden batch publication shutdown * fix(storage): use compat jthread shim * fix(daemon): avoid poller template shadowing * fix(search): keep vector pipeline test enabled * fix: harden shutdown and fallback paths Demote expected shutdown SQL interruptions, ignore readonly FTS integrity noise, avoid stopped-executor socket hangs, and preserve metadata writer fallback outcomes.
147 lines
4.3 KiB
C++
147 lines
4.3 KiB
C++
// Unit tests for extracted doctor check modules
|
|
// Catch2 tests for DbIntegrityCheck
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include <yams/cli/doctor/checks/db_integrity.h>
|
|
#include <yams/cli/doctor/doctor_context.h>
|
|
#include <yams/cli/yams_cli.h>
|
|
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <memory>
|
|
#include <sstream>
|
|
|
|
#include "../../../common/test_helpers_catch2.h"
|
|
|
|
namespace fs = std::filesystem;
|
|
using namespace yams::cli::doctor;
|
|
|
|
// Catch2's expression decomposition intentionally overloads comparison operators.
|
|
// NOLINTBEGIN(bugprone-chained-comparison)
|
|
|
|
namespace {
|
|
|
|
struct DoctorTestEnv {
|
|
fs::path dataDir;
|
|
yams::test::ScopedEnvVar dataEnv;
|
|
yams::test::ScopedEnvVar homeEnv;
|
|
|
|
explicit DoctorTestEnv()
|
|
: dataDir(yams::test::make_temp_dir("yams_doctor_test_")),
|
|
dataEnv("YAMS_DATA_DIR", dataDir.string()), homeEnv("HOME", dataDir.string()) {
|
|
fs::create_directories(dataDir);
|
|
}
|
|
|
|
std::unique_ptr<yams::cli::YamsCLI> makeCli() { return std::make_unique<yams::cli::YamsCLI>(); }
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("DbIntegrityCheck - empty data dir has no issues", "[doctor][db_integrity]") {
|
|
DoctorTestEnv env;
|
|
|
|
auto cli = env.makeCli();
|
|
DoctorContext ctx(cli.get());
|
|
DbIntegrityCheck check;
|
|
auto result = check.execute(ctx);
|
|
|
|
CHECK(result.metadataWalBytes == 0);
|
|
CHECK(result.vectorWalBytes == 0);
|
|
CHECK(result.corruptArtifacts.empty());
|
|
CHECK(result.ok == true);
|
|
}
|
|
|
|
TEST_CASE("DbIntegrityCheck - detects WAL file presence", "[doctor][db_integrity]") {
|
|
DoctorTestEnv env;
|
|
|
|
// Create a small WAL file (under the 128 MB warn threshold)
|
|
fs::path walPath = env.dataDir / "yams.db-wal";
|
|
{
|
|
std::ofstream w(walPath, std::ios::binary);
|
|
for (int i = 0; i < 1024; ++i)
|
|
w.put(static_cast<char>(i % 256));
|
|
w.close();
|
|
}
|
|
|
|
auto cli = env.makeCli();
|
|
DoctorContext ctx(cli.get());
|
|
DbIntegrityCheck check;
|
|
auto result = check.execute(ctx);
|
|
|
|
CHECK(result.metadataWalBytes == 1024);
|
|
CHECK(result.ok == true);
|
|
}
|
|
|
|
TEST_CASE("DbIntegrityCheck - warns on large WAL", "[doctor][db_integrity]") {
|
|
DoctorTestEnv env;
|
|
|
|
// Create a sparse WAL that reports > 128 MB. Use seek + put for sparse file.
|
|
fs::path walPath = env.dataDir / "yams.db-wal";
|
|
{
|
|
std::ofstream w(walPath, std::ios::binary);
|
|
w.seekp(200ULL * 1024ULL * 1024ULL - 1);
|
|
w.put('\0');
|
|
w.close();
|
|
}
|
|
|
|
auto cli = env.makeCli();
|
|
DoctorContext ctx(cli.get());
|
|
DbIntegrityCheck check;
|
|
auto result = check.execute(ctx);
|
|
|
|
CHECK(result.metadataWalBytes >= 128ULL * 1024ULL * 1024ULL);
|
|
CHECK(result.ok == false);
|
|
}
|
|
|
|
TEST_CASE("DbIntegrityCheck - readonly FTS validation row is not corruption",
|
|
"[doctor][db_integrity][readonly][fts5]") {
|
|
const std::string readonlyFtsRow =
|
|
"unable to validate the inverted index for FTS5 table main.kg_aliases_fts: attempt to "
|
|
"write a readonly database";
|
|
|
|
CHECK(testing_isReadOnlyFtsValidationLine(readonlyFtsRow));
|
|
CHECK_FALSE(testing_isReadOnlyFtsValidationLine(
|
|
"unable to validate the inverted index for FTS5 table main.kg_aliases_fts: database is "
|
|
"malformed"));
|
|
CHECK_FALSE(testing_isReadOnlyFtsValidationLine("row 3 missing from index idx_documents_path"));
|
|
}
|
|
|
|
TEST_CASE("DbIntegrityCheck - detects corrupt artifacts", "[doctor][db_integrity]") {
|
|
DoctorTestEnv env;
|
|
|
|
// Create a corrupt artifact file
|
|
fs::path corruptPath = env.dataDir / "yams.db.corrupt-20260501T120000Z";
|
|
{
|
|
std::ofstream c(corruptPath, std::ios::binary);
|
|
c << "corrupt data";
|
|
c.close();
|
|
}
|
|
|
|
auto cli = env.makeCli();
|
|
DoctorContext ctx(cli.get());
|
|
DbIntegrityCheck check;
|
|
auto result = check.execute(ctx);
|
|
|
|
CHECK_FALSE(result.corruptArtifacts.empty());
|
|
CHECK(result.ok == false);
|
|
}
|
|
|
|
TEST_CASE("DbIntegrityCheck - render produces output", "[doctor][db_integrity]") {
|
|
DoctorTestEnv env;
|
|
|
|
auto cli = env.makeCli();
|
|
DoctorContext ctx(cli.get());
|
|
DbIntegrityCheck check;
|
|
auto result = check.execute(ctx);
|
|
|
|
std::ostringstream os;
|
|
DbIntegrityCheck::render(os, result);
|
|
auto output = os.str();
|
|
|
|
CHECK_FALSE(output.empty());
|
|
CHECK(output.find("yams.db-wal") != std::string::npos);
|
|
CHECK(output.find("vectors.db-wal") != std::string::npos);
|
|
}
|
|
|
|
// NOLINTEND(bugprone-chained-comparison)
|