317 lines
10 KiB
C++
317 lines
10 KiB
C++
// CLI Doctor Command tests
|
|
// Catch2 migration from GTest (yams-3s4 / yams-cli)
|
|
//
|
|
// Tests for doctor command subcommands.
|
|
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include <sqlite3.h>
|
|
|
|
#include <yams/cli/yams_cli.h>
|
|
|
|
#include <algorithm>
|
|
#include <filesystem>
|
|
#include <iostream>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// 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<yams::test::ScopedEnvVar> configEnv;
|
|
std::optional<yams::test::ScopedEnvVar> dataEnv;
|
|
std::optional<yams::test::ScopedEnvVar> nonInteractiveEnv;
|
|
std::optional<yams::test::ScopedEnvVar> 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<std::string>("1"));
|
|
// Disable daemon autostart to prevent cleanup crashes
|
|
disableDaemonEnv.emplace(std::string("YAMS_CLI_DISABLE_DAEMON_AUTOSTART"),
|
|
std::optional<std::string>("1"));
|
|
}
|
|
|
|
~CliTestHelper() {
|
|
configEnv.reset();
|
|
dataEnv.reset();
|
|
nonInteractiveEnv.reset();
|
|
disableDaemonEnv.reset();
|
|
|
|
std::error_code ec;
|
|
fs::remove_all(tempDir, ec);
|
|
}
|
|
|
|
std::unique_ptr<YamsCLI> makeCli() const { return std::make_unique<YamsCLI>(); }
|
|
|
|
int runCommand(const std::vector<std::string>& args) {
|
|
auto cli = makeCli();
|
|
std::vector<char*> argv;
|
|
argv.reserve(args.size());
|
|
for (const auto& arg : args) {
|
|
argv.push_back(const_cast<char*>(arg.c_str()));
|
|
}
|
|
return cli->run(static_cast<int>(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<char**>(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<char**>(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<char**>(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<char**>(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<char**>(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<char**>(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<char**>(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<char**>(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);
|
|
}
|