diff --git a/src/plugins/clawhub.test.ts b/src/plugins/clawhub.test.ts index 63d0ebb3eb2..c7f9edc360a 100644 --- a/src/plugins/clawhub.test.ts +++ b/src/plugins/clawhub.test.ts @@ -93,9 +93,10 @@ async function expectClawHubInstallError(params: { }; }) { params.setup?.(); - await expect(installPluginFromClawHub({ spec: params.spec })).resolves.toMatchObject( - params.expected, - ); + const result = await installPluginFromClawHub({ spec: params.spec }); + const failure = expectInstallFailure(result); + expect(failure.code).toBe(params.expected.code); + expect(failure.error).toBe(params.expected.error); } function createLoggerSpies() { @@ -135,44 +136,107 @@ function expectClawHubInstallFlow(params: { version: string; archivePath: string; }) { - expect(fetchClawHubPackageDetailMock).toHaveBeenCalledWith( - expect.objectContaining({ - name: "demo", - baseUrl: params.baseUrl, - }), - ); - expect(fetchClawHubPackageVersionMock).toHaveBeenCalledWith( - expect.objectContaining({ - name: "demo", - version: params.version, - }), - ); - expect(fetchClawHubPackageArtifactMock).toHaveBeenCalledWith( - expect.objectContaining({ - name: "demo", - version: params.version, - }), - ); - expect(installPluginFromArchiveMock).toHaveBeenCalledWith( - expect.objectContaining({ - archivePath: params.archivePath, - }), - ); + expect(packageDetailCall().name).toBe("demo"); + expect(packageDetailCall().baseUrl).toBe(params.baseUrl); + expect(packageVersionCall().name).toBe("demo"); + expect(packageVersionCall().version).toBe(params.version); + expect(packageArtifactCall().name).toBe("demo"); + expect(packageArtifactCall().version).toBe(params.version); + expect(archiveInstallCall().archivePath).toBe(params.archivePath); } function expectSuccessfulClawHubInstall(result: unknown) { - expect(result).toMatchObject({ - ok: true, - pluginId: "demo", - version: "2026.3.22", - clawhub: { - source: "clawhub", - clawhubPackage: "demo", - clawhubFamily: "code-plugin", - clawhubChannel: "official", - integrity: DEMO_ARCHIVE_INTEGRITY, - }, - }); + const success = expectInstallSuccess(result); + expect(success.pluginId).toBe("demo"); + expect(success.version).toBe("2026.3.22"); + expect(success.clawhub?.source).toBe("clawhub"); + expect(success.clawhub?.clawhubPackage).toBe("demo"); + expect(success.clawhub?.clawhubFamily).toBe("code-plugin"); + expect(success.clawhub?.clawhubChannel).toBe("official"); + expect(success.clawhub?.integrity).toBe(DEMO_ARCHIVE_INTEGRITY); +} + +type MockWithCalls = { + mock: { + calls: readonly (readonly unknown[])[]; + }; +}; + +type PackageLookupCall = { + artifact?: string; + baseUrl?: string; + name?: string; + version?: string; +}; + +type ArchiveInstallCall = { + archivePath?: string; + dangerouslyForceUnsafeInstall?: boolean; + trustedSourceLinkedOfficialInstall?: boolean; +}; + +type InstallSuccess = { + clawhub?: Record; + ok: true; + pluginId?: string; + version?: string; +}; + +type InstallFailure = { + code?: string; + error: string; + ok: false; +}; + +function mockCallArg(mock: MockWithCalls, callIndex = 0, argIndex = 0): unknown { + const call = mock.mock.calls[callIndex]; + if (!call) { + throw new Error(`Expected mock call ${callIndex}`); + } + if (call.length <= argIndex) { + throw new Error(`Expected mock call ${callIndex} argument ${argIndex}`); + } + return call[argIndex]; +} + +function packageDetailCall(callIndex = 0): PackageLookupCall { + return mockCallArg(fetchClawHubPackageDetailMock, callIndex) as PackageLookupCall; +} + +function packageVersionCall(callIndex = 0): PackageLookupCall { + return mockCallArg(fetchClawHubPackageVersionMock, callIndex) as PackageLookupCall; +} + +function packageArtifactCall(callIndex = 0): PackageLookupCall { + return mockCallArg(fetchClawHubPackageArtifactMock, callIndex) as PackageLookupCall; +} + +function archiveDownloadCall(callIndex = 0): PackageLookupCall { + return mockCallArg(downloadClawHubPackageArchiveMock, callIndex) as PackageLookupCall; +} + +function archiveInstallCall(callIndex = 0): ArchiveInstallCall { + return mockCallArg(installPluginFromArchiveMock, callIndex) as ArchiveInstallCall; +} + +function expectInstallSuccess(result: unknown): InstallSuccess { + expect((result as { ok?: unknown }).ok).toBe(true); + return result as InstallSuccess; +} + +function expectInstallFailure(result: unknown): InstallFailure { + expect((result as { ok?: unknown }).ok).toBe(false); + return result as InstallFailure; +} + +function expectInstallFailureFields( + result: unknown, + code: (typeof CLAWHUB_INSTALL_ERROR_CODE)[keyof typeof CLAWHUB_INSTALL_ERROR_CODE], + error: string, +) { + const failure = expectInstallFailure(result); + expect(failure.code).toBe(code); + expect(failure.error).toBe(error); } describe("installPluginFromClawHub", () => { @@ -289,11 +353,7 @@ describe("installPluginFromClawHub", () => { baseUrl: "https://clawhub.ai", }); - expect(installPluginFromArchiveMock).toHaveBeenCalledWith( - expect.objectContaining({ - trustedSourceLinkedOfficialInstall: true, - }), - ); + expect(archiveInstallCall().trustedSourceLinkedOfficialInstall).toBe(true); }); it("resolves explicit ClawHub dist tags before fetching version metadata", async () => { @@ -323,18 +383,10 @@ describe("installPluginFromClawHub", () => { }); expectSuccessfulClawHubInstall(result); - expect(fetchClawHubPackageVersionMock).toHaveBeenCalledWith( - expect.objectContaining({ - name: "demo", - version: "2026.3.22", - }), - ); - expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledWith( - expect.objectContaining({ - name: "demo", - version: "2026.3.22", - }), - ); + expect(packageVersionCall().name).toBe("demo"); + expect(packageVersionCall().version).toBe("2026.3.22"); + expect(archiveDownloadCall().name).toBe("demo"); + expect(archiveDownloadCall().version).toBe("2026.3.22"); }); it("returns ClawPack metadata from compatible ClawHub package versions", async () => { @@ -376,26 +428,18 @@ describe("installPluginFromClawHub", () => { baseUrl: "https://clawhub.ai", }); - expect(result).toMatchObject({ - ok: true, - clawhub: { - integrity: DEMO_CLAWPACK_INTEGRITY, - artifactKind: "npm-pack", - artifactFormat: "tgz", - npmIntegrity: "sha512-clawpack", - npmShasum: "1".repeat(40), - npmTarballName: "demo-2026.3.22.tgz", - clawpackSha256: DEMO_CLAWPACK_SHA256, - clawpackSize: 4096, - }, - }); - expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledWith( - expect.objectContaining({ - artifact: "clawpack", - name: "demo", - version: "2026.3.22", - }), - ); + const success = expectInstallSuccess(result); + expect(success.clawhub?.integrity).toBe(DEMO_CLAWPACK_INTEGRITY); + expect(success.clawhub?.artifactKind).toBe("npm-pack"); + expect(success.clawhub?.artifactFormat).toBe("tgz"); + expect(success.clawhub?.npmIntegrity).toBe("sha512-clawpack"); + expect(success.clawhub?.npmShasum).toBe("1".repeat(40)); + expect(success.clawhub?.npmTarballName).toBe("demo-2026.3.22.tgz"); + expect(success.clawhub?.clawpackSha256).toBe(DEMO_CLAWPACK_SHA256); + expect(success.clawhub?.clawpackSize).toBe(4096); + expect(archiveDownloadCall().artifact).toBe("clawpack"); + expect(archiveDownloadCall().name).toBe("demo"); + expect(archiveDownloadCall().version).toBe("2026.3.22"); }); it("uses the artifact resolver response as the install decision", async () => { @@ -439,30 +483,18 @@ describe("installPluginFromClawHub", () => { baseUrl: "https://clawhub.ai", }); - expect(result).toMatchObject({ - ok: true, - clawhub: { - artifactKind: "npm-pack", - artifactFormat: "tgz", - npmIntegrity: "sha512-clawpack", - npmShasum: "1".repeat(40), - clawpackSha256: DEMO_CLAWPACK_SHA256, - }, - }); - expect(fetchClawHubPackageArtifactMock).toHaveBeenCalledWith( - expect.objectContaining({ - name: "demo", - version: "2026.3.22", - }), - ); + const success = expectInstallSuccess(result); + expect(success.clawhub?.artifactKind).toBe("npm-pack"); + expect(success.clawhub?.artifactFormat).toBe("tgz"); + expect(success.clawhub?.npmIntegrity).toBe("sha512-clawpack"); + expect(success.clawhub?.npmShasum).toBe("1".repeat(40)); + expect(success.clawhub?.clawpackSha256).toBe(DEMO_CLAWPACK_SHA256); + expect(packageArtifactCall().name).toBe("demo"); + expect(packageArtifactCall().version).toBe("2026.3.22"); expect(fetchClawHubPackageVersionMock).not.toHaveBeenCalled(); - expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledWith( - expect.objectContaining({ - artifact: "clawpack", - name: "demo", - version: "2026.3.22", - }), - ); + expect(archiveDownloadCall().artifact).toBe("clawpack"); + expect(archiveDownloadCall().name).toBe("demo"); + expect(archiveDownloadCall().version).toBe("2026.3.22"); }); it("accepts the live ClawHub artifact resolver shape with kind/sha256 field names", async () => { @@ -497,24 +529,16 @@ describe("installPluginFromClawHub", () => { baseUrl: "https://clawhub.ai", }); - expect(result).toMatchObject({ - ok: true, - clawhub: { - artifactKind: "npm-pack", - artifactFormat: "tgz", - npmIntegrity: "sha512-clawpack", - npmShasum: "1".repeat(40), - clawpackSha256: DEMO_CLAWPACK_SHA256, - }, - }); + const success = expectInstallSuccess(result); + expect(success.clawhub?.artifactKind).toBe("npm-pack"); + expect(success.clawhub?.artifactFormat).toBe("tgz"); + expect(success.clawhub?.npmIntegrity).toBe("sha512-clawpack"); + expect(success.clawhub?.npmShasum).toBe("1".repeat(40)); + expect(success.clawhub?.clawpackSha256).toBe(DEMO_CLAWPACK_SHA256); expect(fetchClawHubPackageVersionMock).not.toHaveBeenCalled(); - expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledWith( - expect.objectContaining({ - artifact: "clawpack", - name: "demo", - version: "2026.3.22", - }), - ); + expect(archiveDownloadCall().artifact).toBe("clawpack"); + expect(archiveDownloadCall().name).toBe("demo"); + expect(archiveDownloadCall().version).toBe("2026.3.22"); }); it("accepts the live ClawHub legacy zip resolver shape with kind/sha256 field names", async () => { @@ -542,23 +566,15 @@ describe("installPluginFromClawHub", () => { baseUrl: "https://clawhub.ai", }); - expect(result).toMatchObject({ - ok: true, - pluginId: "demo", - clawhub: { - artifactKind: "legacy-zip", - artifactFormat: "zip", - integrity: DEMO_ARCHIVE_INTEGRITY, - }, - }); + const success = expectInstallSuccess(result); + expect(success.pluginId).toBe("demo"); + expect(success.clawhub?.artifactKind).toBe("legacy-zip"); + expect(success.clawhub?.artifactFormat).toBe("zip"); + expect(success.clawhub?.integrity).toBe(DEMO_ARCHIVE_INTEGRITY); expect(fetchClawHubPackageVersionMock).not.toHaveBeenCalled(); - expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledWith( - expect.objectContaining({ - artifact: "archive", - name: "demo", - version: "2026.3.22", - }), - ); + expect(archiveDownloadCall().artifact).toBe("archive"); + expect(archiveDownloadCall().name).toBe("demo"); + expect(archiveDownloadCall().version).toBe("2026.3.22"); }); it("falls back to version metadata when the ClawHub artifact resolver route is missing", async () => { @@ -609,27 +625,15 @@ describe("installPluginFromClawHub", () => { baseUrl: "https://clawhub.ai", }); - expect(result).toMatchObject({ - ok: true, - clawhub: { - artifactKind: "npm-pack", - npmIntegrity: "sha512-clawpack", - clawpackSha256: DEMO_CLAWPACK_SHA256, - }, - }); - expect(fetchClawHubPackageVersionMock).toHaveBeenCalledWith( - expect.objectContaining({ - name: "demo", - version: "2026.3.22", - }), - ); - expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledWith( - expect.objectContaining({ - artifact: "clawpack", - name: "demo", - version: "2026.3.22", - }), - ); + const success = expectInstallSuccess(result); + expect(success.clawhub?.artifactKind).toBe("npm-pack"); + expect(success.clawhub?.npmIntegrity).toBe("sha512-clawpack"); + expect(success.clawhub?.clawpackSha256).toBe(DEMO_CLAWPACK_SHA256); + expect(packageVersionCall().name).toBe("demo"); + expect(packageVersionCall().version).toBe("2026.3.22"); + expect(archiveDownloadCall().artifact).toBe("clawpack"); + expect(archiveDownloadCall().name).toBe("demo"); + expect(archiveDownloadCall().version).toBe("2026.3.22"); }); it("installs ClawPack artifacts when version metadata has no legacy archive hash", async () => { @@ -664,23 +668,11 @@ describe("installPluginFromClawHub", () => { baseUrl: "https://clawhub.ai", }); - expect(result).toMatchObject({ - ok: true, - clawhub: { - integrity: DEMO_CLAWPACK_INTEGRITY, - clawpackSha256: DEMO_CLAWPACK_SHA256, - }, - }); - expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledWith( - expect.objectContaining({ - artifact: "clawpack", - }), - ); - expect(installPluginFromArchiveMock).toHaveBeenCalledWith( - expect.objectContaining({ - archivePath: "/tmp/clawhub-demo/demo-2026.3.22.tgz", - }), - ); + const success = expectInstallSuccess(result); + expect(success.clawhub?.integrity).toBe(DEMO_CLAWPACK_INTEGRITY); + expect(success.clawhub?.clawpackSha256).toBe(DEMO_CLAWPACK_SHA256); + expect(archiveDownloadCall().artifact).toBe("clawpack"); + expect(archiveInstallCall().archivePath).toBe("/tmp/clawhub-demo/demo-2026.3.22.tgz"); }); it("rejects ClawPack artifacts when the download digest does not match version metadata", async () => { @@ -715,11 +707,11 @@ describe("installPluginFromClawHub", () => { baseUrl: "https://clawhub.ai", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, - error: `ClawHub ClawPack integrity mismatch for "demo@2026.3.22": expected ${DEMO_CLAWPACK_SHA256}, got ${mismatchedSha256}.`, - }); + const failure = expectInstallFailure(result); + expect(failure.code).toBe(CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH); + expect(failure.error).toBe( + `ClawHub ClawPack integrity mismatch for "demo@2026.3.22": expected ${DEMO_CLAWPACK_SHA256}, got ${mismatchedSha256}.`, + ); expect(installPluginFromArchiveMock).not.toHaveBeenCalled(); expect(archiveCleanupMock).toHaveBeenCalledTimes(1); }); @@ -754,16 +746,11 @@ describe("installPluginFromClawHub", () => { baseUrl: "https://clawhub.ai", }); - expect(result).toMatchObject({ - ok: false, - error: - 'ClawHub artifact download for "demo@2026.3.22" is not available yet (ClawHub /api/v1/packages/demo/versions/2026.3.22/artifact/download failed (404): Not Found). Use "npm:demo@2026.3.22" for launch installs while ClawHub artifact routing is being rolled out.', - }); - expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledWith( - expect.objectContaining({ - artifact: "clawpack", - }), + const failure = expectInstallFailure(result); + expect(failure.error).toBe( + 'ClawHub artifact download for "demo@2026.3.22" is not available yet (ClawHub /api/v1/packages/demo/versions/2026.3.22/artifact/download failed (404): Not Found). Use "npm:demo@2026.3.22" for launch installs while ClawHub artifact routing is being rolled out.', ); + expect(archiveDownloadCall().artifact).toBe("clawpack"); expect(installPluginFromArchiveMock).not.toHaveBeenCalled(); }); @@ -808,19 +795,12 @@ describe("installPluginFromClawHub", () => { baseUrl: "https://clawhub.ai", }); - expect(result).toMatchObject({ - ok: true, - clawhub: { - source: "clawhub", - }, - }); - if (!result.ok) { - throw new Error(result.error); - } - expect(result.clawhub.clawpackSha256).toBeUndefined(); - expect(result.clawhub.clawpackSpecVersion).toBeUndefined(); - expect(result.clawhub.clawpackManifestSha256).toBeUndefined(); - expect(result.clawhub.clawpackSize).toBeUndefined(); + const success = expectInstallSuccess(result); + expect(success.clawhub?.source).toBe("clawhub"); + expect(success.clawhub?.clawpackSha256).toBeUndefined(); + expect(success.clawhub?.clawpackSpecVersion).toBeUndefined(); + expect(success.clawhub?.clawpackManifestSha256).toBeUndefined(); + expect(success.clawhub?.clawpackSize).toBeUndefined(); }); it("installs when ClawHub advertises a wildcard plugin API range", async () => { @@ -844,11 +824,7 @@ describe("installPluginFromClawHub", () => { expectSuccessfulClawHubInstall(result); expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledTimes(1); - expect(installPluginFromArchiveMock).toHaveBeenCalledWith( - expect.objectContaining({ - archivePath: "/tmp/clawhub-demo/archive.zip", - }), - ); + expect(archiveInstallCall().archivePath).toBe("/tmp/clawhub-demo/archive.zip"); expect(archiveCleanupMock).toHaveBeenCalledTimes(1); }); @@ -874,11 +850,7 @@ describe("installPluginFromClawHub", () => { expectSuccessfulClawHubInstall(result); expect(downloadClawHubPackageArchiveMock).toHaveBeenCalledTimes(1); - expect(installPluginFromArchiveMock).toHaveBeenCalledWith( - expect.objectContaining({ - archivePath: "/tmp/clawhub-demo/archive.zip", - }), - ); + expect(archiveInstallCall().archivePath).toBe("/tmp/clawhub-demo/archive.zip"); expect(archiveCleanupMock).toHaveBeenCalledTimes(1); }); @@ -901,11 +873,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.INCOMPATIBLE_PLUGIN_API, - error: 'Plugin "demo" requires plugin API *, but this OpenClaw runtime exposes invalid.', - }); + const failure = expectInstallFailure(result); + expect(failure.code).toBe(CLAWHUB_INSTALL_ERROR_CODE.INCOMPATIBLE_PLUGIN_API); + expect(failure.error).toBe( + 'Plugin "demo" requires plugin API *, but this OpenClaw runtime exposes invalid.', + ); expect(downloadClawHubPackageArchiveMock).not.toHaveBeenCalled(); expect(installPluginFromArchiveMock).not.toHaveBeenCalled(); expect(archiveCleanupMock).not.toHaveBeenCalled(); @@ -917,12 +889,8 @@ describe("installPluginFromClawHub", () => { dangerouslyForceUnsafeInstall: true, }); - expect(installPluginFromArchiveMock).toHaveBeenCalledWith( - expect.objectContaining({ - archivePath: "/tmp/clawhub-demo/archive.zip", - dangerouslyForceUnsafeInstall: true, - }), - ); + expect(archiveInstallCall().archivePath).toBe("/tmp/clawhub-demo/archive.zip"); + expect(archiveInstallCall().dangerouslyForceUnsafeInstall).toBe(true); }); it("cleans up the downloaded archive even when archive install fails", async () => { @@ -936,10 +904,7 @@ describe("installPluginFromClawHub", () => { baseUrl: "https://clawhub.ai", }); - expect(result).toMatchObject({ - ok: false, - error: "bad archive", - }); + expect(expectInstallFailure(result).error).toBe("bad archive"); expect(archiveCleanupMock).toHaveBeenCalledTimes(1); }); @@ -966,7 +931,8 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ ok: true, pluginId: "demo" }); + const success = expectInstallSuccess(result); + expect(success.pluginId).toBe("demo"); }); it("accepts version-endpoint SHA-256 hashes expressed as unpadded SRI", async () => { @@ -992,7 +958,8 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ ok: true, pluginId: "demo" }); + const success = expectInstallSuccess(result); + expect(success.pluginId).toBe("demo"); }); it("falls back to strict files[] verification when sha256hash is missing", async () => { @@ -1036,7 +1003,8 @@ describe("installPluginFromClawHub", () => { logger, }); - expect(result).toMatchObject({ ok: true, pluginId: "demo" }); + const success = expectInstallSuccess(result); + expect(success.pluginId).toBe("demo"); expect(logger.warn).toHaveBeenCalledWith( 'ClawHub package "demo@2026.3.22" is missing sha256hash; falling back to files[] verification. Validated files: dist/index.js, openclaw.plugin.json. Validated generated metadata files present in archive: _meta.json (JSON parse plus slug/version match only).', ); @@ -1093,18 +1061,12 @@ describe("installPluginFromClawHub", () => { logger, }); - expect(result).toMatchObject({ ok: true, pluginId: "demo", version: "2026.3.22" }); - expect(fetchClawHubPackageDetailMock).toHaveBeenCalledWith( - expect.objectContaining({ - name: "DemoAlias", - }), - ); - expect(fetchClawHubPackageVersionMock).toHaveBeenCalledWith( - expect.objectContaining({ - name: "demo", - version: "latest", - }), - ); + const success = expectInstallSuccess(result); + expect(success.pluginId).toBe("demo"); + expect(success.version).toBe("2026.3.22"); + expect(packageDetailCall().name).toBe("DemoAlias"); + expect(packageVersionCall().name).toBe("demo"); + expect(packageVersionCall().version).toBe("latest"); expect(logger.warn).toHaveBeenCalledWith( 'ClawHub package "demo@2026.3.22" is missing sha256hash; falling back to files[] verification. Validated files: openclaw.plugin.json. Validated generated metadata files present in archive: _meta.json (JSON parse plus slug/version match only).', ); @@ -1135,12 +1097,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY, - error: - 'ClawHub version metadata for "demo@2026.3.22" has an invalid sha256hash (unrecognized value "definitely-not-a-sha256").', - }); + const failure = expectInstallFailure(result); + expect(failure.code).toBe(CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY); + expect(failure.error).toBe( + 'ClawHub version metadata for "demo@2026.3.22" has an invalid sha256hash (unrecognized value "definitely-not-a-sha256").', + ); expect(downloadClawHubPackageArchiveMock).not.toHaveBeenCalled(); }); @@ -1162,12 +1123,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY, - error: - 'ClawHub package "demo@2026.3.22" does not expose a downloadable plugin artifact yet. Use "npm:demo@2026.3.22" for launch installs while ClawHub artifact routing is being rolled out.', - }); + const failure = expectInstallFailure(result); + expect(failure.code).toBe(CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY); + expect(failure.error).toBe( + 'ClawHub package "demo@2026.3.22" does not expose a downloadable plugin artifact yet. Use "npm:demo@2026.3.22" for launch installs while ClawHub artifact routing is being rolled out.', + ); expect(downloadClawHubPackageArchiveMock).not.toHaveBeenCalled(); }); @@ -1188,12 +1148,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY, - error: - 'ClawHub package "demo@2026.3.22" does not expose a downloadable plugin artifact yet. Use "npm:demo@2026.3.22" for launch installs while ClawHub artifact routing is being rolled out.', - }); + const failure = expectInstallFailure(result); + expect(failure.code).toBe(CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY); + expect(failure.error).toBe( + 'ClawHub package "demo@2026.3.22" does not expose a downloadable plugin artifact yet. Use "npm:demo@2026.3.22" for launch installs while ClawHub artifact routing is being rolled out.', + ); expect(downloadClawHubPackageArchiveMock).not.toHaveBeenCalled(); }); @@ -1215,12 +1174,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY, - error: - 'ClawHub version metadata for "demo@2026.3.22" has an invalid files[0] entry (expected an object, got null).', - }); + const failure = expectInstallFailure(result); + expect(failure.code).toBe(CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY); + expect(failure.error).toBe( + 'ClawHub version metadata for "demo@2026.3.22" has an invalid files[0] entry (expected an object, got null).', + ); expect(downloadClawHubPackageArchiveMock).not.toHaveBeenCalled(); }); @@ -1248,12 +1206,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY, - error: - 'ClawHub version metadata for "demo@2026.3.22" has an invalid files[0].sha256 (value "not-a-digest" is not a 64-character hexadecimal SHA-256 digest).', - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY, + 'ClawHub version metadata for "demo@2026.3.22" has an invalid files[0].sha256 (value "not-a-digest" is not a 64-character hexadecimal SHA-256 digest).', + ); expect(downloadClawHubPackageArchiveMock).not.toHaveBeenCalled(); }); @@ -1275,12 +1232,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY, - error: - 'ClawHub version metadata for "demo@2026.3.22" has an invalid sha256hash (non-string value of type number).', - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY, + 'ClawHub version metadata for "demo@2026.3.22" has an invalid sha256hash (non-string value of type number).', + ); expect(downloadClawHubPackageArchiveMock).not.toHaveBeenCalled(); }); @@ -1291,10 +1247,7 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - error: "network timeout", - }); + expect(expectInstallFailure(result).error).toBe("network timeout"); expect(installPluginFromArchiveMock).not.toHaveBeenCalled(); }); @@ -1331,11 +1284,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, - error: "ClawHub archive fallback verification failed while reading the downloaded archive.", - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, + "ClawHub archive fallback verification failed while reading the downloaded archive.", + ); expect(installPluginFromArchiveMock).not.toHaveBeenCalled(); }); @@ -1362,11 +1315,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, - error: `ClawHub archive integrity mismatch for "demo@2026.3.22": expected sha256-ERERERERERERERERERERERERERERERERERERERERERE=, got ${DEMO_ARCHIVE_INTEGRITY}.`, - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, + `ClawHub archive integrity mismatch for "demo@2026.3.22": expected sha256-ERERERERERERERERERERERERERERERERERERERERERE=, got ${DEMO_ARCHIVE_INTEGRITY}.`, + ); expect(installPluginFromArchiveMock).not.toHaveBeenCalled(); expect(archiveCleanupMock).toHaveBeenCalledTimes(1); }); @@ -1407,12 +1360,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, - error: - 'ClawHub archive contents do not match files[] metadata for "demo@2026.3.22": missing "dist/index.js".', - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, + 'ClawHub archive contents do not match files[] metadata for "demo@2026.3.22": missing "dist/index.js".', + ); expect(installPluginFromArchiveMock).not.toHaveBeenCalled(); }); @@ -1454,12 +1406,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, - error: - 'ClawHub archive contents do not match files[] metadata for "demo@2026.3.22": unexpected file "extra.txt".', - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, + 'ClawHub archive contents do not match files[] metadata for "demo@2026.3.22": unexpected file "extra.txt".', + ); expect(installPluginFromArchiveMock).not.toHaveBeenCalled(); }); @@ -1508,7 +1459,7 @@ describe("installPluginFromClawHub", () => { logger, }); - expect(result).toMatchObject({ ok: true, pluginId: "demo" }); + expect(expectInstallSuccess(result).pluginId).toBe("demo"); expect(logger.warn).toHaveBeenCalledWith( 'ClawHub package "demo@2026.3.22" is missing sha256hash; falling back to files[] verification. Validated files: SKILL.md, scripts/search.py. Validated generated metadata files present in archive: _meta.json (JSON parse plus slug/version match only).', ); @@ -1547,7 +1498,7 @@ describe("installPluginFromClawHub", () => { logger, }); - expect(result).toMatchObject({ ok: true, pluginId: "demo" }); + expect(expectInstallSuccess(result).pluginId).toBe("demo"); expect(logger.warn).toHaveBeenCalledWith( 'ClawHub package "demo@2026.3.22" is missing sha256hash; falling back to files[] verification. Validated files: openclaw.plugin.json.', ); @@ -1585,12 +1536,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, - error: - 'ClawHub archive contents do not match files[] metadata for "demo@2026.3.22": _meta.json is not valid JSON.', - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, + 'ClawHub archive contents do not match files[] metadata for "demo@2026.3.22": _meta.json is not valid JSON.', + ); expect(installPluginFromArchiveMock).not.toHaveBeenCalled(); }); @@ -1626,12 +1576,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, - error: - 'ClawHub archive contents do not match files[] metadata for "demo@2026.3.22": _meta.json slug does not match the package name.', - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, + 'ClawHub archive contents do not match files[] metadata for "demo@2026.3.22": _meta.json slug does not match the package name.', + ); expect(installPluginFromArchiveMock).not.toHaveBeenCalled(); }); @@ -1687,12 +1636,11 @@ describe("installPluginFromClawHub", () => { }); loadAsyncSpy.mockRestore(); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, - error: - 'ClawHub archive fallback verification rejected "_meta.json" because it exceeds the per-file size limit.', - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, + 'ClawHub archive fallback verification rejected "_meta.json" because it exceeds the per-file size limit.', + ); expect(installPluginFromArchiveMock).not.toHaveBeenCalled(); }); @@ -1742,11 +1690,11 @@ describe("installPluginFromClawHub", () => { }); loadAsyncSpy.mockRestore(); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, - error: "ClawHub archive fallback verification exceeded the archive entry limit.", - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, + "ClawHub archive fallback verification exceeded the archive entry limit.", + ); expect(installPluginFromArchiveMock).not.toHaveBeenCalled(); }); @@ -1792,11 +1740,11 @@ describe("installPluginFromClawHub", () => { }); loadAsyncSpy.mockRestore(); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, - error: "ClawHub archive fallback verification exceeded the archive entry limit.", - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, + "ClawHub archive fallback verification exceeded the archive entry limit.", + ); expect(loadAsyncSpy).not.toHaveBeenCalled(); expect(installPluginFromArchiveMock).not.toHaveBeenCalled(); }); @@ -1844,12 +1792,11 @@ describe("installPluginFromClawHub", () => { }); statSpy.mockRestore(); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, - error: - "ClawHub archive fallback verification rejected the downloaded archive because it exceeds the ZIP archive size limit.", - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, + "ClawHub archive fallback verification rejected the downloaded archive because it exceeds the ZIP archive size limit.", + ); expect(installPluginFromArchiveMock).not.toHaveBeenCalled(); }); @@ -1884,11 +1831,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, - error: `ClawHub archive contents do not match files[] metadata for "demo@2026.3.22": expected openclaw.plugin.json to hash to ${"1".repeat(64)}, got ${sha256Hex('{"id":"demo"}')}.`, - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, + `ClawHub archive contents do not match files[] metadata for "demo@2026.3.22": expected openclaw.plugin.json to hash to ${"1".repeat(64)}, got ${sha256Hex('{"id":"demo"}')}.`, + ); expect(installPluginFromArchiveMock).not.toHaveBeenCalled(); }); @@ -1916,12 +1863,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY, - error: - 'ClawHub version metadata for "demo@2026.3.22" has an invalid files[0].path (path "../evil.txt" contains dot segments).', - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY, + 'ClawHub version metadata for "demo@2026.3.22" has an invalid files[0].path (path "../evil.txt" contains dot segments).', + ); expect(downloadClawHubPackageArchiveMock).not.toHaveBeenCalled(); }); @@ -1949,12 +1895,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY, - error: - 'ClawHub version metadata for "demo@2026.3.22" has an invalid files[0].path (path "openclaw.plugin.json " has leading or trailing whitespace).', - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY, + 'ClawHub version metadata for "demo@2026.3.22" has an invalid files[0].path (path "openclaw.plugin.json " has leading or trailing whitespace).', + ); expect(downloadClawHubPackageArchiveMock).not.toHaveBeenCalled(); }); @@ -1990,12 +1935,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, - error: - 'ClawHub archive contents do not match files[] metadata for "demo@2026.3.22": invalid package file path "openclaw.plugin.json " (path "openclaw.plugin.json " has leading or trailing whitespace).', - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH, + 'ClawHub archive contents do not match files[] metadata for "demo@2026.3.22": invalid package file path "openclaw.plugin.json " (path "openclaw.plugin.json " has leading or trailing whitespace).', + ); expect(installPluginFromArchiveMock).not.toHaveBeenCalled(); }); @@ -2028,12 +1972,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY, - error: - 'ClawHub version metadata for "demo@2026.3.22" has duplicate files[] path "openclaw.plugin.json".', - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY, + 'ClawHub version metadata for "demo@2026.3.22" has duplicate files[] path "openclaw.plugin.json".', + ); expect(downloadClawHubPackageArchiveMock).not.toHaveBeenCalled(); }); @@ -2061,12 +2004,11 @@ describe("installPluginFromClawHub", () => { spec: "clawhub:demo", }); - expect(result).toMatchObject({ - ok: false, - code: CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY, - error: - 'ClawHub version metadata for "demo@2026.3.22" must not include generated file "_meta.json" in files[].', - }); + expectInstallFailureFields( + result, + CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY, + 'ClawHub version metadata for "demo@2026.3.22" must not include generated file "_meta.json" in files[].', + ); expect(downloadClawHubPackageArchiveMock).not.toHaveBeenCalled(); });