mirror of
https://github.com/logos-co/logos-package-manager.git
synced 2026-08-30 03:31:11 +00:00
feat(deps): evaluate a dependency's version range instead of only carrying it
resolveDependencies called build(dep.name) and discarded dep.version one
character from where it was needed, so a manifest declaring
{"name":"lib","version":"^2.0.0"} with lib 1.0.0 installed reported
"installed". The range travelled intact from metadata.json through the .lgx,
`lgx verify` and `lgpm install` onto disk, and was asked a question nowhere.
build() now takes the whole PackageDependency, because the range and the
signer live on the EDGE rather than on the package and so have to travel with
the recursion, and compares it with logos::semver::satisfies -- already
linked, already called at :114. This was a wiring gap, not a missing
capability; no new semver, no build-system change. A dependency installed at
a version its dependant refuses now reports DependencyStatus::VersionMismatch.
Precedence is deliberate: ABSENCE OUTRANKS MISMATCH. A range can only be
judged against a version we actually have, and "install it" is the remedy
either way, so a dependency that is both absent and constrained still reports
not_installed -- the stronger fact, and the one the user can act on. Naming
the weaker one would point at the wrong fix. Either way the declared range
rides along on the node, so a caller can say WHICH version to install.
An unparseable range is treated as unsatisfied rather than ignored: silently
dropping a typo'd range would fail open, and lgx verify already rejects the
syntax upstream, so a manifest reaching us with one bypassed that gate.
The signer is carried as data and compared by nobody -- who may sign a
dependency is a trust decision that does not belong to the scanner.
VersionMismatch is APPENDED to the enum, never inserted:
logos-package-manager-module compiles against this header and links
libpackage_manager_lib at run time, so existing enumerator values are ABI.
Both new node fields are omitted from the JSON when absent, so a tree of
bare-name dependencies -- every package in the workspace today -- serialises
byte-identically to before.
Tests 137 -> 148. Red on the base, asserted through dependencyStatusToString
so the probe compiles without the new enumerator:
Expected: "version_mismatch" Which is: "installed"
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7c5aad9ae5
commit
b7e2280738
@@ -55,14 +55,21 @@ void to_json(nlohmann::json& j, const DependencyTreeNode& n)
|
||||
j["status"] = dependencyStatusToString(n.status);
|
||||
// For Cycle and NotInstalled nodes, installType has no meaningful value;
|
||||
// emit the empty string to match the legacy wire format produced when
|
||||
// the lib returned JSON directly.
|
||||
if (n.status == DependencyStatus::Installed) {
|
||||
// the lib returned JSON directly. VersionMismatch resolved to a real
|
||||
// installed package, so it carries both — the version actually present is
|
||||
// the half of the report `requiredVersion` is compared against.
|
||||
if (n.status == DependencyStatus::Installed || n.status == DependencyStatus::VersionMismatch) {
|
||||
j["version"] = n.version;
|
||||
j["installType"] = installTypeToString(n.installType);
|
||||
} else {
|
||||
j["version"] = "";
|
||||
j["installType"] = "";
|
||||
}
|
||||
// Additive, and absent unless the parent edge declared one — so a tree of
|
||||
// bare-name dependencies, which is every package in the workspace today,
|
||||
// serialises byte-identically to before.
|
||||
if (n.requiredVersion) j["requiredVersion"] = *n.requiredVersion;
|
||||
if (n.requiredSigner) j["requiredSigner"] = *n.requiredSigner;
|
||||
j["children"] = n.children;
|
||||
}
|
||||
|
||||
|
||||
+45
-17
@@ -98,9 +98,10 @@ const char* installTypeToString(InstallType t) {
|
||||
|
||||
const char* dependencyStatusToString(DependencyStatus s) {
|
||||
switch (s) {
|
||||
case DependencyStatus::Installed: return "installed";
|
||||
case DependencyStatus::NotInstalled: return "not_installed";
|
||||
case DependencyStatus::Cycle: return "cycle";
|
||||
case DependencyStatus::Installed: return "installed";
|
||||
case DependencyStatus::NotInstalled: return "not_installed";
|
||||
case DependencyStatus::Cycle: return "cycle";
|
||||
case DependencyStatus::VersionMismatch: return "version_mismatch";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -817,37 +818,62 @@ std::optional<DependencyTreeNode> PackageManagerLib::resolveDependencies(const s
|
||||
// Recursive walk. Visited-on-path set for cycle detection; the tree is
|
||||
// expanded across different branches (diamond shapes) but a cycle
|
||||
// through a single branch produces a Cycle leaf to stop descent.
|
||||
//
|
||||
// Takes the whole PackageDependency rather than the name alone: the range
|
||||
// and the signer live on the EDGE, not on the package, so they have to
|
||||
// travel with the recursion to be evaluated at the node they constrain.
|
||||
// Passing only `dep.name` here is what made the constraint unreachable.
|
||||
std::set<std::string> path;
|
||||
std::function<DependencyTreeNode(const std::string&)> build = [&](const std::string& name) -> DependencyTreeNode {
|
||||
std::function<DependencyTreeNode(const PackageDependency&)> build =
|
||||
[&](const PackageDependency& dep) -> DependencyTreeNode {
|
||||
DependencyTreeNode node;
|
||||
node.name = name;
|
||||
node.name = dep.name;
|
||||
// Recorded before any early return, so a constraint is visible even on
|
||||
// an edge whose status is decided without consulting it.
|
||||
node.requiredVersion = dep.version;
|
||||
node.requiredSigner = dep.signer;
|
||||
|
||||
if (path.count(name)) {
|
||||
if (path.count(dep.name)) {
|
||||
node.status = DependencyStatus::Cycle;
|
||||
return node;
|
||||
}
|
||||
|
||||
auto it = byName.find(name);
|
||||
auto it = byName.find(dep.name);
|
||||
if (it == byName.end()) {
|
||||
// ABSENCE OUTRANKS MISMATCH, deliberately. A range can only be
|
||||
// judged against a version we actually have, and "install it" is
|
||||
// the action either way — reporting version_mismatch for a package
|
||||
// that is not there would name the weaker fact and point the user
|
||||
// at the wrong fix. The declared range still rides along on
|
||||
// `requiredVersion` so a caller can say which version to install.
|
||||
node.status = DependencyStatus::NotInstalled;
|
||||
return node;
|
||||
}
|
||||
|
||||
const auto& scan = it->second;
|
||||
node.status = DependencyStatus::Installed;
|
||||
node.version = scan.version;
|
||||
node.installType = scan.installType;
|
||||
// An absent range means the parent declared no constraint and anything
|
||||
// installed satisfies it. A range that does not PARSE is treated as
|
||||
// unsatisfied rather than ignored: silently dropping a typo'd range
|
||||
// would fail open, and `lgx verify` already rejects the syntax upstream
|
||||
// (logos::semver::valid_range), so reaching here with one means the
|
||||
// manifest bypassed that gate and deserves to be visible.
|
||||
node.status = (!dep.version || logos::semver::satisfies(scan.version, *dep.version))
|
||||
? DependencyStatus::Installed
|
||||
: DependencyStatus::VersionMismatch;
|
||||
|
||||
path.insert(name);
|
||||
path.insert(dep.name);
|
||||
node.children.reserve(scan.dependencies.size());
|
||||
for (const auto& dep : scan.dependencies) {
|
||||
node.children.push_back(build(dep.name));
|
||||
for (const auto& child : scan.dependencies) {
|
||||
node.children.push_back(build(child));
|
||||
}
|
||||
path.erase(name);
|
||||
path.erase(dep.name);
|
||||
return node;
|
||||
};
|
||||
|
||||
return build(packageName);
|
||||
// The root is not pointed at by any edge, so it carries no constraint.
|
||||
return build(PackageDependency(packageName));
|
||||
}
|
||||
|
||||
std::optional<DependentTreeNode> PackageManagerLib::resolveDependents(const std::string& packageName)
|
||||
@@ -934,10 +960,12 @@ std::vector<DependencyTreeNode> DependencyTreeNode::flatten() const
|
||||
queue.pop_front();
|
||||
if (!seen.insert(n->name).second) continue;
|
||||
DependencyTreeNode copy;
|
||||
copy.name = n->name;
|
||||
copy.status = n->status;
|
||||
copy.version = n->version;
|
||||
copy.installType = n->installType;
|
||||
copy.name = n->name;
|
||||
copy.status = n->status;
|
||||
copy.version = n->version;
|
||||
copy.installType = n->installType;
|
||||
copy.requiredVersion = n->requiredVersion;
|
||||
copy.requiredSigner = n->requiredSigner;
|
||||
out.push_back(std::move(copy));
|
||||
for (const auto& c : n->children) queue.push_back(&c);
|
||||
}
|
||||
|
||||
@@ -19,11 +19,24 @@ enum class InstallType {
|
||||
User,
|
||||
};
|
||||
|
||||
// Whether a dependency is currently installed, absent, or part of a cycle.
|
||||
// Whether a dependency is currently installed, absent, part of a cycle, or
|
||||
// installed at a version its dependant refused.
|
||||
//
|
||||
// New enumerators are APPENDED, never inserted. This enum crosses a shared
|
||||
// library boundary — logos-package-manager-module compiles against this header
|
||||
// and links libpackage_manager_lib at run time — so the numeric value of an
|
||||
// existing enumerator is ABI.
|
||||
enum class DependencyStatus {
|
||||
Installed,
|
||||
NotInstalled,
|
||||
Cycle,
|
||||
// The package IS installed, but its version does not satisfy the semver
|
||||
// range the depending manifest declared for this edge. Distinct from
|
||||
// NotInstalled on purpose: the two call for different remedies (install it
|
||||
// vs. change a version), and only absence can be asserted without reading
|
||||
// a constraint. `version` and `installType` are populated on such a node —
|
||||
// the version actually present is the whole point of the report.
|
||||
VersionMismatch,
|
||||
};
|
||||
|
||||
struct SignatureVerificationResult {
|
||||
@@ -134,14 +147,29 @@ struct InstalledPackage {
|
||||
struct DependencyTreeNode {
|
||||
std::string name;
|
||||
DependencyStatus status;
|
||||
std::string version; // empty unless status == Installed
|
||||
InstallType installType; // meaningful only if status == Installed
|
||||
// Empty for NotInstalled and Cycle; the version actually installed for
|
||||
// both Installed and VersionMismatch.
|
||||
std::string version;
|
||||
InstallType installType; // meaningful on the same two states
|
||||
// The constraint the PARENT declared on this edge, verbatim, absent when
|
||||
// the parent named the dependency without one (and always absent on the
|
||||
// root, which no edge points at). `requiredVersion` is what `status` was
|
||||
// judged against; `requiredSigner` is carried as data only — nothing in
|
||||
// this library compares it, because who may sign a dependency is a trust
|
||||
// decision that does not belong to the scanner.
|
||||
std::optional<std::string> requiredVersion;
|
||||
std::optional<std::string> requiredSigner;
|
||||
std::vector<DependencyTreeNode> children;
|
||||
|
||||
// BFS enumeration of descendants (this node is excluded), deduplicated
|
||||
// by name so diamonds and cycles don't produce repeats. The `children`
|
||||
// vectors on returned copies are left empty — consumers iterate the
|
||||
// flat output without double-counting.
|
||||
//
|
||||
// Note the dedup interacts with the per-edge constraint fields: when two
|
||||
// parents depend on the same package under different ranges, the flat list
|
||||
// keeps whichever edge BFS reached first. Callers that must see every
|
||||
// constraint on a package walk the tree instead of flattening it.
|
||||
std::vector<DependencyTreeNode> flatten() const;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "package_manager_lib.h"
|
||||
#include "package_manager_json.h"
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
#include <filesystem>
|
||||
@@ -628,3 +629,253 @@ TEST_F(DependencyResolutionTest, WellFormedDependenciesWarnAboutNothing) {
|
||||
// job.
|
||||
EXPECT_EQ(err.find("malformed dependencies[] entry"), std::string::npos) << err;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constraint EVALUATION.
|
||||
//
|
||||
// The block above proves the range survives the scan. These prove something
|
||||
// asks it a question. Before this, resolveDependencies called build(dep.name)
|
||||
// and threw dep.version away one character from where it was needed, so a
|
||||
// dependency pinned to ^2.0.0 with 1.0.0 installed reported "installed" —
|
||||
// the range was carried the whole way and evaluated nowhere.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_F(DependencyResolutionTest, UnsatisfiedRangeReportsVersionMismatch) {
|
||||
// THE headline case: neither constraint is met, and the old code said
|
||||
// "installed" because it never compared anything.
|
||||
writeManifestRawDeps(modulesDir, "app",
|
||||
json::array({ json{{"name", "lib"}, {"version", "^2.0.0"}} }));
|
||||
writeManifest(modulesDir, "lib", "core", {}, "1.0.0");
|
||||
|
||||
PackageManagerLib pm;
|
||||
pm.setEmbeddedModulesDirectory(modulesDir.string());
|
||||
|
||||
auto tree = pm.resolveDependencies("app");
|
||||
ASSERT_TRUE(tree);
|
||||
ASSERT_EQ(tree->children.size(), 1u);
|
||||
const auto& dep = tree->children[0];
|
||||
EXPECT_EQ(dep.name, "lib");
|
||||
EXPECT_EQ(dep.status, DependencyStatus::VersionMismatch);
|
||||
EXPECT_STREQ(dependencyStatusToString(dep.status), "version_mismatch");
|
||||
}
|
||||
|
||||
TEST_F(DependencyResolutionTest, SatisfiedRangeStaysInstalled) {
|
||||
// The control. Same shape, a version inside the range: nothing changes.
|
||||
writeManifestRawDeps(modulesDir, "app",
|
||||
json::array({ json{{"name", "lib"}, {"version", "^2.0.0"}} }));
|
||||
writeManifest(modulesDir, "lib", "core", {}, "2.1.0");
|
||||
|
||||
PackageManagerLib pm;
|
||||
pm.setEmbeddedModulesDirectory(modulesDir.string());
|
||||
|
||||
auto tree = pm.resolveDependencies("app");
|
||||
ASSERT_TRUE(tree);
|
||||
ASSERT_EQ(tree->children.size(), 1u);
|
||||
EXPECT_EQ(tree->children[0].status, DependencyStatus::Installed);
|
||||
}
|
||||
|
||||
TEST_F(DependencyResolutionTest, UnconstrainedDependencyIsNeverAMismatch) {
|
||||
// Every package in the fleet today is this case. Whatever is installed
|
||||
// satisfies "no range", so the new status must be unreachable for it.
|
||||
writeManifest(modulesDir, "app", "core", {"lib"});
|
||||
writeManifest(modulesDir, "lib", "core", {}, "0.0.1");
|
||||
|
||||
PackageManagerLib pm;
|
||||
pm.setEmbeddedModulesDirectory(modulesDir.string());
|
||||
|
||||
auto tree = pm.resolveDependencies("app");
|
||||
ASSERT_TRUE(tree);
|
||||
ASSERT_EQ(tree->children.size(), 1u);
|
||||
EXPECT_EQ(tree->children[0].status, DependencyStatus::Installed);
|
||||
EXPECT_FALSE(tree->children[0].requiredVersion.has_value());
|
||||
}
|
||||
|
||||
TEST_F(DependencyResolutionTest, AbsenceOutranksVersionMismatch) {
|
||||
// A dependency that is BOTH absent and version-constrained reports
|
||||
// absence. A range cannot be judged against a version we do not have, the
|
||||
// remedy is "install it" either way, and version_mismatch would point the
|
||||
// user at the wrong fix. The range still rides along so a caller can say
|
||||
// WHICH version to install.
|
||||
writeManifestRawDeps(modulesDir, "app",
|
||||
json::array({ json{{"name", "absent"}, {"version", "^2.0.0"}} }));
|
||||
|
||||
PackageManagerLib pm;
|
||||
pm.setEmbeddedModulesDirectory(modulesDir.string());
|
||||
|
||||
auto tree = pm.resolveDependencies("app");
|
||||
ASSERT_TRUE(tree);
|
||||
ASSERT_EQ(tree->children.size(), 1u);
|
||||
EXPECT_EQ(tree->children[0].status, DependencyStatus::NotInstalled);
|
||||
ASSERT_TRUE(tree->children[0].requiredVersion.has_value());
|
||||
EXPECT_EQ(*tree->children[0].requiredVersion, "^2.0.0");
|
||||
}
|
||||
|
||||
TEST_F(DependencyResolutionTest, MismatchedNodeCarriesBothVersions) {
|
||||
// "Needs ^2.0.0, have 1.4.2" takes two numbers. The installed one comes
|
||||
// from the node, the required one from the edge; a node that reported the
|
||||
// mismatch without the installed version would be half a diagnostic.
|
||||
writeManifestRawDeps(modulesDir, "app",
|
||||
json::array({ json{{"name", "lib"}, {"version", "^2.0.0"}} }));
|
||||
writeManifest(modulesDir, "lib", "core", {}, "1.4.2");
|
||||
|
||||
PackageManagerLib pm;
|
||||
pm.setEmbeddedModulesDirectory(modulesDir.string());
|
||||
|
||||
auto tree = pm.resolveDependencies("app");
|
||||
ASSERT_TRUE(tree);
|
||||
ASSERT_EQ(tree->children.size(), 1u);
|
||||
const auto& dep = tree->children[0];
|
||||
EXPECT_EQ(dep.status, DependencyStatus::VersionMismatch);
|
||||
EXPECT_EQ(dep.version, "1.4.2");
|
||||
EXPECT_EQ(dep.installType, InstallType::Embedded);
|
||||
ASSERT_TRUE(dep.requiredVersion.has_value());
|
||||
EXPECT_EQ(*dep.requiredVersion, "^2.0.0");
|
||||
}
|
||||
|
||||
TEST_F(DependencyResolutionTest, SignerIsCarriedButNotEvaluated) {
|
||||
// Deliberate scope line: whether a publisher DID is acceptable is a trust
|
||||
// decision, not a scan result. The pin travels as data so whoever settles
|
||||
// that policy has it; an unsatisfiable-looking signer changes no status.
|
||||
writeManifestRawDeps(modulesDir, "app",
|
||||
json::array({ json{{"name", "lib"},
|
||||
{"signer", "did:jwk:eyJrdHkiOiJPS1AifQ"}} }));
|
||||
writeManifest(modulesDir, "lib", "core", {}, "1.0.0");
|
||||
|
||||
PackageManagerLib pm;
|
||||
pm.setEmbeddedModulesDirectory(modulesDir.string());
|
||||
|
||||
auto tree = pm.resolveDependencies("app");
|
||||
ASSERT_TRUE(tree);
|
||||
ASSERT_EQ(tree->children.size(), 1u);
|
||||
const auto& dep = tree->children[0];
|
||||
EXPECT_EQ(dep.status, DependencyStatus::Installed);
|
||||
ASSERT_TRUE(dep.requiredSigner.has_value());
|
||||
EXPECT_EQ(*dep.requiredSigner, "did:jwk:eyJrdHkiOiJPS1AifQ");
|
||||
EXPECT_FALSE(dep.requiredVersion.has_value());
|
||||
}
|
||||
|
||||
TEST_F(DependencyResolutionTest, UnparseableRangeIsUnsatisfied) {
|
||||
// Fail CLOSED. Dropping a range we cannot parse would turn a typo into a
|
||||
// silently disabled check; `lgx verify` rejects the syntax upstream, so a
|
||||
// manifest that reaches us with one bypassed that gate and should show.
|
||||
writeManifestRawDeps(modulesDir, "app",
|
||||
json::array({ json{{"name", "lib"}, {"version", "not-a-range"}} }));
|
||||
writeManifest(modulesDir, "lib", "core", {}, "1.0.0");
|
||||
|
||||
PackageManagerLib pm;
|
||||
pm.setEmbeddedModulesDirectory(modulesDir.string());
|
||||
|
||||
auto tree = pm.resolveDependencies("app");
|
||||
ASSERT_TRUE(tree);
|
||||
ASSERT_EQ(tree->children.size(), 1u);
|
||||
EXPECT_EQ(tree->children[0].status, DependencyStatus::VersionMismatch);
|
||||
}
|
||||
|
||||
TEST_F(DependencyResolutionTest, VersionMismatchIsFoundDeepInTheTree) {
|
||||
// The constraint is a property of an EDGE, so it has to survive recursion,
|
||||
// not just the first level. a -> b -> c, and only b constrains c.
|
||||
writeManifest(modulesDir, "a", "core", {"b"});
|
||||
writeManifestRawDeps(modulesDir, "b",
|
||||
json::array({ json{{"name", "c"}, {"version", ">=3.0.0"}} }));
|
||||
writeManifest(modulesDir, "c", "core", {}, "2.9.9");
|
||||
|
||||
PackageManagerLib pm;
|
||||
pm.setEmbeddedModulesDirectory(modulesDir.string());
|
||||
|
||||
auto tree = pm.resolveDependencies("a");
|
||||
ASSERT_TRUE(tree);
|
||||
EXPECT_EQ(tree->status, DependencyStatus::Installed);
|
||||
ASSERT_EQ(tree->children.size(), 1u);
|
||||
EXPECT_EQ(tree->children[0].status, DependencyStatus::Installed);
|
||||
ASSERT_EQ(tree->children[0].children.size(), 1u);
|
||||
const auto& c = tree->children[0].children[0];
|
||||
EXPECT_EQ(c.name, "c");
|
||||
EXPECT_EQ(c.status, DependencyStatus::VersionMismatch);
|
||||
}
|
||||
|
||||
TEST_F(DependencyResolutionTest, VersionMismatchSurvivesFlatten) {
|
||||
// flatten() is what the C ABI and every flat-list consumer read; a status
|
||||
// that only exists on the tree would never reach them.
|
||||
writeManifest(modulesDir, "a", "core", {"b"});
|
||||
writeManifestRawDeps(modulesDir, "b",
|
||||
json::array({ json{{"name", "c"}, {"version", ">=3.0.0"}} }));
|
||||
writeManifest(modulesDir, "c", "core", {}, "2.9.9");
|
||||
|
||||
PackageManagerLib pm;
|
||||
pm.setEmbeddedModulesDirectory(modulesDir.string());
|
||||
|
||||
auto tree = pm.resolveDependencies("a");
|
||||
ASSERT_TRUE(tree);
|
||||
auto flat = tree->flatten();
|
||||
auto it = std::find_if(flat.begin(), flat.end(),
|
||||
[](const DependencyTreeNode& n) { return n.name == "c"; });
|
||||
ASSERT_NE(it, flat.end());
|
||||
EXPECT_EQ(it->status, DependencyStatus::VersionMismatch);
|
||||
ASSERT_TRUE(it->requiredVersion.has_value());
|
||||
EXPECT_EQ(*it->requiredVersion, ">=3.0.0");
|
||||
}
|
||||
|
||||
TEST_F(DependencyResolutionTest, RootCarriesNoConstraint) {
|
||||
// Nothing points AT the root, so it has no edge and no range — and it must
|
||||
// never be judged against one.
|
||||
writeManifestRawDeps(modulesDir, "app",
|
||||
json::array({ json{{"name", "lib"}, {"version", "^2.0.0"}} }));
|
||||
writeManifest(modulesDir, "lib", "core", {}, "1.0.0");
|
||||
|
||||
PackageManagerLib pm;
|
||||
pm.setEmbeddedModulesDirectory(modulesDir.string());
|
||||
|
||||
auto tree = pm.resolveDependencies("app");
|
||||
ASSERT_TRUE(tree);
|
||||
EXPECT_EQ(tree->status, DependencyStatus::Installed);
|
||||
EXPECT_FALSE(tree->requiredVersion.has_value());
|
||||
EXPECT_FALSE(tree->requiredSigner.has_value());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The `lgpm --json` wire format for a dependency tree. Pinned because the two
|
||||
// additions have to be ADDITIVE: a tree of bare-name dependencies — which is
|
||||
// every package in the workspace today — must serialise exactly as before, or
|
||||
// the change is a breaking one dressed up as a feature.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_F(DependencyResolutionTest, JsonOmitsConstraintKeysForBareNameDependencies) {
|
||||
writeManifest(modulesDir, "app", "core", {"lib"});
|
||||
writeManifest(modulesDir, "lib", "core", {}, "1.0.0");
|
||||
|
||||
PackageManagerLib pm;
|
||||
pm.setEmbeddedModulesDirectory(modulesDir.string());
|
||||
|
||||
auto tree = pm.resolveDependencies("app");
|
||||
ASSERT_TRUE(tree);
|
||||
json j = *tree;
|
||||
ASSERT_EQ(j["children"].size(), 1u);
|
||||
const json& dep = j["children"][0];
|
||||
EXPECT_EQ(dep["status"], "installed");
|
||||
EXPECT_FALSE(dep.contains("requiredVersion"));
|
||||
EXPECT_FALSE(dep.contains("requiredSigner"));
|
||||
}
|
||||
|
||||
TEST_F(DependencyResolutionTest, JsonReportsMismatchWithBothVersions) {
|
||||
writeManifestRawDeps(modulesDir, "app",
|
||||
json::array({ json{{"name", "lib"},
|
||||
{"version", "^2.0.0"},
|
||||
{"signer", "did:jwk:eyJrdHkiOiJPS1AifQ"}} }));
|
||||
writeManifest(modulesDir, "lib", "core", {}, "1.0.0");
|
||||
|
||||
PackageManagerLib pm;
|
||||
pm.setEmbeddedModulesDirectory(modulesDir.string());
|
||||
|
||||
auto tree = pm.resolveDependencies("app");
|
||||
ASSERT_TRUE(tree);
|
||||
json j = *tree;
|
||||
ASSERT_EQ(j["children"].size(), 1u);
|
||||
const json& dep = j["children"][0];
|
||||
EXPECT_EQ(dep["status"], "version_mismatch");
|
||||
// Installed, so it keeps the fields an installed node has — unlike
|
||||
// not_installed and cycle, which blank them.
|
||||
EXPECT_EQ(dep["version"], "1.0.0");
|
||||
EXPECT_EQ(dep["installType"], "embedded");
|
||||
EXPECT_EQ(dep["requiredVersion"], "^2.0.0");
|
||||
EXPECT_EQ(dep["requiredSigner"], "did:jwk:eyJrdHkiOiJPS1AifQ");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user