Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6aafd23
test(daemon): split lease provider-credentials suite into daemon refu…
thymikee Oct 15, 2026
5acfaff
test(daemon): split doctor warmup suite into daemon routing (a) and h…
thymikee Oct 15, 2026
7b18195
test(daemon): split relaunch guard suite into daemon routing (a) and …
thymikee Oct 15, 2026
aeae3c3
test(daemon): move claim-cancellation case to the daemon claims suite…
thymikee Oct 15, 2026
cdb9189
test(daemon): split runner-retention close suite into daemon wiring (…
thymikee Oct 15, 2026
fbdfcb3
test(daemon): split relaunch/close suite into daemon wiring (a) and A…
thymikee Oct 15, 2026
15cbfe2
test(daemon): split snapshot handler and freshness suites into daemon…
thymikee Oct 15, 2026
3233394
test(daemon): split wait handler suite into daemon capture/decoration…
thymikee Oct 15, 2026
43c0a36
test(daemon): move scoped-refs suite beside the snapshot handler (a)
thymikee Oct 15, 2026
4c582dc
test(daemon): split sessionless cleanup suite into daemon policy (a) …
thymikee Oct 15, 2026
b1b46e2
test: relocate Android alert occlusion suite beside the Android inter…
thymikee Oct 15, 2026
4a1b15d
test(daemon): move wait landmark-recording suite beside the wait runt…
thymikee Oct 15, 2026
58869a8
style: format the redesigned handler tests
thymikee Oct 15, 2026
5119789
test: drop assertions owned by the daemon open timing and fix output …
thymikee Oct 15, 2026
69ed0e3
test(platform-apple): drop an unread app-resolution mock from the rel…
thymikee Oct 15, 2026
ee9560e
test: point the macOS close coverage claim at the test that now carri…
thymikee Oct 15, 2026
f198599
chore(daemon): drop the seven testing subpaths the handler batch left…
thymikee Oct 15, 2026
4b07e86
test(daemon): run every snapshot-handler diff on the forwarding surfa…
thymikee Oct 15, 2026
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
28 changes: 0 additions & 28 deletions packages/daemon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,6 @@
"types": "./src/__tests__/support/command-surface-fake.ts",
"default": "./src/__tests__/support/command-surface-fake.ts"
},
"./testing/__tests__/support/provider-device-admission-fixture": {
"types": "./src/__tests__/support/provider-device-admission-fixture.ts",
"default": "./src/__tests__/support/provider-device-admission-fixture.ts"
},
"./testing/__tests__/support/screen-recording-live-handle": {
"types": "./src/__tests__/support/screen-recording-live-handle.ts",
"default": "./src/__tests__/support/screen-recording-live-handle.ts"
Expand Down Expand Up @@ -192,18 +188,6 @@
"types": "./src/sessions/handlers/session-appstate.ts",
"default": "./src/sessions/handlers/session-appstate.ts"
},
"./testing/handlers/session-doctor": {
"types": "./src/sessions/handlers/session-doctor.ts",
"default": "./src/sessions/handlers/session-doctor.ts"
},
"./testing/handlers/session-doctor-app": {
"types": "./src/sessions/handlers/session-doctor-app.ts",
"default": "./src/sessions/handlers/session-doctor-app.ts"
},
"./testing/handlers/session-doctor-metro": {
"types": "./src/sessions/handlers/session-doctor-metro.ts",
"default": "./src/sessions/handlers/session-doctor-metro.ts"
},
"./testing/handlers/session-replay-command": {
"types": "./src/sessions/handlers/session-replay-command.ts",
"default": "./src/sessions/handlers/session-replay-command.ts"
Expand Down Expand Up @@ -348,10 +332,6 @@
"types": "./src/core/session-capture-binding.ts",
"default": "./src/core/session-capture-binding.ts"
},
"./testing/session-lifecycle/index": {
"types": "./src/sessions/session-lifecycle/index.ts",
"default": "./src/sessions/session-lifecycle/index.ts"
},
"./testing/session-lifecycle/internal/__tests__/*": {
"types": "./src/sessions/session-lifecycle/internal/__tests__/*.ts",
"default": "./src/sessions/session-lifecycle/internal/__tests__/*.ts"
Expand Down Expand Up @@ -384,10 +364,6 @@
"types": "./src/core/session-store.ts",
"default": "./src/core/session-store.ts"
},
"./testing/snapshot-capture": {
"types": "./src/core/snapshot-capture.ts",
"default": "./src/core/snapshot-capture.ts"
},
"./testing/snapshot-diff-runtime": {
"types": "./src/sessions/snapshot-diff-runtime.ts",
"default": "./src/sessions/snapshot-diff-runtime.ts"
Expand All @@ -404,10 +380,6 @@
"types": "./src/sessions/snapshot-runtime.ts",
"default": "./src/sessions/snapshot-runtime.ts"
},
"./testing/snapshot-session": {
"types": "./src/core/snapshot-session.ts",
"default": "./src/core/snapshot-session.ts"
},
"./testing/wait-runtime": {
"types": "./src/execution/wait-runtime.ts",
"default": "./src/execution/wait-runtime.ts"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { DaemonProviderCredentials } from '@agent-device/daemon-contracts/p
* passthrough, never fingerprint content. So the fake builds a deterministic opaque key from
* the sorted defined pairs rather than reproducing the real reader's digest format (the real
* provider/manifest-aware selection and digest semantics are pinned root-owned in
* request-router-provider-credentials and lease-provider-credentials). Distinct environments
* request-router-provider-credentials and provider-credential-fingerprint). Distinct environments
* produce distinct keys and an empty environment produces `undefined` — the whole contract the
* daemon suites observe.
*/
Expand Down
67 changes: 67 additions & 0 deletions packages/daemon/src/core/__tests__/snapshot-capture.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
androidDevice,
androidTextRows,
inboxBaselineNodes,
inboxRow,
makeAndroidFreshnessSession,
} from '../../__tests__/support/handlers/snapshot-handler.fixtures.ts';
import { pendingSurfaceStabilization } from '../deferred-interaction-outcome.ts';
Expand Down Expand Up @@ -154,3 +155,69 @@ test('captureSnapshot composes post-gesture stabilization with Android freshness
expect(session.androidSnapshotFreshness).toBeUndefined();
expect(pendingSurfaceStabilization(session)).toBeUndefined();
});

