@frontmcp/testing 1.10.15 → 1.10.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -106,15 +106,15 @@ Server entry: ${config.server}
106
106
  Project: ${config.project??"default"}
107
107
  Command: ${serverCommand}
108
108
 
109
- Error: ${errMsg}`,{cause:error})}}else{throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option')}const sharedSecret=config.env?.["JWT_SECRET"];const binding=sharedSecret?gatewayTokenBinding(config.baseUrl??server.info.baseUrl,config.entryPath):void 0;const tokenFactory2=new TestTokenFactory(sharedSecret?{hmacSecret:sharedSecret,...binding}:{});return{server,startedByUs,tokenFactory:tokenFactory2,owner}}function acquireServer(chain){const config=resolveConfig(chain);const key=serverKey(config);let entry=serverEntries.get(key);if(!entry){entry=startServerEntry(config,ownerScopeOf(chain));serverEntries.set(key,entry);entry.catch(()=>serverEntries.delete(key))}return entry}async function stopServers(owner){for(const[key,pending]of Array.from(serverEntries)){let entry;try{entry=await pending}catch{serverEntries.delete(key);continue}if(owner&&entry.owner!==owner)continue;serverEntries.delete(key);if(entry.startedByUs){await entry.server.stop()}}}var fixtureContexts=new WeakMap;async function buildFixtures(chain){const config=resolveConfig(chain);const entry=await acquireServer(chain);const{server,tokenFactory:tokenFactory2}=entry;const publicMode=config.publicMode??config.auth?.mode==="public";const clientConfig={...config,publicMode};const clientInstance=McpTestClient.create({baseUrl:resolveClientBaseUrl(server,clientConfig),entryPath:clientConfig.entryPath,transport:clientConfig.transport??"streamable-http",publicMode}).build();try{await clientInstance.connect()}catch(err){const message=err instanceof Error?err.message:String(err);if(/\b401\b|unauthor/i.test(message)){console.warn(`[TestFixture] MCP client could not connect anonymously \u2014 the server requires authentication (${message}). Tests using only the 'server'/'auth' fixtures are unaffected; tests using the 'mcp' fixture must supply a token.`)}else{throw err}}const context={extraClients:[]};const fixtures={mcp:clientInstance,auth:createAuthFixture(tokenFactory2),server:createServerFixture(server,clientConfig,clientInstance,context)};fixtureContexts.set(fixtures,context);return fixtures}async function cleanupTestFixtures(fixtures,testFailed=false){if(testFailed){const logs=fixtures.server.getLogs();if(logs.length>0){console.error("\n[TestFixture] === Server Logs (test failed) ===");const recentLogs=logs.slice(-50);if(logs.length>50){console.error(`[TestFixture] (showing last 50 of ${logs.length} log entries)`)}console.error(recentLogs.join("\n"));console.error("[TestFixture] === End Server Logs ===\n")}}const extras=fixtureContexts.get(fixtures)?.extraClients??[];for(const client of extras){if(client.isConnected()){await client.disconnect().catch(()=>void 0)}}if(fixtures.mcp.isConnected()){await fixtures.mcp.disconnect()}}function resolveClientBaseUrl(server,config){return config.baseUrl??server.info.baseUrl}function createAuthFixture(factory){const users={admin:{sub:"admin-001",scopes:["admin:*","read","write","delete"],email:"admin@test.local",name:"Test Admin"},user:{sub:"user-001",scopes:["read","write"],email:"user@test.local",name:"Test User"},readOnly:{sub:"readonly-001",scopes:["read"],email:"readonly@test.local",name:"Read Only User"}};return{createToken:opts=>factory.createTestToken({sub:opts.sub,scopes:opts.scopes,claims:{email:opts.email,name:opts.name,...opts.claims},exp:opts.expiresIn}),createExpiredToken:opts=>factory.createExpiredToken(opts),createInvalidToken:opts=>factory.createTokenWithInvalidSignature(opts),users:{admin:users["admin"],user:users["user"],readOnly:users["readOnly"]},getJwks:()=>factory.getPublicJwks(),getIssuer:()=>factory.getIssuer(),getAudience:()=>factory.getAudience()}}function createServerFixture(server,config,mcp,context){return{get info(){return server.info},createClient:async opts=>{const client=await McpTestClient.create({baseUrl:resolveClientBaseUrl(server,config),entryPath:opts?.entryPath??config.entryPath,transport:opts?.transport??"streamable-http",auth:opts?.token?{token:opts.token}:void 0,clientInfo:opts?.clientInfo,publicMode:config.publicMode}).buildAndConnect();context.extraClients.push(client);return client},createClientBuilder:()=>{const builder=new McpTestClientBuilder({baseUrl:resolveClientBaseUrl(server,config),entryPath:config.entryPath,publicMode:config.publicMode});return builder},restart:async()=>{const reconnect=[mcp,...context.extraClients].filter(client=>client.isConnected());await server.restart();for(const client of reconnect){await client.reconnect()}},getLogs:()=>server.getLogs(),clearLogs:()=>server.clearLogs()}}function resolveServerCommand(server){if(server.includes(" ")){return server}return`npx tsx ${server}`}function runWithFixtures(fn,chain){return async(...extra)=>{const fixtures=await buildFixtures(chain);let failure;try{for(const scope of chain){for(const hook of scope.beforeEachHooks)await hook(fixtures)}await fn(fixtures,...extra)}catch(error){failure={error}}try{await runAfterEachHooks(chain,fixtures)}catch(error){failure??={error}}await cleanupTestFixtures(fixtures,failure!==void 0);if(failure)throw failure.error}}async function runAfterEachHooks(chain,fixtures){let firstError;let failed=false;for(let i=chain.length-1;i>=0;i--){for(const hook of chain[i].afterEachHooks){try{await hook(fixtures)}catch(error){if(!failed)firstError=error;failed=true}}}if(failed)throw firstError}function skipTitle(name,scope){return scope.reason?`${name} (skipped: ${scope.reason})`:name}function testWithFixtures(name,fn){const chain=scopes.slice();const scope=currentScope();if(scope.skipped){jestGlobals().it.skip(skipTitle(name,scope),runWithFixtures(fn,chain));return}jestGlobals().it(name,runWithFixtures(fn,chain))}function use(config){const scope=currentScope();const{env,...rest}=config;scope.config={...scope.config,...rest};if(env)scope.envs.push(env);scope.configured=true;if(!scope.cleanupRegistered){scope.cleanupRegistered=true;jestGlobals().afterAll(async()=>{await stopServers(scope===scopes[0]?void 0:scope)})}}function skip(nameOrCondition,fnOrReason){if(typeof nameOrCondition==="boolean"){if(nameOrCondition){const scope=currentScope();scope.skipped=true;scope.reason=typeof fnOrReason==="string"?fnOrReason:void 0}return}if(typeof fnOrReason!=="function"){throw new TypeError(`test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`)}jestGlobals().it.skip(nameOrCondition,runWithFixtures(fnOrReason,scopes.slice()))}function only(name,fn){const scope=currentScope();if(scope.skipped){jestGlobals().it.skip(skipTitle(name,scope),runWithFixtures(fn,scopes.slice()));return}jestGlobals().it.only(name,runWithFixtures(fn,scopes.slice()))}function todo(name){jestGlobals().it.todo(name)}function each(table,...tagged){const chain=scopes.slice();const register=jestGlobals().it.each(table,...tagged);return(name,fn)=>{const scope=currentScope();if(scope.skipped){const skipRegister=jestGlobals().it.skip.each(table,...tagged);skipRegister(skipTitle(name,scope),runWithFixtures(fn,chain));return}register(name,runWithFixtures(fn,chain))}}function withScope(fn){return(...args)=>{scopes.push(newScope(currentScope()));try{fn(...args)}finally{scopes.pop()}}}function scopedRegister(pick){return(name,fn,timeout)=>{pick()(name,withScope(fn),timeout)}}function scopedEach(pick){return(...eachArgs)=>(name,fn,timeout)=>{const register=pick()(...eachArgs);register(name,withScope(fn),timeout)}}var describeWithScope=Object.assign(scopedRegister(()=>jestGlobals().describe),{only:Object.assign(scopedRegister(()=>jestGlobals().describe.only),{each:scopedEach(()=>jestGlobals().describe.only.each)}),skip:Object.assign(scopedRegister(()=>jestGlobals().describe.skip),{each:scopedEach(()=>jestGlobals().describe.skip.each)}),each:scopedEach(()=>jestGlobals().describe.each)});function eachHook(kind){return(fn,timeout)=>{if(typeof fn==="function"&&fn.length>0){const scope=currentScope();(kind==="beforeEach"?scope.beforeEachHooks:scope.afterEachHooks).push(fn);return}jestGlobals()[kind](fn,timeout)}}var test=testWithFixtures;test.use=use;test.describe=describeWithScope;test.beforeAll=((...args)=>jestGlobals().beforeAll(...args));test.beforeEach=eachHook("beforeEach");test.afterEach=eachHook("afterEach");test.afterAll=((...args)=>jestGlobals().afterAll(...args));test.skip=skip;test.only=only;test.todo=todo;test.each=each;var resolvedExpect;function resolveJestExpect(){if(resolvedExpect)return resolvedExpect;try{resolvedExpect=require("@jest/globals").expect;return resolvedExpect}catch(error){throw new Error(`expect() from @frontmcp/testing can only be used inside a Jest test file: ${error instanceof Error?error.message:String(error)}`,{cause:error})}}var expect=new Proxy(function frontmcpExpect(){},{apply:(_target,thisArg,args)=>Reflect.apply(resolveJestExpect(),thisArg,args),get:(_target,prop)=>Reflect.get(resolveJestExpect(),prop),has:(_target,prop)=>Reflect.has(resolveJestExpect(),prop)});function getPlatformMetaNamespace(platform){return"ui"}function getPlatformMimeType(platform){return"text/html;profile=mcp-app"}function isOpenAIPlatform(platform){return platform==="openai"}function isExtAppsPlatform(platform){return platform==="ext-apps"}function isUiPlatform(platform){return true}function getToolsListMetaPrefixes(platform){return["ui/"]}function getToolCallMetaPrefixes(platform){return["ui/"]}function getForbiddenMetaPrefixes(platform){return["openai/","frontmcp/"]}function escapeRegex(str){return str.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function extractUiHtml(received){if(typeof received==="string"){return received}const wrapper=received;const meta=wrapper?.raw?._meta||wrapper?._meta;if(meta&&typeof meta==="object"){const uiHtml=meta["ui/html"];if(typeof uiHtml==="string"){return uiHtml}}return void 0}function extractMeta(received){const wrapper=received;const meta=wrapper?.raw?._meta||wrapper?._meta;if(meta&&typeof meta==="object"){return meta}return void 0}var toHaveRenderedHtml=function(received){const html=extractUiHtml(received);const hasHtml=html!==void 0&&html.length>0;const isFallback=hasHtml&&html.includes("mdx-fallback");const pass=hasHtml&&!isFallback;return{pass,message:()=>{if(isFallback){return"Expected rendered HTML but got mdx-fallback (raw escaped content). MDX rendering may have failed."}if(!hasHtml){return"Expected _meta to have ui/html property with rendered HTML"}return"Expected result not to have rendered HTML"}}};var toContainHtmlElement=function(received,tag){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected to find <${tag}> element, but no HTML content found`}}const regex=new RegExp(`<${escapeRegex(tag)}[\\s>]`,"i");const pass=regex.test(html);return{pass,message:()=>pass?`Expected HTML not to contain <${tag}> element`:`Expected HTML to contain <${tag}> element`}};var toContainBoundValue=function(received,value){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected HTML to contain bound value "${value}", but no HTML content found`}}const stringValue=String(value);const pass=html.includes(stringValue);return{pass,message:()=>pass?`Expected HTML not to contain bound value "${stringValue}"`:`Expected HTML to contain bound value "${stringValue}"`}};var toBeXssSafe=function(received){const html=extractUiHtml(received);if(!html){return{pass:true,message:()=>"Expected HTML to be XSS unsafe (no HTML found)"}}const hasScript=/<script[\s>]/i.test(html);const hasOnHandler=/\son\w+\s*=/i.test(html);const hasJavascriptUri=/javascript:/i.test(html);const issues=[];if(hasScript)issues.push("<script> tag");if(hasOnHandler)issues.push("inline event handler (onclick, etc.)");if(hasJavascriptUri)issues.push("javascript: URI");const pass=!hasScript&&!hasOnHandler&&!hasJavascriptUri;return{pass,message:()=>pass?"Expected HTML not to be XSS safe":`Expected HTML to be XSS safe, but found: ${issues.join(", ")}`}};var toHaveWidgetMetadata=function(received){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>"Expected _meta to have widget metadata, but no _meta found"}}const hasUiHtml=Boolean(meta["ui/html"]);const hasMimeType2=Boolean(meta["ui/mimeType"]);const hasUiObject=Boolean(meta["ui"]&&typeof meta["ui"]==="object");const pass=hasUiHtml||hasMimeType2||hasUiObject;return{pass,message:()=>pass?"Expected result not to have widget metadata":"Expected _meta to have widget metadata (ui/html, ui/mimeType, or ui object)"}};var toHaveCssClass=function(received,className){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected HTML to have CSS class "${className}", but no HTML content found`}}const classRegex=new RegExp(`class(?:Name)?\\s*=\\s*["'][^"']*\\b${escapeRegex(className)}\\b[^"']*["']`,"i");const pass=classRegex.test(html);return{pass,message:()=>pass?`Expected HTML not to have CSS class "${className}"`:`Expected HTML to have CSS class "${className}"`}};var toNotContainRawContent=function(received,content){const html=extractUiHtml(received);if(!html){return{pass:true,message:()=>`Expected HTML to contain raw content "${content}", but no HTML found`}}const pass=!html.includes(content);return{pass,message:()=>pass?`Expected HTML to contain raw content "${content}"`:`Expected HTML not to contain raw content "${content}" (may indicate rendering failure)`}};var toHaveProperHtmlStructure=function(received){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>"Expected proper HTML structure, but no HTML content found"}}const hasEscapedTags=html.includes("&lt;")&&html.includes("&gt;");const hasHtmlTags=/<[a-z]/i.test(html);const pass=hasHtmlTags&&!hasEscapedTags;return{pass,message:()=>{if(hasEscapedTags){return"Expected proper HTML structure, but found escaped HTML entities - content may not have been rendered"}if(!hasHtmlTags){return"Expected proper HTML structure, but found no HTML tags"}return"Expected result not to have proper HTML structure"}}};var toHavePlatformMeta=function(received,platform){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have platform meta for "${platform}", but no _meta found`}}const expectedPrefixes=getToolCallMetaPrefixes(platform);const forbiddenPrefixes=getForbiddenMetaPrefixes(platform);const metaKeys=Object.keys(meta);const hasExpectedPrefix=metaKeys.some(key=>expectedPrefixes.some(prefix=>key.startsWith(prefix)));const forbiddenKeys=metaKeys.filter(key=>forbiddenPrefixes.some(prefix=>key.startsWith(prefix)));const pass=hasExpectedPrefix&&forbiddenKeys.length===0;return{pass,message:()=>{if(!hasExpectedPrefix){return`Expected _meta to have keys with prefixes [${expectedPrefixes.join(", ")}] for platform "${platform}", but found: [${metaKeys.join(", ")}]`}if(forbiddenKeys.length>0){return`Expected _meta NOT to have keys [${forbiddenKeys.join(", ")}] for platform "${platform}" (forbidden prefixes: [${forbiddenPrefixes.join(", ")}])`}return`Expected result not to have platform meta for "${platform}"`}}};var toHaveMetaKey=function(received,key){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have key "${key}", but no _meta found`}}const pass=key in meta;return{pass,message:()=>pass?`Expected _meta not to have key "${key}"`:`Expected _meta to have key "${key}"`}};var toHaveMetaValue=function(received,key,value){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta["${key}"] to equal ${JSON.stringify(value)}, but no _meta found`}}const actualValue=meta[key];const pass=JSON.stringify(actualValue)===JSON.stringify(value);return{pass,message:()=>pass?`Expected _meta["${key}"] not to equal ${JSON.stringify(value)}`:`Expected _meta["${key}"] to equal ${JSON.stringify(value)}, but got ${JSON.stringify(actualValue)}`}};var toNotHaveMetaKey=function(received,key){const meta=extractMeta(received);if(!meta){return{pass:true,message:()=>`Expected _meta to have key "${key}", but no _meta found`}}const pass=!(key in meta);return{pass,message:()=>pass?`Expected _meta to have key "${key}"`:`Expected _meta not to have key "${key}"`}};var toHaveOnlyNamespacedMeta=function(received,namespace){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have keys with namespace "${namespace}", but no _meta found`}}const metaKeys=Object.keys(meta);const wrongKeys=metaKeys.filter(key=>!key.startsWith(namespace));const pass=wrongKeys.length===0&&metaKeys.length>0;return{pass,message:()=>{if(metaKeys.length===0){return`Expected _meta to have keys with namespace "${namespace}", but _meta is empty`}if(wrongKeys.length>0){return`Expected _meta to ONLY have keys with namespace "${namespace}", but found: [${wrongKeys.join(", ")}]`}return`Expected _meta not to have only keys with namespace "${namespace}"`}}};var toHavePlatformMimeType=function(received,platform){const meta=extractMeta(received);const expectedMimeType=getPlatformMimeType(platform);if(!meta){return{pass:false,message:()=>`Expected _meta to have MIME type "${expectedMimeType}" for platform "${platform}", but no _meta found`}}const mimeTypeKey="ui/mimeType";const actualMimeType=meta[mimeTypeKey];const pass=actualMimeType===expectedMimeType;return{pass,message:()=>pass?`Expected _meta["${mimeTypeKey}"] not to be "${expectedMimeType}"`:`Expected _meta["${mimeTypeKey}"] to be "${expectedMimeType}" for platform "${platform}", but got "${actualMimeType}"`}};var toHavePlatformHtml=function(received,platform){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have platform HTML for "${platform}", but no _meta found`}}const htmlKey="ui/html";const html=meta[htmlKey];const pass=typeof html==="string"&&html.length>0;return{pass,message:()=>pass?`Expected _meta not to have platform HTML in "${htmlKey}"`:`Expected _meta["${htmlKey}"] to contain HTML for platform "${platform}", but ${html===void 0?"key not found":`got ${typeof html}`}`}};var uiMatchers={toHaveRenderedHtml,toContainHtmlElement,toContainBoundValue,toBeXssSafe,toHaveWidgetMetadata,toHaveCssClass,toNotContainRawContent,toHaveProperHtmlStructure,toHavePlatformMeta,toHaveMetaKey,toHaveMetaValue,toNotHaveMetaKey,toHaveOnlyNamespacedMeta,toHavePlatformMimeType,toHavePlatformHtml};var toContainTool=function(received,toolName){const tools=received;if(!Array.isArray(tools)){return{pass:false,message:()=>`Expected an array of tools, but received ${typeof received}`}}const pass=tools.some(t=>t.name===toolName);const availableTools=tools.map(t=>t.name).join(", ");return{pass,message:()=>pass?`Expected tools not to contain "${toolName}"`:`Expected tools to contain "${toolName}", but got: [${availableTools}]`}};var toBeSuccessful=function(received){const result=received;if(typeof result!=="object"||result===null||!("isSuccess"in result)){return{pass:false,message:()=>`Expected a result wrapper object with isSuccess property`}}const pass=result.isSuccess;return{pass,message:()=>pass?"Expected result not to be successful":`Expected result to be successful, but got error: ${result.error?.message??"unknown error"}`}};var toBeError=function(received,expectedCode){const result=received;if(typeof result!=="object"||result===null||!("isError"in result)){return{pass:false,message:()=>`Expected a result wrapper object with isError property`}}const receivedCode=typeof expectedCode==="string"?result.raw?._meta?.["code"]:result.error?.code;const codeMatches=expectedCode===void 0||receivedCode===expectedCode;const pass=result.isError&&codeMatches;return{pass,message:()=>{if(!result.isError){return"Expected result to be an error, but it was successful"}if(!codeMatches){return`Expected error code ${expectedCode}, but got ${String(receivedCode)}`}return"Expected result not to be an error"}}};var toHaveTextContent=function(received,expectedText){const result=received;if(typeof result!=="object"||result===null||!("text"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper or ResourceContentWrapper object with text method`}}const text=result.text();const hasText="hasTextContent"in result?result.hasTextContent():text!==void 0;let pass=hasText;if(pass&&expectedText!==void 0){pass=text?.includes(expectedText)??false}return{pass,message:()=>{if(!hasText){return"Expected result to have text content"}if(expectedText!==void 0&&!text?.includes(expectedText)){return`Expected text to contain "${expectedText}", but got: "${text}"`}return"Expected result not to have text content"}}};var toHaveImageContent=function(received){const result=received;if(typeof result!=="object"||result===null||!("hasImageContent"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper object with hasImageContent method`}}const pass=result.hasImageContent();return{pass,message:()=>pass?"Expected result not to have image content":"Expected result to have image content"}};var toHaveResourceContent=function(received){const result=received;if(typeof result!=="object"||result===null||!("hasResourceContent"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper object with hasResourceContent method`}}const pass=result.hasResourceContent();return{pass,message:()=>pass?"Expected result not to have resource content":"Expected result to have resource content"}};var toContainResource=function(received,uri){const resources=received;if(!Array.isArray(resources)){return{pass:false,message:()=>`Expected an array of resources, but received ${typeof received}`}}const pass=resources.some(r=>r.uri===uri);const availableUris=resources.map(r=>r.uri).join(", ");return{pass,message:()=>pass?`Expected resources not to contain "${uri}"`:`Expected resources to contain "${uri}", but got: [${availableUris}]`}};var toContainResourceTemplate=function(received,uriTemplate){const templates=received;if(!Array.isArray(templates)){return{pass:false,message:()=>`Expected an array of resource templates, but received ${typeof received}`}}const pass=templates.some(t=>t.uriTemplate===uriTemplate);const availableTemplates=templates.map(t=>t.uriTemplate).join(", ");return{pass,message:()=>pass?`Expected templates not to contain "${uriTemplate}"`:`Expected templates to contain "${uriTemplate}", but got: [${availableTemplates}]`}};var toHaveMimeType=function(received,mimeType){const result=received;if(typeof result!=="object"||result===null||!("hasMimeType"in result)){return{pass:false,message:()=>`Expected a ResourceContentWrapper object with hasMimeType method`}}const pass=result.hasMimeType(mimeType);const actualMimeType=result.mimeType();return{pass,message:()=>pass?`Expected content not to have MIME type "${mimeType}"`:`Expected MIME type "${mimeType}", but got "${actualMimeType}"`}};var toContainPrompt=function(received,name){const prompts=received;if(!Array.isArray(prompts)){return{pass:false,message:()=>`Expected an array of prompts, but received ${typeof received}`}}const pass=prompts.some(p=>p.name===name);const availablePrompts=prompts.map(p=>p.name).join(", ");return{pass,message:()=>pass?`Expected prompts not to contain "${name}"`:`Expected prompts to contain "${name}", but got: [${availablePrompts}]`}};var toHaveMessages=function(received,count){const result=received;if(typeof result!=="object"||result===null||!("messages"in result)){return{pass:false,message:()=>`Expected a PromptResultWrapper object with messages property`}}const actualCount=result.messages?.length??0;const pass=actualCount===count;return{pass,message:()=>pass?`Expected prompt not to have ${count} messages`:`Expected prompt to have ${count} messages, but got ${actualCount}`}};var toBeValidJsonRpc=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const hasJsonRpc=response["jsonrpc"]==="2.0";const hasId="id"in response;const hasResult="result"in response;const hasError="error"in response;const hasExactlyOneResultOrError=(hasResult||hasError)&&!(hasResult&&hasError);const pass=hasJsonRpc&&hasId&&hasExactlyOneResultOrError;return{pass,message:()=>{if(pass){return"Expected response not to be valid JSON-RPC"}const issues=[];if(!hasJsonRpc)issues.push('missing or invalid "jsonrpc": "2.0"');if(!hasId)issues.push('missing "id" field');if(!hasExactlyOneResultOrError){if(!hasResult&&!hasError)issues.push('missing "result" or "error"');else issues.push('cannot have both "result" and "error"')}return`Expected valid JSON-RPC 2.0 response: ${issues.join(", ")}`}}};var toHaveResult=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const pass="result"in response;return{pass,message:()=>pass?"Expected response not to have result":"Expected response to have result"}};var toHaveError=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const pass="error"in response;return{pass,message:()=>pass?"Expected response not to have error":"Expected response to have error"}};var toHaveErrorCode=function(received,code){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const actualCode=response.error?.code;const pass=actualCode===code;return{pass,message:()=>pass?`Expected response not to have error code ${code}`:`Expected error code ${code}, but got ${actualCode??"no error"}`}};var mcpMatchers={toContainTool,toBeSuccessful,toBeError,toHaveTextContent,toHaveImageContent,toHaveResourceContent,toContainResource,toContainResourceTemplate,toHaveMimeType,toContainPrompt,toHaveMessages,toBeValidJsonRpc,toHaveResult,toHaveError,toHaveErrorCode,...uiMatchers};var originalFetch=null;var mockingEnabled=false;var activeInterceptors=[];var HttpInterceptorImpl=class{mocks=[];_allowPassthrough=false;_isActive=true;mock(definition){const entry={definition,callCount:0,calls:[],remainingUses:definition.times??Infinity};this.mocks.push(entry);return{remove:()=>{const index=this.mocks.indexOf(entry);if(index!==-1){this.mocks.splice(index,1)}},callCount:()=>entry.callCount,calls:()=>[...entry.calls],waitForCalls:async(count,timeoutMs=5e3)=>{const deadline=Date.now()+timeoutMs;while(Date.now()<deadline){if(entry.callCount>=count){return entry.calls.slice(0,count)}await sleep3(50)}throw new Error(`Timeout waiting for ${count} calls, got ${entry.callCount}`)}}}get(url,response,options){return this.mock({match:{url,method:"GET",...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}post(url,response,options){return this.mock({match:{url,method:"POST",...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}put(url,response,options){return this.mock({match:{url,method:"PUT",...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}delete(url,response,options){return this.mock({match:{url,method:"DELETE",...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}any(url,response,options){return this.mock({match:{url,...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}clear(){this.mocks=[]}pending(){return this.mocks.filter(m=>m.remainingUses>0).map(m=>m.definition)}isDone(){return this.mocks.every(m=>m.remainingUses<=0||m.definition.times===void 0)}assertDone(){const pending=this.pending().filter(m=>m.times!==void 0);if(pending.length>0){const descriptions=pending.map(m=>{const url=m.match.url instanceof RegExp?m.match.url.toString():m.match.url;return` - ${m.match.method??"ANY"} ${url}`});throw new Error(`Unused HTTP mocks:
109
+ Error: ${errMsg}`,{cause:error})}}else{throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option')}const sharedSecret=config.env?.["JWT_SECRET"];const binding=sharedSecret?gatewayTokenBinding(config.baseUrl??server.info.baseUrl,config.entryPath):void 0;const tokenFactory2=new TestTokenFactory(sharedSecret?{hmacSecret:sharedSecret,...binding}:{});return{server,startedByUs,tokenFactory:tokenFactory2,owner}}function acquireServer(chain){const config=resolveConfig(chain);const key=serverKey(config);let entry=serverEntries.get(key);if(!entry){entry=startServerEntry(config,ownerScopeOf(chain));serverEntries.set(key,entry);entry.catch(()=>serverEntries.delete(key))}return entry}async function stopServers(owner){for(const[key,pending]of Array.from(serverEntries)){let entry;try{entry=await pending}catch{serverEntries.delete(key);continue}if(owner&&entry.owner!==owner)continue;serverEntries.delete(key);if(entry.startedByUs){await entry.server.stop()}}}var fixtureContexts=new WeakMap;async function buildFixtures(chain){const config=resolveConfig(chain);const entry=await acquireServer(chain);const{server,tokenFactory:tokenFactory2}=entry;const publicMode=config.publicMode??config.auth?.mode==="public";const clientConfig={...config,publicMode};const clientInstance=McpTestClient.create({baseUrl:resolveClientBaseUrl(server,clientConfig),entryPath:clientConfig.entryPath,transport:clientConfig.transport??"streamable-http",publicMode}).build();try{await clientInstance.connect()}catch(err){const message=err instanceof Error?err.message:String(err);if(/\b401\b|unauthor/i.test(message)){console.warn(`[TestFixture] MCP client could not connect anonymously \u2014 the server requires authentication (${message}). Tests using only the 'server'/'auth' fixtures are unaffected; tests using the 'mcp' fixture must supply a token.`)}else{throw err}}const context={extraClients:[]};const fixtures={mcp:clientInstance,auth:createAuthFixture(tokenFactory2),server:createServerFixture(server,clientConfig,clientInstance,context)};fixtureContexts.set(fixtures,context);return fixtures}async function cleanupTestFixtures(fixtures,testFailed=false){if(testFailed){const logs=fixtures.server.getLogs();if(logs.length>0){console.error("\n[TestFixture] === Server Logs (test failed) ===");const recentLogs=logs.slice(-50);if(logs.length>50){console.error(`[TestFixture] (showing last 50 of ${logs.length} log entries)`)}console.error(recentLogs.join("\n"));console.error("[TestFixture] === End Server Logs ===\n")}}const extras=fixtureContexts.get(fixtures)?.extraClients??[];for(const client of extras){if(client.isConnected()){await client.disconnect().catch(()=>void 0)}}if(fixtures.mcp.isConnected()){await fixtures.mcp.disconnect()}}function resolveClientBaseUrl(server,config){return config.baseUrl??server.info.baseUrl}function createAuthFixture(factory){const users={admin:{sub:"admin-001",scopes:["admin:*","read","write","delete"],email:"admin@test.local",name:"Test Admin"},user:{sub:"user-001",scopes:["read","write"],email:"user@test.local",name:"Test User"},readOnly:{sub:"readonly-001",scopes:["read"],email:"readonly@test.local",name:"Read Only User"}};return{createToken:opts=>factory.createTestToken({sub:opts.sub,scopes:opts.scopes,claims:{email:opts.email,name:opts.name,...opts.claims},exp:opts.expiresIn}),createExpiredToken:opts=>factory.createExpiredToken(opts),createInvalidToken:opts=>factory.createTokenWithInvalidSignature(opts),users:{admin:users["admin"],user:users["user"],readOnly:users["readOnly"]},getJwks:()=>factory.getPublicJwks(),getIssuer:()=>factory.getIssuer(),getAudience:()=>factory.getAudience()}}function createServerFixture(server,config,mcp,context){return{get info(){return server.info},createClient:async opts=>{const client=await McpTestClient.create({baseUrl:resolveClientBaseUrl(server,config),entryPath:opts?.entryPath??config.entryPath,transport:opts?.transport??"streamable-http",auth:opts?.token?{token:opts.token}:void 0,clientInfo:opts?.clientInfo,publicMode:config.publicMode}).buildAndConnect();context.extraClients.push(client);return client},createClientBuilder:()=>{const builder=new McpTestClientBuilder({baseUrl:resolveClientBaseUrl(server,config),entryPath:config.entryPath,publicMode:config.publicMode});return builder},restart:async()=>{const reconnect=[mcp,...context.extraClients].filter(client=>client.isConnected());await server.restart();for(const client of reconnect){await client.reconnect()}},getLogs:()=>server.getLogs(),clearLogs:()=>server.clearLogs()}}function resolveServerCommand(server){if(server.includes(" ")){return server}return`npx tsx ${server}`}function runWithFixtures(fn,chain){return async(...extra)=>{const fixtures=await buildFixtures(chain);let failure;try{for(const scope of chain){for(const hook of scope.beforeEachHooks)await hook(fixtures)}await fn(fixtures,...extra)}catch(error){failure={error}}try{await runAfterEachHooks(chain,fixtures)}catch(error){failure??={error}}await cleanupTestFixtures(fixtures,failure!==void 0);if(failure)throw failure.error}}async function runAfterEachHooks(chain,fixtures){let firstError;let failed=false;for(let i=chain.length-1;i>=0;i--){for(const hook of chain[i].afterEachHooks){try{await hook(fixtures)}catch(error){if(!failed)firstError=error;failed=true}}}if(failed)throw firstError}function skipTitle(name,scope){return scope.reason?`${name} (skipped: ${scope.reason})`:name}function testWithFixtures(name,fn){const chain=scopes.slice();const scope=currentScope();if(scope.skipped){jestGlobals().it.skip(skipTitle(name,scope),runWithFixtures(fn,chain));return}jestGlobals().it(name,runWithFixtures(fn,chain))}function use(config){const scope=currentScope();const{env,...rest}=config;scope.config={...scope.config,...rest};if(env)scope.envs.push(env);scope.configured=true;if(!scope.cleanupRegistered){scope.cleanupRegistered=true;jestGlobals().afterAll(async()=>{await stopServers(scope===scopes[0]?void 0:scope)})}}function skip(nameOrCondition,fnOrReason){if(typeof nameOrCondition==="boolean"){if(nameOrCondition){const scope=currentScope();scope.skipped=true;scope.reason=typeof fnOrReason==="string"?fnOrReason:void 0}return}if(typeof fnOrReason!=="function"){throw new TypeError(`test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`)}jestGlobals().it.skip(nameOrCondition,runWithFixtures(fnOrReason,scopes.slice()))}function only(name,fn){const scope=currentScope();if(scope.skipped){jestGlobals().it.skip(skipTitle(name,scope),runWithFixtures(fn,scopes.slice()));return}jestGlobals().it.only(name,runWithFixtures(fn,scopes.slice()))}function todo(name){jestGlobals().it.todo(name)}function each(table,...tagged){const chain=scopes.slice();const register=jestGlobals().it.each(table,...tagged);return(name,fn)=>{const scope=currentScope();if(scope.skipped){const skipRegister=jestGlobals().it.skip.each(table,...tagged);skipRegister(skipTitle(name,scope),runWithFixtures(fn,chain));return}register(name,runWithFixtures(fn,chain))}}function withScope(fn){return(...args)=>{scopes.push(newScope(currentScope()));try{fn(...args)}finally{scopes.pop()}}}function scopedRegister(pick){return(name,fn,timeout)=>{pick()(name,withScope(fn),timeout)}}function scopedEach(pick){return(...eachArgs)=>(name,fn,timeout)=>{const register=pick()(...eachArgs);register(name,withScope(fn),timeout)}}var describeWithScope=Object.assign(scopedRegister(()=>jestGlobals().describe),{only:Object.assign(scopedRegister(()=>jestGlobals().describe.only),{each:scopedEach(()=>jestGlobals().describe.only.each)}),skip:Object.assign(scopedRegister(()=>jestGlobals().describe.skip),{each:scopedEach(()=>jestGlobals().describe.skip.each)}),each:scopedEach(()=>jestGlobals().describe.each)});function eachHook(kind){return(fn,timeout)=>{if(typeof fn==="function"&&fn.length>0){const scope=currentScope();(kind==="beforeEach"?scope.beforeEachHooks:scope.afterEachHooks).push(fn);return}jestGlobals()[kind](fn,timeout)}}var test=testWithFixtures;test.use=use;test.describe=describeWithScope;test.beforeAll=((...args)=>jestGlobals().beforeAll(...args));test.beforeEach=eachHook("beforeEach");test.afterEach=eachHook("afterEach");test.afterAll=((...args)=>jestGlobals().afterAll(...args));test.skip=skip;test.only=only;test.todo=todo;test.each=each;var resolvedExpect;function resolveJestExpect(){if(resolvedExpect)return resolvedExpect;try{resolvedExpect=require("@jest/globals").expect;return resolvedExpect}catch(error){throw new Error(`expect() from @frontmcp/testing can only be used inside a Jest test file: ${error instanceof Error?error.message:String(error)}`,{cause:error})}}var expect=new Proxy(function frontmcpExpect(){},{apply:(_target,thisArg,args)=>Reflect.apply(resolveJestExpect(),thisArg,args),get:(_target,prop)=>Reflect.get(resolveJestExpect(),prop),has:(_target,prop)=>Reflect.has(resolveJestExpect(),prop)});var import_lazy_zod=require("@frontmcp/lazy-zod");var import_utils4=require("@frontmcp/utils");function getPlatformMetaNamespace(platform){return"ui"}function getPlatformMimeType(platform){return"text/html;profile=mcp-app"}function isOpenAIPlatform(platform){return platform==="openai"}function isExtAppsPlatform(platform){return platform==="ext-apps"}function isUiPlatform(platform){return true}function getToolsListMetaPrefixes(platform){return["ui/"]}function getToolCallMetaPrefixes(platform){return["ui/"]}function getForbiddenMetaPrefixes(platform){return["openai/","frontmcp/"]}function escapeRegex(str){return str.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function extractUiHtml(received){if(typeof received==="string"){return received}const wrapper=received;const meta=wrapper?.raw?._meta||wrapper?._meta;if(meta&&typeof meta==="object"){const uiHtml=meta["ui/html"];if(typeof uiHtml==="string"){return uiHtml}}return void 0}function extractMeta(received){const wrapper=received;const meta=wrapper?.raw?._meta||wrapper?._meta;if(meta&&typeof meta==="object"){return meta}return void 0}var toHaveRenderedHtml=function(received){const html=extractUiHtml(received);const hasHtml=html!==void 0&&html.length>0;const isFallback=hasHtml&&html.includes("mdx-fallback");const pass=hasHtml&&!isFallback;return{pass,message:()=>{if(isFallback){return"Expected rendered HTML but got mdx-fallback (raw escaped content). MDX rendering may have failed."}if(!hasHtml){return"Expected _meta to have ui/html property with rendered HTML"}return"Expected result not to have rendered HTML"}}};var toContainHtmlElement=function(received,tag){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected to find <${tag}> element, but no HTML content found`}}const regex=new RegExp(`<${escapeRegex(tag)}[\\s>]`,"i");const pass=regex.test(html);return{pass,message:()=>pass?`Expected HTML not to contain <${tag}> element`:`Expected HTML to contain <${tag}> element`}};var toContainBoundValue=function(received,value){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected HTML to contain bound value "${value}", but no HTML content found`}}const stringValue=String(value);const pass=html.includes(stringValue);return{pass,message:()=>pass?`Expected HTML not to contain bound value "${stringValue}"`:`Expected HTML to contain bound value "${stringValue}"`}};var toBeXssSafe=function(received){const html=extractUiHtml(received);if(!html){return{pass:true,message:()=>"Expected HTML to be XSS unsafe (no HTML found)"}}const hasScript=/<script[\s>]/i.test(html);const hasOnHandler=/\son\w+\s*=/i.test(html);const hasJavascriptUri=/javascript:/i.test(html);const issues=[];if(hasScript)issues.push("<script> tag");if(hasOnHandler)issues.push("inline event handler (onclick, etc.)");if(hasJavascriptUri)issues.push("javascript: URI");const pass=!hasScript&&!hasOnHandler&&!hasJavascriptUri;return{pass,message:()=>pass?"Expected HTML not to be XSS safe":`Expected HTML to be XSS safe, but found: ${issues.join(", ")}`}};var toHaveWidgetMetadata=function(received){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>"Expected _meta to have widget metadata, but no _meta found"}}const hasUiHtml=Boolean(meta["ui/html"]);const hasMimeType2=Boolean(meta["ui/mimeType"]);const hasUiObject=Boolean(meta["ui"]&&typeof meta["ui"]==="object");const pass=hasUiHtml||hasMimeType2||hasUiObject;return{pass,message:()=>pass?"Expected result not to have widget metadata":"Expected _meta to have widget metadata (ui/html, ui/mimeType, or ui object)"}};var toHaveCssClass=function(received,className){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected HTML to have CSS class "${className}", but no HTML content found`}}const classRegex=new RegExp(`class(?:Name)?\\s*=\\s*["'][^"']*\\b${escapeRegex(className)}\\b[^"']*["']`,"i");const pass=classRegex.test(html);return{pass,message:()=>pass?`Expected HTML not to have CSS class "${className}"`:`Expected HTML to have CSS class "${className}"`}};var toNotContainRawContent=function(received,content){const html=extractUiHtml(received);if(!html){return{pass:true,message:()=>`Expected HTML to contain raw content "${content}", but no HTML found`}}const pass=!html.includes(content);return{pass,message:()=>pass?`Expected HTML to contain raw content "${content}"`:`Expected HTML not to contain raw content "${content}" (may indicate rendering failure)`}};var toHaveProperHtmlStructure=function(received){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>"Expected proper HTML structure, but no HTML content found"}}const hasEscapedTags=html.includes("&lt;")&&html.includes("&gt;");const hasHtmlTags=/<[a-z]/i.test(html);const pass=hasHtmlTags&&!hasEscapedTags;return{pass,message:()=>{if(hasEscapedTags){return"Expected proper HTML structure, but found escaped HTML entities - content may not have been rendered"}if(!hasHtmlTags){return"Expected proper HTML structure, but found no HTML tags"}return"Expected result not to have proper HTML structure"}}};var toHavePlatformMeta=function(received,platform){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have platform meta for "${platform}", but no _meta found`}}const expectedPrefixes=getToolCallMetaPrefixes(platform);const forbiddenPrefixes=getForbiddenMetaPrefixes(platform);const metaKeys=Object.keys(meta);const hasExpectedPrefix=metaKeys.some(key=>expectedPrefixes.some(prefix=>key.startsWith(prefix)));const forbiddenKeys=metaKeys.filter(key=>forbiddenPrefixes.some(prefix=>key.startsWith(prefix)));const pass=hasExpectedPrefix&&forbiddenKeys.length===0;return{pass,message:()=>{if(!hasExpectedPrefix){return`Expected _meta to have keys with prefixes [${expectedPrefixes.join(", ")}] for platform "${platform}", but found: [${metaKeys.join(", ")}]`}if(forbiddenKeys.length>0){return`Expected _meta NOT to have keys [${forbiddenKeys.join(", ")}] for platform "${platform}" (forbidden prefixes: [${forbiddenPrefixes.join(", ")}])`}return`Expected result not to have platform meta for "${platform}"`}}};var toHaveMetaKey=function(received,key){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have key "${key}", but no _meta found`}}const pass=key in meta;return{pass,message:()=>pass?`Expected _meta not to have key "${key}"`:`Expected _meta to have key "${key}"`}};var toHaveMetaValue=function(received,key,value){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta["${key}"] to equal ${JSON.stringify(value)}, but no _meta found`}}const actualValue=meta[key];const pass=JSON.stringify(actualValue)===JSON.stringify(value);return{pass,message:()=>pass?`Expected _meta["${key}"] not to equal ${JSON.stringify(value)}`:`Expected _meta["${key}"] to equal ${JSON.stringify(value)}, but got ${JSON.stringify(actualValue)}`}};var toNotHaveMetaKey=function(received,key){const meta=extractMeta(received);if(!meta){return{pass:true,message:()=>`Expected _meta to have key "${key}", but no _meta found`}}const pass=!(key in meta);return{pass,message:()=>pass?`Expected _meta to have key "${key}"`:`Expected _meta not to have key "${key}"`}};var toHaveOnlyNamespacedMeta=function(received,namespace){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have keys with namespace "${namespace}", but no _meta found`}}const metaKeys=Object.keys(meta);const wrongKeys=metaKeys.filter(key=>!key.startsWith(namespace));const pass=wrongKeys.length===0&&metaKeys.length>0;return{pass,message:()=>{if(metaKeys.length===0){return`Expected _meta to have keys with namespace "${namespace}", but _meta is empty`}if(wrongKeys.length>0){return`Expected _meta to ONLY have keys with namespace "${namespace}", but found: [${wrongKeys.join(", ")}]`}return`Expected _meta not to have only keys with namespace "${namespace}"`}}};var toHavePlatformMimeType=function(received,platform){const meta=extractMeta(received);const expectedMimeType=getPlatformMimeType(platform);if(!meta){return{pass:false,message:()=>`Expected _meta to have MIME type "${expectedMimeType}" for platform "${platform}", but no _meta found`}}const mimeTypeKey="ui/mimeType";const actualMimeType=meta[mimeTypeKey];const pass=actualMimeType===expectedMimeType;return{pass,message:()=>pass?`Expected _meta["${mimeTypeKey}"] not to be "${expectedMimeType}"`:`Expected _meta["${mimeTypeKey}"] to be "${expectedMimeType}" for platform "${platform}", but got "${actualMimeType}"`}};var toHavePlatformHtml=function(received,platform){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have platform HTML for "${platform}", but no _meta found`}}const htmlKey="ui/html";const html=meta[htmlKey];const pass=typeof html==="string"&&html.length>0;return{pass,message:()=>pass?`Expected _meta not to have platform HTML in "${htmlKey}"`:`Expected _meta["${htmlKey}"] to contain HTML for platform "${platform}", but ${html===void 0?"key not found":`got ${typeof html}`}`}};var uiMatchers={toHaveRenderedHtml,toContainHtmlElement,toContainBoundValue,toBeXssSafe,toHaveWidgetMetadata,toHaveCssClass,toNotContainRawContent,toHaveProperHtmlStructure,toHavePlatformMeta,toHaveMetaKey,toHaveMetaValue,toNotHaveMetaKey,toHaveOnlyNamespacedMeta,toHavePlatformMimeType,toHavePlatformHtml};var toContainTool=function(received,toolName){const tools=received;if(!Array.isArray(tools)){return{pass:false,message:()=>`Expected an array of tools, but received ${typeof received}`}}const pass=tools.some(t=>t.name===toolName);const availableTools=tools.map(t=>t.name).join(", ");return{pass,message:()=>pass?`Expected tools not to contain "${toolName}"`:`Expected tools to contain "${toolName}", but got: [${availableTools}]`}};var toBeSuccessful=function(received){const result=received;if(typeof result!=="object"||result===null||!("isSuccess"in result)){return{pass:false,message:()=>`Expected a result wrapper object with isSuccess property`}}const pass=result.isSuccess;return{pass,message:()=>pass?"Expected result not to be successful":`Expected result to be successful, but got error: ${result.error?.message??"unknown error"}`}};var toBeError=function(received,expectedCode){const result=received;if(typeof result!=="object"||result===null||!("isError"in result)){return{pass:false,message:()=>`Expected a result wrapper object with isError property`}}const receivedCode=typeof expectedCode==="string"?result.raw?._meta?.["code"]:result.error?.code;const codeMatches=expectedCode===void 0||receivedCode===expectedCode;const pass=result.isError&&codeMatches;return{pass,message:()=>{if(!result.isError){return"Expected result to be an error, but it was successful"}if(!codeMatches){return`Expected error code ${expectedCode}, but got ${String(receivedCode)}`}return"Expected result not to be an error"}}};var toHaveTextContent=function(received,expectedText){const result=received;if(typeof result!=="object"||result===null||!("text"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper or ResourceContentWrapper object with text method`}}const text=result.text();const hasText="hasTextContent"in result?result.hasTextContent():text!==void 0;let pass=hasText;if(pass&&expectedText!==void 0){pass=text?.includes(expectedText)??false}return{pass,message:()=>{if(!hasText){return"Expected result to have text content"}if(expectedText!==void 0&&!text?.includes(expectedText)){return`Expected text to contain "${expectedText}", but got: "${text}"`}return"Expected result not to have text content"}}};var toHaveImageContent=function(received){const result=received;if(typeof result!=="object"||result===null||!("hasImageContent"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper object with hasImageContent method`}}const pass=result.hasImageContent();return{pass,message:()=>pass?"Expected result not to have image content":"Expected result to have image content"}};var toHaveResourceContent=function(received){const result=received;if(typeof result!=="object"||result===null||!("hasResourceContent"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper object with hasResourceContent method`}}const pass=result.hasResourceContent();return{pass,message:()=>pass?"Expected result not to have resource content":"Expected result to have resource content"}};var toPassOutputSchemaWhole=function(received,outputSchema){const schema=outputSchema instanceof import_lazy_zod.z.ZodType?outputSchema:import_lazy_zod.z.object(outputSchema);if((0,import_lazy_zod.hasTransform)(schema)){return{pass:false,message:()=>"toPassOutputSchemaWhole cannot compare keys through a transform (.transform(), z.preprocess(), .pipe()): assert on the parsed answer instead"}}const parsed=schema.safeParse(received);if(!parsed.success){const[firstIssue]=parsed.error.issues;const location=firstIssue?.path.length?` at ${firstIssue.path.join(".")}`:"";return{pass:false,message:()=>`Expected the answer to pass its output schema whole, but it does not match its output schema${location}: ${firstIssue?.message}`}}const droppedKeys=(0,import_utils4.findDroppedKeys)(received,parsed.data);const pass=droppedKeys.length===0;return{pass,message:()=>pass?"Expected the answer not to pass its output schema whole":`Expected the answer to pass its output schema whole, but parsing drops keys it does not declare: ${droppedKeys.join(", ")}`}};var toContainResource=function(received,uri){const resources=received;if(!Array.isArray(resources)){return{pass:false,message:()=>`Expected an array of resources, but received ${typeof received}`}}const pass=resources.some(r=>r.uri===uri);const availableUris=resources.map(r=>r.uri).join(", ");return{pass,message:()=>pass?`Expected resources not to contain "${uri}"`:`Expected resources to contain "${uri}", but got: [${availableUris}]`}};var toContainResourceTemplate=function(received,uriTemplate){const templates=received;if(!Array.isArray(templates)){return{pass:false,message:()=>`Expected an array of resource templates, but received ${typeof received}`}}const pass=templates.some(t=>t.uriTemplate===uriTemplate);const availableTemplates=templates.map(t=>t.uriTemplate).join(", ");return{pass,message:()=>pass?`Expected templates not to contain "${uriTemplate}"`:`Expected templates to contain "${uriTemplate}", but got: [${availableTemplates}]`}};var toHaveMimeType=function(received,mimeType){const result=received;if(typeof result!=="object"||result===null||!("hasMimeType"in result)){return{pass:false,message:()=>`Expected a ResourceContentWrapper object with hasMimeType method`}}const pass=result.hasMimeType(mimeType);const actualMimeType=result.mimeType();return{pass,message:()=>pass?`Expected content not to have MIME type "${mimeType}"`:`Expected MIME type "${mimeType}", but got "${actualMimeType}"`}};var toContainPrompt=function(received,name){const prompts=received;if(!Array.isArray(prompts)){return{pass:false,message:()=>`Expected an array of prompts, but received ${typeof received}`}}const pass=prompts.some(p=>p.name===name);const availablePrompts=prompts.map(p=>p.name).join(", ");return{pass,message:()=>pass?`Expected prompts not to contain "${name}"`:`Expected prompts to contain "${name}", but got: [${availablePrompts}]`}};var toHaveMessages=function(received,count){const result=received;if(typeof result!=="object"||result===null||!("messages"in result)){return{pass:false,message:()=>`Expected a PromptResultWrapper object with messages property`}}const actualCount=result.messages?.length??0;const pass=actualCount===count;return{pass,message:()=>pass?`Expected prompt not to have ${count} messages`:`Expected prompt to have ${count} messages, but got ${actualCount}`}};var toBeValidJsonRpc=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const hasJsonRpc=response["jsonrpc"]==="2.0";const hasId="id"in response;const hasResult="result"in response;const hasError="error"in response;const hasExactlyOneResultOrError=(hasResult||hasError)&&!(hasResult&&hasError);const pass=hasJsonRpc&&hasId&&hasExactlyOneResultOrError;return{pass,message:()=>{if(pass){return"Expected response not to be valid JSON-RPC"}const issues=[];if(!hasJsonRpc)issues.push('missing or invalid "jsonrpc": "2.0"');if(!hasId)issues.push('missing "id" field');if(!hasExactlyOneResultOrError){if(!hasResult&&!hasError)issues.push('missing "result" or "error"');else issues.push('cannot have both "result" and "error"')}return`Expected valid JSON-RPC 2.0 response: ${issues.join(", ")}`}}};var toHaveResult=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const pass="result"in response;return{pass,message:()=>pass?"Expected response not to have result":"Expected response to have result"}};var toHaveError=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const pass="error"in response;return{pass,message:()=>pass?"Expected response not to have error":"Expected response to have error"}};var toHaveErrorCode=function(received,code){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const actualCode=response.error?.code;const pass=actualCode===code;return{pass,message:()=>pass?`Expected response not to have error code ${code}`:`Expected error code ${code}, but got ${actualCode??"no error"}`}};var mcpMatchers={toContainTool,toBeSuccessful,toBeError,toHaveTextContent,toHaveImageContent,toHaveResourceContent,toPassOutputSchemaWhole,toContainResource,toContainResourceTemplate,toHaveMimeType,toContainPrompt,toHaveMessages,toBeValidJsonRpc,toHaveResult,toHaveError,toHaveErrorCode,...uiMatchers};var originalFetch=null;var mockingEnabled=false;var activeInterceptors=[];var HttpInterceptorImpl=class{mocks=[];_allowPassthrough=false;_isActive=true;mock(definition){const entry={definition,callCount:0,calls:[],remainingUses:definition.times??Infinity};this.mocks.push(entry);return{remove:()=>{const index=this.mocks.indexOf(entry);if(index!==-1){this.mocks.splice(index,1)}},callCount:()=>entry.callCount,calls:()=>[...entry.calls],waitForCalls:async(count,timeoutMs=5e3)=>{const deadline=Date.now()+timeoutMs;while(Date.now()<deadline){if(entry.callCount>=count){return entry.calls.slice(0,count)}await sleep3(50)}throw new Error(`Timeout waiting for ${count} calls, got ${entry.callCount}`)}}}get(url,response,options){return this.mock({match:{url,method:"GET",...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}post(url,response,options){return this.mock({match:{url,method:"POST",...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}put(url,response,options){return this.mock({match:{url,method:"PUT",...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}delete(url,response,options){return this.mock({match:{url,method:"DELETE",...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}any(url,response,options){return this.mock({match:{url,...options?.match},response:normalizeResponse(response),...options?.times!==void 0&&{times:options.times},...options?.name!==void 0&&{name:options.name}})}clear(){this.mocks=[]}pending(){return this.mocks.filter(m=>m.remainingUses>0).map(m=>m.definition)}isDone(){return this.mocks.every(m=>m.remainingUses<=0||m.definition.times===void 0)}assertDone(){const pending=this.pending().filter(m=>m.times!==void 0);if(pending.length>0){const descriptions=pending.map(m=>{const url=m.match.url instanceof RegExp?m.match.url.toString():m.match.url;return` - ${m.match.method??"ANY"} ${url}`});throw new Error(`Unused HTTP mocks:
110
110
  ${descriptions.join("\n")}`)}}allowPassthrough(allow){this._allowPassthrough=allow}restore(){this._isActive=false;const index=activeInterceptors.indexOf(this);if(index!==-1){activeInterceptors.splice(index,1)}if(activeInterceptors.length===0){httpMock.disable()}}isActive(){return this._isActive}canPassthrough(){return this._allowPassthrough}async matchRequest(info){if(!this._isActive)return null;for(const entry of this.mocks){if(entry.remainingUses<=0)continue;if(this.requestMatches(info,entry.definition.match)){entry.callCount++;entry.calls.push(info);entry.remainingUses--;let mockResponse2;if(typeof entry.definition.response==="function"){mockResponse2=await entry.definition.response(info)}else{mockResponse2=entry.definition.response}if(mockResponse2.delay&&mockResponse2.delay>0){await sleep3(mockResponse2.delay)}return createMockResponse(mockResponse2)}}return null}requestMatches(info,matcher){if(!this.urlMatches(info.url,matcher.url)){return false}if(matcher.method){const methods=Array.isArray(matcher.method)?matcher.method:[matcher.method];if(!methods.includes(info.method)){return false}}if(matcher.headers){for(const[key,expected]of Object.entries(matcher.headers)){const actual=info.headers[key.toLowerCase()];if(!actual)return false;if(expected instanceof RegExp){if(!expected.test(actual))return false}else if(actual!==expected){return false}}}if(matcher.body!==void 0){if(!this.bodyMatches(info.body,matcher.body)){return false}}return true}urlMatches(url,matcher){if(typeof matcher==="string"){return url===matcher||url.includes(matcher)}else if(matcher instanceof RegExp){return matcher.test(url)}else{return matcher(url)}}bodyMatches(actual,expected){if(typeof expected==="function"){return expected(actual)}if(typeof expected==="string"){const actualStr=typeof actual==="string"?actual:JSON.stringify(actual);return actualStr===expected||actualStr.includes(expected)}if(expected instanceof RegExp){const actualStr=typeof actual==="string"?actual:JSON.stringify(actual);return expected.test(actualStr)}if(typeof actual!=="object"||actual===null){return false}for(const[key,value]of Object.entries(expected)){const actualValue=actual[key];if(typeof value==="object"&&value!==null){if(!this.bodyMatches(actualValue,value)){return false}}else if(actualValue!==value){return false}}return true}};async function interceptedFetch(input,init){const request=typeof Request!=="undefined"&&input instanceof Request?input:void 0;const url=typeof input==="string"?input:input instanceof URL?input.toString():request?.url??String(input);const method=(init?.method??request?.method??"GET").toUpperCase();const headers={};const collectHeaders=source=>{if(!source)return;if(source instanceof Headers){source.forEach((value,key)=>{headers[key.toLowerCase()]=value})}else if(Array.isArray(source)){for(const[key,value]of source){headers[key.toLowerCase()]=value}}else{for(const[key,value]of Object.entries(source)){headers[key.toLowerCase()]=value}}};collectHeaders(init?.headers??request?.headers);let body;let rawBody;let bodySource=init?.body;if((bodySource===void 0||bodySource===null)&&request&&request.body!==null){bodySource=await request.clone().text()}if(bodySource){if(typeof bodySource==="string"){rawBody=bodySource}else if(bodySource instanceof ArrayBuffer||ArrayBuffer.isView(bodySource)){rawBody=new TextDecoder().decode(bodySource)}else if(bodySource instanceof URLSearchParams){rawBody=bodySource.toString()}if(rawBody!==void 0){try{body=JSON.parse(rawBody)}catch{body=rawBody}}else{body=bodySource}}const requestInfo={url,method,headers,body,rawBody};for(let i=activeInterceptors.length-1;i>=0;i--){const interceptor=activeInterceptors[i];if(!interceptor.isActive())continue;const mockResponse2=await interceptor.matchRequest(requestInfo);if(mockResponse2){return mockResponse2}}for(const interceptor of activeInterceptors){if(interceptor.isActive()&&interceptor.canPassthrough()){if(originalFetch){return originalFetch(input,init)}}}throw new Error(`No HTTP mock found for ${method} ${url}
111
- Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`)}var RESPONSE_KEYS=new Set(["status","statusText","headers","body","delay"]);function isResponseDescriptor(value){const keys=Object.keys(value);if(keys.length===0||!keys.every(key=>RESPONSE_KEYS.has(key)))return false;if("status"in value){const status=value["status"];if(typeof status!=="number"||!Number.isInteger(status)||status<200||status>599)return false}return true}function normalizeResponse(response){const isNetworkError=!Array.isArray(response)&&response["_throwError"]===true;if(isNetworkError)return response;if(Array.isArray(response)||!isResponseDescriptor(response)){return{body:response}}return response}function createMockResponse(mock){const mockAny=mock;if(mockAny._throwError){const body2=mock.body;const message=body2&&typeof body2==="object"&&"message"in body2?String(body2["message"]):"Network error";throw new TypeError(`fetch failed: ${message}`)}const status=mock.status??200;const statusText=mock.statusText??"OK";const headers=new Headers(mock.headers??{});let body=null;if(mock.body!==void 0){if(typeof mock.body==="string"){body=mock.body;if(!headers.has("content-type")){headers.set("content-type","text/plain")}}else if(mock.body instanceof ArrayBuffer){body=mock.body}else if(Buffer.isBuffer(mock.body)){body=new Uint8Array(mock.body).buffer}else{body=JSON.stringify(mock.body);if(!headers.has("content-type")){headers.set("content-type","application/json")}}}return new Response(body,{status,statusText,headers})}function sleep3(ms){return new Promise(resolve=>setTimeout(resolve,ms))}var HttpMockManagerImpl=class{interceptor(){if(!mockingEnabled){this.enable()}const interceptor=new HttpInterceptorImpl;activeInterceptors.push(interceptor);return interceptor}enable(){if(mockingEnabled)return;if(typeof globalThis.fetch==="function"){originalFetch=globalThis.fetch;globalThis.fetch=interceptedFetch}mockingEnabled=true}disable(){if(!mockingEnabled)return;if(originalFetch){globalThis.fetch=originalFetch;originalFetch=null}activeInterceptors.length=0;mockingEnabled=false}isEnabled(){return mockingEnabled}clearAll(){for(const interceptor of activeInterceptors){interceptor.clear()}activeInterceptors.length=0}};var httpMock=new HttpMockManagerImpl;var httpResponse={json(data,status=200){return{status,headers:{"content-type":"application/json"},body:data}},text(data,status=200){return{status,headers:{"content-type":"text/plain"},body:data}},html(data,status=200){return{status,headers:{"content-type":"text/html"},body:data}},error(status,message){return{status,statusText:message??getStatusText(status),body:message?{error:message}:void 0}},notFound(message="Not Found"){return httpResponse.error(404,message)},serverError(message="Internal Server Error"){return httpResponse.error(500,message)},unauthorized(message="Unauthorized"){return httpResponse.error(401,message)},forbidden(message="Forbidden"){return httpResponse.error(403,message)},networkError(message="Network error"){return{status:0,body:{_networkError:true,message},_throwError:true}},delayed(data,delayMs,status=200){return{status,body:data,delay:delayMs}}};function getStatusText(status){const texts={200:"OK",201:"Created",204:"No Content",400:"Bad Request",401:"Unauthorized",403:"Forbidden",404:"Not Found",500:"Internal Server Error",502:"Bad Gateway",503:"Service Unavailable"};return texts[status]??"Unknown"}function escapeRegex2(str){return str.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}var UIAssertions={assertRenderedUI(result){const meta=result.raw._meta;if(!meta){throw new Error("Expected tool result to have _meta, but _meta is undefined")}const html=meta["ui/html"];if(!html){throw new Error("Expected tool result to have ui/html in _meta, but it is missing")}if(typeof html!=="string"){throw new Error(`Expected ui/html to be a string, but got ${typeof html}`)}if(html.includes("mdx-fallback")){throw new Error("Got mdx-fallback instead of rendered HTML - MDX/React rendering failed. Check that @mdx-js/mdx is installed and the template syntax is valid.")}return html},assertDataBinding(html,output,keys){const missingKeys=[];for(const key of keys){const value=output[key];if(value===void 0||value===null){continue}const stringValue=String(value);if(!html.includes(stringValue)){missingKeys.push(`${key}="${stringValue}"`)}}if(missingKeys.length>0){throw new Error(`Expected HTML to contain bound values for: ${missingKeys.join(", ")}. Data binding may have failed.`)}},assertXssSafe(html){const vulnerabilities=[];if(/<script[\s>]/i.test(html)){vulnerabilities.push("<script> tag detected")}if(/\son\w+\s*=/i.test(html)){vulnerabilities.push("inline event handler detected (onclick, onerror, etc.)")}if(/javascript:/i.test(html)){vulnerabilities.push("javascript: URI detected")}if(vulnerabilities.length>0){throw new Error(`Potential XSS vulnerabilities found: ${vulnerabilities.join("; ")}`)}},assertProperHtmlStructure(html){if(html.includes("&lt;")&&html.includes("&gt;")){throw new Error("HTML contains escaped HTML entities (&lt;, &gt;) - content was likely not rendered. Check that the template is being processed correctly.")}if(!/<[a-z]/i.test(html)){throw new Error("HTML contains no HTML tags - content may be plain text or rendering failed.")}},assertContainsElement(html,tag){const regex=new RegExp(`<${escapeRegex2(tag)}[\\s>]`,"i");if(!regex.test(html)){throw new Error(`Expected HTML to contain <${tag}> element`)}},assertHasCssClass(html,className){const classRegex=new RegExp(`class(?:Name)?\\s*=\\s*["'][^"']*\\b${escapeRegex2(className)}\\b[^"']*["']`,"i");if(!classRegex.test(html)){throw new Error(`Expected HTML to have CSS class "${className}"`)}},assertNotContainsRaw(html,content){if(html.includes(content)){throw new Error(`HTML contains raw content "${content}" - this component may not have been rendered. Check that all custom components are properly passed to the renderer.`)}},assertWidgetMetadata(result){const meta=result.raw._meta;if(!meta){throw new Error("Expected tool result to have _meta with widget metadata")}const hasUiHtml=Boolean(meta["ui/html"]);const hasMimeType2=Boolean(meta["ui/mimeType"]);const hasUiObject=Boolean(meta["ui"]&&typeof meta["ui"]==="object");if(!hasUiHtml&&!hasMimeType2&&!hasUiObject){throw new Error("Expected _meta to have widget metadata (ui/html, ui/mimeType, or ui object)")}},assertValidUI(result,boundKeys){const html=UIAssertions.assertRenderedUI(result);UIAssertions.assertProperHtmlStructure(html);UIAssertions.assertXssSafe(html);if(boundKeys&&boundKeys.length>0){try{const output=JSON.parse(result.text()||"{}");UIAssertions.assertDataBinding(html,output,boundKeys)}catch{}}return html},assertOpenAIMeta(result){UIAssertions.assertPlatformMeta(result,"openai")},assertExtAppsMeta(result){UIAssertions.assertPlatformMeta(result,"ext-apps")},assertUiMeta(result){UIAssertions.assertPlatformMeta(result,"claude")},assertPlatformMeta(result,platform){const meta=result.raw._meta;if(!meta){throw new Error(`Expected tool result to have _meta with platform meta for "${platform}"`)}const expectedPrefixes=getToolCallMetaPrefixes(platform);const forbiddenPrefixes=getForbiddenMetaPrefixes(platform);const metaKeys=Object.keys(meta);const hasExpectedPrefix=metaKeys.some(key=>expectedPrefixes.some(prefix=>key.startsWith(prefix)));if(!hasExpectedPrefix){throw new Error(`Expected _meta to have keys with prefixes [${expectedPrefixes.join(", ")}] for platform "${platform}", but found: [${metaKeys.join(", ")}]`)}const forbiddenKeys=metaKeys.filter(key=>forbiddenPrefixes.some(prefix=>key.startsWith(prefix)));if(forbiddenKeys.length>0){throw new Error(`Expected _meta NOT to have keys [${forbiddenKeys.join(", ")}] for platform "${platform}" (forbidden prefixes: [${forbiddenPrefixes.join(", ")}])`)}},assertNoMixedNamespaces(result,expectedNamespace){const meta=result.raw._meta;if(!meta){throw new Error(`Expected tool result to have _meta with namespace "${expectedNamespace}"`)}const metaKeys=Object.keys(meta);const wrongKeys=metaKeys.filter(key=>!key.startsWith(expectedNamespace));if(wrongKeys.length>0){throw new Error(`Expected _meta to ONLY have keys with namespace "${expectedNamespace}", but found: [${wrongKeys.join(", ")}]`)}},assertPlatformMimeType(result,platform){const meta=result.raw._meta;const expectedMimeType=getPlatformMimeType(platform);if(!meta){throw new Error(`Expected tool result to have _meta with MIME type for platform "${platform}"`)}const mimeTypeKey="ui/mimeType";const actualMimeType=meta[mimeTypeKey];if(actualMimeType!==expectedMimeType){throw new Error(`Expected _meta["${mimeTypeKey}"] to be "${expectedMimeType}" for platform "${platform}", but got "${actualMimeType}"`)}},assertPlatformHtml(result,platform){const meta=result.raw._meta;if(!meta){throw new Error(`Expected tool result to have _meta with platform HTML for "${platform}"`)}const htmlKey="ui/html";const html=meta[htmlKey];if(typeof html!=="string"||html.length===0){throw new Error(`Expected _meta["${htmlKey}"] to contain HTML for platform "${platform}", but ${html===void 0?"key not found":`got ${typeof html}`}`)}return html},assertValidPlatformMeta(result,platform){UIAssertions.assertPlatformMeta(result,platform);UIAssertions.assertPlatformMimeType(result,platform);return UIAssertions.assertPlatformHtml(result,platform)}};var import_lazy_zod=require("@frontmcp/lazy-zod");var basicUIToolInputSchema=import_lazy_zod.z.object({name:import_lazy_zod.z.string().optional().default("World")});var basicUIToolOutputSchema=import_lazy_zod.z.object({message:import_lazy_zod.z.string(),timestamp:import_lazy_zod.z.number()});var BASIC_UI_TOOL_CONFIG={name:"platform-test-basic",description:"Basic UI tool for platform testing",inputSchema:basicUIToolInputSchema,outputSchema:basicUIToolOutputSchema,ui:{template:`
111
+ Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`)}var RESPONSE_KEYS=new Set(["status","statusText","headers","body","delay"]);function isResponseDescriptor(value){const keys=Object.keys(value);if(keys.length===0||!keys.every(key=>RESPONSE_KEYS.has(key)))return false;if("status"in value){const status=value["status"];if(typeof status!=="number"||!Number.isInteger(status)||status<200||status>599)return false}return true}function normalizeResponse(response){const isNetworkError=!Array.isArray(response)&&response["_throwError"]===true;if(isNetworkError)return response;if(Array.isArray(response)||!isResponseDescriptor(response)){return{body:response}}return response}function createMockResponse(mock){const mockAny=mock;if(mockAny._throwError){const body2=mock.body;const message=body2&&typeof body2==="object"&&"message"in body2?String(body2["message"]):"Network error";throw new TypeError(`fetch failed: ${message}`)}const status=mock.status??200;const statusText=mock.statusText??"OK";const headers=new Headers(mock.headers??{});let body=null;if(mock.body!==void 0){if(typeof mock.body==="string"){body=mock.body;if(!headers.has("content-type")){headers.set("content-type","text/plain")}}else if(mock.body instanceof ArrayBuffer){body=mock.body}else if(Buffer.isBuffer(mock.body)){body=new Uint8Array(mock.body).buffer}else{body=JSON.stringify(mock.body);if(!headers.has("content-type")){headers.set("content-type","application/json")}}}return new Response(body,{status,statusText,headers})}function sleep3(ms){return new Promise(resolve=>setTimeout(resolve,ms))}var HttpMockManagerImpl=class{interceptor(){if(!mockingEnabled){this.enable()}const interceptor=new HttpInterceptorImpl;activeInterceptors.push(interceptor);return interceptor}enable(){if(mockingEnabled)return;if(typeof globalThis.fetch==="function"){originalFetch=globalThis.fetch;globalThis.fetch=interceptedFetch}mockingEnabled=true}disable(){if(!mockingEnabled)return;if(originalFetch){globalThis.fetch=originalFetch;originalFetch=null}activeInterceptors.length=0;mockingEnabled=false}isEnabled(){return mockingEnabled}clearAll(){for(const interceptor of activeInterceptors){interceptor.clear()}activeInterceptors.length=0}};var httpMock=new HttpMockManagerImpl;var httpResponse={json(data,status=200){return{status,headers:{"content-type":"application/json"},body:data}},text(data,status=200){return{status,headers:{"content-type":"text/plain"},body:data}},html(data,status=200){return{status,headers:{"content-type":"text/html"},body:data}},error(status,message){return{status,statusText:message??getStatusText(status),body:message?{error:message}:void 0}},notFound(message="Not Found"){return httpResponse.error(404,message)},serverError(message="Internal Server Error"){return httpResponse.error(500,message)},unauthorized(message="Unauthorized"){return httpResponse.error(401,message)},forbidden(message="Forbidden"){return httpResponse.error(403,message)},networkError(message="Network error"){return{status:0,body:{_networkError:true,message},_throwError:true}},delayed(data,delayMs,status=200){return{status,body:data,delay:delayMs}}};function getStatusText(status){const texts={200:"OK",201:"Created",204:"No Content",400:"Bad Request",401:"Unauthorized",403:"Forbidden",404:"Not Found",500:"Internal Server Error",502:"Bad Gateway",503:"Service Unavailable"};return texts[status]??"Unknown"}function escapeRegex2(str){return str.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}var UIAssertions={assertRenderedUI(result){const meta=result.raw._meta;if(!meta){throw new Error("Expected tool result to have _meta, but _meta is undefined")}const html=meta["ui/html"];if(!html){throw new Error("Expected tool result to have ui/html in _meta, but it is missing")}if(typeof html!=="string"){throw new Error(`Expected ui/html to be a string, but got ${typeof html}`)}if(html.includes("mdx-fallback")){throw new Error("Got mdx-fallback instead of rendered HTML - MDX/React rendering failed. Check that @mdx-js/mdx is installed and the template syntax is valid.")}return html},assertDataBinding(html,output,keys){const missingKeys=[];for(const key of keys){const value=output[key];if(value===void 0||value===null){continue}const stringValue=String(value);if(!html.includes(stringValue)){missingKeys.push(`${key}="${stringValue}"`)}}if(missingKeys.length>0){throw new Error(`Expected HTML to contain bound values for: ${missingKeys.join(", ")}. Data binding may have failed.`)}},assertXssSafe(html){const vulnerabilities=[];if(/<script[\s>]/i.test(html)){vulnerabilities.push("<script> tag detected")}if(/\son\w+\s*=/i.test(html)){vulnerabilities.push("inline event handler detected (onclick, onerror, etc.)")}if(/javascript:/i.test(html)){vulnerabilities.push("javascript: URI detected")}if(vulnerabilities.length>0){throw new Error(`Potential XSS vulnerabilities found: ${vulnerabilities.join("; ")}`)}},assertProperHtmlStructure(html){if(html.includes("&lt;")&&html.includes("&gt;")){throw new Error("HTML contains escaped HTML entities (&lt;, &gt;) - content was likely not rendered. Check that the template is being processed correctly.")}if(!/<[a-z]/i.test(html)){throw new Error("HTML contains no HTML tags - content may be plain text or rendering failed.")}},assertContainsElement(html,tag){const regex=new RegExp(`<${escapeRegex2(tag)}[\\s>]`,"i");if(!regex.test(html)){throw new Error(`Expected HTML to contain <${tag}> element`)}},assertHasCssClass(html,className){const classRegex=new RegExp(`class(?:Name)?\\s*=\\s*["'][^"']*\\b${escapeRegex2(className)}\\b[^"']*["']`,"i");if(!classRegex.test(html)){throw new Error(`Expected HTML to have CSS class "${className}"`)}},assertNotContainsRaw(html,content){if(html.includes(content)){throw new Error(`HTML contains raw content "${content}" - this component may not have been rendered. Check that all custom components are properly passed to the renderer.`)}},assertWidgetMetadata(result){const meta=result.raw._meta;if(!meta){throw new Error("Expected tool result to have _meta with widget metadata")}const hasUiHtml=Boolean(meta["ui/html"]);const hasMimeType2=Boolean(meta["ui/mimeType"]);const hasUiObject=Boolean(meta["ui"]&&typeof meta["ui"]==="object");if(!hasUiHtml&&!hasMimeType2&&!hasUiObject){throw new Error("Expected _meta to have widget metadata (ui/html, ui/mimeType, or ui object)")}},assertValidUI(result,boundKeys){const html=UIAssertions.assertRenderedUI(result);UIAssertions.assertProperHtmlStructure(html);UIAssertions.assertXssSafe(html);if(boundKeys&&boundKeys.length>0){try{const output=JSON.parse(result.text()||"{}");UIAssertions.assertDataBinding(html,output,boundKeys)}catch{}}return html},assertOpenAIMeta(result){UIAssertions.assertPlatformMeta(result,"openai")},assertExtAppsMeta(result){UIAssertions.assertPlatformMeta(result,"ext-apps")},assertUiMeta(result){UIAssertions.assertPlatformMeta(result,"claude")},assertPlatformMeta(result,platform){const meta=result.raw._meta;if(!meta){throw new Error(`Expected tool result to have _meta with platform meta for "${platform}"`)}const expectedPrefixes=getToolCallMetaPrefixes(platform);const forbiddenPrefixes=getForbiddenMetaPrefixes(platform);const metaKeys=Object.keys(meta);const hasExpectedPrefix=metaKeys.some(key=>expectedPrefixes.some(prefix=>key.startsWith(prefix)));if(!hasExpectedPrefix){throw new Error(`Expected _meta to have keys with prefixes [${expectedPrefixes.join(", ")}] for platform "${platform}", but found: [${metaKeys.join(", ")}]`)}const forbiddenKeys=metaKeys.filter(key=>forbiddenPrefixes.some(prefix=>key.startsWith(prefix)));if(forbiddenKeys.length>0){throw new Error(`Expected _meta NOT to have keys [${forbiddenKeys.join(", ")}] for platform "${platform}" (forbidden prefixes: [${forbiddenPrefixes.join(", ")}])`)}},assertNoMixedNamespaces(result,expectedNamespace){const meta=result.raw._meta;if(!meta){throw new Error(`Expected tool result to have _meta with namespace "${expectedNamespace}"`)}const metaKeys=Object.keys(meta);const wrongKeys=metaKeys.filter(key=>!key.startsWith(expectedNamespace));if(wrongKeys.length>0){throw new Error(`Expected _meta to ONLY have keys with namespace "${expectedNamespace}", but found: [${wrongKeys.join(", ")}]`)}},assertPlatformMimeType(result,platform){const meta=result.raw._meta;const expectedMimeType=getPlatformMimeType(platform);if(!meta){throw new Error(`Expected tool result to have _meta with MIME type for platform "${platform}"`)}const mimeTypeKey="ui/mimeType";const actualMimeType=meta[mimeTypeKey];if(actualMimeType!==expectedMimeType){throw new Error(`Expected _meta["${mimeTypeKey}"] to be "${expectedMimeType}" for platform "${platform}", but got "${actualMimeType}"`)}},assertPlatformHtml(result,platform){const meta=result.raw._meta;if(!meta){throw new Error(`Expected tool result to have _meta with platform HTML for "${platform}"`)}const htmlKey="ui/html";const html=meta[htmlKey];if(typeof html!=="string"||html.length===0){throw new Error(`Expected _meta["${htmlKey}"] to contain HTML for platform "${platform}", but ${html===void 0?"key not found":`got ${typeof html}`}`)}return html},assertValidPlatformMeta(result,platform){UIAssertions.assertPlatformMeta(result,platform);UIAssertions.assertPlatformMimeType(result,platform);return UIAssertions.assertPlatformHtml(result,platform)}};var import_lazy_zod2=require("@frontmcp/lazy-zod");var basicUIToolInputSchema=import_lazy_zod2.z.object({name:import_lazy_zod2.z.string().optional().default("World")});var basicUIToolOutputSchema=import_lazy_zod2.z.object({message:import_lazy_zod2.z.string(),timestamp:import_lazy_zod2.z.number()});var BASIC_UI_TOOL_CONFIG={name:"platform-test-basic",description:"Basic UI tool for platform testing",inputSchema:basicUIToolInputSchema,outputSchema:basicUIToolOutputSchema,ui:{template:`
112
112
  <div class="platform-test-basic">
113
113
  <h1>Platform Test - Basic</h1>
114
114
  <p>Message: {output.message}</p>
115
115
  <p>Timestamp: {output.timestamp}</p>
116
116
  </div>
117
- `.trim()}};var fullUIToolInputSchema=import_lazy_zod.z.object({name:import_lazy_zod.z.string().optional().default("World"),count:import_lazy_zod.z.number().optional().default(1)});var fullUIToolOutputSchema=import_lazy_zod.z.object({message:import_lazy_zod.z.string(),count:import_lazy_zod.z.number(),items:import_lazy_zod.z.array(import_lazy_zod.z.string()),timestamp:import_lazy_zod.z.number()});var FULL_UI_TOOL_CONFIG={name:"platform-test-full",description:"Full UI tool with all options for comprehensive platform testing",inputSchema:fullUIToolInputSchema,outputSchema:fullUIToolOutputSchema,ui:{template:`
117
+ `.trim()}};var fullUIToolInputSchema=import_lazy_zod2.z.object({name:import_lazy_zod2.z.string().optional().default("World"),count:import_lazy_zod2.z.number().optional().default(1)});var fullUIToolOutputSchema=import_lazy_zod2.z.object({message:import_lazy_zod2.z.string(),count:import_lazy_zod2.z.number(),items:import_lazy_zod2.z.array(import_lazy_zod2.z.string()),timestamp:import_lazy_zod2.z.number()});var FULL_UI_TOOL_CONFIG={name:"platform-test-full",description:"Full UI tool with all options for comprehensive platform testing",inputSchema:fullUIToolInputSchema,outputSchema:fullUIToolOutputSchema,ui:{template:`
118
118
  <div class="platform-test-full">
119
119
  <h1>Platform Test - Full</h1>
120
120
  <div class="message-box">
@@ -154,7 +154,7 @@ Server entry: ${currentConfig.server}
154
154
  Project: ${currentConfig.project??"default"}
155
155
  Command: ${serverCommand}
156
156
 
157
- Error: ${errMsg}`,{cause:error})}}else{throw new Error('perfTest.use() requires either "server" (entry file path) or "baseUrl" option')}}}async function createTestFixtures(testName){await initializeSharedResources();if(!serverInstance){throw new Error("Server instance not initialized")}if(!tokenFactory){throw new Error("Token factory not initialized")}const clientInstance=await McpTestClient.create({baseUrl:serverInstance.info.baseUrl,transport:currentConfig.transport??"streamable-http",publicMode:currentConfig.publicMode}).buildAndConnect();const auth=createAuthFixture2(tokenFactory);const server=createServerFixture2(serverInstance);const perfImpl=createPerfFixtures(testName,currentConfig.project??"unknown");if(currentConfig.forceGcOnBaseline!==false){await perfImpl.baseline()}return{fixtures:{mcp:clientInstance,auth,server,perf:perfImpl},perfImpl}}async function cleanupTestFixtures2(fixtures,perfImpl,testFailed=false){const measurement=perfImpl.buildMeasurement();addGlobalMeasurement(measurement);if(testFailed&&serverInstance){const logs=serverInstance.getLogs();if(logs.length>0){console.error("\n[PerfTest] === Server Logs (test failed) ===");const recentLogs=logs.slice(-50);if(logs.length>50){console.error(`[PerfTest] (showing last 50 of ${logs.length} log entries)`)}console.error(recentLogs.join("\n"));console.error("[PerfTest] === End Server Logs ===\n")}}if(fixtures.mcp.isConnected()){await fixtures.mcp.disconnect()}}async function cleanupSharedResources(){if(serverInstance&&serverStartedByUs){await serverInstance.stop()}serverInstance=null;tokenFactory=null;serverStartedByUs=false}function createAuthFixture2(factory){const users={admin:{sub:"admin-001",scopes:["admin:*","read","write","delete"],email:"admin@test.local",name:"Test Admin"},user:{sub:"user-001",scopes:["read","write"],email:"user@test.local",name:"Test User"},readOnly:{sub:"readonly-001",scopes:["read"],email:"readonly@test.local",name:"Read Only User"}};return{createToken:opts=>factory.createTestToken({sub:opts.sub,scopes:opts.scopes,claims:{email:opts.email,name:opts.name,...opts.claims},exp:opts.expiresIn}),createExpiredToken:opts=>factory.createExpiredToken(opts),createInvalidToken:opts=>factory.createTokenWithInvalidSignature(opts),users:{admin:users["admin"],user:users["user"],readOnly:users["readOnly"]},getJwks:()=>factory.getPublicJwks(),getIssuer:()=>factory.getIssuer(),getAudience:()=>factory.getAudience()}}function createServerFixture2(server){return{info:server.info,createClient:async opts=>{return McpTestClient.create({baseUrl:server.info.baseUrl,transport:opts?.transport??"streamable-http",auth:opts?.token?{token:opts.token}:void 0,clientInfo:opts?.clientInfo,publicMode:currentConfig.publicMode}).buildAndConnect()},createClientBuilder:()=>{return new McpTestClientBuilder({baseUrl:server.info.baseUrl,publicMode:currentConfig.publicMode})},restart:()=>server.restart(),getLogs:()=>server.getLogs(),clearLogs:()=>server.clearLogs()}}function resolveServerCommand2(server){if(server.includes(" ")){return server}return`npx tsx ${server}`}function perfTestWithFixtures(name,fn){it(name,async()=>{const{fixtures,perfImpl}=await createTestFixtures(name);let testFailed=false;try{await fn(fixtures)}catch(error){testFailed=true;throw error}finally{await cleanupTestFixtures2(fixtures,perfImpl,testFailed)}})}function use2(config){currentConfig={...currentConfig,...config};afterAll(async()=>{await cleanupSharedResources()})}function skip2(name,fn){it.skip(name,async()=>{const{fixtures,perfImpl}=await createTestFixtures(name);let testFailed=false;try{await fn(fixtures)}catch(error){testFailed=true;throw error}finally{await cleanupTestFixtures2(fixtures,perfImpl,testFailed)}})}function only2(name,fn){it.only(name,async()=>{const{fixtures,perfImpl}=await createTestFixtures(name);let testFailed=false;try{await fn(fixtures)}catch(error){testFailed=true;throw error}finally{await cleanupTestFixtures2(fixtures,perfImpl,testFailed)}})}function todo2(name){it.todo(name)}var perfTest=perfTestWithFixtures;perfTest.use=use2;for(const hook of["describe","beforeAll","beforeEach","afterEach","afterAll"]){Object.defineProperty(perfTest,hook,{get:()=>globalThis[hook],enumerable:true,configurable:true})}perfTest.skip=skip2;perfTest.only=only2;perfTest.todo=todo2;var import_utils4=require("@frontmcp/utils");var BASELINE_START_MARKER="<!-- PERF_BASELINE_START -->";var BASELINE_END_MARKER="<!-- PERF_BASELINE_END -->";var DEFAULT_BASELINE_PATH="perf-results/baseline.json";var BaselineStore=class{baseline=null;baselinePath;constructor(baselinePath=DEFAULT_BASELINE_PATH){this.baselinePath=baselinePath}async load(){try{if(!await(0,import_utils4.fileExists)(this.baselinePath)){return null}const content=await(0,import_utils4.readFile)(this.baselinePath);this.baseline=JSON.parse(content);return this.baseline}catch{return null}}async save(baseline){const dir=this.baselinePath.substring(0,this.baselinePath.lastIndexOf("/"));await(0,import_utils4.ensureDir)(dir);await(0,import_utils4.writeFile)(this.baselinePath,JSON.stringify(baseline,null,2));this.baseline=baseline}getTestBaseline(testId){if(!this.baseline){return null}return this.baseline.tests[testId]??null}isLoaded(){return this.baseline!==null}getBaseline(){return this.baseline}static createFromMeasurements(measurements,release,commitHash){const tests={};const grouped=new Map;for(const m of measurements){const key=`${m.project}::${m.name}`;if(!grouped.has(key)){grouped.set(key,[])}const group=grouped.get(key);if(group){group.push(m)}}for(const[key,testMeasurements]of grouped){const[project]=key.split("::");const heapSamples=testMeasurements.map(m=>m.final?.memory.heapUsed??0);const durationSamples=testMeasurements.map(m=>m.timing.durationMs);const cpuSamples=testMeasurements.map(m=>m.final?.cpu.total??0);tests[key]={testId:key,project,heapUsed:calculateMetricBaseline(heapSamples),durationMs:calculateMetricBaseline(durationSamples),cpuTime:calculateMetricBaseline(cpuSamples),createdAt:new Date().toISOString(),commitHash}}return{release,timestamp:new Date().toISOString(),commitHash,tests}}};function parseBaselineFromComment(commentBody){const startIdx=commentBody.indexOf(BASELINE_START_MARKER);const endIdx=commentBody.indexOf(BASELINE_END_MARKER);if(startIdx===-1||endIdx===-1||startIdx>=endIdx){return null}const content=commentBody.substring(startIdx+BASELINE_START_MARKER.length,endIdx);const jsonMatch=content.match(/```json\s*([\s\S]*?)\s*```/);if(!jsonMatch){return null}try{return JSON.parse(jsonMatch[1])}catch{return null}}function formatBaselineAsComment(baseline){const json=JSON.stringify(baseline,null,2);return`## Performance Baseline
157
+ Error: ${errMsg}`,{cause:error})}}else{throw new Error('perfTest.use() requires either "server" (entry file path) or "baseUrl" option')}}}async function createTestFixtures(testName){await initializeSharedResources();if(!serverInstance){throw new Error("Server instance not initialized")}if(!tokenFactory){throw new Error("Token factory not initialized")}const clientInstance=await McpTestClient.create({baseUrl:serverInstance.info.baseUrl,transport:currentConfig.transport??"streamable-http",publicMode:currentConfig.publicMode}).buildAndConnect();const auth=createAuthFixture2(tokenFactory);const server=createServerFixture2(serverInstance);const perfImpl=createPerfFixtures(testName,currentConfig.project??"unknown");if(currentConfig.forceGcOnBaseline!==false){await perfImpl.baseline()}return{fixtures:{mcp:clientInstance,auth,server,perf:perfImpl},perfImpl}}async function cleanupTestFixtures2(fixtures,perfImpl,testFailed=false){const measurement=perfImpl.buildMeasurement();addGlobalMeasurement(measurement);if(testFailed&&serverInstance){const logs=serverInstance.getLogs();if(logs.length>0){console.error("\n[PerfTest] === Server Logs (test failed) ===");const recentLogs=logs.slice(-50);if(logs.length>50){console.error(`[PerfTest] (showing last 50 of ${logs.length} log entries)`)}console.error(recentLogs.join("\n"));console.error("[PerfTest] === End Server Logs ===\n")}}if(fixtures.mcp.isConnected()){await fixtures.mcp.disconnect()}}async function cleanupSharedResources(){if(serverInstance&&serverStartedByUs){await serverInstance.stop()}serverInstance=null;tokenFactory=null;serverStartedByUs=false}function createAuthFixture2(factory){const users={admin:{sub:"admin-001",scopes:["admin:*","read","write","delete"],email:"admin@test.local",name:"Test Admin"},user:{sub:"user-001",scopes:["read","write"],email:"user@test.local",name:"Test User"},readOnly:{sub:"readonly-001",scopes:["read"],email:"readonly@test.local",name:"Read Only User"}};return{createToken:opts=>factory.createTestToken({sub:opts.sub,scopes:opts.scopes,claims:{email:opts.email,name:opts.name,...opts.claims},exp:opts.expiresIn}),createExpiredToken:opts=>factory.createExpiredToken(opts),createInvalidToken:opts=>factory.createTokenWithInvalidSignature(opts),users:{admin:users["admin"],user:users["user"],readOnly:users["readOnly"]},getJwks:()=>factory.getPublicJwks(),getIssuer:()=>factory.getIssuer(),getAudience:()=>factory.getAudience()}}function createServerFixture2(server){return{info:server.info,createClient:async opts=>{return McpTestClient.create({baseUrl:server.info.baseUrl,transport:opts?.transport??"streamable-http",auth:opts?.token?{token:opts.token}:void 0,clientInfo:opts?.clientInfo,publicMode:currentConfig.publicMode}).buildAndConnect()},createClientBuilder:()=>{return new McpTestClientBuilder({baseUrl:server.info.baseUrl,publicMode:currentConfig.publicMode})},restart:()=>server.restart(),getLogs:()=>server.getLogs(),clearLogs:()=>server.clearLogs()}}function resolveServerCommand2(server){if(server.includes(" ")){return server}return`npx tsx ${server}`}function perfTestWithFixtures(name,fn){it(name,async()=>{const{fixtures,perfImpl}=await createTestFixtures(name);let testFailed=false;try{await fn(fixtures)}catch(error){testFailed=true;throw error}finally{await cleanupTestFixtures2(fixtures,perfImpl,testFailed)}})}function use2(config){currentConfig={...currentConfig,...config};afterAll(async()=>{await cleanupSharedResources()})}function skip2(name,fn){it.skip(name,async()=>{const{fixtures,perfImpl}=await createTestFixtures(name);let testFailed=false;try{await fn(fixtures)}catch(error){testFailed=true;throw error}finally{await cleanupTestFixtures2(fixtures,perfImpl,testFailed)}})}function only2(name,fn){it.only(name,async()=>{const{fixtures,perfImpl}=await createTestFixtures(name);let testFailed=false;try{await fn(fixtures)}catch(error){testFailed=true;throw error}finally{await cleanupTestFixtures2(fixtures,perfImpl,testFailed)}})}function todo2(name){it.todo(name)}var perfTest=perfTestWithFixtures;perfTest.use=use2;for(const hook of["describe","beforeAll","beforeEach","afterEach","afterAll"]){Object.defineProperty(perfTest,hook,{get:()=>globalThis[hook],enumerable:true,configurable:true})}perfTest.skip=skip2;perfTest.only=only2;perfTest.todo=todo2;var import_utils5=require("@frontmcp/utils");var BASELINE_START_MARKER="<!-- PERF_BASELINE_START -->";var BASELINE_END_MARKER="<!-- PERF_BASELINE_END -->";var DEFAULT_BASELINE_PATH="perf-results/baseline.json";var BaselineStore=class{baseline=null;baselinePath;constructor(baselinePath=DEFAULT_BASELINE_PATH){this.baselinePath=baselinePath}async load(){try{if(!await(0,import_utils5.fileExists)(this.baselinePath)){return null}const content=await(0,import_utils5.readFile)(this.baselinePath);this.baseline=JSON.parse(content);return this.baseline}catch{return null}}async save(baseline){const dir=this.baselinePath.substring(0,this.baselinePath.lastIndexOf("/"));await(0,import_utils5.ensureDir)(dir);await(0,import_utils5.writeFile)(this.baselinePath,JSON.stringify(baseline,null,2));this.baseline=baseline}getTestBaseline(testId){if(!this.baseline){return null}return this.baseline.tests[testId]??null}isLoaded(){return this.baseline!==null}getBaseline(){return this.baseline}static createFromMeasurements(measurements,release,commitHash){const tests={};const grouped=new Map;for(const m of measurements){const key=`${m.project}::${m.name}`;if(!grouped.has(key)){grouped.set(key,[])}const group=grouped.get(key);if(group){group.push(m)}}for(const[key,testMeasurements]of grouped){const[project]=key.split("::");const heapSamples=testMeasurements.map(m=>m.final?.memory.heapUsed??0);const durationSamples=testMeasurements.map(m=>m.timing.durationMs);const cpuSamples=testMeasurements.map(m=>m.final?.cpu.total??0);tests[key]={testId:key,project,heapUsed:calculateMetricBaseline(heapSamples),durationMs:calculateMetricBaseline(durationSamples),cpuTime:calculateMetricBaseline(cpuSamples),createdAt:new Date().toISOString(),commitHash}}return{release,timestamp:new Date().toISOString(),commitHash,tests}}};function parseBaselineFromComment(commentBody){const startIdx=commentBody.indexOf(BASELINE_START_MARKER);const endIdx=commentBody.indexOf(BASELINE_END_MARKER);if(startIdx===-1||endIdx===-1||startIdx>=endIdx){return null}const content=commentBody.substring(startIdx+BASELINE_START_MARKER.length,endIdx);const jsonMatch=content.match(/```json\s*([\s\S]*?)\s*```/);if(!jsonMatch){return null}try{return JSON.parse(jsonMatch[1])}catch{return null}}function formatBaselineAsComment(baseline){const json=JSON.stringify(baseline,null,2);return`## Performance Baseline
158
158
 
159
159
  This comment contains the performance baseline for release ${baseline.release}.
160
160
 
@@ -166,4 +166,4 @@ ${BASELINE_END_MARKER}
166
166
 
167
167
  Generated at: ${baseline.timestamp}
168
168
  ${baseline.commitHash?`Commit: ${baseline.commitHash}`:""}
169
- `}function calculateMetricBaseline(samples){if(samples.length===0){return{mean:0,stdDev:0,min:0,max:0,p95:0,sampleCount:0}}const sorted=[...samples].sort((a,b)=>a-b);const n=sorted.length;const sum=sorted.reduce((a,b)=>a+b,0);const mean=sum/n;const squaredDiffs=sorted.map(x=>Math.pow(x-mean,2));const variance=squaredDiffs.reduce((a,b)=>a+b,0)/n;const stdDev=Math.sqrt(variance);const min=sorted[0];const max=sorted[n-1];const p95Index=Math.ceil(n*.95)-1;const p95=sorted[Math.min(p95Index,n-1)];return{mean,stdDev,min,max,p95,sampleCount:n}}var globalBaselineStore=null;function getBaselineStore(baselinePath){if(!globalBaselineStore||baselinePath){globalBaselineStore=new BaselineStore(baselinePath)}return globalBaselineStore}var DEFAULT_CONFIG={warningThresholdPercent:10,errorThresholdPercent:25,minAbsoluteChange:1024};var RegressionDetector=class{config;constructor(config){this.config={...DEFAULT_CONFIG,...config}}detectRegression(measurement,baseline){const testId=`${measurement.project}::${measurement.name}`;const metrics=[];const currentHeap=measurement.final?.memory.heapUsed??0;const heapRegression=this.checkMetric("heapUsed",baseline.heapUsed.mean,currentHeap,formatBytes);metrics.push(heapRegression);const durationRegression=this.checkMetric("durationMs",baseline.durationMs.mean,measurement.timing.durationMs,formatDuration);metrics.push(durationRegression);const currentCpu=measurement.final?.cpu.total??0;const cpuRegression=this.checkMetric("cpuTime",baseline.cpuTime.mean,currentCpu,formatMicroseconds);metrics.push(cpuRegression);const hasRegression=metrics.some(m=>m.status==="regression");const hasWarning=metrics.some(m=>m.status==="warning");const status=hasRegression?"regression":hasWarning?"warning":"ok";const message=this.buildMessage(testId,metrics,status);return{testId,status,metrics,message}}detectRegressions(measurements,baselines){const results=[];for(const measurement of measurements){const testId=`${measurement.project}::${measurement.name}`;const baseline=baselines.tests[testId];if(baseline){results.push(this.detectRegression(measurement,baseline))}}return results}checkMetric(name,baseline,current,_formatter){const absoluteChange=current-baseline;const changePercent=baseline>0?absoluteChange/baseline*100:0;let status="ok";if(Math.abs(absoluteChange)>this.config.minAbsoluteChange){if(changePercent>=this.config.errorThresholdPercent){status="regression"}else if(changePercent>=this.config.warningThresholdPercent){status="warning"}}return{metric:name,baseline,current,changePercent,absoluteChange,status}}buildMessage(testId,metrics,status){if(status==="ok"){return`${testId}: All metrics within acceptable range`}const issues=metrics.filter(m=>m.status!=="ok").map(m=>{const direction=m.absoluteChange>0?"+":"";return`${m.metric}: ${direction}${m.changePercent.toFixed(1)}%`});const statusText=status==="regression"?"REGRESSION":"WARNING";return`${testId}: ${statusText} - ${issues.join(", ")}`}};function summarizeRegressions(results){const total=results.length;const ok=results.filter(r=>r.status==="ok").length;const warnings=results.filter(r=>r.status==="warning").length;const regressions=results.filter(r=>r.status==="regression").length;let summary;if(regressions>0){summary=`${regressions} regression(s) detected out of ${total} tests`}else if(warnings>0){summary=`${warnings} warning(s) detected out of ${total} tests`}else{summary=`All ${total} tests within acceptable range`}return{total,ok,warnings,regressions,summary}}var globalDetector=null;function getRegressionDetector(config){if(!globalDetector||config){globalDetector=new RegressionDetector(config)}return globalDetector}var import_utils5=require("@frontmcp/utils");var ReportGenerator=class{detector;constructor(){this.detector=new RegressionDetector}generateReport(measurements,baseline,gitInfo){const projectGroups=new Map;for(const m of measurements){if(!projectGroups.has(m.project)){projectGroups.set(m.project,[])}const group=projectGroups.get(m.project);if(group){group.push(m)}}const projects=[];for(const[projectName,projectMeasurements]of projectGroups){const summary=this.calculateSummary(projectMeasurements);const regressions=baseline?this.detector.detectRegressions(projectMeasurements,baseline):void 0;projects.push({project:projectName,summary,measurements:projectMeasurements,regressions})}const overallSummary=this.calculateSummary(measurements);return{timestamp:new Date().toISOString(),commitHash:gitInfo?.commitHash,branch:gitInfo?.branch,summary:overallSummary,projects,baseline:baseline?{release:baseline.release,timestamp:baseline.timestamp}:void 0}}generateMarkdownReport(report){const lines=[];lines.push("## Performance Test Results");lines.push("");const statusEmoji=this.getStatusEmoji(report.summary);lines.push(`**Status:** ${statusEmoji} ${this.getSummaryText(report.summary)}`);lines.push("");lines.push("### Summary");lines.push("");lines.push("| Metric | Value |");lines.push("|--------|-------|");lines.push(`| Total Tests | ${report.summary.totalTests} |`);lines.push(`| Passed | ${report.summary.passedTests} |`);lines.push(`| Warnings | ${report.summary.warningTests} |`);lines.push(`| Failed | ${report.summary.failedTests} |`);lines.push(`| Memory Leaks | ${report.summary.leakTests} |`);lines.push("");if(report.baseline){lines.push(`**Baseline:** ${report.baseline.release} (${report.baseline.timestamp})`);lines.push("")}lines.push("### Project Breakdown");lines.push("");for(const project of report.projects){lines.push(`#### ${project.project}`);lines.push("");lines.push(this.generateProjectTable(project));lines.push("");const leakTests=project.measurements.filter(m=>m.leakDetectionResults&&m.leakDetectionResults.length>0);if(leakTests.length>0){const parallelTests=leakTests.filter(m=>m.leakDetectionResults?.some(r=>this.isParallelResult(r)));if(parallelTests.length>0){lines.push("**Parallel Stress Test Results:**");lines.push("");lines.push("| Test | Workers | Iterations | Duration | Total req/s |");lines.push("|------|---------|------------|----------|-------------|");for(const m of parallelTests){for(const result of m.leakDetectionResults||[]){if(this.isParallelResult(result)){const parallelResult=result;const durationStr=parallelResult.durationMs?`${(parallelResult.durationMs/1e3).toFixed(2)}s`:"N/A";lines.push(`| ${m.name} | ${parallelResult.workersUsed} | ${parallelResult.totalIterations} | ${durationStr} | ${parallelResult.totalRequestsPerSecond.toFixed(1)} |`)}}}lines.push("")}lines.push("**Memory Interval Analysis:**");lines.push("");for(const m of leakTests){for(const result of m.leakDetectionResults||[]){if(result.intervals&&result.intervals.length>0){lines.push(`*${m.name}:*`);lines.push("");if(this.isParallelResult(result)){const parallelResult=result;lines.push("| Worker | req/s | Iterations |");lines.push("|--------|-------|------------|");for(const worker of parallelResult.perWorkerStats){lines.push(`| ${worker.workerId} | ${worker.requestsPerSecond.toFixed(1)} | ${worker.iterationsCompleted} |`)}lines.push("")}lines.push("| Interval | Heap Start | Heap End | Delta | Rate/iter |");lines.push("|----------|------------|----------|-------|-----------|");for(const interval of result.intervals){lines.push(`| ${interval.startIteration}-${interval.endIteration} | ${formatBytes(interval.heapAtStart)} | ${formatBytes(interval.heapAtEnd)} | ${interval.deltaFormatted} | ${formatBytes(interval.growthRatePerIteration)}/iter |`)}lines.push("");const durationStr=result.durationMs?`${(result.durationMs/1e3).toFixed(2)}s`:"N/A";const rpsStr=result.requestsPerSecond?`${result.requestsPerSecond.toFixed(1)} req/s`:"N/A";lines.push(`Total: ${formatBytes(result.totalGrowth)}, R\xB2=${result.rSquared.toFixed(3)} | ${result.samples.length} iterations in ${durationStr} (${rpsStr})`);lines.push("")}}}}if(project.regressions&&project.regressions.length>0){const regressionsWithIssues=project.regressions.filter(r=>r.status!=="ok");if(regressionsWithIssues.length>0){lines.push("**Regressions:**");for(const r of regressionsWithIssues){const emoji=r.status==="regression"?"\u274C":"\u26A0\uFE0F";lines.push(`- ${emoji} ${r.message}`)}lines.push("")}}}lines.push("---");lines.push(`Generated at: ${report.timestamp}`);if(report.commitHash){lines.push(`Commit: \`${report.commitHash.substring(0,8)}\``)}if(report.branch){lines.push(`Branch: \`${report.branch}\``)}return lines.join("\n")}generateJsonReport(report){return JSON.stringify(report,null,2)}calculateSummary(measurements){let passedTests=0;let warningTests=0;let failedTests=0;let leakTests=0;for(const m of measurements){const hasError=m.issues.some(i=>i.severity==="error");const hasWarning=m.issues.some(i=>i.severity==="warning");const hasLeak=m.issues.some(i=>i.type==="memory-leak");if(hasLeak){leakTests++}if(hasError){failedTests++}else if(hasWarning){warningTests++}else{passedTests++}}return{totalTests:measurements.length,passedTests,warningTests,failedTests,leakTests}}getStatusEmoji(summary){if(summary.failedTests>0||summary.leakTests>0){return"X"}if(summary.warningTests>0){return"!"}return"OK"}getSummaryText(summary){if(summary.failedTests>0){return`${summary.failedTests} test(s) failed`}if(summary.leakTests>0){return`${summary.leakTests} memory leak(s) detected`}if(summary.warningTests>0){return`${summary.warningTests} warning(s)`}return"All tests passed"}generateProjectTable(project){const lines=[];lines.push("| Test | Duration | Heap Delta | CPU Time | Status |");lines.push("|------|----------|------------|----------|--------|");for(const m of project.measurements){const status=this.getTestStatus(m);const heapDelta=m.memoryDelta?formatBytes(m.memoryDelta.heapUsed):"N/A";const cpuTime=m.final?.cpu.total?formatMicroseconds(m.final.cpu.total):"N/A";lines.push(`| ${m.name} | ${formatDuration(m.timing.durationMs)} | ${heapDelta} | ${cpuTime} | ${status} |`)}return lines.join("\n")}getTestStatus(m){const hasError=m.issues.some(i=>i.severity==="error");const hasLeak=m.issues.some(i=>i.type==="memory-leak");const hasWarning=m.issues.some(i=>i.severity==="warning");if(hasLeak){return"LEAK"}if(hasError){return"FAIL"}if(hasWarning){return"WARN"}return"OK"}isParallelResult(result){return typeof result==="object"&&result!==null&&"workersUsed"in result&&"perWorkerStats"in result&&"totalRequestsPerSecond"in result}};async function saveReports(measurements,outputDir,baseline,gitInfo){await(0,import_utils5.ensureDir)(outputDir);const generator=new ReportGenerator;const report=generator.generateReport(measurements,baseline,gitInfo);const jsonPath=`${outputDir}/report.json`;const markdownPath=`${outputDir}/report.md`;await(0,import_utils5.writeFile)(jsonPath,generator.generateJsonReport(report));await(0,import_utils5.writeFile)(markdownPath,generator.generateMarkdownReport(report));return{jsonPath,markdownPath}}function createReportGenerator(){return new ReportGenerator}var import_client=require("@frontmcp/protocol/client");var import_client2=require("@frontmcp/protocol/client");var import_node_net=__toESM(require("node:net"));function isTcpPortListening(port,host="127.0.0.1",timeoutMs=1500){return new Promise(resolve=>{const socket=import_node_net.default.connect({port,host});socket.unref();socket.setTimeout(timeoutMs);let settled=false;const finish=listening=>{if(settled)return;settled=true;socket.destroy();resolve(listening)};socket.once("connect",()=>finish(true));socket.once("error",()=>finish(false));socket.once("timeout",()=>finish(false))})}0&&(module.exports={AssertionError,AuthHeaders,BASIC_UI_TOOL_CONFIG,BaselineStore,ConnectionError,DefaultInterceptorChain,DefaultMockRegistry,EXPECTED_EXTAPPS_TOOLS_LIST_META_KEYS,EXPECTED_EXTAPPS_TOOL_CALL_META_KEYS,EXPECTED_GENERIC_TOOLS_LIST_META_KEYS,EXPECTED_GENERIC_TOOL_CALL_META_KEYS,EXPECTED_OPENAI_TOOLS_LIST_META_KEYS,EXPECTED_OPENAI_TOOL_CALL_META_KEYS,FULL_UI_TOOL_CONFIG,LeakDetector,McpAssertions,McpClient,McpProtocolError,McpStdioClientTransport,McpTestClient,McpTestClientBuilder,MetricsCollector,MockAPIServer,MockCimdServer,MockOAuthServer,PLATFORM_DETECTION_PATTERNS,PerfFixturesImpl,RegressionDetector,ReportGenerator,ServerStartError,SseTransport,StreamableHttpTransport,TestClientError,TestServer,TestTokenFactory,TestUsers,TimeoutError,UIAssertions,assertNoLeak,basicUIToolInputSchema,basicUIToolOutputSchema,buildUserAgent,clearGlobalMeasurements,containsPrompt,containsResource,containsResourceTemplate,containsTool,createLeakDetector,createPerfFixtures,createReportGenerator,createTestUser,expect,forceFullGc,forceGc,formatBaselineAsComment,formatBytes,formatDuration,formatMicroseconds,fullUIToolInputSchema,fullUIToolOutputSchema,generateBasicUIToolOutput,generateFullUIToolOutput,getBaselineStore,getForbiddenMetaPrefixes,getGlobalMeasurements,getPlatformClientInfo,getPlatformMetaNamespace,getPlatformMimeType,getPlatformUserAgent,getRegressionDetector,getToolCallMetaPrefixes,getToolsListMetaPrefixes,hasMimeType,hasTextContent,httpMock,httpResponse,interceptors,isError,isExtAppsPlatform,isGcAvailable,isOpenAIPlatform,isSuccessful,isTcpPortListening,isUiPlatform,mcpMatchers,mockResponse,parseBaselineFromComment,perfTest,saveReports,summarizeRegressions,test,uiMatchers});
169
+ `}function calculateMetricBaseline(samples){if(samples.length===0){return{mean:0,stdDev:0,min:0,max:0,p95:0,sampleCount:0}}const sorted=[...samples].sort((a,b)=>a-b);const n=sorted.length;const sum=sorted.reduce((a,b)=>a+b,0);const mean=sum/n;const squaredDiffs=sorted.map(x=>Math.pow(x-mean,2));const variance=squaredDiffs.reduce((a,b)=>a+b,0)/n;const stdDev=Math.sqrt(variance);const min=sorted[0];const max=sorted[n-1];const p95Index=Math.ceil(n*.95)-1;const p95=sorted[Math.min(p95Index,n-1)];return{mean,stdDev,min,max,p95,sampleCount:n}}var globalBaselineStore=null;function getBaselineStore(baselinePath){if(!globalBaselineStore||baselinePath){globalBaselineStore=new BaselineStore(baselinePath)}return globalBaselineStore}var DEFAULT_CONFIG={warningThresholdPercent:10,errorThresholdPercent:25,minAbsoluteChange:1024};var RegressionDetector=class{config;constructor(config){this.config={...DEFAULT_CONFIG,...config}}detectRegression(measurement,baseline){const testId=`${measurement.project}::${measurement.name}`;const metrics=[];const currentHeap=measurement.final?.memory.heapUsed??0;const heapRegression=this.checkMetric("heapUsed",baseline.heapUsed.mean,currentHeap,formatBytes);metrics.push(heapRegression);const durationRegression=this.checkMetric("durationMs",baseline.durationMs.mean,measurement.timing.durationMs,formatDuration);metrics.push(durationRegression);const currentCpu=measurement.final?.cpu.total??0;const cpuRegression=this.checkMetric("cpuTime",baseline.cpuTime.mean,currentCpu,formatMicroseconds);metrics.push(cpuRegression);const hasRegression=metrics.some(m=>m.status==="regression");const hasWarning=metrics.some(m=>m.status==="warning");const status=hasRegression?"regression":hasWarning?"warning":"ok";const message=this.buildMessage(testId,metrics,status);return{testId,status,metrics,message}}detectRegressions(measurements,baselines){const results=[];for(const measurement of measurements){const testId=`${measurement.project}::${measurement.name}`;const baseline=baselines.tests[testId];if(baseline){results.push(this.detectRegression(measurement,baseline))}}return results}checkMetric(name,baseline,current,_formatter){const absoluteChange=current-baseline;const changePercent=baseline>0?absoluteChange/baseline*100:0;let status="ok";if(Math.abs(absoluteChange)>this.config.minAbsoluteChange){if(changePercent>=this.config.errorThresholdPercent){status="regression"}else if(changePercent>=this.config.warningThresholdPercent){status="warning"}}return{metric:name,baseline,current,changePercent,absoluteChange,status}}buildMessage(testId,metrics,status){if(status==="ok"){return`${testId}: All metrics within acceptable range`}const issues=metrics.filter(m=>m.status!=="ok").map(m=>{const direction=m.absoluteChange>0?"+":"";return`${m.metric}: ${direction}${m.changePercent.toFixed(1)}%`});const statusText=status==="regression"?"REGRESSION":"WARNING";return`${testId}: ${statusText} - ${issues.join(", ")}`}};function summarizeRegressions(results){const total=results.length;const ok=results.filter(r=>r.status==="ok").length;const warnings=results.filter(r=>r.status==="warning").length;const regressions=results.filter(r=>r.status==="regression").length;let summary;if(regressions>0){summary=`${regressions} regression(s) detected out of ${total} tests`}else if(warnings>0){summary=`${warnings} warning(s) detected out of ${total} tests`}else{summary=`All ${total} tests within acceptable range`}return{total,ok,warnings,regressions,summary}}var globalDetector=null;function getRegressionDetector(config){if(!globalDetector||config){globalDetector=new RegressionDetector(config)}return globalDetector}var import_utils6=require("@frontmcp/utils");var ReportGenerator=class{detector;constructor(){this.detector=new RegressionDetector}generateReport(measurements,baseline,gitInfo){const projectGroups=new Map;for(const m of measurements){if(!projectGroups.has(m.project)){projectGroups.set(m.project,[])}const group=projectGroups.get(m.project);if(group){group.push(m)}}const projects=[];for(const[projectName,projectMeasurements]of projectGroups){const summary=this.calculateSummary(projectMeasurements);const regressions=baseline?this.detector.detectRegressions(projectMeasurements,baseline):void 0;projects.push({project:projectName,summary,measurements:projectMeasurements,regressions})}const overallSummary=this.calculateSummary(measurements);return{timestamp:new Date().toISOString(),commitHash:gitInfo?.commitHash,branch:gitInfo?.branch,summary:overallSummary,projects,baseline:baseline?{release:baseline.release,timestamp:baseline.timestamp}:void 0}}generateMarkdownReport(report){const lines=[];lines.push("## Performance Test Results");lines.push("");const statusEmoji=this.getStatusEmoji(report.summary);lines.push(`**Status:** ${statusEmoji} ${this.getSummaryText(report.summary)}`);lines.push("");lines.push("### Summary");lines.push("");lines.push("| Metric | Value |");lines.push("|--------|-------|");lines.push(`| Total Tests | ${report.summary.totalTests} |`);lines.push(`| Passed | ${report.summary.passedTests} |`);lines.push(`| Warnings | ${report.summary.warningTests} |`);lines.push(`| Failed | ${report.summary.failedTests} |`);lines.push(`| Memory Leaks | ${report.summary.leakTests} |`);lines.push("");if(report.baseline){lines.push(`**Baseline:** ${report.baseline.release} (${report.baseline.timestamp})`);lines.push("")}lines.push("### Project Breakdown");lines.push("");for(const project of report.projects){lines.push(`#### ${project.project}`);lines.push("");lines.push(this.generateProjectTable(project));lines.push("");const leakTests=project.measurements.filter(m=>m.leakDetectionResults&&m.leakDetectionResults.length>0);if(leakTests.length>0){const parallelTests=leakTests.filter(m=>m.leakDetectionResults?.some(r=>this.isParallelResult(r)));if(parallelTests.length>0){lines.push("**Parallel Stress Test Results:**");lines.push("");lines.push("| Test | Workers | Iterations | Duration | Total req/s |");lines.push("|------|---------|------------|----------|-------------|");for(const m of parallelTests){for(const result of m.leakDetectionResults||[]){if(this.isParallelResult(result)){const parallelResult=result;const durationStr=parallelResult.durationMs?`${(parallelResult.durationMs/1e3).toFixed(2)}s`:"N/A";lines.push(`| ${m.name} | ${parallelResult.workersUsed} | ${parallelResult.totalIterations} | ${durationStr} | ${parallelResult.totalRequestsPerSecond.toFixed(1)} |`)}}}lines.push("")}lines.push("**Memory Interval Analysis:**");lines.push("");for(const m of leakTests){for(const result of m.leakDetectionResults||[]){if(result.intervals&&result.intervals.length>0){lines.push(`*${m.name}:*`);lines.push("");if(this.isParallelResult(result)){const parallelResult=result;lines.push("| Worker | req/s | Iterations |");lines.push("|--------|-------|------------|");for(const worker of parallelResult.perWorkerStats){lines.push(`| ${worker.workerId} | ${worker.requestsPerSecond.toFixed(1)} | ${worker.iterationsCompleted} |`)}lines.push("")}lines.push("| Interval | Heap Start | Heap End | Delta | Rate/iter |");lines.push("|----------|------------|----------|-------|-----------|");for(const interval of result.intervals){lines.push(`| ${interval.startIteration}-${interval.endIteration} | ${formatBytes(interval.heapAtStart)} | ${formatBytes(interval.heapAtEnd)} | ${interval.deltaFormatted} | ${formatBytes(interval.growthRatePerIteration)}/iter |`)}lines.push("");const durationStr=result.durationMs?`${(result.durationMs/1e3).toFixed(2)}s`:"N/A";const rpsStr=result.requestsPerSecond?`${result.requestsPerSecond.toFixed(1)} req/s`:"N/A";lines.push(`Total: ${formatBytes(result.totalGrowth)}, R\xB2=${result.rSquared.toFixed(3)} | ${result.samples.length} iterations in ${durationStr} (${rpsStr})`);lines.push("")}}}}if(project.regressions&&project.regressions.length>0){const regressionsWithIssues=project.regressions.filter(r=>r.status!=="ok");if(regressionsWithIssues.length>0){lines.push("**Regressions:**");for(const r of regressionsWithIssues){const emoji=r.status==="regression"?"\u274C":"\u26A0\uFE0F";lines.push(`- ${emoji} ${r.message}`)}lines.push("")}}}lines.push("---");lines.push(`Generated at: ${report.timestamp}`);if(report.commitHash){lines.push(`Commit: \`${report.commitHash.substring(0,8)}\``)}if(report.branch){lines.push(`Branch: \`${report.branch}\``)}return lines.join("\n")}generateJsonReport(report){return JSON.stringify(report,null,2)}calculateSummary(measurements){let passedTests=0;let warningTests=0;let failedTests=0;let leakTests=0;for(const m of measurements){const hasError=m.issues.some(i=>i.severity==="error");const hasWarning=m.issues.some(i=>i.severity==="warning");const hasLeak=m.issues.some(i=>i.type==="memory-leak");if(hasLeak){leakTests++}if(hasError){failedTests++}else if(hasWarning){warningTests++}else{passedTests++}}return{totalTests:measurements.length,passedTests,warningTests,failedTests,leakTests}}getStatusEmoji(summary){if(summary.failedTests>0||summary.leakTests>0){return"X"}if(summary.warningTests>0){return"!"}return"OK"}getSummaryText(summary){if(summary.failedTests>0){return`${summary.failedTests} test(s) failed`}if(summary.leakTests>0){return`${summary.leakTests} memory leak(s) detected`}if(summary.warningTests>0){return`${summary.warningTests} warning(s)`}return"All tests passed"}generateProjectTable(project){const lines=[];lines.push("| Test | Duration | Heap Delta | CPU Time | Status |");lines.push("|------|----------|------------|----------|--------|");for(const m of project.measurements){const status=this.getTestStatus(m);const heapDelta=m.memoryDelta?formatBytes(m.memoryDelta.heapUsed):"N/A";const cpuTime=m.final?.cpu.total?formatMicroseconds(m.final.cpu.total):"N/A";lines.push(`| ${m.name} | ${formatDuration(m.timing.durationMs)} | ${heapDelta} | ${cpuTime} | ${status} |`)}return lines.join("\n")}getTestStatus(m){const hasError=m.issues.some(i=>i.severity==="error");const hasLeak=m.issues.some(i=>i.type==="memory-leak");const hasWarning=m.issues.some(i=>i.severity==="warning");if(hasLeak){return"LEAK"}if(hasError){return"FAIL"}if(hasWarning){return"WARN"}return"OK"}isParallelResult(result){return typeof result==="object"&&result!==null&&"workersUsed"in result&&"perWorkerStats"in result&&"totalRequestsPerSecond"in result}};async function saveReports(measurements,outputDir,baseline,gitInfo){await(0,import_utils6.ensureDir)(outputDir);const generator=new ReportGenerator;const report=generator.generateReport(measurements,baseline,gitInfo);const jsonPath=`${outputDir}/report.json`;const markdownPath=`${outputDir}/report.md`;await(0,import_utils6.writeFile)(jsonPath,generator.generateJsonReport(report));await(0,import_utils6.writeFile)(markdownPath,generator.generateMarkdownReport(report));return{jsonPath,markdownPath}}function createReportGenerator(){return new ReportGenerator}var import_client=require("@frontmcp/protocol/client");var import_client2=require("@frontmcp/protocol/client");var import_node_net=__toESM(require("node:net"));function isTcpPortListening(port,host="127.0.0.1",timeoutMs=1500){return new Promise(resolve=>{const socket=import_node_net.default.connect({port,host});socket.unref();socket.setTimeout(timeoutMs);let settled=false;const finish=listening=>{if(settled)return;settled=true;socket.destroy();resolve(listening)};socket.once("connect",()=>finish(true));socket.once("error",()=>finish(false));socket.once("timeout",()=>finish(false))})}0&&(module.exports={AssertionError,AuthHeaders,BASIC_UI_TOOL_CONFIG,BaselineStore,ConnectionError,DefaultInterceptorChain,DefaultMockRegistry,EXPECTED_EXTAPPS_TOOLS_LIST_META_KEYS,EXPECTED_EXTAPPS_TOOL_CALL_META_KEYS,EXPECTED_GENERIC_TOOLS_LIST_META_KEYS,EXPECTED_GENERIC_TOOL_CALL_META_KEYS,EXPECTED_OPENAI_TOOLS_LIST_META_KEYS,EXPECTED_OPENAI_TOOL_CALL_META_KEYS,FULL_UI_TOOL_CONFIG,LeakDetector,McpAssertions,McpClient,McpProtocolError,McpStdioClientTransport,McpTestClient,McpTestClientBuilder,MetricsCollector,MockAPIServer,MockCimdServer,MockOAuthServer,PLATFORM_DETECTION_PATTERNS,PerfFixturesImpl,RegressionDetector,ReportGenerator,ServerStartError,SseTransport,StreamableHttpTransport,TestClientError,TestServer,TestTokenFactory,TestUsers,TimeoutError,UIAssertions,assertNoLeak,basicUIToolInputSchema,basicUIToolOutputSchema,buildUserAgent,clearGlobalMeasurements,containsPrompt,containsResource,containsResourceTemplate,containsTool,createLeakDetector,createPerfFixtures,createReportGenerator,createTestUser,expect,forceFullGc,forceGc,formatBaselineAsComment,formatBytes,formatDuration,formatMicroseconds,fullUIToolInputSchema,fullUIToolOutputSchema,generateBasicUIToolOutput,generateFullUIToolOutput,getBaselineStore,getForbiddenMetaPrefixes,getGlobalMeasurements,getPlatformClientInfo,getPlatformMetaNamespace,getPlatformMimeType,getPlatformUserAgent,getRegressionDetector,getToolCallMetaPrefixes,getToolsListMetaPrefixes,hasMimeType,hasTextContent,httpMock,httpResponse,interceptors,isError,isExtAppsPlatform,isGcAvailable,isOpenAIPlatform,isSuccessful,isTcpPortListening,isUiPlatform,mcpMatchers,mockResponse,parseBaselineFromComment,perfTest,saveReports,summarizeRegressions,test,uiMatchers});
@@ -0,0 +1,11 @@
1
+ /** Jest preset for unit tests, CommonJS projects included; the @frontmcp/testing README shows how to use it. */
2
+
3
+ const base = require('./jest-preset.base.js');
4
+
5
+ module.exports = {
6
+ ...base,
7
+
8
+ testMatch: ['<rootDir>/**/*.spec.ts', '<rootDir>/**/*.spec.tsx'],
9
+
10
+ testPathIgnorePatterns: [...base.testPathIgnorePatterns, '\\.(e2e|perf|pw)\\.spec\\.tsx?$'],
11
+ };
@@ -0,0 +1,35 @@
1
+ /** What both @frontmcp/testing Jest presets share, so they cannot drift. */
2
+
3
+ module.exports = {
4
+ testEnvironment: 'node',
5
+
6
+ // Covers `.js` too: ESM-only deps ship `.js`, and un-ignoring them in `transformIgnorePatterns` needs a matching transform.
7
+ transform: {
8
+ '^.+\\.[tj]sx?$': [
9
+ // The transform `frontmcp test` injects, resolved from here because @frontmcp/testing installs it
10
+ require.resolve('@swc/jest'),
11
+ {
12
+ jsc: {
13
+ target: 'es2022',
14
+ parser: { syntax: 'typescript', tsx: true, decorators: true, dynamicImport: true },
15
+ transform: { decoratorMetadata: true, legacyDecorator: true, react: { runtime: 'automatic' } },
16
+ keepClassNames: true,
17
+ externalHelpers: false,
18
+ loose: true,
19
+ },
20
+ module: { type: 'es6' },
21
+ sourceMaps: true,
22
+ swcrc: false,
23
+ },
24
+ ],
25
+ },
26
+
27
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
28
+
29
+ // The SDK's ESM-only runtime deps: `jose` (auth) and `@noble/*` (`@frontmcp/utils` crypto). The `.pnpm` skip keeps this correct under pnpm (issue #519).
30
+ transformIgnorePatterns: [
31
+ 'node_modules[/\\\\](?!\\.pnpm[/\\\\])(?!(jose|@noble[/\\\\]hashes|@noble[/\\\\]ciphers)[/\\\\])',
32
+ ],
33
+
34
+ testPathIgnorePatterns: ['/node_modules/', '/dist/'],
35
+ };
package/jest-preset.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * @description Jest preset for @frontmcp/testing
4
4
  *
5
5
  * Mirrors the config `frontmcp test` injects (libs/cli/src/commands/dev/test.ts); a CLI test fails when they differ.
6
+ * For unit tests in a CommonJS project, use `@frontmcp/testing/jest-preset-unit`.
6
7
  *
7
8
  * Usage in jest.config.ts:
8
9
  * ```typescript
@@ -13,33 +14,10 @@
13
14
  * ```
14
15
  */
15
16
 
16
- module.exports = {
17
- // Use Node.js environment for E2E tests
18
- testEnvironment: 'node',
19
-
20
- // Covers `.js` too: ESM-only deps ship `.js`, and un-ignoring them in `transformIgnorePatterns` needs a matching transform.
21
- transform: {
22
- '^.+\\.[tj]sx?$': [
23
- // The transform `frontmcp test` injects, resolved from here because @frontmcp/testing installs it
24
- require.resolve('@swc/jest'),
25
- {
26
- jsc: {
27
- target: 'es2022',
28
- parser: { syntax: 'typescript', tsx: true, decorators: true, dynamicImport: true },
29
- transform: { decoratorMetadata: true, legacyDecorator: true, react: { runtime: 'automatic' } },
30
- keepClassNames: true,
31
- externalHelpers: false,
32
- loose: true,
33
- },
34
- module: { type: 'es6' },
35
- sourceMaps: true,
36
- swcrc: false,
37
- },
38
- ],
39
- },
17
+ const base = require('./jest-preset.base.js');
40
18
 
41
- // File extensions to consider
42
- moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
19
+ module.exports = {
20
+ ...base,
43
21
 
44
22
  testTimeout: 60000,
45
23
 
@@ -56,14 +34,6 @@ module.exports = {
56
34
  '<rootDir>/e2e/**/*.e2e.spec.tsx',
57
35
  ],
58
36
 
59
- // Transpile ESM-only deps (`@noble/*` for CodeCall); the `.pnpm` skip keeps this correct under pnpm (issue #519).
60
- transformIgnorePatterns: [
61
- 'node_modules[/\\\\](?!\\.pnpm[/\\\\])(?!(jose|@noble[/\\\\]hashes|@noble[/\\\\]ciphers)[/\\\\])',
62
- ],
63
-
64
- // Ignore patterns
65
- testPathIgnorePatterns: ['/node_modules/', '/dist/'],
66
-
67
37
  // Coverage settings (optional, disabled by default for E2E)
68
38
  collectCoverage: false,
69
39
 
package/matchers/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:true})};var __copyProps=(to,from,except,desc)=>{if(from&&typeof from==="object"||typeof from==="function"){for(let key of __getOwnPropNames(from))if(!__hasOwnProp.call(to,key)&&key!==except)__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable})}return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:true}),mod);var matchers_exports={};__export(matchers_exports,{mcpMatchers:()=>mcpMatchers});module.exports=__toCommonJS(matchers_exports);function getPlatformMimeType(platform){return"text/html;profile=mcp-app"}function getToolCallMetaPrefixes(platform){return["ui/"]}function getForbiddenMetaPrefixes(platform){return["openai/","frontmcp/"]}function escapeRegex(str){return str.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function extractUiHtml(received){if(typeof received==="string"){return received}const wrapper=received;const meta=wrapper?.raw?._meta||wrapper?._meta;if(meta&&typeof meta==="object"){const uiHtml=meta["ui/html"];if(typeof uiHtml==="string"){return uiHtml}}return void 0}function extractMeta(received){const wrapper=received;const meta=wrapper?.raw?._meta||wrapper?._meta;if(meta&&typeof meta==="object"){return meta}return void 0}var toHaveRenderedHtml=function(received){const html=extractUiHtml(received);const hasHtml=html!==void 0&&html.length>0;const isFallback=hasHtml&&html.includes("mdx-fallback");const pass=hasHtml&&!isFallback;return{pass,message:()=>{if(isFallback){return"Expected rendered HTML but got mdx-fallback (raw escaped content). MDX rendering may have failed."}if(!hasHtml){return"Expected _meta to have ui/html property with rendered HTML"}return"Expected result not to have rendered HTML"}}};var toContainHtmlElement=function(received,tag){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected to find <${tag}> element, but no HTML content found`}}const regex=new RegExp(`<${escapeRegex(tag)}[\\s>]`,"i");const pass=regex.test(html);return{pass,message:()=>pass?`Expected HTML not to contain <${tag}> element`:`Expected HTML to contain <${tag}> element`}};var toContainBoundValue=function(received,value){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected HTML to contain bound value "${value}", but no HTML content found`}}const stringValue=String(value);const pass=html.includes(stringValue);return{pass,message:()=>pass?`Expected HTML not to contain bound value "${stringValue}"`:`Expected HTML to contain bound value "${stringValue}"`}};var toBeXssSafe=function(received){const html=extractUiHtml(received);if(!html){return{pass:true,message:()=>"Expected HTML to be XSS unsafe (no HTML found)"}}const hasScript=/<script[\s>]/i.test(html);const hasOnHandler=/\son\w+\s*=/i.test(html);const hasJavascriptUri=/javascript:/i.test(html);const issues=[];if(hasScript)issues.push("<script> tag");if(hasOnHandler)issues.push("inline event handler (onclick, etc.)");if(hasJavascriptUri)issues.push("javascript: URI");const pass=!hasScript&&!hasOnHandler&&!hasJavascriptUri;return{pass,message:()=>pass?"Expected HTML not to be XSS safe":`Expected HTML to be XSS safe, but found: ${issues.join(", ")}`}};var toHaveWidgetMetadata=function(received){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>"Expected _meta to have widget metadata, but no _meta found"}}const hasUiHtml=Boolean(meta["ui/html"]);const hasMimeType=Boolean(meta["ui/mimeType"]);const hasUiObject=Boolean(meta["ui"]&&typeof meta["ui"]==="object");const pass=hasUiHtml||hasMimeType||hasUiObject;return{pass,message:()=>pass?"Expected result not to have widget metadata":"Expected _meta to have widget metadata (ui/html, ui/mimeType, or ui object)"}};var toHaveCssClass=function(received,className){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected HTML to have CSS class "${className}", but no HTML content found`}}const classRegex=new RegExp(`class(?:Name)?\\s*=\\s*["'][^"']*\\b${escapeRegex(className)}\\b[^"']*["']`,"i");const pass=classRegex.test(html);return{pass,message:()=>pass?`Expected HTML not to have CSS class "${className}"`:`Expected HTML to have CSS class "${className}"`}};var toNotContainRawContent=function(received,content){const html=extractUiHtml(received);if(!html){return{pass:true,message:()=>`Expected HTML to contain raw content "${content}", but no HTML found`}}const pass=!html.includes(content);return{pass,message:()=>pass?`Expected HTML to contain raw content "${content}"`:`Expected HTML not to contain raw content "${content}" (may indicate rendering failure)`}};var toHaveProperHtmlStructure=function(received){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>"Expected proper HTML structure, but no HTML content found"}}const hasEscapedTags=html.includes("&lt;")&&html.includes("&gt;");const hasHtmlTags=/<[a-z]/i.test(html);const pass=hasHtmlTags&&!hasEscapedTags;return{pass,message:()=>{if(hasEscapedTags){return"Expected proper HTML structure, but found escaped HTML entities - content may not have been rendered"}if(!hasHtmlTags){return"Expected proper HTML structure, but found no HTML tags"}return"Expected result not to have proper HTML structure"}}};var toHavePlatformMeta=function(received,platform){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have platform meta for "${platform}", but no _meta found`}}const expectedPrefixes=getToolCallMetaPrefixes(platform);const forbiddenPrefixes=getForbiddenMetaPrefixes(platform);const metaKeys=Object.keys(meta);const hasExpectedPrefix=metaKeys.some(key=>expectedPrefixes.some(prefix=>key.startsWith(prefix)));const forbiddenKeys=metaKeys.filter(key=>forbiddenPrefixes.some(prefix=>key.startsWith(prefix)));const pass=hasExpectedPrefix&&forbiddenKeys.length===0;return{pass,message:()=>{if(!hasExpectedPrefix){return`Expected _meta to have keys with prefixes [${expectedPrefixes.join(", ")}] for platform "${platform}", but found: [${metaKeys.join(", ")}]`}if(forbiddenKeys.length>0){return`Expected _meta NOT to have keys [${forbiddenKeys.join(", ")}] for platform "${platform}" (forbidden prefixes: [${forbiddenPrefixes.join(", ")}])`}return`Expected result not to have platform meta for "${platform}"`}}};var toHaveMetaKey=function(received,key){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have key "${key}", but no _meta found`}}const pass=key in meta;return{pass,message:()=>pass?`Expected _meta not to have key "${key}"`:`Expected _meta to have key "${key}"`}};var toHaveMetaValue=function(received,key,value){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta["${key}"] to equal ${JSON.stringify(value)}, but no _meta found`}}const actualValue=meta[key];const pass=JSON.stringify(actualValue)===JSON.stringify(value);return{pass,message:()=>pass?`Expected _meta["${key}"] not to equal ${JSON.stringify(value)}`:`Expected _meta["${key}"] to equal ${JSON.stringify(value)}, but got ${JSON.stringify(actualValue)}`}};var toNotHaveMetaKey=function(received,key){const meta=extractMeta(received);if(!meta){return{pass:true,message:()=>`Expected _meta to have key "${key}", but no _meta found`}}const pass=!(key in meta);return{pass,message:()=>pass?`Expected _meta to have key "${key}"`:`Expected _meta not to have key "${key}"`}};var toHaveOnlyNamespacedMeta=function(received,namespace){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have keys with namespace "${namespace}", but no _meta found`}}const metaKeys=Object.keys(meta);const wrongKeys=metaKeys.filter(key=>!key.startsWith(namespace));const pass=wrongKeys.length===0&&metaKeys.length>0;return{pass,message:()=>{if(metaKeys.length===0){return`Expected _meta to have keys with namespace "${namespace}", but _meta is empty`}if(wrongKeys.length>0){return`Expected _meta to ONLY have keys with namespace "${namespace}", but found: [${wrongKeys.join(", ")}]`}return`Expected _meta not to have only keys with namespace "${namespace}"`}}};var toHavePlatformMimeType=function(received,platform){const meta=extractMeta(received);const expectedMimeType=getPlatformMimeType(platform);if(!meta){return{pass:false,message:()=>`Expected _meta to have MIME type "${expectedMimeType}" for platform "${platform}", but no _meta found`}}const mimeTypeKey="ui/mimeType";const actualMimeType=meta[mimeTypeKey];const pass=actualMimeType===expectedMimeType;return{pass,message:()=>pass?`Expected _meta["${mimeTypeKey}"] not to be "${expectedMimeType}"`:`Expected _meta["${mimeTypeKey}"] to be "${expectedMimeType}" for platform "${platform}", but got "${actualMimeType}"`}};var toHavePlatformHtml=function(received,platform){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have platform HTML for "${platform}", but no _meta found`}}const htmlKey="ui/html";const html=meta[htmlKey];const pass=typeof html==="string"&&html.length>0;return{pass,message:()=>pass?`Expected _meta not to have platform HTML in "${htmlKey}"`:`Expected _meta["${htmlKey}"] to contain HTML for platform "${platform}", but ${html===void 0?"key not found":`got ${typeof html}`}`}};var uiMatchers={toHaveRenderedHtml,toContainHtmlElement,toContainBoundValue,toBeXssSafe,toHaveWidgetMetadata,toHaveCssClass,toNotContainRawContent,toHaveProperHtmlStructure,toHavePlatformMeta,toHaveMetaKey,toHaveMetaValue,toNotHaveMetaKey,toHaveOnlyNamespacedMeta,toHavePlatformMimeType,toHavePlatformHtml};var toContainTool=function(received,toolName){const tools=received;if(!Array.isArray(tools)){return{pass:false,message:()=>`Expected an array of tools, but received ${typeof received}`}}const pass=tools.some(t=>t.name===toolName);const availableTools=tools.map(t=>t.name).join(", ");return{pass,message:()=>pass?`Expected tools not to contain "${toolName}"`:`Expected tools to contain "${toolName}", but got: [${availableTools}]`}};var toBeSuccessful=function(received){const result=received;if(typeof result!=="object"||result===null||!("isSuccess"in result)){return{pass:false,message:()=>`Expected a result wrapper object with isSuccess property`}}const pass=result.isSuccess;return{pass,message:()=>pass?"Expected result not to be successful":`Expected result to be successful, but got error: ${result.error?.message??"unknown error"}`}};var toBeError=function(received,expectedCode){const result=received;if(typeof result!=="object"||result===null||!("isError"in result)){return{pass:false,message:()=>`Expected a result wrapper object with isError property`}}const receivedCode=typeof expectedCode==="string"?result.raw?._meta?.["code"]:result.error?.code;const codeMatches=expectedCode===void 0||receivedCode===expectedCode;const pass=result.isError&&codeMatches;return{pass,message:()=>{if(!result.isError){return"Expected result to be an error, but it was successful"}if(!codeMatches){return`Expected error code ${expectedCode}, but got ${String(receivedCode)}`}return"Expected result not to be an error"}}};var toHaveTextContent=function(received,expectedText){const result=received;if(typeof result!=="object"||result===null||!("text"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper or ResourceContentWrapper object with text method`}}const text=result.text();const hasText="hasTextContent"in result?result.hasTextContent():text!==void 0;let pass=hasText;if(pass&&expectedText!==void 0){pass=text?.includes(expectedText)??false}return{pass,message:()=>{if(!hasText){return"Expected result to have text content"}if(expectedText!==void 0&&!text?.includes(expectedText)){return`Expected text to contain "${expectedText}", but got: "${text}"`}return"Expected result not to have text content"}}};var toHaveImageContent=function(received){const result=received;if(typeof result!=="object"||result===null||!("hasImageContent"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper object with hasImageContent method`}}const pass=result.hasImageContent();return{pass,message:()=>pass?"Expected result not to have image content":"Expected result to have image content"}};var toHaveResourceContent=function(received){const result=received;if(typeof result!=="object"||result===null||!("hasResourceContent"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper object with hasResourceContent method`}}const pass=result.hasResourceContent();return{pass,message:()=>pass?"Expected result not to have resource content":"Expected result to have resource content"}};var toContainResource=function(received,uri){const resources=received;if(!Array.isArray(resources)){return{pass:false,message:()=>`Expected an array of resources, but received ${typeof received}`}}const pass=resources.some(r=>r.uri===uri);const availableUris=resources.map(r=>r.uri).join(", ");return{pass,message:()=>pass?`Expected resources not to contain "${uri}"`:`Expected resources to contain "${uri}", but got: [${availableUris}]`}};var toContainResourceTemplate=function(received,uriTemplate){const templates=received;if(!Array.isArray(templates)){return{pass:false,message:()=>`Expected an array of resource templates, but received ${typeof received}`}}const pass=templates.some(t=>t.uriTemplate===uriTemplate);const availableTemplates=templates.map(t=>t.uriTemplate).join(", ");return{pass,message:()=>pass?`Expected templates not to contain "${uriTemplate}"`:`Expected templates to contain "${uriTemplate}", but got: [${availableTemplates}]`}};var toHaveMimeType=function(received,mimeType){const result=received;if(typeof result!=="object"||result===null||!("hasMimeType"in result)){return{pass:false,message:()=>`Expected a ResourceContentWrapper object with hasMimeType method`}}const pass=result.hasMimeType(mimeType);const actualMimeType=result.mimeType();return{pass,message:()=>pass?`Expected content not to have MIME type "${mimeType}"`:`Expected MIME type "${mimeType}", but got "${actualMimeType}"`}};var toContainPrompt=function(received,name){const prompts=received;if(!Array.isArray(prompts)){return{pass:false,message:()=>`Expected an array of prompts, but received ${typeof received}`}}const pass=prompts.some(p=>p.name===name);const availablePrompts=prompts.map(p=>p.name).join(", ");return{pass,message:()=>pass?`Expected prompts not to contain "${name}"`:`Expected prompts to contain "${name}", but got: [${availablePrompts}]`}};var toHaveMessages=function(received,count){const result=received;if(typeof result!=="object"||result===null||!("messages"in result)){return{pass:false,message:()=>`Expected a PromptResultWrapper object with messages property`}}const actualCount=result.messages?.length??0;const pass=actualCount===count;return{pass,message:()=>pass?`Expected prompt not to have ${count} messages`:`Expected prompt to have ${count} messages, but got ${actualCount}`}};var toBeValidJsonRpc=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const hasJsonRpc=response["jsonrpc"]==="2.0";const hasId="id"in response;const hasResult="result"in response;const hasError="error"in response;const hasExactlyOneResultOrError=(hasResult||hasError)&&!(hasResult&&hasError);const pass=hasJsonRpc&&hasId&&hasExactlyOneResultOrError;return{pass,message:()=>{if(pass){return"Expected response not to be valid JSON-RPC"}const issues=[];if(!hasJsonRpc)issues.push('missing or invalid "jsonrpc": "2.0"');if(!hasId)issues.push('missing "id" field');if(!hasExactlyOneResultOrError){if(!hasResult&&!hasError)issues.push('missing "result" or "error"');else issues.push('cannot have both "result" and "error"')}return`Expected valid JSON-RPC 2.0 response: ${issues.join(", ")}`}}};var toHaveResult=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const pass="result"in response;return{pass,message:()=>pass?"Expected response not to have result":"Expected response to have result"}};var toHaveError=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const pass="error"in response;return{pass,message:()=>pass?"Expected response not to have error":"Expected response to have error"}};var toHaveErrorCode=function(received,code){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const actualCode=response.error?.code;const pass=actualCode===code;return{pass,message:()=>pass?`Expected response not to have error code ${code}`:`Expected error code ${code}, but got ${actualCode??"no error"}`}};var mcpMatchers={toContainTool,toBeSuccessful,toBeError,toHaveTextContent,toHaveImageContent,toHaveResourceContent,toContainResource,toContainResourceTemplate,toHaveMimeType,toContainPrompt,toHaveMessages,toBeValidJsonRpc,toHaveResult,toHaveError,toHaveErrorCode,...uiMatchers};0&&(module.exports={mcpMatchers});
1
+ "use strict";var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:true})};var __copyProps=(to,from,except,desc)=>{if(from&&typeof from==="object"||typeof from==="function"){for(let key of __getOwnPropNames(from))if(!__hasOwnProp.call(to,key)&&key!==except)__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable})}return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:true}),mod);var matchers_exports={};__export(matchers_exports,{mcpMatchers:()=>mcpMatchers});module.exports=__toCommonJS(matchers_exports);var import_lazy_zod=require("@frontmcp/lazy-zod");var import_utils=require("@frontmcp/utils");function getPlatformMimeType(platform){return"text/html;profile=mcp-app"}function getToolCallMetaPrefixes(platform){return["ui/"]}function getForbiddenMetaPrefixes(platform){return["openai/","frontmcp/"]}function escapeRegex(str){return str.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function extractUiHtml(received){if(typeof received==="string"){return received}const wrapper=received;const meta=wrapper?.raw?._meta||wrapper?._meta;if(meta&&typeof meta==="object"){const uiHtml=meta["ui/html"];if(typeof uiHtml==="string"){return uiHtml}}return void 0}function extractMeta(received){const wrapper=received;const meta=wrapper?.raw?._meta||wrapper?._meta;if(meta&&typeof meta==="object"){return meta}return void 0}var toHaveRenderedHtml=function(received){const html=extractUiHtml(received);const hasHtml=html!==void 0&&html.length>0;const isFallback=hasHtml&&html.includes("mdx-fallback");const pass=hasHtml&&!isFallback;return{pass,message:()=>{if(isFallback){return"Expected rendered HTML but got mdx-fallback (raw escaped content). MDX rendering may have failed."}if(!hasHtml){return"Expected _meta to have ui/html property with rendered HTML"}return"Expected result not to have rendered HTML"}}};var toContainHtmlElement=function(received,tag){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected to find <${tag}> element, but no HTML content found`}}const regex=new RegExp(`<${escapeRegex(tag)}[\\s>]`,"i");const pass=regex.test(html);return{pass,message:()=>pass?`Expected HTML not to contain <${tag}> element`:`Expected HTML to contain <${tag}> element`}};var toContainBoundValue=function(received,value){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected HTML to contain bound value "${value}", but no HTML content found`}}const stringValue=String(value);const pass=html.includes(stringValue);return{pass,message:()=>pass?`Expected HTML not to contain bound value "${stringValue}"`:`Expected HTML to contain bound value "${stringValue}"`}};var toBeXssSafe=function(received){const html=extractUiHtml(received);if(!html){return{pass:true,message:()=>"Expected HTML to be XSS unsafe (no HTML found)"}}const hasScript=/<script[\s>]/i.test(html);const hasOnHandler=/\son\w+\s*=/i.test(html);const hasJavascriptUri=/javascript:/i.test(html);const issues=[];if(hasScript)issues.push("<script> tag");if(hasOnHandler)issues.push("inline event handler (onclick, etc.)");if(hasJavascriptUri)issues.push("javascript: URI");const pass=!hasScript&&!hasOnHandler&&!hasJavascriptUri;return{pass,message:()=>pass?"Expected HTML not to be XSS safe":`Expected HTML to be XSS safe, but found: ${issues.join(", ")}`}};var toHaveWidgetMetadata=function(received){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>"Expected _meta to have widget metadata, but no _meta found"}}const hasUiHtml=Boolean(meta["ui/html"]);const hasMimeType=Boolean(meta["ui/mimeType"]);const hasUiObject=Boolean(meta["ui"]&&typeof meta["ui"]==="object");const pass=hasUiHtml||hasMimeType||hasUiObject;return{pass,message:()=>pass?"Expected result not to have widget metadata":"Expected _meta to have widget metadata (ui/html, ui/mimeType, or ui object)"}};var toHaveCssClass=function(received,className){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>`Expected HTML to have CSS class "${className}", but no HTML content found`}}const classRegex=new RegExp(`class(?:Name)?\\s*=\\s*["'][^"']*\\b${escapeRegex(className)}\\b[^"']*["']`,"i");const pass=classRegex.test(html);return{pass,message:()=>pass?`Expected HTML not to have CSS class "${className}"`:`Expected HTML to have CSS class "${className}"`}};var toNotContainRawContent=function(received,content){const html=extractUiHtml(received);if(!html){return{pass:true,message:()=>`Expected HTML to contain raw content "${content}", but no HTML found`}}const pass=!html.includes(content);return{pass,message:()=>pass?`Expected HTML to contain raw content "${content}"`:`Expected HTML not to contain raw content "${content}" (may indicate rendering failure)`}};var toHaveProperHtmlStructure=function(received){const html=extractUiHtml(received);if(!html){return{pass:false,message:()=>"Expected proper HTML structure, but no HTML content found"}}const hasEscapedTags=html.includes("&lt;")&&html.includes("&gt;");const hasHtmlTags=/<[a-z]/i.test(html);const pass=hasHtmlTags&&!hasEscapedTags;return{pass,message:()=>{if(hasEscapedTags){return"Expected proper HTML structure, but found escaped HTML entities - content may not have been rendered"}if(!hasHtmlTags){return"Expected proper HTML structure, but found no HTML tags"}return"Expected result not to have proper HTML structure"}}};var toHavePlatformMeta=function(received,platform){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have platform meta for "${platform}", but no _meta found`}}const expectedPrefixes=getToolCallMetaPrefixes(platform);const forbiddenPrefixes=getForbiddenMetaPrefixes(platform);const metaKeys=Object.keys(meta);const hasExpectedPrefix=metaKeys.some(key=>expectedPrefixes.some(prefix=>key.startsWith(prefix)));const forbiddenKeys=metaKeys.filter(key=>forbiddenPrefixes.some(prefix=>key.startsWith(prefix)));const pass=hasExpectedPrefix&&forbiddenKeys.length===0;return{pass,message:()=>{if(!hasExpectedPrefix){return`Expected _meta to have keys with prefixes [${expectedPrefixes.join(", ")}] for platform "${platform}", but found: [${metaKeys.join(", ")}]`}if(forbiddenKeys.length>0){return`Expected _meta NOT to have keys [${forbiddenKeys.join(", ")}] for platform "${platform}" (forbidden prefixes: [${forbiddenPrefixes.join(", ")}])`}return`Expected result not to have platform meta for "${platform}"`}}};var toHaveMetaKey=function(received,key){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have key "${key}", but no _meta found`}}const pass=key in meta;return{pass,message:()=>pass?`Expected _meta not to have key "${key}"`:`Expected _meta to have key "${key}"`}};var toHaveMetaValue=function(received,key,value){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta["${key}"] to equal ${JSON.stringify(value)}, but no _meta found`}}const actualValue=meta[key];const pass=JSON.stringify(actualValue)===JSON.stringify(value);return{pass,message:()=>pass?`Expected _meta["${key}"] not to equal ${JSON.stringify(value)}`:`Expected _meta["${key}"] to equal ${JSON.stringify(value)}, but got ${JSON.stringify(actualValue)}`}};var toNotHaveMetaKey=function(received,key){const meta=extractMeta(received);if(!meta){return{pass:true,message:()=>`Expected _meta to have key "${key}", but no _meta found`}}const pass=!(key in meta);return{pass,message:()=>pass?`Expected _meta to have key "${key}"`:`Expected _meta not to have key "${key}"`}};var toHaveOnlyNamespacedMeta=function(received,namespace){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have keys with namespace "${namespace}", but no _meta found`}}const metaKeys=Object.keys(meta);const wrongKeys=metaKeys.filter(key=>!key.startsWith(namespace));const pass=wrongKeys.length===0&&metaKeys.length>0;return{pass,message:()=>{if(metaKeys.length===0){return`Expected _meta to have keys with namespace "${namespace}", but _meta is empty`}if(wrongKeys.length>0){return`Expected _meta to ONLY have keys with namespace "${namespace}", but found: [${wrongKeys.join(", ")}]`}return`Expected _meta not to have only keys with namespace "${namespace}"`}}};var toHavePlatformMimeType=function(received,platform){const meta=extractMeta(received);const expectedMimeType=getPlatformMimeType(platform);if(!meta){return{pass:false,message:()=>`Expected _meta to have MIME type "${expectedMimeType}" for platform "${platform}", but no _meta found`}}const mimeTypeKey="ui/mimeType";const actualMimeType=meta[mimeTypeKey];const pass=actualMimeType===expectedMimeType;return{pass,message:()=>pass?`Expected _meta["${mimeTypeKey}"] not to be "${expectedMimeType}"`:`Expected _meta["${mimeTypeKey}"] to be "${expectedMimeType}" for platform "${platform}", but got "${actualMimeType}"`}};var toHavePlatformHtml=function(received,platform){const meta=extractMeta(received);if(!meta){return{pass:false,message:()=>`Expected _meta to have platform HTML for "${platform}", but no _meta found`}}const htmlKey="ui/html";const html=meta[htmlKey];const pass=typeof html==="string"&&html.length>0;return{pass,message:()=>pass?`Expected _meta not to have platform HTML in "${htmlKey}"`:`Expected _meta["${htmlKey}"] to contain HTML for platform "${platform}", but ${html===void 0?"key not found":`got ${typeof html}`}`}};var uiMatchers={toHaveRenderedHtml,toContainHtmlElement,toContainBoundValue,toBeXssSafe,toHaveWidgetMetadata,toHaveCssClass,toNotContainRawContent,toHaveProperHtmlStructure,toHavePlatformMeta,toHaveMetaKey,toHaveMetaValue,toNotHaveMetaKey,toHaveOnlyNamespacedMeta,toHavePlatformMimeType,toHavePlatformHtml};var toContainTool=function(received,toolName){const tools=received;if(!Array.isArray(tools)){return{pass:false,message:()=>`Expected an array of tools, but received ${typeof received}`}}const pass=tools.some(t=>t.name===toolName);const availableTools=tools.map(t=>t.name).join(", ");return{pass,message:()=>pass?`Expected tools not to contain "${toolName}"`:`Expected tools to contain "${toolName}", but got: [${availableTools}]`}};var toBeSuccessful=function(received){const result=received;if(typeof result!=="object"||result===null||!("isSuccess"in result)){return{pass:false,message:()=>`Expected a result wrapper object with isSuccess property`}}const pass=result.isSuccess;return{pass,message:()=>pass?"Expected result not to be successful":`Expected result to be successful, but got error: ${result.error?.message??"unknown error"}`}};var toBeError=function(received,expectedCode){const result=received;if(typeof result!=="object"||result===null||!("isError"in result)){return{pass:false,message:()=>`Expected a result wrapper object with isError property`}}const receivedCode=typeof expectedCode==="string"?result.raw?._meta?.["code"]:result.error?.code;const codeMatches=expectedCode===void 0||receivedCode===expectedCode;const pass=result.isError&&codeMatches;return{pass,message:()=>{if(!result.isError){return"Expected result to be an error, but it was successful"}if(!codeMatches){return`Expected error code ${expectedCode}, but got ${String(receivedCode)}`}return"Expected result not to be an error"}}};var toHaveTextContent=function(received,expectedText){const result=received;if(typeof result!=="object"||result===null||!("text"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper or ResourceContentWrapper object with text method`}}const text=result.text();const hasText="hasTextContent"in result?result.hasTextContent():text!==void 0;let pass=hasText;if(pass&&expectedText!==void 0){pass=text?.includes(expectedText)??false}return{pass,message:()=>{if(!hasText){return"Expected result to have text content"}if(expectedText!==void 0&&!text?.includes(expectedText)){return`Expected text to contain "${expectedText}", but got: "${text}"`}return"Expected result not to have text content"}}};var toHaveImageContent=function(received){const result=received;if(typeof result!=="object"||result===null||!("hasImageContent"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper object with hasImageContent method`}}const pass=result.hasImageContent();return{pass,message:()=>pass?"Expected result not to have image content":"Expected result to have image content"}};var toHaveResourceContent=function(received){const result=received;if(typeof result!=="object"||result===null||!("hasResourceContent"in result)){return{pass:false,message:()=>`Expected a ToolResultWrapper object with hasResourceContent method`}}const pass=result.hasResourceContent();return{pass,message:()=>pass?"Expected result not to have resource content":"Expected result to have resource content"}};var toPassOutputSchemaWhole=function(received,outputSchema){const schema=outputSchema instanceof import_lazy_zod.z.ZodType?outputSchema:import_lazy_zod.z.object(outputSchema);if((0,import_lazy_zod.hasTransform)(schema)){return{pass:false,message:()=>"toPassOutputSchemaWhole cannot compare keys through a transform (.transform(), z.preprocess(), .pipe()): assert on the parsed answer instead"}}const parsed=schema.safeParse(received);if(!parsed.success){const[firstIssue]=parsed.error.issues;const location=firstIssue?.path.length?` at ${firstIssue.path.join(".")}`:"";return{pass:false,message:()=>`Expected the answer to pass its output schema whole, but it does not match its output schema${location}: ${firstIssue?.message}`}}const droppedKeys=(0,import_utils.findDroppedKeys)(received,parsed.data);const pass=droppedKeys.length===0;return{pass,message:()=>pass?"Expected the answer not to pass its output schema whole":`Expected the answer to pass its output schema whole, but parsing drops keys it does not declare: ${droppedKeys.join(", ")}`}};var toContainResource=function(received,uri){const resources=received;if(!Array.isArray(resources)){return{pass:false,message:()=>`Expected an array of resources, but received ${typeof received}`}}const pass=resources.some(r=>r.uri===uri);const availableUris=resources.map(r=>r.uri).join(", ");return{pass,message:()=>pass?`Expected resources not to contain "${uri}"`:`Expected resources to contain "${uri}", but got: [${availableUris}]`}};var toContainResourceTemplate=function(received,uriTemplate){const templates=received;if(!Array.isArray(templates)){return{pass:false,message:()=>`Expected an array of resource templates, but received ${typeof received}`}}const pass=templates.some(t=>t.uriTemplate===uriTemplate);const availableTemplates=templates.map(t=>t.uriTemplate).join(", ");return{pass,message:()=>pass?`Expected templates not to contain "${uriTemplate}"`:`Expected templates to contain "${uriTemplate}", but got: [${availableTemplates}]`}};var toHaveMimeType=function(received,mimeType){const result=received;if(typeof result!=="object"||result===null||!("hasMimeType"in result)){return{pass:false,message:()=>`Expected a ResourceContentWrapper object with hasMimeType method`}}const pass=result.hasMimeType(mimeType);const actualMimeType=result.mimeType();return{pass,message:()=>pass?`Expected content not to have MIME type "${mimeType}"`:`Expected MIME type "${mimeType}", but got "${actualMimeType}"`}};var toContainPrompt=function(received,name){const prompts=received;if(!Array.isArray(prompts)){return{pass:false,message:()=>`Expected an array of prompts, but received ${typeof received}`}}const pass=prompts.some(p=>p.name===name);const availablePrompts=prompts.map(p=>p.name).join(", ");return{pass,message:()=>pass?`Expected prompts not to contain "${name}"`:`Expected prompts to contain "${name}", but got: [${availablePrompts}]`}};var toHaveMessages=function(received,count){const result=received;if(typeof result!=="object"||result===null||!("messages"in result)){return{pass:false,message:()=>`Expected a PromptResultWrapper object with messages property`}}const actualCount=result.messages?.length??0;const pass=actualCount===count;return{pass,message:()=>pass?`Expected prompt not to have ${count} messages`:`Expected prompt to have ${count} messages, but got ${actualCount}`}};var toBeValidJsonRpc=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const hasJsonRpc=response["jsonrpc"]==="2.0";const hasId="id"in response;const hasResult="result"in response;const hasError="error"in response;const hasExactlyOneResultOrError=(hasResult||hasError)&&!(hasResult&&hasError);const pass=hasJsonRpc&&hasId&&hasExactlyOneResultOrError;return{pass,message:()=>{if(pass){return"Expected response not to be valid JSON-RPC"}const issues=[];if(!hasJsonRpc)issues.push('missing or invalid "jsonrpc": "2.0"');if(!hasId)issues.push('missing "id" field');if(!hasExactlyOneResultOrError){if(!hasResult&&!hasError)issues.push('missing "result" or "error"');else issues.push('cannot have both "result" and "error"')}return`Expected valid JSON-RPC 2.0 response: ${issues.join(", ")}`}}};var toHaveResult=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const pass="result"in response;return{pass,message:()=>pass?"Expected response not to have result":"Expected response to have result"}};var toHaveError=function(received){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const pass="error"in response;return{pass,message:()=>pass?"Expected response not to have error":"Expected response to have error"}};var toHaveErrorCode=function(received,code){const response=received;if(typeof response!=="object"||response===null){return{pass:false,message:()=>`Expected an object, but received ${typeof received}`}}const actualCode=response.error?.code;const pass=actualCode===code;return{pass,message:()=>pass?`Expected response not to have error code ${code}`:`Expected error code ${code}, but got ${actualCode??"no error"}`}};var mcpMatchers={toContainTool,toBeSuccessful,toBeError,toHaveTextContent,toHaveImageContent,toHaveResourceContent,toPassOutputSchemaWhole,toContainResource,toContainResourceTemplate,toHaveMimeType,toContainPrompt,toHaveMessages,toBeValidJsonRpc,toHaveResult,toHaveError,toHaveErrorCode,...uiMatchers};0&&(module.exports={mcpMatchers});
@@ -5,10 +5,11 @@
5
5
  * This file extends Jest's expect interface with MCP-specific matchers.
