yams/tests/unit/daemon/grep_result_window_catch2_test.cpp
trvon 67d2a6a9b1
fix(retrieval): preserve content identities in JSON results
Signed-off-by: trvon <me@trevon.dev>
2026-09-05 11:52:46 -06:00

148 lines
5.7 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <any>
#include <yams/daemon/client/global_io_context.h>
#include <yams/daemon/components/grep_result_window.h>
// This focused executable does not link the daemon client's runtime. App service headers
// transitively instantiate the global initializer, so provide inert lifecycle definitions.
namespace yams::daemon {
GlobalIOContextInitializer::GlobalIOContextInitializer() = default;
GlobalIOContextInitializer::~GlobalIOContextInitializer() = default;
} // namespace yams::daemon
namespace {
// Exercise the legacy field contract without depending on the protobuf codec.
struct RecordedFields {
std::vector<std::any> values;
std::size_t offset{0};
template <typename T> RecordedFields& operator<<(const T& value) {
values.emplace_back(value);
return *this;
}
template <typename T> yams::Result<T> read() {
if (offset >= values.size())
return yams::ErrorCode::InvalidData;
const auto* value = std::any_cast<T>(&values[offset]);
if (!value)
return yams::ErrorCode::InvalidData;
++offset;
return *value;
}
yams::Result<std::string> readString() { return read<std::string>(); }
yams::Result<std::vector<std::string>> readStringVector() {
return read<std::vector<std::string>>();
}
yams::Result<std::map<std::string, std::string>> readStringMap() {
return read<std::map<std::string, std::string>>();
}
};
yams::app::services::GrepMatch makeMatch(std::size_t lineNumber, std::string line) {
yams::app::services::GrepMatch match;
match.lineNumber = lineNumber;
match.line = std::move(line);
match.matchType = "regex";
return match;
}
yams::app::services::GrepFileResult
makeFileResult(std::string file, std::vector<yams::app::services::GrepMatch> matches) {
yams::app::services::GrepFileResult result;
result.file = std::move(file);
result.matches = std::move(matches);
result.matchCount = result.matches.size();
return result;
}
} // namespace
TEST_CASE("Legacy grep response appends identity without changing match layout",
"[daemon][grep][identity][serialization]") {
yams::daemon::GrepResponse response;
yams::daemon::GrepMatch match;
match.file = "evidence.txt";
match.hash = std::string(64, 'a');
response.matches = {match, match};
RecordedFields fields;
response.serialize(fields);
bool legacy = false;
SECTION("New response roundtrips hashes") {}
SECTION("Older response remains readable without hashes") {
fields.values.pop_back(); // Old response ends at pathsOnly.
legacy = true;
}
const auto result = yams::daemon::GrepResponse::deserialize(fields);
REQUIRE(result);
REQUIRE(result.value().matches.size() == 2);
for (const auto& decoded : result.value().matches) {
CHECK(decoded.file == match.file);
CHECK(decoded.hash == (legacy ? std::string{} : match.hash));
}
}
TEST_CASE("Grep result window preserves immutable revisions at the same path",
"[daemon][grep][result-window][identity]") {
auto first = makeFileResult("evidence.txt", {makeMatch(1, "same content")});
first.hash = std::string(64, 'a');
auto second = first;
second.hash = std::string(64, 'b');
const auto selected = yams::daemon::grep_result_window::select({first, second, first}, 0);
REQUIRE(selected.matches.size() == 2);
CHECK(selected.matches[0].hash == first.hash);
CHECK(selected.matches[1].hash == second.hash);
}
TEST_CASE("Grep result window deduplicates identities before applying its total cap",
"[daemon][grep][result-window]") {
const std::vector fileResults{
makeFileResult("a.cpp", {makeMatch(10, " alpha beta "), makeMatch(10, "alpha beta"),
makeMatch(11, "second")}),
makeFileResult("b.cpp", {makeMatch(4, "third"), makeMatch(5, "fourth")}),
};
const auto selection = yams::daemon::grep_result_window::select(fileResults, 3);
CHECK(selection.stats.inputMatches == 5);
CHECK(selection.stats.uniqueMatches == 4);
CHECK(selection.stats.emittedMatches == 3);
REQUIRE(selection.matches.size() == 3);
CHECK(selection.matches[0].file == "a.cpp");
CHECK(selection.matches[0].lineNumber == 10);
CHECK(selection.matches[0].line == " alpha beta ");
CHECK(selection.matches[1].lineNumber == 11);
CHECK(selection.matches[2].file == "b.cpp");
CHECK(selection.matches[2].lineNumber == 4);
}
TEST_CASE("Grep result window retains equal text at distinct evidence locations",
"[daemon][grep][result-window]") {
const std::vector fileResults{
makeFileResult("a.cpp", {makeMatch(10, "same"), makeMatch(11, "same")}),
makeFileResult("b.cpp", {makeMatch(10, "same")}),
};
const auto selection = yams::daemon::grep_result_window::select(fileResults, 0);
CHECK(selection.stats.inputMatches == 3);
CHECK(selection.stats.uniqueMatches == 3);
CHECK(selection.stats.emittedMatches == 3);
CHECK(selection.matches.size() == 3);
}
TEST_CASE("Grep result window rejects binary-like indexed lines", "[daemon][grep][result-window]") {
std::string binaryLike{"prefix"};
binaryLike.push_back('\0');
binaryLike += "suffix";
const std::vector fileResults{
makeFileResult("generated.sarif", {makeMatch(1, std::move(binaryLike))}),
makeFileResult("source.cpp", {makeMatch(2, "readable evidence")}),
};
const auto selection = yams::daemon::grep_result_window::select(fileResults, 20);
CHECK(selection.stats.inputMatches == 2);
REQUIRE(selection.matches.size() == 1);
CHECK(selection.matches.front().file == "source.cpp");
}