fix(ci): restore release validation after runtime migrations (#107371)

* test(ci): repair plugin prerelease validation

* test(zalo): preserve adapter narrowing in sender helpers

* test(ci): track current bundled startup metadata

* fix(google-meet): refresh plugin shrinkwrap
This commit is contained in:
Peter Steinberger
2026-07-14 03:52:44 -07:00
committed by GitHub
parent e488dc0012
commit ca83f1ca51
15 changed files with 121 additions and 141 deletions
+1
View File
@@ -181,6 +181,7 @@ Skills own workflows; root owns hard policy and routing.
- Sparse-sync temp checkout may claim kept Testbox; repo-path reuse needs `--reclaim`.
- GitHub Actions: resolve workflow files from `.github/workflows` or API; never infer filenames from display names.
- zsh: quote command globs; unmatched patterns abort before the tool runs.
- Nested remote shell: avoid local `$()` expansion; use remote-safe validation.
- zsh: don't use `path` as a variable; it rewrites `$PATH`.
- `scripts/pr` artifacts: preserve template enum values; validate before prepare.
- `scripts/pr` subcommands require a PR number; no subcommand `--help` placeholder.
@@ -798,8 +798,13 @@ describe("slash-http", () => {
}),
).resolves.toBe(false);
expect(log).toHaveBeenCalledTimes(1);
const message = firstLogMessage(log);
const message = log.mock.calls
.map(([entry]) => (typeof entry === "string" ? entry : ""))
.find((entry) => entry.includes("using team list fallback"));
expect(message).toBeTruthy();
if (!message) {
throw new Error("expected sanitized Mattermost command lookup fallback log");
}
expect(message).toBe(
`mattermost: slash command lookup by id returned deleted command ${"i".repeat(199)} for /oc_status; using team list fallback`,
);
+33 -58
View File
@@ -43,11 +43,18 @@ vi.mock("node:child_process", async () => {
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
return {
...actual,
execFile: execFileMock,
execFileSync: execFileSyncMock,
};
});
vi.mock("openclaw/plugin-sdk/process-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/process-runtime")>();
return {
...actual,
runExec: execFileMock,
};
});
vi.mock("openclaw/plugin-sdk/provider-auth", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/provider-auth")>(
"openclaw/plugin-sdk/provider-auth",
@@ -245,62 +252,35 @@ function buildFoundryRuntimeAuthContext(
}
function mockAzureCliToken(params: { accessToken: string; expiresInMs: number; delayMs?: number }) {
execFileMock.mockImplementationOnce(
(
_file: unknown,
_args: unknown,
_options: unknown,
callback: (error: Error | null, stdout: string, stderr: string) => void,
) => {
const respond = () =>
callback(
null,
JSON.stringify({
accessToken: params.accessToken,
expiresOn: new Date(Date.now() + params.expiresInMs).toISOString(),
}),
"",
);
if (params.delayMs) {
setTimeout(respond, params.delayMs);
return;
}
respond();
},
);
execFileMock.mockImplementationOnce(async () => {
if (params.delayMs) {
await new Promise<void>((resolve) => {
setTimeout(resolve, params.delayMs);
});
}
return {
stdout: JSON.stringify({
accessToken: params.accessToken,
expiresOn: new Date(Date.now() + params.expiresInMs).toISOString(),
}),
stderr: "",
};
});
}
function mockAzureCliTokenRaw(stdout: string) {
execFileMock.mockImplementationOnce(
(
_file: unknown,
_args: unknown,
_options: unknown,
callback: (error: Error | null, stdout: string, stderr: string) => void,
) => {
callback(null, stdout, "");
},
);
execFileMock.mockResolvedValueOnce({ stdout, stderr: "" });
}
function mockAzureCliLoginFailure(delayMs?: number) {
execFileMock.mockImplementationOnce(
(
_file: unknown,
_args: unknown,
_options: unknown,
callback: (error: Error | null, stdout: string, stderr: string) => void,
) => {
const respond = () => {
callback(new Error("az failed"), "", defaultAzureCliLoginError);
};
if (delayMs) {
setTimeout(respond, delayMs);
return;
}
respond();
},
);
execFileMock.mockImplementationOnce(async () => {
if (delayMs) {
await new Promise<void>((resolve) => {
setTimeout(resolve, delayMs);
});
}
throw Object.assign(new Error("az failed"), { stderr: defaultAzureCliLoginError, stdout: "" });
});
}
describe("microsoft-foundry plugin", () => {
@@ -1849,13 +1829,8 @@ describe("microsoft-foundry plugin", () => {
it("keeps bounded Azure CLI error details UTF-16 safe", async () => {
const prefix = "x".repeat(299);
execFileMock.mockImplementationOnce(
(
_file: unknown,
_args: unknown,
_options: unknown,
callback: (error: Error | null, stdout: string, stderr: string) => void,
) => callback(new Error("az failed"), "", `${prefix}😀tail`),
execFileMock.mockRejectedValueOnce(
Object.assign(new Error("az failed"), { stderr: `${prefix}😀tail`, stdout: "" }),
);
await expect(getAccessTokenResultAsync()).rejects.toMatchObject({
@@ -4075,7 +4075,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
await dispatchWithContext({
context: createContext(),
streamMode: "progress",
telegramCfg: { streaming: { mode: "progress" } },
telegramCfg: { streaming: { mode: "progress", progress: { label: "Cracking" } } },
});
expect(answerDraftStream.updatePreview).toHaveBeenCalledWith(
@@ -4118,7 +4118,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
await dispatchWithContext({
context: createContext(),
streamMode: "progress",
telegramCfg: { streaming: { mode: "progress" } },
telegramCfg: { streaming: { mode: "progress", progress: { label: "Cracking" } } },
});
expect(answerDraftStream.update).not.toHaveBeenCalledWith("Terminal block answer");
@@ -4142,7 +4142,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
await dispatchWithContext({
context: createContext(),
streamMode: "progress",
telegramCfg: { streaming: { mode: "progress" } },
telegramCfg: { streaming: { mode: "progress", progress: { label: "Cracking" } } },
});
expect(answerDraftStream.updatePreview).toHaveBeenCalledWith(
@@ -4802,7 +4802,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
await dispatchWithContext({
context: createContext(),
streamMode: "progress",
telegramCfg: { streaming: { mode: "progress" } },
telegramCfg: { streaming: { mode: "progress", progress: { label: "Cracking" } } },
});
expect(answerDraftStream.updatePreview).toHaveBeenCalledWith(
@@ -29,15 +29,10 @@ vi.mock("./channel.runtime.js", () => ({
type ZaloOutbound = NonNullable<typeof zaloPlugin.outbound>;
type ZaloSendPayload = NonNullable<ZaloOutbound["sendPayload"]>;
function requireZaloMessageAdapter(): NonNullable<typeof zaloPlugin.message> {
const adapter = zaloPlugin.message;
if (!adapter) {
throw new Error("Expected Zalo message adapter");
}
return adapter;
const zaloMessageAdapter = zaloPlugin.message;
if (!zaloMessageAdapter) {
throw new Error("Expected Zalo message adapter");
}
const zaloMessageAdapter = requireZaloMessageAdapter();
type ZaloMessageSender = NonNullable<typeof zaloMessageAdapter.send>;
function requireZaloSendPayload(): ZaloSendPayload {
@@ -49,7 +44,7 @@ function requireZaloSendPayload(): ZaloSendPayload {
}
function requireZaloTextSender(): NonNullable<ZaloMessageSender["text"]> {
const text = zaloMessageAdapter.send?.text;
const text = zaloMessageAdapter?.send?.text;
if (!text) {
throw new Error("Expected Zalo message adapter text sender");
}
@@ -57,7 +52,7 @@ function requireZaloTextSender(): NonNullable<ZaloMessageSender["text"]> {
}
function requireZaloMediaSender(): NonNullable<ZaloMessageSender["media"]> {
const media = zaloMessageAdapter.send?.media;
const media = zaloMessageAdapter?.send?.media;
if (!media) {
throw new Error("Expected Zalo message adapter media sender");
}
@@ -8,6 +8,7 @@
"dist/extensions/memory-core/runtime-api.js",
"dist/extensions/ollama/runtime-api.js",
"dist/extensions/open-prose/runtime-api.js",
"dist/extensions/reef/runtime-api.js",
"dist/extensions/telegram/runtime-api.js",
"dist/extensions/telegram/runtime-setter-api.js",
"dist/extensions/webhooks/runtime-api.js",
@@ -214,12 +214,12 @@ describe("gateway cli backend live helpers", () => {
expect(probe.resumePrompt).toBe(
"Do not inspect files or run tools. " +
"What private session note were you asked to remember earlier? " +
"Reply with exactly: CLI backend RESUME OK 445566 <remembered-note>.",
"Reply with CLI-RESUME-445566 and the remembered note.",
);
expect(probe.firstTurnPrompt).not.toContain(memoryToken);
expect(probe.resumePrompt).not.toContain(memoryToken);
expect(probe.injectedContext).toContain(memoryToken);
expect(probe.expectedResumeReply).toBe("CLI backend RESUME OK 445566 CLI-MEM-A1B2C3D4E5F6.");
expect(probe.expectedResumeMarker).toBe("CLI-RESUME-445566");
});
it("finds only Claude-imported native session ids", () => {
@@ -79,7 +79,7 @@ export type ClaudeCliResumeContinuityProbe = {
injectedContext: string;
resumePrompt: string;
expectedFirstReply: string;
expectedResumeReply: string;
expectedResumeMarker: string;
};
function normalizeCliRuntimeModelTarget(raw: string | undefined): string | undefined {
@@ -296,9 +296,9 @@ export function buildClaudeCliResumeContinuityProbe(params: {
resumePrompt:
"Do not inspect files or run tools. " +
"What private session note were you asked to remember earlier? " +
`Reply with exactly: CLI backend RESUME OK ${params.resumeNonce} <remembered-note>.`,
`Reply with CLI-RESUME-${params.resumeNonce} and the remembered note.`,
expectedFirstReply: `${firstTurnMarker}.`,
expectedResumeReply: `CLI backend RESUME OK ${params.resumeNonce} ${params.memoryToken}.`,
expectedResumeMarker: `CLI-RESUME-${params.resumeNonce}`,
};
}
+2 -3
View File
@@ -736,9 +736,8 @@ describeLive("gateway live (cli backend)", () => {
if (providerId === "codex-cli") {
expect(resumeText).toContain(`CLI-RESUME-${resumeNonce}`);
} else if (resumeContinuityProbe) {
expect(
matchesCliBackendReply(resumeText, resumeContinuityProbe.expectedResumeReply),
).toBe(true);
expect(resumeText).toContain(resumeContinuityProbe.expectedResumeMarker);
expect(resumeText).toContain(memoryToken);
if (!continuityOwner || !expectedLiveSessionGeneration) {
throw new Error("Claude CLI continuity probe lost its live-session generation");
}
@@ -45,14 +45,17 @@ const EXPECTED_BUNDLED_STARTUP_PLUGIN_IDS = [
"diffs-language-pack",
"file-transfer",
"google-meet",
"linux-node",
"llm-task",
"lobster",
"logbook",
"memory-wiki",
"ollama",
"opencode",
"openshell",
"phone-control",
"policy",
"reef",
"talk-voice",
"thread-ownership",
"voice-call",
@@ -67,8 +70,10 @@ const EXPECTED_EMPTY_CONFIG_GATEWAY_STARTUP_PLUGIN_IDS = [
"canvas",
"device-pair",
"file-transfer",
"linux-node",
"memory-core",
"ollama",
"opencode",
"phone-control",
"talk-voice",
] as const;
+40 -11
View File
@@ -441,8 +441,32 @@ function mockNpmViewMetadata(params: { name: string; version?: string }) {
});
}
let actualExecModulePromise: Promise<typeof import("../process/exec.js")> | undefined;
async function runActualInstallPolicyCommandIfNeeded(
args: Parameters<typeof runCommandWithTimeout>[0],
options: Parameters<typeof runCommandWithTimeout>[1],
): Promise<Awaited<ReturnType<typeof runCommandWithTimeout>> | null> {
if (typeof options === "number" || options.input === undefined) {
return null;
}
actualExecModulePromise ??=
vi.importActual<typeof import("../process/exec.js")>("../process/exec.js");
const actualExecModule = await actualExecModulePromise;
return await actualExecModule.runCommandWithTimeout(args, options);
}
function countMockedCommands(executable: string): number {
return vi.mocked(runCommandWithTimeout).mock.calls.filter(([args]) => args[0] === executable)
.length;
}
function mockSuccessfulManagedNpmInstall(params: { packageName: string; version?: string }) {
vi.mocked(runCommandWithTimeout).mockImplementation(async (args, options) => {
const policyResult = await runActualInstallPolicyCommandIfNeeded(args, options);
if (policyResult) {
return policyResult;
}
if (args[0] !== "npm" || args[1] !== "install") {
throw new Error(`unexpected command: ${args.join(" ")}`);
}
@@ -617,13 +641,18 @@ function expectHookRequest(
}
function mockSuccessfulCommandRun(run: ReturnType<typeof vi.mocked<typeof runCommandWithTimeout>>) {
run.mockResolvedValue({
code: 0,
stdout: "",
stderr: "",
signal: null,
killed: false,
termination: "exit",
run.mockImplementation(async (args, options) => {
const policyResult = await runActualInstallPolicyCommandIfNeeded(args, options);
return (
policyResult ?? {
code: 0,
stdout: "",
stderr: "",
signal: null,
killed: false,
termination: "exit" as const,
}
);
});
}
@@ -2645,7 +2674,7 @@ describe("installPluginFromNpmSpec", () => {
expect(result.code, result.error).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
expect(result.error).toContain("npm installs are disabled by policy");
}
expect(vi.mocked(runCommandWithTimeout)).toHaveBeenCalledTimes(1);
expect(countMockedCommands("npm")).toBe(1);
expect(vi.mocked(runCommandWithTimeout).mock.calls[0]?.[0]).toEqual([
"npm",
"view",
@@ -2698,7 +2727,7 @@ describe("installPluginFromNpmSpec", () => {
expect(result.code, result.error).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
expect(result.error).toContain("fresh npm installs are disabled by policy");
}
expect(vi.mocked(runCommandWithTimeout)).toHaveBeenCalledTimes(1);
expect(countMockedCommands("npm")).toBe(1);
const requests = readCapturedInstallPolicyRequests(logPath);
expect(requests).toHaveLength(1);
expect(requests[0]?.request.mode).toBe("install");
@@ -2937,7 +2966,7 @@ describe("installPluginFromNpmSpec", () => {
expect(result.code, result.error).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
expect(result.error).toContain("npm installs are disabled by policy");
}
expect(vi.mocked(runCommandWithTimeout)).toHaveBeenCalledTimes(1);
expect(countMockedCommands("npm")).toBe(1);
await expect(fsPromises.stat(npmDir)).rejects.toThrow();
const requests = readCapturedInstallPolicyRequests(logPath);
expect(requests).toHaveLength(1);
@@ -2997,7 +3026,7 @@ describe("installPluginFromNpmSpec", () => {
expect(result.code, result.error).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
expect(result.error).toContain("npm installs are disabled by policy");
}
expect(vi.mocked(runCommandWithTimeout)).toHaveBeenCalledTimes(1);
expect(countMockedCommands("npm")).toBe(1);
await expect(fsPromises.stat(npmDir)).rejects.toThrow();
const requests = readCapturedInstallPolicyRequests(logPath);
expect(requests).toHaveLength(1);
+8 -42
View File
@@ -277,7 +277,7 @@ describe("migration provider runtime", () => {
});
});
it("derives a fresh manifest registry so newly bundled migration providers are discoverable", () => {
it("discovers newly bundled migration providers from current metadata", () => {
const provider = createMigrationProvider("hermes");
const active = createEmptyPluginRegistry();
const loaded = createEmptyPluginRegistry();
@@ -290,55 +290,21 @@ describe("migration provider runtime", () => {
mocks.resolveRuntimePluginRegistry.mockImplementation((params?: unknown) =>
params === undefined ? active : loaded,
);
mocks.loadPluginRegistrySnapshot.mockReturnValue(
createMockPluginIndex([
{
pluginId: "migrate-hermes",
origin: "bundled",
enabled: true,
},
]),
);
mocks.loadPluginManifestRegistry.mockImplementation(() => ({
diagnostics: [],
plugins: [
{
mocks.listBundledPluginMetadata.mockReturnValue([
{
manifest: {
id: "migrate-hermes",
origin: "bundled",
contracts: { migrationProviders: ["hermes"] },
},
],
}));
},
] as never);
const resolved = resolvePluginMigrationProvider({ providerId: "hermes" });
expect(resolved).toBe(provider);
expect(mocks.loadPluginRegistrySnapshotWithMetadata).toHaveBeenCalledWith({
config: {},
env: process.env,
workspaceDir: undefined,
expect(mocks.listBundledPluginMetadata).toHaveBeenCalledWith({
includeChannelConfigs: false,
});
const manifestParams = requireMockCallArg(
mocks.loadPluginManifestRegistry,
"loadPluginManifestRegistry",
) as {
index?: MockPluginIndex;
config?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
includeDisabled?: unknown;
workspaceDir?: unknown;
};
expect(manifestParams.index?.plugins).toEqual([
{
pluginId: "migrate-hermes",
origin: "bundled",
enabled: true,
},
]);
expect(manifestParams.config).toEqual({});
expect(manifestParams.env).toBe(process.env);
expect(manifestParams.includeDisabled).toBe(true);
expect(manifestParams.workspaceDir).toBeUndefined();
expect(mocks.resolveRuntimePluginRegistry).toHaveBeenCalledWith({
onlyPluginIds: ["migrate-hermes"],
});
@@ -33,11 +33,10 @@ const REQUIRED_REVIEWED_PUBLISHABLE_CRITICAL_FINDINGS = new Set([
"@openclaw/discord:dangerous-exec:src/voice/audio.ts",
"@openclaw/google-meet:dangerous-exec:src/node-host.ts",
"@openclaw/google-meet:dangerous-exec:src/realtime.ts",
"@openclaw/matrix:dangerous-exec:src/matrix/deps.ts",
"@openclaw/mxc-sandbox:dangerous-exec:src/readiness.ts",
"@openclaw/raft:dangerous-exec:src/gateway.ts",
"@openclaw/signal:dangerous-exec:src/daemon.ts",
"@openclaw/voice-call:dangerous-exec:src/tunnel.ts",
"@openclaw/voice-call:dangerous-exec:src/webhook/tailscale.ts",
]);
const OPTIONAL_REVIEWED_PUBLISHABLE_DIST_CRITICAL_FINDINGS = new Set([
@@ -14,9 +14,9 @@ const LIVE_RUNTIME_STATE_GUARDS: Record<
forbidden: readonly string[];
}
> = {
[bundledPluginFile("whatsapp", "src/connection-controller-registry.ts")]: {
required: ["globalThis", 'Symbol.for("openclaw.whatsapp.connectionControllerRegistry")'],
forbidden: ["resolveGlobalSingleton"],
[bundledPluginFile("whatsapp", "src/connection-controller-runtime-context.ts")]: {
required: ["getChannelRuntimeContext", "WHATSAPP_CONNECTION_CONTROLLER_CAPABILITY"],
forbidden: ["globalThis", "resolveGlobalSingleton"],
},
};
@@ -80,7 +80,12 @@ function listSourceFilesByDirectory(dir: string): string[] {
}
function isProductionTypeScriptFile(path: string): boolean {
return path.endsWith(".ts") && !path.endsWith(".test.ts") && !path.endsWith(".test.tsx");
return (
path.endsWith(".ts") &&
!path.endsWith(".test.ts") &&
!path.endsWith(".test.tsx") &&
!/\.test-(?:fixtures|harness|helpers|mocks|setup|support|utils)\.tsx?$/u.test(path)
);
}
describe("runtime plugin registry boundary", () => {