mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
fix: stabilize main test gates
This commit is contained in:
@@ -72,10 +72,22 @@ const QQBotAccountSchema = z
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const QQBotNamedAccountSchema = QQBotAccountSchema.superRefine((value, ctx) => {
|
||||
for (const key of ["tts", "stt"] as const) {
|
||||
if (key in value) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: [key],
|
||||
message: `channels.qqbot.accounts entries do not support ${key} overrides`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const QQBotConfigSchema = QQBotAccountSchema.extend({
|
||||
tts: QQBotTtsSchema,
|
||||
stt: QQBotSttSchema,
|
||||
accounts: z.object({}).catchall(QQBotAccountSchema.passthrough()).optional(),
|
||||
accounts: z.object({}).catchall(QQBotNamedAccountSchema).optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
}).passthrough();
|
||||
export const qqbotChannelConfigSchema = buildChannelConfigSchema(QQBotConfigSchema);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import type { MockInstance } from "vitest";
|
||||
|
||||
export function createWhatsAppPollFixture() {
|
||||
const cfg = { marker: "resolved-cfg" } as OpenClawConfig;
|
||||
@@ -14,3 +15,29 @@ export function createWhatsAppPollFixture() {
|
||||
accountId: "work",
|
||||
};
|
||||
}
|
||||
|
||||
export function expectWhatsAppPollSent(
|
||||
sendPollWhatsApp: MockInstance,
|
||||
params: {
|
||||
cfg: OpenClawConfig;
|
||||
poll: { question: string; options: string[]; maxSelections: number };
|
||||
to?: string;
|
||||
accountId?: string;
|
||||
},
|
||||
) {
|
||||
const expected = [
|
||||
params.to ?? "+1555",
|
||||
params.poll,
|
||||
{
|
||||
verbose: false,
|
||||
accountId: params.accountId ?? "work",
|
||||
cfg: params.cfg,
|
||||
},
|
||||
];
|
||||
const actual = sendPollWhatsApp.mock.calls.at(-1);
|
||||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||
throw new Error(
|
||||
`Expected WhatsApp poll send ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,7 +589,7 @@ describe("spawnAcpDirect", () => {
|
||||
agentSessionKey: "agent:main:matrix:channel:!room:example",
|
||||
agentChannel: "matrix",
|
||||
agentAccountId: "default",
|
||||
agentTo: "room:!room:example",
|
||||
agentTo: "channel:!room:example",
|
||||
},
|
||||
);
|
||||
expect(result.status, JSON.stringify(result)).toBe("accepted");
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { CliBundleMcpMode } from "../plugins/types.js";
|
||||
let createEmptyPluginRegistry: typeof import("../plugins/registry.js").createEmptyPluginRegistry;
|
||||
let resetPluginRuntimeStateForTest: typeof import("../plugins/runtime.js").resetPluginRuntimeStateForTest;
|
||||
let setActivePluginRegistry: typeof import("../plugins/runtime.js").setActivePluginRegistry;
|
||||
let normalizeClaudeBackendConfig: typeof import("./cli-backends.js").normalizeClaudeBackendConfig;
|
||||
let resolveCliBackendConfig: typeof import("./cli-backends.js").resolveCliBackendConfig;
|
||||
let resolveCliBackendLiveTest: typeof import("./cli-backends.js").resolveCliBackendLiveTest;
|
||||
|
||||
@@ -34,7 +33,7 @@ function createBackendEntry(params: {
|
||||
: params.id === "codex-cli"
|
||||
? "codex-cli/gpt-5.4"
|
||||
: params.id === "google-gemini-cli"
|
||||
? "google-gemini-cli/gemini-3.1-pro-preview"
|
||||
? "google-gemini-cli/gemini-3-flash-preview"
|
||||
: undefined,
|
||||
defaultImageProbe: true,
|
||||
defaultMcpProbe: true,
|
||||
@@ -93,6 +92,63 @@ const NORMALIZED_CLAUDE_FALLBACK_RESUME_ARGS = [
|
||||
"bypassPermissions",
|
||||
];
|
||||
|
||||
function normalizeTestClaudeArgs(args?: string[]): string[] | undefined {
|
||||
if (!args) {
|
||||
return args;
|
||||
}
|
||||
const normalized: string[] = [];
|
||||
let hasSettingSources = false;
|
||||
let hasPermissionMode = false;
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
if (arg === "--dangerously-skip-permissions") {
|
||||
continue;
|
||||
}
|
||||
if (arg === "--setting-sources") {
|
||||
const maybeValue = args[i + 1];
|
||||
if (maybeValue && !maybeValue.startsWith("-")) {
|
||||
hasSettingSources = true;
|
||||
normalized.push(arg, "user");
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--setting-sources=")) {
|
||||
hasSettingSources = true;
|
||||
normalized.push("--setting-sources=user");
|
||||
continue;
|
||||
}
|
||||
if (arg === "--permission-mode") {
|
||||
const maybeValue = args[i + 1];
|
||||
if (maybeValue && !maybeValue.startsWith("-")) {
|
||||
hasPermissionMode = true;
|
||||
normalized.push(arg, maybeValue);
|
||||
i += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--permission-mode=")) {
|
||||
hasPermissionMode = true;
|
||||
}
|
||||
normalized.push(arg);
|
||||
}
|
||||
if (!hasSettingSources) {
|
||||
normalized.push("--setting-sources", "user");
|
||||
}
|
||||
if (!hasPermissionMode) {
|
||||
normalized.push("--permission-mode", "bypassPermissions");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeTestClaudeBackendConfig(config: CliBackendConfig): CliBackendConfig {
|
||||
return {
|
||||
...config,
|
||||
args: normalizeTestClaudeArgs(config.args),
|
||||
resumeArgs: normalizeTestClaudeArgs(config.resumeArgs),
|
||||
};
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
vi.doUnmock("../plugins/setup-registry.js");
|
||||
vi.doUnmock("../plugins/cli-backends.runtime.js");
|
||||
@@ -100,8 +156,7 @@ beforeAll(async () => {
|
||||
({ createEmptyPluginRegistry } = await import("../plugins/registry.js"));
|
||||
({ resetPluginRuntimeStateForTest, setActivePluginRegistry } =
|
||||
await import("../plugins/runtime.js"));
|
||||
({ normalizeClaudeBackendConfig, resolveCliBackendConfig, resolveCliBackendLiveTest } =
|
||||
await import("./cli-backends.js"));
|
||||
({ resolveCliBackendConfig, resolveCliBackendLiveTest } = await import("./cli-backends.js"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -165,7 +220,7 @@ beforeEach(() => {
|
||||
"CLAUDE_CODE_USE_VERTEX",
|
||||
],
|
||||
},
|
||||
normalizeConfig: normalizeClaudeBackendConfig,
|
||||
normalizeConfig: normalizeTestClaudeBackendConfig,
|
||||
}),
|
||||
createBackendEntry({
|
||||
pluginId: "openai",
|
||||
|
||||
@@ -99,8 +99,14 @@ describe("normalizeProviders", () => {
|
||||
});
|
||||
it("replaces resolved env var value with env var name to prevent plaintext persistence", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-"));
|
||||
const original = process.env.OPENAI_API_KEY;
|
||||
process.env.OPENAI_API_KEY = "sk-test-secret-value-12345"; // pragma: allowlist secret
|
||||
const env = {
|
||||
...process.env,
|
||||
OPENAI_API_KEY: "sk-test-secret-value-12345", // pragma: allowlist secret
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
|
||||
OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
|
||||
OPENCLAW_SKIP_PROVIDERS: undefined,
|
||||
OPENCLAW_TEST_MINIMAL_GATEWAY: undefined,
|
||||
};
|
||||
const secretRefManagedProviders = new Set<string>();
|
||||
try {
|
||||
const providers: NonNullable<NonNullable<OpenClawConfig["models"]>["providers"]> = {
|
||||
@@ -121,15 +127,15 @@ describe("normalizeProviders", () => {
|
||||
],
|
||||
},
|
||||
};
|
||||
const normalized = normalizeProviders({ providers, agentDir, secretRefManagedProviders });
|
||||
const normalized = normalizeProviders({
|
||||
providers,
|
||||
agentDir,
|
||||
env,
|
||||
secretRefManagedProviders,
|
||||
});
|
||||
expect(normalized?.openai?.apiKey).toBe("OPENAI_API_KEY");
|
||||
expect(secretRefManagedProviders.has("openai")).toBe(true);
|
||||
} finally {
|
||||
if (original === undefined) {
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
} else {
|
||||
process.env.OPENAI_API_KEY = original;
|
||||
}
|
||||
await fs.rm(agentDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,10 +7,12 @@ type TimeoutCallbackMock = ReturnType<typeof vi.fn<TimeoutCallback>>;
|
||||
|
||||
async function withFakeTimers(run: () => Promise<void>) {
|
||||
vi.useFakeTimers();
|
||||
vi.clearAllTimers();
|
||||
try {
|
||||
await run();
|
||||
} finally {
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
vi.clearAllTimers();
|
||||
vi.useRealTimers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ afterEach(() => {
|
||||
vi.doUnmock("../../plugins/bundled-plugin-metadata.js");
|
||||
vi.doUnmock("../../plugins/discovery.js");
|
||||
vi.doUnmock("../../plugins/manifest-registry.js");
|
||||
vi.doUnmock("../../plugins/channel-catalog-registry.js");
|
||||
vi.doUnmock("../../infra/boundary-file-read.js");
|
||||
vi.doUnmock("jiti");
|
||||
});
|
||||
|
||||
@@ -42,13 +42,10 @@ vi.mock("../channels/plugins/setup-registry.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../channels/registry.js", () => ({
|
||||
getChatChannelMeta: (channelId: string) => ({ id: channelId, label: channelId }),
|
||||
listChatChannels: () => [],
|
||||
getChatChannelMeta: (channelId?: unknown) => ({
|
||||
id: typeof channelId === "string" ? channelId : "unknown",
|
||||
label: typeof channelId === "string" ? channelId : "Unknown",
|
||||
}),
|
||||
normalizeChatChannelId: (channelId?: unknown) =>
|
||||
typeof channelId === "string" ? channelId.trim().toLowerCase() : undefined,
|
||||
typeof channelId === "string" ? channelId.trim().toLowerCase() || null : null,
|
||||
}));
|
||||
|
||||
vi.mock("../commands/channel-setup/discovery.js", () => ({
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
agentCommand,
|
||||
getFreePort,
|
||||
installGatewayTestHooks,
|
||||
startGatewayServerWithRetries,
|
||||
testState,
|
||||
withGatewayServer,
|
||||
} from "./test-helpers.js";
|
||||
@@ -22,12 +23,21 @@ let enabledPort: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ startGatewayServer } = await import("./server.js"));
|
||||
enabledPort = await getFreePort();
|
||||
enabledServer = await startServer(enabledPort);
|
||||
const started = await startGatewayServerWithRetries({
|
||||
port: await getFreePort(),
|
||||
opts: {
|
||||
host: "127.0.0.1",
|
||||
auth: { mode: "none" },
|
||||
controlUiEnabled: false,
|
||||
openAiChatCompletionsEnabled: true,
|
||||
},
|
||||
});
|
||||
enabledPort = started.port;
|
||||
enabledServer = started.server;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await enabledServer.close({ reason: "openai http enabled suite done" });
|
||||
await enabledServer?.close({ reason: "openai http enabled suite done" });
|
||||
});
|
||||
|
||||
async function startServerWithDefaultConfig(port: number) {
|
||||
|
||||
@@ -6,7 +6,12 @@ import { HISTORY_CONTEXT_MARKER } from "../auto-reply/reply/history.js";
|
||||
import { CURRENT_MESSAGE_MARKER } from "../auto-reply/reply/mentions.js";
|
||||
import { emitAgentEvent } from "../infra/agent-events.js";
|
||||
import { buildAssistantDeltaResult } from "./test-helpers.agent-results.js";
|
||||
import { agentCommand, getFreePort, installGatewayTestHooks } from "./test-helpers.js";
|
||||
import {
|
||||
agentCommand,
|
||||
getFreePort,
|
||||
installGatewayTestHooks,
|
||||
startGatewayServerWithRetries,
|
||||
} from "./test-helpers.js";
|
||||
|
||||
installGatewayTestHooks({ scope: "suite" });
|
||||
|
||||
@@ -30,12 +35,21 @@ let openResponsesTesting: {
|
||||
|
||||
beforeAll(async () => {
|
||||
({ __testing: openResponsesTesting } = await import("./openresponses-http.js"));
|
||||
enabledPort = await getFreePort();
|
||||
enabledServer = await startServer(enabledPort, { openResponsesEnabled: true });
|
||||
const started = await startGatewayServerWithRetries({
|
||||
port: await getFreePort(),
|
||||
opts: {
|
||||
host: "127.0.0.1",
|
||||
auth: { mode: "none" },
|
||||
controlUiEnabled: false,
|
||||
openResponsesEnabled: true,
|
||||
},
|
||||
});
|
||||
enabledPort = started.port;
|
||||
enabledServer = started.server;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await enabledServer.close({ reason: "openresponses enabled suite done" });
|
||||
await enabledServer?.close({ reason: "openresponses enabled suite done" });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -373,31 +373,37 @@ describe("gateway server agent", () => {
|
||||
expectAgentRoutingCall({ channel: "webchat", deliver: false });
|
||||
});
|
||||
|
||||
test("agent routes bare /new through session reset before running greeting prompt", async () => {
|
||||
await writeMainSessionEntry({ sessionId: "sess-main-before-reset" });
|
||||
const spy = vi.mocked(agentCommand);
|
||||
const calls = spy.mock.calls;
|
||||
const callsBefore = calls.length;
|
||||
const res = await rpcReq(
|
||||
ws,
|
||||
"agent",
|
||||
{
|
||||
message: "/new",
|
||||
sessionKey: "main",
|
||||
idempotencyKey: "idem-agent-new",
|
||||
},
|
||||
20_000,
|
||||
);
|
||||
expect(res.ok).toBe(true);
|
||||
test(
|
||||
"agent routes bare /new through session reset before running greeting prompt",
|
||||
{
|
||||
timeout: 45_000,
|
||||
},
|
||||
async () => {
|
||||
await writeMainSessionEntry({ sessionId: "sess-main-before-reset" });
|
||||
const spy = vi.mocked(agentCommand);
|
||||
const calls = spy.mock.calls;
|
||||
const callsBefore = calls.length;
|
||||
const res = await rpcReq(
|
||||
ws,
|
||||
"agent",
|
||||
{
|
||||
message: "/new",
|
||||
sessionKey: "main",
|
||||
idempotencyKey: "idem-agent-new",
|
||||
},
|
||||
30_000,
|
||||
);
|
||||
expect(res.ok).toBe(true);
|
||||
|
||||
await vi.waitFor(() => expect(calls.length).toBeGreaterThan(callsBefore));
|
||||
const call = (calls.at(-1)?.[0] ?? {}) as Record<string, unknown>;
|
||||
expect(call.message).toBeTypeOf("string");
|
||||
expect(call.message).toContain("Run your Session Startup sequence");
|
||||
expect(call.message).toContain("Current time:");
|
||||
expect(typeof call.sessionId).toBe("string");
|
||||
expect(call.sessionId).not.toBe("sess-main-before-reset");
|
||||
});
|
||||
await vi.waitFor(() => expect(calls.length).toBeGreaterThan(callsBefore));
|
||||
const call = (calls.at(-1)?.[0] ?? {}) as Record<string, unknown>;
|
||||
expect(call.message).toBeTypeOf("string");
|
||||
expect(call.message).toContain("Run your Session Startup sequence");
|
||||
expect(call.message).toContain("Current time:");
|
||||
expect(typeof call.sessionId).toBe("string");
|
||||
expect(call.sessionId).not.toBe("sess-main-before-reset");
|
||||
},
|
||||
);
|
||||
|
||||
test("write-scoped callers cannot reset conversations via agent", async () => {
|
||||
await withGatewayServer(async ({ port }) => {
|
||||
|
||||
@@ -607,7 +607,7 @@ export async function startGatewayServer(port: number, opts?: GatewayServerOptio
|
||||
return server;
|
||||
}
|
||||
|
||||
async function startGatewayServerWithRetries(params: {
|
||||
export async function startGatewayServerWithRetries(params: {
|
||||
port: number;
|
||||
opts?: GatewayServerOptions;
|
||||
}): Promise<{ port: number; server: Awaited<ReturnType<typeof startGatewayServer>> }> {
|
||||
|
||||
@@ -318,18 +318,27 @@ describe("drainPendingDeliveries for WhatsApp reconnect", () => {
|
||||
}
|
||||
});
|
||||
|
||||
const nowSpy = vi.spyOn(Date, "now");
|
||||
nowSpy.mockReturnValueOnce(1_000);
|
||||
await enqueueDelivery(
|
||||
const blockerId = await enqueueDelivery(
|
||||
{ channel: "demo-channel-a", to: "+1000", payloads: [{ text: "blocker" }] },
|
||||
tmpDir,
|
||||
);
|
||||
nowSpy.mockReturnValueOnce(2_000);
|
||||
await enqueueDelivery(
|
||||
const whatsappId = await enqueueDelivery(
|
||||
{ channel: "whatsapp", to: "+1555", payloads: [{ text: "hi" }], accountId: "acct1" },
|
||||
tmpDir,
|
||||
);
|
||||
nowSpy.mockRestore();
|
||||
const queueDir = path.join(tmpDir, "delivery-queue");
|
||||
const blockerPath = path.join(queueDir, `${blockerId}.json`);
|
||||
const whatsappPath = path.join(queueDir, `${whatsappId}.json`);
|
||||
const blockerEntry = JSON.parse(fs.readFileSync(blockerPath, "utf-8")) as {
|
||||
enqueuedAt: number;
|
||||
};
|
||||
const whatsappEntry = JSON.parse(fs.readFileSync(whatsappPath, "utf-8")) as {
|
||||
enqueuedAt: number;
|
||||
};
|
||||
blockerEntry.enqueuedAt = 1;
|
||||
whatsappEntry.enqueuedAt = 2;
|
||||
fs.writeFileSync(blockerPath, JSON.stringify(blockerEntry, null, 2));
|
||||
fs.writeFileSync(whatsappPath, JSON.stringify(whatsappEntry, null, 2));
|
||||
|
||||
const startupRecovery = recoverPendingDeliveries({
|
||||
cfg: stubCfg,
|
||||
|
||||
Reference in New Issue
Block a user