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
23 changes: 23 additions & 0 deletions LICENSES/xdelta3-wasm.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
@limrun/xdelta3-wasm 0.2.0 source-file notices

The published package declares Apache-2.0 but omits the license text.
Package source: https://github.com/limrun-inc/typescript-sdk/tree/main/packages/xdelta3-wasm
Native source: https://github.com/jmacd/xdelta/tree/7508fd2a823443b1f0173ca361620f21d62a7d37/xdelta3

xdelta3 - delta compression tools and library
Copyright 2016 Joshua MacDonald

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.


Full Apache-2.0 license follows in THIRD_PARTY_NOTICES.txt.
6 changes: 6 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ the package-level BSD license. These are retained in
[`LICENSES/jpeg-js.txt`](LICENSES/jpeg-js.txt) and included with the Apache License
2.0 in the generated notice file whenever `jpeg-js` is bundled.

`@limrun/xdelta3-wasm` 0.2.0 omits license files from its npm archive. Its Apache-2.0
license text and xdelta3 source attribution are retained through
[`LICENSES/xdelta3-wasm.txt`](LICENSES/xdelta3-wasm.txt) and
[`LICENSES/Apache-2.0.txt`](LICENSES/Apache-2.0.txt). The build verifies the package
version before including these notices.

## Maestro conformance fixtures

