Skip to content

Commit 8bb97ea

Browse files
committed
refactor: clean up runtime router surface
1 parent ca7c1af commit 8bb97ea

12 files changed

Lines changed: 247 additions & 438 deletions

COMMAND_OWNERSHIP.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,14 @@ Their semantics should live in `agent-device/commands` as they migrate.
111111
- `trace`: runtime `trace` router/API command implemented with typed trace
112112
start/stop result unions.
113113
- `logs`: runtime `diagnostics.logs` implemented with bounded, paginated,
114-
redacted log entries.
114+
best-effort redacted log entries.
115115
- `network`: runtime `diagnostics.network` implemented with bounded,
116-
structured, redacted network entries.
116+
structured, best-effort redacted network entries.
117117
- `perf`: runtime `diagnostics.perf` implemented with typed metric entries.
118-
- `replay`: runtime router command implemented for replay scripts or router
119-
steps, executing each step through `createCommandRouter()`.
120-
- `test`: runtime router command implemented for replay test cases with retries
121-
and fail-fast handling.
118+
- `replay`: still daemon/CLI owned; runtime router migration is deferred until
119+
it can reuse the real `.ad` parser and healing semantics.
120+
- `test`: still daemon/CLI owned; runtime router migration is deferred until it
121+
can share daemon replay-suite semantics end to end.
122122

123123
## Boundary Requirements
124124

src/__tests__/runtime-admin-router.test.ts

Lines changed: 10 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ test('router batch preserves per-step failures and enforces per-command policy',
140140
const nested = await router.dispatch({
141141
command: 'batch',
142142
options: {
143-
steps: [{ command: 'replay', options: { steps: [] } }],
143+
steps: [{ command: 'batch', options: { steps: [] } }],
144144
},
145145
});
146146
assert.equal(nested.ok, false);
@@ -284,68 +284,31 @@ test('record keeps successful reserved outputs available after publish', async (
284284
assert.equal(cleanupCalled, false);
285285
});
286286

287-
test('router replay and test execute parsed router scripts through nested dispatch', async () => {
288-
const opened: string[] = [];
287+
test('router replay and test stay planned until phase 7 migration is complete', async () => {
289288
const router = createCommandRouter({
290289
createRuntime: () =>
291290
createAgentDevice({
292-
backend: {
293-
platform: 'ios',
294-
openApp: async (_context, target) => {
295-
if (target.app) opened.push(target.app);
296-
},
297-
},
291+
backend: { platform: 'ios' },
298292
artifacts,
299293
policy: restrictedCommandPolicy(),
300294
}),
301295
});
302296

303297
const replay = await router.dispatch({
304298
command: 'replay',
305-
options: {
306-
script: 'apps.open {"app":"com.example.app"}\napps.open {"app":"com.example.other"}',
307-
},
308-
});
309-
310-
assert.equal(replay.ok, true);
311-
assert.equal(replay.ok && isResultKind(replay.data, 'replay') ? replay.data.failed : 1, 0);
299+
options: { steps: [{ command: 'apps.open', options: { app: 'com.example.app' } }] },
300+
} as never);
301+
assert.equal(replay.ok, false);
302+
assert.equal(replay.ok ? undefined : replay.error.code, 'NOT_IMPLEMENTED');
312303

313304
const suite = await router.dispatch({
314305
command: 'test',
315306
options: {
316307
tests: [{ name: 'opens app', steps: [{ command: 'apps.open', options: { app: 'suite' } }] }],
317308
},
318-
});
319-
320-
assert.equal(suite.ok, true);
321-
assert.equal(suite.ok && isResultKind(suite.data, 'replayTest') ? suite.data.passed : 0, 1);
322-
assert.equal(
323-
suite.ok && isResultKind(suite.data, 'replayTest') ? suite.data.tests[0]?.attempts : 0,
324-
1,
325-
);
326-
assert.deepEqual(opened, ['com.example.app', 'com.example.other', 'suite']);
327-
});
328-
329-
test('router replay update fails explicitly until healing is implemented', async () => {
330-
const router = createCommandRouter({
331-
createRuntime: () =>
332-
createAgentDevice({
333-
backend: { platform: 'ios' },
334-
artifacts,
335-
policy: restrictedCommandPolicy(),
336-
}),
337-
});
338-
339-
const response = await router.dispatch({
340-
command: 'replay',
341-
options: {
342-
update: true,
343-
steps: [{ command: 'apps.open', options: { app: 'com.example.app' } }],
344-
},
345-
});
346-
347-
assert.equal(response.ok, false);
348-
assert.equal(response.ok ? undefined : response.error.code, 'NOT_IMPLEMENTED');
309+
} as never);
310+
assert.equal(suite.ok, false);
311+
assert.equal(suite.ok ? undefined : suite.error.code, 'NOT_IMPLEMENTED');
349312
});
350313

