mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-09-14 19:45:30 +08:00
fix(telemetry): resolve enum values through nested schema wrappers (#2315)
## What
`generateToolMetrics` reads an enum parameter's values by unwrapping
**at most
one** `optional` wrapper:
```ts
if (schema._def.values?.length > 0) {
values = schema._def.values;
} else {
values = schema._def.innerType._def.values; // only one level
}
```
An enum parameter wrapped in `.default().optional()` (or any nested
`optional`/`default`/`nullable` combination) therefore falls through to
`schema._def.innerType._def.values === undefined`, and `npm run
update-metrics`
(run as part of `npm run gen`) crashes:
```
TypeError: Cannot read properties of undefined (reading '0')
at validateEnumHomogeneity (build/src/telemetry/metricsRegistry.js:12)
at generateToolMetrics (build/src/telemetry/metricsRegistry.js:59)
```
## Why it matters
`npm run gen` is required whenever a tool is added or changed (per
`CONTRIBUTING.md`). Declaring an enum parameter with a default — a
natural,
documented zod pattern — silently breaks docs/metrics generation.
## Fix
Add `getEnumValues()` in `transformation.ts` that recursively unwraps
`optional` / `default` / `nullable` / `effects` wrappers, mirroring the
existing
`getZodType()`, and use it in `generateToolMetrics`. Behavior is
unchanged for
the existing bare-enum and single-`optional` cases.
## Tests
- `getEnumValues` unit tests: bare enum, `optional`, `default`, both
orders of
`default`+`optional`, and throws for a non-enum type.
- `generateToolMetrics` regression test with
`zod.enum(...).default(...).optional()`.
- `npm run typecheck`, the telemetry tests, and `npm run check-format`
pass.
---
First-time contributor here — happy to sign the CLA.
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
transformArgName,
|
||||
transformArgType,
|
||||
getZodType,
|
||||
getEnumValues,
|
||||
PARAM_BLOCKLIST,
|
||||
stripUnderscoreBeforeNumber,
|
||||
} from './transformation.js';
|
||||
@@ -103,13 +104,7 @@ export function generateToolMetrics(tools: ToolDefinition[]): ToolMetric[] {
|
||||
let argType = transformArgType(zodType);
|
||||
|
||||
if (argType === 'enum') {
|
||||
let values;
|
||||
if (schema._def.values?.length > 0) {
|
||||
values = schema._def.values;
|
||||
} else {
|
||||
values = schema._def.innerType._def.values;
|
||||
}
|
||||
argType = validateEnumHomogeneity(values);
|
||||
argType = validateEnumHomogeneity(getEnumValues(schema));
|
||||
}
|
||||
|
||||
args.push({
|
||||
|
||||
@@ -53,6 +53,30 @@ export function getZodType(zodType: zod.ZodTypeAny): ZodType {
|
||||
throw new Error(`Unsupported zod type for tool parameter: ${typeName}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the values of an enum parameter, unwrapping any optional/default/
|
||||
* nullable/effects wrappers (in any order), mirroring {@link getZodType}.
|
||||
*/
|
||||
export function getEnumValues(zodType: zod.ZodTypeAny): unknown[] {
|
||||
const def = zodType._def;
|
||||
const typeName = def.typeName;
|
||||
|
||||
if (
|
||||
typeName === 'ZodOptional' ||
|
||||
typeName === 'ZodDefault' ||
|
||||
typeName === 'ZodNullable'
|
||||
) {
|
||||
return getEnumValues(def.innerType);
|
||||
}
|
||||
if (typeName === 'ZodEffects') {
|
||||
return getEnumValues(def.schema);
|
||||
}
|
||||
if (typeName === 'ZodEnum') {
|
||||
return def.values;
|
||||
}
|
||||
throw new Error(`Cannot resolve enum values for zod type: ${typeName}`);
|
||||
}
|
||||
|
||||
export function stripUnderscoreBeforeNumber(name: string): string {
|
||||
return name.replace(/_([0-9])/g, '$1');
|
||||
}
|
||||
|
||||
@@ -85,6 +85,30 @@ describe('metricsRegistry', () => {
|
||||
assert.strictEqual(metrics[0].args[0].argType, 'string');
|
||||
});
|
||||
|
||||
it('should handle enums wrapped in optional and default', () => {
|
||||
const mockTool: ToolDefinition = {
|
||||
name: 'wrapped_enum_tool',
|
||||
description: 'test description',
|
||||
annotations: {
|
||||
category: ToolCategory.INPUT,
|
||||
readOnlyHint: true,
|
||||
},
|
||||
schema: {
|
||||
argEnum: zod.enum(['foo', 'bar']).default('foo').optional(),
|
||||
},
|
||||
blockedByDialog: false,
|
||||
verifyFilesSchema: [],
|
||||
handler: async () => {
|
||||
// no-op
|
||||
},
|
||||
};
|
||||
|
||||
const metrics = generateToolMetrics([mockTool]);
|
||||
assert.strictEqual(metrics.length, 1);
|
||||
assert.strictEqual(metrics[0].args[0].name, 'arg_enum');
|
||||
assert.strictEqual(metrics[0].args[0].argType, 'string');
|
||||
});
|
||||
|
||||
it('should sanitize tool names containing underscores before numbers', () => {
|
||||
const mockTool: ToolDefinition = {
|
||||
name: 'list_3p_developer_tools',
|
||||
|
||||
@@ -9,12 +9,45 @@ import {describe, it} from 'node:test';
|
||||
|
||||
import {
|
||||
bucketizeLatency,
|
||||
getEnumValues,
|
||||
sanitizeParams,
|
||||
stripUnderscoreBeforeNumber,
|
||||
transformArgName,
|
||||
} from '../../src/telemetry/transformation.js';
|
||||
import {zod} from '../../src/third_party/index.js';
|
||||
|
||||
describe('getEnumValues', () => {
|
||||
it('resolves values for a bare enum', () => {
|
||||
assert.deepStrictEqual(getEnumValues(zod.enum(['a', 'b'])), ['a', 'b']);
|
||||
});
|
||||
|
||||
it('resolves values through optional/default wrappers in any order', () => {
|
||||
assert.deepStrictEqual(getEnumValues(zod.enum(['a', 'b']).optional()), [
|
||||
'a',
|
||||
'b',
|
||||
]);
|
||||
assert.deepStrictEqual(getEnumValues(zod.enum(['a', 'b']).default('a')), [
|
||||
'a',
|
||||
'b',
|
||||
]);
|
||||
assert.deepStrictEqual(
|
||||
getEnumValues(zod.enum(['a', 'b']).default('a').optional()),
|
||||
['a', 'b'],
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
getEnumValues(zod.enum(['a', 'b']).optional().default('a')),
|
||||
['a', 'b'],
|
||||
);
|
||||
});
|
||||
|
||||
it('throws for a non-enum type', () => {
|
||||
assert.throws(
|
||||
() => getEnumValues(zod.string()),
|
||||
/Cannot resolve enum values/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bucketizeLatency', () => {
|
||||
it('should bucketize values correctly', () => {
|
||||
assert.strictEqual(bucketizeLatency(0), 50);
|
||||
|
||||
Reference in New Issue
Block a user