Skip to content

Commit 607883d

Browse files
authored
feat: build batch allowlist from descriptors, delete the hand list — Phase 1 step 4 (#909)
1 parent 96bc7b1 commit 607883d

2 files changed

Lines changed: 165 additions & 56 deletions

File tree

‎src/batch-policy.ts‎

Lines changed: 68 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,75 @@
11
import { PUBLIC_COMMANDS } from './command-catalog.ts';
2+
import { deriveStructuredBatchCommandNames } from './core/command-descriptor/derive.ts';
3+
import { commandDescriptors } from './core/command-descriptor/registry.ts';
24
import { AppError } from './utils/errors.ts';
35

4-
export const STRUCTURED_BATCH_COMMAND_NAMES = [
5-
PUBLIC_COMMANDS.devices,
6-
PUBLIC_COMMANDS.boot,
7-
PUBLIC_COMMANDS.shutdown,
8-
PUBLIC_COMMANDS.apps,
9-
PUBLIC_COMMANDS.open,
10-
PUBLIC_COMMANDS.close,
11-
PUBLIC_COMMANDS.install,
12-
PUBLIC_COMMANDS.reinstall,
13-
PUBLIC_COMMANDS.installFromSource,
14-
PUBLIC_COMMANDS.push,
15-
PUBLIC_COMMANDS.triggerAppEvent,
16-
PUBLIC_COMMANDS.snapshot,
17-
PUBLIC_COMMANDS.screenshot,
18-
PUBLIC_COMMANDS.diff,
19-
PUBLIC_COMMANDS.wait,
20-
PUBLIC_COMMANDS.alert,
21-
PUBLIC_COMMANDS.settings,
22-
PUBLIC_COMMANDS.click,
23-
PUBLIC_COMMANDS.press,
24-
PUBLIC_COMMANDS.longPress,
25-
PUBLIC_COMMANDS.swipe,
26-
PUBLIC_COMMANDS.focus,
27-
PUBLIC_COMMANDS.type,
28-
PUBLIC_COMMANDS.fill,
29-
PUBLIC_COMMANDS.scroll,
30-
PUBLIC_COMMANDS.get,
31-
PUBLIC_COMMANDS.gesture,
32-
PUBLIC_COMMANDS.is,
33-
PUBLIC_COMMANDS.find,
34-
PUBLIC_COMMANDS.perf,
35-
PUBLIC_COMMANDS.logs,
36-
PUBLIC_COMMANDS.network,
37-
PUBLIC_COMMANDS.record,
38-
PUBLIC_COMMANDS.trace,
39-
PUBLIC_COMMANDS.test,
40-
PUBLIC_COMMANDS.appState,
41-
PUBLIC_COMMANDS.back,
42-
PUBLIC_COMMANDS.home,
43-
PUBLIC_COMMANDS.rotate,
44-
PUBLIC_COMMANDS.appSwitcher,
45-
PUBLIC_COMMANDS.keyboard,
46-
PUBLIC_COMMANDS.clipboard,
47-
PUBLIC_COMMANDS.reactNative,
48-
] as const;
6+
/**
7+
* The exact set of command names exposed through `batch`, as a narrow union.
8+
*
9+
* This type is kept HAND-AUTHORED on purpose (ADR-0008, Phase 1 step 4): the
10+
* runtime allowlist below is now derived from the command-descriptor registry,
11+
* but the registry types each `name` as `string`, so deriving the value yields
12+
* `string[]`. Re-deriving the type from that value would WIDEN this union to
13+
* `string` and silently widen its downstream consumers — most notably
14+
* `BatchCommandName` (re-exported from `command-surface.ts`) and the
15+
* `satisfies readonly DaemonCommandName[]` guard in `commands/batch/projection.ts`.
16+
* Keeping the union spelled out preserves those compile-time contracts. The
17+
* derived runtime value is proven to match this union, member-for-member, by
18+
* `core/command-descriptor/__tests__/parity.test.ts`.
19+
*/
20+
export type StructuredBatchCommandName =
21+
| (typeof PUBLIC_COMMANDS)['devices']
22+
| (typeof PUBLIC_COMMANDS)['boot']
23+
| (typeof PUBLIC_COMMANDS)['shutdown']
24+
| (typeof PUBLIC_COMMANDS)['apps']
25+
| (typeof PUBLIC_COMMANDS)['open']
26+
| (typeof PUBLIC_COMMANDS)['close']
27+
| (typeof PUBLIC_COMMANDS)['install']
28+
| (typeof PUBLIC_COMMANDS)['reinstall']
29+
| (typeof PUBLIC_COMMANDS)['installFromSource']
30+
| (typeof PUBLIC_COMMANDS)['push']
31+
| (typeof PUBLIC_COMMANDS)['triggerAppEvent']
32+
| (typeof PUBLIC_COMMANDS)['snapshot']
33+
| (typeof PUBLIC_COMMANDS)['screenshot']
34+
| (typeof PUBLIC_COMMANDS)['diff']
35+
| (typeof PUBLIC_COMMANDS)['wait']
36+
| (typeof PUBLIC_COMMANDS)['alert']
37+
| (typeof PUBLIC_COMMANDS)['settings']
38+
| (typeof PUBLIC_COMMANDS)['click']
39+
| (typeof PUBLIC_COMMANDS)['press']
40+
| (typeof PUBLIC_COMMANDS)['longPress']
41+
| (typeof PUBLIC_COMMANDS)['swipe']
42+
| (typeof PUBLIC_COMMANDS)['focus']
43+
| (typeof PUBLIC_COMMANDS)['type']
44+
| (typeof PUBLIC_COMMANDS)['fill']
45+
| (typeof PUBLIC_COMMANDS)['scroll']
46+
| (typeof PUBLIC_COMMANDS)['get']
47+
| (typeof PUBLIC_COMMANDS)['gesture']
48+
| (typeof PUBLIC_COMMANDS)['is']
49+
| (typeof PUBLIC_COMMANDS)['find']
50+
| (typeof PUBLIC_COMMANDS)['perf']
51+
| (typeof PUBLIC_COMMANDS)['logs']
52+
| (typeof PUBLIC_COMMANDS)['network']
53+
| (typeof PUBLIC_COMMANDS)['record']
54+
| (typeof PUBLIC_COMMANDS)['trace']
55+
| (typeof PUBLIC_COMMANDS)['test']
56+
| (typeof PUBLIC_COMMANDS)['appState']
57+
| (typeof PUBLIC_COMMANDS)['back']
58+
| (typeof PUBLIC_COMMANDS)['home']
59+
| (typeof PUBLIC_COMMANDS)['rotate']
60+
| (typeof PUBLIC_COMMANDS)['appSwitcher']
61+
| (typeof PUBLIC_COMMANDS)['keyboard']
62+
| (typeof PUBLIC_COMMANDS)['clipboard']
63+
| (typeof PUBLIC_COMMANDS)['reactNative'];
4964

50-
export type StructuredBatchCommandName = (typeof STRUCTURED_BATCH_COMMAND_NAMES)[number];
65+
/**
66+
* The structured-batch allowlist, BUILT from the command-descriptor registry
67+
* (the `batchable` flag) rather than hand-maintained. The derived value is a
68+
* `string[]`; the cast re-applies the narrow {@link StructuredBatchCommandName}
69+
* union, whose membership equality with this value is asserted by the parity test.
70+
*/
71+
export const STRUCTURED_BATCH_COMMAND_NAMES: readonly StructuredBatchCommandName[] =
72+
deriveStructuredBatchCommandNames(commandDescriptors) as readonly StructuredBatchCommandName[];
5173

5274
export const BATCH_BLOCKED_COMMANDS: ReadonlySet<string> = new Set(['batch', 'replay']);
5375

‎src/core/command-descriptor/__tests__/parity.test.ts‎

Lines changed: 97 additions & 10 deletions
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 { STRUCTURED_BATCH_COMMAND_NAMES } from '../../../batch-policy.ts';
3+
import {
4+
STRUCTURED_BATCH_COMMAND_NAMES,
5+
type StructuredBatchCommandName,
6+
} from '../../../batch-policy.ts';
47
import { PUBLIC_COMMANDS } from '../../../command-catalog.ts';
58
import { BASE_COMMAND_CAPABILITY_MATRIX } from '../../capabilities.ts';
69
import {
@@ -129,17 +132,101 @@ test('capability matrix holds its admission invariants', () => {
129132
}
130133
});
131134

132-
test('derived structured-batch command names match the hand table (membership)', () => {
133-
// Membership, not order: STRUCTURED_BATCH_COMMAND_NAMES and
134-
// DAEMON_COMMAND_DESCRIPTORS are independently hand-ordered, so a single
135-
// registry table cannot reproduce both array orders. The batchable flags are
136-
// proven byte-equal as a set; ordering is cosmetic (the consumer dedupes into
137-
// a Set) and is deferred to a later slice.
135+
// Exhaustive map over the StructuredBatchCommandName union. The compiler errors
136+
// if a union member is missing a key OR a non-member key is added, so its keys
137+
// ARE the union at type level — giving us a runtime handle on the (otherwise
138+
// hand-authored) union to compare against the derived allowlist.
139+
const STRUCTURED_BATCH_UNION_MEMBERS: Record<StructuredBatchCommandName, true> = {
140+
[PUBLIC_COMMANDS.devices]: true,
141+
[PUBLIC_COMMANDS.boot]: true,
142+
[PUBLIC_COMMANDS.shutdown]: true,
143+
[PUBLIC_COMMANDS.apps]: true,
144+
[PUBLIC_COMMANDS.open]: true,
145+
[PUBLIC_COMMANDS.close]: true,
146+
[PUBLIC_COMMANDS.install]: true,
147+
[PUBLIC_COMMANDS.reinstall]: true,
148+
[PUBLIC_COMMANDS.installFromSource]: true,
149+
[PUBLIC_COMMANDS.push]: true,
150+
[PUBLIC_COMMANDS.triggerAppEvent]: true,
151+
[PUBLIC_COMMANDS.snapshot]: true,
152+
[PUBLIC_COMMANDS.screenshot]: true,
153+
[PUBLIC_COMMANDS.diff]: true,
154+
[PUBLIC_COMMANDS.wait]: true,
155+
[PUBLIC_COMMANDS.alert]: true,
156+
[PUBLIC_COMMANDS.settings]: true,
157+
[PUBLIC_COMMANDS.click]: true,
158+
[PUBLIC_COMMANDS.press]: true,
159+
[PUBLIC_COMMANDS.longPress]: true,
160+
[PUBLIC_COMMANDS.swipe]: true,
161+
[PUBLIC_COMMANDS.focus]: true,
162+
[PUBLIC_COMMANDS.type]: true,
163+
[PUBLIC_COMMANDS.fill]: true,
164+
[PUBLIC_COMMANDS.scroll]: true,
165+
[PUBLIC_COMMANDS.get]: true,
166+
[PUBLIC_COMMANDS.gesture]: true,
167+
[PUBLIC_COMMANDS.is]: true,
168+
[PUBLIC_COMMANDS.find]: true,
169+
[PUBLIC_COMMANDS.perf]: true,
170+
[PUBLIC_COMMANDS.logs]: true,
171+
[PUBLIC_COMMANDS.network]: true,
172+
[PUBLIC_COMMANDS.record]: true,
173+
[PUBLIC_COMMANDS.trace]: true,
174+
[PUBLIC_COMMANDS.test]: true,
175+
[PUBLIC_COMMANDS.appState]: true,
176+
[PUBLIC_COMMANDS.back]: true,
177+
[PUBLIC_COMMANDS.home]: true,
178+
[PUBLIC_COMMANDS.rotate]: true,
179+
[PUBLIC_COMMANDS.appSwitcher]: true,
180+
[PUBLIC_COMMANDS.keyboard]: true,
181+
[PUBLIC_COMMANDS.clipboard]: true,
182+
[PUBLIC_COMMANDS.reactNative]: true,
183+
};
184+
185+
// Control-plane / non-batchable commands that must never enter the allowlist.
186+
const NON_BATCHABLE_COMMANDS = [
187+
PUBLIC_COMMANDS.batch,
188+
PUBLIC_COMMANDS.replay,
189+
PUBLIC_COMMANDS.prepare,
190+
'pinch',
191+
'viewport',
192+
'pan',
193+
'fling',
194+
'rotate-gesture',
195+
'transform-gesture',
196+
];
197+
198+
test('structured-batch allowlist is built from descriptors and matches the kept union', () => {
138199
const derived = deriveStructuredBatchCommandNames(commandDescriptors);
200+
201+
// No duplicates in the derived allowlist.
139202
assert.equal(new Set(derived).size, derived.length, 'no duplicate batchable names');
203+
204+
// The exported allowlist is now BUILT from these derived descriptors, so it is
205+
// the derived list (order included) — guards the wiring, not a tautology of
206+
// membership.
140207
assert.deepEqual(
141-
[...derived].sort(),
142-
[...STRUCTURED_BATCH_COMMAND_NAMES].sort(),
143-
'structured-batch membership',
208+
[...STRUCTURED_BATCH_COMMAND_NAMES],
209+
derived,
210+
'exported allowlist is built from the descriptors',
144211
);
212+
213+
// Membership equals the kept StructuredBatchCommandName union (proves the
214+
// narrow type the consumers rely on did not drift from the runtime value).
215+
assert.deepEqual(
216+
[...new Set(derived)].sort(),
217+
Object.keys(STRUCTURED_BATCH_UNION_MEMBERS).sort(),
218+
'derived membership equals the kept union',
219+
);
220+
221+
// Every batchable command is a real public command (no internal/control name leaks in).
222+
const publicCommands = new Set<string>(Object.values(PUBLIC_COMMANDS));
223+
for (const name of derived) {
224+
assert.ok(publicCommands.has(name), `batchable command ${name} is a public command`);
225+
}
226+
227+
// Control-plane commands stay out of the allowlist.
228+
const batchable = new Set(derived);
229+
for (const excluded of NON_BATCHABLE_COMMANDS) {
230+
assert.ok(!batchable.has(excluded), `${excluded} is not batchable`);
231+
}
145232
});

0 commit comments

Comments
 (0)