Skip to content

Commit b15121f

Browse files
authored
test(ios): establish snapshot convergence baselines and permanent evidence (#2204)
* test(ios): add snapshot convergence evidence harness * fix(ios): satisfy benchmark CI guards * fix(ios): constrain benchmark proxy routes * fix(ios-benchmark): enforce cell admission evidence * fix(ios-benchmark): protect benchmark state ownership * fix(ios-benchmark): use proxy port flag * fix(ios-benchmark): let proxy choose an ephemeral port * test(ios-benchmark): keep CLI process seam local * fix(ios-benchmark): parse proxy startup envelope * fix(ios-benchmark): bind proxy lease to simulator * fix(ios-benchmark): keep fresh proxy CLI sessions isolated * fix(ios-benchmark): preserve async timeout evidence * docs(ios-benchmark): retain exact-head evidence * test(ios): reveal offscreen alert fixture controls * test(ios): reset alert between relaunch samples * test(ios): admit native alert snapshots * docs(ios): publish snapshot convergence corpus * chore(ios): format benchmark evidence * fix(ios-benchmark): admit proxy fixture anchors * docs(ios): republish exact-head benchmark corpus * fix(size): make publish asset evidence hermetic * style(size): format package evidence test * test(size): update publish preparation contracts * fix: retire stale utils layering zone * test: pin shared publish asset owner * test: verify preserved size reporter closure * fix: move mutation ownership to snapshot module * test(ios): add snapshot convergence evidence harness * fix(ios): satisfy benchmark CI guards * fix(ios): constrain benchmark proxy routes * fix(ios-benchmark): enforce cell admission evidence * fix(ios-benchmark): protect benchmark state ownership * fix(ios-benchmark): use proxy port flag * fix(ios-benchmark): let proxy choose an ephemeral port * test(ios-benchmark): keep CLI process seam local * fix(ios-benchmark): parse proxy startup envelope * fix(ios-benchmark): bind proxy lease to simulator * fix(ios-benchmark): keep fresh proxy CLI sessions isolated * fix(ios-benchmark): preserve async timeout evidence * docs(ios-benchmark): retain exact-head evidence * test(ios): reveal offscreen alert fixture controls * test(ios): reset alert between relaunch samples * test(ios): admit native alert snapshots * docs(ios): publish snapshot convergence corpus * chore(ios): format benchmark evidence * fix(ios-benchmark): admit proxy fixture anchors * docs(ios): republish exact-head benchmark corpus * fix(size): make publish asset evidence hermetic * test(size): update publish preparation contracts * fix: keep git-state gates out of mutation sandboxes
1 parent 04fb10d commit b15121f

72 files changed

Lines changed: 25109 additions & 48 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/size.yml‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,21 @@ jobs:
4444
run: |
4545
mkdir -p /tmp/agent-device-size-report
4646
cp scripts/size-report*.mjs /tmp/agent-device-size-report/
47+
cp scripts/prepare-publish-assets.mjs /tmp/agent-device-size-report/
48+
49+
- name: Install Android helper toolchain
50+
run: |
51+
SDK_ROOT="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}}"
52+
SDKMANAGER="$SDK_ROOT/cmdline-tools/latest/bin/sdkmanager"
53+
if [ ! -x "$SDKMANAGER" ]; then
54+
SDKMANAGER="$SDK_ROOT/cmdline-tools/bin/sdkmanager"
55+
fi
56+
if [ ! -x "$SDKMANAGER" ]; then
57+
echo "sdkmanager not found under $SDK_ROOT" >&2
58+
exit 1
59+
fi
60+
yes | "$SDKMANAGER" --licenses >/dev/null || true
61+
"$SDKMANAGER" "platforms;android-36" "build-tools;36.0.0"
4762
4863
# dist is fully determined by the base commit, so reuse it across PR runs
4964
# against the same base. Startup medians are still measured fresh on this

‎package.json‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@
107107
"package:android-ime-helper:npm": "rm -rf android/ime-helper/dist && AGENT_DEVICE_ANDROID_HELPER=ime sh ./scripts/package-android-helper.sh $(node -p \"require('./package.json').version\") android/ime-helper/dist",
108108
"build:macos-helper": "node --experimental-strip-types scripts/swift-toolchain-tmpdir.ts swift build -c release --package-path apple/macos-helper",
109109
"build:macos-helper:clean": "node --experimental-strip-types scripts/swift-toolchain-tmpdir.ts swift package --package-path apple/macos-helper clean && pnpm build:macos-helper",
110-
"build:package": "pnpm build && pnpm build:xcuitest:ios && pnpm build:xcuitest:macos && pnpm build:xcuitest:tvos && pnpm build:xcuitest:visionos && pnpm build:macos-helper:clean && pnpm package:apple-runner:npm && pnpm build:android",
110+
"prepare:publish-assets": "node scripts/prepare-publish-assets.mjs",
111+
"build:package": "pnpm build && pnpm build:xcuitest:ios && pnpm build:xcuitest:macos && pnpm build:xcuitest:tvos && pnpm build:xcuitest:visionos && pnpm build:macos-helper:clean && pnpm prepare:publish-assets",
111112
"package:npm": "pnpm build:package && pnpm check:package",
112113
"release:prepare": "node scripts/release-mark-dev.mjs --check-release-version && rm -rf .tmp/release && pnpm check:mcp-metadata && pnpm build:package && pnpm check:package -- --pack-destination .tmp/release",
113114
"release:publish": "pnpm release:prepare && npm publish --ignore-scripts .tmp/release/*.tgz && pnpm release:mark-dev",
@@ -120,6 +121,8 @@
120121
"test:ios-snapshot-differential": "node --experimental-strip-types scripts/swift-toolchain-tmpdir.ts swift test --package-path apple/snapshot-presentation && node --experimental-strip-types scripts/node-test-tmpdir.ts --experimental-strip-types --test scripts/ios-snapshot-differential.test.ts",
121122
"size": "node scripts/size-report.mjs",
122123
"perf": "node --experimental-strip-types scripts/perf/run.ts",
124+
"bench:ios-snapshot": "node --experimental-strip-types scripts/ios-snapshot-benchmark/run.ts",
125+
"bench:ios-snapshot:deep-button": "node --experimental-strip-types scripts/ios-snapshot-benchmark/deep-button.ts",
123126
"mutation:run": "node --experimental-strip-types scripts/mutation/run.ts",
124127
"mutation:check": "node --experimental-strip-types scripts/mutation/run.ts --no-run",
125128
"mutation:affected": "node --experimental-strip-types scripts/mutation/run.ts --affected",

src/__tests__/eager-closure-budgets.test.ts renamed to scripts/__tests__/eager-closure-budgets.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { execFileSync } from 'node:child_process';
33
import fs from 'node:fs';
44
import os from 'node:os';
55
import path from 'node:path';
6-
import { eagerClosureGraphOf } from './eager-import-closure.fixtures.ts';
6+
import { eagerClosureGraphOf } from '../../src/__tests__/eager-import-closure.fixtures.ts';
77
import {
88
classifyBudget,
99
describeClosurePressure,
@@ -22,7 +22,8 @@ import {
2222
* ADR-0019 requires platform-package façades to stay implementation-lazy and is explicit that a
2323
* startup threshold alone is not a substitute for preserving the loading shape: "the tracking
2424
* issue owns the exact probe and planted-red procedure." #1950 built the walker this file reuses
25-
* (`eager-import-closure.fixtures.ts`, AST-level: static value edges plus top-level dynamic
25+
* (`src/__tests__/eager-import-closure.fixtures.ts`, AST-level: static value edges plus top-level
26+
* dynamic
2627
* imports, type-only erased) and proved the planted-red procedure on one file. This is that
2728
* probe, generalized to every workspace-package entry surface plus designated hub modules.
2829
*

src/__tests__/eager-closure-budgets.ts renamed to scripts/__tests__/eager-closure-budgets.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
// implementation-lazy and is explicit that a startup-time threshold alone is not a substitute for
55
// preserving the loading shape (`docs/adr/0019-request-bound-platform-runtime.md`): "the tracking
66
// issue owns the exact probe and planted-red procedure." #1950 built the AST-level walker
7-
// (`eager-import-closure.fixtures.ts`); #1959/#1969 fixed two more instances of the regression
7+
// (`src/__tests__/eager-import-closure.fixtures.ts`); #1959/#1969 fixed two more instances of the
8+
// regression
89
// class by hand. This table generalizes the proof: every package entry surface gets an exact pin
910
// on how many repo modules importing it evaluates, plus a standing assertion that the closure
1011
// never reaches a concrete platform implementation before discovery/binding selects one.
@@ -22,7 +23,7 @@
2223
// enforced by the compiler instead of by a runtime check that a `Set` conversion would hide.
2324

2425
import path from 'node:path';
25-
import { facadeEntryFiles } from '../../scripts/layering/package-boundaries.ts';
26+
import { facadeEntryFiles } from '../layering/package-boundaries.ts';
2627

2728
export type EagerClosureBudget = {
2829
/** Stable label for test names and failure messages -- the entry's repo-relative path. */

‎scripts/__tests__/fixtures/size-report-npm-pack.json‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
{
2-
"unpackedSize": 1814,
2+
"unpackedSize": 2319,
33
"files": [
44
{ "path": "dist/src/index.js", "size": 401 },
55
{ "path": "dist/src/index.d.ts", "size": 102 },
66
{ "path": "dist/apple/runner/RunnerTests.swift", "size": 503 },
77
{ "path": "dist/apple/snapshot-presentation/Package.swift", "size": 113 },
88
{ "path": "apple/macos-helper/Sources/main.swift", "size": 211 },
99
{ "path": "android/snapshot-helper/dist/helper.apk", "size": 307 },
10+
{ "path": "android/snapshot-helper/dist/helper.manifest.json", "size": 99 },
11+
{ "path": "android/ime-helper/dist/helper.apk", "size": 307 },
12+
{ "path": "android/ime-helper/dist/helper.manifest.json", "size": 99 },
1013
{ "path": "package.json", "size": 99 },
1114
{ "path": "vendor/unknown.bin", "size": 78 }
1215
]
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import assert from 'node:assert/strict';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
import { test } from 'vitest';
5+
import { mkdtempForTestSync } from '../../src/__tests__/test-utils/tmp-dir.ts';
6+
import { preparePublishAssets } from '../prepare-publish-assets.mjs';
7+
8+
test('prepares both Android runtime helpers through the shared publish owner', () => {
9+
const root = mkdtempForTestSync('agent-device-publish-assets-');
10+
const scriptsDirectory = path.join(root, 'scripts');
11+
fs.mkdirSync(scriptsDirectory, { recursive: true });
12+
fs.writeFileSync(path.join(root, 'package.json'), '{"version":"1.2.3"}\n');
13+
fs.writeFileSync(path.join(scriptsDirectory, 'package-apple-runner-source.mjs'), '');
14+
fs.writeFileSync(
15+
path.join(scriptsDirectory, 'package-android-helper.sh'),
16+
`#!/bin/sh
17+
set -eu
18+
if [ "$AGENT_DEVICE_ANDROID_HELPER" = "snapshot" ]; then
19+
output="$3"
20+
else
21+
output="$2"
22+
fi
23+
mkdir -p "$output"
24+
prefix="agent-device-android-$AGENT_DEVICE_ANDROID_HELPER-helper-$1"
25+
printf apk > "$output/$prefix.apk"
26+
printf manifest > "$output/$prefix.manifest.json"
27+
printf checksum > "$output/$prefix.apk.sha256"
28+
`,
29+
);
30+
31+
const stalePath = path.join(root, 'android', 'ime-helper', 'dist', 'stale.apk');
32+
fs.mkdirSync(path.dirname(stalePath), { recursive: true });
33+
fs.writeFileSync(stalePath, 'stale');
34+
35+
preparePublishAssets({ root });
36+
37+
assert.equal(fs.existsSync(stalePath), false);
38+
for (const helper of ['snapshot', 'ime']) {
39+
const prefix = `agent-device-android-${helper}-helper-1.2.3`;
40+
const directory = path.join(root, 'android', `${helper}-helper`, 'dist');
41+
assert.deepEqual(fs.readdirSync(directory).sort(), [
42+
`${prefix}.apk`,
43+
`${prefix}.apk.sha256`,
44+
`${prefix}.manifest.json`,
45+
]);
46+
}
47+
});

‎scripts/__tests__/size-report-package.test.ts‎

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { readFile } from 'node:fs/promises';
33
import { join } from 'node:path';
44
import { test } from 'vitest';
55
import { formatMarkdown } from '../size-report.mjs';
6-
import { classifyNpmPackEntry, summarizeNpmPackComponents } from '../size-report-package.mjs';
6+
import {
7+
assertPublishPackageContents,
8+
classifyNpmPackEntry,
9+
summarizeNpmPackComponents,
10+
} from '../size-report-package.mjs';
711

812
const fixturePack = JSON.parse(
913
await readFile(join(import.meta.dirname, 'fixtures', 'size-report-npm-pack.json'), 'utf8'),
@@ -21,6 +25,9 @@ test('classifies every shipped entry into one named component', () => {
2125
['dist/apple/snapshot-presentation/Package.swift', 'apple-snapshot-presentation'],
2226
['apple/macos-helper/Sources/main.swift', 'macos-helper'],
2327
['android/snapshot-helper/dist/helper.apk', 'android-helpers'],
28+
['android/snapshot-helper/dist/helper.manifest.json', 'android-helpers'],
29+
['android/ime-helper/dist/helper.apk', 'android-helpers'],
30+
['android/ime-helper/dist/helper.manifest.json', 'android-helpers'],
2431
['package.json', 'other'],
2532
['vendor/unknown.bin', 'other'],
2633
],
@@ -34,6 +41,25 @@ test('unknown package paths fall into other', () => {
3441
);
3542
});
3643

44+
test('publish package requires both Android helpers and excludes benchmark scripts', () => {
45+
assert.doesNotThrow(() => assertPublishPackageContents(fixturePack.files));
46+
assert.throws(
47+
() =>
48+
assertPublishPackageContents(
49+
fixturePack.files.filter((entry) => !entry.path.startsWith('android/ime-helper/')),
50+
),
51+
/android\/ime-helper/,
52+
);
53+
assert.throws(
54+
() =>
55+
assertPublishPackageContents([
56+
...fixturePack.files,
57+
{ path: 'scripts/ios-snapshot-benchmark/run.ts', size: 1 },
58+
]),
59+
/benchmark or build scripts/,
60+
);
61+
});
62+
3763
test('component bytes sum exactly to npm pack unpackedSize', () => {
3864
const components = summarizeNpmPackComponents(fixturePack);
3965

@@ -48,12 +74,12 @@ test('component bytes sum exactly to npm pack unpackedSize', () => {
4874
'apple-runner': 503,
4975
'apple-snapshot-presentation': 113,
5076
'macos-helper': 211,
51-
'android-helpers': 307,
77+
'android-helpers': 812,
5278
other: 177,
5379
},
5480
);
5581
assert.throws(
56-
() => summarizeNpmPackComponents({ ...fixturePack, unpackedSize: 1800 }),
82+
() => summarizeNpmPackComponents({ ...fixturePack, unpackedSize: 2318 }),
5783
/does not match npm pack unpackedSize/,
5884
);
5985
});
@@ -63,7 +89,7 @@ test('Markdown reports component diffs and changed packed files', () => {
6389
js: { rawBytes: 10, gzipBytes: 8 },
6490
npmPack: {
6591
tarballBytes: 100,
66-
unpackedBytes: 1814,
92+
unpackedBytes: 2319,
6793
components: summarizeNpmPackComponents(fixturePack),
6894
entries: fixturePack.files,
6995
},
@@ -76,10 +102,10 @@ test('Markdown reports component diffs and changed packed files', () => {
76102
js: { rawBytes: 10, gzipBytes: 8 },
77103
npmPack: {
78104
tarballBytes: 100,
79-
unpackedBytes: 1713,
105+
unpackedBytes: 2218,
80106
components: summarizeNpmPackComponents({
81107
...fixturePack,
82-
unpackedSize: 1713,
108+
unpackedSize: 2218,
83109
files: baseEntries,
84110
}),
85111
entries: baseEntries,
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# iOS snapshot convergence evidence
2+
3+
This harness owns the reproducible evidence contract for [#2189](https://github.com/callstack/agent-device/issues/2189), which supplies measurements to [#2188](https://github.com/callstack/agent-device/issues/2188). It does not change daemon, runner, snapshot, or package runtime behavior.
4+
5+
## Prepare a real target
6+
7+
Use a dedicated iOS Simulator and the checked-in test app. Build the CLI and app before measuring:
8+
9+
```sh
10+
pnpm install --frozen-lockfile
11+
pnpm build
12+
pnpm test-app:install
13+
pnpm test-app:ios -- --device "bench-golden-v1"
14+
```
15+
16+
The app build must succeed on the host. If signing, Xcode, XCTest, simulator, runner, or daemon setup cannot be made deterministic, stop and attach the exact command, target, toolchain, and diagnostic; do not report fixture-only timings.
17+
18+
## Local state matrix
19+
20+
Replace `SIMULATOR_UDID` with the dedicated simulator UDID. The default screen set is quiet, list, nested-scroll, alert, system-surface, and xctest-stress. Cold cells require at least 10 samples; warm and relaunch cells require at least 20.
21+
22+
```sh
23+
pnpm bench:ios-snapshot -- \
24+
--mode local \
25+
--udid SIMULATOR_UDID \
26+
--state cold-cold,cold,warm,relaunch \
27+
--screen quiet,list,nested-scroll,alert,system-surface,xctest-stress \
28+
--samples 20 \
29+
--out .tmp/ios-snapshot-convergence.v1.json
30+
```
31+
32+
The cells mean:
33+
34+
- `cold-cold`: simulator off, daemon off, derived runner data cleared before each sample.
35+
- `cold`: simulator booted, daemon stopped, and app terminated before each sample.
36+
- `warm`: app, daemon, runner, and target are prepared once; each sample is a fresh CLI snapshot.
37+
- `relaunch`: the same prepared tooling is retained while each sample launches a new app process.
38+
39+
Every sample keeps daemon duration and fresh-process wall time separately, the first-tree status, response bytes, target generation, and typed failure details. Each raw result also records the typed host model, model identifier, CPU, and core count needed to compare performance baselines. The raw JSON is validated against `raw-result.schema.v1.json`; the adjacent Markdown is a human-readable summary.
40+
41+
Each local cell is admitted only after the simulator state, verified daemon identity, app process generation, and exact fixture anchor are checked. A mismatch stops the run with a typed cell-state or fixture-anchor reason. Derived data is cleared only below the benchmark-owned state directory.
42+
43+
When `--state-dir` is omitted, the harness allocates a fresh marker-owned root under the host temporary directory. A caller-supplied state directory must already be a real, marker-owned directory; the CLI never initializes ownership for an existing path. Any explicit `--derived-path` must remain below that root.
44+
45+
## Proxy matrix
46+
47+
The proxy mode starts the repository proxy, then inserts a local deterministic conditioner in front of it. It runs both a persistent Node client and a fresh-process CLI at RTT 0, 20, and 80 ms. Request and response body bytes, failures, bandwidth, packet-loss rate, and seed are retained in the raw result.
48+
49+
```sh
50+
pnpm bench:ios-snapshot -- \
51+
--mode proxy \
52+
--udid SIMULATOR_UDID \
53+
--screen quiet \
54+
--rtt 0,20,80 \
55+
--samples 20 \
56+
--bandwidth-kbps unlimited \
57+
--packet-loss 0 \
58+
--out .tmp/ios-snapshot-proxy.v1.json
59+
```
60+
61+
The conditioner is semantics-preserving at zero packet loss. Non-zero loss is an explicit failure experiment, not a successful baseline.
62+
63+
The complete exact-head corpus from `bench-golden-v2` (iPhone 17 Pro, iOS 27.0) is retained under
64+
[`evidence/`](./evidence/) from revision `71fb2483f30d90e615e949601c836aeebbf450c5`:
65+
66+
- [`ios-snapshot-cold-local-71fb2483f.json`](./evidence/ios-snapshot-cold-local-71fb2483f.json)
67+
covers cold-cold and cold lifecycle cells across all six screens with 10 samples per cell.
68+
- [`ios-snapshot-warm-relaunch-local-71fb2483f.json`](./evidence/ios-snapshot-warm-relaunch-local-71fb2483f.json)
69+
covers warm and relaunch lifecycle cells across all six screens with 20 samples per cell and
70+
includes package-size measurements.
71+
- [`ios-snapshot-proxy-71fb2483f.json`](./evidence/ios-snapshot-proxy-71fb2483f.json) covers
72+
persistent-client and fresh-process CLI cells at RTT 0, 20, and 80 ms with 20 samples per cell.
73+
74+
Each JSON file is the schema-validated raw result from the commit named in its `revision` field;
75+
each has an adjacent Markdown summary. Superseded pre-admission captures are not part of the
76+
published corpus.
77+
78+
## Package-size evidence
79+
80+
`pnpm size --json .tmp/size.json --markdown .tmp/size.md` prepares the same Apple and Android
81+
publish assets used by `pnpm build:package`, then measures bundled JavaScript, packed tarball,
82+
packed unpacked tree, and the package tree after a clean `npm install` into an isolated consumer.
83+
The report rejects packages missing either Android helper or containing benchmark/build scripts.
84+
The iOS harness includes those three package measurements unless `--skip-package-size` is supplied.
85+
86+
## Permanent deep-button control
87+
88+
This is an implementation-independent control for [#1626](https://github.com/callstack/agent-device/issues/1626): the checked-in `deep-button-fixture.v1.json` artifact contains a 72-level ancestor chain and independently recorded shallow/full outputs. The changed leaf is intentionally omitted by the shallow observation, so a no-effect assertion must fail. The full observation includes the leaf and passes.
89+
90+
```sh
91+
pnpm bench:ios-snapshot:deep-button -- --rule invalid-shallow
92+
# expected exit 1:
93+
# AssertionError: changed descendant was omitted by shallow observation; no-effect claim is invalid.
94+
pnpm bench:ios-snapshot:deep-button -- --rule safe-full
95+
# expected exit 0
96+
```
97+
98+
The schema links [#1571](https://github.com/callstack/agent-device/issues/1571) so an unreadable or empty first tree remains distinct from bridge, runner, timeout, stale-generation, packet-loss, and upstream failures. This harness does not modify either issue’s runtime behavior.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import assert from 'node:assert/strict';
2+
import path from 'node:path';
3+
import { test } from 'vitest';
4+
import { mkdtempForTest } from '../../src/__tests__/test-utils/tmp-dir.ts';
5+
import { parseConfig } from './benchmark-config.ts';
6+
import { createBenchmarkStateRoot } from './state-ownership.ts';
7+
8+
test('proxy measurements retain the warm sample minimum', async () => {
9+
const stateDir = createBenchmarkStateRoot();
10+
const args = [
11+
'--mode',
12+
'proxy',
13+
'--udid',
14+
'simulator',
15+
'--state',
16+
'cold',
17+
'--state-dir',
18+
stateDir,
19+
];
20+
assert.throws(() => parseConfig([...args, '--samples', '10']), /at least 20/);
21+
assert.equal(parseConfig([...args, '--samples', '20']).samples, 20);
22+
});
23+
24+
test('unknown benchmark flags fail closed', async () => {
25+
const stateDir = createBenchmarkStateRoot();
26+
assert.throws(
27+
() => parseConfig(['--udid', 'simulator', '--state-dir', stateDir, '--unknown']),
28+
/Unknown option: --unknown/,
29+
);
30+
});
31+
32+
test('derived data is confined to the owned benchmark state directory', async () => {
33+
const stateDir = createBenchmarkStateRoot();
34+
assert.doesNotThrow(() =>
35+
parseConfig([
36+
'--udid',
37+
'simulator',
38+
'--state-dir',
39+
stateDir,
40+
'--derived-path',
41+
path.join(stateDir, 'derived-data', 'cell'),
42+
]),
43+
);
44+
assert.throws(
45+
() =>
46+
parseConfig([
47+
'--udid',
48+
'simulator',
49+
'--state-dir',
50+
stateDir,
51+
'--derived-path',
52+
path.join(stateDir, '..', 'unowned-derived-data'),
53+
]),
54+
/descendant of the benchmark state directory/,
55+
);
56+
});
57+
58+
test('caller-supplied state directories must already be benchmark-owned', async () => {
59+
const unmarked = await mkdtempForTest('agent-device-ios-benchmark-unmarked-');
60+
assert.throws(
61+
() => parseConfig(['--udid', 'simulator', '--state-dir', unmarked]),
62+
/not benchmark-owned/,
63+
);
64+
const stateDir = createBenchmarkStateRoot();
65+
assert.equal(parseConfig(['--udid', 'simulator', '--state-dir', stateDir]).stateDir, stateDir);
66+
});

0 commit comments

Comments
 (0)