Files
logos-liblogos/tests/test_module_manager.cpp
Dario LipicarandClaude Opus 5 ca0d5bf1a1 feat: enforce a dependency's declared version range at load (#196)
* feat: enforce a dependency's declared version range at load

A module could declare `{"name": "lib", "version": "^2.0.0"}` and load happily
against an installed 1.0.0: the range was dropped before the registry ever saw
it, ModuleInfo::dependencies was a list of bare names, and DependencyResolver
only tested presence.

ModuleInfo::dependencies now holds LogosCore::ModuleDependency (name + range +
signer). moduleDependencies() still returns names, so all thirteen graph call
sites, the modules-info wire shape (logos_core.h documents an array of names)
and the modules_state snapshot record are untouched; moduleDependencyEntries()
is the constraint view. ModuleInfo::version caches the module's own stamp.

Both come from parseEmbeddedDeclaration, which decodes the metadata blob the
registry already caches at discovery rather than re-opening the plugin through
ModuleLib — one plugin read at discovery instead of two, and the gate's input
does not depend on the logos-module pin, so this builds and tests green against
master's lock with no stacked lock bump. Verified both ways: `nix build
.#checks.<system>.tests` passes 216/216 with the committed lock and again with
--override-input logos-module pointed at the constraint-carrying branch.

The decision itself is std-only logic in dependency_gate.h/.cpp, shaped after
protocol_gate.h, and loadModuleInternal applies it immediately after the
protocol gate with the same three-part refusal: spdlog::error, a loading->error
record on the observer carrying the reason, and `return false`. Ranges are
evaluated by liblgx's semver -- the implementation the packaging stack already
shares -- rather than a sixth copy.

It fails closed. lgx_semver_valid_range rejects a malformed range and the load
is refused rather than the constraint ignored; likewise a range declared
against a dependency whose version is unreadable. A dependency that declares no
range is not gated, which is every module in the fleet today.
DependencyGate.RangeDialectLiblgxImplements pins the dialect, including the two
refusals that surprise: npm hyphen ranges are unsupported and a prerelease
satisfies no caret range.

Posture, stated because it decides the version source: this is a COMPATIBILITY
gate like the protocol gate, not a security control. The range and the
installed version are both self-asserted plugin metadata, so the cheap,
pin-independent source (the embedded stamp) is the right one. A security-grade
check would want InstalledPackage::version from the signed manifest instead.

SIGNER: carried, not enforced, and deliberately so. lgpm verifies a package's
signature against the .lgx at install time and persists nothing: Package::
extractVariant copies only variants/<v>/ and assets/, so manifest.sig never
reaches the install directory, and InstalledPackage has no signer field. A
check written here would read a file that is never written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: cover the production dependency-range path with a real plugin

The gate's only production input is discovery: processModuleInternal reads
the plugin's embedded metadata blob and hands parseEmbeddedDeclaration's
constraints to ModuleInfo::dependencies. Nothing tested that assignment.
Every existing constraint test either injects entries through the
constraint-carrying registerDependencies overload (which has no production
caller) or exercises parseEmbeddedDeclaration in isolation.

Measured: replacing that one line with
    info.dependencies = toDependencyEntries(dependencyNames(declared.dependencies));
i.e. keeping dependency names and dropping their ranges -- reinstating the
bug this feature exists to fix, one layer downstream -- left all 218 tests
green.

It stayed uncovered because no metadata.json in the fleet uses the object
form, so no buildable module declares a range and there was no input to
discover. tests/fixtures/ supplies one: a bare QObject whose only content is
the Q_PLUGIN_METADATA blob declaring
    "dependencies": [ { "name": "range_probe_dep", "version": "^9.0.0" } ]
Discovery reads the blob with QPluginLoader::metaData() and never
instantiates, so it needs no interface and no behaviour.

RealDependencyRangeTest discovers it through logos_core_process_module and
asserts the range reaches the registry, refuses an out-of-range install, and
allows an in-range one. Under the control above all three fail (the gate
reports Unconstrained, and versionRange is empty); restored, all three pass.

CI ran ./result/bin/logos_core_tests directly, which supplies neither
TEST_PLUGIN nor the fixture, so nine real-plugin tests skipped and the run
still went green. It now builds the check, which supplies both and sets
LOGOS_REQUIRE_TEST_FIXTURES so an absent fixture is red rather than skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: a constraint we cannot read is not a constraint we can ignore

`"version": 2` — unquoted — read as ABSENT through a string accessor, so
the edge took the unconstrained arm and loaded against any installed
version. That is the fail-open this gate exists to remove, re-entered
through a typo, and it contradicted the gate's own header.

A `version` or `signer` that is present but not a string is now
malformedConstraint and refuses. Declaring a constraint we cannot read
is not the same as declaring none.

* refactor: one decode for a dependency's declared constraints

logos-module #24 has merged, so the object-form decode liblogos was
carrying alongside it is now a duplicate. Bump the pin (9812dc8 ->
0c6b921) and consume ModuleLib::ModuleMetadata instead: delete
parseEmbeddedDeclaration and EmbeddedDeclaration, and take name,
version, dependencies and the cached blob from a single
extractMetadata() -- discovery used to open the plugin twice
(getModuleName, then getRawMetadataJson) for two of those fields.

dependency_gate.h stays std-only, so LogosCore::ModuleDependency
remains the gate's own type and the mapping lives in module_registry.cpp,
the TU that already speaks Qt.

The decode's unit tests move with it (logos-module tests it). What
liblogos must keep proving is that a constraint declared in a REAL
plugin survives into the gate, so the malformed-constraint case gains a
fixture plugin of its own: a non-string `version` reads as ABSENT
through any string accessor, and dropping the flag in the mapping now
turns RealMalformedConstraintTest red instead of silently
unconstraining the edge.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 17:41:57 -03:00

