fix(e2e): stage candidate providers for legacy-operator survivor lanes

Legacy published baselines (2026.7.35, 2026.6.34) author plugins.allow from
their bundled provider inventory. The candidate's doctor restores those
now-external providers at the exact candidate version, which does not exist
on npm or ClawHub before publication, so the prepublish registry must carry
them. Stage every official provider from the selected candidate's external
provider catalog for legacy-operator-state lanes, loaded once per plan.
This commit is contained in:
Peter Steinberger 2026-09-28 15:37:59 -07:00
parent 6188881e05
commit ac59199ad3
2 changed files with 98 additions and 7 deletions

View file

@ -21,6 +21,8 @@ import {
type DockerE2eReleaseProfileInput,
} from "./docker-e2e-scenarios.mts";
import officialExternalChannelCatalog from "./official-external-channel-catalog.json" with { type: "json" };
import officialExternalProviderCatalog from "./official-external-provider-catalog.json" with { type: "json" };
import { isRecord } from "./record-shared.mjs";
import {
UPDATE_FIRST_HOP_COMPAT_LANE,
isUpdateFirstHopCompatLane,
@ -758,9 +760,43 @@ function upgradeSurvivorBaselineVersionForLane(poolLane: DockerE2eLane): string
return /(?:^|\/|@)(\d{4}\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/u.exec(spec ?? "")?.[1] ?? null;
}
export function requiredPrepublishPluginPackagesForLanes(poolLanes: DockerE2eLane[]): string[] {
function legacyOperatorProviderPackages(
targetRoot?: string,
frozenTarget?: InertTargetContract,
): string[] {
const catalogPath = "scripts/lib/official-external-provider-catalog.json";
const text =
targetRoot || frozenTarget ? readTargetMetadata(targetRoot, catalogPath, frozenTarget) : null;
// Older candidates without the external catalog still carry their bundled providers.
const catalog: unknown =
targetRoot || frozenTarget
? text === null
? { entries: [] }
: JSON.parse(text)
: officialExternalProviderCatalog;
if (!isRecord(catalog) || !Array.isArray(catalog.entries)) {
throw new Error(`invalid candidate provider catalog: ${catalogPath}`);
}
return catalog.entries.flatMap((entry: unknown) =>
isRecord(entry) &&
entry.source === "official" &&
typeof entry.name === "string" &&
isRecord(entry.openclaw) &&
isRecord(entry.openclaw.install) &&
entry.openclaw.install.npmSpec === entry.name
? [entry.name]
: [],
);
}
export function requiredPrepublishPluginPackagesForLanes(
poolLanes: DockerE2eLane[],
targetRoot?: string,
frozenTarget?: InertTargetContract,
): string[] {
const configuredChannelIds = new Set<string>();
const requiredPackages = new Set<string>();
let legacyOperatorProviders: string[] | undefined;
for (const poolLane of poolLanes) {
for (const packageName of poolLane.prepublishPluginPackages ?? []) {
requiredPackages.add(packageName);
@ -783,6 +819,12 @@ export function requiredPrepublishPluginPackagesForLanes(poolLanes: DockerE2eLan
requiredPackages.add("@openclaw/codex");
requiredPackages.add("@openclaw/discord");
requiredPackages.add("@openclaw/duckduckgo-plugin");
// The baseline authors an allowlist from its enabled bundled inventory.
// Supply candidate providers once, even when several baselines share the registry.
legacyOperatorProviders ??= legacyOperatorProviderPackages(targetRoot, frozenTarget);
for (const packageName of legacyOperatorProviders) {
requiredPackages.add(packageName);
}
continue;
}
for (const packageName of UPGRADE_SURVIVOR_RUNTIME_COMPANION_PACKAGES) {
@ -836,12 +878,18 @@ function buildPlanJson(params: {
releaseChunk: string;
releaseProfile: DockerE2eReleaseProfile;
selectedLaneNames: string[];
targetRoot?: string;
frozenTarget?: InertTargetContract;
}) {
const scheduledLanes = [...params.orderedLanes, ...params.orderedTailLanes];
const imageKinds = unique(scheduledLanes.map((poolLane) => poolLane.e2eImageKind)).toSorted(
(a, b) => a.localeCompare(b),
);
const requiredPrepublishPluginPackages = requiredPrepublishPluginPackagesForLanes(scheduledLanes);
const requiredPrepublishPluginPackages = requiredPrepublishPluginPackagesForLanes(
scheduledLanes,
params.targetRoot,
params.frozenTarget,
);
return {
chunk: params.releaseChunk || undefined,
credentials: unique(scheduledLanes.flatMap(laneCredentialRequirements)).toSorted((a, b) =>
@ -1058,6 +1106,8 @@ export function resolveDockerE2ePlan(options: DockerE2ePlanOptions) {
releaseChunk: options.releaseChunk,
releaseProfile,
selectedLaneNames: options.selectedLaneNames,
targetRoot: options.upgradeSurvivorTargetRoot,
frozenTarget: options.frozenTarget,
}),
scheduledLanes: [...orderedLanes, ...orderedTailLanes],
};

View file

@ -1510,11 +1510,16 @@ await import('./scripts/check-docker-e2e-boundaries.mts');`,
"published-upgrade-survivor-2026.9.4-legacy-operator-state",
"published-upgrade-survivor-2026.9.6-legacy-operator-state",
]);
expect(plan.requiredPrepublishPluginPackages).toEqual([
"@openclaw/codex",
"@openclaw/discord",
"@openclaw/duckduckgo-plugin",
]);
expect(plan.requiredPrepublishPluginPackages).toEqual(
expect.arrayContaining([
"@openclaw/codex",
"@openclaw/discord",
"@openclaw/duckduckgo-plugin",
"@openclaw/byteplus-provider",
"@openclaw/voyage-provider",
]),
);
expect(plan.requiredPrepublishPluginPackages).not.toContain("@telnyx/openclaw-provider");
expect(plan.lanes.every((lane) => lane.weight === 3)).toBe(true);
for (const alias of ["reported-issues", "far-reaching"]) {
expect(
@ -1527,6 +1532,42 @@ await import('./scripts/check-docker-e2e-boundaries.mts');`,
}
});
it("stages legacy operator providers from the selected candidate catalog", () => {
const entry = (name: string, source = "official") => ({
name,
source,
openclaw: { install: { npmSpec: name } },
});
const plan = planFor({
selectedLaneNames: ["published-upgrade-survivor"],
upgradeSurvivorBaselines: "2026.7.35 2026.6.34",
upgradeSurvivorScenarios: "legacy-operator-state",
frozenTarget: {
mode: "inert",
source: {
readText: (relativePath) =>
relativePath === "scripts/lib/official-external-provider-catalog.json"
? JSON.stringify({
entries: [
entry("@openclaw/candidate-provider"),
entry("@vendor/provider", "external"),
],
})
: existsSync(relativePath)
? readFileSync(relativePath, "utf8")
: null,
},
},
});
expect(plan.requiredPrepublishPluginPackages).toEqual([
"@openclaw/candidate-provider",
"@openclaw/codex",
"@openclaw/discord",
"@openclaw/duckduckgo-plugin",
]);
});
it.each([
["workshop-doctor-recovery", "2026.9.4", "2026.9.3 2026.9.4 2026.9.5"],
["update-report-recovery", "2026.9.6", "2026.9.5 2026.9.6 2026.9.7"],