mirror of
https://github.com/logos-co/logos-logoscore-cli.git
synced 2026-08-30 04:11:09 +00:00
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>
313 lines
11 KiB
C++
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());
|
|
}
|