mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
* chore: update plugin inspector to 0.3.20 * feat: gate plugin compatibility emails on hard errors
18896 lines
589 KiB
TypeScript
18896 lines
589 KiB
TypeScript
/* @vitest-environment node */
|
|
|
|
import { getAuthUserId } from "@convex-dev/auth/server";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { sha256Hex } from "./lib/clawpack";
|
|
import { MAX_PUBLISH_FILE_BYTES } from "./lib/publishLimits";
|
|
import {
|
|
computeRecommendationScore,
|
|
RECOMMENDATION_SCORE_VERSION,
|
|
} from "./lib/recommendationScore";
|
|
import { buildDeterministicPackageZip } from "./lib/skillZip";
|
|
import {
|
|
backfillLatestPackageScanStatusInternal,
|
|
backfillPackageReleaseScansInternal,
|
|
normalizeOfficialPublisherPackagesInternal,
|
|
getPackageReleaseScanBackfillBatchInternal,
|
|
getByName,
|
|
list,
|
|
publishPackageForTrustedPublisherInternal,
|
|
publishPackageForUserInternal,
|
|
generateChangelogPreview,
|
|
assertCanGenerateChangelogPreviewInternal,
|
|
listPackageReportsInternal,
|
|
getPackageModerationStatusForUserInternal,
|
|
getManageContext,
|
|
canDeleteVersions,
|
|
getPackageInspectorValidationSummaryPublic,
|
|
listPackageInspectorWarningsForManager,
|
|
listPackageInspectorFindingsPublic,
|
|
insertPackageInspectorWarningsInternal,
|
|
ingestPackageInspectorScanResultsInternal,
|
|
claimPackageInspectorScanBatchInternal,
|
|
acknowledgePackageInspectorScanBatchInternal,
|
|
previewPackageInspectorScanBatchInternal,
|
|
getPackageInspectorEmailContextInternal,
|
|
markPackageInspectorFindingsEmailedInternal,
|
|
reportPackageForUserInternal,
|
|
triagePackageReportForUserInternal,
|
|
submitPackageAppealForUserInternal,
|
|
listPackageAppealsInternal,
|
|
listOfficialPluginMigrationsInternal,
|
|
resolvePackageAppealForUserInternal,
|
|
upsertOfficialPluginMigrationForUserInternal,
|
|
getVersionByName,
|
|
getVersionSecurityByNameForViewerInternal,
|
|
insertReleaseInternal,
|
|
cleanupReassignedPackageReleaseTagsInternal,
|
|
publishPendingReleaseInternal,
|
|
finalizePackagePublishAttemptInternal,
|
|
findPackagePublishResultInternal,
|
|
listPackageModerationQueueInternal,
|
|
listPluginExportPageInternal,
|
|
listPluginValidationReportPageInternal,
|
|
reservePackageNameInternal,
|
|
listPublicPage,
|
|
listPublicNewPluginsPage,
|
|
listPageForViewerInternal,
|
|
listVersions,
|
|
listVersionsForManager,
|
|
updateReleaseLlmAnalysisInternal,
|
|
updateReleaseStaticScanInternal,
|
|
applyAccountDeletionToOwnedPackagesBatchInternal,
|
|
applyPublisherDeletionToOwnedPackagesBatchInternal,
|
|
applyBanToOwnedPackagesBatchInternal,
|
|
revokePackagePublishTokensForPackageBatchInternal,
|
|
restoreOwnedPackagesForUnbanBatchInternal,
|
|
softDeletePackageInternal,
|
|
restorePackageInternal,
|
|
transferPackageOwnerForUserInternal,
|
|
transferPackageOwnerInternal,
|
|
repairPackageIdentityInternal,
|
|
searchForViewerInternal,
|
|
searchPublic,
|
|
} from "./packages";
|
|
|
|
vi.mock("@convex-dev/auth/server", () => ({
|
|
getAuthUserId: vi.fn(),
|
|
authTables: {},
|
|
}));
|
|
|
|
type WrappedHandler<TArgs, TResult> = {
|
|
_handler: (ctx: unknown, args: TArgs) => Promise<TResult>;
|
|
};
|
|
type TestPackageInspectorAuthorRemediation = {
|
|
summary: string;
|
|
docsUrl?: string;
|
|
};
|
|
|
|
const getByNameHandler = (
|
|
getByName as unknown as WrappedHandler<
|
|
{ name: string },
|
|
{
|
|
package: { name: string; latestVersion: string | null };
|
|
latestRelease: { version: string } | null;
|
|
} | null
|
|
>
|
|
)._handler;
|
|
const listHandler = (
|
|
list as unknown as WrappedHandler<
|
|
{
|
|
ownerUserId?: string;
|
|
ownerPublisherId?: string;
|
|
limit?: number;
|
|
},
|
|
Array<{
|
|
name: string;
|
|
pendingReview?: boolean;
|
|
scanStatus?: string;
|
|
latestRelease: {
|
|
vtStatus: string | null;
|
|
staticScanStatus: string | null;
|
|
} | null;
|
|
}>
|
|
>
|
|
)._handler;
|
|
const applyPublisherDeletionToOwnedPackagesBatchInternalHandler = (
|
|
applyPublisherDeletionToOwnedPackagesBatchInternal as unknown as WrappedHandler<
|
|
{
|
|
ownerPublisherId: string;
|
|
actorUserId: string;
|
|
deletedAt: number;
|
|
cursor?: string;
|
|
},
|
|
{
|
|
deletedCount: number;
|
|
revokedTokenCount: number;
|
|
scheduled: boolean;
|
|
stale?: true;
|
|
}
|
|
>
|
|
)._handler;
|
|
const getVersionByNameHandler = (
|
|
getVersionByName as unknown as WrappedHandler<
|
|
{ name: string; version: string },
|
|
{ package: { name: string; scanStatus?: string }; version: { version: string } } | null
|
|
>
|
|
)._handler;
|
|
const getVersionSecurityByNameForViewerInternalHandler = (
|
|
getVersionSecurityByNameForViewerInternal as unknown as WrappedHandler<
|
|
{ name: string; version: string; viewerUserId?: string },
|
|
{ package: { name: string }; version: { version: string } } | null
|
|
>
|
|
)._handler;
|
|
const listPublicPageHandler = (
|
|
listPublicPage as unknown as WrappedHandler<
|
|
{
|
|
family?: "skill" | "code-plugin" | "bundle-plugin";
|
|
channel?: "official" | "community" | "private";
|
|
isOfficial?: boolean;
|
|
executesCode?: boolean;
|
|
capabilityTag?: string;
|
|
category?: string;
|
|
topic?: string;
|
|
officialFirst?: boolean;
|
|
highlightedOnly?: boolean;
|
|
excludedScanStatuses?: Array<"clean" | "suspicious" | "malicious" | "pending" | "not-run">;
|
|
sort?: "updated" | "downloads" | "recommended" | "installs";
|
|
paginationOpts: { cursor: string | null; numItems: number };
|
|
},
|
|
{
|
|
page: Array<{ name: string; featuredAt?: number }>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}
|
|
>
|
|
)._handler;
|
|
const listPublicNewPluginsPageHandler = (
|
|
listPublicNewPluginsPage as unknown as WrappedHandler<
|
|
{
|
|
category?: string;
|
|
createdAfter: number;
|
|
paginationOpts: { cursor: string | null; numItems: number };
|
|
},
|
|
{
|
|
page: Array<{ name: string; createdAt: number }>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}
|
|
>
|
|
)._handler;
|
|
const listPageForViewerInternalHandler = (
|
|
listPageForViewerInternal as unknown as WrappedHandler<
|
|
{
|
|
family?: "skill" | "code-plugin" | "bundle-plugin";
|
|
families?: Array<"skill" | "code-plugin" | "bundle-plugin">;
|
|
channel?: "official" | "community" | "private";
|
|
isOfficial?: boolean;
|
|
executesCode?: boolean;
|
|
capabilityTag?: string;
|
|
category?: string;
|
|
topic?: string;
|
|
officialFirst?: boolean;
|
|
highlightedOnly?: boolean;
|
|
excludedScanStatuses?: Array<"clean" | "suspicious" | "malicious" | "pending" | "not-run">;
|
|
sort?: "updated" | "downloads" | "recommended" | "installs";
|
|
viewerUserId?: string;
|
|
paginationOpts: { cursor: string | null; numItems: number };
|
|
},
|
|
{
|
|
page: Array<{ name: string; featuredAt?: number }>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}
|
|
>
|
|
)._handler;
|
|
const listPluginExportPageInternalHandler = (
|
|
listPluginExportPageInternal as unknown as WrappedHandler<
|
|
{
|
|
startDate: number;
|
|
endDate: number;
|
|
cursor?: string;
|
|
numItems?: number;
|
|
family?: "code-plugin" | "bundle-plugin";
|
|
},
|
|
{
|
|
page: Array<{ name: string; family: "code-plugin" | "bundle-plugin" }>;
|
|
nextCursor: string | null;
|
|
hasMore: boolean;
|
|
}
|
|
>
|
|
)._handler;
|
|
const listPluginValidationReportPageInternalHandler = (
|
|
listPluginValidationReportPageInternal as unknown as WrappedHandler<
|
|
{ cursor?: string; numItems?: number },
|
|
{
|
|
items: Array<{
|
|
package: { id: string; name: string; displayName: string };
|
|
scan: { status: string; scannedAt: number | null };
|
|
findings: Array<{ severity: string; code: string; message: string }>;
|
|
}>;
|
|
nextCursor: string | null;
|
|
done: boolean;
|
|
}
|
|
>
|
|
)._handler;
|
|
const listVersionsHandler = (
|
|
listVersions as unknown as WrappedHandler<
|
|
{
|
|
name: string;
|
|
paginationOpts: { cursor: string | null; numItems: number };
|
|
},
|
|
{
|
|
page: Array<{ version: string }>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}
|
|
>
|
|
)._handler;
|
|
const listVersionsForManagerHandler = (
|
|
listVersionsForManager as unknown as WrappedHandler<
|
|
{
|
|
name: string;
|
|
paginationOpts: { cursor: string | null; numItems: number };
|
|
},
|
|
{
|
|
page: Array<{ version: string; softDeletedAt?: number; ownerDeletedAt?: number }>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}
|
|
>
|
|
)._handler;
|
|
const insertReleaseInternalHandler = (
|
|
insertReleaseInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
ownerUserId: string;
|
|
ownerPublisherId?: string;
|
|
publishActor?:
|
|
| { kind: "user"; userId: string }
|
|
| {
|
|
kind: "github-actions";
|
|
repository: string;
|
|
workflow: string;
|
|
runId: string;
|
|
runAttempt: string;
|
|
sha: string;
|
|
};
|
|
name: string;
|
|
displayName: string;
|
|
family: "skill" | "code-plugin" | "bundle-plugin";
|
|
version: string;
|
|
publicationStatus?: "pending" | "published";
|
|
changelog: string;
|
|
icon?: string;
|
|
tags: string[];
|
|
summary: string;
|
|
categories?: string[];
|
|
topics?: string[];
|
|
files: Array<{
|
|
path: string;
|
|
size: number;
|
|
storageId: string;
|
|
sha256: string;
|
|
contentType?: string;
|
|
}>;
|
|
integritySha256: string;
|
|
sha256hash?: string;
|
|
sourceRepo?: string;
|
|
runtimeId?: string;
|
|
channel?: "official" | "community" | "private";
|
|
compatibility?: unknown;
|
|
capabilities?: unknown;
|
|
verification?: unknown;
|
|
staticScan?: unknown;
|
|
llmAnalysis?: unknown;
|
|
artifactKind?: "legacy-zip" | "npm-pack";
|
|
clawpackStorageId?: string;
|
|
clawpackSha256?: string;
|
|
clawpackSize?: number;
|
|
clawpackFormat?: "tgz";
|
|
npmIntegrity?: string;
|
|
npmShasum?: string;
|
|
npmTarballName?: string;
|
|
npmUnpackedSize?: number;
|
|
npmFileCount?: number;
|
|
allowExistingRelease?: boolean;
|
|
extractedPackageJson?: unknown;
|
|
extractedPluginManifest?: unknown;
|
|
normalizedBundleManifest?: unknown;
|
|
source?: unknown;
|
|
},
|
|
unknown
|
|
>
|
|
)._handler;
|
|
const findPackagePublishResultInternalHandler = (
|
|
findPackagePublishResultInternal as unknown as WrappedHandler<
|
|
{
|
|
name: string;
|
|
version: string;
|
|
integritySha256: string;
|
|
ownerUserId: string;
|
|
ownerPublisherId?: string;
|
|
},
|
|
{ ok: true; packageId: string; releaseId: string } | null
|
|
>
|
|
)._handler;
|
|
const publishPendingReleaseInternalHandler = (
|
|
publishPendingReleaseInternal as unknown as WrappedHandler<
|
|
{
|
|
releaseId: string;
|
|
},
|
|
unknown
|
|
>
|
|
)._handler;
|
|
const cleanupReassignedPackageReleaseTagsInternalHandler = (
|
|
cleanupReassignedPackageReleaseTagsInternal as unknown as WrappedHandler<
|
|
{
|
|
packageId: string;
|
|
assignments: Array<{ releaseId: string; tags: string[] }>;
|
|
offset?: number;
|
|
},
|
|
unknown
|
|
>
|
|
)._handler;
|
|
const finalizePackagePublishAttemptInternalHandler = (
|
|
finalizePackagePublishAttemptInternal as unknown as WrappedHandler<
|
|
{
|
|
attemptId: string;
|
|
},
|
|
unknown
|
|
>
|
|
)._handler;
|
|
const reservePackageNameInternalHandler = (
|
|
reservePackageNameInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
ownerUserId: string;
|
|
ownerPublisherId?: string;
|
|
name: string;
|
|
displayName?: string;
|
|
summary?: string;
|
|
family?: "skill" | "code-plugin" | "bundle-plugin";
|
|
reason?: string;
|
|
},
|
|
{ ok: true; action: string; packageId: string; name: string }
|
|
>
|
|
)._handler;
|
|
const searchPublicHandler = (
|
|
searchPublic as unknown as WrappedHandler<
|
|
{
|
|
query: string;
|
|
limit?: number;
|
|
family?: "skill" | "code-plugin" | "bundle-plugin";
|
|
channel?: "official" | "community" | "private";
|
|
isOfficial?: boolean;
|
|
executesCode?: boolean;
|
|
capabilityTag?: string;
|
|
category?: string;
|
|
topic?: string;
|
|
excludedScanStatuses?: Array<"clean" | "suspicious" | "malicious" | "pending" | "not-run">;
|
|
},
|
|
Array<{ package: { name: string } }>
|
|
>
|
|
)._handler;
|
|
const searchForViewerInternalHandler = (
|
|
searchForViewerInternal as unknown as WrappedHandler<
|
|
{
|
|
query: string;
|
|
limit?: number;
|
|
family?: "skill" | "code-plugin" | "bundle-plugin";
|
|
channel?: "official" | "community" | "private";
|
|
isOfficial?: boolean;
|
|
executesCode?: boolean;
|
|
capabilityTag?: string;
|
|
category?: string;
|
|
topic?: string;
|
|
excludedScanStatuses?: Array<"clean" | "suspicious" | "malicious" | "pending" | "not-run">;
|
|
viewerUserId?: string;
|
|
},
|
|
Array<{ package: { name: string } }>
|
|
>
|
|
)._handler;
|
|
const publishPackageForUserInternalHandler = (
|
|
publishPackageForUserInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
payload: unknown;
|
|
},
|
|
unknown
|
|
>
|
|
)._handler;
|
|
const publishPackageForTrustedPublisherInternalHandler = (
|
|
publishPackageForTrustedPublisherInternal as unknown as WrappedHandler<
|
|
{
|
|
publishTokenId: string;
|
|
payload: unknown;
|
|
},
|
|
unknown
|
|
>
|
|
)._handler;
|
|
const generateChangelogPreviewHandler = (
|
|
generateChangelogPreview as unknown as WrappedHandler<
|
|
{
|
|
name: string;
|
|
family: "code-plugin" | "bundle-plugin";
|
|
version: string;
|
|
readmeText: string;
|
|
filePaths?: string[];
|
|
},
|
|
{ changelog: string }
|
|
>
|
|
)._handler;
|
|
const assertCanGenerateChangelogPreviewInternalHandler = (
|
|
assertCanGenerateChangelogPreviewInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
name: string;
|
|
},
|
|
{ ok: true }
|
|
>
|
|
)._handler;
|
|
|
|
const packageManifestFile = {
|
|
path: "openclaw.plugin.json",
|
|
size: 32,
|
|
storageId: "storage:manifest",
|
|
sha256: "manifest",
|
|
contentType: "application/json",
|
|
};
|
|
|
|
function makePackageManifestStorage() {
|
|
return {
|
|
get: vi.fn(async (id: string) =>
|
|
id === "storage:manifest" ? new Blob([JSON.stringify({ id: "demo.plugin" })]) : null,
|
|
),
|
|
store: vi.fn(async () => "storage:legacy-zip"),
|
|
};
|
|
}
|
|
|
|
function makeCleanPackageInspectorResult() {
|
|
return {
|
|
status: "pass",
|
|
summary: {
|
|
breakageCount: 0,
|
|
warningCount: 0,
|
|
deprecationWarningCount: 0,
|
|
issueCount: 0,
|
|
},
|
|
warnings: [],
|
|
breakages: [],
|
|
};
|
|
}
|
|
|
|
function makeEmptyPackageInspectorWarningsQuery() {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([]),
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([]),
|
|
paginate: vi.fn().mockResolvedValue({ page: [], isDone: true, continueCursor: "" }),
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
|
|
const reportPackageForUserInternalHandler = (
|
|
reportPackageForUserInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
name: string;
|
|
version?: string;
|
|
reason: string;
|
|
},
|
|
{
|
|
ok: true;
|
|
reported: boolean;
|
|
alreadyReported: boolean;
|
|
packageId: string;
|
|
releaseId: string | null;
|
|
reportCount: number;
|
|
}
|
|
>
|
|
)._handler;
|
|
const listPackageReportsInternalHandler = (
|
|
listPackageReportsInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
cursor?: string | null;
|
|
limit?: number;
|
|
status?: "open" | "confirmed" | "dismissed" | "all";
|
|
},
|
|
{
|
|
items: Array<{ reportId: string; name: string; status: string; reason?: string | null }>;
|
|
nextCursor: string | null;
|
|
done: boolean;
|
|
}
|
|
>
|
|
)._handler;
|
|
const triagePackageReportForUserInternalHandler = (
|
|
triagePackageReportForUserInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
reportId: string;
|
|
status: "open" | "confirmed" | "dismissed";
|
|
note?: string;
|
|
finalAction?: "none" | "quarantine" | "revoke";
|
|
},
|
|
{
|
|
ok: true;
|
|
reportId: string;
|
|
packageId: string;
|
|
status: string;
|
|
reportCount: number;
|
|
actionTaken?: string;
|
|
}
|
|
>
|
|
)._handler;
|
|
const getPackageModerationStatusForUserInternalHandler = (
|
|
getPackageModerationStatusForUserInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
name: string;
|
|
},
|
|
{
|
|
package: { name: string; reportCount: number };
|
|
latestRelease: { version: string; scanStatus: string; blockedFromDownload: boolean } | null;
|
|
}
|
|
>
|
|
)._handler;
|
|
const getManageContextHandler = (
|
|
getManageContext as unknown as WrappedHandler<
|
|
{ name: string; candidateNames?: string[] },
|
|
{
|
|
package: { _id: string; name: string; displayName: string };
|
|
latestRelease: { _id: string; version: string };
|
|
} | null
|
|
>
|
|
)._handler;
|
|
const canDeleteVersionsHandler = (
|
|
canDeleteVersions as unknown as WrappedHandler<
|
|
{ name: string; candidateNames?: string[] },
|
|
boolean
|
|
>
|
|
)._handler;
|
|
const listPackageInspectorWarningsForManagerHandler = (
|
|
listPackageInspectorWarningsForManager as unknown as WrappedHandler<
|
|
{ name: string; limit?: number },
|
|
Array<{
|
|
packageName: string;
|
|
version: string;
|
|
code: string;
|
|
issueClass?: string;
|
|
message: string;
|
|
}>
|
|
>
|
|
)._handler;
|
|
const listPackageInspectorFindingsPublicHandler = (
|
|
listPackageInspectorFindingsPublic as unknown as WrappedHandler<
|
|
{ name: string; limit?: number },
|
|
Array<{
|
|
packageName: string;
|
|
version: string;
|
|
findingKind: "warning" | "error";
|
|
code: string;
|
|
issueClass?: string;
|
|
message: string;
|
|
inspectorVersion?: string;
|
|
targetOpenClawVersion?: string;
|
|
scanSource?: "publish" | "nightly";
|
|
}>
|
|
>
|
|
)._handler;
|
|
const getPackageInspectorValidationSummaryPublicHandler = (
|
|
getPackageInspectorValidationSummaryPublic as unknown as WrappedHandler<
|
|
{ name: string },
|
|
{
|
|
findingCount: number;
|
|
errorCount: number;
|
|
warningCount: number;
|
|
incompatibleAfterOpenClawVersion: string | null;
|
|
}
|
|
>
|
|
)._handler;
|
|
const insertPackageInspectorWarningsInternalHandler = (
|
|
insertPackageInspectorWarningsInternal as unknown as WrappedHandler<
|
|
{
|
|
packageId: string;
|
|
releaseId: string;
|
|
ownerUserId: string;
|
|
ownerPublisherId?: string;
|
|
packageName: string;
|
|
version: string;
|
|
scanSource?: "publish" | "nightly";
|
|
inspectorVersion?: string;
|
|
targetOpenClawVersion?: string;
|
|
notifyOwners?: boolean;
|
|
findings?: Array<{
|
|
id?: string;
|
|
code: string;
|
|
message: string;
|
|
level?: string;
|
|
issueClass?: string;
|
|
evidence?: string[];
|
|
fixture?: string;
|
|
authorRemediation?: TestPackageInspectorAuthorRemediation;
|
|
}>;
|
|
warnings?: Array<{
|
|
id?: string;
|
|
code: string;
|
|
message: string;
|
|
issueClass?: string;
|
|
evidence?: string[];
|
|
fixture?: string;
|
|
authorRemediation?: TestPackageInspectorAuthorRemediation;
|
|
}>;
|
|
},
|
|
{ ok: true; inserted: number; shouldEmailOwner: boolean }
|
|
>
|
|
)._handler;
|
|
const ingestPackageInspectorScanResultsInternalHandler = (
|
|
ingestPackageInspectorScanResultsInternal as unknown as WrappedHandler<
|
|
{
|
|
packageId: string;
|
|
releaseId: string;
|
|
inspectorVersion?: string;
|
|
targetOpenClawVersion?: string;
|
|
notifyOwners?: boolean;
|
|
findings: Array<{
|
|
id?: string;
|
|
code: string;
|
|
message: string;
|
|
level?: string;
|
|
authorRemediation?: TestPackageInspectorAuthorRemediation;
|
|
}>;
|
|
},
|
|
{ ok: true; inserted: number; shouldEmailOwner: boolean }
|
|
>
|
|
)._handler;
|
|
const claimPackageInspectorScanBatchInternalHandler = (
|
|
claimPackageInspectorScanBatchInternal as unknown as WrappedHandler<
|
|
{
|
|
batchSize?: number;
|
|
leaseMs?: number;
|
|
cursor?: string | null;
|
|
runId?: string;
|
|
inspectorVersion?: string;
|
|
targetOpenClawVersion?: string;
|
|
notifyOwners?: boolean;
|
|
},
|
|
{
|
|
ok: true;
|
|
leased: boolean;
|
|
nextCursor: string | null;
|
|
items: Array<{
|
|
packageId: string;
|
|
releaseId: string;
|
|
packageName: string;
|
|
version: string;
|
|
artifactKind: "legacy-zip" | "npm-pack";
|
|
}>;
|
|
}
|
|
>
|
|
)._handler;
|
|
const acknowledgePackageInspectorScanBatchInternalHandler = (
|
|
acknowledgePackageInspectorScanBatchInternal as unknown as WrappedHandler<
|
|
{
|
|
runId: string;
|
|
cursor: string | null;
|
|
leaseMs?: number;
|
|
},
|
|
{ ok: true; cursor: string | null; completed: boolean }
|
|
>
|
|
)._handler;
|
|
const previewPackageInspectorScanBatchInternalHandler = (
|
|
previewPackageInspectorScanBatchInternal as unknown as WrappedHandler<
|
|
{
|
|
batchSize?: number;
|
|
cursor?: string | null;
|
|
inspectorVersion?: string;
|
|
targetOpenClawVersion?: string;
|
|
notifyOwners?: boolean;
|
|
},
|
|
{
|
|
ok: true;
|
|
leased: false;
|
|
nextCursor: string | null;
|
|
skippedUnchanged: number;
|
|
items: Array<{
|
|
packageId: string;
|
|
releaseId: string;
|
|
ownerUserId: string;
|
|
ownerPublisherId?: string;
|
|
packageName: string;
|
|
version: string;
|
|
artifactKind: "legacy-zip" | "npm-pack";
|
|
}>;
|
|
}
|
|
>
|
|
)._handler;
|
|
const getPackageInspectorEmailContextInternalHandler = (
|
|
getPackageInspectorEmailContextInternal as unknown as WrappedHandler<
|
|
{
|
|
packageId: string;
|
|
releaseId: string;
|
|
inspectorVersion?: string;
|
|
targetOpenClawVersion?: string;
|
|
},
|
|
{
|
|
packageName: string;
|
|
version: string;
|
|
findings: Array<{
|
|
code: string;
|
|
authorRemediation?: TestPackageInspectorAuthorRemediation;
|
|
}>;
|
|
} | null
|
|
>
|
|
)._handler;
|
|
const markPackageInspectorFindingsEmailedInternalHandler = (
|
|
markPackageInspectorFindingsEmailedInternal as unknown as WrappedHandler<
|
|
{
|
|
packageId: string;
|
|
releaseId: string;
|
|
ownerUserId: string;
|
|
ownerPublisherId?: string;
|
|
packageName: string;
|
|
version: string;
|
|
findingCount: number;
|
|
email: string;
|
|
inspectorVersion?: string;
|
|
targetOpenClawVersion?: string;
|
|
},
|
|
{ ok: true; created: boolean }
|
|
>
|
|
)._handler;
|
|
const submitPackageAppealForUserInternalHandler = (
|
|
submitPackageAppealForUserInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
name: string;
|
|
version: string;
|
|
message: string;
|
|
},
|
|
{
|
|
ok: true;
|
|
submitted: boolean;
|
|
alreadyOpen: boolean;
|
|
appealId: string;
|
|
packageId: string;
|
|
releaseId: string;
|
|
status: string;
|
|
}
|
|
>
|
|
)._handler;
|
|
const listPackageAppealsInternalHandler = (
|
|
listPackageAppealsInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
cursor?: string | null;
|
|
limit?: number;
|
|
status?: "open" | "accepted" | "rejected" | "all";
|
|
},
|
|
{
|
|
items: Array<{ appealId: string; name: string; status: string; message: string }>;
|
|
nextCursor: string | null;
|
|
done: boolean;
|
|
}
|
|
>
|
|
)._handler;
|
|
const resolvePackageAppealForUserInternalHandler = (
|
|
resolvePackageAppealForUserInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
appealId: string;
|
|
status: "open" | "accepted" | "rejected";
|
|
note?: string;
|
|
finalAction?: "none" | "approve";
|
|
},
|
|
{
|
|
ok: true;
|
|
appealId: string;
|
|
packageId: string;
|
|
releaseId: string;
|
|
status: string;
|
|
actionTaken?: string;
|
|
}
|
|
>
|
|
)._handler;
|
|
const getPackageReleaseScanBackfillBatchInternalHandler = (
|
|
getPackageReleaseScanBackfillBatchInternal as unknown as WrappedHandler<
|
|
{
|
|
cursor?: number;
|
|
batchSize?: number;
|
|
prioritizeRecent?: boolean;
|
|
},
|
|
{
|
|
releases: Array<{
|
|
releaseId: string;
|
|
packageId: string;
|
|
needsVt: boolean;
|
|
needsLlm: boolean;
|
|
needsStatic: boolean;
|
|
}>;
|
|
nextCursor: number;
|
|
done: boolean;
|
|
}
|
|
>
|
|
)._handler;
|
|
const backfillLatestPackageScanStatusInternalHandler = (
|
|
backfillLatestPackageScanStatusInternal as unknown as WrappedHandler<
|
|
{
|
|
cursor?: string | null;
|
|
batchSize?: number;
|
|
},
|
|
{
|
|
patched: number;
|
|
isDone: boolean;
|
|
scanned: number;
|
|
}
|
|
>
|
|
)._handler;
|
|
const backfillPackageReleaseScansInternalHandler = (
|
|
backfillPackageReleaseScansInternal as unknown as WrappedHandler<
|
|
{
|
|
cursor?: number;
|
|
batchSize?: number;
|
|
scheduled?: number;
|
|
},
|
|
{ scheduled: number; nextCursor: number; done: boolean }
|
|
>
|
|
)._handler;
|
|
const normalizeOfficialPublisherPackagesInternalHandler = (
|
|
normalizeOfficialPublisherPackagesInternal as unknown as WrappedHandler<
|
|
{
|
|
family?: "code-plugin" | "bundle-plugin";
|
|
cursor?: string | null;
|
|
batchSize?: number;
|
|
dryRun?: boolean;
|
|
},
|
|
{
|
|
family: "code-plugin" | "bundle-plugin";
|
|
cursor: string;
|
|
isDone: boolean;
|
|
scanned: number;
|
|
matched: number;
|
|
patched: number;
|
|
skippedPrivate: number;
|
|
dryRun: boolean;
|
|
}
|
|
>
|
|
)._handler;
|
|
const listOfficialPluginMigrationsInternalHandler = (
|
|
listOfficialPluginMigrationsInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
cursor?: string | null;
|
|
limit?: number;
|
|
phase?: "planned" | "blocked" | "ready-for-openclaw" | "all";
|
|
},
|
|
{
|
|
items: Array<{ bundledPluginId: string; packageName: string; phase: string }>;
|
|
nextCursor: string | null;
|
|
done: boolean;
|
|
}
|
|
>
|
|
)._handler;
|
|
const upsertOfficialPluginMigrationForUserInternalHandler = (
|
|
upsertOfficialPluginMigrationForUserInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
bundledPluginId: string;
|
|
packageName: string;
|
|
owner?: string;
|
|
sourceRepo?: string;
|
|
sourcePath?: string;
|
|
sourceCommit?: string;
|
|
phase?: "planned" | "blocked" | "ready-for-openclaw";
|
|
blockers?: string[];
|
|
hostTargetsComplete?: boolean;
|
|
scanClean?: boolean;
|
|
moderationApproved?: boolean;
|
|
runtimeBundlesReady?: boolean;
|
|
notes?: string;
|
|
},
|
|
{
|
|
ok: true;
|
|
migration: { bundledPluginId: string; packageName: string; phase: string };
|
|
}
|
|
>
|
|
)._handler;
|
|
const listPackageModerationQueueInternalHandler = (
|
|
listPackageModerationQueueInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
cursor?: string | null;
|
|
limit?: number;
|
|
status?: "open" | "blocked" | "manual" | "all";
|
|
},
|
|
{
|
|
items: Array<{
|
|
packageId: string;
|
|
releaseId: string;
|
|
name: string;
|
|
version: string;
|
|
scanStatus: string;
|
|
moderationState?: string | null;
|
|
reasons: string[];
|
|
}>;
|
|
nextCursor: string | null;
|
|
done: boolean;
|
|
}
|
|
>
|
|
)._handler;
|
|
const updateReleaseStaticScanInternalHandler = (
|
|
updateReleaseStaticScanInternal as unknown as WrappedHandler<
|
|
{
|
|
releaseId: string;
|
|
staticScan: {
|
|
status: "clean" | "suspicious" | "malicious";
|
|
reasonCodes: string[];
|
|
findings: Array<{
|
|
code: string;
|
|
severity: string;
|
|
file: string;
|
|
line: number;
|
|
message: string;
|
|
evidence: string;
|
|
}>;
|
|
summary: string;
|
|
engineVersion: string;
|
|
checkedAt: number;
|
|
};
|
|
},
|
|
unknown
|
|
>
|
|
)._handler;
|
|
const updateReleaseLlmAnalysisInternalHandler = (
|
|
updateReleaseLlmAnalysisInternal as unknown as WrappedHandler<
|
|
{
|
|
releaseId: string;
|
|
llmAnalysis: {
|
|
status: string;
|
|
verdict?: string;
|
|
confidence?: string;
|
|
summary?: string;
|
|
guidance?: string;
|
|
findings?: string;
|
|
checkedAt: number;
|
|
};
|
|
},
|
|
unknown
|
|
>
|
|
)._handler;
|
|
const softDeletePackageInternalHandler = (
|
|
softDeletePackageInternal as unknown as WrappedHandler<
|
|
{ userId: string; name: string },
|
|
{
|
|
ok: true;
|
|
packageId: string;
|
|
releaseCount: number;
|
|
alreadyDeleted: boolean;
|
|
}
|
|
>
|
|
)._handler;
|
|
const applyBanToOwnedPackagesBatchInternalHandler = (
|
|
applyBanToOwnedPackagesBatchInternal as unknown as WrappedHandler<
|
|
{
|
|
ownerUserId: string;
|
|
bannedAt: number;
|
|
deletedBy: string;
|
|
deletedByRole: "admin" | "moderator" | "user";
|
|
cursor?: string;
|
|
scope?: "ownerUserId" | "personalPublisher";
|
|
},
|
|
{ deletedCount: number; revokedTokenCount: number; scheduled: boolean }
|
|
>
|
|
)._handler;
|
|
const revokePackagePublishTokensForPackageBatchInternalHandler = (
|
|
revokePackagePublishTokensForPackageBatchInternal as unknown as WrappedHandler<
|
|
{ packageId: string; revokedAt: number },
|
|
{ ok: true; revokedCount: number; scheduled: boolean }
|
|
>
|
|
)._handler;
|
|
const restoreOwnedPackagesForUnbanBatchInternalHandler = (
|
|
restoreOwnedPackagesForUnbanBatchInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId?: string;
|
|
ownerUserId: string;
|
|
bannedAt: number;
|
|
cursor?: string;
|
|
scope?: "ownerUserId" | "personalPublisher";
|
|
},
|
|
{ restoredCount: number; scheduled: boolean; stale?: true }
|
|
>
|
|
)._handler;
|
|
const applyAccountDeletionToOwnedPackagesBatchInternalHandler = (
|
|
applyAccountDeletionToOwnedPackagesBatchInternal as unknown as WrappedHandler<
|
|
{
|
|
ownerUserId: string;
|
|
deletedAt: number;
|
|
cursor?: string;
|
|
scope?: "ownerUserId" | "personalPublisher";
|
|
},
|
|
{ deletedCount: number; revokedTokenCount: number; scheduled: boolean }
|
|
>
|
|
)._handler;
|
|
const restorePackageInternalHandler = (
|
|
restorePackageInternal as unknown as WrappedHandler<
|
|
{ userId: string; name: string },
|
|
{
|
|
ok: true;
|
|
packageId: string;
|
|
releaseCount: number;
|
|
alreadyRestored: boolean;
|
|
}
|
|
>
|
|
)._handler;
|
|
const transferPackageOwnerInternalHandler = (
|
|
transferPackageOwnerInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
name: string;
|
|
ownerUserId: string;
|
|
ownerPublisherId?: string;
|
|
channel?: "official" | "community" | "private";
|
|
reason?: string;
|
|
},
|
|
{ ok: true; packageId: string; ownerPublisherId?: string; channel: string }
|
|
>
|
|
)._handler;
|
|
const transferPackageOwnerForUserInternalHandler = (
|
|
transferPackageOwnerForUserInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
name: string;
|
|
toOwner: string;
|
|
reason?: string;
|
|
},
|
|
{ ok: true; packageId: string; ownerPublisherId?: string; channel: string }
|
|
>
|
|
)._handler;
|
|
const repairPackageIdentityInternalHandler = (
|
|
repairPackageIdentityInternal as unknown as WrappedHandler<
|
|
{
|
|
actorUserId: string;
|
|
name: string;
|
|
nextName?: string;
|
|
nextRuntimeId?: string;
|
|
reason: string;
|
|
},
|
|
{ ok: true; packageId: string; name: string; runtimeId?: string }
|
|
>
|
|
)._handler;
|
|
|
|
beforeEach(() => {
|
|
process.env.CLAWHUB_EXPERIMENTAL_CLAWS = "1";
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.mocked(getAuthUserId).mockReset();
|
|
vi.mocked(getAuthUserId).mockResolvedValue(null);
|
|
});
|
|
|
|
function makeDigest(
|
|
name: string,
|
|
overrides: Partial<Record<string, unknown>> = {},
|
|
): Record<string, unknown> {
|
|
return {
|
|
_id: `packageSearchDigest:${name}`,
|
|
packageId: `packages:${name}`,
|
|
name,
|
|
normalizedName: name,
|
|
displayName: name,
|
|
family: "code-plugin",
|
|
runtimeId: null,
|
|
channel: "community",
|
|
isOfficial: false,
|
|
summary: `${name} summary`,
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: undefined,
|
|
ownerHandle: "owner",
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
latestVersion: "1.0.0",
|
|
capabilityTags: [],
|
|
pluginCategoryTags: [],
|
|
executesCode: false,
|
|
verificationTier: null,
|
|
softDeletedAt: undefined,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makePackageDoc(overrides: Partial<Record<string, unknown>> = {}) {
|
|
return {
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
normalizedName: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
channel: "community",
|
|
isOfficial: false,
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: undefined,
|
|
tags: {},
|
|
latestReleaseId: "packageReleases:demo-1",
|
|
latestVersionSummary: { version: "1.0.0" },
|
|
compatibility: null,
|
|
capabilities: null,
|
|
verification: null,
|
|
scanStatus: "clean",
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
softDeletedAt: undefined,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeCanDeleteVersionsCtx(options: {
|
|
viewerId: string;
|
|
viewerRole?: "user" | "admin" | "moderator";
|
|
ownerUserId: string;
|
|
ownerPublisherId?: string;
|
|
membershipRole?: "owner" | "admin" | "publisher";
|
|
packageLookupNames?: string[];
|
|
packageMatchName?: string | null;
|
|
packageOverrides?: Partial<Record<string, unknown>>;
|
|
}) {
|
|
const pkg = makePackageDoc({
|
|
ownerUserId: options.ownerUserId,
|
|
ownerPublisherId: options.ownerPublisherId,
|
|
...options.packageOverrides,
|
|
});
|
|
const publisher = options.ownerPublisherId
|
|
? {
|
|
_id: options.ownerPublisherId,
|
|
kind: "org",
|
|
handle: "demo-org",
|
|
displayName: "Demo Org",
|
|
}
|
|
: null;
|
|
|
|
return {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === options.viewerId) {
|
|
return { _id: id, role: options.viewerRole ?? "user" };
|
|
}
|
|
if (id === options.ownerPublisherId) return publisher;
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "users") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
_indexName: string,
|
|
builder?: (q: { eq: (field: string, value: string) => unknown }) => unknown,
|
|
) => {
|
|
let normalizedName = "";
|
|
const queryBuilder = {
|
|
eq: (field: string, value: string) => {
|
|
if (field === "normalizedName") normalizedName = value;
|
|
return queryBuilder;
|
|
},
|
|
};
|
|
builder?.(queryBuilder);
|
|
options.packageLookupNames?.push(normalizedName);
|
|
return {
|
|
unique: vi
|
|
.fn()
|
|
.mockResolvedValue(
|
|
options.packageMatchName === undefined ||
|
|
normalizedName === options.packageMatchName
|
|
? pkg
|
|
: null,
|
|
),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
}
|
|
if (table === "publisherMembers") {
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
_indexName: string,
|
|
builder?: (q: { eq: (field: string, value: string) => unknown }) => unknown,
|
|
) => {
|
|
let publisherId = "";
|
|
let userId = "";
|
|
const queryBuilder = {
|
|
eq: (field: string, value: string) => {
|
|
if (field === "publisherId") publisherId = value;
|
|
if (field === "userId") userId = value;
|
|
return queryBuilder;
|
|
},
|
|
};
|
|
builder?.(queryBuilder);
|
|
return {
|
|
unique: vi.fn().mockResolvedValue(
|
|
options.membershipRole &&
|
|
publisherId === options.ownerPublisherId &&
|
|
userId === options.viewerId
|
|
? {
|
|
_id: "publisherMembers:viewer",
|
|
publisherId,
|
|
userId,
|
|
role: options.membershipRole,
|
|
}
|
|
: null,
|
|
),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
};
|
|
}
|
|
|
|
function readTestField(row: Record<string, unknown>, field: string): unknown {
|
|
return field.split(".").reduce<unknown>((current, key) => {
|
|
if (typeof current !== "object" || current === null || Array.isArray(current)) return undefined;
|
|
return (current as Record<string, unknown>)[key];
|
|
}, row);
|
|
}
|
|
|
|
function makeReleaseDoc(overrides: Partial<Record<string, unknown>> = {}) {
|
|
return {
|
|
_id: "packageReleases:demo-1",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
createdAt: 1,
|
|
softDeletedAt: undefined,
|
|
createdBy: "users:owner",
|
|
publishActor: { kind: "user", userId: "users:owner" },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeDigestCtx(options: {
|
|
pages?: Array<{
|
|
page: Array<Record<string, unknown>>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}>;
|
|
capabilityPages?: Array<{
|
|
page: Array<Record<string, unknown>>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}>;
|
|
topicPages?: Array<{
|
|
page: Array<Record<string, unknown>>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}>;
|
|
topicPagesByFamily?: Partial<
|
|
Record<
|
|
"skill" | "code-plugin" | "bundle-plugin",
|
|
Array<{
|
|
page: Array<Record<string, unknown>>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}>
|
|
>
|
|
>;
|
|
categoryPages?: Array<{
|
|
page: Array<Record<string, unknown>>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}>;
|
|
categoryRows?: Array<Record<string, unknown>>;
|
|
packagePages?: Array<{
|
|
page: Array<Record<string, unknown>>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}>;
|
|
exactPackages?: Array<Record<string, unknown>>;
|
|
exactDigests?: Array<Record<string, unknown>>;
|
|
publisherDocs?: Record<string, Record<string, unknown>>;
|
|
publisherMemberships?: Record<string, "owner" | "admin" | "publisher">;
|
|
highlightedBadges?: Array<Record<string, unknown>>;
|
|
}) {
|
|
const pageByTable = new Map<
|
|
string,
|
|
Map<
|
|
string | null,
|
|
{
|
|
page: Array<Record<string, unknown>>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}
|
|
>
|
|
>();
|
|
const rowsByTable = new Map<string, Array<Record<string, unknown>>>();
|
|
const familyPagesByTable = new Map<
|
|
string,
|
|
Map<
|
|
string,
|
|
Map<
|
|
string | null,
|
|
{
|
|
page: Array<Record<string, unknown>>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}
|
|
>
|
|
>
|
|
>();
|
|
const indexNames: string[] = [];
|
|
const indexFilters: Array<{
|
|
indexName: string;
|
|
filters: Array<{ field: string; value: unknown }>;
|
|
}> = [];
|
|
const searchIndexNames: string[] = [];
|
|
const tableNames: string[] = [];
|
|
|
|
const setPages = (
|
|
table: string,
|
|
pages: Array<{
|
|
page: Array<Record<string, unknown>>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}>,
|
|
) => {
|
|
const pageByCursor = new Map<
|
|
string | null,
|
|
{
|
|
page: Array<Record<string, unknown>>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}
|
|
>();
|
|
let cursor: string | null = null;
|
|
for (const page of pages) {
|
|
pageByCursor.set(cursor, page);
|
|
cursor = page.continueCursor || null;
|
|
}
|
|
pageByTable.set(table, pageByCursor);
|
|
rowsByTable.set(
|
|
table,
|
|
pages.flatMap((page) => page.page),
|
|
);
|
|
};
|
|
|
|
setPages("packageSearchDigest", options.pages ?? []);
|
|
setPages("packageCapabilitySearchDigest", options.capabilityPages ?? []);
|
|
setPages("packageTopicSearchDigest", options.topicPages ?? []);
|
|
if (options.topicPagesByFamily) {
|
|
const pagesByFamily = new Map<
|
|
string,
|
|
Map<
|
|
string | null,
|
|
{
|
|
page: Array<Record<string, unknown>>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}
|
|
>
|
|
>();
|
|
for (const [family, pages] of Object.entries(options.topicPagesByFamily)) {
|
|
const pagesByCursor = new Map<
|
|
string | null,
|
|
{
|
|
page: Array<Record<string, unknown>>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
}
|
|
>();
|
|
let cursor: string | null = null;
|
|
for (const page of pages ?? []) {
|
|
pagesByCursor.set(cursor, page);
|
|
cursor = page.continueCursor || null;
|
|
}
|
|
pagesByFamily.set(family, pagesByCursor);
|
|
}
|
|
familyPagesByTable.set("packageTopicSearchDigest", pagesByFamily);
|
|
}
|
|
setPages("packagePluginCategorySearchDigest", options.categoryPages ?? []);
|
|
if (options.categoryRows) {
|
|
rowsByTable.set("packagePluginCategorySearchDigest", options.categoryRows);
|
|
}
|
|
setPages("packages", options.packagePages ?? []);
|
|
|
|
const paginate = vi.fn();
|
|
const take = vi.fn();
|
|
const paginateForTable = (table: string, family?: string) =>
|
|
vi.fn(async (args: { cursor: string | null }) => {
|
|
paginate(args);
|
|
return (
|
|
familyPagesByTable
|
|
.get(table)
|
|
?.get(family ?? "")
|
|
?.get(args.cursor ?? null) ??
|
|
pageByTable.get(table)?.get(args.cursor ?? null) ?? {
|
|
page: [],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
}
|
|
);
|
|
});
|
|
const paginateByTable = new Map<string, ReturnType<typeof vi.fn>>();
|
|
const getPaginate = (table: string, family?: string) => {
|
|
const key = `${table}:${family ?? ""}`;
|
|
const existing = paginateByTable.get(key);
|
|
if (existing) return existing;
|
|
const next = paginateForTable(table, family);
|
|
paginateByTable.set(key, next);
|
|
return next;
|
|
};
|
|
const takeForTable = (table: string) =>
|
|
vi.fn(async (limit: number) => {
|
|
take(limit);
|
|
return (rowsByTable.get(table) ?? []).slice(0, limit);
|
|
});
|
|
const takeByTable = new Map<string, ReturnType<typeof vi.fn>>();
|
|
const getTake = (table: string) => {
|
|
const existing = takeByTable.get(table);
|
|
if (existing) return existing;
|
|
const next = takeForTable(table);
|
|
takeByTable.set(table, next);
|
|
return next;
|
|
};
|
|
const searchTake = vi.fn();
|
|
const stringifySearchValue = (value: unknown) =>
|
|
typeof value === "string" ||
|
|
typeof value === "number" ||
|
|
typeof value === "boolean" ||
|
|
typeof value === "bigint"
|
|
? String(value)
|
|
: "";
|
|
const makeSearchQuery = (table: string, searchField: string, query: string) => ({
|
|
take: vi.fn(async (limit: number) => {
|
|
searchTake(limit);
|
|
const queryTokens: string[] = query.toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
|
if (queryTokens.length === 0) return [];
|
|
return [...(options.exactDigests ?? []), ...(rowsByTable.get(table) ?? [])]
|
|
.filter((row, index, rows) => {
|
|
const rowId = stringifySearchValue(row._id ?? row.packageId) || String(index);
|
|
return (
|
|
rows.findIndex(
|
|
(candidate) => stringifySearchValue(candidate._id ?? candidate.packageId) === rowId,
|
|
) === index
|
|
);
|
|
})
|
|
.filter((row) => {
|
|
const text = stringifySearchValue(readTestField(row, searchField)).toLowerCase();
|
|
const textTokens: string[] = text.match(/[a-z0-9]+/g) ?? [];
|
|
return queryTokens.some((queryToken) => textTokens.includes(queryToken));
|
|
})
|
|
.slice(0, limit);
|
|
}),
|
|
});
|
|
|
|
const withIndex = vi.fn((table: string, indexName: string) => {
|
|
indexNames.push(indexName);
|
|
let ordered = false;
|
|
return {
|
|
first: vi.fn(async () => null),
|
|
order: vi.fn(() => {
|
|
if (ordered) throw new Error("query builder reused after iteration");
|
|
ordered = true;
|
|
return {
|
|
paginate: getPaginate(table),
|
|
take: getTake(table),
|
|
};
|
|
}),
|
|
};
|
|
});
|
|
|
|
return {
|
|
indexNames,
|
|
indexFilters,
|
|
searchIndexNames,
|
|
tableNames,
|
|
paginate,
|
|
take,
|
|
searchTake,
|
|
ctx: {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
const exactPackage = (options.exactPackages ?? []).find((pkg) => pkg._id === id);
|
|
if (exactPackage) return exactPackage;
|
|
if (options.publisherDocs?.[id]) return options.publisherDocs[id];
|
|
if (options.publisherMemberships?.[id]) return { _id: id, kind: "org" };
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageBadges") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue(options.highlightedBadges ?? []),
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
indexName: string,
|
|
builder?: (q: {
|
|
eq: (field: string, value: unknown) => unknown;
|
|
gte: (field: string, value: string) => unknown;
|
|
lt: (field: string, value: string) => unknown;
|
|
}) => unknown,
|
|
) => {
|
|
let matchedValue = "";
|
|
let lowerBound = "";
|
|
let upperBound = "";
|
|
const filters: Array<{ field: string; value: unknown }> = [];
|
|
const rangeFilters: Array<{ field: string; value: number }> = [];
|
|
const queryBuilder = {
|
|
eq: (field: string, value: unknown) => {
|
|
filters.push({ field, value });
|
|
matchedValue = typeof value === "string" ? value : "";
|
|
return queryBuilder;
|
|
},
|
|
gte: (_field: string, value: string) => {
|
|
lowerBound = value;
|
|
return queryBuilder;
|
|
},
|
|
lt: (field: string, value: string | number) => {
|
|
upperBound = typeof value === "string" ? value : "";
|
|
if (typeof value === "number") {
|
|
rangeFilters.push({ field, value });
|
|
}
|
|
return queryBuilder;
|
|
},
|
|
};
|
|
builder?.(queryBuilder);
|
|
if (
|
|
indexName === "by_active_downloads" ||
|
|
indexName === "by_active_family_downloads" ||
|
|
indexName === "by_active_family_official_downloads" ||
|
|
indexName === "by_active_installs" ||
|
|
indexName === "by_active_family_installs" ||
|
|
indexName === "by_active_family_official_installs" ||
|
|
indexName === "by_active_recommended_rank" ||
|
|
indexName === "by_active_family_recommended_rank" ||
|
|
indexName === "by_active_recommended_score" ||
|
|
indexName === "by_active_family_recommended_score" ||
|
|
indexName === "by_active_recommended_score_version" ||
|
|
indexName === "by_active_family_recommended_score_version"
|
|
) {
|
|
indexFilters.push({ indexName, filters });
|
|
const indexedQuery = withIndex(table, indexName);
|
|
return {
|
|
...indexedQuery,
|
|
first: vi.fn().mockResolvedValue(
|
|
(rowsByTable.get(table) ?? []).find(
|
|
(row) =>
|
|
filters.every(
|
|
({ field, value }) => readTestField(row, field) === value,
|
|
) &&
|
|
rangeFilters.every(({ field, value }) => {
|
|
const current = readTestField(row, field);
|
|
return typeof current === "number" && current < value;
|
|
}),
|
|
) ?? null,
|
|
),
|
|
};
|
|
}
|
|
if (indexName !== "by_name" && indexName !== "by_runtime_id") {
|
|
throw new Error(`Unexpected packages index ${indexName}`);
|
|
}
|
|
const matches = (options.exactPackages ?? []).filter((pkg) =>
|
|
indexName === "by_name"
|
|
? matchedValue
|
|
? String(pkg.normalizedName) === matchedValue
|
|
: String(pkg.normalizedName) >= lowerBound &&
|
|
String(pkg.normalizedName) < upperBound
|
|
: matchedValue
|
|
? String(pkg.runtimeId) === matchedValue
|
|
: String(pkg.runtimeId) >= lowerBound && String(pkg.runtimeId) < upperBound,
|
|
);
|
|
return {
|
|
unique: vi.fn().mockResolvedValue(matches[0] ?? null),
|
|
take: vi.fn().mockResolvedValue(matches),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
}
|
|
if (table === "publisherMembers") {
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
_indexName: string,
|
|
builder?: (q: { eq: (field: string, value: string) => unknown }) => unknown,
|
|
) => {
|
|
let publisherId = "";
|
|
const queryBuilder = {
|
|
eq: (field: string, value: string) => {
|
|
if (field === "publisherId") publisherId = value;
|
|
return queryBuilder;
|
|
},
|
|
};
|
|
builder?.(queryBuilder);
|
|
const role = options.publisherMemberships?.[publisherId];
|
|
return {
|
|
unique: vi.fn().mockResolvedValue(
|
|
role
|
|
? {
|
|
_id: `publisherMembers:${publisherId}`,
|
|
publisherId,
|
|
userId: "users:member",
|
|
role,
|
|
}
|
|
: null,
|
|
),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
}
|
|
if (table === "packageSearchDigest") {
|
|
tableNames.push(table);
|
|
return {
|
|
withIndex: (
|
|
indexName: string,
|
|
builder?: (q: {
|
|
eq: (field: string, value: string | undefined) => unknown;
|
|
gte: (field: string, value: string) => unknown;
|
|
lt: (field: string, value: string) => unknown;
|
|
}) => unknown,
|
|
) => {
|
|
if (indexName === "by_package") {
|
|
let packageId = "";
|
|
const queryBuilder = {
|
|
eq: (field: string, value: string | undefined) => {
|
|
if (field === "packageId") packageId = value ?? "";
|
|
return queryBuilder;
|
|
},
|
|
gte: () => queryBuilder,
|
|
lt: () => queryBuilder,
|
|
};
|
|
builder?.(queryBuilder);
|
|
const match = (options.exactDigests ?? []).find(
|
|
(digest) => digest.packageId === packageId,
|
|
);
|
|
return {
|
|
unique: vi.fn().mockResolvedValue(match ?? null),
|
|
};
|
|
}
|
|
if (
|
|
indexName === "by_active_normalized_name" ||
|
|
indexName === "by_active_runtime_id" ||
|
|
indexName === "by_active_owner_handle"
|
|
) {
|
|
let lowerBound = "";
|
|
let upperBound = "";
|
|
const queryBuilder = {
|
|
eq: () => queryBuilder,
|
|
gte: (_field: string, value: string) => {
|
|
lowerBound = value;
|
|
return queryBuilder;
|
|
},
|
|
lt: (_field: string, value: string) => {
|
|
upperBound = value;
|
|
return queryBuilder;
|
|
},
|
|
};
|
|
builder?.(queryBuilder);
|
|
const matches = (options.exactDigests ?? []).filter((digest) =>
|
|
indexName === "by_active_normalized_name"
|
|
? String(digest.normalizedName) >= lowerBound &&
|
|
String(digest.normalizedName) < upperBound
|
|
: indexName === "by_active_runtime_id"
|
|
? String(digest.runtimeId) >= lowerBound &&
|
|
String(digest.runtimeId) < upperBound
|
|
: String(digest.ownerHandle) >= lowerBound &&
|
|
String(digest.ownerHandle) < upperBound,
|
|
);
|
|
return {
|
|
take: vi.fn().mockResolvedValue(matches),
|
|
};
|
|
}
|
|
if (indexName.includes("_family_")) {
|
|
indexNames.push(indexName);
|
|
let family = "";
|
|
let lowerField = "";
|
|
let lowerBound = "";
|
|
let upperBound = "";
|
|
const queryBuilder = {
|
|
eq: (field: string, value: string | undefined) => {
|
|
if (field === "family") family = value ?? "";
|
|
return queryBuilder;
|
|
},
|
|
gte: (field: string, value: string) => {
|
|
lowerField = field;
|
|
lowerBound = value;
|
|
return queryBuilder;
|
|
},
|
|
lt: (_field: string, value: string) => {
|
|
upperBound = value;
|
|
return queryBuilder;
|
|
},
|
|
};
|
|
builder?.(queryBuilder);
|
|
const takeFamilyRows = async (limit: number) => {
|
|
take(limit);
|
|
return (rowsByTable.get(table) ?? [])
|
|
.filter((row) => row.family === family)
|
|
.filter((row) => {
|
|
if (!lowerField) return true;
|
|
const value = readTestField(row, lowerField);
|
|
return (
|
|
typeof value === "string" &&
|
|
value >= lowerBound &&
|
|
(!upperBound || value < upperBound)
|
|
);
|
|
})
|
|
.slice(0, limit);
|
|
};
|
|
return {
|
|
take: vi.fn(takeFamilyRows),
|
|
order: vi.fn(() => ({
|
|
paginate: getPaginate(table),
|
|
take: vi.fn(takeFamilyRows),
|
|
})),
|
|
};
|
|
}
|
|
return withIndex(table, indexName);
|
|
},
|
|
withSearchIndex: (
|
|
indexName: string,
|
|
builder?: (q: {
|
|
search: (
|
|
field: string,
|
|
query: string,
|
|
) => {
|
|
eq: (field: string, value: string | undefined) => unknown;
|
|
};
|
|
}) => unknown,
|
|
) => {
|
|
searchIndexNames.push(indexName);
|
|
let searchField = "";
|
|
let query = "";
|
|
const queryBuilder = {
|
|
eq: () => queryBuilder,
|
|
search: (field: string, value: string) => {
|
|
searchField = field;
|
|
query = value;
|
|
return queryBuilder;
|
|
},
|
|
};
|
|
builder?.(queryBuilder);
|
|
return makeSearchQuery(table, searchField, query);
|
|
},
|
|
};
|
|
}
|
|
if (
|
|
table !== "packageCapabilitySearchDigest" &&
|
|
table !== "packageTopicSearchDigest" &&
|
|
table !== "packagePluginCategorySearchDigest"
|
|
) {
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}
|
|
tableNames.push(table);
|
|
return {
|
|
withIndex: (
|
|
indexName: string,
|
|
builder?: (q: {
|
|
eq: (field: string, value: unknown) => unknown;
|
|
gte: (field: string, value: string) => unknown;
|
|
lt: (field: string, value: string) => unknown;
|
|
}) => unknown,
|
|
) => {
|
|
if (indexName.includes("_family_")) {
|
|
indexNames.push(indexName);
|
|
let family = "";
|
|
let exactTopic = "";
|
|
let exactCategory = "";
|
|
let lowerTopic = "";
|
|
let upperTopic = "";
|
|
const queryBuilder = {
|
|
eq: (field: string, value: unknown) => {
|
|
if (field === "family" && typeof value === "string") family = value;
|
|
if (field === "topic" && typeof value === "string") exactTopic = value;
|
|
if (field === "pluginCategory" && typeof value === "string") {
|
|
exactCategory = value;
|
|
}
|
|
return queryBuilder;
|
|
},
|
|
gte: (field: string, value: string) => {
|
|
if (field === "topic") lowerTopic = value;
|
|
return queryBuilder;
|
|
},
|
|
lt: (field: string, value: string) => {
|
|
if (field === "topic") upperTopic = value;
|
|
return queryBuilder;
|
|
},
|
|
};
|
|
builder?.(queryBuilder);
|
|
const takeFamilyRows = async (limit: number) => {
|
|
take(limit);
|
|
return (rowsByTable.get(table) ?? [])
|
|
.filter((row) => row.family === family)
|
|
.filter((row) => !exactTopic || row.topic === exactTopic)
|
|
.filter((row) => !exactCategory || row.pluginCategory === exactCategory)
|
|
.filter((row) => {
|
|
if (!lowerTopic && !upperTopic) return true;
|
|
const topic = typeof row.topic === "string" ? row.topic : "";
|
|
return topic >= lowerTopic && topic < upperTopic;
|
|
})
|
|
.slice(0, limit);
|
|
};
|
|
return {
|
|
order: vi.fn(() => ({
|
|
paginate: getPaginate(table, family),
|
|
take: vi.fn(takeFamilyRows),
|
|
})),
|
|
};
|
|
}
|
|
if (table !== "packageTopicSearchDigest" || indexName !== "by_active_topic_updated") {
|
|
return withIndex(table, indexName);
|
|
}
|
|
let exactTopic = "";
|
|
let lowerBound = "";
|
|
let upperBound = "";
|
|
const queryBuilder = {
|
|
eq: (field: string, value: unknown) => {
|
|
if (field === "topic" && typeof value === "string") exactTopic = value;
|
|
return queryBuilder;
|
|
},
|
|
gte: (field: string, value: string) => {
|
|
if (field === "topic") lowerBound = value;
|
|
return queryBuilder;
|
|
},
|
|
lt: (field: string, value: string) => {
|
|
if (field === "topic") upperBound = value;
|
|
return queryBuilder;
|
|
},
|
|
};
|
|
builder?.(queryBuilder);
|
|
const baseQuery = withIndex(table, indexName);
|
|
return {
|
|
...baseQuery,
|
|
order: () => {
|
|
const ordered = baseQuery.order();
|
|
return {
|
|
...ordered,
|
|
take: async (limit: number) => {
|
|
take(limit);
|
|
const rows = rowsByTable.get(table) ?? [];
|
|
return rows
|
|
.filter((row) => {
|
|
const rowTopic = typeof row.topic === "string" ? row.topic : "";
|
|
if (exactTopic) return rowTopic === exactTopic;
|
|
return rowTopic >= lowerBound && rowTopic < upperBound;
|
|
})
|
|
.slice(0, limit);
|
|
},
|
|
};
|
|
},
|
|
};
|
|
},
|
|
};
|
|
}),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function makePluginExportDigest(
|
|
name: string,
|
|
family: "code-plugin" | "bundle-plugin",
|
|
updatedAt: number,
|
|
) {
|
|
return makeDigest(name, {
|
|
_id: `packageSearchDigest:${name}`,
|
|
packageId: `packages:${name}`,
|
|
family,
|
|
scanStatus: "clean",
|
|
updatedAt,
|
|
_creationTime: updatedAt,
|
|
});
|
|
}
|
|
|
|
function readIndexField(row: Record<string, unknown>, field: string) {
|
|
return field.split(".").reduce<unknown>((value, segment) => {
|
|
if (!value || typeof value !== "object") return undefined;
|
|
return (value as Record<string, unknown>)[segment];
|
|
}, row);
|
|
}
|
|
|
|
function makePluginExportIndexKey(row: Record<string, unknown>, field = "updatedAt") {
|
|
return [row.softDeletedAt, row.family, row[field], row._creationTime, row._id] as unknown[];
|
|
}
|
|
|
|
function makePluginExportCtx(
|
|
digests: Array<Record<string, unknown>>,
|
|
options?: { recommendationScoresMissing?: () => boolean },
|
|
) {
|
|
const packagesById = new Map(
|
|
digests.map((digest) => [
|
|
String(digest.packageId),
|
|
makePackageDoc({
|
|
_id: digest.packageId,
|
|
name: digest.name,
|
|
normalizedName: digest.normalizedName,
|
|
displayName: digest.displayName,
|
|
family: digest.family,
|
|
ownerUserId: digest.ownerUserId,
|
|
ownerPublisherId: digest.ownerPublisherId,
|
|
latestReleaseId: `packageReleases:${String(digest.name)}-1`,
|
|
latestVersionSummary: { version: digest.latestVersion },
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
scanStatus: "clean",
|
|
createdAt: digest.createdAt,
|
|
updatedAt: digest.updatedAt,
|
|
}),
|
|
]),
|
|
);
|
|
|
|
return {
|
|
db: {
|
|
get: vi.fn(async (id: string) => packagesById.get(id) ?? null),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
first: vi.fn(async () =>
|
|
options?.recommendationScoresMissing?.() ? { _id: "packages:missing-score" } : null,
|
|
),
|
|
})),
|
|
};
|
|
}
|
|
if (table !== "packageSearchDigest") throw new Error(`Unexpected table ${table}`);
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
indexName: string,
|
|
builder?: (q: {
|
|
eq: (field: string, value: unknown) => unknown;
|
|
gt: (field: string, value: unknown) => unknown;
|
|
gte: (field: string, value: unknown) => unknown;
|
|
lt: (field: string, value: unknown) => unknown;
|
|
lte: (field: string, value: unknown) => unknown;
|
|
}) => unknown,
|
|
) => {
|
|
if (
|
|
indexName !== "by_active_family_updated" &&
|
|
indexName !== "by_active_family_created"
|
|
) {
|
|
throw new Error(`Unexpected packageSearchDigest index ${indexName}`);
|
|
}
|
|
const filters: Array<{
|
|
op: "eq" | "gt" | "gte" | "lt" | "lte";
|
|
field: string;
|
|
value: unknown;
|
|
}> = [];
|
|
const queryBuilder = {
|
|
eq: (field: string, value: unknown) => {
|
|
filters.push({ op: "eq", field, value });
|
|
return queryBuilder;
|
|
},
|
|
gt: (field: string, value: unknown) => {
|
|
filters.push({ op: "gt", field, value });
|
|
return queryBuilder;
|
|
},
|
|
gte: (field: string, value: unknown) => {
|
|
filters.push({ op: "gte", field, value });
|
|
return queryBuilder;
|
|
},
|
|
lt: (field: string, value: unknown) => {
|
|
filters.push({ op: "lt", field, value });
|
|
return queryBuilder;
|
|
},
|
|
lte: (field: string, value: unknown) => {
|
|
filters.push({ op: "lte", field, value });
|
|
return queryBuilder;
|
|
},
|
|
};
|
|
builder?.(queryBuilder);
|
|
return {
|
|
order: vi.fn((order: "asc" | "desc") => {
|
|
const matches = digests
|
|
.filter((row) =>
|
|
filters.every(({ op, field, value }) => {
|
|
const current = readIndexField(row, field);
|
|
if (op === "eq") return current === value;
|
|
if (op === "gt") return (current as number) > (value as number);
|
|
if (op === "gte") return (current as number) >= (value as number);
|
|
if (op === "lt") return (current as number) < (value as number);
|
|
return (current as number) <= (value as number);
|
|
}),
|
|
)
|
|
.sort((a, b) => {
|
|
const sortField =
|
|
indexName === "by_active_family_created" ? "createdAt" : "updatedAt";
|
|
const metricDiff = Number(a[sortField]) - Number(b[sortField]);
|
|
if (metricDiff !== 0) return order === "desc" ? -metricDiff : metricDiff;
|
|
const idDiff = String(a._id).localeCompare(String(b._id));
|
|
return order === "desc" ? -idDiff : idDiff;
|
|
});
|
|
return {
|
|
async *[Symbol.asyncIterator]() {
|
|
for (const row of matches) {
|
|
yield row;
|
|
}
|
|
},
|
|
async *iterWithKeys() {
|
|
for (const row of matches) {
|
|
yield [
|
|
row,
|
|
makePluginExportIndexKey(
|
|
row,
|
|
indexName === "by_active_family_created" ? "createdAt" : "updatedAt",
|
|
),
|
|
];
|
|
}
|
|
},
|
|
};
|
|
}),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
}),
|
|
},
|
|
};
|
|
}
|
|
|
|
function makePluginValidationReportCtx(
|
|
digests: Array<Record<string, unknown>>,
|
|
options: {
|
|
statesByRelease?: Record<string, Array<Record<string, unknown>>>;
|
|
findingsByRelease?: Record<string, Array<Record<string, unknown>>>;
|
|
} = {},
|
|
) {
|
|
const base = makePluginExportCtx(digests);
|
|
const releasesById = new Map(
|
|
digests.map((digest) => {
|
|
const name = String(digest.name);
|
|
const packageId = String(digest.packageId);
|
|
return [
|
|
`packageReleases:${name}-1`,
|
|
{
|
|
_id: `packageReleases:${name}-1`,
|
|
packageId,
|
|
version: typeof digest.latestVersion === "string" ? digest.latestVersion : "1.0.0",
|
|
createdAt: Number(digest.createdAt),
|
|
},
|
|
];
|
|
}),
|
|
);
|
|
return {
|
|
db: {
|
|
get: vi.fn(async (id: string) => (await base.db.get(id)) ?? releasesById.get(id) ?? null),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageSearchDigest" || table === "packages") {
|
|
return base.db.query(table);
|
|
}
|
|
if (table === "packageInspectorScanStates") {
|
|
let releaseId = "";
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
_indexName: string,
|
|
builder: (q: { eq: (field: string, value: unknown) => unknown }) => unknown,
|
|
) => {
|
|
const queryBuilder = {
|
|
eq: (field: string, value: unknown) => {
|
|
if (field === "releaseId") releaseId = String(value);
|
|
return queryBuilder;
|
|
},
|
|
};
|
|
builder(queryBuilder);
|
|
return {
|
|
order: vi.fn(() => ({
|
|
first: vi.fn(
|
|
async () =>
|
|
[...(options.statesByRelease?.[releaseId] ?? [])].sort(
|
|
(a, b) => Number(b.completedAt) - Number(a.completedAt),
|
|
)[0] ?? null,
|
|
),
|
|
})),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
}
|
|
if (table === "packageInspectorWarnings") {
|
|
let releaseId = "";
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
_indexName: string,
|
|
builder: (q: { eq: (field: string, value: unknown) => unknown }) => unknown,
|
|
) => {
|
|
const queryBuilder = {
|
|
eq: (field: string, value: unknown) => {
|
|
if (field === "releaseId") releaseId = String(value);
|
|
return queryBuilder;
|
|
},
|
|
};
|
|
builder(queryBuilder);
|
|
return {
|
|
order: vi.fn(() => ({
|
|
collect: vi.fn(async () => options.findingsByRelease?.[releaseId] ?? []),
|
|
})),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
};
|
|
}
|
|
|
|
function makeInsertReleaseCtx(
|
|
existing: Record<string, unknown> | null,
|
|
priorReleases: Array<Record<string, unknown>> = [],
|
|
recordsById: Record<string, Record<string, unknown>> = {},
|
|
runtimePackages: Array<Record<string, unknown>> = [],
|
|
finalPublisherMembershipRole?: "owner" | "admin" | "publisher" | null,
|
|
packageReleaseReadLimitBytes?: number,
|
|
) {
|
|
let insertedPackage: Record<string, unknown> | null = null;
|
|
const patch = vi.fn(async (id: string, value: Record<string, unknown>) => {
|
|
const target =
|
|
recordsById[id] ??
|
|
priorReleases.find((release) => release._id === id) ??
|
|
(existing?._id === id ? existing : null) ??
|
|
(insertedPackage?._id === id ? insertedPackage : null);
|
|
if (target) Object.assign(target, value);
|
|
});
|
|
const scheduler = {
|
|
runAfter: vi.fn(),
|
|
};
|
|
let packageReleaseBytesRead = 0;
|
|
const readPackageRelease = (record: Record<string, unknown> | null) => {
|
|
if (!record || packageReleaseReadLimitBytes === undefined) return record;
|
|
packageReleaseBytesRead += JSON.stringify(record).length;
|
|
if (packageReleaseBytesRead > packageReleaseReadLimitBytes) {
|
|
throw new Error(
|
|
`Too many bytes read in a single function execution (limit: ${packageReleaseReadLimitBytes} bytes).`,
|
|
);
|
|
}
|
|
return record;
|
|
};
|
|
const insert = vi.fn(async (table: string, doc: Record<string, unknown>) => {
|
|
if (table === "packages") {
|
|
insertedPackage = makePackageDoc({
|
|
...doc,
|
|
_id: "packages:new",
|
|
tags: {},
|
|
latestReleaseId: undefined,
|
|
latestVersionSummary: undefined,
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 0 },
|
|
});
|
|
return "packages:new";
|
|
}
|
|
if (table === "packageReleases") return "packageReleases:new";
|
|
return `${table}:new`;
|
|
});
|
|
return {
|
|
patch,
|
|
insert,
|
|
scheduler,
|
|
resetPackageReleaseReadBudget: () => {
|
|
packageReleaseBytesRead = 0;
|
|
},
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id in recordsById) {
|
|
return id.startsWith("packageReleases:")
|
|
? readPackageRelease(recordsById[id])
|
|
: recordsById[id];
|
|
}
|
|
if (id === "packages:new") return insertedPackage;
|
|
if (existing?._id === id) return existing;
|
|
if (id === "users:owner") return { _id: id, role: "user", trustedPublisher: false };
|
|
const priorRelease = priorReleases.find((release) => release._id === id);
|
|
if (priorRelease) return readPackageRelease(priorRelease);
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
indexName: string,
|
|
buildQuery?: (q: { eq: (field: string, value: unknown) => unknown }) => unknown,
|
|
) => {
|
|
const filters = new Map<string, unknown>();
|
|
const query = {
|
|
eq(field: string, value: unknown) {
|
|
filters.set(field, value);
|
|
return query;
|
|
},
|
|
};
|
|
buildQuery?.(query);
|
|
if (indexName === "by_runtime_id") {
|
|
const runtimeId = filters.get("runtimeId");
|
|
const matches = runtimePackages.filter((pkg) => pkg.runtimeId === runtimeId);
|
|
return {
|
|
collect: vi.fn().mockResolvedValue(matches),
|
|
unique: vi.fn().mockResolvedValue(matches[0] ?? null),
|
|
};
|
|
}
|
|
return {
|
|
unique: vi.fn().mockResolvedValue(existing),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
indexName: string,
|
|
buildQuery?: (q: { eq: (field: string, value: unknown) => unknown }) => unknown,
|
|
) => {
|
|
if (indexName === "by_package") {
|
|
return {
|
|
collect: vi.fn(async () => {
|
|
if (
|
|
packageReleaseReadLimitBytes !== undefined &&
|
|
JSON.stringify(priorReleases).length > packageReleaseReadLimitBytes
|
|
) {
|
|
throw new Error(
|
|
`Too many bytes read in a single function execution (limit: ${packageReleaseReadLimitBytes} bytes).`,
|
|
);
|
|
}
|
|
return priorReleases;
|
|
}),
|
|
};
|
|
}
|
|
if (indexName === "by_package_version") {
|
|
const filters = new Map<string, unknown>();
|
|
const query = {
|
|
eq(field: string, value: unknown) {
|
|
filters.set(field, value);
|
|
return query;
|
|
},
|
|
};
|
|
buildQuery?.(query);
|
|
return {
|
|
unique: vi
|
|
.fn()
|
|
.mockResolvedValue(
|
|
priorReleases.find(
|
|
(release) =>
|
|
release.packageId === filters.get("packageId") &&
|
|
release.version === filters.get("version"),
|
|
) ?? null,
|
|
),
|
|
};
|
|
}
|
|
return {
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
}
|
|
if (table === "publisherMembers") {
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
_indexName: string,
|
|
buildQuery?: (q: { eq: (field: string, value: unknown) => unknown }) => unknown,
|
|
) => {
|
|
const filters = new Map<string, unknown>();
|
|
const query = {
|
|
eq(field: string, value: unknown) {
|
|
filters.set(field, value);
|
|
return query;
|
|
},
|
|
};
|
|
buildQuery?.(query);
|
|
const membership =
|
|
finalPublisherMembershipRole &&
|
|
filters.get("publisherId") === "publishers:org" &&
|
|
filters.get("userId") === "users:member"
|
|
? {
|
|
_id: "publisherMembers:org-member",
|
|
publisherId: "publishers:org",
|
|
userId: "users:member",
|
|
role: finalPublisherMembershipRole,
|
|
}
|
|
: null;
|
|
return {
|
|
unique: vi.fn().mockResolvedValue(membership),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
}
|
|
if (table === "officialPublishers") {
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
_indexName: string,
|
|
buildQuery?: (q: { eq: (field: string, value: unknown) => unknown }) => unknown,
|
|
) => {
|
|
const filters = new Map<string, unknown>();
|
|
const query = {
|
|
eq(field: string, value: unknown) {
|
|
filters.set(field, value);
|
|
return query;
|
|
},
|
|
};
|
|
buildQuery?.(query);
|
|
const rawPublisherId = filters.get("publisherId");
|
|
const publisherId = typeof rawPublisherId === "string" ? rawPublisherId : "";
|
|
const publisher = recordsById[publisherId];
|
|
return {
|
|
unique: vi
|
|
.fn()
|
|
.mockResolvedValue(
|
|
publisher?.handle === "openclaw"
|
|
? { _id: "officialPublishers:openclaw", publisherId }
|
|
: null,
|
|
),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert,
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
};
|
|
}
|
|
|
|
async function flushScheduledPackageReleaseTagCleanup(
|
|
ctx: ReturnType<typeof makeInsertReleaseCtx>,
|
|
) {
|
|
for (let callIndex = 0; callIndex < ctx.scheduler.runAfter.mock.calls.length; callIndex += 1) {
|
|
const [, , args] = ctx.scheduler.runAfter.mock.calls[callIndex] as [
|
|
number,
|
|
unknown,
|
|
{
|
|
packageId: string;
|
|
assignments: Array<{ releaseId: string; tags: string[] }>;
|
|
offset?: number;
|
|
},
|
|
];
|
|
ctx.resetPackageReleaseReadBudget();
|
|
await cleanupReassignedPackageReleaseTagsInternalHandler(ctx, args);
|
|
}
|
|
}
|
|
|
|
function makeReservePackageNameCtx(options?: {
|
|
existing?: Record<string, unknown> | null;
|
|
actor?: Record<string, unknown> | null;
|
|
owner?: Record<string, unknown> | null;
|
|
ownerPublisher?: Record<string, unknown> | null;
|
|
}) {
|
|
const insert = vi
|
|
.fn()
|
|
.mockResolvedValueOnce("packages:reserved")
|
|
.mockResolvedValueOnce("auditLogs:reserved");
|
|
return {
|
|
insert,
|
|
ctx: {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:admin") {
|
|
return options?.actor ?? { _id: id, role: "admin" };
|
|
}
|
|
if (id === "users:openclaw") {
|
|
return options?.owner ?? { _id: id, role: "user" };
|
|
}
|
|
if (id === "publishers:openclaw") {
|
|
return (
|
|
options?.ownerPublisher ?? {
|
|
_id: id,
|
|
kind: "org",
|
|
handle: "openclaw",
|
|
displayName: "OpenClaw",
|
|
trustedPublisher: true,
|
|
}
|
|
);
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table !== "packages") throw new Error(`Unexpected table ${table}`);
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(options?.existing ?? null),
|
|
})),
|
|
};
|
|
}),
|
|
insert,
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function makeTransferPackageOwnerCtx(options?: {
|
|
pkg?: Record<string, unknown> | null;
|
|
actor?: Record<string, unknown> | null;
|
|
owner?: Record<string, unknown> | null;
|
|
ownerPublisher?: Record<string, unknown> | null;
|
|
}) {
|
|
const pkg = options?.pkg ?? makePackageDoc();
|
|
const packageSearchDigest = { _id: "packageSearchDigest:demo", packageId: pkg?._id };
|
|
const patch = vi.fn();
|
|
const insert = vi.fn();
|
|
return {
|
|
insert,
|
|
patch,
|
|
ctx: {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:admin") {
|
|
return options?.actor ?? { _id: id, role: "admin" };
|
|
}
|
|
if (id === "users:openclaw") {
|
|
return options?.owner ?? { _id: id, role: "user" };
|
|
}
|
|
if (id === "publishers:openclaw") {
|
|
return (
|
|
options?.ownerPublisher ?? {
|
|
_id: id,
|
|
kind: "org",
|
|
handle: "openclaw",
|
|
displayName: "OpenClaw",
|
|
trustedPublisher: true,
|
|
}
|
|
);
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(pkg),
|
|
collect: vi.fn().mockResolvedValue(pkg ? [pkg] : []),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageSearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(packageSearchDigest),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "officialPublishers") {
|
|
return {
|
|
withIndex: vi.fn((_indexName: string, builder: (q: unknown) => unknown) => {
|
|
const terms: Record<string, unknown> = {};
|
|
builder({
|
|
eq: (field: string, value: unknown) => {
|
|
terms[field] = value;
|
|
return {};
|
|
},
|
|
});
|
|
const ownerPublisher =
|
|
terms.publisherId === "publishers:openclaw"
|
|
? (options?.ownerPublisher ?? {
|
|
_id: "publishers:openclaw",
|
|
kind: "org",
|
|
handle: "openclaw",
|
|
displayName: "OpenClaw",
|
|
trustedPublisher: true,
|
|
})
|
|
: null;
|
|
return {
|
|
unique: vi
|
|
.fn()
|
|
.mockResolvedValue(
|
|
ownerPublisher?.handle === "openclaw"
|
|
? { _id: "officialPublishers:openclaw", publisherId: terms.publisherId }
|
|
: null,
|
|
),
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
if (
|
|
table === "packageCapabilitySearchDigest" ||
|
|
table === "packageTopicSearchDigest" ||
|
|
table === "packagePluginCategorySearchDigest"
|
|
) {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert,
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function makeUserTransferPackageOwnerCtx(options?: {
|
|
pkg?: Record<string, unknown> | null;
|
|
actor?: Record<string, unknown> | null;
|
|
sourcePublisher?: Record<string, unknown> | null;
|
|
destinationPublisher?: Record<string, unknown> | null;
|
|
sourceMembershipRole?: "owner" | "admin" | "publisher" | null;
|
|
destinationMembershipRole?: "owner" | "admin" | "publisher" | null;
|
|
trustedPublisher?: Record<string, unknown> | null;
|
|
}) {
|
|
const pkg =
|
|
options?.pkg ??
|
|
makePackageDoc({
|
|
_id: "packages:opik",
|
|
name: "@opik/opik-openclaw",
|
|
normalizedName: "@opik/opik-openclaw",
|
|
ownerUserId: "users:vincent",
|
|
ownerPublisherId: "publishers:vincent",
|
|
stats: { downloads: 42, installs: 7, stars: 3, versions: 1 },
|
|
});
|
|
const actor = options?.actor ?? { _id: "users:vincent", role: "user" };
|
|
const destinationPublisher =
|
|
options?.destinationPublisher === undefined
|
|
? {
|
|
_id: "publishers:opik",
|
|
kind: "org",
|
|
handle: "opik",
|
|
displayName: "Opik",
|
|
trustedPublisher: false,
|
|
}
|
|
: options.destinationPublisher;
|
|
const patch = vi.fn();
|
|
const insert = vi.fn();
|
|
const packageSearchDigest = { _id: "packageSearchDigest:opik", packageId: pkg?._id };
|
|
return {
|
|
insert,
|
|
patch,
|
|
pkg,
|
|
ctx: {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:vincent") return actor;
|
|
if (id === "publishers:vincent") {
|
|
if (options?.sourcePublisher !== undefined) return options.sourcePublisher;
|
|
return {
|
|
_id: id,
|
|
kind: "user",
|
|
handle: "vincentkoc",
|
|
linkedUserId: "users:vincent",
|
|
};
|
|
}
|
|
if (id === "publishers:opik") return destinationPublisher;
|
|
if (id === "packageTrustedPublishers:opik") return options?.trustedPublisher ?? null;
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(pkg),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageSearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(packageSearchDigest),
|
|
})),
|
|
};
|
|
}
|
|
if (
|
|
table === "packageCapabilitySearchDigest" ||
|
|
table === "packageTopicSearchDigest" ||
|
|
table === "packagePluginCategorySearchDigest"
|
|
) {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "publishers") {
|
|
return {
|
|
withIndex: vi.fn((_indexName: string, builder: (q: unknown) => unknown) => {
|
|
const terms: Record<string, unknown> = {};
|
|
builder({
|
|
eq: (field: string, value: unknown) => {
|
|
terms[field] = value;
|
|
return {};
|
|
},
|
|
});
|
|
return {
|
|
unique: vi
|
|
.fn()
|
|
.mockResolvedValue(terms.handle === "opik" ? destinationPublisher : null),
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
if (table === "publisherMembers") {
|
|
return {
|
|
withIndex: vi.fn((_indexName: string, builder: (q: unknown) => unknown) => {
|
|
const terms: Record<string, unknown> = {};
|
|
builder({
|
|
eq: (field: string, value: unknown) => {
|
|
terms[field] = value;
|
|
return {
|
|
eq: (nextField: string, nextValue: unknown) => {
|
|
terms[nextField] = nextValue;
|
|
return {};
|
|
},
|
|
};
|
|
},
|
|
});
|
|
const publisherId = typeof terms.publisherId === "string" ? terms.publisherId : "";
|
|
const role =
|
|
publisherId === "publishers:vincent"
|
|
? options?.sourceMembershipRole
|
|
: publisherId === "publishers:opik"
|
|
? options?.destinationMembershipRole
|
|
: null;
|
|
return {
|
|
unique: vi
|
|
.fn()
|
|
.mockResolvedValue(
|
|
role ? { _id: `publisherMembers:${publisherId}`, publisherId, role } : null,
|
|
),
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
if (table === "officialPublishers") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert,
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function makePackageCtx(options: {
|
|
pkg?: Record<string, unknown> | null;
|
|
latestRelease?: Record<string, unknown> | null;
|
|
versionRelease?: Record<string, unknown> | null;
|
|
versionsPage?: {
|
|
page: Array<Record<string, unknown>>;
|
|
isDone: boolean;
|
|
continueCursor: string;
|
|
};
|
|
ownerPublisher?: Record<string, unknown> | null;
|
|
viewerMembershipRole?: "owner" | "admin" | "publisher" | null;
|
|
}) {
|
|
const pkg = options.pkg ?? makePackageDoc();
|
|
const latestRelease = options.latestRelease ?? makeReleaseDoc();
|
|
const versionRelease = options.versionRelease ?? latestRelease;
|
|
const ownerPublisher = options.ownerPublisher ?? null;
|
|
const versionsPage = options.versionsPage ?? {
|
|
page: [latestRelease].filter(Boolean),
|
|
isDone: true,
|
|
continueCursor: "",
|
|
};
|
|
|
|
const releaseIndexNames: string[] = [];
|
|
return {
|
|
releaseIndexNames,
|
|
ctx: {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (typeof id === "string" && id.startsWith("users:")) {
|
|
return { _id: id, handle: id.split(":").pop() ?? "user" };
|
|
}
|
|
if (ownerPublisher && pkg && id === pkg.ownerPublisherId) return ownerPublisher;
|
|
if (pkg && id === pkg.latestReleaseId) return latestRelease;
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(pkg),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
const filteredVersionsPage = {
|
|
...versionsPage,
|
|
page: versionsPage.page.filter((release) => release.softDeletedAt === undefined),
|
|
};
|
|
return {
|
|
withIndex: vi.fn((indexName: string) => {
|
|
releaseIndexNames.push(indexName);
|
|
if (indexName === "by_package_active_created") {
|
|
return {
|
|
order: vi.fn(() => ({
|
|
paginate: vi.fn().mockResolvedValue(filteredVersionsPage),
|
|
})),
|
|
};
|
|
}
|
|
return {
|
|
unique: vi.fn().mockResolvedValue(versionRelease),
|
|
filter: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
paginate: vi.fn().mockResolvedValue(filteredVersionsPage),
|
|
})),
|
|
})),
|
|
order: vi.fn(() => ({
|
|
paginate: vi.fn().mockResolvedValue(versionsPage),
|
|
})),
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
if (table === "publisherMembers") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(
|
|
options.viewerMembershipRole
|
|
? {
|
|
_id: "publisherMembers:1",
|
|
publisherId: pkg?.ownerPublisherId,
|
|
userId: "users:member",
|
|
role: options.viewerMembershipRole,
|
|
}
|
|
: null,
|
|
),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function makeSoftDeletePackageCtx(options?: {
|
|
pkg?: Record<string, unknown> | null;
|
|
releases?: Array<Record<string, unknown>>;
|
|
user?: Record<string, unknown> | null;
|
|
membership?: Record<string, unknown> | null;
|
|
packageSearchDigest?: Record<string, unknown> | null;
|
|
capabilityDigests?: Array<Record<string, unknown>>;
|
|
}) {
|
|
const pkg = options?.pkg ?? makePackageDoc();
|
|
const releases = options?.releases ?? [makeReleaseDoc()];
|
|
const user = options?.user ?? { _id: "users:owner", role: "user" };
|
|
const membership = options?.membership ?? null;
|
|
const packageSearchDigest =
|
|
options?.packageSearchDigest === undefined
|
|
? { _id: "packageSearchDigest:demo", packageId: pkg?._id }
|
|
: options.packageSearchDigest;
|
|
const capabilityDigests = options?.capabilityDigests ?? [];
|
|
const patch = vi.fn();
|
|
const insert = vi.fn();
|
|
return {
|
|
patch,
|
|
insert,
|
|
ctx: {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:owner" || id === "users:moderator") return user;
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(pkg),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue(releases),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "publisherMembers") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(membership),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageSearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(packageSearchDigest),
|
|
})),
|
|
};
|
|
}
|
|
if (
|
|
table === "packageCapabilitySearchDigest" ||
|
|
table === "packageTopicSearchDigest" ||
|
|
table === "packagePluginCategorySearchDigest"
|
|
) {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue(capabilityDigests),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
patch,
|
|
insert,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function makePackagePreviewAccessCtx(options: {
|
|
actorUserId: string;
|
|
actorRole?: "user" | "admin" | "moderator";
|
|
pkg: ReturnType<typeof makePackageDoc> | null;
|
|
membershipRole?: "owner" | "admin" | "publisher";
|
|
}) {
|
|
const publisher = options.pkg?.ownerPublisherId
|
|
? {
|
|
_id: options.pkg.ownerPublisherId,
|
|
kind: "org",
|
|
handle: "owner-org",
|
|
displayName: "Owner Org",
|
|
}
|
|
: null;
|
|
|
|
return {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === options.actorUserId) return { _id: id, role: options.actorRole ?? "user" };
|
|
if (publisher && id === publisher._id) return publisher;
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(options.pkg),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "publisherMembers") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(
|
|
options.membershipRole && publisher
|
|
? {
|
|
_id: "publisherMembers:preview",
|
|
publisherId: publisher._id,
|
|
userId: options.actorUserId,
|
|
role: options.membershipRole,
|
|
}
|
|
: null,
|
|
),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("packages public queries", () => {
|
|
it("generates a package changelog preview from prior release history", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:preview" as never);
|
|
const previousKey = process.env.OPENAI_API_KEY;
|
|
delete process.env.OPENAI_API_KEY;
|
|
try {
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "users:preview",
|
|
role: "user",
|
|
})
|
|
.mockResolvedValueOnce({ ok: true, latestReleaseId: "packageReleases:demo-1" })
|
|
.mockResolvedValueOnce({
|
|
_id: "packageReleases:demo-1",
|
|
files: [{ path: "README.md", storageId: "storage:readme", sha256: "old" }],
|
|
});
|
|
const storageGet = vi.fn(async () => new Blob(["# Demo\n\nOld README"]));
|
|
|
|
const result = await generateChangelogPreviewHandler(
|
|
{
|
|
runQuery,
|
|
storage: { get: storageGet },
|
|
} as never,
|
|
{
|
|
name: "demo-plugin",
|
|
family: "code-plugin",
|
|
version: "1.2.0",
|
|
readmeText: "# Demo\n\nNew README",
|
|
filePaths: ["README.md", "src/index.ts"],
|
|
},
|
|
);
|
|
|
|
expect(result.changelog).toContain("Updated README and package contents");
|
|
expect(runQuery).toHaveBeenCalledTimes(3);
|
|
expect(storageGet).toHaveBeenCalledWith("storage:readme");
|
|
} finally {
|
|
if (previousKey === undefined) {
|
|
delete process.env.OPENAI_API_KEY;
|
|
} else {
|
|
process.env.OPENAI_API_KEY = previousKey;
|
|
}
|
|
}
|
|
});
|
|
|
|
it("blocks package changelog previews before reading release storage when access is denied", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:preview" as never);
|
|
const runQuery = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "users:preview",
|
|
role: "user",
|
|
})
|
|
.mockRejectedValueOnce(new Error("Forbidden"));
|
|
const storageGet = vi.fn();
|
|
|
|
await expect(
|
|
generateChangelogPreviewHandler(
|
|
{
|
|
runQuery,
|
|
storage: { get: storageGet },
|
|
} as never,
|
|
{
|
|
name: "demo-plugin",
|
|
family: "code-plugin",
|
|
version: "1.2.0",
|
|
readmeText: "# Demo\n\nNew README",
|
|
filePaths: ["README.md"],
|
|
},
|
|
),
|
|
).rejects.toThrow("Forbidden");
|
|
|
|
expect(runQuery).toHaveBeenCalledTimes(2);
|
|
expect(storageGet).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects non-owner package changelog previews for existing packages", async () => {
|
|
const pkg = makePackageDoc({
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner-org",
|
|
});
|
|
|
|
await expect(
|
|
assertCanGenerateChangelogPreviewInternalHandler(
|
|
makePackagePreviewAccessCtx({
|
|
actorUserId: "users:viewer",
|
|
pkg,
|
|
}) as never,
|
|
{ actorUserId: "users:viewer", name: "demo-plugin" },
|
|
),
|
|
).rejects.toThrow("Forbidden");
|
|
});
|
|
|
|
it("allows direct owners to generate package changelog previews", async () => {
|
|
await expect(
|
|
assertCanGenerateChangelogPreviewInternalHandler(
|
|
makePackagePreviewAccessCtx({
|
|
actorUserId: "users:owner",
|
|
pkg: makePackageDoc({ ownerUserId: "users:owner" }),
|
|
}) as never,
|
|
{ actorUserId: "users:owner", name: "demo-plugin" },
|
|
),
|
|
).resolves.toMatchObject({ ok: true, latestReleaseId: expect.anything() });
|
|
});
|
|
|
|
it.each(["owner", "admin", "publisher"] as const)(
|
|
"allows org %s members to generate package changelog previews",
|
|
async (membershipRole) => {
|
|
const pkg = makePackageDoc({
|
|
ownerUserId: "users:creator",
|
|
ownerPublisherId: "publishers:owner-org",
|
|
});
|
|
|
|
await expect(
|
|
assertCanGenerateChangelogPreviewInternalHandler(
|
|
makePackagePreviewAccessCtx({
|
|
actorUserId: "users:publisher-member",
|
|
pkg,
|
|
membershipRole,
|
|
}) as never,
|
|
{ actorUserId: "users:publisher-member", name: "demo-plugin" },
|
|
),
|
|
).resolves.toMatchObject({ ok: true, latestReleaseId: expect.anything() });
|
|
},
|
|
);
|
|
|
|
it.each(["admin", "moderator"] as const)(
|
|
"allows platform %s users to generate package changelog previews",
|
|
async (actorRole) => {
|
|
await expect(
|
|
assertCanGenerateChangelogPreviewInternalHandler(
|
|
makePackagePreviewAccessCtx({
|
|
actorUserId: "users:staff",
|
|
actorRole,
|
|
pkg: makePackageDoc({ ownerUserId: "users:owner" }),
|
|
}) as never,
|
|
{ actorUserId: "users:staff", name: "demo-plugin" },
|
|
),
|
|
).resolves.toMatchObject({ ok: true, latestReleaseId: expect.anything() });
|
|
},
|
|
);
|
|
|
|
it("pages eligible plugins by immutable creation time without scanning the catalog client-side", async () => {
|
|
const ctx = makePluginExportCtx([
|
|
makeDigest("updated-old", {
|
|
family: "code-plugin",
|
|
createdAt: 50,
|
|
updatedAt: 500,
|
|
_creationTime: 1,
|
|
}),
|
|
makeDigest("newest-bundle", {
|
|
family: "bundle-plugin",
|
|
createdAt: 300,
|
|
updatedAt: 300,
|
|
_creationTime: 2,
|
|
}),
|
|
makeDigest("newer-code", {
|
|
family: "code-plugin",
|
|
createdAt: 200,
|
|
updatedAt: 200,
|
|
_creationTime: 3,
|
|
}),
|
|
makeDigest("native-skill", {
|
|
family: "skill",
|
|
createdAt: 400,
|
|
updatedAt: 400,
|
|
_creationTime: 4,
|
|
}),
|
|
]);
|
|
|
|
const first = await listPublicNewPluginsPageHandler(ctx, {
|
|
createdAfter: 100,
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
const second = await listPublicNewPluginsPageHandler(ctx, {
|
|
createdAfter: 100,
|
|
paginationOpts: { cursor: first.continueCursor, numItems: 1 },
|
|
});
|
|
|
|
expect(first.page.map((entry) => entry.name)).toEqual(["newest-bundle"]);
|
|
expect(first.isDone).toBe(false);
|
|
expect(second.page.map((entry) => entry.name)).toEqual(["newer-code"]);
|
|
expect(second.isDone).toBe(true);
|
|
});
|
|
|
|
it("keeps partially consumed plugin export family pages active", async () => {
|
|
const ctx = makePluginExportCtx([
|
|
makePluginExportDigest("newer-code", "code-plugin", 300),
|
|
makePluginExportDigest("older-bundle-a", "bundle-plugin", 200),
|
|
makePluginExportDigest("older-bundle-b", "bundle-plugin", 100),
|
|
]);
|
|
|
|
const first = await listPluginExportPageInternalHandler(ctx, {
|
|
startDate: 0,
|
|
endDate: 1_000,
|
|
numItems: 1,
|
|
});
|
|
const second = await listPluginExportPageInternalHandler(ctx, {
|
|
startDate: 0,
|
|
endDate: 1_000,
|
|
cursor: first.nextCursor ?? undefined,
|
|
numItems: 1,
|
|
});
|
|
const third = await listPluginExportPageInternalHandler(ctx, {
|
|
startDate: 0,
|
|
endDate: 1_000,
|
|
cursor: second.nextCursor ?? undefined,
|
|
numItems: 1,
|
|
});
|
|
|
|
expect(first.page.map((entry) => entry.name)).toEqual(["newer-code"]);
|
|
expect(first.hasMore).toBe(true);
|
|
expect(first.nextCursor).toBeTruthy();
|
|
expect(second.page.map((entry) => entry.name)).toEqual(["older-bundle-a"]);
|
|
expect(second.hasMore).toBe(true);
|
|
expect(second.nextCursor).toBeTruthy();
|
|
expect(third.page.map((entry) => entry.name)).toEqual(["older-bundle-b"]);
|
|
expect(third.hasMore).toBe(false);
|
|
expect(third.nextCursor).toBeNull();
|
|
});
|
|
|
|
it("exports the latest scan state and normalized current findings", async () => {
|
|
const digest = makePluginExportDigest("demo", "code-plugin", 300);
|
|
const releaseId = "packageReleases:demo-1";
|
|
const ctx = makePluginValidationReportCtx([digest], {
|
|
statesByRelease: {
|
|
[releaseId]: [
|
|
{
|
|
releaseId,
|
|
inspectorVersion: "0.3.18",
|
|
targetOpenClawVersion: "2026.7.29-beta.1",
|
|
completedAt: 100,
|
|
},
|
|
{
|
|
releaseId,
|
|
inspectorVersion: "0.3.19",
|
|
targetOpenClawVersion: "2026.7.30-beta.1",
|
|
completedAt: 200,
|
|
},
|
|
],
|
|
},
|
|
findingsByRelease: {
|
|
[releaseId]: [
|
|
{
|
|
scanSource: "nightly",
|
|
inspectorVersion: "0.3.18",
|
|
targetOpenClawVersion: "2026.7.29-beta.1",
|
|
findingKind: "error",
|
|
code: "stale-missing-api",
|
|
message: "Old target API is unavailable",
|
|
},
|
|
{
|
|
scanSource: "publish",
|
|
findingKind: "warning",
|
|
code: "deprecated-api",
|
|
message: "API is deprecated",
|
|
},
|
|
{
|
|
scanSource: "nightly",
|
|
inspectorVersion: "0.3.19",
|
|
targetOpenClawVersion: "2026.7.30-beta.1",
|
|
findingKind: "error",
|
|
code: "missing-api",
|
|
message: "Required API is unavailable",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const result = await listPluginValidationReportPageInternalHandler(ctx, { numItems: 20 });
|
|
|
|
expect(result).toEqual({
|
|
items: [
|
|
expect.objectContaining({
|
|
package: { id: "packages:demo", name: "demo", displayName: "demo" },
|
|
scan: expect.objectContaining({
|
|
status: "error",
|
|
scannedAt: 200,
|
|
target: { channel: "beta", version: "2026.7.30-beta.1" },
|
|
inspectorVersion: "0.3.19",
|
|
}),
|
|
findings: [
|
|
{ severity: "warning", code: "deprecated-api", message: "API is deprecated" },
|
|
{ severity: "error", code: "missing-api", message: "Required API is unavailable" },
|
|
],
|
|
}),
|
|
],
|
|
nextCursor: null,
|
|
done: true,
|
|
});
|
|
});
|
|
|
|
it("represents missing scans and preserves plugin pagination continuity", async () => {
|
|
const ctx = makePluginValidationReportCtx(
|
|
[
|
|
makePluginExportDigest("newer-code", "code-plugin", 300),
|
|
makePluginExportDigest("older-bundle", "bundle-plugin", 200),
|
|
],
|
|
{
|
|
findingsByRelease: {
|
|
"packageReleases:newer-code-1": [
|
|
{
|
|
scanSource: "publish",
|
|
findingKind: "error",
|
|
code: "publish-error",
|
|
message: "Publish-time validation failed",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
);
|
|
|
|
const first = await listPluginValidationReportPageInternalHandler(ctx, { numItems: 1 });
|
|
const second = await listPluginValidationReportPageInternalHandler(ctx, {
|
|
cursor: first.nextCursor ?? undefined,
|
|
numItems: 1,
|
|
});
|
|
|
|
expect(first.items).toHaveLength(1);
|
|
expect(first.items[0]).toMatchObject({
|
|
package: { name: "newer-code" },
|
|
scan: { status: "error", scannedAt: null },
|
|
findings: [{ severity: "error", code: "publish-error" }],
|
|
});
|
|
expect(first.done).toBe(false);
|
|
expect(first.nextCursor).toBeTruthy();
|
|
expect(second.items).toHaveLength(1);
|
|
expect(second.items[0]).toMatchObject({
|
|
package: { name: "older-bundle" },
|
|
scan: { status: "not-scanned", scannedAt: null },
|
|
findings: [],
|
|
});
|
|
expect(second.done).toBe(true);
|
|
expect(second.nextCursor).toBeNull();
|
|
});
|
|
|
|
it("keeps buffered cursor items aligned across paginated public pages", async () => {
|
|
const { ctx, paginate } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("alpha"),
|
|
makeDigest("bravo"),
|
|
makeDigest("charlie"),
|
|
makeDigest("delta"),
|
|
],
|
|
isDone: false,
|
|
continueCursor: "cursor:1",
|
|
},
|
|
{
|
|
page: [makeDigest("echo")],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const first = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 2 },
|
|
});
|
|
const second = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: first.continueCursor, numItems: 2 },
|
|
});
|
|
const third = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: second.continueCursor, numItems: 2 },
|
|
});
|
|
|
|
expect(first.page.map((entry) => entry.name)).toEqual(["alpha", "bravo"]);
|
|
expect(second.page.map((entry) => entry.name)).toEqual(["charlie", "delta"]);
|
|
expect(third.page.map((entry) => entry.name)).toEqual(["echo"]);
|
|
expect(paginate).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it("returns the buffered final-page tail even when the stored cursor is done", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: Array.from({ length: 26 }, (_, index) => makeDigest(`pkg-${index + 1}`)),
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const first = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 25 },
|
|
});
|
|
const second = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: first.continueCursor, numItems: 25 },
|
|
});
|
|
|
|
expect(first.page).toHaveLength(25);
|
|
expect(second.page.map((entry) => entry.name)).toEqual(["pkg-26"]);
|
|
expect(second.isDone).toBe(true);
|
|
expect(second.continueCursor).toBe("");
|
|
});
|
|
|
|
it("keeps package page cursors compact even with large summaries", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("alpha", { summary: "a".repeat(8_000) }),
|
|
makeDigest("bravo", { summary: "b".repeat(8_000) }),
|
|
makeDigest("charlie", { summary: "c".repeat(8_000) }),
|
|
],
|
|
isDone: false,
|
|
continueCursor: "cursor:1",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["alpha"]);
|
|
expect(result.continueCursor.length).toBeLessThan(512);
|
|
expect(result.continueCursor).not.toContain("aaaaaaaa");
|
|
expect(result.continueCursor).not.toContain("bravo summary");
|
|
});
|
|
|
|
it("includes package stats on public list items", async () => {
|
|
const stats = { downloads: 43, installs: 3, stars: 1, versions: 2 };
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [makeDigest("stats-demo", { stats })],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
});
|
|
|
|
expect((result.page[0] as { stats?: unknown }).stats).toEqual(stats);
|
|
});
|
|
|
|
it("fills filtered pages without pending or suspicious packages", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("pending", { scanStatus: "pending", updatedAt: 40 }),
|
|
makeDigest("suspicious", { scanStatus: "suspicious", updatedAt: 30 }),
|
|
makeDigest("clean", { scanStatus: "clean", updatedAt: 20 }),
|
|
makeDigest("not-run", { scanStatus: "not-run", updatedAt: 10 }),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
sort: "updated",
|
|
excludedScanStatuses: ["pending", "suspicious"],
|
|
paginationOpts: { cursor: null, numItems: 2 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["clean", "not-run"]);
|
|
expect(result.isDone).toBe(true);
|
|
});
|
|
|
|
it("uses current package stats when digest stats are stale", async () => {
|
|
const currentStats = { downloads: 99, installs: 7, stars: 2, versions: 3 };
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("stats-demo", {
|
|
stats: { downloads: 1, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
exactPackages: [
|
|
makePackageDoc({
|
|
_id: "packages:stats-demo",
|
|
name: "stats-demo",
|
|
normalizedName: "stats-demo",
|
|
displayName: "stats-demo",
|
|
stats: currentStats,
|
|
}),
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
});
|
|
|
|
expect((result.page[0] as { stats?: unknown }).stats).toEqual(currentStats);
|
|
});
|
|
|
|
it("uses a family-scoped downloads index for download-sorted family pages", async () => {
|
|
const { ctx, indexFilters, indexNames, paginate } = makeDigestCtx({
|
|
packagePages: [
|
|
{
|
|
page: [
|
|
makePackageDoc({
|
|
_id: "packages:code-plugin-a",
|
|
name: "code-plugin-a",
|
|
normalizedName: "code-plugin-a",
|
|
displayName: "Code Plugin A",
|
|
family: "code-plugin",
|
|
stats: { downloads: 200, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
makePackageDoc({
|
|
_id: "packages:code-plugin-b",
|
|
name: "code-plugin-b",
|
|
normalizedName: "code-plugin-b",
|
|
displayName: "Code Plugin B",
|
|
family: "code-plugin",
|
|
stats: { downloads: 100, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
family: "code-plugin",
|
|
sort: "downloads",
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["code-plugin-a"]);
|
|
expect(result.isDone).toBe(false);
|
|
expect(result.continueCursor.startsWith("pkgpage:")).toBe(true);
|
|
expect(indexNames).toEqual(["by_active_family_downloads"]);
|
|
expect(indexFilters).toEqual([
|
|
{
|
|
indexName: "by_active_family_downloads",
|
|
filters: [
|
|
{ field: "softDeletedAt", value: undefined },
|
|
{ field: "family", value: "code-plugin" },
|
|
],
|
|
},
|
|
]);
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
expect(paginate).toHaveBeenCalledWith({ cursor: null, numItems: 50 });
|
|
});
|
|
|
|
it("uses a family-scoped installs index for install-sorted family pages", async () => {
|
|
const { ctx, indexFilters, indexNames, paginate } = makeDigestCtx({
|
|
packagePages: [
|
|
{
|
|
page: [
|
|
makePackageDoc({
|
|
_id: "packages:code-plugin-a",
|
|
name: "code-plugin-a",
|
|
normalizedName: "code-plugin-a",
|
|
displayName: "Code Plugin A",
|
|
family: "code-plugin",
|
|
stats: { downloads: 100, installs: 200, stars: 0, versions: 1 },
|
|
}),
|
|
makePackageDoc({
|
|
_id: "packages:code-plugin-b",
|
|
name: "code-plugin-b",
|
|
normalizedName: "code-plugin-b",
|
|
displayName: "Code Plugin B",
|
|
family: "code-plugin",
|
|
stats: { downloads: 500, installs: 100, stars: 0, versions: 1 },
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
family: "code-plugin",
|
|
sort: "installs",
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["code-plugin-a"]);
|
|
expect(result.isDone).toBe(false);
|
|
expect(result.continueCursor.startsWith("pkgpage:")).toBe(true);
|
|
expect(indexNames).toEqual(["by_active_family_installs"]);
|
|
expect(indexFilters).toEqual([
|
|
{
|
|
indexName: "by_active_family_installs",
|
|
filters: [
|
|
{ field: "softDeletedAt", value: undefined },
|
|
{ field: "family", value: "code-plugin" },
|
|
],
|
|
},
|
|
]);
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
expect(paginate).toHaveBeenCalledWith({ cursor: null, numItems: 50 });
|
|
});
|
|
|
|
it("uses a family-and-official installs index for official plugin pages", async () => {
|
|
const { ctx, indexFilters, indexNames, paginate } = makeDigestCtx({
|
|
packagePages: [
|
|
{
|
|
page: [
|
|
makePackageDoc({
|
|
_id: "packages:official-code-plugin",
|
|
name: "official-code-plugin",
|
|
normalizedName: "official-code-plugin",
|
|
displayName: "Official Code Plugin",
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
channel: "official",
|
|
stats: { downloads: 100, installs: 200, stars: 0, versions: 1 },
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPageForViewerInternalHandler(ctx, {
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
sort: "installs",
|
|
paginationOpts: { cursor: null, numItems: 24 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["official-code-plugin"]);
|
|
expect(result.isDone).toBe(true);
|
|
expect(indexNames).toEqual(["by_active_family_official_installs"]);
|
|
expect(indexFilters).toEqual([
|
|
{
|
|
indexName: "by_active_family_official_installs",
|
|
filters: [
|
|
{ field: "softDeletedAt", value: undefined },
|
|
{ field: "family", value: "code-plugin" },
|
|
{ field: "isOfficial", value: true },
|
|
],
|
|
},
|
|
]);
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
expect(paginate).toHaveBeenCalledWith({ cursor: null, numItems: 120 });
|
|
});
|
|
|
|
it("returns a cursor instead of paginating twice when sorted package filters skip rows", async () => {
|
|
const firstOfficialChannel = makePackageDoc({
|
|
_id: "packages:official-channel-1",
|
|
name: "official-channel-1",
|
|
normalizedName: "official-channel-1",
|
|
displayName: "Official Channel 1",
|
|
family: "bundle-plugin",
|
|
channel: "official",
|
|
stats: { downloads: 100, installs: 0, stars: 0, versions: 1 },
|
|
});
|
|
const community = makePackageDoc({
|
|
_id: "packages:community",
|
|
name: "community",
|
|
normalizedName: "community",
|
|
displayName: "Community",
|
|
family: "bundle-plugin",
|
|
channel: "community",
|
|
stats: { downloads: 90, installs: 0, stars: 0, versions: 1 },
|
|
});
|
|
const secondOfficialChannel = makePackageDoc({
|
|
_id: "packages:official-channel-2",
|
|
name: "official-channel-2",
|
|
normalizedName: "official-channel-2",
|
|
displayName: "Official Channel 2",
|
|
family: "bundle-plugin",
|
|
channel: "official",
|
|
stats: { downloads: 80, installs: 0, stars: 0, versions: 1 },
|
|
});
|
|
const { ctx, paginate } = makeDigestCtx({
|
|
packagePages: [
|
|
{ page: [firstOfficialChannel, community], isDone: false, continueCursor: "next-page" },
|
|
{ page: [secondOfficialChannel], isDone: true, continueCursor: "" },
|
|
],
|
|
});
|
|
|
|
const first = await listPageForViewerInternalHandler(ctx, {
|
|
family: "bundle-plugin",
|
|
channel: "official",
|
|
sort: "downloads",
|
|
paginationOpts: { cursor: null, numItems: 2 },
|
|
});
|
|
|
|
expect(first.page.map((entry) => entry.name)).toEqual(["official-channel-1"]);
|
|
expect(first.isDone).toBe(false);
|
|
expect(first.continueCursor).toMatch(/^pkgpage:/);
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
|
|
const second = await listPageForViewerInternalHandler(ctx, {
|
|
family: "bundle-plugin",
|
|
channel: "official",
|
|
sort: "downloads",
|
|
paginationOpts: { cursor: first.continueCursor, numItems: 2 },
|
|
});
|
|
|
|
expect(second.page.map((entry) => entry.name)).toEqual(["official-channel-2"]);
|
|
expect(second.isDone).toBe(true);
|
|
expect(paginate).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("returns a cursor instead of paginating twice when digest scan filters skip rows", async () => {
|
|
const firstClean = makeDigest("clean-1", {
|
|
scanStatus: "clean",
|
|
updatedAt: 100,
|
|
});
|
|
const suspicious = makeDigest("suspicious", {
|
|
scanStatus: "suspicious",
|
|
updatedAt: 90,
|
|
});
|
|
const secondClean = makeDigest("clean-2", {
|
|
scanStatus: "clean",
|
|
updatedAt: 80,
|
|
});
|
|
const thirdClean = makeDigest("clean-3", {
|
|
scanStatus: "clean",
|
|
updatedAt: 70,
|
|
});
|
|
const { ctx, paginate } = makeDigestCtx({
|
|
pages: [
|
|
{ page: [firstClean, suspicious, secondClean], isDone: false, continueCursor: "next-page" },
|
|
{ page: [thirdClean], isDone: true, continueCursor: "" },
|
|
],
|
|
});
|
|
|
|
const first = await listPageForViewerInternalHandler(ctx, {
|
|
family: "code-plugin",
|
|
sort: "updated",
|
|
excludedScanStatuses: ["suspicious"],
|
|
paginationOpts: { cursor: null, numItems: 3 },
|
|
});
|
|
|
|
expect(first.page.map((entry) => entry.name)).toEqual(["clean-1", "clean-2"]);
|
|
expect(first.isDone).toBe(false);
|
|
expect(first.continueCursor).toMatch(/^pkgpage:/);
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
|
|
const second = await listPageForViewerInternalHandler(ctx, {
|
|
family: "code-plugin",
|
|
sort: "updated",
|
|
excludedScanStatuses: ["suspicious"],
|
|
paginationOpts: { cursor: first.continueCursor, numItems: 3 },
|
|
});
|
|
|
|
expect(second.page.map((entry) => entry.name)).toEqual(["clean-3"]);
|
|
expect(second.isDone).toBe(true);
|
|
expect(paginate).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("uses a family-and-official downloads index for official plugin pages", async () => {
|
|
const { ctx, indexFilters, indexNames, paginate } = makeDigestCtx({
|
|
packagePages: [
|
|
{
|
|
page: [
|
|
makePackageDoc({
|
|
_id: "packages:official-code-plugin",
|
|
name: "official-code-plugin",
|
|
normalizedName: "official-code-plugin",
|
|
displayName: "Official Code Plugin",
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
channel: "official",
|
|
stats: { downloads: 200, installs: 100, stars: 0, versions: 1 },
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPageForViewerInternalHandler(ctx, {
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
sort: "downloads",
|
|
paginationOpts: { cursor: null, numItems: 24 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["official-code-plugin"]);
|
|
expect(result.isDone).toBe(true);
|
|
expect(indexNames).toEqual(["by_active_family_official_downloads"]);
|
|
expect(indexFilters).toEqual([
|
|
{
|
|
indexName: "by_active_family_official_downloads",
|
|
filters: [
|
|
{ field: "softDeletedAt", value: undefined },
|
|
{ field: "family", value: "code-plugin" },
|
|
{ field: "isOfficial", value: true },
|
|
],
|
|
},
|
|
]);
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
expect(paginate).toHaveBeenCalledWith({ cursor: null, numItems: 120 });
|
|
});
|
|
|
|
it("keeps new official downloads cursors on the family-and-official index", async () => {
|
|
const { ctx, indexNames } = makeDigestCtx({
|
|
packagePages: [
|
|
{
|
|
page: [
|
|
makePackageDoc({
|
|
_id: "packages:official-code-plugin-1",
|
|
name: "official-code-plugin-1",
|
|
normalizedName: "official-code-plugin-1",
|
|
displayName: "Official Code Plugin 1",
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
channel: "official",
|
|
}),
|
|
],
|
|
isDone: false,
|
|
continueCursor: "official-downloads-next",
|
|
},
|
|
{
|
|
page: [
|
|
makePackageDoc({
|
|
_id: "packages:official-code-plugin-2",
|
|
name: "official-code-plugin-2",
|
|
normalizedName: "official-code-plugin-2",
|
|
displayName: "Official Code Plugin 2",
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
channel: "official",
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const first = await listPageForViewerInternalHandler(ctx, {
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
sort: "downloads",
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
const second = await listPageForViewerInternalHandler(ctx, {
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
sort: "downloads",
|
|
paginationOpts: { cursor: first.continueCursor, numItems: 1 },
|
|
});
|
|
|
|
expect(first.page.map((entry) => entry.name)).toEqual(["official-code-plugin-1"]);
|
|
expect(second.page.map((entry) => entry.name)).toEqual(["official-code-plugin-2"]);
|
|
expect(indexNames).toEqual([
|
|
"by_active_family_official_downloads",
|
|
"by_active_family_official_downloads",
|
|
]);
|
|
});
|
|
|
|
it("keeps legacy official downloads cursors on the family downloads index", async () => {
|
|
const legacyCursor = `pkgpage:${JSON.stringify({
|
|
cursor: "legacy-official-downloads-next",
|
|
offset: 0,
|
|
pageSize: 50,
|
|
done: false,
|
|
mode: "packages",
|
|
})}`;
|
|
const { ctx, indexNames } = makeDigestCtx({
|
|
packagePages: [
|
|
{
|
|
page: [],
|
|
isDone: false,
|
|
continueCursor: "legacy-official-downloads-next",
|
|
},
|
|
{
|
|
page: [
|
|
makePackageDoc({
|
|
_id: "packages:official-code-plugin",
|
|
name: "official-code-plugin",
|
|
normalizedName: "official-code-plugin",
|
|
displayName: "Official Code Plugin",
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
channel: "official",
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPageForViewerInternalHandler(ctx, {
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
sort: "downloads",
|
|
paginationOpts: { cursor: legacyCursor, numItems: 1 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["official-code-plugin"]);
|
|
expect(indexNames).toEqual(["by_active_family_downloads"]);
|
|
});
|
|
|
|
it("normalizes public plugins from official publishers for official browse", async () => {
|
|
const officialPublisher = {
|
|
_id: "publishers:openclaw",
|
|
handle: "openclaw",
|
|
kind: "org",
|
|
deletedAt: undefined,
|
|
deactivatedAt: undefined,
|
|
};
|
|
const staleOfficialPlugin = makePackageDoc({
|
|
_id: "packages:memory",
|
|
name: "@openclaw/memory-lancedb",
|
|
normalizedName: "@openclaw/memory-lancedb",
|
|
displayName: "Memory LanceDB",
|
|
ownerPublisherId: officialPublisher._id,
|
|
channel: "community",
|
|
isOfficial: false,
|
|
capabilityTags: ["memory"],
|
|
});
|
|
const privateOfficialPlugin = makePackageDoc({
|
|
_id: "packages:private",
|
|
name: "@openclaw/private-plugin",
|
|
normalizedName: "@openclaw/private-plugin",
|
|
displayName: "Private Plugin",
|
|
ownerPublisherId: officialPublisher._id,
|
|
channel: "private",
|
|
isOfficial: false,
|
|
});
|
|
const communityPlugin = makePackageDoc({
|
|
_id: "packages:community",
|
|
name: "community-plugin",
|
|
normalizedName: "community-plugin",
|
|
displayName: "Community Plugin",
|
|
ownerPublisherId: "publishers:community",
|
|
channel: "community",
|
|
isOfficial: false,
|
|
});
|
|
const patch = vi.fn();
|
|
const insert = vi.fn();
|
|
const packageIndex = vi.fn();
|
|
const paginatePackages = vi.fn().mockResolvedValue({
|
|
page: [staleOfficialPlugin, privateOfficialPlugin, communityPlugin],
|
|
continueCursor: "",
|
|
isDone: true,
|
|
});
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === officialPublisher._id) return officialPublisher;
|
|
if (id === "publishers:community") {
|
|
return {
|
|
_id: id,
|
|
handle: "community",
|
|
kind: "org",
|
|
deletedAt: undefined,
|
|
deactivatedAt: undefined,
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: packageIndex.mockImplementation((_indexName, builder) => {
|
|
builder?.({
|
|
eq: (_field: string, _value: string) => ({}),
|
|
});
|
|
return { paginate: paginatePackages };
|
|
}),
|
|
};
|
|
}
|
|
if (table === "officialPublishers") {
|
|
return {
|
|
withIndex: vi.fn((_indexName, builder) => {
|
|
let publisherId: string | undefined;
|
|
builder?.({
|
|
eq: (_field: string, value: string) => {
|
|
publisherId = value;
|
|
return {};
|
|
},
|
|
});
|
|
return {
|
|
unique: vi
|
|
.fn()
|
|
.mockResolvedValue(
|
|
publisherId === officialPublisher._id
|
|
? { _id: "officialPublishers:openclaw", publisherId }
|
|
: null,
|
|
),
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
if (table === "packageSearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageSearchDigest:memory",
|
|
packageId: staleOfficialPlugin._id,
|
|
channel: "community",
|
|
isOfficial: false,
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (
|
|
table === "packageCapabilitySearchDigest" ||
|
|
table === "packagePluginCategorySearchDigest" ||
|
|
table === "packageTopicSearchDigest"
|
|
) {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
patch,
|
|
insert,
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
};
|
|
|
|
const result = await normalizeOfficialPublisherPackagesInternalHandler(ctx as never, {
|
|
family: "code-plugin",
|
|
dryRun: false,
|
|
batchSize: 10,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
family: "code-plugin",
|
|
scanned: 3,
|
|
matched: 1,
|
|
patched: 1,
|
|
skippedPrivate: 1,
|
|
dryRun: false,
|
|
});
|
|
expect(packageIndex).toHaveBeenCalledWith("by_family_updated", expect.any(Function));
|
|
expect(paginatePackages).toHaveBeenCalledWith({ cursor: null, numItems: 10 });
|
|
expect(patch).toHaveBeenCalledWith("packages:memory", {
|
|
channel: "official",
|
|
isOfficial: true,
|
|
});
|
|
expect(patch).not.toHaveBeenCalledWith(
|
|
"packages:private",
|
|
expect.objectContaining({ channel: "official" }),
|
|
);
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageSearchDigest:memory",
|
|
expect.objectContaining({
|
|
channel: "official",
|
|
isOfficial: true,
|
|
ownerHandle: "openclaw",
|
|
ownerKind: "org",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("includes current inferred taxonomy on install-sorted package pages", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
packagePages: [
|
|
{
|
|
page: [
|
|
makePackageDoc({
|
|
categories: undefined,
|
|
topics: undefined,
|
|
inferredCategories: ["memory", "tools"],
|
|
inferredTopics: ["Agent Memory", "Retrieval"],
|
|
inferredFromReleaseId: "packageReleases:demo-1",
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
family: "code-plugin",
|
|
sort: "installs",
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(result.page[0]).toMatchObject({
|
|
categories: ["memory", "tools"],
|
|
topics: ["Agent Memory", "Retrieval"],
|
|
});
|
|
});
|
|
|
|
it("preserves stored skill taxonomy on install-sorted package pages", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
packagePages: [
|
|
{
|
|
page: [
|
|
makePackageDoc({
|
|
family: "skill",
|
|
categories: ["developer-tools"],
|
|
topics: ["Automation"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
family: "skill",
|
|
sort: "installs",
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(result.page[0]).toMatchObject({
|
|
categories: ["developer-tools"],
|
|
topics: ["Automation"],
|
|
});
|
|
});
|
|
|
|
it("uses a family-scoped weighted recommended score index after backfill", async () => {
|
|
const { ctx, indexFilters, indexNames, paginate } = makeDigestCtx({
|
|
packagePages: [
|
|
{
|
|
page: [
|
|
makePackageDoc({
|
|
_id: "packages:code-plugin-downloaded",
|
|
name: "code-plugin-downloaded",
|
|
normalizedName: "code-plugin-downloaded",
|
|
displayName: "Code Plugin Downloaded",
|
|
family: "code-plugin",
|
|
stats: { downloads: 43_080, installs: 2, stars: 0, versions: 1 },
|
|
recommendedScore: computeRecommendationScore({
|
|
downloads: 43_080,
|
|
installs: 2,
|
|
stars: 0,
|
|
}),
|
|
recommendedScoreVersion: RECOMMENDATION_SCORE_VERSION,
|
|
}),
|
|
makePackageDoc({
|
|
_id: "packages:code-plugin-installed",
|
|
name: "code-plugin-installed",
|
|
normalizedName: "code-plugin-installed",
|
|
displayName: "Code Plugin Installed",
|
|
family: "code-plugin",
|
|
stats: { downloads: 393, installs: 74, stars: 0, versions: 1 },
|
|
recommendedScore: computeRecommendationScore({
|
|
downloads: 393,
|
|
installs: 74,
|
|
stars: 0,
|
|
}),
|
|
recommendedScoreVersion: RECOMMENDATION_SCORE_VERSION,
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
family: "code-plugin",
|
|
sort: "recommended",
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["code-plugin-downloaded"]);
|
|
expect(result.isDone).toBe(false);
|
|
expect(result.continueCursor.startsWith("pkgpage:")).toBe(true);
|
|
expect(indexNames).toEqual([
|
|
"by_active_family_recommended_score",
|
|
"by_active_family_recommended_score_version",
|
|
"by_active_family_recommended_score_version",
|
|
"by_active_family_recommended_score",
|
|
]);
|
|
expect(indexFilters).toEqual([
|
|
{
|
|
indexName: "by_active_family_recommended_score",
|
|
filters: [
|
|
{ field: "softDeletedAt", value: undefined },
|
|
{ field: "family", value: "code-plugin" },
|
|
{ field: "recommendedScore", value: undefined },
|
|
],
|
|
},
|
|
{
|
|
indexName: "by_active_family_recommended_score_version",
|
|
filters: [
|
|
{ field: "softDeletedAt", value: undefined },
|
|
{ field: "family", value: "code-plugin" },
|
|
{ field: "recommendedScoreVersion", value: undefined },
|
|
],
|
|
},
|
|
{
|
|
indexName: "by_active_family_recommended_score_version",
|
|
filters: [
|
|
{ field: "softDeletedAt", value: undefined },
|
|
{ field: "family", value: "code-plugin" },
|
|
],
|
|
},
|
|
{
|
|
indexName: "by_active_family_recommended_score",
|
|
filters: [
|
|
{ field: "softDeletedAt", value: undefined },
|
|
{ field: "family", value: "code-plugin" },
|
|
],
|
|
},
|
|
]);
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
expect(paginate).toHaveBeenCalledWith({ cursor: null, numItems: 50 });
|
|
});
|
|
|
|
it("falls back to updated family digests while recommendation scores are missing", async () => {
|
|
const { ctx, indexFilters, indexNames } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("code-plugin-downloaded", {
|
|
packageId: "packages:code-plugin-downloaded",
|
|
displayName: "Code Plugin Downloaded",
|
|
family: "code-plugin",
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
packagePages: [
|
|
{
|
|
page: [
|
|
makePackageDoc({
|
|
_id: "packages:code-plugin-downloaded",
|
|
name: "code-plugin-downloaded",
|
|
normalizedName: "code-plugin-downloaded",
|
|
displayName: "Code Plugin Downloaded",
|
|
family: "code-plugin",
|
|
stats: { downloads: 43_080, installs: 2, stars: 0, versions: 1 },
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
await listPublicPageHandler(ctx, {
|
|
family: "code-plugin",
|
|
sort: "recommended",
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(indexNames).toEqual(["by_active_family_recommended_score", "by_active_family_updated"]);
|
|
expect(indexFilters).toEqual([
|
|
{
|
|
indexName: "by_active_family_recommended_score",
|
|
filters: [
|
|
{ field: "softDeletedAt", value: undefined },
|
|
{ field: "family", value: "code-plugin" },
|
|
{ field: "recommendedScore", value: undefined },
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("falls back to updated family digests while recommendation score versions are missing", async () => {
|
|
const { ctx, indexFilters, indexNames } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("code-plugin-updated", {
|
|
packageId: "packages:code-plugin-updated",
|
|
displayName: "Code Plugin Updated",
|
|
family: "code-plugin",
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
packagePages: [
|
|
{
|
|
page: [
|
|
makePackageDoc({
|
|
_id: "packages:code-plugin-stale-score",
|
|
name: "code-plugin-stale-score",
|
|
normalizedName: "code-plugin-stale-score",
|
|
displayName: "Code Plugin Stale Score",
|
|
family: "code-plugin",
|
|
stats: { downloads: 43_080, installs: 2, stars: 0, versions: 1 },
|
|
recommendedScore: computeRecommendationScore({
|
|
downloads: 43_080,
|
|
installs: 2,
|
|
stars: 0,
|
|
}),
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
await listPublicPageHandler(ctx, {
|
|
family: "code-plugin",
|
|
sort: "recommended",
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(indexNames).toEqual([
|
|
"by_active_family_recommended_score",
|
|
"by_active_family_recommended_score_version",
|
|
"by_active_family_updated",
|
|
]);
|
|
expect(indexFilters).toEqual([
|
|
{
|
|
indexName: "by_active_family_recommended_score",
|
|
filters: [
|
|
{ field: "softDeletedAt", value: undefined },
|
|
{ field: "family", value: "code-plugin" },
|
|
{ field: "recommendedScore", value: undefined },
|
|
],
|
|
},
|
|
{
|
|
indexName: "by_active_family_recommended_score_version",
|
|
filters: [
|
|
{ field: "softDeletedAt", value: undefined },
|
|
{ field: "family", value: "code-plugin" },
|
|
{ field: "recommendedScoreVersion", value: undefined },
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps legacy recommended package cursors on the recommended score index", async () => {
|
|
const legacyCursor = `pkgpage:${JSON.stringify({
|
|
cursor: "legacy-recommended-next",
|
|
offset: 0,
|
|
pageSize: 50,
|
|
done: false,
|
|
})}`;
|
|
const { ctx, indexFilters, indexNames } = makeDigestCtx({
|
|
packagePages: [
|
|
{
|
|
page: [],
|
|
isDone: false,
|
|
continueCursor: "legacy-recommended-next",
|
|
},
|
|
{
|
|
page: [
|
|
makePackageDoc({
|
|
_id: "packages:code-plugin-next",
|
|
name: "code-plugin-next",
|
|
normalizedName: "code-plugin-next",
|
|
displayName: "Code Plugin Next",
|
|
family: "code-plugin",
|
|
stats: { downloads: 10, installs: 1, stars: 0, versions: 1 },
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
family: "code-plugin",
|
|
sort: "recommended",
|
|
paginationOpts: { cursor: legacyCursor, numItems: 1 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["code-plugin-next"]);
|
|
expect(indexNames).toEqual(["by_active_family_recommended_score"]);
|
|
expect(indexFilters).toEqual([
|
|
{
|
|
indexName: "by_active_family_recommended_score",
|
|
filters: [
|
|
{ field: "softDeletedAt", value: undefined },
|
|
{ field: "family", value: "code-plugin" },
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps recommended digest fallback cursors on the digest path after backfill", async () => {
|
|
const fallbackCursor = `pkgpage:${JSON.stringify({
|
|
cursor: "digest-next",
|
|
offset: 0,
|
|
pageSize: 50,
|
|
done: false,
|
|
mode: "digest",
|
|
})}`;
|
|
const { ctx, indexFilters, indexNames } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("code-plugin-first", {
|
|
packageId: "packages:code-plugin-first",
|
|
displayName: "Code Plugin First",
|
|
family: "code-plugin",
|
|
}),
|
|
],
|
|
isDone: false,
|
|
continueCursor: "digest-next",
|
|
},
|
|
{
|
|
page: [
|
|
makeDigest("code-plugin-second", {
|
|
packageId: "packages:code-plugin-second",
|
|
displayName: "Code Plugin Second",
|
|
family: "code-plugin",
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
packagePages: [
|
|
{
|
|
page: [
|
|
makePackageDoc({
|
|
_id: "packages:code-plugin-second",
|
|
name: "code-plugin-second",
|
|
normalizedName: "code-plugin-second",
|
|
displayName: "Code Plugin Second",
|
|
family: "code-plugin",
|
|
stats: { downloads: 1, installs: 1, stars: 0, versions: 1 },
|
|
recommendedScore: computeRecommendationScore({
|
|
downloads: 1,
|
|
installs: 1,
|
|
stars: 0,
|
|
}),
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
family: "code-plugin",
|
|
sort: "recommended",
|
|
paginationOpts: { cursor: fallbackCursor, numItems: 1 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["code-plugin-second"]);
|
|
expect(indexNames).toEqual(["by_active_family_updated"]);
|
|
expect(indexFilters).toEqual([]);
|
|
});
|
|
|
|
it("uses global download-sorted pages without retired capability filters", async () => {
|
|
const { ctx, indexNames, paginate } = makeDigestCtx({
|
|
packagePages: [
|
|
{
|
|
page: [
|
|
makePackageDoc({
|
|
_id: "packages:bundle-plugin",
|
|
name: "bundle-plugin",
|
|
normalizedName: "bundle-plugin",
|
|
displayName: "Bundle Plugin",
|
|
family: "bundle-plugin",
|
|
stats: { downloads: 500, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
],
|
|
isDone: false,
|
|
continueCursor: "cursor:next",
|
|
},
|
|
{
|
|
page: [
|
|
makePackageDoc({
|
|
_id: "packages:code-plugin",
|
|
name: "code-plugin",
|
|
normalizedName: "code-plugin",
|
|
displayName: "Code Plugin",
|
|
family: "code-plugin",
|
|
stats: { downloads: 200, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
sort: "downloads",
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["bundle-plugin"]);
|
|
expect(result.isDone).toBe(false);
|
|
expect(indexNames).toEqual(["by_active_downloads"]);
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("excludes private packages from public list pages", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [makeDigest("secret-plugin", { channel: "private" }), makeDigest("public-plugin")],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["public-plugin"]);
|
|
});
|
|
|
|
it("allows owners to list their private packages", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("secret-plugin", {
|
|
channel: "private",
|
|
ownerUserId: "users:owner",
|
|
}),
|
|
makeDigest("public-plugin"),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPageForViewerInternalHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
viewerUserId: "users:owner",
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["secret-plugin", "public-plugin"]);
|
|
});
|
|
|
|
it("keeps highlighted package pages in newest-featured order", async () => {
|
|
const newestFeatured = makeDigest("newest-featured", {
|
|
updatedAt: 20,
|
|
stats: { downloads: 100, installs: 5, stars: 0, versions: 1 },
|
|
});
|
|
const olderFeatured = makeDigest("older-featured", {
|
|
updatedAt: 10,
|
|
stats: { downloads: 1, installs: 50, stars: 0, versions: 1 },
|
|
});
|
|
const newerSkill = makeDigest("newer-skill", {
|
|
family: "skill",
|
|
updatedAt: 30,
|
|
});
|
|
const { ctx } = makeDigestCtx({
|
|
highlightedBadges: [
|
|
{ packageId: newerSkill.packageId, at: 300 },
|
|
{ packageId: newestFeatured.packageId, at: 200 },
|
|
{ packageId: olderFeatured.packageId, at: 100 },
|
|
],
|
|
exactDigests: [newerSkill, newestFeatured, olderFeatured],
|
|
});
|
|
|
|
const result = await listPageForViewerInternalHandler(ctx, {
|
|
families: ["code-plugin", "bundle-plugin"],
|
|
highlightedOnly: true,
|
|
sort: "installs",
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["newest-featured", "older-featured"]);
|
|
expect(result.page.map((entry) => entry.featuredAt)).toEqual([200, 100]);
|
|
});
|
|
|
|
it("keeps official packages first without re-ranking featured recency within each group", async () => {
|
|
const newestCommunity = makeDigest("newest-community", {
|
|
isOfficial: false,
|
|
updatedAt: 30,
|
|
});
|
|
const newestOfficial = makeDigest("newest-official", {
|
|
isOfficial: true,
|
|
updatedAt: 20,
|
|
});
|
|
const olderOfficial = makeDigest("older-official", {
|
|
isOfficial: true,
|
|
updatedAt: 10,
|
|
});
|
|
const { ctx } = makeDigestCtx({
|
|
highlightedBadges: [
|
|
{ packageId: newestCommunity.packageId, at: 300 },
|
|
{ packageId: newestOfficial.packageId, at: 200 },
|
|
{ packageId: olderOfficial.packageId, at: 100 },
|
|
],
|
|
exactDigests: [newestCommunity, newestOfficial, olderOfficial],
|
|
});
|
|
|
|
const result = await listPageForViewerInternalHandler(ctx, {
|
|
highlightedOnly: true,
|
|
officialFirst: true,
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual([
|
|
"newest-official",
|
|
"older-official",
|
|
"newest-community",
|
|
]);
|
|
});
|
|
|
|
it("does not let stale personal ownerUserId expose private package digests", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("stale-personal-secret", {
|
|
channel: "private",
|
|
ownerKind: "user",
|
|
ownerUserId: "users:viewer",
|
|
ownerPublisherId: "publishers:other-personal",
|
|
}),
|
|
makeDigest("public-plugin"),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPageForViewerInternalHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
viewerUserId: "users:viewer",
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["public-plugin"]);
|
|
});
|
|
|
|
it("lets legacy no-link personal package owners list private package digests", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
publisherDocs: {
|
|
"publishers:legacy-personal": {
|
|
_id: "publishers:legacy-personal",
|
|
kind: "user",
|
|
handle: "viewer",
|
|
linkedUserId: undefined,
|
|
},
|
|
},
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("legacy-personal-secret", {
|
|
channel: "private",
|
|
ownerKind: "user",
|
|
ownerUserId: "users:viewer",
|
|
ownerPublisherId: "publishers:legacy-personal",
|
|
}),
|
|
makeDigest("public-plugin"),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPageForViewerInternalHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
viewerUserId: "users:viewer",
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual([
|
|
"legacy-personal-secret",
|
|
"public-plugin",
|
|
]);
|
|
});
|
|
|
|
it("does not let inactive no-link personal publishers expose private package digests", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
publisherDocs: {
|
|
"publishers:legacy-personal": {
|
|
_id: "publishers:legacy-personal",
|
|
kind: "user",
|
|
handle: "viewer",
|
|
linkedUserId: undefined,
|
|
deactivatedAt: 123,
|
|
},
|
|
},
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("legacy-personal-secret", {
|
|
channel: "private",
|
|
ownerKind: "user",
|
|
ownerUserId: "users:viewer",
|
|
ownerPublisherId: "publishers:legacy-personal",
|
|
}),
|
|
makeDigest("public-plugin"),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPageForViewerInternalHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
viewerUserId: "users:viewer",
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["public-plugin"]);
|
|
});
|
|
|
|
it("does not reuse legacy no-link personal access across package owners", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
publisherDocs: {
|
|
"publishers:legacy-personal": {
|
|
_id: "publishers:legacy-personal",
|
|
kind: "user",
|
|
handle: "viewer",
|
|
linkedUserId: undefined,
|
|
},
|
|
},
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("own-legacy-secret", {
|
|
channel: "private",
|
|
ownerKind: "user",
|
|
ownerUserId: "users:viewer",
|
|
ownerPublisherId: "publishers:legacy-personal",
|
|
}),
|
|
makeDigest("stale-legacy-secret", {
|
|
channel: "private",
|
|
ownerKind: "user",
|
|
ownerUserId: "users:other",
|
|
ownerPublisherId: "publishers:legacy-personal",
|
|
}),
|
|
makeDigest("public-plugin"),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPageForViewerInternalHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
viewerUserId: "users:viewer",
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["own-legacy-secret", "public-plugin"]);
|
|
});
|
|
|
|
it("allows owners to filter to only their private packages", async () => {
|
|
const { ctx, indexNames } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("secret-plugin", {
|
|
channel: "private",
|
|
ownerUserId: "users:owner",
|
|
}),
|
|
makeDigest("other-secret", {
|
|
channel: "private",
|
|
ownerUserId: "users:other",
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPageForViewerInternalHandler(ctx, {
|
|
channel: "private",
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
viewerUserId: "users:owner",
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["secret-plugin"]);
|
|
expect(indexNames).toEqual(["by_active_channel_updated"]);
|
|
});
|
|
|
|
it("allows org collaborators to list their private packages", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("secret-plugin", {
|
|
channel: "private",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:org",
|
|
}),
|
|
makeDigest("public-plugin"),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
publisherMemberships: {
|
|
"publishers:org": "publisher",
|
|
},
|
|
});
|
|
|
|
const result = await listPageForViewerInternalHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
viewerUserId: "users:member",
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["secret-plugin", "public-plugin"]);
|
|
});
|
|
|
|
it("applies isOfficial filtering even with family and channel set", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("official-demo", {
|
|
family: "code-plugin",
|
|
channel: "community",
|
|
isOfficial: true,
|
|
}),
|
|
makeDigest("community-demo", {
|
|
family: "code-plugin",
|
|
channel: "community",
|
|
isOfficial: false,
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
family: "code-plugin",
|
|
channel: "community",
|
|
isOfficial: true,
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["official-demo"]);
|
|
});
|
|
|
|
it("uses the official index for official-only listings without a family filter", async () => {
|
|
const { ctx, indexNames, paginate } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [makeDigest("official-late", { isOfficial: true })],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
isOfficial: true,
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["official-late"]);
|
|
expect(indexNames).toEqual(["by_active_official_updated"]);
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("filters private packages in public search", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("secret-tools", {
|
|
channel: "private",
|
|
}),
|
|
makeDigest("tools-demo"),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "demo",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["tools-demo"]);
|
|
});
|
|
|
|
it("excludes selected scan statuses from package search", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("pending-demo", { scanStatus: "pending" }),
|
|
makeDigest("suspicious-demo", { scanStatus: "suspicious" }),
|
|
makeDigest("clean-demo", { scanStatus: "clean" }),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "demo",
|
|
limit: 10,
|
|
excludedScanStatuses: ["pending", "suspicious"],
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["clean-demo"]);
|
|
});
|
|
|
|
it("does not let a fresh exact-name package headline over adopted matches at limit 1", async () => {
|
|
// Regression for #3054: the untrusted exact hit fills the direct-match
|
|
// slot, but it must not satisfy the collection quota, or the fallback
|
|
// scan never gathers the adopted alternative it is ranked against.
|
|
const { ctx } = makeDigestCtx({
|
|
exactDigests: [makeDigest("youtube")],
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("youtube-transcript", {
|
|
stats: { downloads: 4200, installs: 800, stars: 12, versions: 3 },
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, { query: "youtube", limit: 1 });
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["youtube-transcript"]);
|
|
});
|
|
|
|
it("keeps trusted exact-name matches on top at limit 1", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
exactDigests: [makeDigest("youtube", { isOfficial: true })],
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("youtube-transcript", {
|
|
stats: { downloads: 4200, installs: 800, stars: 12, versions: 3 },
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, { query: "youtube", limit: 1 });
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["youtube"]);
|
|
});
|
|
|
|
it("allows owners to search their private packages", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("secret-tools", {
|
|
channel: "private",
|
|
ownerUserId: "users:owner",
|
|
}),
|
|
makeDigest("other-secret-tools", {
|
|
channel: "private",
|
|
ownerUserId: "users:other",
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchForViewerInternalHandler(ctx, {
|
|
query: "secret",
|
|
channel: "private",
|
|
limit: 10,
|
|
viewerUserId: "users:owner",
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["secret-tools"]);
|
|
});
|
|
|
|
it("uses bounded topic and fallback digest takes for search", async () => {
|
|
const { ctx, paginate, take } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [makeDigest("first-page-miss")],
|
|
isDone: false,
|
|
continueCursor: "next",
|
|
},
|
|
{
|
|
page: [makeDigest("needle-plugin")],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchForViewerInternalHandler(ctx, {
|
|
query: "needle",
|
|
family: "code-plugin",
|
|
limit: 2,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["needle-plugin"]);
|
|
expect(paginate).not.toHaveBeenCalled();
|
|
expect(take.mock.calls.length).toBeLessThanOrEqual(8);
|
|
expect(take).toHaveBeenCalledWith(20);
|
|
expect(take).toHaveBeenCalledWith(50);
|
|
});
|
|
|
|
it("recalls plugins whose category matches the query", async () => {
|
|
const { ctx, tableNames } = makeDigestCtx({
|
|
categoryPages: [
|
|
{
|
|
page: [
|
|
makeDigest("focused-helper", {
|
|
displayName: "Focused Helper",
|
|
summary: "Keeps projects tidy.",
|
|
pluginCategory: "runtime",
|
|
pluginCategoryTags: ["runtime"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "runtime",
|
|
family: "code-plugin",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["focused-helper"]);
|
|
expect(tableNames).toContain("packagePluginCategorySearchDigest");
|
|
});
|
|
|
|
it("does not use the fallback other category as search evidence", async () => {
|
|
const { ctx, tableNames } = makeDigestCtx({
|
|
categoryPages: [
|
|
{
|
|
page: [
|
|
makeDigest("focused-helper", {
|
|
displayName: "Focused Helper",
|
|
summary: "Keeps projects tidy.",
|
|
pluginCategory: "other",
|
|
pluginCategoryTags: ["other"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "other",
|
|
family: "code-plugin",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result).toEqual([]);
|
|
expect(tableNames).not.toContain("packagePluginCategorySearchDigest");
|
|
});
|
|
|
|
it("uses partial author topics as plugin search evidence", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
topicPages: [
|
|
{
|
|
page: [
|
|
makeDigest("focused-helper", {
|
|
displayName: "Focused Helper",
|
|
summary: "Keeps projects tidy.",
|
|
topic: "gpu-development",
|
|
topics: ["GPU development"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "gpu",
|
|
family: "code-plugin",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["focused-helper"]);
|
|
});
|
|
|
|
it("prioritizes exact plugin topics ahead of bounded prefix recall", async () => {
|
|
const prefixTopics = Array.from({ length: 100 }, (_, index) =>
|
|
makeDigest(`react-prefix-${index}`, {
|
|
topic: `react-${index}`,
|
|
topics: [`React ${index}`],
|
|
}),
|
|
);
|
|
const exactTopic = makeDigest("react-exact", {
|
|
topic: "react",
|
|
topics: ["React"],
|
|
});
|
|
const { ctx } = makeDigestCtx({
|
|
topicPages: [
|
|
{
|
|
page: [...prefixTopics, exactTopic],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "react",
|
|
family: "code-plugin",
|
|
limit: 1,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["react-exact"]);
|
|
});
|
|
|
|
it("does not let official status make unrelated packages eligible for search", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("openclaw-nostr", {
|
|
displayName: "OpenClaw Nostr",
|
|
isOfficial: true,
|
|
summary: "Protocol integration.",
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "zzzznonexistentquery123",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("does not treat punctuation-only queries as package matches", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("openclaw-nostr", {
|
|
isOfficial: true,
|
|
runtimeId: "openclaw.nostr",
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: ".",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("does not match short queries through arbitrary summary substrings", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("local-tools", {
|
|
summary: "Available helper tools.",
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "ai",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("does not drop short tokens from exploratory package matches", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("database-tools", {
|
|
summary: "Postgres database helper.",
|
|
capabilityTags: ["postgres"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "ai postgres",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("orders lexical matches before summary-only matches without exposing rank metadata", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("official-helper", {
|
|
displayName: "Official Helper",
|
|
isOfficial: true,
|
|
summary: "Ghost CMS integration.",
|
|
updatedAt: 100,
|
|
}),
|
|
makeDigest("ghost-tools", {
|
|
displayName: "Ghost Tools",
|
|
isOfficial: false,
|
|
summary: "CMS helper.",
|
|
updatedAt: 1,
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "ghost",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["ghost-tools", "official-helper"]);
|
|
expect(result[0]).not.toHaveProperty("rankTier");
|
|
expect(result[0]).not.toHaveProperty("matchReason");
|
|
});
|
|
|
|
it("allows org collaborators to search their private packages", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("secret-tools", {
|
|
channel: "private",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:org",
|
|
}),
|
|
makeDigest("other-secret-tools", {
|
|
channel: "private",
|
|
ownerUserId: "users:other",
|
|
ownerPublisherId: "publishers:other",
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
publisherMemberships: {
|
|
"publishers:org": "publisher",
|
|
},
|
|
});
|
|
|
|
const result = await searchForViewerInternalHandler(ctx, {
|
|
query: "secret",
|
|
channel: "private",
|
|
limit: 10,
|
|
viewerUserId: "users:member",
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["secret-tools"]);
|
|
});
|
|
|
|
it("uses the active updated index for public listings", async () => {
|
|
const { ctx, indexNames, tableNames } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [makeDigest("demo")],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["demo"]);
|
|
expect(tableNames).toEqual(["packageSearchDigest"]);
|
|
expect(indexNames).toEqual(["by_active_updated"]);
|
|
});
|
|
|
|
it("ignores retired capability filters for package search", async () => {
|
|
const { ctx, indexNames, tableNames } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [makeDigest("tools-demo")],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "tools",
|
|
capabilityTag: "tools",
|
|
executesCode: true,
|
|
limit: 10,
|
|
} as Parameters<typeof searchPublicHandler>[1] & {
|
|
capabilityTag?: string;
|
|
executesCode?: boolean;
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["tools-demo"]);
|
|
expect(new Set(tableNames)).toEqual(
|
|
new Set([
|
|
"packageSearchDigest",
|
|
"packageTopicSearchDigest",
|
|
"packagePluginCategorySearchDigest",
|
|
]),
|
|
);
|
|
expect(new Set(indexNames)).toEqual(
|
|
new Set(["by_active_topic_updated", "by_active_category_updated", "by_active_updated"]),
|
|
);
|
|
});
|
|
|
|
it("uses plugin category digests for category-filtered listings", async () => {
|
|
const { ctx, indexNames, tableNames } = makeDigestCtx({
|
|
categoryPages: [
|
|
{
|
|
page: [
|
|
makeDigest("api-demo", {
|
|
pluginCategory: "tools",
|
|
pluginCategoryTags: ["tools"],
|
|
executesCode: true,
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
category: "tools",
|
|
sort: "recommended",
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["api-demo"]);
|
|
expect(tableNames).toEqual(["packagePluginCategorySearchDigest"]);
|
|
expect(indexNames).toEqual([
|
|
"by_active_recommended_score",
|
|
"by_active_recommended_score_version",
|
|
"by_active_recommended_score_version",
|
|
"by_active_category_recommended_score",
|
|
]);
|
|
});
|
|
|
|
it("uses plugin category digest sort indexes for filtered listings", async () => {
|
|
const { ctx, indexNames } = makeDigestCtx({
|
|
categoryPages: [
|
|
{
|
|
page: [
|
|
makeDigest("api-demo", {
|
|
pluginCategory: "tools",
|
|
pluginCategoryTags: ["tools"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
await listPublicPageHandler(ctx, {
|
|
category: "tools",
|
|
sort: "installs",
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
});
|
|
|
|
expect(indexNames).toEqual(["by_active_category_installs"]);
|
|
});
|
|
|
|
it("uses family-aware plugin category sort indexes for plugin browse sources", async () => {
|
|
const { ctx, indexNames } = makeDigestCtx({
|
|
categoryPages: [
|
|
{
|
|
page: [
|
|
makeDigest("api-demo", {
|
|
family: "code-plugin",
|
|
pluginCategory: "tools",
|
|
pluginCategoryTags: ["tools"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
await listPageForViewerInternalHandler(ctx, {
|
|
family: "code-plugin",
|
|
category: "tools",
|
|
sort: "installs",
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
});
|
|
|
|
expect(indexNames).toEqual(["by_active_family_category_installs"]);
|
|
});
|
|
|
|
it("uses channel-aware plugin category download indexes", async () => {
|
|
const { ctx, indexNames } = makeDigestCtx({
|
|
categoryPages: [
|
|
{
|
|
page: [
|
|
makeDigest("community-tools", {
|
|
channel: "community",
|
|
pluginCategory: "tools",
|
|
pluginCategoryTags: ["tools"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
await listPublicPageHandler(ctx, {
|
|
channel: "community",
|
|
category: "tools",
|
|
sort: "downloads",
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
});
|
|
|
|
expect(indexNames).toEqual(["by_active_channel_category_downloads"]);
|
|
});
|
|
|
|
it("uses family-aware official category sort indexes for official-first sources", async () => {
|
|
const { ctx, indexNames } = makeDigestCtx({
|
|
categoryPages: [
|
|
{
|
|
page: [
|
|
makeDigest("api-demo", {
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
pluginCategory: "tools",
|
|
pluginCategoryTags: ["tools"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
await listPageForViewerInternalHandler(ctx, {
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
category: "tools",
|
|
sort: "installs",
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
});
|
|
|
|
expect(indexNames).toEqual(["by_active_family_official_category_installs"]);
|
|
});
|
|
|
|
it("resumes legacy category digest cursors on the updated index", async () => {
|
|
const legacyCursor = `pkgpage:${JSON.stringify({
|
|
cursor: "category:next",
|
|
offset: 0,
|
|
pageSize: 50,
|
|
done: false,
|
|
mode: "digest",
|
|
})}`;
|
|
const { ctx, indexNames } = makeDigestCtx({
|
|
categoryPages: [
|
|
{
|
|
page: [
|
|
makeDigest("api-demo", {
|
|
pluginCategory: "tools",
|
|
pluginCategoryTags: ["tools"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
await listPublicPageHandler(ctx, {
|
|
category: "tools",
|
|
sort: "installs",
|
|
paginationOpts: { cursor: legacyCursor, numItems: 10 },
|
|
});
|
|
|
|
expect(indexNames).toEqual(["by_active_category_updated"]);
|
|
});
|
|
|
|
it("preserves family filters on sorted category digest pages", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
categoryPages: [
|
|
{
|
|
page: [
|
|
makeDigest("bundle-tools", {
|
|
family: "bundle-plugin",
|
|
pluginCategory: "tools",
|
|
pluginCategoryTags: ["tools"],
|
|
}),
|
|
makeDigest("code-tools", {
|
|
family: "code-plugin",
|
|
pluginCategory: "tools",
|
|
pluginCategoryTags: ["tools"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
family: "code-plugin",
|
|
category: "tools",
|
|
sort: "downloads",
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["code-tools"]);
|
|
});
|
|
|
|
it("uses topic digests so topic-filtered listings do not skip later matches", async () => {
|
|
const { ctx, indexNames, tableNames } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [makeDigest("newer-noise", { topics: ["notes"] })],
|
|
isDone: false,
|
|
continueCursor: "later",
|
|
},
|
|
{
|
|
page: [makeDigest("calendar-demo", { topics: ["calendar"] })],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
topicPages: [
|
|
{
|
|
page: [
|
|
makeDigest("calendar-demo", {
|
|
topic: "calendar",
|
|
topics: ["calendar", "Official"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
topic: " Calendar ",
|
|
sort: "recommended",
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["calendar-demo"]);
|
|
expect(tableNames).toEqual(["packageTopicSearchDigest"]);
|
|
expect(indexNames).toEqual([
|
|
"by_active_recommended_score",
|
|
"by_active_recommended_score_version",
|
|
"by_active_recommended_score_version",
|
|
"by_active_topic_recommended_score",
|
|
]);
|
|
});
|
|
|
|
it("uses family-aware topic digest sort indexes for filtered listings", async () => {
|
|
const { ctx, indexNames } = makeDigestCtx({
|
|
topicPages: [
|
|
{
|
|
page: [makeDigest("calendar-demo", { topic: "calendar", topics: ["calendar"] })],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
await listPublicPageHandler(ctx, {
|
|
family: "code-plugin",
|
|
topic: "calendar",
|
|
sort: "downloads",
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
});
|
|
|
|
expect(indexNames).toEqual(["by_active_family_topic_downloads"]);
|
|
});
|
|
|
|
it("keeps metadata recommendation fallback cursors on the updated digest index", async () => {
|
|
const { ctx, indexNames } = makeDigestCtx({
|
|
packagePages: [
|
|
{
|
|
page: [makePackageDoc({ recommendedScore: undefined })],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
topicPages: [
|
|
{
|
|
page: [makeDigest("calendar-first", { topic: "calendar", topics: ["calendar"] })],
|
|
isDone: false,
|
|
continueCursor: "topic:next",
|
|
},
|
|
{
|
|
page: [makeDigest("calendar-second", { topic: "calendar", topics: ["calendar"] })],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const first = await listPublicPageHandler(ctx, {
|
|
topic: "calendar",
|
|
sort: "recommended",
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
const second = await listPublicPageHandler(ctx, {
|
|
topic: "calendar",
|
|
sort: "recommended",
|
|
paginationOpts: { cursor: first.continueCursor, numItems: 1 },
|
|
});
|
|
|
|
expect(first.page.map((entry) => entry.name)).toEqual(["calendar-first"]);
|
|
expect(second.page.map((entry) => entry.name)).toEqual(["calendar-second"]);
|
|
expect(indexNames).toEqual([
|
|
"by_active_recommended_score",
|
|
"by_active_topic_updated",
|
|
"by_active_topic_updated",
|
|
]);
|
|
});
|
|
|
|
it("does not revive retired capability filters for topic-filtered listings", async () => {
|
|
const { ctx, indexNames } = makeDigestCtx({
|
|
topicPages: [
|
|
{
|
|
page: [makeDigest("calendar-demo", { topic: "calendar", topics: ["calendar"] })],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
topic: "calendar",
|
|
capabilityTag: "tools",
|
|
executesCode: true,
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
} as Parameters<typeof listPublicPageHandler>[1] & {
|
|
capabilityTag?: string;
|
|
executesCode?: boolean;
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["calendar-demo"]);
|
|
expect(indexNames).toEqual(["by_active_topic_updated"]);
|
|
});
|
|
|
|
it("continues topic digest scans after filtering private packages", async () => {
|
|
const { ctx, paginate } = makeDigestCtx({
|
|
topicPages: [
|
|
{
|
|
page: [
|
|
makeDigest("calendar-private", {
|
|
channel: "private",
|
|
topic: "calendar",
|
|
topics: ["calendar"],
|
|
}),
|
|
],
|
|
isDone: false,
|
|
continueCursor: "topic:public",
|
|
},
|
|
{
|
|
page: [makeDigest("calendar-public", { topic: "calendar", topics: ["calendar"] })],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const first = await listPublicPageHandler(ctx, {
|
|
topic: "calendar",
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(first.page).toEqual([]);
|
|
expect(first.isDone).toBe(false);
|
|
expect(first.continueCursor).toMatch(/^pkgpage:/);
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
|
|
const second = await listPublicPageHandler(ctx, {
|
|
topic: "calendar",
|
|
paginationOpts: { cursor: first.continueCursor, numItems: 1 },
|
|
});
|
|
|
|
expect(second.page.map((entry) => entry.name)).toEqual(["calendar-public"]);
|
|
expect(second.isDone).toBe(true);
|
|
expect(paginate).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("rejects invalid topic filters instead of returning an unfiltered listing", async () => {
|
|
const { ctx, tableNames } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [makeDigest("unfiltered-plugin")],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
topic: "!!!",
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
});
|
|
|
|
expect(result).toEqual({ page: [], isDone: true, continueCursor: "" });
|
|
expect(tableNames).toEqual([]);
|
|
});
|
|
|
|
it("scans topic digest pages until a combined category match is found", async () => {
|
|
const { ctx, indexNames, tableNames, paginate } = makeDigestCtx({
|
|
topicPages: [
|
|
{
|
|
page: [
|
|
makeDigest("calendar-chat", {
|
|
topic: "calendar",
|
|
topics: ["calendar"],
|
|
pluginCategoryTags: ["channels"],
|
|
}),
|
|
],
|
|
isDone: false,
|
|
continueCursor: "later",
|
|
},
|
|
{
|
|
page: [
|
|
makeDigest("calendar-api", {
|
|
topic: "calendar",
|
|
topics: ["calendar"],
|
|
pluginCategoryTags: ["tools"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const first = await listPublicPageHandler(ctx, {
|
|
topic: "calendar",
|
|
category: "tools",
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(first.page).toEqual([]);
|
|
expect(first.isDone).toBe(false);
|
|
expect(first.continueCursor).toMatch(/^pkgpage:/);
|
|
expect(tableNames).toEqual(["packageTopicSearchDigest"]);
|
|
expect(indexNames).toEqual(["by_active_topic_updated"]);
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
|
|
const second = await listPublicPageHandler(ctx, {
|
|
topic: "calendar",
|
|
category: "tools",
|
|
paginationOpts: { cursor: first.continueCursor, numItems: 1 },
|
|
});
|
|
|
|
expect(second.page.map((entry) => entry.name)).toEqual(["calendar-api"]);
|
|
expect(tableNames).toEqual(["packageTopicSearchDigest", "packageTopicSearchDigest"]);
|
|
expect(indexNames).toEqual(["by_active_topic_updated", "by_active_topic_updated"]);
|
|
expect(paginate).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("bounds sparse combined-filter scans and resumes from the returned cursor", async () => {
|
|
const topicPages = Array.from({ length: 7 }, (_, index) => ({
|
|
page: [
|
|
makeDigest(index === 6 ? "calendar-api" : `calendar-noise-${index}`, {
|
|
topic: "calendar",
|
|
topics: ["calendar"],
|
|
pluginCategoryTags: [index === 6 ? "tools" : "channels"],
|
|
}),
|
|
],
|
|
isDone: index === 6,
|
|
continueCursor: index === 6 ? "" : `topic:${index + 1}`,
|
|
}));
|
|
const { ctx, paginate } = makeDigestCtx({ topicPages });
|
|
|
|
let cursor: string | null = null;
|
|
for (let index = 0; index < 6; index += 1) {
|
|
const result = await listPublicPageHandler(ctx, {
|
|
topic: "calendar",
|
|
category: "tools",
|
|
paginationOpts: { cursor, numItems: 1 },
|
|
});
|
|
expect(result.page).toEqual([]);
|
|
expect(result.isDone).toBe(false);
|
|
expect(result.continueCursor.startsWith("pkgpage:")).toBe(true);
|
|
cursor = result.continueCursor;
|
|
}
|
|
|
|
const final = await listPublicPageHandler(ctx, {
|
|
topic: "calendar",
|
|
category: "tools",
|
|
paginationOpts: { cursor, numItems: 1 },
|
|
});
|
|
|
|
expect(final.page.map((entry) => entry.name)).toEqual(["calendar-api"]);
|
|
expect(final.isDone).toBe(true);
|
|
expect(paginate).toHaveBeenCalledTimes(7);
|
|
});
|
|
|
|
it("paginates official category plugins before community fallback", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
categoryPages: [
|
|
{
|
|
page: [
|
|
makeDigest("community-security", {
|
|
isOfficial: false,
|
|
pluginCategory: "security",
|
|
pluginCategoryTags: ["security"],
|
|
}),
|
|
makeDigest("official-security", {
|
|
isOfficial: true,
|
|
pluginCategory: "security",
|
|
pluginCategoryTags: ["security"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const first = await listPublicPageHandler(ctx, {
|
|
category: "security",
|
|
officialFirst: true,
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
expect(first.page.map((entry) => entry.name)).toEqual(["official-security"]);
|
|
expect(first.isDone).toBe(false);
|
|
|
|
const second = await listPublicPageHandler(ctx, {
|
|
category: "security",
|
|
officialFirst: true,
|
|
paginationOpts: { cursor: first.continueCursor, numItems: 1 },
|
|
});
|
|
expect(second.page.map((entry) => entry.name)).toEqual(["community-security"]);
|
|
expect(second.isDone).toBe(false);
|
|
|
|
const third = await listPublicPageHandler(ctx, {
|
|
category: "security",
|
|
officialFirst: true,
|
|
paginationOpts: { cursor: second.continueCursor, numItems: 1 },
|
|
});
|
|
expect(third.page).toEqual([]);
|
|
expect(third.isDone).toBe(true);
|
|
});
|
|
|
|
it("does not advertise an empty community page after a full official category page", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
categoryPages: [
|
|
{
|
|
page: [
|
|
makeDigest("official-security", {
|
|
isOfficial: true,
|
|
pluginCategory: "security",
|
|
pluginCategoryTags: ["security"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
category: "security",
|
|
officialFirst: true,
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["official-security"]);
|
|
expect(result.isDone).toBe(true);
|
|
expect(result.continueCursor).toBe("");
|
|
});
|
|
|
|
it("keeps community continuation when the bounded probe is saturated by excluded rows", async () => {
|
|
const officialDigest = makeDigest("official-security", {
|
|
isOfficial: true,
|
|
pluginCategory: "security",
|
|
pluginCategoryTags: ["security"],
|
|
});
|
|
const excludedCommunityDigests = Array.from({ length: 200 }, (_, index) =>
|
|
makeDigest(`pending-community-security-${index}`, {
|
|
scanStatus: "pending",
|
|
pluginCategory: "security",
|
|
pluginCategoryTags: ["security"],
|
|
}),
|
|
);
|
|
const { ctx, paginate, take } = makeDigestCtx({
|
|
categoryPages: [
|
|
{
|
|
page: [officialDigest],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
categoryRows: excludedCommunityDigests,
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
category: "security",
|
|
officialFirst: true,
|
|
excludedScanStatuses: ["pending"],
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["official-security"]);
|
|
expect(result.isDone).toBe(false);
|
|
expect(result.continueCursor).toMatch(/^pkgofficialfirst:/);
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
expect(take).toHaveBeenCalledWith(200);
|
|
});
|
|
|
|
it("fills under-limit official-first category pages with community digests", async () => {
|
|
const officialDigest = makeDigest("official-security", {
|
|
isOfficial: true,
|
|
pluginCategory: "security",
|
|
pluginCategoryTags: ["security"],
|
|
});
|
|
const firstCommunityDigest = makeDigest("community-security-one", {
|
|
isOfficial: false,
|
|
pluginCategory: "security",
|
|
pluginCategoryTags: ["security"],
|
|
});
|
|
const secondCommunityDigest = makeDigest("community-security-two", {
|
|
isOfficial: false,
|
|
pluginCategory: "security",
|
|
pluginCategoryTags: ["security"],
|
|
});
|
|
const { ctx, paginate, take } = makeDigestCtx({
|
|
categoryPages: [
|
|
{
|
|
page: [officialDigest],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
categoryRows: [officialDigest, firstCommunityDigest, secondCommunityDigest],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
category: "security",
|
|
officialFirst: true,
|
|
paginationOpts: { cursor: null, numItems: 3 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual([
|
|
"official-security",
|
|
"community-security-one",
|
|
"community-security-two",
|
|
]);
|
|
expect(result.isDone).toBe(true);
|
|
expect(result.continueCursor).toBe("");
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
expect(take).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("keeps highlighted-only filtering while filling official-first category pages", async () => {
|
|
const officialHighlighted = makeDigest("official-highlighted-security", {
|
|
isOfficial: true,
|
|
pluginCategory: "security",
|
|
pluginCategoryTags: ["security"],
|
|
});
|
|
const communityHighlighted = makeDigest("community-highlighted-security", {
|
|
isOfficial: false,
|
|
pluginCategory: "security",
|
|
pluginCategoryTags: ["security"],
|
|
});
|
|
const communityUnhighlighted = makeDigest("community-unhighlighted-security", {
|
|
isOfficial: false,
|
|
pluginCategory: "security",
|
|
pluginCategoryTags: ["security"],
|
|
});
|
|
const { ctx } = makeDigestCtx({
|
|
highlightedBadges: [
|
|
{ packageId: officialHighlighted.packageId },
|
|
{ packageId: communityHighlighted.packageId },
|
|
],
|
|
exactDigests: [officialHighlighted, communityHighlighted],
|
|
categoryRows: [communityUnhighlighted],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
category: "security",
|
|
officialFirst: true,
|
|
highlightedOnly: true,
|
|
paginationOpts: { cursor: null, numItems: 3 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual([
|
|
"official-highlighted-security",
|
|
"community-highlighted-security",
|
|
]);
|
|
expect(result.isDone).toBe(true);
|
|
expect(result.continueCursor).toBe("");
|
|
});
|
|
|
|
it("does not advertise unhighlighted community rows after a full highlighted official page", async () => {
|
|
const officialHighlighted = makeDigest("official-highlighted-security", {
|
|
isOfficial: true,
|
|
pluginCategory: "security",
|
|
pluginCategoryTags: ["security"],
|
|
});
|
|
const communityUnhighlighted = makeDigest("community-unhighlighted-security", {
|
|
isOfficial: false,
|
|
pluginCategory: "security",
|
|
pluginCategoryTags: ["security"],
|
|
});
|
|
const { ctx } = makeDigestCtx({
|
|
highlightedBadges: [{ packageId: officialHighlighted.packageId }],
|
|
exactDigests: [officialHighlighted],
|
|
categoryRows: [communityUnhighlighted],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
category: "security",
|
|
officialFirst: true,
|
|
highlightedOnly: true,
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["official-highlighted-security"]);
|
|
expect(result.isDone).toBe(true);
|
|
expect(result.continueCursor).toBe("");
|
|
});
|
|
|
|
it("checks official-first category community availability without a second paginate", async () => {
|
|
const officialDigest = makeDigest("official-security", {
|
|
isOfficial: true,
|
|
pluginCategory: "security",
|
|
pluginCategoryTags: ["security"],
|
|
});
|
|
const communityDigest = makeDigest("community-security", {
|
|
isOfficial: false,
|
|
pluginCategory: "security",
|
|
pluginCategoryTags: ["security"],
|
|
});
|
|
const { ctx, paginate, take } = makeDigestCtx({
|
|
categoryPages: [
|
|
{
|
|
page: [officialDigest],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
categoryRows: [officialDigest, communityDigest],
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
category: "security",
|
|
officialFirst: true,
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["official-security"]);
|
|
expect(result.isDone).toBe(false);
|
|
expect(result.continueCursor).toMatch(/^pkgofficialfirst:/);
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
expect(take).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("uses plugin category digests for category-filtered search", async () => {
|
|
const { ctx, indexNames, tableNames } = makeDigestCtx({
|
|
categoryPages: [
|
|
{
|
|
page: [
|
|
makeDigest("api-demo", {
|
|
pluginCategory: "tools",
|
|
pluginCategoryTags: ["tools"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "api",
|
|
category: "tools",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["api-demo"]);
|
|
expect(tableNames).toEqual(["packagePluginCategorySearchDigest"]);
|
|
expect(indexNames).toEqual(["by_active_category_updated"]);
|
|
});
|
|
|
|
it("uses topic digests for topic-filtered search", async () => {
|
|
const { ctx, indexNames, tableNames } = makeDigestCtx({
|
|
topicPages: [
|
|
{
|
|
page: [
|
|
makeDigest("calendar-demo", {
|
|
topic: "calendar",
|
|
topics: ["calendar"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "calendar",
|
|
topic: "calendar",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["calendar-demo"]);
|
|
expect(tableNames).toContain("packageTopicSearchDigest");
|
|
expect(indexNames).toContain("by_active_topic_updated");
|
|
});
|
|
|
|
it("includes exact package-name matches in topic-filtered search", async () => {
|
|
const exactPkg = makePackageDoc({
|
|
_id: "packages:exact",
|
|
name: "demo-plugin",
|
|
normalizedName: "demo-plugin",
|
|
});
|
|
const exactDigest = makeDigest("demo-plugin", {
|
|
packageId: "packages:exact",
|
|
topics: ["calendar"],
|
|
});
|
|
const { ctx } = makeDigestCtx({
|
|
topicPages: [{ page: [], isDone: true, continueCursor: "" }],
|
|
exactPackages: [exactPkg],
|
|
exactDigests: [exactDigest],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "demo-plugin",
|
|
topic: "calendar",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["demo-plugin"]);
|
|
});
|
|
|
|
it("scans topic digest pages until a combined category search match is found", async () => {
|
|
const { ctx, paginate } = makeDigestCtx({
|
|
topicPages: [
|
|
{
|
|
page: Array.from({ length: 50 }, (_, index) =>
|
|
makeDigest(`calendar-chat-${index}`, {
|
|
topic: "calendar",
|
|
topics: ["calendar"],
|
|
pluginCategoryTags: ["channels"],
|
|
}),
|
|
),
|
|
isDone: false,
|
|
continueCursor: "later",
|
|
},
|
|
{
|
|
page: [
|
|
makeDigest("calendar-api", {
|
|
topic: "calendar",
|
|
topics: ["calendar"],
|
|
pluginCategoryTags: ["tools"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "calendar",
|
|
topic: "calendar",
|
|
category: "tools",
|
|
limit: 1,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["calendar-api"]);
|
|
expect(paginate).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("ranks bounded candidates from every stable family before applying the search limit", async () => {
|
|
const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("skill-helper", {
|
|
family: "skill",
|
|
summary: "Calendar integration",
|
|
}),
|
|
makeDigest("official-plugin", {
|
|
family: "code-plugin",
|
|
isOfficial: true,
|
|
summary: "Calendar integration",
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
try {
|
|
const result = await searchPublicHandler(ctx, { query: "calendar", limit: 1 });
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["official-plugin"]);
|
|
} finally {
|
|
if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous;
|
|
}
|
|
});
|
|
|
|
it("gives every stable family a fair combined-filter scan budget", async () => {
|
|
const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
const noisePage = (family: "skill" | "code-plugin" | "bundle-plugin", page: number) =>
|
|
Array.from({ length: 50 }, (_, index) =>
|
|
makeDigest(`${family}-noise-${page}-${index}`, {
|
|
family,
|
|
topic: "calendar",
|
|
topics: ["calendar"],
|
|
pluginCategoryTags: ["channels"],
|
|
}),
|
|
);
|
|
const { ctx } = makeDigestCtx({
|
|
topicPagesByFamily: {
|
|
skill: [
|
|
{ page: noisePage("skill", 1), isDone: false, continueCursor: "skill:2" },
|
|
{ page: noisePage("skill", 2), isDone: false, continueCursor: "skill:3" },
|
|
{ page: noisePage("skill", 3), isDone: true, continueCursor: "" },
|
|
],
|
|
"code-plugin": [
|
|
{
|
|
page: noisePage("code-plugin", 1),
|
|
isDone: false,
|
|
continueCursor: "code:2",
|
|
},
|
|
{
|
|
page: noisePage("code-plugin", 2),
|
|
isDone: false,
|
|
continueCursor: "code:3",
|
|
},
|
|
{ page: noisePage("code-plugin", 3), isDone: true, continueCursor: "" },
|
|
],
|
|
"bundle-plugin": [
|
|
{
|
|
page: noisePage("bundle-plugin", 1),
|
|
isDone: false,
|
|
continueCursor: "bundle:2",
|
|
},
|
|
{
|
|
page: noisePage("bundle-plugin", 2),
|
|
isDone: false,
|
|
continueCursor: "bundle:3",
|
|
},
|
|
{
|
|
page: [
|
|
makeDigest("calendar-bundle-api", {
|
|
family: "bundle-plugin",
|
|
topic: "calendar",
|
|
topics: ["calendar"],
|
|
pluginCategoryTags: ["tools"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
try {
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "calendar",
|
|
topic: "calendar",
|
|
category: "tools",
|
|
limit: 1,
|
|
});
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["calendar-bundle-api"]);
|
|
} finally {
|
|
if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous;
|
|
}
|
|
});
|
|
|
|
it("scans each stable family past the first combined-filter window while Claws are disabled", async () => {
|
|
const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
const { ctx, paginate } = makeDigestCtx({
|
|
topicPages: [
|
|
{
|
|
page: Array.from({ length: 50 }, (_, index) =>
|
|
makeDigest(`calendar-skill-noise-${index}`, {
|
|
family: "skill",
|
|
topic: "calendar",
|
|
topics: ["calendar"],
|
|
pluginCategoryTags: ["channels"],
|
|
}),
|
|
),
|
|
isDone: false,
|
|
continueCursor: "later",
|
|
},
|
|
{
|
|
page: [
|
|
makeDigest("calendar-skill-api", {
|
|
family: "skill",
|
|
topic: "calendar",
|
|
topics: ["calendar"],
|
|
pluginCategoryTags: ["tools"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
try {
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "calendar",
|
|
topic: "calendar",
|
|
category: "tools",
|
|
limit: 1,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["calendar-skill-api"]);
|
|
expect(paginate).toHaveBeenCalled();
|
|
} finally {
|
|
if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous;
|
|
}
|
|
});
|
|
|
|
it("bounds sparse combined-filter search scans", async () => {
|
|
const topicPages = Array.from({ length: 7 }, (_page, pageIndex) => ({
|
|
page: Array.from({ length: 50 }, (_digest, digestIndex) =>
|
|
makeDigest(`calendar-noise-${pageIndex}-${digestIndex}`, {
|
|
topic: "calendar",
|
|
topics: ["calendar"],
|
|
pluginCategoryTags: ["channels"],
|
|
}),
|
|
),
|
|
isDone: pageIndex === 6,
|
|
continueCursor: pageIndex === 6 ? "" : `topic:${pageIndex + 1}`,
|
|
}));
|
|
const { ctx, paginate, take } = makeDigestCtx({ topicPages });
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "calendar",
|
|
topic: "calendar",
|
|
category: "tools",
|
|
limit: 1,
|
|
});
|
|
|
|
expect(result).toEqual([]);
|
|
expect(paginate).toHaveBeenCalledTimes(6);
|
|
expect(take).toHaveBeenCalledTimes(1);
|
|
expect(take).toHaveBeenCalledWith(20);
|
|
});
|
|
|
|
it("recalls exact author topics without an explicit topic filter", async () => {
|
|
const { ctx, indexNames, tableNames } = makeDigestCtx({
|
|
topicPages: [
|
|
{
|
|
page: [
|
|
makeDigest("accelerated-helper", {
|
|
topic: "gpu-development",
|
|
topics: ["GPU development"],
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "GPU development",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["accelerated-helper"]);
|
|
expect(tableNames).toContain("packageTopicSearchDigest");
|
|
expect(indexNames).toContain("by_active_topic_updated");
|
|
});
|
|
|
|
it("rejects invalid topic filters instead of returning unfiltered search results", async () => {
|
|
const { ctx, tableNames } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [makeDigest("unfiltered-plugin")],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "plugin",
|
|
topic: "!!!",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result).toEqual([]);
|
|
expect(tableNames).toEqual([]);
|
|
});
|
|
|
|
it("bounds fallback search to the first digest take window", async () => {
|
|
const olderMatch = makeDigest("old-package", {
|
|
displayName: "Old Package",
|
|
summary: "legacy helper",
|
|
updatedAt: 10,
|
|
});
|
|
const { ctx, paginate, take } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: Array.from({ length: 200 }, (_, index) =>
|
|
makeDigest(`noise-${index}`, { updatedAt: 5_000 - index }),
|
|
),
|
|
isDone: false,
|
|
continueCursor: "cursor:1",
|
|
},
|
|
{
|
|
page: [olderMatch],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "legacy",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result).toEqual([]);
|
|
expect(paginate).not.toHaveBeenCalled();
|
|
expect(take).toHaveBeenCalledTimes(3);
|
|
expect(take).toHaveBeenCalledWith(20);
|
|
expect(take).toHaveBeenCalledWith(50);
|
|
});
|
|
|
|
it("includes exact package-name matches before digest scanning", async () => {
|
|
const exactPkg = makePackageDoc({
|
|
_id: "packages:exact",
|
|
name: "demo-plugin",
|
|
normalizedName: "demo-plugin",
|
|
});
|
|
const exactDigest = makeDigest("demo-plugin", {
|
|
packageId: "packages:exact",
|
|
});
|
|
const { ctx, take } = makeDigestCtx({
|
|
pages: [],
|
|
exactPackages: [exactPkg],
|
|
exactDigests: [exactDigest],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "demo-plugin",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["demo-plugin"]);
|
|
expect(take).toHaveBeenCalledTimes(3);
|
|
expect(ctx.db.query).toHaveBeenCalledWith("packageSearchDigest");
|
|
});
|
|
|
|
it("includes exact runtime-id matches before digest scanning", async () => {
|
|
const exactPkg = makePackageDoc({
|
|
_id: "packages:runtime",
|
|
name: "runtime-demo",
|
|
normalizedName: "runtime-demo",
|
|
runtimeId: "demo.plugin",
|
|
});
|
|
const exactDigest = makeDigest("runtime-demo", {
|
|
packageId: "packages:runtime",
|
|
runtimeId: "demo.plugin",
|
|
});
|
|
const { ctx, take } = makeDigestCtx({
|
|
pages: [],
|
|
exactPackages: [exactPkg],
|
|
exactDigests: [exactDigest],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "demo.plugin",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["runtime-demo"]);
|
|
expect(take).toHaveBeenCalledTimes(3);
|
|
expect(ctx.db.query).toHaveBeenCalledWith("packageSearchDigest");
|
|
});
|
|
|
|
it("includes display-name matches before digest scanning", async () => {
|
|
const displayDigest = makeDigest("vpai-plugin", {
|
|
packageId: "packages:vpai-plugin",
|
|
displayName: "Vibe Prospecting",
|
|
ownerHandle: "vibeprospecting",
|
|
});
|
|
const { ctx, searchIndexNames } = makeDigestCtx({
|
|
pages: [],
|
|
exactDigests: [displayDigest],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "vibe prospecting",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["vpai-plugin"]);
|
|
expect(searchIndexNames).toContain("search_by_display_name");
|
|
});
|
|
|
|
it("includes creator handle matches before digest scanning", async () => {
|
|
const ownerDigest = makeDigest("vpai-plugin", {
|
|
packageId: "packages:vpai-plugin",
|
|
displayName: "Vibe Prospecting",
|
|
ownerHandle: "vibeprospecting",
|
|
});
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [],
|
|
exactDigests: [ownerDigest],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "@vibeprospecting",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["vpai-plugin"]);
|
|
});
|
|
|
|
it("includes prefix package-name matches before digest scanning", async () => {
|
|
const prefixPkg = makePackageDoc({
|
|
_id: "packages:prefix",
|
|
name: "demo-prefix",
|
|
normalizedName: "demo-prefix",
|
|
});
|
|
const prefixDigest = makeDigest("demo-prefix", {
|
|
packageId: "packages:prefix",
|
|
});
|
|
const { ctx, take } = makeDigestCtx({
|
|
pages: [],
|
|
exactPackages: [prefixPkg],
|
|
exactDigests: [prefixDigest],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "demo",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["demo-prefix"]);
|
|
expect(take).toHaveBeenCalledTimes(3);
|
|
expect(ctx.db.query).toHaveBeenCalledWith("packageSearchDigest");
|
|
});
|
|
|
|
it("keeps direct package-name matches scoped to the requested family", async () => {
|
|
const exactPkg = makePackageDoc({
|
|
_id: "packages:code",
|
|
name: "demo-plugin",
|
|
normalizedName: "demo-plugin",
|
|
family: "code-plugin",
|
|
});
|
|
const exactDigest = makeDigest("demo-plugin", {
|
|
packageId: "packages:code",
|
|
family: "code-plugin",
|
|
});
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [],
|
|
exactPackages: [exactPkg],
|
|
exactDigests: [exactDigest],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "demo-plugin",
|
|
family: "bundle-plugin",
|
|
limit: 1,
|
|
});
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("stops fallback scanning after enough package search matches", async () => {
|
|
const { ctx, take } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("alpha", { displayName: "Alpha", summary: "useful demo", updatedAt: 20 }),
|
|
makeDigest("beta", { displayName: "Beta", summary: "useful demo", updatedAt: 10 }),
|
|
],
|
|
isDone: false,
|
|
continueCursor: "cursor:1",
|
|
},
|
|
{
|
|
page: [makeDigest("gamma", { displayName: "Gamma", summary: "useful demo" })],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "useful",
|
|
limit: 2,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["alpha", "beta"]);
|
|
expect(take).toHaveBeenCalledTimes(3);
|
|
expect(take).toHaveBeenCalledWith(20);
|
|
expect(take).toHaveBeenCalledWith(50);
|
|
});
|
|
|
|
it("keeps spaced queries on the scan path without throwing", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("demo-plugin", {
|
|
displayName: "Demo Plugin",
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "demo plugin",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["demo-plugin"]);
|
|
});
|
|
|
|
it("skips publisher membership lookups for public search rows", async () => {
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("demo-plugin", {
|
|
ownerPublisherId: "publishers:org",
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
publisherMemberships: {
|
|
"publishers:org": "publisher",
|
|
},
|
|
});
|
|
|
|
const result = await searchForViewerInternalHandler(ctx, {
|
|
query: "demo",
|
|
limit: 10,
|
|
viewerUserId: "users:member",
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["demo-plugin"]);
|
|
expect(ctx.db.query).not.toHaveBeenCalledWith("publisherMembers");
|
|
});
|
|
|
|
it("keeps public list pages to one paginated query per invocation", async () => {
|
|
const { ctx, paginate } = makeDigestCtx({
|
|
pages: Array.from({ length: 120 }, (_, index) => ({
|
|
page: [makeDigest(`noise-${index}`)],
|
|
isDone: false,
|
|
continueCursor: `cursor:${index + 1}`,
|
|
})),
|
|
});
|
|
|
|
const result = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 100 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["noise-0"]);
|
|
expect(result.isDone).toBe(false);
|
|
expect(result.continueCursor).toBeTruthy();
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
expect(paginate).toHaveBeenCalledWith({ cursor: null, numItems: 100 });
|
|
});
|
|
|
|
it("caps public search scans below the Convex read limit budget", async () => {
|
|
const { ctx, paginate, take } = makeDigestCtx({
|
|
pages: Array.from({ length: 170 }, (_, index) => ({
|
|
page: [
|
|
makeDigest(`noise-${index}`, {
|
|
executesCode: false,
|
|
updatedAt: 10_000 - index,
|
|
}),
|
|
],
|
|
isDone: false,
|
|
continueCursor: `cursor:${index + 1}`,
|
|
})),
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "demo",
|
|
executesCode: true,
|
|
limit: 100,
|
|
});
|
|
|
|
expect(result).toEqual([]);
|
|
expect(paginate).not.toHaveBeenCalled();
|
|
expect(take).toHaveBeenCalledTimes(3);
|
|
expect(take).toHaveBeenCalledWith(20);
|
|
expect(take).toHaveBeenCalledWith(200);
|
|
});
|
|
|
|
it("uses the official index for no-family official search filters", async () => {
|
|
const { ctx, indexNames } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [makeDigest("official-demo", { isOfficial: true })],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "official",
|
|
isOfficial: true,
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["official-demo"]);
|
|
expect(indexNames).toEqual([
|
|
"by_active_topic_updated",
|
|
"by_active_topic_updated",
|
|
"by_active_official_updated",
|
|
]);
|
|
});
|
|
|
|
it("uses the channel index for no-family channel search filters", async () => {
|
|
const { ctx, indexNames } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [makeDigest("community-demo", { channel: "community" })],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "community",
|
|
channel: "community",
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["community-demo"]);
|
|
expect(indexNames).toEqual([
|
|
"by_active_topic_updated",
|
|
"by_active_topic_updated",
|
|
"by_active_channel_updated",
|
|
]);
|
|
});
|
|
|
|
it("uses the combined channel and official index when both filters are set", async () => {
|
|
const { ctx, indexNames } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: [
|
|
makeDigest("official-community-demo", {
|
|
channel: "community",
|
|
isOfficial: true,
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await searchPublicHandler(ctx, {
|
|
query: "official-community",
|
|
channel: "community",
|
|
isOfficial: true,
|
|
limit: 10,
|
|
});
|
|
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["official-community-demo"]);
|
|
expect(indexNames).toEqual([
|
|
"by_active_topic_updated",
|
|
"by_active_topic_updated",
|
|
"by_active_channel_official_updated",
|
|
]);
|
|
});
|
|
|
|
it("blocks anonymous reads of private packages", async () => {
|
|
const { ctx } = makePackageCtx({
|
|
pkg: makePackageDoc({ channel: "private" }),
|
|
});
|
|
|
|
await expect(
|
|
getByNameHandler(ctx, {
|
|
name: "demo-plugin",
|
|
viewerUserId: "users:owner",
|
|
} as never),
|
|
).resolves.toBeNull();
|
|
await expect(
|
|
listVersionsHandler(ctx, {
|
|
name: "demo-plugin",
|
|
viewerUserId: "users:owner",
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
} as never),
|
|
).resolves.toEqual({
|
|
page: [],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
});
|
|
await expect(
|
|
getVersionByNameHandler(ctx, {
|
|
name: "demo-plugin",
|
|
version: "1.0.0",
|
|
viewerUserId: "users:owner",
|
|
} as never),
|
|
).resolves.toBeNull();
|
|
await expect(
|
|
getVersionSecurityByNameForViewerInternalHandler(ctx, {
|
|
name: "demo-plugin",
|
|
version: "1.0.0",
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it("allows anonymous exact security reads for blocked public packages", async () => {
|
|
const { ctx } = makePackageCtx({
|
|
pkg: makePackageDoc({ channel: "community", scanStatus: "malicious" }),
|
|
});
|
|
|
|
await expect(
|
|
getByNameHandler(ctx, {
|
|
name: "demo-plugin",
|
|
}),
|
|
).resolves.toBeNull();
|
|
await expect(
|
|
getVersionSecurityByNameForViewerInternalHandler(ctx, {
|
|
name: "demo-plugin",
|
|
version: "1.0.0",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
package: { name: "demo-plugin", publicDownloadBlocked: true },
|
|
version: { version: "1.0.0" },
|
|
});
|
|
});
|
|
|
|
it("normalizes legacy static-only package blocks through latest ClawScan state", async () => {
|
|
const verification = {
|
|
tier: "source-linked",
|
|
scope: "artifact-only",
|
|
scanStatus: "malicious",
|
|
};
|
|
const latestRelease = makeReleaseDoc({
|
|
sha256hash: "a".repeat(64),
|
|
verification,
|
|
staticScan: {
|
|
status: "malicious",
|
|
reasonCodes: ["malicious.static_fixture"],
|
|
findings: [],
|
|
summary: "Static fixture only.",
|
|
engineVersion: "test",
|
|
checkedAt: 1,
|
|
},
|
|
llmAnalysis: {
|
|
status: "clean",
|
|
verdict: "clean",
|
|
checkedAt: 2,
|
|
},
|
|
});
|
|
const { ctx } = makePackageCtx({
|
|
pkg: makePackageDoc({
|
|
channel: "community",
|
|
scanStatus: "malicious",
|
|
verification,
|
|
latestVersionSummary: {
|
|
version: "1.0.0",
|
|
verification,
|
|
},
|
|
}),
|
|
latestRelease,
|
|
});
|
|
|
|
await expect(
|
|
getByNameHandler(ctx, {
|
|
name: "demo-plugin",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
package: {
|
|
name: "demo-plugin",
|
|
scanStatus: "clean",
|
|
verification: { scanStatus: "clean" },
|
|
},
|
|
});
|
|
await expect(
|
|
getVersionByNameHandler(ctx, {
|
|
name: "demo-plugin",
|
|
version: "1.0.0",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
package: {
|
|
name: "demo-plugin",
|
|
scanStatus: "clean",
|
|
},
|
|
version: { version: "1.0.0" },
|
|
});
|
|
await expect(
|
|
getVersionSecurityByNameForViewerInternalHandler(ctx, {
|
|
name: "demo-plugin",
|
|
version: "1.0.0",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
package: {
|
|
name: "demo-plugin",
|
|
scanStatus: "clean",
|
|
publicDownloadBlocked: false,
|
|
},
|
|
version: { version: "1.0.0" },
|
|
});
|
|
});
|
|
|
|
it("derives missing public verification source paths from legacy release provenance", async () => {
|
|
const verification = {
|
|
tier: "source-linked",
|
|
scope: "artifact-only",
|
|
sourceRepo: "OpenViking/OpenViking",
|
|
sourceCommit: "abcdef0123456789abcdef0123456789abcdef01",
|
|
scanStatus: "clean",
|
|
};
|
|
const latestRelease = makeReleaseDoc({
|
|
verification,
|
|
capabilities: { capabilityTags: ["legacy"], executesCode: true },
|
|
source: {
|
|
kind: "github",
|
|
repo: "OpenViking/OpenViking",
|
|
path: "openclaw-plugin",
|
|
},
|
|
});
|
|
const { ctx } = makePackageCtx({
|
|
pkg: makePackageDoc({
|
|
name: "@openviking/openclaw-plugin",
|
|
normalizedName: "@openviking/openclaw-plugin",
|
|
verification,
|
|
latestVersionSummary: {
|
|
version: "1.0.0",
|
|
verification,
|
|
},
|
|
}),
|
|
latestRelease,
|
|
});
|
|
|
|
const detail = await getByNameHandler(ctx, {
|
|
name: "@openviking/openclaw-plugin",
|
|
});
|
|
expect(detail).toMatchObject({
|
|
package: {
|
|
verification: { sourcePath: "openclaw-plugin" },
|
|
},
|
|
latestRelease: {
|
|
verification: { sourcePath: "openclaw-plugin" },
|
|
},
|
|
});
|
|
expect(detail?.latestRelease).not.toHaveProperty("capabilities");
|
|
|
|
const version = await getVersionByNameHandler(ctx, {
|
|
name: "@openviking/openclaw-plugin",
|
|
version: "1.0.0",
|
|
});
|
|
expect(version).toMatchObject({
|
|
package: {
|
|
verification: { sourcePath: "openclaw-plugin" },
|
|
},
|
|
version: {
|
|
verification: { sourcePath: "openclaw-plugin" },
|
|
},
|
|
});
|
|
expect(version?.version).not.toHaveProperty("capabilities");
|
|
});
|
|
|
|
it("does not mark owner-readable blocked public packages as public download blocked", async () => {
|
|
const { ctx } = makePackageCtx({
|
|
pkg: makePackageDoc({
|
|
channel: "community",
|
|
scanStatus: "malicious",
|
|
ownerUserId: "users:owner",
|
|
}),
|
|
});
|
|
|
|
await expect(
|
|
getVersionSecurityByNameForViewerInternalHandler(ctx, {
|
|
name: "demo-plugin",
|
|
version: "1.0.0",
|
|
viewerUserId: "users:owner",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
package: { name: "demo-plugin", publicDownloadBlocked: false },
|
|
version: { version: "1.0.0" },
|
|
});
|
|
});
|
|
|
|
it("allows owners to read their private packages", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
|
|
const { ctx } = makePackageCtx({
|
|
pkg: makePackageDoc({ channel: "private" }),
|
|
});
|
|
|
|
const detail = await getByNameHandler(ctx, {
|
|
name: "demo-plugin",
|
|
});
|
|
const version = await getVersionByNameHandler(ctx, {
|
|
name: "demo-plugin",
|
|
version: "1.0.0",
|
|
});
|
|
|
|
expect(detail?.package.name).toBe("demo-plugin");
|
|
expect(version?.version.version).toBe("1.0.0");
|
|
});
|
|
|
|
it("allows org collaborators to read org-owned private packages", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:member" as never);
|
|
const { ctx } = makePackageCtx({
|
|
pkg: makePackageDoc({
|
|
channel: "private",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:org",
|
|
}),
|
|
ownerPublisher: {
|
|
_id: "publishers:org",
|
|
_creationTime: 1,
|
|
kind: "org",
|
|
handle: "acme",
|
|
displayName: "Acme",
|
|
linkedUserId: undefined,
|
|
},
|
|
viewerMembershipRole: "publisher",
|
|
});
|
|
|
|
const detail = await getByNameHandler(ctx, {
|
|
name: "demo-plugin",
|
|
});
|
|
|
|
expect(detail?.package.name).toBe("demo-plugin");
|
|
});
|
|
|
|
it("treats auth resolution failures as anonymous for public package detail", async () => {
|
|
vi.mocked(getAuthUserId).mockRejectedValue(new Error("stale session"));
|
|
const { ctx } = makePackageCtx({
|
|
pkg: makePackageDoc({ channel: "community" }),
|
|
});
|
|
|
|
const detail = await getByNameHandler(ctx, {
|
|
name: "demo-plugin",
|
|
});
|
|
|
|
expect(detail?.package.name).toBe("demo-plugin");
|
|
});
|
|
|
|
it("treats invalid auth user lookups as anonymous for public package detail", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:broken" as never);
|
|
const { ctx } = makePackageCtx({
|
|
pkg: makePackageDoc({ channel: "community" }),
|
|
});
|
|
const get = ctx.db.get as ReturnType<typeof vi.fn>;
|
|
get.mockImplementation(async (id: string) => {
|
|
if (id === "users:broken") throw new Error("Table mismatch");
|
|
if (id === "users:owner") return { _id: id, handle: "owner" };
|
|
if (id === "packageReleases:demo-1") return makeReleaseDoc();
|
|
return null;
|
|
});
|
|
|
|
const detail = await getByNameHandler(ctx, {
|
|
name: "demo-plugin",
|
|
});
|
|
|
|
expect(detail?.package.name).toBe("demo-plugin");
|
|
});
|
|
|
|
it("does not expose a soft-deleted latest release as latestVersion", async () => {
|
|
const { ctx } = makePackageCtx({
|
|
latestRelease: makeReleaseDoc({ softDeletedAt: 10 }),
|
|
});
|
|
|
|
const result = await getByNameHandler(ctx, {
|
|
name: "demo-plugin",
|
|
});
|
|
|
|
expect(result?.package.latestVersion).toBeNull();
|
|
expect(result?.latestRelease).toBeNull();
|
|
});
|
|
|
|
it("hides package shells that only have pending unpublished releases", async () => {
|
|
const { ctx } = makePackageCtx({
|
|
pkg: makePackageDoc({
|
|
latestReleaseId: undefined,
|
|
latestVersionSummary: undefined,
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 0 },
|
|
}),
|
|
latestRelease: null,
|
|
});
|
|
|
|
await expect(
|
|
getByNameHandler(ctx, {
|
|
name: "demo-plugin",
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it("keeps published non-latest packages visible without latest pointers", async () => {
|
|
const { ctx } = makePackageCtx({
|
|
pkg: makePackageDoc({
|
|
latestReleaseId: undefined,
|
|
latestVersionSummary: undefined,
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
latestRelease: null,
|
|
});
|
|
|
|
await expect(
|
|
getByNameHandler(ctx, {
|
|
name: "demo-plugin",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
package: { name: "demo-plugin", latestVersion: null },
|
|
latestRelease: null,
|
|
});
|
|
});
|
|
|
|
it("hides soft-deleted releases from public version lists", async () => {
|
|
const { ctx, releaseIndexNames } = makePackageCtx({
|
|
versionsPage: {
|
|
page: [
|
|
makeReleaseDoc({ version: "1.1.0", softDeletedAt: 10 }),
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:demo-2",
|
|
version: "1.0.0",
|
|
capabilities: { capabilityTags: ["legacy"], executesCode: true },
|
|
}),
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
});
|
|
|
|
const result = await listVersionsHandler(ctx, {
|
|
name: "demo-plugin",
|
|
paginationOpts: { cursor: null, numItems: 10 },
|
|
});
|
|
|
|
expect(result.page.map((entry) => entry.version)).toEqual(["1.0.0"]);
|
|
expect(result.page[0]).not.toHaveProperty("capabilities");
|
|
expect(releaseIndexNames).toContain("by_package_active_created");
|
|
});
|
|
|
|
it("fills public package version pages after skipping pending releases", async () => {
|
|
const releases = [
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:pending",
|
|
version: "2.0.0",
|
|
publicationStatus: "pending",
|
|
}),
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:published",
|
|
version: "1.0.0",
|
|
}),
|
|
];
|
|
const paginate = vi.fn(
|
|
async ({ cursor, numItems }: { cursor: string | null; numItems: number }) => {
|
|
const start = cursor ? Number(cursor) : 0;
|
|
const page = releases.slice(start, start + numItems);
|
|
const next = start + page.length;
|
|
return {
|
|
page,
|
|
isDone: next >= releases.length,
|
|
continueCursor: next >= releases.length ? "" : String(next),
|
|
};
|
|
},
|
|
);
|
|
const releaseIndexNames: string[] = [];
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) =>
|
|
typeof id === "string" && id.startsWith("users:")
|
|
? { _id: id, handle: id.split(":").pop() ?? "user" }
|
|
: null,
|
|
),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(makePackageDoc()),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn((indexName: string) => {
|
|
releaseIndexNames.push(indexName);
|
|
return {
|
|
order: vi.fn(() => ({
|
|
paginate,
|
|
})),
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
};
|
|
|
|
const result = await listVersionsHandler(ctx as never, {
|
|
name: "demo-plugin",
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
page: [{ version: "1.0.0" }],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
});
|
|
expect(paginate).toHaveBeenNthCalledWith(1, { cursor: null, numItems: 1 });
|
|
expect(paginate).toHaveBeenNthCalledWith(2, { cursor: "1", numItems: 1 });
|
|
expect(releaseIndexNames).toEqual(["by_package_active_created", "by_package_active_created"]);
|
|
});
|
|
|
|
it("soft-deletes packages and active releases for the owner", async () => {
|
|
const { ctx, insert, patch } = makeSoftDeletePackageCtx({
|
|
releases: [
|
|
makeReleaseDoc(),
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:demo-2",
|
|
version: "1.1.0",
|
|
softDeletedAt: 123,
|
|
}),
|
|
],
|
|
});
|
|
|
|
const result = await softDeletePackageInternalHandler(ctx, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseCount: 1,
|
|
alreadyDeleted: false,
|
|
});
|
|
expect(patch).toHaveBeenCalledWith("packageReleases:demo-1", {
|
|
softDeletedAt: expect.any(Number),
|
|
});
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
softDeletedAt: expect.any(Number),
|
|
updatedAt: expect.any(Number),
|
|
}),
|
|
);
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageSearchDigest:demo",
|
|
expect.objectContaining({
|
|
packageId: "packages:demo",
|
|
softDeletedAt: expect.any(Number),
|
|
updatedAt: expect.any(Number),
|
|
}),
|
|
);
|
|
const digestPatch = patch.mock.calls.find(([id]) => id === "packageSearchDigest:demo")?.[1];
|
|
expect(digestPatch).not.toHaveProperty("softDeletedBy");
|
|
expect(digestPatch).not.toHaveProperty("softDeletedByRole");
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({
|
|
actorUserId: "users:owner",
|
|
action: "package.delete",
|
|
targetType: "package",
|
|
targetId: "packages:demo",
|
|
metadata: expect.objectContaining({
|
|
name: "demo-plugin",
|
|
normalizedName: "demo-plugin",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: undefined,
|
|
releaseCount: 1,
|
|
releaseIds: ["packageReleases:demo-1"],
|
|
source: "cli",
|
|
}),
|
|
createdAt: expect.any(Number),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("rejects non-owner package soft deletes without moderator access", async () => {
|
|
const { ctx } = makeSoftDeletePackageCtx({
|
|
pkg: makePackageDoc({ ownerUserId: "users:someone-else" }),
|
|
user: { _id: "users:owner", role: "user" },
|
|
});
|
|
|
|
await expect(
|
|
softDeletePackageInternalHandler(ctx, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
}),
|
|
).rejects.toThrow("Forbidden");
|
|
});
|
|
|
|
it("allows moderators to soft-delete packages without ownership", async () => {
|
|
const { ctx, patch } = makeSoftDeletePackageCtx({
|
|
pkg: makePackageDoc({ ownerUserId: "users:someone-else" }),
|
|
user: { _id: "users:moderator", role: "moderator" },
|
|
});
|
|
|
|
await expect(
|
|
softDeletePackageInternalHandler(ctx, {
|
|
userId: "users:moderator",
|
|
name: "demo-plugin",
|
|
}),
|
|
).resolves.toMatchObject({ ok: true, alreadyDeleted: false });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
softDeletedAt: expect.any(Number),
|
|
softDeletedBy: "users:moderator",
|
|
softDeletedByRole: "moderator",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("allows org publisher admins to soft-delete packages", async () => {
|
|
const { ctx, patch } = makeSoftDeletePackageCtx({
|
|
pkg: makePackageDoc({
|
|
ownerUserId: "users:org-linked",
|
|
ownerPublisherId: "publishers:org",
|
|
}),
|
|
user: { _id: "users:owner", role: "user" },
|
|
membership: { _id: "publisherMembers:1", role: "admin" },
|
|
});
|
|
|
|
const result = await softDeletePackageInternalHandler(ctx, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseCount: 1,
|
|
alreadyDeleted: false,
|
|
});
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
softDeletedAt: expect.any(Number),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("allows org publisher admins to restore packages and releases", async () => {
|
|
const { ctx, patch, insert } = makeSoftDeletePackageCtx({
|
|
pkg: makePackageDoc({
|
|
ownerUserId: "users:org-linked",
|
|
ownerPublisherId: "publishers:org",
|
|
softDeletedAt: 123,
|
|
softDeletedBy: "users:owner",
|
|
softDeletedByRole: "user",
|
|
latestReleaseId: undefined,
|
|
latestVersionSummary: undefined,
|
|
tags: {},
|
|
}),
|
|
releases: [
|
|
makeReleaseDoc({
|
|
softDeletedAt: 123,
|
|
distTags: [],
|
|
summary: "restored release",
|
|
capabilities: { capabilityTags: ["tools"], executesCode: true },
|
|
compatibility: { pluginApi: ">=1.0.0" },
|
|
verification: { scanStatus: "clean" },
|
|
artifactKind: "legacy-zip",
|
|
integritySha256: "a".repeat(64),
|
|
scanStatus: "clean",
|
|
}),
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:demo-2",
|
|
version: "1.1.0",
|
|
softDeletedAt: 123,
|
|
distTags: ["beta"],
|
|
createdAt: 2,
|
|
artifactKind: "legacy-zip",
|
|
integritySha256: "b".repeat(64),
|
|
}),
|
|
],
|
|
user: { _id: "users:owner", role: "user" },
|
|
membership: { _id: "publisherMembers:1", role: "admin" },
|
|
});
|
|
|
|
const result = await restorePackageInternalHandler(ctx, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseCount: 2,
|
|
alreadyRestored: false,
|
|
});
|
|
expect(patch).toHaveBeenCalledWith("packageReleases:demo-1", {
|
|
softDeletedAt: undefined,
|
|
});
|
|
expect(patch).toHaveBeenCalledWith("packageReleases:demo-2", {
|
|
softDeletedAt: undefined,
|
|
});
|
|
expect(patch).toHaveBeenCalledWith("packageReleases:demo-2", {
|
|
distTags: ["beta", "latest"],
|
|
});
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
softDeletedAt: undefined,
|
|
softDeletedBy: undefined,
|
|
softDeletedByRole: undefined,
|
|
latestReleaseId: "packageReleases:demo-2",
|
|
latestVersionSummary: expect.objectContaining({ version: "1.1.0" }),
|
|
tags: {
|
|
beta: "packageReleases:demo-2",
|
|
latest: "packageReleases:demo-2",
|
|
},
|
|
updatedAt: expect.any(Number),
|
|
}),
|
|
);
|
|
const digestPatch = patch.mock.calls.find(([id]) => id === "packageSearchDigest:demo")?.[1];
|
|
expect(digestPatch).not.toHaveProperty("softDeletedBy");
|
|
expect(digestPatch).not.toHaveProperty("softDeletedByRole");
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({
|
|
actorUserId: "users:owner",
|
|
action: "package.undelete",
|
|
targetId: "packages:demo",
|
|
metadata: expect.objectContaining({
|
|
ownerPublisherId: "publishers:org",
|
|
releaseIds: ["packageReleases:demo-1", "packageReleases:demo-2"],
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not restore owner-deleted releases with the whole package", async () => {
|
|
const { ctx, patch } = makeSoftDeletePackageCtx({
|
|
pkg: makePackageDoc({
|
|
softDeletedAt: 123,
|
|
softDeletedBy: "users:owner",
|
|
softDeletedByRole: "user",
|
|
latestReleaseId: undefined,
|
|
latestVersionSummary: undefined,
|
|
tags: {},
|
|
}),
|
|
releases: [
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:ownerDeleted",
|
|
version: "2.0.0",
|
|
softDeletedAt: 100,
|
|
ownerDeletedAt: 100,
|
|
ownerDeletedBy: "users:owner",
|
|
distTags: ["latest"],
|
|
createdAt: 20,
|
|
}),
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:restorable",
|
|
version: "1.0.0",
|
|
softDeletedAt: 123,
|
|
distTags: ["stable"],
|
|
createdAt: 10,
|
|
}),
|
|
],
|
|
});
|
|
|
|
const result = await restorePackageInternalHandler(ctx, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, releaseCount: 1, alreadyRestored: false });
|
|
expect(patch).not.toHaveBeenCalledWith("packageReleases:ownerDeleted", {
|
|
softDeletedAt: undefined,
|
|
});
|
|
expect(patch).toHaveBeenCalledWith("packageReleases:restorable", {
|
|
softDeletedAt: undefined,
|
|
});
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
latestReleaseId: "packageReleases:restorable",
|
|
latestVersionSummary: expect.objectContaining({ version: "1.0.0" }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("rejects owner restore for moderator-deleted packages", async () => {
|
|
const { ctx, patch } = makeSoftDeletePackageCtx({
|
|
pkg: makePackageDoc({
|
|
ownerUserId: "users:org-linked",
|
|
ownerPublisherId: "publishers:org",
|
|
softDeletedAt: 123,
|
|
softDeletedBy: "users:moderator",
|
|
softDeletedByRole: "moderator",
|
|
}),
|
|
user: { _id: "users:owner", role: "user" },
|
|
membership: { _id: "publisherMembers:1", role: "admin" },
|
|
});
|
|
|
|
await expect(
|
|
restorePackageInternalHandler(ctx, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
}),
|
|
).rejects.toThrow(/^Forbidden:/i);
|
|
|
|
expect(patch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects owner restore for packages with unknown delete provenance", async () => {
|
|
const { ctx, patch } = makeSoftDeletePackageCtx({
|
|
pkg: makePackageDoc({
|
|
ownerUserId: "users:org-linked",
|
|
ownerPublisherId: "publishers:org",
|
|
softDeletedAt: 123,
|
|
softDeletedBy: undefined,
|
|
softDeletedByRole: undefined,
|
|
}),
|
|
user: { _id: "users:owner", role: "user" },
|
|
membership: { _id: "publisherMembers:1", role: "admin" },
|
|
});
|
|
|
|
await expect(
|
|
restorePackageInternalHandler(ctx, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
}),
|
|
).rejects.toThrow(/^Forbidden:/i);
|
|
|
|
expect(patch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("lets moderators restamp already-deleted package provenance", async () => {
|
|
const { ctx, patch } = makeSoftDeletePackageCtx({
|
|
pkg: makePackageDoc({
|
|
ownerUserId: "users:owner",
|
|
softDeletedAt: 123,
|
|
softDeletedBy: "users:owner",
|
|
softDeletedByRole: "user",
|
|
}),
|
|
user: { _id: "users:moderator", role: "moderator" },
|
|
});
|
|
|
|
await expect(
|
|
softDeletePackageInternalHandler(ctx, {
|
|
userId: "users:moderator",
|
|
name: "demo-plugin",
|
|
}),
|
|
).resolves.toMatchObject({ ok: true, alreadyDeleted: true });
|
|
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
softDeletedBy: "users:moderator",
|
|
softDeletedByRole: "moderator",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("preserves existing latest tags when restoring packages", async () => {
|
|
const { ctx, patch } = makeSoftDeletePackageCtx({
|
|
pkg: makePackageDoc({
|
|
softDeletedAt: 123,
|
|
softDeletedBy: "users:owner",
|
|
softDeletedByRole: "user",
|
|
latestReleaseId: "packageReleases:demo-1",
|
|
tags: {
|
|
latest: "packageReleases:demo-1",
|
|
beta: "packageReleases:demo-2",
|
|
},
|
|
}),
|
|
releases: [
|
|
makeReleaseDoc({
|
|
version: "1.0.0",
|
|
softDeletedAt: 123,
|
|
distTags: ["latest"],
|
|
artifactKind: "legacy-zip",
|
|
integritySha256: "a".repeat(64),
|
|
}),
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:demo-2",
|
|
version: "2.0.0-beta.1",
|
|
softDeletedAt: 123,
|
|
distTags: ["beta"],
|
|
createdAt: 2,
|
|
artifactKind: "legacy-zip",
|
|
integritySha256: "b".repeat(64),
|
|
}),
|
|
],
|
|
});
|
|
|
|
await expect(
|
|
restorePackageInternalHandler(ctx, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
}),
|
|
).resolves.toMatchObject({ ok: true, alreadyRestored: false });
|
|
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
latestReleaseId: "packageReleases:demo-1",
|
|
latestVersionSummary: expect.objectContaining({ version: "1.0.0" }),
|
|
tags: {
|
|
latest: "packageReleases:demo-1",
|
|
beta: "packageReleases:demo-2",
|
|
},
|
|
}),
|
|
);
|
|
expect(patch).not.toHaveBeenCalledWith("packageReleases:demo-2", {
|
|
distTags: ["beta", "latest"],
|
|
});
|
|
});
|
|
|
|
it("syncs package search digest when packages are soft-deleted", async () => {
|
|
const { ctx, patch } = makeSoftDeletePackageCtx({
|
|
packageSearchDigest: {
|
|
_id: "packageSearchDigest:demo",
|
|
packageId: "packages:demo",
|
|
softDeletedAt: undefined,
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
softDeletePackageInternalHandler(ctx, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
}),
|
|
).resolves.toMatchObject({ ok: true, alreadyDeleted: false });
|
|
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageSearchDigest:demo",
|
|
expect.objectContaining({
|
|
softDeletedAt: expect.any(Number),
|
|
updatedAt: expect.any(Number),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("reserves private package placeholders without releases", async () => {
|
|
const { ctx, insert } = makeReservePackageNameCtx();
|
|
|
|
await expect(
|
|
reservePackageNameInternalHandler(ctx, {
|
|
actorUserId: "users:admin",
|
|
ownerUserId: "users:openclaw",
|
|
ownerPublisherId: "publishers:openclaw",
|
|
name: " @openclaw/diffs ",
|
|
reason: "reserve official plugin",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
action: "reserved",
|
|
packageId: "packages:reserved",
|
|
name: "@openclaw/diffs",
|
|
});
|
|
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"packages",
|
|
expect.objectContaining({
|
|
name: "@openclaw/diffs",
|
|
normalizedName: "@openclaw/diffs",
|
|
displayName: "@openclaw/diffs",
|
|
ownerUserId: "users:openclaw",
|
|
ownerPublisherId: "publishers:openclaw",
|
|
family: "code-plugin",
|
|
channel: "private",
|
|
isOfficial: false,
|
|
tags: {},
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 0 },
|
|
recommendedScore: 170,
|
|
recommendedScoreVersion: 4,
|
|
}),
|
|
);
|
|
expect(insert).not.toHaveBeenCalledWith("packageReleases", expect.anything());
|
|
});
|
|
|
|
it("rejects reserving package names owned by another publisher", async () => {
|
|
const { ctx } = makeReservePackageNameCtx({
|
|
existing: makePackageDoc({
|
|
ownerUserId: "users:other",
|
|
ownerPublisherId: "publishers:other",
|
|
}),
|
|
});
|
|
|
|
await expect(
|
|
reservePackageNameInternalHandler(ctx, {
|
|
actorUserId: "users:admin",
|
|
ownerUserId: "users:openclaw",
|
|
ownerPublisherId: "publishers:openclaw",
|
|
name: "@openclaw/diffs",
|
|
}),
|
|
).rejects.toThrow("Package already exists and belongs to another publisher");
|
|
});
|
|
|
|
it("lets admins transfer a package to the OpenClaw publisher and make it official", async () => {
|
|
const { ctx, patch, insert } = makeTransferPackageOwnerCtx();
|
|
|
|
await expect(
|
|
transferPackageOwnerInternalHandler(ctx, {
|
|
actorUserId: "users:admin",
|
|
name: " demo-plugin ",
|
|
ownerUserId: "users:openclaw",
|
|
ownerPublisherId: "publishers:openclaw",
|
|
channel: "official",
|
|
reason: "move official plugin package under OpenClaw",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
ownerPublisherId: "publishers:openclaw",
|
|
channel: "official",
|
|
});
|
|
|
|
expect(patch).toHaveBeenCalledWith("packages:demo", {
|
|
ownerUserId: "users:openclaw",
|
|
ownerPublisherId: "publishers:openclaw",
|
|
channel: "official",
|
|
isOfficial: true,
|
|
updatedAt: expect.any(Number),
|
|
});
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({
|
|
action: "package.owner.transfer",
|
|
targetType: "package",
|
|
targetId: "packages:demo",
|
|
metadata: expect.objectContaining({
|
|
name: "demo-plugin",
|
|
previousOwnerUserId: "users:owner",
|
|
nextOwnerUserId: "users:openclaw",
|
|
nextOwnerPublisherId: "publishers:openclaw",
|
|
previousChannel: "community",
|
|
nextChannel: "official",
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("lets package owners transfer legacy scoped plugins to the matching org without changing package identity", async () => {
|
|
const trustedPublisher = {
|
|
_id: "packageTrustedPublishers:opik",
|
|
packageId: "packages:opik",
|
|
provider: "github-actions",
|
|
repository: "comet-ml/opik-openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "comet-ml",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "publish-clawhub.yml",
|
|
};
|
|
const { ctx, patch, insert, pkg } = makeUserTransferPackageOwnerCtx({
|
|
destinationMembershipRole: "owner",
|
|
trustedPublisher,
|
|
});
|
|
|
|
await expect(
|
|
transferPackageOwnerForUserInternalHandler(ctx, {
|
|
actorUserId: "users:vincent",
|
|
name: "@opik/opik-openclaw",
|
|
toOwner: "opik",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
packageId: "packages:opik",
|
|
name: "@opik/opik-openclaw",
|
|
ownerPublisherId: "publishers:opik",
|
|
channel: "community",
|
|
});
|
|
|
|
expect(patch).toHaveBeenCalledWith("packages:opik", {
|
|
ownerUserId: "users:vincent",
|
|
ownerPublisherId: "publishers:opik",
|
|
channel: "community",
|
|
isOfficial: false,
|
|
updatedAt: expect.any(Number),
|
|
});
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageSearchDigest:opik",
|
|
expect.objectContaining({
|
|
ownerUserId: "users:vincent",
|
|
ownerPublisherId: "publishers:opik",
|
|
channel: "community",
|
|
isOfficial: false,
|
|
}),
|
|
);
|
|
expect(patch).not.toHaveBeenCalledWith(
|
|
"packages:opik",
|
|
expect.objectContaining({ stats: expect.anything() }),
|
|
);
|
|
expect((pkg as { stats?: unknown }).stats).toEqual({
|
|
downloads: 42,
|
|
installs: 7,
|
|
stars: 3,
|
|
versions: 1,
|
|
});
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({
|
|
action: "package.owner.transfer",
|
|
targetId: "packages:opik",
|
|
metadata: expect.objectContaining({
|
|
name: "@opik/opik-openclaw",
|
|
previousOwnerPublisherId: "publishers:vincent",
|
|
nextOwnerPublisherId: "publishers:opik",
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("rejects user package transfers without source admin access", async () => {
|
|
const { ctx } = makeUserTransferPackageOwnerCtx({
|
|
pkg: makePackageDoc({
|
|
name: "@opik/opik-openclaw",
|
|
normalizedName: "@opik/opik-openclaw",
|
|
ownerUserId: "users:someoneelse",
|
|
ownerPublisherId: "publishers:source-org",
|
|
}),
|
|
sourceMembershipRole: "publisher",
|
|
destinationMembershipRole: "owner",
|
|
});
|
|
|
|
await expect(
|
|
transferPackageOwnerForUserInternalHandler(ctx, {
|
|
actorUserId: "users:vincent",
|
|
name: "@opik/opik-openclaw",
|
|
toOwner: "opik",
|
|
}),
|
|
).rejects.toThrow("Forbidden");
|
|
});
|
|
|
|
it("rejects user package transfers through stale personal-publisher memberships", async () => {
|
|
const { ctx } = makeUserTransferPackageOwnerCtx({
|
|
pkg: makePackageDoc({
|
|
name: "@owner/demo",
|
|
normalizedName: "@owner/demo",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:vincent",
|
|
}),
|
|
sourcePublisher: {
|
|
_id: "publishers:vincent",
|
|
kind: "user",
|
|
handle: "owner",
|
|
linkedUserId: "users:owner",
|
|
},
|
|
sourceMembershipRole: "admin",
|
|
destinationMembershipRole: "owner",
|
|
});
|
|
|
|
await expect(
|
|
transferPackageOwnerForUserInternalHandler(ctx, {
|
|
actorUserId: "users:vincent",
|
|
name: "demo-plugin",
|
|
toOwner: "opik",
|
|
}),
|
|
).rejects.toThrow("Forbidden");
|
|
});
|
|
|
|
it("lets owners transfer packages from legacy personal publishers without linked users", async () => {
|
|
const { ctx } = makeUserTransferPackageOwnerCtx({
|
|
sourcePublisher: {
|
|
_id: "publishers:vincent",
|
|
kind: "user",
|
|
handle: "vincentkoc",
|
|
linkedUserId: undefined,
|
|
},
|
|
sourceMembershipRole: null,
|
|
destinationMembershipRole: "owner",
|
|
});
|
|
|
|
await expect(
|
|
transferPackageOwnerForUserInternalHandler(ctx, {
|
|
actorUserId: "users:vincent",
|
|
name: "@opik/opik-openclaw",
|
|
toOwner: "opik",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
ownerUserId: "users:vincent",
|
|
ownerPublisherId: "publishers:opik",
|
|
});
|
|
});
|
|
|
|
it("rejects package transfers through stale no-link personal memberships", async () => {
|
|
const { ctx } = makeUserTransferPackageOwnerCtx({
|
|
pkg: makePackageDoc({
|
|
name: "@owner/demo",
|
|
normalizedName: "@owner/demo",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:vincent",
|
|
}),
|
|
sourcePublisher: {
|
|
_id: "publishers:vincent",
|
|
kind: "user",
|
|
handle: "owner",
|
|
linkedUserId: undefined,
|
|
},
|
|
sourceMembershipRole: "admin",
|
|
destinationMembershipRole: "owner",
|
|
});
|
|
|
|
await expect(
|
|
transferPackageOwnerForUserInternalHandler(ctx, {
|
|
actorUserId: "users:vincent",
|
|
name: "demo-plugin",
|
|
toOwner: "opik",
|
|
}),
|
|
).rejects.toThrow("Forbidden");
|
|
});
|
|
|
|
it("rejects package transfer destinations through stale no-link personal memberships", async () => {
|
|
const { ctx } = makeUserTransferPackageOwnerCtx({
|
|
destinationPublisher: {
|
|
_id: "publishers:opik",
|
|
kind: "user",
|
|
handle: "opik",
|
|
linkedUserId: undefined,
|
|
},
|
|
destinationMembershipRole: "admin",
|
|
});
|
|
|
|
await expect(
|
|
transferPackageOwnerForUserInternalHandler(ctx, {
|
|
actorUserId: "users:vincent",
|
|
name: "@opik/opik-openclaw",
|
|
toOwner: "opik",
|
|
}),
|
|
).rejects.toThrow('admin access for "@opik"');
|
|
});
|
|
|
|
it("rejects user package transfers without destination admin access", async () => {
|
|
const { ctx } = makeUserTransferPackageOwnerCtx({ destinationMembershipRole: "publisher" });
|
|
|
|
await expect(
|
|
transferPackageOwnerForUserInternalHandler(ctx, {
|
|
actorUserId: "users:vincent",
|
|
name: "@opik/opik-openclaw",
|
|
toOwner: "opik",
|
|
}),
|
|
).rejects.toThrow('admin access for "@opik"');
|
|
});
|
|
|
|
it("rejects scoped package transfers to a mismatched destination", async () => {
|
|
const { ctx } = makeUserTransferPackageOwnerCtx({ destinationMembershipRole: "owner" });
|
|
|
|
await expect(
|
|
transferPackageOwnerForUserInternalHandler(ctx, {
|
|
actorUserId: "users:vincent",
|
|
name: "@opik/opik-openclaw",
|
|
toOwner: "otherorg",
|
|
}),
|
|
).rejects.toThrow('Package scope "@opik" can only be transferred to publisher "@opik"');
|
|
});
|
|
|
|
it("rejects user package transfers to missing publishers with clear guidance", async () => {
|
|
const { ctx } = makeUserTransferPackageOwnerCtx({
|
|
destinationPublisher: null,
|
|
});
|
|
|
|
await expect(
|
|
transferPackageOwnerForUserInternalHandler(ctx, {
|
|
actorUserId: "users:vincent",
|
|
name: "@opik/opik-openclaw",
|
|
toOwner: "opik",
|
|
}),
|
|
).rejects.toThrow('Create the "@opik" organization');
|
|
});
|
|
|
|
it("lets admins repair a package name and runtime id with an audit trail", async () => {
|
|
const { ctx, patch, insert } = makeTransferPackageOwnerCtx({
|
|
pkg: makePackageDoc({
|
|
name: "whatsapp",
|
|
normalizedName: "whatsapp",
|
|
runtimeId: "whatsapp",
|
|
}),
|
|
});
|
|
|
|
await expect(
|
|
repairPackageIdentityInternalHandler(ctx, {
|
|
actorUserId: "users:admin",
|
|
name: "whatsapp",
|
|
nextName: "ivangdavila-whatsapp",
|
|
nextRuntimeId: "ivangdavila-whatsapp",
|
|
reason: "free official OpenClaw WhatsApp package id",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
name: "ivangdavila-whatsapp",
|
|
runtimeId: "ivangdavila-whatsapp",
|
|
});
|
|
|
|
expect(patch).toHaveBeenCalledWith("packages:demo", {
|
|
name: "ivangdavila-whatsapp",
|
|
normalizedName: "ivangdavila-whatsapp",
|
|
runtimeId: "ivangdavila-whatsapp",
|
|
updatedAt: expect.any(Number),
|
|
});
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageSearchDigest:demo",
|
|
expect.objectContaining({
|
|
name: "ivangdavila-whatsapp",
|
|
normalizedName: "ivangdavila-whatsapp",
|
|
runtimeId: "ivangdavila-whatsapp",
|
|
}),
|
|
);
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({
|
|
action: "package.identity.repair",
|
|
targetType: "package",
|
|
targetId: "packages:demo",
|
|
metadata: expect.objectContaining({
|
|
previousName: "whatsapp",
|
|
nextName: "ivangdavila-whatsapp",
|
|
previousRuntimeId: "whatsapp",
|
|
nextRuntimeId: "ivangdavila-whatsapp",
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("rejects official package transfers to non-official publishers", async () => {
|
|
const { ctx } = makeTransferPackageOwnerCtx({
|
|
ownerPublisher: {
|
|
_id: "publishers:openclaw",
|
|
kind: "org",
|
|
handle: "other",
|
|
displayName: "Other",
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
transferPackageOwnerInternalHandler(ctx, {
|
|
actorUserId: "users:admin",
|
|
name: "demo-plugin",
|
|
ownerUserId: "users:openclaw",
|
|
ownerPublisherId: "publishers:openclaw",
|
|
channel: "official",
|
|
}),
|
|
).rejects.toThrow("Only official publishers may own official packages");
|
|
});
|
|
|
|
it("lets owners publish real releases into reserved package placeholders", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
makePackageDoc({
|
|
name: "@openclaw/diffs",
|
|
normalizedName: "@openclaw/diffs",
|
|
ownerUserId: "users:openclaw",
|
|
ownerPublisherId: "publishers:openclaw",
|
|
family: "bundle-plugin",
|
|
channel: "private",
|
|
isOfficial: false,
|
|
latestReleaseId: undefined,
|
|
latestVersionSummary: undefined,
|
|
tags: {},
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 0 },
|
|
}),
|
|
[],
|
|
{
|
|
"users:admin": {
|
|
_id: "users:admin",
|
|
role: "admin",
|
|
trustedPublisher: false,
|
|
},
|
|
"users:openclaw": {
|
|
_id: "users:openclaw",
|
|
role: "user",
|
|
trustedPublisher: false,
|
|
},
|
|
"publishers:openclaw": {
|
|
_id: "publishers:openclaw",
|
|
kind: "org",
|
|
handle: "openclaw",
|
|
displayName: "OpenClaw",
|
|
trustedPublisher: true,
|
|
},
|
|
},
|
|
);
|
|
|
|
await insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:admin",
|
|
ownerUserId: "users:openclaw",
|
|
ownerPublisherId: "publishers:openclaw",
|
|
name: "@openclaw/diffs",
|
|
displayName: "@openclaw/diffs",
|
|
family: "code-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
tags: ["latest"],
|
|
summary: "diff tools",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
});
|
|
|
|
expect(ctx.patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
family: "code-plugin",
|
|
channel: "official",
|
|
isOfficial: true,
|
|
latestReleaseId: "packageReleases:new",
|
|
tags: { latest: "packageReleases:new" },
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("creates pending package releases without updating public latest pointers", async () => {
|
|
const existingPackage = makePackageDoc({
|
|
latestReleaseId: "packageReleases:old",
|
|
latestVersionSummary: { version: "1.0.0" },
|
|
tags: { latest: "packageReleases:old" },
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
});
|
|
const priorRelease = makeReleaseDoc({
|
|
_id: "packageReleases:old",
|
|
version: "1.0.0",
|
|
distTags: ["latest"],
|
|
});
|
|
const ctx = makeInsertReleaseCtx(existingPackage, [priorRelease]);
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "2.0.0",
|
|
publicationStatus: "pending",
|
|
changelog: "next",
|
|
tags: ["latest"],
|
|
summary: "pending summary",
|
|
files: [],
|
|
integritySha256: "pending-sha",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:new",
|
|
publicationStatus: "pending",
|
|
createdNewParent: false,
|
|
});
|
|
|
|
expect(ctx.insert).toHaveBeenCalledWith(
|
|
"packageReleases",
|
|
expect.objectContaining({
|
|
publicationStatus: "pending",
|
|
pendingPublication: expect.objectContaining({
|
|
tags: ["latest"],
|
|
displayName: "Demo Plugin",
|
|
}),
|
|
}),
|
|
);
|
|
expect(ctx.patch).not.toHaveBeenCalledWith("packages:demo", expect.anything());
|
|
expect(ctx.patch).not.toHaveBeenCalledWith("packageReleases:old", expect.anything());
|
|
});
|
|
|
|
it("publishes a pending package release by promoting the existing row", async () => {
|
|
vi.spyOn(Date, "now").mockReturnValue(1_700_000_000_000);
|
|
const existingPackage = makePackageDoc({
|
|
latestReleaseId: "packageReleases:old",
|
|
latestVersionSummary: { version: "1.0.0" },
|
|
tags: { latest: "packageReleases:old" },
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
});
|
|
const priorRelease = makeReleaseDoc({
|
|
_id: "packageReleases:old",
|
|
version: "1.0.0",
|
|
distTags: ["latest"],
|
|
publicationStatus: "published",
|
|
});
|
|
const pendingRelease = makeReleaseDoc({
|
|
_id: "packageReleases:pending",
|
|
version: "2.0.0",
|
|
publicationStatus: "pending",
|
|
pendingPublication: {
|
|
displayName: "Demo Plugin",
|
|
tags: ["latest"],
|
|
channel: "community",
|
|
isOfficial: false,
|
|
},
|
|
distTags: ["latest"],
|
|
summary: "pending summary",
|
|
changelog: "next",
|
|
integritySha256: "pending-sha",
|
|
verification: { scanStatus: "pending" },
|
|
llmAnalysis: {
|
|
status: "clean",
|
|
verdict: "clean",
|
|
checkedAt: 1_700_000_000_000,
|
|
},
|
|
});
|
|
const ctx = makeInsertReleaseCtx(existingPackage, [priorRelease, pendingRelease], {
|
|
"packageReleases:pending": pendingRelease,
|
|
"packages:demo": existingPackage,
|
|
});
|
|
|
|
await expect(
|
|
publishPendingReleaseInternalHandler(ctx, {
|
|
releaseId: "packageReleases:pending",
|
|
}),
|
|
).resolves.toEqual({
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:pending",
|
|
});
|
|
|
|
expect(ctx.patch).toHaveBeenCalledWith("packageReleases:pending", {
|
|
publicationStatus: "published",
|
|
pendingPublication: undefined,
|
|
distTags: ["latest"],
|
|
verification: { scanStatus: "clean" },
|
|
});
|
|
await flushScheduledPackageReleaseTagCleanup(ctx);
|
|
expect(ctx.patch).toHaveBeenCalledWith("packageReleases:old", { distTags: [] });
|
|
expect(ctx.patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
latestReleaseId: "packageReleases:pending",
|
|
latestVersionSummary: expect.objectContaining({ version: "2.0.0" }),
|
|
tags: { latest: "packageReleases:pending" },
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 2 },
|
|
scanStatus: "clean",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("keeps the current latest when finalizing an older package release", async () => {
|
|
const existingPackage = makePackageDoc({
|
|
latestReleaseId: "packageReleases:latest",
|
|
latestVersionSummary: { version: "2.0.0" },
|
|
tags: { latest: "packageReleases:latest" },
|
|
summary: "current summary",
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
});
|
|
const latestRelease = makeReleaseDoc({
|
|
_id: "packageReleases:latest",
|
|
version: "2.0.0",
|
|
distTags: ["latest"],
|
|
publicationStatus: "published",
|
|
});
|
|
const pendingRelease = makeReleaseDoc({
|
|
_id: "packageReleases:pending",
|
|
version: "1.0.1",
|
|
publicationStatus: "pending",
|
|
pendingPublication: {
|
|
displayName: "Demo Plugin v1",
|
|
tags: ["latest", "v1"],
|
|
channel: "community",
|
|
isOfficial: false,
|
|
},
|
|
distTags: ["latest", "v1"],
|
|
summary: "backport summary",
|
|
changelog: "backport",
|
|
integritySha256: "backport-sha",
|
|
verification: { scanStatus: "pending" },
|
|
llmAnalysis: {
|
|
status: "clean",
|
|
verdict: "clean",
|
|
checkedAt: 1_700_000_000_000,
|
|
},
|
|
});
|
|
const ctx = makeInsertReleaseCtx(existingPackage, [latestRelease, pendingRelease], {
|
|
"packageReleases:latest": latestRelease,
|
|
"packageReleases:pending": pendingRelease,
|
|
"packages:demo": existingPackage,
|
|
});
|
|
|
|
await publishPendingReleaseInternalHandler(ctx, {
|
|
releaseId: "packageReleases:pending",
|
|
});
|
|
|
|
expect(ctx.patch).toHaveBeenCalledWith(
|
|
"packageReleases:pending",
|
|
expect.objectContaining({
|
|
publicationStatus: "published",
|
|
distTags: ["v1"],
|
|
}),
|
|
);
|
|
expect(ctx.patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
latestReleaseId: "packageReleases:latest",
|
|
latestVersionSummary: { version: "2.0.0" },
|
|
summary: "current summary",
|
|
tags: {
|
|
latest: "packageReleases:latest",
|
|
v1: "packageReleases:pending",
|
|
},
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 2 },
|
|
}),
|
|
);
|
|
expect(ctx.scheduler.runAfter).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("publishes a pending release without reading its file-heavy release history", async () => {
|
|
const priorRelease = makeReleaseDoc({
|
|
_id: "packageReleases:old",
|
|
version: "1.0.0",
|
|
distTags: ["latest"],
|
|
publicationStatus: "published",
|
|
extractedPackageJson: {
|
|
description: "x".repeat(190_000),
|
|
},
|
|
});
|
|
const unrelatedReleases = Array.from({ length: 91 }, (_, index) =>
|
|
makeReleaseDoc({
|
|
_id: `packageReleases:history-${index}`,
|
|
version: `0.0.${index}`,
|
|
distTags: [],
|
|
publicationStatus: "published",
|
|
extractedPackageJson: {
|
|
description: "x".repeat(190_000),
|
|
},
|
|
}),
|
|
);
|
|
const pendingRelease = makeReleaseDoc({
|
|
_id: "packageReleases:pending",
|
|
version: "2.0.0",
|
|
publicationStatus: "pending",
|
|
pendingPublication: {
|
|
displayName: "Demo Plugin",
|
|
tags: ["latest"],
|
|
channel: "community",
|
|
isOfficial: false,
|
|
},
|
|
distTags: ["latest"],
|
|
summary: "pending summary",
|
|
changelog: "next",
|
|
integritySha256: "pending-sha",
|
|
verification: { scanStatus: "pending" },
|
|
llmAnalysis: {
|
|
status: "clean",
|
|
verdict: "clean",
|
|
checkedAt: 1_700_000_000_000,
|
|
},
|
|
});
|
|
const existingPackage = makePackageDoc({
|
|
latestReleaseId: "packageReleases:old",
|
|
latestVersionSummary: { version: "1.0.0" },
|
|
tags: { latest: "packageReleases:old" },
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 92 },
|
|
});
|
|
const allReleases = [priorRelease, ...unrelatedReleases, pendingRelease];
|
|
const ctx = makeInsertReleaseCtx(
|
|
existingPackage,
|
|
allReleases,
|
|
{
|
|
"packageReleases:old": priorRelease,
|
|
"packageReleases:pending": pendingRelease,
|
|
"packages:demo": existingPackage,
|
|
},
|
|
[],
|
|
undefined,
|
|
16 * 1024 * 1024,
|
|
);
|
|
|
|
await expect(
|
|
publishPendingReleaseInternalHandler(ctx, {
|
|
releaseId: "packageReleases:pending",
|
|
}),
|
|
).resolves.toEqual({
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:pending",
|
|
});
|
|
|
|
await flushScheduledPackageReleaseTagCleanup(ctx);
|
|
expect(ctx.patch).toHaveBeenCalledWith("packageReleases:old", { distTags: [] });
|
|
expect(ctx.patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
latestReleaseId: "packageReleases:pending",
|
|
tags: { latest: "packageReleases:pending" },
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 93 },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("publishes a pending release without hydrating every file-heavy tag owner", async () => {
|
|
const tagNames = ["latest", ...Array.from({ length: 89 }, (_, index) => `tag-${index}`)];
|
|
const priorReleases = tagNames.map((tag, index) =>
|
|
makeReleaseDoc({
|
|
_id: `packageReleases:old-${index}`,
|
|
version: `1.0.${index}`,
|
|
distTags: [tag],
|
|
publicationStatus: "published",
|
|
extractedPackageJson: {
|
|
description: "x".repeat(190_000),
|
|
},
|
|
}),
|
|
);
|
|
const pendingRelease = makeReleaseDoc({
|
|
_id: "packageReleases:pending",
|
|
version: "2.0.0",
|
|
publicationStatus: "pending",
|
|
pendingPublication: {
|
|
displayName: "Demo Plugin",
|
|
tags: tagNames,
|
|
channel: "community",
|
|
isOfficial: false,
|
|
},
|
|
distTags: tagNames,
|
|
summary: "pending summary",
|
|
changelog: "next",
|
|
integritySha256: "pending-sha",
|
|
verification: { scanStatus: "pending" },
|
|
llmAnalysis: {
|
|
status: "clean",
|
|
verdict: "clean",
|
|
checkedAt: 1_700_000_000_000,
|
|
},
|
|
});
|
|
const packageTags = Object.fromEntries(
|
|
tagNames.map((tag, index) => [tag, `packageReleases:old-${index}`]),
|
|
);
|
|
const existingPackage = makePackageDoc({
|
|
latestReleaseId: "packageReleases:old-0",
|
|
latestVersionSummary: { version: "1.0.0" },
|
|
tags: packageTags,
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: priorReleases.length },
|
|
});
|
|
const recordsById: Record<string, Record<string, unknown>> = Object.fromEntries(
|
|
[...priorReleases, pendingRelease].map((release) => [release._id, release]),
|
|
);
|
|
recordsById["packages:demo"] = existingPackage;
|
|
const ctx = makeInsertReleaseCtx(
|
|
existingPackage,
|
|
[...priorReleases, pendingRelease],
|
|
recordsById,
|
|
[],
|
|
undefined,
|
|
16 * 1024 * 1024,
|
|
);
|
|
|
|
await expect(
|
|
publishPendingReleaseInternalHandler(ctx, {
|
|
releaseId: "packageReleases:pending",
|
|
}),
|
|
).resolves.toEqual({
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:pending",
|
|
});
|
|
|
|
expect(ctx.scheduler.runAfter).toHaveBeenCalledOnce();
|
|
expect(ctx.patch).not.toHaveBeenCalledWith(
|
|
expect.stringMatching(/^packageReleases:old-/),
|
|
expect.anything(),
|
|
);
|
|
await flushScheduledPackageReleaseTagCleanup(ctx);
|
|
expect(ctx.scheduler.runAfter).toHaveBeenCalledTimes(23);
|
|
expect(
|
|
ctx.patch.mock.calls.filter(([id]) => id.startsWith("packageReleases:old-")),
|
|
).toHaveLength(90);
|
|
});
|
|
|
|
it("keeps a dist-tag when a later publish moves it back before cleanup runs", async () => {
|
|
const priorRelease = makeReleaseDoc({
|
|
_id: "packageReleases:old",
|
|
distTags: ["latest"],
|
|
publicationStatus: "published",
|
|
});
|
|
const pkg = makePackageDoc({
|
|
latestReleaseId: "packageReleases:old",
|
|
tags: { latest: "packageReleases:old" },
|
|
});
|
|
const ctx = makeInsertReleaseCtx(pkg, [priorRelease]);
|
|
|
|
await cleanupReassignedPackageReleaseTagsInternalHandler(ctx, {
|
|
packageId: "packages:demo",
|
|
assignments: [{ releaseId: "packageReleases:old", tags: ["latest"] }],
|
|
});
|
|
|
|
expect(ctx.patch).not.toHaveBeenCalledWith("packageReleases:old", expect.anything());
|
|
expect(ctx.scheduler.runAfter).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("releases release-backed finalization claims when pending promotion fails", async () => {
|
|
const promotionError = new Error("promotion failed");
|
|
const runMutation = vi.fn(async (_ref: unknown, args: unknown) => {
|
|
if (typeof args === "object" && args !== null && "attemptId" in args && !("error" in args)) {
|
|
return {
|
|
status: "claimed",
|
|
attemptId: "publishAttempts:demo",
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:pending",
|
|
packageFollowup: {},
|
|
};
|
|
}
|
|
if (typeof args === "object" && args !== null && "releaseId" in args) {
|
|
throw promotionError;
|
|
}
|
|
if (typeof args === "object" && args !== null && "error" in args) {
|
|
return { attemptId: "publishAttempts:demo", status: "ready_to_finalize" };
|
|
}
|
|
throw new Error(`Unexpected mutation args ${JSON.stringify(args)}`);
|
|
});
|
|
const ctx = {
|
|
runMutation,
|
|
runQuery: vi.fn(async () => {
|
|
throw new Error("legacy insert recovery should not run for release-backed attempts");
|
|
}),
|
|
};
|
|
|
|
await expect(
|
|
finalizePackagePublishAttemptInternalHandler(ctx as never, {
|
|
attemptId: "publishAttempts:demo",
|
|
}),
|
|
).rejects.toThrow("promotion failed");
|
|
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({ releaseId: "packageReleases:pending" }),
|
|
);
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
attemptId: "publishAttempts:demo",
|
|
error: "promotion failed",
|
|
}),
|
|
);
|
|
expect(ctx.runQuery).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("recovers idempotent package publish results for the same owner", async () => {
|
|
const release = makeReleaseDoc({ integritySha256: "abc123" });
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(
|
|
makePackageDoc({
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner",
|
|
}),
|
|
),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(release),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert: vi.fn(),
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
findPackagePublishResultInternalHandler(ctx as never, {
|
|
name: "demo-plugin",
|
|
version: "1.0.0",
|
|
integritySha256: "abc123",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner",
|
|
}),
|
|
).resolves.toEqual({
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
});
|
|
});
|
|
|
|
it("does not recover pending package publish results as public successes", async () => {
|
|
const release = makeReleaseDoc({
|
|
integritySha256: "abc123",
|
|
publicationStatus: "pending",
|
|
});
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(
|
|
makePackageDoc({
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner",
|
|
}),
|
|
),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(release),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
findPackagePublishResultInternalHandler(ctx as never, {
|
|
name: "demo-plugin",
|
|
version: "1.0.0",
|
|
integritySha256: "abc123",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner",
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it("does not recover idempotent package publish results for another owner", async () => {
|
|
const ctx = {
|
|
db: {
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(
|
|
makePackageDoc({
|
|
ownerUserId: "users:other",
|
|
ownerPublisherId: "publishers:other",
|
|
}),
|
|
),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
throw new Error("release query should be skipped for owner mismatch");
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
findPackagePublishResultInternalHandler(ctx as never, {
|
|
name: "demo-plugin",
|
|
version: "1.0.0",
|
|
integritySha256: "abc123",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner",
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it("clears inferred catalog state when a publisher promotes a latest package release", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
makePackageDoc({
|
|
inferredCategories: ["tools"],
|
|
inferredTopics: ["Old inference"],
|
|
inferredFromReleaseId: "packageReleases:demo-1",
|
|
inferredCategoryConfidence: "high",
|
|
inferredTopicConfidence: "high",
|
|
inferredClassifierVersion: "taxonomy-prototype-v9",
|
|
inferredTopicClassifierVersion: "topic-prototype-v1",
|
|
inferredInputHash: "category-hash",
|
|
inferredTopicInputHash: "topic-hash",
|
|
inferredAt: 123,
|
|
}),
|
|
);
|
|
|
|
await insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.1",
|
|
changelog: "replace catalog metadata",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
categories: ["models"],
|
|
topics: ["Local models"],
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
});
|
|
|
|
const packagePatch = ctx.patch.mock.calls.find(([id]) => id === "packages:demo")?.[1];
|
|
expect(packagePatch).toBeDefined();
|
|
for (const field of [
|
|
"inferredCategories",
|
|
"inferredTopics",
|
|
"inferredFromReleaseId",
|
|
"inferredCategoryConfidence",
|
|
"inferredTopicConfidence",
|
|
"inferredClassifierVersion",
|
|
"inferredTopicClassifierVersion",
|
|
"inferredInputHash",
|
|
"inferredTopicInputHash",
|
|
"inferredAt",
|
|
]) {
|
|
expect(packagePatch).toHaveProperty(field, undefined);
|
|
}
|
|
});
|
|
|
|
it("uses the latest tag when the version summary and release pointer are stale", async () => {
|
|
const existingPackage = makePackageDoc({
|
|
latestReleaseId: "packageReleases:stale",
|
|
latestVersionSummary: undefined,
|
|
tags: { latest: "packageReleases:latest" },
|
|
summary: "current summary",
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
});
|
|
const staleRelease = makeReleaseDoc({
|
|
_id: "packageReleases:stale",
|
|
version: "1.0.0",
|
|
distTags: [],
|
|
});
|
|
const latestRelease = makeReleaseDoc({
|
|
_id: "packageReleases:latest",
|
|
version: "2.0.0",
|
|
distTags: ["latest"],
|
|
});
|
|
const ctx = makeInsertReleaseCtx(existingPackage, [staleRelease, latestRelease]);
|
|
|
|
await insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin v1",
|
|
family: "code-plugin",
|
|
version: "1.5.0",
|
|
changelog: "backport",
|
|
tags: ["latest", "v1"],
|
|
summary: "backport summary",
|
|
files: [],
|
|
integritySha256: "backport-sha",
|
|
});
|
|
|
|
expect(ctx.insert).toHaveBeenCalledWith(
|
|
"packageReleases",
|
|
expect.objectContaining({
|
|
version: "1.5.0",
|
|
distTags: ["v1"],
|
|
}),
|
|
);
|
|
expect(ctx.patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
latestReleaseId: "packageReleases:stale",
|
|
latestVersionSummary: undefined,
|
|
summary: "current summary",
|
|
tags: {
|
|
latest: "packageReleases:latest",
|
|
v1: "packageReleases:new",
|
|
},
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 2 },
|
|
}),
|
|
);
|
|
expect(ctx.scheduler.runAfter).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("publishes suspicious prepublication plugin results with a suspicious scan status", async () => {
|
|
const ctx = makeInsertReleaseCtx(makePackageDoc());
|
|
const llmAnalysis = {
|
|
status: "completed",
|
|
verdict: "suspicious",
|
|
summary: "Review before installing.",
|
|
checkedAt: 123,
|
|
};
|
|
|
|
await insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.1",
|
|
changelog: "reviewed release",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
verification: { tier: "structural", scanStatus: "pending" },
|
|
llmAnalysis,
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
sha256hash: "abc123",
|
|
});
|
|
|
|
expect(ctx.insert).toHaveBeenCalledWith(
|
|
"packageReleases",
|
|
expect.objectContaining({
|
|
llmAnalysis,
|
|
verification: expect.objectContaining({ scanStatus: "suspicious" }),
|
|
}),
|
|
);
|
|
expect(ctx.patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
scanStatus: "suspicious",
|
|
verification: expect.objectContaining({ scanStatus: "suspicious" }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("rejects family changes on an existing package name", async () => {
|
|
const ctx = makeInsertReleaseCtx(makePackageDoc({ family: "bundle-plugin" }));
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
}),
|
|
).rejects.toThrow("family changes are not allowed");
|
|
});
|
|
|
|
it("rejects new releases on a soft-deleted package", async () => {
|
|
const ctx = makeInsertReleaseCtx(makePackageDoc({ softDeletedAt: 123 }));
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.1",
|
|
changelog: "try deleted package",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
}),
|
|
).rejects.toThrow("Restore it before publishing another release");
|
|
});
|
|
|
|
it("rejects final package publish inserts when the actor was banned mid-publish", async () => {
|
|
const ctx = makeInsertReleaseCtx(makePackageDoc(), [], {
|
|
"users:owner": {
|
|
_id: "users:owner",
|
|
role: "user",
|
|
trustedPublisher: false,
|
|
deletedAt: 123,
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.1",
|
|
changelog: "try banned owner",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
}),
|
|
).rejects.toThrow("Unauthorized");
|
|
expect(ctx.insert).not.toHaveBeenCalledWith("packageReleases", expect.anything());
|
|
});
|
|
|
|
it("rejects final package publish inserts when the requested owner was banned mid-publish", async () => {
|
|
const ctx = makeInsertReleaseCtx(makePackageDoc(), [], {
|
|
"users:admin": { _id: "users:admin", role: "admin", trustedPublisher: false },
|
|
"users:owner": {
|
|
_id: "users:owner",
|
|
role: "user",
|
|
trustedPublisher: false,
|
|
deletedAt: 123,
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:admin",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.1",
|
|
changelog: "try banned owner",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
}),
|
|
).rejects.toThrow("Package owner is unavailable");
|
|
expect(ctx.insert).not.toHaveBeenCalledWith("packageReleases", expect.anything());
|
|
});
|
|
|
|
it("rejects final package publish inserts when the owner publisher was deleted mid-publish", async () => {
|
|
const ctx = makeInsertReleaseCtx(makePackageDoc(), [], {
|
|
"users:owner": { _id: "users:owner", role: "user", trustedPublisher: false },
|
|
"publishers:org": {
|
|
_id: "publishers:org",
|
|
kind: "org",
|
|
handle: "org",
|
|
deletedAt: 123,
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:org",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.1",
|
|
changelog: "try deleted publisher",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
}),
|
|
).rejects.toThrow("Package owner publisher is unavailable");
|
|
expect(ctx.insert).not.toHaveBeenCalledWith("packageReleases", expect.anything());
|
|
});
|
|
|
|
it("rejects final package publish inserts when a personal publisher owner was banned mid-publish", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
makePackageDoc({
|
|
ownerUserId: "users:publishing-actor",
|
|
ownerPublisherId: "publishers:personal",
|
|
}),
|
|
[],
|
|
{
|
|
"users:publishing-actor": {
|
|
_id: "users:publishing-actor",
|
|
role: "user",
|
|
trustedPublisher: false,
|
|
},
|
|
"users:owner": {
|
|
_id: "users:owner",
|
|
role: "user",
|
|
trustedPublisher: false,
|
|
deletedAt: 1_000,
|
|
},
|
|
"publishers:personal": {
|
|
_id: "publishers:personal",
|
|
kind: "user",
|
|
handle: "owner",
|
|
linkedUserId: "users:owner",
|
|
},
|
|
},
|
|
);
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:publishing-actor",
|
|
ownerUserId: "users:publishing-actor",
|
|
ownerPublisherId: "publishers:personal",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.1",
|
|
changelog: "try banned publisher owner",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
}),
|
|
).rejects.toThrow("Package owner publisher is unavailable");
|
|
expect(ctx.insert).not.toHaveBeenCalledWith("packageReleases", expect.anything());
|
|
});
|
|
|
|
it("rejects final user org package publish inserts when membership was removed mid-publish", async () => {
|
|
const ctx = makeInsertReleaseCtx(null, [], {
|
|
"users:member": { _id: "users:member", role: "user", trustedPublisher: false },
|
|
"publishers:org": {
|
|
_id: "publishers:org",
|
|
kind: "org",
|
|
handle: "org",
|
|
trustedPublisher: false,
|
|
},
|
|
});
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:member",
|
|
ownerUserId: "users:member",
|
|
ownerPublisherId: "publishers:org",
|
|
publishActor: { kind: "user", userId: "users:member" },
|
|
name: "@org/demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "bundle-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
}),
|
|
).rejects.toThrow('publish access for "@org"');
|
|
expect(ctx.insert).not.toHaveBeenCalledWith("packages", expect.anything());
|
|
expect(ctx.insert).not.toHaveBeenCalledWith("packageReleases", expect.anything());
|
|
});
|
|
|
|
it("rejects final user org package publish inserts when publish actor drifts from actor user", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
null,
|
|
[],
|
|
{
|
|
"users:member": { _id: "users:member", role: "user", trustedPublisher: false },
|
|
"publishers:org": {
|
|
_id: "publishers:org",
|
|
kind: "org",
|
|
handle: "org",
|
|
trustedPublisher: false,
|
|
},
|
|
},
|
|
[],
|
|
"publisher",
|
|
);
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:member",
|
|
ownerUserId: "users:member",
|
|
ownerPublisherId: "publishers:org",
|
|
publishActor: { kind: "user", userId: "users:other" },
|
|
name: "@org/demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "bundle-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
}),
|
|
).rejects.toThrow("Publish actor must match the authenticated actor");
|
|
expect(ctx.insert).not.toHaveBeenCalledWith("packages", expect.anything());
|
|
expect(ctx.insert).not.toHaveBeenCalledWith("packageReleases", expect.anything());
|
|
});
|
|
|
|
it("keeps final user org package publishes working when membership remains valid", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
null,
|
|
[],
|
|
{
|
|
"users:member": { _id: "users:member", role: "user", trustedPublisher: false },
|
|
"publishers:org": {
|
|
_id: "publishers:org",
|
|
kind: "org",
|
|
handle: "org",
|
|
trustedPublisher: false,
|
|
},
|
|
},
|
|
[],
|
|
"publisher",
|
|
);
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:member",
|
|
ownerUserId: "users:member",
|
|
ownerPublisherId: "publishers:org",
|
|
publishActor: { kind: "user", userId: "users:member" },
|
|
name: "@org/demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "bundle-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
}),
|
|
).resolves.toMatchObject({ ok: true, packageId: "packages:new" });
|
|
expect(ctx.insert).toHaveBeenCalledWith("packageReleases", expect.anything());
|
|
});
|
|
|
|
it("preserves trusted GitHub Actions package publishes without org membership", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
makePackageDoc({
|
|
name: "@org/demo-plugin",
|
|
normalizedName: "@org/demo-plugin",
|
|
ownerUserId: "users:member",
|
|
ownerPublisherId: "publishers:org",
|
|
}),
|
|
[],
|
|
{
|
|
"users:member": { _id: "users:member", role: "user", trustedPublisher: false },
|
|
"publishers:org": {
|
|
_id: "publishers:org",
|
|
kind: "org",
|
|
handle: "org",
|
|
trustedPublisher: false,
|
|
},
|
|
},
|
|
);
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:member",
|
|
ownerUserId: "users:member",
|
|
ownerPublisherId: "publishers:org",
|
|
publishActor: {
|
|
kind: "github-actions",
|
|
repository: "org/demo-plugin",
|
|
workflow: "publish.yml",
|
|
runId: "1",
|
|
runAttempt: "1",
|
|
sha: "abc123",
|
|
},
|
|
name: "@org/demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.1",
|
|
changelog: "trusted publish",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
}),
|
|
).resolves.toMatchObject({ ok: true, packageId: "packages:demo" });
|
|
expect(ctx.insert).toHaveBeenCalledWith("packageReleases", expect.anything());
|
|
});
|
|
|
|
it("rejects package scopes that do not match the selected owner handle", async () => {
|
|
const runMutation = vi.fn();
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:vintageayu",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:vintageayu",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
}),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: {
|
|
get: vi.fn(),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:vintageayu",
|
|
payload: {
|
|
name: "@openclaw/dronzer",
|
|
displayName: "Dronzer Controller",
|
|
ownerHandle: "vintageayu",
|
|
family: "code-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
files: [],
|
|
},
|
|
}),
|
|
).rejects.toThrow('Package scope "@openclaw" must match selected owner "@vintageayu"');
|
|
|
|
expect(runMutation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects unscoped package names that collide with publish routes", async () => {
|
|
const ctx = {
|
|
runQuery: vi.fn(),
|
|
runMutation: vi.fn(),
|
|
scheduler: { runAfter: vi.fn() },
|
|
storage: { get: vi.fn() },
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:owner",
|
|
payload: {
|
|
name: "publish",
|
|
displayName: "Publish",
|
|
family: "code-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
files: [],
|
|
},
|
|
}),
|
|
).rejects.toThrow('Package name "publish" is reserved for ClawHub routes');
|
|
|
|
expect(ctx.runMutation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects runtime id changes on an existing code plugin package", async () => {
|
|
const ctx = makeInsertReleaseCtx(makePackageDoc({ runtimeId: "demo.plugin" }));
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.1",
|
|
changelog: "retarget runtime id",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
runtimeId: "other.plugin",
|
|
}),
|
|
).rejects.toThrow("runtime id changes are not allowed");
|
|
});
|
|
|
|
it("rejects plugin id collisions with active packages", async () => {
|
|
const ctx = makeInsertReleaseCtx(null, [], {}, [
|
|
makePackageDoc({
|
|
_id: "packages:claimed",
|
|
name: "claimed-plugin",
|
|
normalizedName: "claimed-plugin",
|
|
runtimeId: "dronzer",
|
|
softDeletedAt: undefined,
|
|
}),
|
|
]);
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "dronzerclaw",
|
|
displayName: "Dronzer Claw",
|
|
family: "code-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
runtimeId: "dronzer",
|
|
}),
|
|
).rejects.toThrow('Plugin id "dronzer" is already claimed by another package');
|
|
});
|
|
|
|
it("allows plugin ids held only by soft-deleted packages", async () => {
|
|
const ctx = makeInsertReleaseCtx(null, [], {}, [
|
|
makePackageDoc({
|
|
_id: "packages:deleted",
|
|
name: "@openclaw/dronzer",
|
|
normalizedName: "@openclaw/dronzer",
|
|
runtimeId: "dronzer",
|
|
softDeletedAt: 123,
|
|
}),
|
|
]);
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "dronzerclaw",
|
|
displayName: "Dronzer Claw",
|
|
family: "code-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
runtimeId: "dronzer",
|
|
}),
|
|
).resolves.toMatchObject({ ok: true, packageId: "packages:new" });
|
|
});
|
|
|
|
it("promotes existing packages to official when owner is the OpenClaw publisher", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
makePackageDoc({
|
|
ownerUserId: "users:openclaw",
|
|
ownerPublisherId: "publishers:openclaw",
|
|
channel: "community",
|
|
isOfficial: false,
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
[],
|
|
{
|
|
"users:owner": { _id: "users:owner", role: "admin" },
|
|
"users:openclaw": { _id: "users:openclaw", role: "user" },
|
|
"publishers:openclaw": {
|
|
_id: "publishers:openclaw",
|
|
kind: "org",
|
|
handle: "openclaw",
|
|
displayName: "OpenClaw",
|
|
},
|
|
},
|
|
);
|
|
|
|
await insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:openclaw",
|
|
ownerPublisherId: "publishers:openclaw",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.1.0",
|
|
changelog: "promote",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
capabilities: { capabilityTags: ["tools"], executesCode: true },
|
|
});
|
|
|
|
expect(ctx.patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
channel: "official",
|
|
isOfficial: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("lets admins publish package releases on behalf of another owner", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
makePackageDoc({
|
|
ownerUserId: "users:openclaw",
|
|
ownerPublisherId: "publishers:openclaw",
|
|
channel: "official",
|
|
isOfficial: true,
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
[],
|
|
{
|
|
"users:admin": {
|
|
_id: "users:admin",
|
|
role: "admin",
|
|
trustedPublisher: false,
|
|
},
|
|
"users:openclaw": {
|
|
_id: "users:openclaw",
|
|
role: "user",
|
|
},
|
|
"publishers:openclaw": {
|
|
_id: "publishers:openclaw",
|
|
kind: "org",
|
|
handle: "openclaw",
|
|
displayName: "OpenClaw",
|
|
},
|
|
},
|
|
);
|
|
|
|
await insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:admin",
|
|
ownerUserId: "users:openclaw",
|
|
ownerPublisherId: "publishers:openclaw",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.1.0",
|
|
changelog: "promote",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
channel: "official",
|
|
});
|
|
|
|
expect(ctx.insert).toHaveBeenCalledWith(
|
|
"packageReleases",
|
|
expect.objectContaining({
|
|
createdBy: "users:admin",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("rejects non-admin publishes on behalf of another owner", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
makePackageDoc({
|
|
ownerUserId: "users:openclaw",
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
[],
|
|
{
|
|
"users:owner": {
|
|
_id: "users:owner",
|
|
role: "user",
|
|
trustedPublisher: false,
|
|
},
|
|
"users:openclaw": {
|
|
_id: "users:openclaw",
|
|
role: "user",
|
|
trustedPublisher: true,
|
|
},
|
|
},
|
|
);
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:openclaw",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.1.0",
|
|
changelog: "promote",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
}),
|
|
).rejects.toThrow("Forbidden");
|
|
});
|
|
|
|
it("rejects publishing the same package name across different publishers", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
makePackageDoc({
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: undefined,
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
[],
|
|
{
|
|
"users:owner": {
|
|
_id: "users:owner",
|
|
role: "user",
|
|
trustedPublisher: false,
|
|
},
|
|
"publishers:org": {
|
|
_id: "publishers:org",
|
|
kind: "org",
|
|
handle: "acme",
|
|
displayName: "Acme",
|
|
trustedPublisher: false,
|
|
},
|
|
},
|
|
);
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:org",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.1.0",
|
|
changelog: "org release",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
}),
|
|
).rejects.toThrow("Package already exists and belongs to another publisher");
|
|
});
|
|
|
|
it("treats a legacy personal package as the same personal publisher", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
makePackageDoc({
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: undefined,
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
[],
|
|
{
|
|
"users:owner": {
|
|
_id: "users:owner",
|
|
role: "user",
|
|
trustedPublisher: false,
|
|
},
|
|
"publishers:owner": {
|
|
_id: "publishers:owner",
|
|
kind: "user",
|
|
handle: "owner",
|
|
displayName: "Owner",
|
|
linkedUserId: "users:owner",
|
|
trustedPublisher: false,
|
|
},
|
|
},
|
|
);
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.1.0",
|
|
changelog: "personal release",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
}),
|
|
).resolves.toMatchObject({ ok: true, packageId: "packages:demo" });
|
|
|
|
expect(ctx.patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not overwrite latest package metadata for non-latest releases", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
makePackageDoc({
|
|
tags: { latest: "packageReleases:demo-1" },
|
|
latestReleaseId: "packageReleases:demo-1",
|
|
latestVersionSummary: {
|
|
version: "1.0.0",
|
|
createdAt: 1,
|
|
changelog: "latest",
|
|
compatibility: null,
|
|
verification: null,
|
|
artifact: null,
|
|
},
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
);
|
|
|
|
await insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "0.9.9",
|
|
changelog: "branch patch",
|
|
tags: ["legacy"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
});
|
|
|
|
const packagePatch = ctx.patch.mock.calls.find(([id]) => id === "packages:demo")?.[1];
|
|
expect(packagePatch).toEqual(
|
|
expect.objectContaining({
|
|
latestReleaseId: "packageReleases:demo-1",
|
|
latestVersionSummary: expect.objectContaining({ version: "1.0.0" }),
|
|
}),
|
|
);
|
|
expect(packagePatch).not.toHaveProperty("capabilityTags");
|
|
expect(packagePatch).not.toHaveProperty("capabilities");
|
|
expect(packagePatch).not.toHaveProperty("executesCode");
|
|
});
|
|
|
|
it("adds artifact summary for promoted ClawPack releases", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
makePackageDoc({
|
|
tags: { latest: "packageReleases:demo-1" },
|
|
latestReleaseId: "packageReleases:demo-1",
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
);
|
|
|
|
await insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.1.0",
|
|
changelog: "clawpack",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
sha256hash: "legacy-zip-sha",
|
|
artifactKind: "npm-pack",
|
|
clawpackStorageId: "storage:clawpack",
|
|
clawpackSha256: "a".repeat(64),
|
|
clawpackSize: 1024,
|
|
clawpackFormat: "tgz",
|
|
npmIntegrity: "sha512-demo",
|
|
npmShasum: "b".repeat(40),
|
|
npmTarballName: "demo-plugin-1.1.0.tgz",
|
|
});
|
|
|
|
expect(ctx.insert).toHaveBeenCalledWith(
|
|
"packageReleases",
|
|
expect.objectContaining({
|
|
artifactKind: "npm-pack",
|
|
clawpackStorageId: "storage:clawpack",
|
|
npmIntegrity: "sha512-demo",
|
|
}),
|
|
);
|
|
expect(ctx.patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
latestVersionSummary: expect.objectContaining({
|
|
artifact: expect.objectContaining({
|
|
kind: "npm-pack",
|
|
sha256: "a".repeat(64),
|
|
npmIntegrity: "sha512-demo",
|
|
npmShasum: "b".repeat(40),
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("uses the exact legacy ZIP hash in promoted legacy artifact summaries", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
makePackageDoc({
|
|
tags: { latest: "packageReleases:demo-1" },
|
|
latestReleaseId: "packageReleases:demo-1",
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
);
|
|
|
|
await insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.1.0",
|
|
changelog: "legacy zip",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "file-set-sha",
|
|
sha256hash: "legacy-zip-sha",
|
|
artifactKind: "legacy-zip",
|
|
});
|
|
|
|
expect(ctx.patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
latestVersionSummary: expect.objectContaining({
|
|
artifact: {
|
|
kind: "legacy-zip",
|
|
sha256: "legacy-zip-sha",
|
|
format: "zip",
|
|
},
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("keeps package summary pinned to the promoted release for non-latest publishes", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
makePackageDoc({
|
|
summary: "latest summary",
|
|
tags: { latest: "packageReleases:demo-1" },
|
|
latestReleaseId: "packageReleases:demo-1",
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
);
|
|
|
|
await insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "0.9.9",
|
|
changelog: "branch patch",
|
|
tags: ["legacy"],
|
|
summary: "legacy branch summary",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
});
|
|
|
|
expect(ctx.patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
summary: "latest summary",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("keeps runtimeId pinned to the promoted release for non-latest publishes", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
makePackageDoc({
|
|
family: "bundle-plugin",
|
|
runtimeId: "bundle.current",
|
|
tags: { latest: "packageReleases:demo-1" },
|
|
latestReleaseId: "packageReleases:demo-1",
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
);
|
|
|
|
await insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "bundle-plugin",
|
|
version: "0.9.9",
|
|
changelog: "legacy branch",
|
|
tags: ["legacy"],
|
|
summary: "legacy summary",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
runtimeId: "bundle.legacy",
|
|
});
|
|
|
|
expect(ctx.patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
runtimeId: "bundle.current",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("removes moved dist-tags from older package releases", async () => {
|
|
const olderRelease = makeReleaseDoc({
|
|
_id: "packageReleases:old",
|
|
version: "1.0.0",
|
|
distTags: ["latest", "stable"],
|
|
});
|
|
const ctx = makeInsertReleaseCtx(
|
|
makePackageDoc({
|
|
tags: { latest: "packageReleases:old", stable: "packageReleases:old" },
|
|
latestReleaseId: "packageReleases:old",
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
|
}),
|
|
[olderRelease],
|
|
);
|
|
|
|
await insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.1.0",
|
|
changelog: "promote",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
});
|
|
|
|
await flushScheduledPackageReleaseTagCleanup(ctx);
|
|
expect(ctx.patch).toHaveBeenCalledWith("packageReleases:old", {
|
|
distTags: ["stable"],
|
|
});
|
|
});
|
|
|
|
it("rejects an owner-deleted package version even when matching workflow retries are allowed", async () => {
|
|
const ctx = makeInsertReleaseCtx(makePackageDoc(), [
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:existing",
|
|
version: "1.0.0",
|
|
integritySha256: "abc123",
|
|
softDeletedAt: 123,
|
|
ownerDeletedAt: 123,
|
|
ownerDeletedBy: "users:owner",
|
|
}),
|
|
]);
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.0",
|
|
changelog: "retry",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
allowExistingRelease: true,
|
|
}),
|
|
).rejects.toThrow("Version 1.0.0 already exists. Increment the version number and try again.");
|
|
});
|
|
|
|
it("treats matching workflow duplicate package releases as idempotent", async () => {
|
|
const ctx = makeInsertReleaseCtx(makePackageDoc(), [
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:existing",
|
|
version: "1.0.0",
|
|
integritySha256: "abc123",
|
|
}),
|
|
]);
|
|
|
|
await expect(
|
|
insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.0",
|
|
changelog: "retry",
|
|
tags: ["latest"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
allowExistingRelease: true,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:existing",
|
|
});
|
|
|
|
expect(ctx.insert).not.toHaveBeenCalled();
|
|
expect(ctx.patch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps an initial beta-only package publish off latest", async () => {
|
|
const ctx = makeInsertReleaseCtx(
|
|
makePackageDoc({
|
|
latestReleaseId: undefined,
|
|
latestVersionSummary: undefined,
|
|
tags: {},
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 0 },
|
|
}),
|
|
);
|
|
|
|
await insertReleaseInternalHandler(ctx, {
|
|
actorUserId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.0",
|
|
changelog: "beta",
|
|
tags: ["beta"],
|
|
summary: "demo",
|
|
files: [],
|
|
integritySha256: "abc123",
|
|
verification: {
|
|
tier: "source-linked",
|
|
scope: "artifact-only",
|
|
scanStatus: "suspicious",
|
|
},
|
|
staticScan: {
|
|
status: "suspicious",
|
|
reasonCodes: ["suspicious.dynamic_code_execution"],
|
|
findings: [],
|
|
summary: "Detected: suspicious.dynamic_code_execution",
|
|
engineVersion: "test",
|
|
checkedAt: 123,
|
|
},
|
|
});
|
|
|
|
expect(ctx.insert).toHaveBeenCalledWith(
|
|
"packageReleases",
|
|
expect.objectContaining({
|
|
distTags: ["beta"],
|
|
verification: expect.objectContaining({ scanStatus: "suspicious" }),
|
|
staticScan: expect.objectContaining({ status: "suspicious" }),
|
|
}),
|
|
);
|
|
expect(ctx.patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
latestReleaseId: undefined,
|
|
tags: { beta: "packageReleases:new" },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("validates package publish payloads inside the action path", async () => {
|
|
await expect(
|
|
publishPackageForUserInternalHandler({} as never, {
|
|
actorUserId: "users:owner",
|
|
payload: {
|
|
name: "demo-plugin",
|
|
family: "bundle-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: "invalid",
|
|
},
|
|
}),
|
|
).rejects.toThrow(/Package publish payload/i);
|
|
});
|
|
|
|
it("rejects skill publishes on the package endpoint", async () => {
|
|
await expect(
|
|
publishPackageForUserInternalHandler({} as never, {
|
|
actorUserId: "users:owner",
|
|
payload: {
|
|
name: "demo-skill",
|
|
family: "skill",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
files: [],
|
|
},
|
|
}),
|
|
).rejects.toThrow("Skill packages must use the skills publish flow");
|
|
});
|
|
|
|
it("rejects Claw publication before mutation when the experimental gate is disabled", async () => {
|
|
const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
try {
|
|
await expect(
|
|
publishPackageForUserInternalHandler({} as never, {
|
|
actorUserId: "users:owner",
|
|
payload: {
|
|
name: "demo-claw",
|
|
family: "claw",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
files: [],
|
|
},
|
|
}),
|
|
).rejects.toThrow("Experimental Claw publication is disabled");
|
|
} finally {
|
|
if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous;
|
|
}
|
|
});
|
|
|
|
it("rechecks the Claw gate at release insertion for staged publications", async () => {
|
|
const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
const dbGet = vi.fn();
|
|
try {
|
|
await expect(
|
|
insertReleaseInternalHandler(
|
|
{
|
|
db: {
|
|
system: {},
|
|
get: dbGet,
|
|
insert: vi.fn(),
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
query: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
} as never,
|
|
{
|
|
family: "claw",
|
|
} as never,
|
|
),
|
|
).rejects.toThrow("Experimental Claw publication is disabled");
|
|
expect(dbGet).not.toHaveBeenCalled();
|
|
} finally {
|
|
if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous;
|
|
}
|
|
});
|
|
|
|
it("publishes a profile-bearing Claw through the existing release pipeline when enabled", async () => {
|
|
const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
process.env.CLAWHUB_EXPERIMENTAL_CLAWS = "1";
|
|
const longClawDescription = "x".repeat(1_100);
|
|
const storedFiles = new Map<string, string>([
|
|
[
|
|
"storage:package",
|
|
JSON.stringify({
|
|
name: "demo-claw",
|
|
version: "1.0.0",
|
|
openclaw: { claw: "manifests/CLAW.md" },
|
|
}),
|
|
],
|
|
[
|
|
"storage:claw",
|
|
`---\nschemaVersion: 1\nagent:\n id: demo-claw\n name: Demo Claw\n description: ${longClawDescription}\nmetadata:\n openclaw.config: profiles/openclaw.yml\n---\nRun the demo workflow precisely.\n`,
|
|
],
|
|
["storage:profile", "schemaVersion: 1\nagent:\n tools:\n profile: coding\n"],
|
|
]);
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if (args.minimumRole === "publisher") {
|
|
return { publisherId: "publishers:owner", linkedUserId: "users:owner" };
|
|
}
|
|
if (args.family === "claw") {
|
|
return { ok: true, packageId: "packages:claw", releaseId: "releases:claw-1" };
|
|
}
|
|
return null;
|
|
});
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "publishers:owner",
|
|
kind: "user",
|
|
handle: "owner",
|
|
linkedUserId: "users:owner",
|
|
}),
|
|
runMutation,
|
|
scheduler: { runAfter: vi.fn() },
|
|
storage: {
|
|
get: vi.fn(async (storageId: string) => {
|
|
const content = storedFiles.get(storageId);
|
|
return content === undefined ? null : new Blob([content]);
|
|
}),
|
|
store: vi.fn(async () => "storage:legacy-zip"),
|
|
},
|
|
};
|
|
|
|
try {
|
|
await expect(
|
|
publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:owner",
|
|
payload: {
|
|
name: "demo-claw",
|
|
displayName: "Demo Claw",
|
|
family: "claw",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
files: [
|
|
{ path: "package.json", size: 1, storageId: "storage:package", sha256: "package" },
|
|
{
|
|
path: "manifests/CLAW.md",
|
|
size: 1,
|
|
storageId: "storage:claw",
|
|
sha256: "claw",
|
|
},
|
|
{
|
|
path: "profiles/openclaw.yml",
|
|
size: 1,
|
|
storageId: "storage:profile",
|
|
sha256: "profile",
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
).resolves.toEqual({
|
|
ok: true,
|
|
packageId: "packages:claw",
|
|
releaseId: "releases:claw-1",
|
|
publicationStatus: "published",
|
|
});
|
|
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
family: "claw",
|
|
summary: expect.any(String),
|
|
artifactKind: "legacy-zip",
|
|
clawpackStorageId: "storage:legacy-zip",
|
|
clawpackSha256: expect.stringMatching(/^[a-f0-9]{64}$/),
|
|
clawpackSize: expect.any(Number),
|
|
clawManifestSummary: expect.objectContaining({
|
|
agent: expect.objectContaining({
|
|
id: "demo-claw",
|
|
description: "x".repeat(1_024),
|
|
}),
|
|
workspace: expect.objectContaining({ bootstrapFiles: ["SOUL.md"] }),
|
|
}),
|
|
pluginManifestSummary: undefined,
|
|
}),
|
|
);
|
|
const publishMutationArgs = runMutation.mock.calls.find(
|
|
([, args]) => args.family === "claw",
|
|
)?.[1];
|
|
expect(publishMutationArgs?.summary).toBe("x".repeat(1_024));
|
|
expect(publishMutationArgs).not.toHaveProperty("extractedClawManifest");
|
|
} finally {
|
|
if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous;
|
|
}
|
|
});
|
|
|
|
it("keeps raw package publishes behind the per-file size limit", async () => {
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce(null),
|
|
runMutation: vi.fn(),
|
|
storage: {
|
|
get: vi.fn(async (storageId: string) => {
|
|
if (storageId !== "storage:large") return null;
|
|
return new Blob([new Uint8Array(MAX_PUBLISH_FILE_BYTES + 1)]);
|
|
}),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:owner",
|
|
payload: {
|
|
name: "demo-plugin",
|
|
family: "bundle-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [
|
|
{
|
|
path: "assets/viewer-runtime.js",
|
|
size: 1,
|
|
storageId: "storage:large",
|
|
sha256: "large",
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
).rejects.toThrow('File "assets/viewer-runtime.js" exceeds 10MB limit');
|
|
});
|
|
|
|
it("allows large files inside ClawPack npm package artifacts", async () => {
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if (args.name === "demo-plugin" && args.version === "1.0.0") {
|
|
return { ok: true, packageId: "packages:demo", releaseId: "releases:demo-1" };
|
|
}
|
|
return null;
|
|
});
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce(null),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(async () => {}),
|
|
},
|
|
storage: {
|
|
get: vi.fn(async (storageId: string) => {
|
|
const files = new Map<string, BlobPart>([
|
|
[
|
|
"storage:package",
|
|
JSON.stringify({
|
|
name: "demo-plugin",
|
|
version: "1.0.0",
|
|
openclaw: {
|
|
extensions: ["./dist/index.js"],
|
|
compat: { pluginApi: "^1.0.0" },
|
|
build: { openclawVersion: "2026.5.28" },
|
|
configSchema: { type: "object", additionalProperties: false },
|
|
},
|
|
}),
|
|
],
|
|
["storage:manifest", JSON.stringify({ id: "demo-plugin" })],
|
|
["storage:runtime", "export {};"],
|
|
["storage:large", new Uint8Array(MAX_PUBLISH_FILE_BYTES + 1)],
|
|
]);
|
|
const content = files.get(storageId);
|
|
return content ? new Blob([content]) : null;
|
|
}),
|
|
store: vi.fn(async () => "storage:legacy-zip"),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:owner",
|
|
payload: {
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
source: {
|
|
kind: "github",
|
|
url: "https://github.com/openclaw/demo-plugin",
|
|
repo: "openclaw/demo-plugin",
|
|
ref: "refs/tags/v1.0.0",
|
|
commit: "abc123",
|
|
path: ".",
|
|
importedAt: Date.now(),
|
|
},
|
|
files: [
|
|
{
|
|
path: "package.json",
|
|
size: 1,
|
|
storageId: "storage:package",
|
|
sha256: "package",
|
|
contentType: "application/json",
|
|
},
|
|
{
|
|
path: "openclaw.plugin.json",
|
|
size: 1,
|
|
storageId: "storage:manifest",
|
|
sha256: "manifest",
|
|
contentType: "application/json",
|
|
},
|
|
{
|
|
path: "dist/index.js",
|
|
size: 1,
|
|
storageId: "storage:runtime",
|
|
sha256: "runtime",
|
|
contentType: "application/javascript",
|
|
},
|
|
{
|
|
path: "assets/viewer-runtime.js",
|
|
size: MAX_PUBLISH_FILE_BYTES + 1,
|
|
storageId: "storage:large",
|
|
sha256: "large",
|
|
contentType: "application/javascript",
|
|
},
|
|
],
|
|
artifact: {
|
|
kind: "npm-pack",
|
|
storageId: "storage:clawpack",
|
|
sha256: "clawpack",
|
|
size: MAX_PUBLISH_FILE_BYTES + 1,
|
|
format: "tgz",
|
|
npmIntegrity: "sha512-test",
|
|
npmShasum: "shasum",
|
|
npmTarballName: "demo-plugin-1.0.0.tgz",
|
|
npmUnpackedSize: MAX_PUBLISH_FILE_BYTES + 1,
|
|
npmFileCount: 4,
|
|
},
|
|
},
|
|
}),
|
|
).resolves.toEqual({
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseId: "releases:demo-1",
|
|
publicationStatus: "published",
|
|
});
|
|
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
artifactKind: "npm-pack",
|
|
files: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
path: "assets/viewer-runtime.js",
|
|
size: MAX_PUBLISH_FILE_BYTES + 1,
|
|
}),
|
|
]),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("rejects trusted publish tokens after trusted publisher rotation or deletion", async () => {
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packagePublishTokens:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: "clawhub-release",
|
|
version: "1.0.0",
|
|
sha: "abc123",
|
|
ref: "refs/heads/main",
|
|
runId: "100",
|
|
runAttempt: "1",
|
|
expiresAt: Date.now() + 60_000,
|
|
})
|
|
.mockResolvedValueOnce(null),
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForTrustedPublisherInternalHandler(ctx as never, {
|
|
publishTokenId: "packagePublishTokens:1",
|
|
payload: {
|
|
name: "demo-plugin",
|
|
family: "bundle-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [],
|
|
},
|
|
}),
|
|
).rejects.toThrow(
|
|
"Trusted publish token no longer matches the current package trusted publisher",
|
|
);
|
|
});
|
|
|
|
it("revokes trusted publish tokens after a successful publish", async () => {
|
|
const runMutation = vi.fn(async (_ref: unknown, args: unknown) => {
|
|
if (
|
|
typeof args === "object" &&
|
|
args !== null &&
|
|
"name" in args &&
|
|
"version" in args &&
|
|
"files" in args
|
|
) {
|
|
return {
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-2",
|
|
};
|
|
}
|
|
return null;
|
|
});
|
|
const trustedPublisher = {
|
|
_id: "packageTrustedPublishers:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: "clawhub-release",
|
|
};
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packagePublishTokens:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: "clawhub-release",
|
|
version: "1.0.0",
|
|
sha: "abc123",
|
|
ref: "refs/heads/main",
|
|
runId: "100",
|
|
runAttempt: "1",
|
|
expiresAt: Date.now() + 60_000,
|
|
})
|
|
.mockResolvedValueOnce(trustedPublisher)
|
|
.mockResolvedValueOnce(makePackageDoc({ family: "bundle-plugin" }))
|
|
.mockResolvedValueOnce(trustedPublisher)
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce(null),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: makePackageManifestStorage(),
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForTrustedPublisherInternalHandler(ctx as never, {
|
|
publishTokenId: "packagePublishTokens:1",
|
|
payload: {
|
|
name: "demo-plugin",
|
|
family: "bundle-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [packageManifestFile],
|
|
},
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-2",
|
|
});
|
|
|
|
expect(runMutation).toHaveBeenCalledWith(expect.anything(), {
|
|
tokenId: "packagePublishTokens:1",
|
|
});
|
|
});
|
|
|
|
it("revokes trusted publish tokens when a staged publish is accepted for checks", async () => {
|
|
const previousFlag = process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES;
|
|
process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES = "1";
|
|
const runMutation = vi.fn(async (_ref: unknown, args: unknown) => {
|
|
if (
|
|
typeof args === "object" &&
|
|
args !== null &&
|
|
"publicationStatus" in args &&
|
|
(args as { publicationStatus?: string }).publicationStatus === "pending"
|
|
) {
|
|
return {
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:pending",
|
|
publicationStatus: "pending",
|
|
createdNewParent: false,
|
|
};
|
|
}
|
|
if (
|
|
typeof args === "object" &&
|
|
args !== null &&
|
|
"packageReleaseId" in args &&
|
|
"packageFollowup" in args
|
|
) {
|
|
return {
|
|
attemptId: "publishAttempts:demo",
|
|
status: "pending_checks",
|
|
};
|
|
}
|
|
return null;
|
|
});
|
|
const trustedPublisher = {
|
|
_id: "packageTrustedPublishers:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: "clawhub-release",
|
|
};
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packagePublishTokens:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: "clawhub-release",
|
|
version: "1.0.0",
|
|
sha: "abc123",
|
|
ref: "refs/heads/main",
|
|
runId: "100",
|
|
runAttempt: "1",
|
|
expiresAt: Date.now() + 60_000,
|
|
})
|
|
.mockResolvedValueOnce(trustedPublisher)
|
|
.mockResolvedValueOnce(makePackageDoc({ family: "bundle-plugin" }))
|
|
.mockResolvedValueOnce(trustedPublisher)
|
|
.mockResolvedValueOnce(null),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: makePackageManifestStorage(),
|
|
};
|
|
|
|
try {
|
|
await expect(
|
|
publishPackageForTrustedPublisherInternalHandler(ctx as never, {
|
|
publishTokenId: "packagePublishTokens:1",
|
|
payload: {
|
|
name: "demo-plugin",
|
|
family: "bundle-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [packageManifestFile],
|
|
},
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
status: "pending",
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:pending",
|
|
publicationStatus: "pending",
|
|
attemptId: "publishAttempts:demo",
|
|
packageName: "demo-plugin",
|
|
version: "1.0.0",
|
|
});
|
|
} finally {
|
|
if (previousFlag === undefined) {
|
|
delete process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES;
|
|
} else {
|
|
process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES = previousFlag;
|
|
}
|
|
}
|
|
|
|
expect(runMutation).toHaveBeenCalledWith(expect.anything(), {
|
|
tokenId: "packagePublishTokens:1",
|
|
});
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
packageId: "packages:demo",
|
|
packageReleaseId: "packageReleases:pending",
|
|
scanContext: expect.any(Object),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("cleans up orphan pending package releases instead of wedging future retries", async () => {
|
|
const previousFlag = process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES;
|
|
process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES = "1";
|
|
const runMutation = vi.fn(async (_ref: unknown, args: unknown) => {
|
|
if (
|
|
typeof args === "object" &&
|
|
args !== null &&
|
|
"publicationStatus" in args &&
|
|
(args as { publicationStatus?: string }).publicationStatus === "pending"
|
|
) {
|
|
throw new Error("orphan recovery should not insert a second pending release");
|
|
}
|
|
if (
|
|
typeof args === "object" &&
|
|
args !== null &&
|
|
"packageReleaseId" in args &&
|
|
"packageFollowup" in args
|
|
) {
|
|
throw new Error("orphan cleanup should reject before creating a publish attempt");
|
|
}
|
|
if (
|
|
typeof args === "object" &&
|
|
args !== null &&
|
|
"releaseId" in args &&
|
|
"createdNewParent" in args
|
|
) {
|
|
return { deleted: true, parentDeleted: true };
|
|
}
|
|
return null;
|
|
});
|
|
const trustedPublisher = {
|
|
_id: "packageTrustedPublishers:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: "clawhub-release",
|
|
};
|
|
const orphanParent = makePackageDoc({
|
|
latestReleaseId: undefined,
|
|
latestVersionSummary: undefined,
|
|
stats: { downloads: 0, installs: 0, stars: 0, versions: 0 },
|
|
});
|
|
const orphanRelease = makeReleaseDoc({
|
|
_id: "packageReleases:orphan",
|
|
packageId: "packages:demo",
|
|
integritySha256: "b8107c6a51a6a7554e20d2963348212b80bc816fb59492ee163792103a6b7df6",
|
|
publicationStatus: "pending",
|
|
});
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packagePublishTokens:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: "clawhub-release",
|
|
version: "1.0.0",
|
|
sha: "abc123",
|
|
ref: "refs/heads/main",
|
|
runId: "100",
|
|
runAttempt: "1",
|
|
expiresAt: Date.now() + 60_000,
|
|
})
|
|
.mockResolvedValueOnce(trustedPublisher)
|
|
.mockResolvedValueOnce(orphanParent)
|
|
.mockResolvedValueOnce(trustedPublisher)
|
|
.mockResolvedValueOnce(orphanRelease)
|
|
.mockResolvedValueOnce(null),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: makePackageManifestStorage(),
|
|
};
|
|
|
|
try {
|
|
await expect(
|
|
publishPackageForTrustedPublisherInternalHandler(ctx as never, {
|
|
publishTokenId: "packagePublishTokens:1",
|
|
payload: {
|
|
name: "demo-plugin",
|
|
family: "bundle-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [packageManifestFile],
|
|
},
|
|
}),
|
|
).rejects.toThrow(
|
|
"Previous pending publish for 1.0.0 did not finish creating security checks. It was cleaned up; retry the publish.",
|
|
);
|
|
} finally {
|
|
if (previousFlag === undefined) {
|
|
delete process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES;
|
|
} else {
|
|
process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES = previousFlag;
|
|
}
|
|
}
|
|
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:orphan",
|
|
createdNewParent: true,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("cleans up pending package releases when staged publish attempt creation fails", async () => {
|
|
const previousFlag = process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES;
|
|
process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES = "1";
|
|
const runMutation = vi.fn(async (_ref: unknown, args: unknown) => {
|
|
if (
|
|
typeof args === "object" &&
|
|
args !== null &&
|
|
"publicationStatus" in args &&
|
|
(args as { publicationStatus?: string }).publicationStatus === "pending"
|
|
) {
|
|
return {
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:pending",
|
|
publicationStatus: "pending",
|
|
createdNewParent: false,
|
|
};
|
|
}
|
|
if (typeof args === "object" && args !== null && "packageReleaseId" in args) {
|
|
throw new Error("attempt creation outage");
|
|
}
|
|
if (
|
|
typeof args === "object" &&
|
|
args !== null &&
|
|
"releaseId" in args &&
|
|
"createdNewParent" in args
|
|
) {
|
|
return { deleted: true, parentDeleted: true };
|
|
}
|
|
return null;
|
|
});
|
|
const trustedPublisher = {
|
|
_id: "packageTrustedPublishers:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: "clawhub-release",
|
|
};
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packagePublishTokens:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: "clawhub-release",
|
|
version: "1.0.0",
|
|
sha: "abc123",
|
|
ref: "refs/heads/main",
|
|
runId: "100",
|
|
runAttempt: "1",
|
|
expiresAt: Date.now() + 60_000,
|
|
})
|
|
.mockResolvedValueOnce(trustedPublisher)
|
|
.mockResolvedValueOnce(makePackageDoc({ family: "bundle-plugin" }))
|
|
.mockResolvedValueOnce(trustedPublisher)
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce(null),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: makePackageManifestStorage(),
|
|
};
|
|
|
|
try {
|
|
await expect(
|
|
publishPackageForTrustedPublisherInternalHandler(ctx as never, {
|
|
publishTokenId: "packagePublishTokens:1",
|
|
payload: {
|
|
name: "demo-plugin",
|
|
family: "bundle-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [packageManifestFile],
|
|
},
|
|
}),
|
|
).rejects.toThrow("attempt creation outage");
|
|
} finally {
|
|
if (previousFlag === undefined) {
|
|
delete process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES;
|
|
} else {
|
|
process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES = previousFlag;
|
|
}
|
|
}
|
|
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:pending",
|
|
createdNewParent: false,
|
|
}),
|
|
);
|
|
expect(runMutation).not.toHaveBeenCalledWith(expect.anything(), {
|
|
tokenId: "packagePublishTokens:1",
|
|
});
|
|
});
|
|
|
|
it("rejects duplicate staged package releases before creating a publish attempt", async () => {
|
|
const previousFlag = process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES;
|
|
process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES = "1";
|
|
const runMutation = vi.fn(async () => {
|
|
throw new Error("duplicate publish should not create an attempt");
|
|
});
|
|
const trustedPublisher = {
|
|
_id: "packageTrustedPublishers:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: "clawhub-release",
|
|
};
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packagePublishTokens:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: "clawhub-release",
|
|
version: "1.0.0",
|
|
sha: "abc123",
|
|
ref: "refs/heads/main",
|
|
runId: "100",
|
|
runAttempt: "1",
|
|
expiresAt: Date.now() + 60_000,
|
|
})
|
|
.mockResolvedValueOnce(trustedPublisher)
|
|
.mockResolvedValueOnce(makePackageDoc({ family: "bundle-plugin" }))
|
|
.mockResolvedValueOnce(trustedPublisher)
|
|
.mockResolvedValueOnce(
|
|
makeReleaseDoc({
|
|
integritySha256: "different-existing-artifact",
|
|
}),
|
|
),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: makePackageManifestStorage(),
|
|
};
|
|
|
|
try {
|
|
await expect(
|
|
publishPackageForTrustedPublisherInternalHandler(ctx as never, {
|
|
publishTokenId: "packagePublishTokens:1",
|
|
payload: {
|
|
name: "demo-plugin",
|
|
family: "bundle-plugin",
|
|
version: "1.0.0",
|
|
changelog: "duplicate",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [packageManifestFile],
|
|
},
|
|
}),
|
|
).rejects.toThrow(
|
|
"Version 1.0.0 already exists. Increment the version number and try again.",
|
|
);
|
|
} finally {
|
|
if (previousFlag === undefined) {
|
|
delete process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES;
|
|
} else {
|
|
process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES = previousFlag;
|
|
}
|
|
}
|
|
|
|
expect(runMutation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects staged package releases reserved by a retained publish attempt", async () => {
|
|
const previousFlag = process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES;
|
|
process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES = "1";
|
|
const runMutation = vi.fn(async () => {
|
|
throw new Error("duplicate publish should not create an attempt");
|
|
});
|
|
const trustedPublisher = {
|
|
_id: "packageTrustedPublishers:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: "clawhub-release",
|
|
};
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packagePublishTokens:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: "clawhub-release",
|
|
version: "1.0.0",
|
|
sha: "abc123",
|
|
ref: "refs/heads/main",
|
|
runId: "100",
|
|
runAttempt: "1",
|
|
expiresAt: Date.now() + 60_000,
|
|
})
|
|
.mockResolvedValueOnce(trustedPublisher)
|
|
.mockResolvedValueOnce(makePackageDoc({ family: "bundle-plugin" }))
|
|
.mockResolvedValueOnce(trustedPublisher)
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
attemptId: "publishAttempts:secret-blocked",
|
|
status: "blocked",
|
|
}),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: makePackageManifestStorage(),
|
|
};
|
|
|
|
try {
|
|
await expect(
|
|
publishPackageForTrustedPublisherInternalHandler(ctx as never, {
|
|
publishTokenId: "packagePublishTokens:1",
|
|
payload: {
|
|
name: "demo-plugin",
|
|
family: "bundle-plugin",
|
|
version: "1.0.0",
|
|
changelog: "duplicate",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [packageManifestFile],
|
|
},
|
|
}),
|
|
).rejects.toThrow(
|
|
"Version 1.0.0 already exists. Increment the version number and try again.",
|
|
);
|
|
} finally {
|
|
if (previousFlag === undefined) {
|
|
delete process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES;
|
|
} else {
|
|
process.env.CLAWHUB_STAGED_PREPUBLICATION_PUBLISHES = previousFlag;
|
|
}
|
|
}
|
|
|
|
expect(runMutation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("accepts trusted publish tokens when no environment is pinned", async () => {
|
|
const runMutation = vi.fn(async (_ref: unknown, args: unknown) => {
|
|
if (
|
|
typeof args === "object" &&
|
|
args !== null &&
|
|
"name" in args &&
|
|
"version" in args &&
|
|
"files" in args
|
|
) {
|
|
return {
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-2",
|
|
};
|
|
}
|
|
return null;
|
|
});
|
|
const trustedPublisher = {
|
|
_id: "packageTrustedPublishers:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
};
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packagePublishTokens:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
version: "1.0.0",
|
|
sha: "abc123",
|
|
ref: "refs/heads/main",
|
|
runId: "100",
|
|
runAttempt: "1",
|
|
expiresAt: Date.now() + 60_000,
|
|
})
|
|
.mockResolvedValueOnce(trustedPublisher)
|
|
.mockResolvedValueOnce(makePackageDoc({ family: "bundle-plugin" }))
|
|
.mockResolvedValueOnce(trustedPublisher)
|
|
.mockResolvedValueOnce(null),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: makePackageManifestStorage(),
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForTrustedPublisherInternalHandler(ctx as never, {
|
|
publishTokenId: "packagePublishTokens:1",
|
|
payload: {
|
|
name: "demo-plugin",
|
|
family: "bundle-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [packageManifestFile],
|
|
},
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-2",
|
|
});
|
|
});
|
|
|
|
it("requires manual override for user-auth publishes when trusted publisher config exists", async () => {
|
|
const runMutation = vi.fn(async (_ref: unknown, args: unknown) => {
|
|
if (
|
|
typeof args === "object" &&
|
|
args !== null &&
|
|
"actorUserId" in args &&
|
|
"minimumRole" in args
|
|
) {
|
|
return null;
|
|
}
|
|
if (
|
|
typeof args === "object" &&
|
|
args !== null &&
|
|
"name" in args &&
|
|
"version" in args &&
|
|
"files" in args
|
|
) {
|
|
return {
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-2",
|
|
};
|
|
}
|
|
return null;
|
|
});
|
|
const trustedPublisher = {
|
|
_id: "packageTrustedPublishers:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: "clawhub-release",
|
|
};
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(makePackageDoc({ family: "bundle-plugin" }))
|
|
.mockResolvedValueOnce(trustedPublisher)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce(null),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: {
|
|
get: vi.fn(),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:owner",
|
|
payload: {
|
|
name: "demo-plugin",
|
|
family: "bundle-plugin",
|
|
version: "1.0.0",
|
|
changelog: "tag publish",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
source: {
|
|
kind: "github",
|
|
url: "https://github.com/openclaw/openclaw",
|
|
repo: "openclaw/openclaw",
|
|
ref: "refs/tags/plugins-2026.4.1-beta.1",
|
|
commit: "abc123",
|
|
path: "extensions/discord",
|
|
importedAt: Date.now(),
|
|
},
|
|
files: [],
|
|
},
|
|
}),
|
|
).rejects.toThrow(
|
|
"Manual publishes for packages with trusted publisher config require manualOverrideReason",
|
|
);
|
|
});
|
|
|
|
it("lets admins user-publish trusted packages without a manual override reason", async () => {
|
|
const runMutation = vi.fn(async (_ref: unknown, args: unknown) => {
|
|
if (
|
|
typeof args === "object" &&
|
|
args !== null &&
|
|
"actorUserId" in args &&
|
|
"minimumRole" in args
|
|
) {
|
|
return { publisherId: "publishers:openclaw" };
|
|
}
|
|
return null;
|
|
});
|
|
const trustedPublisher = {
|
|
_id: "packageTrustedPublishers:1",
|
|
packageId: "packages:demo",
|
|
provider: "github-actions",
|
|
repository: "openclaw/openclaw",
|
|
repositoryId: "1",
|
|
repositoryOwner: "openclaw",
|
|
repositoryOwnerId: "2",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: "clawhub-release",
|
|
};
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(makePackageDoc({ family: "bundle-plugin" }))
|
|
.mockResolvedValueOnce(trustedPublisher)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:admin",
|
|
role: "admin",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:admin",
|
|
role: "admin",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce(null),
|
|
runMutation,
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: {
|
|
get: vi.fn(),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:admin",
|
|
payload: {
|
|
name: "@openclaw/discord",
|
|
family: "bundle-plugin",
|
|
version: "2026.5.3-beta.2",
|
|
changelog: "tag publish",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
source: {
|
|
kind: "github",
|
|
url: "https://github.com/openclaw/openclaw",
|
|
repo: "openclaw/openclaw",
|
|
ref: "refs/tags/plugins-2026.5.3-beta.2",
|
|
commit: "abc123",
|
|
path: "extensions/discord",
|
|
importedAt: Date.now(),
|
|
},
|
|
files: [],
|
|
},
|
|
}),
|
|
).rejects.toThrow("openclaw.plugin.json is required");
|
|
});
|
|
|
|
it("allows package names that match an unrelated skill slug", async () => {
|
|
const storedFiles = new Map<string, string>([
|
|
[
|
|
"storage:package",
|
|
JSON.stringify({
|
|
name: "shared-slug",
|
|
openclaw: {
|
|
extensions: ["./dist/index.js"],
|
|
hostTargets: ["darwin-arm64", "linux-x64"],
|
|
environment: {},
|
|
compat: { pluginApi: "^1.0.0" },
|
|
build: { openclawVersion: "2026.3.14" },
|
|
configSchema: { type: "object" },
|
|
},
|
|
}),
|
|
],
|
|
["storage:manifest", JSON.stringify({ id: "shared.plugin" })],
|
|
["storage:code", "export default {};"],
|
|
]);
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if (args.minimumRole === "publisher") {
|
|
return { publisherId: "publishers:owner", linkedUserId: "users:owner" };
|
|
}
|
|
if ("name" in args && "version" in args && "files" in args) {
|
|
return { ok: true, packageId: "packages:demo", releaseId: "releases:demo-1" };
|
|
}
|
|
return null;
|
|
});
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "publishers:owner",
|
|
kind: "user",
|
|
handle: "owner",
|
|
linkedUserId: "users:owner",
|
|
}),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: {
|
|
get: vi.fn(async (storageId: string) => {
|
|
const content = storedFiles.get(storageId);
|
|
return content ? new Blob([content]) : null;
|
|
}),
|
|
store: vi.fn(async () => "storage:legacy-zip"),
|
|
},
|
|
};
|
|
|
|
const result = await publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:owner",
|
|
payload: {
|
|
name: "shared-slug",
|
|
displayName: "Shared Slug Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
source: {
|
|
kind: "github",
|
|
url: "https://github.com/openclaw/shared-slug",
|
|
repo: "openclaw/shared-slug",
|
|
ref: "refs/tags/v1.0.0",
|
|
commit: "abc123",
|
|
path: ".",
|
|
importedAt: Date.now(),
|
|
},
|
|
files: [
|
|
{
|
|
path: "package.json",
|
|
size: 1,
|
|
storageId: "storage:package",
|
|
sha256: "package",
|
|
contentType: "application/json",
|
|
},
|
|
{
|
|
path: "openclaw.plugin.json",
|
|
size: 1,
|
|
storageId: "storage:manifest",
|
|
sha256: "manifest",
|
|
contentType: "application/json",
|
|
},
|
|
{
|
|
path: "dist/index.js",
|
|
size: 1,
|
|
storageId: "storage:code",
|
|
sha256: "code",
|
|
contentType: "application/javascript",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
packageId: "packages:demo",
|
|
releaseId: "releases:demo-1",
|
|
});
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
name: "shared-slug",
|
|
ownerPublisherId: "publishers:owner",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("forwards only valid HTTPS plugin manifest icons to release insertion", async () => {
|
|
async function publishWithManifestIcon(icon: unknown) {
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if (args.minimumRole === "publisher") {
|
|
return { publisherId: "publishers:owner", linkedUserId: "users:owner" };
|
|
}
|
|
if ("name" in args && "version" in args && "files" in args) {
|
|
return { ok: true, packageId: "packages:demo", releaseId: "releases:demo-1" };
|
|
}
|
|
return null;
|
|
});
|
|
const storedFiles = new Map<string, string>([
|
|
[
|
|
"storage:package",
|
|
JSON.stringify({
|
|
name: "demo-plugin",
|
|
openclaw: {
|
|
extensions: ["./dist/index.js"],
|
|
hostTargets: ["darwin-arm64", "linux-x64"],
|
|
environment: {},
|
|
compat: { pluginApi: "^1.0.0" },
|
|
build: { openclawVersion: "2026.3.14" },
|
|
configSchema: { type: "object" },
|
|
},
|
|
}),
|
|
],
|
|
["storage:manifest", JSON.stringify({ id: "demo.plugin", icon })],
|
|
["storage:code", "export default {};"],
|
|
]);
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "publishers:owner",
|
|
kind: "user",
|
|
handle: "owner",
|
|
linkedUserId: "users:owner",
|
|
}),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: {
|
|
get: vi.fn(async (storageId: string) => {
|
|
const content = storedFiles.get(storageId);
|
|
return content ? new Blob([content]) : null;
|
|
}),
|
|
store: vi.fn(async () => "storage:legacy-zip"),
|
|
},
|
|
};
|
|
|
|
await publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:owner",
|
|
payload: {
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
source: {
|
|
kind: "github",
|
|
url: "https://github.com/openclaw/demo-plugin",
|
|
repo: "openclaw/demo-plugin",
|
|
ref: "refs/tags/v1.0.0",
|
|
commit: "abc123",
|
|
path: ".",
|
|
importedAt: Date.now(),
|
|
},
|
|
files: [
|
|
{
|
|
path: "package.json",
|
|
size: 1,
|
|
storageId: "storage:package",
|
|
sha256: "package",
|
|
contentType: "application/json",
|
|
},
|
|
{
|
|
path: "openclaw.plugin.json",
|
|
size: 1,
|
|
storageId: "storage:manifest",
|
|
sha256: "manifest",
|
|
contentType: "application/json",
|
|
},
|
|
{
|
|
path: "dist/index.js",
|
|
size: 1,
|
|
storageId: "storage:code",
|
|
sha256: "code",
|
|
contentType: "application/javascript",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const insertCall = runMutation.mock.calls.find(
|
|
([, args]) =>
|
|
typeof args === "object" &&
|
|
args !== null &&
|
|
(args as { name?: string }).name === "demo-plugin" &&
|
|
(args as { version?: string }).version === "1.0.0",
|
|
);
|
|
return insertCall?.[1] as Record<string, unknown>;
|
|
}
|
|
|
|
await expect(
|
|
publishWithManifestIcon("https://cdn.example.test/icons/demo.svg"),
|
|
).resolves.toMatchObject({ icon: "https://cdn.example.test/icons/demo.svg" });
|
|
|
|
for (const icon of [
|
|
"http://cdn.example.test/icons/demo.svg",
|
|
"/icons/demo.svg",
|
|
"not a url",
|
|
"",
|
|
123,
|
|
{ src: "https://cdn.example.test/icons/demo.svg" },
|
|
]) {
|
|
await expect(publishWithManifestIcon(icon)).resolves.not.toHaveProperty("icon");
|
|
}
|
|
});
|
|
|
|
it("scans plugin publishes and forwards scan status to insertReleaseInternal", async () => {
|
|
const runMutation = vi.fn(async (_ref: unknown, args: unknown) => args);
|
|
const storedFiles = new Map<string, string>([
|
|
[
|
|
"storage:package",
|
|
JSON.stringify({
|
|
name: "demo-plugin",
|
|
keywords: [
|
|
"one",
|
|
"two",
|
|
"three",
|
|
"four",
|
|
"five",
|
|
"six",
|
|
"seven",
|
|
"eight",
|
|
"nine",
|
|
"a".repeat(33),
|
|
],
|
|
openclaw: {
|
|
extensions: ["./dist/index.js"],
|
|
hostTargets: ["darwin-arm64", "linux-x64"],
|
|
environment: {},
|
|
compat: { pluginApi: "^1.0.0" },
|
|
build: { openclawVersion: "2026.3.14" },
|
|
configSchema: { type: "object" },
|
|
},
|
|
}),
|
|
],
|
|
[
|
|
"storage:manifest",
|
|
JSON.stringify({
|
|
id: "demo.plugin",
|
|
categories: ["security"],
|
|
topics: ["Manifest Topic"],
|
|
contracts: { tools: ["demoTool"] },
|
|
}),
|
|
],
|
|
[
|
|
"storage:code",
|
|
"import { execSync } from 'node:child_process';\nexecSync('curl http://x');\n",
|
|
],
|
|
]);
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(makePackageDoc({ categories: ["retired-category"] }))
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce(null),
|
|
runMutation,
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: {
|
|
get: vi.fn(async (storageId: string) => {
|
|
const content = storedFiles.get(storageId);
|
|
return content ? new Blob([content]) : null;
|
|
}),
|
|
store: vi.fn(async () => "storage:legacy-zip"),
|
|
},
|
|
runAction: vi.fn(async () => ({
|
|
status: "pass",
|
|
summary: {
|
|
breakageCount: 0,
|
|
warningCount: 0,
|
|
deprecationWarningCount: 0,
|
|
issueCount: 0,
|
|
},
|
|
warnings: [],
|
|
})),
|
|
};
|
|
|
|
const result = (await publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:owner",
|
|
payload: {
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
source: {
|
|
kind: "github",
|
|
url: "https://github.com/openclaw/demo-plugin",
|
|
repo: "openclaw/demo-plugin",
|
|
ref: "refs/tags/v1.0.0",
|
|
commit: "abc123",
|
|
path: ".",
|
|
importedAt: Date.now(),
|
|
},
|
|
files: [
|
|
{
|
|
path: "package.json",
|
|
size: 1,
|
|
storageId: "storage:package",
|
|
sha256: "package",
|
|
contentType: "application/json",
|
|
},
|
|
{
|
|
path: "openclaw.plugin.json",
|
|
size: 1,
|
|
storageId: "storage:manifest",
|
|
sha256: "manifest",
|
|
contentType: "application/json",
|
|
},
|
|
{
|
|
path: "dist/index.js",
|
|
size: 1,
|
|
storageId: "storage:code",
|
|
sha256: "code",
|
|
contentType: "application/javascript",
|
|
},
|
|
],
|
|
},
|
|
})) as Record<string, unknown>;
|
|
const expectedLegacyZipSha256 = await sha256Hex(
|
|
buildDeterministicPackageZip([
|
|
{
|
|
path: "package.json",
|
|
bytes: new TextEncoder().encode(storedFiles.get("storage:package")),
|
|
},
|
|
{
|
|
path: "openclaw.plugin.json",
|
|
bytes: new TextEncoder().encode(storedFiles.get("storage:manifest")),
|
|
},
|
|
{
|
|
path: "dist/index.js",
|
|
bytes: new TextEncoder().encode(storedFiles.get("storage:code")),
|
|
},
|
|
]),
|
|
);
|
|
|
|
expect(runMutation).toHaveBeenCalled();
|
|
expect(result.categories).toEqual(["other"]);
|
|
expect(result.topics).toBeUndefined();
|
|
expect(result.sha256hash).toBe(expectedLegacyZipSha256);
|
|
expect(result.verification).toEqual(expect.objectContaining({ scanStatus: "pending" }));
|
|
expect(result.staticScan).toEqual(
|
|
expect.objectContaining({
|
|
status: "suspicious",
|
|
reasonCodes: expect.arrayContaining(["suspicious.dangerous_exec"]),
|
|
}),
|
|
);
|
|
expect(ctx.scheduler.runAfter).toHaveBeenNthCalledWith(
|
|
1,
|
|
30_000,
|
|
expect.anything(),
|
|
expect.any(Object),
|
|
);
|
|
});
|
|
|
|
it("blocks plugin publishes when plugin inspector reports hard breakages", async () => {
|
|
const runMutation = vi.fn(async (_ref: unknown, args: unknown) => {
|
|
if (typeof args === "object" && args !== null && "minimumRole" in args) return null;
|
|
return args;
|
|
});
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce(null),
|
|
runMutation,
|
|
runAction: vi.fn(async () => ({
|
|
status: "fail",
|
|
summary: {
|
|
breakageCount: 1,
|
|
warningCount: 0,
|
|
deprecationWarningCount: 0,
|
|
issueCount: 1,
|
|
},
|
|
warnings: [],
|
|
breakages: [
|
|
{
|
|
code: "missing-expected-seam",
|
|
severity: "P0",
|
|
message: "missing expected registration registerTool",
|
|
evidence: ["registerTool"],
|
|
},
|
|
],
|
|
})),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: {
|
|
get: vi.fn(async (storageId: string) => {
|
|
const files = new Map<string, string>([
|
|
[
|
|
"storage:package",
|
|
JSON.stringify({
|
|
name: "demo-plugin",
|
|
openclaw: {
|
|
extensions: ["./dist/index.js"],
|
|
compat: { pluginApi: "^1.0.0" },
|
|
build: { openclawVersion: "2026.3.14" },
|
|
configSchema: { type: "object" },
|
|
},
|
|
}),
|
|
],
|
|
["storage:manifest", JSON.stringify({ id: "demo.plugin" })],
|
|
["storage:code", "export const demo = true;\n"],
|
|
]);
|
|
const content = files.get(storageId);
|
|
return content ? new Blob([content]) : null;
|
|
}),
|
|
store: vi.fn(async () => "storage:legacy-zip"),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:owner",
|
|
payload: {
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
source: {
|
|
kind: "github",
|
|
url: "https://github.com/openclaw/demo-plugin",
|
|
repo: "openclaw/demo-plugin",
|
|
ref: "refs/tags/v1.0.0",
|
|
commit: "abc123",
|
|
path: ".",
|
|
importedAt: Date.now(),
|
|
},
|
|
files: [
|
|
{
|
|
path: "package.json",
|
|
size: 1,
|
|
storageId: "storage:package",
|
|
sha256: "package",
|
|
contentType: "application/json",
|
|
},
|
|
{
|
|
path: "openclaw.plugin.json",
|
|
size: 1,
|
|
storageId: "storage:manifest",
|
|
sha256: "manifest",
|
|
contentType: "application/json",
|
|
},
|
|
{
|
|
path: "dist/index.js",
|
|
size: 1,
|
|
storageId: "storage:code",
|
|
sha256: "code",
|
|
contentType: "application/javascript",
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
).rejects.toThrow(/Plugin Inspector blocked publish: 1 breakage/);
|
|
|
|
expect(runMutation).not.toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({ version: "1.0.0" }),
|
|
);
|
|
expect(ctx.scheduler.runAfter).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("stores plugin inspector warnings after successful warning-only publishes", async () => {
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if ("minimumRole" in args) return null;
|
|
if ("warnings" in args) return { ok: true, inserted: 1 };
|
|
return { ok: true, packageId: "packages:demo", releaseId: "packageReleases:demo-1" };
|
|
});
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:owner",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce(null),
|
|
runMutation,
|
|
runAction: vi.fn(async () => ({
|
|
status: "pass",
|
|
summary: {
|
|
breakageCount: 0,
|
|
warningCount: 1,
|
|
deprecationWarningCount: 1,
|
|
issueCount: 1,
|
|
},
|
|
warnings: [
|
|
{
|
|
id: "demo:legacy-before-agent-start",
|
|
code: "legacy-before-agent-start",
|
|
severity: "P2",
|
|
issueClass: "deprecation-warning",
|
|
compatStatus: "deprecated",
|
|
message: "legacy before_agent_start hook is deprecated",
|
|
evidence: ["src/index.ts:4"],
|
|
authorRemediation: {
|
|
summary: "Move prompt mutation work to before_prompt_build.",
|
|
docsUrl:
|
|
"https://docs.openclaw.ai/clawhub/plugin-validation-fixes#legacy-before-agent-start",
|
|
},
|
|
},
|
|
],
|
|
metadata: {
|
|
inspectorVersion: "0.3.19",
|
|
targetOpenClawVersion: "2026.7.1-2",
|
|
},
|
|
})),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: {
|
|
get: vi.fn(async (storageId: string) => {
|
|
const files = new Map<string, string>([
|
|
[
|
|
"storage:package",
|
|
JSON.stringify({
|
|
name: "demo-plugin",
|
|
openclaw: {
|
|
extensions: ["./dist/index.js"],
|
|
compat: { pluginApi: "^1.0.0" },
|
|
build: { openclawVersion: "2026.3.14" },
|
|
configSchema: { type: "object" },
|
|
},
|
|
}),
|
|
],
|
|
["storage:manifest", JSON.stringify({ id: "demo.plugin" })],
|
|
["storage:code", "export const demo = true;\n"],
|
|
]);
|
|
const content = files.get(storageId);
|
|
return content ? new Blob([content]) : null;
|
|
}),
|
|
store: vi.fn(async () => "storage:legacy-zip"),
|
|
},
|
|
};
|
|
|
|
await publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:owner",
|
|
payload: {
|
|
name: "demo-plugin",
|
|
displayName: "Demo Plugin",
|
|
family: "code-plugin",
|
|
version: "1.0.0",
|
|
changelog: "init",
|
|
source: {
|
|
kind: "github",
|
|
url: "https://github.com/openclaw/demo-plugin",
|
|
repo: "openclaw/demo-plugin",
|
|
ref: "refs/tags/v1.0.0",
|
|
commit: "abc123",
|
|
path: ".",
|
|
importedAt: Date.now(),
|
|
},
|
|
files: [
|
|
{
|
|
path: "package.json",
|
|
size: 1,
|
|
storageId: "storage:package",
|
|
sha256: "package",
|
|
contentType: "application/json",
|
|
},
|
|
{
|
|
path: "openclaw.plugin.json",
|
|
size: 1,
|
|
storageId: "storage:manifest",
|
|
sha256: "manifest",
|
|
contentType: "application/json",
|
|
},
|
|
{
|
|
path: "dist/index.js",
|
|
size: 1,
|
|
storageId: "storage:code",
|
|
sha256: "code",
|
|
contentType: "application/javascript",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const inspectorWarningCall = runMutation.mock.calls.find(
|
|
([, args]) =>
|
|
typeof args === "object" &&
|
|
args !== null &&
|
|
(args as { packageId?: string }).packageId === "packages:demo" &&
|
|
Array.isArray((args as { findings?: unknown[] }).findings),
|
|
);
|
|
expect(inspectorWarningCall?.[1]).toMatchObject({
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
packageName: "demo-plugin",
|
|
version: "1.0.0",
|
|
inspectorVersion: "0.3.19",
|
|
targetOpenClawVersion: "2026.7.1-2",
|
|
findings: [
|
|
expect.objectContaining({
|
|
code: "legacy-before-agent-start",
|
|
issueClass: "deprecation-warning",
|
|
message: "legacy before_agent_start hook is deprecated",
|
|
authorRemediation: {
|
|
summary: "Move prompt mutation work to before_prompt_build.",
|
|
docsUrl:
|
|
"https://docs.openclaw.ai/clawhub/plugin-validation-fixes#legacy-before-agent-start",
|
|
},
|
|
}),
|
|
],
|
|
});
|
|
});
|
|
|
|
it("deduplicates plugin inspector warnings when an idempotent publish retry returns an existing release", async () => {
|
|
const insert = vi.fn();
|
|
const collect = vi.fn(async () => [
|
|
{
|
|
inspectorFindingId: "demo:legacy-before-agent-start",
|
|
code: "legacy-before-agent-start",
|
|
message: "legacy before_agent_start hook is deprecated",
|
|
evidence: ["src/index.ts:4"],
|
|
fixture: "demo",
|
|
},
|
|
]);
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn(() => ({
|
|
withIndex: vi.fn(() => ({
|
|
collect,
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
})),
|
|
})),
|
|
insert,
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn((tableName: string, id: string) =>
|
|
id.startsWith(`${tableName}:`) ? id : null,
|
|
),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
insertPackageInspectorWarningsInternalHandler(ctx as never, {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
ownerUserId: "users:owner",
|
|
packageName: "demo-plugin",
|
|
version: "1.0.0",
|
|
warnings: [
|
|
{
|
|
id: "demo:legacy-before-agent-start",
|
|
code: "legacy-before-agent-start",
|
|
message: "legacy before_agent_start hook is deprecated",
|
|
evidence: ["src/index.ts:4"],
|
|
fixture: "demo",
|
|
},
|
|
{
|
|
id: "demo:manifest-name-missing",
|
|
code: "manifest-name-missing",
|
|
message: "openclaw.plugin.json does not declare a display name",
|
|
evidence: ["openclaw.plugin.json"],
|
|
fixture: "demo",
|
|
authorRemediation: {
|
|
summary: "Add a display name to the plugin manifest.",
|
|
docsUrl:
|
|
"https://docs.openclaw.ai/clawhub/plugin-validation-fixes#manifest-name-missing",
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
).resolves.toMatchObject({ ok: true, inserted: 1, shouldEmailOwner: false });
|
|
|
|
expect(insert).toHaveBeenCalledTimes(1);
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"packageInspectorWarnings",
|
|
expect.objectContaining({
|
|
code: "manifest-name-missing",
|
|
inspectorFindingId: "demo:manifest-name-missing",
|
|
authorRemediation: {
|
|
summary: "Add a display name to the plugin manifest.",
|
|
docsUrl: "https://docs.openclaw.ai/clawhub/plugin-validation-fixes#manifest-name-missing",
|
|
},
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("keeps the same plugin inspector finding when the inspector version changes", async () => {
|
|
const insert = vi.fn();
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn(() => ({
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([
|
|
{
|
|
inspectorFindingId: "demo:legacy-before-agent-start",
|
|
code: "legacy-before-agent-start",
|
|
message: "legacy before_agent_start hook is deprecated",
|
|
evidence: ["src/index.ts:4"],
|
|
fixture: "demo",
|
|
inspectorVersion: "0.4.0",
|
|
targetOpenClawVersion: "2026.4.0",
|
|
},
|
|
]),
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
})),
|
|
})),
|
|
insert,
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn((tableName: string, id: string) =>
|
|
id.startsWith(`${tableName}:`) ? id : null,
|
|
),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
insertPackageInspectorWarningsInternalHandler(ctx as never, {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
ownerUserId: "users:owner",
|
|
packageName: "demo-plugin",
|
|
version: "1.0.0",
|
|
scanSource: "nightly",
|
|
inspectorVersion: "0.5.0",
|
|
targetOpenClawVersion: "2026.5.0",
|
|
findings: [
|
|
{
|
|
id: "demo:legacy-before-agent-start",
|
|
code: "legacy-before-agent-start",
|
|
message: "legacy before_agent_start hook is deprecated",
|
|
evidence: ["src/index.ts:4"],
|
|
fixture: "demo",
|
|
authorRemediation: {
|
|
summary: "Move prompt mutation work to before_prompt_build.",
|
|
docsUrl:
|
|
"https://docs.openclaw.ai/clawhub/plugin-validation-fixes#legacy-before-agent-start",
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
).resolves.toMatchObject({ ok: true, inserted: 1, shouldEmailOwner: false });
|
|
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"packageInspectorWarnings",
|
|
expect.objectContaining({
|
|
code: "legacy-before-agent-start",
|
|
inspectorVersion: "0.5.0",
|
|
targetOpenClawVersion: "2026.5.0",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("replaces the current nightly beta findings while preserving publish findings", async () => {
|
|
const insert = vi.fn();
|
|
const deleteRow = vi.fn();
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageInspectorWarnings") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([
|
|
{
|
|
_id: "packageInspectorWarnings:static",
|
|
scanSource: "publish",
|
|
inspectorFindingId: "demo:manifest-name-missing",
|
|
code: "manifest-name-missing",
|
|
message: "manifest name is missing",
|
|
authorRemediation: { summary: "Add a manifest name." },
|
|
},
|
|
{
|
|
_id: "packageInspectorWarnings:old-beta",
|
|
scanSource: "nightly",
|
|
inspectorFindingId: "demo:old-beta-api",
|
|
code: "old-beta-api",
|
|
message: "old beta API is missing",
|
|
targetOpenClawVersion: "2026.7.0-beta.1",
|
|
authorRemediation: { summary: "Use the supported API." },
|
|
},
|
|
]),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageInspectorFindingNotifications") {
|
|
return {
|
|
withIndex: vi.fn(() => ({ unique: vi.fn().mockResolvedValue(null) })),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert,
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: deleteRow,
|
|
normalizeId: vi.fn((tableName: string, id: string) =>
|
|
id.startsWith(`${tableName}:`) ? id : null,
|
|
),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
insertPackageInspectorWarningsInternalHandler(ctx as never, {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
ownerUserId: "users:owner",
|
|
packageName: "demo-plugin",
|
|
version: "1.0.0",
|
|
scanSource: "nightly",
|
|
inspectorVersion: "0.6.0",
|
|
targetOpenClawVersion: "2026.8.0-beta.1",
|
|
findings: [
|
|
{
|
|
id: "demo:new-beta-api",
|
|
code: "new-beta-api",
|
|
level: "breakage",
|
|
message: "new beta API is missing",
|
|
authorRemediation: { summary: "Use the replacement API." },
|
|
},
|
|
],
|
|
}),
|
|
).resolves.toMatchObject({ ok: true, inserted: 1, shouldEmailOwner: false });
|
|
|
|
expect(deleteRow).toHaveBeenCalledTimes(1);
|
|
expect(deleteRow).toHaveBeenCalledWith("packageInspectorWarnings:old-beta");
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"packageInspectorWarnings",
|
|
expect.objectContaining({
|
|
scanSource: "nightly",
|
|
targetOpenClawVersion: "2026.8.0-beta.1",
|
|
code: "new-beta-api",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not notify for a warning-only nightly result with a preserved publish error", async () => {
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageInspectorWarnings") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([
|
|
{
|
|
_id: "packageInspectorWarnings:publish",
|
|
scanSource: "publish",
|
|
findingKind: "error",
|
|
code: "manifest-name-missing",
|
|
message: "manifest name is missing",
|
|
authorRemediation: { summary: "Add a manifest name." },
|
|
},
|
|
]),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageInspectorScanStates") {
|
|
return {
|
|
withIndex: vi.fn(() => ({ unique: vi.fn().mockResolvedValue(null) })),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert: vi.fn(),
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
insertPackageInspectorWarningsInternalHandler(ctx as never, {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
ownerUserId: "users:owner",
|
|
packageName: "demo-plugin",
|
|
version: "1.0.0",
|
|
scanSource: "nightly",
|
|
inspectorVersion: "0.6.0",
|
|
targetOpenClawVersion: "2026.8.0-beta.1",
|
|
notifyOwners: true,
|
|
findings: [
|
|
{
|
|
id: "demo:deprecated-api",
|
|
code: "deprecated-api",
|
|
level: "warning",
|
|
message: "The API is deprecated but still available.",
|
|
authorRemediation: { summary: "Migrate before a future release." },
|
|
},
|
|
],
|
|
}),
|
|
).resolves.toEqual({ ok: true, inserted: 1, shouldEmailOwner: false });
|
|
});
|
|
|
|
it("records a clean beta scan identity and completes a no-op notification", async () => {
|
|
const insert = vi.fn().mockResolvedValue("packageInspectorScanStates:demo");
|
|
const deleteRow = vi.fn();
|
|
const scanStateWithIndex = vi.fn((indexName: string) => ({
|
|
unique: vi.fn().mockResolvedValue(
|
|
indexName === "by_release"
|
|
? {
|
|
_id: "packageInspectorScanStates:previous",
|
|
inspectorVersion: "0.5.0",
|
|
targetOpenClawVersion: "2026.7.0-beta.1",
|
|
}
|
|
: null,
|
|
),
|
|
}));
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packages:demo") {
|
|
return {
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
ownerUserId: "users:owner",
|
|
};
|
|
}
|
|
if (id === "packageReleases:demo-1") {
|
|
return {
|
|
_id: "packageReleases:demo-1",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageInspectorWarnings") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([
|
|
{
|
|
_id: "packageInspectorWarnings:old-beta",
|
|
scanSource: "nightly",
|
|
code: "old-beta-api",
|
|
message: "old beta API is missing",
|
|
authorRemediation: { summary: "Use the supported API." },
|
|
},
|
|
]),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageInspectorScanStates") {
|
|
return { withIndex: scanStateWithIndex };
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert,
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: deleteRow,
|
|
normalizeId: vi.fn((tableName: string, id: string) =>
|
|
id.startsWith(`${tableName}:`) ? id : null,
|
|
),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
ingestPackageInspectorScanResultsInternalHandler(ctx as never, {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
inspectorVersion: "0.6.0",
|
|
targetOpenClawVersion: "2026.8.0-beta.1",
|
|
notifyOwners: true,
|
|
findings: [],
|
|
}),
|
|
).resolves.toEqual({ ok: true, inserted: 0, shouldEmailOwner: false });
|
|
|
|
expect(deleteRow).toHaveBeenCalledWith("packageInspectorWarnings:old-beta");
|
|
expect(scanStateWithIndex).toHaveBeenCalledWith(
|
|
"by_release_and_inspector_version_and_target_openclaw_version",
|
|
expect.any(Function),
|
|
);
|
|
expect(insert).toHaveBeenCalledWith("packageInspectorScanStates", {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
inspectorVersion: "0.6.0",
|
|
targetOpenClawVersion: "2026.8.0-beta.1",
|
|
completedAt: expect.any(Number),
|
|
notificationCompletedAt: expect.any(Number),
|
|
});
|
|
});
|
|
|
|
it("stores nightly errors and does not reuse an earlier target's email dedupe", async () => {
|
|
const insert = vi.fn();
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn((table: string) => ({
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
unique: vi
|
|
.fn()
|
|
.mockResolvedValue(
|
|
table === "packageInspectorFindingNotifications"
|
|
? { _id: "packageInspectorFindingNotifications:old-target" }
|
|
: null,
|
|
),
|
|
})),
|
|
})),
|
|
insert,
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn((tableName: string, id: string) =>
|
|
id.startsWith(`${tableName}:`) ? id : null,
|
|
),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
insertPackageInspectorWarningsInternalHandler(ctx as never, {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
ownerUserId: "users:owner",
|
|
packageName: "demo-plugin",
|
|
version: "1.0.0",
|
|
scanSource: "nightly",
|
|
inspectorVersion: "0.5.0",
|
|
targetOpenClawVersion: "0.10.0",
|
|
notifyOwners: true,
|
|
findings: [
|
|
{
|
|
id: "demo:missing-expected-seam",
|
|
code: "missing-expected-seam",
|
|
level: "breakage",
|
|
message: "registerTool is no longer available",
|
|
evidence: ["dist/index.js:2"],
|
|
fixture: "demo",
|
|
authorRemediation: {
|
|
summary: "Replace registerTool with the current plugin API.",
|
|
docsUrl:
|
|
"https://docs.openclaw.ai/clawhub/plugin-validation-fixes#missing-expected-seam",
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
).resolves.toMatchObject({ ok: true, inserted: 1, shouldEmailOwner: true });
|
|
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"packageInspectorWarnings",
|
|
expect.objectContaining({
|
|
findingKind: "error",
|
|
scanSource: "nightly",
|
|
inspectorVersion: "0.5.0",
|
|
targetOpenClawVersion: "0.10.0",
|
|
code: "missing-expected-seam",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("drops plugin inspector coverage gaps before storing public findings", async () => {
|
|
const insert = vi.fn();
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn(() => ({
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
})),
|
|
})),
|
|
insert,
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn((tableName: string, id: string) =>
|
|
id.startsWith(`${tableName}:`) ? id : null,
|
|
),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
insertPackageInspectorWarningsInternalHandler(ctx as never, {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
ownerUserId: "users:owner",
|
|
packageName: "demo-plugin",
|
|
version: "1.0.0",
|
|
scanSource: "nightly",
|
|
inspectorVersion: "0.5.0",
|
|
targetOpenClawVersion: "0.10.0",
|
|
notifyOwners: true,
|
|
findings: [
|
|
{
|
|
id: "demo:runtime-tool-capture",
|
|
code: "runtime-tool-capture",
|
|
issueClass: "inspector-gap",
|
|
message: "runtime tools need capture before contract judgment",
|
|
evidence: ["src/index.ts:2"],
|
|
fixture: "demo",
|
|
},
|
|
{
|
|
id: "demo:legacy-before-agent-start",
|
|
code: "legacy-before-agent-start",
|
|
issueClass: "deprecation-warning",
|
|
message: "legacy before_agent_start hook is deprecated",
|
|
evidence: ["src/index.ts:4"],
|
|
fixture: "demo",
|
|
authorRemediation: {
|
|
summary: "Move prompt mutation work to before_prompt_build.",
|
|
docsUrl:
|
|
"https://docs.openclaw.ai/clawhub/plugin-validation-fixes#legacy-before-agent-start",
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
).resolves.toMatchObject({ ok: true, inserted: 1, shouldEmailOwner: false });
|
|
|
|
expect(insert).toHaveBeenCalledTimes(1);
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"packageInspectorWarnings",
|
|
expect.objectContaining({
|
|
code: "legacy-before-agent-start",
|
|
issueClass: "deprecation-warning",
|
|
authorRemediation: {
|
|
summary: "Move prompt mutation work to before_prompt_build.",
|
|
docsUrl:
|
|
"https://docs.openclaw.ai/clawhub/plugin-validation-fixes#legacy-before-agent-start",
|
|
},
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("stores an exact nightly hard error despite an identical preserved finding", async () => {
|
|
const insert = vi.fn();
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn(() => ({
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([
|
|
{
|
|
findingKind: "error",
|
|
inspectorFindingId: "demo:legacy-before-agent-start",
|
|
code: "legacy-before-agent-start",
|
|
message: "legacy before_agent_start hook is deprecated",
|
|
evidence: ["src/index.ts:4"],
|
|
fixture: "demo",
|
|
inspectorVersion: "0.5.0",
|
|
targetOpenClawVersion: "0.10.0",
|
|
authorRemediation: {
|
|
summary: "Move prompt mutation work to before_prompt_build.",
|
|
docsUrl:
|
|
"https://docs.openclaw.ai/clawhub/plugin-validation-fixes#legacy-before-agent-start",
|
|
},
|
|
},
|
|
]),
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
})),
|
|
})),
|
|
insert,
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn((tableName: string, id: string) =>
|
|
id.startsWith(`${tableName}:`) ? id : null,
|
|
),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
insertPackageInspectorWarningsInternalHandler(ctx as never, {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
ownerUserId: "users:owner",
|
|
packageName: "demo-plugin",
|
|
version: "1.0.0",
|
|
scanSource: "nightly",
|
|
inspectorVersion: "0.5.0",
|
|
targetOpenClawVersion: "0.10.0",
|
|
notifyOwners: true,
|
|
findings: [
|
|
{
|
|
id: "demo:legacy-before-agent-start",
|
|
code: "legacy-before-agent-start",
|
|
level: "breakage",
|
|
message: "legacy before_agent_start hook is deprecated",
|
|
evidence: ["src/index.ts:4"],
|
|
fixture: "demo",
|
|
authorRemediation: {
|
|
summary: "Move prompt mutation work to before_prompt_build.",
|
|
docsUrl:
|
|
"https://docs.openclaw.ai/clawhub/plugin-validation-fixes#legacy-before-agent-start",
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
).resolves.toMatchObject({ ok: true, inserted: 1, shouldEmailOwner: true });
|
|
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"packageInspectorWarnings",
|
|
expect.objectContaining({
|
|
findingKind: "error",
|
|
scanSource: "nightly",
|
|
inspectorVersion: "0.5.0",
|
|
targetOpenClawVersion: "0.10.0",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("summarizes plugin inspector validation findings for signed-out viewers", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue(null);
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packages:demo") {
|
|
return {
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
normalizedName: "demo-plugin",
|
|
family: "code-plugin",
|
|
latestReleaseId: "packageReleases:demo-1",
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
normalizedName: "demo-plugin",
|
|
family: "code-plugin",
|
|
latestReleaseId: "packageReleases:demo-1",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageInspectorWarnings") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([
|
|
{
|
|
_id: "packageInspectorWarnings:1",
|
|
packageName: "demo-plugin",
|
|
version: "1.0.0",
|
|
findingKind: "error",
|
|
code: "missing-expected-seam",
|
|
issueClass: "compatibility-error",
|
|
message: "registerTool is no longer available",
|
|
evidence: ["dist/index.js:2"],
|
|
inspectorVersion: "0.5.0",
|
|
targetOpenClawVersion: "0.10.0",
|
|
scanSource: "nightly",
|
|
authorRemediation: {
|
|
summary: "Replace registerTool with the current plugin API.",
|
|
docsUrl:
|
|
"https://docs.openclaw.ai/clawhub/plugin-validation-fixes#missing-expected-seam",
|
|
},
|
|
createdAt: 2,
|
|
},
|
|
]),
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
getPackageInspectorValidationSummaryPublicHandler(ctx as never, { name: "demo-plugin" }),
|
|
).resolves.toEqual({
|
|
findingCount: 1,
|
|
errorCount: 1,
|
|
warningCount: 0,
|
|
incompatibleAfterOpenClawVersion: "0.10.0",
|
|
});
|
|
});
|
|
|
|
it("summarizes only latest-release plugin inspector findings", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue(null);
|
|
const warningIndexEq = vi.fn(() => ({}));
|
|
const warningWithIndex = vi.fn(
|
|
(_indexName: string, build: (q: { eq: typeof warningIndexEq }) => unknown) => {
|
|
build({ eq: warningIndexEq });
|
|
return {
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
},
|
|
);
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
normalizedName: "demo-plugin",
|
|
family: "code-plugin",
|
|
latestReleaseId: "packageReleases:demo-2",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageInspectorWarnings") {
|
|
return { withIndex: warningWithIndex };
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
getPackageInspectorValidationSummaryPublicHandler(ctx as never, { name: "demo-plugin" }),
|
|
).resolves.toEqual({
|
|
findingCount: 0,
|
|
errorCount: 0,
|
|
warningCount: 0,
|
|
incompatibleAfterOpenClawVersion: null,
|
|
});
|
|
expect(warningWithIndex).toHaveBeenCalledWith("by_release_created", expect.any(Function));
|
|
expect(warningIndexEq).toHaveBeenCalledWith("releaseId", "packageReleases:demo-2");
|
|
expect(warningIndexEq).not.toHaveBeenCalledWith("packageId", "packages:demo");
|
|
});
|
|
|
|
it("does not list detailed plugin inspector findings for signed-out viewers", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue(null);
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn(),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
listPackageInspectorFindingsPublicHandler(ctx as never, { name: "demo-plugin" }),
|
|
).resolves.toEqual([]);
|
|
expect(ctx.db.query).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not expose detailed plugin inspector findings for private packages", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue(null);
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packages:private",
|
|
name: "private-plugin",
|
|
normalizedName: "private-plugin",
|
|
family: "code-plugin",
|
|
channel: "private",
|
|
scanStatus: "clean",
|
|
ownerUserId: "users:owner",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
listPackageInspectorFindingsPublicHandler(ctx as never, { name: "private-plugin" }),
|
|
).resolves.toEqual([]);
|
|
expect(ctx.db.query).not.toHaveBeenCalled();
|
|
expect(ctx.db.query).not.toHaveBeenCalledWith("packageInspectorWarnings");
|
|
});
|
|
|
|
it("dedupes package inspector finding emails per release", async () => {
|
|
const insert = vi.fn(async () => "packageInspectorFindingNotifications:1");
|
|
const unique = vi.fn().mockResolvedValueOnce(null).mockResolvedValueOnce({
|
|
_id: "packageInspectorFindingNotifications:1",
|
|
});
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn(() => ({
|
|
withIndex: vi.fn(() => ({
|
|
unique,
|
|
})),
|
|
})),
|
|
insert,
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn((tableName: string, id: string) =>
|
|
id.startsWith(`${tableName}:`) ? id : null,
|
|
),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
markPackageInspectorFindingsEmailedInternalHandler(ctx as never, {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
ownerUserId: "users:owner",
|
|
packageName: "demo-plugin",
|
|
version: "1.0.0",
|
|
findingCount: 2,
|
|
email: "owner@example.com",
|
|
}),
|
|
).resolves.toEqual({ ok: true, created: true });
|
|
await expect(
|
|
markPackageInspectorFindingsEmailedInternalHandler(ctx as never, {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
ownerUserId: "users:owner",
|
|
packageName: "demo-plugin",
|
|
version: "1.0.0",
|
|
findingCount: 3,
|
|
email: "owner@example.com",
|
|
}),
|
|
).resolves.toEqual({ ok: true, created: false });
|
|
expect(insert).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("marks the exact scan notification complete after owner email succeeds", async () => {
|
|
const patch = vi.fn();
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn((table: string) => ({
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi
|
|
.fn()
|
|
.mockResolvedValue(
|
|
table === "packageInspectorScanStates"
|
|
? { _id: "packageInspectorScanStates:demo" }
|
|
: null,
|
|
),
|
|
})),
|
|
})),
|
|
insert: vi.fn(),
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
markPackageInspectorFindingsEmailedInternalHandler(ctx as never, {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
ownerUserId: "users:owner",
|
|
packageName: "demo-plugin",
|
|
version: "1.0.0",
|
|
findingCount: 2,
|
|
email: "owner@example.com",
|
|
inspectorVersion: "0.6.0",
|
|
targetOpenClawVersion: "2026.8.0-beta.1",
|
|
}),
|
|
).resolves.toEqual({ ok: true, created: true });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageInspectorScanStates:demo",
|
|
expect.objectContaining({ notificationCompletedAt: expect.any(Number) }),
|
|
);
|
|
});
|
|
|
|
it("excludes internal and warning-only findings from owner emails", async () => {
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packages:demo") {
|
|
return makePackageDoc({
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
ownerUserId: "users:owner",
|
|
});
|
|
}
|
|
if (id === "packageReleases:demo-1") {
|
|
return makeReleaseDoc({
|
|
_id: "packageReleases:demo-1",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
});
|
|
}
|
|
if (id === "users:owner") {
|
|
return { _id: "users:owner", handle: "owner", email: "owner@example.com" };
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageInspectorWarnings") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([
|
|
{
|
|
_id: "packageInspectorWarnings:internal",
|
|
packageName: "demo-plugin",
|
|
version: "1.0.0",
|
|
findingKind: "warning",
|
|
code: "runtime-tool-capture",
|
|
issueClass: "inspector-gap",
|
|
message: "runtime tool schema needs registration capture",
|
|
createdAt: 2,
|
|
},
|
|
{
|
|
_id: "packageInspectorWarnings:author",
|
|
packageName: "demo-plugin",
|
|
version: "1.0.0",
|
|
findingKind: "warning",
|
|
code: "legacy-before-agent-start",
|
|
issueClass: "deprecation-warning",
|
|
message: "legacy before_agent_start hook is deprecated",
|
|
authorRemediation: {
|
|
summary: "Move prompt mutation work to before_prompt_build.",
|
|
docsUrl:
|
|
"https://docs.openclaw.ai/clawhub/plugin-validation-fixes#legacy-before-agent-start",
|
|
},
|
|
createdAt: 1,
|
|
},
|
|
{
|
|
_id: "packageInspectorWarnings:error",
|
|
packageName: "demo-plugin",
|
|
version: "1.0.0",
|
|
findingKind: "error",
|
|
code: "missing-api",
|
|
issueClass: "compatibility-error",
|
|
message: "an API is unavailable in the upcoming OpenClaw release",
|
|
authorRemediation: {
|
|
summary: "Use the replacement API.",
|
|
},
|
|
createdAt: 0,
|
|
},
|
|
]),
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageInspectorFindingNotifications") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
};
|
|
|
|
const result = await getPackageInspectorEmailContextInternalHandler(ctx as never, {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
});
|
|
expect(result?.packageName).toBe("demo-plugin");
|
|
expect(result?.findings.map((finding) => finding.code)).toEqual(["missing-api"]);
|
|
expect(result?.findings[0]?.authorRemediation).toMatchObject({
|
|
summary: "Use the replacement API.",
|
|
});
|
|
});
|
|
|
|
it("rejects a notification request whose release does not belong to the package", async () => {
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packages:demo") {
|
|
return makePackageDoc({
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
ownerUserId: "users:owner",
|
|
});
|
|
}
|
|
if (id === "packageReleases:other") {
|
|
return makeReleaseDoc({
|
|
_id: "packageReleases:other",
|
|
packageId: "packages:other",
|
|
version: "1.0.0",
|
|
});
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn(() => {
|
|
throw new Error("Mismatched releases must be rejected before querying findings");
|
|
}),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
getPackageInspectorEmailContextInternalHandler(ctx as never, {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:other",
|
|
inspectorVersion: "0.3.20",
|
|
targetOpenClawVersion: "2026.8.0-beta.1",
|
|
}),
|
|
).resolves.toBeNull();
|
|
});
|
|
|
|
it("builds email context for a new exact target despite an older release notification", async () => {
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packages:demo") {
|
|
return makePackageDoc({
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
ownerUserId: "users:owner",
|
|
});
|
|
}
|
|
if (id === "packageReleases:demo-1") {
|
|
return makeReleaseDoc({
|
|
_id: "packageReleases:demo-1",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
});
|
|
}
|
|
if (id === "users:owner") {
|
|
return { _id: "users:owner", handle: "owner", email: "owner@example.com" };
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageInspectorWarnings") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
filter: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([
|
|
{
|
|
findingKind: "error",
|
|
code: "missing-api",
|
|
scanSource: "nightly",
|
|
inspectorVersion: "0.6.0",
|
|
targetOpenClawVersion: "2026.8.0-beta.1",
|
|
message: "API removed",
|
|
authorRemediation: { summary: "Use the replacement API." },
|
|
},
|
|
]),
|
|
})),
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageInspectorScanStates") {
|
|
return {
|
|
withIndex: vi.fn(() => ({ unique: vi.fn().mockResolvedValue(null) })),
|
|
};
|
|
}
|
|
if (table === "packageInspectorFindingNotifications") {
|
|
throw new Error("Exact-target email context must not use release-wide dedupe");
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
getPackageInspectorEmailContextInternalHandler(ctx as never, {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
inspectorVersion: "0.6.0",
|
|
targetOpenClawVersion: "2026.8.0-beta.1",
|
|
}),
|
|
).resolves.toMatchObject({ packageName: "demo-plugin" });
|
|
});
|
|
|
|
it("selects the exact nightly scan before limiting owner email findings", async () => {
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packages:demo") {
|
|
return makePackageDoc({
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
ownerUserId: "users:owner",
|
|
});
|
|
}
|
|
if (id === "packageReleases:demo-1") {
|
|
return makeReleaseDoc({
|
|
_id: "packageReleases:demo-1",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
});
|
|
}
|
|
if (id === "users:owner") {
|
|
return { _id: "users:owner", handle: "owner", email: "owner@example.com" };
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageInspectorWarnings") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
filter: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([
|
|
{
|
|
findingKind: "error",
|
|
code: "missing-api",
|
|
scanSource: "nightly",
|
|
inspectorVersion: "0.6.0",
|
|
targetOpenClawVersion: "2026.8.0-beta.1",
|
|
message: "API removed",
|
|
authorRemediation: { summary: "Use the replacement API." },
|
|
},
|
|
]),
|
|
})),
|
|
})),
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([
|
|
{
|
|
code: "publish-warning-100",
|
|
scanSource: "publish",
|
|
message: "The release-wide limit was exhausted by publish findings.",
|
|
authorRemediation: { summary: "Fix the package metadata." },
|
|
},
|
|
]),
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageInspectorScanStates") {
|
|
return {
|
|
withIndex: vi.fn(() => ({ unique: vi.fn().mockResolvedValue(null) })),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
};
|
|
|
|
const result = await getPackageInspectorEmailContextInternalHandler(ctx as never, {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
inspectorVersion: "0.6.0",
|
|
targetOpenClawVersion: "2026.8.0-beta.1",
|
|
});
|
|
|
|
expect(result?.findings.map((finding) => finding.code)).toEqual(["missing-api"]);
|
|
});
|
|
|
|
it("leases a scan page without advancing past unprocessed releases", async () => {
|
|
const paginate = vi.fn(async () => ({
|
|
page: [
|
|
{
|
|
_id: "packageReleases:one",
|
|
packageId: "packages:one",
|
|
version: "1.0.0",
|
|
artifactKind: "legacy-zip",
|
|
},
|
|
{
|
|
_id: "packageReleases:two",
|
|
packageId: "packages:two",
|
|
version: "2.0.0",
|
|
artifactKind: "npm-pack",
|
|
},
|
|
],
|
|
isDone: false,
|
|
continueCursor: "cursor-after-returned-page",
|
|
}));
|
|
const insert = vi.fn();
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packages:one") {
|
|
return {
|
|
_id: "packages:one",
|
|
name: "one-plugin",
|
|
family: "code-plugin",
|
|
latestReleaseId: "packageReleases:one",
|
|
};
|
|
}
|
|
if (id === "packages:two") {
|
|
return {
|
|
_id: "packages:two",
|
|
name: "two-plugin",
|
|
family: "bundle-plugin",
|
|
tags: { latest: "packageReleases:two" },
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageInspectorScanCursors") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
paginate,
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert,
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
claimPackageInspectorScanBatchInternalHandler(ctx as never, {
|
|
batchSize: 2,
|
|
leaseMs: 60_000,
|
|
runId: "run-42",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
leased: false,
|
|
nextCursor: "cursor-after-returned-page",
|
|
items: [
|
|
{ packageName: "one-plugin", artifactKind: "legacy-zip" },
|
|
{ packageName: "two-plugin", artifactKind: "npm-pack" },
|
|
],
|
|
});
|
|
expect(paginate).toHaveBeenCalledWith({ cursor: null, numItems: 2 });
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"packageInspectorScanCursors",
|
|
expect.objectContaining({
|
|
cursor: null,
|
|
pendingCursor: "cursor-after-returned-page",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("advances a leased scan page only after the owning run acknowledges it", async () => {
|
|
const patch = vi.fn();
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn((table: string) => {
|
|
if (table !== "packageInspectorScanCursors") {
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageInspectorScanCursors:nightly",
|
|
name: "nightly",
|
|
cursor: null,
|
|
pendingCursor: "page-2",
|
|
runId: "run-42",
|
|
leaseExpiresAt: Date.now() + 60_000,
|
|
}),
|
|
})),
|
|
};
|
|
}),
|
|
patch,
|
|
insert: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
acknowledgePackageInspectorScanBatchInternalHandler(ctx as never, {
|
|
runId: " run-42 ",
|
|
cursor: "page-2",
|
|
leaseMs: 60_000,
|
|
}),
|
|
).resolves.toEqual({ ok: true, cursor: "page-2", completed: false });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageInspectorScanCursors:nightly",
|
|
expect.objectContaining({
|
|
cursor: "page-2",
|
|
pendingCursor: undefined,
|
|
runId: "run-42",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("rejects acknowledgement from a different scan run", async () => {
|
|
const patch = vi.fn();
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn(() => ({
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageInspectorScanCursors:nightly",
|
|
cursor: null,
|
|
pendingCursor: "page-2",
|
|
runId: "run-42",
|
|
}),
|
|
})),
|
|
})),
|
|
patch,
|
|
insert: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
acknowledgePackageInspectorScanBatchInternalHandler(ctx as never, {
|
|
runId: "run-other",
|
|
cursor: "page-2",
|
|
}),
|
|
).rejects.toThrow("does not own the active lease");
|
|
expect(patch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("continues only from the cursor owned by the same nightly scan run", async () => {
|
|
const paginate = vi.fn(async () => ({
|
|
page: [],
|
|
isDone: true,
|
|
continueCursor: null,
|
|
}));
|
|
const patch = vi.fn();
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageInspectorScanCursors") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageInspectorScanCursors:nightly",
|
|
name: "nightly",
|
|
runId: "run-42",
|
|
cursor: "page-2",
|
|
leaseExpiresAt: Date.now() + 60_000,
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({ order: vi.fn(() => ({ paginate })) })),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert: vi.fn(),
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
claimPackageInspectorScanBatchInternalHandler(ctx as never, {
|
|
batchSize: 25,
|
|
leaseMs: 60_000,
|
|
cursor: "page-2",
|
|
runId: "run-42",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
leased: false,
|
|
nextCursor: null,
|
|
items: [],
|
|
});
|
|
expect(paginate).toHaveBeenCalledWith({ cursor: "page-2", numItems: 25 });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageInspectorScanCursors:nightly",
|
|
expect.objectContaining({ cursor: "page-2", pendingCursor: null, runId: "run-42" }),
|
|
);
|
|
});
|
|
|
|
it("reclaims an unacknowledged page immediately for the same nightly scan run", async () => {
|
|
const paginate = vi.fn(async () => ({
|
|
page: [],
|
|
isDone: true,
|
|
continueCursor: null,
|
|
}));
|
|
const patch = vi.fn();
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageInspectorScanCursors") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageInspectorScanCursors:nightly",
|
|
name: "nightly",
|
|
runId: "run-42",
|
|
cursor: "page-1",
|
|
pendingCursor: "page-2",
|
|
leaseExpiresAt: Date.now() + 60_000,
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({ order: vi.fn(() => ({ paginate })) })),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert: vi.fn(),
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
claimPackageInspectorScanBatchInternalHandler(ctx as never, {
|
|
batchSize: 25,
|
|
leaseMs: 60_000,
|
|
cursor: "page-1",
|
|
runId: "run-42",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
leased: false,
|
|
nextCursor: null,
|
|
});
|
|
expect(paginate).toHaveBeenCalledWith({ cursor: "page-1", numItems: 25 });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageInspectorScanCursors:nightly",
|
|
expect.objectContaining({
|
|
cursor: "page-1",
|
|
pendingCursor: null,
|
|
runId: "run-42",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("resumes the persisted cursor when the same nightly run restarts", async () => {
|
|
const paginate = vi.fn(async () => ({
|
|
page: [],
|
|
isDone: true,
|
|
continueCursor: null,
|
|
}));
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageInspectorScanCursors") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageInspectorScanCursors:nightly",
|
|
runId: "run-42",
|
|
cursor: "page-3",
|
|
leaseExpiresAt: Date.now() + 60_000,
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({ order: vi.fn(() => ({ paginate })) })),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert: vi.fn(),
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
claimPackageInspectorScanBatchInternalHandler(ctx as never, {
|
|
cursor: "page-2",
|
|
runId: "run-42",
|
|
}),
|
|
).resolves.toMatchObject({ ok: true, leased: false, nextCursor: null, items: [] });
|
|
expect(paginate).toHaveBeenCalledWith({ cursor: "page-3", numItems: 25 });
|
|
});
|
|
|
|
it("resumes the persisted cursor when a replacement nightly run acquires an expired lease", async () => {
|
|
const paginate = vi.fn(async () => ({
|
|
page: [
|
|
{
|
|
_id: "packageReleases:current",
|
|
packageId: "packages:current",
|
|
version: "1.0.0",
|
|
artifactKind: "legacy-zip",
|
|
},
|
|
],
|
|
isDone: false,
|
|
continueCursor: "page-3",
|
|
}));
|
|
const patch = vi.fn();
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) =>
|
|
id === "packages:current"
|
|
? {
|
|
_id: "packages:current",
|
|
name: "current-plugin",
|
|
family: "code-plugin",
|
|
channel: "community",
|
|
ownerUserId: "users:owner",
|
|
latestReleaseId: "packageReleases:current",
|
|
}
|
|
: null,
|
|
),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageInspectorScanCursors") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageInspectorScanCursors:nightly",
|
|
runId: "run-old",
|
|
cursor: "page-2",
|
|
leaseExpiresAt: Date.now() - 1,
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({ order: vi.fn(() => ({ paginate })) })),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
patch,
|
|
insert: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
claimPackageInspectorScanBatchInternalHandler(ctx as never, {
|
|
batchSize: 1,
|
|
cursor: null,
|
|
runId: "run-new",
|
|
}),
|
|
).resolves.toMatchObject({ ok: true, leased: false, nextCursor: "page-3" });
|
|
expect(paginate).toHaveBeenCalledWith({ cursor: "page-2", numItems: 1 });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageInspectorScanCursors:nightly",
|
|
expect.objectContaining({ cursor: "page-2", pendingCursor: "page-3", runId: "run-new" }),
|
|
);
|
|
});
|
|
|
|
it("claims only latest releases for nightly plugin inspector scans", async () => {
|
|
const paginate = vi.fn(async () => ({
|
|
page: [
|
|
{
|
|
_id: "packageReleases:old",
|
|
packageId: "packages:modern",
|
|
version: "1.0.0",
|
|
artifactKind: "legacy-zip",
|
|
},
|
|
{
|
|
_id: "packageReleases:current",
|
|
packageId: "packages:modern",
|
|
version: "2.0.0",
|
|
artifactKind: "npm-pack",
|
|
},
|
|
{
|
|
_id: "packageReleases:legacy-latest",
|
|
packageId: "packages:legacy",
|
|
version: "1.5.0",
|
|
artifactKind: "legacy-zip",
|
|
},
|
|
],
|
|
isDone: true,
|
|
continueCursor: null,
|
|
}));
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packages:modern") {
|
|
return {
|
|
_id: "packages:modern",
|
|
name: "modern-plugin",
|
|
family: "code-plugin",
|
|
channel: "community",
|
|
latestReleaseId: "packageReleases:current",
|
|
tags: { latest: "packageReleases:current" },
|
|
};
|
|
}
|
|
if (id === "packages:legacy") {
|
|
return {
|
|
_id: "packages:legacy",
|
|
name: "legacy-plugin",
|
|
family: "code-plugin",
|
|
channel: "community",
|
|
tags: { latest: "packageReleases:legacy-latest" },
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageInspectorScanCursors") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
paginate,
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert: vi.fn(),
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
claimPackageInspectorScanBatchInternalHandler(ctx as never, {
|
|
batchSize: 3,
|
|
leaseMs: 60_000,
|
|
runId: "run-42",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
leased: false,
|
|
items: [
|
|
{ releaseId: "packageReleases:current", packageName: "modern-plugin" },
|
|
{ releaseId: "packageReleases:legacy-latest", packageName: "legacy-plugin" },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("skips latest releases already scanned by the same beta target and inspector", async () => {
|
|
const paginate = vi.fn(async () => ({
|
|
page: [
|
|
{
|
|
_id: "packageReleases:current",
|
|
packageId: "packages:modern",
|
|
version: "2.0.0",
|
|
artifactKind: "npm-pack",
|
|
},
|
|
],
|
|
isDone: true,
|
|
continueCursor: null,
|
|
}));
|
|
const scanStateWithIndex = vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
releaseId: "packageReleases:current",
|
|
inspectorVersion: "0.6.0",
|
|
targetOpenClawVersion: "2026.8.0-beta.1",
|
|
}),
|
|
}));
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) =>
|
|
id === "packages:modern"
|
|
? {
|
|
_id: "packages:modern",
|
|
name: "modern-plugin",
|
|
family: "code-plugin",
|
|
channel: "community",
|
|
ownerUserId: "users:owner",
|
|
latestReleaseId: "packageReleases:current",
|
|
}
|
|
: null,
|
|
),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({ order: vi.fn(() => ({ paginate })) })),
|
|
};
|
|
}
|
|
if (table === "packageInspectorScanStates") {
|
|
return { withIndex: scanStateWithIndex };
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
previewPackageInspectorScanBatchInternalHandler(ctx as never, {
|
|
batchSize: 1,
|
|
inspectorVersion: "0.6.0",
|
|
targetOpenClawVersion: "2026.8.0-beta.1",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
items: [],
|
|
skippedUnchanged: 1,
|
|
});
|
|
expect(scanStateWithIndex).toHaveBeenCalledWith(
|
|
"by_release_and_inspector_version_and_target_openclaw_version",
|
|
expect.any(Function),
|
|
);
|
|
|
|
await expect(
|
|
previewPackageInspectorScanBatchInternalHandler(ctx as never, {
|
|
batchSize: 1,
|
|
inspectorVersion: "0.6.0",
|
|
targetOpenClawVersion: "2026.8.0-beta.1",
|
|
notifyOwners: true,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
items: [{ releaseId: "packageReleases:current" }],
|
|
skippedUnchanged: 0,
|
|
});
|
|
});
|
|
|
|
it("returns a filtered source page without issuing a second paginated query", async () => {
|
|
const paginate = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
page: [
|
|
{
|
|
_id: "packageReleases:old",
|
|
packageId: "packages:modern",
|
|
version: "1.0.0",
|
|
artifactKind: "legacy-zip",
|
|
},
|
|
{
|
|
_id: "packageReleases:current",
|
|
packageId: "packages:modern",
|
|
version: "2.0.0",
|
|
artifactKind: "npm-pack",
|
|
},
|
|
],
|
|
isDone: false,
|
|
continueCursor: "page-2",
|
|
})
|
|
.mockResolvedValueOnce({
|
|
page: [
|
|
{
|
|
_id: "packageReleases:other",
|
|
packageId: "packages:other",
|
|
version: "1.0.0",
|
|
artifactKind: "npm-pack",
|
|
},
|
|
],
|
|
isDone: true,
|
|
continueCursor: null,
|
|
});
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) =>
|
|
id === "packages:modern"
|
|
? {
|
|
_id: "packages:modern",
|
|
name: "modern-plugin",
|
|
family: "code-plugin",
|
|
channel: "community",
|
|
ownerUserId: "users:owner",
|
|
latestReleaseId: "packageReleases:current",
|
|
}
|
|
: {
|
|
_id: "packages:other",
|
|
name: "other-plugin",
|
|
family: "bundle-plugin",
|
|
channel: "community",
|
|
ownerUserId: "users:owner",
|
|
latestReleaseId: "packageReleases:other",
|
|
},
|
|
),
|
|
query: vi.fn((table: string) => {
|
|
if (table !== "packageReleases") throw new Error(`Unexpected table ${table}`);
|
|
return { withIndex: vi.fn(() => ({ order: vi.fn(() => ({ paginate })) })) };
|
|
}),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
previewPackageInspectorScanBatchInternalHandler(ctx as never, { batchSize: 2 }),
|
|
).resolves.toMatchObject({
|
|
nextCursor: "page-2",
|
|
items: [{ releaseId: "packageReleases:current", packageName: "modern-plugin" }],
|
|
});
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
expect(paginate).toHaveBeenCalledWith({ cursor: null, numItems: 2 });
|
|
});
|
|
|
|
it("rejects a non-advancing nightly scan pagination cursor", async () => {
|
|
const paginate = vi.fn(async () => ({
|
|
page: [],
|
|
isDone: false,
|
|
continueCursor: "stuck-cursor",
|
|
}));
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn((table: string) => {
|
|
if (table !== "packageReleases") throw new Error(`Unexpected table ${table}`);
|
|
return { withIndex: vi.fn(() => ({ order: vi.fn(() => ({ paginate })) })) };
|
|
}),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
previewPackageInspectorScanBatchInternalHandler(ctx as never, {
|
|
cursor: "stuck-cursor",
|
|
batchSize: 25,
|
|
}),
|
|
).rejects.toThrow("Package Inspector scan pagination cursor did not advance");
|
|
expect(paginate).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("previews nightly plugin inspector batches without leasing or advancing the cursor", async () => {
|
|
const paginate = vi.fn(async () => ({
|
|
page: [
|
|
{
|
|
_id: "packageReleases:old",
|
|
packageId: "packages:modern",
|
|
version: "1.0.0",
|
|
artifactKind: "legacy-zip",
|
|
},
|
|
{
|
|
_id: "packageReleases:current",
|
|
packageId: "packages:modern",
|
|
version: "2.0.0",
|
|
artifactKind: "npm-pack",
|
|
},
|
|
],
|
|
isDone: false,
|
|
continueCursor: "cursor-after-preview",
|
|
}));
|
|
const insert = vi.fn();
|
|
const patch = vi.fn();
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packages:modern") {
|
|
return {
|
|
_id: "packages:modern",
|
|
name: "modern-plugin",
|
|
family: "code-plugin",
|
|
channel: "community",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner",
|
|
latestReleaseId: "packageReleases:current",
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
paginate,
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert,
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
previewPackageInspectorScanBatchInternalHandler(ctx as never, {
|
|
cursor: "cursor-before-preview",
|
|
batchSize: 1,
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
leased: false,
|
|
nextCursor: "cursor-after-preview",
|
|
items: [
|
|
{
|
|
releaseId: "packageReleases:current",
|
|
packageName: "modern-plugin",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner",
|
|
},
|
|
],
|
|
});
|
|
expect(paginate).toHaveBeenCalledWith({ cursor: "cursor-before-preview", numItems: 1 });
|
|
expect(insert).not.toHaveBeenCalled();
|
|
expect(patch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not claim private or public-blocked releases for unauthenticated nightly scans", async () => {
|
|
const paginate = vi.fn(async () => ({
|
|
page: [
|
|
{
|
|
_id: "packageReleases:public",
|
|
packageId: "packages:public",
|
|
version: "1.0.0",
|
|
artifactKind: "legacy-zip",
|
|
},
|
|
{
|
|
_id: "packageReleases:private",
|
|
packageId: "packages:private",
|
|
version: "1.0.0",
|
|
artifactKind: "legacy-zip",
|
|
},
|
|
{
|
|
_id: "packageReleases:blocked",
|
|
packageId: "packages:blocked",
|
|
version: "1.0.0",
|
|
artifactKind: "legacy-zip",
|
|
manualModeration: { state: "revoked" },
|
|
},
|
|
],
|
|
isDone: true,
|
|
continueCursor: null,
|
|
}));
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packages:public") {
|
|
return {
|
|
_id: "packages:public",
|
|
name: "public-plugin",
|
|
family: "code-plugin",
|
|
channel: "community",
|
|
scanStatus: "clean",
|
|
latestReleaseId: "packageReleases:public",
|
|
};
|
|
}
|
|
if (id === "packages:private") {
|
|
return {
|
|
_id: "packages:private",
|
|
name: "private-plugin",
|
|
family: "code-plugin",
|
|
channel: "private",
|
|
scanStatus: "clean",
|
|
latestReleaseId: "packageReleases:private",
|
|
};
|
|
}
|
|
if (id === "packages:blocked") {
|
|
return {
|
|
_id: "packages:blocked",
|
|
name: "blocked-plugin",
|
|
family: "code-plugin",
|
|
channel: "community",
|
|
scanStatus: "clean",
|
|
latestReleaseId: "packageReleases:blocked",
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageInspectorScanCursors") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
paginate,
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert: vi.fn(),
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(() => null),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
claimPackageInspectorScanBatchInternalHandler(ctx as never, {
|
|
batchSize: 3,
|
|
leaseMs: 60_000,
|
|
runId: "run-42",
|
|
}),
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
leased: false,
|
|
items: [{ packageName: "public-plugin" }],
|
|
});
|
|
});
|
|
|
|
it("does not list plugin inspector warnings for signed-out viewers", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue(null);
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(),
|
|
query: vi.fn(),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
listPackageInspectorWarningsForManagerHandler(ctx as never, {
|
|
name: "demo-plugin",
|
|
}),
|
|
).resolves.toEqual([]);
|
|
expect(ctx.db.query).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("lists manager plugin inspector findings only for the latest release", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
|
|
const warningIndexEq = vi.fn(() => ({}));
|
|
const warningWithIndex = vi.fn(
|
|
(_indexName: string, build: (q: { eq: typeof warningIndexEq }) => unknown) => {
|
|
build({ eq: warningIndexEq });
|
|
return {
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([
|
|
{
|
|
_id: "packageInspectorWarnings:latest",
|
|
packageName: "demo-plugin",
|
|
version: "2.0.0",
|
|
findingKind: "warning",
|
|
code: "latest-warning",
|
|
message: "latest release warning",
|
|
createdAt: 2,
|
|
authorRemediation: {
|
|
summary: "Update the plugin API usage.",
|
|
},
|
|
},
|
|
]),
|
|
})),
|
|
};
|
|
},
|
|
);
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:owner") return { _id: "users:owner", role: "user" };
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packages:demo",
|
|
name: "demo-plugin",
|
|
normalizedName: "demo-plugin",
|
|
family: "code-plugin",
|
|
ownerUserId: "users:owner",
|
|
latestReleaseId: "packageReleases:demo-2",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageInspectorWarnings") {
|
|
return { withIndex: warningWithIndex };
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
listPackageInspectorWarningsForManagerHandler(ctx as never, {
|
|
name: "demo-plugin",
|
|
}),
|
|
).resolves.toMatchObject([{ code: "latest-warning", version: "2.0.0" }]);
|
|
expect(warningWithIndex).toHaveBeenCalledWith("by_release_created", expect.any(Function));
|
|
expect(warningIndexEq).toHaveBeenCalledWith("releaseId", "packageReleases:demo-2");
|
|
expect(warningIndexEq).not.toHaveBeenCalledWith("packageId", "packages:demo");
|
|
});
|
|
|
|
it("infers owner handle from scoped package names for user package publishes", async () => {
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if (args.minimumRole === "publisher") {
|
|
return { publisherId: "publishers:openclaw" };
|
|
}
|
|
return { ok: true, packageId: "packages:discord", releaseId: "releases:discord-1" };
|
|
});
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:steipete",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:steipete",
|
|
role: "admin",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce(null),
|
|
runMutation,
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: {
|
|
get: vi.fn(),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:steipete",
|
|
payload: {
|
|
name: "@openclaw/discord",
|
|
displayName: "Discord",
|
|
family: "bundle-plugin",
|
|
version: "2026.5.3-beta.2",
|
|
changelog: "beta",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [],
|
|
},
|
|
}),
|
|
).rejects.toThrow("openclaw.plugin.json is required");
|
|
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
actorUserId: "users:steipete",
|
|
ownerHandle: "openclaw",
|
|
minimumRole: "publisher",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("treats blank owner handles as omitted for scoped package publishes", async () => {
|
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
|
if (args.minimumRole === "publisher") {
|
|
return { publisherId: "publishers:opik" };
|
|
}
|
|
return { ok: true, packageId: "packages:opik", releaseId: "releases:opik-1" };
|
|
});
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:vincent",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:vincent",
|
|
role: "admin",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce(null),
|
|
runMutation,
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: {
|
|
get: vi.fn(),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:vincent",
|
|
payload: {
|
|
name: "@opik/opik-openclaw",
|
|
ownerHandle: " ",
|
|
displayName: "Opik",
|
|
family: "bundle-plugin",
|
|
version: "0.2.15",
|
|
changelog: "beta",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [],
|
|
},
|
|
}),
|
|
).rejects.toThrow("openclaw.plugin.json is required");
|
|
|
|
expect(runMutation).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
actorUserId: "users:vincent",
|
|
ownerHandle: "opik",
|
|
minimumRole: "publisher",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("rejects scoped package publishes to missing publishers with package.json guidance", async () => {
|
|
const runMutation = vi.fn(async () => {
|
|
throw new Error('Publisher "@opik" not found');
|
|
});
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:vincent",
|
|
handle: "vincentkoc",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:vincent",
|
|
handle: "vincentkoc",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
}),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: {
|
|
get: vi.fn(),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:vincent",
|
|
payload: {
|
|
name: "@opik/opik-openclaw",
|
|
displayName: "Opik",
|
|
family: "bundle-plugin",
|
|
version: "0.2.15",
|
|
changelog: "beta",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [],
|
|
},
|
|
}),
|
|
).rejects.toThrow('Create it with "clawhub publisher create opik".');
|
|
});
|
|
|
|
it("guides legacy personal scoped packages through org creation and transfer", async () => {
|
|
const runMutation = vi.fn(async () => {
|
|
throw new Error('Publisher "@opik" not found');
|
|
});
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packages:opik-openclaw",
|
|
ownerUserId: "users:vincent",
|
|
ownerPublisherId: "publishers:vincent",
|
|
})
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:vincent",
|
|
handle: "vincentkoc",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:vincent",
|
|
handle: "vincentkoc",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "publishers:vincent",
|
|
kind: "user",
|
|
handle: "vincentkoc",
|
|
linkedUserId: "users:vincent",
|
|
}),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: {
|
|
get: vi.fn(),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:vincent",
|
|
payload: {
|
|
name: "@opik/opik-openclaw",
|
|
displayName: "Opik",
|
|
family: "bundle-plugin",
|
|
version: "0.2.15",
|
|
changelog: "beta",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [],
|
|
},
|
|
}),
|
|
).rejects.toThrow(
|
|
[
|
|
'Cannot publish @opik/opik-openclaw: package.json name is scoped to "@opik", but ClawHub has no "@opik" publisher.',
|
|
"",
|
|
'This package already exists under your personal publisher "@vincentkoc". To move it into the matching org publisher, run:',
|
|
"",
|
|
' clawhub publisher create opik --display-name "Opik"',
|
|
' clawhub package transfer @opik/opik-openclaw --to opik --reason "Move legacy personal package into @opik"',
|
|
"",
|
|
"Then rerun publish.",
|
|
].join("\n"),
|
|
);
|
|
});
|
|
|
|
it("does not show transfer guidance for scoped packages owned by another publisher", async () => {
|
|
const runMutation = vi.fn(async () => {
|
|
throw new Error('Publisher "@opik" not found');
|
|
});
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
_id: "packages:opik-openclaw",
|
|
ownerUserId: "users:other",
|
|
ownerPublisherId: "publishers:other",
|
|
})
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:vincent",
|
|
handle: "vincentkoc",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:vincent",
|
|
handle: "vincentkoc",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
}),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: {
|
|
get: vi.fn(),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:vincent",
|
|
payload: {
|
|
name: "@opik/opik-openclaw",
|
|
displayName: "Opik",
|
|
family: "bundle-plugin",
|
|
version: "0.2.15",
|
|
changelog: "beta",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [],
|
|
},
|
|
}),
|
|
).rejects.toThrow(
|
|
[
|
|
'Cannot publish @opik/opik-openclaw: package.json name is scoped to "@opik", but ClawHub has no "@opik" publisher.',
|
|
'Create it with "clawhub publisher create opik".',
|
|
].join(" "),
|
|
);
|
|
});
|
|
|
|
it("suggests publisher creation for missing npm-compatible package scopes", async () => {
|
|
const runMutation = vi.fn(async () => {
|
|
throw new Error('Publisher "@example.tools" not found');
|
|
});
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:vincent",
|
|
handle: "vincentkoc",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:vincent",
|
|
handle: "vincentkoc",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
}),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: {
|
|
get: vi.fn(),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:vincent",
|
|
payload: {
|
|
name: "@example.tools/demo-plugin",
|
|
displayName: "Demo",
|
|
family: "bundle-plugin",
|
|
version: "1.0.0",
|
|
changelog: "beta",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [],
|
|
},
|
|
}),
|
|
).rejects.toThrow('Create it with "clawhub publisher create example.tools".');
|
|
});
|
|
|
|
it("explains scoped package publish access failures from package.json", async () => {
|
|
const runMutation = vi.fn(async () => {
|
|
throw new Error('Forbidden: you do not have publish access to publisher "@openclaw"');
|
|
});
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:steipete",
|
|
handle: "steipete",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:steipete",
|
|
handle: "steipete",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
}),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: {
|
|
get: vi.fn(),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:steipete",
|
|
payload: {
|
|
name: "@openclaw/discord",
|
|
displayName: "Discord",
|
|
family: "bundle-plugin",
|
|
version: "2026.5.3-beta.2",
|
|
changelog: "beta",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [],
|
|
},
|
|
}),
|
|
).rejects.toThrow(
|
|
[
|
|
'Cannot publish @openclaw/discord: package.json name is scoped to "@openclaw", but your account does not have publish rights to the "@openclaw" ClawHub organization.',
|
|
"Create the matching ClawHub organization if it does not exist, get publish rights to that organization, or rename package.json name to use an organization scope you control.",
|
|
].join("\n\n"),
|
|
);
|
|
});
|
|
|
|
it("rejects scoped package publishes when --owner conflicts with the package scope", async () => {
|
|
const runMutation = vi.fn();
|
|
const ctx = {
|
|
runQuery: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(null)
|
|
.mockResolvedValueOnce({
|
|
_id: "users:steipete",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
_id: "users:steipete",
|
|
role: "user",
|
|
githubCreatedAt: Date.now() - 20 * 24 * 60 * 60 * 1000,
|
|
}),
|
|
runMutation,
|
|
runAction: vi.fn(async () => makeCleanPackageInspectorResult()),
|
|
scheduler: {
|
|
runAfter: vi.fn(),
|
|
},
|
|
storage: {
|
|
get: vi.fn(),
|
|
},
|
|
};
|
|
|
|
await expect(
|
|
publishPackageForUserInternalHandler(ctx as never, {
|
|
actorUserId: "users:steipete",
|
|
payload: {
|
|
name: "@opik/opik-openclaw",
|
|
ownerHandle: "vincentkoc",
|
|
displayName: "Opik",
|
|
family: "bundle-plugin",
|
|
version: "0.2.15",
|
|
changelog: "beta",
|
|
bundle: { hostTargets: ["desktop"] },
|
|
files: [],
|
|
},
|
|
}),
|
|
).rejects.toThrow('Package scope "@opik" must match selected owner "@vincentkoc"');
|
|
expect(runMutation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps pending-scan packages visible to public reads", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue(null);
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packageReleases:demo-1") return makeReleaseDoc({ version: "1.0.0" });
|
|
if (id === "users:owner") return { _id: "users:owner", handle: "owner" };
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table !== "packages") throw new Error(`Unexpected table ${table}`);
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(makePackageDoc({ scanStatus: "pending" })),
|
|
})),
|
|
};
|
|
}),
|
|
},
|
|
};
|
|
|
|
const result = await getByNameHandler(ctx as never, {
|
|
name: "demo-plugin",
|
|
});
|
|
expect(result?.package?.name).toBe("demo-plugin");
|
|
});
|
|
|
|
it("keeps pending-scan packages visible to the owner", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
|
|
const result = await getByNameHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packageReleases:demo-1") return makeReleaseDoc({ version: "1.0.0" });
|
|
if (id === "users:owner") return { _id: "users:owner", handle: "owner" };
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table !== "packages") throw new Error(`Unexpected table ${table}`);
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(
|
|
makePackageDoc({
|
|
ownerUserId: "users:owner",
|
|
scanStatus: "pending",
|
|
}),
|
|
),
|
|
})),
|
|
};
|
|
}),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
} as never,
|
|
{ name: "demo-plugin" },
|
|
);
|
|
|
|
expect(result?.package?.name).toBe("demo-plugin");
|
|
});
|
|
|
|
it("lists owner packages with pending review and latest release scan state", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
|
|
const result = await listHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packageReleases:demo-1") {
|
|
return makeReleaseDoc({
|
|
version: "1.0.0",
|
|
vtAnalysis: { status: "pending" },
|
|
llmAnalysis: { status: "clean" },
|
|
staticScan: { status: "clean" },
|
|
});
|
|
}
|
|
if (id === "users:owner") {
|
|
return { _id: "users:owner", handle: "owner" };
|
|
}
|
|
if (id === "publishers:owner") {
|
|
return {
|
|
_id: "publishers:owner",
|
|
kind: "user",
|
|
linkedUserId: "users:owner",
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn((indexName: string) => {
|
|
if (indexName === "by_owner_publisher") {
|
|
return {
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([
|
|
makePackageDoc({
|
|
ownerPublisherId: "publishers:owner",
|
|
scanStatus: "pending",
|
|
}),
|
|
]),
|
|
})),
|
|
};
|
|
}
|
|
if (indexName === "by_owner") {
|
|
return {
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected index ${indexName}`);
|
|
}),
|
|
};
|
|
}
|
|
if (table === "publisherMembers") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageInspectorWarnings") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([
|
|
{
|
|
_id: "packageInspectorWarnings:internal",
|
|
code: "runtime-tool-capture",
|
|
issueClass: "inspector-gap",
|
|
message: "runtime tool schema needs registration capture",
|
|
},
|
|
{
|
|
_id: "packageInspectorWarnings:author",
|
|
code: "legacy-before-agent-start",
|
|
issueClass: "deprecation-warning",
|
|
message: "legacy before_agent_start hook is deprecated",
|
|
authorRemediation: {
|
|
summary: "Move prompt mutation work to before_prompt_build.",
|
|
},
|
|
},
|
|
]),
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
} as never,
|
|
{ ownerPublisherId: "publishers:owner", limit: 20 },
|
|
);
|
|
|
|
expect(result).toEqual([
|
|
expect.objectContaining({
|
|
name: "demo-plugin",
|
|
pendingReview: true,
|
|
scanStatus: "pending",
|
|
inspectorWarningCount: 1,
|
|
latestRelease: expect.objectContaining({
|
|
vtStatus: "pending",
|
|
staticScanStatus: "clean",
|
|
}),
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("lists packages for the viewer's legacy no-link personal publisher dashboard", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
|
|
const result = await listHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packageReleases:demo-1") return makeReleaseDoc({ version: "1.0.0" });
|
|
if (id === "packageReleases:legacy-1") {
|
|
return makeReleaseDoc({
|
|
_id: "packageReleases:legacy-1",
|
|
packageId: "packages:legacy-direct",
|
|
version: "1.0.0",
|
|
});
|
|
}
|
|
if (id === "users:owner") {
|
|
return {
|
|
_id: "users:owner",
|
|
handle: "owner",
|
|
personalPublisherId: "publishers:owner",
|
|
};
|
|
}
|
|
if (id === "publishers:owner") {
|
|
return {
|
|
_id: "publishers:owner",
|
|
kind: "user",
|
|
linkedUserId: undefined,
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn((indexName: string) => {
|
|
if (indexName === "by_owner_publisher") {
|
|
return {
|
|
order: vi.fn(() => ({
|
|
take: vi
|
|
.fn()
|
|
.mockResolvedValue([
|
|
makePackageDoc({ ownerPublisherId: "publishers:owner" }),
|
|
]),
|
|
})),
|
|
};
|
|
}
|
|
if (indexName === "by_owner") {
|
|
return {
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([
|
|
makePackageDoc({
|
|
_id: "packages:legacy-direct",
|
|
name: "legacy-direct-plugin",
|
|
normalizedName: "legacy-direct-plugin",
|
|
displayName: "Legacy Direct Plugin",
|
|
ownerPublisherId: undefined,
|
|
latestReleaseId: "packageReleases:legacy-1",
|
|
}),
|
|
]),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected index ${indexName}`);
|
|
}),
|
|
};
|
|
}
|
|
if (table === "publisherMembers") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageInspectorWarnings") {
|
|
return makeEmptyPackageInspectorWarningsQuery();
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
} as never,
|
|
{ ownerPublisherId: "publishers:owner", limit: 20 },
|
|
);
|
|
|
|
expect(result).toEqual([
|
|
expect.objectContaining({
|
|
name: "demo-plugin",
|
|
ownerPublisherId: "publishers:owner",
|
|
}),
|
|
expect.objectContaining({
|
|
name: "legacy-direct-plugin",
|
|
ownerPublisherId: undefined,
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("keeps stale publisher-owned package rows out of owner-user dashboards", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
|
|
const result = await listHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:owner") {
|
|
return { _id: "users:owner", handle: "owner" };
|
|
}
|
|
if (id === "publishers:other-personal") {
|
|
return {
|
|
_id: "publishers:other-personal",
|
|
kind: "user",
|
|
linkedUserId: "users:other",
|
|
};
|
|
}
|
|
if (id === "publishers:org") {
|
|
return { _id: "publishers:org", kind: "org" };
|
|
}
|
|
if (id === "packageReleases:legacy-1") {
|
|
return makeReleaseDoc({
|
|
_id: "packageReleases:legacy-1",
|
|
packageId: "packages:legacy-direct",
|
|
version: "1.0.0",
|
|
});
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn((indexName: string) => {
|
|
if (indexName !== "by_owner") throw new Error(`Unexpected index ${indexName}`);
|
|
return {
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([
|
|
makePackageDoc({
|
|
_id: "packages:other-personal",
|
|
name: "other-personal-plugin",
|
|
ownerPublisherId: "publishers:other-personal",
|
|
}),
|
|
makePackageDoc({
|
|
_id: "packages:org",
|
|
name: "org-plugin",
|
|
ownerPublisherId: "publishers:org",
|
|
}),
|
|
makePackageDoc({
|
|
_id: "packages:legacy-direct",
|
|
name: "legacy-direct-plugin",
|
|
normalizedName: "legacy-direct-plugin",
|
|
displayName: "Legacy Direct Plugin",
|
|
ownerPublisherId: undefined,
|
|
latestReleaseId: "packageReleases:legacy-1",
|
|
}),
|
|
]),
|
|
})),
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
if (table === "packageInspectorWarnings") {
|
|
return makeEmptyPackageInspectorWarningsQuery();
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
} as never,
|
|
{ ownerUserId: "users:owner", limit: 20 },
|
|
);
|
|
|
|
expect(result.map((entry) => entry.name)).toEqual(["legacy-direct-plugin"]);
|
|
});
|
|
|
|
it("returns no owner packages when the viewer lacks access", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:stranger" as never);
|
|
const result = await listHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:stranger") {
|
|
return { _id: "users:stranger", handle: "stranger", displayName: "Stranger" };
|
|
}
|
|
if (id === "publishers:owner") {
|
|
return {
|
|
_id: "publishers:owner",
|
|
kind: "user",
|
|
linkedUserId: "users:owner",
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "publisherMembers") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
} as never,
|
|
{ ownerPublisherId: "publishers:owner", limit: 20 },
|
|
);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("ignores stale personal memberships for package dashboards", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:stranger" as never);
|
|
const result = await listHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "publishers:owner") {
|
|
return {
|
|
_id: "publishers:owner",
|
|
kind: "user",
|
|
linkedUserId: "users:owner",
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
take: vi
|
|
.fn()
|
|
.mockResolvedValue([
|
|
makePackageDoc({ ownerPublisherId: "publishers:owner" }),
|
|
]),
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "publisherMembers") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "publisherMembers:stale",
|
|
publisherId: "publishers:owner",
|
|
userId: "users:stranger",
|
|
role: "owner",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageInspectorWarnings") {
|
|
return makeEmptyPackageInspectorWarningsQuery();
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
} as never,
|
|
{ ownerPublisherId: "publishers:owner", limit: 20 },
|
|
);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it("keeps org memberships authorized for package dashboards", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:member" as never);
|
|
const result = await listHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:member") {
|
|
return { _id: "users:member", handle: "member", displayName: "Member" };
|
|
}
|
|
if (id === "publishers:org") {
|
|
return {
|
|
_id: "publishers:org",
|
|
kind: "org",
|
|
handle: "team",
|
|
displayName: "Team",
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
take: vi
|
|
.fn()
|
|
.mockResolvedValue([makePackageDoc({ ownerPublisherId: "publishers:org" })]),
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "publisherMembers") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "publisherMembers:member",
|
|
publisherId: "publishers:org",
|
|
userId: "users:member",
|
|
role: "publisher",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageInspectorWarnings") {
|
|
return makeEmptyPackageInspectorWarningsQuery();
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
} as never,
|
|
{ ownerPublisherId: "publishers:org", limit: 20 },
|
|
);
|
|
|
|
expect(result).toEqual([expect.objectContaining({ name: "demo-plugin" })]);
|
|
});
|
|
|
|
it("records package reports for moderation", async () => {
|
|
const insert = vi.fn(async (table: string) =>
|
|
table === "packageReports" ? "packageReports:1" : "auditLogs:1",
|
|
);
|
|
const patch = vi.fn();
|
|
const reportReason = "x".repeat(520);
|
|
|
|
const result = await reportPackageForUserInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:reporter") return { _id: id };
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(makePackageDoc()),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(makeReleaseDoc({ version: "1.2.3" })),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReports") {
|
|
return {
|
|
withIndex: vi.fn((indexName: string) => {
|
|
if (indexName === "by_user") {
|
|
return { collect: vi.fn().mockResolvedValue([]) };
|
|
}
|
|
return { unique: vi.fn().mockResolvedValue(null) };
|
|
}),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert,
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
} as never,
|
|
{
|
|
actorUserId: "users:reporter",
|
|
name: "@scope/demo",
|
|
version: "1.2.3",
|
|
reason: reportReason,
|
|
},
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
reported: true,
|
|
alreadyReported: false,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
reportCount: 1,
|
|
});
|
|
expect(insert).toHaveBeenCalledWith("packageReports", {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
version: "1.2.3",
|
|
userId: "users:reporter",
|
|
reason: "x".repeat(500),
|
|
status: "open",
|
|
createdAt: expect.any(Number),
|
|
});
|
|
expect(patch).toHaveBeenCalledWith("packages:demo", {
|
|
reportCount: 1,
|
|
lastReportedAt: expect.any(Number),
|
|
});
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({
|
|
actorUserId: "users:reporter",
|
|
action: "package.report",
|
|
targetType: "package",
|
|
targetId: "packages:demo",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("dedupes package reports by user and package", async () => {
|
|
const insert = vi.fn();
|
|
const patch = vi.fn();
|
|
|
|
const result = await reportPackageForUserInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:reporter") return { _id: id };
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(makePackageDoc({ reportCount: 2 })),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReports") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageReports:existing",
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
userId: "users:reporter",
|
|
status: "open",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert,
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
} as never,
|
|
{
|
|
actorUserId: "users:reporter",
|
|
name: "@scope/demo",
|
|
reason: "already reported",
|
|
},
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
reported: false,
|
|
alreadyReported: true,
|
|
reportCount: 2,
|
|
});
|
|
expect(insert).not.toHaveBeenCalled();
|
|
expect(patch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("lists package reports for moderators", async () => {
|
|
const result = await listPackageReportsInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:moderator") return { _id: id, role: "moderator" };
|
|
if (id === "users:reporter") {
|
|
return { _id: id, handle: "reporter", displayName: "Reporter" };
|
|
}
|
|
if (id === "packages:demo") return makePackageDoc({ name: "@scope/demo" });
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table !== "packageReports") throw new Error(`Unexpected table ${table}`);
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
paginate: vi.fn().mockResolvedValue({
|
|
page: [
|
|
{
|
|
_id: "packageReports:1",
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
version: "1.2.3",
|
|
userId: "users:reporter",
|
|
reason: "suspicious",
|
|
status: "open",
|
|
createdAt: 123,
|
|
},
|
|
],
|
|
continueCursor: null,
|
|
isDone: true,
|
|
}),
|
|
})),
|
|
})),
|
|
};
|
|
}),
|
|
},
|
|
} as never,
|
|
{ actorUserId: "users:moderator", status: "open", limit: 10 },
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
items: [
|
|
expect.objectContaining({
|
|
reportId: "packageReports:1",
|
|
name: "@scope/demo",
|
|
version: "1.2.3",
|
|
reason: "suspicious",
|
|
status: "open",
|
|
reporter: expect.objectContaining({ handle: "reporter" }),
|
|
}),
|
|
],
|
|
nextCursor: null,
|
|
done: true,
|
|
});
|
|
});
|
|
|
|
it("triages package reports and decrements open count", async () => {
|
|
const patch = vi.fn();
|
|
const insert = vi.fn(async () => "auditLogs:1");
|
|
|
|
const result = await triagePackageReportForUserInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:moderator") return { _id: id, role: "moderator" };
|
|
if (id === "packageReports:1") {
|
|
return {
|
|
_id: id,
|
|
packageId: "packages:demo",
|
|
userId: "users:reporter",
|
|
status: "open",
|
|
createdAt: 123,
|
|
};
|
|
}
|
|
if (id === "packages:demo") return makePackageDoc({ reportCount: 2 });
|
|
return null;
|
|
}),
|
|
patch,
|
|
insert,
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageSearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageSearchDigest:demo",
|
|
packageId: "packages:demo",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (
|
|
table === "packageCapabilitySearchDigest" ||
|
|
table === "packageTopicSearchDigest" ||
|
|
table === "packagePluginCategorySearchDigest"
|
|
) {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
} as never,
|
|
{
|
|
actorUserId: "users:moderator",
|
|
reportId: "packageReports:1",
|
|
status: "confirmed",
|
|
note: "handled",
|
|
},
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
reportId: "packageReports:1",
|
|
status: "confirmed",
|
|
reportCount: 1,
|
|
});
|
|
expect(patch).toHaveBeenCalledWith("packageReports:1", {
|
|
status: "confirmed",
|
|
triagedAt: expect.any(Number),
|
|
triagedBy: "users:moderator",
|
|
triageNote: "handled",
|
|
actionTaken: "none",
|
|
});
|
|
expect(patch).toHaveBeenCalledWith("packages:demo", { reportCount: 1 });
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({
|
|
action: "package.report.triage",
|
|
targetType: "packageReport",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("can quarantine a package release while triaging a valid report", async () => {
|
|
const patch = vi.fn();
|
|
const insert = vi.fn(async () => "auditLogs:1");
|
|
const pkg = makePackageDoc({ reportCount: 1, latestReleaseId: "packageReleases:demo-1" });
|
|
const release = makeReleaseDoc({
|
|
_id: "packageReleases:demo-1",
|
|
packageId: "packages:demo",
|
|
version: "1.2.3",
|
|
verification: { scanStatus: "clean" },
|
|
});
|
|
|
|
const result = await triagePackageReportForUserInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:moderator") return { _id: id, role: "moderator" };
|
|
if (id === "packageReports:1") {
|
|
return {
|
|
_id: id,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
version: "1.2.3",
|
|
userId: "users:reporter",
|
|
status: "open",
|
|
createdAt: 123,
|
|
};
|
|
}
|
|
if (id === "packages:demo") return pkg;
|
|
if (id === "packageReleases:demo-1") return release;
|
|
return null;
|
|
}),
|
|
patch,
|
|
insert,
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageSearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageSearchDigest:demo",
|
|
packageId: "packages:demo",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (
|
|
table === "packageCapabilitySearchDigest" ||
|
|
table === "packageTopicSearchDigest" ||
|
|
table === "packagePluginCategorySearchDigest"
|
|
) {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
} as never,
|
|
{
|
|
actorUserId: "users:moderator",
|
|
reportId: "packageReports:1",
|
|
status: "confirmed",
|
|
note: "confirmed malicious behavior",
|
|
finalAction: "quarantine",
|
|
},
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
status: "confirmed",
|
|
actionTaken: "quarantine",
|
|
});
|
|
expect(patch).toHaveBeenCalledWith("packageReports:1", {
|
|
status: "confirmed",
|
|
triagedAt: expect.any(Number),
|
|
triagedBy: "users:moderator",
|
|
triageNote: "confirmed malicious behavior",
|
|
actionTaken: "quarantine",
|
|
});
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageReleases:demo-1",
|
|
expect.objectContaining({
|
|
manualModeration: expect.objectContaining({ state: "quarantined" }),
|
|
}),
|
|
);
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({
|
|
action: "package.release.moderation",
|
|
targetType: "packageRelease",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("returns package moderation status to package owners", async () => {
|
|
const result = await getPackageModerationStatusForUserInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:owner") return { _id: id, role: "user" };
|
|
if (id === "packageReleases:demo-1") {
|
|
return makeReleaseDoc({
|
|
_id: "packageReleases:demo-1",
|
|
version: "1.2.3",
|
|
artifactKind: "npm-pack",
|
|
manualModeration: {
|
|
state: "quarantined",
|
|
reason: "manual review",
|
|
reviewerUserId: "users:moderator",
|
|
updatedAt: 2,
|
|
},
|
|
});
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table !== "packages") throw new Error(`Unexpected table ${table}`);
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(
|
|
makePackageDoc({
|
|
name: "@scope/demo",
|
|
ownerUserId: "users:owner",
|
|
reportCount: 2,
|
|
lastReportedAt: 456,
|
|
}),
|
|
),
|
|
})),
|
|
};
|
|
}),
|
|
},
|
|
} as never,
|
|
{ actorUserId: "users:owner", name: "@scope/demo" },
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
package: {
|
|
name: "@scope/demo",
|
|
reportCount: 2,
|
|
},
|
|
latestRelease: {
|
|
version: "1.2.3",
|
|
scanStatus: "malicious",
|
|
moderationState: "quarantined",
|
|
blockedFromDownload: true,
|
|
reasons: ["manual:quarantined", "scan:malicious", "reports:2"],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("does not let stale personal ownerUserId read package moderation status", async () => {
|
|
await expect(
|
|
getPackageModerationStatusForUserInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:viewer") return { _id: id, role: "user" };
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(
|
|
makePackageDoc({
|
|
name: "@scope/demo",
|
|
ownerKind: "user",
|
|
ownerUserId: "users:viewer",
|
|
ownerPublisherId: "publishers:other-personal",
|
|
}),
|
|
),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "publisherMembers") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
} as never,
|
|
{ actorUserId: "users:viewer", name: "@scope/demo" },
|
|
),
|
|
).rejects.toThrow("Unauthorized");
|
|
});
|
|
|
|
it("submits owner appeals for quarantined package releases", async () => {
|
|
const insert = vi.fn(async (table: string) =>
|
|
table === "packageAppeals" ? "packageAppeals:1" : "auditLogs:1",
|
|
);
|
|
|
|
const result = await submitPackageAppealForUserInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:owner") return { _id: id, role: "user" };
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(
|
|
makePackageDoc({
|
|
name: "@scope/demo",
|
|
ownerUserId: "users:owner",
|
|
}),
|
|
),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(
|
|
makeReleaseDoc({
|
|
version: "1.2.3",
|
|
manualModeration: {
|
|
state: "quarantined",
|
|
reason: "manual review",
|
|
reviewerUserId: "users:moderator",
|
|
updatedAt: 2,
|
|
},
|
|
}),
|
|
),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageAppeals") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
first: vi.fn().mockResolvedValue(null),
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert,
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
} as never,
|
|
{
|
|
actorUserId: "users:owner",
|
|
name: "@scope/demo",
|
|
version: "1.2.3",
|
|
message: "please review",
|
|
},
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
submitted: true,
|
|
appealId: "packageAppeals:1",
|
|
status: "open",
|
|
});
|
|
expect(insert).toHaveBeenCalledWith("packageAppeals", {
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
version: "1.2.3",
|
|
userId: "users:owner",
|
|
message: "please review",
|
|
status: "open",
|
|
createdAt: expect.any(Number),
|
|
});
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({
|
|
action: "package.appeal.submit",
|
|
targetType: "packageAppeal",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not let stale personal-publisher memberships submit package appeals", async () => {
|
|
const insert = vi.fn();
|
|
|
|
await expect(
|
|
submitPackageAppealForUserInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:stale-member") return { _id: id, role: "user" };
|
|
if (id === "publishers:owner") {
|
|
return {
|
|
_id: id,
|
|
kind: "user",
|
|
handle: "owner",
|
|
linkedUserId: "users:owner",
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(
|
|
makePackageDoc({
|
|
name: "@scope/demo",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner",
|
|
}),
|
|
),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "publisherMembers") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "publisherMembers:stale",
|
|
publisherId: "publishers:owner",
|
|
userId: "users:stale-member",
|
|
role: "admin",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
insert,
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
} as never,
|
|
{
|
|
actorUserId: "users:stale-member",
|
|
name: "@scope/demo",
|
|
version: "1.2.3",
|
|
message: "please review",
|
|
},
|
|
),
|
|
).rejects.toThrow("Unauthorized");
|
|
|
|
expect(insert).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not let stale personal-publisher memberships read package manage context", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:stale-member" as never);
|
|
|
|
const result = await getManageContextHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:stale-member") return { _id: id, role: "user" };
|
|
if (id === "publishers:owner") {
|
|
return {
|
|
_id: id,
|
|
kind: "user",
|
|
handle: "owner",
|
|
linkedUserId: "users:owner",
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(
|
|
makePackageDoc({
|
|
name: "@scope/demo",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner",
|
|
}),
|
|
),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "publisherMembers") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "publisherMembers:stale",
|
|
publisherId: "publishers:owner",
|
|
userId: "users:stale-member",
|
|
role: "admin",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
} as never,
|
|
{ name: "@scope/demo" },
|
|
);
|
|
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("paginates owner-withdrawn releases with explicit restore markers", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
|
|
const indexNames: string[] = [];
|
|
const filters = new Map<string, unknown>();
|
|
const paginate = vi.fn().mockResolvedValue({
|
|
page: [
|
|
{
|
|
_id: "packageReleases:withdrawn",
|
|
packageId: "packages:demo",
|
|
version: "0.8.0",
|
|
changelog: "Withdrawn release",
|
|
distTags: [],
|
|
files: [],
|
|
integritySha256: "sha256:withdrawn",
|
|
compatibility: null,
|
|
verification: null,
|
|
createdBy: "users:owner",
|
|
publishActor: { kind: "user", userId: "users:owner" },
|
|
createdAt: 1,
|
|
softDeletedAt: 123,
|
|
ownerDeletedAt: 123,
|
|
ownerDeletedBy: "users:owner",
|
|
},
|
|
],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
});
|
|
const ctx = {
|
|
db: {
|
|
get: vi.fn(async (id: string) => (id === "users:owner" ? { _id: id, role: "user" } : null)),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(makePackageDoc()),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
index: string,
|
|
buildQuery?: (q: { eq: (field: string, value: unknown) => unknown }) => unknown,
|
|
) => {
|
|
indexNames.push(index);
|
|
const query = {
|
|
eq(field: string, value: unknown) {
|
|
filters.set(field, value);
|
|
return query;
|
|
},
|
|
};
|
|
buildQuery?.(query);
|
|
return { order: vi.fn(() => ({ paginate })) };
|
|
},
|
|
),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
} as never;
|
|
|
|
const result = await listVersionsForManagerHandler(ctx, {
|
|
name: "demo-plugin",
|
|
paginationOpts: { cursor: null, numItems: 20 },
|
|
});
|
|
|
|
expect(result.page).toEqual([
|
|
expect.objectContaining({
|
|
version: "0.8.0",
|
|
softDeletedAt: 123,
|
|
ownerDeletedAt: 123,
|
|
}),
|
|
]);
|
|
expect(indexNames).toEqual(["by_package_owner_deleted_created"]);
|
|
expect(filters).toEqual(
|
|
new Map<string, unknown>([
|
|
["packageId", "packages:demo"],
|
|
["ownerDeletedBy", "users:owner"],
|
|
]),
|
|
);
|
|
expect(paginate).toHaveBeenCalledWith({ cursor: null, numItems: 20 });
|
|
});
|
|
|
|
it("allows direct package owners to delete versions", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
|
|
|
|
const result = await canDeleteVersionsHandler(
|
|
makeCanDeleteVersionsCtx({
|
|
viewerId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
}) as never,
|
|
{ name: "demo-plugin" },
|
|
);
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("does not let package owners delete versions when the package is blocked from public use", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
|
|
|
|
const result = await canDeleteVersionsHandler(
|
|
makeCanDeleteVersionsCtx({
|
|
viewerId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
packageOverrides: { scanStatus: "malicious" },
|
|
}) as never,
|
|
{ name: "demo-plugin" },
|
|
);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it.each(["owner", "admin"] as const)(
|
|
"allows org %s members to delete versions",
|
|
async (membershipRole) => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:org-manager" as never);
|
|
|
|
const result = await canDeleteVersionsHandler(
|
|
makeCanDeleteVersionsCtx({
|
|
viewerId: "users:org-manager",
|
|
ownerUserId: "users:creator",
|
|
ownerPublisherId: "publishers:demo-org",
|
|
membershipRole,
|
|
}) as never,
|
|
{ name: "demo-plugin" },
|
|
);
|
|
|
|
expect(result).toBe(true);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{ label: "ordinary org publisher", membershipRole: "publisher" as const },
|
|
{ label: "non-member", membershipRole: undefined },
|
|
])("does not let $label delete versions", async ({ membershipRole }) => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:org-viewer" as never);
|
|
|
|
const result = await canDeleteVersionsHandler(
|
|
makeCanDeleteVersionsCtx({
|
|
viewerId: "users:org-viewer",
|
|
ownerUserId: "users:creator",
|
|
ownerPublisherId: "publishers:demo-org",
|
|
membershipRole,
|
|
}) as never,
|
|
{ name: "demo-plugin" },
|
|
);
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it.each(["admin", "moderator"] as const)(
|
|
"does not let platform %s staff delete versions without ownership",
|
|
async (viewerRole) => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:staff" as never);
|
|
|
|
const result = await canDeleteVersionsHandler(
|
|
makeCanDeleteVersionsCtx({
|
|
viewerId: "users:staff",
|
|
viewerRole,
|
|
ownerUserId: "users:owner",
|
|
}) as never,
|
|
{ name: "demo-plugin" },
|
|
);
|
|
|
|
expect(result).toBe(false);
|
|
},
|
|
);
|
|
|
|
it("bounds normalized unique candidate lookups when checking version delete capability", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
|
|
const packageLookupNames: string[] = [];
|
|
|
|
const result = await canDeleteVersionsHandler(
|
|
makeCanDeleteVersionsCtx({
|
|
viewerId: "users:owner",
|
|
ownerUserId: "users:owner",
|
|
packageLookupNames,
|
|
packageMatchName: null,
|
|
}) as never,
|
|
{
|
|
name: " primary-plugin ",
|
|
candidateNames: [
|
|
"PRIMARY-PLUGIN",
|
|
"candidate-one",
|
|
"candidate-two",
|
|
"candidate-three",
|
|
"candidate-four",
|
|
"candidate-five",
|
|
"candidate-one",
|
|
],
|
|
},
|
|
);
|
|
|
|
expect(result).toBe(false);
|
|
expect(packageLookupNames).toEqual([
|
|
"primary-plugin",
|
|
"candidate-one",
|
|
"candidate-two",
|
|
"candidate-three",
|
|
]);
|
|
});
|
|
|
|
it("returns slim package metadata and generated category suggestions for manage context", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue("users:owner" as never);
|
|
|
|
const pkg = makePackageDoc({
|
|
name: "large-plugin",
|
|
displayName: "Large Plugin",
|
|
sourceRepo: "owner/large-plugin",
|
|
latestVersionSummary: {
|
|
version: "1.2.3",
|
|
changelog: "x".repeat(10_000),
|
|
artifact: { kind: "legacy-zip", sha256: "abc", format: "zip" },
|
|
},
|
|
tags: {
|
|
latest: "packageReleases:demo-1",
|
|
beta: "packageReleases:demo-beta",
|
|
},
|
|
});
|
|
const release = makeReleaseDoc({
|
|
version: "1.2.3",
|
|
files: [
|
|
{
|
|
path: "README.md",
|
|
size: 10_000,
|
|
sha256: "abc",
|
|
contentType: "text/plain; charset=utf-8",
|
|
},
|
|
],
|
|
llmAnalysis: {
|
|
status: "clean",
|
|
checkedAt: 2,
|
|
findings: "x".repeat(10_000),
|
|
},
|
|
});
|
|
|
|
const result = await getManageContextHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:owner") return { _id: id, role: "user" };
|
|
if (id === "packageReleases:demo-1") return release;
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(pkg),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
} as never,
|
|
{ name: "large-plugin" },
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
package: {
|
|
_id: "packages:demo",
|
|
name: "large-plugin",
|
|
displayName: "Large Plugin",
|
|
categories: undefined,
|
|
topics: undefined,
|
|
},
|
|
latestRelease: {
|
|
_id: "packageReleases:demo-1",
|
|
version: "1.2.3",
|
|
},
|
|
suggestedCategories: ["other"],
|
|
});
|
|
});
|
|
|
|
it("lists package appeals for moderators", async () => {
|
|
const result = await listPackageAppealsInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:moderator") return { _id: id, role: "moderator" };
|
|
if (id === "users:owner") return { _id: id, handle: "owner" };
|
|
if (id === "packages:demo") return makePackageDoc({ name: "@scope/demo" });
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table !== "packageAppeals") throw new Error(`Unexpected table ${table}`);
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
paginate: vi.fn().mockResolvedValue({
|
|
page: [
|
|
{
|
|
_id: "packageAppeals:1",
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
version: "1.2.3",
|
|
userId: "users:owner",
|
|
message: "please review",
|
|
status: "open",
|
|
createdAt: 123,
|
|
},
|
|
],
|
|
continueCursor: null,
|
|
isDone: true,
|
|
}),
|
|
})),
|
|
})),
|
|
};
|
|
}),
|
|
},
|
|
} as never,
|
|
{ actorUserId: "users:moderator", status: "open", limit: 10 },
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
items: [
|
|
expect.objectContaining({
|
|
appealId: "packageAppeals:1",
|
|
name: "@scope/demo",
|
|
version: "1.2.3",
|
|
message: "please review",
|
|
status: "open",
|
|
submitter: expect.objectContaining({ handle: "owner" }),
|
|
}),
|
|
],
|
|
nextCursor: null,
|
|
done: true,
|
|
});
|
|
});
|
|
|
|
it("resolves package appeals for moderators", async () => {
|
|
const patch = vi.fn();
|
|
const insert = vi.fn(async () => "auditLogs:1");
|
|
|
|
const result = await resolvePackageAppealForUserInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:moderator") return { _id: id, role: "moderator" };
|
|
if (id === "packageAppeals:1") {
|
|
return {
|
|
_id: id,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
version: "1.2.3",
|
|
userId: "users:owner",
|
|
message: "please review",
|
|
status: "open",
|
|
createdAt: 123,
|
|
};
|
|
}
|
|
if (id === "packages:demo") return makePackageDoc({ name: "@scope/demo" });
|
|
return null;
|
|
}),
|
|
patch,
|
|
insert,
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageSearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageSearchDigest:demo",
|
|
packageId: "packages:demo",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (
|
|
table === "packageCapabilitySearchDigest" ||
|
|
table === "packageTopicSearchDigest" ||
|
|
table === "packagePluginCategorySearchDigest"
|
|
) {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
} as never,
|
|
{
|
|
actorUserId: "users:moderator",
|
|
appealId: "packageAppeals:1",
|
|
status: "rejected",
|
|
note: "scanner finding still applies",
|
|
},
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
appealId: "packageAppeals:1",
|
|
status: "rejected",
|
|
});
|
|
expect(patch).toHaveBeenCalledWith("packageAppeals:1", {
|
|
status: "rejected",
|
|
resolvedAt: expect.any(Number),
|
|
resolvedBy: "users:moderator",
|
|
resolutionNote: "scanner finding still applies",
|
|
actionTaken: "none",
|
|
});
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({
|
|
action: "package.appeal.resolve",
|
|
targetType: "packageAppeal",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("can approve a package release while accepting an appeal", async () => {
|
|
const patch = vi.fn();
|
|
const insert = vi.fn(async () => "auditLogs:1");
|
|
const pkg = makePackageDoc({ name: "@scope/demo", latestReleaseId: "packageReleases:demo-1" });
|
|
const release = makeReleaseDoc({
|
|
_id: "packageReleases:demo-1",
|
|
packageId: "packages:demo",
|
|
version: "1.2.3",
|
|
manualModeration: {
|
|
state: "quarantined",
|
|
reason: "manual review",
|
|
reviewerUserId: "users:moderator",
|
|
updatedAt: 2,
|
|
},
|
|
verification: { scanStatus: "malicious" },
|
|
});
|
|
|
|
const result = await resolvePackageAppealForUserInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:moderator") return { _id: id, role: "moderator" };
|
|
if (id === "packageAppeals:1") {
|
|
return {
|
|
_id: id,
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:demo-1",
|
|
version: "1.2.3",
|
|
userId: "users:owner",
|
|
message: "please review",
|
|
status: "open",
|
|
createdAt: 123,
|
|
};
|
|
}
|
|
if (id === "packages:demo") return pkg;
|
|
if (id === "packageReleases:demo-1") return release;
|
|
return null;
|
|
}),
|
|
patch,
|
|
insert,
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageSearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageSearchDigest:demo",
|
|
packageId: "packages:demo",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (
|
|
table === "packageCapabilitySearchDigest" ||
|
|
table === "packageTopicSearchDigest" ||
|
|
table === "packagePluginCategorySearchDigest"
|
|
) {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
} as never,
|
|
{
|
|
actorUserId: "users:moderator",
|
|
appealId: "packageAppeals:1",
|
|
status: "accepted",
|
|
note: "false positive confirmed",
|
|
finalAction: "approve",
|
|
},
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
status: "accepted",
|
|
actionTaken: "approve",
|
|
});
|
|
expect(patch).toHaveBeenCalledWith("packageAppeals:1", {
|
|
status: "accepted",
|
|
resolvedAt: expect.any(Number),
|
|
resolvedBy: "users:moderator",
|
|
resolutionNote: "false positive confirmed",
|
|
actionTaken: "approve",
|
|
});
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageReleases:demo-1",
|
|
expect.objectContaining({
|
|
manualModeration: expect.objectContaining({ state: "approved" }),
|
|
}),
|
|
);
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({
|
|
action: "package.release.moderation",
|
|
targetType: "packageRelease",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("upserts official plugin migration rows for admins", async () => {
|
|
let insertedMigration: Record<string, unknown> | null = null;
|
|
const insert = vi.fn(async (table: string, doc: Record<string, unknown>) => {
|
|
if (table === "officialPluginMigrations") {
|
|
insertedMigration = doc;
|
|
return "officialPluginMigrations:1";
|
|
}
|
|
return "auditLogs:1";
|
|
});
|
|
|
|
const result = await upsertOfficialPluginMigrationForUserInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:admin") return { _id: id, role: "admin" };
|
|
if (id === "officialPluginMigrations:1") {
|
|
return { _id: id, ...insertedMigration };
|
|
}
|
|
return null;
|
|
}),
|
|
insert,
|
|
patch: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(makePackageDoc({ _id: "packages:demo" })),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "officialPluginMigrations") {
|
|
return {
|
|
withIndex: vi.fn(() => ({ unique: vi.fn().mockResolvedValue(null) })),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
},
|
|
} as never,
|
|
{
|
|
actorUserId: "users:admin",
|
|
bundledPluginId: "Core.Search",
|
|
packageName: "@scope/demo",
|
|
phase: "blocked",
|
|
blockers: ["missing ClawPack", "missing ClawPack"],
|
|
hostTargetsComplete: true,
|
|
},
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
migration: {
|
|
bundledPluginId: "core.search",
|
|
packageName: "@scope/demo",
|
|
packageId: "packages:demo",
|
|
phase: "blocked",
|
|
blockers: ["missing ClawPack"],
|
|
hostTargetsComplete: true,
|
|
},
|
|
});
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({ action: "package.official_migration.upsert" }),
|
|
);
|
|
});
|
|
|
|
it("lists official plugin migration rows for moderators", async () => {
|
|
const result = await listOfficialPluginMigrationsInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:moderator") return { _id: id, role: "moderator" };
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table !== "officialPluginMigrations") throw new Error(`Unexpected table ${table}`);
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
paginate: vi.fn().mockResolvedValue({
|
|
page: [
|
|
{
|
|
_id: "officialPluginMigrations:1",
|
|
bundledPluginId: "core.search",
|
|
packageName: "@scope/demo",
|
|
packageId: "packages:demo",
|
|
phase: "ready-for-openclaw",
|
|
blockers: [],
|
|
hostTargetsComplete: true,
|
|
scanClean: true,
|
|
moderationApproved: true,
|
|
runtimeBundlesReady: false,
|
|
createdAt: 100,
|
|
updatedAt: 200,
|
|
},
|
|
],
|
|
continueCursor: null,
|
|
isDone: true,
|
|
}),
|
|
})),
|
|
})),
|
|
};
|
|
}),
|
|
},
|
|
} as never,
|
|
{ actorUserId: "users:moderator", phase: "all", limit: 10 },
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
items: [
|
|
expect.objectContaining({
|
|
bundledPluginId: "core.search",
|
|
packageName: "@scope/demo",
|
|
phase: "ready-for-openclaw",
|
|
}),
|
|
],
|
|
nextCursor: null,
|
|
done: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("package scan backfill", () => {
|
|
it("lists blocked package releases for the moderation queue", async () => {
|
|
const result = await listPackageModerationQueueInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:moderator") return { _id: id, role: "moderator" };
|
|
if (id === "packages:demo") {
|
|
return {
|
|
...makePackageDoc(),
|
|
_id: "packages:demo",
|
|
name: "@scope/demo",
|
|
displayName: "Demo",
|
|
family: "code-plugin",
|
|
channel: "community",
|
|
isOfficial: false,
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageReports") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([
|
|
{
|
|
_id: "packageReports:1",
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:latest",
|
|
userId: "users:reporter",
|
|
status: "open",
|
|
createdAt: 500,
|
|
},
|
|
]),
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
if (table !== "packageReleases") throw new Error(`Unexpected table ${table}`);
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
paginate: vi.fn().mockResolvedValue({
|
|
page: [
|
|
{
|
|
_id: "packageReleases:blocked",
|
|
packageId: "packages:demo",
|
|
version: "1.2.3",
|
|
createdAt: 123,
|
|
artifactKind: "npm-pack",
|
|
sha256hash: "a".repeat(64),
|
|
verification: { scanStatus: "suspicious" },
|
|
staticScan: {
|
|
status: "malicious",
|
|
reasonCodes: ["malware.test"],
|
|
findings: [],
|
|
summary: "malware",
|
|
engineVersion: "test",
|
|
checkedAt: 1,
|
|
},
|
|
manualModeration: {
|
|
state: "quarantined",
|
|
reason: "manual review",
|
|
reviewerUserId: "users:moderator",
|
|
updatedAt: 2,
|
|
},
|
|
source: {
|
|
repo: "openclaw/demo",
|
|
commit: "abc123",
|
|
},
|
|
},
|
|
],
|
|
continueCursor: null,
|
|
isDone: true,
|
|
}),
|
|
})),
|
|
})),
|
|
};
|
|
}),
|
|
},
|
|
} as never,
|
|
{ actorUserId: "users:moderator", limit: 10, status: "blocked" },
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
items: [
|
|
expect.objectContaining({
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:blocked",
|
|
name: "@scope/demo",
|
|
version: "1.2.3",
|
|
artifactKind: "npm-pack",
|
|
scanStatus: "malicious",
|
|
moderationState: "quarantined",
|
|
moderationReason: "manual review",
|
|
sourceRepo: "openclaw/demo",
|
|
sourceCommit: "abc123",
|
|
reasons: ["manual:quarantined", "scan:malicious"],
|
|
}),
|
|
],
|
|
nextCursor: null,
|
|
done: true,
|
|
});
|
|
});
|
|
|
|
it("lists reported packages on their latest release for the moderation queue", async () => {
|
|
const result = await listPackageModerationQueueInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:moderator") return { _id: id, role: "moderator" };
|
|
if (id === "packages:demo") {
|
|
return {
|
|
...makePackageDoc({
|
|
_id: "packages:demo",
|
|
name: "@scope/demo",
|
|
displayName: "Demo",
|
|
family: "code-plugin",
|
|
latestReleaseId: "packageReleases:latest",
|
|
reportCount: 2,
|
|
lastReportedAt: 456,
|
|
}),
|
|
};
|
|
}
|
|
if (id === "packageReleases:latest") {
|
|
return makeReleaseDoc({
|
|
_id: "packageReleases:latest",
|
|
packageId: "packages:demo",
|
|
version: "1.1.0",
|
|
createdAt: 101,
|
|
verification: { scanStatus: "clean" },
|
|
});
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageReports") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([
|
|
{
|
|
_id: "packageReports:1",
|
|
packageId: "packages:demo",
|
|
releaseId: "packageReleases:latest",
|
|
userId: "users:reporter",
|
|
status: "open",
|
|
createdAt: 500,
|
|
},
|
|
]),
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
if (table !== "packageReleases") throw new Error(`Unexpected table ${table}`);
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
paginate: vi.fn().mockResolvedValue({
|
|
page: [
|
|
{
|
|
...makeReleaseDoc({
|
|
_id: "packageReleases:old",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
createdAt: 100,
|
|
verification: { scanStatus: "clean" },
|
|
}),
|
|
},
|
|
{
|
|
...makeReleaseDoc({
|
|
_id: "packageReleases:latest",
|
|
packageId: "packages:demo",
|
|
version: "1.1.0",
|
|
createdAt: 101,
|
|
verification: { scanStatus: "clean" },
|
|
}),
|
|
},
|
|
],
|
|
continueCursor: null,
|
|
isDone: true,
|
|
}),
|
|
})),
|
|
})),
|
|
};
|
|
}),
|
|
},
|
|
} as never,
|
|
{ actorUserId: "users:moderator", limit: 10, status: "open" },
|
|
);
|
|
|
|
expect(result.items).toEqual([
|
|
expect.objectContaining({
|
|
releaseId: "packageReleases:latest",
|
|
version: "1.1.0",
|
|
reportCount: 2,
|
|
lastReportedAt: 456,
|
|
reasons: ["reports:2"],
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("normalizes legacy package release timestamps for the moderation queue", async () => {
|
|
const result = await listPackageModerationQueueInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:moderator") return { _id: id, role: "moderator" };
|
|
if (id === "packages:demo") {
|
|
return {
|
|
...makePackageDoc(),
|
|
_id: "packages:demo",
|
|
name: "@scope/demo",
|
|
displayName: "Demo",
|
|
family: "code-plugin",
|
|
channel: "community",
|
|
isOfficial: false,
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table !== "packageReleases") throw new Error(`Unexpected table ${table}`);
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
paginate: vi.fn().mockResolvedValue({
|
|
page: [
|
|
{
|
|
...makeReleaseDoc({
|
|
_id: "packageReleases:legacy",
|
|
_creationTime: 321,
|
|
packageId: "packages:demo",
|
|
createdAt: undefined,
|
|
manualModeration: {
|
|
state: "quarantined",
|
|
reason: "manual review",
|
|
reviewerUserId: "users:moderator",
|
|
updatedAt: 2,
|
|
},
|
|
}),
|
|
},
|
|
],
|
|
continueCursor: null,
|
|
isDone: true,
|
|
}),
|
|
})),
|
|
})),
|
|
};
|
|
}),
|
|
},
|
|
} as never,
|
|
{ actorUserId: "users:moderator", limit: 10, status: "manual" },
|
|
);
|
|
|
|
expect(result.items).toEqual([
|
|
expect.objectContaining({
|
|
releaseId: "packageReleases:legacy",
|
|
createdAt: 321,
|
|
moderationState: "quarantined",
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("includes releases missing static scan in the backfill batch", async () => {
|
|
const result = await getPackageReleaseScanBackfillBatchInternalHandler(
|
|
{
|
|
db: {
|
|
query: vi.fn((table: string) => {
|
|
if (table !== "packageReleases") throw new Error(`Unexpected table ${table}`);
|
|
return {
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([
|
|
{
|
|
_id: "packageReleases:missing-static",
|
|
_creationTime: 10,
|
|
packageId: "packages:demo",
|
|
sha256hash: "hash",
|
|
vtAnalysis: { status: "clean" },
|
|
llmAnalysis: { status: "clean" },
|
|
staticScan: undefined,
|
|
},
|
|
{
|
|
_id: "packageReleases:fully-scanned",
|
|
_creationTime: 11,
|
|
packageId: "packages:demo",
|
|
sha256hash: "hash",
|
|
vtAnalysis: { status: "clean" },
|
|
llmAnalysis: { status: "clean" },
|
|
staticScan: { status: "clean" },
|
|
},
|
|
]),
|
|
})),
|
|
})),
|
|
};
|
|
}),
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packages:demo") return makePackageDoc();
|
|
return null;
|
|
}),
|
|
},
|
|
} as never,
|
|
{ batchSize: 10 },
|
|
);
|
|
|
|
expect(result.releases).toEqual([
|
|
{
|
|
releaseId: "packageReleases:missing-static",
|
|
packageId: "packages:demo",
|
|
needsVt: false,
|
|
needsLlm: false,
|
|
needsStatic: true,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("does not repeatedly rescan a release solely because its artifact hash is missing", async () => {
|
|
const result = await getPackageReleaseScanBackfillBatchInternalHandler(
|
|
{
|
|
db: {
|
|
query: vi.fn((table: string) => {
|
|
if (table !== "packageReleases") throw new Error(`Unexpected table ${table}`);
|
|
return {
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([
|
|
{
|
|
_id: "packageReleases:npm-missing-artifact-hash",
|
|
_creationTime: 10,
|
|
packageId: "packages:demo",
|
|
artifactKind: "npm-pack",
|
|
sha256hash: "legacy-zip-hash",
|
|
vtAnalysis: { status: "clean" },
|
|
llmAnalysis: { status: "clean" },
|
|
staticScan: { status: "clean" },
|
|
},
|
|
]),
|
|
})),
|
|
})),
|
|
};
|
|
}),
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packages:demo") return makePackageDoc();
|
|
return null;
|
|
}),
|
|
},
|
|
} as never,
|
|
{ batchSize: 10 },
|
|
);
|
|
|
|
expect(result.releases).toEqual([]);
|
|
});
|
|
|
|
it("prioritizes recent releases before draining older backlog", async () => {
|
|
const result = await getPackageReleaseScanBackfillBatchInternalHandler(
|
|
{
|
|
db: {
|
|
query: vi.fn((table: string) => {
|
|
if (table !== "packageReleases") throw new Error(`Unexpected table ${table}`);
|
|
return {
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([
|
|
{
|
|
_id: "packageReleases:recent-vt",
|
|
_creationTime: 200,
|
|
packageId: "packages:demo",
|
|
sha256hash: "hash",
|
|
vtAnalysis: undefined,
|
|
llmAnalysis: { status: "clean" },
|
|
staticScan: { status: "clean" },
|
|
},
|
|
]),
|
|
})),
|
|
withIndex: vi.fn(() => ({
|
|
order: vi.fn(() => ({
|
|
take: vi.fn().mockResolvedValue([
|
|
{
|
|
_id: "packageReleases:old-static",
|
|
_creationTime: 10,
|
|
packageId: "packages:demo",
|
|
sha256hash: "hash",
|
|
vtAnalysis: { status: "clean" },
|
|
llmAnalysis: { status: "clean" },
|
|
staticScan: undefined,
|
|
},
|
|
]),
|
|
})),
|
|
})),
|
|
};
|
|
}),
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packages:demo") return makePackageDoc();
|
|
return null;
|
|
}),
|
|
},
|
|
} as never,
|
|
{ batchSize: 2, prioritizeRecent: true },
|
|
);
|
|
|
|
expect(result.releases).toEqual([
|
|
{
|
|
releaseId: "packageReleases:recent-vt",
|
|
packageId: "packages:demo",
|
|
needsVt: true,
|
|
needsLlm: false,
|
|
needsStatic: false,
|
|
},
|
|
{
|
|
releaseId: "packageReleases:old-static",
|
|
packageId: "packages:demo",
|
|
needsVt: false,
|
|
needsLlm: false,
|
|
needsStatic: true,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("schedules static rescans for releases missing only static scan data", async () => {
|
|
const originalVtApiKey = process.env.VT_API_KEY;
|
|
process.env.VT_API_KEY = "vt-test-key";
|
|
|
|
try {
|
|
const runAfter = vi.fn().mockResolvedValue(undefined);
|
|
const result = await backfillPackageReleaseScansInternalHandler(
|
|
{
|
|
runQuery: vi.fn().mockResolvedValue({
|
|
releases: [
|
|
{
|
|
releaseId: "packageReleases:static-only",
|
|
needsVt: false,
|
|
needsLlm: false,
|
|
needsStatic: true,
|
|
},
|
|
],
|
|
nextCursor: 123,
|
|
done: true,
|
|
}),
|
|
scheduler: { runAfter },
|
|
} as never,
|
|
{ batchSize: 10 },
|
|
);
|
|
|
|
expect(result).toEqual({ scheduled: 1, nextCursor: 123, done: true });
|
|
expect(runAfter).toHaveBeenCalledTimes(1);
|
|
expect(runAfter).toHaveBeenCalledWith(
|
|
0,
|
|
expect.anything(),
|
|
expect.objectContaining({ releaseId: "packageReleases:static-only" }),
|
|
);
|
|
} finally {
|
|
if (originalVtApiKey === undefined) {
|
|
delete process.env.VT_API_KEY;
|
|
} else {
|
|
process.env.VT_API_KEY = originalVtApiKey;
|
|
}
|
|
}
|
|
});
|
|
|
|
it("backfills legacy static-only package scan status into the package search digest", async () => {
|
|
const verification = {
|
|
tier: "source-linked",
|
|
scope: "artifact-only",
|
|
scanStatus: "malicious",
|
|
};
|
|
const pkg = makePackageDoc({
|
|
scanStatus: "malicious",
|
|
verification,
|
|
latestVersionSummary: {
|
|
version: "1.0.0",
|
|
verification,
|
|
},
|
|
});
|
|
const release = makeReleaseDoc({
|
|
sha256hash: "a".repeat(64),
|
|
verification,
|
|
staticScan: {
|
|
status: "malicious",
|
|
reasonCodes: ["malicious.static_fixture"],
|
|
findings: [],
|
|
summary: "Static fixture only.",
|
|
engineVersion: "test",
|
|
checkedAt: 1,
|
|
},
|
|
llmAnalysis: {
|
|
status: "clean",
|
|
verdict: "clean",
|
|
checkedAt: 2,
|
|
},
|
|
});
|
|
const patch = vi.fn();
|
|
|
|
const result = await backfillLatestPackageScanStatusInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packageReleases:demo-1") return release;
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
paginate: vi.fn().mockResolvedValue({
|
|
page: [pkg],
|
|
continueCursor: null,
|
|
isDone: true,
|
|
}),
|
|
};
|
|
}
|
|
if (table === "packageSearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageSearchDigest:demo",
|
|
packageId: "packages:demo",
|
|
scanStatus: "malicious",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (
|
|
table === "packageCapabilitySearchDigest" ||
|
|
table === "packageTopicSearchDigest" ||
|
|
table === "packagePluginCategorySearchDigest"
|
|
) {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
patch,
|
|
insert: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
scheduler: { runAfter: vi.fn() },
|
|
} as never,
|
|
{ batchSize: 10 },
|
|
);
|
|
|
|
expect(result).toEqual({ patched: 1, isDone: true, scanned: 1 });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageReleases:demo-1",
|
|
expect.objectContaining({
|
|
verification: expect.objectContaining({ scanStatus: "clean" }),
|
|
}),
|
|
);
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
scanStatus: "clean",
|
|
verification: expect.objectContaining({ scanStatus: "clean" }),
|
|
latestVersionSummary: expect.objectContaining({
|
|
verification: expect.objectContaining({ scanStatus: "clean" }),
|
|
}),
|
|
}),
|
|
);
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageSearchDigest:demo",
|
|
expect.objectContaining({ scanStatus: "clean" }),
|
|
);
|
|
});
|
|
|
|
it("repairs stale package search digests even when package scan status is already current", async () => {
|
|
const verification = {
|
|
tier: "source-linked",
|
|
scope: "artifact-only",
|
|
scanStatus: "clean",
|
|
};
|
|
const pkg = makePackageDoc({
|
|
ownerPublisherId: "publishers:owner",
|
|
scanStatus: "clean",
|
|
verification,
|
|
latestVersionSummary: {
|
|
version: "1.0.0",
|
|
verification,
|
|
},
|
|
});
|
|
const release = makeReleaseDoc({
|
|
verification,
|
|
llmAnalysis: {
|
|
status: "clean",
|
|
verdict: "clean",
|
|
checkedAt: 2,
|
|
},
|
|
});
|
|
const patch = vi.fn();
|
|
|
|
const result = await backfillLatestPackageScanStatusInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packageReleases:demo-1") return release;
|
|
if (id === "publishers:owner") {
|
|
return {
|
|
_id: "publishers:owner",
|
|
kind: "user",
|
|
handle: "tongfei11",
|
|
linkedUserId: "users:owner",
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
paginate: vi.fn().mockResolvedValue({
|
|
page: [pkg],
|
|
continueCursor: null,
|
|
isDone: true,
|
|
}),
|
|
};
|
|
}
|
|
if (table === "packageSearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageSearchDigest:demo",
|
|
packageId: "packages:demo",
|
|
scanStatus: "malicious",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (
|
|
table === "packagePluginCategorySearchDigest" ||
|
|
table === "packageTopicSearchDigest"
|
|
) {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table ${table}`);
|
|
}),
|
|
patch,
|
|
insert: vi.fn(),
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
scheduler: { runAfter: vi.fn() },
|
|
} as never,
|
|
{ batchSize: 10 },
|
|
);
|
|
|
|
expect(result).toEqual({ patched: 0, isDone: true, scanned: 1 });
|
|
expect(patch).not.toHaveBeenCalledWith("packages:demo", expect.anything());
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageSearchDigest:demo",
|
|
expect.objectContaining({
|
|
scanStatus: "clean",
|
|
ownerHandle: "tongfei11",
|
|
ownerKind: "user",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("stores static package scan results without promoting malware status", async () => {
|
|
const patch = vi.fn().mockResolvedValue(undefined);
|
|
const release = {
|
|
_id: "packageReleases:demo-1",
|
|
packageId: "packages:demo",
|
|
verification: {
|
|
tier: "source-linked",
|
|
scope: "artifact-only",
|
|
scanStatus: "pending",
|
|
},
|
|
softDeletedAt: undefined,
|
|
};
|
|
const pkg = {
|
|
...makePackageDoc(),
|
|
_id: "packages:demo",
|
|
latestReleaseId: "packageReleases:demo-1",
|
|
verification: {
|
|
tier: "source-linked",
|
|
scope: "artifact-only",
|
|
scanStatus: "pending",
|
|
},
|
|
latestVersionSummary: {
|
|
version: "1.0.0",
|
|
verification: {
|
|
tier: "source-linked",
|
|
scope: "artifact-only",
|
|
scanStatus: "pending",
|
|
},
|
|
},
|
|
};
|
|
|
|
await updateReleaseStaticScanInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packageReleases:demo-1") return release;
|
|
if (id === "packages:demo") return pkg;
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageSearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageSearchDigest:demo",
|
|
packageId: "packages:demo",
|
|
scanStatus: "pending",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (
|
|
table === "packageCapabilitySearchDigest" ||
|
|
table === "packageTopicSearchDigest" ||
|
|
table === "packagePluginCategorySearchDigest"
|
|
) {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected query table: ${table}`);
|
|
}),
|
|
insert: vi.fn(),
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
} as never,
|
|
{
|
|
releaseId: "packageReleases:demo-1",
|
|
staticScan: {
|
|
status: "malicious",
|
|
reasonCodes: ["malware.test"],
|
|
findings: [],
|
|
summary: "Malware detected",
|
|
engineVersion: "test",
|
|
checkedAt: 1,
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(patch).toHaveBeenNthCalledWith(
|
|
1,
|
|
"packageReleases:demo-1",
|
|
expect.objectContaining({
|
|
staticScan: expect.objectContaining({ status: "malicious" }),
|
|
verification: expect.objectContaining({ scanStatus: "pending" }),
|
|
}),
|
|
);
|
|
expect(patch).toHaveBeenNthCalledWith(
|
|
2,
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
scanStatus: "pending",
|
|
verification: expect.objectContaining({ scanStatus: "pending" }),
|
|
latestVersionSummary: expect.objectContaining({
|
|
verification: expect.objectContaining({ scanStatus: "pending" }),
|
|
}),
|
|
}),
|
|
);
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageSearchDigest:demo",
|
|
expect.objectContaining({ scanStatus: "pending" }),
|
|
);
|
|
});
|
|
|
|
it("quarantines a malicious latest plugin release and restores the previous clean latest", async () => {
|
|
vi.spyOn(Date, "now").mockReturnValue(1_700_000_000_000);
|
|
const patch = vi.fn().mockResolvedValue(undefined);
|
|
const runAfter = vi.fn().mockResolvedValue(undefined);
|
|
const previousRelease = makeReleaseDoc({
|
|
_id: "packageReleases:demo-1",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
distTags: [],
|
|
verification: { scanStatus: "clean" },
|
|
createdAt: 1_600_000_000_000,
|
|
});
|
|
const candidateRelease = makeReleaseDoc({
|
|
_id: "packageReleases:demo-2",
|
|
packageId: "packages:demo",
|
|
version: "2.0.0",
|
|
runtimeId: "demo.plugin",
|
|
sourceRepo: "openclaw/demo-malicious",
|
|
distTags: ["latest"],
|
|
verification: { scanStatus: "pending" },
|
|
createdBy: "users:member",
|
|
publishActor: { kind: "user", userId: "users:member" },
|
|
staticScan: {
|
|
status: "clean",
|
|
reasonCodes: [],
|
|
findings: [],
|
|
summary: "No static findings.",
|
|
engineVersion: "test",
|
|
checkedAt: 1,
|
|
},
|
|
createdAt: 1_700_000_000_000,
|
|
});
|
|
const pkg = makePackageDoc({
|
|
_id: "packages:demo",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:org",
|
|
runtimeId: "malicious.plugin",
|
|
sourceRepo: "openclaw/demo-malicious",
|
|
latestReleaseId: "packageReleases:demo-2",
|
|
tags: { latest: "packageReleases:demo-2" },
|
|
latestVersionSummary: { version: "2.0.0", verification: { scanStatus: "pending" } },
|
|
verification: { scanStatus: "pending" },
|
|
scanStatus: "pending",
|
|
});
|
|
|
|
await updateReleaseLlmAnalysisInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packageReleases:demo-2") return candidateRelease;
|
|
if (id === "packages:demo") return pkg;
|
|
if (id === "publishers:org") {
|
|
return {
|
|
_id: "publishers:org",
|
|
kind: "org",
|
|
handle: "org",
|
|
deletedAt: undefined,
|
|
deactivatedAt: undefined,
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([previousRelease, candidateRelease]),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageSearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageSearchDigest:demo",
|
|
packageId: "packages:demo",
|
|
scanStatus: "pending",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (
|
|
table === "packageCapabilitySearchDigest" ||
|
|
table === "packageTopicSearchDigest" ||
|
|
table === "packagePluginCategorySearchDigest"
|
|
) {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected query table: ${table}`);
|
|
}),
|
|
insert: vi.fn(),
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
scheduler: { runAfter },
|
|
} as never,
|
|
{
|
|
releaseId: "packageReleases:demo-2",
|
|
llmAnalysis: {
|
|
status: "malicious",
|
|
verdict: "malicious",
|
|
confidence: "high",
|
|
summary: "ClawScan found malicious behavior.",
|
|
guidance: "Fix locally and rescan.",
|
|
checkedAt: 1_700_000_000_000,
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageReleases:demo-2",
|
|
expect.objectContaining({
|
|
softDeletedAt: 1_700_000_000_000,
|
|
verification: expect.objectContaining({ scanStatus: "malicious" }),
|
|
}),
|
|
);
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageReleases:demo-1",
|
|
expect.objectContaining({ distTags: ["latest"] }),
|
|
);
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
latestReleaseId: "packageReleases:demo-1",
|
|
runtimeId: undefined,
|
|
sourceRepo: undefined,
|
|
scanStatus: "clean",
|
|
tags: { latest: "packageReleases:demo-1" },
|
|
}),
|
|
);
|
|
expect(runAfter).toHaveBeenCalledWith(
|
|
0,
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
ownerUserId: "users:member",
|
|
artifactKind: "plugin",
|
|
artifactName: "demo-plugin",
|
|
version: "2.0.0",
|
|
findingSummary: "ClawScan found malicious behavior.",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("quarantines a malicious non-latest plugin release without changing the clean latest", async () => {
|
|
vi.spyOn(Date, "now").mockReturnValue(1_700_000_000_000);
|
|
const patch = vi.fn().mockResolvedValue(undefined);
|
|
const runAfter = vi.fn().mockResolvedValue(undefined);
|
|
const candidateRelease = makeReleaseDoc({
|
|
_id: "packageReleases:demo-beta",
|
|
packageId: "packages:demo",
|
|
version: "1.5.0",
|
|
distTags: ["beta"],
|
|
verification: { scanStatus: "pending" },
|
|
createdBy: "users:member",
|
|
publishActor: { kind: "user", userId: "users:member" },
|
|
staticScan: {
|
|
status: "clean",
|
|
reasonCodes: [],
|
|
findings: [],
|
|
summary: "No static findings.",
|
|
engineVersion: "test",
|
|
checkedAt: 1,
|
|
},
|
|
createdAt: 1_650_000_000_000,
|
|
});
|
|
const pkg = makePackageDoc({
|
|
_id: "packages:demo",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:org",
|
|
latestReleaseId: "packageReleases:demo-latest",
|
|
tags: {
|
|
latest: "packageReleases:demo-latest",
|
|
beta: "packageReleases:demo-beta",
|
|
},
|
|
latestVersionSummary: { version: "2.0.0", verification: { scanStatus: "clean" } },
|
|
verification: { scanStatus: "clean" },
|
|
scanStatus: "clean",
|
|
});
|
|
|
|
await updateReleaseLlmAnalysisInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packageReleases:demo-beta") return candidateRelease;
|
|
if (id === "packages:demo") return pkg;
|
|
if (id === "publishers:org") {
|
|
return {
|
|
_id: "publishers:org",
|
|
kind: "org",
|
|
handle: "org",
|
|
deletedAt: undefined,
|
|
deactivatedAt: undefined,
|
|
};
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageSearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageSearchDigest:demo",
|
|
packageId: "packages:demo",
|
|
scanStatus: "clean",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (
|
|
table === "packageCapabilitySearchDigest" ||
|
|
table === "packageTopicSearchDigest" ||
|
|
table === "packagePluginCategorySearchDigest"
|
|
) {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected query table: ${table}`);
|
|
}),
|
|
insert: vi.fn(),
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
scheduler: { runAfter },
|
|
} as never,
|
|
{
|
|
releaseId: "packageReleases:demo-beta",
|
|
llmAnalysis: {
|
|
status: "malicious",
|
|
verdict: "malicious",
|
|
confidence: "high",
|
|
summary: "ClawScan found malicious behavior.",
|
|
guidance: "Fix locally and rescan.",
|
|
checkedAt: 1_700_000_000_000,
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageReleases:demo-beta",
|
|
expect.objectContaining({
|
|
llmAnalysis: expect.objectContaining({ verdict: "malicious" }),
|
|
}),
|
|
);
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageReleases:demo-beta",
|
|
expect.objectContaining({
|
|
softDeletedAt: 1_700_000_000_000,
|
|
verification: expect.objectContaining({ scanStatus: "malicious" }),
|
|
}),
|
|
);
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
tags: { latest: "packageReleases:demo-latest" },
|
|
}),
|
|
);
|
|
expect(patch).not.toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({ latestReleaseId: "packageReleases:demo-beta" }),
|
|
);
|
|
expect(runAfter).toHaveBeenCalledWith(
|
|
0,
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
ownerUserId: "users:member",
|
|
artifactKind: "plugin",
|
|
artifactName: "demo-plugin",
|
|
version: "1.5.0",
|
|
findingSummary: "ClawScan found malicious behavior.",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("keeps a first malicious plugin release out of public package lists", async () => {
|
|
vi.spyOn(Date, "now").mockReturnValue(1_700_000_000_000);
|
|
const patch = vi.fn().mockResolvedValue(undefined);
|
|
const candidateRelease = makeReleaseDoc({
|
|
_id: "packageReleases:demo-1",
|
|
packageId: "packages:demo",
|
|
version: "1.0.0",
|
|
distTags: ["latest"],
|
|
verification: { scanStatus: "pending" },
|
|
createdAt: 1_700_000_000_000,
|
|
});
|
|
const pkg = makePackageDoc({
|
|
_id: "packages:demo",
|
|
latestReleaseId: "packageReleases:demo-1",
|
|
tags: { latest: "packageReleases:demo-1" },
|
|
latestVersionSummary: { version: "1.0.0", verification: { scanStatus: "pending" } },
|
|
verification: { scanStatus: "pending" },
|
|
scanStatus: "pending",
|
|
});
|
|
|
|
await updateReleaseLlmAnalysisInternalHandler(
|
|
{
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "packageReleases:demo-1") return candidateRelease;
|
|
if (id === "packages:demo") return pkg;
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([candidateRelease]),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageSearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue({
|
|
_id: "packageSearchDigest:demo",
|
|
packageId: "packages:demo",
|
|
scanStatus: "pending",
|
|
}),
|
|
})),
|
|
};
|
|
}
|
|
if (
|
|
table === "packageCapabilitySearchDigest" ||
|
|
table === "packageTopicSearchDigest" ||
|
|
table === "packagePluginCategorySearchDigest"
|
|
) {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected query table: ${table}`);
|
|
}),
|
|
insert: vi.fn(),
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
} as never,
|
|
{
|
|
releaseId: "packageReleases:demo-1",
|
|
llmAnalysis: {
|
|
status: "malicious",
|
|
verdict: "malicious",
|
|
confidence: "high",
|
|
summary: "ClawScan found malicious behavior.",
|
|
guidance: "Fix locally and rescan.",
|
|
checkedAt: 1_700_000_000_000,
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
latestReleaseId: undefined,
|
|
latestVersionSummary: undefined,
|
|
scanStatus: "malicious",
|
|
tags: {},
|
|
}),
|
|
);
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageSearchDigest:demo",
|
|
expect.objectContaining({
|
|
latestVersion: undefined,
|
|
scanStatus: "malicious",
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// softDeletePackageInternal / restorePackageInternal
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Build a ctx that exercises the full softDeletePackageDoc / restorePackageDoc
|
|
* path, including the upsertPackageSearchDigest branch that previously crashed
|
|
* when ownerHandle was missing.
|
|
*/
|
|
function makeSoftDeleteCtx(options?: {
|
|
pkg?: Record<string, unknown>;
|
|
actor?: Record<string, unknown>;
|
|
/** When true, no retired digest rows exist. */
|
|
noCapabilityDigest?: boolean;
|
|
/** Personal publisher linked to the owner user. */
|
|
personalPublisher?: Record<string, unknown> | null;
|
|
/** Override the releases returned for this package. */
|
|
releases?: Array<Record<string, unknown>>;
|
|
}) {
|
|
const pkg =
|
|
options?.pkg ??
|
|
makePackageDoc({
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner-personal",
|
|
});
|
|
|
|
const personalPublisher =
|
|
options?.personalPublisher === undefined
|
|
? {
|
|
_id: "publishers:owner-personal",
|
|
kind: "user",
|
|
handle: "tongfei11",
|
|
linkedUserId: "users:owner",
|
|
}
|
|
: options.personalPublisher;
|
|
|
|
const existingCapabilityRows =
|
|
options?.noCapabilityDigest === true
|
|
? []
|
|
: [
|
|
{
|
|
_id: "packageCapabilitySearchDigest:demo-read-files",
|
|
packageId: pkg._id,
|
|
capabilityTag: "read-files",
|
|
ownerHandle: "tongfei11",
|
|
},
|
|
];
|
|
|
|
const releases = options?.releases ?? [
|
|
makeReleaseDoc({ _id: "packageReleases:demo-1", softDeletedAt: undefined }),
|
|
];
|
|
|
|
const patch = vi.fn();
|
|
const insert = vi.fn().mockResolvedValue("auditLogs:1");
|
|
|
|
return {
|
|
patch,
|
|
insert,
|
|
ctx: {
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:owner") return options?.actor ?? { _id: id, role: "user" };
|
|
if (id === "publishers:owner-personal") return personalPublisher;
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(pkg),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue(releases),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageSearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi
|
|
.fn()
|
|
.mockResolvedValue({ _id: "packageSearchDigest:demo", packageId: pkg._id }),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageCapabilitySearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue(existingCapabilityRows),
|
|
})),
|
|
};
|
|
}
|
|
if (
|
|
table === "packageTopicSearchDigest" ||
|
|
table === "packagePluginCategorySearchDigest"
|
|
) {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "publisherMemberships") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi.fn().mockResolvedValue(null),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table: ${table}`);
|
|
}),
|
|
insert,
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function makeOwnedPackageBatchCtx(options?: {
|
|
pkg?: Record<string, unknown>;
|
|
owner?: Record<string, unknown> | null;
|
|
packageTokens?: Array<Record<string, unknown>>;
|
|
releases?: Array<Record<string, unknown>>;
|
|
publisherPackages?: Array<Record<string, unknown>>;
|
|
publishers?: Record<string, Record<string, unknown> | null>;
|
|
isDone?: boolean;
|
|
continueCursor?: string;
|
|
}) {
|
|
const pkg = options?.pkg ?? makePackageDoc({ ownerUserId: "users:owner" });
|
|
const releases = options?.releases ?? [
|
|
makeReleaseDoc({ _id: "packageReleases:demo-1", packageId: pkg._id }),
|
|
];
|
|
const packageTokens = options?.packageTokens ?? [
|
|
{
|
|
_id: "packagePublishTokens:demo",
|
|
packageId: pkg._id,
|
|
version: "1.0.1",
|
|
revokedAt: undefined,
|
|
},
|
|
];
|
|
const patch = vi.fn();
|
|
const insert = vi.fn().mockResolvedValue("auditLogs:1");
|
|
const runAfter = vi.fn();
|
|
|
|
return {
|
|
patch,
|
|
insert,
|
|
runAfter,
|
|
ctx: {
|
|
scheduler: { runAfter },
|
|
db: {
|
|
get: vi.fn(async (id: string) => {
|
|
if (id === "users:admin") {
|
|
return {
|
|
_id: "users:admin",
|
|
role: "admin",
|
|
deletedAt: undefined,
|
|
deactivatedAt: undefined,
|
|
};
|
|
}
|
|
if (id === "users:owner") {
|
|
return options?.owner === undefined
|
|
? { _id: "users:owner", deletedAt: undefined, deactivatedAt: undefined }
|
|
: options.owner;
|
|
}
|
|
if (options?.publishers && id in options.publishers) {
|
|
return options.publishers[id];
|
|
}
|
|
return null;
|
|
}),
|
|
query: vi.fn((table: string) => {
|
|
if (table === "packages") {
|
|
return {
|
|
withIndex: vi.fn((index: string) => ({
|
|
order: vi.fn(() => ({
|
|
paginate: vi.fn().mockResolvedValue({
|
|
page:
|
|
index === "by_owner_publisher" ? (options?.publisherPackages ?? []) : [pkg],
|
|
isDone: options?.isDone ?? true,
|
|
continueCursor: options?.continueCursor ?? "",
|
|
}),
|
|
})),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "publishers") {
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
index: string,
|
|
cb: (q: { eq: (field: string, value: string) => unknown }) => unknown,
|
|
) => {
|
|
expect(index).toBe("by_linked_user");
|
|
let linkedUserId = "";
|
|
cb({
|
|
eq: (field: string, value: string) => {
|
|
if (field === "linkedUserId") linkedUserId = value;
|
|
return {};
|
|
},
|
|
});
|
|
return {
|
|
unique: vi.fn(
|
|
async () =>
|
|
Object.values(options?.publishers ?? {}).find(
|
|
(publisher) =>
|
|
publisher?.kind === "user" && publisher.linkedUserId === linkedUserId,
|
|
) ?? null,
|
|
),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
}
|
|
if (table === "packagePublishTokens") {
|
|
return {
|
|
withIndex: vi.fn(
|
|
(
|
|
_index: string,
|
|
builder?: (q: {
|
|
eq: (field: string, value: string | undefined) => unknown;
|
|
lte: (field: string, value: number) => unknown;
|
|
}) => unknown,
|
|
) => {
|
|
let maxCreatedAt = Number.POSITIVE_INFINITY;
|
|
const queryBuilder = {
|
|
eq: () => queryBuilder,
|
|
lte: (field: string, value: number) => {
|
|
if (field === "createdAt") maxCreatedAt = value;
|
|
return queryBuilder;
|
|
},
|
|
};
|
|
builder?.(queryBuilder);
|
|
return {
|
|
order: vi.fn(() => ({
|
|
take: vi.fn(async (limit: number) =>
|
|
packageTokens
|
|
.filter(
|
|
(token) =>
|
|
typeof token.createdAt !== "number" ||
|
|
token.createdAt <= maxCreatedAt,
|
|
)
|
|
.slice(0, limit),
|
|
),
|
|
})),
|
|
};
|
|
},
|
|
),
|
|
};
|
|
}
|
|
if (table === "packageReleases") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue(releases),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageSearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
unique: vi
|
|
.fn()
|
|
.mockResolvedValue({ _id: "packageSearchDigest:demo", packageId: pkg._id }),
|
|
})),
|
|
};
|
|
}
|
|
if (table === "packageCapabilitySearchDigest") {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
if (
|
|
table === "packageTopicSearchDigest" ||
|
|
table === "packagePluginCategorySearchDigest"
|
|
) {
|
|
return {
|
|
withIndex: vi.fn(() => ({
|
|
collect: vi.fn().mockResolvedValue([]),
|
|
})),
|
|
};
|
|
}
|
|
throw new Error(`Unexpected table: ${table}`);
|
|
}),
|
|
insert,
|
|
patch,
|
|
replace: vi.fn(),
|
|
delete: vi.fn(),
|
|
normalizeId: vi.fn(),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("owned package sanction batches", () => {
|
|
it("soft-deletes owned packages with a ban reason and revokes package publish tokens", async () => {
|
|
const { ctx, patch } = makeOwnedPackageBatchCtx({
|
|
owner: { _id: "users:owner", deletedAt: 1_000, deactivatedAt: undefined },
|
|
});
|
|
|
|
const result = await applyBanToOwnedPackagesBatchInternalHandler(ctx as never, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
deletedBy: "users:moderator",
|
|
deletedByRole: "moderator",
|
|
});
|
|
|
|
expect(result).toMatchObject({ deletedCount: 1, revokedTokenCount: 1, scheduled: false });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
softDeletedAt: 1_000,
|
|
softDeletedReason: "user.banned",
|
|
softDeletedBy: "users:moderator",
|
|
softDeletedByRole: "moderator",
|
|
}),
|
|
);
|
|
expect(patch).toHaveBeenCalledWith("packagePublishTokens:demo", { revokedAt: 1_000 });
|
|
});
|
|
|
|
it("bounds package publish token revocation during owned package bans", async () => {
|
|
const packageTokens = Array.from({ length: 26 }, (_, index) => ({
|
|
_id: `packagePublishTokens:active-${index}`,
|
|
packageId: "packages:demo",
|
|
version: "1.0.1",
|
|
revokedAt: undefined,
|
|
}));
|
|
const { ctx, patch, runAfter } = makeOwnedPackageBatchCtx({
|
|
owner: { _id: "users:owner", deletedAt: 1_000, deactivatedAt: undefined },
|
|
packageTokens,
|
|
});
|
|
|
|
const result = await applyBanToOwnedPackagesBatchInternalHandler(ctx as never, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
deletedBy: "users:moderator",
|
|
deletedByRole: "moderator",
|
|
});
|
|
|
|
expect(result).toMatchObject({ deletedCount: 1, revokedTokenCount: 25, scheduled: false });
|
|
expect(patch).toHaveBeenCalledWith("packagePublishTokens:active-24", { revokedAt: 1_000 });
|
|
expect(patch).not.toHaveBeenCalledWith("packagePublishTokens:active-25", expect.anything());
|
|
expect(runAfter).toHaveBeenCalledWith(0, expect.anything(), {
|
|
packageId: "packages:demo",
|
|
revokedAt: 1_000,
|
|
});
|
|
});
|
|
|
|
it("does not let stale token revocation batches revoke tokens minted after the ban marker", async () => {
|
|
const { ctx, patch } = makeOwnedPackageBatchCtx({
|
|
packageTokens: [
|
|
{
|
|
_id: "packagePublishTokens:before-ban",
|
|
packageId: "packages:demo",
|
|
version: "1.0.1",
|
|
revokedAt: undefined,
|
|
createdAt: 999,
|
|
},
|
|
{
|
|
_id: "packagePublishTokens:after-unban",
|
|
packageId: "packages:demo",
|
|
version: "1.0.2",
|
|
revokedAt: undefined,
|
|
createdAt: 1_001,
|
|
},
|
|
],
|
|
});
|
|
|
|
const result = await revokePackagePublishTokensForPackageBatchInternalHandler(ctx as never, {
|
|
packageId: "packages:demo",
|
|
revokedAt: 1_000,
|
|
});
|
|
|
|
expect(result).toMatchObject({ revokedCount: 1, scheduled: false });
|
|
expect(patch).toHaveBeenCalledWith("packagePublishTokens:before-ban", { revokedAt: 1_000 });
|
|
expect(patch).not.toHaveBeenCalledWith("packagePublishTokens:after-unban", expect.anything());
|
|
});
|
|
|
|
it("soft-deletes packages owned through the user's personal publisher", async () => {
|
|
const personalPublisherPackage = makePackageDoc({
|
|
_id: "packages:personal-publisher",
|
|
ownerUserId: "users:publishing-actor",
|
|
ownerPublisherId: "publishers:personal",
|
|
});
|
|
const { ctx, patch } = makeOwnedPackageBatchCtx({
|
|
owner: {
|
|
_id: "users:owner",
|
|
deletedAt: 1_000,
|
|
deactivatedAt: undefined,
|
|
personalPublisherId: "publishers:personal",
|
|
},
|
|
publisherPackages: [personalPublisherPackage],
|
|
packageTokens: [
|
|
{
|
|
_id: "packagePublishTokens:personal-publisher",
|
|
packageId: "packages:personal-publisher",
|
|
version: "1.0.1",
|
|
revokedAt: undefined,
|
|
},
|
|
],
|
|
publishers: {
|
|
"publishers:personal": {
|
|
_id: "publishers:personal",
|
|
kind: "user",
|
|
linkedUserId: "users:owner",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await applyBanToOwnedPackagesBatchInternalHandler(ctx as never, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
deletedBy: "users:moderator",
|
|
deletedByRole: "moderator",
|
|
scope: "personalPublisher",
|
|
});
|
|
|
|
expect(result).toMatchObject({ deletedCount: 1, revokedTokenCount: 1, scheduled: false });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:personal-publisher",
|
|
expect.objectContaining({ softDeletedAt: 1_000, softDeletedReason: "user.banned" }),
|
|
);
|
|
expect(patch).toHaveBeenCalledWith("packagePublishTokens:personal-publisher", {
|
|
revokedAt: 1_000,
|
|
});
|
|
});
|
|
|
|
it("schedules hard deletes for packages owned by a deleted publisher", async () => {
|
|
const orgPackage = makePackageDoc({
|
|
_id: "packages:org-plugin",
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:org",
|
|
});
|
|
const { ctx, patch, runAfter } = makeOwnedPackageBatchCtx({
|
|
publisherPackages: [orgPackage],
|
|
releases: [
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:org-plugin-1",
|
|
packageId: "packages:org-plugin",
|
|
}),
|
|
],
|
|
packageTokens: [
|
|
{
|
|
_id: "packagePublishTokens:org-plugin",
|
|
packageId: "packages:org-plugin",
|
|
version: "1.0.0",
|
|
revokedAt: undefined,
|
|
},
|
|
],
|
|
publishers: {
|
|
"publishers:org": {
|
|
_id: "publishers:org",
|
|
kind: "org",
|
|
deletedAt: 3_000,
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await applyPublisherDeletionToOwnedPackagesBatchInternalHandler(ctx as never, {
|
|
ownerPublisherId: "publishers:org",
|
|
actorUserId: "users:owner",
|
|
deletedAt: 3_000,
|
|
});
|
|
|
|
expect(result).toMatchObject({ deletedCount: 1, revokedTokenCount: 0, scheduled: false });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:org-plugin",
|
|
expect.objectContaining({
|
|
softDeletedAt: 3_000,
|
|
softDeletedReason: "publisher.deleted",
|
|
softDeletedBy: "users:owner",
|
|
softDeletedByRole: "user",
|
|
}),
|
|
);
|
|
expect(runAfter).toHaveBeenCalledWith(0, expect.anything(), {
|
|
packageId: "packages:org-plugin",
|
|
actorUserId: "users:owner",
|
|
deletedAt: 3_000,
|
|
source: "publisher.delete",
|
|
});
|
|
expect(patch).not.toHaveBeenCalledWith("packagePublishTokens:org-plugin", expect.anything());
|
|
});
|
|
|
|
it("schedules linked legacy personal publisher scans when the user row lacks the publisher id", async () => {
|
|
const { ctx, runAfter } = makeOwnedPackageBatchCtx({
|
|
owner: {
|
|
_id: "users:owner",
|
|
deletedAt: 1_000,
|
|
deactivatedAt: undefined,
|
|
},
|
|
publishers: {
|
|
"publishers:personal": {
|
|
_id: "publishers:personal",
|
|
kind: "user",
|
|
linkedUserId: "users:owner",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await applyBanToOwnedPackagesBatchInternalHandler(ctx as never, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
deletedBy: "users:moderator",
|
|
deletedByRole: "moderator",
|
|
});
|
|
|
|
expect(result).toMatchObject({ scheduled: true });
|
|
expect(runAfter).toHaveBeenCalledWith(
|
|
0,
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
scope: "personalPublisher",
|
|
cursor: undefined,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("soft-deletes linked legacy personal publisher packages without users.personalPublisherId", async () => {
|
|
const personalPublisherPackage = makePackageDoc({
|
|
_id: "packages:personal-publisher",
|
|
ownerUserId: "users:publishing-actor",
|
|
ownerPublisherId: "publishers:personal",
|
|
});
|
|
const { ctx, patch } = makeOwnedPackageBatchCtx({
|
|
owner: {
|
|
_id: "users:owner",
|
|
deletedAt: 1_000,
|
|
deactivatedAt: undefined,
|
|
},
|
|
publisherPackages: [personalPublisherPackage],
|
|
packageTokens: [
|
|
{
|
|
_id: "packagePublishTokens:personal-publisher",
|
|
packageId: "packages:personal-publisher",
|
|
version: "1.0.1",
|
|
revokedAt: undefined,
|
|
},
|
|
],
|
|
publishers: {
|
|
"publishers:personal": {
|
|
_id: "publishers:personal",
|
|
kind: "user",
|
|
linkedUserId: "users:owner",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await applyBanToOwnedPackagesBatchInternalHandler(ctx as never, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
deletedBy: "users:moderator",
|
|
deletedByRole: "moderator",
|
|
scope: "personalPublisher",
|
|
});
|
|
|
|
expect(result).toMatchObject({ deletedCount: 1, revokedTokenCount: 1, scheduled: false });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:personal-publisher",
|
|
expect.objectContaining({ softDeletedAt: 1_000, softDeletedReason: "user.banned" }),
|
|
);
|
|
expect(patch).toHaveBeenCalledWith("packagePublishTokens:personal-publisher", {
|
|
revokedAt: 1_000,
|
|
});
|
|
});
|
|
|
|
it("processes the initial package ban batch before the user ban is visible", async () => {
|
|
const { ctx, patch } = makeOwnedPackageBatchCtx({
|
|
owner: { _id: "users:owner", deletedAt: undefined, deactivatedAt: undefined },
|
|
});
|
|
|
|
const result = await applyBanToOwnedPackagesBatchInternalHandler(ctx as never, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
deletedBy: "users:moderator",
|
|
deletedByRole: "moderator",
|
|
});
|
|
|
|
expect(result).toMatchObject({ deletedCount: 1, revokedTokenCount: 1, scheduled: false });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({ softDeletedAt: 1_000, softDeletedReason: "user.banned" }),
|
|
);
|
|
expect(patch).toHaveBeenCalledWith("packagePublishTokens:demo", { revokedAt: 1_000 });
|
|
});
|
|
|
|
it("stops stale package ban pages when the owner has already been unbanned", async () => {
|
|
const { ctx, patch } = makeOwnedPackageBatchCtx();
|
|
|
|
const result = await applyBanToOwnedPackagesBatchInternalHandler(ctx as never, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
deletedBy: "users:moderator",
|
|
deletedByRole: "moderator",
|
|
cursor: "next-page",
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
stale: true,
|
|
deletedCount: 0,
|
|
revokedTokenCount: 0,
|
|
scheduled: false,
|
|
});
|
|
expect(patch).not.toHaveBeenCalledWith("packages:demo", expect.anything());
|
|
expect(patch).not.toHaveBeenCalledWith("packagePublishTokens:demo", expect.anything());
|
|
});
|
|
|
|
it("continues committed package ban pages without a pre-commit bypass", async () => {
|
|
const firstPage = makeOwnedPackageBatchCtx({
|
|
owner: { _id: "users:owner", deletedAt: 1_000, deactivatedAt: undefined },
|
|
pkg: makePackageDoc({ _id: "packages:first", ownerUserId: "users:owner" }),
|
|
packageTokens: [
|
|
{
|
|
_id: "packagePublishTokens:first",
|
|
packageId: "packages:first",
|
|
version: "1.0.1",
|
|
revokedAt: undefined,
|
|
},
|
|
],
|
|
isDone: false,
|
|
continueCursor: "next-page",
|
|
});
|
|
|
|
const firstResult = await applyBanToOwnedPackagesBatchInternalHandler(firstPage.ctx as never, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
deletedBy: "users:moderator",
|
|
deletedByRole: "moderator",
|
|
});
|
|
|
|
expect(firstResult).toMatchObject({ deletedCount: 1, revokedTokenCount: 1, scheduled: true });
|
|
expect(firstPage.patch).toHaveBeenCalledWith(
|
|
"packages:first",
|
|
expect.objectContaining({ softDeletedAt: 1_000, softDeletedReason: "user.banned" }),
|
|
);
|
|
expect(firstPage.patch).toHaveBeenCalledWith("packagePublishTokens:first", {
|
|
revokedAt: 1_000,
|
|
});
|
|
expect(firstPage.runAfter).toHaveBeenCalledWith(
|
|
0,
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
cursor: "next-page",
|
|
}),
|
|
);
|
|
|
|
const staleContinuationPage = makeOwnedPackageBatchCtx({
|
|
owner: { _id: "users:owner", deletedAt: undefined, deactivatedAt: undefined },
|
|
pkg: makePackageDoc({ _id: "packages:second", ownerUserId: "users:owner" }),
|
|
packageTokens: [
|
|
{
|
|
_id: "packagePublishTokens:second",
|
|
packageId: "packages:second",
|
|
version: "1.0.1",
|
|
revokedAt: undefined,
|
|
},
|
|
],
|
|
});
|
|
|
|
const staleContinuationResult = await applyBanToOwnedPackagesBatchInternalHandler(
|
|
staleContinuationPage.ctx as never,
|
|
{
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
deletedBy: "users:moderator",
|
|
deletedByRole: "moderator",
|
|
cursor: "next-page",
|
|
},
|
|
);
|
|
|
|
expect(staleContinuationResult).toMatchObject({
|
|
stale: true,
|
|
deletedCount: 0,
|
|
revokedTokenCount: 0,
|
|
scheduled: false,
|
|
});
|
|
expect(staleContinuationPage.patch).not.toHaveBeenCalledWith(
|
|
"packages:second",
|
|
expect.anything(),
|
|
);
|
|
expect(staleContinuationPage.patch).not.toHaveBeenCalledWith(
|
|
"packagePublishTokens:second",
|
|
expect.anything(),
|
|
);
|
|
|
|
const committedContinuationPage = makeOwnedPackageBatchCtx({
|
|
owner: { _id: "users:owner", deletedAt: 1_000, deactivatedAt: undefined },
|
|
pkg: makePackageDoc({ _id: "packages:second", ownerUserId: "users:owner" }),
|
|
packageTokens: [
|
|
{
|
|
_id: "packagePublishTokens:second",
|
|
packageId: "packages:second",
|
|
version: "1.0.1",
|
|
revokedAt: undefined,
|
|
},
|
|
],
|
|
});
|
|
|
|
const committedContinuationResult = await applyBanToOwnedPackagesBatchInternalHandler(
|
|
committedContinuationPage.ctx as never,
|
|
{
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
deletedBy: "users:moderator",
|
|
deletedByRole: "moderator",
|
|
cursor: "next-page",
|
|
},
|
|
);
|
|
|
|
expect(committedContinuationResult).toMatchObject({
|
|
deletedCount: 1,
|
|
revokedTokenCount: 1,
|
|
scheduled: false,
|
|
});
|
|
expect(committedContinuationPage.patch).toHaveBeenCalledWith(
|
|
"packages:second",
|
|
expect.objectContaining({ softDeletedAt: 1_000, softDeletedReason: "user.banned" }),
|
|
);
|
|
expect(committedContinuationPage.patch).toHaveBeenCalledWith("packagePublishTokens:second", {
|
|
revokedAt: 1_000,
|
|
});
|
|
});
|
|
|
|
it("retimestamps earlier ban-hidden packages during a later ban", async () => {
|
|
const { ctx, patch } = makeOwnedPackageBatchCtx({
|
|
owner: { _id: "users:owner", deletedAt: 2_000, deactivatedAt: undefined },
|
|
pkg: makePackageDoc({
|
|
softDeletedAt: 1_000,
|
|
softDeletedReason: "user.banned",
|
|
softDeletedBy: "users:first-moderator",
|
|
softDeletedByRole: "moderator",
|
|
}),
|
|
releases: [
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:ban-hidden",
|
|
softDeletedAt: 1_000,
|
|
}),
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:moderation-hidden",
|
|
softDeletedAt: 500,
|
|
}),
|
|
],
|
|
});
|
|
|
|
const result = await applyBanToOwnedPackagesBatchInternalHandler(ctx as never, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 2_000,
|
|
deletedBy: "users:second-moderator",
|
|
deletedByRole: "admin",
|
|
});
|
|
|
|
expect(result).toMatchObject({ deletedCount: 0, revokedTokenCount: 1, scheduled: false });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageReleases:ban-hidden",
|
|
expect.objectContaining({ softDeletedAt: 2_000 }),
|
|
);
|
|
expect(patch).not.toHaveBeenCalledWith("packageReleases:moderation-hidden", expect.anything());
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
softDeletedAt: 2_000,
|
|
softDeletedBy: "users:second-moderator",
|
|
softDeletedByRole: "admin",
|
|
updatedAt: 2_000,
|
|
}),
|
|
);
|
|
expect(patch).toHaveBeenCalledWith("packagePublishTokens:demo", { revokedAt: 2_000 });
|
|
});
|
|
|
|
it("does not hide org-owned packages when banning a member in the legacy owner field", async () => {
|
|
const { ctx, patch } = makeOwnedPackageBatchCtx({
|
|
owner: {
|
|
_id: "users:owner",
|
|
deletedAt: 1_000,
|
|
deactivatedAt: undefined,
|
|
personalPublisherId: "publishers:personal",
|
|
},
|
|
pkg: makePackageDoc({
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:org",
|
|
}),
|
|
publishers: {
|
|
"publishers:org": { _id: "publishers:org", kind: "org" },
|
|
},
|
|
});
|
|
|
|
const result = await applyBanToOwnedPackagesBatchInternalHandler(ctx as never, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
deletedBy: "users:moderator",
|
|
deletedByRole: "moderator",
|
|
});
|
|
|
|
expect(result).toMatchObject({ deletedCount: 0, revokedTokenCount: 0, scheduled: true });
|
|
expect(patch).not.toHaveBeenCalledWith("packages:demo", expect.anything());
|
|
expect(patch).not.toHaveBeenCalledWith("packagePublishTokens:demo", expect.anything());
|
|
});
|
|
|
|
it("restores only packages that were hidden by the matching ban batch", async () => {
|
|
const { ctx, patch, insert } = makeOwnedPackageBatchCtx({
|
|
pkg: makePackageDoc({
|
|
softDeletedAt: 1_000,
|
|
softDeletedReason: "user.banned",
|
|
softDeletedByRole: "moderator",
|
|
}),
|
|
releases: [
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:demo-1",
|
|
softDeletedAt: 1_000,
|
|
distTags: ["latest"],
|
|
version: "1.0.0",
|
|
changelog: "",
|
|
compatibility: null,
|
|
capabilities: null,
|
|
verification: null,
|
|
}),
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:malicious",
|
|
softDeletedAt: 500,
|
|
distTags: ["malicious"],
|
|
version: "0.9.0",
|
|
changelog: "",
|
|
compatibility: null,
|
|
capabilities: null,
|
|
verification: null,
|
|
}),
|
|
],
|
|
});
|
|
|
|
const result = await restoreOwnedPackagesForUnbanBatchInternalHandler(ctx as never, {
|
|
actorUserId: "users:admin",
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
});
|
|
|
|
expect(result).toMatchObject({ restoredCount: 1, scheduled: false });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
softDeletedAt: undefined,
|
|
softDeletedReason: undefined,
|
|
softDeletedBy: undefined,
|
|
softDeletedByRole: undefined,
|
|
}),
|
|
);
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({
|
|
actorUserId: "users:admin",
|
|
action: "package.undelete",
|
|
}),
|
|
);
|
|
expect(patch).not.toHaveBeenCalledWith("packageReleases:malicious", expect.anything());
|
|
});
|
|
|
|
it("restores appeal-service packages without attributing package audit to the target user", async () => {
|
|
const { ctx, insert } = makeOwnedPackageBatchCtx({
|
|
pkg: makePackageDoc({
|
|
softDeletedAt: 1_000,
|
|
softDeletedReason: "user.banned",
|
|
softDeletedByRole: "moderator",
|
|
}),
|
|
releases: [
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:demo-1",
|
|
softDeletedAt: 1_000,
|
|
distTags: ["latest"],
|
|
version: "1.0.0",
|
|
changelog: "",
|
|
compatibility: null,
|
|
capabilities: null,
|
|
verification: null,
|
|
}),
|
|
],
|
|
});
|
|
|
|
const result = await restoreOwnedPackagesForUnbanBatchInternalHandler(ctx as never, {
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
});
|
|
|
|
expect(result).toMatchObject({ restoredCount: 1, scheduled: false });
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({
|
|
action: "package.undelete",
|
|
metadata: expect.objectContaining({ source: "service" }),
|
|
}),
|
|
);
|
|
const packageAudit = insert.mock.calls.find(
|
|
([table, doc]) => table === "auditLogs" && doc.action === "package.undelete",
|
|
)?.[1];
|
|
expect(packageAudit).not.toHaveProperty("actorUserId");
|
|
});
|
|
|
|
it("restores ban-hidden packages owned through the user's personal publisher", async () => {
|
|
const personalPublisherPackage = makePackageDoc({
|
|
_id: "packages:personal-publisher",
|
|
ownerUserId: "users:publishing-actor",
|
|
ownerPublisherId: "publishers:personal",
|
|
softDeletedAt: 1_000,
|
|
softDeletedReason: "user.banned",
|
|
softDeletedByRole: "moderator",
|
|
latestReleaseId: "packageReleases:personal-publisher-1",
|
|
tags: { latest: "packageReleases:personal-publisher-1" },
|
|
});
|
|
const { ctx, patch } = makeOwnedPackageBatchCtx({
|
|
owner: {
|
|
_id: "users:owner",
|
|
deletedAt: undefined,
|
|
deactivatedAt: undefined,
|
|
personalPublisherId: "publishers:personal",
|
|
},
|
|
publisherPackages: [personalPublisherPackage],
|
|
publishers: {
|
|
"publishers:personal": {
|
|
_id: "publishers:personal",
|
|
kind: "user",
|
|
linkedUserId: "users:owner",
|
|
},
|
|
},
|
|
releases: [
|
|
makeReleaseDoc({
|
|
_id: "packageReleases:personal-publisher-1",
|
|
packageId: "packages:personal-publisher",
|
|
softDeletedAt: 1_000,
|
|
distTags: ["latest"],
|
|
version: "1.0.0",
|
|
changelog: "",
|
|
compatibility: null,
|
|
capabilities: null,
|
|
verification: null,
|
|
}),
|
|
],
|
|
});
|
|
|
|
const result = await restoreOwnedPackagesForUnbanBatchInternalHandler(ctx as never, {
|
|
actorUserId: "users:admin",
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
scope: "personalPublisher",
|
|
});
|
|
|
|
expect(result).toMatchObject({ restoredCount: 1, scheduled: false });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:personal-publisher",
|
|
expect.objectContaining({
|
|
softDeletedAt: undefined,
|
|
softDeletedReason: undefined,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("stops stale package restore batches when the owner is banned again", async () => {
|
|
const { ctx, patch } = makeOwnedPackageBatchCtx({
|
|
owner: { _id: "users:owner", deletedAt: 2_000, deactivatedAt: undefined },
|
|
pkg: makePackageDoc({
|
|
softDeletedAt: 1_000,
|
|
softDeletedReason: "user.banned",
|
|
}),
|
|
});
|
|
|
|
const result = await restoreOwnedPackagesForUnbanBatchInternalHandler(ctx as never, {
|
|
actorUserId: "users:admin",
|
|
ownerUserId: "users:owner",
|
|
bannedAt: 1_000,
|
|
});
|
|
|
|
expect(result).toMatchObject({ stale: true, restoredCount: 0, scheduled: false });
|
|
expect(patch).not.toHaveBeenCalledWith("packages:demo", expect.anything());
|
|
});
|
|
|
|
it("hides and schedules hard deletes for account-deleted packages", async () => {
|
|
const { ctx, patch, runAfter } = makeOwnedPackageBatchCtx();
|
|
|
|
const result = await applyAccountDeletionToOwnedPackagesBatchInternalHandler(ctx as never, {
|
|
ownerUserId: "users:owner",
|
|
deletedAt: 3_000,
|
|
});
|
|
|
|
expect(result).toMatchObject({ deletedCount: 1, revokedTokenCount: 0, scheduled: false });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
softDeletedAt: 3_000,
|
|
softDeletedReason: "user.deactivated",
|
|
softDeletedBy: "users:owner",
|
|
softDeletedByRole: "user",
|
|
}),
|
|
);
|
|
expect(runAfter).toHaveBeenCalledWith(0, expect.anything(), {
|
|
packageId: "packages:demo",
|
|
actorUserId: "users:owner",
|
|
deletedAt: 3_000,
|
|
source: "account.delete",
|
|
});
|
|
});
|
|
|
|
it("schedules hard deletes for account-deleted packages owned through the user's personal publisher", async () => {
|
|
const personalPublisherPackage = makePackageDoc({
|
|
_id: "packages:personal-publisher",
|
|
ownerUserId: "users:publishing-actor",
|
|
ownerPublisherId: "publishers:personal",
|
|
});
|
|
const { ctx, patch, runAfter } = makeOwnedPackageBatchCtx({
|
|
owner: {
|
|
_id: "users:owner",
|
|
deactivatedAt: 3_000,
|
|
personalPublisherId: "publishers:personal",
|
|
},
|
|
publisherPackages: [personalPublisherPackage],
|
|
packageTokens: [
|
|
{
|
|
_id: "packagePublishTokens:personal-publisher",
|
|
packageId: "packages:personal-publisher",
|
|
version: "1.0.1",
|
|
revokedAt: undefined,
|
|
},
|
|
],
|
|
publishers: {
|
|
"publishers:personal": {
|
|
_id: "publishers:personal",
|
|
kind: "user",
|
|
linkedUserId: "users:owner",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await applyAccountDeletionToOwnedPackagesBatchInternalHandler(ctx as never, {
|
|
ownerUserId: "users:owner",
|
|
deletedAt: 3_000,
|
|
scope: "personalPublisher",
|
|
});
|
|
|
|
expect(result).toMatchObject({ deletedCount: 1, revokedTokenCount: 0, scheduled: false });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:personal-publisher",
|
|
expect.objectContaining({
|
|
softDeletedAt: 3_000,
|
|
softDeletedReason: "user.deactivated",
|
|
softDeletedBy: "users:owner",
|
|
softDeletedByRole: "user",
|
|
}),
|
|
);
|
|
expect(runAfter).toHaveBeenCalledWith(0, expect.anything(), {
|
|
packageId: "packages:personal-publisher",
|
|
actorUserId: "users:owner",
|
|
deletedAt: 3_000,
|
|
source: "account.delete",
|
|
});
|
|
});
|
|
|
|
it("does not delete org-owned packages when deleting a member account", async () => {
|
|
const { ctx, patch } = makeOwnedPackageBatchCtx({
|
|
owner: {
|
|
_id: "users:owner",
|
|
deletedAt: undefined,
|
|
deactivatedAt: 3_000,
|
|
personalPublisherId: "publishers:personal",
|
|
},
|
|
pkg: makePackageDoc({
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:org",
|
|
}),
|
|
publishers: {
|
|
"publishers:org": { _id: "publishers:org", kind: "org" },
|
|
},
|
|
});
|
|
|
|
const result = await applyAccountDeletionToOwnedPackagesBatchInternalHandler(ctx as never, {
|
|
ownerUserId: "users:owner",
|
|
deletedAt: 3_000,
|
|
});
|
|
|
|
expect(result).toMatchObject({ deletedCount: 0, revokedTokenCount: 0, scheduled: true });
|
|
expect(patch).not.toHaveBeenCalledWith("packages:demo", expect.anything());
|
|
expect(patch).not.toHaveBeenCalledWith("packagePublishTokens:demo", expect.anything());
|
|
});
|
|
});
|
|
|
|
describe("softDeletePackageInternal", () => {
|
|
it("soft-deletes a package owned by a personal publisher and writes ownerHandle to the search digest", async () => {
|
|
const { ctx, patch, insert } = makeSoftDeleteCtx();
|
|
|
|
const result = await softDeletePackageInternalHandler(ctx as never, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, alreadyDeleted: false, releaseCount: 1 });
|
|
|
|
// The package doc must be soft-deleted.
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({ softDeletedAt: expect.any(Number) }),
|
|
);
|
|
|
|
// The packageSearchDigest row must be updated with ownerHandle resolved.
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageSearchDigest:demo",
|
|
expect.objectContaining({ ownerHandle: "tongfei11", softDeletedAt: expect.any(Number) }),
|
|
);
|
|
|
|
// An audit log must be inserted.
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({ action: "package.delete" }),
|
|
);
|
|
});
|
|
|
|
it("does not recreate retired capability digest rows", async () => {
|
|
const { ctx, insert } = makeSoftDeleteCtx({ noCapabilityDigest: true });
|
|
|
|
await softDeletePackageInternalHandler(ctx as never, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
});
|
|
|
|
expect(insert).not.toHaveBeenCalledWith("packageCapabilitySearchDigest", expect.anything());
|
|
});
|
|
|
|
it("returns alreadyDeleted:true without touching releases when package is already soft-deleted", async () => {
|
|
const { ctx, patch } = makeSoftDeleteCtx({
|
|
pkg: makePackageDoc({ softDeletedAt: 999, softDeletedByRole: "user" }),
|
|
});
|
|
|
|
const result = await softDeletePackageInternalHandler(ctx as never, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, alreadyDeleted: true, releaseCount: 0 });
|
|
// No release patches should have been made.
|
|
expect(patch).not.toHaveBeenCalledWith("packageReleases:demo-1", expect.anything());
|
|
});
|
|
|
|
it("keeps manually moderated ban-hidden packages out of unban restore scope", async () => {
|
|
vi.spyOn(Date, "now").mockReturnValue(2_000);
|
|
const { ctx, patch } = makeSoftDeleteCtx({
|
|
actor: { _id: "users:owner", role: "moderator" },
|
|
pkg: makePackageDoc({
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner-personal",
|
|
softDeletedAt: 1_000,
|
|
softDeletedReason: "user.banned",
|
|
softDeletedBy: "users:first-moderator",
|
|
softDeletedByRole: "moderator",
|
|
}),
|
|
});
|
|
|
|
const result = await softDeletePackageInternalHandler(ctx as never, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, alreadyDeleted: true });
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({
|
|
softDeletedAt: 2_000,
|
|
softDeletedReason: undefined,
|
|
softDeletedBy: "users:owner",
|
|
softDeletedByRole: "moderator",
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("restorePackageInternal", () => {
|
|
it("restores a soft-deleted package and writes ownerHandle to the search digest", async () => {
|
|
const pkg = makePackageDoc({
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner-personal",
|
|
softDeletedAt: 500,
|
|
softDeletedByRole: "user",
|
|
softDeletedBy: "users:owner",
|
|
latestReleaseId: "packageReleases:demo-1",
|
|
tags: { latest: "packageReleases:demo-1" },
|
|
});
|
|
// The release was soft-deleted together with the package.
|
|
const restoredRelease = makeReleaseDoc({
|
|
_id: "packageReleases:demo-1",
|
|
softDeletedAt: 500,
|
|
distTags: ["latest"],
|
|
version: "1.0.0",
|
|
changelog: "",
|
|
integritySha256: "abc",
|
|
compatibility: null,
|
|
verification: null,
|
|
scanStatus: "clean",
|
|
});
|
|
|
|
const { ctx, patch, insert } = makeSoftDeleteCtx({
|
|
pkg,
|
|
noCapabilityDigest: true,
|
|
releases: [restoredRelease],
|
|
});
|
|
|
|
const result = await restorePackageInternalHandler(ctx as never, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, alreadyRestored: false });
|
|
|
|
// The package doc must have softDeletedAt cleared.
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packages:demo",
|
|
expect.objectContaining({ softDeletedAt: undefined }),
|
|
);
|
|
|
|
// The packageSearchDigest row must be updated with ownerHandle resolved.
|
|
expect(patch).toHaveBeenCalledWith(
|
|
"packageSearchDigest:demo",
|
|
expect.objectContaining({ ownerHandle: "tongfei11", softDeletedAt: undefined }),
|
|
);
|
|
|
|
expect(insert).not.toHaveBeenCalledWith("packageCapabilitySearchDigest", expect.anything());
|
|
|
|
// An audit log must be inserted.
|
|
expect(insert).toHaveBeenCalledWith(
|
|
"auditLogs",
|
|
expect.objectContaining({ action: "package.undelete" }),
|
|
);
|
|
});
|
|
|
|
it("returns alreadyRestored:true when package is not soft-deleted", async () => {
|
|
const { ctx, patch } = makeSoftDeleteCtx();
|
|
|
|
const result = await restorePackageInternalHandler(ctx as never, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
});
|
|
|
|
expect(result).toMatchObject({ ok: true, alreadyRestored: true, releaseCount: 0 });
|
|
expect(patch).not.toHaveBeenCalledWith("packages:demo", expect.anything());
|
|
});
|
|
|
|
it("does not let package owners directly restore packages hidden by a user ban", async () => {
|
|
const { ctx, patch } = makeSoftDeleteCtx({
|
|
pkg: makePackageDoc({
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner-personal",
|
|
softDeletedAt: 1_000,
|
|
softDeletedReason: "user.banned",
|
|
softDeletedByRole: "moderator",
|
|
}),
|
|
});
|
|
|
|
await expect(
|
|
restorePackageInternalHandler(ctx as never, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
}),
|
|
).rejects.toThrow("Forbidden");
|
|
expect(patch).not.toHaveBeenCalledWith("packages:demo", expect.anything());
|
|
});
|
|
|
|
it("does not let moderators directly restore packages hidden by a user ban", async () => {
|
|
const { ctx, patch } = makeSoftDeleteCtx({
|
|
actor: { _id: "users:owner", role: "moderator" },
|
|
pkg: makePackageDoc({
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner-personal",
|
|
softDeletedAt: 1_000,
|
|
softDeletedReason: "user.banned",
|
|
softDeletedByRole: "moderator",
|
|
}),
|
|
});
|
|
|
|
await expect(
|
|
restorePackageInternalHandler(ctx as never, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
}),
|
|
).rejects.toThrow("Forbidden");
|
|
expect(patch).not.toHaveBeenCalledWith("packages:demo", expect.anything());
|
|
});
|
|
|
|
it("does not let moderators directly restore packages hidden by account deletion", async () => {
|
|
const { ctx, patch } = makeSoftDeleteCtx({
|
|
actor: { _id: "users:owner", role: "moderator" },
|
|
pkg: makePackageDoc({
|
|
ownerUserId: "users:owner",
|
|
ownerPublisherId: "publishers:owner-personal",
|
|
softDeletedAt: 1_000,
|
|
softDeletedReason: "user.deactivated",
|
|
softDeletedByRole: "user",
|
|
}),
|
|
});
|
|
|
|
await expect(
|
|
restorePackageInternalHandler(ctx as never, {
|
|
userId: "users:owner",
|
|
name: "demo-plugin",
|
|
}),
|
|
).rejects.toThrow("Forbidden");
|
|
expect(patch).not.toHaveBeenCalledWith("packages:demo", expect.anything());
|
|
});
|
|
|
|
it("gates Claw named reads and exposes only the safe manifest summary when enabled", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue(null);
|
|
const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
const latestClawManifestSummary = {
|
|
schemaVersion: 1,
|
|
agent: { id: "demo-claw-latest", name: "Demo Claw Latest", description: "Latest" },
|
|
workspace: { bootstrapFiles: ["SOUL.md"], fileCount: 1 },
|
|
packages: { skillCount: 1, pluginCount: 1 },
|
|
mcpServerCount: 1,
|
|
cronJobCount: 1,
|
|
};
|
|
const exactClawManifestSummary = {
|
|
schemaVersion: 1,
|
|
agent: { id: "demo-claw-v1", name: "Demo Claw v1", description: "Exact" },
|
|
workspace: { bootstrapFiles: ["SOUL.md"], fileCount: 2 },
|
|
packages: { skillCount: 2, pluginCount: 1 },
|
|
mcpServerCount: 2,
|
|
cronJobCount: 2,
|
|
};
|
|
const latestRelease = makeReleaseDoc({
|
|
_id: "packageReleases:demo-latest",
|
|
version: "2.0.0",
|
|
changelog: "Latest release",
|
|
distTags: ["latest"],
|
|
files: [],
|
|
integritySha256: "latest-integrity",
|
|
clawManifestSummary: latestClawManifestSummary,
|
|
createdBy: "users:must-not-project-latest",
|
|
publishActor: { kind: "user", userId: "users:must-not-project-latest" },
|
|
extractedPackageJson: {
|
|
openclaw: {
|
|
packageCoordinate: "must-not-project-latest",
|
|
profile: { workspaceRoot: "must-not-project-latest", policy: "must-not-project-latest" },
|
|
manifest: {
|
|
workspace: { content: "must-not-project-latest" },
|
|
mcp: { command: "must-not-project-latest", env: "must-not-project-latest" },
|
|
cron: { schedule: "must-not-project-latest", message: "must-not-project-latest" },
|
|
},
|
|
},
|
|
},
|
|
extractedClawManifest: {
|
|
schemaVersion: 1,
|
|
agent: { id: "demo-claw-latest" },
|
|
},
|
|
});
|
|
const { ctx } = makePackageCtx({
|
|
pkg: makePackageDoc({
|
|
family: "claw",
|
|
latestReleaseId: "packageReleases:demo-latest",
|
|
latestVersionSummary: { version: "2.0.0" },
|
|
}),
|
|
latestRelease,
|
|
versionRelease: makeReleaseDoc({
|
|
_id: "packageReleases:demo-v1",
|
|
changelog: "Exact release",
|
|
distTags: [],
|
|
files: [],
|
|
integritySha256: "exact-integrity",
|
|
clawManifestSummary: exactClawManifestSummary,
|
|
createdBy: "users:must-not-project-exact",
|
|
publishActor: { kind: "user", userId: "users:must-not-project-exact" },
|
|
extractedPackageJson: {
|
|
openclaw: {
|
|
packageCoordinate: "must-not-project-exact",
|
|
profile: { workspaceRoot: "must-not-project-exact", policy: "must-not-project-exact" },
|
|
manifest: {
|
|
workspace: { content: "must-not-project-exact" },
|
|
mcp: { command: "must-not-project-exact", env: "must-not-project-exact" },
|
|
cron: { schedule: "must-not-project-exact", message: "must-not-project-exact" },
|
|
},
|
|
},
|
|
},
|
|
extractedClawManifest: {
|
|
schemaVersion: 1,
|
|
agent: { id: "demo-claw-v1" },
|
|
workspace: { bootstrapFiles: { "SOUL.md": { source: "workspace/SOUL.md" } } },
|
|
},
|
|
}),
|
|
});
|
|
|
|
try {
|
|
delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
await expect(getByNameHandler(ctx, { name: "demo-plugin" })).resolves.toBeNull();
|
|
await expect(
|
|
getVersionByNameHandler(ctx, { name: "demo-plugin", version: "1.0.0" }),
|
|
).resolves.toBeNull();
|
|
|
|
process.env.CLAWHUB_EXPERIMENTAL_CLAWS = "1";
|
|
const detail = await getByNameHandler(ctx, { name: "demo-plugin" });
|
|
expect(detail).toMatchObject({
|
|
package: { family: "claw", clawManifestSummary: latestClawManifestSummary },
|
|
latestRelease: { clawManifestSummary: latestClawManifestSummary },
|
|
});
|
|
expect(detail?.latestRelease).not.toHaveProperty("extractedClawManifest");
|
|
expect(JSON.stringify(detail)).not.toContain("must-not-project");
|
|
|
|
const version = await getVersionByNameHandler(ctx, {
|
|
name: "demo-plugin",
|
|
version: "1.0.0",
|
|
});
|
|
expect(version).toMatchObject({
|
|
package: { family: "claw", clawManifestSummary: latestClawManifestSummary },
|
|
version: { clawManifestSummary: exactClawManifestSummary },
|
|
});
|
|
expect(version?.version).not.toHaveProperty("extractedClawManifest");
|
|
expect(JSON.stringify(version)).not.toContain("must-not-project");
|
|
} finally {
|
|
if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous;
|
|
}
|
|
});
|
|
|
|
it("selects the bounded release projection from package family, not summary presence", async () => {
|
|
vi.mocked(getAuthUserId).mockResolvedValue(null);
|
|
const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
process.env.CLAWHUB_EXPERIMENTAL_CLAWS = "1";
|
|
const staleSummary = {
|
|
schemaVersion: 1,
|
|
agent: { id: "stale-non-claw-summary" },
|
|
workspace: { bootstrapFiles: [], fileCount: 0 },
|
|
packages: { skillCount: 0, pluginCount: 0 },
|
|
mcpServerCount: 0,
|
|
cronJobCount: 0,
|
|
};
|
|
|
|
try {
|
|
const clawRelease = makeReleaseDoc({
|
|
files: [],
|
|
clawManifestSummary: undefined,
|
|
createdBy: "users:must-not-project",
|
|
publishActor: { kind: "user", userId: "users:must-not-project" },
|
|
extractedPackageJson: { privateWorkspace: "must-not-project" },
|
|
});
|
|
const { ctx: clawCtx } = makePackageCtx({
|
|
pkg: makePackageDoc({ family: "claw" }),
|
|
latestRelease: clawRelease,
|
|
});
|
|
const claw = await getByNameHandler(clawCtx, { name: "demo-plugin" });
|
|
expect(claw?.latestRelease).not.toHaveProperty("extractedPackageJson");
|
|
expect(JSON.stringify(claw)).not.toContain("must-not-project");
|
|
|
|
const ownerDeletedRelease = makeReleaseDoc({
|
|
files: [],
|
|
ownerDeletedAt: 123,
|
|
clawManifestSummary: staleSummary,
|
|
extractedPackageJson: { privateWorkspace: "must-not-project-owner-deleted" },
|
|
});
|
|
const { ctx: ownerDeletedCtx } = makePackageCtx({
|
|
pkg: makePackageDoc({ family: "claw" }),
|
|
latestRelease: ownerDeletedRelease,
|
|
});
|
|
const ownerDeleted = await getByNameHandler(ownerDeletedCtx, { name: "demo-plugin" });
|
|
expect(ownerDeleted).toMatchObject({ package: { latestVersion: null }, latestRelease: null });
|
|
expect(JSON.stringify(ownerDeleted)).not.toContain("stale-non-claw-summary");
|
|
expect(JSON.stringify(ownerDeleted)).not.toContain("must-not-project-owner-deleted");
|
|
|
|
const pluginPackageJson = { name: "demo-plugin", openclaw: { extensions: ["index.js"] } };
|
|
const pluginRelease = makeReleaseDoc({
|
|
clawManifestSummary: staleSummary,
|
|
extractedPackageJson: pluginPackageJson,
|
|
});
|
|
const { ctx: pluginCtx } = makePackageCtx({ latestRelease: pluginRelease });
|
|
const plugin = await getByNameHandler(pluginCtx, { name: "demo-plugin" });
|
|
expect(plugin?.latestRelease).toMatchObject({ extractedPackageJson: pluginPackageJson });
|
|
expect(plugin?.latestRelease).not.toHaveProperty("clawManifestSummary");
|
|
} finally {
|
|
if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous;
|
|
}
|
|
});
|
|
|
|
it("omits stored Claws from unfiltered public lists while disabled", async () => {
|
|
const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
const ctx = makePluginExportCtx([
|
|
makeDigest("demo-claw", { family: "claw", updatedAt: 2, _creationTime: 2 }),
|
|
makeDigest("demo-plugin", { updatedAt: 1, _creationTime: 1 }),
|
|
]);
|
|
|
|
try {
|
|
const result = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 25 },
|
|
});
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["demo-plugin"]);
|
|
} finally {
|
|
if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous;
|
|
}
|
|
});
|
|
|
|
it("does not let disabled Claws starve unfiltered public list pages", async () => {
|
|
const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
const hiddenClaws = Array.from({ length: 50 }, (_, index) =>
|
|
makeDigest(`demo-claw-${index}`, {
|
|
family: "claw",
|
|
updatedAt: 100 - index,
|
|
_creationTime: 100 - index,
|
|
}),
|
|
);
|
|
const ctx = makePluginExportCtx([
|
|
...hiddenClaws,
|
|
makeDigest("visible-plugin", { updatedAt: 1, _creationTime: 1 }),
|
|
]);
|
|
|
|
try {
|
|
const result = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
expect(result.page.map((entry) => entry.name)).toEqual(["visible-plugin"]);
|
|
} finally {
|
|
if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous;
|
|
}
|
|
});
|
|
|
|
it("paginates stable families beyond the per-query read window while Claws are disabled", async () => {
|
|
const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
const digests = Array.from({ length: 250 }, (_, index) =>
|
|
makeDigest(`stable-${index.toString().padStart(3, "0")}`, {
|
|
updatedAt: 250 - index,
|
|
_creationTime: 250 - index,
|
|
}),
|
|
);
|
|
const ctx = makePluginExportCtx(digests);
|
|
|
|
try {
|
|
const first = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 200 },
|
|
});
|
|
const second = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: first.continueCursor, numItems: 200 },
|
|
});
|
|
expect(first.page).toHaveLength(200);
|
|
expect(first.isDone).toBe(false);
|
|
expect(second.page).toHaveLength(50);
|
|
expect(second.isDone).toBe(true);
|
|
} finally {
|
|
if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous;
|
|
}
|
|
});
|
|
|
|
it("preserves each stable family's index order across tied public list pages", async () => {
|
|
const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
const ctx = makePluginExportCtx([
|
|
makeDigest("zulu-plugin", {
|
|
_id: "packageSearchDigest:zulu",
|
|
packageId: "packages:zulu",
|
|
updatedAt: 1,
|
|
_creationTime: 1,
|
|
}),
|
|
makeDigest("alpha-plugin", {
|
|
_id: "packageSearchDigest:alpha",
|
|
packageId: "packages:alpha",
|
|
updatedAt: 1,
|
|
_creationTime: 1,
|
|
}),
|
|
]);
|
|
|
|
try {
|
|
const first = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
const second = await listPublicPageHandler(ctx, {
|
|
paginationOpts: { cursor: first.continueCursor, numItems: 1 },
|
|
});
|
|
expect([...first.page, ...second.page].map((entry) => entry.name)).toEqual([
|
|
"zulu-plugin",
|
|
"alpha-plugin",
|
|
]);
|
|
expect(second.isDone).toBe(true);
|
|
} finally {
|
|
if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous;
|
|
}
|
|
});
|
|
|
|
it("keeps the cursor's effective sort when recommendation backfill completes between pages", async () => {
|
|
const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
let recommendationScoresMissing = true;
|
|
const ctx = makePluginExportCtx(
|
|
[
|
|
makeDigest("newer-plugin", { updatedAt: 2, _creationTime: 2 }),
|
|
makeDigest("older-plugin", { updatedAt: 1, _creationTime: 1 }),
|
|
],
|
|
{ recommendationScoresMissing: () => recommendationScoresMissing },
|
|
);
|
|
|
|
try {
|
|
const first = await listPublicPageHandler(ctx, {
|
|
sort: "recommended",
|
|
paginationOpts: { cursor: null, numItems: 1 },
|
|
});
|
|
recommendationScoresMissing = false;
|
|
const second = await listPublicPageHandler(ctx, {
|
|
sort: "recommended",
|
|
paginationOpts: { cursor: first.continueCursor, numItems: 1 },
|
|
});
|
|
expect(first.page.map((entry) => entry.name)).toEqual(["newer-plugin"]);
|
|
expect(second.page.map((entry) => entry.name)).toEqual(["older-plugin"]);
|
|
} finally {
|
|
if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous;
|
|
}
|
|
});
|
|
|
|
it("does not let disabled Claws starve unfiltered public search", async () => {
|
|
const previous = process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
const hiddenClaws = Array.from({ length: 50 }, (_, index) =>
|
|
makeDigest(`matching-claw-${index}`, {
|
|
family: "claw",
|
|
displayName: `Matching Claw ${index}`,
|
|
updatedAt: 100 - index,
|
|
}),
|
|
);
|
|
const { ctx } = makeDigestCtx({
|
|
pages: [
|
|
{
|
|
page: hiddenClaws,
|
|
isDone: false,
|
|
continueCursor: "after-claws",
|
|
},
|
|
{
|
|
page: [makeDigest("matching-plugin", { displayName: "Matching Plugin" })],
|
|
isDone: true,
|
|
continueCursor: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
try {
|
|
const result = await searchPublicHandler(ctx, { query: "matching", limit: 1 });
|
|
expect(result.map((entry) => entry.package.name)).toEqual(["matching-plugin"]);
|
|
} finally {
|
|
if (previous === undefined) delete process.env.CLAWHUB_EXPERIMENTAL_CLAWS;
|
|
else process.env.CLAWHUB_EXPERIMENTAL_CLAWS = previous;
|
|
}
|
|
});
|
|
});
|