351314
function isResultKind<TKind extends string>(

src/__tests__/runtime-diagnostics-router.test.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,20 @@ test('diagnostics runtime commands call typed backend primitives and redact sens
5454
assert.equal(network.redacted, true);
5555
assert.match(network.entries[0]?.url ?? '', /token=%5BREDACTED%5D/);
5656
assert.equal(network.entries[0]?.requestHeaders?.Authorization, '[REDACTED]');
57-
assert.match(network.entries[0]?.requestBody ?? '', /password=\[REDACTED\]/);
57+
assert.deepEqual(JSON.parse(network.entries[0]?.requestBody ?? '{}'), {
58+
token: '[REDACTED]',
59+
nested: {
60+
apiKey: '[REDACTED]',
61+
items: [{ password: '[REDACTED]' }],
62+
},
63+
});
64+
assert.deepEqual(JSON.parse(network.entries[0]?.responseBody ?? '{}'), {
65+
ok: true,
66+
session: {
67+
authorization: '[REDACTED]',
68+
},
69+
items: [{ secret: '[REDACTED]' }],
70+
});
5871

5972
const perf = await device.observability.perf({ session: 'default', sampleMs: 100 });
6073
assert.equal(perf.kind, 'diagnosticsPerf');
@@ -128,8 +141,10 @@ function createDiagnosticsBackend(contexts: BackendCommandContext[]): AgentDevic
128141
status: 200,
129142
requestHeaders: { Authorization: 'Bearer secret' },
130143
responseHeaders: { 'content-type': 'application/json' },
131-
requestBody: 'password=secret',
132-
responseBody: '{"ok":true}',
144+
requestBody:
145+
'{"token":"secret","nested":{"apiKey":"top-secret","items":[{"password":"hidden"}]}}',
146+
responseBody:
147+
'{"ok":true,"session":{"authorization":"Bearer secret"},"items":[{"secret":"classified"}]}',
133148
},
134149
],
135150
};

src/__tests__/runtime-public.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
createCommandRouter,
2727
type ScreenshotCommandOptions,
2828
} from '../commands/index.ts';
29+
import { runtimeRouterCommandNames } from '../commands/router.ts';
2930
import type { ArtifactAdapter, FileInputRef, FileOutputRef } from '../io.ts';
3031
import {
3132
commandConformanceSuites,
@@ -174,6 +175,31 @@ test('public runtime policy helpers expose local and restricted defaults', async
174175
assert.equal((await store.get('default'))?.name, 'default');
175176
});
176177

