mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-20 10:11:40 +00:00
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.
55 lines
1.7 KiB
TypeScript
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();
|
|
});
|
|
});
|