Skip to content

Commit b019acc

Browse files
committed
fix(test): move MCP smoke test to manual suite; harden unobfuscated detection
- Move __tests__/mcp/stdio-server-smoke.test.ts into __tests__/manual/mcp/ so heavy decompile-on-first-run tests are excluded from the default npm test run; remove the now-redundant test:mcp:e2e script - Fix misleading timeout comment on the unobfuscated-yarn throw test (throws before any JAR download, only needs a version JSON fetch) - Add UNOBFUSCATED_VERSION_OVERRIDES escape hatch in VersionManager for future Mojang metadata anomalies - Simplify isVersionUnobfuscated to time-gate only; drop the redundant version-id regex guard (client_mappings check + cutoff is sufficient) - Extend version-manager tests to cover the exact 26.1-snapshot-1 boundary and a newer snapshot (26.1-snapshot-9) - Refactor stdio-matrix test constants to a flat version list; classify obfuscated vs unobfuscated at runtime via isVersionUnobfuscated() rather than a hardcoded version-id pattern
1 parent 5b126d7 commit b019acc

8 files changed

Lines changed: 85 additions & 93 deletions

File tree

__tests__/core/remap-service.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ describe('JAR Remapping', () => {
382382
await expect(remapService.getRemappedJar(UNOBFUSCATED_TEST_VERSION, 'yarn')).rejects.toThrow(
383383
/yarn mappings are not supported for unobfuscated/i,
384384
);
385-
}, 60000); // includes client JAR download on first run
385+
}, 60000); // network call to fetch version JSON on first run
386386

387387
it('should throw a clear error when requesting intermediary mappings for an unobfuscated version', async () => {
388388
const remapService = getRemapService();

__tests__/core/version-manager.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,17 @@ describe('Version Management', () => {
5858
const result = await versionManager.isVersionUnobfuscated(UNOBFUSCATED_TEST_VERSION);
5959
expect(result).toBe(true);
6060
}, 30000);
61+
62+
it('should return true for first unobfuscated boundary version (26.1-snapshot-1)', async () => {
63+
const versionManager = getVersionManager();
64+
const result = await versionManager.isVersionUnobfuscated('26.1-snapshot-1');
65+
expect(result).toBe(true);
66+
}, 30000);
67+
68+
it('should return true for newer unobfuscated snapshot versions (26.1-snapshot-9)', async () => {
69+
const versionManager = getVersionManager();
70+
const result = await versionManager.isVersionUnobfuscated('26.1-snapshot-9');
71+
expect(result).toBe(true);
72+
}, 30000);
6173
});
6274
});

