codemod iterations 5 (#2398)

This commit is contained in:
Konstantin Konstantinov 2026-07-02 00:34:43 +03:00 • committed by GitHub
commit 1772473b76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 441 additions and 28 deletions

View file

@ -3,7 +3,7 @@ import { tmpdir } from 'node:os';
import path from 'node:path';
import { describe, it, expect, vi, afterEach } from 'vitest';
import { parseArgs, parseCodemodCliOutput, runCodemod } from '../src/bin/batchTest';
import { parseArgs, parseCodemodCliOutput, runCodemod, installCommand } from '../src/bin/batchTest';
import { computeResultsDirName, type ResolvedConfig } from '../src/bin/batchTest';
import { parseNpmViewVersion, rewriteToPublishedVersion, cleanSubprocessEnv } from '../src/bin/batchTest';
import { getMigration } from '../src/migrations/index';
@ -12,6 +12,35 @@ afterEach(() => {
vi.restoreAllMocks();
});
describe('installCommand', () => {
it('uses plain --ignore-scripts for non-pnpm managers', () => {
expect(installCommand('npm', { hasOwnPnpmWorkspace: false, packageDirs: ['.'] })).toBe('npm install --ignore-scripts');
expect(installCommand('yarn', { hasOwnPnpmWorkspace: true, packageDirs: ['packages/a'] })).toBe('yarn install --ignore-scripts');
});
it('isolates a pnpm clone with no workspace of its own via --ignore-workspace', () => {
expect(installCommand('pnpm', { hasOwnPnpmWorkspace: false, packageDirs: ['.'] })).toBe(
'pnpm install --ignore-scripts --ignore-workspace --no-frozen-lockfile'
);
});
it('respects a pnpm clone that is its own workspace and scopes the install to the target packages', () => {
const cmd = installCommand('pnpm', { hasOwnPnpmWorkspace: true, packageDirs: ['packages/core', 'packages/mcp'] });
expect(cmd).not.toContain('--ignore-workspace');
// braces required: pnpm ignores the `...` on a bare `./dir...` selector (drops workspace deps);
// `{./dir}...` includes them.
expect(cmd).toBe(
'pnpm install --ignore-scripts --no-frozen-lockfile --filter "{./packages/core}..." --filter "{./packages/mcp}..."'
);
});
it('installs the whole workspace (no --filter) when the only target is the clone root', () => {
expect(installCommand('pnpm', { hasOwnPnpmWorkspace: true, packageDirs: ['.'] })).toBe(
'pnpm install --ignore-scripts --no-frozen-lockfile'
);
});
});
describe('parseArgs', () => {
it('defaults to local/local with latest codemod version and unset sdk version', () => {
const opts = parseArgs([]);

View file

@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
import { Project } from 'ts-morph';
import { contextTypesTransform } from '../../../src/migrations/v1-to-v2/transforms/contextTypes';
import type { TransformContext } from '../../../src/types';
import type { Diagnostic, TransformContext } from '../../../src/types';
const ctx: TransformContext = { projectType: 'server' };
@ -637,3 +637,121 @@ describe('fallback handler and annotated context params (B5, #152)', () => {
expect(diag?.advisoryOnly).toBe(true);
});
});
describe('v1 mock context literals in tests (test doubles)', () => {
function apply(code: string) {
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', code);
const result = contextTypesTransform.apply(sourceFile, { projectType: 'server' });
return { text: sourceFile.getFullText(), result };
}
const IMPORT = `import { McpServer } from '@modelcontextprotocol/server';\n`;
const findMockDiag = (diags: Diagnostic[]) => diags.find(d => d.message.includes('v1 handler-context mock'));
it('advises (without rewriting) on a v1 context mock passed inline as a call argument', () => {
const code = IMPORT + [`const r = await handler({}, { sendRequest: mockSendRequest });`, ''].join('\n');
const { text, result } = apply(code);
// Advisory only — the literal is left untouched.
expect(text).toContain('{ sendRequest: mockSendRequest }');
const diag = findMockDiag(result.diagnostics);
expect(diag).toBeDefined();
expect(diag?.advisoryOnly).toBe(true);
expect(diag?.message).toContain('sendRequest → mcpReq.send');
});
it('advises on a v1 context mock bound to a variable, and notes sessionId stays top-level', () => {
const code = IMPORT + [`const extra = { sessionId: 'session-1', sendRequest: fn };`, `await handler(args, extra);`, ''].join('\n');
const { result } = apply(code);
const diag = findMockDiag(result.diagnostics);
expect(diag).toBeDefined();
expect(diag?.advisoryOnly).toBe(true);
expect(diag?.message).toContain('sessionId stays top-level');
});
it('advises on the _meta/requestId progress-notification mock shape', () => {
const code = IMPORT + [`await handler({ duration: 1 }, { _meta: {}, requestId: 'test-123' });`, ''].join('\n');
const { result } = apply(code);
const diag = findMockDiag(result.diagnostics);
expect(diag).toBeDefined();
expect(diag?.message).toContain('requestId → mcpReq.id');
});
it('advises when two generic context keys appear together (no distinctive key)', () => {
const code = IMPORT + [`await handler(args, { signal: ac.signal, sessionId: 's' });`, ''].join('\n');
const { result } = apply(code);
expect(findMockDiag(result.diagnostics)).toBeDefined();
});
it('does not advise on an already-migrated v2 context mock', () => {
const code = IMPORT + [`await handler({}, { mcpReq: { send: fn } });`, ''].join('\n');
const { result } = apply(code);
expect(findMockDiag(result.diagnostics)).toBeUndefined();
});
it('does not advise on a non-context object literal', () => {
const code = IMPORT + [`const config = { title: 'x', description: 'y' };`, ''].join('\n');
const { result } = apply(code);
expect(findMockDiag(result.diagnostics)).toBeUndefined();
});
it('does not advise on a lone generic key (needs a distinctive key or >=2 context keys)', () => {
const code = IMPORT + [`doThing({ signal: ac.signal });`, ''].join('\n');
const { result } = apply(code);
expect(findMockDiag(result.diagnostics)).toBeUndefined();
});
it('does not advise on a lone requestId (a bare correlation-ID literal, not a context mock)', () => {
const code = IMPORT + [`logger.info('handling lookup', { requestId: args.id });`, ''].join('\n');
const { result } = apply(code);
expect(findMockDiag(result.diagnostics)).toBeUndefined();
});
it('advises on a cast-wrapped inline mock (`{ … } as unknown as RequestHandlerExtra`)', () => {
const code = IMPORT + [`await handler(args, { sendRequest: fn } as unknown as RequestHandlerExtra);`, ''].join('\n');
const { result } = apply(code);
const diag = findMockDiag(result.diagnostics);
expect(diag).toBeDefined();
expect(diag?.advisoryOnly).toBe(true);
});
it('advises on a cast-wrapped variable mock (`const ctx = { … } as any`)', () => {
const code = IMPORT + [`const mockCtx = { requestId: 1, sendRequest: fn } as any;`, `await handler(args, mockCtx);`, ''].join('\n');
const { result } = apply(code);
expect(findMockDiag(result.diagnostics)).toBeDefined();
});
it('does not advise on already-v2 values (an options object reading ctx.mcpReq / ctx.sessionId)', () => {
const code = IMPORT + [`doThing({ signal: ctx.mcpReq.signal, sessionId: ctx.sessionId });`, ''].join('\n');
const { result } = apply(code);
expect(findMockDiag(result.diagnostics)).toBeUndefined();
});
it("does not re-flag the codemod's own migrated handler output", () => {
const code =
IMPORT +
[
`server.setRequestHandler('x', async (req, extra) => {`,
` await loadData(req.uri, { signal: extra.signal, sessionId: extra.sessionId });`,
` return {};`,
`});`,
''
].join('\n');
const { text, result } = apply(code);
// sanity: the handler body WAS migrated to the v2 nested shape …
expect(text).toContain('ctx.mcpReq.signal');
// … and the resulting options object is not mistaken for a stale v1 mock.
expect(findMockDiag(result.diagnostics)).toBeUndefined();
});
it('does not advise on a flat MessageExtraInfo literal passed to onmessage', () => {
const code = IMPORT + [`transport.onmessage(msg, { requestInfo: req, authInfo });`, ''].join('\n');
const { result } = apply(code);
expect(findMockDiag(result.diagnostics)).toBeUndefined();
});
it('does not advise on a flat MessageExtraInfo literal passed to handleMessage', () => {
const code = IMPORT + [`await transport.handleMessage(msg, { authInfo, closeSSEStream: fn });`, ''].join('\n');
const { result } = apply(code);
expect(findMockDiag(result.diagnostics)).toBeUndefined();
});
});

View file

@ -394,3 +394,72 @@ describe('variable-hop guard rails (B6 review)', () => {
expect(result.diagnostics.some(d => d.message.includes('Custom method handler'))).toBe(true);
});
});
describe('stale registration-schema references (analysis: mcp-servers/memory)', () => {
function applyWithDiagnostics(code: string) {
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile('test.ts', code);
const result = handlerRegistrationTransform.apply(sourceFile, { projectType: 'server' });
return { text: sourceFile.getFullText(), result };
}
it('flags a request schema used as a setRequestHandler-mock assertion arg with the method string', () => {
const code = [
`import { SubscribeRequestSchema } from '@modelcontextprotocol/sdk/types.js';`,
`const schemas = inner.setRequestHandler.mock.calls.map((c) => c[0]);`,
`expect(schemas).toContain(SubscribeRequestSchema);`,
''
].join('\n');
const { result } = applyWithDiagnostics(code);
const diag = result.diagnostics.find(
d => d.message.includes('SubscribeRequestSchema') && d.message.includes("'resources/subscribe'")
);
expect(diag).toBeDefined();
});
it('flags a schema passed to a registration-lookup helper', () => {
const code = [
`import { UnsubscribeRequestSchema } from '@modelcontextprotocol/sdk/types.js';`,
`const c = inner.setRequestHandler.mock.calls.find((x) => x[0] === s);`,
`handlerFor(inner, UnsubscribeRequestSchema);`,
''
].join('\n');
const { result } = applyWithDiagnostics(code);
expect(result.diagnostics.some(d => d.message.includes("'resources/unsubscribe'"))).toBe(true);
});
it('does NOT flag a request schema used as a schema (.parse) — property-access base', () => {
const code = [
`import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';`,
`server.setRequestHandler(CallToolRequestSchema, async () => ({ content: [] }));`,
`const parsed = CallToolRequestSchema.parse(x);`,
''
].join('\n');
const { result } = applyWithDiagnostics(code);
expect(result.diagnostics.some(d => d.message.includes('no longer the setRequestHandler'))).toBe(false);
});
it('does NOT flag in a file that performs no handler registration', () => {
const code = [
`import { SubscribeRequestSchema } from '@modelcontextprotocol/sdk/types.js';`,
`someValidator(SubscribeRequestSchema);`,
''
].join('\n');
const { result } = applyWithDiagnostics(code);
expect(result.diagnostics.some(d => d.message.includes('no longer the setRequestHandler'))).toBe(false);
});
it('does NOT flag a schema consumed by a validator when setRequestHandler is only a real registration', () => {
// The file registers a handler for real (a call the pass rewrites); an unrelated schema-consuming
// call like validateSchema(S) / zodToJsonSchema(S) is not a stale registration key — only a
// setRequestHandler MOCK/lookup reference signals that surviving schema refs are stale.
const code = [
`import { CallToolRequestSchema, SubscribeRequestSchema } from '@modelcontextprotocol/sdk/types.js';`,
`server.setRequestHandler(CallToolRequestSchema, async () => ({ content: [] }));`,
`validateSchema(SubscribeRequestSchema);`,
''
].join('\n');
const { result } = applyWithDiagnostics(code);
expect(result.diagnostics.some(d => d.message.includes('no longer the setRequestHandler'))).toBe(false);
});
});