import assert from "node:assert/strict"; import test from "node:test"; import { normalizeQuestions } from "../src/normalize.ts"; import { runRpcQuestions, type RpcQuestionUI } from "../src/rpc.ts"; function mockUi(responses: { select?: Array; input?: Array; }): RpcQuestionUI & { calls: Array<{ kind: "select" | "input"; title: string; options?: string[] }>; notifications: Array<{ message: string; type?: "info" | "warning" | "error" }>; } { const selectResponses = [...(responses.select ?? [])]; const inputResponses = [...(responses.input ?? [])]; const calls: Array<{ kind: "select" | "input"; title: string; options?: string[] }> = []; const notifications: Array<{ message: string; type?: "info" | "warning" | "error" }> = []; return { calls, notifications, async select(title, options) { calls.push({ kind: "select", title, options }); assert.ok(selectResponses.length > 0, "unexpected select dialog"); return selectResponses.shift(); }, async input(title) { calls.push({ kind: "input", title }); assert.ok(inputResponses.length > 0, "unexpected input dialog"); return inputResponses.shift(); }, notify(message, type) { notifications.push({ message, type }); }, }; } test("asks RPC questions sequentially and preserves stable option values", async () => { const questions = normalizeQuestions([ { id: "language", label: "Language", prompt: "Which language?", type: "select", options: [ { value: "ts", label: "TypeScript", description: "Strict typing" }, { value: "js", label: "JavaScript" }, ], }, { id: "notes", prompt: "Anything else?", type: "text", required: false }, ]); const ui = mockUi({ select: ["2. JavaScript"], input: [" "] }); const result = await runRpcQuestions(ui, questions); assert.equal(result.cancelled, false); assert.deepEqual(result.answers, [ { id: "language", question: "Which language?", type: "select", answer: "JavaScript", value: "js", label: "JavaScript", custom: false, skipped: false, }, { id: "notes", question: "Anything else?", type: "text", answer: "", value: "", label: "Skipped", custom: false, skipped: true, }, ]); assert.match(ui.calls[0]?.title ?? "", /^\[1\/2\] Language:/); assert.ok(ui.calls[0]?.options?.includes("1. TypeScript — Strict typing")); }); test("supports custom RPC answers and repeats required blank input", async () => { const questions = normalizeQuestions([{ id: "other", prompt: "Choose or write one", type: "select", options: [{ value: "known", label: "Known" }], }]); const ui = mockUi({ select: ["Type something."], input: [" ", " Custom value "] }); const result = await runRpcQuestions(ui, questions); assert.equal(result.cancelled, false); assert.equal(result.answers[0]?.answer, "Custom value"); assert.equal(result.answers[0]?.custom, true); assert.deepEqual(ui.notifications, [{ message: "An answer is required.", type: "warning" }]); }); test("returns cancelled with partial answers when an RPC dialog is dismissed", async () => { const questions = normalizeQuestions([ { id: "name", prompt: "Name?", type: "text" }, { id: "confirm", prompt: "Continue?", type: "select", options: [{ value: "yes", label: "Yes" }] }, ]); const ui = mockUi({ input: ["Ada"], select: [undefined] }); const result = await runRpcQuestions(ui, questions); assert.equal(result.cancelled, true); assert.equal(result.answers.length, 1); assert.equal(result.answers[0]?.answer, "Ada"); }); test("does not open RPC dialogs after abort", async () => { const questions = normalizeQuestions([{ id: "name", prompt: "Name?", type: "text" }]); const ui = mockUi({}); const controller = new AbortController(); controller.abort(); const result = await runRpcQuestions(ui, questions, controller.signal); assert.equal(result.cancelled, true); assert.deepEqual(result.answers, []); assert.deepEqual(ui.calls, []); });