Skip to content

Commit 88b1b9f

Browse files
committed
fix(update): fail closed on preserved plugin metadata miss
1 parent 965efbc commit 88b1b9f

2 files changed

Lines changed: 114 additions & 25 deletions

File tree

src/plugins/install.npm-spec.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ type MockNpmPackage = {
178178
skipLockfileEntry?: boolean;
179179
packArchivePath?: string;
180180
packTarballName?: string;
181+
viewFailure?: string;
181182
};
182183

183184
function writeNpmRootPackageLock(params: {
@@ -303,6 +304,16 @@ function mockNpmViewAndInstallMany(packages: MockNpmPackage[]) {
303304
}
304305
const viewPackage = viewPackagesByArgv.get(argvKey);
305306
if (viewPackage) {
307+
if (viewPackage.viewFailure) {
308+
return {
309+
code: 1,
310+
stdout: "",
311+
stderr: viewPackage.viewFailure,
312+
signal: null,
313+
killed: false,
314+
termination: "exit" as const,
315+
};
316+
}
306317
return successfulSpawn(
307318
JSON.stringify({
308319
name: viewPackage.packageName,
@@ -754,6 +765,75 @@ describe("installPluginFromNpmSpec", () => {
754765
expect(manifest.dependencies?.[packageName]).toBe("0.11.2");
755766
});
756767

768+
it("fails closed when preserved newer managed dependency metadata cannot be resolved", async () => {
769+
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
770+
const packageName = "@martian-engineering/lossless-claw";
771+
fs.mkdirSync(npmRoot, { recursive: true });
772+
fs.writeFileSync(
773+
path.join(npmRoot, "package.json"),
774+
`${JSON.stringify(
775+
{
776+
private: true,
777+
dependencies: {
778+
[packageName]: "0.11.2",
779+
},
780+
},
781+
null,
782+
2,
783+
)}\n`,
784+
"utf8",
785+
);
786+
writeInstalledNpmPlugin({
787+
npmRoot,
788+
packageName,
789+
version: "0.11.2",
790+
pluginId: "lossless-claw",
791+
});
792+
mockNpmViewAndInstallMany([
793+
{
794+
spec: `${packageName}@0.10.0`,
795+
packageName,
796+
version: "0.10.0",
797+
pluginId: "lossless-claw",
798+
integrity: "sha512-old",
799+
shasum: "oldshasum",
800+
npmRoot,
801+
},
802+
{
803+
spec: `${packageName}@0.11.2`,
804+
packageName,
805+
version: "0.11.2",
806+
pluginId: "lossless-claw",
807+
npmRoot,
808+
viewFailure: "npm ERR! registry unavailable",
809+
},
810+
]);
811+
812+
const result = await installPluginFromNpmSpec({
813+
spec: `${packageName}@0.10.0`,
814+
npmDir: npmRoot,
815+
mode: "update",
816+
logger: { info: () => {}, warn: () => {} },
817+
});
818+
819+
expect(result.ok).toBe(false);
820+
if (result.ok) {
821+
return;
822+
}
823+
expect(result.error).toContain(
824+
`Failed to resolve npm metadata for preserved newer managed dependency ${packageName}@0.11.2`,
825+
);
826+
expect(result.error).toContain("npm view failed: npm ERR! registry unavailable");
827+
const installCalls = runCommandWithTimeoutMock.mock.calls.filter((call) =>
828+
isManagedNpmInstallCommand(call[0]),
829+
);
830+
expect(installCalls).toHaveLength(0);
831+
const manifest = JSON.parse(
832+
await fs.promises.readFile(path.join(npmRoot, "package.json"), "utf8"),
833+
) as { dependencies?: Record<string, string> };
834+
expect(manifest.dependencies?.[packageName]).toBe("0.11.2");
835+
});
836+
757837
it("rejects npm installs when the installed artifact drifts from verified metadata", async () => {
758838
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
759839
mockNpmViewAndInstall({

src/plugins/install.ts

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -496,11 +496,19 @@ async function resolveReusableNewerManagedDependency(params: {
496496
requestedResolution: NpmSpecResolution;
497497
mode: "install" | "update";
498498
timeoutMs: number;
499-
}): Promise<{
500-
dependencySpec: string;
501-
installed: ManagedNpmRootInstalledDependency | null;
502-
resolution: NpmSpecResolution;
503-
} | null> {
499+
}): Promise<
500+
| {
501+
ok: true;
502+
dependencySpec: string;
503+
installed: ManagedNpmRootInstalledDependency | null;
504+
resolution: NpmSpecResolution;
505+
}
506+
| {
507+
ok: false;
508+
error: string;
509+
}
510+
| null
511+
> {
504512
const requestedVersion = params.requestedResolution.version;
505513
if (params.mode !== "update" || !requestedVersion) {
506514
return null;
@@ -530,32 +538,29 @@ async function resolveReusableNewerManagedDependency(params: {
530538
return null;
531539
}
532540
const preservedSpec = `${params.packageName}@${dependencySpec}`;
533-
let preservedResolution: NpmSpecResolution = {
534-
name: params.requestedResolution.name ?? params.packageName,
535-
version: dependencySpec,
536-
resolvedSpec: preservedSpec,
537-
};
538-
if (installed?.version === dependencySpec) {
539-
if (installed.integrity) {
540-
preservedResolution = {
541-
...preservedResolution,
542-
integrity: installed.integrity,
543-
};
544-
}
541+
let preservedResolution: NpmSpecResolution;
542+
if (installed?.version === dependencySpec && installed.integrity) {
543+
preservedResolution = {
544+
name: params.requestedResolution.name ?? params.packageName,
545+
version: dependencySpec,
546+
resolvedSpec: preservedSpec,
547+
integrity: installed.integrity,
548+
};
545549
} else {
546550
const metadataResult = await resolveNpmSpecMetadata({
547551
spec: preservedSpec,
548552
timeoutMs: params.timeoutMs,
549553
});
550-
preservedResolution = metadataResult.ok
551-
? metadataResult.metadata
552-
: {
553-
name: params.requestedResolution.name ?? params.packageName,
554-
version: dependencySpec,
555-
resolvedSpec: preservedSpec,
556-
};
554+
if (!metadataResult.ok) {
555+
return {
556+
ok: false,
557+
error: `Failed to resolve npm metadata for preserved newer managed dependency ${preservedSpec}: ${metadataResult.error}`,
558+
};
559+
}
560+
preservedResolution = metadataResult.metadata;
557561
}
558562
return {
563+
ok: true,
559564
dependencySpec,
560565
installed,
561566
resolution: {
@@ -711,12 +716,16 @@ async function installPluginFromManagedNpmRoot(
711716
mode: effectiveMode,
712717
timeoutMs,
713718
});
719+
if (reusableNewerDependency && !reusableNewerDependency.ok) {
720+
return { ok: false, error: reusableNewerDependency.error };
721+
}
714722
const expectedNpmResolution = reusableNewerDependency?.resolution ?? params.npmResolution;
715723
const managedDependencySpec = reusableNewerDependency?.dependencySpec ?? params.dependencySpec;
716724
const installedNewerDependency = reusableNewerDependency?.installed;
717725
if (
718726
reusableNewerDependency &&
719-
installedNewerDependency?.version === reusableNewerDependency.dependencySpec
727+
installedNewerDependency?.version === reusableNewerDependency.dependencySpec &&
728+
installedNewerDependency.integrity
720729
) {
721730
logger.warn?.(
722731
`Keeping newer installed ${params.packageName}@${installedNewerDependency.version} instead of downgrading to ${params.npmResolution.version}.`,

0 commit comments

Comments
 (0)