test(apps): cover the modern MRTR leg of app-rendered elicitations

Both review findings from round two.

The routing change sits in the funnel BOTH entry points share, but only the
legacy inbound handler was driven by a test — the modern leg, where an
elicitation arrives as an `input_required` result the MRTR driver unpacks and
retries, was covered by construction alone. Adds
`mrtr_app_choose_option` (a modern MRTR tool whose *embedded* elicitation
carries `_meta.ui.resourceUri`) and drives it live over a real transport: the
app answers, the retry carries that answer through `inputResponses`, and the
server echoes it back as the tool's result. A second case cancels the tool
call while the app is up and asserts the request-scoped signal aborts; a third
runs the same tool against a modern server that never advertised the
capability and asserts the native queue takes it.

The cancellation case turned up a real gap: `tryAppElicitation` mounted an app
for an already-aborted signal (the caller cancelled during an earlier MRTR
round), leaving a modal nobody was waiting on and a promise that could never
settle. It now throws the abort straight through.

Also fixes the smoke count in the root README, which still said "two"
headless-Chromium smokes and then introduced a third.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-22 20:17:37 -04:00
commit 44010c313b
2 changed files with 196 additions and 5 deletions

View file

@ -57,6 +57,7 @@ import {
createMcpAppDemoResource,
createAppElicitationTool,
createAppElicitationResource,
createMrtrAppElicitationTool,
createArchitectureResource,
createTestCwdResource,
createTestEnvResource,
@ -227,6 +228,8 @@ function resolveToolPreset(
return createMcpAppDemoTool();
case "app_choose_option":
return createAppElicitationTool();
case "mrtr_app_choose_option":
return createMrtrAppElicitationTool();
default:
throw new Error(`Unknown tool preset: ${name}`);
}

View file

@ -641,6 +641,66 @@ export function createAppElicitationTool(): ToolDefinition {
};
}
/**
* The modern (2026-07-28) counterpart of {@link createAppElicitationTool}: an
* MRTR tool whose EMBEDDED elicitation carries `_meta.ui.resourceUri` (#1854).
*
* The two paths reach the Inspector completely differently — a server→client
* `elicitation/create` request on the legacy leg, an `input_required` result
* the client unpacks and retries on the modern one — and the routing decision
* has to be identical on both. It is, because both funnel through the same
* `enqueuePendingElicitation`; this fixture is what lets a test prove that
* rather than assert it.
*
* Completes on the retry by echoing the `ElicitResult` the app produced, so a
* caller can see the app's answer round-trip through `inputResponses`.
*/
export function createMrtrAppElicitationTool(): ToolDefinition {
return {
name: "mrtr_app_choose_option",
description:
"Modern MRTR tool whose embedded elicitation offers an MCP App to render the form",
inputSchema: {
prompt: z.string().optional().describe("Message shown above the choice"),
},
handler: async (
params: Record<string, unknown>,
_context?: TestServerContext,
extra?: HandlerExtra,
) => {
const responses = extra?.inputResponses;
if (!responses || responses.choice === undefined) {
return inputRequired({
inputRequests: {
choice: inputRequired.elicit({
message:
typeof params.prompt === "string"
? params.prompt
: "Choose option A or B.",
requestedSchema: {
type: "object",
properties: {
choice: {
type: "string",
enum: ["option-a", "option-b"],
title: "Choice",
},
},
required: ["choice"],
},
_meta: { ui: { resourceUri: APP_ELICITATION_URI } },
}),
},
requestState: `mrtr-app:${++mrtrMintCount}`,
});
}
return toToolResult(
`Elicitation response: ${JSON.stringify(responses.choice)}`,
);
},
};
}
/**
* Create an "mrtr_confirm" tool exercising the modern (2026-07-28) multi
* round-trip request (MRTR) flow. On the first call it returns an