__tests__/manual/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ Example:
8181
cross-env MCP_E2E_VERSIONS=1.21.11,1.20.1,26.1-snapshot-1,26.1-snapshot-9 npm run test:manual:mcp
8282
```
8383

84+
Versions are classified at runtime using Mojang metadata via
85+
`VersionManager.isVersionUnobfuscated()`, not by hardcoded version-id patterns.
86+
8487
Default matrix includes:
8588
- `1.21.11`
8689
- `1.21.10`

__tests__/manual/mcp/stdio-matrix.test.ts

Lines changed: 37 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,23 @@ import {
44
createMcpSession,
55
extractFirstText,
66
} from '../../helpers/mcp-stdio.js';
7+
import { getVersionManager } from '../../../src/services/version-manager.js';
78
import { parseMatrixVersionsFromEnv } from './test-constants.js';
89

9-
const { obfuscatedVersions, unobfuscatedVersions } = parseMatrixVersionsFromEnv();
10+
const matrixVersions = parseMatrixVersionsFromEnv();
11+
const unobfuscatedCache = new Map<string, boolean>();
12+
13+
async function isUnobfuscatedVersion(version: string): Promise<boolean> {
14+
const cached = unobfuscatedCache.get(version);
15+
if (cached !== undefined) {
16+
return cached;
17+
}
18+
19+
const versionManager = getVersionManager();
20+
const result = await versionManager.isVersionUnobfuscated(version);
21+
unobfuscatedCache.set(version, result);
22+
return result;
23+
}
1024

1125
/**
1226
* Manual matrix for true MCP stdio E2E validation.
@@ -45,8 +59,10 @@ describe('Manual MCP Stdio Matrix', () => {
4559
expect(resources.resources.length).toBeGreaterThan(0);
4660
}, 30000);
4761

48-
for (const version of obfuscatedVersions) {
49-
it(`should decompile ${version} with yarn over stdio`, async () => {
62+
for (const version of matrixVersions) {
63+
it(`should enforce expected yarn behavior for ${version} over stdio`, async () => {
64+
const unobfuscated = await isUnobfuscatedVersion(version);
65+
5066
const result = await session.client.callTool(
5167
{
5268
name: 'decompile_minecraft_version',
@@ -60,21 +76,31 @@ describe('Manual MCP Stdio Matrix', () => {
6076
longRequest,
6177
);
6278

63-
expect(result.isError).not.toBe(true);
6479
const text = extractFirstText(result.content);
65-
expect(text).toContain(version);
66-
expect(text).toContain('yarn');
67-
expect(text).toMatch(/completed|classes/i);
80+
if (unobfuscated) {
81+
expect(result.isError).toBe(true);
82+
expect(text).toContain('mojmap');
83+
expect(text).toMatch(/use ['"]mojmap['"] mapping/i);
84+
} else {
85+
expect(result.isError).not.toBe(true);
86+
expect(text).toContain(version);
87+
expect(text).toContain('yarn');
88+
expect(text).toMatch(/completed|classes/i);
89+
}
6890
}, 900000);
6991

70-
it(`should return Entity source for ${version} via stdio`, async () => {
92+
it(`should return MinecraftServer source for ${version} using the expected mapping over stdio`, async () => {
93+
const unobfuscated = await isUnobfuscatedVersion(version);
94+
const mapping = unobfuscated ? 'mojmap' : 'yarn';
95+
const className = 'net.minecraft.server.MinecraftServer';
96+
7197
const result = await session.client.callTool(
7298
{
7399
name: 'get_minecraft_source',
74100
arguments: {
75101
version,
76-
className: 'net.minecraft.entity.Entity',
77-
mapping: 'yarn',
102+
className,
103+
mapping,
78104
},
79105
},
80106
undefined,
@@ -83,50 +109,7 @@ describe('Manual MCP Stdio Matrix', () => {
83109

84110
expect(result.isError).not.toBe(true);
85111
const text = extractFirstText(result.content);
86-
expect(text).toContain('class Entity');
112+
expect(text).toContain('class MinecraftServer');
87113
}, 600000);
88114
}
89-
90-
for (const unobfuscatedVersion of unobfuscatedVersions) {
91-
it(`should reject yarn for unobfuscated ${unobfuscatedVersion} over stdio`, async () => {
92-
const result = await session.client.callTool(
93-
{
94-
name: 'decompile_minecraft_version',
95-
arguments: {
96-
version: unobfuscatedVersion,
97-
mapping: 'yarn',
98-
force: false,
99-
},
100-
},
101-
undefined,
102-
longRequest,
103-
);
104-
105-
expect(result.isError).toBe(true);
106-
const text = extractFirstText(result.content);
107-
expect(text).toContain('mojmap');
108-
expect(text).toMatch(/use ['"]mojmap['"] mapping/i);
109-
}, 120000);
110-
111-
it(`should decompile unobfuscated ${unobfuscatedVersion} with mojmap over stdio`, async () => {
112-
const result = await session.client.callTool(
113-
{
114-
name: 'decompile_minecraft_version',
115-
arguments: {
116-
version: unobfuscatedVersion,
117-
mapping: 'mojmap',
118-
force: false,
119-
},
120-
},
121-
undefined,
122-
longRequest,
123-
);
124-
125-
expect(result.isError).not.toBe(true);
126-
const text = extractFirstText(result.content);
127-
expect(text).toContain(unobfuscatedVersion);
128-
expect(text).toContain('mojmap');
129-
expect(text).toMatch(/completed|classes/i);
130-
}, 900000);
131-
}
132115
});
File renamed without changes.
Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,21 @@
1-
export const DEFAULT_OBFUSCATED_MATRIX_VERSIONS = [
1+
export const DEFAULT_MATRIX_VERSIONS = [
22
'1.21.11',
33
'1.21.10',
44
'1.20.1',
55
'1.19.4',
6-
] as const;
7-
export const DEFAULT_UNOBFUSCATED_MATRIX_VERSIONS = [
86
'26.1-snapshot-1',
97
'26.1-snapshot-8',
108
'26.1-snapshot-9',
119
] as const;
1210

13-
function isUnobfuscatedSnapshot(version: string): boolean {
14-
return /^26\.1-snapshot-\d+$/.test(version);
15-
}
16-
17-
export function parseMatrixVersionsFromEnv(): {
18-
obfuscatedVersions: string[];
19-
unobfuscatedVersions: string[];
20-
} {
11+
export function parseMatrixVersionsFromEnv(): string[] {
2112
const envList = process.env.MCP_E2E_VERSIONS?.trim();
2213
if (!envList) {
23-
return {
24-
obfuscatedVersions: [...DEFAULT_OBFUSCATED_MATRIX_VERSIONS],
25-
unobfuscatedVersions: [...DEFAULT_UNOBFUSCATED_MATRIX_VERSIONS],
26-
};
14+
return [...DEFAULT_MATRIX_VERSIONS];
2715
}
2816

29-
const requested = envList
17+
return envList
3018
.split(',')
3119
.map((value) => value.trim())
3220
.filter(Boolean);
33-
34-
const obfuscatedVersions = requested.filter((version) => !isUnobfuscatedSnapshot(version));
35-
const unobfuscatedVersions = requested.filter(isUnobfuscatedSnapshot);
36-
37-
return {
38-
obfuscatedVersions,
39-
unobfuscatedVersions,
40-
};
4121
}

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
"inspect": "npm run build && npx @modelcontextprotocol/inspector node dist/index.js",
1616
"inspect:dev": "npx @modelcontextprotocol/inspector tsx src/index.ts",
1717
"test": "vitest",
18-
"test:mcp:e2e": "vitest __tests__/mcp",
1918
"test:ui": "vitest --ui",
2019
"test:manual": "vitest --config vitest.manual.config.ts",
2120
"test:manual:mcp": "vitest --config vitest.manual.config.ts __tests__/manual/mcp",

src/services/version-manager.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ export class VersionManager {
1313
* 26.1-snapshot-1 released at this timestamp and removed client obfuscation.
1414
*/
1515
private static readonly UNOBFUSCATED_CUTOFF_MS = Date.parse('2025-12-16T12:42:29+00:00');
16+
/**
17+
* Emergency overrides for Mojang metadata anomalies.
18+
* Keyed by exact version id from version JSON.
19+
*/
20+
private static readonly UNOBFUSCATED_VERSION_OVERRIDES: Readonly<Record<string, boolean>> = {};
1621

1722
private downloader = getMojangDownloader();
1823
private cache = getCacheManager();
@@ -151,30 +156,40 @@ export class VersionManager {
151156
/**
152157
* Check if a Minecraft version ships an unobfuscated JAR.
153158
*
154-
* Starting with Minecraft 26.1 snapshots, Mojang stopped obfuscating the
155-
* client JAR. These versions have no `client_mappings` entry in their
156-
* version JSON because there is nothing to reverse-map.
159+
* Mojang's authoritative signal is the presence/absence of `client_mappings`
160+
* in the version JSON:
161+
* - present: obfuscated client JAR (reverse mapping required)
162+
* - absent: potentially unobfuscated client JAR
157163
*
158164
* Important: some older obfuscated versions (e.g. early 1.14.x) also lack
159-
* `client_mappings` metadata, so missing metadata alone is not sufficient.
165+
* `client_mappings` metadata, so we also gate by the known 26.1 cutover time.
160166
*/
161167
async isVersionUnobfuscated(version: string): Promise<boolean> {
162168
const versionJson = await this.downloader.getVersionJson(version);
169+
170+
const override = VersionManager.UNOBFUSCATED_VERSION_OVERRIDES[versionJson.id];
171+
if (override !== undefined) {
172+
logger.warn(
173+
`Using unobfuscated override for ${versionJson.id}: ${override ? 'unobfuscated' : 'obfuscated'}`,
174+
);
175+
return override;
176+
}
177+
163178
if (versionJson.downloads.client_mappings) {
164179
return false;
165180
}
166181

167-
// Early 1.14.x releases can be missing client_mappings metadata while still obfuscated.
168-
// Treat versions as unobfuscated only after the known 26.1 cutover and only for modern ids.
182+
// Early legacy versions can be missing client_mappings while still obfuscated.
183+
// Treat missing client_mappings as unobfuscated only at/after the known cutover.
169184
const releaseTimeMs = Date.parse(versionJson.releaseTime);
170-
const isAfterUnobfuscatedCutover =
171-
Number.isFinite(releaseTimeMs) && releaseTimeMs >= VersionManager.UNOBFUSCATED_CUTOFF_MS;
172-
173-
const modernVersionId = /^(\d+)\.(\d+)(?:\.\d+)?(?:-snapshot-\d+)?$/.exec(versionJson.id);
174-
const modernMajor = modernVersionId ? Number.parseInt(modernVersionId[1], 10) : Number.NaN;
175-
const isModernUnobfuscatedSeries = Number.isFinite(modernMajor) && modernMajor >= 26;
185+
if (!Number.isFinite(releaseTimeMs)) {
186+
logger.warn(
187+
`Version ${versionJson.id} has invalid releaseTime '${versionJson.releaseTime}', defaulting to obfuscated`,
188+
);
189+
return false;
190+
}
176191

177-
return isAfterUnobfuscatedCutover && isModernUnobfuscatedSeries;
192+
return releaseTimeMs >= VersionManager.UNOBFUSCATED_CUTOFF_MS;
178193
}
179194

180195
/**

0 commit comments

Comments
 (0)