Files
openclaw/extensions/slack/src/question-finalization.test.ts
rabsef-bicrym f229356689 fix(slack): preserve native tables during outbound delivery (#111955)
* fix(slack): preserve rendered presentations across cloning

* test(slack): cover serialized presentation provenance

* test(slack): prove restart fallback for rendered payloads

- Reload the Slack adapter to simulate a runtime restart.
- Verify stale rendered provenance falls back to text without blocks.

* fix(slack): fail closed on invalid rendered metadata

---------

Co-authored-by: Pavonis <pavonis@martian.engineering>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-27 23:39:20 -04:00

101 lines
3.5 KiB
TypeScript

// Covers Slack question delivery capture and Block Kit final edit.
import { describe, expect, it, vi } from "vitest";
const hoisted = vi.hoisted(() => ({
update: vi.fn(),
registration: undefined as
| { finalize: (statusLine: string) => void | Promise<void>; deliveryId: string }
| undefined,
}));
vi.mock("openclaw/plugin-sdk/question-gateway-runtime", async (importOriginal) => {
const original =
await importOriginal<typeof import("openclaw/plugin-sdk/question-gateway-runtime")>();
return {
...original,
questionGatewayRuntime: {
...original.questionGatewayRuntime,
registerChannelDelivery: (registration: typeof hoisted.registration) => {
hoisted.registration = registration;
},
},
};
});
vi.mock("./send.js", () => ({ updateMessageSlack: hoisted.update }));
import { slackOutbound } from "./outbound-adapter.js";
function jsonRoundTrip<T>(value: T): T {
// oxlint-disable-next-line unicorn/prefer-structured-clone -- This test exercises JSON transport.
return JSON.parse(JSON.stringify(value)) as T;
}
describe("Slack question finalization", () => {
it("removes action blocks and appends terminal context", async () => {
const questionId = "ask_0123456789abcdef0123456789abcdef";
const headers = Array.from({ length: 21 }, (_value, index) => `Column ${String(index)}`);
const payload = {
channelData: { askUser: { questionId } },
presentation: {
blocks: [
{
type: "table" as const,
caption: "Option metadata",
headers,
rows: [headers.map((_header, index) => `Value ${String(index)}`)],
},
{ type: "text" as const, text: "Pick one" },
{
type: "buttons" as const,
buttons: ["One", "Two"].map((label) => ({
label,
action: { type: "question" as const, questionId, optionValue: label },
})),
},
],
},
};
const rendered = await slackOutbound.renderPresentation?.({
payload,
presentation: payload.presentation,
ctx: { cfg: {}, to: "C123", text: "Pick one", payload },
});
expect(rendered).not.toBeNull();
const renderedAfterTransport = jsonRoundTrip(rendered);
const renderedSegments = (
renderedAfterTransport?.channelData?.slack as
| { renderedPresentationSegments?: unknown[] }
| undefined
)?.renderedPresentationSegments;
expect(renderedSegments?.map((segment) => (segment as { kind?: unknown }).kind)).toEqual([
"text",
"blocks",
]);
await slackOutbound.afterDeliverPayload?.({
cfg: {},
target: { channel: "slack", to: "C123", accountId: "default" },
payload: renderedAfterTransport!,
results: [
{ channel: "slack", messageId: "44", channelId: "C123" },
{ channel: "slack", messageId: "55", channelId: "C123" },
],
});
await hoisted.registration?.finalize("Answered: <!channel>");
expect(hoisted.update).toHaveBeenCalledWith(
expect.objectContaining({
channelId: "C123",
messageTs: "55",
text: expect.stringContaining("Answered: &lt;!channel&gt;"),
blocks: expect.arrayContaining([
{
type: "context",
elements: [{ type: "mrkdwn", text: "Answered: &lt;!channel&gt;" }],
},
]),
}),
);
const blocks = hoisted.update.mock.calls[0]?.[0]?.blocks as Array<{ type?: string }>;
expect(blocks.some((block) => block.type === "actions")).toBe(false);
});
});