Export Protocol and mergeCapabilities from client/server package roots (#2501)
This commit is contained in:
parent
e81758caed
commit
1480241e2a
2 changed files with 246 additions and 73 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { Project } from 'ts-morph';
|
||||
|
||||
import { IMPORT_MAP } from '../../../src/migrations/v1-to-v2/mappings/importMap';
|
||||
import { importPathsTransform } from '../../../src/migrations/v1-to-v2/transforms/importPaths';
|
||||
import type { TransformContext } from '../../../src/types';
|
||||
|
||||
|
|
@ -11,6 +12,13 @@ function applyTransform(code: string, context: TransformContext = { projectType:
|
|||
return sourceFile.getFullText();
|
||||
}
|
||||
|
||||
function applyWithDiagnostics(code: string, context: TransformContext = { projectType: 'server' }) {
|
||||
const project = new Project({ useInMemoryFileSystem: true });
|
||||
const sourceFile = project.createSourceFile('test.ts', code);
|
||||
const result = importPathsTransform.apply(sourceFile, context);
|
||||
return { text: sourceFile.getFullText(), result };
|
||||
}
|
||||
|
||||
describe('import-paths transform', () => {
|
||||
it('rewrites client imports to @modelcontextprotocol/client', () => {
|
||||
const input = `import { Client } from '@modelcontextprotocol/sdk/client/index.js';\n`;
|
||||
|
|
@ -1006,93 +1014,139 @@ describe('auth types routing (B3)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('symbols with no v2 export (removedSymbols)', () => {
|
||||
function applyWithDiagnostics(code: string, context: TransformContext = { projectType: 'server' }) {
|
||||
const project = new Project({ useInMemoryFileSystem: true });
|
||||
const sourceFile = project.createSourceFile('test.ts', code);
|
||||
const result = importPathsTransform.apply(sourceFile, context);
|
||||
return { text: sourceFile.getFullText(), result };
|
||||
}
|
||||
|
||||
it('drops Protocol from a shared/protocol.js import and flags it', () => {
|
||||
describe('Protocol and mergeCapabilities route like other shared/protocol.js symbols', () => {
|
||||
it('rewrites a Protocol import to the context package root', () => {
|
||||
const input = `import { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).not.toContain('Protocol }');
|
||||
expect(text).toMatch(/import \{ Protocol \} from ['"]@modelcontextprotocol\/server['"]/);
|
||||
expect(text).not.toContain('@modelcontextprotocol/sdk');
|
||||
const diag = result.diagnostics.find(d => d.insertComment && d.message.includes('Protocol base class'));
|
||||
expect(result.diagnostics.filter(d => d.insertComment)).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps Protocol alongside its siblings in a mixed import', () => {
|
||||
const input = `import { Protocol, type ProtocolOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toContain('Protocol');
|
||||
expect(text).toContain('ProtocolOptions');
|
||||
expect(text).toContain('@modelcontextprotocol/server');
|
||||
expect(result.diagnostics.filter(d => d.insertComment)).toEqual([]);
|
||||
});
|
||||
|
||||
it('rewrites a mergeCapabilities import with no spread guidance', () => {
|
||||
const input = `import { mergeCapabilities } from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toMatch(/import \{ mergeCapabilities \} from ['"]@modelcontextprotocol\/server['"]/);
|
||||
expect(result.diagnostics.some(d => d.message.includes('object spread'))).toBe(false);
|
||||
});
|
||||
|
||||
it('rewrites a named re-export of Protocol', () => {
|
||||
const input = `export { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toMatch(/export \{ Protocol \} from ['"]@modelcontextprotocol\/server['"]/);
|
||||
expect(result.diagnostics.some(d => d.message.includes('no v2 export'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('symbols with no v2 export (removedSymbols)', () => {
|
||||
// No live mapping uses removedSymbols today (Protocol/mergeCapabilities were the
|
||||
// last, until they became public exports). The machinery stays for future
|
||||
// removals; these tests pin it against a synthetic mapping.
|
||||
const SYNTHETIC = '@modelcontextprotocol/sdk/shared/synthetic.js';
|
||||
|
||||
beforeAll(() => {
|
||||
IMPORT_MAP[SYNTHETIC] = {
|
||||
target: 'RESOLVE_BY_CONTEXT',
|
||||
status: 'moved',
|
||||
removedSymbols: {
|
||||
GoneClass: 'The GoneClass base class is not exported by the v2 packages. ' + 'See the migration guide for the replacement.',
|
||||
goneHelper: 'goneHelper() is not exported by the v2 packages. Use a plain object spread.'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete IMPORT_MAP[SYNTHETIC];
|
||||
});
|
||||
|
||||
it('drops a removed symbol from an import and flags it', () => {
|
||||
const input = `import { GoneClass } from '${SYNTHETIC}';\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).not.toContain('GoneClass }');
|
||||
expect(text).not.toContain('@modelcontextprotocol/sdk');
|
||||
const diag = result.diagnostics.find(d => d.insertComment && d.message.includes('GoneClass base class'));
|
||||
expect(diag).toBeDefined();
|
||||
expect(diag?.message).toContain('fallbackRequestHandler');
|
||||
expect(diag?.message).toContain('migration guide');
|
||||
});
|
||||
|
||||
it('routes surviving siblings while dropping the removed symbol', () => {
|
||||
const input = `import { Protocol, type ProtocolOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
const input = `import { GoneClass, type Survivor } from '${SYNTHETIC}';\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toContain('ProtocolOptions');
|
||||
expect(text).toContain('Survivor');
|
||||
expect(text).toContain('@modelcontextprotocol/server');
|
||||
expect(text).not.toMatch(/\bProtocol\b(?!Options)/);
|
||||
expect(result.diagnostics.some(d => d.message.includes('Protocol base class'))).toBe(true);
|
||||
expect(text).not.toMatch(/\bGoneClass\b/);
|
||||
expect(result.diagnostics.some(d => d.message.includes('GoneClass base class'))).toBe(true);
|
||||
});
|
||||
|
||||
it('flags mergeCapabilities with spread guidance', () => {
|
||||
const input = `import { mergeCapabilities } from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
it('flags a removed helper with its configured guidance', () => {
|
||||
const input = `import { goneHelper } from '${SYNTHETIC}';\n`;
|
||||
const { result } = applyWithDiagnostics(input);
|
||||
const diag = result.diagnostics.find(d => d.message.includes('mergeCapabilities'));
|
||||
const diag = result.diagnostics.find(d => d.message.includes('goneHelper'));
|
||||
expect(diag).toBeDefined();
|
||||
expect(diag?.message).toContain('object spread');
|
||||
});
|
||||
|
||||
it('does not resolve a context package for an import of only removed symbols', () => {
|
||||
const input = `import { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
const input = `import { GoneClass } from '${SYNTHETIC}';\n`;
|
||||
const { result } = applyWithDiagnostics(input, { projectType: 'unknown' });
|
||||
// projectType 'unknown' would emit a could-not-determine warning if context were resolved.
|
||||
expect(result.diagnostics.some(d => d.message.includes('could not determine'))).toBe(false);
|
||||
});
|
||||
|
||||
it('flags qualified accesses to removed symbols on a namespace import', () => {
|
||||
const input = `import * as proto from '@modelcontextprotocol/sdk/shared/protocol.js';\nclass Mine extends proto.Protocol {}\n`;
|
||||
const input = `import * as synth from '${SYNTHETIC}';\nclass Mine extends synth.GoneClass {}\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toContain('@modelcontextprotocol/server');
|
||||
expect(result.diagnostics.some(d => d.insertComment && d.message.includes('Protocol base class'))).toBe(true);
|
||||
expect(result.diagnostics.some(d => d.insertComment && d.message.includes('GoneClass base class'))).toBe(true);
|
||||
});
|
||||
|
||||
it('warns on a named re-export of Protocol with module-scoped guidance', () => {
|
||||
const input = `export { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
it('warns on a named re-export of a removed symbol with module-scoped guidance', () => {
|
||||
const input = `export { GoneClass } from '${SYNTHETIC}';\n`;
|
||||
const { result } = applyWithDiagnostics(input);
|
||||
const diag = result.diagnostics.find(d => d.message.includes('Re-exported Protocol has no v2 export'));
|
||||
const diag = result.diagnostics.find(d => d.message.includes('Re-exported GoneClass has no v2 export'));
|
||||
expect(diag).toBeDefined();
|
||||
expect(diag?.message).toContain('fallbackRequestHandler');
|
||||
expect(diag?.message).toContain('migration guide');
|
||||
});
|
||||
|
||||
it('flags a star re-export of a module with removed symbols', () => {
|
||||
const input = `export * from '@modelcontextprotocol/sdk/shared/protocol.js';\n`;
|
||||
const input = `export * from '${SYNTHETIC}';\n`;
|
||||
const { result } = applyWithDiagnostics(input);
|
||||
const messages = result.diagnostics.map(d => d.message);
|
||||
expect(messages.some(m => m.includes('Star re-export') && m.includes('Protocol'))).toBe(true);
|
||||
expect(messages.some(m => m.includes('Star re-export') && m.includes('mergeCapabilities'))).toBe(true);
|
||||
expect(messages.some(m => m.includes('Star re-export') && m.includes('GoneClass'))).toBe(true);
|
||||
expect(messages.some(m => m.includes('Star re-export') && m.includes('goneHelper'))).toBe(true);
|
||||
});
|
||||
|
||||
it('flags type-position namespace accesses (QualifiedName) to removed symbols', () => {
|
||||
const input = `import * as proto from '@modelcontextprotocol/sdk/shared/protocol.js';\nexport function f(p: proto.Protocol): void {}\n`;
|
||||
const input = `import * as synth from '${SYNTHETIC}';\nexport function f(p: synth.GoneClass): void {}\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toContain('@modelcontextprotocol/server');
|
||||
expect(result.diagnostics.some(d => d.insertComment && d.message.includes('Protocol base class'))).toBe(true);
|
||||
expect(result.diagnostics.some(d => d.insertComment && d.message.includes('GoneClass base class'))).toBe(true);
|
||||
});
|
||||
|
||||
it('anchors the dropped-symbol marker to a usage site that survives the rewrite', () => {
|
||||
const project = new Project({ useInMemoryFileSystem: true });
|
||||
const sourceFile = project.createSourceFile(
|
||||
'test.ts',
|
||||
`import { Protocol } from '@modelcontextprotocol/sdk/shared/protocol.js';\n\nexport class Mine extends Protocol {}\n`
|
||||
`import { GoneClass } from '${SYNTHETIC}';\n\nexport class Mine extends GoneClass {}\n`
|
||||
);
|
||||
const result = importPathsTransform.apply(sourceFile, { projectType: 'server' });
|
||||
const diag = result.diagnostics.find(d => d.message.includes('Protocol base class'));
|
||||
const diag = result.diagnostics.find(d => d.message.includes('GoneClass base class'));
|
||||
expect(diag).toBeDefined();
|
||||
// resolveCurrentLine resolves against the live usage node, not the removed import.
|
||||
const finalLine =
|
||||
sourceFile
|
||||
.getFullText()
|
||||
.split('\n')
|
||||
.findIndex(l => l.includes('extends Protocol')) + 1;
|
||||
.findIndex(l => l.includes('extends GoneClass')) + 1;
|
||||
expect(diag?.resolveCurrentLine?.()).toBe(finalLine);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { Project } from 'ts-morph';
|
||||
|
||||
import { IMPORT_MAP } from '../../../src/migrations/v1-to-v2/mappings/importMap';
|
||||
import { mockPathsTransform } from '../../../src/migrations/v1-to-v2/transforms/mockPaths';
|
||||
import type { TransformContext } from '../../../src/types';
|
||||
|
||||
|
|
@ -578,19 +579,49 @@ describe('removed symbols in mocks and dynamic imports', () => {
|
|||
return { text: sourceFile.getFullText(), result };
|
||||
}
|
||||
|
||||
it('leaves a mock factory providing Protocol unrewritten and flags it', () => {
|
||||
const input = `vi.mock('@modelcontextprotocol/sdk/shared/protocol.js', () => ({ Protocol: class {} }));\n`;
|
||||
const SYNTHETIC = '@modelcontextprotocol/sdk/shared/synthetic.js';
|
||||
|
||||
beforeAll(() => {
|
||||
IMPORT_MAP[SYNTHETIC] = {
|
||||
target: 'RESOLVE_BY_CONTEXT',
|
||||
status: 'moved',
|
||||
removedSymbols: {
|
||||
GoneClass: 'The GoneClass base class is not exported by the v2 packages.'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete IMPORT_MAP[SYNTHETIC];
|
||||
});
|
||||
|
||||
it('rewrites a mock factory providing Protocol like other surviving symbols', () => {
|
||||
const input = `vi.mock('@modelcontextprotocol/sdk/shared/protocol.js', () => ({ Protocol: class {} }));\nimport '@modelcontextprotocol/sdk/server/mcp.js';\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toContain('@modelcontextprotocol/sdk/shared/protocol.js');
|
||||
expect(text).toContain(`vi.mock('@modelcontextprotocol/server'`);
|
||||
expect(result.diagnostics.filter(d => d.insertComment)).toEqual([]);
|
||||
});
|
||||
|
||||
it('rewrites a dynamic import destructuring Protocol', () => {
|
||||
const input = `const { Protocol } = await import('@modelcontextprotocol/sdk/shared/protocol.js');\nexport { Protocol };\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toContain(`await import('@modelcontextprotocol/server')`);
|
||||
expect(result.diagnostics.filter(d => d.insertComment)).toEqual([]);
|
||||
});
|
||||
|
||||
it('leaves a mock factory providing a removed symbol unrewritten and flags it', () => {
|
||||
const input = `vi.mock('${SYNTHETIC}', () => ({ GoneClass: class {} }));\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toContain(SYNTHETIC);
|
||||
const diag = result.diagnostics.find(d => d.insertComment);
|
||||
expect(diag?.message).toContain('Protocol');
|
||||
expect(diag?.message).toContain('GoneClass');
|
||||
expect(diag?.message).toContain('no v2 package exports');
|
||||
});
|
||||
|
||||
it('leaves a dynamic import destructuring Protocol unrewritten and flags it', () => {
|
||||
const input = `const { Protocol } = await import('@modelcontextprotocol/sdk/shared/protocol.js');\nexport { Protocol };\n`;
|
||||
it('leaves a dynamic import destructuring a removed symbol unrewritten and flags it', () => {
|
||||
const input = `const { GoneClass } = await import('${SYNTHETIC}');\nexport { GoneClass };\n`;
|
||||
const { text, result } = applyWithDiagnostics(input);
|
||||
expect(text).toContain('@modelcontextprotocol/sdk/shared/protocol.js');
|
||||
expect(text).toContain(SYNTHETIC);
|
||||
const diag = result.diagnostics.find(d => d.insertComment);
|
||||
expect(diag?.message).toContain('undefined at runtime');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue