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
12 changes: 9 additions & 3 deletions packages/provider-limrun/src/app-log-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,15 @@ test('keeps exact-owner app-log recovery available without a process-local sessi
available: false,
reason: 'owner-capability-missing',
});
expect(() => narrowDeviceBinding(binding, appStateUse)).toThrow();
expect(() => narrowDeviceBinding(binding, bootTargetUse)).toThrow();
expect(() => narrowDeviceBinding(binding, appsRuntimeUse)).toThrow();
expect(() => narrowDeviceBinding(binding, appStateUse)).toThrow(
expect.objectContaining({ code: 'UNSUPPORTED_OPERATION' }),
);
expect(() => narrowDeviceBinding(binding, bootTargetUse)).toThrow(
expect.objectContaining({ code: 'UNSUPPORTED_OPERATION' }),
);
expect(() => narrowDeviceBinding(binding, appsRuntimeUse)).toThrow(
expect.objectContaining({ code: 'UNSUPPORTED_OPERATION' }),
);
await expect(binding.operations.appLogReattach?.({ envelope })).resolves.toEqual({
status: 'missing',
});
Expand Down
9 changes: 8 additions & 1 deletion packages/provider-webdriver/src/webdriver-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,14 @@ test('activeElement bounds its two sequential requests by one shared budget', as
});
});

await assert.rejects(client.activeElement(budgetMs));
// Not an AppError: this is the raw AbortSignal.timeout() rejection the transport
// re-throws unwrapped once the combined signal is already aborted (see
// shouldRetryWebDriverRequest in webdriver-transport.ts) — assert the timeout's
// own reason instead of any failure.
await assert.rejects(
client.activeElement(budgetMs),
(error: unknown) => error instanceof Error && error.name === 'TimeoutError',
);

assert.ok(rectRequestBudgetMs !== undefined, 'the rect request should have been made');
// It must get what the first call left (~120ms), never a fresh 200ms.
Expand Down
20 changes: 20 additions & 0 deletions src/__tests__/test-utils/app-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,23 @@ export async function assertRejectsAppError(
return true;
});
}

/**
* Synchronous sibling of {@link assertRejectsAppError}: asserts that `fn`
* throws an {@link AppError} carrying `code` and, when given, a message
* matching `message`.
*/
export function assertThrowsAppError(
fn: () => unknown,
expected: { code: string; message?: RegExp },
): void {
assert.throws(fn, (error: unknown) => {
assert.ok(
error instanceof AppError,
`expected AppError, got ${error?.constructor?.name ?? typeof error}: ${String(error)}`,
);
assert.equal(error.code, expected.code);
if (expected.message) assert.match(error.message, expected.message);
return true;
});
}
2 changes: 1 addition & 1 deletion src/__tests__/test-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export { withFakeAdb, type FakeAdbResponse } from './fake-adb.ts';

export { withFakeAppleTool, type FakeAppleToolResponse } from './fake-apple-tool.ts';

export { assertRejectsAppError } from './app-error.ts';
export { assertRejectsAppError, assertThrowsAppError } from './app-error.ts';

export {
COMPACT_VIEWPORTS,
Expand Down
7 changes: 6 additions & 1 deletion src/daemon/__tests__/app-log.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,16 @@ test.each(['metadata', 'mark', 'clear'] as const)(
fs.writeFileSync(outsidePath, 'outside');
fs.symlinkSync(outsidePath, outPath);

// `mark` runs through ensureAppLogPath's own symlink guard ("must not be a symbolic
// link"); `metadata` and `clear` go straight to the shared verified-file open path,
// whose identity check reports the path as simply not a regular file.
const expectedMessage =
operation === 'mark' ? /must not be a symbolic link/ : /must be a regular file/;
expect(() => {
if (operation === 'metadata') getAppLogPathMetadata(outPath);
else if (operation === 'mark') appendAppLogMarker(outPath, 'checkpoint');
else clearAppLogFiles(outPath);
}).toThrow();
}).toThrow(expectedMessage);
expect(fs.readFileSync(outsidePath, 'utf8')).toBe('outside');
expect(fs.lstatSync(outPath).isSymbolicLink()).toBe(true);
},
Expand Down
14 changes: 12 additions & 2 deletions src/daemon/__tests__/resumable-upload-range.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import { assertThrowsAppError } from '../../__tests__/test-utils/index.ts';
import { parseUploadContentLength, parseUploadContentRange } from '../resumable-upload-range.ts';

