// CLI Doctor Command tests // Catch2 migration from GTest (yams-3s4 / yams-cli) // // Tests for doctor command subcommands. #include #include #include #include #include #include #include #include #include #include // Include test helpers for ScopedEnvVar #include "../../common/test_helpers_catch2.h" namespace fs = std::filesystem; using namespace yams::cli; namespace { /** * RAII helper for CLI tests with proper isolation and cleanup. */ struct CliTestHelper { fs::path tempDir; std::optional configEnv; std::optional dataEnv; std::optional nonInteractiveEnv; std::optional disableDaemonEnv; CliTestHelper() { tempDir = yams::test::make_temp_dir("yams_cli_catch2_test_"); fs::create_directories(tempDir); 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")); // Disable daemon autostart to prevent cleanup crashes disableDaemonEnv.emplace(std::string("YAMS_CLI_DISABLE_DAEMON_AUTOSTART"), std::optional("1")); } ~CliTestHelper() { configEnv.reset(); dataEnv.reset(); nonInteractiveEnv.reset(); disableDaemonEnv.reset(); std::error_code ec; fs::remove_all(tempDir, ec); } std::unique_ptr makeCli() const { return std::make_unique(); } int runCommand(const std::vector& args) { auto cli = makeCli(); 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()); } }; /** * RAII helper to capture and restore cout. */ 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_; }; } // namespace TEST_CASE("DoctorCommand - prune help does not run doctor diagnostics", "[cli][doctor][catch2]") { CliTestHelper helper; // When running `yams doctor prune` without arguments (help mode), // it should NOT execute the default doctor diagnostics (runAll) CaptureStdout capture; auto cli = helper.makeCli(); const char* argv[] = {"yams", "doctor", "prune"}; cli->run(3, const_cast(argv)); std::string output = capture.str(); // Should show prune help text CHECK(output.find("YAMS Doctor Prune") != std::string::npos); CHECK(output.find("Composite Categories") != std::string::npos); // Should NOT show doctor diagnostics output CHECK(output.find("Collecting system information") == std::string::npos); CHECK(output.find("Daemon Status") == std::string::npos); } TEST_CASE("DoctorCommand - prune with category does not run doctor diagnostics", "[cli][doctor][catch2]") { CliTestHelper helper; CaptureStdout capture; auto cli = helper.makeCli(); // Parse args: yams doctor prune --category build-artifacts const char* argv[] = {"yams", "doctor", "prune", "--category", "build-artifacts"}; // Note: This will fail if daemon not running, but that's OK for the test // We're just checking it doesn't run doctor diagnostics cli->run(5, const_cast(argv)); std::string output = capture.str(); // Should NOT show doctor diagnostics output CHECK(output.find("Collecting system information") == std::string::npos); } // Note: We don't test bare "yams doctor" because it actually runs diagnostics // which may hang or take a long time. The key fix is ensuring subcommands // like "prune" don't trigger diagnostics. TEST_CASE("DoctorCommand - prune help displays without errors", "[cli][doctor][catch2]") { CliTestHelper helper; // Test that the UI frames are built correctly CaptureStdout capture; auto cli = helper.makeCli(); const char* argv[] = {"yams", "doctor", "prune"}; // Should not throw REQUIRE_NOTHROW(cli->run(3, const_cast(argv))); std::string output = capture.str(); // Check for proper formatting CHECK(output.find("YAMS Doctor Prune") != std::string::npos); CHECK(output.find("build-artifacts") != std::string::npos); CHECK(output.find("Usage Examples") != std::string::npos); // Should not contain error messages (case-insensitive check) std::string lowerOutput = output; std::transform(lowerOutput.begin(), lowerOutput.end(), lowerOutput.begin(), ::tolower); // Allow "error" in words like "stderr" but not standalone bool hasError = lowerOutput.find(" error") != std::string::npos || lowerOutput.find("error ") != std::string::npos || lowerOutput.find("failed") != std::string::npos; CHECK_FALSE(hasError); } TEST_CASE("DoctorCommand - dedupe help displays correctly", "[cli][doctor][catch2]") { CliTestHelper helper; CaptureStdout capture; auto cli = helper.makeCli(); const char* argv[] = {"yams", "doctor", "dedupe", "--help"}; (void)cli->run(4, const_cast(argv)); std::string output = capture.str(); // Should show dedupe options CHECK(output.find("--mode") != std::string::npos); CHECK(output.find("--strategy") != std::string::npos); CHECK(output.find("--apply") != std::string::npos); CHECK(output.find("semantic") != std::string::npos); CHECK(output.find("--semantic-threshold") != std::string::npos); } TEST_CASE("DoctorCommand - repair subcommand help displays correctly", "[cli][doctor][catch2]") { CliTestHelper helper; CaptureStdout capture; auto cli = helper.makeCli(); const char* argv[] = {"yams", "doctor", "repair", "--help"}; (void)cli->run(4, const_cast(argv)); std::string output = capture.str(); // Should show repair options CHECK(output.find("--embeddings") != std::string::npos); CHECK(output.find("--fts5") != std::string::npos); } TEST_CASE("DoctorCommand - repair with no flags does not fall through to full diagnostics", "[cli][doctor][catch2]") { CliTestHelper helper; CaptureStdout capture; const int rc = helper.runCommand({"yams", "doctor", "repair"}); const std::string output = capture.str(); CHECK(rc == 0); CHECK(output.find("Nothing to repair.") != std::string::npos); CHECK(output.find("Collecting system information") == std::string::npos); } TEST_CASE("DoctorCommand - validate help displays correctly", "[cli][doctor][catch2]") { CliTestHelper helper; CaptureStdout capture; auto cli = helper.makeCli(); const char* argv[] = {"yams", "doctor", "validate", "--help"}; (void)cli->run(4, const_cast(argv)); std::string output = capture.str(); // Should show validate options CHECK(output.find("--graph") != std::string::npos); } TEST_CASE("DoctorCommand - validate with no flags does not run full doctor", "[cli][doctor][catch2]") { CliTestHelper helper; CaptureStdout capture; const int rc = helper.runCommand({"yams", "doctor", "validate"}); const std::string output = capture.str(); CHECK(rc == 0); CHECK(output.find("Nothing to validate. Use --graph.") != std::string::npos); CHECK(output.find("Collecting system information") == std::string::npos); } TEST_CASE("DoctorCommand - tuning help displays correctly", "[cli][doctor][catch2]") { CliTestHelper helper; CaptureStdout capture; auto cli = helper.makeCli(); const char* argv[] = {"yams", "doctor", "tuning", "--help"}; (void)cli->run(4, const_cast(argv)); std::string output = capture.str(); // Should show tuning options CHECK(output.find("--apply") != std::string::npos); } TEST_CASE("DoctorCommand - top-level help displays all subcommands", "[cli][doctor][catch2]") { CliTestHelper helper; CaptureStdout capture; auto cli = helper.makeCli(); const char* argv[] = {"yams", "doctor", "--help"}; (void)cli->run(3, const_cast(argv)); std::string output = capture.str(); // Should list available subcommands CHECK(output.find("prune") != std::string::npos); CHECK(output.find("dedupe") != std::string::npos); CHECK(output.find("repair") != std::string::npos); } TEST_CASE("DoctorCommand - plugin missing target fails cleanly without full doctor fallback", "[cli][doctor][catch2]") { CliTestHelper helper; CaptureStdout capture; const int rc = helper.runCommand({"yams", "doctor", "plugin", "definitely-not-a-real-plugin"}); const std::string output = capture.str(); CHECK(rc == 0); CHECK(output.find("Plugin Doctor: definitely-not-a-real-plugin") != std::string::npos); CHECK(output.find("Not found as path or name in default dirs") != std::string::npos); CHECK(output.find("Collecting system information") == std::string::npos); } TEST_CASE("DoctorCommand - reports non-vec0 vector schema with fix hint", "[cli][doctor][vec0][catch2]") { CliTestHelper helper; const fs::path dataDir = helper.tempDir / "data"; fs::create_directories(dataDir); const fs::path dbPath = dataDir / "vectors.db"; sqlite3* db = nullptr; REQUIRE(sqlite3_open(dbPath.string().c_str(), &db) == SQLITE_OK); char* err = nullptr; REQUIRE(sqlite3_exec(db, "CREATE TABLE doc_embeddings(rowid INTEGER PRIMARY KEY, embedding BLOB)", nullptr, nullptr, &err) == SQLITE_OK); if (err) { sqlite3_free(err); } sqlite3_close(db); CaptureStdout capture; const int rc = helper.runCommand({"yams", "doctor"}); const std::string output = capture.str(); CHECK(rc == 0); CHECK(output.find("doc_embeddings table not using vec0 virtual table") != std::string::npos); CHECK(output.find("yams doctor --recreate-vectors --stop-daemon") != std::string::npos); }