mirror of
https://github.com/logos-co/logos-package-manager-module.git
synced 2026-08-31 12:31:11 +00:00
* feat(deps): carry dependency constraints and the mismatch status across the ABI
toLogosMap(const InstalledPackage&) is where the range and the signer died.
lgpm carried {name, version, signer} intact from metadata.json through the
.lgx, `lgx verify` and `lgpm install` onto disk; two readers over the SAME
installed tree then disagreed:
lgpm --json info app -> dependencyConstraints:[{...}]
package_manager.getInstalledPackages -> dependencies:["lib"]
Same library, same files, one serialiser -- this one. Everything past it,
basecamp included, was reading a payload the constraint had been stripped
from.
`dependencyConstraints` is added as a SEPARATE key rather than by widening
`dependencies`, because widening it would fail SILENTLY. basecamp's
PluginLoader::loadCoreDependencies reads that very list (via UIPluginManager's
m_uiPluginMetadata, filled from getInstalledUiPlugins) and does
QString depName = dep.toString(); if (depName.isEmpty()) continue;
-- a QVariantMap stringifies to empty, so an object-form entry would be
skipped and the dependency simply never loaded, with no diagnostic. The graph
edge and the constraint on that edge are different facts; they now travel in
different keys.
toFlatLogosMap gains the same treatment for the tree APIs: version_mismatch
keeps `version` and `installType` because it resolved to a real installed
package and "needs ^2.0.0, have 1.0.0" takes both numbers, and the per-edge
`requiredVersion` / `requiredSigner` are emitted only when the parent declared
them. Nothing here evaluates the signer.
Both keys are absent whenever every entry is a bare name, which is every
package in the fleet today, so this is backward-compatible by construction.
The unit tests build against tests/stubs/package_manager_lib.h, so the stub
and its mock are updated to mirror the real header -- otherwise the impl
compiles in the integration target and not the mocked one.
Tests 100 -> 107. Red on the base, reading only emitted keys so the probe
needs no field the base lacks:
ASSERT failed: list[0].contains("dependencyConstraints")
Note: the five integration test files are NOT built by the `unit-tests` check
-- CMake reports "package_manager_lib not found in ../lib -- skipping
integration tests" because the test derivation never stages the external lib.
The end-to-end tests added to tests/test_scanning.cpp are therefore correct
but unexercised in CI; they were driven by hand against the real library.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(deps): carry the observed signer, and the two signer statuses, across the ABI
Two facts have to reach the far side, and they are different facts:
requiredSigner — a PIN a dependant declared. A demand.
observedSigner — the DID whose signature over the installed package was
VERIFIED at install. An observation.
Everything downstream ("published by a different signer") needs both. The pin
alone does not say what went wrong; the observation alone is an accusation with
no charge attached. `requiredSigner` already crossed and was, in this file's own
words, "carried and compared by nobody" — logos-package-manager now compares it,
so the other half has to arrive too.
New statuses, both about packages that ARE installed:
signer_mismatch — provably not the package the dependant named
signer_unknown — the pin could not be checked; nothing recorded a
publisher. Absence of evidence, its own status.
`observedSigner` is additive and emitted only when something was observed, so
an unsigned, embedded or pre-sidecar install crosses byte-identically to
before, and a reader can tell "nothing recorded" (key absent) from a recorded
value. A key present with an empty string would be neither, and would read as
"observed to be unsigned" — which nothing on disk records.
Also replaces `Installed || VersionMismatch` with
nodeResolvedToAnInstalledPackage at the version/installType gate. That chain
named the two statuses that existed when it was written, so the moment these
two were appended it would have started blanking `version` on packages sitting
right there on disk — in this file and in the library's JSON emitter, quietly,
at two call sites in two repositories. The RED run below shows exactly that:
"expected [1.0.0] but got []" for a signer-mismatched dependency.
And the mock's flatten() is brought back in line with the real library, which
PROMOTES on dedup (a later edge judging a package more harshly wins) rather
than merely deduping. The mock had been left behind by that change, so a module
test could have seen a satisfied row for a package the real resolver rejects. A
test double that disagrees with the thing it doubles is worse than none.
RED (observedSigner dropped from both emitters, gate back to the two-status
chain): 109 passed, 4 failed
getInstalledPackages_carries_the_observed_signer
resolveDependencies_surfaces_signer_mismatch_with_both_dids
resolveFlatDependencies_surfaces_signer_mismatch
resolveDependencies_surfaces_signer_unknown_without_an_observed_did
ASSERT_EQ failed: expected [1.0.0] but got []
GREEN: 113 passed.
Both runs use --override-input logos-package-manager path:<local checkout>:
this branch's flake.lock is still pinned at the parent branch's mid-history
commit and gets relocked when the parent merges, per the stack's merge order.
checks.x86_64-linux.unit-tests
-> /nix/store/rd7lrab96jvi9n3ycjx2h8czhs9j91a3-logos-package_manager-tests-1.0.0
packages.x86_64-linux.lib
-> /nix/store/hlnjk6rrz73q6hjcsn4c3vr14y57liis-logos-package_manager-module-lib-1.0.0
packages.x86_64-linux.default
-> /nix/store/wvlj38yw066icjypvppzncjpc442ympr-logos-package_manager-module
That .so was then staged into a real installed tree and driven through a real
logoscore daemon; the five wire payloads are in the branch summary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(deps): follow the rename of observedSigner to signerDid
logos-package-manager stopped recording who it verified at install and started
carrying the package's own manifest.sig into the install tree, so the value
that crosses this ABI changed meaning and changed name with it.
`observedSigner` was an observation the installer made and wrote down.
`signerDid` is what the installed signature says about itself, reported only
once that signature has been checked against the key its own DID carries. The
difference matters to a reader on the far side: it is not corroborated by
anything outside the document, so it is for display, and the verdict beside it
is NOT these two values compared — it comes from verifying the installed
signature under the PIN's key. A `signer_mismatch` row therefore carries a
`signerDid` that differs from `requiredSigner`, and that is the normal shape of
that row rather than an inconsistency.
Wire key renamed to match, in both the installed-packages map and the
dependency-tree node. Still additive and still absent when there is no usable
signature, so an unsigned or embedded package crosses the ABI byte-identically
to before.
The test stub mirrors the real struct and moves with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(deps): build against the library this module needs, and pin the wire shape exactly
CI here runs `nix build -L` — the real module, linking the real
logos-package-manager. The lock pinned 7c5aad9a (2026-08-22), whose
DependencyTreeNode has neither `requiredSigner` nor `signerDid`, so that build
does not compile:
error: no member named requiredSigner in const struct DependencyTreeNode
error: no member named signerDid in const struct DependencyTreeNode
That is a red build, not a latent risk. And the one check would never have said
so: `tests.mockCLibs = ["logos_pm"]` builds the unit tests against
tests/stubs/package_manager_lib.h, a hand-maintained MIRROR of the real header.
It compiles and passes against a library it never links, so the check is green
while `nix build` is red and nothing connects the two. flake.nix now pins the
branch carrying those fields, with a REVERT-ON-MERGE note saying exactly this.
Measured: `nix build` and `nix build .#unit-tests` both pass with NO override.
The backward-compatibility test moves from three absences to an EXACT KEY SET.
Naming three keys only rules out the three somebody thought of, and the way an
additive key fails is by turning out not to be conditional. `signerDid` is that
shape: it is a property of the PACKAGE, not of the edge, so it does not become
absent because an edge declared no constraint. A real capture of a satisfied
pin carries `status:"installed"` and a `signerDid` together — so a test that
only looked for requiredVersion/requiredSigner would not have noticed it appear
on an unconstrained node. Counting and naming every key does. Proven by
mutation: one unconditional extra key turns the test red.
Stub and test prose follow the mechanism. What an embedded package is missing
is a manifest.sig, and it can never have one because nothing but
installPluginFile copies one into an install tree — not a record nobody wrote.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(deps): cut this PR's comments back to what a reader needs
217 added comment lines -> 132. Comments only; no code line moved or changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2117 lines
87 KiB
C++
2117 lines
87 KiB
C++
// Unit tests for PackageManagerImpl — PackageManagerLib is mocked
|
|
// (mock_package_manager_lib.cpp).
|
|
//
|
|
// Structured lib returns (installed packages, dependency tree, dependents)
|
|
// are registered via setMock*() helpers declared in mock_package_manager_lib.h.
|
|
// The registries reset automatically on each LogosTestContext, so tests just
|
|
// construct a context, set the mocks, and instantiate the impl.
|
|
|
|
#include <logos_test.h>
|
|
#include "package_manager_impl.h"
|
|
#include "mocks/mock_package_manager_lib.h"
|
|
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
// Qt-free event capture lives in the test framework (logos_test_events.h,
|
|
// pulled in by <logos_test.h>); the module-specific event-method bodies these
|
|
// drive are in package_manager_events_test.cpp.
|
|
using logos_test::EventCapture;
|
|
using logos_test::ScopedEventSink;
|
|
|
|
LOGOS_TEST(onInit_does_not_throw) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_FALSE(t.moduleCalled("any_module", "any_method"));
|
|
}
|
|
|
|
LOGOS_TEST(installPlugin_success_core_emits_core_event) {
|
|
auto t = LogosTestContext("package_manager");
|
|
t.mockCFunction("installPluginFile_result").returns("/installed/core.dylib");
|
|
t.mockCFunction("installPluginFile_installedPath").returns("/installed/core.dylib");
|
|
t.mockCFunction("installPluginFile_error").returns("");
|
|
t.mockCFunction("installPluginFile_isCore").returns(true);
|
|
|
|
std::string lastEvent;
|
|
std::string lastEventData;
|
|
PackageManagerImpl impl;
|
|
ScopedEventSink _sink([&](const std::string& name, const std::string& data) {
|
|
lastEvent = name;
|
|
lastEventData = data;
|
|
});
|
|
|
|
LogosMap m = impl.installPlugin("/path/to/foo.lgx", false);
|
|
LOGOS_ASSERT_EQ(m["path"].get<std::string>(), std::string("/installed/core.dylib"));
|
|
LOGOS_ASSERT_TRUE(m["isCoreModule"].get<bool>());
|
|
LOGOS_ASSERT_FALSE(m.contains("error"));
|
|
LOGOS_ASSERT_EQ(m["name"].get<std::string>(), std::string("foo"));
|
|
LOGOS_ASSERT_EQ(m["signatureStatus"].get<std::string>(), std::string("unsigned"));
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("verifyPackageSignature"));
|
|
LOGOS_ASSERT_EQ(lastEvent, std::string("corePluginFileInstalled"));
|
|
LOGOS_ASSERT_EQ(lastEventData, std::string("/installed/core.dylib"));
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("installPluginFile"));
|
|
}
|
|
|
|
LOGOS_TEST(installPlugin_success_ui_emits_ui_event) {
|
|
auto t = LogosTestContext("package_manager");
|
|
t.mockCFunction("installPluginFile_result").returns("/ui/plugin.qml");
|
|
t.mockCFunction("installPluginFile_installedPath").returns("/ui/plugin.qml");
|
|
t.mockCFunction("installPluginFile_error").returns("");
|
|
t.mockCFunction("installPluginFile_isCore").returns(false);
|
|
|
|
std::string lastEvent;
|
|
PackageManagerImpl impl;
|
|
ScopedEventSink _sink([&](const std::string& name, const std::string&) { lastEvent = name; });
|
|
|
|
LogosMap m = impl.installPlugin("/path/bar.lgx", false);
|
|
LOGOS_ASSERT_FALSE(m["isCoreModule"].get<bool>());
|
|
LOGOS_ASSERT_EQ(lastEvent, std::string("uiPluginFileInstalled"));
|
|
}
|
|
|
|
// A QML-only ui_qml package has no backend library, so the library reports the
|
|
// installed module DIRECTORY rather than a main file. That must still be a
|
|
// success: event emitted, non-empty "path", no "error" key. Before the fix the
|
|
// library handed back an empty string here and the impl gated its event on it,
|
|
// so uiPluginFileInstalled never fired and logos-package-manager-ui — which
|
|
// treats an empty "path" as failure — rendered a red RETRY on a package that
|
|
// had installed perfectly.
|
|
LOGOS_TEST(installPlugin_ui_qml_without_main_emits_event_with_directory_path) {
|
|
auto t = LogosTestContext("package_manager");
|
|
t.mockCFunction("installPluginFile_result").returns("/user/ui_plugins");
|
|
t.mockCFunction("installPluginFile_installedPath").returns("/user/ui_plugins/hello_ui");
|
|
t.mockCFunction("installPluginFile_error").returns("");
|
|
t.mockCFunction("installPluginFile_isCore").returns(false);
|
|
|
|
std::string lastEvent;
|
|
std::string lastEventData;
|
|
PackageManagerImpl impl;
|
|
ScopedEventSink _sink([&](const std::string& name, const std::string& data) {
|
|
lastEvent = name;
|
|
lastEventData = data;
|
|
});
|
|
|
|
LogosMap m = impl.installPlugin("/path/hello_ui.lgx", false);
|
|
LOGOS_ASSERT_EQ(m["path"].get<std::string>(), std::string("/user/ui_plugins/hello_ui"));
|
|
LOGOS_ASSERT_FALSE(m.contains("error"));
|
|
LOGOS_ASSERT_EQ(lastEvent, std::string("uiPluginFileInstalled"));
|
|
LOGOS_ASSERT_EQ(lastEventData, std::string("/user/ui_plugins/hello_ui"));
|
|
}
|
|
|
|
// Defence in depth against an OLDER logos-package-manager that still leaves
|
|
// installedPluginPath empty for a QML-only package: the impl must not take the
|
|
// empty value as failure. It falls back to the library's return value (the
|
|
// install root), which keeps the event firing and "path" non-empty.
|
|
LOGOS_TEST(installPlugin_empty_installedPath_still_succeeds_via_result_fallback) {
|
|
auto t = LogosTestContext("package_manager");
|
|
t.mockCFunction("installPluginFile_result").returns("/user/ui_plugins");
|
|
t.mockCFunction("installPluginFile_installedPath").returns("");
|
|
t.mockCFunction("installPluginFile_error").returns("");
|
|
t.mockCFunction("installPluginFile_isCore").returns(false);
|
|
|
|
std::string lastEvent;
|
|
std::string lastEventData;
|
|
PackageManagerImpl impl;
|
|
ScopedEventSink _sink([&](const std::string& name, const std::string& data) {
|
|
lastEvent = name;
|
|
lastEventData = data;
|
|
});
|
|
|
|
LogosMap m = impl.installPlugin("/path/hello_ui.lgx", false);
|
|
LOGOS_ASSERT_EQ(m["path"].get<std::string>(), std::string("/user/ui_plugins"));
|
|
LOGOS_ASSERT_FALSE(m.contains("error"));
|
|
LOGOS_ASSERT_EQ(lastEvent, std::string("uiPluginFileInstalled"));
|
|
LOGOS_ASSERT_EQ(lastEventData, std::string("/user/ui_plugins"));
|
|
}
|
|
|
|
LOGOS_TEST(installPlugin_failure_sets_error_no_event) {
|
|
auto t = LogosTestContext("package_manager");
|
|
t.mockCFunction("installPluginFile_result").returns("");
|
|
t.mockCFunction("installPluginFile_error").returns("invalid lgx");
|
|
t.mockCFunction("installPluginFile_installedPath").returns("");
|
|
|
|
std::string lastEvent;
|
|
PackageManagerImpl impl;
|
|
ScopedEventSink _sink([&](const std::string& name, const std::string&) { lastEvent = name; });
|
|
|
|
LogosMap m = impl.installPlugin("/bad.lgx", false);
|
|
LOGOS_ASSERT_TRUE(m["path"].get<std::string>().empty());
|
|
LOGOS_ASSERT_EQ(m["error"].get<std::string>(), std::string("invalid lgx"));
|
|
LOGOS_ASSERT_TRUE(lastEvent.empty());
|
|
}
|
|
|
|
LOGOS_TEST(installPlugin_skipIfNotNewerVersion_passed_to_mock) {
|
|
auto t = LogosTestContext("package_manager");
|
|
t.mockCFunction("installPluginFile_result").returns("/ok");
|
|
t.mockCFunction("installPluginFile_installedPath").returns("/ok");
|
|
t.mockCFunction("installPluginFile_error").returns("");
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
impl.installPlugin("/x.lgx", true);
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("installPluginFile_skipIfNotNewer_true"));
|
|
|
|
impl.installPlugin("/y.lgx", false);
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("installPluginFile_skipIfNotNewer_false"));
|
|
}
|
|
|
|
LOGOS_TEST(setEmbeddedModulesDirectory_forwards_to_lib) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
impl.setEmbeddedModulesDirectory("/emb/mod");
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("setEmbeddedModulesDirectory"));
|
|
}
|
|
|
|
LOGOS_TEST(addEmbeddedModulesDirectory_forwards_to_lib) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
impl.addEmbeddedModulesDirectory("/emb/m2");
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("addEmbeddedModulesDirectory"));
|
|
}
|
|
|
|
LOGOS_TEST(setEmbeddedUiPluginsDirectory_forwards_to_lib) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
impl.setEmbeddedUiPluginsDirectory("/emb/ui");
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("setEmbeddedUiPluginsDirectory"));
|
|
}
|
|
|
|
LOGOS_TEST(addEmbeddedUiPluginsDirectory_forwards_to_lib) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
impl.addEmbeddedUiPluginsDirectory("/emb/ui2");
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("addEmbeddedUiPluginsDirectory"));
|
|
}
|
|
|
|
LOGOS_TEST(setUserModulesDirectory_forwards_to_lib) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
impl.setUserModulesDirectory("/user/mod");
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("setUserModulesDirectory"));
|
|
}
|
|
|
|
LOGOS_TEST(setUserUiPluginsDirectory_forwards_to_lib) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
impl.setUserUiPluginsDirectory("/user/ui");
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("setUserUiPluginsDirectory"));
|
|
}
|
|
|
|
LOGOS_TEST(getInstalledPackages_returns_struct_registry) {
|
|
auto t = LogosTestContext("package_manager");
|
|
InstalledPackage pkg;
|
|
pkg.name = "pkg1";
|
|
pkg.version = "1.0.0";
|
|
setMockInstalledPackages({pkg});
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosList list = impl.getInstalledPackages();
|
|
LOGOS_ASSERT_EQ(list.size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(list[0]["name"].get<std::string>(), std::string("pkg1"));
|
|
LOGOS_ASSERT_EQ(list[0]["version"].get<std::string>(), std::string("1.0.0"));
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("getInstalledPackages"));
|
|
}
|
|
|
|
LOGOS_TEST(getInstalledModules_returns_struct_registry) {
|
|
auto t = LogosTestContext("package_manager");
|
|
InstalledPackage mod;
|
|
mod.name = "mod_a";
|
|
mod.type = "core";
|
|
setMockInstalledModules({mod});
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosList list = impl.getInstalledModules();
|
|
LOGOS_ASSERT_EQ(list.size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(list[0]["name"].get<std::string>(), std::string("mod_a"));
|
|
LOGOS_ASSERT_EQ(list[0]["type"].get<std::string>(), std::string("core"));
|
|
}
|
|
|
|
LOGOS_TEST(getInstalledUiPlugins_returns_struct_registry) {
|
|
auto t = LogosTestContext("package_manager");
|
|
InstalledPackage ui;
|
|
ui.name = "ui_z";
|
|
ui.type = "ui";
|
|
setMockInstalledUiPlugins({ui});
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosList list = impl.getInstalledUiPlugins();
|
|
LOGOS_ASSERT_EQ(list.size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(list[0]["name"].get<std::string>(), std::string("ui_z"));
|
|
}
|
|
|
|
LOGOS_TEST(getInstalledPackages_empty_registry) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
|
|
LOGOS_ASSERT_TRUE(impl.getInstalledPackages().empty());
|
|
}
|
|
|
|
LOGOS_TEST(getValidVariants_uses_platformVariantsToTry) {
|
|
auto t = LogosTestContext("package_manager");
|
|
t.mockCFunction("platformVariantsToTry_first").returns("custom-variant");
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
std::vector<std::string> v = impl.getValidVariants();
|
|
LOGOS_ASSERT_EQ(static_cast<int>(v.size()), 1);
|
|
LOGOS_ASSERT_EQ(v[0], std::string("custom-variant"));
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("platformVariantsToTry"));
|
|
}
|
|
|
|
LOGOS_TEST(getValidVariants_default_mock_variant) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
|
|
std::vector<std::string> v = impl.getValidVariants();
|
|
LOGOS_ASSERT_FALSE(v.empty());
|
|
LOGOS_ASSERT_EQ(v[0], std::string("mock-variant"));
|
|
}
|
|
|
|
LOGOS_TEST(no_cross_module_calls_by_default) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_EQ(t.moduleCallCount("capability_module", "requestModule"), 0);
|
|
}
|
|
|
|
LOGOS_TEST(setSignaturePolicy_forwards_to_lib) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
impl.setSignaturePolicy("warn");
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("setSignaturePolicy"));
|
|
}
|
|
|
|
LOGOS_TEST(setKeyringDirectory_forwards_to_lib) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
impl.setKeyringDirectory("/kr");
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("setKeyringDirectory"));
|
|
}
|
|
|
|
LOGOS_TEST(uninstallPackage_success_core_emits_core_event) {
|
|
auto t = LogosTestContext("package_manager");
|
|
// Scan returns a core package — impl uses this to route the event.
|
|
InstalledPackage pkg;
|
|
pkg.name = "foo";
|
|
pkg.type = "core";
|
|
pkg.installType = InstallType::User;
|
|
setMockInstalledPackages({pkg});
|
|
t.mockCFunction("uninstallPackage_success").returns(true);
|
|
t.mockCFunction("uninstallPackage_removed").returns("/a/foo.dylib,/a/manifest.json");
|
|
|
|
std::string lastEvent;
|
|
std::string lastEventData;
|
|
PackageManagerImpl impl;
|
|
ScopedEventSink _sink([&](const std::string& name, const std::string& data) {
|
|
lastEvent = name;
|
|
lastEventData = data;
|
|
});
|
|
|
|
LogosMap m = impl.uninstallPackage("foo");
|
|
LOGOS_ASSERT_TRUE(m["success"].get<bool>());
|
|
LOGOS_ASSERT_FALSE(m.contains("error"));
|
|
LOGOS_ASSERT_EQ(m["removedFiles"].size(), static_cast<size_t>(2));
|
|
LOGOS_ASSERT_EQ(lastEvent, std::string("corePluginUninstalled"));
|
|
LOGOS_ASSERT_EQ(lastEventData, std::string("foo"));
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("uninstallPackage"));
|
|
}
|
|
|
|
LOGOS_TEST(uninstallPackage_success_ui_emits_ui_event) {
|
|
auto t = LogosTestContext("package_manager");
|
|
InstalledPackage pkg;
|
|
pkg.name = "widget";
|
|
pkg.type = "ui";
|
|
pkg.installType = InstallType::User;
|
|
setMockInstalledPackages({pkg});
|
|
t.mockCFunction("uninstallPackage_success").returns(true);
|
|
|
|
std::string lastEvent;
|
|
PackageManagerImpl impl;
|
|
ScopedEventSink _sink([&](const std::string& name, const std::string&) { lastEvent = name; });
|
|
|
|
LogosMap m = impl.uninstallPackage("widget");
|
|
LOGOS_ASSERT_TRUE(m["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(lastEvent, std::string("uiPluginUninstalled"));
|
|
}
|
|
|
|
LOGOS_TEST(uninstallPackage_failure_sets_error_no_event) {
|
|
auto t = LogosTestContext("package_manager");
|
|
InstalledPackage pkg;
|
|
pkg.name = "foo";
|
|
pkg.type = "core";
|
|
pkg.installType = InstallType::Embedded;
|
|
setMockInstalledPackages({pkg});
|
|
t.mockCFunction("uninstallPackage_success").returns(false);
|
|
t.mockCFunction("uninstallPackage_error").returns("Cannot uninstall embedded package");
|
|
|
|
std::string lastEvent;
|
|
PackageManagerImpl impl;
|
|
ScopedEventSink _sink([&](const std::string& name, const std::string&) { lastEvent = name; });
|
|
|
|
LogosMap m = impl.uninstallPackage("foo");
|
|
LOGOS_ASSERT_FALSE(m["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(m["error"].get<std::string>(),
|
|
std::string("Cannot uninstall embedded package"));
|
|
LOGOS_ASSERT_FALSE(m.contains("removedFiles"));
|
|
LOGOS_ASSERT_TRUE(lastEvent.empty());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Dependency-resolution helpers — tree fixtures reused across tests.
|
|
// ---------------------------------------------------------------------------
|
|
// Forward tree:
|
|
// root (installed)
|
|
// └── a (installed)
|
|
// └── c (installed)
|
|
// Used by the resolveDependencies + resolveFlatDependencies tests.
|
|
static DependencyTreeNode makeForwardTree() {
|
|
DependencyTreeNode root;
|
|
root.name = "root";
|
|
root.status = DependencyStatus::Installed;
|
|
DependencyTreeNode a;
|
|
a.name = "a";
|
|
a.status = DependencyStatus::Installed;
|
|
DependencyTreeNode c;
|
|
c.name = "c";
|
|
c.status = DependencyStatus::Installed;
|
|
a.children = {c};
|
|
root.children = {a};
|
|
return root;
|
|
}
|
|
|
|
// Reverse tree rooted at "dep":
|
|
// dep
|
|
// └── parent_direct
|
|
// └── parent_transitive
|
|
static DependentTreeNode makeReverseTree() {
|
|
DependentTreeNode root;
|
|
root.name = "dep";
|
|
DependentTreeNode directChild;
|
|
directChild.name = "parent_direct";
|
|
directChild.version = "1.2.3";
|
|
DependentTreeNode transitiveChild;
|
|
transitiveChild.name = "parent_transitive";
|
|
transitiveChild.version = "4.5.6";
|
|
directChild.children = {transitiveChild};
|
|
root.children = {directChild};
|
|
return root;
|
|
}
|
|
|
|
LOGOS_TEST(resolveDependencies_recursive_returns_full_tree) {
|
|
auto t = LogosTestContext("package_manager");
|
|
setMockDependencyTree(makeForwardTree());
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap out = impl.resolveDependencies("root", true);
|
|
LOGOS_ASSERT_EQ(out["name"].get<std::string>(), std::string("root"));
|
|
LOGOS_ASSERT_EQ(out["status"].get<std::string>(), std::string("installed"));
|
|
LOGOS_ASSERT_EQ(out["children"].size(), static_cast<size_t>(1));
|
|
// Recursive walks the full tree — grandchild survives.
|
|
LOGOS_ASSERT_EQ(out["children"][0]["name"].get<std::string>(), std::string("a"));
|
|
LOGOS_ASSERT_EQ(out["children"][0]["children"].size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(out["children"][0]["children"][0]["name"].get<std::string>(), std::string("c"));
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("resolveDependencies"));
|
|
}
|
|
|
|
LOGOS_TEST(resolveDependencies_non_recursive_clips_to_depth_one) {
|
|
auto t = LogosTestContext("package_manager");
|
|
setMockDependencyTree(makeForwardTree());
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap out = impl.resolveDependencies("root", false);
|
|
LOGOS_ASSERT_EQ(out["name"].get<std::string>(), std::string("root"));
|
|
LOGOS_ASSERT_EQ(out["children"].size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(out["children"][0]["name"].get<std::string>(), std::string("a"));
|
|
// Non-recursive clips at depth 1: "a" has an empty children array, the
|
|
// grandchild "c" does not appear.
|
|
LOGOS_ASSERT_TRUE(out["children"][0]["children"].is_array());
|
|
LOGOS_ASSERT_TRUE(out["children"][0]["children"].empty());
|
|
}
|
|
|
|
LOGOS_TEST(resolveDependencies_unknown_returns_empty_object) {
|
|
auto t = LogosTestContext("package_manager");
|
|
// No registered tree — mock returns nullopt; impl serialises to {}.
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap out = impl.resolveDependencies("ghost", true);
|
|
LOGOS_ASSERT_TRUE(out.is_object());
|
|
LOGOS_ASSERT_TRUE(out.empty());
|
|
}
|
|
|
|
LOGOS_TEST(resolveDependents_recursive_returns_full_tree) {
|
|
auto t = LogosTestContext("package_manager");
|
|
setMockDependentTree(makeReverseTree());
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap out = impl.resolveDependents("dep", true);
|
|
LOGOS_ASSERT_EQ(out["name"].get<std::string>(), std::string("dep"));
|
|
LOGOS_ASSERT_EQ(out["children"].size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(out["children"][0]["name"].get<std::string>(), std::string("parent_direct"));
|
|
LOGOS_ASSERT_EQ(out["children"][0]["version"].get<std::string>(), std::string("1.2.3"));
|
|
// Full tree — grandchild survives.
|
|
LOGOS_ASSERT_EQ(out["children"][0]["children"].size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(out["children"][0]["children"][0]["name"].get<std::string>(),
|
|
std::string("parent_transitive"));
|
|
// No `direct` field in the new wire format.
|
|
LOGOS_ASSERT_FALSE(out["children"][0].contains("direct"));
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("resolveDependents"));
|
|
}
|
|
|
|
LOGOS_TEST(resolveDependents_non_recursive_clips_to_depth_one) {
|
|
auto t = LogosTestContext("package_manager");
|
|
setMockDependentTree(makeReverseTree());
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap out = impl.resolveDependents("dep", false);
|
|
LOGOS_ASSERT_EQ(out["name"].get<std::string>(), std::string("dep"));
|
|
LOGOS_ASSERT_EQ(out["children"].size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(out["children"][0]["name"].get<std::string>(), std::string("parent_direct"));
|
|
// Clipped: grandchild does not appear.
|
|
LOGOS_ASSERT_TRUE(out["children"][0]["children"].is_array());
|
|
LOGOS_ASSERT_TRUE(out["children"][0]["children"].empty());
|
|
}
|
|
|
|
LOGOS_TEST(resolveDependents_unknown_returns_empty_object) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap out = impl.resolveDependents("ghost", true);
|
|
LOGOS_ASSERT_TRUE(out.is_object());
|
|
LOGOS_ASSERT_TRUE(out.empty());
|
|
}
|
|
|
|
LOGOS_TEST(resolveFlatDependencies_recursive_flattens_tree) {
|
|
auto t = LogosTestContext("package_manager");
|
|
setMockDependencyTree(makeForwardTree());
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosList list = impl.resolveFlatDependencies("root", true);
|
|
// Descendants: a + c. Root itself is NOT included.
|
|
LOGOS_ASSERT_EQ(list.size(), static_cast<size_t>(2));
|
|
std::set<std::string> names;
|
|
for (const auto& item : list) names.insert(item["name"].get<std::string>());
|
|
LOGOS_ASSERT_TRUE(names.count("a") != 0);
|
|
LOGOS_ASSERT_TRUE(names.count("c") != 0);
|
|
// No `children` field in the flat wire format.
|
|
LOGOS_ASSERT_FALSE(list[0].contains("children"));
|
|
}
|
|
|
|
LOGOS_TEST(resolveFlatDependencies_non_recursive_emits_children_only) {
|
|
auto t = LogosTestContext("package_manager");
|
|
setMockDependencyTree(makeForwardTree());
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosList list = impl.resolveFlatDependencies("root", false);
|
|
// Only direct children of root — "a". Grandchild "c" does not appear.
|
|
LOGOS_ASSERT_EQ(list.size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(list[0]["name"].get<std::string>(), std::string("a"));
|
|
LOGOS_ASSERT_FALSE(list[0].contains("children"));
|
|
}
|
|
|
|
LOGOS_TEST(resolveFlatDependencies_unknown_returns_empty_list) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
|
|
LogosList list = impl.resolveFlatDependencies("ghost", true);
|
|
LOGOS_ASSERT_TRUE(list.is_array());
|
|
LOGOS_ASSERT_TRUE(list.empty());
|
|
}
|
|
|
|
LOGOS_TEST(resolveFlatDependents_recursive_flattens_tree) {
|
|
auto t = LogosTestContext("package_manager");
|
|
setMockDependentTree(makeReverseTree());
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosList list = impl.resolveFlatDependents("dep", true);
|
|
LOGOS_ASSERT_EQ(list.size(), static_cast<size_t>(2));
|
|
std::set<std::string> names;
|
|
for (const auto& item : list) names.insert(item["name"].get<std::string>());
|
|
LOGOS_ASSERT_TRUE(names.count("parent_direct") != 0);
|
|
LOGOS_ASSERT_TRUE(names.count("parent_transitive") != 0);
|
|
LOGOS_ASSERT_FALSE(list[0].contains("direct"));
|
|
LOGOS_ASSERT_FALSE(list[0].contains("children"));
|
|
}
|
|
|
|
LOGOS_TEST(resolveFlatDependents_non_recursive_emits_children_only) {
|
|
auto t = LogosTestContext("package_manager");
|
|
setMockDependentTree(makeReverseTree());
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosList list = impl.resolveFlatDependents("dep", false);
|
|
LOGOS_ASSERT_EQ(list.size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(list[0]["name"].get<std::string>(), std::string("parent_direct"));
|
|
LOGOS_ASSERT_FALSE(list[0].contains("children"));
|
|
}
|
|
|
|
LOGOS_TEST(resolveFlatDependents_unknown_returns_empty_list) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
|
|
LogosList list = impl.resolveFlatDependents("ghost", true);
|
|
LOGOS_ASSERT_TRUE(list.is_array());
|
|
LOGOS_ASSERT_TRUE(list.empty());
|
|
}
|
|
|
|
LOGOS_TEST(verifyPackage_maps_signature_result) {
|
|
auto t = LogosTestContext("package_manager");
|
|
t.mockCFunction("verifyPackageSignature_is_signed").returns(true);
|
|
t.mockCFunction("verifyPackageSignature_signature_valid").returns(true);
|
|
t.mockCFunction("verifyPackageSignature_package_valid").returns(true);
|
|
t.mockCFunction("verifyPackageSignature_signer_did").returns("did:jwk:test");
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap m = impl.verifyPackage("/any.lgx");
|
|
LOGOS_ASSERT_TRUE(m["isSigned"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(m["signatureValid"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(m["packageValid"].get<bool>());
|
|
LOGOS_ASSERT_EQ(m["signerDid"].get<std::string>(), std::string("did:jwk:test"));
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("verifyPackageSignature"));
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Gated uninstall / upgrade flow
|
|
// ===========================================================================
|
|
//
|
|
// Tests the two-phase listener-ack protocol:
|
|
//
|
|
// requestXxx -> beforeXxx event -> ackPendingAction (within ackTimeout)
|
|
// -> confirmXxx (destructive) | cancelXxx (aborts)
|
|
//
|
|
// The ack-timeout worker emits on a background thread, so every test uses
|
|
// the thread-safe EventCapture from the test framework (<logos_test.h>).
|
|
// Synchronous paths (request / confirm / cancel) also go through the same
|
|
// capture to keep assertions uniform. Timeout-path tests shrink the timeout
|
|
// via setAckTimeoutMsForTest so they fire in milliseconds rather than the
|
|
// 3-second production default.
|
|
|
|
namespace {
|
|
|
|
// EventCapture / ScopedEventSink come from the test framework
|
|
// (<logos_test.h>); the PackageManagerImpl event-method bodies they observe
|
|
// are defined in package_manager_events_test.cpp (linked into both targets).
|
|
|
|
// Convenience: registers `pkgName` as an installed user-package so
|
|
// isEmbedded / installedDependentsNames return sensible values. `type` is
|
|
// "core" or "ui" — controls which *Uninstalled event doUninstall emits.
|
|
static void primeInstalledUserPackage(const std::string& pkgName,
|
|
const std::string& type = "core") {
|
|
InstalledPackage pkg;
|
|
pkg.name = pkgName;
|
|
pkg.type = type;
|
|
pkg.installType = InstallType::User;
|
|
setMockInstalledPackages({pkg});
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// requestUninstall / requestUpgrade: validation, happy path, pending lock-out
|
|
// ---------------------------------------------------------------------------
|
|
|
|
LOGOS_TEST(requestUninstall_rejects_empty_name) {
|
|
auto t = LogosTestContext("package_manager");
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LogosMap r = impl.requestUninstall("");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(r["error"].get<std::string>(),
|
|
std::string("Package name cannot be empty"));
|
|
LOGOS_ASSERT_EQ(events.size(), static_cast<size_t>(0));
|
|
}
|
|
|
|
LOGOS_TEST(requestUninstall_rejects_embedded_package) {
|
|
auto t = LogosTestContext("package_manager");
|
|
InstalledPackage pkg;
|
|
pkg.name = "core_embed";
|
|
pkg.type = "core";
|
|
pkg.installType = InstallType::Embedded;
|
|
setMockInstalledPackages({pkg});
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LogosMap r = impl.requestUninstall("core_embed");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(r["error"].get<std::string>(),
|
|
std::string("Cannot uninstall embedded module 'core_embed'"));
|
|
LOGOS_ASSERT_FALSE(events.has("beforeUninstall"));
|
|
}
|
|
|
|
LOGOS_TEST(requestUninstall_happy_emits_beforeUninstall_with_dependents) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
// A listener-style reverse tree rooted at "foo" with one direct dependent.
|
|
DependentTreeNode root;
|
|
root.name = "foo";
|
|
DependentTreeNode parent;
|
|
parent.name = "parent_pkg";
|
|
root.children = {parent};
|
|
setMockDependentTree(root);
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LogosMap r = impl.requestUninstall("foo");
|
|
LOGOS_ASSERT_TRUE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_FALSE(r.contains("error"));
|
|
|
|
auto matches = events.all("beforeUninstall");
|
|
LOGOS_ASSERT_EQ(matches.size(), static_cast<size_t>(1));
|
|
LogosMap payload = LogosMap::parse(matches[0].data);
|
|
LOGOS_ASSERT_EQ(payload["name"].get<std::string>(), std::string("foo"));
|
|
LOGOS_ASSERT_EQ(payload["installedDependents"].size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(payload["installedDependents"][0].get<std::string>(),
|
|
std::string("parent_pkg"));
|
|
|
|
// Clean up the pending action so the dtor doesn't emit a timeout event.
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(requestUninstall_rejects_when_already_pending) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LogosMap first = impl.requestUninstall("foo");
|
|
LOGOS_ASSERT_TRUE(first["success"].get<bool>());
|
|
|
|
LogosMap second = impl.requestUninstall("foo");
|
|
LOGOS_ASSERT_FALSE(second["success"].get<bool>());
|
|
// Error message mentions the in-progress op ("uninstall") and its name.
|
|
std::string err = second["error"].get<std::string>();
|
|
LOGOS_ASSERT_TRUE(err.find("uninstall") != std::string::npos);
|
|
LOGOS_ASSERT_TRUE(err.find("foo") != std::string::npos);
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(requestUpgrade_rejects_empty_name) {
|
|
auto t = LogosTestContext("package_manager");
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LogosMap r = impl.requestUpgrade("", "v1.0.0", 0, "");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(r["error"].get<std::string>(),
|
|
std::string("Package name cannot be empty"));
|
|
LOGOS_ASSERT_FALSE(events.has("beforeUpgrade"));
|
|
}
|
|
|
|
LOGOS_TEST(requestUpgrade_rejects_embedded_package) {
|
|
auto t = LogosTestContext("package_manager");
|
|
InstalledPackage pkg;
|
|
pkg.name = "core_embed";
|
|
pkg.type = "core";
|
|
pkg.installType = InstallType::Embedded;
|
|
setMockInstalledPackages({pkg});
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LogosMap r = impl.requestUpgrade("core_embed", "v2.0.0", 0, "");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(r["error"].get<std::string>(),
|
|
std::string("Cannot upgrade embedded module 'core_embed'"));
|
|
LOGOS_ASSERT_FALSE(events.has("beforeUpgrade"));
|
|
}
|
|
|
|
LOGOS_TEST(requestUpgrade_happy_emits_beforeUpgrade_with_tag_and_mode) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LogosMap r = impl.requestUpgrade("foo", "v2.0.0", 7, "");
|
|
LOGOS_ASSERT_TRUE(r["success"].get<bool>());
|
|
|
|
auto matches = events.all("beforeUpgrade");
|
|
LOGOS_ASSERT_EQ(matches.size(), static_cast<size_t>(1));
|
|
LogosMap payload = LogosMap::parse(matches[0].data);
|
|
LOGOS_ASSERT_EQ(payload["name"].get<std::string>(), std::string("foo"));
|
|
LOGOS_ASSERT_EQ(payload["releaseTag"].get<std::string>(), std::string("v2.0.0"));
|
|
LOGOS_ASSERT_EQ(payload["mode"].get<int64_t>(), static_cast<int64_t>(7));
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(requestUpgrade_rejects_when_already_pending) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUpgrade("foo", "v1.0.0", 0, "")["success"].get<bool>());
|
|
|
|
// A second gated op (of either kind) must fail while another is pending.
|
|
LogosMap blocked = impl.requestUninstall("foo");
|
|
LOGOS_ASSERT_FALSE(blocked["success"].get<bool>());
|
|
std::string err = blocked["error"].get<std::string>();
|
|
LOGOS_ASSERT_TRUE(err.find("upgrade") != std::string::npos);
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ackPendingAction: success / missing / mismatch / idempotent
|
|
// ---------------------------------------------------------------------------
|
|
|
|
LOGOS_TEST(ackPendingAction_success_on_pending_uninstall) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUninstall("foo")["success"].get<bool>());
|
|
|
|
LogosMap ack = impl.ackPendingAction("foo");
|
|
LOGOS_ASSERT_TRUE(ack["success"].get<bool>());
|
|
LOGOS_ASSERT_FALSE(ack.contains("error"));
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(ackPendingAction_no_pending_returns_error) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap ack = impl.ackPendingAction("anything");
|
|
LOGOS_ASSERT_FALSE(ack["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(ack["error"].get<std::string>().find("No matching pending") != std::string::npos);
|
|
}
|
|
|
|
LOGOS_TEST(ackPendingAction_name_mismatch_rejected) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUninstall("foo")["success"].get<bool>());
|
|
|
|
LogosMap ack = impl.ackPendingAction("not_foo");
|
|
LOGOS_ASSERT_FALSE(ack["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(ack["error"].get<std::string>().find("not_foo") != std::string::npos);
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(ackPendingAction_idempotent_on_repeat) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUninstall("foo")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
// Second ack should still succeed (same pending, already acked).
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// confirmUninstall: happy / failed-uninstall / missing / mismatch
|
|
// ---------------------------------------------------------------------------
|
|
|
|
LOGOS_TEST(confirmUninstall_happy_performs_uninstall_and_emits_core_event) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo", "core");
|
|
t.mockCFunction("uninstallPackage_success").returns(true);
|
|
t.mockCFunction("uninstallPackage_removed").returns("/m/foo.dylib");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUninstall("foo")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.confirmUninstall("foo");
|
|
LOGOS_ASSERT_TRUE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(r["removedFiles"].size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_TRUE(events.has("corePluginUninstalled"));
|
|
LOGOS_ASSERT_TRUE(t.cFunctionCalled("uninstallPackage"));
|
|
|
|
// Pending cleared — a subsequent confirm should fail with "no matching".
|
|
LogosMap again = impl.confirmUninstall("foo");
|
|
LOGOS_ASSERT_FALSE(again["success"].get<bool>());
|
|
}
|
|
|
|
LOGOS_TEST(confirmUninstall_surfaces_uninstall_failure) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo", "core");
|
|
t.mockCFunction("uninstallPackage_success").returns(false);
|
|
t.mockCFunction("uninstallPackage_error").returns("delete failed");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUninstall("foo")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.confirmUninstall("foo");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(r["error"].get<std::string>(), std::string("delete failed"));
|
|
LOGOS_ASSERT_FALSE(events.has("corePluginUninstalled"));
|
|
LOGOS_ASSERT_FALSE(events.has("uiPluginUninstalled"));
|
|
}
|
|
|
|
LOGOS_TEST(confirmUninstall_no_pending_returns_error) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap r = impl.confirmUninstall("foo");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(r["error"].get<std::string>().find("No matching pending uninstall") != std::string::npos);
|
|
}
|
|
|
|
LOGOS_TEST(confirmUninstall_name_mismatch_returns_error) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo", "core");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUninstall("foo")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.confirmUninstall("bar");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
// Original pending still valid.
|
|
LOGOS_ASSERT_TRUE(impl.cancelUninstall("foo")["success"].get<bool>());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// confirmUpgrade: happy / tag-mismatch / failed-uninstall suppresses event
|
|
// ---------------------------------------------------------------------------
|
|
|
|
LOGOS_TEST(confirmUpgrade_happy_emits_upgradeUninstallDone) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo", "core");
|
|
t.mockCFunction("uninstallPackage_success").returns(true);
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUpgrade("foo", "v2.0.0", 3, "")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.confirmUpgrade("foo", "v2.0.0");
|
|
LOGOS_ASSERT_TRUE(r["success"].get<bool>());
|
|
|
|
auto done = events.all("upgradeUninstallDone");
|
|
LOGOS_ASSERT_EQ(done.size(), static_cast<size_t>(1));
|
|
LogosMap payload = LogosMap::parse(done[0].data);
|
|
LOGOS_ASSERT_EQ(payload["name"].get<std::string>(), std::string("foo"));
|
|
LOGOS_ASSERT_EQ(payload["releaseTag"].get<std::string>(), std::string("v2.0.0"));
|
|
LOGOS_ASSERT_EQ(payload["mode"].get<int64_t>(), static_cast<int64_t>(3));
|
|
}
|
|
|
|
LOGOS_TEST(confirmUpgrade_suppresses_event_on_uninstall_failure) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo", "core");
|
|
// Uninstall step inside confirmUpgrade fails — upgradeUninstallDone must NOT fire.
|
|
t.mockCFunction("uninstallPackage_success").returns(false);
|
|
t.mockCFunction("uninstallPackage_error").returns("cannot remove");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUpgrade("foo", "v2.0.0", 0, "")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.confirmUpgrade("foo", "v2.0.0");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_FALSE(events.has("upgradeUninstallDone"));
|
|
}
|
|
|
|
LOGOS_TEST(confirmUpgrade_tag_mismatch_returns_error) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo", "core");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUpgrade("foo", "v2.0.0", 0, "")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.confirmUpgrade("foo", "v3.0.0");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(r["error"].get<std::string>().find("No matching pending upgrade") != std::string::npos);
|
|
|
|
// Original upgrade still pending — the mismatched tag must not have cleared state.
|
|
LOGOS_ASSERT_TRUE(impl.cancelUpgrade("foo", "v2.0.0")["success"].get<bool>());
|
|
}
|
|
|
|
LOGOS_TEST(confirmUpgrade_no_pending_returns_error) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap r = impl.confirmUpgrade("foo", "v1.0.0");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(r["error"].get<std::string>().find("No matching pending upgrade") != std::string::npos);
|
|
}
|
|
|
|
LOGOS_TEST(confirmUpgrade_requires_ack_before_confirm) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo", "core");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUpgrade("foo", "v2.0.0", 0, "")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.confirmUpgrade("foo", "v2.0.0");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(r["error"].get<std::string>().find("has not been acknowledged") != std::string::npos);
|
|
LOGOS_ASSERT_FALSE(events.has("upgradeUninstallDone"));
|
|
|
|
// Pending action should remain active until acknowledged/cancelled/reset.
|
|
// Ack first because cancelUpgrade (like confirmUpgrade) now requires a
|
|
// prior ack for protocol uniformity — see cancelUpgrade_requires_ack_before_cancel.
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.cancelUpgrade("foo", "v2.0.0")["success"].get<bool>());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// requestInstall / confirmInstall / cancelInstall: the fresh-install gate.
|
|
// Mirrors the upgrade gate but with no in-module uninstall step — confirm
|
|
// emits installApproved so the initiator runs its own download+install.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
LOGOS_TEST(requestInstall_rejects_empty_name) {
|
|
auto t = LogosTestContext("package_manager");
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LogosMap r = impl.requestInstall("", "v1.0.0", "https://repo", "");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(r["error"].get<std::string>(),
|
|
std::string("Package name cannot be empty"));
|
|
LOGOS_ASSERT_FALSE(events.has("beforeInstall"));
|
|
}
|
|
|
|
LOGOS_TEST(requestInstall_happy_emits_beforeInstall_with_repo_and_depChanges) {
|
|
auto t = LogosTestContext("package_manager");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
// A fresh install need not be already installed — the gate is pure
|
|
// confirmation. depChanges is opaque JSON the host dialog will render.
|
|
const std::string depChanges =
|
|
"[{\"name\":\"dep1\",\"action\":\"install\",\"toVersion\":\"1.2.0\"}]";
|
|
LogosMap r = impl.requestInstall("newpkg", "v1.0.0", "https://repo/x", depChanges);
|
|
LOGOS_ASSERT_TRUE(r["success"].get<bool>());
|
|
|
|
auto matches = events.all("beforeInstall");
|
|
LOGOS_ASSERT_EQ(matches.size(), static_cast<size_t>(1));
|
|
LogosMap payload = LogosMap::parse(matches[0].data);
|
|
LOGOS_ASSERT_EQ(payload["name"].get<std::string>(), std::string("newpkg"));
|
|
LOGOS_ASSERT_EQ(payload["releaseTag"].get<std::string>(), std::string("v1.0.0"));
|
|
LOGOS_ASSERT_EQ(payload["repositoryUrl"].get<std::string>(), std::string("https://repo/x"));
|
|
// depChanges is embedded as a parsed JSON array, not a re-stringified blob.
|
|
LOGOS_ASSERT_TRUE(payload["depChanges"].is_array());
|
|
LOGOS_ASSERT_EQ(payload["depChanges"].size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(payload["depChanges"][0]["name"].get<std::string>(), std::string("dep1"));
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(requestInstall_empty_depChanges_yields_empty_array) {
|
|
auto t = LogosTestContext("package_manager");
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestInstall("newpkg", "v1.0.0", "", "")["success"].get<bool>());
|
|
|
|
LogosMap payload = LogosMap::parse(events.all("beforeInstall")[0].data);
|
|
LOGOS_ASSERT_TRUE(payload["depChanges"].is_array());
|
|
LOGOS_ASSERT_EQ(payload["depChanges"].size(), static_cast<size_t>(0));
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(requestInstall_rejects_when_already_pending) {
|
|
auto t = LogosTestContext("package_manager");
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestInstall("newpkg", "v1.0.0", "", "")["success"].get<bool>());
|
|
|
|
LogosMap blocked = impl.requestInstall("other", "v1.0.0", "", "");
|
|
LOGOS_ASSERT_FALSE(blocked["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(blocked["error"].get<std::string>().find("install") != std::string::npos);
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(confirmInstall_happy_emits_installApproved) {
|
|
auto t = LogosTestContext("package_manager");
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestInstall("newpkg", "v1.0.0", "https://repo/x", "")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("newpkg")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.confirmInstall("newpkg");
|
|
LOGOS_ASSERT_TRUE(r["success"].get<bool>());
|
|
|
|
auto approved = events.all("installApproved");
|
|
LOGOS_ASSERT_EQ(approved.size(), static_cast<size_t>(1));
|
|
LogosMap payload = LogosMap::parse(approved[0].data);
|
|
LOGOS_ASSERT_EQ(payload["name"].get<std::string>(), std::string("newpkg"));
|
|
LOGOS_ASSERT_EQ(payload["releaseTag"].get<std::string>(), std::string("v1.0.0"));
|
|
LOGOS_ASSERT_EQ(payload["repositoryUrl"].get<std::string>(), std::string("https://repo/x"));
|
|
|
|
// Gate cleared — a fresh request must now succeed.
|
|
LOGOS_ASSERT_TRUE(impl.requestInstall("again", "v1.0.0", "", "")["success"].get<bool>());
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(confirmInstall_requires_ack_before_confirm) {
|
|
auto t = LogosTestContext("package_manager");
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestInstall("newpkg", "v1.0.0", "", "")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.confirmInstall("newpkg");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(r["error"].get<std::string>().find("has not been acknowledged") != std::string::npos);
|
|
LOGOS_ASSERT_FALSE(events.has("installApproved"));
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(cancelInstall_emits_installCancelled_with_user_reason) {
|
|
auto t = LogosTestContext("package_manager");
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestInstall("newpkg", "v1.0.0", "https://repo/x", "")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("newpkg")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.cancelInstall("newpkg");
|
|
LOGOS_ASSERT_TRUE(r["success"].get<bool>());
|
|
|
|
auto cancelled = events.all("installCancelled");
|
|
LOGOS_ASSERT_EQ(cancelled.size(), static_cast<size_t>(1));
|
|
LogosMap payload = LogosMap::parse(cancelled[0].data);
|
|
LOGOS_ASSERT_EQ(payload["name"].get<std::string>(), std::string("newpkg"));
|
|
LOGOS_ASSERT_EQ(payload["reason"].get<std::string>(), std::string("user cancelled"));
|
|
LOGOS_ASSERT_FALSE(events.has("installApproved"));
|
|
}
|
|
|
|
LOGOS_TEST(requestUpgrade_carries_depChanges_in_beforeUpgrade) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
const std::string depChanges =
|
|
"[{\"name\":\"dep1\",\"action\":\"upgrade\",\"fromVersion\":\"1.0.0\",\"toVersion\":\"1.2.0\"}]";
|
|
LOGOS_ASSERT_TRUE(impl.requestUpgrade("foo", "v2.0.0", 0, depChanges)["success"].get<bool>());
|
|
|
|
LogosMap payload = LogosMap::parse(events.all("beforeUpgrade")[0].data);
|
|
LOGOS_ASSERT_TRUE(payload["depChanges"].is_array());
|
|
LOGOS_ASSERT_EQ(payload["depChanges"].size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(payload["depChanges"][0]["action"].get<std::string>(), std::string("upgrade"));
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// cancelUninstall / cancelUpgrade: happy / missing / mismatch
|
|
// ---------------------------------------------------------------------------
|
|
|
|
LOGOS_TEST(cancelUninstall_emits_uninstallCancelled_with_user_reason) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUninstall("foo")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.cancelUninstall("foo");
|
|
LOGOS_ASSERT_TRUE(r["success"].get<bool>());
|
|
|
|
auto cancelled = events.all("uninstallCancelled");
|
|
LOGOS_ASSERT_EQ(cancelled.size(), static_cast<size_t>(1));
|
|
LogosMap payload = LogosMap::parse(cancelled[0].data);
|
|
LOGOS_ASSERT_EQ(payload["name"].get<std::string>(), std::string("foo"));
|
|
LOGOS_ASSERT_EQ(payload["reason"].get<std::string>(), std::string("user cancelled"));
|
|
|
|
// State cleared — a second cancel is a no-match error.
|
|
LogosMap again = impl.cancelUninstall("foo");
|
|
LOGOS_ASSERT_FALSE(again["success"].get<bool>());
|
|
}
|
|
|
|
LOGOS_TEST(cancelUninstall_no_pending_returns_error) {
|
|
auto t = LogosTestContext("package_manager");
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LogosMap r = impl.cancelUninstall("foo");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_FALSE(events.has("uninstallCancelled"));
|
|
}
|
|
|
|
LOGOS_TEST(cancelUpgrade_emits_upgradeCancelled_with_tag_and_reason) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUpgrade("foo", "v2.0.0", 0, "")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.cancelUpgrade("foo", "v2.0.0");
|
|
LOGOS_ASSERT_TRUE(r["success"].get<bool>());
|
|
|
|
auto cancelled = events.all("upgradeCancelled");
|
|
LOGOS_ASSERT_EQ(cancelled.size(), static_cast<size_t>(1));
|
|
LogosMap payload = LogosMap::parse(cancelled[0].data);
|
|
LOGOS_ASSERT_EQ(payload["name"].get<std::string>(), std::string("foo"));
|
|
LOGOS_ASSERT_EQ(payload["releaseTag"].get<std::string>(), std::string("v2.0.0"));
|
|
LOGOS_ASSERT_EQ(payload["reason"].get<std::string>(), std::string("user cancelled"));
|
|
}
|
|
|
|
LOGOS_TEST(cancelUpgrade_tag_mismatch_returns_error) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUpgrade("foo", "v2.0.0", 0, "")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.cancelUpgrade("foo", "v9.9.9");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_FALSE(events.has("upgradeCancelled"));
|
|
|
|
// Original pending survives the mismatched cancel.
|
|
LOGOS_ASSERT_TRUE(impl.cancelUpgrade("foo", "v2.0.0")["success"].get<bool>());
|
|
}
|
|
|
|
LOGOS_TEST(cancelUninstall_requires_ack_before_cancel) {
|
|
// Symmetric with confirmUninstall_requires_ack_before_confirm: cancel
|
|
// also demands a prior ack so the gated protocol's two-phase contract
|
|
// is uniform across all four confirm/cancel slots. An un-acked pending
|
|
// state belongs to the ack-reception timer — cancelling it directly
|
|
// would suppress the "no listener acknowledged" timeout event that
|
|
// initiators rely on for uniform cancellation handling.
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUninstall("foo")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.cancelUninstall("foo");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(r["error"].get<std::string>().find("has not been acknowledged") != std::string::npos);
|
|
// No uninstallCancelled event fires — the un-acked request is still
|
|
// owned by the timer, not by a confirming listener.
|
|
LOGOS_ASSERT_FALSE(events.has("uninstallCancelled"));
|
|
|
|
// Pending survives — an ack+cancel still succeeds.
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.cancelUninstall("foo")["success"].get<bool>());
|
|
}
|
|
|
|
LOGOS_TEST(cancelUpgrade_requires_ack_before_cancel) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUpgrade("foo", "v2.0.0", 0, "")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.cancelUpgrade("foo", "v2.0.0");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(r["error"].get<std::string>().find("has not been acknowledged") != std::string::npos);
|
|
LOGOS_ASSERT_FALSE(events.has("upgradeCancelled"));
|
|
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.cancelUpgrade("foo", "v2.0.0")["success"].get<bool>());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Ack-timeout worker: fires cancellation on no-ack; ack / confirm / cancel /
|
|
// reset all cancel the timer.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
LOGOS_TEST(ackTimeout_fires_uninstallCancelled_with_timeout_reason) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
// Hook must be set before requestUninstall — otherwise the worker is
|
|
// already running with the 3s default.
|
|
impl.setAckTimeoutMsForTest(30);
|
|
LOGOS_ASSERT_TRUE(impl.requestUninstall("foo")["success"].get<bool>());
|
|
|
|
// No ack within 30 ms — worker should fire uninstallCancelled.
|
|
auto e = events.waitFor("uninstallCancelled", 1000);
|
|
LOGOS_ASSERT_EQ(e.name, std::string("uninstallCancelled"));
|
|
LogosMap payload = LogosMap::parse(e.data);
|
|
LOGOS_ASSERT_EQ(payload["name"].get<std::string>(), std::string("foo"));
|
|
std::string reason = payload["reason"].get<std::string>();
|
|
LOGOS_ASSERT_TRUE(reason.find("no listener acknowledged") != std::string::npos);
|
|
LOGOS_ASSERT_TRUE(reason.find("30ms") != std::string::npos);
|
|
}
|
|
|
|
LOGOS_TEST(ackTimeout_fires_upgradeCancelled_with_timeout_reason) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
impl.setAckTimeoutMsForTest(30);
|
|
LOGOS_ASSERT_TRUE(impl.requestUpgrade("foo", "v2.0.0", 0, "")["success"].get<bool>());
|
|
|
|
auto e = events.waitFor("upgradeCancelled", 1000);
|
|
LOGOS_ASSERT_EQ(e.name, std::string("upgradeCancelled"));
|
|
LogosMap payload = LogosMap::parse(e.data);
|
|
LOGOS_ASSERT_EQ(payload["name"].get<std::string>(), std::string("foo"));
|
|
LOGOS_ASSERT_EQ(payload["releaseTag"].get<std::string>(), std::string("v2.0.0"));
|
|
LOGOS_ASSERT_TRUE(payload["reason"].get<std::string>().find("no listener acknowledged") != std::string::npos);
|
|
}
|
|
|
|
LOGOS_TEST(ack_cancels_timer_no_timeout_event) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
impl.setAckTimeoutMsForTest(30);
|
|
LOGOS_ASSERT_TRUE(impl.requestUninstall("foo")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
// Sleep past the timeout window — no cancellation should be emitted.
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
LOGOS_ASSERT_FALSE(events.has("uninstallCancelled"));
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(confirmUninstall_cancels_timer_no_timeout_event) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo", "core");
|
|
t.mockCFunction("uninstallPackage_success").returns(true);
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
impl.setAckTimeoutMsForTest(30);
|
|
LOGOS_ASSERT_TRUE(impl.requestUninstall("foo")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.confirmUninstall("foo")["success"].get<bool>());
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
LOGOS_ASSERT_FALSE(events.has("uninstallCancelled"));
|
|
// The real uninstall event did still fire.
|
|
LOGOS_ASSERT_TRUE(events.has("corePluginUninstalled"));
|
|
}
|
|
|
|
LOGOS_TEST(cancelUninstall_cancels_timer_only_user_reason_emitted) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
impl.setAckTimeoutMsForTest(30);
|
|
LOGOS_ASSERT_TRUE(impl.requestUninstall("foo")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.cancelUninstall("foo")["success"].get<bool>());
|
|
|
|
// Wait well past the short timeout — exactly one uninstallCancelled
|
|
// (from cancelUninstall) with reason "user cancelled", no duplicate
|
|
// from the worker.
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
auto cancelled = events.all("uninstallCancelled");
|
|
LOGOS_ASSERT_EQ(cancelled.size(), static_cast<size_t>(1));
|
|
LogosMap payload = LogosMap::parse(cancelled[0].data);
|
|
LOGOS_ASSERT_EQ(payload["reason"].get<std::string>(), std::string("user cancelled"));
|
|
}
|
|
|
|
LOGOS_TEST(resetPendingAction_cancels_timer_no_timeout_event) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
impl.setAckTimeoutMsForTest(30);
|
|
LOGOS_ASSERT_TRUE(impl.requestUninstall("foo")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.resetPendingAction()["success"].get<bool>());
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
LOGOS_ASSERT_FALSE(events.has("uninstallCancelled"));
|
|
LOGOS_ASSERT_FALSE(events.has("upgradeCancelled"));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// resetPendingAction: clears pending state so the next request goes through.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
LOGOS_TEST(resetPendingAction_allows_new_request) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackage("foo");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestUninstall("foo")["success"].get<bool>());
|
|
// Without reset the second request fails:
|
|
LOGOS_ASSERT_FALSE(impl.requestUninstall("foo")["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.resetPendingAction()["success"].get<bool>());
|
|
// Now it goes through again.
|
|
LOGOS_ASSERT_TRUE(impl.requestUninstall("foo")["success"].get<bool>());
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// requestMultiUninstall / confirmMultiUninstall / cancelMultiUninstall
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// Multi-package gated uninstall: same protocol as the single-package version
|
|
// (one pending slot, one ack, one confirm/cancel) but operates on a batch.
|
|
// Tests below mirror the single-uninstall structure (validation → happy →
|
|
// confirm → cancel → cross-op) plus the batch-specific bits: dependents
|
|
// dedup + batch-member exclusion, the multiUninstallCancelled payload shape.
|
|
|
|
namespace {
|
|
// Prime N user-installed packages — installedDependentsNames lookups in
|
|
// requestMultiUninstall need each one visible to isEmbedded.
|
|
static void primeInstalledUserPackages(const std::vector<std::string>& names,
|
|
const std::string& type = "core") {
|
|
std::vector<InstalledPackage> pkgs;
|
|
pkgs.reserve(names.size());
|
|
for (const auto& n : names) {
|
|
InstalledPackage p;
|
|
p.name = n;
|
|
p.type = type;
|
|
p.installType = InstallType::User;
|
|
pkgs.push_back(p);
|
|
}
|
|
setMockInstalledPackages(pkgs);
|
|
}
|
|
} // namespace
|
|
|
|
LOGOS_TEST(requestMultiUninstall_rejects_empty_list) {
|
|
auto t = LogosTestContext("package_manager");
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LogosMap r = impl.requestMultiUninstall({});
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(r["error"].get<std::string>(),
|
|
std::string("Package list cannot be empty"));
|
|
LOGOS_ASSERT_FALSE(events.has("beforeMultiUninstall"));
|
|
}
|
|
|
|
LOGOS_TEST(requestMultiUninstall_rejects_empty_name_in_list) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackages({"foo"});
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LogosMap r = impl.requestMultiUninstall({"foo", ""});
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(r["error"].get<std::string>(),
|
|
std::string("Package names cannot be empty"));
|
|
LOGOS_ASSERT_FALSE(events.has("beforeMultiUninstall"));
|
|
}
|
|
|
|
LOGOS_TEST(requestMultiUninstall_rejects_when_any_embedded) {
|
|
auto t = LogosTestContext("package_manager");
|
|
InstalledPackage user;
|
|
user.name = "foo";
|
|
user.type = "core";
|
|
user.installType = InstallType::User;
|
|
InstalledPackage embedded;
|
|
embedded.name = "core_embed";
|
|
embedded.type = "core";
|
|
embedded.installType = InstallType::Embedded;
|
|
setMockInstalledPackages({user, embedded});
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LogosMap r = impl.requestMultiUninstall({"foo", "core_embed"});
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
// Error message lists the offending embedded package(s).
|
|
std::string err = r["error"].get<std::string>();
|
|
LOGOS_ASSERT_TRUE(err.find("embedded") != std::string::npos);
|
|
LOGOS_ASSERT_TRUE(err.find("core_embed") != std::string::npos);
|
|
LOGOS_ASSERT_FALSE(events.has("beforeMultiUninstall"));
|
|
}
|
|
|
|
LOGOS_TEST(requestMultiUninstall_rejects_when_already_pending) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackages({"foo", "bar"});
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LogosMap first = impl.requestMultiUninstall({"foo", "bar"});
|
|
LOGOS_ASSERT_TRUE(first["success"].get<bool>());
|
|
|
|
// A subsequent multi-uninstall while one is pending must be rejected,
|
|
// and the error message must mention the in-progress op.
|
|
LogosMap second = impl.requestMultiUninstall({"foo", "bar"});
|
|
LOGOS_ASSERT_FALSE(second["success"].get<bool>());
|
|
std::string err = second["error"].get<std::string>();
|
|
LOGOS_ASSERT_TRUE(err.find("multi-uninstall") != std::string::npos);
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(requestMultiUninstall_emits_beforeMultiUninstall_with_names_and_dependents) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackages({"foo", "bar"});
|
|
|
|
// The same dependent tree is returned for every resolveDependents call
|
|
// (mock semantics). Root="foo" with child "parent_pkg" — verifies the
|
|
// payload carries `installedDependents`.
|
|
DependentTreeNode root;
|
|
root.name = "foo";
|
|
DependentTreeNode parent;
|
|
parent.name = "parent_pkg";
|
|
root.children = {parent};
|
|
setMockDependentTree(root);
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LogosMap r = impl.requestMultiUninstall({"foo", "bar"});
|
|
LOGOS_ASSERT_TRUE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_FALSE(r.contains("error"));
|
|
|
|
auto matches = events.all("beforeMultiUninstall");
|
|
LOGOS_ASSERT_EQ(matches.size(), static_cast<size_t>(1));
|
|
LogosMap payload = LogosMap::parse(matches[0].data);
|
|
|
|
// Payload carries the full `names` array (not a single `name` string).
|
|
LOGOS_ASSERT_TRUE(payload.contains("names"));
|
|
LOGOS_ASSERT_FALSE(payload.contains("name"));
|
|
LOGOS_ASSERT_EQ(payload["names"].size(), static_cast<size_t>(2));
|
|
LOGOS_ASSERT_EQ(payload["names"][0].get<std::string>(), std::string("foo"));
|
|
LOGOS_ASSERT_EQ(payload["names"][1].get<std::string>(), std::string("bar"));
|
|
|
|
// installedDependents has at least parent_pkg (mock returns same tree
|
|
// for both packages, dedup keeps it just once).
|
|
LOGOS_ASSERT_TRUE(payload["installedDependents"].is_array());
|
|
bool hasParent = false;
|
|
for (size_t i = 0; i < payload["installedDependents"].size(); ++i) {
|
|
if (payload["installedDependents"][i].get<std::string>() == "parent_pkg") {
|
|
hasParent = true;
|
|
break;
|
|
}
|
|
}
|
|
LOGOS_ASSERT_TRUE(hasParent);
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(requestMultiUninstall_dependents_excludes_batch_members) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackages({"foo", "bar"});
|
|
|
|
// Tree where one of the dependents IS a member of the batch (`bar`).
|
|
// The non-trivial logic under test: bar should NOT appear in
|
|
// installedDependents because it's already being uninstalled.
|
|
DependentTreeNode root;
|
|
root.name = "foo";
|
|
DependentTreeNode barNode;
|
|
barNode.name = "bar";
|
|
DependentTreeNode parent;
|
|
parent.name = "parent_pkg";
|
|
root.children = {barNode, parent};
|
|
setMockDependentTree(root);
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestMultiUninstall({"foo", "bar"})["success"].get<bool>());
|
|
|
|
auto matches = events.all("beforeMultiUninstall");
|
|
LogosMap payload = LogosMap::parse(matches[0].data);
|
|
// Walk installedDependents: bar must be filtered out, parent_pkg must remain.
|
|
bool sawBar = false;
|
|
bool sawParent = false;
|
|
for (size_t i = 0; i < payload["installedDependents"].size(); ++i) {
|
|
const std::string n = payload["installedDependents"][i].get<std::string>();
|
|
if (n == "bar") sawBar = true;
|
|
if (n == "parent_pkg") sawParent = true;
|
|
}
|
|
LOGOS_ASSERT_FALSE(sawBar);
|
|
LOGOS_ASSERT_TRUE(sawParent);
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(ackPendingAction_works_using_first_batch_name) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackages({"foo", "bar"});
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestMultiUninstall({"foo", "bar"})["success"].get<bool>());
|
|
|
|
// Per the impl: m_pendingAction.name is set to names[0], so single-name
|
|
// ackPendingAction continues to work unchanged for the multi flow.
|
|
LogosMap ack = impl.ackPendingAction("foo");
|
|
LOGOS_ASSERT_TRUE(ack["success"].get<bool>());
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(confirmMultiUninstall_all_succeed_emits_per_package_events) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackages({"foo", "bar"}, "core");
|
|
t.mockCFunction("uninstallPackage_success").returns(true);
|
|
t.mockCFunction("uninstallPackage_removed").returns("/m/x.dylib");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestMultiUninstall({"foo", "bar"})["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.confirmMultiUninstall({"foo", "bar"});
|
|
LOGOS_ASSERT_TRUE(r["success"].get<bool>());
|
|
// Per-package results array carries one entry per submitted name.
|
|
LOGOS_ASSERT_EQ(r["results"].size(), static_cast<size_t>(2));
|
|
LOGOS_ASSERT_EQ(r["results"][0]["name"].get<std::string>(), std::string("foo"));
|
|
LOGOS_ASSERT_TRUE(r["results"][0]["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(r["results"][1]["name"].get<std::string>(), std::string("bar"));
|
|
LOGOS_ASSERT_TRUE(r["results"][1]["success"].get<bool>());
|
|
|
|
// Per-package corePluginUninstalled events fired (one per uninstall, two total).
|
|
LOGOS_ASSERT_EQ(events.all("corePluginUninstalled").size(), static_cast<size_t>(2));
|
|
|
|
// A second confirm fails — pending was cleared.
|
|
LogosMap again = impl.confirmMultiUninstall({"foo", "bar"});
|
|
LOGOS_ASSERT_FALSE(again["success"].get<bool>());
|
|
}
|
|
|
|
LOGOS_TEST(confirmMultiUninstall_all_fail_returns_top_level_success_false) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackages({"foo", "bar"}, "core");
|
|
t.mockCFunction("uninstallPackage_success").returns(false);
|
|
t.mockCFunction("uninstallPackage_error").returns("delete failed");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestMultiUninstall({"foo", "bar"})["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.confirmMultiUninstall({"foo", "bar"});
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(r["results"].size(), static_cast<size_t>(2));
|
|
// Both results carry success=false and the per-package error.
|
|
for (size_t i = 0; i < r["results"].size(); ++i) {
|
|
LOGOS_ASSERT_FALSE(r["results"][i]["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(r["results"][i]["error"].get<std::string>(),
|
|
std::string("delete failed"));
|
|
}
|
|
// No per-package corePluginUninstalled events when uninstalls fail.
|
|
LOGOS_ASSERT_FALSE(events.has("corePluginUninstalled"));
|
|
}
|
|
|
|
LOGOS_TEST(confirmMultiUninstall_no_pending_returns_error) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap r = impl.confirmMultiUninstall({"foo", "bar"});
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(r["error"].get<std::string>()
|
|
.find("No matching pending multi-uninstall") != std::string::npos);
|
|
}
|
|
|
|
LOGOS_TEST(confirmMultiUninstall_name_mismatch_returns_error) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackages({"foo", "bar"}, "core");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestMultiUninstall({"foo", "bar"})["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
// Confirm with a different list — same first name but a different second name.
|
|
LogosMap r = impl.confirmMultiUninstall({"foo", "baz"});
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
|
|
// Pending state must still be valid — a correct cancel should succeed.
|
|
LOGOS_ASSERT_TRUE(impl.cancelMultiUninstall({"foo", "bar"})["success"].get<bool>());
|
|
}
|
|
|
|
LOGOS_TEST(confirmMultiUninstall_without_ack_returns_error) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackages({"foo", "bar"});
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestMultiUninstall({"foo", "bar"})["success"].get<bool>());
|
|
// Skip ackPendingAction — confirm should fail.
|
|
LogosMap r = impl.confirmMultiUninstall({"foo", "bar"});
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(r["error"].get<std::string>()
|
|
.find("not been acknowledged") != std::string::npos);
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(cancelMultiUninstall_emits_multiUninstallCancelled_with_names_array) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackages({"foo", "bar"});
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestMultiUninstall({"foo", "bar"})["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.cancelMultiUninstall({"foo", "bar"});
|
|
LOGOS_ASSERT_TRUE(r["success"].get<bool>());
|
|
|
|
auto matches = events.all("multiUninstallCancelled");
|
|
LOGOS_ASSERT_EQ(matches.size(), static_cast<size_t>(1));
|
|
LogosMap payload = LogosMap::parse(matches[0].data);
|
|
// Distinct from uninstallCancelled / upgradeCancelled: payload uses
|
|
// `names` (array) rather than `name` (string).
|
|
LOGOS_ASSERT_TRUE(payload.contains("names"));
|
|
LOGOS_ASSERT_FALSE(payload.contains("name"));
|
|
LOGOS_ASSERT_EQ(payload["names"].size(), static_cast<size_t>(2));
|
|
LOGOS_ASSERT_EQ(payload["reason"].get<std::string>(), std::string("user cancelled"));
|
|
}
|
|
|
|
LOGOS_TEST(cancelMultiUninstall_no_pending_returns_error) {
|
|
auto t = LogosTestContext("package_manager");
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap r = impl.cancelMultiUninstall({"foo", "bar"});
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(r["error"].get<std::string>()
|
|
.find("No matching pending multi-uninstall") != std::string::npos);
|
|
}
|
|
|
|
LOGOS_TEST(cancelMultiUninstall_name_mismatch_returns_error) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackages({"foo", "bar"});
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestMultiUninstall({"foo", "bar"})["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.cancelMultiUninstall({"foo", "baz"});
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
// Original pending still valid.
|
|
LOGOS_ASSERT_TRUE(impl.cancelMultiUninstall({"foo", "bar"})["success"].get<bool>());
|
|
}
|
|
|
|
LOGOS_TEST(requestMultiUninstall_blocks_subsequent_requestUninstall) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackages({"foo", "bar"});
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestMultiUninstall({"foo", "bar"})["success"].get<bool>());
|
|
|
|
// A single-package request while a multi is pending must fail.
|
|
LogosMap r = impl.requestUninstall("foo");
|
|
LOGOS_ASSERT_FALSE(r["success"].get<bool>());
|
|
std::string err = r["error"].get<std::string>();
|
|
LOGOS_ASSERT_TRUE(err.find("multi-uninstall") != std::string::npos);
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(requestMultiUninstall_dedupes_duplicate_input_names) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackages({"foo", "bar"});
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
// Caller passes duplicates; impl must dedupe so doUninstall isn't called
|
|
// twice for the same name in confirmMultiUninstall.
|
|
LogosMap r = impl.requestMultiUninstall({"foo", "foo", "bar", "foo"});
|
|
LOGOS_ASSERT_TRUE(r["success"].get<bool>());
|
|
|
|
auto matches = events.all("beforeMultiUninstall");
|
|
LogosMap payload = LogosMap::parse(matches[0].data);
|
|
// First-occurrence order preserved: foo (first), bar (third). The
|
|
// duplicate "foo"s drop out.
|
|
LOGOS_ASSERT_EQ(payload["names"].size(), static_cast<size_t>(2));
|
|
LOGOS_ASSERT_EQ(payload["names"][0].get<std::string>(), std::string("foo"));
|
|
LOGOS_ASSERT_EQ(payload["names"][1].get<std::string>(), std::string("bar"));
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(confirmMultiUninstall_accepts_caller_with_duplicates) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackages({"foo", "bar"}, "core");
|
|
t.mockCFunction("uninstallPackage_success").returns(true);
|
|
t.mockCFunction("uninstallPackage_removed").returns("/m/x.dylib");
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
// Stored pending state holds the deduped list — confirm with the original
|
|
// duplicated form must still match (both sides dedupe at the boundary)
|
|
// and must call doUninstall exactly once per unique name.
|
|
LOGOS_ASSERT_TRUE(impl.requestMultiUninstall({"foo", "foo", "bar"})["success"].get<bool>());
|
|
LOGOS_ASSERT_TRUE(impl.ackPendingAction("foo")["success"].get<bool>());
|
|
|
|
LogosMap r = impl.confirmMultiUninstall({"foo", "foo", "bar"});
|
|
LOGOS_ASSERT_TRUE(r["success"].get<bool>());
|
|
LOGOS_ASSERT_EQ(r["results"].size(), static_cast<size_t>(2)); // not 3
|
|
LOGOS_ASSERT_EQ(events.all("corePluginUninstalled").size(),
|
|
static_cast<size_t>(2)); // not 3
|
|
}
|
|
|
|
LOGOS_TEST(ackPendingAction_accepts_any_name_in_multi_uninstall_batch) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackages({"foo", "bar", "baz"});
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestMultiUninstall({"foo", "bar", "baz"})["success"].get<bool>());
|
|
|
|
// Listener acks with the THIRD batch member — must succeed. Coupling the
|
|
// wire protocol to names[0] specifically would force callers to know an
|
|
// internal storage detail.
|
|
LogosMap ack = impl.ackPendingAction("baz");
|
|
LOGOS_ASSERT_TRUE(ack["success"].get<bool>());
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
LOGOS_TEST(ackPendingAction_rejects_name_not_in_multi_uninstall_batch) {
|
|
auto t = LogosTestContext("package_manager");
|
|
primeInstalledUserPackages({"foo", "bar"});
|
|
|
|
EventCapture events;
|
|
PackageManagerImpl impl;
|
|
LOGOS_ASSERT_TRUE(impl.requestMultiUninstall({"foo", "bar"})["success"].get<bool>());
|
|
|
|
// Names not in the batch must still be rejected — the relaxation is
|
|
// "any batch member", not "any name at all".
|
|
LogosMap ack = impl.ackPendingAction("not_in_batch");
|
|
LOGOS_ASSERT_FALSE(ack["success"].get<bool>());
|
|
|
|
impl.resetPendingAction();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Dependency CONSTRAINTS across the module ABI: the constraint crosses, and an
|
|
// unconstrained package crosses byte-identically to before.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
LOGOS_TEST(getInstalledPackages_carries_dependency_constraints) {
|
|
auto t = LogosTestContext("package_manager");
|
|
InstalledPackage pkg;
|
|
pkg.name = "app";
|
|
pkg.version = "1.0.0";
|
|
pkg.dependencies = {"plain", "lib"};
|
|
PackageDependency constrained;
|
|
constrained.name = "lib";
|
|
constrained.version = "^2.0.0";
|
|
constrained.signer = "did:jwk:eyJrdHkiOiJPS1AifQ";
|
|
pkg.dependencyConstraints = {constrained};
|
|
setMockInstalledPackages({pkg});
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosList list = impl.getInstalledPackages();
|
|
LOGOS_ASSERT_EQ(list.size(), static_cast<size_t>(1));
|
|
|
|
// The edge set is unchanged: plain name STRINGS, both entries, in
|
|
// declared order — consumers index this array as text.
|
|
LOGOS_ASSERT_EQ(list[0]["dependencies"].size(), static_cast<size_t>(2));
|
|
LOGOS_ASSERT_EQ(list[0]["dependencies"][0].get<std::string>(), std::string("plain"));
|
|
LOGOS_ASSERT_EQ(list[0]["dependencies"][1].get<std::string>(), std::string("lib"));
|
|
|
|
LOGOS_ASSERT_TRUE(list[0].contains("dependencyConstraints"));
|
|
LOGOS_ASSERT_EQ(list[0]["dependencyConstraints"].size(), static_cast<size_t>(1));
|
|
LogosMap c = list[0]["dependencyConstraints"][0];
|
|
LOGOS_ASSERT_EQ(c["name"].get<std::string>(), std::string("lib"));
|
|
LOGOS_ASSERT_EQ(c["version"].get<std::string>(), std::string("^2.0.0"));
|
|
LOGOS_ASSERT_EQ(c["signer"].get<std::string>(), std::string("did:jwk:eyJrdHkiOiJPS1AifQ"));
|
|
}
|
|
|
|
LOGOS_TEST(getInstalledPackages_omits_constraints_for_bare_names) {
|
|
// The key must be ABSENT, not an empty array, so a reader that does not
|
|
// know it sees the payload it has always seen.
|
|
auto t = LogosTestContext("package_manager");
|
|
InstalledPackage pkg;
|
|
pkg.name = "app";
|
|
pkg.dependencies = {"lib"};
|
|
setMockInstalledPackages({pkg});
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosList list = impl.getInstalledPackages();
|
|
LOGOS_ASSERT_EQ(list.size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(list[0]["dependencies"].size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_FALSE(list[0].contains("dependencyConstraints"));
|
|
}
|
|
|
|
LOGOS_TEST(getInstalledPackages_carries_a_range_only_constraint) {
|
|
// `signer` is optional independently of `version`; a range-only entry must
|
|
// not grow an empty signer key.
|
|
auto t = LogosTestContext("package_manager");
|
|
InstalledPackage pkg;
|
|
pkg.name = "app";
|
|
pkg.dependencies = {"lib"};
|
|
PackageDependency constrained;
|
|
constrained.name = "lib";
|
|
constrained.version = "^2.0.0";
|
|
pkg.dependencyConstraints = {constrained};
|
|
setMockInstalledPackages({pkg});
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosList list = impl.getInstalledPackages();
|
|
LogosMap c = list[0]["dependencyConstraints"][0];
|
|
LOGOS_ASSERT_EQ(c["version"].get<std::string>(), std::string("^2.0.0"));
|
|
LOGOS_ASSERT_FALSE(c.contains("signer"));
|
|
}
|
|
|
|
// Child installed at a version its parent's range rejects: lib is 1.0.0, the
|
|
// edge asked for ^2.0.0.
|
|
static DependencyTreeNode makeVersionMismatchTree() {
|
|
DependencyTreeNode root;
|
|
root.name = "app";
|
|
root.status = DependencyStatus::Installed;
|
|
DependencyTreeNode lib;
|
|
lib.name = "lib";
|
|
lib.status = DependencyStatus::VersionMismatch;
|
|
lib.version = "1.0.0";
|
|
lib.installType = InstallType::User;
|
|
lib.requiredVersion = "^2.0.0";
|
|
lib.requiredSigner = "did:jwk:eyJrdHkiOiJPS1AifQ";
|
|
root.children = {lib};
|
|
return root;
|
|
}
|
|
|
|
LOGOS_TEST(resolveDependencies_surfaces_version_mismatch) {
|
|
auto t = LogosTestContext("package_manager");
|
|
setMockDependencyTree(makeVersionMismatchTree());
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap out = impl.resolveDependencies("app", true);
|
|
LogosMap dep = out["children"][0];
|
|
LOGOS_ASSERT_EQ(dep["status"].get<std::string>(), std::string("version_mismatch"));
|
|
// Installed, so it keeps the fields an installed node has; blanking them
|
|
// would hide the version that makes the mismatch actionable.
|
|
LOGOS_ASSERT_EQ(dep["version"].get<std::string>(), std::string("1.0.0"));
|
|
LOGOS_ASSERT_EQ(dep["installType"].get<std::string>(), std::string("user"));
|
|
LOGOS_ASSERT_EQ(dep["requiredVersion"].get<std::string>(), std::string("^2.0.0"));
|
|
LOGOS_ASSERT_EQ(dep["requiredSigner"].get<std::string>(),
|
|
std::string("did:jwk:eyJrdHkiOiJPS1AifQ"));
|
|
}
|
|
|
|
LOGOS_TEST(resolveFlatDependencies_surfaces_version_mismatch) {
|
|
// A status that only existed on the tree would never reach a list-shaped
|
|
// consumer.
|
|
auto t = LogosTestContext("package_manager");
|
|
setMockDependencyTree(makeVersionMismatchTree());
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosList flat = impl.resolveFlatDependencies("app", true);
|
|
LOGOS_ASSERT_EQ(flat.size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(flat[0]["name"].get<std::string>(), std::string("lib"));
|
|
LOGOS_ASSERT_EQ(flat[0]["status"].get<std::string>(), std::string("version_mismatch"));
|
|
LOGOS_ASSERT_EQ(flat[0]["requiredVersion"].get<std::string>(), std::string("^2.0.0"));
|
|
}
|
|
|
|
LOGOS_TEST(resolveDependencies_omits_constraint_keys_when_unconstrained) {
|
|
// Pinned as an EXACT KEY SET, not as a few absences: a new key is additive
|
|
// only if it is truly conditional, and `signerDid` is a property of the
|
|
// PACKAGE, not the edge, so it does not go absent just because this edge is
|
|
// unconstrained. Naming only requiredVersion/requiredSigner would not have
|
|
// caught it appearing here. These five are what this API emitted before.
|
|
auto t = LogosTestContext("package_manager");
|
|
setMockDependencyTree(makeForwardTree());
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap out = impl.resolveDependencies("root", true);
|
|
LogosMap dep = out["children"][0];
|
|
LOGOS_ASSERT_EQ(dep["status"].get<std::string>(), std::string("installed"));
|
|
LOGOS_ASSERT_EQ(dep.size(), static_cast<size_t>(5));
|
|
LOGOS_ASSERT_TRUE(dep.contains("name"));
|
|
LOGOS_ASSERT_TRUE(dep.contains("status"));
|
|
LOGOS_ASSERT_TRUE(dep.contains("version"));
|
|
LOGOS_ASSERT_TRUE(dep.contains("installType"));
|
|
LOGOS_ASSERT_TRUE(dep.contains("children"));
|
|
// Named individually too, so a failure says WHICH key appeared.
|
|
LOGOS_ASSERT_FALSE(dep.contains("requiredVersion"));
|
|
LOGOS_ASSERT_FALSE(dep.contains("requiredSigner"));
|
|
LOGOS_ASSERT_FALSE(dep.contains("signerDid"));
|
|
}
|
|
|
|
LOGOS_TEST(resolveDependencies_absent_dependency_keeps_its_declared_range) {
|
|
// Absence outranks mismatch — the library decides that, but a
|
|
// not_installed node still reports the range, so a caller can say WHICH
|
|
// version to go and install.
|
|
auto t = LogosTestContext("package_manager");
|
|
DependencyTreeNode root;
|
|
root.name = "app";
|
|
root.status = DependencyStatus::Installed;
|
|
DependencyTreeNode absent;
|
|
absent.name = "lib";
|
|
absent.status = DependencyStatus::NotInstalled;
|
|
absent.requiredVersion = "^2.0.0";
|
|
root.children = {absent};
|
|
setMockDependencyTree(root);
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap out = impl.resolveDependencies("app", true);
|
|
LogosMap dep = out["children"][0];
|
|
LOGOS_ASSERT_EQ(dep["status"].get<std::string>(), std::string("not_installed"));
|
|
LOGOS_ASSERT_EQ(dep["version"].get<std::string>(), std::string(""));
|
|
LOGOS_ASSERT_EQ(dep["requiredVersion"].get<std::string>(), std::string("^2.0.0"));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The signer identity, across the ABI
|
|
//
|
|
// Two different facts, and downstream needs both: `requiredSigner` is the pin
|
|
// the dependant declared, `signerDid` is what the installed package's own
|
|
// signature says of itself. The verdict is NOT the two compared — it comes
|
|
// from verifying under the pin's key, so a signer_mismatch row legitimately
|
|
// carries a signerDid that differs from requiredSigner.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
LOGOS_TEST(getInstalledPackages_carries_the_signer_did) {
|
|
auto t = LogosTestContext("package_manager");
|
|
InstalledPackage pkg;
|
|
pkg.name = "app";
|
|
pkg.version = "1.0.0";
|
|
pkg.signerDid = "did:jwk:eyJrdHkiOiJPS1AifQ";
|
|
setMockInstalledPackages({pkg});
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosList list = impl.getInstalledPackages();
|
|
LOGOS_ASSERT_EQ(list.size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(list[0]["signerDid"].get<std::string>(),
|
|
std::string("did:jwk:eyJrdHkiOiJPS1AifQ"));
|
|
}
|
|
|
|
LOGOS_TEST(getInstalledPackages_omits_the_signer_did_when_unsigned) {
|
|
// ABSENT, not an empty string: a reader must be able to tell "no
|
|
// signature" from signed, and an empty string reads as neither.
|
|
auto t = LogosTestContext("package_manager");
|
|
InstalledPackage pkg;
|
|
pkg.name = "app";
|
|
setMockInstalledPackages({pkg});
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosList list = impl.getInstalledPackages();
|
|
LOGOS_ASSERT_FALSE(list[0].contains("signerDid"));
|
|
}
|
|
|
|
// A dependency installed under the right name, signed by the WRONG KEY.
|
|
static DependencyTreeNode makeSignerMismatchTree() {
|
|
DependencyTreeNode root;
|
|
root.name = "app";
|
|
root.status = DependencyStatus::Installed;
|
|
DependencyTreeNode lib;
|
|
lib.name = "lib";
|
|
lib.status = DependencyStatus::SignerMismatch;
|
|
lib.version = "1.0.0";
|
|
lib.installType = InstallType::User;
|
|
lib.requiredSigner = "did:jwk:PINNED";
|
|
lib.signerDid = "did:jwk:SOMEBODY_ELSE";
|
|
root.children = {lib};
|
|
return root;
|
|
}
|
|
|
|
LOGOS_TEST(resolveDependencies_surfaces_signer_mismatch_with_both_dids) {
|
|
auto t = LogosTestContext("package_manager");
|
|
setMockDependencyTree(makeSignerMismatchTree());
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap out = impl.resolveDependencies("app", true);
|
|
LogosMap dep = out["children"][0];
|
|
LOGOS_ASSERT_EQ(dep["status"].get<std::string>(), std::string("signer_mismatch"));
|
|
LOGOS_ASSERT_EQ(dep["requiredSigner"].get<std::string>(), std::string("did:jwk:PINNED"));
|
|
LOGOS_ASSERT_EQ(dep["signerDid"].get<std::string>(),
|
|
std::string("did:jwk:SOMEBODY_ELSE"));
|
|
// A signer-mismatched package IS on disk, so it keeps these; the
|
|
// `Installed || VersionMismatch` chain this replaced blanked them.
|
|
LOGOS_ASSERT_EQ(dep["version"].get<std::string>(), std::string("1.0.0"));
|
|
LOGOS_ASSERT_EQ(dep["installType"].get<std::string>(), std::string("user"));
|
|
}
|
|
|
|
LOGOS_TEST(resolveFlatDependencies_surfaces_signer_mismatch) {
|
|
// The flat projection is the ONLY one basecamp's load gate reads; a status
|
|
// that reached the tree and not this list would block nothing.
|
|
auto t = LogosTestContext("package_manager");
|
|
setMockDependencyTree(makeSignerMismatchTree());
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosList flat = impl.resolveFlatDependencies("app", true);
|
|
LOGOS_ASSERT_EQ(flat.size(), static_cast<size_t>(1));
|
|
LOGOS_ASSERT_EQ(flat[0]["status"].get<std::string>(), std::string("signer_mismatch"));
|
|
LOGOS_ASSERT_EQ(flat[0]["requiredSigner"].get<std::string>(), std::string("did:jwk:PINNED"));
|
|
LOGOS_ASSERT_EQ(flat[0]["signerDid"].get<std::string>(),
|
|
std::string("did:jwk:SOMEBODY_ELSE"));
|
|
LOGOS_ASSERT_EQ(flat[0]["version"].get<std::string>(), std::string("1.0.0"));
|
|
}
|
|
|
|
LOGOS_TEST(resolveDependencies_surfaces_signer_unknown_without_a_signer_did) {
|
|
// Absence of evidence crosses as its own status with no signerDid key, so
|
|
// the far side names no signer it does not have.
|
|
auto t = LogosTestContext("package_manager");
|
|
DependencyTreeNode root;
|
|
root.name = "app";
|
|
root.status = DependencyStatus::Installed;
|
|
DependencyTreeNode lib;
|
|
lib.name = "lib";
|
|
lib.status = DependencyStatus::SignerUnknown;
|
|
lib.version = "1.0.0";
|
|
lib.installType = InstallType::Embedded;
|
|
lib.requiredSigner = "did:jwk:PINNED";
|
|
root.children = {lib};
|
|
setMockDependencyTree(root);
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap dep = impl.resolveDependencies("app", true)["children"][0];
|
|
LOGOS_ASSERT_EQ(dep["status"].get<std::string>(), std::string("signer_unknown"));
|
|
LOGOS_ASSERT_EQ(dep["requiredSigner"].get<std::string>(), std::string("did:jwk:PINNED"));
|
|
LOGOS_ASSERT_FALSE(dep.contains("signerDid"));
|
|
// Still on disk. An embedded package can NEVER carry a signature: only
|
|
// installPluginFile copies a manifest.sig into an install tree.
|
|
LOGOS_ASSERT_EQ(dep["version"].get<std::string>(), std::string("1.0.0"));
|
|
LOGOS_ASSERT_EQ(dep["installType"].get<std::string>(), std::string("embedded"));
|
|
}
|
|
|
|
LOGOS_TEST(resolveDependencies_omits_signer_did_for_an_absent_dependency) {
|
|
// NotInstalled blanks version/installType, and an absent package has no
|
|
// signature to report.
|
|
auto t = LogosTestContext("package_manager");
|
|
DependencyTreeNode root;
|
|
root.name = "app";
|
|
root.status = DependencyStatus::Installed;
|
|
DependencyTreeNode absent;
|
|
absent.name = "lib";
|
|
absent.status = DependencyStatus::NotInstalled;
|
|
absent.requiredSigner = "did:jwk:PINNED";
|
|
root.children = {absent};
|
|
setMockDependencyTree(root);
|
|
|
|
PackageManagerImpl impl;
|
|
|
|
LogosMap dep = impl.resolveDependencies("app", true)["children"][0];
|
|
LOGOS_ASSERT_EQ(dep["status"].get<std::string>(), std::string("not_installed"));
|
|
LOGOS_ASSERT_EQ(dep["version"].get<std::string>(), std::string(""));
|
|
LOGOS_ASSERT_FALSE(dep.contains("signerDid"));
|
|
// The pin still rides along, so a caller can name the publisher to get it
|
|
// from — as requiredVersion does on an absent row.
|
|
LOGOS_ASSERT_EQ(dep["requiredSigner"].get<std::string>(), std::string("did:jwk:PINNED"));
|
|
}
|