yams/tests/unit/app/services/service_utilities_test.cpp
2026-08-04 21:31:39 -04:00

270 lines
9.3 KiB
C++

#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <string>
#include "../../../common/env_compat.h"
#include "../../../common/test_helpers_catch2.h"
#include <catch2/catch_test_macros.hpp>
#include <yams/app/services/factory.hpp>
#include <yams/app/services/list_input_resolver.hpp>
#include <yams/app/services/path_projection.hpp>
#include <yams/app/services/services.hpp>
#include <yams/app/services/session_service.hpp>
#include <yams/compat/unistd.h>
using namespace yams::app::services;
namespace {
std::filesystem::path createTempStateRoot() {
auto p = std::filesystem::temp_directory_path() / "yams_test_state";
std::error_code ec;
std::filesystem::create_directories(p, ec);
return p;
}
struct ScopedWorkingDirectory {
std::filesystem::path original;
explicit ScopedWorkingDirectory(const std::filesystem::path& target) {
original = std::filesystem::current_path();
std::filesystem::current_path(target);
}
~ScopedWorkingDirectory() {
std::error_code error;
std::filesystem::current_path(original, error);
}
};
} // namespace
TEST_CASE("SessionService - Configuration and State", "[session][service][config]") {
SECTION("Watch configuration roundtrip") {
auto root = std::filesystem::temp_directory_path() / "yams_watch_rt";
yams::test::ScopedEnvVar stateHome{"XDG_STATE_HOME", root.string()};
AppContext ctx;
auto svc = makeSessionService(&ctx);
svc->init("watchme", "");
CHECK_FALSE(svc->watchEnabled());
svc->enableWatch(true);
svc->setWatchIntervalMs(1500);
CHECK(svc->watchEnabled());
CHECK((svc->watchIntervalMs() >= 1000u));
}
SECTION("Session initialization and selectors") {
auto root = createTempStateRoot();
yams::test::ScopedEnvVar stateHome{"XDG_STATE_HOME", root.string()};
AppContext ctx;
auto svc = makeSessionService(&ctx);
REQUIRE(svc);
svc->init("unittest", "test session");
REQUIRE(svc->current().has_value());
CHECK((svc->current().value() == "unittest"));
svc->addPathSelector("src/**/*.cpp", {"pinned"}, {});
auto sels = svc->listPathSelectors("unittest");
CHECK_FALSE(sels.empty());
svc->removePathSelector("src/**/*.cpp");
sels = svc->listPathSelectors("unittest");
CHECK(sels.empty());
}
SECTION("Session create generates UUID") {
auto uniqueSuffix =
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
auto root = std::filesystem::temp_directory_path() / ("yams_uuid_test_" + uniqueSuffix);
std::filesystem::create_directories(root);
yams::test::ScopedEnvVar stateHome{"XDG_STATE_HOME", root.string()};
AppContext ctx;
auto svc = makeSessionService(&ctx);
REQUIRE(svc);
svc->create("uuid-test", "uuid test session");
auto info = svc->getSessionInfo("uuid-test");
REQUIRE(info.has_value());
CHECK_FALSE(info->uuid.empty());
CHECK((info->uuid.size() == 36));
CHECK((info->uuid.find('-') != std::string::npos));
}
SECTION("Materialized view listing") {
auto root = createTempStateRoot();
yams::test::ScopedEnvVar stateHome{"XDG_STATE_HOME", root.string()};
AppContext ctx;
auto svc = makeSessionService(&ctx);
svc->init("mat", "mat test");
auto items = svc->listMaterialized("mat");
CHECK(items.empty());
}
}
TEST_CASE("Service Utils - Path Normalization", "[utils][service][paths]") {
using utils::NormalizedLookupPath;
using utils::normalizeLookupPath;
SECTION("Convert relative path to canonical") {
namespace fs = std::filesystem;
const auto uniqueSuffix =
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
fs::path tempRoot = fs::temp_directory_path() / ("yams_utils_test_" + uniqueSuffix);
fs::create_directories(tempRoot);
fs::path nestedDir = tempRoot / "nested";
fs::create_directories(nestedDir);
fs::path targetFile = nestedDir / "file.txt";
std::ofstream{targetFile} << "sample";
{
ScopedWorkingDirectory cwdScope(tempRoot);
NormalizedLookupPath result = normalizeLookupPath("./nested/../nested/file.txt");
CHECK(result.changed);
CHECK_FALSE(result.hasWildcards);
CHECK((fs::weakly_canonical(targetFile).string() == result.normalized));
CHECK((result.original == "./nested/../nested/file.txt"));
} // cwdScope destructor restores original CWD before cleanup
std::error_code ec;
fs::remove_all(tempRoot, ec); // Ignore errors on cleanup
}
SECTION("Glob inputs canonicalize directory prefix but preserve pattern") {
NormalizedLookupPath result = normalizeLookupPath("docs/**/*.md");
CHECK(result.hasWildcards);
CHECK((result.normalized.find("**") != std::string::npos));
CHECK((result.normalized.find("*.md") != std::string::npos));
}
SECTION("Leave dash literal untouched") {
NormalizedLookupPath result = normalizeLookupPath("-");
CHECK_FALSE(result.changed);
CHECK_FALSE(result.hasWildcards);
CHECK((result.normalized == "-"));
}
}
TEST_CASE("Service Utils - Query snippets center matched code", "[utils][service][snippet]") {
const std::string prefix(400, 'x');
const std::string content =
prefix + "\nint affectedTests() {\n return traverseGraph();\n}\n";
const auto result =
utils::createQuerySnippet(content, "graph affected tests traversal", 120, true);
CHECK(result.queryMatched);
CHECK((result.matchedTerms >= 2));
CHECK((result.sourceOffset > 0));
CHECK((result.text.find("affectedTests") != std::string::npos));
CHECK((result.text.find("traverseGraph") != std::string::npos));
CHECK((result.text.find(std::string(80, 'x')) == std::string::npos));
}
TEST_CASE("Service Utils - Query snippets retain prefix fallback", "[utils][service][snippet]") {
const std::string content = "alpha beta gamma delta";
const auto result = utils::createQuerySnippet(content, "missing terms", 10, true);
CHECK_FALSE(result.queryMatched);
CHECK((result.text == utils::createSnippet(content, 10, true)));
}
TEST_CASE("Path projection gives paths-only responses one shared contract",
"[utils][service][paths-only]") {
SearchResponse resp;
std::vector<SearchItem> items;
SearchItem first;
first.path = "/tmp/a.cpp";
first.fileName = "a.cpp";
items.push_back(first);
SearchItem duplicate = first;
items.push_back(duplicate);
SearchItem fallback;
fallback.fileName = "b.cpp";
items.push_back(fallback);
path_projection::assignPathsOnly(resp, items, 10, [](const SearchItem& item) {
return path_projection::displayPath(item.path, item.fileName);
});
REQUIRE((resp.paths.size() == 2));
CHECK((resp.paths[0] == "/tmp/a.cpp"));
CHECK((resp.paths[1] == "b.cpp"));
CHECK((resp.total == resp.paths.size()));
CHECK((resp.results.empty()));
SearchResponse fuzzy;
fuzzy.paths = {"/tmp/a.cpp", "/tmp/c.cpp"};
path_projection::mergePathsOnly(resp, fuzzy, 2);
REQUIRE((resp.paths.size() == 2));
CHECK((resp.paths[0] == "/tmp/a.cpp"));
CHECK((resp.paths[1] == "b.cpp"));
CHECK((resp.total == 2));
}
TEST_CASE("List input resolver classifies wildcards and directories", "[utils][service][list]") {
using yams::app::services::ResolvedListInputKind;
using yams::app::services::resolveNameToPatternIfLocalFile;
SECTION("Absolute wildcard path stays a pattern") {
auto result = resolveNameToPatternIfLocalFile("/tmp/project/**/*");
CHECK((result.kind == ResolvedListInputKind::Pattern));
CHECK_FALSE(result.isLocalFile);
CHECK_FALSE(result.isLocalDirectory);
CHECK((result.pattern.find("**") != std::string::npos));
}
SECTION("Existing directory resolves as local directory") {
namespace fs = std::filesystem;
const auto uniqueSuffix =
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
fs::path tempRoot = fs::temp_directory_path() / ("yams_list_resolver_" + uniqueSuffix);
fs::create_directories(tempRoot / "nested");
auto result = resolveNameToPatternIfLocalFile((tempRoot / "nested").string());
CHECK((result.kind == ResolvedListInputKind::LocalDirectory));
CHECK_FALSE(result.isLocalFile);
CHECK(result.isLocalDirectory);
REQUIRE(result.absPath.has_value());
CHECK((*result.absPath == fs::weakly_canonical(tempRoot / "nested").string()));
std::error_code ec;
fs::remove_all(tempRoot, ec);
}
}
TEST_CASE("DownloadService - Factory Construction", "[download][service][factory]") {
SECTION("Factory constructs with minimal context") {
AppContext ctx;
auto svc = makeDownloadService(ctx);
CHECK((svc != nullptr));
}
}
TEST_CASE("Service factory handles incomplete context", "[service][factory]") {
AppContext ctx;
auto bundle = makeServices(ctx);
CHECK((bundle.restore == nullptr));
CHECK((bundle.graph == nullptr));
CHECK_FALSE(bundle.valid());
CHECK((makeGraphQueryService(ctx) == nullptr));
}