1749 lines
68 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <gtest/gtest.h>
#include "logos_core.h"
#include "logos_core/dependency_gate.h"
#include "logos_core/module_state_observer.h"
#include "qt_test_adapter.h"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <set>
#include <string>
#include <vector>
namespace fs = std::filesystem;
static void clearModuleState() {
logos_core_terminate_all();
logos_core_clear();
}
// RAII temporary directory (uses mkdtemp, cleaned up on destruction)
struct TmpDir {
fs::path path;
TmpDir() {
std::string tmpl = (fs::temp_directory_path() / "logos_test_XXXXXX").string();
char* buf = new char[tmpl.size() + 1];
memcpy(buf, tmpl.c_str(), tmpl.size() + 1);
if (!mkdtemp(buf)) {
delete[] buf;
throw std::runtime_error("mkdtemp failed");
}
path = buf;
delete[] buf;
}
~TmpDir() {
std::error_code ec;
fs::remove_all(path, ec);
}
bool isValid() const { return fs::is_directory(path); }
// Returns path.string().c_str()-compatible value as std::string
std::string str() const { return path.string(); }
};
static void createFakeModule(const fs::path& parentDir,
const std::string& moduleName,
const std::string& mainFile,
const std::string& type = "core",
const std::vector<std::string>& dependencies = {}) {
fs::path moduleDir = parentDir / moduleName;
fs::create_directories(moduleDir);
nlohmann::json manifest;
manifest["name"] = moduleName;
manifest["version"] = "1.0.0";
manifest["type"] = type;
manifest["main"] = mainFile;
manifest["description"] = "Fake test module";
if (!dependencies.empty())
manifest["dependencies"] = dependencies;
std::ofstream mf(moduleDir / "manifest.json");
mf << manifest.dump();
mf.close();
std::ofstream bf(moduleDir / mainFile);
bf << "fake";
bf.close();
}
// Helpers to free null-terminated char** arrays returned by the C API.
static void freeStringArray(char** arr) {
if (!arr) return;
for (int i = 0; arr[i] != nullptr; ++i)
delete[] arr[i];
delete[] arr;
}
static int stringArrayLen(char** arr) {
if (!arr) return 0;
int n = 0;
while (arr[n]) ++n;
return n;
}
static std::set<std::string> stringArrayToSet(char** arr) {
std::set<std::string> s;
if (!arr) return s;
for (int i = 0; arr[i]; ++i)
s.insert(arr[i]);
return s;
}
class ModuleManagerTest : public ::testing::Test {
protected:
void SetUp() override {
clearModuleState();
}
void TearDown() override {
clearModuleState();
}
};
// =============================================================================
// Module Query Functions Tests
// =============================================================================
TEST_F(ModuleManagerTest, GetLoadedModules_ReturnsEmptyList) {
char** result = logos_core_get_loaded_modules();
ASSERT_NE(result, nullptr);
EXPECT_EQ(result[0], nullptr);
delete[] result;
}
TEST_F(ModuleManagerTest, GetKnownModules_ReturnsEmptyHash) {
char** result = logos_core_get_known_modules();
ASSERT_NE(result, nullptr);
EXPECT_EQ(result[0], nullptr);
delete[] result;
}
TEST_F(ModuleManagerTest, GetKnownModules_ReturnsCorrectHash) {
logos_core_register_module("module1", "/path/to/module1.dylib");
logos_core_register_module("module2", "/path/to/module2.dylib");
char** result = logos_core_get_known_modules();
ASSERT_NE(result, nullptr);
ASSERT_EQ(stringArrayLen(result), 2);
auto moduleSet = stringArrayToSet(result);
EXPECT_TRUE(moduleSet.count("module1"));
EXPECT_TRUE(moduleSet.count("module2"));
char* path1 = logos_core_get_module_path("module1");
char* path2 = logos_core_get_module_path("module2");
ASSERT_NE(path1, nullptr);
ASSERT_NE(path2, nullptr);
EXPECT_EQ(std::string(path1), "/path/to/module1.dylib");
EXPECT_EQ(std::string(path2), "/path/to/module2.dylib");
delete[] path1;
delete[] path2;
freeStringArray(result);
}
// logos_core_get_modules_info returns one rich JSON entry per known module:
// name, path, loaded flag, direct dependencies, direct dependents, and the
// embedded metadata. (Registered fake modules have no plugin file, so their
// metadata is null — the real-plugin metadata is covered separately.)
TEST_F(ModuleManagerTest, GetModulesInfo_ReturnsRichEntryPerModule) {
logos_core_register_module("module_a", "/path/to/module_a.dylib");
logos_core_register_module("module_b", "/path/to/module_b.dylib");
const char* depsA[] = {"module_b"};
logos_core_register_module_dependencies("module_a", depsA, 1);
logos_core_mark_module_loaded("module_b");
char* json = logos_core_get_modules_info();
ASSERT_NE(json, nullptr);
nlohmann::json info = nlohmann::json::parse(json, nullptr, /*allow_exceptions=*/false);
free(json);
ASSERT_TRUE(info.is_array());
ASSERT_EQ(info.size(), 2u);
auto find = [&](const std::string& n) -> nlohmann::json {
for (const auto& e : info)
if (e.value("name", std::string{}) == n) return e;
return nlohmann::json();
};
nlohmann::json a = find("module_a");
ASSERT_FALSE(a.is_null());
EXPECT_EQ(a.value("path", std::string{}), "/path/to/module_a.dylib");
EXPECT_FALSE(a.value("loaded", true));
// Not loaded ⇒ loaded_at is 0.
EXPECT_EQ(a.value("loaded_at", int64_t{-1}), 0);
ASSERT_TRUE(a["dependencies"].is_array());
ASSERT_EQ(a["dependencies"].size(), 1u);
EXPECT_EQ(a["dependencies"][0].get<std::string>(), "module_b");
EXPECT_TRUE(a["dependents"].is_array());
EXPECT_TRUE(a["dependents"].empty());
// metadata key is always present; null for a registered (un-processed) module.
ASSERT_TRUE(a.contains("metadata"));
EXPECT_TRUE(a["metadata"].is_null());
nlohmann::json b = find("module_b");
ASSERT_FALSE(b.is_null());
EXPECT_TRUE(b.value("loaded", false));
// Loaded ⇒ loaded_at is a real timestamp (stamped at markLoaded).
EXPECT_GT(b.value("loaded_at", int64_t{0}), 0);
// module_a depends on module_b ⇒ module_b lists module_a as a dependent.
ASSERT_TRUE(b["dependents"].is_array());
ASSERT_EQ(b["dependents"].size(), 1u);
EXPECT_EQ(b["dependents"][0].get<std::string>(), "module_a");
}
TEST_F(ModuleManagerTest, GetModulesInfo_EmptyWhenNoModules) {
char* json = logos_core_get_modules_info();
ASSERT_NE(json, nullptr);
nlohmann::json info = nlohmann::json::parse(json, nullptr, /*allow_exceptions=*/false);
free(json);
ASSERT_TRUE(info.is_array());
EXPECT_TRUE(info.empty());
}
TEST_F(ModuleManagerTest, IsModuleLoaded_ReturnsFalseForUnloaded) {
EXPECT_EQ(logos_core_is_module_loaded("nonexistent_module"), 0);
}
TEST_F(ModuleManagerTest, IsModuleKnown_ReturnsFalseForUnknown) {
EXPECT_EQ(logos_core_is_module_known("nonexistent_module"), 0);
}
TEST_F(ModuleManagerTest, IsModuleKnown_ReturnsTrueForKnown) {
logos_core_register_module("test_module", "/path/to/module");
EXPECT_EQ(logos_core_is_module_known("test_module"), 1);
}
// =============================================================================
// C String Array Functions Tests
// =============================================================================
TEST_F(ModuleManagerTest, GetLoadedModulesCStr_ReturnsNullTerminatedArrayWhenEmpty) {
char** result = logos_core_get_loaded_modules();
ASSERT_NE(result, nullptr);
EXPECT_EQ(result[0], nullptr);
delete[] result;
}
TEST_F(ModuleManagerTest, GetKnownModulesCStr_ReturnsNullTerminatedArrayWhenEmpty) {
char** result = logos_core_get_known_modules();
ASSERT_NE(result, nullptr);
EXPECT_EQ(result[0], nullptr);
delete[] result;
}
TEST_F(ModuleManagerTest, GetKnownModulesCStr_ReturnsCorrectArray) {
logos_core_register_module("module1", "/path/to/module1");
logos_core_register_module("module2", "/path/to/module2");
char** result = logos_core_get_known_modules();
ASSERT_NE(result, nullptr);
ASSERT_NE(result[0], nullptr);
ASSERT_NE(result[1], nullptr);
EXPECT_EQ(result[2], nullptr);
auto modules = stringArrayToSet(result);
EXPECT_TRUE(modules.count("module1"));
EXPECT_TRUE(modules.count("module2"));
freeStringArray(result);
}
// =============================================================================
// loadModule Error Cases Tests
// =============================================================================
TEST_F(ModuleManagerTest, LoadModule_ReturnsFalseForUnknownModule) {
int result = logos_core_load_module("nonexistent_module", false);
EXPECT_EQ(result, 0);
}
// =============================================================================
// unloadModule Error Cases Tests
// =============================================================================
TEST_F(ModuleManagerTest, UnloadModule_ReturnsFalseForNotLoaded) {
int result = logos_core_unload_module("nonexistent_module", false);
EXPECT_EQ(result, 0);
}
// =============================================================================
// resolveDependencies Function Tests
// =============================================================================
TEST_F(ModuleManagerTest, ResolveDependencies_ReturnsEmptyForEmptyInput) {
char** result = logos_core_resolve_dependencies(nullptr, 0);
ASSERT_NE(result, nullptr);
EXPECT_EQ(result[0], nullptr);
delete[] result;
}
TEST_F(ModuleManagerTest, ResolveDependencies_ReturnsEmptyForUnknownModule) {
const char* names[] = {"unknown_module"};
char** result = logos_core_resolve_dependencies(names, 1);
ASSERT_NE(result, nullptr);
EXPECT_EQ(result[0], nullptr);
delete[] result;
}
TEST_F(ModuleManagerTest, ResolveDependencies_ReturnsSingleModuleWithNoDeps) {
logos_core_register_module("module_a", "/path/to/module_a");
logos_core_register_module_dependencies("module_a", nullptr, 0);
const char* names[] = {"module_a"};
char** result = logos_core_resolve_dependencies(names, 1);
ASSERT_EQ(stringArrayLen(result), 1);
EXPECT_EQ(std::string(result[0]), "module_a");
freeStringArray(result);
}
TEST_F(ModuleManagerTest, ResolveDependencies_ReturnsCorrectOrder) {
logos_core_register_module("module_a", "/path/to/module_a");
logos_core_register_module("module_b", "/path/to/module_b");
const char* depsA[] = {"module_b"};
logos_core_register_module_dependencies("module_a", depsA, 1);
logos_core_register_module_dependencies("module_b", nullptr, 0);
const char* names[] = {"module_a"};
char** result = logos_core_resolve_dependencies(names, 1);
ASSERT_EQ(stringArrayLen(result), 2);
EXPECT_EQ(std::string(result[0]), "module_b");
EXPECT_EQ(std::string(result[1]), "module_a");
freeStringArray(result);
}
TEST_F(ModuleManagerTest, ResolveDependencies_HandlesTransitiveDeps) {
logos_core_register_module("module_a", "/path/to/module_a");
logos_core_register_module("module_b", "/path/to/module_b");
logos_core_register_module("module_c", "/path/to/module_c");
const char* depsA[] = {"module_b"};
const char* depsB[] = {"module_c"};
logos_core_register_module_dependencies("module_a", depsA, 1);
logos_core_register_module_dependencies("module_b", depsB, 1);
logos_core_register_module_dependencies("module_c", nullptr, 0);
const char* names[] = {"module_a"};
char** result = logos_core_resolve_dependencies(names, 1);
ASSERT_EQ(stringArrayLen(result), 3);
EXPECT_EQ(std::string(result[0]), "module_c");
EXPECT_EQ(std::string(result[1]), "module_b");
EXPECT_EQ(std::string(result[2]), "module_a");
freeStringArray(result);
}
// =============================================================================
// C API: logos_core_load_module with_dependencies=true Tests
// =============================================================================
TEST_F(ModuleManagerTest, LoadModuleWithDeps_AbortsForNull) {
EXPECT_DEATH(logos_core_load_module(nullptr, true), "");
}
TEST_F(ModuleManagerTest, LoadModuleWithDeps_ReturnsZeroForUnknown) {
int result = logos_core_load_module("unknown_module", true);
EXPECT_EQ(result, 0);
}
// =============================================================================
// Idempotent-load contract: "already loaded ⇒ success"
//
// logos_core_load_module is an "ensure loaded" guard, not a "load fresh"
// command. Pinning this in tests so the contract documented in
// logos_core.h doesn't quietly regress — basecamp's PluginLoader and
// logoscore-cli's load-module both rely on calling it against modules
// the loader may have already brought up at startup, and we don't want
// a future refactor to start returning 0 in that case (which previously
// caused UI-plugin loads to abort when a core dep was pre-loaded).
//
// We exercise this without a loader: register fake modules, mark them
// loaded via the registry adapter, then call the C entry point. The
// short-circuit at the top of ModuleManager::loadModuleInternal never
// reaches the descriptor / loader path, so no subprocess is spawned.
// =============================================================================
TEST_F(ModuleManagerTest, LoadModule_ReturnsTrueWhenAlreadyLoaded) {
logos_core_register_module("preloaded", "/fake/path");
logos_core_mark_module_loaded("preloaded");
ASSERT_EQ(logos_core_is_module_loaded("preloaded"), 1);
// First call: module is already loaded ⇒ no-op success.
EXPECT_EQ(logos_core_load_module("preloaded", false), 1)
<< "loading an already-loaded module must return 1 (no-op success)";
// Repeating the call must stay idempotent — still success, still loaded.
EXPECT_EQ(logos_core_load_module("preloaded", false), 1);
EXPECT_EQ(logos_core_is_module_loaded("preloaded"), 1);
}
TEST_F(ModuleManagerTest, LoadModuleWithDeps_ReturnsTrueWhenAllAlreadyLoaded) {
// Build a tiny dep graph: parent → child. Both pre-marked loaded.
logos_core_register_module("parent", "/fake/parent");
logos_core_register_module("child", "/fake/child");
const char* deps[] = {"child"};
logos_core_register_module_dependencies("parent", deps, 1);
logos_core_mark_module_loaded("child");
logos_core_mark_module_loaded("parent");
// with_dependencies=true walks the resolved order and calls
// loadModuleInternal for each; every step short-circuits on
// isLoaded() and returns true, so the overall call returns 1.
EXPECT_EQ(logos_core_load_module("parent", true), 1)
<< "with_dependencies=true must return 1 when the target and "
"all of its deps were already loaded before the call";
EXPECT_EQ(logos_core_is_module_loaded("parent"), 1);
EXPECT_EQ(logos_core_is_module_loaded("child"), 1);
}
// =============================================================================
// Dependency resolution failure: logos_core_load_module(name, true) must
// return 0 when the dependency graph cannot be fully resolved.
//
// The resolver silently drops unknown modules and detects cycles. Before
// this fix, loadModuleWithDependencies only checked whether the *target*
// appeared in the (possibly partial) resolved order — it didn't verify
// the resolution was clean. A module whose transitive dependency was
// unknown would load successfully, violating the contract in logos_core.h
// ("returns 0 when dependency resolution fails").
// =============================================================================
TEST_F(ModuleManagerTest, LoadModuleWithDeps_FailsWhenDirectDependencyUnknown) {
logos_core_register_module("parent", "/fake/parent");
const char* deps[] = {"unknown_dep"};
logos_core_register_module_dependencies("parent", deps, 1);
// "unknown_dep" is not registered → resolution has missing deps → fail.
EXPECT_EQ(logos_core_load_module("parent", true), 0)
<< "must return 0 when a direct dependency is unknown";
}
TEST_F(ModuleManagerTest, LoadModuleWithDeps_FailsWhenTransitiveDependencyUnknown) {
logos_core_register_module("top", "/fake/top");
logos_core_register_module("mid", "/fake/mid");
const char* depsTop[] = {"mid"};
const char* depsMid[] = {"bottom_unknown"};
logos_core_register_module_dependencies("top", depsTop, 1);
logos_core_register_module_dependencies("mid", depsMid, 1);
// "bottom_unknown" not registered → transitive resolution fails.
EXPECT_EQ(logos_core_load_module("top", true), 0)
<< "must return 0 when a transitive dependency is unknown";
}
TEST_F(ModuleManagerTest, LoadModuleWithDeps_FailsOnCircularDependency) {
logos_core_register_module("cyc_a", "/fake/cyc_a");
logos_core_register_module("cyc_b", "/fake/cyc_b");
const char* depsA[] = {"cyc_b"};
const char* depsB[] = {"cyc_a"};
logos_core_register_module_dependencies("cyc_a", depsA, 1);
logos_core_register_module_dependencies("cyc_b", depsB, 1);
// Cycle detected → must return 0.
EXPECT_EQ(logos_core_load_module("cyc_a", true), 0)
<< "must return 0 when a circular dependency is detected";
}
// =============================================================================
// Module Directory Management Tests
// =============================================================================
TEST_F(ModuleManagerTest, AddModulesDir_SetsFirstDirectory) {
logos_core_add_modules_dir("/tmp/test_modules");
ASSERT_EQ(logos_core_get_modules_dirs_count(), 1);
char* dir = logos_core_get_modules_dir_at(0);
ASSERT_NE(dir, nullptr);
EXPECT_EQ(std::string(dir), "/tmp/test_modules");
delete[] dir;
}
TEST_F(ModuleManagerTest, AddModulesDir_AppendsDirectory) {
logos_core_add_modules_dir("/tmp/dir1");
logos_core_add_modules_dir("/tmp/dir2");
logos_core_add_modules_dir("/tmp/dir3");
ASSERT_EQ(logos_core_get_modules_dirs_count(), 3);
char* d0 = logos_core_get_modules_dir_at(0);
char* d1 = logos_core_get_modules_dir_at(1);
char* d2 = logos_core_get_modules_dir_at(2);
ASSERT_NE(d0, nullptr);
ASSERT_NE(d1, nullptr);
ASSERT_NE(d2, nullptr);
EXPECT_EQ(std::string(d0), "/tmp/dir1");
EXPECT_EQ(std::string(d1), "/tmp/dir2");
EXPECT_EQ(std::string(d2), "/tmp/dir3");
delete[] d0;
delete[] d1;
delete[] d2;
}
TEST_F(ModuleManagerTest, GetModulesDirs_ReturnsEmptyAfterClear) {
logos_core_add_modules_dir("/tmp/dir1");
clearModuleState();
EXPECT_EQ(logos_core_get_modules_dirs_count(), 0);
}
// =============================================================================
// Discovery Tests — fake installed modules
// =============================================================================
TEST_F(ModuleManagerTest, DiscoverInstalledModules_DoesNotCrashWithEmptyDir) {
TmpDir tmpDir;
ASSERT_TRUE(tmpDir.isValid());
logos_core_add_modules_dir(tmpDir.str().c_str());
logos_core_refresh_modules();
char** known = logos_core_get_known_modules();
ASSERT_NE(known, nullptr);
EXPECT_EQ(known[0], nullptr);
delete[] known;
}
TEST_F(ModuleManagerTest, DiscoverInstalledModules_DoesNotCrashWithNonexistentDir) {
logos_core_add_modules_dir("/tmp/nonexistent_dir_12345");
logos_core_refresh_modules();
char** known = logos_core_get_known_modules();
ASSERT_NE(known, nullptr);
EXPECT_EQ(known[0], nullptr);
delete[] known;
}
TEST_F(ModuleManagerTest, DiscoverInstalledModules_FindsFakeModulesWithoutCrash) {
TmpDir tmpDir;
ASSERT_TRUE(tmpDir.isValid());
createFakeModule(tmpDir.path, "fake_module_a", "fake_module_a_plugin.so");
createFakeModule(tmpDir.path, "fake_module_b", "fake_module_b_plugin.so");
logos_core_add_modules_dir(tmpDir.str().c_str());
logos_core_refresh_modules();
char** known = logos_core_get_known_modules();
ASSERT_NE(known, nullptr);
EXPECT_EQ(known[0], nullptr);
delete[] known;
}
TEST_F(ModuleManagerTest, DiscoverInstalledModules_IgnoresModulesWithoutManifest) {
TmpDir tmpDir;
ASSERT_TRUE(tmpDir.isValid());
fs::path moduleDir = tmpDir.path / "no_manifest_module";
fs::create_directories(moduleDir);
std::ofstream bf(moduleDir / "plugin.so");
bf << "fake";
bf.close();
logos_core_add_modules_dir(tmpDir.str().c_str());
logos_core_refresh_modules();
char** known = logos_core_get_known_modules();
ASSERT_NE(known, nullptr);
EXPECT_EQ(known[0], nullptr);
delete[] known;
}
TEST_F(ModuleManagerTest, DiscoverInstalledModules_IgnoresUiTypeModules) {
TmpDir tmpDir;
ASSERT_TRUE(tmpDir.isValid());
createFakeModule(tmpDir.path, "ui_module", "ui_module_plugin.so", "ui");
logos_core_add_modules_dir(tmpDir.str().c_str());
logos_core_refresh_modules();
char** known = logos_core_get_known_modules();
ASSERT_NE(known, nullptr);
EXPECT_EQ(known[0], nullptr);
delete[] known;
}
TEST_F(ModuleManagerTest, DiscoverInstalledModules_MultipleDirectories) {
TmpDir tmpDir1;
TmpDir tmpDir2;
ASSERT_TRUE(tmpDir1.isValid());
ASSERT_TRUE(tmpDir2.isValid());
createFakeModule(tmpDir1.path, "module_in_dir1", "module_in_dir1_plugin.so");
createFakeModule(tmpDir2.path, "module_in_dir2", "module_in_dir2_plugin.so");
logos_core_add_modules_dir(tmpDir1.str().c_str());
logos_core_add_modules_dir(tmpDir2.str().c_str());
ASSERT_EQ(logos_core_get_modules_dirs_count(), 2);
logos_core_refresh_modules();
char** known = logos_core_get_known_modules();
ASSERT_NE(known, nullptr);
EXPECT_EQ(known[0], nullptr);
delete[] known;
}
TEST_F(ModuleManagerTest, DiscoverInstalledModules_InvalidManifestJson) {
TmpDir tmpDir;
ASSERT_TRUE(tmpDir.isValid());
fs::path moduleDir = tmpDir.path / "bad_manifest_module";
fs::create_directories(moduleDir);
std::ofstream mf(moduleDir / "manifest.json");
mf << "{ this is not valid json }}}";
mf.close();
logos_core_add_modules_dir(tmpDir.str().c_str());
logos_core_refresh_modules();
char** known = logos_core_get_known_modules();
ASSERT_NE(known, nullptr);
EXPECT_EQ(known[0], nullptr);
delete[] known;
}
// =============================================================================
// Loaded-flag preservation across re-registration
// =============================================================================
TEST_F(ModuleManagerTest, RegisterModule_PreservesLoadedFlagOnReregister) {
logos_core_register_module("test_module", "/path/v1");
logos_core_mark_module_loaded("test_module");
ASSERT_EQ(logos_core_is_module_loaded("test_module"), 1);
logos_core_register_module("test_module", "/path/v2");
EXPECT_EQ(logos_core_is_module_loaded("test_module"), 1)
<< "Re-registering a known module must preserve its loaded flag";
char* path = logos_core_get_module_path("test_module");
ASSERT_NE(path, nullptr);
EXPECT_EQ(std::string(path), "/path/v2");
delete[] path;
}
TEST_F(ModuleManagerTest, RegisterDependencies_PreservesLoadedFlag) {
logos_core_register_module("test_module", "/path/to/module");
logos_core_mark_module_loaded("test_module");
ASSERT_EQ(logos_core_is_module_loaded("test_module"), 1);
const char* deps[] = {"dep_a", "dep_b"};
logos_core_register_module_dependencies("test_module", deps, 2);
EXPECT_EQ(logos_core_is_module_loaded("test_module"), 1)
<< "Updating dependencies must not wipe the loaded flag";
EXPECT_EQ(logos_core_get_module_dependencies_count("test_module"), 2);
}
// =============================================================================
// Dependency version-range gate
//
// A module declares each dependency either as a bare name or as
// { name, version, signer }. The declared range is checked against the
// installed dependency's version before the loader is ever consulted; an
// unsatisfied or unevaluatable range refuses the load exactly the way the
// protocol gate refuses an incompatible major.
//
// The refusal cases run through the real logos_core_load_module and are read
// back off the lifecycle observer, which is where a refusal is reported. The
// permissive cases are asserted on the gate itself, because a load that gets
// PAST the gate goes on to spawn a module host — not something these tests
// can (or should) drive with a placeholder file on disk.
// =============================================================================
class DependencyGateLoadTest : public ::testing::Test {
protected:
void SetUp() override {
clearModuleState();
auto& o = logos::ModuleStateObserver::instance();
o.setSink({});
o.clearPending();
seen.clear();
o.setSink([this](const std::vector<logos::ModuleTransition>& batch) {
for (const auto& t : batch) seen.push_back(t);
});
}
void TearDown() override {
auto& o = logos::ModuleStateObserver::instance();
o.setSink({});
o.clearPending();
clearModuleState();
}
// `app` depends on `lib`, which is installed at `installedVersion`.
void plantGraph(const std::string& range, const std::string& installedVersion) {
logos_core_register_module("lib", "/path/to/lib");
ModuleManager::registry().registerModuleVersion("lib", installedVersion);
logos_core_register_module("app", (tmpDir.path / "app_plugin.so").string().c_str());
ModuleManager::registry().registerDependencies(
"app", std::vector<LogosCore::ModuleDependency>{{"lib", range, ""}});
std::ofstream f(tmpDir.path / "app_plugin.so");
f << "not a real plugin";
}
// The reason recorded on the loading -> error edge for `module`, or "".
std::string errorReason(const std::string& module) const {
for (const auto& t : seen) {
if (t.module == module && t.newState == logos::module_state::kError)
return t.reason.value_or(std::string{});
}
return {};
}
TmpDir tmpDir;
std::vector<logos::ModuleTransition> seen;
};
TEST_F(DependencyGateLoadTest, UnsatisfiedRange_RefusesLoad) {
plantGraph("^2.0.0", "1.0.0");
EXPECT_EQ(logos_core_load_module("app", /*with_dependencies=*/false), 0);
EXPECT_EQ(logos_core_is_module_loaded("app"), 0);
const std::string reason = errorReason("app");
EXPECT_NE(reason.find("lib"), std::string::npos) << reason;
EXPECT_NE(reason.find("^2.0.0"), std::string::npos) << reason;
EXPECT_NE(reason.find("1.0.0"), std::string::npos) << reason;
}
TEST_F(DependencyGateLoadTest, MalformedRange_RefusesLoad) {
plantGraph("^^2.0.0", "2.1.0");
EXPECT_EQ(logos_core_load_module("app", /*with_dependencies=*/false), 0);
const std::string reason = errorReason("app");
EXPECT_NE(reason.find("unparseable"), std::string::npos) << reason;
}
TEST_F(DependencyGateLoadTest, SatisfiedRange_GateAllows) {
plantGraph("^2.0.0", "2.1.0");
const auto gate = ModuleManager::dependencyGateFor("app");
EXPECT_EQ(gate.decision, LogosCore::DependencyGateDecision::Allow);
EXPECT_TRUE(gate.reason.empty());
}
// The string form of a dependency entry keeps behaving exactly as before: an
// edge, no constraint, nothing for the gate to refuse.
TEST_F(DependencyGateLoadTest, BareNameDependency_GateIsUnconstrained) {
logos_core_register_module("lib", "/path/to/lib");
logos_core_register_module("app", "/path/to/app");
const char* deps[] = {"lib"};
logos_core_register_module_dependencies("app", deps, 1);
const auto gate = ModuleManager::dependencyGateFor("app");
EXPECT_EQ(gate.decision, LogosCore::DependencyGateDecision::Unconstrained);
EXPECT_EQ(logos_core_get_module_dependencies_count("app"), 1);
}
// Carrying constraints must not widen the name-only shapes the graph and the
// modules-info wire format are built on.
TEST_F(DependencyGateLoadTest, ConstraintsDoNotChangeTheNameOnlyViews) {
plantGraph("^2.0.0", "2.1.0");
EXPECT_EQ(ModuleManager::registry().moduleDependencies("app"),
(std::vector<std::string>{"lib"}));
EXPECT_EQ(ModuleManager::registry().moduleDependents("lib"),
(std::vector<std::string>{"app"}));
char* json = logos_core_get_modules_info();
ASSERT_NE(json, nullptr);
nlohmann::json info = nlohmann::json::parse(json, nullptr, /*allow_exceptions=*/false);
free(json);
ASSERT_TRUE(info.is_array());
for (const auto& entry : info) {
if (entry.value("name", std::string{}) != "app") continue;
ASSERT_TRUE(entry["dependencies"].is_array());
EXPECT_EQ(entry["dependencies"], nlohmann::json::array({"lib"}));
}
}
// =============================================================================
// End-to-end regression tests using a real Qt module.
// =============================================================================
class RealModuleRegistryTest : public ::testing::Test {
protected:
std::string modulePath;
void SetUp() override {
clearModuleState();
const char* envPlugin = std::getenv("TEST_PLUGIN");
if (envPlugin && std::strlen(envPlugin) > 0 &&
fs::exists(envPlugin)) {
modulePath = envPlugin;
return;
}
GTEST_SKIP() << "No real test module available. "
<< "Set TEST_PLUGIN env var to a built Qt plugin (.so/.dylib).";
}
void TearDown() override {
clearModuleState();
}
};
TEST_F(RealModuleRegistryTest, ProcessModule_RegistersRealModule) {
char* name = logos_core_process_module(modulePath.c_str());
ASSERT_NE(name, nullptr) << "process_module failed for " << modulePath;
EXPECT_NE(std::string(name), "");
EXPECT_EQ(logos_core_is_module_known(name), 1);
EXPECT_EQ(logos_core_is_module_loaded(name), 0);
delete[] name;
}
// For a real plugin, get_modules_info must carry the embedded metadata parsed
// straight from the binary (via ModuleLib::LogosModule) — name + version.
TEST_F(RealModuleRegistryTest, GetModulesInfo_PopulatesEmbeddedMetadata) {
char* name = logos_core_process_module(modulePath.c_str());
ASSERT_NE(name, nullptr) << "process_module failed for " << modulePath;
std::string moduleName(name);
delete[] name;
char* json = logos_core_get_modules_info();
ASSERT_NE(json, nullptr);
nlohmann::json info = nlohmann::json::parse(json, nullptr, /*allow_exceptions=*/false);
free(json);
ASSERT_TRUE(info.is_array());
nlohmann::json entry;
for (const auto& e : info)
if (e.value("name", std::string{}) == moduleName) { entry = e; break; }
ASSERT_FALSE(entry.is_null()) << "processed module absent from modules-info";
EXPECT_FALSE(entry.value("path", std::string{}).empty());
ASSERT_TRUE(entry["metadata"].is_object())
<< "real plugin must yield a non-null metadata object";
EXPECT_EQ(entry["metadata"].value("name", std::string{}), moduleName);
EXPECT_FALSE(entry["metadata"].value("version", std::string{}).empty())
<< "built test modules declare a version in metadata.json";
}
// The version a dependent's range is evaluated against is the dependency's own
// embedded stamp, cached at discovery without loading the plugin. Closes the
// loop from a real binary's metadata to a gate decision.
TEST_F(RealModuleRegistryTest, EmbeddedVersionFeedsTheDependencyGate) {
char* name = logos_core_process_module(modulePath.c_str());
ASSERT_NE(name, nullptr) << "process_module failed for " << modulePath;
const std::string dep(name);
delete[] name;
const std::string version = ModuleManager::registry().moduleVersion(dep);
ASSERT_FALSE(version.empty()) << "built test modules declare a version";
logos_core_register_module("dependent", "/path/to/dependent");
ModuleManager::registry().registerDependencies(
"dependent", std::vector<LogosCore::ModuleDependency>{{dep, "=" + version, ""}});
EXPECT_EQ(ModuleManager::dependencyGateFor("dependent").decision,
LogosCore::DependencyGateDecision::Allow);
ModuleManager::registry().registerDependencies(
"dependent", std::vector<LogosCore::ModuleDependency>{{dep, "=" + version + "-nope.1", ""}});
EXPECT_EQ(ModuleManager::dependencyGateFor("dependent").decision,
LogosCore::DependencyGateDecision::Refuse);
}
// =============================================================================
// The production dependency-range path, end to end from a real binary.
//
// This is the ONLY coverage of it. Every other constraint test injects entries
// through the constraint-carrying registerDependencies overload, which has no
// production caller. The real path is
// processModuleInternal -> ModuleLib::extractMetadata
// -> toGateDependencies -> ModuleInfo::dependencies
// -> dependencyGateFor
// and it was measurably untested: dropping the constraints where the registry
// assigns them (keeping only the names) left all 218 tests green, i.e. it
// silently reinstated the bug the gate exists to fix.
//
// It stayed uncovered because no metadata.json in the fleet uses the object
// form, so no shipped module declares a range. tests/fixtures supplies the
// missing input: a real Qt plugin whose embedded blob declares
// "dependencies": [ { "name": "range_probe_dep", "version": "^9.0.0" } ]
// =============================================================================
namespace {
// Mirrors tests/fixtures/dep_range_fixture.json. A drift here shows up as an
// assertion, not as a test that quietly stops covering anything.
constexpr const char* kFixtureModule = "dep_range_fixture";
constexpr const char* kFixtureDep = "range_probe_dep";
constexpr const char* kFixtureRange = "^9.0.0";
} // namespace
class RealDependencyRangeTest : public ::testing::Test {
protected:
std::string fixturePath;
void SetUp() override {
clearModuleState();
const char* env = std::getenv("TEST_PLUGIN_DEP_RANGE");
if (env && std::strlen(env) > 0 && fs::exists(env)) {
fixturePath = env;
return;
}
// A skipped test renders as a pass, and a pass here would mean the
// production path is unguarded again. CI sets LOGOS_REQUIRE_TEST_FIXTURES
// so an absent fixture is a red run; a local dev without one still skips.
const char* strict = std::getenv("LOGOS_REQUIRE_TEST_FIXTURES");
if (strict && std::strcmp(strict, "0") != 0) {
FAIL() << "TEST_PLUGIN_DEP_RANGE is unset or missing (got '"
<< (env ? env : "") << "') while LOGOS_REQUIRE_TEST_FIXTURES is "
<< "set: the dependency-range fixture must be supplied, not skipped.";
}
GTEST_SKIP() << "No dependency-range fixture available. Set "
<< "TEST_PLUGIN_DEP_RANGE to tests/fixtures' built plugin.";
}
void TearDown() override {
clearModuleState();
}
// Discovers the fixture through the real registry entry point and returns
// the name it registered under.
std::string discover() {
char* name = logos_core_process_module(fixturePath.c_str());
EXPECT_NE(name, nullptr) << "process_module failed for " << fixturePath;
if (!name) return {};
std::string registered(name);
delete[] name;
return registered;
}
};
// The range survives the trip from the plugin's embedded metadata into the
// registry's dependency entries. Fails the moment the registry keeps names and
// drops constraints.
TEST_F(RealDependencyRangeTest, EmbeddedRangeReachesTheRegistry) {
ASSERT_EQ(discover(), kFixtureModule);
const auto entries =
ModuleManager::registry().moduleDependencyEntries(kFixtureModule);
ASSERT_EQ(entries.size(), 1u) << "fixture declares exactly one dependency";
EXPECT_EQ(entries[0].name, kFixtureDep);
EXPECT_EQ(entries[0].versionRange, kFixtureRange)
<< "the range declared in the plugin's embedded metadata was dropped "
<< "before it reached ModuleInfo::dependencies";
// The name-only view the graph and the modules-info wire shape are built on
// is unchanged by carrying the constraint.
EXPECT_EQ(ModuleManager::registry().moduleDependencies(kFixtureModule),
(std::vector<std::string>{kFixtureDep}));
}
// ...and it decides. An installed dependency outside the declared range must be
// refused; the refusal names the range, so a gate that saw no constraint cannot
// produce this message.
TEST_F(RealDependencyRangeTest, EmbeddedRangeRefusesAnOutOfRangeDependency) {
ASSERT_EQ(discover(), kFixtureModule);
logos_core_register_module(kFixtureDep, "/path/to/range_probe_dep");
ModuleManager::registry().registerModuleVersion(kFixtureDep, "1.2.3");
const auto gate = ModuleManager::dependencyGateFor(kFixtureModule);
EXPECT_EQ(gate.decision, LogosCore::DependencyGateDecision::Refuse)
<< "installed 1.2.3 does not satisfy " << kFixtureRange
<< ", so the gate must refuse; decision was "
<< static_cast<int>(gate.decision);
EXPECT_EQ(gate.dependency, kFixtureDep);
EXPECT_EQ(gate.range, kFixtureRange);
EXPECT_EQ(gate.installedVersion, "1.2.3");
EXPECT_NE(gate.reason.find(kFixtureRange), std::string::npos) << gate.reason;
}
// Positive control on the same fixture: satisfy the range and the gate reports
// Allow, not the Unconstrained a dropped constraint would yield.
TEST_F(RealDependencyRangeTest, EmbeddedRangeAllowsAnInRangeDependency) {
ASSERT_EQ(discover(), kFixtureModule);
logos_core_register_module(kFixtureDep, "/path/to/range_probe_dep");
ModuleManager::registry().registerModuleVersion(kFixtureDep, "9.4.1");
EXPECT_EQ(ModuleManager::dependencyGateFor(kFixtureModule).decision,
LogosCore::DependencyGateDecision::Allow)
<< "a satisfied range must read as Allow; Unconstrained here means the "
<< "gate never saw the constraint the plugin declared";
}
// =============================================================================
// The same production path, for a constraint that cannot be READ.
//
// `"version": 2` comes back empty from any string accessor, so a decoder that
// only asks for the string unconstrains the edge and the gate waves it through
// — the precise fail-open ModuleDependency::malformedConstraint exists to
// refuse. The decode itself now lives in logos-module, so this fixture is what
// proves the flag still survives the mapping at liblogos' boundary; break
// toGateDependencies and EmbeddedMalformedConstraintRefuses goes red.
//
// The fixture also carries a bare-name entry alongside the malformed one, so
// the mixed-form array is covered here rather than in a decode unit test.
// =============================================================================
namespace {
// Mirrors tests/fixtures/dep_malformed_fixture.json.
constexpr const char* kMalformedModule = "dep_malformed_fixture";
constexpr const char* kMalformedPlainDep = "plain_probe_dep";
constexpr const char* kMalformedBadDep = "malformed_probe_dep";
} // namespace
class RealMalformedConstraintTest : public ::testing::Test {
protected:
std::string fixturePath;
void SetUp() override {
clearModuleState();
const char* env = std::getenv("TEST_PLUGIN_DEP_MALFORMED");
if (env && std::strlen(env) > 0 && fs::exists(env)) {
fixturePath = env;
return;
}
// A skip renders as a pass, and a pass here would mean the fail-open is
// unguarded again. See RealDependencyRangeTest for the same contract.
const char* strict = std::getenv("LOGOS_REQUIRE_TEST_FIXTURES");
if (strict && std::strcmp(strict, "0") != 0) {
FAIL() << "TEST_PLUGIN_DEP_MALFORMED is unset or missing (got '"
<< (env ? env : "") << "') while LOGOS_REQUIRE_TEST_FIXTURES is "
<< "set: the malformed-constraint fixture must be supplied, "
<< "not skipped.";
}
GTEST_SKIP() << "No malformed-constraint fixture available. Set "
<< "TEST_PLUGIN_DEP_MALFORMED to tests/fixtures' built plugin.";
}
void TearDown() override { clearModuleState(); }
std::string discover() {
char* name = logos_core_process_module(fixturePath.c_str());
EXPECT_NE(name, nullptr) << "process_module failed for " << fixturePath;
if (!name) return {};
std::string registered(name);
delete[] name;
return registered;
}
};
// The flag survives the trip from the plugin's embedded metadata into the
// registry's entries, and the bare-name sibling is left unconstrained.
TEST_F(RealMalformedConstraintTest, EmbeddedMalformedConstraintReachesTheRegistry) {
ASSERT_EQ(discover(), kMalformedModule);
const auto entries =
ModuleManager::registry().moduleDependencyEntries(kMalformedModule);
ASSERT_EQ(entries.size(), 2u) << "fixture declares a bare and an object entry";
EXPECT_EQ(entries[0].name, kMalformedPlainDep);
EXPECT_FALSE(entries[0].malformedConstraint);
EXPECT_TRUE(entries[0].versionRange.empty());
EXPECT_EQ(entries[1].name, kMalformedBadDep);
EXPECT_TRUE(entries[1].malformedConstraint)
<< "a non-string `version` reached the registry as an UNCONSTRAINED "
<< "edge — the fail-open this flag exists to refuse";
EXPECT_TRUE(entries[1].versionRange.empty());
}
// ...and it refuses. Both dependencies are installed and readable, so nothing
// but the unreadable constraint can produce this decision.
TEST_F(RealMalformedConstraintTest, EmbeddedMalformedConstraintRefuses) {
ASSERT_EQ(discover(), kMalformedModule);
logos_core_register_module(kMalformedPlainDep, "/path/to/plain_probe_dep");
ModuleManager::registry().registerModuleVersion(kMalformedPlainDep, "1.0.0");
logos_core_register_module(kMalformedBadDep, "/path/to/malformed_probe_dep");
ModuleManager::registry().registerModuleVersion(kMalformedBadDep, "1.0.0");
const auto gate = ModuleManager::dependencyGateFor(kMalformedModule);
EXPECT_EQ(gate.decision, LogosCore::DependencyGateDecision::Refuse)
<< "an unreadable constraint must refuse, not unconstrain the edge; "
<< "decision was " << static_cast<int>(gate.decision);
EXPECT_EQ(gate.dependency, kMalformedBadDep);
EXPECT_NE(gate.reason.find("not a string"), std::string::npos) << gate.reason;
}
// =============================================================================
// Security regression: privileged-name impersonation during discovery (F-022).
//
// Module identity used to be taken from the name embedded in the plugin's own
// Qt metadata, ignoring the trusted package name the package manager scanned.
// That let a package installed under an innocuous name ship a binary whose
// embedded metadata claims a privileged name (e.g. "capability_module"), and
// the registry would key the module under that privileged name — wiring the
// attacker's plugin into the impersonated module's token/trust relationships.
//
// The discovery path (logos_core_refresh_modules → discoverInstalledModules)
// must bind identity to the *trusted package name* (InstalledPackage::name)
// and refuse a plugin whose embedded name disagrees.
//
// These tests use the real TEST_PLUGIN as the impersonating payload: we first
// read its real embedded name via the raw process-module path, then plant a
// package whose manifest name differs from it, and assert the embedded name
// never leaks into the registry.
// =============================================================================
class ImpersonationRegistryTest : public ::testing::Test {
protected:
std::string modulePath; // real TEST_PLUGIN on disk
std::string embeddedName; // the name baked into TEST_PLUGIN's metadata
void SetUp() override {
clearModuleState();
const char* envPlugin = std::getenv("TEST_PLUGIN");
if (!envPlugin || std::strlen(envPlugin) == 0 || !fs::exists(envPlugin)) {
GTEST_SKIP() << "No real test module available. "
<< "Set TEST_PLUGIN env var to a built Qt plugin (.so/.dylib).";
}
modulePath = envPlugin;
// Discover the plugin's self-asserted embedded name via the raw
// process-module path (which intentionally trusts the embedded name).
// This is the name an attacker's binary would carry to impersonate.
char* name = logos_core_process_module(modulePath.c_str());
ASSERT_NE(name, nullptr) << "process_module failed for " << modulePath;
embeddedName = name;
delete[] name;
ASSERT_FALSE(embeddedName.empty());
// Wipe the scratch registration + modules dirs so each test below
// starts from a clean registry.
clearModuleState();
}
void TearDown() override {
clearModuleState();
}
// Plant a package directory named `packageName` whose manifest declares
// name=packageName but whose main binary is a byte copy of the real
// TEST_PLUGIN (embedding `embeddedName`).
void plantPackage(const fs::path& parentDir, const std::string& packageName) {
const std::string mainFile = packageName + "_plugin.so";
createFakeModule(parentDir, packageName, mainFile); // manifest + placeholder
std::error_code ec;
fs::copy_file(modulePath, parentDir / packageName / mainFile,
fs::copy_options::overwrite_existing, ec);
ASSERT_FALSE(ec) << "failed to copy real plugin into package dir: " << ec.message();
}
};
// The core repro: an "innocent_helper" package carrying a binary that claims
// the privileged embedded name must NOT register under that privileged name,
// and must not silently bind it either. Before the fix the registry keyed the
// module under `embeddedName`, so is_module_known(embeddedName) was 1.
TEST_F(ImpersonationRegistryTest, Discovery_RefusesPrivilegedNameImpersonation) {
// Only meaningful when the trusted package name differs from the embedded
// one (true for the capability_module fixture: package "innocent_helper"
// vs embedded "capability_module").
const std::string packageName = "innocent_helper";
ASSERT_NE(packageName, embeddedName);
TmpDir tmpDir;
ASSERT_TRUE(tmpDir.isValid());
plantPackage(tmpDir.path, packageName);
logos_core_add_modules_dir(tmpDir.str().c_str());
logos_core_refresh_modules();
// The impersonated privileged identity must never enter the registry.
EXPECT_EQ(logos_core_is_module_known(embeddedName.c_str()), 0)
<< "a package must not be able to claim the embedded name '"
<< embeddedName << "' it does not legitimately own";
// And the lying package is refused outright (its binary's identity does
// not match its package name), so the innocuous name isn't bound either.
EXPECT_EQ(logos_core_is_module_known(packageName.c_str()), 0)
<< "a package whose binary impersonates another module must be refused";
char** known = logos_core_get_known_modules();
ASSERT_NE(known, nullptr);
EXPECT_EQ(known[0], nullptr) << "no module should be registered from a lying package";
freeStringArray(known);
}
// Positive control: an honest package whose manifest name matches the binary's
// embedded name still registers normally. The fix must not break legitimate
// discovery of (even reserved-named) modules installed under their true name.
TEST_F(ImpersonationRegistryTest, Discovery_HonestPackageRegistersUnderItsName) {
TmpDir tmpDir;
ASSERT_TRUE(tmpDir.isValid());
plantPackage(tmpDir.path, embeddedName); // manifest name == embedded name
logos_core_add_modules_dir(tmpDir.str().c_str());
logos_core_refresh_modules();
EXPECT_EQ(logos_core_is_module_known(embeddedName.c_str()), 1)
<< "an honest package (manifest name == embedded name) must register";
char* path = logos_core_get_module_path(embeddedName.c_str());
ASSERT_NE(path, nullptr);
EXPECT_NE(std::string(path), "");
delete[] path;
}
// =============================================================================
// Cascading unload: logos_core_unload_module(name, true)
//
// The cascade is exercised without real Qt modules. We:
// 1. Set up fake manifests on disk (PackageManagerLib scan sees the
// dependency edges).
// 2. Register the same modules directly in ModuleRegistry so it believes
// they exist (the fake .so files are not loadable Qt plugins, so
// refresh_modules alone wouldn't populate the registry).
// 3. Register placeholder "processes" + mark loaded so hasProcess() returns
// true — `terminateProcess` on a placeholder is a no-op but still
// removes the entry cleanly.
// =============================================================================
class CascadeUnloadTest : public ::testing::Test {
protected:
TmpDir tmpDir;
void SetUp() override {
clearModuleState();
logos_core_clear_processes();
}
void TearDown() override {
clearModuleState();
logos_core_clear_processes();
}
// Registers a module in both ModuleRegistry and as a loaded fake process.
void setupLoaded(const std::string& name,
const std::vector<std::string>& deps = {}) {
std::string path = (tmpDir.path / name / (name + "_plugin.so")).string();
logos_core_register_module(name.c_str(), path.c_str());
std::vector<const char*> depPtrs;
depPtrs.reserve(deps.size());
for (const auto& d : deps) depPtrs.push_back(d.c_str());
logos_core_register_module_dependencies(
name.c_str(),
depPtrs.empty() ? nullptr : depPtrs.data(),
static_cast<int>(depPtrs.size()));
logos_core_register_process(name.c_str());
logos_core_mark_module_loaded(name.c_str());
}
void writeManifestsAndScan(
const std::vector<std::tuple<std::string, std::vector<std::string>>>& modules)
{
for (const auto& [name, deps] : modules) {
createFakeModule(tmpDir.path, name, name + "_plugin.so", "core", deps);
}
logos_core_add_modules_dir(tmpDir.str().c_str());
logos_core_refresh_modules();
}
};
TEST_F(CascadeUnloadTest, UnloadWithDependents_ReturnsZeroWhenTargetNotLoaded) {
// Module is known but not loaded.
logos_core_register_module("foo", "/foo");
int result = logos_core_unload_module("foo", true);
EXPECT_EQ(result, 0);
}
TEST_F(CascadeUnloadTest, UnloadWithDependents_NoDependents_UnloadsTargetOnly) {
// Single loaded module with no dependents on disk → cascade is just the
// target.
writeManifestsAndScan({ {"solo", {}} });
setupLoaded("solo");
ASSERT_EQ(logos_core_is_module_loaded("solo"), 1);
int result = logos_core_unload_module("solo", true);
EXPECT_EQ(result, 1);
EXPECT_EQ(logos_core_is_module_loaded("solo"), 0);
EXPECT_EQ(logos_core_has_process("solo"), 0);
}
TEST_F(CascadeUnloadTest, UnloadWithDependents_RecursiveDependentsLeavesFirst) {
// Graph: a -> b -> c (a depends on b, b depends on c).
// Unloading c should also bring down b and a, in the order a, b, c.
writeManifestsAndScan({
{"c", {}},
{"b", {"c"}},
{"a", {"b"}},
});
setupLoaded("c", {});
setupLoaded("b", {"c"});
setupLoaded("a", {"b"});
ASSERT_EQ(logos_core_is_module_loaded("a"), 1);
ASSERT_EQ(logos_core_is_module_loaded("b"), 1);
ASSERT_EQ(logos_core_is_module_loaded("c"), 1);
int result = logos_core_unload_module("c", true);
EXPECT_EQ(result, 1);
EXPECT_EQ(logos_core_is_module_loaded("a"), 0);
EXPECT_EQ(logos_core_is_module_loaded("b"), 0);
EXPECT_EQ(logos_core_is_module_loaded("c"), 0);
EXPECT_EQ(logos_core_has_process("a"), 0);
EXPECT_EQ(logos_core_has_process("b"), 0);
EXPECT_EQ(logos_core_has_process("c"), 0);
}
TEST_F(CascadeUnloadTest, UnloadWithDependents_UnloadedDependentsIgnored) {
// b depends on c. Only c is loaded; b is known but not loaded. Cascade
// should only touch c. b stays unloaded (not "failed to unload").
writeManifestsAndScan({
{"c", {}},
{"b", {"c"}},
});
setupLoaded("c", {});
// Register b in registry but don't mark it loaded.
logos_core_register_module("b", "/b");
const char* depsB[] = {"c"};
logos_core_register_module_dependencies("b", depsB, 1);
int result = logos_core_unload_module("c", true);
EXPECT_EQ(result, 1);
EXPECT_EQ(logos_core_is_module_loaded("c"), 0);
EXPECT_EQ(logos_core_is_module_loaded("b"), 0);
}
TEST_F(CascadeUnloadTest, UnloadWithDependents_DiamondDependents) {
// Diamond: a -> b -> d ; a -> c -> d. Unloading d should bring down
// a, b, c in some valid order (a before b and c; b and c before d).
writeManifestsAndScan({
{"d", {}},
{"b", {"d"}},
{"c", {"d"}},
{"a", {"b", "c"}},
});
setupLoaded("d", {});
setupLoaded("b", {"d"});
setupLoaded("c", {"d"});
setupLoaded("a", {"b", "c"});
int result = logos_core_unload_module("d", true);
EXPECT_EQ(result, 1);
EXPECT_EQ(logos_core_is_module_loaded("a"), 0);
EXPECT_EQ(logos_core_is_module_loaded("b"), 0);
EXPECT_EQ(logos_core_is_module_loaded("c"), 0);
EXPECT_EQ(logos_core_is_module_loaded("d"), 0);
}
TEST_F(CascadeUnloadTest, UnloadWithDependents_AbortsForNull) {
EXPECT_DEATH(logos_core_unload_module(nullptr, true), "");
}
TEST_F(RealModuleRegistryTest, ProcessModule_PreservesLoadedFlagOnReprocess) {
char* name1 = logos_core_process_module(modulePath.c_str());
ASSERT_NE(name1, nullptr) << "process_module failed for " << modulePath;
std::string modName = name1;
delete[] name1;
ASSERT_EQ(logos_core_is_module_known(modName.c_str()), 1);
logos_core_mark_module_loaded(modName.c_str());
ASSERT_EQ(logos_core_is_module_loaded(modName.c_str()), 1);
char* name2 = logos_core_process_module(modulePath.c_str());
ASSERT_NE(name2, nullptr);
EXPECT_EQ(std::string(name2), modName);
delete[] name2;
EXPECT_EQ(logos_core_is_module_loaded(modName.c_str()), 1)
<< "Re-processing a loaded module must preserve its loaded flag";
// Verify it still appears in the loaded list
char** loaded = logos_core_get_loaded_modules();
auto loadedSet = stringArrayToSet(loaded);
freeStringArray(loaded);
EXPECT_TRUE(loadedSet.count(modName))
<< "get_loaded_modules() must still report the module as loaded";
}
// =============================================================================
// Dependency graph queries:
// logos_core_get_module_dependencies(name, recursive)
// logos_core_get_module_dependents(name, recursive)
//
// These read from the in-process registry. We populate it with
// logos_core_register_module + logos_core_register_module_dependencies
// (which in turn trigger recomputeDependentsLocked), then check both the
// direct and recursive traversals against known-shaped graphs.
// =============================================================================
class DependencyQueryTest : public ::testing::Test {
protected:
void SetUp() override {
clearModuleState();
}
void TearDown() override {
clearModuleState();
}
// Register a module with a (possibly empty) direct dependency list. Path
// value isn't exercised by the queries — anything non-empty is fine.
void reg(const std::string& name,
const std::vector<std::string>& deps = {}) {
logos_core_register_module(name.c_str(), ("/" + name).c_str());
std::vector<const char*> depPtrs;
depPtrs.reserve(deps.size());
for (const auto& d : deps) depPtrs.push_back(d.c_str());
logos_core_register_module_dependencies(
name.c_str(),
depPtrs.empty() ? nullptr : depPtrs.data(),
static_cast<int>(depPtrs.size()));
}
};
TEST_F(DependencyQueryTest, GetModuleDependencies_UnknownName_ReturnsEmpty) {
char** deps = logos_core_get_module_dependencies("ghost", false);
EXPECT_EQ(stringArrayLen(deps), 0);
freeStringArray(deps);
deps = logos_core_get_module_dependencies("ghost", true);
EXPECT_EQ(stringArrayLen(deps), 0);
freeStringArray(deps);
}
TEST_F(DependencyQueryTest, GetModuleDependents_UnknownName_ReturnsEmpty) {
char** d = logos_core_get_module_dependents("ghost", false);
EXPECT_EQ(stringArrayLen(d), 0);
freeStringArray(d);
d = logos_core_get_module_dependents("ghost", true);
EXPECT_EQ(stringArrayLen(d), 0);
freeStringArray(d);
}
TEST_F(DependencyQueryTest, GetModuleDependencies_NoDeps_ReturnsEmpty) {
reg("leaf");
char** deps = logos_core_get_module_dependencies("leaf", false);
EXPECT_EQ(stringArrayLen(deps), 0);
freeStringArray(deps);
deps = logos_core_get_module_dependencies("leaf", true);
EXPECT_EQ(stringArrayLen(deps), 0);
freeStringArray(deps);
}
TEST_F(DependencyQueryTest, GetModuleDependencies_DirectVsRecursive) {
// Chain: a -> b -> c. Direct deps of a = {b}. Recursive deps of a = {b, c}.
reg("c");
reg("b", {"c"});
reg("a", {"b"});
char** direct = logos_core_get_module_dependencies("a", false);
EXPECT_EQ(stringArrayToSet(direct), (std::set<std::string>{"b"}));
freeStringArray(direct);
char** recursive = logos_core_get_module_dependencies("a", true);
EXPECT_EQ(stringArrayToSet(recursive), (std::set<std::string>{"b", "c"}));
freeStringArray(recursive);
}
TEST_F(DependencyQueryTest, GetModuleDependents_DirectVsRecursive) {
// Chain: a -> b -> c. Direct dependents of c = {b}. Recursive = {b, a}.
reg("c");
reg("b", {"c"});
reg("a", {"b"});
char** direct = logos_core_get_module_dependents("c", false);
EXPECT_EQ(stringArrayToSet(direct), (std::set<std::string>{"b"}));
freeStringArray(direct);
char** recursive = logos_core_get_module_dependents("c", true);
EXPECT_EQ(stringArrayToSet(recursive), (std::set<std::string>{"b", "a"}));
freeStringArray(recursive);
}
TEST_F(DependencyQueryTest, GetModuleDependencies_Diamond_RecursiveDeduplicates) {
// Diamond: a -> b -> d ; a -> c -> d. Recursive deps of a must include
// {b, c, d} with no duplicate entries for d.
reg("d");
reg("b", {"d"});
reg("c", {"d"});
reg("a", {"b", "c"});
char** recursive = logos_core_get_module_dependencies("a", true);
std::set<std::string> got = stringArrayToSet(recursive);
int n = stringArrayLen(recursive);
freeStringArray(recursive);
EXPECT_EQ(got, (std::set<std::string>{"b", "c", "d"}));
// No duplicate d entries — set and array length must agree.
EXPECT_EQ(n, static_cast<int>(got.size()));
}
TEST_F(DependencyQueryTest, GetModuleDependents_Diamond_RecursiveDeduplicates) {
// Same diamond — d has {b, c} as direct dependents and {b, c, a}
// transitively. The BFS must not report a twice even though both
// b and c list it as a dependent.
reg("d");
reg("b", {"d"});
reg("c", {"d"});
reg("a", {"b", "c"});
char** direct = logos_core_get_module_dependents("d", false);
EXPECT_EQ(stringArrayToSet(direct), (std::set<std::string>{"b", "c"}));
freeStringArray(direct);
char** recursive = logos_core_get_module_dependents("d", true);
std::set<std::string> got = stringArrayToSet(recursive);
int n = stringArrayLen(recursive);
freeStringArray(recursive);
EXPECT_EQ(got, (std::set<std::string>{"a", "b", "c"}));
EXPECT_EQ(n, static_cast<int>(got.size()));
}
TEST_F(DependencyQueryTest, GetModuleDependencies_SelfNotIncluded) {
reg("leaf");
reg("root", {"leaf"});
char** recursive = logos_core_get_module_dependencies("root", true);
std::set<std::string> got = stringArrayToSet(recursive);
freeStringArray(recursive);
EXPECT_EQ(got.count("root"), 0u);
EXPECT_EQ(got, (std::set<std::string>{"leaf"}));
}
TEST_F(DependencyQueryTest, GetModuleDependencies_AbortsForNull) {
EXPECT_DEATH(logos_core_get_module_dependencies(nullptr, false), "");
}
TEST_F(DependencyQueryTest, GetModuleDependents_AbortsForNull) {
EXPECT_DEATH(logos_core_get_module_dependents(nullptr, false), "");
}
// =============================================================================
// Derived access-restriction computation (graph + policy -> allowed callers)
// =============================================================================
//
// computeDerivedAllowedCallers() is the registry-backed counterpart of the
// pure derivation seam: it reads the live dependency graph + loaded set + the
// access policy and returns what core would register with capability_module for
// a target — without any RPC. We drive it with the test registry adapters
// (register_module / register_module_dependencies / mark_module_loaded) and the
// ModuleManager::setAccessPolicy entry point.
class DerivedRestrictionsManagerTest : public ::testing::Test {
protected:
void SetUp() override { clearModuleState(); }
void TearDown() override {
// Clear the policy so it doesn't leak into other suites.
ModuleManager::setAccessPolicy("");
clearModuleState();
}
// Register `name` with `deps` declared as dependencies.
static void reg(const std::string& name, const std::vector<std::string>& deps) {
logos_core_register_module(name.c_str(), ("/fake/" + name).c_str());
std::vector<const char*> d;
for (const auto& s : deps) d.push_back(s.c_str());
logos_core_register_module_dependencies(name.c_str(), d.data(),
static_cast<int>(d.size()));
}
static std::set<std::string> derived(const std::string& target) {
auto v = ModuleManager::computeDerivedAllowedCallers(target);
return std::set<std::string>(v.begin(), v.end());
}
// Minimal enforce policy with no explicit restrictions — turns derivation on.
static const char* enforceEnvelope() {
return "{\"version\":1,\"mode\":\"enforce\",\"restrictions\":{}}";
}
};
TEST_F(DerivedRestrictionsManagerTest, LoadedDependentPlusTrusted) {
// a depends on b; both loaded. b's allowed callers = {a} trusted.
reg("b", {});
reg("a", {"b"});
logos_core_mark_module_loaded("b");
logos_core_mark_module_loaded("a");
ModuleManager::setAccessPolicy(enforceEnvelope());
EXPECT_EQ(derived("b"),
(std::set<std::string>{"a", "core", "core_service"}));
}
TEST_F(DerivedRestrictionsManagerTest, UnloadedDependentExcluded) {
// a declares b but is NOT loaded — a must not appear in b's callers.
reg("b", {});
reg("a", {"b"});
logos_core_mark_module_loaded("b"); // a left unloaded
ModuleManager::setAccessPolicy(enforceEnvelope());
EXPECT_EQ(derived("b"), (std::set<std::string>{"core", "core_service"}));
}
TEST_F(DerivedRestrictionsManagerTest, ZeroDependentsIsTrustedOnly) {
reg("solo", {});
logos_core_mark_module_loaded("solo");
ModuleManager::setAccessPolicy(enforceEnvelope());
EXPECT_EQ(derived("solo"), (std::set<std::string>{"core", "core_service"}));
}
TEST_F(DerivedRestrictionsManagerTest, NoEnforcePolicyDerivesNothing) {
reg("b", {});
reg("a", {"b"});
logos_core_mark_module_loaded("b");
logos_core_mark_module_loaded("a");
// No policy set at all -> derivation off -> empty.
EXPECT_TRUE(derived("b").empty());
// A non-enforce policy is also inert.
ModuleManager::setAccessPolicy(
"{\"version\":1,\"mode\":\"audit\",\"restrictions\":{}}");
EXPECT_TRUE(derived("b").empty());
}
TEST_F(DerivedRestrictionsManagerTest, ExplicitPolicyOverridesDerived) {
reg("b", {});
reg("a", {"b"});
logos_core_mark_module_loaded("b");
logos_core_mark_module_loaded("a");
// Explicit entry for b names only "x" — replaces the derived {a, trusted}.
ModuleManager::setAccessPolicy(
"{\"version\":1,\"mode\":\"enforce\",\"restrictions\":{"
"\"b\":{\"allowedCallers\":[\"x\"]}}}");
EXPECT_EQ(derived("b"), (std::set<std::string>{"x"}));
}
TEST_F(DerivedRestrictionsManagerTest, ExemptTargetsNeverDerived) {
reg("capability_module", {});
logos_core_mark_module_loaded("capability_module");
ModuleManager::setAccessPolicy(enforceEnvelope());
EXPECT_TRUE(derived("capability_module").empty());
}
TEST_F(DerivedRestrictionsManagerTest, UnloadDropsDependentFromCallers) {
reg("b", {});
reg("a", {"b"});
logos_core_mark_module_loaded("b");
logos_core_mark_module_loaded("a");
ModuleManager::setAccessPolicy(enforceEnvelope());
EXPECT_TRUE(derived("b").count("a"));
// Unloading a (it stays known, dependency edge remains) drops it.
ModuleManager::registry().markUnloaded("a");
EXPECT_FALSE(derived("b").count("a"));
EXPECT_EQ(derived("b"), (std::set<std::string>{"core", "core_service"}));
}
TEST_F(DerivedRestrictionsManagerTest, TrustedDependentNotDuplicated) {
// A loaded dependent that shares a trusted name must appear exactly once in
// the registered list (the set-based `derived()` helper would hide a dup, so
// inspect the raw vector here).
reg("b", {});
reg("core", {"b"});
logos_core_mark_module_loaded("b");
logos_core_mark_module_loaded("core");
ModuleManager::setAccessPolicy(enforceEnvelope());
auto callers = ModuleManager::computeDerivedAllowedCallers("b");
EXPECT_EQ(std::count(callers.begin(), callers.end(), std::string("core")), 1);
}
// ── The deny-by-default switch, both directions ─────────────────────────────
//
// These two are the contract for the operator-facing flag (`mode: "enforce"`,
// reached as `logoscore --access-policy enforce` / `LogosBasecamp
// --access-policy enforce`). They share one scenario deliberately: the SAME
// undeclared pair must be allowed with the flag off and denied with it on, and
// the declared pair must survive the flip. A change that denied everything
// would pass the "denied" half on its own, so the declared-caller assertion is
// the one carrying the weight.
class DenyByDefaultFlagTest : public DerivedRestrictionsManagerTest {
protected:
// target — the module being reached
// declared — loaded, and declares `target` as a dependency
// undeclared — loaded, declares nothing (the shape D-a found in this
// tree: counter_qml calling package_manager with
// "dependencies": [])
void SetUp() override {
DerivedRestrictionsManagerTest::SetUp();
reg("target", {});
reg("declared", {"target"});
reg("undeclared", {});
logos_core_mark_module_loaded("target");
logos_core_mark_module_loaded("declared");
logos_core_mark_module_loaded("undeclared");
}
};
TEST_F(DenyByDefaultFlagTest, FlagOff_UndeclaredCallerStaysUnrestricted) {
// No policy installed — the default every host has today. Core derives
// nothing, so it registers NO restriction for `target`, and
// capability_module's unrestricted-target path leaves `undeclared ->
// target` working exactly as before.
EXPECT_TRUE(derived("target").empty());
// Same for a policy that isn't in enforce mode: still off, still open.
ModuleManager::setAccessPolicy(
"{\"version\":1,\"mode\":\"audit\",\"restrictions\":{}}");
EXPECT_TRUE(derived("target").empty());
}
TEST_F(DenyByDefaultFlagTest, FlagOn_DeclaredCallerAllowed_UndeclaredRefused) {
ModuleManager::setAccessPolicy(enforceEnvelope());
const auto callers = derived("target");
// A restriction IS registered now — that is what makes the target closed.
ASSERT_FALSE(callers.empty());
// The declared dependent keeps working…
EXPECT_TRUE(callers.count("declared"))
<< "enforce must not break a caller that declared the target";
// …and the undeclared caller is not on the list, so capability_module
// refuses to mint it a token.
EXPECT_FALSE(callers.count("undeclared"))
<< "enforce must refuse a caller that never declared the target";
}
TEST_F(DenyByDefaultFlagTest, FlagOn_ExplicitPolicyCanReadmitAnUndeclaredCaller) {
// The escape hatch an operator needs when a real deployment has a caller
// that legitimately can't declare its target (out-of-process ui_qml
// plugins, for one): an explicit entry replaces the derived list verbatim.
ModuleManager::setAccessPolicy(
"{\"version\":1,\"mode\":\"enforce\",\"restrictions\":{"
"\"target\":{\"allowedCallers\":[\"declared\",\"undeclared\"]}}}");
const auto callers = derived("target");
EXPECT_TRUE(callers.count("declared"));
EXPECT_TRUE(callers.count("undeclared"));
}
TEST_F(DenyByDefaultFlagTest, FlagIsReversible) {
// Clearing the policy must restore today's behaviour byte-for-byte, not
// leave a latched restriction behind (hosts call setAccessPolicy once per
// boot, but a restart in the same process must not inherit enforcement).
ModuleManager::setAccessPolicy(enforceEnvelope());
ASSERT_FALSE(derived("target").empty());
ModuleManager::setAccessPolicy("");
EXPECT_TRUE(derived("target").empty());
}