|
1 | 1 | import assert from 'node:assert/strict'; |
2 | 2 | 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'; |
4 | 7 | import { PUBLIC_COMMANDS } from '../../../command-catalog.ts'; |
5 | 8 | import { BASE_COMMAND_CAPABILITY_MATRIX } from '../../capabilities.ts'; |
6 | 9 | import { |
@@ -129,17 +132,101 @@ test('capability matrix holds its admission invariants', () => { |
129 | 132 | } |
130 | 133 | }); |
131 | 134 |
|
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', () => { |
138 | 199 | const derived = deriveStructuredBatchCommandNames(commandDescriptors); |
| 200 | + |
| 201 | + // No duplicates in the derived allowlist. |
139 | 202 | 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. |
140 | 207 | 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', |
144 | 211 | ); |
| 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 | + } |
145 | 232 | }); |
0 commit comments