test('captureSnapshot retries a stale Android tree after recent navigation until the tree changes', async () => {
resetLegacySnapshotCapture(captureSnapshotWithInteractor);
const session = makeAndroidFreshnessSession(
'android-stale-retry',
'press',
inboxBaselineNodes(24),
);
legacyDispatchCapture
.mockResolvedValueOnce(
androidCapture(androidTextRows(24, inboxRow), { rawNodeCount: 24, maxDepth: 2 }),
)
.mockResolvedValueOnce(
androidCapture(
[
{ index: 0, depth: 0, type: 'android.widget.TextView', label: 'Create document' },
{ index: 1, depth: 0, type: 'android.widget.Button', label: 'Submit', hittable: true },
],
{ rawNodeCount: 2, maxDepth: 1 },
),
);

const result = await captureSnapshot({
device: androidDevice,
session,
flags: { snapshotInteractiveOnly: true },
logPath: '/tmp/daemon.log',
});

expect(result.snapshot.nodes).toEqual(
expect.arrayContaining([expect.objectContaining({ label: 'Create document' })]),
);
expect(legacyDispatchCapture).toHaveBeenCalledTimes(2);
expect(session.androidSnapshotFreshness).toBeUndefined();
});

test('Android ref refresh mode does not retry narrow snapshots as sharp drops', async () => {
resetLegacySnapshotCapture(captureSnapshotWithInteractor);
const session = makeAndroidFreshnessSession(
'android-ref-refresh-no-sharp-drop',
'press',
buildNodes(androidTextRows(50, (row) => `Previous row ${row}`)),
);
legacyDispatchCapture.mockResolvedValue(
androidCapture(
Array.from({ length: 8 }, (_, index) => ({
index,
depth: 0,
type: 'android.widget.TextView',
})),
{ rawNodeCount: 8, maxDepth: 1 },
),
);

const result = await captureSnapshot({
device: androidDevice,
session,
flags: { snapshotInteractiveOnly: true },
logPath: '/tmp/daemon.log',
androidFreshnessMode: 'ref-refresh',
});

expect(result.freshness).toBeUndefined();
expect(legacyDispatchCapture).toHaveBeenCalledTimes(1);
expect(session.androidSnapshotFreshness).toBeUndefined();
});
83 changes: 83 additions & 0 deletions packages/daemon/src/core/snapshot-session.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { afterEach, expect, test } from 'vitest';
import { IOS_SIMULATOR } from '@agent-device/test-kit/device-fixtures';
import { withSessionlessRunnerCleanup } from './snapshot-session.ts';
import { installProviderDeviceAdmission } from './provider-device-admission.ts';
import { platformResourceCleanupFixture } from '../__tests__/support/platform-resource-cleanup-fixture.ts';
import { makeSession } from '../__tests__/support/session-factories.ts';

const returnOk = async () => 'ok';

afterEach(() => {
installProviderDeviceAdmission({ isActive: () => false });
});

test('sessionless cleanup releases the local execution host after the task', async () => {
const events: string[] = [];
const platformCleanup = platformResourceCleanupFixture({
cleanupSessionlessExecutionHost: async () => {
events.push('cleanup');
},
});

const result = await withSessionlessRunnerCleanup(
undefined,
IOS_SIMULATOR,
async () => {
events.push('task');
return 'ok';
},
platformCleanup,
);

expect(result).toBe('ok');
expect(events).toEqual(['task', 'cleanup']);
expect(platformCleanup.cleanupSessionlessExecutionHost).toHaveBeenCalledWith(IOS_SIMULATOR);
Comment thread
thymikee marked this conversation as resolved.
});

