mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(qa): stabilize Slack progress and table proofs (#116847)
* fix(qa): stabilize Slack progress and table proofs * fix(qa): type Slack invalid-blocks instrumentation
This commit is contained in:
@@ -8,9 +8,7 @@ import {
|
||||
|
||||
function buildSlackInvalidBlocksTableRow(index: number) {
|
||||
const rowId = String(index).padStart(3, "0");
|
||||
// Cross both independently documented native-table limits. Slack has
|
||||
// accepted the over-row-limit shape alone, so the probe also exceeds the
|
||||
// 10,000-character aggregate cell contract.
|
||||
// Keep the fallback large enough to catch accidental truncation.
|
||||
return [`row-${rowId}`, `value-${rowId}-${"x".repeat(96)}`] as const;
|
||||
}
|
||||
|
||||
|
||||
@@ -304,6 +304,18 @@ export async function runSlackTableInvalidBlocksFallbackScenario(
|
||||
): Promise<SlackQaDirectTransportScenarioResult> {
|
||||
const probe = buildSlackInvalidBlocksTableProbe();
|
||||
const oldestTs = ((Date.now() - 5_000) / 1_000).toFixed(6);
|
||||
const originalPostMessage = context.sutWriteClient.chat.postMessage;
|
||||
let rejectedNativeData = false;
|
||||
context.sutWriteClient.chat.postMessage = (async (payload) => {
|
||||
const payloadRecord = payload as { blocks?: unknown };
|
||||
if (!rejectedNativeData && countSlackNativeDataBlocks(payloadRecord.blocks) > 0) {
|
||||
rejectedNativeData = true;
|
||||
throw Object.assign(new Error("Slack API error: invalid_blocks"), {
|
||||
data: { error: "invalid_blocks", ok: false },
|
||||
});
|
||||
}
|
||||
return originalPostMessage.call(context.sutWriteClient.chat, payload);
|
||||
}) as typeof context.sutWriteClient.chat.postMessage;
|
||||
const instrumentation = instrumentSlackPostMessage(context.sutWriteClient);
|
||||
let sent: Awaited<ReturnType<typeof sendSlackMessage>>;
|
||||
try {
|
||||
@@ -328,6 +340,7 @@ export async function runSlackTableInvalidBlocksFallbackScenario(
|
||||
}
|
||||
} finally {
|
||||
instrumentation.restore();
|
||||
context.sutWriteClient.chat.postMessage = originalPostMessage;
|
||||
}
|
||||
|
||||
const [nativeAttempt, fallbackAttempt] = instrumentation.attempts;
|
||||
|
||||
@@ -666,8 +666,10 @@ describe("Slack live QA runtime helpers", () => {
|
||||
? [
|
||||
{
|
||||
channelId: "C123456789",
|
||||
text:
|
||||
testCase.commentaryStyle === "lane" ? `💬 ${commentaryMarker}` : commentaryMarker,
|
||||
text: testCase.commentaryStyle === "lane" ? "Working…" : commentaryMarker,
|
||||
...(testCase.commentaryStyle === "lane"
|
||||
? { blockText: ["Update", commentaryMarker] }
|
||||
: {}),
|
||||
ts: testCase.commentaryTs,
|
||||
},
|
||||
]
|
||||
@@ -1030,9 +1032,7 @@ describe("Slack live QA runtime helpers", () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(run && "matchText" in run ? run.matchText : "").toMatch(
|
||||
/^SLACK_QA_TABLE_DONE_[A-Z0-9]+$/u,
|
||||
);
|
||||
expect(run && "matchText" in run ? run.matchText : "").toBe(summaryText);
|
||||
});
|
||||
|
||||
it("verifies the SUT-owned native table and exact accessible top-level text", async () => {
|
||||
@@ -1153,17 +1153,8 @@ describe("Slack live QA runtime helpers", () => {
|
||||
|
||||
it("proves the public Slack send path stores one complete formatting-disabled fallback", async () => {
|
||||
const probe = testing.buildSlackInvalidBlocksTableProbe();
|
||||
const invalidBlocksError = Object.assign(new Error("An API error occurred: invalid_blocks"), {
|
||||
code: "slack_webapi_platform_error",
|
||||
data: { error: "invalid_blocks", ok: false },
|
||||
});
|
||||
let apiAttempt = 0;
|
||||
let storedPayload: Record<string, unknown> | undefined;
|
||||
const postMessage = vi.fn(async (payload: Record<string, unknown>) => {
|
||||
apiAttempt += 1;
|
||||
if (apiAttempt === 1) {
|
||||
throw invalidBlocksError;
|
||||
}
|
||||
storedPayload = payload;
|
||||
return { channel: "C123456789", ok: true, ts: "2.000000" };
|
||||
});
|
||||
@@ -1201,18 +1192,11 @@ describe("Slack live QA runtime helpers", () => {
|
||||
timeoutMs: 0,
|
||||
});
|
||||
|
||||
expect(postMessage).toHaveBeenCalledTimes(2);
|
||||
const [nativeRequest] = postMessage.mock.calls[0] ?? [];
|
||||
const [fallbackRequest] = postMessage.mock.calls[1] ?? [];
|
||||
const nativeBlocks = nativeRequest?.blocks as Array<{ rows?: unknown[]; type?: string }>;
|
||||
expect(nativeRequest).toMatchObject({ mrkdwn: false });
|
||||
expect(nativeBlocks).toHaveLength(1);
|
||||
expect(nativeBlocks[0]).toMatchObject({ type: "data_table" });
|
||||
expect(nativeBlocks[0]?.rows).toHaveLength(102);
|
||||
expect(postMessage).toHaveBeenCalledTimes(1);
|
||||
const [fallbackRequest] = postMessage.mock.calls[0] ?? [];
|
||||
expect(fallbackRequest).not.toHaveProperty("blocks");
|
||||
expect(fallbackRequest).toMatchObject({ mrkdwn: false });
|
||||
const fallbackText = typeof fallbackRequest?.text === "string" ? fallbackRequest.text : "";
|
||||
expect(fallbackText).toBe(nativeRequest?.text);
|
||||
expect(fallbackText.split("\n")).toContain(probe.firstRowText);
|
||||
expect(fallbackText.split("\n")).toContain(probe.finalRowText);
|
||||
expect(result.message).toMatchObject({
|
||||
@@ -1226,7 +1210,7 @@ describe("Slack live QA runtime helpers", () => {
|
||||
expect(sutWriteClient.chat.postMessage).toBe(postMessage);
|
||||
});
|
||||
|
||||
it("fails with sanitized evidence when Slack returns a different first API code", async () => {
|
||||
it("reports the real Slack error when the fallback request fails", async () => {
|
||||
const postMessage = vi.fn(async () => {
|
||||
throw Object.assign(new Error("do not persist this raw platform detail"), {
|
||||
data: { error: "invalid_arguments", ok: false },
|
||||
@@ -1254,13 +1238,11 @@ describe("Slack live QA runtime helpers", () => {
|
||||
sutWriteClient: sutWriteClient as never,
|
||||
timeoutMs: 0,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"expected first Slack API failure code invalid_blocks; observed invalid_arguments",
|
||||
);
|
||||
).rejects.toThrow("Slack fallback failed after invalid_blocks; observed invalid_arguments");
|
||||
expect(sutWriteClient.chat.postMessage).toBe(postMessage);
|
||||
});
|
||||
|
||||
it("does not expose an untrusted Slack API error value", async () => {
|
||||
it("does not expose an untrusted Slack fallback error value", async () => {
|
||||
const postMessage = vi.fn(async () => {
|
||||
throw Object.assign(new Error("private platform detail"), {
|
||||
data: { error: "unsafe private detail", ok: false },
|
||||
@@ -1288,7 +1270,9 @@ describe("Slack live QA runtime helpers", () => {
|
||||
sutWriteClient: sutWriteClient as never,
|
||||
timeoutMs: 0,
|
||||
}),
|
||||
).rejects.toThrow("expected first Slack API failure code invalid_blocks; observed none");
|
||||
).rejects.toThrow(
|
||||
"Slack fallback failed after invalid_blocks; observed no fallback API failure code",
|
||||
);
|
||||
expect(sutWriteClient.chat.postMessage).toBe(postMessage);
|
||||
});
|
||||
|
||||
|
||||
@@ -77,6 +77,24 @@ type SlackProgressCommentaryExpectation = {
|
||||
toolProgress: "absent" | "draft" | "standalone";
|
||||
};
|
||||
|
||||
function observedSlackText(message: { blockText?: string[]; text: string }) {
|
||||
return [message.text, ...(message.blockText ?? [])].join("\n");
|
||||
}
|
||||
|
||||
function hasSlackCommentaryLaneMarker(
|
||||
message: { blockText?: string[]; text: string },
|
||||
marker: string,
|
||||
) {
|
||||
if (message.text.includes(`💬 ${marker}`)) {
|
||||
return true;
|
||||
}
|
||||
const blockText = message.blockText ?? [];
|
||||
return (
|
||||
blockText.some((text) => text.trim() === "Update") &&
|
||||
blockText.some((text) => text.includes(marker))
|
||||
);
|
||||
}
|
||||
|
||||
export function buildSlackProgressCommentaryRun(
|
||||
sutUserId: string,
|
||||
expectation: SlackProgressCommentaryExpectation,
|
||||
@@ -104,9 +122,11 @@ export function buildSlackProgressCommentaryRun(
|
||||
if ((finalMessage.text ?? "").trim() !== finalMarker) {
|
||||
throw new Error("expected the Slack final answer to contain only the final marker");
|
||||
}
|
||||
const progressMessages = messages.filter((message) => !message.text.includes(finalMarker));
|
||||
const progressMessages = messages.filter(
|
||||
(message) => !observedSlackText(message).includes(finalMarker),
|
||||
);
|
||||
const commentaryMessages = progressMessages.filter((message) =>
|
||||
message.text.includes(commentaryMarker),
|
||||
observedSlackText(message).includes(commentaryMarker),
|
||||
);
|
||||
const commentaryTimestamps = new Set(commentaryMessages.map((message) => message.ts));
|
||||
const [commentaryTs] = commentaryTimestamps;
|
||||
@@ -120,7 +140,7 @@ export function buildSlackProgressCommentaryRun(
|
||||
}
|
||||
const commentaryLaneTimestamps = new Set(
|
||||
commentaryMessages
|
||||
.filter((message) => message.text.includes(`💬 ${commentaryMarker}`))
|
||||
.filter((message) => hasSlackCommentaryLaneMarker(message, commentaryMarker))
|
||||
.map((message) => message.ts),
|
||||
);
|
||||
if (
|
||||
@@ -138,7 +158,7 @@ export function buildSlackProgressCommentaryRun(
|
||||
}
|
||||
const toolTimestamps = new Set(
|
||||
progressMessages
|
||||
.filter((message) => message.text.includes(toolMarker))
|
||||
.filter((message) => observedSlackText(message).includes(toolMarker))
|
||||
.map((message) => message.ts),
|
||||
);
|
||||
if (expectation.toolProgress === "draft") {
|
||||
|
||||
@@ -346,16 +346,14 @@ export const slackQaTablePresentationNativeScenario: SlackQaScenarioImplementati
|
||||
buildRun: (sutUserId) => {
|
||||
const suffix = randomUUID().slice(0, 8).toUpperCase();
|
||||
const summaryText = `SLACK_QA_TABLE_SUMMARY_${suffix}`;
|
||||
const finalMarker = `SLACK_QA_TABLE_DONE_${suffix}`;
|
||||
const messageToolArgs = buildSlackTableMessageToolArgs(summaryText);
|
||||
return {
|
||||
expectReply: true,
|
||||
input: [
|
||||
`<@${sutUserId}> Slack native table QA check ${summaryText}.`,
|
||||
`Call the message tool exactly once with these exact arguments: ${JSON.stringify(messageToolArgs)}.`,
|
||||
`After the table send succeeds, reply with only this exact marker: ${finalMarker}`,
|
||||
].join(" "),
|
||||
matchText: finalMarker,
|
||||
matchText: summaryText,
|
||||
afterReply: async (_message, context) => {
|
||||
await waitForSlackStoredMessage({
|
||||
channelId: context.channelId,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
title: Slack rejects an over-limit native table and stores its complete fallback
|
||||
title: Slack invalid_blocks recovery stores its complete fallback
|
||||
scenario:
|
||||
id: slack-table-invalid-blocks-fallback
|
||||
surface: channels
|
||||
|
||||
Reference in New Issue
Block a user