// Defines the plugin prerelease validation surface and matching test lanes. import { BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS } from "./docker-e2e-scenarios.mts"; import type { ExtensionTestPlanGroup } from "./extension-test-plan.mts"; type PrereleaseSurfaceEntry = { surfaces: readonly string[] }; /** Required behavioral surfaces that plugin prerelease validation must cover. */ const PLUGIN_PRERELEASE_REQUIRED_SURFACES = Object.freeze([ "package-artifact", "bundled-lifecycle", "external-plugins", "update-no-op", "installed-plugin-deps", "doctor-fix", "config-round-trip", "gateway-bootstrap", "sdk-compatibility", "external-install-boundary", "status-diagnostics", "npm-registry-plugin", "clawhub-registry-plugin", "resource-guardrails", "plugin-gateway-rpc", "live-ish-availability", ]); const pluginPrereleaseDockerLanes = Object.freeze([ { lane: "npm-onboard-channel-agent", surfaces: ["package-artifact", "gateway-bootstrap", "status-diagnostics"], }, { lane: "npm-onboard-discord-candidate-channel-agent", surfaces: [ "package-artifact", "external-plugins", "installed-plugin-deps", "gateway-bootstrap", "status-diagnostics", ], }, { lane: "npm-onboard-slack-candidate-channel-agent", surfaces: ["package-artifact", "gateway-bootstrap", "status-diagnostics"], }, { lane: "doctor-switch", surfaces: ["package-artifact", "doctor-fix"], }, { lane: "update-channel-switch", surfaces: ["package-artifact", "installed-plugin-deps", "update-no-op"], }, { lane: "plugins-offline", surfaces: ["external-plugins", "sdk-compatibility", "status-diagnostics"], }, { lane: "plugins", surfaces: [ "external-plugins", "sdk-compatibility", "external-install-boundary", "status-diagnostics", ], }, { lane: "kitchen-sink-plugin", surfaces: [ "external-plugins", "sdk-compatibility", "external-install-boundary", "status-diagnostics", "npm-registry-plugin", "clawhub-registry-plugin", "resource-guardrails", ], }, { lane: "kitchen-sink-rpc", surfaces: [ "external-plugins", "sdk-compatibility", "gateway-bootstrap", "status-diagnostics", "npm-registry-plugin", "resource-guardrails", "plugin-gateway-rpc", ], }, { lane: "plugin-update", surfaces: ["package-artifact", "update-no-op"], }, { lane: "config-reload", surfaces: ["config-round-trip", "gateway-bootstrap"], }, { lane: "gateway-network", surfaces: ["gateway-bootstrap", "status-diagnostics"], }, { lane: "mcp-channels", surfaces: ["gateway-bootstrap", "status-diagnostics"], }, { lane: "cron-mcp-cleanup", surfaces: ["gateway-bootstrap", "status-diagnostics"], }, ...Array.from({ length: BUNDLED_PLUGIN_INSTALL_UNINSTALL_SHARDS }, (_, index) => ({ lane: `bundled-plugin-install-uninstall-${index}`, surfaces: ["bundled-lifecycle", "package-artifact", "status-diagnostics"], })), ]); const staticChecks = Object.freeze([ { check: "test:extensions:package-boundary:compile", checkName: "checks-plugin-prerelease-package-boundary-compile", command: "pnpm run test:extensions:package-boundary:compile", surfaces: ["package-artifact", "sdk-compatibility"], }, { check: "test:extensions:package-boundary:canary", checkName: "checks-plugin-prerelease-package-boundary-canary", command: "pnpm run test:extensions:package-boundary:canary", surfaces: ["package-artifact", "sdk-compatibility"], }, { check: "live-ish-availability", checkName: "checks-plugin-prerelease-live-ish-availability", command: "node --import tsx scripts/plugin-prerelease-liveish-matrix.mts", surfaces: ["live-ish-availability"], }, ]); function coveredSurfaces(entries: readonly PrereleaseSurfaceEntry[]): string[] { return [ ...new Set( entries .flatMap((entry) => entry.surfaces) .filter((surface) => typeof surface === "string" && surface.length > 0), ), ].toSorted((a, b) => a.localeCompare(b)); } /** Keep each release batch on Node and add only its qualified Bun groups. */ export async function resolvePluginPrereleaseExtensionRuntime({ planGroups, fullReleaseValidation, vitestArgs = [], }: { planGroups: readonly Pick[]; fullReleaseValidation: boolean; vitestArgs?: readonly string[]; }): Promise<{ test_runtime_policy: "dual" | "node"; requires_bun: boolean }> { if (!fullReleaseValidation) { return { test_runtime_policy: "node", requires_bun: false }; } // Static release plans also run from bounded tooling copies without test inventories. const { ciTestShardRequiresBun } = await import("./ci-test-runtime.mts"); const requiresBun = ciTestShardRequiresBun( { groups: planGroups.map(({ config, roots }) => ({ configs: [config], includePatterns: roots.map((root) => /\.test\.tsx?$/u.test(root) ? root : `${root}/**/*.test.ts`, ), vitestArgs, })), }, "dual", ); return { test_runtime_policy: requiresBun ? "dual" : "node", requires_bun: requiresBun }; } /** Build the plugin prerelease plan from Docker lanes and static checks. */ export function createPluginPrereleaseTestPlan() { const dockerLanes = pluginPrereleaseDockerLanes.map((entry) => entry.lane); const allEntries = [...pluginPrereleaseDockerLanes, ...staticChecks]; return { dockerLanes, staticChecks: staticChecks.map((entry) => ({ check: entry.check, checkName: entry.checkName, command: entry.command, surfaces: entry.surfaces.slice(), })), surfaces: coveredSurfaces(allEntries), }; } /** Assert that a plugin prerelease plan covers every required surface. */ export function assertPluginPrereleaseTestPlanComplete( plan: ReturnType = createPluginPrereleaseTestPlan(), ) { const missing = PLUGIN_PRERELEASE_REQUIRED_SURFACES.filter( (surface) => !plan.surfaces.includes(surface), ); if (missing.length > 0) { throw new Error(`Plugin prerelease test plan is missing surfaces: ${missing.join(", ")}`); } return plan; }