mirror of
https://github.com/logos-co/logos-package-manager-module.git
synced 2026-08-27 10:31:10 +00:00
* fix: emit the install event and report a path for QML-only ui_qml packages installPlugin() gated its corePluginFileInstalled/uiPluginFileInstalled event — and response["path"] — on installedPluginPath being non-empty. That out-param is a REPORTING detail, not the success signal: a QML-only ui_qml package has no backend library, so an older logos-package-manager left it empty on a perfectly good install. The result was that uiPluginFileInstalled never fired (Basecamp only discovered the plugin after an app restart) and "path" came back empty, which logos-package-manager-ui reads as failure and draws as a red RETRY. Success is now the library's own signal — a non-empty return from installPluginFile() — and the reported path falls back to that return value when installedPluginPath is empty, so this stays correct against an unpatched logos-package-manager. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Merge origin/master into feat/windows-cross master landed #58 (manifest version). Merged clean -- no conflicts -- then the lock was re-pinned to the merged logos-module-builder (L6) and logos-package-manager (L3). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1798 lines
74 KiB
C++
1798 lines
74 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();
|
|
}
|