mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
fix(qa): assert standalone Slack commentary by identity (#117808)
* fix(qa): assert standalone Slack commentary by identity * test(qa): satisfy Slack observation message contract * test(qa): keep Slack final message shape distinct * qa(slack): add deterministic commentary protocol fixture
This commit is contained in:
@@ -782,6 +782,37 @@ describe("Slack live QA runtime helpers", () => {
|
||||
).toThrow("exactly one Slack message identity containing commentary");
|
||||
});
|
||||
|
||||
it("accepts one standalone commentary identity even when Slack prefixes it as commentary", () => {
|
||||
const scenario = testing.findScenario(["slack-progress-commentary-verbose-dedupe"])[0];
|
||||
const run = scenario?.buildRun("U_SUT");
|
||||
if (
|
||||
!run ||
|
||||
run.kind === "approval" ||
|
||||
run.kind === "codex-approval" ||
|
||||
run.kind === "direct-transport" ||
|
||||
!run.verifyObserved
|
||||
) {
|
||||
throw new Error("expected Slack commentary message scenario");
|
||||
}
|
||||
const commentaryMarker = run.input.match(/SLACK-QA-COMMENTARY-[0-9A-F]{8}/u)?.[0];
|
||||
const toolMarker = run.input.match(/SLACK-QA-TOOL-[0-9A-F]{8}/u)?.[0];
|
||||
const finalMarker = run.input.match(/SLACK-QA-COMMENTARY-DONE-[0-9A-F]{8}/u)?.[0];
|
||||
if (!commentaryMarker || !toolMarker || !finalMarker) {
|
||||
throw new Error("missing Slack progress markers");
|
||||
}
|
||||
|
||||
expect(() =>
|
||||
run.verifyObserved?.({
|
||||
finalMessage: { text: finalMarker, ts: "3.000000" },
|
||||
messages: [
|
||||
{ channelId: "C123456789", text: `💬 ${commentaryMarker}`, ts: "1.000000" },
|
||||
{ channelId: "C123456789", text: toolMarker, ts: "2.000000" },
|
||||
{ channelId: "C123456789", text: finalMarker, ts: "3.000000" },
|
||||
],
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("settles complete channel and thread observations after the final reply", async () => {
|
||||
let historyCalls = 0;
|
||||
const observedMessages: Array<{ text: string }> = [];
|
||||
|
||||
@@ -138,23 +138,23 @@ export function buildSlackProgressCommentaryRun(
|
||||
if (commentaryTs === finalMessage.ts) {
|
||||
throw new Error("expected Slack progress commentary to stay separate from the fresh final");
|
||||
}
|
||||
const commentaryLaneTimestamps = new Set(
|
||||
commentaryMessages
|
||||
.filter((message) => hasSlackCommentaryLaneMarker(message, commentaryMarker))
|
||||
.map((message) => message.ts),
|
||||
);
|
||||
if (
|
||||
expectation.commentary === "lane" &&
|
||||
(commentaryLaneTimestamps.size !== 1 || !commentaryLaneTimestamps.has(commentaryTs))
|
||||
) {
|
||||
throw new Error("expected commentary in the Slack progress commentary lane");
|
||||
}
|
||||
if (expectation.commentary !== "lane" && commentaryLaneTimestamps.size !== 0) {
|
||||
throw new Error(
|
||||
expectation.commentary === "headline"
|
||||
? "expected the preamble as the Slack progress status headline"
|
||||
: "expected commentary only in the standalone verbose message",
|
||||
// Slack prefixes durable standalone commentary with the same glyph used by
|
||||
// draft-lane rendering, so message identity—not that marker—owns dedupe proof.
|
||||
if (expectation.commentary !== "standalone") {
|
||||
const commentaryLaneTimestamps = new Set(
|
||||
commentaryMessages
|
||||
.filter((message) => hasSlackCommentaryLaneMarker(message, commentaryMarker))
|
||||
.map((message) => message.ts),
|
||||
);
|
||||
if (
|
||||
expectation.commentary === "lane" &&
|
||||
(commentaryLaneTimestamps.size !== 1 || !commentaryLaneTimestamps.has(commentaryTs))
|
||||
) {
|
||||
throw new Error("expected commentary in the Slack progress commentary lane");
|
||||
}
|
||||
if (expectation.commentary === "headline" && commentaryLaneTimestamps.size !== 0) {
|
||||
throw new Error("expected the preamble as the Slack progress status headline");
|
||||
}
|
||||
}
|
||||
const toolTimestamps = new Set(
|
||||
progressMessages
|
||||
|
||||
@@ -53,6 +53,35 @@ export function extractExactMarkerDirective(text: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export const QA_SLACK_PROGRESS_COMMENTARY_MARKER_RE =
|
||||
/\bSLACK-QA-COMMENTARY-(?!DONE-)[A-F0-9]{8}\b/u;
|
||||
|
||||
export function extractSlackProgressCommentaryDirectives(text: string) {
|
||||
const commentaryMarker = extractLastCapture(
|
||||
text,
|
||||
/\b(SLACK-QA-COMMENTARY-(?!DONE-)[A-F0-9]{8})\b/u,
|
||||
);
|
||||
const toolMarker = extractLastCapture(text, /\b(SLACK-QA-TOOL-[A-F0-9]{8})\b/u);
|
||||
const finalMarker = extractLastCapture(text, /\b(SLACK-QA-COMMENTARY-DONE-[A-F0-9]{8})\b/u);
|
||||
if (!commentaryMarker || !toolMarker || !finalMarker) {
|
||||
return null;
|
||||
}
|
||||
const suffix = commentaryMarker.slice("SLACK-QA-COMMENTARY-".length);
|
||||
const execCommand = `grep 'SLACK-QA-TOOL-${suffix}' /dev/null || sleep 5`;
|
||||
const commandDirective = extractLastCapture(
|
||||
text,
|
||||
/\b(grep 'SLACK-QA-TOOL-[A-F0-9]{8}' \/dev\/null \|\| sleep 5)(?=[.`\s]|$)/u,
|
||||
);
|
||||
if (
|
||||
toolMarker !== `SLACK-QA-TOOL-${suffix}` ||
|
||||
finalMarker !== `SLACK-QA-COMMENTARY-DONE-${suffix}` ||
|
||||
commandDirective !== execCommand
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return { commentaryMarker, execCommand, finalMarker, toolMarker };
|
||||
}
|
||||
|
||||
export function extractWhatsAppLocationMarkerDirective(text: string) {
|
||||
return extractLastCapture(
|
||||
text,
|
||||
|
||||
@@ -139,6 +139,7 @@ describe("mock OpenAI Responses output item slots", () => {
|
||||
const events = buildAssistantThenToolCallEvents(
|
||||
{
|
||||
id: "assistant-before-tool",
|
||||
phase: "commentary",
|
||||
streamDeltas: ["looking up"],
|
||||
text: "looking up",
|
||||
},
|
||||
@@ -176,6 +177,23 @@ describe("mock OpenAI Responses output item slots", () => {
|
||||
delta: JSON.stringify({ path: "README.md" }),
|
||||
},
|
||||
);
|
||||
expect(
|
||||
events
|
||||
.filter(
|
||||
(event) =>
|
||||
event.type === "response.output_item.added" ||
|
||||
event.type === "response.output_item.done",
|
||||
)
|
||||
.map((event) => event.item)
|
||||
.filter((item) => item.type === "message"),
|
||||
).toEqual([
|
||||
expect.objectContaining({ id: "assistant-before-tool", phase: "commentary" }),
|
||||
expect.objectContaining({ id: "assistant-before-tool", phase: "commentary" }),
|
||||
]);
|
||||
const completed = events.find((event) => event.type === "response.completed");
|
||||
expect(completed?.response.output[0]).toEqual(
|
||||
expect.objectContaining({ id: "assistant-before-tool", phase: "commentary" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("indexes reasoning before the streamed assistant answer", () => {
|
||||
|
||||
@@ -916,6 +916,79 @@ describe("qa mock openai server", () => {
|
||||
expect(body).toContain(command);
|
||||
});
|
||||
|
||||
it("dispatches structured Slack commentary, exec, and final phases", async () => {
|
||||
const server = await startMockServer();
|
||||
const suffix = "A1B2C3D4";
|
||||
const commentaryMarker = `SLACK-QA-COMMENTARY-${suffix}`;
|
||||
const toolMarker = `SLACK-QA-TOOL-${suffix}`;
|
||||
const finalMarker = `SLACK-QA-COMMENTARY-DONE-${suffix}`;
|
||||
const command = `grep '${toolMarker}' /dev/null || sleep 5`;
|
||||
const prompt = `${commentaryMarker} ${command} ${finalMarker}`;
|
||||
const stalePrompt =
|
||||
"SLACK-QA-COMMENTARY-11112222 grep 'SLACK-QA-TOOL-11112222' /dev/null || sleep 5 SLACK-QA-COMMENTARY-DONE-11112222";
|
||||
const currentEnvelope = `${stalePrompt}\n${prompt}`;
|
||||
|
||||
const planResponse = await postStreamingResponses(server, {
|
||||
tools: [{ type: "function", name: "exec" }],
|
||||
input: [
|
||||
makeUserInput(stalePrompt),
|
||||
makeToolOutputWithCallId("call_stale_slack_progress", ""),
|
||||
makeUserInput(currentEnvelope),
|
||||
],
|
||||
});
|
||||
expect(planResponse.status).toBe(200);
|
||||
const events = (await planResponse.text())
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("data: {"))
|
||||
.map((line) => requireRecord(JSON.parse(line.slice("data: ".length)), "Slack SSE event"));
|
||||
const completedItems = events
|
||||
.filter((event) => event.type === "response.output_item.done")
|
||||
.map((event) => requireRecord(event.item, "Slack completed item"));
|
||||
expect(completedItems).toHaveLength(2);
|
||||
expect(completedItems[0]).toMatchObject({
|
||||
type: "message",
|
||||
phase: "commentary",
|
||||
content: [{ type: "output_text", text: commentaryMarker }],
|
||||
});
|
||||
const exec = completedItems[1];
|
||||
if (!exec) {
|
||||
throw new Error("expected Slack progress exec output item");
|
||||
}
|
||||
expect(exec).toMatchObject({ type: "function_call", name: "exec" });
|
||||
expect(outputToolArgsFromItem(exec)).toEqual({ command });
|
||||
expect(JSON.stringify(events)).not.toContain(finalMarker);
|
||||
|
||||
const final = await expectNonStreamingResponsesJson(server, {
|
||||
tools: [{ type: "function", name: "exec" }],
|
||||
input: [
|
||||
makeUserInput(stalePrompt),
|
||||
makeToolOutputWithCallId("call_stale_slack_progress", ""),
|
||||
makeUserInput(currentEnvelope),
|
||||
makeToolOutputWithCallId(outputToolCallId(exec, "call_slack_progress"), ""),
|
||||
],
|
||||
});
|
||||
expect(outputItem(final)).toMatchObject({ type: "message", phase: "final_answer" });
|
||||
expect(outputText(final)).toBe(finalMarker);
|
||||
expect(outputItems(final)).toHaveLength(1);
|
||||
expect(JSON.stringify(final)).not.toContain(commentaryMarker);
|
||||
});
|
||||
|
||||
it("does not dispatch Slack progress when structured marker suffixes disagree", async () => {
|
||||
const server = await startMockServer();
|
||||
const payload = await expectNonStreamingResponsesJson(server, {
|
||||
tools: [{ type: "function", name: "exec" }],
|
||||
input: [
|
||||
makeUserInput(
|
||||
"SLACK-QA-COMMENTARY-A1B2C3D4 grep 'SLACK-QA-TOOL-11112222' /dev/null || sleep 5 SLACK-QA-COMMENTARY-DONE-A1B2C3D4",
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
expect(outputItems(payload)).not.toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ name: "exec" })]),
|
||||
);
|
||||
});
|
||||
|
||||
it("honors exact replies after QA kickoff reads without marker wording", async () => {
|
||||
const server = await startMockServer();
|
||||
const prompt =
|
||||
|
||||
@@ -98,6 +98,8 @@ import {
|
||||
shouldUseWhatsAppContactMarker,
|
||||
shouldUseWhatsAppStickerMarker,
|
||||
extractBlockStreamingMarkerDirectives,
|
||||
extractSlackProgressCommentaryDirectives,
|
||||
QA_SLACK_PROGRESS_COMMENTARY_MARKER_RE,
|
||||
hasDeclaredTool,
|
||||
hasToolDefinition,
|
||||
isQaToolSearchFixture,
|
||||
@@ -556,6 +558,16 @@ async function buildResponsesPayload(
|
||||
const command = execCommandFromToolProgressPrompt(scenarioFamilyPrompt);
|
||||
return command ? buildToolCallEventsWithArgs("exec", { command }) : null;
|
||||
};
|
||||
const slackProgressTurn = extractLastMatchingUserTurn(
|
||||
input,
|
||||
QA_SLACK_PROGRESS_COMMENTARY_MARKER_RE,
|
||||
);
|
||||
const slackProgressDirectives = slackProgressTurn
|
||||
? extractSlackProgressCommentaryDirectives(slackProgressTurn.text)
|
||||
: null;
|
||||
const hasSlackProgressToolOutput = slackProgressTurn
|
||||
? hasToolOutput(input.slice(slackProgressTurn.index))
|
||||
: false;
|
||||
if (QA_TOOL_LOOP_GLOBAL_BREAKER_PROMPT_RE.test(allInputText)) {
|
||||
if (!hasCompletedToolOutput) {
|
||||
scenarioState.toolLoopReadAttempts = 0;
|
||||
@@ -960,6 +972,30 @@ async function buildResponsesPayload(
|
||||
},
|
||||
]);
|
||||
}
|
||||
if (slackProgressDirectives) {
|
||||
if (hasSlackProgressToolOutput) {
|
||||
return buildAssistantEvents([
|
||||
{
|
||||
id: "msg_mock_slack_progress_final",
|
||||
phase: "final_answer",
|
||||
streamDeltas: splitMockStreamingText(slackProgressDirectives.finalMarker),
|
||||
text: slackProgressDirectives.finalMarker,
|
||||
},
|
||||
]);
|
||||
}
|
||||
if (hasDeclaredTool(body, "exec")) {
|
||||
return buildAssistantThenToolCallEvents(
|
||||
{
|
||||
id: "msg_mock_slack_progress_commentary",
|
||||
phase: "commentary",
|
||||
streamDeltas: splitMockStreamingText(slackProgressDirectives.commentaryMarker),
|
||||
text: slackProgressDirectives.commentaryMarker,
|
||||
},
|
||||
"exec",
|
||||
{ command: slackProgressDirectives.execCommand },
|
||||
);
|
||||
}
|
||||
}
|
||||
const toolProgressReplyDirective =
|
||||
extractExactReplyDirective(toolProgressToolOutput) ??
|
||||
extractExactMarkerDirective(toolProgressToolOutput) ??
|
||||
|
||||
Reference in New Issue
Block a user