


















@@ -34,7 +34,7 @@ function npmViewArgv(spec: string): string[] {
3434return ["npm", "view", spec, "name", "version", "dist.integrity", "dist.shasum", "--json"];
3535}
363637-function expectNpmInstallIntoRoot(params: { calls: unknown[][]; npmRoot: string; spec: string }) {
37+function expectNpmInstallIntoRoot(params: { calls: unknown[][]; npmRoot: string }) {
3838const installCalls = params.calls.filter(
3939(call) => Array.isArray(call[0]) && call[0][0] === "npm" && call[0][1] === "install",
4040);
@@ -49,7 +49,6 @@ function expectNpmInstallIntoRoot(params: { calls: unknown[][]; npmRoot: string;
4949"--no-fund",
5050"--prefix",
5151params.npmRoot,
52-params.spec,
5352]);
5453}
5554@@ -150,7 +149,6 @@ function mockNpmViewAndInstallMany(
150149peerDependencies?: Record<string, string>;
151150}>,
152151) {
153-const packagesBySpec = new Map(packages.map((pkg) => [pkg.spec, pkg]));
154152const packagesByName = new Map(packages.map((pkg) => [pkg.packageName, pkg]));
155153runCommandWithTimeoutMock.mockImplementation(async (argv: string[]) => {
156154const viewPackage = packages.find(
@@ -169,12 +167,21 @@ function mockNpmViewAndInstallMany(
169167);
170168}
171169if (argv[0] === "npm" && argv[1] === "install") {
172-const spec = argv.at(-1);
173-const pkg = spec ? packagesBySpec.get(spec) : undefined;
174-if (!pkg) {
175-throw new Error(`unexpected npm install spec: ${spec ?? ""}`);
170+const prefixIndex = argv.indexOf("--prefix");
171+const npmRoot = prefixIndex >= 0 ? argv[prefixIndex + 1] : undefined;
172+if (!npmRoot) {
173+throw new Error(`unexpected npm install command: ${argv.join(" ")}`);
174+}
175+const manifest = JSON.parse(fs.readFileSync(path.join(npmRoot, "package.json"), "utf8")) as {
176+dependencies?: Record<string, string>;
177+};
178+for (const packageName of Object.keys(manifest.dependencies ?? {})) {
179+const pkg = packagesByName.get(packageName);
180+if (!pkg) {
181+throw new Error(`unexpected managed npm dependency: ${packageName}`);
182+}
183+writeInstalledNpmPlugin(pkg);
176184}
177-writeInstalledNpmPlugin(pkg);
178185return successfulSpawn();
179186}
180187if (argv[0] === "npm" && argv[1] === "uninstall") {
@@ -236,7 +243,6 @@ describe("installPluginFromNpmSpec", () => {
236243expectNpmInstallIntoRoot({
237244calls: runCommandWithTimeoutMock.mock.calls,
238245 npmRoot,
239-spec: "@openclaw/voice-call@0.0.1",
240246});
241247});
242248@@ -348,7 +354,6 @@ describe("installPluginFromNpmSpec", () => {
348354expectNpmInstallIntoRoot({
349355calls: runCommandWithTimeoutMock.mock.calls,
350356 npmRoot,
351-spec: "dangerous-plugin@1.0.0",
352357});
353358});
354359@@ -525,7 +530,6 @@ describe("installPluginFromNpmSpec", () => {
525530expectNpmInstallIntoRoot({
526531calls: runCommandWithTimeoutMock.mock.calls,
527532 npmRoot,
528- spec,
529533});
530534},
531535);
@@ -599,10 +603,53 @@ describe("installPluginFromNpmSpec", () => {
599603expectNpmInstallIntoRoot({
600604calls: runCommandWithTimeoutMock.mock.calls,
601605 npmRoot,
602-spec: "@openclaw/voice-call@0.0.2",
603606});
604607});
605608609+it("preserves previously installed sibling plugins during npm install", async () => {
610+const stateDir = suiteTempRootTracker.makeTempDir();
611+const npmRoot = path.join(stateDir, "npm");
612+613+mockNpmViewAndInstallMany([
614+{
615+spec: "@openclaw/voice-call@0.0.1",
616+packageName: "@openclaw/voice-call",
617+version: "0.0.1",
618+pluginId: "voice-call",
619+ npmRoot,
620+},
621+{
622+spec: "@openclaw/whatsapp@0.0.1",
623+packageName: "@openclaw/whatsapp",
624+version: "0.0.1",
625+pluginId: "whatsapp",
626+ npmRoot,
627+},
628+]);
629+630+const result1 = await installPluginFromNpmSpec({
631+spec: "@openclaw/voice-call@0.0.1",
632+npmDir: npmRoot,
633+logger: { info: () => {}, warn: () => {} },
634+});
635+expect(result1.ok).toBe(true);
636+637+runCommandWithTimeoutMock.mockClear();
638+const result2 = await installPluginFromNpmSpec({
639+spec: "@openclaw/whatsapp@0.0.1",
640+npmDir: npmRoot,
641+logger: { info: () => {}, warn: () => {} },
642+});
643+expect(result2.ok).toBe(true);
644+645+expectNpmInstallIntoRoot({
646+calls: runCommandWithTimeoutMock.mock.calls,
647+ npmRoot,
648+});
649+expect(fs.existsSync(path.join(npmRoot, "node_modules", "@openclaw", "voice-call"))).toBe(true);
650+expect(fs.existsSync(path.join(npmRoot, "node_modules", "@openclaw", "whatsapp"))).toBe(true);
651+});
652+606653it("aborts when integrity drift callback rejects the fetched artifact", async () => {
607654mockNpmViewMetadataResult(runCommandWithTimeoutMock, {
608655name: "@openclaw/voice-call",
@@ -689,7 +736,6 @@ describe("installPluginFromNpmSpec", () => {
689736expectNpmInstallIntoRoot({
690737calls: runCommandWithTimeoutMock.mock.calls,
691738 npmRoot,
692-spec: "@openclaw/voice-call@beta",
693739});
694740});
695741});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。