mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:30:44 +00:00
test(release): harden qa live canaries
This commit is contained in:
@@ -28,6 +28,7 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", async () => {
|
||||
|
||||
describe("telegram live qa runtime", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
fetchWithSsrFGuardMock.mockClear();
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
@@ -100,6 +101,46 @@ describe("telegram live qa runtime", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("normalizes the Telegram canary timeout env", () => {
|
||||
expect(
|
||||
__testing.resolveTelegramQaCanaryTimeoutMs({
|
||||
OPENCLAW_QA_TELEGRAM_CANARY_TIMEOUT_MS: "12345",
|
||||
}),
|
||||
).toBe(12345);
|
||||
expect(
|
||||
__testing.resolveTelegramQaCanaryTimeoutMs({
|
||||
OPENCLAW_QA_TELEGRAM_CANARY_TIMEOUT_MS: "nope",
|
||||
}),
|
||||
).toBe(60_000);
|
||||
});
|
||||
|
||||
it("waits for Telegram polling connectivity before treating the account as ready", async () => {
|
||||
vi.useFakeTimers();
|
||||
const gateway = {
|
||||
call: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
channelAccounts: {
|
||||
telegram: [{ accountId: "sut", connected: false, running: true }],
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
channelAccounts: {
|
||||
telegram: [{ accountId: "sut", connected: true, running: true }],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const ready = __testing.waitForTelegramChannelRunning(gateway as never, "sut", {
|
||||
pollIntervalMs: 10,
|
||||
timeoutMs: 1_000,
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
await expect(ready).resolves.toBeUndefined();
|
||||
expect(gateway.call).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("sanitizes and truncates Telegram live progress details", () => {
|
||||
expect(__testing.sanitizeTelegramQaProgressValue("scenario\nid\tvalue")).toBe(
|
||||
"scenario id value",
|
||||
|
||||
@@ -302,6 +302,7 @@ const TELEGRAM_QA_ENV_KEYS = [
|
||||
"OPENCLAW_QA_TELEGRAM_DRIVER_BOT_TOKEN",
|
||||
"OPENCLAW_QA_TELEGRAM_SUT_BOT_TOKEN",
|
||||
] as const;
|
||||
const DEFAULT_TELEGRAM_QA_CANARY_TIMEOUT_MS = 60_000;
|
||||
const TELEGRAM_QA_CAPTURE_CONTENT_ENV = "OPENCLAW_QA_TELEGRAM_CAPTURE_CONTENT";
|
||||
const QA_REDACT_PUBLIC_METADATA_ENV = "OPENCLAW_QA_REDACT_PUBLIC_METADATA";
|
||||
const QA_SUITE_PROGRESS_ENV = "OPENCLAW_QA_SUITE_PROGRESS";
|
||||
@@ -342,6 +343,26 @@ function parseTelegramQaProgressBooleanEnv(value: string | undefined): boolean |
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parsePositiveTelegramQaEnvMs(env: NodeJS.ProcessEnv, name: string, fallback: number) {
|
||||
const raw = env[name];
|
||||
if (raw === undefined) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isFinite(parsed) || parsed < 1) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.floor(parsed);
|
||||
}
|
||||
|
||||
function resolveTelegramQaCanaryTimeoutMs(env: NodeJS.ProcessEnv = process.env) {
|
||||
return parsePositiveTelegramQaEnvMs(
|
||||
env,
|
||||
"OPENCLAW_QA_TELEGRAM_CANARY_TIMEOUT_MS",
|
||||
DEFAULT_TELEGRAM_QA_CANARY_TIMEOUT_MS,
|
||||
);
|
||||
}
|
||||
|
||||
function shouldLogTelegramQaLiveProgress(env: NodeJS.ProcessEnv = process.env) {
|
||||
const override = parseTelegramQaProgressBooleanEnv(env[QA_SUITE_PROGRESS_ENV]);
|
||||
if (override !== undefined) {
|
||||
@@ -633,9 +654,15 @@ async function waitForObservedMessage(params: {
|
||||
async function waitForTelegramChannelRunning(
|
||||
gateway: Awaited<ReturnType<typeof startQaGatewayChild>>,
|
||||
accountId: string,
|
||||
opts: {
|
||||
pollIntervalMs?: number;
|
||||
timeoutMs?: number;
|
||||
} = {},
|
||||
) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < 45_000) {
|
||||
const timeoutMs = opts.timeoutMs ?? 90_000;
|
||||
const pollIntervalMs = opts.pollIntervalMs ?? 500;
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
try {
|
||||
const payload = (await gateway.call(
|
||||
"channels.status",
|
||||
@@ -644,20 +671,25 @@ async function waitForTelegramChannelRunning(
|
||||
)) as {
|
||||
channelAccounts?: Record<
|
||||
string,
|
||||
Array<{ accountId?: string; running?: boolean; restartPending?: boolean }>
|
||||
Array<{
|
||||
accountId?: string;
|
||||
connected?: boolean;
|
||||
running?: boolean;
|
||||
restartPending?: boolean;
|
||||
}>
|
||||
>;
|
||||
};
|
||||
const accounts = payload.channelAccounts?.telegram ?? [];
|
||||
const match = accounts.find((entry) => entry.accountId === accountId);
|
||||
if (match?.running && match.restartPending !== true) {
|
||||
if (match?.running && match.connected === true && match.restartPending !== true) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// retry
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
||||
}
|
||||
throw new Error(`telegram account "${accountId}" did not become ready`);
|
||||
throw new Error(`telegram account "${accountId}" did not become ready and connected`);
|
||||
}
|
||||
|
||||
function renderTelegramQaMarkdown(params: {
|
||||
@@ -831,6 +863,7 @@ async function runCanary(params: {
|
||||
params.groupId,
|
||||
`/help@${params.sutUsername}`,
|
||||
);
|
||||
const canaryTimeoutMs = resolveTelegramQaCanaryTimeoutMs();
|
||||
const requestStartedAt = new Date(requestStartedAtMs).toISOString();
|
||||
let firstUnthreadedReply:
|
||||
| Pick<TelegramObservedMessage, "messageId" | "replyToMessageId" | "text">
|
||||
@@ -840,7 +873,7 @@ async function runCanary(params: {
|
||||
sutObserved = await waitForObservedMessage({
|
||||
token: params.driverToken,
|
||||
initialOffset: offset,
|
||||
timeoutMs: 30_000,
|
||||
timeoutMs: canaryTimeoutMs,
|
||||
observedMessages: params.observedMessages,
|
||||
observationScenarioId: "telegram-canary",
|
||||
observationScenarioTitle: "Telegram canary",
|
||||
@@ -881,7 +914,7 @@ async function runCanary(params: {
|
||||
}
|
||||
throw new TelegramQaCanaryError(
|
||||
"sut_reply_timeout",
|
||||
"SUT bot did not send any group reply after the canary command within 30s.",
|
||||
`SUT bot did not send any group reply after the canary command within ${Math.round(canaryTimeoutMs / 1000)}s.`,
|
||||
{
|
||||
groupId: params.groupId,
|
||||
sutBotId: params.sutBotId,
|
||||
@@ -1439,10 +1472,12 @@ export const __testing = {
|
||||
matchesTelegramScenarioReply,
|
||||
normalizeTelegramObservedMessage,
|
||||
parseTelegramQaProgressBooleanEnv,
|
||||
resolveTelegramQaCanaryTimeoutMs,
|
||||
parseTelegramQaCredentialPayload,
|
||||
resolveTelegramQaRuntimeEnv,
|
||||
sanitizeTelegramQaProgressValue,
|
||||
shouldLogTelegramQaLiveProgress,
|
||||
waitForTelegramChannelRunning,
|
||||
formatTelegramQaProgressDetails,
|
||||
renderTelegramQaMarkdown,
|
||||
};
|
||||
|
||||
@@ -106,6 +106,22 @@ describe("matrix live qa runtime", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("normalizes the Matrix QA canary timeout env", () => {
|
||||
const previous = process.env.OPENCLAW_QA_MATRIX_CANARY_TIMEOUT_MS;
|
||||
try {
|
||||
process.env.OPENCLAW_QA_MATRIX_CANARY_TIMEOUT_MS = "12345";
|
||||
expect(liveTesting.resolveMatrixQaCanaryTimeoutMs()).toBe(12345);
|
||||
process.env.OPENCLAW_QA_MATRIX_CANARY_TIMEOUT_MS = "nope";
|
||||
expect(liveTesting.resolveMatrixQaCanaryTimeoutMs()).toBe(90_000);
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.OPENCLAW_QA_MATRIX_CANARY_TIMEOUT_MS;
|
||||
} else {
|
||||
process.env.OPENCLAW_QA_MATRIX_CANARY_TIMEOUT_MS = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("injects a temporary Matrix account into the QA gateway config", () => {
|
||||
const baseCfg: OpenClawConfig = {
|
||||
plugins: {
|
||||
|
||||
@@ -50,6 +50,7 @@ type MatrixQaGatewayChild = {
|
||||
};
|
||||
|
||||
const DEFAULT_MATRIX_QA_RUN_TIMEOUT_MS = 30 * 60_000;
|
||||
const DEFAULT_MATRIX_QA_CANARY_TIMEOUT_MS = 90_000;
|
||||
const DEFAULT_MATRIX_QA_CLEANUP_TIMEOUT_MS = 90_000;
|
||||
|
||||
type MatrixQaLiveLaneGatewayHarness = {
|
||||
@@ -192,6 +193,13 @@ function createMatrixQaRunDeadline() {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMatrixQaCanaryTimeoutMs() {
|
||||
return parsePositiveMatrixQaEnvMs(
|
||||
"OPENCLAW_QA_MATRIX_CANARY_TIMEOUT_MS",
|
||||
DEFAULT_MATRIX_QA_CANARY_TIMEOUT_MS,
|
||||
);
|
||||
}
|
||||
|
||||
function remainingMatrixQaRunMs(deadline: { deadlineMs: number }) {
|
||||
return Math.max(1, deadline.deadlineMs - Date.now());
|
||||
}
|
||||
@@ -720,7 +728,7 @@ export async function runMatrixQaLive(params: {
|
||||
syncState,
|
||||
syncStreams,
|
||||
sutUserId: provisioning.sut.userId,
|
||||
timeoutMs: 45_000,
|
||||
timeoutMs: resolveMatrixQaCanaryTimeoutMs(),
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -1128,6 +1136,7 @@ export const __testing = {
|
||||
findMatrixQaScenarios,
|
||||
isMatrixAccountReady,
|
||||
patchMatrixQaGatewayConfig,
|
||||
resolveMatrixQaCanaryTimeoutMs,
|
||||
resolveMatrixQaModels,
|
||||
shouldWriteMatrixQaProgress,
|
||||
summarizeMatrixQaConfigSnapshot,
|
||||
|
||||
Reference in New Issue
Block a user