// Root-owned integration may combine public plugin surfaces with Gateway-owned runtime. import { questionGatewayRuntime } from "openclaw/plugin-sdk/question-gateway-runtime"; import { afterEach, describe, expect, it, vi } from "vitest"; import { telegramOutbound } from "../extensions/telegram/api.js"; import { buildAgentHarnessQuestionPromptPayload } from "../src/agents/harness/user-input-bridge.js"; import { QuestionManager } from "../src/gateway/question-manager.js"; import { createQuestionHandlers } from "../src/gateway/server-methods/question.js"; import { callGatewayHandler } from "../src/gateway/server-methods/skills.test-helpers.js"; type QuestionGatewayCall = { method: string; params?: Record }; const questionGatewayTransport = vi.hoisted(() => ({ dispatch: undefined as ((request: QuestionGatewayCall) => Promise) | undefined, })); vi.mock("../src/gateway/call.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, callGateway: async (request: QuestionGatewayCall) => { if (!questionGatewayTransport.dispatch) { throw new Error("expected the in-process question Gateway transport"); } return await questionGatewayTransport.dispatch(request); }, }; }); afterEach(() => { questionGatewayTransport.dispatch = undefined; }); describe("Telegram question Gateway resolution", () => { it("resolves canonical option C when rendered option A repeats across blocks", async () => { const manager = new QuestionManager(); const handlers = createQuestionHandlers(manager); const gatewayCalls: string[] = []; const dispatch = async ({ method, params }: QuestionGatewayCall): Promise => { gatewayCalls.push(method); const result = await callGatewayHandler(handlers, method, params ?? {}, { context: { broadcast: () => undefined }, }); if (!result.ok) { throw new Error(`question Gateway method ${method} rejected its request`); } return result.response; }; questionGatewayTransport.dispatch = dispatch; try { const questionId = "ask_0123456789abcdef0123456789abcdef"; const optionValues = ["A", "B", "C"]; await dispatch({ method: "question.request", params: { id: questionId, questions: [ { questionId: "destination", header: "Destination", question: "Where next?", options: optionValues.map((label) => ({ label })), multiSelect: false, isOther: false, isSecret: false, }, ], timeoutMs: 15_000, }, }); expect(manager.get(questionId)?.questions[0]?.options).toEqual( optionValues.map((label) => ({ label })), ); const questionButton = (optionValue: string) => ({ label: optionValue, action: { type: "question" as const, questionId, optionValue }, }); const presentation = { blocks: [ { type: "buttons" as const, buttons: [questionButton("C"), questionButton("A")] }, { type: "buttons" as const, buttons: [questionButton("A"), questionButton("B")] }, ], }; const payload = buildAgentHarnessQuestionPromptPayload({ questionId, questions: [ { id: "destination", header: "Destination", question: "Where next?", options: optionValues.map((label) => ({ label })), }, ], options: { presentation }, }); expect(payload.channelData.askUser).toEqual({ questionId, optionValues }); const rendered = await telegramOutbound.renderPresentation?.({ payload, presentation, ctx: { cfg: {}, to: "42", text: payload.text, payload }, }); const telegram = rendered?.channelData?.telegram as | { buttons?: ReadonlyArray> } | undefined; const rows = telegram?.buttons; expect(rows?.map((row) => row.length)).toEqual([2, 2]); expect(rows?.flatMap((row) => row.map((button) => button.callback_data))).toEqual([ `tgq1:${questionId}:2`, `tgq1:${questionId}:0`, `tgq1:${questionId}:0`, `tgq1:${questionId}:1`, ]); const callbackData = rows?.[0]?.[0]?.callback_data; if (!callbackData) { throw new Error("expected canonical Telegram option C callback data"); } const optionIndex = Number(callbackData.slice(`tgq1:${questionId}:`.length)); expect(optionIndex).toBe(2); await expect( questionGatewayRuntime.resolveOption({ cfg: {}, questionId, optionIndex, senderId: "42" }), ).resolves.toEqual({ status: "answered", questionId: "destination", optionValue: "C" }); expect(gatewayCalls).toEqual(["question.request", "question.get", "question.resolve"]); expect(manager.get(questionId)).toMatchObject({ status: "answered", answers: { answers: { destination: ["C"] } }, resolvedBy: "42", }); } finally { manager.reset(); } }); });