



















@@ -810,6 +810,173 @@ describe("installPluginFromClawHub", () => {
810810expect(success.clawhub?.clawpackSize).toBeUndefined();
811811});
812812813+it("does not inherit package-level compatibility when version-specific compatibility is absent for pinned older version", async () => {
814+parseClawHubPluginSpecMock.mockReturnValueOnce({ name: "demo", version: "2026.6.8" });
815+resolveLatestVersionFromPackageMock.mockReturnValue("2026.6.10");
816+fetchClawHubPackageDetailMock.mockResolvedValueOnce({
817+package: {
818+name: "demo",
819+displayName: "Demo",
820+family: "code-plugin",
821+channel: "official",
822+isOfficial: true,
823+createdAt: 0,
824+updatedAt: 0,
825+latestVersion: "2026.6.10",
826+compatibility: {
827+pluginApiRange: ">=2026.6.10",
828+minGatewayVersion: "2026.6.10",
829+},
830+},
831+});
832+resolveCompatibilityHostVersionMock.mockReturnValue("2026.6.8");
833+fetchClawHubPackageVersionMock.mockResolvedValueOnce({
834+version: {
835+version: "2026.6.8",
836+createdAt: 0,
837+changelog: "",
838+sha256hash: "a9eac48c6129bc44b6f93c9a9f48f6c700d191b7279a1e1915f28df6f59bb1af",
839+},
840+});
841+842+const result = await installPluginFromClawHub({
843+spec: "clawhub:demo@2026.6.8",
844+baseUrl: "https://clawhub.ai",
845+});
846+847+expectSuccessfulClawHubInstall(result);
848+});
849+850+it("recovers version-specific compatibility from version endpoint when artifact metadata is sparse", async () => {
851+parseClawHubPluginSpecMock.mockReturnValueOnce({ name: "demo", version: "2026.6.8" });
852+resolveLatestVersionFromPackageMock.mockReturnValue("2026.6.10");
853+fetchClawHubPackageDetailMock.mockResolvedValueOnce({
854+package: {
855+name: "demo",
856+displayName: "Demo",
857+family: "code-plugin",
858+channel: "official",
859+isOfficial: true,
860+createdAt: 0,
861+updatedAt: 0,
862+latestVersion: "2026.6.10",
863+compatibility: {
864+pluginApiRange: ">=2026.6.10",
865+minGatewayVersion: "2026.6.10",
866+},
867+},
868+});
869+resolveCompatibilityHostVersionMock.mockReturnValue("2026.6.5");
870+// Artifact endpoint returns sparse metadata (no compatibility).
871+fetchClawHubPackageArtifactMock.mockResolvedValueOnce({
872+version: {
873+version: "2026.6.8",
874+createdAt: 0,
875+changelog: "",
876+sha256hash: "a9eac48c6129bc44b6f93c9a9f48f6c700d191b7279a1e1915f28df6f59bb1af",
877+},
878+});
879+// Version endpoint has the real version-specific compatibility.
880+fetchClawHubPackageVersionMock.mockResolvedValueOnce({
881+version: {
882+version: "2026.6.8",
883+createdAt: 0,
884+changelog: "",
885+sha256hash: "a9eac48c6129bc44b6f93c9a9f48f6c700d191b7279a1e1915f28df6f59bb1af",
886+compatibility: {
887+pluginApiRange: ">=2026.6.8",
888+minGatewayVersion: "2026.6.8",
889+},
890+},
891+});
892+893+const result = await installPluginFromClawHub({
894+spec: "clawhub:demo@2026.6.8",
895+baseUrl: "https://clawhub.ai",
896+});
897+898+const failure = expectInstallFailure(result);
899+expect(failure.error).toContain("2026.6.8");
900+});
901+902+it("fails closed when version endpoint is unavailable for sparse artifact metadata on pinned version", async () => {
903+parseClawHubPluginSpecMock.mockReturnValueOnce({ name: "demo", version: "2026.6.8" });
904+resolveLatestVersionFromPackageMock.mockReturnValue("2026.6.10");
905+fetchClawHubPackageDetailMock.mockResolvedValueOnce({
906+package: {
907+name: "demo",
908+displayName: "Demo",
909+family: "code-plugin",
910+channel: "official",
911+isOfficial: true,
912+createdAt: 0,
913+updatedAt: 0,
914+latestVersion: "2026.6.10",
915+compatibility: {
916+pluginApiRange: ">=2026.6.10",
917+minGatewayVersion: "2026.6.10",
918+},
919+},
920+});
921+resolveCompatibilityHostVersionMock.mockReturnValue("2026.6.8");
922+// Artifact endpoint returns sparse metadata (no compatibility).
923+fetchClawHubPackageArtifactMock.mockResolvedValueOnce({
924+version: {
925+version: "2026.6.8",
926+createdAt: 0,
927+changelog: "",
928+sha256hash: "a9eac48c6129bc44b6f93c9a9f48f6c700d191b7279a1e1915f28df6f59bb1af",
929+},
930+});
931+// Version endpoint fails.
932+fetchClawHubPackageVersionMock.mockRejectedValueOnce(new Error("500 Internal Server Error"));
933+934+const result = await installPluginFromClawHub({
935+spec: "clawhub:demo@2026.6.8",
936+baseUrl: "https://clawhub.ai",
937+});
938+939+const failure = expectInstallFailure(result);
940+expect(failure.ok).toBe(false);
941+});
942+943+it("enforces package-level compatibility for unpinned latest install when version response omits compatibility", async () => {
944+resolveLatestVersionFromPackageMock.mockReturnValue("2026.6.10");
945+fetchClawHubPackageDetailMock.mockResolvedValueOnce({
946+package: {
947+name: "demo",
948+displayName: "Demo",
949+family: "code-plugin",
950+channel: "official",
951+isOfficial: true,
952+createdAt: 0,
953+updatedAt: 0,
954+latestVersion: "2026.6.10",
955+compatibility: {
956+pluginApiRange: ">=2026.6.10",
957+minGatewayVersion: "2026.6.10",
958+},
959+},
960+});
961+resolveCompatibilityHostVersionMock.mockReturnValue("2026.6.8");
962+fetchClawHubPackageVersionMock.mockResolvedValueOnce({
963+version: {
964+version: "2026.6.10",
965+createdAt: 0,
966+changelog: "",
967+sha256hash: "a9eac48c6129bc44b6f93c9a9f48f6c700d191b7279a1e1915f28df6f59bb1af",
968+},
969+});
970+971+const result = await installPluginFromClawHub({
972+spec: "clawhub:demo",
973+baseUrl: "https://clawhub.ai",
974+});
975+976+const failure = expectInstallFailure(result);
977+expect(failure.error).toContain("2026.6.10");
978+});
979+813980it("installs when ClawHub advertises a wildcard plugin API range", async () => {
814981fetchClawHubPackageVersionMock.mockResolvedValueOnce({
815982version: {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。