fix(qa): fail slack no-reply on any reply

This commit is contained in:
Vincent Koc
2026-05-03 15:31:28 -07:00
parent 95ef5eb762
commit a027ac0195
3 changed files with 69 additions and 8 deletions

View File

@@ -36,6 +36,7 @@ Docs: https://docs.openclaw.ai
- Google Meet: refresh realtime browser state during status and retry delayed speech after Meet finishes joining, so a just-opened in-call tab no longer leaves speech stuck behind stale `not-in-call` health.
- Plugins/install: recover the install ledger from the managed npm root when `plugins/installs.json` is empty or partial, so reinstalling Discord and Codex no longer makes the other installed plugin disappear.
- Google Meet: grant Meet media permissions through the Playwright browser context when CDP grants do not affect the attached Chrome page, and report in-call microphone/speaker permission problems instead of marking realtime speech ready.
- QA/Slack: fail the live mention-gating scenario on any unexpected SUT reply, even when the reply does not echo the expected marker. Thanks @vincentkoc.
- Channel docs: keep JSON5 channel config examples parseable and schema-valid, fixing BlueBubbles, QQ Bot, and Slack snippets that could not be copied into config or app manifests as shown. Thanks @vincentkoc.
- Tlon: expose `groupInviteAllowlist` in the channel config schema and clarify that group invite auto-accept fails closed without an invite allowlist. Thanks @vincentkoc.
- Google Chat: update the setup example to use the accepted `groups.<space>.enabled` key instead of the legacy `allow` alias, with a schema regression for the documented group shape. Thanks @vincentkoc.

View File

@@ -54,4 +54,41 @@ describe("Slack live QA runtime helpers", () => {
"slack-canary",
]);
});
it("fails mention-gating when the SUT replies without the marker", async () => {
const observedMessages: Array<unknown> = [];
await expect(
__testing.waitForSlackNoReply({
channelId: "C123456789",
client: {
conversations: {
history: async () => ({
messages: [
{
text: "I should not have replied",
ts: "2.000000",
user: "U999999999",
},
],
}),
},
} as never,
matchText: "SLACK_QA_NOMENTION_MARKER",
observedMessages: observedMessages as never,
observationScenarioId: "slack-mention-gating",
observationScenarioTitle: "Slack unmentioned bot message does not trigger",
sentTs: "1.000000",
sutIdentity: { userId: "U999999999" },
timeoutMs: 1_000,
}),
).rejects.toThrow("unexpected Slack SUT reply observed");
expect(observedMessages).toMatchObject([
{
matchedScenario: false,
text: "I should not have replied",
ts: "2.000000",
userId: "U999999999",
},
]);
});
});

View File

@@ -428,16 +428,38 @@ async function waitForSlackNoReply(params: {
sutIdentity: SlackAuthIdentity;
timeoutMs: number;
}) {
try {
await waitForSlackScenarioReply(params);
} catch (error) {
const message = formatErrorMessage(error);
if (message === `timed out after ${params.timeoutMs}ms waiting for Slack message`) {
return;
const startedAt = Date.now();
while (Date.now() - startedAt < params.timeoutMs) {
const messages = await listSlackMessages({
channelId: params.channelId,
client: params.client,
oldestTs: params.sentTs,
});
for (const message of messages) {
const text = message.text ?? "";
if (
!message.ts ||
message.ts === params.sentTs ||
!isSutSlackMessage(message, params.sutIdentity)
) {
continue;
}
const matchedScenario = text.includes(params.matchText);
params.observedMessages.push({
botId: message.bot_id,
channelId: params.channelId,
matchedScenario,
scenarioId: params.observationScenarioId,
scenarioTitle: params.observationScenarioTitle,
text,
threadTs: message.thread_ts,
ts: message.ts,
userId: message.user,
});
throw new Error("unexpected Slack SUT reply observed");
}
throw error;
await new Promise((resolve) => setTimeout(resolve, 1_000));
}
throw new Error("unexpected Slack SUT reply observed");
}
async function waitForSlackChannelRunning(
@@ -816,4 +838,5 @@ export const __testing = {
parseSlackQaCredentialPayload,
resolveSlackQaRuntimeEnv,
SLACK_QA_STANDARD_SCENARIO_IDS,
waitForSlackNoReply,
};