178+
test('runtime router command map stays aligned with implemented catalog entries', () => {
179+
const catalogRuntimeCommands = commandCatalog
180+
.filter(
181+
(entry) =>
182+
entry.status === 'implemented' &&
183+
(entry.command.includes('.') || entry.command === 'record' || entry.command === 'trace'),
184+
)
185+
.map((entry) => entry.command)
186+
.sort();
187+
188+
assert.deepEqual([...runtimeRouterCommandNames].sort(), catalogRuntimeCommands);
189+
assert.equal(
190+
commandCatalog.some((entry) => entry.command === 'batch' && entry.status === 'implemented'),
191+
true,
192+
);
193+
assert.equal(
194+
commandCatalog.some((entry) => entry.command === 'replay' && entry.status === 'planned'),
195+
true,
196+
);
197+
assert.equal(
198+
commandCatalog.some((entry) => entry.command === 'test' && entry.status === 'planned'),
199+
true,
200+
);
201+
});
202+
177203
test('local artifact adapter marks command outputs and temp files by visibility', async () => {
178204
const adapter = createLocalArtifactAdapter();
179205
const output = await adapter.reserveOutput(undefined, {

src/commands/catalog.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,26 @@ export const commandCatalog: readonly CommandCatalogEntry[] = [
1818
{ command: 'diff screenshot', category: 'portable-runtime', status: 'implemented' },
1919
{ command: 'snapshot', category: 'portable-runtime', status: 'implemented' },
2020
{ command: 'diff snapshot', category: 'portable-runtime', status: 'implemented' },
21+
{ command: 'capture.screenshot', category: 'portable-runtime', status: 'implemented' },
22+
{ command: 'capture.diffScreenshot', category: 'portable-runtime', status: 'implemented' },
23+
{ command: 'capture.snapshot', category: 'portable-runtime', status: 'implemented' },
24+
{ command: 'capture.diffSnapshot', category: 'portable-runtime', status: 'implemented' },
2125
{ command: 'find read-only', category: 'portable-runtime', status: 'implemented' },
2226
{ command: 'get', category: 'portable-runtime', status: 'implemented' },
2327
{ command: 'is', category: 'portable-runtime', status: 'implemented' },
2428
{ command: 'wait', category: 'portable-runtime', status: 'implemented' },
29+
{ command: 'selectors.find', category: 'portable-runtime', status: 'implemented' },
30+
{ command: 'selectors.get', category: 'portable-runtime', status: 'implemented' },
31+
{ command: 'selectors.is', category: 'portable-runtime', status: 'implemented' },
32+
{ command: 'selectors.wait', category: 'portable-runtime', status: 'implemented' },
2533
{ command: 'alert', category: 'portable-runtime', status: 'planned' },
2634
{ command: 'click', category: 'portable-runtime', status: 'implemented' },
2735
{ command: 'press', category: 'portable-runtime', status: 'implemented' },
2836
{ command: 'fill', category: 'portable-runtime', status: 'implemented' },
37+
{ command: 'interactions.click', category: 'portable-runtime', status: 'implemented' },
38+
{ command: 'interactions.press', category: 'portable-runtime', status: 'implemented' },
39+
{ command: 'interactions.fill', category: 'portable-runtime', status: 'implemented' },
40+
{ command: 'interactions.typeText', category: 'portable-runtime', status: 'implemented' },
2941
{ command: 'longpress', category: 'portable-runtime', status: 'planned' },
3042
{ command: 'swipe', category: 'portable-runtime', status: 'planned' },
3143
{ command: 'focus', category: 'portable-runtime', status: 'planned' },
@@ -83,8 +95,8 @@ export const commandCatalog: readonly CommandCatalogEntry[] = [
8395
{ command: 'metro', category: 'environment', status: 'planned' },
8496
{ command: 'record', category: 'capability-gated', status: 'implemented' },
8597
{ command: 'trace', category: 'capability-gated', status: 'implemented' },
86-
{ command: 'replay', category: 'capability-gated', status: 'implemented' },
87-
{ command: 'test', category: 'capability-gated', status: 'implemented' },
98+
{ command: 'replay', category: 'capability-gated', status: 'planned' },
99+
{ command: 'test', category: 'capability-gated', status: 'planned' },
88100
{ command: 'batch', category: 'capability-gated', status: 'implemented' },
89101
{ command: 'logs', category: 'capability-gated', status: 'implemented' },
90102
{ command: 'network', category: 'capability-gated', status: 'implemented' },

src/commands/diagnostics-format.ts

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,9 @@ export function formatNetworkResult(
5151
const responseHeaders =
5252
include === 'headers' || include === 'all' ? redactHeaders(entry.responseHeaders) : undefined;
5353
const requestBody =
54-
include === 'body' || include === 'all'
55-
? redactAndTruncate(entry.requestBody, PAYLOAD_MAX_CHARS)
56-
: undefined;
54+
include === 'body' || include === 'all' ? redactPayload(entry.requestBody) : undefined;
5755
const responseBody =
58-
include === 'body' || include === 'all'
59-
? redactAndTruncate(entry.responseBody, PAYLOAD_MAX_CHARS)
60-
: undefined;
56+
include === 'body' || include === 'all' ? redactPayload(entry.responseBody) : undefined;
6157
const metadata = redactUnknown(entry.metadata);
6258
redacted ||=
6359
(url?.redacted ?? false) ||
@@ -151,6 +147,22 @@ function redactUrl(url: string): { value: string; redacted: boolean } {
151147
}
152148
}
153149

150+
function redactPayload(value: string | undefined): { value?: string; redacted: boolean } {
151+
if (value === undefined) return { redacted: false };
152+
const structured = redactJsonPayload(value);
153+
return structured ?? redactAndTruncate(value, PAYLOAD_MAX_CHARS);
154+
}
155+
156+
function redactJsonPayload(value: string): { value?: string; redacted: boolean } | undefined {
157+
try {
158+
const parsed = JSON.parse(value);
159+
const result = redactStructured(parsed);
160+
return truncateRedacted(JSON.stringify(result.value), PAYLOAD_MAX_CHARS, result.redacted);
161+
} catch {
162+
return undefined;
163+
}
164+
}
165+
154166
function redactUnknown(value: unknown): { value?: unknown; redacted: boolean } {
155167
if (value === undefined) return { redacted: false };
156168
if (typeof value === 'string') return redactAndTruncate(value, PAYLOAD_MAX_CHARS);
@@ -179,11 +191,44 @@ function redactUnknown(value: unknown): { value?: unknown; redacted: boolean } {
179191
return { value: next, redacted };
180192
}
181193

194+
function redactStructured(value: unknown): { value?: unknown; redacted: boolean } {
195+
if (value === undefined) return { redacted: false };
196+
if (typeof value === 'string') return redactText(value);
197+
if (!value || typeof value !== 'object') return { value, redacted: false };
198+
if (Array.isArray(value)) {
199+
let redacted = false;
200+
const next = value.map((entry) => {
201+
const result = redactStructured(entry);
202+
redacted ||= result.redacted;
203+
return result.value;
204+
});
205+
return { value: next, redacted };
206+
}
207+
let redacted = false;
208+
const next: Record<string, unknown> = {};
209+
for (const [key, entry] of Object.entries(value)) {
210+
if (SECRET_KEY_PATTERN.test(key)) {
211+
next[key] = '[REDACTED]';
212+
redacted = true;
213+
continue;
214+
}
215+
const result = redactStructured(entry);
216+
next[key] = result.value;
217+
redacted ||= result.redacted;
218+
}
219+
return { value: next, redacted };
220+
}
221+
182222
function redactAndTruncate(
183223
value: string | undefined,
184224
maxChars: number,
185225
): { value?: string; redacted: boolean } {
186226
if (value === undefined) return { redacted: false };
227+
const result = redactText(value);
228+
return truncateRedacted(result.value, maxChars, result.redacted);
229+
}
230+
231+
function redactText(value: string): { value: string; redacted: boolean } {
187232
let redacted = false;
188233
let next = value.replaceAll(
189234
/(authorization|token|secret|password|passwd|api[-_]?key)=([^&\s]+)/gi,
@@ -192,6 +237,34 @@ function redactAndTruncate(
192237
return `${String(key)}=[REDACTED]`;
193238
},
194239
);
240+
next = next.replaceAll(
241+
/("(?:authorization|cookie|token|secret|password|passwd|api[-_]?key)"\s*:\s*")([^"]*)(")/gi,
242+
(_match, prefix, _value, suffix) => {
243+
redacted = true;
244+
return `${String(prefix)}[REDACTED]${String(suffix)}`;
245+
},
246+
);
247+
next = next.replaceAll(/\b(Bearer\s+)([^\s",;]+)/gi, (_match, prefix) => {
248+
redacted = true;
249+
return `${String(prefix)}[REDACTED]`;
250+
});
251+
next = next.replaceAll(
252+
/((?:authorization|cookie|token|secret|password|passwd|api[-_]?key)\s*[:=]\s*)([^\s,;]+)/gi,
253+
(_match, prefix) => {
254+
redacted = true;
255+
return `${String(prefix)}[REDACTED]`;
256+
},
257+
);
258+
return { value: next, redacted };
259+
}
260+
261+
function truncateRedacted(
262+
value: string | undefined,
263+
maxChars: number,
264+
redacted: boolean,
265+
): { value?: string; redacted: boolean } {
266+
if (value === undefined) return { redacted };
267+
let next = value;
195268
if (next.length > maxChars) {
196269
next = `${next.slice(0, maxChars)}...[truncated]`;
197270
redacted = true;

src/commands/index.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,6 @@ export type {
277277
CommandRouterRequest,
278278
CommandRouterResponse,
279279
CommandRouterResult,
280-
ReplayCommandOptions,
281-
ReplayCommandResult,
282-
ReplayTestCase,
283-
ReplayTestCaseResult,
284-
ReplayTestCommandOptions,
285-
ReplayTestCommandResult,
286280
} from './router.ts';
287281

288282
export type CommandResult = Record<string, unknown>;

0 commit comments

Comments
 (0)