



















@@ -34,6 +34,10 @@ function npmViewArgv(spec: string): string[] {
3434return ["npm", "view", spec, "name", "version", "dist.integrity", "dist.shasum", "--json"];
3535}
363637+function npmViewVersionsArgv(spec: string): string[] {
38+return ["npm", "view", spec, "versions", "--json"];
39+}
40+3741function expectNpmInstallIntoRoot(params: { calls: unknown[][]; npmRoot: string }) {
3842const installCalls = params.calls.filter(
3943(call) => Array.isArray(call[0]) && call[0][0] === "npm" && call[0][1] === "install",
@@ -134,6 +138,7 @@ type MockNpmPackage = {
134138hoistedDependency?: { name: string; version: string };
135139peerDependencies?: Record<string, string>;
136140expectedDependencySpec?: string;
141+versions?: string[];
137142installedVersion?: string;
138143installedIntegrity?: string;
139144skipLockfileEntry?: boolean;
@@ -178,6 +183,7 @@ function mockNpmViewAndInstall(params: {
178183hoistedDependency?: { name: string; version: string };
179184peerDependencies?: Record<string, string>;
180185expectedDependencySpec?: string;
186+versions?: string[];
181187installedVersion?: string;
182188installedIntegrity?: string;
183189skipLockfileEntry?: boolean;
@@ -204,6 +210,14 @@ function mockNpmViewAndInstallMany(packages: MockNpmPackage[]) {
204210}),
205211);
206212}
213+const versionsPackage = packages.find(
214+(pkg) => JSON.stringify(argv) === JSON.stringify(npmViewVersionsArgv(pkg.packageName)),
215+);
216+if (versionsPackage) {
217+return successfulSpawn(
218+JSON.stringify(versionsPackage.versions ?? [versionsPackage.version]),
219+);
220+}
207221if (argv[0] === "npm" && argv[1] === "install") {
208222const prefixIndex = argv.indexOf("--prefix");
209223const prefixValue = prefixIndex >= 0 ? argv[prefixIndex + 1] : undefined;
@@ -882,6 +896,45 @@ describe("installPluginFromNpmSpec", () => {
882896expect(rejected.error).toContain('"@openclaw/voice-call@beta"');
883897}
884898899+runCommandWithTimeoutMock.mockReset();
900+const officialNpmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
901+const warnings: string[] = [];
902+mockNpmViewAndInstallMany([
903+{
904+spec: "@openclaw/voice-call",
905+packageName: "@openclaw/voice-call",
906+version: "0.0.2-beta.1",
907+npmRoot: officialNpmRoot,
908+versions: ["0.0.1", "0.0.2-beta.1"],
909+},
910+{
911+spec: "@openclaw/voice-call@0.0.1",
912+packageName: "@openclaw/voice-call",
913+version: "0.0.1",
914+pluginId: "voice-call",
915+npmRoot: officialNpmRoot,
916+expectedDependencySpec: "0.0.1",
917+},
918+]);
919+920+const officialFallback = await installPluginFromNpmSpec({
921+spec: "@openclaw/voice-call",
922+npmDir: officialNpmRoot,
923+expectedPluginId: "voice-call",
924+trustedSourceLinkedOfficialInstall: true,
925+logger: {
926+info: () => {},
927+warn: (msg: string) => warnings.push(msg),
928+},
929+});
930+expect(officialFallback.ok).toBe(true);
931+if (!officialFallback.ok) {
932+return;
933+}
934+expect(officialFallback.npmResolution?.version).toBe("0.0.1");
935+expect(officialFallback.npmResolution?.resolvedSpec).toBe("@openclaw/voice-call@0.0.1");
936+expect(warnings.join("\n")).toContain("falling back to stable @openclaw/voice-call@0.0.1");
937+885938runCommandWithTimeoutMock.mockReset();
886939const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
887940mockNpmViewAndInstall({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。