yams/tests/unit/integrity/chunk_validator_test.cpp
Trevon bfe7cd94ca
test: expand storage-readiness coverage — 48.1%→59.8% broad, 74.3% narrow (#38)
* fix(daemon): avoid socket cancel on stop

Signal active sockets with shutdown only so pending I/O wakes without entering Boost.Asio's reactor cancel path during teardown.

* fix(metadata): open read pools read-only

* ci: fix coverage report generation

* fix(metadata): tag unscoped pool holders

* test(daemon): avoid ipc teardown takeover

* ci: skip plugin coverage objects

* fix(daemon): repair completed FTS5 ghosts

* build(vector): guard unusable FAISS headers

* ci: use clang gcov for coverage

* test(daemon): stabilize arm CI flakes

* Harden search and compression numeric config

* fix(daemon): reconcile orphan vector embeddings

* fix: address vector cleanup review comments

* chore(main): release 0.16.1

* Harden search ranking divisors

* Harden snippet distance and command registration

* test(storage): add readiness coverage gate

Document the distributed sync readiness surface and add direct adapter, refcount, and GC failure-path coverage.

* test(storage): cover strict resolver fallback

* fix(search): tighten retrieval ranking

Honor component weights in default RRF, over-fetch vector chunks before document aggregation, and persist topology centroids for sparse-guided routing.

* fix(search): route weak queries through topology

* fix(search): improve rerank and weak queries

Wire provider-backed cross reranking, add direct Simeon candidates for weak lexical pools, and report stage-level retrieval metrics.

* fix(storage): honor explicit plugin names

Add readiness coverage for factory, URL backend, and compressed storage failure paths.

* fix(storage): avoid remote size listing

* fix(api): rollback failed content adds

* test(storage): cover engine readiness

* test(storage): cover compressed readiness

* fix(core): harden delete and recovery paths

Route prune and local delete fallback through DocumentService so content, metadata, vectors, and KG cleanup stay consistent.

Treat transient SQLite quick_check lock failures as ResourceBusy, refresh IPC activity on streaming writes, and preserve reranker config in tuner seeding.

* test(storage): cover URL backend readiness

* fix(api): harden content rollback

* feat(storage): add list seam, URL listing, and write-edge coverage

* feat(api): reconcile ambiguous remote manifest writes

* feat(storage): add deep compressed scan validation

* test(storage): add plugin loader error path coverage

* fix(gc): prune reference rows after collection

* test(api,storage): add health, compact, gc, and verify edge coverage

* docs: update storage-readiness coverage to 71.8%

* test(metadata): add connection pool shutdown-while-leased coverage

* test(metadata): add tree builder snapshot root stability coverage

* test(vector): add before-init and after-close operation rejection coverage

* test(metadata): add database post-close, open-failure, and cache-clear coverage

* test(integrity): add P2P wrong-hash rejection + batch matrix resolution

* docs: audit and resolve all matrix rows — 18 fully covered

* test(metadata): add failed-migration and rollback coverage

* test(storage): add getBlockSize-invalid-key, empty-verify, PUT/DELETE retry coverage

* feat(storage): add file-open and rename failure hooks + ContentStore retrieve edge coverage

* docs: add broad-surface coverage with per-subsystem breakdown

* test(integrity): add chunk validator manifest validation coverage

* test(vector): add batch insert, delete, and update operation coverage

* docs: update integrity subsystem coverage to 46.4%

* docs: consolidate coverage report — clean matrix, remove boilerplate

* docs: update coverage with complete-suite numbers — 59.8% broad, 74.3% narrow

* test(storage): add URLBackend HEAD exists with 2xx coverage

* bump submodules

* fix: address PR review — curl_slist leak, ec ordering, CAS rollback, production hooks

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-25 22:58:15 -04:00

161 lines
5.5 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <yams/crypto/hasher.h>
#include <yams/integrity/chunk_validator.h>
#include <yams/manifest/manifest_manager.h>
#include <cstring>
#include <string>
#include <vector>
using namespace yams::integrity;
namespace {
std::vector<std::byte> toBytes(const std::string& s) {
std::vector<std::byte> out(s.size());
std::memcpy(out.data(), s.data(), s.size());
return out;
}
} // namespace
TEST_CASE("ChunkValidator single chunk validation", "[integrity][chunk-validator]") {
ChunkValidationConfig cfg;
cfg.maxParallelValidations = 4;
ChunkValidator validator(cfg);
SECTION("validates chunk with correct hash") {
auto hasher = yams::crypto::createSHA256Hasher();
std::string data = "test chunk data";
auto bytes = toBytes(data);
std::string hash = hasher->hash(std::span<const std::byte>(bytes.data(), bytes.size()));
auto result =
validator.validateChunk(std::span<const std::byte>(bytes.data(), bytes.size()), hash);
CHECK(result.isValid);
CHECK(result.errorMessage.empty());
}
SECTION("detects chunk with incorrect hash") {
std::string data = "test chunk data";
auto bytes = toBytes(data);
auto result = validator.validateChunk(
std::span<const std::byte>(bytes.data(), bytes.size()), "bad-hash");
CHECK_FALSE(result.isValid);
CHECK_FALSE(result.errorMessage.empty());
}
SECTION("validates empty chunk") {
auto hasher = yams::crypto::createSHA256Hasher();
std::vector<std::byte> emptyBytes;
std::string hash = hasher->hash(std::span<const std::byte>(emptyBytes.data(), 0));
auto result =
validator.validateChunk(std::span<const std::byte>(emptyBytes.data(), 0), hash);
CHECK(result.isValid);
}
}
TEST_CASE("ChunkValidator configuration", "[integrity][chunk-validator]") {
ChunkValidationConfig cfg;
cfg.maxParallelValidations = 8;
cfg.failOnFirstError = true;
ChunkValidator validator(cfg);
SECTION("getConfig returns current configuration") {
const auto& currentCfg = validator.getConfig();
CHECK(currentCfg.maxParallelValidations == 8);
CHECK(currentCfg.failOnFirstError);
}
SECTION("setConfig updates configuration") {
ChunkValidationConfig newCfg;
newCfg.maxParallelValidations = 2;
newCfg.failOnFirstError = false;
validator.setConfig(newCfg);
const auto& currentCfg = validator.getConfig();
CHECK(currentCfg.maxParallelValidations == 2);
CHECK_FALSE(currentCfg.failOnFirstError);
}
}
TEST_CASE("ChunkValidator validates manifest with valid chunks", "[integrity][chunk-validator]") {
ChunkValidationConfig cfg;
cfg.maxParallelValidations = 4;
ChunkValidator validator(cfg);
auto hasher = yams::crypto::createSHA256Hasher();
std::string data1 = "chunk one data";
std::string data2 = "chunk two is longer";
auto bytes1 = toBytes(data1);
auto bytes2 = toBytes(data2);
std::string hash1 = hasher->hash(std::span<const std::byte>(bytes1.data(), bytes1.size()));
std::string hash2 = hasher->hash(std::span<const std::byte>(bytes2.data(), bytes2.size()));
yams::manifest::Manifest manifest;
manifest.fileHash = hasher->hash(std::span<const std::byte>(bytes1.data(), bytes1.size()));
manifest.fileSize = data1.size() + data2.size();
manifest.chunks.push_back(
{.hash = hash1, .offset = 0, .size = static_cast<uint32_t>(data1.size())});
manifest.chunks.push_back({.hash = hash2,
.offset = static_cast<uint64_t>(data1.size()),
.size = static_cast<uint32_t>(data2.size())});
auto chunkProvider = [&](const std::string& hash) -> yams::Result<std::vector<std::byte>> {
if (hash == hash1)
return bytes1;
if (hash == hash2)
return bytes2;
return yams::Error{yams::ErrorCode::ChunkNotFound, "no such chunk"};
};
auto report = validator.validateManifest(manifest, chunkProvider);
CHECK(report.overallSuccess);
CHECK(report.validChunks == 2);
CHECK(report.invalidChunks == 0);
CHECK(report.failures.empty());
}
TEST_CASE("ChunkValidator detects invalid chunk in manifest", "[integrity][chunk-validator]") {
ChunkValidationConfig cfg;
cfg.failOnFirstError = true;
ChunkValidator validator(cfg);
auto hasher = yams::crypto::createSHA256Hasher();
std::string goodData = "good chunk";
std::string badData = "bad chunk data";
auto goodBytes = toBytes(goodData);
auto badBytes = toBytes(badData);
std::string goodHash =
hasher->hash(std::span<const std::byte>(goodBytes.data(), goodBytes.size()));
yams::manifest::Manifest manifest;
manifest.fileHash = goodHash;
manifest.fileSize = goodData.size() + badData.size();
manifest.chunks.push_back(
{.hash = goodHash, .offset = 0, .size = static_cast<uint32_t>(goodData.size())});
manifest.chunks.push_back({.hash = "wrong-hash-for-bad-chunk",
.offset = static_cast<uint64_t>(goodData.size()),
.size = static_cast<uint32_t>(badData.size())});
auto chunkProvider = [&](const std::string& hash) -> yams::Result<std::vector<std::byte>> {
if (hash == goodHash)
return goodBytes;
return badBytes;
};
auto report = validator.validateManifest(manifest, chunkProvider);
CHECK_FALSE(report.overallSuccess);
CHECK(report.invalidChunks > 0);
}