From fe4b94a661054cef7603a3f07aae71f1df0a701a Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:42:56 +0800 Subject: [PATCH] test(mcp): align completion checks with provider readiness Exercise cold and warm completion through ordinary provider operations. Bind fixture observations to the selected analysis profile so the warm-up call satisfies session validation, and make package completion candidates explicit with useful failure diagnostics. --- scripts/verify-package-mcp.mjs | 8 +-- scripts/verify-package-prompts.mjs | 9 +-- tests/boundary/mcp/promptCompletion.test.ts | 62 +++++++++++++-------- 3 files changed, 48 insertions(+), 31 deletions(-) diff --git a/scripts/verify-package-mcp.mjs b/scripts/verify-package-mcp.mjs index 5f43bf9b27..fda067dbe7 100644 --- a/scripts/verify-package-mcp.mjs +++ b/scripts/verify-package-mcp.mjs @@ -20,7 +20,7 @@ const execute = promisify(execFile); const verifyMcpToolsAndPrompts = async (client, mcpOptions) => { await verifyCompleteToolCatalog(client, mcpOptions); await prompts.verifyPromptCatalog(client, mcpOptions, prompts.names); - await prompts.verifyPromptCompletion(client, mcpOptions, false); + await prompts.verifyPromptCompletion(client, mcpOptions, []); }; const verifyMcpTargetFree = async (client, mcpOptions) => { @@ -122,7 +122,7 @@ const verifyMcpNonLinuxCurrentDocument = async ( const verifyMcpBinaryLifecycle = async (client, mcpOptions) => { await verifyMcpOpenAndBind(client, mcpOptions); // Completion never starts a provider; only an ordinary operation readies it. - await prompts.verifyPromptCompletion(client, mcpOptions, false); + await prompts.verifyPromptCompletion(client, mcpOptions, []); if (process.platform === "linux") { await verifyMcpLinuxToolAvailability(client, mcpOptions); } else { @@ -130,8 +130,8 @@ const verifyMcpBinaryLifecycle = async (client, mcpOptions) => { { name: "current_document", arguments: {} }, mcpOptions, ); - await prompts.verifyPromptCompletion(client, mcpOptions, true); await verifyMcpNonLinuxCurrentDocument(client, mcpOptions, current); + await prompts.verifyPromptCompletion(client, mcpOptions, ["fixture"]); } const closed = await client.callTool( { name: "close_binary", arguments: {} }, @@ -139,7 +139,7 @@ const verifyMcpBinaryLifecycle = async (client, mcpOptions) => { ); if (closed.isError === true) throw new Error("packaged MCP could not close its binary"); - await prompts.verifyPromptCompletion(client, mcpOptions, false); + await prompts.verifyPromptCompletion(client, mcpOptions, []); }; const verifyMcpEvidenceBundle = async (client, mcpOptions, evidenceRoot) => { diff --git a/scripts/verify-package-prompts.mjs b/scripts/verify-package-prompts.mjs index 079f925bab..9adff0179f 100644 --- a/scripts/verify-package-prompts.mjs +++ b/scripts/verify-package-prompts.mjs @@ -36,9 +36,8 @@ export const verifyPromptCatalog = async (client, options, expectedNames) => { throw new Error("packaged MCP guided prompt rendering failed"); }; -/** Verify live completion through each installed stdio target state. */ -export const verifyPromptCompletion = async (client, options, targetOpen) => { - const expected = targetOpen ? ["fixture"] : []; +/** Verify completion candidates for the installed provider readiness state. */ +export const verifyPromptCompletion = async (client, options, expected) => { const requests = [ { ref: { type: "ref/prompt", name: "investigate_feature" }, @@ -57,6 +56,8 @@ export const verifyPromptCompletion = async (client, options, targetOpen) => { result.completion.total !== expected.length || result.completion.hasMore !== false ) - throw new Error("packaged MCP prompt completion lifecycle failed"); + throw new Error( + `packaged MCP ${request.argument.name} completion mismatch: expected ${JSON.stringify(expected)}, received ${JSON.stringify(result.completion)}`, + ); } }; diff --git a/tests/boundary/mcp/promptCompletion.test.ts b/tests/boundary/mcp/promptCompletion.test.ts index f09725e120..009ed7ad84 100644 --- a/tests/boundary/mcp/promptCompletion.test.ts +++ b/tests/boundary/mcp/promptCompletion.test.ts @@ -5,7 +5,11 @@ import { afterEach, describe, expect, it } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import type { AnalysisClient } from "../../../src/application/AnalysisProvider.js"; +import { + createAnalysisExecution, + type AnalysisClient, +} from "../../../src/application/AnalysisProvider.js"; +import type { AnalysisProfileCommitment } from "../../../src/domain/analysisProfile.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { ARTIFACT_COMPARISON_EXAMPLE } from "../../../src/contracts/artifactComparisonExample.js"; import { PROCESS_CAPTURE_REFERENCE } from "../../../src/contracts/investigationExamples.js"; @@ -29,30 +33,34 @@ afterEach(async () => { describe("guided prompt completion from live analysis", () => { it("reads live documents and complete procedures without ambiguous names", async () => { const requests: Array>> = []; - const session = createTestBinarySession(() => client(requests), { - resolveAnalysisProfile: () => - Promise.resolve( - resultOk({ - profile: createAnalysisProfile( - { - id: "fixture", - name: "Fixture analysis provider", - version: "1", - }, - { fixture: true }, - ), - }), - ), - }); + const session = createTestBinarySession( + (_target, profile) => client(requests, profile), + { + resolveAnalysisProfile: () => + Promise.resolve( + resultOk({ + profile: createAnalysisProfile( + { + id: "fixture", + name: "Fixture analysis provider", + version: "1", + }, + { fixture: true }, + ), + }), + ), + }, + ); directory = await createTestTempDirectory("rea-prompt-completion-"); const target = join(directory, "fixture.hop"); await writeFile(target, "fixture"); expect((await session.open(target)).ok).toBe(true); const completion = createPromptCompletionSource(session, session); - - // Completion never starts a provider; an ordinary operation readies it. expect(await completion.complete("document", "app")).toEqual([]); - expect((await session.execute("list_documents", {})).ok).toBe(true); + expect(await completion.complete("procedure", "")).toEqual([]); + const warmed = await session.execute("list_documents", {}); + if (!warmed.ok) throw warmed.error; + expect(await completion.complete("document", "app")).toEqual([ "App", "AppTests", @@ -272,14 +280,23 @@ const fixtureEnvironment = { const client = ( requests: Array>>, + analysisProfile?: AnalysisProfileCommitment, ): AnalysisClient => { + const observe = (value: unknown) => + resultOk( + createAnalysisExecution( + value, + analysisProfile?.provider ?? fixtureProvider, + analysisProfile === undefined ? {} : { analysisProfile }, + ), + ); const execute: AnalysisClient["execute"] = (operation, parameters) => { if (operation === "list_documents") - return Promise.resolve(observed(["AppTests", "App", "App"])); + return Promise.resolve(observe(["AppTests", "App", "App"])); if (operation === "list_procedures") { requests.push(parameters); return Promise.resolve( - observed([ + observe([ { address: "0x1000", value: "duplicate" }, { address: "0x2000", value: "unique" }, { address: "0x3000", value: "duplicate" }, @@ -287,9 +304,8 @@ const client = ( ]), ); } - return Promise.resolve(observed(null)); + return Promise.resolve(observe(null)); }; - // An in-process fixture client is ready as soon as the session opens. return { execute, executeIfReady: execute,