yams/tests/unit/app/services/grep_service_test.cpp
Trevon 4e97b31fef
Some checks failed
Fuzz Smoke / libFuzzer smoke (push) Has been cancelled
chore(warnings): clean up compiler warnings on GCC, Clang and Apple Clang (#279)
* chore(warnings): default-initialize aggregate members left out of brace-init

GCC -Wmissing-field-initializers fired about 1300 times where tests and
src build structs with a short positional brace list. Give the omitted
members an empty default member initializer. Value-initialization from
{} is what aggregate init already did for them, so behavior is
unchanged.

Signed-off-by: trvon <me@trevon.dev>

* chore(warnings): remove or annotate unused parameters, functions and variables

- init_command, s3_signer test, request_handler_lifetime test: comment out
  the unused parameter name.
- video_content_handler: path is only read under YAMS_HAVE_FFPROBE.
- vector_memory_scaling_bench: main() takes no arguments.
- daemon_status_render: delete unused neutralText helper.
- DaemonMetrics: mallocStackLoggingNoCompactEnabled is only called in the
  __APPLE__ block, so define it there.
- SocketServer: einval_streak only matters in the __APPLE__ EINVAL path.
- onnx_plugin_harness test, vector_backend_engine_compare bench,
  work_coordinator test: mark platform-conditional or busy-loop
  variables [[maybe_unused]].

Signed-off-by: trvon <git@trevon.dev>

* chore(warnings): fix sign-compare, range-loop, nodiscard and dangling-ref in tests

- search_admission_controller: count started workers in std::uint32_t so
  the comparison with the uint32 cap is same-signed.
- p2p_manager: iterate the invalid-host list by const reference.
- integrity_verifier: discard the nodiscard results the tests do not
  inspect with static_cast<void>.
- post_ingest_queue: findNode/findEdge take std::string_view so no
  temporary std::string is created for the call (GCC
  -Wdangling-reference false positive on the returned reference).
- topology_ablation_bench: the doc comment ended a line with a backslash,
  which continues the comment (-Wcomment); show the env export on its own
  line.

Signed-off-by: trvon <git@trevon.dev>

* chore(warnings): drop redundant std::move of a const captured json

The coroutine lambda is not mutable, so the captured optional<json> is
const and std::move(*progressToken) already selected the copy assignment.
Drop the std::move so the code states what actually happens.

Signed-off-by: trvon <git@trevon.dev>

* chore(warnings): mark __int128 use in db_integrity_stamp as an extension

-Wpedantic flags __int128. Use a __extension__ typedef, the GCC/Clang
idiom for an intentional extension, instead of the bare keyword.

Signed-off-by: trvon <git@trevon.dev>

* chore(warnings): remove always-true unsigned >= 0 comparisons

GCC -Wtype-limits flagged 51 test assertions of the form
CHECK(unsignedValue >= 0) that can never fail, plus a dead
`totalCount.value() < 0` clause in the graph listByType handler (the
count is std::size_t). In the tests keep the evaluation as a
static_cast<void> so the accessors are still exercised; in the handler
drop the dead clause and keep the range check.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>
Signed-off-by: trvon <git@trevon.dev>
2026-10-08 14:30:28 -06:00

900 lines
30 KiB
C++

#include <atomic>
#include <catch2/catch_test_macros.hpp>
#include <yams/api/content_store_builder.h>
#include <yams/app/services/retrieval_path_policy.hpp>
#include <yams/app/services/services.hpp>
#include <yams/compat/unistd.h>
#include <yams/metadata/connection_pool.h>
#include <yams/metadata/database.h>
#include <yams/metadata/knowledge_graph_store.h>
#include <yams/metadata/metadata_repository.h>
#include <yams/metadata/migration.h>
#include <yams/metadata/query_helpers.h>
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <ranges>
#include "common/test_helpers_catch2.h"
#if defined(_WIN32) && __has_include(<onnxruntime_c_api.h>)
#include <onnxruntime_c_api.h>
#define YAMS_ORT_API_VERSION ORT_API_VERSION
#else
#define YAMS_ORT_API_VERSION 0
#endif
#ifdef _WIN32
TEST_CASE("GrepService - Windows disabled due to ONNX runtime instability",
"[grep][windows][skip]") {
SUCCEED("GrepService tests are disabled on Windows pending ONNX Runtime cleanup fixes; this "
"keeps the suite green.");
}
#else
#define SKIP_GREP_ON_WINDOWS() ((void)0)
using namespace yams;
using namespace yams::app::services;
using namespace yams::metadata;
using namespace yams::api;
namespace {
class FlakyMetadataRepository final : public MetadataRepository {
public:
explicit FlakyMetadataRepository(ConnectionPool& pool) : MetadataRepository(pool) {}
void setGetAllMetadataFailures(std::size_t count) { getAllMetadataFailures_ = count; }
void setQueryDocumentsFailures(std::size_t count) { queryFailures_ = count; }
void setFindDocumentsByTagsFailures(std::size_t count) { findDocumentsByTagsFailures_ = count; }
Result<std::unordered_map<std::string, MetadataValue>>
getAllMetadata(int64_t documentId) override {
if (yams::test::consume_failure(getAllMetadataFailures_)) {
return Error{ErrorCode::NotInitialized, "metadata warming up"};
}
return MetadataRepository::getAllMetadata(documentId);
}
Result<std::vector<DocumentInfo>>
queryDocuments(const metadata::DocumentQueryOptions& options) override {
if (yams::test::consume_failure(queryFailures_)) {
return Error{ErrorCode::DatabaseError, "database is locked"};
}
return MetadataRepository::queryDocuments(options);
}
Result<std::unordered_map<int64_t, std::unordered_map<std::string, MetadataValue>>>
getMetadataForDocuments(std::span<const int64_t> documentIds) override {
if (yams::test::consume_failure(getAllMetadataFailures_)) {
return Error{ErrorCode::NotInitialized, "metadata warming up"};
}
return MetadataRepository::getMetadataForDocuments(documentIds);
}
Result<std::vector<DocumentInfo>> findDocumentsByTags(const std::vector<std::string>& tags,
bool matchAll = false) override {
if (yams::test::consume_failure(findDocumentsByTagsFailures_)) {
return Error{ErrorCode::NotInitialized, "tag index warming up"};
}
return MetadataRepository::findDocumentsByTags(tags, matchAll);
}
Result<std::optional<DocumentInfo>> findDocumentByExactPath(const std::string& path) override {
return MetadataRepository::findDocumentByExactPath(path);
}
private:
std::size_t getAllMetadataFailures_{0};
std::size_t queryFailures_{0};
std::size_t findDocumentsByTagsFailures_{0};
};
class RetrieveBytesFailStore final : public IContentStore {
public:
explicit RetrieveBytesFailStore(std::shared_ptr<IContentStore> inner)
: inner_(std::move(inner)) {}
Result<StoreResult> store(const std::filesystem::path& path, const ContentMetadata& metadata,
api::ProgressCallback progress) override {
return inner_->store(path, metadata, progress);
}
Result<RetrieveResult> retrieve(const std::string& hash,
const std::filesystem::path& outputPath,
api::ProgressCallback progress) override {
return inner_->retrieve(hash, outputPath, progress);
}
Result<StoreResult> storeStream(std::istream& stream, const ContentMetadata& metadata,
api::ProgressCallback progress) override {
return inner_->storeStream(stream, metadata, progress);
}
Result<RetrieveResult> retrieveStream(const std::string& hash, std::ostream& output,
api::ProgressCallback progress) override {
return inner_->retrieveStream(hash, output, progress);
}
Result<StoreResult> storeBytes(std::span<const std::byte> data,
const ContentMetadata& metadata) override {
return inner_->storeBytes(data, metadata);
}
Result<std::vector<std::byte>> retrieveBytes(const std::string&) override {
return Error{ErrorCode::NotFound, "retrieveBytes disabled for test"};
}
Result<std::vector<std::byte>> retrieveBytesPrefix(const std::string&, std::size_t) override {
return Error{ErrorCode::NotFound, "retrieveBytesPrefix disabled for test"};
}
Result<RawContent> retrieveRaw(const std::string& hash) override {
return inner_->retrieveRaw(hash);
}
std::future<Result<RawContent>> retrieveRawAsync(const std::string& hash) override {
return inner_->retrieveRawAsync(hash);
}
Result<bool> exists(const std::string& hash) const override { return inner_->exists(hash); }
Result<bool> remove(const std::string& hash) override { return inner_->remove(hash); }
Result<ContentMetadata> getMetadata(const std::string& hash) const override {
return inner_->getMetadata(hash);
}
Result<void> updateMetadata(const std::string& hash, const ContentMetadata& metadata) override {
return inner_->updateMetadata(hash, metadata);
}
std::vector<Result<StoreResult>>
storeBatch(const std::vector<std::filesystem::path>& paths,
const std::vector<ContentMetadata>& metadata) override {
return inner_->storeBatch(paths, metadata);
}
std::vector<Result<bool>> removeBatch(const std::vector<std::string>& hashes) override {
return inner_->removeBatch(hashes);
}
ContentStoreStats getStats() const override { return inner_->getStats(); }
HealthStatus checkHealth() const override { return inner_->checkHealth(); }
Result<void> verify(api::ProgressCallback progress) override {
return inner_->verify(progress);
}
Result<void> compact(api::ProgressCallback progress) override {
return inner_->compact(progress);
}
Result<void> garbageCollect(api::ProgressCallback progress) override {
return inner_->garbageCollect(progress);
}
private:
std::shared_ptr<IContentStore> inner_;
};
struct GrepFixture {
GrepFixture() {
tmpDir_ =
std::filesystem::temp_directory_path() / ("grep_test_" + std::to_string(::getpid()));
std::filesystem::create_directories(tmpDir_);
db_ = std::make_unique<Database>();
auto dbPath = tmpDir_ / "yams.db";
REQUIRE(db_->open(dbPath.string(), ConnectionMode::Create));
pool_ = std::make_unique<ConnectionPool>(dbPath.string(), ConnectionPoolConfig{});
repo_ = std::make_shared<MetadataRepository>(*pool_);
KnowledgeGraphStoreConfig kgConfig;
kgConfig.enable_wal = false;
auto kgResult = makeSqliteKnowledgeGraphStore(*pool_, kgConfig);
REQUIRE(kgResult.has_value());
kgStore_ = std::shared_ptr<KnowledgeGraphStore>(std::move(kgResult).value());
repo_->setKnowledgeGraphStore(kgStore_);
MigrationManager mm(*db_);
REQUIRE(mm.initialize());
mm.registerMigrations(YamsMetadataMigrations::getAllMigrations());
REQUIRE(mm.migrate());
ContentStoreBuilder builder;
auto storeRes = builder.withStoragePath(tmpDir_ / "storage")
.withCompression(false)
.withDeduplication(false)
.build();
REQUIRE(storeRes);
store_ = std::shared_ptr<IContentStore>(
const_cast<std::unique_ptr<IContentStore>&>(storeRes.value()).release());
ctx_.service_manager = nullptr;
ctx_.store = store_;
ctx_.metadataRepo = repo_;
ctx_.searchEngine = nullptr;
ctx_.kgStore = kgStore_;
grepService_ = makeGrepService(ctx_);
}
~GrepFixture() {
grepService_.reset();
ctx_.metadataRepo.reset();
ctx_.store.reset();
ctx_.searchEngine.reset();
ctx_.kgStore.reset();
repo_.reset();
kgStore_.reset();
pool_.reset();
db_.reset();
store_.reset();
std::filesystem::remove_all(tmpDir_);
}
void addDocument(std::string_view name, std::string_view content) {
auto path = tmpDir_ / name;
std::filesystem::create_directories(path.parent_path());
std::ofstream{path} << content;
auto docService = makeDocumentService(ctx_);
StoreDocumentRequest req;
req.path = path.string();
REQUIRE(docService->store(req));
}
void addHotContent(std::string_view name, std::string_view content,
std::string_view mimeType = "text/plain") {
(void)mimeType;
auto path = (tmpDir_ / name).string();
auto docRes = repo_->findDocumentByExactPath(path);
REQUIRE(docRes.has_value());
REQUIRE(docRes.value().has_value());
DocumentContent docContent;
docContent.documentId = docRes.value()->id;
docContent.contentText = std::string(content);
docContent.contentLength = static_cast<int64_t>(docContent.contentText.size());
docContent.extractionMethod = "test";
docContent.language = "en";
REQUIRE(repo_->insertContent(docContent));
}
auto grep(const GrepRequest& req) const { return grepService_->grep(req); }
void seedRelationsForFile(const std::string& filePath, const std::string& hash) const {
REQUIRE(kgStore_ != nullptr);
REQUIRE_FALSE(filePath.empty());
REQUIRE_FALSE(hash.empty());
KGNode fileNode;
fileNode.nodeKey = "path:file:" + filePath;
fileNode.type = std::string("file");
fileNode.label = filePath;
auto fileNodeId = kgStore_->upsertNode(fileNode);
REQUIRE(fileNodeId.has_value());
KGNode docNode;
docNode.nodeKey = "doc:" + hash;
docNode.type = std::string("document");
docNode.label = filePath;
auto docNodeId = kgStore_->upsertNode(docNode);
REQUIRE(docNodeId.has_value());
KGNode symbolNode;
symbolNode.nodeKey = "symbol:test:" + hash;
symbolNode.type = std::string("symbol");
symbolNode.label = std::string("SeededSymbol");
auto symbolNodeId = kgStore_->upsertNode(symbolNode);
REQUIRE(symbolNodeId.has_value());
KGNode moduleNode;
moduleNode.nodeKey = "module:test:" + hash;
moduleNode.type = std::string("module");
moduleNode.label = std::string("SeededModule");
auto moduleNodeId = kgStore_->upsertNode(moduleNode);
REQUIRE(moduleNodeId.has_value());
KGEdge versionEdge;
versionEdge.srcNodeId = fileNodeId.value();
versionEdge.dstNodeId = docNodeId.value();
versionEdge.relation = "has_version";
REQUIRE(kgStore_->addEdge(versionEdge).has_value());
KGEdge usesOne;
usesOne.srcNodeId = fileNodeId.value();
usesOne.dstNodeId = symbolNodeId.value();
usesOne.relation = "uses";
REQUIRE(kgStore_->addEdge(usesOne).has_value());
KGEdge usesTwo;
usesTwo.srcNodeId = fileNodeId.value();
usesTwo.dstNodeId = moduleNodeId.value();
usesTwo.relation = "uses";
REQUIRE(kgStore_->addEdge(usesTwo).has_value());
}
std::filesystem::path tmpDir_;
std::unique_ptr<Database> db_;
std::unique_ptr<ConnectionPool> pool_;
std::shared_ptr<MetadataRepository> repo_;
std::shared_ptr<KnowledgeGraphStore> kgStore_;
std::shared_ptr<IContentStore> store_;
AppContext ctx_;
std::shared_ptr<IGrepService> grepService_;
};
} // namespace
TEST_CASE("GrepService - Basic Functionality", "[grep][service][basic]") {
SKIP_GREP_ON_WINDOWS();
GrepFixture fixture;
fixture.addDocument("a.txt", "alpha beta gamma\nhello world\nregex target\n");
fixture.addDocument("b.txt", "semantic related content about programming\n");
SECTION("Regex-only mode finds pattern matches") {
GrepRequest req;
req.pattern = "regex";
req.regexOnly = true;
req.lineNumbers = true;
auto res = fixture.grep(req);
REQUIRE(res);
CHECK(res.value().totalMatches > 0);
CHECK(res.value().semanticMatches == 0);
CHECK(res.value().executionTimeMs >= 0);
REQUIRE(res.value().searchStats.contains("metadata_operations"));
CHECK(res.value().searchStats.at("metadata_operations") != "0");
REQUIRE(res.value().searchStats.contains("latency_ms"));
REQUIRE(res.value().searchStats.contains("match_render_us"));
REQUIRE(res.value().searchStats.contains("matches_rendered"));
CHECK(std::stoll(res.value().searchStats.at("match_render_us")) >= 0);
CHECK(std::stoll(res.value().searchStats.at("matches_rendered")) > 0);
}
SECTION("Relation metadata is added for matched files") {
auto docRes = fixture.repo_->findDocumentByExactPath((fixture.tmpDir_ / "a.txt").string());
REQUIRE(docRes.has_value());
REQUIRE(docRes.value().has_value());
fixture.seedRelationsForFile(docRes.value()->filePath, docRes.value()->sha256Hash);
GrepRequest req;
req.pattern = "regex";
req.regexOnly = true;
auto res = fixture.grep(req);
REQUIRE(res);
auto it = std::find_if(res.value().results.begin(), res.value().results.end(),
[](const GrepFileResult& file) { return file.fileName == "a.txt"; });
REQUIRE(it != res.value().results.end());
REQUIRE(it->metadata.contains("relation_count"));
CHECK(it->metadata.at("relation_count") == "3");
REQUIRE(it->metadata.contains("relation_types"));
CHECK(it->metadata.at("relation_types").find("uses:2") != std::string::npos);
REQUIRE(it->metadata.contains("relation_summary"));
CHECK(it->metadata.at("relation_summary").find("uses(2)") != std::string::npos);
REQUIRE(it->metadata.contains("primary_relation"));
CHECK(it->metadata.at("primary_relation") == "uses");
REQUIRE(res.value().searchStats.contains("relation_results_enriched"));
CHECK(res.value().searchStats.at("relation_results_enriched") != "0");
}
SECTION("Literal short pattern matches with boundaries") {
fixture.addDocument("c.txt", "alpha beta alpha\nalpha-beta\n");
GrepRequest req;
req.pattern = "alpha";
req.literalText = true;
req.word = true;
// Scope grep to only c.txt to avoid counting matches in a.txt
req.paths = {(fixture.tmpDir_ / "c.txt").string()};
auto res = fixture.grep(req);
REQUIRE(res);
CHECK(res.value().totalMatches == 2);
REQUIRE(res.value().searchStats.contains("retrieval_mode"));
CHECK(res.value().searchStats.at("retrieval_mode") == "literal");
CHECK(res.value().searchStats.at("fts_literal_filter") == "off");
}
SECTION("Hybrid mode includes semantic suggestions") {
// Skip semantic search on Windows - ONNX initialization hangs
#ifdef _WIN32
SKIP("Semantic search unavailable on Windows - ONNX init issues");
#endif
GrepRequest req;
req.pattern = "how to learn programming";
req.regexOnly = false;
req.semanticLimit = 2;
auto res = fixture.grep(req);
REQUIRE(res);
static_cast<void>(res.value().semanticMatches);
REQUIRE(res.value().searchStats.contains("retrieval_mode"));
CHECK(res.value().searchStats.at("retrieval_mode") == "semantic");
}
SECTION("Path filters normalize relative segments") {
auto res = fixture.grep({
.pattern = "hello",
.paths = {(fixture.tmpDir_ / "./a.txt").string()},
});
REQUIRE(res);
REQUIRE_FALSE(res.value().results.empty());
CHECK(res.value().results.front().fileName == "a.txt");
CHECK(res.value().totalMatches > 0);
}
SECTION("Subpath filters match correctly") {
std::filesystem::create_directory(fixture.tmpDir_ / "sub");
fixture.addDocument("sub/c.txt", "subpath content");
auto res = fixture.grep({
.pattern = "subpath",
.paths = {"sub/c.txt"},
});
REQUIRE(res);
REQUIRE_FALSE(res.value().results.empty());
CHECK(res.value().results.front().fileName == "c.txt");
CHECK(res.value().totalMatches > 0);
}
}
TEST_CASE("GrepService - Output Modes", "[grep][service][modes]") {
SKIP_GREP_ON_WINDOWS();
GrepFixture fixture;
fixture.addDocument("a.txt", "programming content\n");
SECTION("Paths-only and file-set projections are deterministic") {
fixture.addDocument("c.txt", "needle content c\n");
fixture.addDocument("b.txt", "needle content b\n");
fixture.addDocument("z.txt", "haystack only\n");
GrepRequest req;
req.pattern = "needle";
req.regexOnly = true;
req.pathsOnly = true;
auto res = fixture.grep(req);
REQUIRE(res);
const std::vector<std::string> expectedWith = {
std::filesystem::weakly_canonical(fixture.tmpDir_ / "b.txt").string(),
std::filesystem::weakly_canonical(fixture.tmpDir_ / "c.txt").string(),
};
CHECK(res.value().filesWith == expectedWith);
CHECK(res.value().pathsOnly == expectedWith);
GrepRequest withoutReq;
withoutReq.pattern = "needle";
withoutReq.regexOnly = true;
withoutReq.filesWithoutMatch = true;
auto withoutRes = fixture.grep(withoutReq);
REQUIRE(withoutRes);
const std::vector<std::string> expectedWithout = {
std::filesystem::weakly_canonical(fixture.tmpDir_ / "a.txt").string(),
std::filesystem::weakly_canonical(fixture.tmpDir_ / "z.txt").string(),
};
CHECK(withoutRes.value().filesWithout == expectedWithout);
}
SECTION("Count mode allows semantic suggestions") {
// Skip semantic search on Windows - ONNX initialization hangs
#ifdef _WIN32
SKIP("Semantic search unavailable on Windows - ONNX init issues");
#endif
GrepRequest req;
req.pattern = "programming";
req.regexOnly = false;
req.semanticLimit = 2;
req.count = true;
auto res = fixture.grep(req);
REQUIRE(res);
static_cast<void>(res.value().semanticMatches);
}
SECTION("Files-only mode allows semantic suggestions") {
// Skip semantic search on Windows - ONNX initialization hangs
#ifdef _WIN32
SKIP("Semantic search unavailable on Windows - ONNX init issues");
#endif
GrepRequest req;
req.pattern = "programming";
req.regexOnly = false;
req.semanticLimit = 2;
req.filesWithMatches = true;
auto res = fixture.grep(req);
REQUIRE(res);
static_cast<void>(res.value().semanticMatches);
}
SECTION("Paths-only mode allows semantic suggestions") {
// Skip semantic search on Windows - ONNX initialization hangs
#ifdef _WIN32
SKIP("Semantic search unavailable on Windows - ONNX init issues");
#endif
GrepRequest req;
req.pattern = "programming";
req.regexOnly = false;
req.semanticLimit = 2;
req.pathsOnly = true;
auto res = fixture.grep(req);
REQUIRE(res);
static_cast<void>(res.value().semanticMatches);
}
}
TEST_CASE("GrepService - Error Handling", "[grep][service][reliability]") {
SKIP_GREP_ON_WINDOWS();
GrepFixture fixture;
fixture.addDocument("a.txt", "alpha beta gamma\n");
SECTION("Retries transient metadata errors") {
auto flakyRepo = std::make_shared<FlakyMetadataRepository>(*fixture.pool_);
auto docsRes = metadata::queryDocumentsByPattern(*flakyRepo, "%");
REQUIRE(docsRes);
REQUIRE_FALSE(docsRes.value().empty());
const auto docId = docsRes.value().front().id;
REQUIRE(flakyRepo->setMetadata(docId, "force_cold", MetadataValue("true")));
flakyRepo->setGetAllMetadataFailures(1);
fixture.repo_ = flakyRepo;
fixture.ctx_.metadataRepo = flakyRepo;
fixture.grepService_ = makeGrepService(fixture.ctx_);
GrepRequest req;
req.pattern = "alpha";
req.literalText = true;
auto res = fixture.grep(req);
REQUIRE(res);
CHECK(res.value().totalMatches > 0);
}
SECTION("Propagates tag query failures after retry exhaustion") {
auto flakyRepo = std::make_shared<FlakyMetadataRepository>(*fixture.pool_);
auto docsRes = metadata::queryDocumentsByPattern(*flakyRepo, "%");
REQUIRE(docsRes);
REQUIRE_FALSE(docsRes.value().empty());
const auto docId = docsRes.value().front().id;
REQUIRE(
flakyRepo->setMetadata(docId, "tag:ready_flag", MetadataValue(std::string("true"))));
flakyRepo->setFindDocumentsByTagsFailures(8);
fixture.repo_ = flakyRepo;
fixture.ctx_.metadataRepo = flakyRepo;
fixture.grepService_ = makeGrepService(fixture.ctx_);
auto res = fixture.grep({
.pattern = "alpha",
.literalText = true,
.tags = {"ready_flag"},
});
REQUIRE_FALSE(res);
CHECK(res.error().code == ErrorCode::NotInitialized);
CHECK_FALSE(res.error().message.empty());
}
SECTION("Tag-filtered grep tolerates metadata warming when tag index query succeeds") {
auto flakyRepo = std::make_shared<FlakyMetadataRepository>(*fixture.pool_);
auto docsRes = metadata::queryDocumentsByPattern(*flakyRepo, "%");
REQUIRE(docsRes);
REQUIRE_FALSE(docsRes.value().empty());
const auto docId = docsRes.value().front().id;
REQUIRE(
flakyRepo->setMetadata(docId, "tag:ready_flag", MetadataValue(std::string("true"))));
flakyRepo->setGetAllMetadataFailures(8);
fixture.repo_ = flakyRepo;
fixture.ctx_.metadataRepo = flakyRepo;
fixture.grepService_ = makeGrepService(fixture.ctx_);
auto res = fixture.grep({
.pattern = "alpha",
.literalText = true,
.tags = {"ready_flag"},
});
REQUIRE(res);
CHECK(res.value().totalMatches > 0);
}
}
TEST_CASE("GrepService - Stops scanning when the host cancels",
"[grep][service][reliability][cancellation]") {
SKIP_GREP_ON_WINDOWS();
GrepFixture fixture;
fixture.addDocument("a.txt", "alpha beta gamma\n");
fixture.addDocument("b.txt", "alpha delta\n");
auto cancelled = std::make_shared<std::atomic<bool>>(true);
fixture.ctx_.cancellationSignal = cancelled;
fixture.grepService_ = makeGrepService(fixture.ctx_);
GrepRequest req;
req.pattern = "alpha";
req.literalText = true;
auto res = fixture.grep(req);
REQUIRE_FALSE(res);
CHECK((res.error().code == ErrorCode::OperationCancelled));
cancelled->store(false);
auto resumed = fixture.grep(req);
REQUIRE(resumed);
CHECK((resumed.value().totalMatches > 0));
}
TEST_CASE("GrepService - Falls back to extracted content when blob unavailable",
"[grep][service][session]") {
SKIP_GREP_ON_WINDOWS();
GrepFixture fixture;
fixture.addDocument("session_hot.txt", "alpha from metadata\n");
fixture.addHotContent("session_hot.txt", "alpha from metadata\n");
fixture.store_ = std::make_shared<RetrieveBytesFailStore>(fixture.store_);
fixture.ctx_.store = fixture.store_;
fixture.grepService_ = makeGrepService(fixture.ctx_);
auto coldRes = fixture.grep({
.pattern = "alpha",
.literalText = true,
.useSession = false,
});
REQUIRE(coldRes);
CHECK(coldRes.value().totalMatches == 1);
auto sessionRes = fixture.grep({
.pattern = "alpha",
.literalText = true,
.useSession = true,
});
REQUIRE(sessionRes);
CHECK(sessionRes.value().totalMatches == 1);
REQUIRE(sessionRes.value().filesWith.size() == 1);
CHECK(sessionRes.value().filesWith.front().find("session_hot.txt") != std::string::npos);
}
TEST_CASE("GrepService - Path-scoped grep prefers latest indexed version",
"[grep][service][versioning]") {
SKIP_GREP_ON_WINDOWS();
GrepFixture fixture;
yams::test::ScopedEnvVar hotCap("YAMS_GREP_MAX_DOCS_HOT", std::string("1"));
yams::test::ScopedEnvVar coldCap("YAMS_GREP_MAX_DOCS_COLD", std::string("1"));
fixture.addDocument("versioned.txt", "old-token only\n");
fixture.addDocument("versioned.txt", "new-token only\n");
auto docs = metadata::queryDocumentsByPattern(*fixture.repo_, "%versioned.txt");
REQUIRE(docs);
REQUIRE(docs.value().size() >= 2);
auto res = fixture.grep({
.pattern = "new-token",
.paths = {"versioned.txt"},
.literalText = true,
});
REQUIRE(res);
CHECK(res.value().totalMatches == 1);
REQUIRE(res.value().filesWith.size() == 1);
CHECK(res.value().filesWith.front().find("versioned.txt") != std::string::npos);
}
TEST_CASE("GrepService - Broad regex grep does not stop at candidate cap",
"[grep][service][versioning]") {
SKIP_GREP_ON_WINDOWS();
GrepFixture fixture;
yams::test::ScopedEnvVar hotCap("YAMS_GREP_MAX_DOCS_HOT", std::string("2"));
yams::test::ScopedEnvVar coldCap("YAMS_GREP_MAX_DOCS_COLD", std::string("0"));
yams::test::ScopedEnvVar budget("YAMS_GREP_TIME_BUDGET_MS", std::string("10000"));
fixture.addDocument("old_match.txt", "rare_token_42\n");
fixture.addDocument("newer_a.txt", "alpha\n");
fixture.addDocument("newer_b.txt", "beta\n");
fixture.addDocument("newer_c.txt", "gamma\n");
auto res = fixture.grep({
.pattern = "rare_token_[0-9]+",
.regexOnly = true,
});
REQUIRE(res);
CHECK(res.value().totalMatches == 1);
REQUIRE(res.value().filesWith.size() == 1);
CHECK(res.value().filesWith.front().find("old_match.txt") != std::string::npos);
}
TEST_CASE("GrepService - Workspace scope rejects generated retrieval artifacts",
"[grep][service][scope]") {
SKIP_GREP_ON_WINDOWS();
GrepFixture fixture;
fixture.addDocument("src/authored.cpp", "shared_scope_token authored\n");
fixture.addDocument("meson.build", "shared_scope_token build definition\n");
fixture.addDocument("bin/tool.py", "shared_scope_token tool\n");
fixture.addDocument("build/fold/findings.sarif", "shared_scope_token fold\n");
fixture.addDocument(".pi/tasks/agent.json", "shared_scope_token agent\n");
fixture.addDocument("audit_artifacts/report.json", "shared_scope_token audit\n");
auto res = fixture.grep({
.pattern = "shared_scope_token.*",
.scopePathPrefix = fixture.tmpDir_.string(),
.regexOnly = true,
.useSession = false,
});
REQUIRE(res);
REQUIRE(res.value().results.size() == 3);
CHECK(std::ranges::none_of(res.value().results, [](const auto& result) {
return utils::isGeneratedWorkspacePath(result.file);
}));
CHECK(res.value().searchStats.at("generated_candidates_rejected") == "3");
}
TEST_CASE("GrepService - Edge Cases", "[grep][service][edge]") {
SKIP_GREP_ON_WINDOWS();
GrepFixture fixture;
fixture.addDocument("edge.txt", "foo bar\nfoo-bar\nfoo_bar\n(hello) and (foo)\nHELLO world\n");
SECTION("Word boundary excludes hyphens") {
GrepRequest req;
req.pattern = "foo";
req.word = true;
req.lineNumbers = true;
req.withFilename = false;
auto res = fixture.grep(req);
REQUIRE(res);
CHECK(res.value().totalMatches > 0);
for (const auto& fileResult : res.value().results) {
for (const auto& match : fileResult.matches) {
CHECK(match.line.find("foo-") == std::string::npos);
}
}
}
SECTION("Literal parentheses not interpreted as regex") {
auto literalRes = fixture.grep({
.pattern = "(foo)",
.literalText = true,
});
REQUIRE(literalRes);
CHECK(literalRes.value().totalMatches > 0);
auto regexRes = fixture.grep({
.pattern = "(foo)",
.literalText = false,
});
REQUIRE(regexRes);
CHECK(regexRes.value().totalMatches > 0);
}
SECTION("Ignore case matches uppercase and lowercase") {
GrepRequest req;
req.pattern = "HELLO";
req.ignoreCase = true;
auto res = fixture.grep(req);
REQUIRE(res);
CHECK(res.value().totalMatches > 0);
}
}
TEST_CASE("GrepService - Unicode Support", "[grep][service][unicode]") {
SKIP_GREP_ON_WINDOWS();
GrepFixture fixture;
fixture.addDocument("unicode.txt",
"café\nCAFÉ\nnaïve\n(naïve)\nsmile 😊 end\n東京大学 CJK line\n");
SECTION("Literal unicode patterns match correctly") {
auto cafeRes = fixture.grep({
.pattern = "café",
.literalText = true,
});
REQUIRE(cafeRes);
CHECK(cafeRes.value().totalMatches > 0);
auto emojiRes = fixture.grep({
.pattern = "😊",
.literalText = true,
});
REQUIRE(emojiRes);
CHECK(emojiRes.value().totalMatches > 0);
auto diacriticRes = fixture.grep({
.pattern = "(naïve)",
.literalText = true,
});
REQUIRE(diacriticRes);
CHECK(diacriticRes.value().totalMatches > 0);
}
SECTION("Case-insensitive unicode matching (best effort)") {
GrepRequest req;
req.pattern = "café";
req.literalText = true;
req.ignoreCase = true;
auto res = fixture.grep(req);
REQUIRE(res);
if (res.value().totalMatches == 0) {
SKIP("Unicode case-folding not available in this build");
}
CHECK(res.value().totalMatches > 0);
}
}
#endif // _WIN32