Files
my-pi/pi-ask-user/test/rpc.test.ts
T

125 lines
4.1 KiB
TypeScript

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<string | undefined>;
input?: Array<string | undefined>;
}): 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, []);
});