Skip to content

Commit 0631e84

Browse files
committed
fix: keep wait absent coverage gates green
1 parent 2c12e7d commit 0631e84

5 files changed

Lines changed: 195 additions & 184 deletions

File tree

scripts/__tests__/eager-closure-budgets.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,11 @@ export const HUB_BUDGETS: Readonly<Record<string, number>> = Object.freeze({
411411
// #2148 moves output-only CLI dependencies behind call-time imports and reduces the entry
412412
// closure by two modules.
413413
// #2146 splits one eagerly reached URL utility into its client and Metro owners.
414-
'src/cli.ts': 380,
414+
// #2236 adds the typed pre-admission --scope/--depth refusal for `wait absent` to the existing
415+
// wait command reader. That deliberately keeps the shared absence option contract and error
416+
// modules on the CLI path; the measured two-module growth is the contract being loaded, not
417+
// implementation or platform machinery being pulled in eagerly.
418+
'src/cli.ts': 382,
415419
'src/platform-runtime.ts': 47,
416420
'src/core/command-descriptor/registry.ts': 72,
417421
'src/core/command-descriptor/platform-execution-entry.ts': 3,

src/__tests__/client-wait.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'vitest';
3+
import { createAgentDeviceClient } from '../agent-device-client.ts';
4+
import { createTransport } from './client-transport-fixture.ts';
5+
6+
test('client.command.wait round-trips strict wait absent positionals', async () => {
7+
const setup = createTransport(async () => ({ ok: true, data: { waitedMs: 0 } }));
8+
const client = createAgentDeviceClient(setup.config, { transport: setup.transport });
9+
10+
await client.command.wait({ absent: 'label="Removed"', timeoutMs: 2500 });
11+
12+
assert.equal(setup.calls[0]?.command, 'wait');
13+
assert.deepEqual(setup.calls[0]?.positionals, ['absent', 'label="Removed"', '2500']);
14+
15+
await assert.rejects(
16+
async () => await client.command.wait({ absent: 'label="Removed"', depth: 2 }),
17+
/wait absent does not support --depth/,
18+
);
19+
await assert.rejects(
20+
async () => await client.command.wait({ absent: 'label="Removed"', scope: 'Root' }),
21+
/wait absent does not support --scope/,
22+
);
23+
});

src/__tests__/client.test.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,25 +1015,6 @@ test('client.command.wait prepares selector options and rejects invalid selector
10151015
assert.equal(setup.calls.length, 1);
10161016
});
10171017

1018-
test('client.command.wait round-trips strict wait absent positionals', async () => {
1019-
const setup = createTransport(async () => ({ ok: true, data: { waitedMs: 0 } }));
1020-
const client = createAgentDeviceClient(setup.config, { transport: setup.transport });
1021-
1022-
await client.command.wait({ absent: 'label="Removed"', timeoutMs: 2500 });
1023-
1024-
assert.equal(setup.calls[0]?.command, 'wait');
1025-
assert.deepEqual(setup.calls[0]?.positionals, ['absent', 'label="Removed"', '2500']);
1026-
1027-
await assert.rejects(
1028-
async () => await client.command.wait({ absent: 'label="Removed"', depth: 2 }),
1029-
/wait absent does not support --depth/,
1030-
);
1031-
await assert.rejects(
1032-
async () => await client.command.wait({ absent: 'label="Removed"', scope: 'Root' }),
1033-
/wait absent does not support --scope/,
1034-
);
1035-
});
1036-
10371018
test('lease helpers forward scope through daemon-backed client methods', async () => {
10381019
const setup = createTransport(async (req) => ({
10391020
ok: true,
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'vitest';
3+
import { listCommandTools } from '../command-tools.ts';
4+
5+
// Guidance no longer restates input fields in prose, so a tool's inputSchema is the only
6+
// place its inputs are documented — for the model, for `--help`, and for the docs site. An
7+
// undescribed property is therefore a silent gap rather than a cosmetic one.
8+
//
9+
// The baseline pins exact `tool.property` identities, not bare property names plus a total:
10+
// a name-and-count baseline stays green when a gap migrates (describe `foo.text`, add an
11+
// undescribed `bar.text`, and both the allowed-name set and the total are unchanged), and
12+
// stale names keep authorizing gaps that appear later. Exact identities make every new or
13+
// moved gap fail, and require deleting an entry to record a fix.
14+
const UNDESCRIBED_TOOL_INPUTS = new Set([
15+
'alert.action',
16+
'alert.timeoutMs',
17+
'audio.action',
18+
'audio.probeAction',
19+
'back.mode',
20+
'clipboard.action',
21+
'clipboard.text',
22+
'close.saveScript',
23+
'debug.action',
24+
'diff.depth',
25+
'diff.interactiveOnly',
26+
'diff.kind',
27+
'diff.out',
28+
'diff.raw',
29+
'diff.scope',
30+
'events.cursor',
31+
'events.limit',
32+
'find.action',
33+
'find.depth',
34+
'find.first',
35+
'find.last',
36+
'find.locator',
37+
'find.query',
38+
'find.raw',
39+
'find.timeoutMs',
40+
'find.value',
41+
'get.format',
42+
'install-from-source.retainPaths',
43+
'install-from-source.retentionMs',
44+
'is.predicate',
45+
'is.selector',
46+
'is.value',
47+
'keyboard.action',
48+
'logs.action',
49+
'logs.message',
50+
'logs.restart',
51+
'metro.action',
52+
'metro.bridgeScope',
53+
'metro.bundleUrl',
54+
'metro.installDependenciesIfNeeded',
55+
'metro.kind',
56+
'metro.launchUrl',
57+
'metro.listenHost',
58+
'metro.logPath',
59+
'metro.metroHost',
60+
'metro.metroPort',
61+
'metro.port',
62+
'metro.probeTimeoutMs',
63+
'metro.projectRoot',
64+
'metro.publicBaseUrl',
65+
'metro.reuseExisting',
66+
'metro.runtimeFilePath',
67+
'metro.startupTimeoutMs',
68+
'metro.statusHost',
69+
'metro.timeoutMs',
70+
'network.action',
71+
'network.include',
72+
'network.limit',
73+
'open.saveScript',
74+
'orientation.orientation',
75+
'perf.action',
76+
'perf.area',
77+
'perf.kind',
78+
'perf.subject',
79+
'push.app',
80+
'push.payload',
81+
'react-native.action',
82+
'record.action',
83+
'record.fps',
84+
'record.hideTouches',
85+
'record.path',
86+
'record.quality',
87+
'record.recordingScope',
88+
'reinstall.app',
89+
'replay.backend',
90+
'replay.env',
91+
'replay.force',
92+
'replay.maestro',
93+
'replay.path',
94+
'replay.resumeFrom',
95+
'replay.resumePlanDigest',
96+
'replay.saveScript',
97+
'replay.update',
98+
'screenshot.fullscreen',
99+
'screenshot.normalizeStatusBar',
100+
'screenshot.overlayRefs',
101+
'screenshot.stabilize',
102+
'screenshot.surface',
103+
'scroll.direction',
104+
'settings.app',
105+
'settings.latitude',
106+
'settings.longitude',
107+
'settings.mode',
108+
'settings.permission',
109+
'settings.setting',
110+
'settings.state',
111+
'snapshot.depth',
112+
'snapshot.forceFull',
113+
'snapshot.interactiveOnly',
114+
'snapshot.raw',
115+
'snapshot.scope',
116+
'swipe.pattern',
117+
'test.artifactsDir',
118+
'test.backend',
119+
'test.env',
120+
'test.failFast',
121+
'test.maestro',
122+
'test.paths',
123+
'test.recordVideo',
124+
'test.retries',
125+
'test.shardAll',
126+
'test.shardSplit',
127+
'test.timeoutMs',
128+
'test.update',
129+
'trace.action',
130+
'trace.path',
131+
'tv-remote.button',
132+
'wait.depth',
133+
'wait.durationMs',
134+
'wait.absent',
135+
'wait.kind',
136+
'wait.quietMs',
137+
'wait.raw',
138+
'wait.ref',
139+
'wait.scope',
140+
'wait.selector',
141+
'wait.stable',
142+
'wait.text',
143+
'wait.timeoutMs',
144+
]);
145+
146+
test('MCP tool inputs do not add undocumented properties', () => {
147+
const undescribed: string[] = [];
148+
for (const tool of listCommandTools()) {
149+
for (const [key, schema] of Object.entries(tool.inputSchema.properties ?? {})) {
150+
if (!schema.description) undescribed.push(`${tool.name}.${key}`);
151+
}
152+
}
153+
154+
const added = undescribed.filter((entry) => !UNDESCRIBED_TOOL_INPUTS.has(entry)).sort();
155+
assert.deepEqual(
156+
added,
157+
[],
158+
`These MCP tool inputs need a schema description: ${added.join(', ')}`,
159+
);
160+
161+
const fixed = [...UNDESCRIBED_TOOL_INPUTS].filter((entry) => !undescribed.includes(entry)).sort();
162+
assert.deepEqual(
163+
fixed,
164+
[],
165+
`These MCP tool inputs are documented now — remove them from UNDESCRIBED_TOOL_INPUTS: ${fixed.join(', ')}`,
166+
);
167+
});

0 commit comments

Comments
 (0)