///
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
const temporaryDirectories: string[] = [];
function writeFile(root: string, relativePath: string, source: string): void {
const filePath = path.join(root, relativePath);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, source);
}
function toolSource(name: string): string {
return [
`export const ${name.replace(/_/g, '')}ToolDefinition = {`,
` name: '${name}',`,
" inputSchema: { properties: { action: { enum: ['run'], description: 'Action' } }, required: ['action'] }",
'};',
''
].join('\n');
}
function nativeToolSource(name: string): string {
return [
'class FFixtureTool {',
` FString GetName() const override { return TEXT("${name}"); }`,
' auto GetSchema() {',
' return Builder.StringEnum(TEXT("action"), { TEXT("run") }, TEXT("Action"));',
' }',
'};',
''
].join('\n');
}
function createParityFixture(
typeScriptNames: readonly string[],
registryNames: readonly string[],
nativeDefinitionNames: readonly string[]
): string {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'native-parity-audit-'));
temporaryDirectories.push(root);
writeFile(
root,
'src/tools/definitions/shared/action-sets.ts',
"export const UNUSED_ACTIONS = ['unused'] as const;\n"
);
typeScriptNames.forEach((name, index) => {
writeFile(
root,
`src/tools/definitions/group-${index % 2}/${index}-${name}-tool.ts`,
toolSource(name)
);
});
const registryValues = registryNames.map((name) => `TEXT("${name}")`).join(', ');
writeFile(
root,
'plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/MCP/Registry/McpToolRegistry.cpp',
`const TArray CanonicalToolNames = { ${registryValues} };\n`
);
writeFile(
root,
'plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/MCP/Routing/McpConsolidatedActionRoutingFixture.h',
''
);
nativeDefinitionNames.forEach((name, index) => {
writeFile(
root,
`plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/MCP/Tools/group-${index % 2}/${index}-${name}.cpp`,
nativeToolSource(name)
);
});
return root;
}
function createClassSlicingFixture(fileName: string, alphaMembers: readonly string[]): string {
const root = createParityFixture(['alpha', 'beta'], ['alpha', 'beta'], []);
writeFile(
root,
`plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/MCP/Tools/${fileName}`,
[
'class FAlphaTool {',
...alphaMembers,
'};',
'class FBetaTool {',
' FString GetName() const override { return TEXT("beta"); }',
' auto GetSchema() {',
' return Builder.StringEnum(TEXT("action"), { TEXT("run") }, TEXT("Action"));',
' }',
'};',
''
].join('\n')
);
return root;
}
afterEach(() => {
for (const directory of temporaryDirectories.splice(0)) {
fs.rmSync(directory, { recursive: true, force: true });
}
});
describe('native MCP parity audit', () => {
it('fails when 24 TypeScript definitions collapse to 23 unique names', async () => {
const { auditNativeMcpParity } = await import('../native-mcp-parity-audit.mjs');
const canonicalNames = Array.from({ length: 23 }, (_, index) => `tool_${index + 1}`);
const root = createParityFixture(
[...canonicalNames, canonicalNames[22] ?? 'tool_23'],
canonicalNames,
canonicalNames
);
const result = auditNativeMcpParity({ repoRoot: root });
expect(result.counts).toEqual({
typeScriptDefinitions: 24,
uniqueTypeScriptNames: 23,
nativeRegistryEntries: 23,
uniqueNativeRegistryNames: 23,
nativeDefinitions: 23,
uniqueNativeDefinitionNames: 23
});
expect(result.duplicateNames.typeScriptTools).toEqual(['tool_23']);
expect(result.hasMismatches).toBe(true);
});
it('reports duplicate native registry and GetName entries before indexing', async () => {
const { auditNativeMcpParity } = await import('../native-mcp-parity-audit.mjs');
const root = createParityFixture(
['alpha', 'beta'],
['alpha', 'alpha', 'beta'],
['alpha', 'alpha', 'beta']
);
const result = auditNativeMcpParity({ repoRoot: root });
expect(result.duplicateNames.nativeCanonicalRegistry).toEqual(['alpha']);
expect(result.duplicateNames.nativeToolDefinitions).toEqual(['alpha']);
expect(result.hasMismatches).toBe(true);
});
it('excludes non-canonical native definitions from parity counts', async () => {
const { auditNativeMcpParity } = await import('../native-mcp-parity-audit.mjs');
const root = createParityFixture(
['alpha', 'beta'],
['alpha', 'beta'],
['alpha', 'beta', 'legacy_tool']
);
const result = auditNativeMcpParity({ repoRoot: root });
expect(result.counts.nativeDefinitions).toBe(2);
expect(result.hasMismatches).toBe(false);
});
it('attributes action enums to the matching class in multi-tool source files', async () => {
const { auditNativeMcpParity } = await import('../native-mcp-parity-audit.mjs');
const root = createParityFixture(['alpha', 'beta'], ['alpha', 'beta'], []);
writeFile(
root,
'plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/MCP/Tools/multi.cpp',
[
'class FAlphaTool {',
' FString GetName() const override { return TEXT("alpha"); }',
' auto GetSchema() {',
' return Builder.StringEnum(TEXT("action"), { TEXT("run") }, TEXT("Action"));',
' }',
'};',
'class FBetaTool {',
' FString GetName() const override { return TEXT("beta"); }',
' auto GetSchema() {',
' return Builder.StringEnum(TEXT("action"), { TEXT("stop") }, TEXT("Action"));',
' }',
'};',
'',
].join('\n'),
);
const result = auditNativeMcpParity({ repoRoot: root });
expect(result.actionGaps).toEqual([
{
tool: 'beta',
missingNativeActions: ['run'],
extraNativeActions: ['stop'],
},
]);
});
it('prefers the tool definition action enum over slug-prefixed helpers', async () => {
const { auditNativeMcpParity } = await import('../native-mcp-parity-audit.mjs');
const root = createParityFixture(['alpha'], ['alpha'], ['alpha']);
writeFile(
root,
'src/tools/definitions/group-0/0-alpha-a-helper.ts',
[
'export const unrelated = {',
" action: { enum: ['fake'], description: 'Action' }",
'};',
'',
].join('\n'),
);
const result = auditNativeMcpParity({ repoRoot: root });
expect(result.actionGaps).toEqual([]);
expect(result.hasMismatches).toBe(false);
});
it('ignores literal and comment braces when slicing a native tool class', async () => {
const { auditNativeMcpParity } = await import('../native-mcp-parity-audit.mjs');
const root = createClassSlicingFixture('literals.cpp', [
' FString GetName() const override { return TEXT("alpha"); }',
' const TCHAR* DoubleQuoted = TEXT("{");',
" const TCHAR SingleQuoted = '{';",
' // { unmatched line-comment brace',
' /* { unmatched block-comment brace */'
]);
const result = auditNativeMcpParity({ repoRoot: root });
expect(result.actionGaps).toEqual([
{
tool: 'alpha',
missingNativeActions: ['run'],
extraNativeActions: [],
},
]);
});
it('ignores quotes and braces inside C++ raw string literals', async () => {
const { auditNativeMcpParity } = await import('../native-mcp-parity-audit.mjs');
const root = createClassSlicingFixture('raw-literal.cpp', [
' FString GetName() const override { return TEXT("alpha"); }',
' const TCHAR* Payload = R"json("quoted { brace")json";'
]);
const result = auditNativeMcpParity({ repoRoot: root });
expect(result.actionGaps).toEqual([
{ tool: 'alpha', missingNativeActions: ['run'], extraNativeActions: [] },
]);
});
it('selects the containing tool class after a closed nested type', async () => {
const { auditNativeMcpParity } = await import('../native-mcp-parity-audit.mjs');
const root = createClassSlicingFixture('nested-class.cpp', [
' struct FMetadata {',
' };',
' auto GetSchema() {',
' return Builder.StringEnum(TEXT("action"), { TEXT("run") }, TEXT("Action"));',
' }',
' FString GetName() const override { return TEXT("alpha"); }'
]);
const result = auditNativeMcpParity({ repoRoot: root });
expect(result.actionGaps).toEqual([]);
});
it('keeps backslash-continued physical lines inside line comments', async () => {
const { auditNativeMcpParity } = await import('../native-mcp-parity-audit.mjs');
const root = createClassSlicingFixture('continued-comment.cpp', [
' FString GetName() const override { return TEXT("alpha"); }',
' // continued comment \\',
' {'
]);
const result = auditNativeMcpParity({ repoRoot: root });
expect(result.actionGaps).toEqual([
{ tool: 'alpha', missingNativeActions: ['run'], extraNativeActions: [] },
]);
});
});