yams/tests/unit/cli/content_roundtrip_catch2_test.cpp
trvon 11c391579c
fix(cli): preserve content bytes during hash hydration
Signed-off-by: trvon <me@trevon.dev>
2026-09-05 11:52:46 -06:00

179 lines
7.2 KiB
C++

#include <nlohmann/json.hpp>
#include "../../../tools/yams-cli/logging.h"
#include "../../common/test_helpers_catch2.h"
#include <catch2/catch_test_macros.hpp>
#include <yams/cli/command.h>
#include <yams/cli/yams_cli.h>
#include <yams/crypto/hasher.h>
#include <filesystem>
#include <iostream>
#include <optional>
#include <span>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
namespace yams::cli {
std::unique_ptr<ICommand> createCatCommand();
}
TEST_CASE("CLI cat accepts exactly one content selector", "[cli][cat][parser]") {
CLI::App app{"yams"};
auto command = yams::cli::createCatCommand();
command->registerCommand(app, nullptr);
app.get_subcommand("cat")->callback([] {});
const std::string hash(64, 'a');
SECTION("Explicit hash hydration") {
REQUIRE_NOTHROW(app.parse("cat --hash " + hash + " --raw"));
}
SECTION("Positional hash remains supported") {
REQUIRE_NOTHROW(app.parse("cat " + hash));
}
SECTION("Positional path remains supported") {
REQUIRE_NOTHROW(app.parse("cat evidence.txt"));
}
SECTION("Explicit hash prefix remains supported") {
REQUIRE_NOTHROW(app.parse("cat --hash abcdef"));
}
SECTION("Explicit hash must be hexadecimal") {
REQUIRE_THROWS_AS(app.parse("cat --hash " + std::string(64, 'z')), CLI::ParseError);
}
SECTION("Explicit hash must not be empty") {
REQUIRE_THROWS_AS(app.parse("cat --hash"), CLI::ParseError);
}
SECTION("A selector is required") {
REQUIRE_THROWS_AS(app.parse("cat --raw"), CLI::ParseError);
}
SECTION("Conflicting selectors are rejected") {
REQUIRE_THROWS_AS(app.parse("cat evidence.txt --hash " + hash), CLI::ParseError);
}
}
namespace {
struct StreamCapture {
std::ostringstream output;
std::istringstream input;
std::streambuf* oldOutput;
std::streambuf* oldInput;
explicit StreamCapture(const std::string& payload)
: input(payload), oldOutput(std::cout.rdbuf(output.rdbuf())),
oldInput(std::cin.rdbuf(input.rdbuf())) {}
~StreamCapture() {
std::cout.rdbuf(oldOutput);
std::cin.rdbuf(oldInput);
std::cin.clear();
}
};
std::pair<int, std::string> runCli(std::vector<std::string> args, const std::string& payload = {}) {
yams::cli::YamsCLI cli;
std::vector<char*> argv;
for (auto& arg : args)
argv.push_back(arg.data());
StreamCapture capture(payload);
const auto code = cli.run(static_cast<int>(argv.size()), argv.data());
return {code, capture.output.str()};
}
} // namespace
TEST_CASE("CLI startup diagnostics do not contaminate content output", "[cli][logging]") {
const auto previousLogger = spdlog::default_logger();
std::ostringstream diagnostics;
struct RestoreLogging {
std::shared_ptr<spdlog::logger> previous;
~RestoreLogging() { spdlog::set_default_logger(std::move(previous)); }
} restore{previousLogger};
StreamCapture content("");
yams::cli::initializeCliLogging(diagnostics);
spdlog::warn("startup-warning-probe");
spdlog::default_logger()->flush();
std::cout << "immutable content";
CHECK(content.output.str() == "immutable content");
CHECK(diagnostics.str().find("startup-warning-probe") != std::string::npos);
}
TEST_CASE("CLI acknowledged stdin content hydrates immediately by full hash", "[cli][roundtrip]") {
const auto root = yams::test::make_temp_dir("yams_content_roundtrip_");
struct Cleanup {
std::filesystem::path root;
~Cleanup() {
std::error_code ec;
std::filesystem::remove_all(root, ec);
}
} cleanup{root};
yams::test::ScopedEnvVar embedded("YAMS_EMBEDDED", std::string("1"));
yams::test::ScopedEnvVar inDaemon("YAMS_IN_DAEMON", std::nullopt);
yams::test::ScopedEnvVar socket("YAMS_DAEMON_SOCKET", std::nullopt);
yams::test::ScopedEnvVar socketAlias("YAMS_DAEMON_SOCKET_PATH", std::nullopt);
yams::test::ScopedEnvVar config("YAMS_CONFIG", (root / "config.toml").string());
yams::test::ScopedEnvVar data("YAMS_DATA_DIR", (root / "data").string());
yams::test::ScopedEnvVar storage("YAMS_STORAGE", (root / "data").string());
yams::test::ScopedEnvVar vectors("YAMS_DISABLE_VECTORS", std::string("1"));
yams::test::ScopedEnvVar models("YAMS_SKIP_MODEL_LOADING", std::string("1"));
yams::test::ScopedEnvVar watcher("YAMS_DISABLE_SESSION_WATCHER", std::string("1"));
yams::test::ScopedEnvVar autostart("YAMS_CLI_DISABLE_DAEMON_AUTOSTART", std::string("1"));
std::string payload = "Immutable evidence\nsecond line\n\tindent\n";
bool checkSearch = true;
SECTION("Text is searchable and hydrates byte-for-byte") {}
SECTION("Multi-chunk binary content preserves embedded NUL bytes") {
payload.assign(131073, 'x');
payload[65535] = '\0';
payload[65536] = '\n';
payload.back() = '\0';
checkSearch = false;
}
const auto expectedHash =
yams::crypto::SHA256Hasher::hash(std::as_bytes(std::span(payload.data(), payload.size())));
const auto [added, output] =
runCli({"yams", "--data-dir", (root / "data").string(), "--json", "add", "-", "--name",
"evidence.txt", "--no-embeddings", "--sync"},
payload);
INFO(output);
REQUIRE(added == 0);
const auto response = nlohmann::json::parse(output);
REQUIRE(response.at("summary").at("failed") == 0);
const auto hash = response.at("results").at(0).at("hash").get<std::string>();
REQUIRE(hash.size() == 64);
CHECK(hash == expectedHash);
const auto [retrieved, content] =
runCli({"yams", "--data-dir", (root / "data").string(), "cat", hash, "--raw"});
INFO("Retrieved bytes: " << content.size());
REQUIRE(retrieved == 0);
CHECK(content == payload);
const auto [explicitCode, explicitContent] =
runCli({"yams", "--data-dir", (root / "data").string(), "cat", "--hash", hash, "--raw"});
REQUIRE(explicitCode == 0);
CHECK(explicitContent == payload);
// Explicit corpus selection must not leak content from the default corpus
// or reuse an embedded host belonging to another data directory.
const auto [wrongCorpusCode, wrongCorpusContent] =
runCli({"yams", "--data-dir", (root / "other-data").string(), "cat", hash, "--raw"});
CHECK(wrongCorpusCode != 0);
CHECK(wrongCorpusContent.empty());
if (!checkSearch)
return;
const auto [searched, searchOutput] =
runCli({"yams", "--data-dir", (root / "data").string(), "--json", "search", "Immutable",
"--type", "keyword"});
INFO(searchOutput);
REQUIRE(searched == 0);
const auto results = nlohmann::json::parse(searchOutput).at("results");
REQUIRE_FALSE(results.empty());
CHECK(results.at(0).at("hash") == hash);
CHECK(results.at(0).at("hydration").at("hash") == hash);
const auto [grepped, grepOutput] =
runCli({"yams", "--data-dir", (root / "data").string(), "grep", "Immutable", "--json"});
INFO(grepOutput);
REQUIRE(grepped == 0);
const auto matches = nlohmann::json::parse(grepOutput).at("matches");
REQUIRE_FALSE(matches.empty());
CHECK(matches.at(0).at("hash") == hash);
CHECK(matches.at(0).at("hydration").at("hash") == hash);
}