Files
logos-logoscore-cli/tests/test_package_bootstrap.cpp
Dario LipicarandClaude Opus 5 2ddbb36f23 fix(logosctl): don't let one bundled-module failure disarm signature_policy (#90)
bootstrapPackageModules loaded "package_manager" and "package_downloader"
in a loop that `return`ed on the first failure -- before every
set*Directory / setSignaturePolicy call that followed it. Three defects
came out of that one early return:

* It left the daemon half-configured on any platform, not just Windows.
  A package_downloader that failed to load for any reason on Linux or
  macOS took the whole configuration block with it, while
  package_manager stayed up.

* The consequence was not the one originally recorded in flake.nix
  ("installs would land wherever its unset defaults point"). Every
  directory in package-manager-lib fails closed when unset --
  installPlugin refuses with "User modules directory is not set". What
  does NOT fail closed is the signature policy: it defaults to WARN, so
  an operator's `signature_policy: require` was read, advertised in
  state.json and by `logosctl config get`, and enforced nowhere.
  Unsigned packages, and packages signed by untrusted keys, would
  install with a printed warning.

* Because "package_manager" was first in the list, its failure meant
  "package_downloader" was never attempted, even when it would have
  loaded fine.

The stderr warning was wrong in the same way: both failures claimed
"package commands will be unavailable", which is untrue for either
module on its own.

The sequencing now lives in src/daemon/package_bootstrap.cpp behind
injected hooks -- it was not reachable from a test through
logos_core_load_module and a live socket. Each module loads
independently, package_manager is configured whenever it loaded, and
each failure names only the capability it actually costs.

Delivery is now checked. The setters return void, so a dispatched call
and one that never arrived are indistinguishable in the return value;
the CallError overload of invokeRemoteMethod tells them apart. A
configured policy that cannot be delivered unloads package_manager
rather than leave it enforcing less than the session advertises. An
undelivered directory only warns, since those fail closed on their own.

This matters more since cbd4c09: modules-pkg now ships on Windows, so
both modules are loaded on a platform where the daemon had never run
them before.

flake.nix already records both corrections (cbd4c09 reached them
independently); its remaining forward-reference to a defect "tracked
separately" is retired to point at the fix.

tests/test_package_bootstrap.cpp covers all three defects and the
warning text: 7 of its 12 cases fail against the old control flow.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 23:39:25 -03:00

313 lines
11 KiB
C++

