fix(release): repair beta.2 plugin and package validation

Repair the beta.2 validation candidate at its owning boundaries:

- fail closed on conflicting approval channels and keep transient Slack member events retryable;
- tolerate sparse assistant usage history and align QA/release-only evidence with current runtime contracts;
- remove package/Docker harness dependency leaks and assert the installed `tsx` launcher path.

Verified by exact-head CI, focused QA/plugin/package Testboxes, full release preflight, clean Codex autoreview, and exact-head ClawSweeper review with no actionable findings or security concerns.
This commit is contained in:
Peter Steinberger
2026-08-10 22:59:00 -07:00
committed by GitHub
parent a652994a7c
commit 0a848362fd
33 changed files with 265 additions and 99 deletions
@@ -1,6 +1,6 @@
// Browser tests cover browser cli inspect plugin behavior.
import { Command } from "commander";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { createCliRuntimeCapture } from "../../test-support.js";
import * as browserCliSharedModule from "./browser-cli-shared.js";
import * as cliCoreApiModule from "./core-api.js";
@@ -58,15 +58,6 @@ const sharedMocks = vi.hoisted(() => ({
},
),
}));
vi.spyOn(browserCliSharedModule, "callBrowserRequest").mockImplementation(
sharedMocks.callBrowserRequest,
);
vi.spyOn(cliCoreApiModule, "getRuntimeConfig").mockImplementation(configMocks.loadConfig);
vi.spyOn(cliCoreApiModule.defaultRuntime, "log").mockImplementation(runtime.log);
vi.spyOn(cliCoreApiModule.defaultRuntime, "writeJson").mockImplementation(runtime.writeJson);
vi.spyOn(cliCoreApiModule.defaultRuntime, "error").mockImplementation(runtime.error);
vi.spyOn(cliCoreApiModule.defaultRuntime, "exit").mockImplementation(runtime.exit);
let registerBrowserInspectCommands: typeof import("./browser-cli-inspect.js").registerBrowserInspectCommands;
type SnapshotDefaultsCase = {
@@ -75,11 +66,22 @@ type SnapshotDefaultsCase = {
expectMode: "efficient" | undefined;
};
function installInspectSpies() {
vi.spyOn(browserCliSharedModule, "callBrowserRequest").mockImplementation(
sharedMocks.callBrowserRequest,
);
vi.spyOn(cliCoreApiModule, "getRuntimeConfig").mockImplementation(configMocks.loadConfig);
vi.spyOn(cliCoreApiModule.defaultRuntime, "log").mockImplementation(runtime.log);
vi.spyOn(cliCoreApiModule.defaultRuntime, "writeJson").mockImplementation(runtime.writeJson);
vi.spyOn(cliCoreApiModule.defaultRuntime, "error").mockImplementation(runtime.error);
vi.spyOn(cliCoreApiModule.defaultRuntime, "exit").mockImplementation(runtime.exit);
}
describe("browser cli snapshot defaults", () => {
const runBrowserInspect = async (args: string[], withJson = false) => {
const program = new Command();
const browser = program.command("browser").option("--json", "JSON output", false);
registerBrowserInspectCommands(browser, () => ({}));
registerBrowserInspectCommands(browser, (cmd) => cmd.parent?.opts() ?? {});
await program.parseAsync(withJson ? ["browser", "--json", ...args] : ["browser", ...args], {
from: "user",
});
@@ -91,11 +93,17 @@ describe("browser cli snapshot defaults", () => {
const runSnapshot = async (args: string[]) => await runBrowserInspect(["snapshot", ...args]);
beforeAll(async () => {
installInspectSpies();
({ registerBrowserInspectCommands } = await import("./browser-cli-inspect.js"));
});
beforeEach(() => {
installInspectSpies();
});
afterEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
resetRuntimeCapture();
configMocks.loadConfig.mockReturnValue({ browser: {} });
});
@@ -180,6 +180,7 @@ describe("iMessage approval reaction poller", () => {
approvalKind: "exec",
decision: "allow-once",
channel: "imessage",
accountId,
senderId: "+15551230000",
gatewayUrl: undefined,
});
@@ -412,6 +413,7 @@ describe("iMessage approval reaction poller", () => {
approvalKind: "exec",
decision: "allow-once",
channel: "imessage",
accountId,
senderId: "+15551239999",
gatewayUrl: undefined,
});
@@ -461,6 +463,7 @@ describe("iMessage approval reaction poller", () => {
approvalKind: "exec",
decision: "allow-once",
channel: "imessage",
accountId,
senderId: "+15551230000",
gatewayUrl: undefined,
});
@@ -568,6 +571,7 @@ describe("iMessage approval reaction poller", () => {
approvalKind: "exec",
decision: "allow-once",
channel: "imessage",
accountId,
senderId: "+15551230000",
gatewayUrl: undefined,
});
@@ -130,11 +130,14 @@ describe("registerPolicyDoctorChecks", () => {
source: "oc://openclaw.config/session/maintenance/mode",
value: "warn",
}),
expect.objectContaining({
{
id: "agents-defaults-memory-session-transcripts",
kind: "memorySessionTranscriptIndexing",
scope: "global",
source: "oc://openclaw.config/memory/search/rememberAcrossConversations",
value: true,
}),
value: false,
explicit: true,
},
]),
);
expect(result.findings).toEqual(
@@ -149,11 +152,6 @@ describe("registerPolicyDoctorChecks", () => {
ocPath: "oc://openclaw.config/session/maintenance/mode",
requirement: "oc://policy.jsonc/dataHandling/retention/requireSessionMaintenance",
}),
expect.objectContaining({
checkId: "policy/data-handling-session-transcript-memory-enabled",
ocPath: "oc://openclaw.config/memory/search/rememberAcrossConversations",
requirement: "oc://policy.jsonc/dataHandling/memory/denySessionTranscriptIndexing",
}),
]),
);
});
@@ -23,6 +23,7 @@ function normalizeModelRef(raw: string) {
async function runToolContinuity(
alternateTools: string[],
params?: {
alternateWireToolName?: "read" | "exec";
primaryOutboundText?: string;
primaryDelivery?: { status: string; resultCount: number } | null;
alternateReplyText?: string;
@@ -34,8 +35,14 @@ async function runToolContinuity(
) {
const state = createQaBusState();
let call = 0;
const requests: Array<{
cursor: number;
allInputText: string;
plannedToolName: string;
plannedWireToolName: string;
}> = [];
const runAgentPrompt = vi.fn(
async (_env: unknown, prompt: { provider?: string; model?: string }) => {
async (_env: unknown, prompt: { provider?: string; model?: string; message: string }) => {
call += 1;
const runId = `run-${call}`;
const provider = prompt.provider ?? "openai";
@@ -68,6 +75,13 @@ async function runToolContinuity(
});
}
const terminalDelivery = call === 1 ? params?.primaryDelivery : params?.alternateDelivery;
const wireToolName = call === 1 ? "read" : (params?.alternateWireToolName ?? "read");
requests.push({
cursor: call,
allInputText: prompt.message,
plannedToolName: "read",
plannedWireToolName: wireToolName,
});
return {
started: { runId },
waited: {
@@ -84,7 +98,7 @@ async function runToolContinuity(
turnId: `turn-${call}`,
requested: { provider, model },
effective: { provider, model, responseModel: model },
successfulToolNames: call === 1 ? ["read"] : alternateTools,
successfulToolNames: call === 1 ? [wireToolName] : alternateTools,
rerouted: false,
terminalDisposition: "visible",
},
@@ -99,8 +113,15 @@ async function runToolContinuity(
providerMode: "mock-openai",
primaryModel: "openai/primary-model",
alternateModel: "OPENAI/alternate-alias",
mock: { baseUrl: "http://mock.test" },
gateway: {},
},
fetchJson: async (url: string) =>
url.includes("request-cursor")
? { cursor: call }
: requests.filter(
(request) => request.cursor > Number(url.match(/after=(\d+)/)?.[1] ?? -1),
),
splitModelRef,
normalizeModelRef,
normalizeLowercaseStringOrEmpty: (value: unknown) =>
@@ -135,12 +156,26 @@ describe("model-switch tool continuity terminal evidence", () => {
text: "the **model handoff** preserved the QA mission after rereading the scenario pack",
},
terminalDelivery: { status: "sent", resultCount: 1 },
alternateTool: { logical: "read", wire: "read" },
});
expect(result.steps[0]?.details).toBe(
"the **model handoff** preserved the QA mission after rereading the scenario pack",
);
});
it("accepts Code Mode exec receipts for logically planned reads", async () => {
const { result } = await runToolContinuity(["exec"], {
alternateWireToolName: "exec",
});
expect(result.status).toBe("pass");
expect(result.modelSwitchEvidence).toMatchObject({
primary: { successfulToolNames: ["read"] },
alternate: { successfulToolNames: ["exec"] },
alternateTool: { logical: "read", wire: "exec" },
});
});
it("does not let a successful prior-run read satisfy the alternate run", async () => {
await expect(runToolContinuity([])).rejects.toThrow(
"alternate-model run did not return exact owned successful read evidence",
@@ -205,7 +205,7 @@ describe("qa scenario catalog", () => {
expect(marked.map((scenario) => scenario.id).toSorted()).toEqual(expected);
const ssh = readQaScenarioById("gateway-ssh-tunnels");
expect(ssh.execution).toMatchObject({ kind: "script", parallelSafe: true });
expect(ssh.execution).not.toHaveProperty("allowBlockedEvidence");
expect(ssh.execution).toMatchObject({ allowBlockedEvidence: true });
});
it("rejects invalid provider metadata at the catalog boundary", () => {
@@ -66,6 +66,7 @@ export function registerSlackMemberEvents(params: {
ctx.runtime.error?.(
danger(`slack ${paramsLocal.verb} handler failed: ${formatErrorMessage(err)}`),
);
throw err;
}
};
@@ -13,11 +13,14 @@ import {
} from "../monitor.test-helpers.js";
import { formatSlackChannelResolved, formatSlackUserResolved } from "./provider-support.js";
const webClientMocks = vi.hoisted(() => ({ constructed: vi.fn() }));
const { monitorSlackProvider } = await import("./provider.js");
const slackTestState = getSlackTestState();
beforeEach(() => {
resetSlackTestState();
webClientMocks.constructed.mockClear();
});
function createRuntimeContextCapture(): {
@@ -103,6 +106,23 @@ describe("slack allowlist log formatting", () => {
describe("slack startup user allowlist resolution", () => {
it("registers one native approval client per Enterprise Grid team", async () => {
vi.resetModules();
vi.doMock("../client.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../client.js")>();
const webApi = await vi.importActual<typeof import("@slack/web-api")>("@slack/web-api");
return {
...actual,
createSlackStartupAuthClient: () => getSlackClient(),
createSlackWebClient: (
token: string,
options?: import("@slack/web-api").WebClientOptions,
) => {
webClientMocks.constructed(token, options);
return new webApi.WebClient(token, options);
},
};
});
const { monitorSlackProvider: cacheAwareMonitor } = await import("./provider.js");
resetSlackTestState({
channels: {
slack: {
@@ -120,13 +140,15 @@ describe("slack startup user allowlist resolution", () => {
},
});
getSlackClient().auth.test.mockResolvedValueOnce({
user_id: "U_BOT",
bot_id: "B_BOT",
enterprise_id: "E123",
app_id: "A123",
is_enterprise_install: true,
});
const { channelRuntime, register } = createRuntimeContextCapture();
const monitor = startSlackMonitor(monitorSlackProvider, {
const monitor = startSlackMonitor(cacheAwareMonitor, {
channelRuntime,
appToken: "xapp-1-A123-test",
});
@@ -139,16 +161,27 @@ describe("slack startup user allowlist resolution", () => {
| undefined;
const resolveClient = registration?.context?.resolveClient;
expect(resolveClient).toBeTypeOf("function");
const teamOne = resolveClient?.("T111") as { teamId?: string };
const teamOneAgain = resolveClient?.("T111") as { teamId?: string };
const teamTwo = resolveClient?.("T222") as { teamId?: string };
const teamOne = resolveClient?.("T111");
const teamOneAgain = resolveClient?.("T111");
const teamTwo = resolveClient?.("T222");
expect(webClientMocks.constructed).toHaveBeenCalledTimes(2);
expect(teamOneAgain).toBe(teamOne);
expect(teamTwo).not.toBe(teamOne);
expect(teamOne.teamId).toBe("T111");
expect(teamTwo.teamId).toBe("T222");
expect(webClientMocks.constructed).toHaveBeenNthCalledWith(
1,
"bot-token",
expect.objectContaining({ teamId: "T111" }),
);
expect(webClientMocks.constructed).toHaveBeenNthCalledWith(
2,
"bot-token",
expect.objectContaining({ teamId: "T222" }),
);
} finally {
await stopSlackMonitor(monitor);
vi.doUnmock("../client.js");
vi.resetModules();
}
});
@@ -2,13 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const spawnSyncMock = vi.hoisted(() => vi.fn());
vi.mock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return { ...actual, spawnSync: spawnSyncMock };
});
import { teamsMeetingsConfig } from "./config.js";
import { handleTeamsMeetingsNodeHostCommand } from "./node-host.js";
let handleTeamsMeetingsNodeHostCommand: typeof import("./node-host.js").handleTeamsMeetingsNodeHostCommand;
const successfulProbe = {
pid: 123,
@@ -29,13 +23,20 @@ function setupParams() {
}
describe("Teams meeting node-host prerequisite deadline", () => {
beforeEach(() => {
beforeEach(async () => {
vi.resetModules();
vi.doMock("node:child_process", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:child_process")>();
return { ...actual, spawnSync: spawnSyncMock };
});
({ handleTeamsMeetingsNodeHostCommand } = await import("./node-host.js"));
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
spawnSyncMock.mockReset();
spawnSyncMock.mockReturnValue(successfulProbe);
});
afterEach(() => {
vi.doUnmock("node:child_process");
vi.restoreAllMocks();
});
@@ -58,32 +59,6 @@ describe("Teams meeting node-host prerequisite deadline", () => {
).toEqual([10_000, 7_000, 3_000]);
});
it("probes the default sox executable only once", async () => {
await expect(
handleTeamsMeetingsNodeHostCommand(
JSON.stringify({
action: "setup",
audioInputCommand: teamsMeetingsConfig.defaultAudioInputCommand,
audioOutputCommand: teamsMeetingsConfig.defaultAudioOutputCommand,
}),
),
).resolves.toBe(
JSON.stringify({
ok: true,
audioBackend: "blackhole-2ch",
audioDeviceLabel: "BlackHole 2ch",
}),
);
expect(spawnSyncMock).toHaveBeenCalledTimes(2);
expect(spawnSyncMock.mock.calls[1]?.[1]).toEqual([
"-lc",
'command -v "$1" >/dev/null 2>&1',
"sh",
"sox",
]);
});
it("does not start another probe after the shared deadline expires", async () => {
const now = vi.spyOn(Date, "now");
for (const value of [1_000, 1_000, 11_000]) {
@@ -32,6 +32,9 @@ describe("Microsoft Teams meetings node invoke policy", () => {
expect(invokeNode).toHaveBeenCalledWith({
params: {
action: "setup",
audioBackend: "auto",
audioBufferBytes: 4_096,
audioFormat: "pcm16-24khz",
audioInputCommand: ["trusted-input", "--read"],
audioOutputCommand: ["trusted-output", "--write"],
bargeInInputCommand: ["trusted-barge-in"],
@@ -32,6 +32,9 @@ describe("Zoom meetings node invoke policy", () => {
expect(invokeNode).toHaveBeenCalledWith({
params: {
action: "setup",
audioBackend: "auto",
audioBufferBytes: 4_096,
audioFormat: "pcm16-24khz",
audioInputCommand: ["trusted-input", "--read"],
audioOutputCommand: ["trusted-output", "--write"],
bargeInInputCommand: ["trusted-barge-in"],
@@ -243,6 +243,27 @@ describe("calculateContextTokens", () => {
});
expect(estimateContextTokens(messages).trailingTokens).toBeGreaterThan(0);
});
it("scans past a sparse assistant row to retain older valid usage", () => {
const validUsage = createUsage(20);
const sparseAssistant = {
role: "assistant",
content: [{ type: "text", text: "seeded without provider usage" }],
api: "test-api",
provider: "test-provider",
model: "test-model",
stopReason: "stop",
timestamp: 2,
} as AgentMessage;
const messages = [createAssistant("complete", validUsage, 1), sparseAssistant];
expect(getLastAssistantUsage(messages.map(createMessageEntry))).toBe(validUsage);
expect(estimateContextTokens(messages)).toMatchObject({
usageTokens: 20,
lastUsageIndex: 0,
});
expect(estimateContextTokens(messages).trailingTokens).toBeGreaterThan(0);
});
});
describe("session-entry compaction budgeting", () => {
@@ -128,13 +128,17 @@ function isUnavailableContextBarrier(message: AgentMessage): boolean {
if (message.role !== "assistant") {
return false;
}
if (message.api === "cli" && message.usage.contextUsage === undefined) {
return true;
}
if (message.usage.contextUsage?.state !== "unavailable") {
const usage = "usage" in message ? message.usage : undefined;
if (!usage) {
return false;
}
return calculateContextTokens(message.usage) === 0;
if (message.api === "cli" && usage.contextUsage === undefined) {
return true;
}
if (usage.contextUsage?.state !== "unavailable") {
return false;
}
return calculateContextTokens(usage) === 0;
}
/** Return usage from the last valid assistant message in session entries. */
@@ -18,7 +18,7 @@ scenario:
- docs/gateway/protocol.md
- docs/help/testing.md
codeRefs:
- src/gateway/server-methods/agent-dedupe.ts
- src/gateway/server-methods/agent.ts
- src/gateway/server-methods/agent-wait.ts
- test/e2e/qa-lab/runtime/agent-session-dedup-reconnect.e2e.test.ts
execution:
@@ -17,7 +17,7 @@ scenario:
- docs/concepts/qa-e2e-automation.md
codeRefs:
- extensions/qa-lab/src/qa-transport.ts
- src/channels/turn/kernel.ts
- src/channels/message-access/sender-gates.ts
execution:
kind: flow
summary: Exercise sender allowlist rejection and the configured driver override in one ordered flow.
@@ -30,7 +30,7 @@ scenario:
docsRefs:
- docs/channels/qa-channel.md
codeRefs:
- src/auto-reply/reply/provider-request-error-classifier.ts
- src/agents/failover/request-error-facets.ts
- src/auto-reply/reply/agent-runner-error-handler.ts
- extensions/qa-lab/src/providers/mock-openai/server.ts
execution:
@@ -18,7 +18,7 @@ scenario:
codeRefs:
- src/agents/tools/image-generate-tool.ts
- src/agents/tools/media-generate-background-shared.ts
- src/agents/image-generation-task-status.ts
- src/agents/tools/media-generate-background.ts
- test/e2e/qa-lab/media/image-generation-lifecycle.e2e.test.ts
execution:
kind: vitest
@@ -53,11 +53,14 @@ flow:
timeoutMs:
expr: liveTurnTimeoutMs(env, 30000)
- assert:
expr: "(() => { const expected = normalizeModelRef(env.primaryModel); const receipt = primaryRun?.waited?.terminalReceipt; return receipt?.runId === primaryRun?.started?.runId && normalizeLowercaseStringOrEmpty(receipt.requested?.provider) === expected.provider && receipt.requested?.model === expected.model && receipt.successfulToolNames?.includes('read') && receipt.terminalDisposition === 'visible'; })()"
message: default-model run did not return owned successful read evidence
expr: "(() => { const expected = normalizeModelRef(env.primaryModel); const receipt = primaryRun?.waited?.terminalReceipt; return receipt?.runId === primaryRun?.started?.runId && Boolean(receipt.sessionId) && Boolean(receipt.turnId) && normalizeLowercaseStringOrEmpty(receipt.requested?.provider) === expected.provider && receipt.requested?.model === expected.model && receipt.terminalDisposition === 'visible'; })()"
message: default-model run did not return exact owned terminal evidence
- assert:
expr: "primaryRun?.waited?.terminalDelivery?.status === 'sent' && typeof primaryRun.waited.terminalDelivery.resultCount === 'number' && primaryRun.waited.terminalDelivery.resultCount > 0"
message: default-model run did not return owned sent delivery evidence
- set: alternateRequestCursor
value:
expr: "env.mock ? (await fetchJson(`${env.mock.baseUrl}/debug/request-cursor`)).cursor : null"
- call: runAgentPrompt
saveAs: alternateRun
args:
@@ -71,8 +74,27 @@ flow:
expr: expectedAlternate.model
timeoutMs:
expr: resolveQaLiveTurnTimeoutMs(env, 30000, env.alternateModel)
- set: alternateToolRequests
value:
expr: "env.mock ? (await fetchJson(`${env.mock.baseUrl}/debug/requests?after=${alternateRequestCursor}`)).filter((request) => String(request.allInputText ?? '').includes(config.followupPrompt) && request.plannedToolName === 'read') : []"
- assert:
expr: "(() => { const receipt = alternateRun?.waited?.terminalReceipt; return receipt?.runId === alternateRun?.started?.runId && Boolean(receipt.sessionId) && Boolean(receipt.turnId) && normalizeLowercaseStringOrEmpty(receipt.requested?.provider) === expectedAlternate.provider && receipt.requested?.model === expectedAlternate.model && receipt.effective?.model === receipt.effective?.responseModel && receipt.successfulToolNames?.includes('read') && receipt.terminalDisposition === 'visible' && typeof receipt.rerouted === 'boolean' && `${receipt.effective?.provider}/${receipt.effective?.responseModel}` !== `${primaryRun.waited.terminalReceipt.effective?.provider}/${primaryRun.waited.terminalReceipt.effective?.responseModel}`; })()"
expr: "!env.mock || alternateToolRequests.length > 0"
message: alternate-model debug evidence did not plan the logical read tool
- set: alternateToolRequest
value:
expr: "env.mock ? alternateToolRequests.find((request) => alternateRun?.waited?.terminalReceipt?.successfulToolNames?.includes(request.plannedWireToolName ?? request.plannedToolName)) : null"
- assert:
expr: "!env.mock || Boolean(alternateToolRequest)"
message:
expr: "`alternate-model run did not return exact owned successful read evidence: planned=${JSON.stringify(alternateToolRequests.map((request) => ({ logical: request.plannedToolName ?? null, wire: request.plannedWireToolName ?? request.plannedToolName ?? null })))} receipt=${JSON.stringify(alternateRun?.waited?.terminalReceipt ?? null)}`"
- set: alternateWireToolName
value:
expr: "alternateToolRequest?.plannedWireToolName ?? alternateToolRequest?.plannedToolName ?? (alternateRun?.waited?.terminalReceipt?.successfulToolNames?.includes('read') ? 'read' : 'exec')"
- assert:
expr: "alternateWireToolName === 'read' || alternateWireToolName === 'exec'"
message: alternate-model read did not resolve to a supported direct or Code Mode wire tool
- assert:
expr: "(() => { const receipt = alternateRun?.waited?.terminalReceipt; return receipt?.runId === alternateRun?.started?.runId && Boolean(receipt.sessionId) && Boolean(receipt.turnId) && normalizeLowercaseStringOrEmpty(receipt.requested?.provider) === expectedAlternate.provider && receipt.requested?.model === expectedAlternate.model && receipt.effective?.model === receipt.effective?.responseModel && receipt.successfulToolNames?.includes(alternateWireToolName) && receipt.terminalDisposition === 'visible' && typeof receipt.rerouted === 'boolean' && `${receipt.effective?.provider}/${receipt.effective?.responseModel}` !== `${primaryRun.waited.terminalReceipt.effective?.provider}/${primaryRun.waited.terminalReceipt.effective?.responseModel}`; })()"
message: alternate-model run did not return exact owned successful read evidence
- assert:
expr: "alternateRun?.waited?.terminalReply?.disposition === 'visible' && hasModelSwitchContinuitySignal(alternateRun.waited.terminalReply.text)"
@@ -86,6 +108,11 @@ flow:
ref: primaryRun.waited.terminalReceipt
alternate:
ref: alternateRun.waited.terminalReceipt
alternateTool:
logical:
ref: alternateToolRequest.plannedToolName
wire:
ref: alternateWireToolName
terminalReply:
ref: alternateRun.waited.terminalReply
terminalDelivery:
@@ -22,7 +22,7 @@ scenario:
codeRefs:
- src/cli/plugins-search-command.ts
- src/plugins/catalog-search.ts
- src/infra/clawhub.ts
- src/plugins/clawhub.ts
- scripts/e2e/lib/clawhub-fixture-server.cjs
- src/cli/plugins-search-command.clawhub.e2e.test.ts
execution:
@@ -28,7 +28,7 @@ scenario:
codeRefs:
- packages/sdk/src/client.ts
- packages/sdk/src/app-sdk-composed-resources.e2e.test.ts
- src/gateway/server-methods/agent-job.ts
- src/gateway/server-methods/agent-run-handler.ts
- src/gateway/server-methods/artifacts.ts
- src/gateway/server-methods/environments.ts
execution:
@@ -19,7 +19,7 @@ scenario:
- docs/concepts/qa-e2e-automation.md
codeRefs:
- src/gateway/server-methods/agent.ts
- src/gateway/server-methods/agent-delivery-phase.ts
- src/gateway/server-methods/agent-run-handler.ts
- src/gateway/server.agent.rpc-contracts.test.ts
execution:
kind: vitest
+2 -2
View File
@@ -45,7 +45,7 @@ run_suspension_phase() {
-e "GW_MODE=suspension-$stage-restart" \
-e "GW_STATE_PATH=$SUSPENSION_STATE_PATH" \
"$GW_NAME" \
node --import tsx scripts/e2e/lib/gateway-network/client.mts
tsx scripts/e2e/lib/gateway-network/client.mts
}
trap cleanup EXIT
@@ -84,7 +84,7 @@ DOCKER_COMMAND_TIMEOUT="$CLIENT_TIMEOUT" run_logged gateway-network-client docke
-e "GW_URL=ws://$GW_NAME:$PORT" \
-e "GW_TOKEN=$TOKEN" \
"$IMAGE_NAME" \
node --import tsx scripts/e2e/lib/gateway-network/client.mts
tsx scripts/e2e/lib/gateway-network/client.mts
phase_started="$SECONDS"
echo "Running cooperative suspension lifecycle before container stop..."
+1 -1
View File
@@ -52,7 +52,7 @@ echo "Running kitchen-sink RPC Docker E2E..."
docker_e2e_docker_cmd rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
docker_e2e_harness_mount_args
DOCKER_COMMAND_TIMEOUT="$DOCKER_RUN_TIMEOUT" docker_e2e_docker_run_cmd run --name "$CONTAINER_NAME" "${DOCKER_E2E_HARNESS_ARGS[@]}" "${DOCKER_ENV_ARGS[@]}" -i "$IMAGE_NAME" \
node --import tsx scripts/e2e/kitchen-sink-rpc-walk.mts >"$RUN_LOG" 2>&1 &
tsx scripts/e2e/kitchen-sink-rpc-walk.mts >"$RUN_LOG" 2>&1 &
docker_pid="$!"
docker_e2e_sample_stats_until_exit \
+4 -3
View File
@@ -12,7 +12,7 @@ import path from "node:path";
import process from "node:process";
import { setTimeout as delay } from "node:timers/promises";
import { fileURLToPath, pathToFileURL } from "node:url";
import { asRecord, isRecord } from "@openclaw/normalization-core/record-coerce";
import { asRecord, isRecord } from "../../packages/normalization-core/src/record-coerce.ts";
import {
createBoundedResponseTooLargeError,
readBoundedResponseText,
@@ -1208,7 +1208,8 @@ function isRetryableTransientNetworkError(error: unknown, seen = new Set<unknown
const message =
candidate instanceof Error ? candidate.message : typeof candidate === "string" ? candidate : "";
const code = asRecord(candidate).code;
const text = `${String(code ?? "")} ${message}`;
const normalizedCode = typeof code === "string" || typeof code === "number" ? String(code) : "";
const text = `${normalizedCode} ${message}`;
if (
/\b(?:ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE|EHOSTUNREACH|ENETUNREACH)\b/iu.test(text) ||
/\b(?:fetch failed|socket hang up|connection reset)\b/iu.test(text)
@@ -1971,7 +1972,7 @@ export async function assertOperatorRpcDenied(
} catch (error) {
const candidate = asRecord(error);
const gatewayCode = candidate.gatewayCode;
const message = String(candidate.message ?? "");
const message = typeof candidate.message === "string" ? candidate.message : "";
if (gatewayCode === "INVALID_REQUEST" && message.includes("unauthorized role: operator")) {
return;
}
+1 -5
View File
@@ -167,11 +167,7 @@ docker_e2e_test_state_entrypoint() {
docker_e2e_run_test_state() {
local entrypoint
entrypoint="$(docker_e2e_test_state_entrypoint)" || return
if [[ "$entrypoint" == *.mts ]]; then
node --import tsx "$entrypoint" "$@"
else
node "$entrypoint" "$@"
fi
node "$entrypoint" "$@"
}
docker_e2e_test_state_shell_b64() {
+1
View File
@@ -268,6 +268,7 @@ docker_e2e_harness_mount_args() {
DOCKER_E2E_HARNESS_ARGS=(
-v "$harness_root/scripts/e2e:/app/scripts/e2e:ro"
-v "$harness_root/scripts/lib:/app/scripts/lib:ro"
-v "$harness_root/packages/gateway-client/src:/app/packages/gateway-client/src:ro"
-v "$harness_root/packages/normalization-core/src:/app/packages/normalization-core/src:ro"
-v "$harness_root/test/e2e/qa-lab:/app/test/e2e/qa-lab:ro"
-v "$harness_root/test/helpers:/app/test/helpers:ro"
+3 -3
View File
@@ -31,9 +31,9 @@ type TestStateOptions = {
function usage() {
return `Usage:
node --import tsx scripts/lib/openclaw-test-state.mts -- create [--label <name>] [--scenario <name>] [--env-file <path>] [--json]
node --import tsx scripts/lib/openclaw-test-state.mts shell [--label <name>] [--scenario <name>]
node --import tsx scripts/lib/openclaw-test-state.mts shell-function
node scripts/lib/openclaw-test-state.mts -- create [--label <name>] [--scenario <name>] [--env-file <path>] [--json]
node scripts/lib/openclaw-test-state.mts shell [--label <name>] [--scenario <name>]
node scripts/lib/openclaw-test-state.mts shell-function
Scenarios: ${[...SCENARIOS].join(", ")}
`;
@@ -250,6 +250,11 @@ export function doesApprovalRequestSelectChannelAccount(params: {
if (!accountId) {
return false;
}
const expectedChannel = normalizeOptionalChannel(params.channel);
const turnSourceChannel = normalizeOptionalChannel(params.request.request.turnSourceChannel);
if (!expectedChannel || (turnSourceChannel && turnSourceChannel !== expectedChannel)) {
return false;
}
const boundAccountId = resolveApprovalRequestChannelAccountId(params);
if (accountId === normalizeOptionalAccountId(boundAccountId)) {
return true;
@@ -67,6 +67,23 @@ const baseRequest: ExecApprovalRequest = {
};
describe("native approval account selection", () => {
it("does not let a conflicting turn-source channel fall through to the sole account", () => {
const discordRequest = buildRequest({
turnSourceChannel: "discord",
turnSourceAccountId: "default",
});
expect(
doesApprovalRequestSelectChannelAccount({
cfg: {},
request: discordRequest,
channel: "slack",
accountId: "default",
defaultAccountId: "default",
eligibleAccountIds: ["default"],
}),
).toBe(false);
});
it("selects only the sole eligible account when no owner is recorded", () => {
expect(
doesApprovalRequestSelectChannelAccount({
+26 -3
View File
@@ -11,7 +11,12 @@ vi.mock("node:child_process", async (importOriginal) => ({
import { createMeetingConfiguredNodeHost } from "./configured-node-host.js";
function createHost() {
function createHost(
commands: {
input?: string[];
output?: string[];
} = {},
) {
return createMeetingConfiguredNodeHost({
agentMode: "agent",
bridgeIdPrefix: "test-node-",
@@ -28,8 +33,8 @@ function createHost() {
bufferBytes: 4_096,
format: "pcm16-24khz",
},
defaultAudioInputCommand: ["legacy-capture"],
defaultAudioOutputCommand: ["legacy-playback"],
defaultAudioInputCommand: commands.input ?? ["legacy-capture"],
defaultAudioOutputCommand: commands.output ?? ["legacy-playback"],
displayName: "Test meeting",
meetingLabel: "Test meeting",
normalizeMeetingKey: (url) => url,
@@ -147,4 +152,22 @@ describe("configured meeting node host", () => {
);
}
});
it("probes one executable once when every configured audio command shares it", async () => {
vi.spyOn(process, "platform", "get").mockReturnValue("darwin");
childProcessMocks.spawnSync.mockImplementation((command: string) => ({
status: 0,
stdout: command.endsWith("system_profiler") ? "BlackHole 2ch" : "",
stderr: "",
}));
await createHost({ input: ["sox", "capture"], output: ["sox", "play"] })(
JSON.stringify({ action: "setup", bargeInInputCommand: ["sox", "barge"] }),
);
const soxProbes = childProcessMocks.spawnSync.mock.calls.filter(
(call) => Array.isArray(call[1]) && call[1].at(-1) === "sox",
);
expect(soxProbes).toHaveLength(1);
});
});
@@ -32,7 +32,6 @@ const REQUIRED_REVIEWED_PUBLISHABLE_CRITICAL_FINDING_COUNTS = new Map<string, nu
["@openclaw/codex:dangerous-exec:src/app-server/transport-stdio.ts", 1],
["@openclaw/codex:dangerous-exec:src/node-cli-sessions.ts", 1],
["@openclaw/discord:dangerous-exec:src/voice/audio.ts", 1],
["@openclaw/google-meet:dangerous-exec:src/node-host.ts", 1],
["@openclaw/imessage:dangerous-exec:src/client.ts", 1],
["@openclaw/mxc-sandbox:dangerous-exec:src/readiness.ts", 2],
["@openclaw/opencode-provider:dangerous-exec:session-catalog.ts", 1],
+3 -3
View File
@@ -268,7 +268,7 @@ describe("resolvePluginRuntimeLoadContext", () => {
...metadataSnapshot,
index: {
installRecords: {
demo: { source: "registry", version: "1.0.0" },
demo: { source: "npm", version: "1.0.0" },
},
plugins: [],
policyHash: "policy",
@@ -282,10 +282,10 @@ describe("resolvePluginRuntimeLoadContext", () => {
});
expect(context.installRecords).toEqual({
demo: { source: "registry", version: "1.0.0" },
demo: { source: "npm", version: "1.0.0" },
});
expect(buildPluginRuntimeLoadOptions(context).installRecords).toEqual({
demo: { source: "registry", version: "1.0.0" },
demo: { source: "npm", version: "1.0.0" },
});
});
+12
View File
@@ -565,6 +565,10 @@ print_log_tail "$LOG_PATH"
const compiledResult = resolveEntrypoint(compiledRoot);
expect(compiledResult.status, compiledResult.stderr).toBe(0);
expect(compiledResult.stdout.trim()).toBe(compiledEntrypoint);
const helper = readFileSync(DOCKER_E2E_IMAGE_HELPER_PATH, "utf8");
expect(helper).toContain('node "$entrypoint" "$@"');
expect(helper).not.toContain('node --import tsx "$entrypoint"');
});
it("rejects malformed Docker E2E resource limits before a suite starts", () => {
@@ -4060,9 +4064,14 @@ source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh"
it("keeps the kitchen-sink RPC Docker watchdog above the internal walk budgets", () => {
const runner = readFileSync(KITCHEN_SINK_RPC_DOCKER_E2E_PATH, "utf8");
const walk = readFileSync("scripts/e2e/kitchen-sink-rpc-walk.mts", "utf8");
expect(runner).toContain(
'DOCKER_RUN_TIMEOUT="${OPENCLAW_KITCHEN_SINK_RPC_DOCKER_RUN_TIMEOUT:-1500s}"',
);
expect(runner).toContain("tsx scripts/e2e/kitchen-sink-rpc-walk.mts");
expect(runner).not.toContain("node --import tsx scripts/e2e/kitchen-sink-rpc-walk.mts");
expect(walk).toContain("../../packages/normalization-core/src/record-coerce.ts");
expect(walk).not.toContain("@openclaw/normalization-core");
});
it("bounds kitchen-sink plugin CLI commands inside the Docker sweep", () => {
@@ -4197,6 +4206,8 @@ source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh"
expect(runner).not.toContain(
'run_logged gateway-network-client timeout "$CLIENT_TIMEOUT" docker run --rm',
);
expect(runner).toContain("tsx scripts/e2e/lib/gateway-network/client.mts");
expect(runner).not.toContain("node --import tsx scripts/e2e/lib/gateway-network/client.mts");
});
it("proves gateway suspension across a same-container process restart", () => {
@@ -4280,6 +4291,7 @@ source "$ROOT_DIR/scripts/lib/docker-e2e-logs.sh"
"--allow-unreleased-changelog",
'local harness_root="${DOCKER_E2E_HARNESS_ROOT_DIR:-$ROOT_DIR}"',
'-v "$harness_root/scripts/windows-cmd-helpers.mjs:/app/scripts/windows-cmd-helpers.mjs:ro"',
'-v "$harness_root/packages/gateway-client/src:/app/packages/gateway-client/src:ro"',
'-v "$harness_root/packages/normalization-core/src:/app/packages/normalization-core/src:ro"',
'-v "$harness_root/test/e2e/qa-lab:/app/test/e2e/qa-lab:ro"',
'-v "$harness_root/test/helpers:/app/test/helpers:ro"',
@@ -268,7 +268,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => {
expect(script).toContain("OPENCLAW_KITCHEN_SINK_COMMAND_MAX_RSS_MIB");
expect(script).toContain("docker_e2e_sample_stats_until_exit");
expect(script).toContain("scripts/e2e/lib/docker-stats/assert-resource-ceiling.mjs");
expect(script).toContain("node --import tsx scripts/e2e/kitchen-sink-rpc-walk.mts");
expect(script).toContain("tsx scripts/e2e/kitchen-sink-rpc-walk.mts");
expect(walkScript).toContain("commands.list");
expect(walkScript).toContain("tools.invoke");
expect(walkScript).toContain("tts.providers");