#include #include #include #include #include #include "../../../common/env_compat.h" #include #include #include #include #include #include #include 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() { try { std::filesystem::current_path(original); } catch (...) { } } }; } // namespace TEST_CASE("SessionService - Configuration and State", "[session][service][config]") { SECTION("Watch configuration roundtrip") { auto root = std::filesystem::temp_directory_path() / "yams_watch_rt"; setenv("XDG_STATE_HOME", root.string().c_str(), 1); 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(); setenv("XDG_STATE_HOME", root.string().c_str(), 1); 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); setenv("XDG_STATE_HOME", root.string().c_str(), 1); 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(); setenv("XDG_STATE_HOME", root.string().c_str(), 1); 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 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); }