Add an adapter-selected AF_UNIX stream transport while preserving the pathname compatibility default and owner/mode identity checks.
491 lines
23 KiB
C++
491 lines
23 KiB
C++
#include "glove/host/config.hpp"
|
|
#include "glove/host/doctor.hpp"
|
|
#include "glove/host/setup.hpp"
|
|
#include "glove/supervisor/path_exposure.hpp"
|
|
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#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-host-config-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 run() -> int {
|
|
using namespace glove::host;
|
|
temporary_directory temporary;
|
|
REQUIRE(!temporary.root().empty());
|
|
environment values{};
|
|
values.home = temporary.root().string();
|
|
values.xdg_runtime_dir = (temporary.root() / "run/user/501").string();
|
|
auto directories = resolve_directories(values);
|
|
REQUIRE(directories.has_value());
|
|
REQUIRE(directories->config == temporary.root() / ".config/glove");
|
|
REQUIRE(directories->state == temporary.root() / ".local/state/glove");
|
|
REQUIRE(directories->runtime == temporary.root() / ".local/state/glove/runtime");
|
|
|
|
const auto session_policy = temporary.root() / "session-policy.json";
|
|
setup_options managed_options{};
|
|
managed_options.session_policy = session_policy;
|
|
managed_options.dry_run = true;
|
|
auto managed_plan = plan_setup(managed_options, values);
|
|
REQUIRE(managed_plan.has_value());
|
|
REQUIRE(managed_plan->service.session_policy == session_policy);
|
|
REQUIRE(managed_plan->service.session_store == directories->state / "sessions.journal");
|
|
REQUIRE(managed_plan->service.materialization_root == directories->state / "materializations");
|
|
REQUIRE(managed_plan->service.library_bundle_root == directories->data / "library-bundles");
|
|
|
|
const auto project_root = temporary.root() / "projects\"quoted";
|
|
const auto project = project_root / "sage-protocol";
|
|
REQUIRE(std::filesystem::create_directories(project));
|
|
setup_options options{};
|
|
options.protected_root = project_root;
|
|
options.dry_run = true;
|
|
auto dry_run = plan_setup(options, values);
|
|
REQUIRE(dry_run.has_value());
|
|
REQUIRE(execute_setup(*dry_run).has_value());
|
|
REQUIRE(!std::filesystem::exists(dry_run->config_path));
|
|
|
|
options.dry_run = false;
|
|
auto plan = plan_setup(options, values);
|
|
REQUIRE(plan.has_value());
|
|
REQUIRE(execute_setup(*plan).has_value());
|
|
auto loaded = load_config(plan->config_path);
|
|
REQUIRE(loaded.has_value());
|
|
REQUIRE(loaded->runtime_directory == directories->runtime);
|
|
REQUIRE(loaded->path_exposure_policy.has_value());
|
|
auto registry = glove::supervisor::path_exposure_registry::load(
|
|
*loaded->path_exposure_policy, *loaded->path_exposure_journal, 1024U * 1024U
|
|
);
|
|
REQUIRE(registry.has_value());
|
|
REQUIRE(execute_setup(*plan).has_value());
|
|
|
|
// An explicit config path must not assume the separate XDG config child
|
|
// used for path-exposure policy already exists.
|
|
const auto isolated_config_home = temporary.root() / "isolated-config";
|
|
const auto isolated_state_home = temporary.root() / "isolated-state";
|
|
const auto isolated_data_home = temporary.root() / "isolated-data";
|
|
const auto isolated_runtime_home = temporary.root() / "isolated-runtime";
|
|
const auto explicit_config_parent = temporary.root() / "explicit-config";
|
|
for (const auto& directory : {
|
|
isolated_config_home,
|
|
isolated_state_home,
|
|
isolated_data_home,
|
|
isolated_runtime_home,
|
|
explicit_config_parent,
|
|
}) {
|
|
REQUIRE(std::filesystem::create_directory(directory));
|
|
REQUIRE(::chmod(directory.c_str(), 0700) == 0);
|
|
}
|
|
auto isolated_values = values;
|
|
isolated_values.xdg_config_home = isolated_config_home.string();
|
|
isolated_values.xdg_state_home = isolated_state_home.string();
|
|
isolated_values.xdg_data_home = isolated_data_home.string();
|
|
isolated_values.xdg_runtime_dir = isolated_runtime_home.string();
|
|
setup_options explicit_config_options{};
|
|
explicit_config_options.protected_root = project_root;
|
|
explicit_config_options.config_path = explicit_config_parent / "glove.json";
|
|
auto explicit_config_plan = plan_setup(explicit_config_options, isolated_values);
|
|
REQUIRE(explicit_config_plan.has_value());
|
|
REQUIRE(execute_setup(*explicit_config_plan).has_value());
|
|
REQUIRE(
|
|
std::filesystem::is_regular_file(
|
|
isolated_config_home / "glove" / "path-exposure-policy.json"
|
|
)
|
|
);
|
|
|
|
const auto legacy_runtime = temporary.root() / "run/user/501/glove";
|
|
REQUIRE(std::filesystem::create_directories(legacy_runtime));
|
|
REQUIRE(::chmod(legacy_runtime.c_str(), 0700) == 0);
|
|
REQUIRE(std::filesystem::remove(plan->config_path));
|
|
auto legacy_config = *loaded;
|
|
legacy_config.runtime_directory = legacy_runtime;
|
|
REQUIRE(write_config_exclusive(plan->config_path, legacy_config).has_value());
|
|
auto migration = plan_setup(options, values);
|
|
REQUIRE(migration.has_value());
|
|
REQUIRE(migration->migrate_runtime_from == legacy_runtime);
|
|
REQUIRE(execute_setup(*migration).has_value());
|
|
auto migrated = load_config(plan->config_path);
|
|
REQUIRE(migrated.has_value());
|
|
REQUIRE(migrated->runtime_directory == directories->runtime);
|
|
REQUIRE(std::filesystem::exists(legacy_runtime));
|
|
#if defined(__linux__)
|
|
REQUIRE(std::filesystem::remove(plan->config_path));
|
|
auto ssh_legacy_config = *loaded;
|
|
ssh_legacy_config.runtime_directory =
|
|
std::filesystem::path{"/run/user"} / std::to_string(::geteuid()) / "glove";
|
|
REQUIRE(write_config_exclusive(plan->config_path, ssh_legacy_config).has_value());
|
|
auto ssh_values = values;
|
|
ssh_values.xdg_runtime_dir.reset();
|
|
auto ssh_migration = plan_setup(options, ssh_values);
|
|
REQUIRE(ssh_migration.has_value());
|
|
REQUIRE(ssh_migration->migrate_runtime_from == ssh_legacy_config.runtime_directory);
|
|
#endif
|
|
REQUIRE(std::filesystem::remove(plan->config_path));
|
|
REQUIRE(execute_setup(*plan).has_value());
|
|
REQUIRE(load_config(plan->config_path).has_value());
|
|
const auto ledger_path = setup_ledger_path(plan->service);
|
|
auto ledger = load_setup_ledger(ledger_path);
|
|
REQUIRE(ledger.has_value());
|
|
REQUIRE(ledger->config_path == plan->config_path);
|
|
REQUIRE(!ledger->resources.empty());
|
|
REQUIRE(execute_setup(*plan).has_value());
|
|
REQUIRE(load_setup_ledger(ledger_path) == ledger);
|
|
|
|
const auto report = diagnose(plan->config_path);
|
|
REQUIRE(report.healthy());
|
|
REQUIRE(report.checks.size() >= 5U);
|
|
|
|
const auto derived_path = temporary.root() / ".config/glove/derived.json";
|
|
auto derived = *loaded;
|
|
derived.session_policy = session_policy;
|
|
REQUIRE(write_config_exclusive(derived_path, derived).has_value());
|
|
auto loaded_derived = load_config(derived_path);
|
|
REQUIRE(loaded_derived.has_value());
|
|
REQUIRE(*loaded_derived == derived);
|
|
REQUIRE(!write_config_exclusive(derived_path, derived).has_value());
|
|
|
|
const auto proxy_path = temporary.root() / ".config/glove/local-services.json";
|
|
auto proxy_config = managed_plan->service;
|
|
proxy_config.apple_container.reset();
|
|
proxy_config.local_service_proxy = local_service_proxy_config{
|
|
.io_timeout_ms = 2'500,
|
|
.max_concurrency = 3,
|
|
.guest_channel_adapter =
|
|
guest_channel_adapter_config{
|
|
.adapter_id = "sage-observation",
|
|
.channel_schema_id = "sage.glove-observation.v1",
|
|
.transport_id = "inherited-stream-v1",
|
|
},
|
|
.endpoints = {
|
|
local_service_proxy_endpoint{
|
|
.alias = "sage-observe",
|
|
.socket_path = temporary.root() / "services/sage.sock",
|
|
.runtime_ids = {"codex", "pi"},
|
|
},
|
|
local_service_proxy_endpoint{
|
|
.alias = "tooling.v1",
|
|
.socket_path = temporary.root() / "services/tooling.sock",
|
|
.runtime_ids = {"pi"},
|
|
},
|
|
},
|
|
};
|
|
REQUIRE(validate(proxy_config).has_value());
|
|
REQUIRE(write_config_exclusive(proxy_path, proxy_config).has_value());
|
|
REQUIRE(load_config(proxy_path) == proxy_config);
|
|
auto legacy_proxy_config = proxy_config;
|
|
legacy_proxy_config.local_service_proxy->guest_channel_adapter->transport_id.reset();
|
|
REQUIRE(validate(legacy_proxy_config).has_value());
|
|
const auto legacy_proxy_path = temporary.root() / "legacy-proxy-config.json";
|
|
REQUIRE(write_config_exclusive(legacy_proxy_path, legacy_proxy_config).has_value());
|
|
REQUIRE(load_config(legacy_proxy_path) == legacy_proxy_config);
|
|
auto generic_proxy_config = proxy_config;
|
|
generic_proxy_config.local_service_proxy->guest_channel_adapter.reset();
|
|
REQUIRE(validate(generic_proxy_config).has_value());
|
|
|
|
const auto local_service_audit =
|
|
proxy_config.receipt_journal.parent_path() / local_service_audit_filename;
|
|
const auto rejects_local_service_audit_collision = [&](config candidate) {
|
|
auto result = validate(candidate);
|
|
return !result.has_value() && result.error() == "configuration paths must be distinct" &&
|
|
result.error().find(temporary.root().string()) == std::string::npos;
|
|
};
|
|
auto audit_collision = proxy_config;
|
|
audit_collision.receipt_journal = local_service_audit;
|
|
REQUIRE(rejects_local_service_audit_collision(std::move(audit_collision)));
|
|
audit_collision = proxy_config;
|
|
audit_collision.audit_key = local_service_audit;
|
|
REQUIRE(rejects_local_service_audit_collision(std::move(audit_collision)));
|
|
audit_collision = proxy_config;
|
|
audit_collision.session_store = local_service_audit;
|
|
REQUIRE(rejects_local_service_audit_collision(std::move(audit_collision)));
|
|
|
|
// The derived control delivery journal (control-audit.jsonl next to the
|
|
// receipt journal) is always registered in the configured-path set: no
|
|
// configured path may alias it.
|
|
const auto control_audit_path =
|
|
proxy_config.receipt_journal.parent_path() / control_audit_filename;
|
|
auto control_collision = proxy_config;
|
|
control_collision.receipt_journal = control_audit_path;
|
|
REQUIRE(rejects_local_service_audit_collision(std::move(control_collision)));
|
|
control_collision = proxy_config;
|
|
control_collision.audit_key = control_audit_path;
|
|
REQUIRE(rejects_local_service_audit_collision(std::move(control_collision)));
|
|
control_collision = proxy_config;
|
|
control_collision.session_store = control_audit_path;
|
|
REQUIRE(rejects_local_service_audit_collision(std::move(control_collision)));
|
|
audit_collision = proxy_config;
|
|
audit_collision.local_service_proxy->endpoints.front().socket_path = local_service_audit;
|
|
auto endpoint_audit_collision = validate(audit_collision);
|
|
REQUIRE(!endpoint_audit_collision.has_value());
|
|
REQUIRE(endpoint_audit_collision.error().find(temporary.root().string()) == std::string::npos);
|
|
|
|
auto proxy_json = encode_config(proxy_config);
|
|
REQUIRE(proxy_json.has_value());
|
|
const auto endpoints_marker = proxy_json->find("\"endpoints\":[");
|
|
REQUIRE(endpoints_marker != std::string::npos);
|
|
const auto write_rejected_shape = [&](std::string name, std::string field) {
|
|
auto rejected = *proxy_json;
|
|
rejected.insert(endpoints_marker + std::string_view{"\"endpoints\":["}.size() + 1U, field);
|
|
const auto path = temporary.root() / ".config/glove" / name;
|
|
std::ofstream output{path, std::ios::binary};
|
|
output << rejected;
|
|
output.close();
|
|
if (!output.good() || ::chmod(path.c_str(), 0600) != 0) {
|
|
return false;
|
|
}
|
|
return !load_config(path).has_value();
|
|
};
|
|
REQUIRE(write_rejected_shape("proxy-tcp.json", "\"tcp_port\":443,"));
|
|
REQUIRE(
|
|
write_rejected_shape("proxy-guest-target.json", "\"guest_target\":\"/run/host.sock\",")
|
|
);
|
|
REQUIRE(write_rejected_shape("proxy-token.json", "\"token\":\"secret\","));
|
|
|
|
auto invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->io_timeout_ms = 0;
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->max_concurrency = 0;
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->max_concurrency = 17;
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->endpoints.clear();
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
const auto repeated_endpoint = invalid_proxy.local_service_proxy->endpoints.front();
|
|
invalid_proxy.local_service_proxy->endpoints.resize(17U, repeated_endpoint);
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->endpoints.front().alias = "../escape";
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->endpoints.back().alias = "sage-observe";
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->endpoints.back().socket_path =
|
|
invalid_proxy.local_service_proxy->endpoints.front().socket_path;
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->endpoints.front().socket_path = "relative.sock";
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->endpoints.front().runtime_ids.clear();
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->endpoints.front().runtime_ids = {"pi", "codex"};
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->endpoints.front().runtime_ids = {"pi", "pi"};
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
std::ranges::swap(
|
|
invalid_proxy.local_service_proxy->endpoints.front(),
|
|
invalid_proxy.local_service_proxy->endpoints.back()
|
|
);
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->endpoints.front().alias = "non-ascii-\xC3\xA9";
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->endpoints.front().alias = std::string{"nul\0alias", 9U};
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
auto nul_path = invalid_proxy.local_service_proxy->endpoints.front().socket_path.string();
|
|
nul_path.append("\0hidden", 7U);
|
|
invalid_proxy.local_service_proxy->endpoints.front().socket_path = nul_path;
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->guest_channel_adapter->adapter_id =
|
|
std::string{"sage\0hidden", 11U};
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->guest_channel_adapter->transport_id = "unknown-v1";
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->endpoints.front().socket_path =
|
|
*invalid_proxy.materialization_root / "service.sock";
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.local_service_proxy->endpoints.front().socket_path =
|
|
*invalid_proxy.materialization_root;
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
invalid_proxy = proxy_config;
|
|
invalid_proxy.remote_backend = remote_backend_config{};
|
|
REQUIRE(!validate(invalid_proxy).has_value());
|
|
|
|
const auto malformed_path = temporary.root() / ".config/glove/malformed-secret.json";
|
|
const std::string secret_path = "/operator/private/endpoint.sock";
|
|
{
|
|
std::ofstream malformed{malformed_path, std::ios::binary};
|
|
malformed << "{\"local_service_proxy\":{\"endpoint\":\"" << secret_path;
|
|
}
|
|
REQUIRE(::chmod(malformed_path.c_str(), 0600) == 0);
|
|
auto malformed = load_config(malformed_path);
|
|
REQUIRE(!malformed.has_value());
|
|
REQUIRE(malformed.error() == "configuration JSON is invalid");
|
|
REQUIRE(malformed.error().find(secret_path) == std::string::npos);
|
|
|
|
#if defined(__linux__)
|
|
temporary_directory preservation_temporary;
|
|
REQUIRE(!preservation_temporary.root().empty());
|
|
environment preservation_values{};
|
|
preservation_values.home = preservation_temporary.root().string();
|
|
const auto preservation_policy = preservation_temporary.root() / "session-policy.json";
|
|
setup_options preservation_options{};
|
|
preservation_options.session_policy = preservation_policy;
|
|
auto preservation_plan = plan_setup(preservation_options, preservation_values);
|
|
REQUIRE(preservation_plan.has_value());
|
|
preservation_plan->service.local_service_proxy = proxy_config.local_service_proxy;
|
|
preservation_plan->service.local_service_proxy->endpoints.front().socket_path =
|
|
preservation_temporary.root() / "services/sage.sock";
|
|
preservation_plan->service.local_service_proxy->endpoints.back().socket_path =
|
|
preservation_temporary.root() / "services/tooling.sock";
|
|
REQUIRE(std::filesystem::create_directories(preservation_plan->config_path.parent_path()));
|
|
REQUIRE(::chmod(preservation_plan->config_path.parent_path().c_str(), 0700) == 0);
|
|
REQUIRE(write_config_exclusive(preservation_plan->config_path, preservation_plan->service)
|
|
.has_value());
|
|
auto replanned = plan_setup(preservation_options, preservation_values);
|
|
REQUIRE(replanned.has_value());
|
|
REQUIRE(
|
|
replanned->service.local_service_proxy == preservation_plan->service.local_service_proxy
|
|
);
|
|
#endif
|
|
|
|
const auto apple_path = temporary.root() / ".config/glove/apple.json";
|
|
auto apple = managed_plan->service;
|
|
apple.apple_container = apple_container_config{
|
|
.cli = "/usr/local/bin/container",
|
|
.image = "ghcr.io/sage-protocol/glove-agent-runtime@sha256:" + std::string(64U, 'a'),
|
|
.image_digest = "sha256:" + std::string(64U, 'a'),
|
|
.harness_closure_digest = "sha256:" + std::string(64U, 'b'),
|
|
.sage_guest = sage_guest_config{
|
|
.binary_digest = "sha256:" + std::string(64U, 'c'),
|
|
.source_revision = std::string(40U, 'd'),
|
|
.policy_schema_version = 1,
|
|
.library_projection_schema = "sage_bundle_v1",
|
|
},
|
|
};
|
|
REQUIRE(write_config_exclusive(apple_path, apple).has_value());
|
|
REQUIRE(load_config(apple_path) == apple);
|
|
auto invalid_apple = apple;
|
|
invalid_apple.apple_container->image_digest = "sha256:latest";
|
|
REQUIRE(!validate(invalid_apple).has_value());
|
|
invalid_apple = apple;
|
|
invalid_apple.apple_container->image = "ghcr.io/sage-protocol/glove-agent-runtime:latest";
|
|
REQUIRE(!validate(invalid_apple).has_value());
|
|
invalid_apple = apple;
|
|
invalid_apple.apple_container->harness_closure_digest = "sha256:latest";
|
|
REQUIRE(!validate(invalid_apple).has_value());
|
|
invalid_apple = apple;
|
|
invalid_apple.apple_container->sage_guest->library_projection_schema = "latest";
|
|
REQUIRE(!validate(invalid_apple).has_value());
|
|
invalid_apple = apple;
|
|
invalid_apple.apple_container->harness_closure_digest.reset();
|
|
REQUIRE(!validate(invalid_apple).has_value());
|
|
invalid_apple = apple;
|
|
invalid_apple.materialization_root.reset();
|
|
REQUIRE(!validate(invalid_apple).has_value());
|
|
|
|
REQUIRE(::chmod(plan->config_path.c_str(), 0644) == 0);
|
|
REQUIRE(!load_config(plan->config_path).has_value());
|
|
|
|
temporary_directory cleanup_temporary;
|
|
REQUIRE(!cleanup_temporary.root().empty());
|
|
environment cleanup_values{};
|
|
cleanup_values.home = cleanup_temporary.root().string();
|
|
auto cleanup_setup = plan_setup(setup_options{}, cleanup_values);
|
|
REQUIRE(cleanup_setup.has_value());
|
|
REQUIRE(execute_setup(*cleanup_setup).has_value());
|
|
auto cleanup_preview = plan_setup_cleanup(cleanup_setup->config_path);
|
|
REQUIRE(cleanup_preview.has_value());
|
|
REQUIRE(!cleanup_preview->blocked());
|
|
auto cleanup_digest = setup_ledger_sha256(cleanup_preview->ledger);
|
|
REQUIRE(cleanup_digest.has_value());
|
|
REQUIRE(!execute_setup_cleanup(*cleanup_preview, std::string(64U, '0')).has_value());
|
|
REQUIRE(std::filesystem::exists(cleanup_setup->config_path));
|
|
REQUIRE(execute_setup_cleanup(*cleanup_preview, *cleanup_digest).has_value());
|
|
REQUIRE(!std::filesystem::exists(cleanup_setup->config_path));
|
|
REQUIRE(!std::filesystem::exists(cleanup_preview->ledger.ledger_path));
|
|
|
|
temporary_directory adoption_temporary;
|
|
REQUIRE(!adoption_temporary.root().empty());
|
|
environment adoption_values{};
|
|
adoption_values.home = adoption_temporary.root().string();
|
|
auto adoption_setup = plan_setup(setup_options{}, adoption_values);
|
|
REQUIRE(adoption_setup.has_value());
|
|
REQUIRE(execute_setup(*adoption_setup).has_value());
|
|
REQUIRE(std::filesystem::remove(setup_ledger_path(adoption_setup->service)));
|
|
auto adoption = plan_setup_adoption(adoption_setup->config_path);
|
|
REQUIRE(adoption.has_value());
|
|
REQUIRE(std::ranges::none_of(adoption->resources, [](const auto& resource) {
|
|
return resource.owned;
|
|
}));
|
|
REQUIRE(execute_setup_adoption(*adoption).has_value());
|
|
auto adopted = load_setup_ledger(adoption->ledger_path);
|
|
REQUIRE(adopted == adoption);
|
|
auto adopted_cleanup = plan_setup_cleanup(adoption_setup->config_path);
|
|
REQUIRE(adopted_cleanup.has_value());
|
|
REQUIRE(!adopted_cleanup->blocked());
|
|
REQUIRE(std::ranges::all_of(adopted_cleanup->items, [](const auto& item) {
|
|
return !item.resource.owned && !item.removable;
|
|
}));
|
|
|
|
temporary_directory changed_temporary;
|
|
REQUIRE(!changed_temporary.root().empty());
|
|
environment changed_values{};
|
|
changed_values.home = changed_temporary.root().string();
|
|
auto changed_setup = plan_setup(setup_options{}, changed_values);
|
|
REQUIRE(changed_setup.has_value());
|
|
REQUIRE(execute_setup(*changed_setup).has_value());
|
|
{
|
|
std::ofstream changed{changed_setup->config_path, std::ios::app};
|
|
changed << ' ';
|
|
}
|
|
auto changed_preview = plan_setup_cleanup(changed_setup->config_path);
|
|
REQUIRE(changed_preview.has_value());
|
|
REQUIRE(changed_preview->blocked());
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
auto main() -> int {
|
|
return run();
|
|
}
|