glove/tests/supervisor/path_exposure_journal_test.cpp
0xkite 3e9cd0680c
test(journal): inject append failures
Exercise partial-write plus disk-full and sync failures to prove rollback, retry, and replay behavior.
2026-07-21 22:25:05 -04:00

159 lines
6 KiB
C++

#include "glove/supervisor/path_exposure_journal.hpp"
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstdio>
#include <filesystem>
#include <string>
#include <system_error>
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-path-exposure-journal-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 create_record() -> glove::supervisor::path_exposure_create_record {
using namespace glove::supervisor;
return {
.descriptor =
path_exposure_descriptor{
.schema_version = 1,
.exposure_id = "sage-workspace",
.generation = 1,
.root_id = "projects",
.source_identity_digest = std::string(64, 'a'),
.scope_digest = std::string(64, 'b'),
.display_label = "Sage protocol",
.allowed_modes =
{
path_exposure_mode{
.access = path_access::read,
.materialization = path_materialization::bind,
.max_bytes = 0,
.cleanup_policy = path_cleanup_policy::retain,
},
path_exposure_mode{
.access = path_access::retained_write,
.materialization = path_materialization::copy,
.max_bytes = 1'048'576,
.cleanup_policy = path_cleanup_policy::retain,
},
},
.expires_at_ms = 3'601'000,
.allowed_runtime_template_ids = {"codex-safe"},
.state = path_exposure_state::active,
},
.request_id = "request-1",
.request_digest = std::string(64, 'c'),
.host_path = "/srv/projects/sage-protocol",
.parent_identity_digest = std::string(64, 'd'),
};
}
auto run() -> int {
using namespace glove::supervisor;
temporary_directory temporary;
REQUIRE(!temporary.root().empty());
const auto journal_path = temporary.root() / "exposures.journal";
const path_exposure_journal_record create = create_record();
const path_exposure_journal_record revoke = path_exposure_revoke_record{
.request_id = "revoke-1",
.request_digest = std::string(64, 'd'),
.exposure_id = "sage-workspace",
.generation = 1,
.state = path_exposure_state::revoked,
.revoked_at_ms = 2'000,
};
{
auto journal = path_exposure_journal::open(journal_path);
REQUIRE(journal.has_value());
REQUIRE(journal->records().empty());
REQUIRE(journal->append(create).has_value());
REQUIRE(journal->append(revoke).has_value());
REQUIRE(journal->records().size() == 2U);
REQUIRE(!path_exposure_journal::open(journal_path).has_value());
}
{
auto replayed = path_exposure_journal::open(journal_path);
REQUIRE(replayed.has_value());
REQUIRE(replayed->records().size() == 2U);
REQUIRE(replayed->records().front() == create);
REQUIRE(replayed->records().back() == revoke);
}
const auto corrupt_path = temporary.root() / "corrupt.journal";
{
auto journal = path_exposure_journal::open(corrupt_path);
REQUIRE(journal.has_value());
REQUIRE(journal->append(create).has_value());
}
const int corrupt = ::open(corrupt_path.c_str(), O_RDWR | O_CLOEXEC | O_NOFOLLOW);
REQUIRE(corrupt >= 0);
struct stat metadata{};
REQUIRE(::fstat(corrupt, &metadata) == 0);
REQUIRE(metadata.st_size > 0);
unsigned char changed = 0;
REQUIRE(::pread(corrupt, &changed, 1, metadata.st_size - 1) == 1);
changed ^= 1U;
REQUIRE(::pwrite(corrupt, &changed, 1, metadata.st_size - 1) == 1);
REQUIRE(::fsync(corrupt) == 0);
REQUIRE(::close(corrupt) == 0);
REQUIRE(!path_exposure_journal::open(corrupt_path).has_value());
const auto truncated_path = temporary.root() / "truncated.journal";
{
auto journal = path_exposure_journal::open(truncated_path);
REQUIRE(journal.has_value());
REQUIRE(journal->append(create).has_value());
}
struct stat truncated_metadata{};
REQUIRE(::stat(truncated_path.c_str(), &truncated_metadata) == 0);
REQUIRE(truncated_metadata.st_size > 0);
REQUIRE(::truncate(truncated_path.c_str(), truncated_metadata.st_size - 1) == 0);
REQUIRE(!path_exposure_journal::open(truncated_path).has_value());
const auto weak_path = temporary.root() / "weak.journal";
const int weak = ::open(weak_path.c_str(), O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
REQUIRE(weak >= 0);
REQUIRE(::close(weak) == 0);
REQUIRE(!path_exposure_journal::open(weak_path).has_value());
return 0;
}
} // namespace
auto main() -> int {
return run();
}