fix(release): unblock full validation

This commit is contained in:
Peter Steinberger
2026-05-15 05:13:44 +01:00
parent 93c799eb16
commit f1deb53d43
4 changed files with 107 additions and 88 deletions

View File

@@ -62,6 +62,14 @@ export async function ensureCodexPluginActivation(
} satisfies v2.PluginListParams)) as v2.PluginListResponse;
const resolved = findOpenAiCuratedPluginSummary(listed, params.identity.pluginName);
if (!resolved) {
const hasCuratedMarketplace = listed.marketplaces.some(
(marketplace) => marketplace.name === CODEX_PLUGINS_MARKETPLACE_NAME,
);
if (!hasCuratedMarketplace) {
return activationFailure(params.identity, "marketplace_missing", {
message: `Codex marketplace ${CODEX_PLUGINS_MARKETPLACE_NAME} was not found.`,
});
}
return activationFailure(params.identity, "plugin_missing", {
message: `${params.identity.pluginName} was not found in ${CODEX_PLUGINS_MARKETPLACE_NAME}.`,
});

View File

@@ -91,6 +91,7 @@ vi.mock("../config/config.js", () => ({
},
readConfigFileSnapshot: vi.fn(),
readSourceConfigBestEffort: vi.fn(),
mutateConfigFileWithRetry: vi.fn(),
replaceConfigFile: vi.fn(),
resolveGatewayPort: vi.fn(() => 18789),
}));
@@ -268,7 +269,7 @@ vi.mock("../runtime.js", () => ({
const { runGatewayUpdate } = await import("../infra/update-runner.js");
const { resolveOpenClawPackageRoot } = await import("../infra/openclaw-root.js");
const {
ConfigMutationConflictError,
mutateConfigFileWithRetry,
readConfigFileSnapshot,
readSourceConfigBestEffort,
replaceConfigFile,
@@ -426,6 +427,31 @@ describe("update-cli", () => {
const replaceConfigCall = (index = 0) => vi.mocked(replaceConfigFile).mock.calls[index]?.[0];
const lastReplaceConfigCall = () =>
replaceConfigCall(vi.mocked(replaceConfigFile).mock.calls.length - 1);
const setupConfigMutationWithRetryMock = () => {
vi.mocked(mutateConfigFileWithRetry).mockImplementation(async (params) => {
const snapshot = await readConfigFileSnapshot();
const nextConfig = structuredClone(snapshot.sourceConfig) as OpenClawConfig;
await params.mutate(nextConfig, {
snapshot,
previousHash: snapshot.hash ?? null,
attempt: 0,
});
await replaceConfigFile({
nextConfig,
...(snapshot.hash !== undefined ? { baseHash: snapshot.hash } : {}),
});
return {
path: snapshot.path,
previousHash: snapshot.hash ?? null,
snapshot,
nextConfig,
result: undefined,
attempts: 1,
afterWrite: { mode: "none", reason: "test" },
followUp: { mode: "none", reason: "test", requiresRestart: false },
};
});
};
const writeJsonCall = (index = 0) => vi.mocked(defaultRuntime.writeJson).mock.calls[index]?.[0];
const lastWriteJsonCall = () =>
@@ -562,6 +588,7 @@ describe("update-cli", () => {
vi.mocked(resolveOpenClawPackageRoot).mockResolvedValue(process.cwd());
vi.mocked(readConfigFileSnapshot).mockResolvedValue(baseSnapshot);
vi.mocked(readSourceConfigBestEffort).mockResolvedValue(baseSnapshot.config);
setupConfigMutationWithRetryMock();
vi.mocked(fetchNpmTagVersion).mockResolvedValue({
tag: "latest",
version: "9999.0.0",
@@ -1118,47 +1145,57 @@ describe("update-cli", () => {
});
it("post-core resume mode retries update channel persistence after config hash drift", async () => {
vi.mocked(readConfigFileSnapshot)
.mockResolvedValueOnce({
...baseSnapshot,
parsed: { update: { channel: "stable" } },
resolved: { update: { channel: "stable" } } as OpenClawConfig,
sourceConfig: { update: { channel: "stable" } } as OpenClawConfig,
runtimeConfig: { update: { channel: "stable" } } as OpenClawConfig,
config: { update: { channel: "stable" } } as OpenClawConfig,
hash: "stable-hash",
})
.mockResolvedValueOnce({
...baseSnapshot,
parsed: {
meta: { lastTouchedVersion: "2026.4.30" },
update: { channel: "stable" },
},
resolved: {
meta: { lastTouchedVersion: "2026.4.30" },
update: { channel: "stable" },
} as OpenClawConfig,
sourceConfig: {
meta: { lastTouchedVersion: "2026.4.30" },
update: { channel: "stable" },
} as OpenClawConfig,
runtimeConfig: {
meta: { lastTouchedVersion: "2026.4.30" },
update: { channel: "stable" },
} as OpenClawConfig,
config: {
meta: { lastTouchedVersion: "2026.4.30" },
update: { channel: "stable" },
} as OpenClawConfig,
hash: "newer-hash",
vi.mocked(readConfigFileSnapshot).mockResolvedValueOnce({
...baseSnapshot,
parsed: { update: { channel: "stable" } },
resolved: { update: { channel: "stable" } } as OpenClawConfig,
sourceConfig: { update: { channel: "stable" } } as OpenClawConfig,
runtimeConfig: { update: { channel: "stable" } } as OpenClawConfig,
config: { update: { channel: "stable" } } as OpenClawConfig,
hash: "stable-hash",
});
const newerSnapshot = {
...baseSnapshot,
parsed: {
meta: { lastTouchedVersion: "2026.4.30" },
update: { channel: "stable" },
},
resolved: {
meta: { lastTouchedVersion: "2026.4.30" },
update: { channel: "stable" },
} as OpenClawConfig,
sourceConfig: {
meta: { lastTouchedVersion: "2026.4.30" },
update: { channel: "stable" },
} as OpenClawConfig,
runtimeConfig: {
meta: { lastTouchedVersion: "2026.4.30" },
update: { channel: "stable" },
} as OpenClawConfig,
config: {
meta: { lastTouchedVersion: "2026.4.30" },
update: { channel: "stable" },
} as OpenClawConfig,
hash: "newer-hash",
};
vi.mocked(mutateConfigFileWithRetry).mockImplementationOnce(async (params) => {
const nextConfig = structuredClone(newerSnapshot.sourceConfig);
await params.mutate(nextConfig, {
snapshot: newerSnapshot,
previousHash: newerSnapshot.hash,
attempt: 1,
});
vi.mocked(replaceConfigFile)
.mockRejectedValueOnce(
new ConfigMutationConflictError("config changed since last load", {
currentHash: "newer-hash",
}),
)
.mockResolvedValueOnce({} as Awaited<ReturnType<typeof replaceConfigFile>>);
return {
path: newerSnapshot.path,
previousHash: newerSnapshot.hash,
snapshot: newerSnapshot,
nextConfig,
result: undefined,
attempts: 2,
afterWrite: { mode: "none", reason: "test" },
followUp: { mode: "none", reason: "test", requiresRestart: false },
};
});
await withEnvAsync(
{
@@ -1171,14 +1208,7 @@ describe("update-cli", () => {
},
);
expect(replaceConfigFile).toHaveBeenCalledTimes(2);
expect(replaceConfigFile).toHaveBeenLastCalledWith({
nextConfig: {
meta: { lastTouchedVersion: "2026.4.30" },
update: { channel: "dev" },
},
baseHash: "newer-hash",
});
expect(mutateConfigFileWithRetry).toHaveBeenCalledTimes(1);
expect(syncPluginCall()?.config?.meta?.lastTouchedVersion).toBe("2026.4.30");
expect(syncPluginCall()?.config?.update?.channel).toBe("dev");
});
@@ -2717,6 +2747,16 @@ describe("update-cli", () => {
},
],
})
.mockResolvedValueOnce({
...baseSnapshot,
parsed: migratedConfig,
resolved: migratedConfig,
sourceConfig: migratedConfig,
config: migratedConfig,
runtimeConfig: migratedConfig,
valid: true,
hash: "migrated-hash",
})
.mockResolvedValueOnce({
...baseSnapshot,
parsed: migratedConfig,

View File

@@ -11,10 +11,9 @@ import {
import { doctorCommand } from "../../commands/doctor.js";
import { createPreUpdateConfigSnapshot } from "../../config/backup-rotation.js";
import {
ConfigMutationConflictError,
assertConfigWriteAllowedInCurrentMode,
mutateConfigFileWithRetry,
readConfigFileSnapshot,
replaceConfigFile,
resolveGatewayPort,
} from "../../config/config.js";
import { formatConfigIssueLines } from "../../config/issue-format.js";
@@ -1920,46 +1919,17 @@ async function persistRequestedUpdateChannel(params: {
if (params.requestedChannel === storedChannel) {
return params.configSnapshot;
}
const requestedChannel = params.requestedChannel;
const next = {
...params.configSnapshot.sourceConfig,
update: {
...params.configSnapshot.sourceConfig.update,
channel: params.requestedChannel,
const mutation = await mutateConfigFileWithRetry({
mutate: (draft) => {
draft.update = {
...draft.update,
channel: requestedChannel,
};
},
};
try {
await replaceConfigFile({
nextConfig: next,
baseHash: params.configSnapshot.hash,
});
return createUpdatedChannelSnapshot(params.configSnapshot, next);
} catch (error) {
if (!(error instanceof ConfigMutationConflictError)) {
throw error;
}
}
const refreshed = await readConfigFileSnapshot();
if (!refreshed.valid) {
return refreshed;
}
const refreshedChannel = normalizeUpdateChannel(refreshed.config.update?.channel);
if (refreshedChannel === params.requestedChannel) {
return refreshed;
}
const refreshedNext = {
...refreshed.sourceConfig,
update: {
...refreshed.sourceConfig.update,
channel: params.requestedChannel,
},
};
await replaceConfigFile({
nextConfig: refreshedNext,
baseHash: refreshed.hash,
});
return createUpdatedChannelSnapshot(refreshed, refreshedNext);
return createUpdatedChannelSnapshot(mutation.snapshot, mutation.nextConfig);
}
function createUpdatedChannelSnapshot(

View File

@@ -261,6 +261,7 @@ function buildDockerE2eHarnessEntries(): Record<string, string> {
"agents/pi-bundle-mcp-runtime": "src/agents/pi-bundle-mcp-runtime.ts",
"agents/pi-embedded-runner/effective-tool-policy":
"src/agents/pi-embedded-runner/effective-tool-policy.ts",
"agents/pi-embedded-runner/tool-split": "src/agents/pi-embedded-runner/tool-split.ts",
"agents/pi-embedded-runner/run/runtime-context-prompt":
"src/agents/pi-embedded-runner/run/runtime-context-prompt.ts",
"auto-reply/reply/commands-crestodian": "src/auto-reply/reply/commands-crestodian.ts",