test: speed extension and contract scenarios

This commit is contained in:
Peter Steinberger
2026-05-06 00:54:06 +01:00
parent cb42efb6e6
commit 093b2b9b5f
16 changed files with 270 additions and 89 deletions

View File

@@ -188,6 +188,7 @@ describe("active-memory plugin", () => {
payloads: [{ text: "- lemon pepper wings\n- blue cheese" }],
});
__testing.resetActiveRecallCacheForTests();
__testing.setTimeoutPartialDataGraceMsForTests(5);
plugin.register(api as unknown as OpenClawPluginApi);
});

View File

@@ -248,6 +248,7 @@ const toggleStoreLocks = new Map<string, AsyncLock>();
let lastActiveRecallCacheSweepAt = 0;
let minimumTimeoutMs = DEFAULT_MIN_TIMEOUT_MS;
let setupGraceTimeoutMs = DEFAULT_SETUP_GRACE_TIMEOUT_MS;
let timeoutPartialDataGraceMs = TIMEOUT_PARTIAL_DATA_GRACE_MS;
function createAsyncLock(): AsyncLock {
let lock: Promise<void> = Promise.resolve();
@@ -1906,7 +1907,7 @@ async function waitForSubagentPartialTimeoutData(
}
let timeoutId: ReturnType<typeof setTimeout> | undefined;
const timeoutPromise = new Promise<undefined>((resolve) => {
timeoutId = setTimeout(() => resolve(undefined), TIMEOUT_PARTIAL_DATA_GRACE_MS);
timeoutId = setTimeout(() => resolve(undefined), timeoutPartialDataGraceMs);
timeoutId.unref?.();
});
try {
@@ -3009,6 +3010,7 @@ const testing = {
lastActiveRecallCacheSweepAt = 0;
minimumTimeoutMs = DEFAULT_MIN_TIMEOUT_MS;
setupGraceTimeoutMs = DEFAULT_SETUP_GRACE_TIMEOUT_MS;
timeoutPartialDataGraceMs = TIMEOUT_PARTIAL_DATA_GRACE_MS;
},
setMinimumTimeoutMsForTests(value: number) {
minimumTimeoutMs = value;
@@ -3016,6 +3018,9 @@ const testing = {
setSetupGraceTimeoutMsForTests(value: number) {
setupGraceTimeoutMs = Math.max(0, Math.floor(value));
},
setTimeoutPartialDataGraceMsForTests(value: number) {
timeoutPartialDataGraceMs = Math.max(0, Math.floor(value));
},
setCachedResult,
getCircuitBreakerEntry(key: string) {
return timeoutCircuitBreaker.get(key);

View File

@@ -30,6 +30,7 @@ import {
convertGoogleMeetTtsAudioForBridge,
extendGoogleMeetOutputEchoSuppression,
isGoogleMeetLikelyAssistantEchoTranscript,
GOOGLE_MEET_AGENT_TRANSCRIPT_DEBOUNCE_MS,
resolveGoogleMeetRealtimeProvider,
resolveGoogleMeetRealtimeTranscriptionProvider,
startCommandAgentAudioBridge,
@@ -315,6 +316,7 @@ describe("google-meet plugin", () => {
});
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
chromeTransportTesting.setDepsForTest(null);
googleMeetPluginTesting.setCallGatewayFromCliForTests();
@@ -1250,14 +1252,16 @@ describe("google-meet plugin", () => {
introSent: true,
},
});
expect(voiceCallMocks.joinMeetViaVoiceCallGateway).toHaveBeenCalledWith({
config: expect.objectContaining({ defaultTransport: "twilio" }),
dialInNumber: "+15551234567",
dtmfSequence: "123456#",
logger: expect.objectContaining({ info: expect.any(Function) }),
message: "Say exactly: I'm here and listening.",
sessionKey: expect.stringMatching(/^voice:google-meet:meet_/),
});
expect(voiceCallMocks.joinMeetViaVoiceCallGateway).toHaveBeenCalledWith(
expect.objectContaining({
config: expect.objectContaining({ defaultTransport: "twilio" }),
dialInNumber: "+15551234567",
dtmfSequence: "123456#",
logger: expect.objectContaining({ info: expect.any(Function) }),
message: "Say exactly: I'm here and listening.",
sessionKey: expect.stringMatching(/^voice:google-meet:meet_/),
}),
);
});
it("passes the caller session key through tool joins for agent context forking", async () => {
@@ -2762,6 +2766,7 @@ describe("google-meet plugin", () => {
url: "https://meet.google.com/abc-defg-hij",
});
const { methods } = setup({
realtime: { introMessage: "" },
chrome: {
audioBridgeCommand: ["bridge", "start"],
waitForInCallMs: 1,
@@ -3781,6 +3786,7 @@ describe("google-meet plugin", () => {
const { methods, runCommandWithTimeout } = setup({
defaultMode: "bidi",
chrome: {
waitForInCallMs: 1,
audioBridgeHealthCommand: ["bridge", "status"],
audioBridgeCommand: ["bridge", "start"],
},
@@ -3822,6 +3828,7 @@ describe("google-meet plugin", () => {
});
it("uses realtime transcription plus regular TTS in Chrome agent mode", async () => {
vi.useFakeTimers();
let callbacks: Parameters<RealtimeTranscriptionProviderPlugin["createSession"]>[0] | undefined;
const sendAudio = vi.fn();
const sttSession = {
@@ -3919,7 +3926,7 @@ describe("google-meet plugin", () => {
);
inputStdout.write(Buffer.from([1, 0, 2, 0, 3, 0, 4, 0]));
callbacks?.onTranscript?.("Please summarize the launch.");
await new Promise((resolve) => setTimeout(resolve, 1100));
await vi.advanceTimersByTimeAsync(GOOGLE_MEET_AGENT_TRANSCRIPT_DEBOUNCE_MS);
expect(sendAudio).toHaveBeenCalledWith(expect.any(Buffer));
expect(runtime.agent.runEmbeddedPiAgent).toHaveBeenCalled();
@@ -4497,7 +4504,7 @@ describe("google-meet plugin", () => {
if (pullCount === 1) {
return { bridgeId: "bridge-1", base64: Buffer.from([9, 8, 7]).toString("base64") };
}
await new Promise((resolve) => setTimeout(resolve, 1_000));
await new Promise((resolve) => setTimeout(resolve, 10));
return { bridgeId: "bridge-1" };
}
return { ok: true };
@@ -4683,7 +4690,7 @@ describe("google-meet plugin", () => {
if (pullCount === 2) {
return { bridgeId: "bridge-1", base64: Buffer.from([5, 4, 3]).toString("base64") };
}
await new Promise((resolve) => setTimeout(resolve, 1_000));
await new Promise((resolve) => setTimeout(resolve, 10));
return { bridgeId: "bridge-1" };
}
return { ok: true };

View File

@@ -318,6 +318,14 @@ describe("matrix thread bindings", () => {
await vi.advanceTimersByTimeAsync(61_000);
await vi.waitFor(
() => expect(sendMessageMatrixMock.mock.calls.length).toBeGreaterThanOrEqual(2),
{
interval: 1,
timeout: 1_000,
},
);
await vi.waitFor(
async () => {
const persistedRaw = await fs.readFile(resolveBindingsFilePath(), "utf-8");

View File

@@ -325,6 +325,7 @@ describe("qa-lab server", () => {
port: 0,
repoRoot,
embeddedGateway: "disabled",
selfCheckWaitTimeoutMs: 1,
});
cleanups.push(async () => {
await lab.stop();

View File

@@ -244,6 +244,7 @@ export async function startQaLabServer(
transportId: "qa-channel",
outputPath: params?.outputPath,
repoRoot,
waitTimeoutMs: params?.selfCheckWaitTimeoutMs,
});
latestScenarioRun = withQaLabRunCounts({
kind: "self-check",

View File

@@ -55,6 +55,7 @@ export type QaLabServerStartParams = {
autoKickoffTarget?: string;
embeddedGateway?: string;
sendKickoffOnStart?: boolean;
selfCheckWaitTimeoutMs?: number;
};
export type QaLabServerHandle = {

View File

@@ -1,7 +1,10 @@
import { extractQaToolPayload } from "./extract-tool-payload.js";
import type { QaScenarioDefinition } from "./scenario.js";
export function createQaSelfCheckScenario(): QaScenarioDefinition {
export function createQaSelfCheckScenario(options?: {
waitTimeoutMs?: number;
}): QaScenarioDefinition {
const waitTimeoutMs = options?.waitTimeoutMs ?? 5_000;
return {
name: "Synthetic Slack-class roundtrip",
steps: [
@@ -18,7 +21,7 @@ export function createQaSelfCheckScenario(): QaScenarioDefinition {
kind: "message-text",
textIncludes: "qa-echo: hello from qa",
direction: "outbound",
timeoutMs: 5_000,
timeoutMs: waitTimeoutMs,
});
},
},
@@ -52,7 +55,7 @@ export function createQaSelfCheckScenario(): QaScenarioDefinition {
kind: "message-text",
textIncludes: "qa-echo: inside thread",
direction: "outbound",
timeoutMs: 5_000,
timeoutMs: waitTimeoutMs,
});
return threadId;
},

View File

@@ -29,6 +29,7 @@ export async function runQaSelfCheckAgainstState(params: {
outputPath?: string;
repoRoot?: string;
notes?: string[];
waitTimeoutMs?: number;
}): Promise<QaSelfCheckResult> {
const startedAt = new Date();
const transport = createQaTransportAdapter({
@@ -36,16 +37,19 @@ export async function runQaSelfCheckAgainstState(params: {
state: params.state,
});
params.state.reset();
const scenarioResult = await runQaScenario(createQaSelfCheckScenario(), {
state: params.state,
performAction: async (action, args) =>
await transport.handleAction({
action,
args,
cfg: params.cfg,
accountId: transport.accountId,
}),
});
const scenarioResult = await runQaScenario(
createQaSelfCheckScenario({ waitTimeoutMs: params.waitTimeoutMs }),
{
state: params.state,
performAction: async (action, args) =>
await transport.handleAction({
action,
args,
cfg: params.cfg,
accountId: transport.accountId,
}),
},
);
const checks = [
{
name: "QA self-check scenario",

View File

@@ -105,6 +105,7 @@ export function startMatrixQaOpenClawCli(params: {
const stderr: Buffer[] = [];
let closed = false;
let closeResult: MatrixQaCliRunResult | undefined;
let timedOut = false;
let settleWait:
| {
reject: (error: Error) => void;
@@ -138,24 +139,31 @@ export function startMatrixQaOpenClawCli(params: {
};
const timeout = setTimeout(() => {
const result = buildMatrixQaCliResult({
args: params.args,
exitCode: 1,
output: readOutput(),
});
timedOut = true;
child.kill("SIGTERM");
finish(
result,
new Error(
[
`${formatMatrixQaCliCommand(params.args)} timed out after ${params.timeoutMs}ms`,
result.stderr.trim() ? `stderr:\n${redactMatrixQaCliOutput(result.stderr.trim())}` : null,
result.stdout.trim() ? `stdout:\n${redactMatrixQaCliOutput(result.stdout.trim())}` : null,
]
.filter(Boolean)
.join("\n"),
),
);
setTimeout(() => {
const result = buildMatrixQaCliResult({
args: params.args,
exitCode: 1,
output: readOutput(),
});
finish(
result,
new Error(
[
`${formatMatrixQaCliCommand(params.args)} timed out after ${params.timeoutMs}ms`,
result.stderr.trim()
? `stderr:\n${redactMatrixQaCliOutput(result.stderr.trim())}`
: null,
result.stdout.trim()
? `stdout:\n${redactMatrixQaCliOutput(result.stdout.trim())}`
: null,
]
.filter(Boolean)
.join("\n"),
),
);
}, 25);
}, params.timeoutMs);
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
@@ -176,6 +184,9 @@ export function startMatrixQaOpenClawCli(params: {
});
child.on("close", (exitCode) => {
clearTimeout(timeout);
if (timedOut) {
return;
}
const result = buildMatrixQaCliResult({
args: params.args,
exitCode: exitCode ?? 1,