Skip to content

Commit 9758a00

Browse files
committed
fix(plugins): make managed downgrade guard ENOENT-safe and stop leaking stale resolution
Addresses ClawSweeper [P1]/[P2] on #85305. [P1] readManagedNpmRootInstalledDependency read package-lock.json with the strict readJson, so a fresh managed npm root (no lockfile yet) threw instead of returning null. The downgrade guard reads installed metadata before npm install creates the lockfile, so a fresh plugin install crashed. Switch to the ENOENT-safe readJsonIfExists (already used elsewhere in this file for the same lockfile), so a missing lockfile yields null (no installed dependency). [P2] When the guard preserved a newer installed dependency, the returned expectedNpmResolution spread the older incoming resolution and only overwrote version/integrity, leaving stale resolvedSpec/shasum/resolvedAt from the rejected downgrade target. Construct a clean resolution carrying only the kept version's name/version/integrity so persisted install records do not mix metadata. Tests: fresh-root reader test (returns null without a lockfile) and a preserved-resolution assertion (no stale shasum/resolvedSpec).
1 parent 51b946a commit 9758a00

4 files changed

Lines changed: 34 additions & 6 deletions

File tree

src/infra/npm-managed-root.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,21 @@ describe("managed npm root", () => {
373373
});
374374
});
375375

376+
it("returns null for a fresh managed root without a package-lock.json", async () => {
377+
// #85184 P1: the downgrade guard reads installed dependency metadata before
378+
// npm install creates package-lock.json. A fresh managed root has no
379+
// lockfile yet, so the reader must return null instead of throwing -
380+
// otherwise a fresh plugin install crashes before npm can create the
381+
// lockfile (regression caught by the downgrade guard's pre-install read).
382+
const npmRoot = await makeTempRoot();
383+
await expect(
384+
readManagedNpmRootInstalledDependency({
385+
npmRoot,
386+
packageName: "@openclaw/discord",
387+
}),
388+
).resolves.toBeNull();
389+
});
390+
376391
it("syncs managed peer dependencies from npm's resolved lockfile plan", async () => {
377392
const npmRoot = await makeTempRoot();
378393
await fs.writeFile(

src/infra/npm-managed-root.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1002,7 +1002,10 @@ export async function readManagedNpmRootInstalledDependency(params: {
10021002
packageName: string;
10031003
}): Promise<ManagedNpmRootInstalledDependency | null> {
10041004
const lockPath = path.join(params.npmRoot, "package-lock.json");
1005-
const parsed = await readJson<unknown>(lockPath);
1005+
// A fresh managed npm root has no package-lock.json yet (npm install creates
1006+
// it). Use the ENOENT-safe reader so a missing lockfile yields null (no
1007+
// installed dependency) instead of throwing and breaking fresh installs.
1008+
const parsed = await readJsonIfExists<unknown>(lockPath);
10061009
if (!isRecord(parsed) || !isRecord(parsed.packages)) {
10071010
return null;
10081011
}

src/plugins/install.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,13 @@ describe("installPluginFromNpmSpec downgrade guard (#85184 integration)", () =>
786786
// downstream install records/config match the managed npm root.
787787
if (result.ok) {
788788
expect(result.npmResolution?.version).toBe("0.11.2");
789+
// P2: the preserved resolution must not carry stale metadata from the
790+
// rejected downgrade target (incoming 0.10.0 reported shasum "abc" and a
791+
// 0.10.0 integrity). Only kept-version fields should survive, so install
792+
// records do not mix the kept version with the downgrade target's hashes.
793+
expect(result.npmResolution?.shasum).toBeUndefined();
794+
expect(result.npmResolution?.resolvedSpec).toBeUndefined();
795+
expect(result.npmResolution?.integrity).not.toBe("sha512-0.10.0");
789796
}
790797
});
791798

src/plugins/install.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -916,12 +916,15 @@ async function installPluginFromManagedNpmRoot(
916916
// install and trigger a rollback that would remove the plugin.
917917
const expectedNpmResolution: NpmSpecResolution =
918918
preserveAgainstDowngrade && installedDependency?.version
919-
? {
920-
...params.npmResolution,
919+
? // Describe the preserved (kept) dependency, not the older incoming
920+
// resolution. Only carry fields we can attribute to the kept version
921+
// (name/version/integrity from the managed root); leave incoming-only
922+
// fields (resolvedSpec/shasum/resolvedAt) unset so persisted install
923+
// records do not mix stale metadata from the rejected downgrade target.
924+
{
925+
name: params.npmResolution.name ?? params.packageName,
921926
version: installedDependency.version,
922-
...(installedDependency.integrity
923-
? { integrity: installedDependency.integrity }
924-
: { integrity: undefined }),
927+
integrity: installedDependency.integrity,
925928
}
926929
: params.npmResolution;
927930
const resolutionMismatch = resolveInstalledNpmResolutionMismatch({

0 commit comments

Comments
 (0)