Files
openclaw/extensions/imessage/src/monitor/poll-render.test.ts
Omar Shahine 8c7ac9b9b9 fix(imessage): native poll vote-cue, echo suppression, and same-sender comment fold
Makes native iMessage polls behave correctly end to end.

What changed:
- Inbound polls render with a numbered-options vote cue so the agent casts a native vote instead of answering the poll in prose.
- poll-vote resolves the poll reference from pollId/pollGuid/messageId and now defaults to the current inbound poll message when the model omits it; still errors when no reference exists.
- poll-vote echo suppression is session-scoped, so the redundant spoken answer is dropped across the separate poll and comment runs.
- A poll's inline-reply caption is folded (not delivered as a standalone question) only when the poll creator and reply sender are both known and equal; unknown/mismatched sender falls through to the normal inbound decision gate, so no inbound reply is silently dropped.

Evidence:
- 64 passing tests in the poll suites (poll-comment, poll-render, actions), incl. sender fail-closed regressions; pnpm build clean.
- Two Codex autoreviews clean (patch is correct); ClawSweeper re-review rated it platinum hermit.
- Live-verified on macOS 26.4.1 on the deployed gateway: poll "What color pill?" -> native vote delivered with a 7333-byte payload, caption folded, zero echo.

Note: vote delivery also depends on the imsg vote-stamp fix (openclaw/imsg#150); OpenClaw ships ahead of imsg per owner decision.
2026-07-01 21:30:29 -07:00

55 lines
1.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { renderIMessagePollBody } from "./poll-render.js";
describe("renderIMessagePollBody", () => {
it("renders a created poll with numbered options", () => {
const out = renderIMessagePollBody({
kind: "created",
question: "Favorite color?",
options: [
{ id: "a", text: "Red" },
{ id: "b", text: "Blue" },
],
});
expect(out).toContain("Favorite color?");
expect(out).toContain("1) Red");
expect(out).toContain("2) Blue");
// Must cue the vote action so the agent votes instead of replying in prose;
// the 📊 Poll prefix matches agents' vote-instruction trigger.
expect(out).toContain("poll-vote");
expect(out).toContain("\u{1F4CA} Poll");
});
it("folds in vote tallies", () => {
const out = renderIMessagePollBody({
kind: "created",
options: [
{ id: "a", text: "Red" },
{ id: "b", text: "Blue" },
],
votes: [
{ option_id: "b", event_type: "selected" },
{ option_id: "b", event_type: "selected" },
{ option_id: "a", event_type: "removed" },
],
});
expect(out).toContain("2) Blue [2]");
// Removed votes and unvoted options carry no tally suffix.
expect(out).toContain("1) Red");
expect(out).not.toContain("Red [");
});
it("renders a vote update", () => {
const out = renderIMessagePollBody({
kind: "vote",
vote: { participant: "+12065550123", option_text: "Blue", event_type: "selected" },
});
expect(out).toContain("Poll vote");
expect(out).toContain("Blue");
});
it("returns null for a poll with no options and no vote", () => {
expect(renderIMessagePollBody({ kind: "created", options: [] })).toBeNull();
});
});