Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .device-evidence/CHECKLIST-runner-failures.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ diagnostic a row keys on.
`runner-startup-failure-fixtures.ts` so the rows read as host-gated, not unexamined.

#2680 §4 needs a build that fails for an unrelated reason while naming no signing fact; the
classifier's behaviour there is pinned by `runner-startup-failure-reasons.test.ts` and needs no
classifier's behaviour there is pinned by `runner-xctestrun-startup-failure-reasons.test.ts` and needs no
device claim to hold.

### #2683 — what the phone actually reported
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ jobs:
run: |
pnpm build
pnpm exec vitest run \
packages/platform-apple/src/snapshot-source/native-runtime.test.ts \
packages/platform-apple/src/snapshot-source/cache-native-runtime.test.ts \
packages/platform-apple/src/foldable/fold-helper-cache.test.ts \
packages/platform-apple/src/foldable/simulator-hid-native.test.ts \
packages/capture-kit/src/recording/__tests__/recording-scripts.test.ts
Expand Down
128 changes: 64 additions & 64 deletions contracts/fixtures/runner-requests.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/adr/0005-ios-runner-interaction-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ What a `RUNNER_BUSY` refusal then costs the caller turns on the daemon's own per
trait, not on any runner-side classification of the command: `RUNNER_COMMAND_TRAITS` in
`packages/platform-apple/src/runner/runner-command-traits.ts`, read through
`isReadOnlyRunnerCommand` where `runAppleRunnerCommand` decides whether to wrap the send in a resend
loop. The asymmetry is shipped, and asserted in `runner-command-busy-resend.test.ts`: a read-only
loop. The asymmetry is shipped, and asserted in `runner-client-busy-resend.test.ts`: a read-only
command treats the refusal as *not yet*, waiting the drain out on the busy-specific budget sized to
outlast it and resending until the runner answers or the window ends; a mutating command treats the
same refusal as *not mine to send again* — one attempt, and the refusal reaches the caller unwrapped
Expand Down
4 changes: 2 additions & 2 deletions docs/ios-runner-protocol-optimizations.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ Acceptance criteria:

iOS simulator validation:

- Unit: `pnpm exec vitest run packages/platform-apple/src/runner/__tests__/runner-command-retry.test.ts`.
- Unit bundle: `pnpm exec vitest run packages/platform-apple/src/runner/__tests__/runner-client.test.ts packages/platform-apple/src/runner/__tests__/runner-session.test.ts packages/platform-apple/src/runner/__tests__/runner-command-retry.test.ts packages/platform-apple/src/runner/__tests__/runner-provider.test.ts`.
- Unit: `pnpm exec vitest run packages/platform-apple/src/runner/__tests__/runner-client-command-retry.test.ts`.
- Unit bundle: `pnpm exec vitest run packages/platform-apple/src/runner/__tests__/runner-client.test.ts packages/platform-apple/src/runner/__tests__/runner-session.test.ts packages/platform-apple/src/runner/__tests__/runner-client-command-retry.test.ts packages/platform-apple/src/runner/__tests__/runner-provider.test.ts`.
- Build: `pnpm build:xcuitest`.
- Manual sim smoke after build:
- `pnpm build`
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/src/dispatch-disclosure.fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const DISPATCH_DISCLOSURE_DRIVER_OWNERS: Readonly<Record<string, string>>
'daemon.route.': 'src/__tests__/daemon-composition/request-dispatch-disclosure.test.ts',
'post-action-guard.':
'src/__tests__/daemon-composition/interaction/interaction-dispatch-disclosure.test.ts',
'ios-runner.': 'packages/platform-apple/src/runner/__tests__/runner-dispatch-disclosure.test.ts',
'ios-runner.': 'packages/platform-apple/src/runner/__tests__/index-dispatch-disclosure.test.ts',
'ios-runner.pre-send.':
'packages/platform-apple/src/runner/__tests__/runner-lifecycle-dispatch-disclosure.test.ts',
'ios-runner.transport.':
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/src/interactor-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export const MAESTRO_NON_HITTABLE_FALLBACK_MESSAGE = 'tapped via non-hittable co
* its only producer and the boundary that narrows it
* (readTypeTextBackendResult, packages/platform-apple/src/interactions.ts) drops a
* route it cannot name — so a Swift-side addition would silently vanish. The
* route-parity test in packages/platform-apple/src/core/__tests__/interactions.test.ts
* route-parity test in packages/platform-apple/src/interactions.test.ts
* reads the Swift sources and fails instead.
*/
export const TEXT_ENTRY_ROUTES = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { mkdtempForTest } from './tmp-dir.ts';