test('content ranges are bounded by the declared upload size', () => {
Expand All @@ -10,13 +11,22 @@ test('content ranges are bounded by the declared upload size', () => {
span: 3,
});
for (const value of ['bytes 0-5/5', 'bytes 5-5/5', 'bytes 0-0/0']) {
assert.throws(() => parseUploadContentRange(value, Number(value.split('/')[1])));
assertThrowsAppError(() => parseUploadContentRange(value, Number(value.split('/')[1])), {
code: 'INVALID_ARGS',
message: /Invalid content-range header/,
});
}
});

test('content range and length numbers use decimal safe-integer grammar', () => {
for (const value of ['+1', '1e3', '0x10', '1.5', '-1', '9007199254740992']) {
assert.throws(() => parseUploadContentLength(value), value);
// A string second argument to node:assert's throws helper is its failure message,
// never an error matcher (a documented Node.js gotcha) — this loop was silently
// accepting any failure. Assert the real code instead.
assertThrowsAppError(() => parseUploadContentLength(value), {
code: 'INVALID_ARGS',
message: /Invalid content-length header/,
});
}
assert.equal(parseUploadContentLength('0'), 0);
assert.equal(parseUploadContentLength('123'), 123);
Expand Down
107 changes: 61 additions & 46 deletions src/daemon/handlers/__tests__/session-device-claims.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,43 +124,53 @@ test('failed local open before device setup rolls its device claim back', async
// now, so this is where a pre-device-effect failure surfaces.
const stoppedAvd = { ...android, id: 'pixel-avd', booted: false };
mockResolveTargetDevice.mockResolvedValue(stoppedAvd);
mockDiscoverReadyAndroidEmulators.mockRejectedValue(new Error('device not ready'));

await assert.rejects(async () =>
handleOpenCommand({
req: {
command: 'open',
token: 'test',
session: 'claim-rollback',
positionals: ['Demo'],
flags: { platform: 'android' },
},
sessionName: 'claim-rollback',
logPath: path.join(stateDir, 'daemon.log'),
sessionStore: store,
}),
// The point of this test is claim-rollback behavior around an opaque pre-device-effect
// failure, not any particular error shape, so assert identity of the propagated error.
const rejectionError = new Error('device not ready');
mockDiscoverReadyAndroidEmulators.mockRejectedValue(rejectionError);

await assert.rejects(
async () =>
handleOpenCommand({
req: {
command: 'open',
token: 'test',
session: 'claim-rollback',
positionals: ['Demo'],
flags: { platform: 'android' },
},
sessionName: 'claim-rollback',
logPath: path.join(stateDir, 'daemon.log'),
sessionStore: store,
}),
(error: unknown) => error === rejectionError,
);
assert.deepEqual(inspectDeviceClaims({ serial: stoppedAvd.id }), []);
});

test('failed local open after dispatch retains its device claim for recovery', async () => {
const { store, stateDir } = setup();
mockResolveTargetDevice.mockResolvedValue(android);
mockDispatch.mockRejectedValue(new Error('open failed'));

await assert.rejects(async () =>
handleOpenCommand({
req: {
command: 'open',
token: 'test',
session: 'claim-dispatch-failure',
positionals: ['Demo'],
flags: { platform: 'android' },
},
sessionName: 'claim-dispatch-failure',
logPath: path.join(stateDir, 'daemon.log'),
sessionStore: store,
}),
// The point of this test is claim-retention behavior around an opaque post-dispatch
// failure, not any particular error shape, so assert identity of the propagated error.
const rejectionError = new Error('open failed');
mockDispatch.mockRejectedValue(rejectionError);

await assert.rejects(
async () =>
handleOpenCommand({
req: {
command: 'open',
token: 'test',
session: 'claim-dispatch-failure',
positionals: ['Demo'],
flags: { platform: 'android' },
},
sessionName: 'claim-dispatch-failure',
logPath: path.join(stateDir, 'daemon.log'),
sessionStore: store,
}),
(error: unknown) => error === rejectionError,
);
assert.equal(inspectDeviceClaims({ serial: android.id })[0]?.classification, 'live');
});
Expand All @@ -169,22 +179,27 @@ test('failed local runtime-hint setup retains its device claim before open dispa
const { store, stateDir } = setup();
mockResolveTargetDevice.mockResolvedValue(android);
mockResolveAndroidPackage.mockResolvedValue('com.example.demo');
mockApplyRuntimeHints.mockRejectedValue(new Error('runtime hints changed before failure'));

await assert.rejects(async () =>
handleOpenCommand({
req: {
command: 'open',
token: 'test',
session: 'claim-runtime-hint-failure',
positionals: ['Demo'],
flags: { platform: 'android' },
runtime: { metroHost: '10.0.0.10', metroPort: 8081 },
},
sessionName: 'claim-runtime-hint-failure',
logPath: path.join(stateDir, 'daemon.log'),
sessionStore: store,
}),
// The point of this test is claim-retention behavior around an opaque pre-dispatch
// failure, not any particular error shape, so assert identity of the propagated error.
const rejectionError = new Error('runtime hints changed before failure');
mockApplyRuntimeHints.mockRejectedValue(rejectionError);

await assert.rejects(
async () =>
handleOpenCommand({
req: {
command: 'open',
token: 'test',
session: 'claim-runtime-hint-failure',
positionals: ['Demo'],
flags: { platform: 'android' },
runtime: { metroHost: '10.0.0.10', metroPort: 8081 },
},
sessionName: 'claim-runtime-hint-failure',
logPath: path.join(stateDir, 'daemon.log'),
sessionStore: store,
}),
(error: unknown) => error === rejectionError,
);

assert.equal(mockApplyRuntimeHints.mock.calls.length, 1);
Expand Down
5 changes: 4 additions & 1 deletion src/platforms/android/__tests__/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ test('setAndroidSetting fingerprint does not use adb emu command on physical dev
await withFakeAdb(
() => ({ stderr: 'unknown command', exitCode: 1 }),
async ({ calls, device }) => {
await assert.rejects(() => setAndroidSetting(device, 'fingerprint', 'match'));
await assertRejectsAppError(() => setAndroidSetting(device, 'fingerprint', 'match'), {
code: 'UNSUPPORTED_OPERATION',
message: /Android fingerprint simulation is not supported/,
});
const emuCalls = calls.filter((args) => args[0] === 'emu');
assert.deepEqual(emuCalls, []);
},
Expand Down
20 changes: 18 additions & 2 deletions src/platforms/android/__tests__/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1068,7 +1068,15 @@ test('snapshotAndroid emits helper failure diagnostics', async () => {
const diagnostics = await captureDiagnostics(
{ session: 'snapshot-failure', requestId: 'req-2', command: 'snapshot', debug: true },
async () => {
await assert.rejects(() => snapshotAndroid(device, { helperAdb, helperArtifact }));
await assert.rejects(
() => snapshotAndroid(device, { helperAdb, helperArtifact }),
(error: unknown) => {
assert(error instanceof AppError);
assert.equal(error.code, 'COMMAND_FAILED');
assert.match(error.message, /helper unavailable/);
return true;
},
);
return flushDiagnosticsToSessionFile({ force: true });
},
);
Expand Down Expand Up @@ -1339,7 +1347,15 @@ test('snapshotAndroid re-probes helper install after helper capture failure', as
helperArtifact,
};

await assert.rejects(() => snapshotAndroid(device, helperOptions));
await assert.rejects(
() => snapshotAndroid(device, helperOptions),
(error: unknown) => {
assert(error instanceof AppError);
assert.equal(error.code, 'COMMAND_FAILED');
assert.match(error.message, /instrumentation failed/);
return true;
},
);
const helper = await snapshotAndroid(device, helperOptions);

assert.equal(helper.androidSnapshot.backend, 'android-helper');
Expand Down
6 changes: 6 additions & 0 deletions src/platforms/android/__tests__/touch-helper-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,12 @@ test('a malformed session gesture response stops the session and does not fall b
{ serial: device.id },
async () => await executeAndroidTouchHelperPlan(device, lowerAndroidTouchPlan(flingPlan())),
),
(error: unknown) => {
assert.ok(error instanceof AppError);
assert.equal(error.code, 'COMMAND_FAILED');
assert.match(error.message, /wrong protocol/);
return true;
},
);

assert.equal(instrumentCalled, false);
Expand Down
Loading
Loading