#include #include #include #include #include #include #include #include #include #include #include #include "../../common/test_helpers_catch2.h" #ifdef _WIN32 #define WIFEXITED(x) ((x) != -1) #define WEXITSTATUS(x) (x) #else #include #endif using Catch::Matchers::ContainsSubstring; namespace { namespace fs = std::filesystem; struct CompletionCliTestHelper { fs::path tempDir; std::optional configEnv; std::optional dataEnv; std::optional nonInteractiveEnv; std::optional disableDaemonEnv; CompletionCliTestHelper() { tempDir = yams::test::make_temp_dir("yams_cli_completion_catch2_test_"); fs::create_directories(tempDir / "data"); { std::ofstream cfg(tempDir / "config.toml"); cfg << "[core]\n"; cfg << "data_dir = \"" << (tempDir / "data").string() << "\"\n"; } configEnv.emplace("YAMS_CONFIG", (tempDir / "config.toml").string()); dataEnv.emplace("YAMS_DATA_DIR", (tempDir / "data").string()); nonInteractiveEnv.emplace(std::string("YAMS_NON_INTERACTIVE"), std::optional("1")); disableDaemonEnv.emplace(std::string("YAMS_CLI_DISABLE_DAEMON_AUTOSTART"), std::optional("1")); } ~CompletionCliTestHelper() { configEnv.reset(); dataEnv.reset(); nonInteractiveEnv.reset(); disableDaemonEnv.reset(); std::error_code ec; fs::remove_all(tempDir, ec); } int runCommand(const std::vector& args) { auto cli = std::make_unique(); std::vector argv; argv.reserve(args.size()); for (const auto& arg : args) { argv.push_back(const_cast(arg.c_str())); } return cli->run(static_cast(argv.size()), argv.data()); } }; class CaptureStdout { public: CaptureStdout() : oldCout_(std::cout.rdbuf(buffer_.rdbuf())) {} ~CaptureStdout() { std::cout.rdbuf(oldCout_); } std::string str() const { return buffer_.str(); } CaptureStdout(const CaptureStdout&) = delete; CaptureStdout& operator=(const CaptureStdout&) = delete; private: std::ostringstream buffer_; std::streambuf* oldCout_; }; std::string lineContaining(const std::string& text, std::string_view needle) { const auto pos = text.find(needle); if (pos == std::string::npos) { return {}; } const auto lineEnd = text.find('\n', pos); return text.substr(pos, lineEnd == std::string::npos ? std::string::npos : lineEnd - pos); } std::string stripCoverageProfilingDiagnostics(const std::string& text) { std::istringstream input(text); std::ostringstream filtered; std::string line; while (std::getline(input, line)) { if (line.rfind("profiling: ", 0) == 0) { continue; } filtered << line; if (!input.eof()) { filtered << '\n'; } } return filtered.str(); } auto findBuiltYamsCli() -> fs::path { std::vector candidates; if (const char* buildRoot = std::getenv("MESON_BUILD_ROOT")) { candidates.emplace_back(fs::path(buildRoot) / "tools" / "yams-cli" / "yams-cli"); } std::vector bases; if (const char* sourceRoot = std::getenv("MESON_SOURCE_ROOT")) { bases.emplace_back(sourceRoot); } std::error_code ec; auto cwd = fs::current_path(ec); if (!ec) { bases.push_back(cwd); if (cwd.has_parent_path()) { bases.push_back(cwd.parent_path()); } if (cwd.has_parent_path() && cwd.parent_path().has_parent_path()) { bases.push_back(cwd.parent_path().parent_path()); } } const std::vector suffixes = { fs::path("tools") / "yams-cli" / "yams-cli", fs::path("builddir-local") / "tools" / "yams-cli" / "yams-cli", fs::path("builddir-asan-local") / "tools" / "yams-cli" / "yams-cli", fs::path("builddir-ubsan-local") / "tools" / "yams-cli" / "yams-cli", fs::path("builddir-tsan-local") / "tools" / "yams-cli" / "yams-cli", fs::path("builddir-nosan-local") / "tools" / "yams-cli" / "yams-cli", fs::path("builddir") / "tools" / "yams-cli" / "yams-cli", fs::path("builddir-asan") / "tools" / "yams-cli" / "yams-cli", fs::path("builddir-ubsan") / "tools" / "yams-cli" / "yams-cli", fs::path("builddir-tsan") / "tools" / "yams-cli" / "yams-cli", fs::path("builddir-nosan") / "tools" / "yams-cli" / "yams-cli", fs::path("build") / "asan" / "tools" / "yams-cli" / "yams-cli", fs::path("build") / "coverage" / "tools" / "yams-cli" / "yams-cli", fs::path("build") / "release" / "tools" / "yams-cli" / "yams-cli", fs::path("build") / "debug" / "tools" / "yams-cli" / "yams-cli", }; for (const auto& base : bases) { for (const auto& suffix : suffixes) { candidates.push_back(base / suffix); } } for (const auto& candidate : candidates) { if (fs::exists(candidate)) { return candidate; } } return {}; } } // namespace TEST_CASE("CompletionCommand - bash script includes registered top-level commands", "[cli][completion][catch2]") { CompletionCliTestHelper helper; CaptureStdout capture; const int rc = helper.runCommand({"yams", "completion", "bash"}); const std::string output = capture.str(); REQUIRE((rc == 0)); CHECK((output.rfind("#!/bin/bash\n", 0) == 0)); CHECK((output.find("Configuration successfully migrated") == std::string::npos)); CHECK_THAT(output, ContainsSubstring("'restore'")); CHECK_THAT(output, ContainsSubstring("'grep'")); CHECK_THAT(output, ContainsSubstring("'status'")); CHECK_THAT(output, ContainsSubstring("'daemon'")); CHECK_THAT(output, ContainsSubstring("'doctor'")); CHECK_THAT(output, ContainsSubstring("'graph'")); CHECK_THAT(output, ContainsSubstring("'diff'")); } TEST_CASE("CompletionCommand - bash status and stats use current flags", "[cli][completion][catch2]") { CompletionCliTestHelper helper; CaptureStdout capture; const int rc = helper.runCommand({"yams", "completion", "bash"}); const std::string output = capture.str(); REQUIRE((rc == 0)); const std::string statusLine = lineContaining(output, "local status_flags="); REQUIRE_FALSE(statusLine.empty()); CHECK_THAT(statusLine, ContainsSubstring("--no-physical")); CHECK_THAT(statusLine, ContainsSubstring("--corpus")); CHECK((statusLine.find("--format") == std::string::npos)); CHECK_THAT(output, ContainsSubstring("status|stats)")); } TEST_CASE("CompletionCommand - bash completes nested subcommands from command tree", "[cli][completion][catch2]") { #ifdef _WIN32 SKIP("bash nested completion smoke test requires Unix shell support"); #else const fs::path yamsCli = findBuiltYamsCli(); REQUIRE_FALSE(yamsCli.empty()); const fs::path tempDir = yams::test::make_temp_dir("yams_bash_completion_tree_"); const fs::path dataDir = tempDir / "data"; fs::create_directories(dataDir); const fs::path script = tempDir / "yams.bash"; const fs::path out = tempDir / "bash-tree.out"; const std::string cmd = "env YAMS_CONFIG='" + (tempDir / "missing-config.toml").string() + "' YAMS_DATA_DIR='" + dataDir.string() + "' YAMS_NON_INTERACTIVE=1 YAMS_CLI_DISABLE_DAEMON_AUTOSTART=1 bash -lc " "'set -euo pipefail; \"" + yamsCli.string() + "\" completion bash > \"" + script.string() + "\"; source \"" + script.string() + "\"; COMP_WORDS=(yams config em); COMP_CWORD=2; _yams_completion; printf \"CONFIG:%s\\n\" " "\"${COMPREPLY[*]}\"; COMP_WORDS=(yams plugin trust re); COMP_CWORD=3; " "_yams_completion; printf \"PLUGIN:%s\\n\" \"${COMPREPLY[*]}\"; " "COMP_WORDS=(yams daemon start \"\"); COMP_CWORD=3; _yams_completion; printf " "\"START:%s\\n\" \"${COMPREPLY[*]}\"; " "COMP_WORDS=(yams daemon start --log-level \"\"); COMP_CWORD=4; _yams_completion; " "printf \"LOGLEVEL:%s\\n\" \"${COMPREPLY[*]}\"; " "COMP_WORDS=(yams config search path-tree enable --mode \"\"); COMP_CWORD=6; " "_yams_completion; " "printf \"PATHTREE:%s\\n\" \"${COMPREPLY[*]}\"; " "COMP_WORDS=(yams plugin trust list \"\"); COMP_CWORD=4; _yams_completion; printf " "\"TRUSTLIST:%s\\n\" \"${COMPREPLY[*]}\"; " "COMP_WORDS=(yams plugins trust list \"\"); COMP_CWORD=4; _yams_completion; printf " "\"PLUGINSALIAS:%s\\n\" \"${COMPREPLY[*]}\"' > '" + out.string() + "'"; const int rc = std::system(cmd.c_str()); REQUIRE(WIFEXITED(rc)); REQUIRE((WEXITSTATUS(rc) == 0)); std::ifstream input(out); REQUIRE(input.good()); std::string configLine; std::string pluginLine; std::string daemonStartLine; std::string logLevelLine; std::string pathTreeLine; std::string pluginTrustListLine; std::string pluginsAliasLine; std::getline(input, configLine); std::getline(input, pluginLine); std::getline(input, daemonStartLine); std::getline(input, logLevelLine); std::getline(input, pathTreeLine); std::getline(input, pluginTrustListLine); std::getline(input, pluginsAliasLine); CHECK_THAT(configLine, ContainsSubstring("CONFIG:embeddings")); CHECK_THAT(pluginLine, ContainsSubstring("PLUGIN:remove")); CHECK_THAT(pluginLine, ContainsSubstring("reset")); CHECK_THAT(daemonStartLine, ContainsSubstring("START:--socket")); CHECK_THAT(daemonStartLine, ContainsSubstring("--foreground")); CHECK_THAT(daemonStartLine, ContainsSubstring("--daemon-binary")); CHECK_THAT(logLevelLine, ContainsSubstring("LOGLEVEL:trace")); CHECK_THAT(logLevelLine, ContainsSubstring("debug")); CHECK_THAT(logLevelLine, ContainsSubstring("error")); CHECK_THAT(pathTreeLine, ContainsSubstring("PATHTREE:fallback")); CHECK_THAT(pathTreeLine, ContainsSubstring("preferred")); CHECK_THAT(pluginTrustListLine, ContainsSubstring("TRUSTLIST:--details")); CHECK_THAT(pluginsAliasLine, ContainsSubstring("PLUGINSALIAS:--details")); std::error_code ec; fs::remove_all(tempDir, ec); #endif } TEST_CASE("CompletionCommand - zsh and fish status aliases no longer advertise format", "[cli][completion][catch2]") { CompletionCliTestHelper helper; CaptureStdout zshCapture; const int zshRc = helper.runCommand({"yams", "completion", "zsh"}); const std::string zshOutput = zshCapture.str(); REQUIRE((zshRc == 0)); CHECK((zshOutput.rfind("#compdef yams\n", 0) == 0)); CHECK((zshOutput.find("Configuration successfully migrated") == std::string::npos)); CHECK_THAT(zshOutput, ContainsSubstring("status|stats")); CHECK_THAT(zshOutput, ContainsSubstring("--no-physical")); CHECK_THAT(zshOutput, ContainsSubstring("--corpus")); CHECK((zshOutput.find("--format[Output format]:format:(text json)") == std::string::npos)); CHECK_THAT(zshOutput, ContainsSubstring("_yams_subcommand_names_for()")); CHECK_THAT(zshOutput, ContainsSubstring("command_path=\"$(_yams_command_path $CURRENT)\"")); CHECK_THAT(zshOutput, ContainsSubstring("compdef _yams yams")); CHECK_THAT(zshOutput, ContainsSubstring("'config')")); CHECK_THAT(zshOutput, ContainsSubstring("'embeddings:Manage embedding configuration'")); CHECK_THAT(zshOutput, ContainsSubstring("'plugin trust')")); CHECK_THAT(zshOutput, ContainsSubstring("'remove:Remove a trusted plugin path'")); CHECK((zshOutput.find("'plugins trust')") == std::string::npos)); CHECK_THAT(zshOutput, ContainsSubstring("'daemon start')")); CHECK_THAT(zshOutput, ContainsSubstring("'--socket:Socket path for daemon communication'")); CHECK_THAT(zshOutput, ContainsSubstring("'plugin trust list')")); CHECK_THAT(zshOutput, ContainsSubstring( "'--details:Show trust file, defaults, strict mode, and effective roots'")); CHECK_THAT(zshOutput, ContainsSubstring("'daemon start:--log-level')")); CHECK_THAT(zshOutput, ContainsSubstring("'trace' 'debug' 'info' 'warn' 'error'")); CHECK_THAT(zshOutput, ContainsSubstring("'config search path-tree enable:--mode')")); CHECK_THAT(zshOutput, ContainsSubstring("'fallback' 'preferred'")); CaptureStdout fishCapture; const int fishRc = helper.runCommand({"yams", "completion", "fish"}); const std::string fishOutput = fishCapture.str(); REQUIRE((fishRc == 0)); CHECK_THAT(fishOutput, ContainsSubstring("__fish_yams_path_is status; or __fish_yams_path_is stats")); CHECK_THAT(fishOutput, ContainsSubstring("-l no-physical")); CHECK_THAT(fishOutput, ContainsSubstring("-l corpus")); CHECK((fishOutput.find("__fish_yams_path_is stats' -l format") == std::string::npos)); CHECK_THAT(fishOutput, ContainsSubstring("case 'config'")); CHECK_THAT(fishOutput, ContainsSubstring("'embeddings' 'Manage embedding configuration'")); CHECK_THAT(fishOutput, ContainsSubstring("case 'plugin trust'")); CHECK_THAT(fishOutput, ContainsSubstring("'remove' 'Remove a trusted plugin path'")); CHECK((fishOutput.find("case 'plugins trust'") == std::string::npos)); CHECK_THAT(fishOutput, ContainsSubstring("__fish_yams_path_is daemon start")); CHECK_THAT(fishOutput, ContainsSubstring("-l socket -d 'Socket path for daemon communication' -r")); CHECK_THAT(fishOutput, ContainsSubstring("__fish_yams_path_is plugin trust list")); CHECK_THAT(fishOutput, ContainsSubstring( "-l details -d 'Show trust file, defaults, strict mode, and effective roots'")); CHECK_THAT( fishOutput, ContainsSubstring( "__fish_yams_path_is daemon start' -l log-level -a 'trace debug info warn error'")); CHECK_THAT( fishOutput, ContainsSubstring( "__fish_yams_path_is config search path-tree enable' -l mode -a 'fallback preferred'")); } TEST_CASE("CompletionCommand - PowerShell status aliases no longer advertise format", "[cli][completion][catch2]") { CompletionCliTestHelper helper; CaptureStdout capture; const int rc = helper.runCommand({"yams", "completion", "powershell"}); const std::string output = capture.str(); REQUIRE((rc == 0)); CHECK_THAT(output, ContainsSubstring("'status' = @(")); CHECK_THAT(output, ContainsSubstring("'stats' = @(")); CHECK_THAT(output, ContainsSubstring("'config' = @(")); CHECK_THAT(output, ContainsSubstring("'plugin trust' = @(")); CHECK((output.find("'plugins trust' = @(") == std::string::npos)); CHECK_THAT(output, ContainsSubstring("'daemon start' = @(")); CHECK_THAT(output, ContainsSubstring("'plugin trust list' = @(")); CHECK_THAT( output, ContainsSubstring("'daemon start:--log-level' = @('trace','debug','info','warn','error')")); CHECK_THAT(output, ContainsSubstring( "'config search path-tree enable:--mode' = @('fallback','preferred')")); CHECK_THAT(output, ContainsSubstring( "@{ Name = '--socket'; Description = 'Socket path for daemon communication'")); CHECK_THAT(output, ContainsSubstring("@{ Name = '--details'; Description = 'Show trust file, " "defaults, strict mode, and effective roots'")); CHECK_THAT(output, ContainsSubstring("--no-physical")); CHECK_THAT(output, ContainsSubstring("--corpus")); CHECK((output.find("'stats' = @(\n @{ Name = '--format'") == std::string::npos)); } TEST_CASE("CompletionCommand - sourced zsh completion registers compdef", "[cli][completion][catch2]") { #ifdef _WIN32 SKIP("zsh completion smoke test requires zsh and Unix process substitution"); #else const fs::path yamsCli = findBuiltYamsCli(); REQUIRE_FALSE(yamsCli.empty()); const fs::path tempDir = yams::test::make_temp_dir("yams_zsh_completion_source_"); const fs::path dataDir = tempDir / "data"; fs::create_directories(dataDir); const fs::path configPath = tempDir / "config.toml"; const fs::path out = tempDir / "zsh-sourced-registration.out"; const fs::path err = tempDir / "zsh-sourced-registration.err"; { std::ofstream cfg(configPath); cfg << "[core]\n"; cfg << "data_dir = \"" << dataDir.string() << "\"\n"; } if (std::system("command -v zsh >/dev/null 2>&1") != 0) { SKIP("zsh completion smoke test requires zsh on PATH"); } const std::string cmd = "env YAMS_CONFIG='" + configPath.string() + "' YAMS_DATA_DIR='" + dataDir.string() + "' YAMS_NON_INTERACTIVE=1 YAMS_CLI_DISABLE_DAEMON_AUTOSTART=1 " "zsh -fc 'autoload -U compinit; compinit; source <(\"" + yamsCli.string() + "\" completion zsh); print -- ${_comps[yams]:-missing}' > '" + out.string() + "' 2> '" + err.string() + "'"; const int rc = std::system(cmd.c_str()); REQUIRE(WIFEXITED(rc)); std::ifstream outInput(out); const std::string stdoutOutput((std::istreambuf_iterator(outInput)), std::istreambuf_iterator()); std::ifstream errInput(err); const std::string stderrOutput((std::istreambuf_iterator(errInput)), std::istreambuf_iterator()); const std::string commandStderr = stripCoverageProfilingDiagnostics(stderrOutput); INFO("zsh completion command: " << cmd); INFO("zsh stdout: " << stdoutOutput); INFO("zsh stderr: " << stderrOutput); INFO("zsh stderr without coverage profiling diagnostics: " << commandStderr); REQUIRE((WEXITSTATUS(rc) == 0)); std::string registered; std::istringstream registeredInput(stdoutOutput); std::getline(registeredInput, registered); CHECK((registered == "_yams")); CHECK(commandStderr.empty()); std::error_code ec; fs::remove_all(tempDir, ec); #endif }