Skip to content

Commit af046a6

Browse files
committed
test: guard response transform field ownership
1 parent 9a91f47 commit af046a6

3 files changed

Lines changed: 54 additions & 4 deletions

File tree

‎src/commands/__tests__/command-surface-metadata.test.ts‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import assert from 'node:assert/strict';
22
import { test } from 'vitest';
3-
import { listMcpExposedCommandNames } from '../../core/command-descriptor/registry.ts';
3+
import {
4+
listCommandResponseDataTransforms,
5+
listMcpExposedCommandNames,
6+
} from '../../core/command-descriptor/registry.ts';
47
import {
58
listCommandMetadata,
69
listCommandMetadataNames,
@@ -49,6 +52,25 @@ test('common command input accepts web platform selector', () => {
4952
assert.equal(input.platform, 'web');
5053
});
5154

55+
test('command response data transforms reference command input fields', () => {
56+
const metadataByName = new Map<string, ReturnType<typeof listCommandMetadata>[number]>(
57+
listCommandMetadata().map((metadata) => [metadata.name, metadata] as const),
58+
);
59+
60+
for (const { command, transform } of listCommandResponseDataTransforms()) {
61+
const metadata = metadataByName.get(command);
62+
assert.ok(metadata, `${command} response data transform must have command metadata`);
63+
64+
const inputFields = new Set(Object.keys(metadata.inputSchema.properties ?? {}));
65+
for (const field of Object.keys(transform.fields)) {
66+
assert.ok(
67+
inputFields.has(field),
68+
`${command} response data transform field ${field} must be declared by command input metadata`,
69+
);
70+
}
71+
}
72+
});
73+
5274
test('command family facets expose one complete metadata and executable surface', () => {
5375
const familyNames = commandFamilies.map((family) => family.name);
5476
assert.deepEqual(familyNames, [...new Set(familyNames)], 'command family names must be unique');

‎src/core/command-descriptor/registry.ts‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,6 @@ const TOUCH_INTERACTION_RESPONSE_DATA_TRANSFORM = {
141141

142142
const FILL_INTERACTION_RESPONSE_DATA_TRANSFORM = {
143143
fields: {
144-
...TOUCH_INTERACTION_RESPONSE_DATA_TRANSFORM.fields,
145144
delayMs: { defaultValue: 0 },
146145
},
147146
} as const satisfies CommandResponseDataTransform;
@@ -840,3 +839,23 @@ export function resolveCommandResponseDataTransform(
840839
if (command === undefined) return undefined;
841840
return RESPONSE_DATA_TRANSFORM_BY_COMMAND.get(command);
842841
}
842+
843+
export function listCommandResponseDataTransforms(): Array<{
844+
command: string;
845+
transform: CommandResponseDataTransform;
846+
}> {
847+
return Array.from(RESPONSE_DATA_TRANSFORM_BY_COMMAND, ([command, transform]) => ({
848+
command,
849+
transform,
850+
}));
851+
}
852+
853+
export function listCommandResponseDataTransformFieldNames(): string[] {
854+
return [
855+
...new Set(
856+
Array.from(RESPONSE_DATA_TRANSFORM_BY_COMMAND.values()).flatMap((transform) =>
857+
Object.keys(transform.fields),
858+
),
859+
),
860+
].sort();
861+
}

‎src/core/interaction-response-data-transform.ts‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
import { resolveCommandResponseDataTransform } from './command-descriptor/registry.ts';
1+
import {
2+
listCommandResponseDataTransformFieldNames,
3+
resolveCommandResponseDataTransform,
4+
} from './command-descriptor/registry.ts';
25
import type { ResponseDataFieldTransform } from './command-descriptor/types.ts';
36

47
export type InteractionResponseDataTransformCommand = 'click' | 'press' | 'fill';
58

9+
const controlledResponseDataFieldNames = new Set(listCommandResponseDataTransformFieldNames());
10+
611
export function transformInteractionResponseData(params: {
712
command: InteractionResponseDataTransformCommand;
813
input: Record<string, unknown> | undefined;
@@ -12,6 +17,7 @@ export function transformInteractionResponseData(params: {
1217
fields: readCommandResponseDataTransformFields(params.command),
1318
input: params.input ?? {},
1419
data: params.data,
20+
controlledFields: controlledResponseDataFieldNames,
1521
});
1622
}
1723

@@ -29,8 +35,11 @@ function applyResponseDataFieldTransforms(params: {
2935
fields: Record<string, ResponseDataFieldTransform>;
3036
input: Record<string, unknown>;
3137
data: Record<string, unknown> | undefined;
38+
controlledFields: ReadonlySet<string>;
3239
}): Record<string, unknown> {
33-
const transformed = { ...(params.data ?? {}) };
40+
const transformed = Object.fromEntries(
41+
Object.entries(params.data ?? {}).filter(([key]) => !params.controlledFields.has(key)),
42+
);
3443
for (const [key, field] of Object.entries(params.fields)) {
3544
const value = params.input[key] === undefined ? field.defaultValue : params.input[key];
3645
if (value === undefined || (field.omitDefault === true && value === field.defaultValue)) {

0 commit comments

Comments
 (0)