// CLI Repair Command tests // Catch2 tests for repair command option parsing and behavior // // Tests for repair command flag handling, verbose mode defaults, // and CLI option isolation. #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 yams::cli { std::unique_ptr createRepairCommand(); } namespace { /** * RAII helper for repair command tests with proper isolation and cleanup. */ struct RepairTestHelper { fs::path tempDir; std::optional configEnv; std::optional dataEnv; std::optional nonInteractiveEnv; std::optional disableDaemonEnv; RepairTestHelper() { tempDir = yams::test::make_temp_dir("yams_repair_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")); } ~RepairTestHelper() { 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_; }; void parseRepairCommandWithHelp(const std::vector& argv) { CLI::App app{"yams"}; auto repairCmd = yams::cli::createRepairCommand(); repairCmd->registerCommand(app, /*cli*/ nullptr); bool gotHelp = false; bool gotParseError = false; std::string parseErrorMsg; try { app.parse(static_cast(argv.size()), const_cast(argv.data())); } catch (const CLI::CallForHelp&) { gotHelp = true; } catch (const CLI::ParseError& e) { gotParseError = true; parseErrorMsg = e.what(); } CHECK(gotHelp); if (gotParseError) { FAIL("Unexpected parse error: " << parseErrorMsg); } } } // namespace TEST_CASE("RepairCommand - help displays without errors", "[cli][repair][catch2]") { RepairTestHelper helper; CaptureStdout capture; auto cli = helper.makeCli(); const char* argv[] = {"yams", "repair", "--help"}; // Should not throw - --help exits early (void)cli->run(3, const_cast(argv)); std::string output = capture.str(); // Should show repair help text CHECK(output.find("repair") != std::string::npos); CHECK(output.find("--orphans") != std::string::npos); CHECK(output.find("--fts5") != std::string::npos); CHECK(output.find("--embeddings") != std::string::npos); CHECK(output.find("--dedupe") != std::string::npos); } TEST_CASE("RepairCommand - verbose flag is documented correctly", "[cli][repair][catch2]") { RepairTestHelper helper; CaptureStdout capture; auto cli = helper.makeCli(); const char* argv[] = {"yams", "repair", "--help"}; (void)cli->run(3, const_cast(argv)); std::string output = capture.str(); // Verbose flag should mention transient errors (the key change from the refactor) CHECK(output.find("--verbose") != std::string::npos); CHECK(output.find("-v") != std::string::npos); } TEST_CASE("RepairCommand - option flags parse correctly", "[cli][repair][catch2]") { parseRepairCommandWithHelp({"yams", "repair", "--orphans", "--mime", "--chunks", "--downloads", "--path-tree", "--stuck", "--dedupe", "--help"}); } // Note: Tests that actually run repair operations (--fts5, --embeddings, --all, etc.) // are skipped because they require database access and can hang in unit test environment. // Integration tests should cover these scenarios with proper daemon/database setup. TEST_CASE("RepairCommand - fts5 flag parses correctly", "[cli][repair][catch2]") { parseRepairCommandWithHelp({"yams", "repair", "--fts5", "--help"}); } TEST_CASE("RepairCommand - embeddings flag with verbose parses correctly", "[cli][repair][catch2]") { parseRepairCommandWithHelp( {"yams", "repair", "--embeddings", "--verbose", "--foreground", "--help"}); } TEST_CASE("RepairCommand - all flag and refs parse correctly", "[cli][repair][catch2]") { parseRepairCommandWithHelp( {"yams", "repair", "--all", "--refs", "--graph", "--dedupe", "--help"}); } TEST_CASE("RepairCommand - model option accepts value", "[cli][repair][catch2]") { parseRepairCommandWithHelp( {"yams", "repair", "--embeddings", "--model", "test-embed-model", "--help"}); } TEST_CASE("RepairCommand - max retries option accepts value", "[cli][repair][catch2]") { parseRepairCommandWithHelp({"yams", "repair", "--stuck", "--max-retries", "5", "--help"}); } TEST_CASE("RepairCommand - complex flag set parses with help", "[cli][repair][catch2]") { CLI::App app{"yams"}; auto repairCmd = yams::cli::createRepairCommand(); repairCmd->registerCommand(app, /*cli*/ nullptr); std::vector argv = {"yams", "repair", "--all", "--refs", "--graph", "--foreground", "--dry-run", "--force", "--max-retries", "5", "--model", "test-embed-model", "--include-mime", "application/pdf", "--include-mime", "text/markdown", "--help"}; parseRepairCommandWithHelp(argv); } TEST_CASE("RepairCommand - default invocation reaches callback without throwing", "[cli][repair][catch2]") { RepairTestHelper helper; REQUIRE_NOTHROW((void)helper.runCommand({"yams", "repair", "--help"})); }