yams/tests/unit/cli/add_command_stdin_catch2_test.cpp
Trevon b603738f44
Stabilize sanitizer lanes and daemon concurrency (#40)
* fix(repair): keep RPC progress alive

* fix(daemon): propagate DB open failure to FSM and add shutdown cancellation

* fix(db): classify FTS5 inverted-index errors as repairable, not transient locks

* fix(daemon): clean up stale corrupt files after salvage and suppress optional readiness keys

* test(db_recovery): update FTS5 classification test for new transient-lock exclusion

* test(gc): add garbage collector unit tests for collection, dry-run, scheduling

* test(repair): expand repair manager tests with hash-verified recovery and source ordering

* fix(gc): dry-run mode no longer increments blocksDeleted/bytesReclaimed

* fix(mock,mcp): add atomic bool for lock consistency, move field assignments

* style: fix unnecessary-copy, dead-store, and const-refable findings from Infer

* fix(daemon): increase default shutdown timeout from 15s to 120s

* fix(daemon): demote non-critical warnings to info (FTS5, VACUUM, WorkCoordinator timeout)

* fix(db): re-enable WAL auto-checkpoint to prevent unbounded WAL growth

* test(pool): add WAL auto-checkpoint and concurrency coverage for connection pool

* test(pool): add exhaustion, stats, lifecycle, and read-only connection pool tests

* test(pool): add tagging, WAL journal, reuse, and busy timeout tests

* test(metrics): relax session snippet assertion to tolerate stub behavior

* fix(test): stabilize sanitizer triage failures

* fix(build): wire version and zyp deps

* fix(daemon): close TSAN race clusters

* fix(test): guard optional zyp dep

* fix(test): address PR review findings
2026-06-01 12:14:43 -04:00

161 lines
5.5 KiB
C++

// CLI Add Command stdin tests
// Catch2 migration from GTest (yams-3s4 / yams-cli)
//
// Tests for add command reading from piped stdin.
#include <catch2/catch_test_macros.hpp>
#include <filesystem>
#include <fstream>
#include <string>
#include <chrono>
#include <nlohmann/json.hpp>
#include "../../common/test_helpers_catch2.h"
#include <yams/compat/unistd.h>
#ifdef _WIN32
#define WIFEXITED(x) ((x) != -1)
#define WEXITSTATUS(x) (x)
#else
#include <sys/wait.h>
#endif
namespace fs = std::filesystem;
namespace {
auto findBuiltYamsCli() -> fs::path {
std::vector<fs::path> candidates;
if (const char* buildRoot = std::getenv("MESON_BUILD_ROOT")) {
candidates.emplace_back(fs::path(buildRoot) / "tools" / "yams-cli" / "yams-cli");
}
std::vector<fs::path> 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<fs::path> 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("AddCommand - reads from piped stdin and stores content", "[cli][add][stdin][catch2]") {
#ifdef _WIN32
SKIP("Test requires Unix shell with 'cat' and pipe support");
#else
const fs::path yamsCli = findBuiltYamsCli();
REQUIRE_FALSE(yamsCli.empty());
const std::string payload = "line1\nline2\n\tindent\n";
const auto unique = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
fs::path tmp = fs::temp_directory_path() / ("yams_add_stdin_" + unique);
fs::create_directories(tmp);
// Use RAII to restore environment on exit (including exceptions/early returns)
yams::test::ScopedEnvVar configEnv("YAMS_CONFIG", (tmp / "config.toml").string());
yams::test::ScopedEnvVar dataEnv("YAMS_DATA_DIR", tmp.string());
yams::test::ScopedEnvVar nonInteractiveEnv("YAMS_NON_INTERACTIVE",
std::optional<std::string>{"1"});
yams::test::ScopedEnvVar disableDaemonEnv("YAMS_CLI_DISABLE_DAEMON_AUTOSTART",
std::optional<std::string>{"1"});
fs::path in = tmp / "stdin.txt";
{
std::ofstream o(in, std::ios::binary);
o << payload;
}
fs::path out = tmp / "out.json";
std::string cmd = "cat '" + in.string() + "' | '" + yamsCli.string() +
"' --json add - --name piped.txt > '" + out.string() + "'";
int rc = std::system(cmd.c_str());
REQUIRE(WIFEXITED(rc));
CHECK(WEXITSTATUS(rc) == 0);
// Validate JSON output
{
std::ifstream i(out);
REQUIRE(i.good());
std::string content((std::istreambuf_iterator<char>(i)), std::istreambuf_iterator<char>());
REQUIRE(!content.empty());
// Some invocations may prepend or append non-JSON lines; isolate the JSON object.
const auto first = content.find('{');
const auto last = content.rfind('}');
REQUIRE(first != std::string::npos);
REQUIRE(last != std::string::npos);
REQUIRE(last >= first);
content = content.substr(first, last - first + 1);
nlohmann::json j;
REQUIRE_NOTHROW(j = nlohmann::json::parse(content));
REQUIRE(j.is_object());
REQUIRE(j.contains("results"));
REQUIRE(j["results"].is_array());
REQUIRE(!j["results"].empty());
REQUIRE(j.contains("summary"));
REQUIRE(j["summary"].is_object());
REQUIRE(j["summary"].contains("added"));
REQUIRE(j["summary"].contains("updated"));
REQUIRE(j["summary"].contains("skipped"));
REQUIRE(j["summary"].contains("failed"));
const auto& r0 = j["results"][0];
REQUIRE(r0.is_object());
REQUIRE(r0.contains("path"));
REQUIRE(r0.contains("success"));
}
// Cleanup temp directory
std::error_code ec;
fs::remove_all(tmp, ec);
#endif
}