Some checks failed
Fuzz Smoke / libFuzzer smoke (push) Has been cancelled
* 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>
900 lines
30 KiB
C++
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
|