// Every Apple runner request site outside `runner/`, driven through its production entry point and
// pinned to contracts/fixtures/runner-requests.json. The drives never write a request themselves;
// runner/__tests__/runner-requests.test.ts reads this source to keep it that way.
// runner/__tests__/index-runner-requests.test.ts reads this source to keep it that way.

const APP = 'com.example.app';
const XCTEST_DEVICE: DeviceInfo = { ...IOS_DEVICE, iosPhysicalDeviceBackend: 'xctest' };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ test('an abort while a second caller waits on the fold-helper lock reports a can
//
// The compile runs in beforeAll, not in the test body: it is a real clang invocation (see the
// unit slow-test budget in docs/agents/testing.md), and the snapshot-bridge sibling gate in
// native-runtime.test.ts keeps its compile out of test-case wall time the same way.
// cache-native-runtime.test.ts keeps its compile out of test-case wall time the same way.
describe.skipIf(process.platform !== 'darwin')('fold helper warning gate', () => {
let compiled: { exitCode: number; stderr: string };
beforeAll(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,31 @@ import {
iosRunnerOverrides,
performGestureApple,
resolveAppleBackRunnerCommand,
} from '../../interactions.ts';
import { runAppleRunnerCommand } from '../runner-client.ts';
} from './interactions.ts';
import { runAppleRunnerCommand } from './core/runner-client.ts';
import { AppError } from '@agent-device/kernel/errors';
import {
gestureRefusalMessage,
PHYSICAL_IOS_MULTI_TOUCH_UNSUPPORTED_HINT,
} from '@agent-device/contracts/gesture-admission';
import type { GesturePlan } from '@agent-device/contracts/gesture-plan-types';
import type { RunnerCommand } from '../../runner/index.ts';
import type { RunnerCommand } from './runner/index.ts';
import { TEXT_ENTRY_ROUTES } from '@agent-device/contracts/interactor-types';
import {
IOS_TEST_DEVICE,
IOS_TEST_SIMULATOR,
MACOS_TEST_DEVICE,
TVOS_TEST_SIMULATOR,
VISIONOS_TEST_SIMULATOR,
} from './apple-core-stub-helpers.ts';
} from './core/__tests__/apple-core-stub-helpers.ts';

vi.mock('../runner-client.ts', async (importOriginal) => {
const actual = await importOriginal<typeof import('../runner-client.ts')>();
vi.mock('./core/runner-client.ts', async (importOriginal) => {
const actual = await importOriginal<typeof import('./core/runner-client.ts')>();
return { ...actual, runAppleRunnerCommand: vi.fn(actual.runAppleRunnerCommand) };
});

const runnerActual =
await vi.importActual<typeof import('../runner-client.ts')>('../runner-client.ts');
await vi.importActual<typeof import('./core/runner-client.ts')>('./core/runner-client.ts');

const mockRunAppleRunnerCommand = vi.mocked(runAppleRunnerCommand);

Expand Down Expand Up @@ -630,7 +630,7 @@ test('iosRunnerOverrides reports no route when the runner names one it does not
test('TEXT_ENTRY_ROUTES names every route the Swift runner assigns', () => {
const runnerTestsDir = path.resolve(
import.meta.dirname,
'../../../../../apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests',
'../../../apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests',
);
const swiftRoutes = new Set<string>();
for (const entry of fs.readdirSync(runnerTestsDir)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,9 @@ import {
isRunnerResponseOk,
readRunnerResponseData,
} from '../runner-contract.ts';
import { isStructuredRunnerFailure } from '../runner-error-classification.ts';
import { parseRunnerResponse } from '../runner-exchange.ts';
import type { RunnerSessionState } from '../runner-session-types.ts';

// A body cut off mid-write: the shape a runner that died while answering leaves behind.
const TRUNCATED_BODY = '{"ok":true,"data":{"nodes":[{"label":"Sign In"';
// `ok` is a Swift `Bool`, so this is what a private truthiness rule used to accept as an answer.
const STRINGLY_TYPED_BODY = '{"ok":"true","data":{"tapped":true}}';

describe('decodeRunnerResponseBody', () => {
test('decodes the runner envelope', () => {
Expand Down Expand Up @@ -111,59 +106,3 @@ describe('buildRunnerResponseError', () => {
assert.equal(error.details?.hint, undefined);
});
});

describe('parseRunnerResponse', () => {
test('refuses a body whose ok is not the boolean true', async () => {
const session: { state: RunnerSessionState } = { state: 'starting' };

await assert.rejects(
() => parseRunnerResponse(new Response(STRINGLY_TYPED_BODY), session),
(error: unknown) => {
assert.ok(error instanceof AppError);
assert.equal(error.code, 'COMMAND_FAILED');
assert.deepEqual(error.details?.runner, { ok: 'true', data: { tapped: true } });
return true;
},
);

assert.equal(session.state, 'starting');
});

test('refuses a body that is not readable JSON', async () => {
const session: { state: RunnerSessionState } = { state: 'starting' };

await assert.rejects(
() => parseRunnerResponse(new Response(TRUNCATED_BODY), session),
(error: unknown) => {
assert.ok(error instanceof AppError);
assert.equal(error.code, 'COMMAND_FAILED');
assert.equal(error.details?.text, TRUNCATED_BODY);
// Transport-shaped: no `runner` detail, so the session keeps its recency bets (#2552).
assert.equal(error.details?.runner, undefined);
return true;
},
);

assert.equal(session.state, 'starting');
});

test('reads a JSON scalar body as transport-shaped, not as an empty reply', async () => {
// The chain #2965 turns on: an error carrying a `runner` detail counts as an answer and discharges
// the command's charge. A bare `null` is not an envelope the runner's encoder emits, so decoding it
// into `{}` and erroring from that would have answered a command the runner may still be running.
const session: { state: RunnerSessionState } = { state: 'starting' };

await assert.rejects(
() => parseRunnerResponse(new Response('null'), session),
(error: unknown) => {
assert.ok(error instanceof AppError);
assert.equal(error.message, 'Invalid runner response');
assert.equal(error.details?.runner, undefined);
assert.equal(isStructuredRunnerFailure(error), false);
return true;
},
);

assert.equal(session.state, 'starting');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import assert from 'node:assert/strict';
import { describe, test } from 'vitest';
import { AppError } from '@agent-device/kernel/errors';
import { isStructuredRunnerFailure } from '../runner-error-classification.ts';
import { parseRunnerResponse } from '../runner-exchange.ts';
import type { RunnerSessionState } from '../runner-session-types.ts';

// A body cut off mid-write: the shape a runner that died while answering leaves behind.
const TRUNCATED_BODY = '{"ok":true,"data":{"nodes":[{"label":"Sign In"';
// `ok` is a Swift `Bool`, so this is what a private truthiness rule used to accept as an answer.
const STRINGLY_TYPED_BODY = '{"ok":"true","data":{"tapped":true}}';

describe('parseRunnerResponse', () => {
test('refuses a body whose ok is not the boolean true', async () => {
const session: { state: RunnerSessionState } = { state: 'starting' };

await assert.rejects(
() => parseRunnerResponse(new Response(STRINGLY_TYPED_BODY), session),
(error: unknown) => {
assert.ok(error instanceof AppError);
assert.equal(error.code, 'COMMAND_FAILED');
assert.deepEqual(error.details?.runner, { ok: 'true', data: { tapped: true } });
return true;
},
);

assert.equal(session.state, 'starting');
});

test('refuses a body that is not readable JSON', async () => {
const session: { state: RunnerSessionState } = { state: 'starting' };

await assert.rejects(
() => parseRunnerResponse(new Response(TRUNCATED_BODY), session),
(error: unknown) => {
assert.ok(error instanceof AppError);
assert.equal(error.code, 'COMMAND_FAILED');
assert.equal(error.details?.text, TRUNCATED_BODY);
// Transport-shaped: no `runner` detail, so the session keeps its recency bets (#2552).
assert.equal(error.details?.runner, undefined);
return true;
},
);

assert.equal(session.state, 'starting');
});

test('reads a JSON scalar body as transport-shaped, not as an empty reply', async () => {
// The chain #2965 turns on: an error carrying a `runner` detail counts as an answer and discharges
// the command's charge. A bare `null` is not an envelope the runner's encoder emits, so decoding it
// into `{}` and erroring from that would have answered a command the runner may still be running.
const session: { state: RunnerSessionState } = { state: 'starting' };

await assert.rejects(
() => parseRunnerResponse(new Response('null'), session),
(error: unknown) => {
assert.ok(error instanceof AppError);
assert.equal(error.message, 'Invalid runner response');
assert.equal(error.details?.runner, undefined);
assert.equal(isStructuredRunnerFailure(error), false);
return true;
},
);

assert.equal(session.state, 'starting');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ test('a command abandoned by a cancelled transport keeps the runner occupied', a
// An answered queued exchange forgives the abandoned charge: the serial queue makes it evidence that
// the abandoned handling ahead of it finished, and whatever is still draining is stamped onto that
// very reply. An answer the runner serves inline forgives nothing (#2965); that case is pinned in
// `runner-recovery-wiring.test.ts`, where the probe reaches a real session.
// `runner-client-recovery-wiring.test.ts`, where the probe reaches a real session.
await serveOneCommand(device, session);
assert.equal(session.commandCharges.hasOutstandingCharges, false);
assert.equal(await detachIosRunnerSessionsForShutdown(), 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const CASE_HEADER = /\bcase\s+(\.\w+(?:\s*,\s*\.\w+)*):/g;
* command queue, which is exactly why a reply to one is no evidence about queued work.
*
* A reader here can only fail in two directions, and each is caught: a misread that names an extra
* command fails `runner-readiness-routing.test.ts`, which compares this list against the daemon's
* command fails `runner-command-traits-readiness-routing.test.ts`, which compares this list against the daemon's
* `readinessProbe` trait in both directions, and a misread that loses an arm makes a real probe look
* queued, which that same tie reports. What is left to guard locally is the case both miss — a `case`
* these two patterns do not recognize, which would otherwise drop an arm and leave a set that still
Expand Down
2 changes: 1 addition & 1 deletion packages/platform-apple/src/runner/runner-exchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ async function sendRunnerCommandAfterPreflight(params: {
// nobody is waiting for any more. A readiness probe is charged no more than the preflight's own
// probe is: the runner serves it inline, so its reply says nothing about the queued work a charge
// stands for, and charging it would let one probe settle another command's debt (#2965). The trait
// that names the probe is pinned to the runner's inline routing by `runner-readiness-routing.test.ts`.
// that names the probe is pinned to the runner's inline routing by `runner-command-traits-readiness-routing.test.ts`.
const charged = !isRunnerReadinessProbeCommand(runnerCommand);
if (charged) session.commandCharges.charge(runnerCommand.commandId);
try {
Expand Down
4 changes: 3 additions & 1 deletion packages/test-kit/src/boundary-fault-matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,9 @@ export const BOUNDARY_FAULT_MATRIX = {
'lost-response-after-mutation': {
mutation: {
kind: 'covered',
evidence: ['packages/platform-apple/src/runner/__tests__/runner-recovery-wiring.test.ts'],
evidence: [
'packages/platform-apple/src/runner/__tests__/runner-client-recovery-wiring.test.ts',
],
invariants: ['no-mutation-replay', 'typed-error-identity'],
},
read: {
Expand Down
2 changes: 1 addition & 1 deletion test/integration/command-coverage/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,7 @@ const COMMAND_COVERAGE_DECLARATIONS = {
'fixture gesture counters prove pan/fling/pinch/rotate',
),
macos: macos.contract(
'packages/platform-apple/src/core/__tests__/interactions.test.ts',
'packages/platform-apple/src/interactions.test.ts',
'performGestureApple composes macOS one-contact plans with the drag executor',
'macOS gesture dispatch preserves the one-contact drag plan',
),
Expand Down
Loading