The 42 upstream flows in `scripts/maestro-conformance/corpus/upstream/` are copied
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@
"@agent-device/xml": "workspace:*",
"@arethetypeswrong/cli": "^0.18.5",
"@chenglou/freerange": "^0.0.4",
"@limrun/api": "^0.68.1",
"@limrun/api": "^0.69.1",
"@nkzw/eslint-plugin": "^2.0.0",
"@nkzw/oxlint-config": "^1.3.0",
"@statelyai/graph": "2.4.0",
Expand Down
40 changes: 29 additions & 11 deletions packages/platform-apple/src/launch-confirmation.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test, vi } from 'vitest';
import { setTimeout as realDelay } from 'node:timers/promises';
import { DEFAULT_ALERT_TIMEOUT_MS } from '@agent-device/contracts/alert-contract';
import { awaitAppleAlert, readAppleAlert } from './alert.ts';
import { runAppleRunnerCommand } from './core/runner-client.ts';
Expand Down Expand Up @@ -218,17 +219,20 @@ test.each([
*/
function wedgedCoreSimulator(
answers: Readonly<Record<string, string>> = {},
onSpawn?: (count: number) => void,
): AppleToolProvider & { spawns: () => number } {
let spawns = 0;
const answered = (key: string): ExecResult | undefined => {
const stdout = answers[key];
if (stdout === undefined) return undefined;
spawns += 1;
onSpawn?.(spawns);
return { stdout, stderr: '', exitCode: 0 } as ExecResult;
};
const hang = async (options?: ExecOptions): Promise<ExecResult> =>
await new Promise((_resolve, reject) => {
spawns += 1;
onSpawn?.(spawns);
if (options?.signal?.aborted) {
reject(options.signal.reason);
return;
Expand Down Expand Up @@ -257,14 +261,6 @@ function wedgedCoreSimulator(
};
}

/** Real I/O turns until the provider saw `count` spawns; bounded so a lookup that stops early fails here, not at the test timeout. */
async function untilSpawned(coreSimulator: { spawns: () => number }, count: number): Promise<void> {
for (let turn = 0; turn < 1000 && coreSimulator.spawns() < count; turn += 1) {
await new Promise((resolve) => setImmediate(resolve));
}
expect(coreSimulator.spawns()).toBe(count);
}

const LISTED_APP = JSON.stringify({
'com.example.app': {
ApplicationType: 'User',
Expand All @@ -286,21 +282,42 @@ test.each([
// spawns plutil, and that I/O must complete before the budget is advanced past.
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'Date'] });
vi.mocked(emitDiagnostic).mockClear();
const lookupAbort = new AbortController();
try {
const { interactor, acceptAlert } = confirmationRunner();
const coreSimulator = wedgedCoreSimulator(answers);
let signalLookupStarted: () => void;
const lookupStarted = new Promise<void>((resolve) => {
signalLookupStarted = resolve;
});
const coreSimulator = wedgedCoreSimulator(answers, (count) => {
if (count === 2) signalLookupStarted();
});
let answered: unknown = 'pending';
const answer = withAppleToolProvider(coreSimulator, async () =>
answerSimulatorLaunchConfirmation(
simulator,
{ url: 'example://automation', appBundleId: 'com.example.app' },
Promise.resolve(interactor),
new AbortController().signal,
lookupAbort.signal,
undefined,
).then((value) => (answered = value)),
);

await untilSpawned(coreSimulator, 2);
const ioWait = new AbortController();
try {
await Promise.race([
lookupStarted,
answer.then(() => {
throw new Error('Owner lookup completed before the second command started');
}),
realDelay(1_000, undefined, { signal: ioWait.signal }).then(() => {
throw new Error('Owner lookup did not start its second command');
}),
]);
} finally {
ioWait.abort();
}
expect(coreSimulator.spawns()).toBe(2);
await vi.advanceTimersByTimeAsync(URL_OWNER_LOOKUP_TIMEOUT_MS + 1);
expect(answered).toEqual({ outcome: 'unreadable', step: 'url-owner' });
await answer;
Expand All @@ -311,6 +328,7 @@ test.each([
}),
);
} finally {
lookupAbort.abort();
vi.useRealTimers();
}
},
Expand Down
3 changes: 2 additions & 1 deletion packages/provider-limrun/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"devDependencies": {
"@agent-device/platform-apple": "workspace:*",
"@limrun/api": "^0.68.1"
"@agent-device/test-kit": "workspace:*",
"@limrun/api": "^0.69.1"
}
}
50 changes: 20 additions & 30 deletions packages/provider-limrun/src/android.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import path from 'node:path';
import type { LimrunSyncCache } from './sync-cache.ts';
import { AsyncLocalStorage } from 'node:async_hooks';
import type Limrun from '@limrun/api';
import {
createInstanceClient as createAndroidInstanceClient,
type InstanceClient as LimrunAndroidClient,
Expand Down Expand Up @@ -38,6 +37,7 @@ type LimrunAndroidAdbSession = {
instanceId: string;
readonly ownership: LimrunInstanceOwnership;
device: DeviceInfo;
syncCache?: LimrunSyncCache;
client: LimrunAndroidClient;
/** Instance bearer token; the recording download the SDK would run inline is done by the host instead. */
readonly token: string;
Expand Down Expand Up @@ -114,39 +114,35 @@ export function createLimrunAndroidInteractor(session: LimrunAndroidSession): In
}

export async function installLimrunAndroidApp(
limrun: Limrun,
session: LimrunAndroidSession,
installablePath: string,
options?: ProviderDeviceInstallOptions,
signal?: AbortSignal,
operationDrain?: LimrunRequestOperationDrain,
): Promise<ProviderDeviceInstallResult> {
signal?.throwIfAborted();
const packageName = normalizeOptionalString(options?.packageNameHint);
if (options?.relaunch && packageName) {
await session.dependencies.android.forceStopApp(
async (adbArgs, adbOptions) => await runLimrunAndroidAdb(session, adbArgs, adbOptions),
packageName,
signal,
);
}
const asset = await awaitLimrunDeploymentOperation(
const { LimrunSyncCache } = await import('./sync-cache.ts');
const syncCache = (session.syncCache ??= new LimrunSyncCache());
// The shared deployment owner must retain the artifact until sync finishes reading it.
const result = await awaitLimrunDeploymentOperation(
operationDrain,
limrun.assets.getOrUpload(
{
path: installablePath,
name: buildAndroidAssetName(packageName, installablePath),
},
{ signal },
syncCache.run(
installablePath,
'android',
(basisCacheDir) =>
session.client.syncApp(installablePath, {
basisCacheDir,
install: true,
watch: false,
launchMode: options?.relaunch ? 'RelaunchIfRunning' : 'None',
}),
signal,
),
signal,
);
await awaitLimrunDeploymentOperation(
operationDrain,
session.client.sendAsset(asset.signedDownloadUrl),
signal,
);
signal?.throwIfAborted();
const packageName =
normalizeOptionalString(result.installedBundleId) ??
normalizeOptionalString(options?.packageNameHint);
const appName = packageName
? await session.dependencies.android.inferAppName(packageName)
: undefined;
Expand Down Expand Up @@ -364,9 +360,3 @@ function tcpEndpoint(port: number): LimrunPortReverseEndpoint {
}
return `tcp:${port}`;
}

function buildAndroidAssetName(packageName: string | undefined, artifactPath: string): string {
const extension = path.extname(artifactPath) || '.apk';
const prefix = packageName?.replaceAll(/[^a-zA-Z0-9_.-]+/g, '-') || 'android-app';
return `${prefix}${extension}`;
}
8 changes: 6 additions & 2 deletions packages/provider-limrun/src/deployment-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import type {
import { unavailableFact } from '@agent-device/contracts/platform-runtime';
import type { DeviceInfo } from '@agent-device/kernel/device';
import { AppError } from '@agent-device/kernel/errors';
import type { LimrunRequestOperationDrain } from './request-cancellation.ts';
import {
awaitLimrunDeploymentOperation,
type LimrunRequestOperationDrain,
} from './request-cancellation.ts';
import { isSupportedLimrunRuntimeDevice } from './runtime-device.ts';

const available = Object.freeze({ available: true } as const);
Expand Down Expand Up @@ -95,7 +98,8 @@ export function createLimrunAppDeploymentOperations(
},
}
: {}),
deployApp: async (input) => (await load()).deployApp(input),
deployApp: async (input) =>
awaitLimrunDeploymentOperation(operationDrain, (await load()).deployApp(input), signal),
materializeAppSource: async (input) => (await load()).materializeAppSource(input),
deployMaterializedApp: async (input) => (await load()).deployMaterializedApp(input),
Comment thread
muvaf marked this conversation as resolved.
Resolved
uninstallApp: async (input) => (await load()).uninstallApp(input),
Expand Down
Loading
Loading