#include <gtest/gtest.h>
#include <algorithm>
#include <map>
#include <set>
#include <string>
#include <vector>
#include "daemon/package_bootstrap.h"
namespace {
// Records what the bootstrap did, and lets a test choose which module fails to
// load and which configuration call fails to reach the module.
struct Harness {
std::set<std::string> loadFailures; // modules whose load returns false
std::set<std::string> configureFailures; // methods whose delivery fails
std::vector<std::string> loadAttempts;
std::vector<std::string> unloaded;
std::vector<std::pair<std::string, std::vector<std::string>>> configured;
std::vector<std::string> warnings;
std::vector<std::string> notes;
package_bootstrap::Hooks hooks()
{
package_bootstrap::Hooks h;
h.loadModule = [this](const std::string& m) {
loadAttempts.push_back(m);
return loadFailures.count(m) == 0;
};
h.unloadModule = [this](const std::string& m) { unloaded.push_back(m); };
h.configure = [this](const std::string& method,
const std::vector<std::string>& args) {
configured.emplace_back(method, args);
return configureFailures.count(method) == 0;
};
h.warn = [this](const std::string& line) { warnings.push_back(line); };
h.note = [this](const std::string& line) { notes.push_back(line); };
return h;
}
bool called(const std::string& method) const
{
return std::any_of(configured.begin(), configured.end(),
[&](const auto& c) { return c.first == method; });
}
std::vector<std::string> argsOf(const std::string& method) const
{
for (const auto& c : configured)
if (c.first == method) return c.second;
return {};
}
bool warnedAbout(const std::string& needle) const
{
return std::any_of(warnings.begin(), warnings.end(),
[&](const std::string& w) {
return w.find(needle) != std::string::npos;
});
}
};
package_bootstrap::Dirs sessionDirs()
{
package_bootstrap::Dirs d;
d.embeddedModules = "/opt/logos/modules";
d.embeddedUiPlugins = "/opt/logos/plugins";
d.userModules = "/home/u/.logosctl/modules";
d.userUiPlugins = "/home/u/.logosctl/plugins";
d.keyring = "/home/u/.logosctl/keyring";
return d;
}
const char* kPm = package_bootstrap::kPackageManager;
const char* kPd = package_bootstrap::kPackageDownloader;
} // namespace
// -- happy path -------------------------------------------------------------
TEST(PackageBootstrap, LoadsBothAndConfiguresEverything)
{
Harness h;
const auto out = package_bootstrap::run(h.hooks(), sessionDirs(), "require");
EXPECT_TRUE(out.managerLoaded);
EXPECT_TRUE(out.downloaderLoaded);
EXPECT_TRUE(out.directoriesSet);
EXPECT_TRUE(out.policyArmed);
EXPECT_FALSE(out.managerDisabled);
EXPECT_TRUE(h.called("setEmbeddedModulesDirectory"));
EXPECT_TRUE(h.called("setEmbeddedUiPluginsDirectory"));
EXPECT_TRUE(h.called("setUserModulesDirectory"));
EXPECT_TRUE(h.called("setUserUiPluginsDirectory"));
EXPECT_TRUE(h.called("setKeyringDirectory"));
EXPECT_TRUE(h.called("setSignaturePolicy"));
EXPECT_TRUE(h.called("resetPendingAction"));
EXPECT_TRUE(h.warnings.empty());
EXPECT_EQ(h.argsOf("setUserModulesDirectory"),
(std::vector<std::string>{"/home/u/.logosctl/modules"}));
EXPECT_EQ(h.argsOf("setKeyringDirectory"),
(std::vector<std::string>{"/home/u/.logosctl/keyring"}));
EXPECT_EQ(h.argsOf("setSignaturePolicy"),
(std::vector<std::string>{"require"}));
}
TEST(PackageBootstrap, UnsetPolicyIsLeftToTheModuleDefault)
{
Harness h;
const auto out = package_bootstrap::run(h.hooks(), sessionDirs(), "");
EXPECT_FALSE(h.called("setSignaturePolicy"));
EXPECT_FALSE(out.policyArmed);
EXPECT_FALSE(out.managerDisabled);
EXPECT_TRUE(out.managerLoaded);
}
TEST(PackageBootstrap, EmptyBundledDirSkipsOnlyTheEmbeddedCalls)
{
Harness h;
package_bootstrap::Dirs dirs = sessionDirs();
dirs.embeddedModules.clear();
dirs.embeddedUiPlugins.clear();
const auto out = package_bootstrap::run(h.hooks(), dirs, "require");
EXPECT_FALSE(h.called("setEmbeddedModulesDirectory"));
EXPECT_FALSE(h.called("setEmbeddedUiPluginsDirectory"));
EXPECT_TRUE(h.called("setUserModulesDirectory"));
EXPECT_TRUE(out.policyArmed);
}
// -- regression: a downloader failure must not skip configuration -----------
//
// The loop used to `return` on the first load failure, which happened BEFORE
// every set*Directory / setSignaturePolicy call below it. package_manager came
// up loaded and completely unconfigured on any platform where the second
// module failed to load, not just Windows.
TEST(PackageBootstrap, DownloaderFailureStillConfiguresTheManager)
{
Harness h;
h.loadFailures.insert(kPd);
const auto out = package_bootstrap::run(h.hooks(), sessionDirs(), "require");
EXPECT_TRUE(out.managerLoaded);
EXPECT_FALSE(out.downloaderLoaded);
EXPECT_TRUE(h.called("setEmbeddedModulesDirectory"));
EXPECT_TRUE(h.called("setUserModulesDirectory"));
EXPECT_TRUE(h.called("setUserUiPluginsDirectory"));
EXPECT_TRUE(h.called("setKeyringDirectory"));
EXPECT_TRUE(h.called("resetPendingAction"));
}
// The specific security consequence of the old early return: the operator's
// `signature_policy: require` never reached the module, which then ran at its
// own default (`warn`) — unsigned packages install with a printed warning, and
// a package signed by an untrusted key installs too — while state.json and
// `logosctl config get` kept reporting `require`.
TEST(PackageBootstrap, DownloaderFailureStillArmsTheSignaturePolicy)
{
Harness h;
h.loadFailures.insert(kPd);
const auto out = package_bootstrap::run(h.hooks(), sessionDirs(), "require");
ASSERT_TRUE(h.called("setSignaturePolicy"));
EXPECT_EQ(h.argsOf("setSignaturePolicy"),
(std::vector<std::string>{"require"}));
EXPECT_TRUE(out.policyArmed);
EXPECT_TRUE(out.managerLoaded);
EXPECT_FALSE(out.managerDisabled);
}
// -- regression: the first entry must not suppress the second ---------------
TEST(PackageBootstrap, ManagerFailureStillAttemptsTheDownloader)
{
Harness h;
h.loadFailures.insert(kPm);
const auto out = package_bootstrap::run(h.hooks(), sessionDirs(), "require");
EXPECT_EQ(h.loadAttempts, (std::vector<std::string>{kPm, kPd}));
EXPECT_FALSE(out.managerLoaded);
EXPECT_TRUE(out.downloaderLoaded);
// Nothing to configure, and nothing to fail closed on: an absent manager
// enforces nothing and answers nothing.
EXPECT_TRUE(h.configured.empty());
EXPECT_TRUE(h.unloaded.empty());
EXPECT_FALSE(out.managerDisabled);
}
TEST(PackageBootstrap, BothFailingWarnsAboutEachAndConfiguresNothing)
{
Harness h;
h.loadFailures.insert(kPm);
h.loadFailures.insert(kPd);
const auto out = package_bootstrap::run(h.hooks(), sessionDirs(), "require");
EXPECT_EQ(h.warnings.size(), 2u);
EXPECT_TRUE(h.configured.empty());
EXPECT_FALSE(out.managerLoaded);
EXPECT_FALSE(out.downloaderLoaded);
}
// -- regression: the warning must name what actually went away --------------
//
// Both failures used to emit the same line: "Package commands will be
// unavailable in this session." That is wrong for either module on its own.
TEST(PackageBootstrap, WarningNamesOnlyTheLostCapability)
{
{
Harness h;
h.loadFailures.insert(kPd);
package_bootstrap::run(h.hooks(), sessionDirs(), "");
ASSERT_EQ(h.warnings.size(), 1u);
const std::string& w = h.warnings.front();
EXPECT_NE(w.find(kPd), std::string::npos);
EXPECT_NE(w.find("package search"), std::string::npos);
// The manager DID load, so the session's local package commands work.
EXPECT_NE(w.find("locally installed packages are unaffected"),
std::string::npos);
EXPECT_EQ(w.find("`package install`"), std::string::npos);
}
{
Harness h;
h.loadFailures.insert(kPm);
package_bootstrap::run(h.hooks(), sessionDirs(), "");
ASSERT_EQ(h.warnings.size(), 1u);
const std::string& w = h.warnings.front();
EXPECT_NE(w.find(kPm), std::string::npos);
EXPECT_NE(w.find("`package install`"), std::string::npos);
EXPECT_NE(w.find("catalog commands are unaffected"), std::string::npos);
}
}
// -- fail closed on an undelivered policy -----------------------------------
TEST(PackageBootstrap, UndeliveredPolicyUnloadsTheManager)
{
Harness h;
h.configureFailures.insert("setSignaturePolicy");
const auto out = package_bootstrap::run(h.hooks(), sessionDirs(), "require");
EXPECT_EQ(h.unloaded, (std::vector<std::string>{kPm}));
EXPECT_TRUE(out.managerDisabled);
EXPECT_FALSE(out.managerLoaded);
EXPECT_FALSE(out.policyArmed);
EXPECT_TRUE(h.warnedAbout("signature_policy='require'"));
// Do not leave the gate open and then clear the pending-action slot as if
// the manager were in service.
EXPECT_FALSE(h.called("resetPendingAction"));
}
TEST(PackageBootstrap, UndeliveredPolicyWithNoPolicyConfiguredIsNotFatal)
{
Harness h;
// No `signature_policy:` in the config, so the call is never made and
// there is nothing to fail closed on — the module's own default applies,
// which is what the session advertises.
h.configureFailures.insert("setSignaturePolicy");
const auto out = package_bootstrap::run(h.hooks(), sessionDirs(), "");
EXPECT_TRUE(h.unloaded.empty());
EXPECT_TRUE(out.managerLoaded);
EXPECT_FALSE(out.managerDisabled);
}
// A missing directory is loud but safe: the module fails closed on every unset
// directory (install refuses with "User modules directory is not set"), so it
// warns rather than taking the manager out of the session.
TEST(PackageBootstrap, UndeliveredDirectoryWarnsButKeepsTheManager)
{
Harness h;
h.configureFailures.insert("setUserModulesDirectory");
const auto out = package_bootstrap::run(h.hooks(), sessionDirs(), "require");
EXPECT_TRUE(h.unloaded.empty());
EXPECT_TRUE(out.managerLoaded);
EXPECT_FALSE(out.directoriesSet);
EXPECT_TRUE(out.policyArmed);
EXPECT_TRUE(h.warnedAbout("directory settings did not reach the module"));
}
// A null note hook (the non-verbose daemon) must not be called.
TEST(PackageBootstrap, VerboseNotesAreOptional)
{
Harness h;
package_bootstrap::Hooks hooks = h.hooks();
hooks.note = nullptr;
const auto out = package_bootstrap::run(hooks, sessionDirs(), "require");
EXPECT_TRUE(out.managerLoaded);
EXPECT_TRUE(h.notes.empty());
}