glove/tests/host/onboarding_cli_test.cpp
trvon afecee2c90
feat(remote): add construction-only backend
Co-authored-by: trvon <git@trevon.dev>
Signed-off-by: trvon <git@trevon.dev>
2026-08-07 22:27:35 -04:00

250 lines
8.8 KiB
C++

#include <signal.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
#include <system_error>
#include <thread>
#include <vector>
namespace {
#define REQUIRE(condition) \
do { \
if (!(condition)) { \
std::fprintf(stderr, "REQUIRE failed: %s @ %s:%d\n", #condition, __FILE__, __LINE__); \
return 1; \
} \
} while (false)
class temporary_directory {
public:
temporary_directory() {
std::string pattern = "/tmp/glove-onboarding-cli-test-XXXXXX";
if (char* created = ::mkdtemp(pattern.data()); created != nullptr) {
root_ = created;
}
}
temporary_directory(const temporary_directory&) = delete;
auto operator=(const temporary_directory&) -> temporary_directory& = delete;
~temporary_directory() {
std::error_code ignored;
std::filesystem::remove_all(root_, ignored);
}
[[nodiscard]] auto root() const -> const std::filesystem::path& { return root_; }
private:
std::filesystem::path root_;
};
auto write_owner_file(const std::filesystem::path& path, std::string_view contents, mode_t mode)
-> bool {
std::ofstream output{path};
output << contents;
output.close();
return output.good() && ::chmod(path.c_str(), mode) == 0;
}
auto run_glove(
const std::filesystem::path& glove_bin,
const std::filesystem::path& home,
std::vector<std::string> argv_owned,
const std::filesystem::path& stdout_path = {},
std::chrono::milliseconds timeout = std::chrono::seconds{10}
) -> int {
std::vector<char*> argv;
argv.reserve(argv_owned.size() + 1U);
for (auto& argument : argv_owned) {
argv.push_back(argument.data());
}
argv.push_back(nullptr);
const auto child = ::fork();
if (child < 0) {
return -1;
}
if (child == 0) {
if (::setenv("HOME", home.c_str(), 1) != 0) {
std::_Exit(127);
}
if (!stdout_path.empty()) {
if (std::freopen(stdout_path.c_str(), "w", stdout) == nullptr) {
std::_Exit(127);
}
}
::execv(glove_bin.c_str(), argv.data());
std::_Exit(127);
}
int status = 0;
const auto deadline = std::chrono::steady_clock::now() + timeout;
for (;;) {
const auto waited = ::waitpid(child, &status, WNOHANG);
if (waited == child) {
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}
if (waited < 0 && errno != EINTR) {
return -1;
}
if (std::chrono::steady_clock::now() >= deadline) {
(void)::kill(child, SIGKILL);
while (::waitpid(child, &status, 0) < 0 && errno == EINTR) {}
return -2;
}
std::this_thread::sleep_for(std::chrono::milliseconds{10});
}
}
auto run() -> int {
const char* glove_bin_value = std::getenv("GLOVE_BIN");
REQUIRE(glove_bin_value != nullptr && glove_bin_value[0] != '\0');
const std::string glove_bin{glove_bin_value};
temporary_directory temporary;
REQUIRE(!temporary.root().empty());
const auto project = temporary.root() / "project";
const auto harness_bin = temporary.root() / "harness-bin";
const auto output = temporary.root() / "plan.json";
REQUIRE(std::filesystem::create_directory(project));
REQUIRE(std::filesystem::create_directory(harness_bin));
{
std::ofstream harness{harness_bin / "codex"};
harness << "#!/bin/sh\nexit 0\n";
REQUIRE(harness.good());
}
REQUIRE(::chmod((harness_bin / "codex").c_str(), 0700) == 0);
REQUIRE(::chmod(harness_bin.c_str(), 0700) == 0);
const std::vector<std::string> plan_argv{
glove_bin,
"setup",
"plan",
"--path-root",
project.string(),
"--search-path",
harness_bin.string(),
"--runtime",
"codex",
"--json",
};
REQUIRE(run_glove(glove_bin, temporary.root(), plan_argv, output) == 0);
std::ifstream report{output};
const std::string json{
std::istreambuf_iterator<char>{report},
std::istreambuf_iterator<char>{},
};
REQUIRE(json.find(R"("mode":"read_only")") != std::string::npos);
REQUIRE(json.find(R"("session_policy_json")") == std::string::npos);
REQUIRE(json.find(R"("network_denied":true)") != std::string::npos);
REQUIRE(json.find(R"("credentials_configured":false)") != std::string::npos);
REQUIRE(!std::filesystem::exists(temporary.root() / ".config/glove/config.json"));
// A project-controlled path can mimic Homebrew's Cellar layout and place
// an arbitrary executable at the derived <prefix>/bin/brew path. Planning
// must fail closed from filesystem metadata alone: it must neither execute
// that binary nor wait for it, and it must not perform advertised writes.
const auto fake_prefix = temporary.root() / "fake-homebrew";
const auto fake_cellar_bin = fake_prefix / "Cellar" / "node" / "1.0" / "bin";
const auto fake_harness_bin = temporary.root() / "fake-harness-bin";
const auto marker = temporary.root() / "fake-brew-executed";
const auto protected_file = temporary.root() / "must-not-change";
REQUIRE(std::filesystem::create_directories(fake_cellar_bin));
REQUIRE(std::filesystem::create_directories(fake_prefix / "bin"));
REQUIRE(std::filesystem::create_directory(fake_harness_bin));
REQUIRE(write_owner_file(protected_file, "unchanged\n", 0600));
const auto fake_node = fake_cellar_bin / "node";
REQUIRE(write_owner_file(fake_node, "#!/bin/sh\nexit 0\n", 0700));
REQUIRE(write_owner_file(
fake_prefix / "bin" / "brew",
"#!/bin/sh\nprintf executed > '" + marker.string() + "'\nprintf changed > '" +
protected_file.string() + "'\nsleep 10\n",
0700
));
REQUIRE(
write_owner_file(fake_harness_bin / "codex", "#!" + fake_node.string() + "\nexit 0\n", 0700)
);
const auto adversarial_output = temporary.root() / "adversarial-plan.json";
const int adversarial_status = run_glove(
glove_bin,
temporary.root(),
{glove_bin,
"setup",
"plan",
"--path-root",
project.string(),
"--search-path",
fake_harness_bin.string(),
"--runtime",
"codex",
"--json"},
adversarial_output,
std::chrono::seconds{2}
);
REQUIRE(!std::filesystem::exists(marker));
{
std::ifstream protected_input{protected_file};
const std::string protected_contents{
std::istreambuf_iterator<char>{protected_input},
std::istreambuf_iterator<char>{},
};
REQUIRE(protected_contents == "unchanged\n");
}
REQUIRE(adversarial_status == 1);
REQUIRE(!std::filesystem::exists(temporary.root() / ".config/glove/config.json"));
REQUIRE(!std::filesystem::exists(temporary.root() / ".config/glove/session-policy.json"));
REQUIRE(!std::filesystem::exists(temporary.root() / ".config/glove/harnesses"));
auto show_policy_argv = plan_argv;
show_policy_argv.insert(show_policy_argv.end() - 1, "--show-policy");
const auto full_output = temporary.root() / "full-plan.json";
REQUIRE(run_glove(glove_bin, temporary.root(), std::move(show_policy_argv), full_output) == 0);
std::ifstream full_report{full_output};
const std::string full_json{
std::istreambuf_iterator<char>{full_report},
std::istreambuf_iterator<char>{},
};
REQUIRE(full_json.find(R"("session_policy_json":")") != std::string::npos);
REQUIRE(
run_glove(
glove_bin,
temporary.root(),
{glove_bin,
"setup",
"plan",
"--path-root",
project.string(),
"--search-path",
harness_bin.string(),
"--runtime",
"pi",
"--pi-settings",
(temporary.root() / "pi-settings.json").string()}
) == 1
);
REQUIRE(
run_glove(
glove_bin, temporary.root(), {glove_bin, "setup", "plan", "--backend", "unsupported"}
) == 1
);
REQUIRE(run_glove(glove_bin, temporary.root(), {glove_bin, "setup", "plan", "--help"}) == 0);
REQUIRE(run_glove(glove_bin, temporary.root(), {glove_bin, "setup", "plan", "--unknown"}) == 2);
return 0;
}
} // namespace
int main() {
return run();
}