6
6
  * Import this file or ensure it's included in your tsconfig to get type checking.
7
7
  */
8
+ import type { ZodRawShape, ZodType } from '@frontmcp/lazy-zod';
8
9
  /**
9
- * Custom MCP matchers for Jest
10
+ * Custom MCP matchers for Jest: tool, resource, prompt and protocol results
10
11
  */
11
- export interface McpMatchers<R = unknown> {
12
+ export interface McpResultMatchers<R = unknown> {
12
13
  /**
13
14
  * Check if tools array contains a tool with the given name
14
15
  * @param toolName - The name of the tool to find
@@ -75,6 +76,20 @@ export interface McpMatchers<R = unknown> {
75
76
  * ```
76
77
  */
77
78
  toHaveResourceContent(): R;
79
+ /**
80
+ * Check that a tool's answer passes its output schema whole: it matches the schema, and parsing
81
+ * drops no key the schema does not declare. The failure message names the undeclared keys, which
82
+ * the server would strip, warn about or refuse, per its `output.undeclaredKeys` policy. A schema with
83
+ * a transform (`.transform()`, `z.preprocess()`, `.pipe()`) cannot be compared key by key and fails.
84
+ * @param outputSchema - The tool's `outputSchema`: a Zod schema or a raw shape
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const answer = await tool.execute({ id: 'INV-1' });
89
+ * expect(answer).toPassOutputSchemaWhole(invoiceOutputSchema);
90
+ * ```
91
+ */
92
+ toPassOutputSchemaWhole(outputSchema: ZodType | ZodRawShape): R;
78
93
  /**
79
94
  * Check if resources array contains a resource with the given URI
80
95
  * @param uri - The URI of the resource to find
@@ -171,6 +186,11 @@ export interface McpMatchers<R = unknown> {
171
186
  * ```
172
187
  */
173
188
  toHaveErrorCode(code: number): R;
189
+ }
190
+ /**
191
+ * Custom MCP matchers for Jest: rendered tool UI
192
+ */
193
+ export interface McpUiMatchers<R = unknown> {
174
194
  /**
175
195
  * Check if tool result has rendered HTML in _meta['ui/html'].
176
196
  * Fails if the HTML is the mdx-fallback (escaped raw content).
@@ -254,13 +274,18 @@ export interface McpMatchers<R = unknown> {
254
274
  */
255
275
  toHaveProperHtmlStructure(): R;
256
276
  }
277
+ /**
278
+ * Custom MCP matchers for Jest
279
+ */
280
+ export interface McpMatchers<R = unknown> extends McpResultMatchers<R>, McpUiMatchers<R> {
281
+ }
257
282
  declare global {
258
283
  namespace jest {
259
- interface Matchers<R> extends McpMatchers<R> {
284
+ interface Matchers<R> extends McpResultMatchers<R>, McpUiMatchers<R> {
260
285
  }
261
- interface Expect extends McpMatchers<void> {
286
+ interface Expect extends McpResultMatchers<void>, McpUiMatchers<void> {
262
287
  }
263
- interface InverseAsymmetricMatchers extends McpMatchers<void> {
288
+ interface InverseAsymmetricMatchers extends McpResultMatchers<void>, McpUiMatchers<void> {
264
289
  }
265
290
  }
266
291
  }