test('sessionless cleanup runs when the task fails and keeps the failure', async () => {
const platformCleanup = platformResourceCleanupFixture();
const failure = new Error('capture failed');

await expect(
withSessionlessRunnerCleanup(
undefined,
IOS_SIMULATOR,
async () => {
throw failure;
},
platformCleanup,
),
).rejects.toBe(failure);
expect(platformCleanup.cleanupSessionlessExecutionHost).toHaveBeenCalledWith(IOS_SIMULATOR);
});

test('sessionless cleanup leaves a provider-owned device to its provider', async () => {
const device = { ...IOS_SIMULATOR, id: 'limrun:ios:lease-a' };
installProviderDeviceAdmission({ isActive: (candidate) => candidate.id === device.id });
const platformCleanup = platformResourceCleanupFixture();

const result = await withSessionlessRunnerCleanup(undefined, device, returnOk, platformCleanup);

expect(result).toBe('ok');
expect(platformCleanup.cleanupSessionlessExecutionHost).not.toHaveBeenCalled();
});

test('a session keeps its execution host past the task', async () => {
const platformCleanup = platformResourceCleanupFixture();

const result = await withSessionlessRunnerCleanup(
makeSession('open-session', { device: IOS_SIMULATOR }),
IOS_SIMULATOR,
returnOk,
platformCleanup,
);

expect(result).toBe('ok');
expect(platformCleanup.cleanupSessionlessExecutionHost).not.toHaveBeenCalled();
});

test('sessionless cleanup refuses to run without an injected platform cleanup', async () => {
await expect(withSessionlessRunnerCleanup(undefined, IOS_SIMULATOR, returnOk)).rejects.toThrow(
'Platform resource cleanup was not injected',
);
});
98 changes: 97 additions & 1 deletion packages/daemon/src/execution/wait-current-surface.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const req = {
* not on the `snapshot-capture` module — and "no capture started" is provable as "the binding
* was never invoked".
*/
function boundCapture(nodes: { index: number; depth: number; type: string; label?: string }[]) {
function boundCapture(nodes: readonly Record<string, unknown>[]) {
return vi.fn(async () => ({ nodes, backend: 'android' })) as unknown as BoundSelectorCapture &
ReturnType<typeof vi.fn>;
}
Expand Down Expand Up @@ -97,6 +97,102 @@ test('an absent-target wait describes the current surface through its own reques
});
});

test('the surface summary prefers content labels over chrome and identifier noise', async () => {
const capture = boundCapture([
{
index: 0,
depth: 0,
type: 'XCUIElementTypeApplication',
label: 'Expo Go',
rect: { x: 0, y: 0, width: 393, height: 852 },
},
{
index: 1,
depth: 1,
type: 'XCUIElementTypeImage',
label: 'gearshape.fill',
rect: { x: 12, y: 54, width: 24, height: 24 },
},
{
index: 2,
depth: 1,
type: 'XCUIElementTypeOther',
label: 'Tab Bar',
rect: { x: 0, y: 760, width: 393, height: 92 },
},
{
index: 3,
depth: 1,
type: 'XCUIElementTypeStaticText',
label: 'Confirm catalog refresh',
rect: { x: 48, y: 280, width: 297, height: 36 },
},
{
index: 4,
depth: 1,
type: 'XCUIElementTypeButton',
label: 'Keep browsing',
rect: { x: 48, y: 360, width: 297, height: 48 },
},
{
index: 5,
depth: 1,
type: 'XCUIElementTypeButton',
identifier: 'host.exp.exponent:id/reload_button',
rect: { x: 260, y: 54, width: 48, height: 48 },
},
]);

const result = await maybeWaitTimeoutSurfaceResponse(
{ req, session: undefined, device: ANDROID_EMULATOR, capture },
{
ok: false as const,
error: {
code: 'COMMAND_FAILED' as const,
message: 'wait timed out for text: Impossible success text',
details: { reason: 'wait_target_absent' },
},
},
);

expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.message).toBe(
'wait timed out for text: Impossible success text. Current surface: Confirm catalog refresh, Keep browsing.',
);
expect(result.error.details?.currentSurface).toEqual({
labels: [
'Confirm catalog refresh',
'Keep browsing',
'host.exp.exponent:id/reload_button',
'Expo Go',
'gearshape.fill',
'Tab Bar',
],
buttons: ['Keep browsing', 'host.exp.exponent:id/reload_button'],
});
});

test('a capture-stalled wait failure is returned untouched without inspecting the surface', async () => {
const capture = boundCapture([]);
const response = {
ok: false as const,
error: {
code: 'COMMAND_FAILED' as const,
message: 'wait timed out for text: Receipt uploaded',
details: { reason: 'wait_capture_stalled', retriable: true, readableCaptures: 0 },
},
};

const result = await maybeWaitTimeoutSurfaceResponse(
{ req, session: undefined, device: ANDROID_EMULATOR, capture },
response,
);

expect(result).toBe(response);
expect(capture).not.toHaveBeenCalled();
});

/**
* The decoration capture is a real capture: when it is the command's own foreground repair, the
* timed-out wait that reads this surface owes the disclosure, and the proof has to be recorded where
Expand Down
Loading
Loading