Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
e90b776
test(ios): add snapshot convergence evidence harness
thymikee Aug 31, 2026
30120e1
fix(ios): satisfy benchmark CI guards
thymikee Sep 1, 2026
82d3f44
fix(ios): constrain benchmark proxy routes
thymikee Sep 1, 2026
4aebee2
fix(ios-benchmark): enforce cell admission evidence
thymikee Sep 1, 2026
8e2120a
fix(ios-benchmark): protect benchmark state ownership
thymikee Sep 1, 2026
562406b
fix(ios-benchmark): use proxy port flag
thymikee Sep 1, 2026
bac3cd3
fix(ios-benchmark): let proxy choose an ephemeral port
thymikee Sep 1, 2026
6f2a74f
test(ios-benchmark): keep CLI process seam local
thymikee Sep 1, 2026
80ed800
fix(ios-benchmark): parse proxy startup envelope
thymikee Sep 1, 2026
747a939
fix(ios-benchmark): bind proxy lease to simulator
thymikee Sep 1, 2026
e9b1fc5
fix(ios-benchmark): keep fresh proxy CLI sessions isolated
thymikee Sep 1, 2026
0ae8894
fix(ios-benchmark): preserve async timeout evidence
thymikee Sep 1, 2026
8820e0a
docs(ios-benchmark): retain exact-head evidence
thymikee Sep 1, 2026
d6e642e
test(ios): reveal offscreen alert fixture controls
thymikee Sep 1, 2026
fdb5a93
test(ios): reset alert between relaunch samples
thymikee Sep 1, 2026
955ed76
test(ios): admit native alert snapshots
thymikee Sep 1, 2026
eb95d40
docs(ios): publish snapshot convergence corpus
thymikee Sep 1, 2026
9228dba
chore(ios): format benchmark evidence
thymikee Sep 1, 2026
71fb248
fix(ios-benchmark): admit proxy fixture anchors
thymikee Sep 1, 2026
c1634ed
docs(ios): republish exact-head benchmark corpus
thymikee Sep 1, 2026
6a6b63d
fix(size): make publish asset evidence hermetic
thymikee Sep 1, 2026
0eacf75
style(size): format package evidence test
thymikee Sep 1, 2026
509d0f3
test(size): update publish preparation contracts
thymikee Sep 1, 2026
3a69062
fix: reconcile package evidence with current main
thymikee Sep 1, 2026
6c113f1
fix: retire stale utils layering zone
thymikee Sep 1, 2026
8e4c382
test: pin shared publish asset owner
thymikee Sep 1, 2026
99abc39
test: verify preserved size reporter closure
thymikee Sep 1, 2026
bfa4d46
fix: move mutation ownership to snapshot module
thymikee Sep 1, 2026
9169674
test(ios): add snapshot convergence evidence harness
thymikee Aug 31, 2026
dcc9ec4
fix(ios): satisfy benchmark CI guards
thymikee Sep 1, 2026
a2689f9
fix(ios): constrain benchmark proxy routes
thymikee Sep 1, 2026
cbc9f01
fix(ios-benchmark): enforce cell admission evidence
thymikee Sep 1, 2026
7fd54be
fix(ios-benchmark): protect benchmark state ownership
thymikee Sep 1, 2026
e88fb67
fix(ios-benchmark): use proxy port flag
thymikee Sep 1, 2026
9d1f1a4
fix(ios-benchmark): let proxy choose an ephemeral port
thymikee Sep 1, 2026
156d379
test(ios-benchmark): keep CLI process seam local
thymikee Sep 1, 2026
49c4364
fix(ios-benchmark): parse proxy startup envelope
thymikee Sep 1, 2026
8fe4eeb
fix(ios-benchmark): bind proxy lease to simulator
thymikee Sep 1, 2026
f4dc1a1
fix(ios-benchmark): keep fresh proxy CLI sessions isolated
thymikee Sep 1, 2026
bbb8f92
fix(ios-benchmark): preserve async timeout evidence
thymikee Sep 1, 2026
40990f5
docs(ios-benchmark): retain exact-head evidence
thymikee Sep 1, 2026
faae452
test(ios): reveal offscreen alert fixture controls
thymikee Sep 1, 2026
0bd4c45
test(ios): reset alert between relaunch samples
thymikee Sep 1, 2026
2cdfd8f
test(ios): admit native alert snapshots
thymikee Sep 1, 2026
e7d7d5e
docs(ios): publish snapshot convergence corpus
thymikee Sep 1, 2026
cfb92ce
chore(ios): format benchmark evidence
thymikee Sep 1, 2026
064bf02
fix(ios-benchmark): admit proxy fixture anchors
thymikee Sep 1, 2026
7ae5ece
docs(ios): republish exact-head benchmark corpus
thymikee Sep 1, 2026
bde42dd
fix(size): make publish asset evidence hermetic
thymikee Sep 1, 2026
cd2b1e0
test(size): update publish preparation contracts
thymikee Sep 1, 2026
ea79642
Merge remote-tracking branch 'origin/codex/2189-ios-snapshot-baseline…
thymikee Sep 1, 2026
1809662
Merge remote-tracking branch 'origin/codex/2189-ios-snapshot-baseline…
thymikee Sep 1, 2026
14aa6fa
fix: keep git-state gates out of mutation sandboxes
thymikee Sep 1, 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
15 changes: 15 additions & 0 deletions .github/workflows/size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ jobs:
run: |
mkdir -p /tmp/agent-device-size-report
cp scripts/size-report*.mjs /tmp/agent-device-size-report/
cp scripts/prepare-publish-assets.mjs /tmp/agent-device-size-report/

- name: Install Android helper toolchain
run: |
SDK_ROOT="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}}"
SDKMANAGER="$SDK_ROOT/cmdline-tools/latest/bin/sdkmanager"
if [ ! -x "$SDKMANAGER" ]; then
SDKMANAGER="$SDK_ROOT/cmdline-tools/bin/sdkmanager"
fi
if [ ! -x "$SDKMANAGER" ]; then
echo "sdkmanager not found under $SDK_ROOT" >&2
exit 1
fi
yes | "$SDKMANAGER" --licenses >/dev/null || true
"$SDKMANAGER" "platforms;android-36" "build-tools;36.0.0"

# dist is fully determined by the base commit, so reuse it across PR runs
# against the same base. Startup medians are still measured fresh on this
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@
"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",
"build:macos-helper": "node --experimental-strip-types scripts/swift-toolchain-tmpdir.ts swift build -c release --package-path apple/macos-helper",
"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",
"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",
"prepare:publish-assets": "node scripts/prepare-publish-assets.mjs",
"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",
"package:npm": "pnpm build:package && pnpm check:package",
"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",
"release:publish": "pnpm release:prepare && npm publish --ignore-scripts .tmp/release/*.tgz && pnpm release:mark-dev",
Expand All @@ -120,6 +121,8 @@
"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",
"size": "node scripts/size-report.mjs",
"perf": "node --experimental-strip-types scripts/perf/run.ts",
"bench:ios-snapshot": "node --experimental-strip-types scripts/ios-snapshot-benchmark/run.ts",
"bench:ios-snapshot:deep-button": "node --experimental-strip-types scripts/ios-snapshot-benchmark/deep-button.ts",
"mutation:run": "node --experimental-strip-types scripts/mutation/run.ts",
"mutation:check": "node --experimental-strip-types scripts/mutation/run.ts --no-run",
"mutation:affected": "node --experimental-strip-types scripts/mutation/run.ts --affected",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { eagerClosureGraphOf } from './eager-import-closure.fixtures.ts';
import { eagerClosureGraphOf } from '../../src/__tests__/eager-import-closure.fixtures.ts';
import {
classifyBudget,
describeClosurePressure,
Expand All @@ -22,7 +22,8 @@ import {
* ADR-0019 requires platform-package façades to stay implementation-lazy and is explicit that a
* startup threshold alone is not a substitute for preserving the loading shape: "the tracking
* issue owns the exact probe and planted-red procedure." #1950 built the walker this file reuses
* (`eager-import-closure.fixtures.ts`, AST-level: static value edges plus top-level dynamic
* (`src/__tests__/eager-import-closure.fixtures.ts`, AST-level: static value edges plus top-level
* dynamic
* imports, type-only erased) and proved the planted-red procedure on one file. This is that
* probe, generalized to every workspace-package entry surface plus designated hub modules.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
// implementation-lazy and is explicit that a startup-time threshold alone is not a substitute for
// preserving the loading shape (`docs/adr/0019-request-bound-platform-runtime.md`): "the tracking
// issue owns the exact probe and planted-red procedure." #1950 built the AST-level walker
// (`eager-import-closure.fixtures.ts`); #1959/#1969 fixed two more instances of the regression
// (`src/__tests__/eager-import-closure.fixtures.ts`); #1959/#1969 fixed two more instances of the
// regression
// class by hand. This table generalizes the proof: every package entry surface gets an exact pin
// on how many repo modules importing it evaluates, plus a standing assertion that the closure
// never reaches a concrete platform implementation before discovery/binding selects one.
Expand All @@ -22,7 +23,7 @@
// enforced by the compiler instead of by a runtime check that a `Set` conversion would hide.

import path from 'node:path';
import { facadeEntryFiles } from '../../scripts/layering/package-boundaries.ts';
import { facadeEntryFiles } from '../layering/package-boundaries.ts';

export type EagerClosureBudget = {
/** Stable label for test names and failure messages -- the entry's repo-relative path. */
Expand Down
5 changes: 4 additions & 1 deletion scripts/__tests__/fixtures/size-report-npm-pack.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
{
"unpackedSize": 1814,
"unpackedSize": 2319,
"files": [
{ "path": "dist/src/index.js", "size": 401 },
{ "path": "dist/src/index.d.ts", "size": 102 },
{ "path": "dist/apple/runner/RunnerTests.swift", "size": 503 },
{ "path": "dist/apple/snapshot-presentation/Package.swift", "size": 113 },
{ "path": "apple/macos-helper/Sources/main.swift", "size": 211 },
{ "path": "android/snapshot-helper/dist/helper.apk", "size": 307 },
{ "path": "android/snapshot-helper/dist/helper.manifest.json", "size": 99 },
{ "path": "android/ime-helper/dist/helper.apk", "size": 307 },
{ "path": "android/ime-helper/dist/helper.manifest.json", "size": 99 },
{ "path": "package.json", "size": 99 },
{ "path": "vendor/unknown.bin", "size": 78 }
]
Expand Down
47 changes: 47 additions & 0 deletions scripts/__tests__/prepare-publish-assets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { test } from 'vitest';
import { mkdtempForTestSync } from '../../src/__tests__/test-utils/tmp-dir.ts';
import { preparePublishAssets } from '../prepare-publish-assets.mjs';

test('prepares both Android runtime helpers through the shared publish owner', () => {
const root = mkdtempForTestSync('agent-device-publish-assets-');
const scriptsDirectory = path.join(root, 'scripts');
fs.mkdirSync(scriptsDirectory, { recursive: true });
fs.writeFileSync(path.join(root, 'package.json'), '{"version":"1.2.3"}\n');
fs.writeFileSync(path.join(scriptsDirectory, 'package-apple-runner-source.mjs'), '');
fs.writeFileSync(
path.join(scriptsDirectory, 'package-android-helper.sh'),
`#!/bin/sh
set -eu
if [ "$AGENT_DEVICE_ANDROID_HELPER" = "snapshot" ]; then
output="$3"
else
output="$2"
fi
mkdir -p "$output"
prefix="agent-device-android-$AGENT_DEVICE_ANDROID_HELPER-helper-$1"
printf apk > "$output/$prefix.apk"
printf manifest > "$output/$prefix.manifest.json"
printf checksum > "$output/$prefix.apk.sha256"
`,
);

const stalePath = path.join(root, 'android', 'ime-helper', 'dist', 'stale.apk');
fs.mkdirSync(path.dirname(stalePath), { recursive: true });
fs.writeFileSync(stalePath, 'stale');

preparePublishAssets({ root });

assert.equal(fs.existsSync(stalePath), false);
for (const helper of ['snapshot', 'ime']) {
const prefix = `agent-device-android-${helper}-helper-1.2.3`;
const directory = path.join(root, 'android', `${helper}-helper`, 'dist');
assert.deepEqual(fs.readdirSync(directory).sort(), [
`${prefix}.apk`,
`${prefix}.apk.sha256`,
`${prefix}.manifest.json`,
]);
}
});
38 changes: 32 additions & 6 deletions scripts/__tests__/size-report-package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { test } from 'vitest';
import { formatMarkdown } from '../size-report.mjs';
import { classifyNpmPackEntry, summarizeNpmPackComponents } from '../size-report-package.mjs';
import {
assertPublishPackageContents,
classifyNpmPackEntry,
summarizeNpmPackComponents,
} from '../size-report-package.mjs';

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

test('publish package requires both Android helpers and excludes benchmark scripts', () => {
assert.doesNotThrow(() => assertPublishPackageContents(fixturePack.files));
assert.throws(
() =>
assertPublishPackageContents(
fixturePack.files.filter((entry) => !entry.path.startsWith('android/ime-helper/')),
),
/android\/ime-helper/,
);
assert.throws(
() =>
assertPublishPackageContents([
...fixturePack.files,
{ path: 'scripts/ios-snapshot-benchmark/run.ts', size: 1 },
]),
/benchmark or build scripts/,
);
});

test('component bytes sum exactly to npm pack unpackedSize', () => {
const components = summarizeNpmPackComponents(fixturePack);

Expand All @@ -48,12 +74,12 @@ test('component bytes sum exactly to npm pack unpackedSize', () => {
'apple-runner': 503,
'apple-snapshot-presentation': 113,
'macos-helper': 211,
'android-helpers': 307,
'android-helpers': 812,
other: 177,
},
);
assert.throws(
() => summarizeNpmPackComponents({ ...fixturePack, unpackedSize: 1800 }),
() => summarizeNpmPackComponents({ ...fixturePack, unpackedSize: 2318 }),
/does not match npm pack unpackedSize/,
);
});
Expand All @@ -63,7 +89,7 @@ test('Markdown reports component diffs and changed packed files', () => {
js: { rawBytes: 10, gzipBytes: 8 },
npmPack: {
tarballBytes: 100,
unpackedBytes: 1814,
unpackedBytes: 2319,
components: summarizeNpmPackComponents(fixturePack),
entries: fixturePack.files,
},
Expand All @@ -76,10 +102,10 @@ test('Markdown reports component diffs and changed packed files', () => {
js: { rawBytes: 10, gzipBytes: 8 },
npmPack: {
tarballBytes: 100,
unpackedBytes: 1713,
unpackedBytes: 2218,
components: summarizeNpmPackComponents({
...fixturePack,
unpackedSize: 1713,
unpackedSize: 2218,
files: baseEntries,
}),
entries: baseEntries,
Expand Down
98 changes: 98 additions & 0 deletions scripts/ios-snapshot-benchmark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# iOS snapshot convergence evidence

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.

## Prepare a real target

Use a dedicated iOS Simulator and the checked-in test app. Build the CLI and app before measuring:

```sh
pnpm install --frozen-lockfile
pnpm build
pnpm test-app:install
pnpm test-app:ios -- --device "bench-golden-v1"
```

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.

## Local state matrix

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.

```sh
pnpm bench:ios-snapshot -- \
--mode local \
--udid SIMULATOR_UDID \
--state cold-cold,cold,warm,relaunch \
--screen quiet,list,nested-scroll,alert,system-surface,xctest-stress \
--samples 20 \
--out .tmp/ios-snapshot-convergence.v1.json
```

The cells mean:

- `cold-cold`: simulator off, daemon off, derived runner data cleared before each sample.
- `cold`: simulator booted, daemon stopped, and app terminated before each sample.
- `warm`: app, daemon, runner, and target are prepared once; each sample is a fresh CLI snapshot.
- `relaunch`: the same prepared tooling is retained while each sample launches a new app process.

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.

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.

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.

## Proxy matrix

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.

```sh
pnpm bench:ios-snapshot -- \
--mode proxy \
--udid SIMULATOR_UDID \
--screen quiet \
--rtt 0,20,80 \
--samples 20 \
--bandwidth-kbps unlimited \
--packet-loss 0 \
--out .tmp/ios-snapshot-proxy.v1.json
```

The conditioner is semantics-preserving at zero packet loss. Non-zero loss is an explicit failure experiment, not a successful baseline.

The complete exact-head corpus from `bench-golden-v2` (iPhone 17 Pro, iOS 27.0) is retained under
[`evidence/`](./evidence/) from revision `71fb2483f30d90e615e949601c836aeebbf450c5`:

- [`ios-snapshot-cold-local-71fb2483f.json`](./evidence/ios-snapshot-cold-local-71fb2483f.json)
covers cold-cold and cold lifecycle cells across all six screens with 10 samples per cell.
- [`ios-snapshot-warm-relaunch-local-71fb2483f.json`](./evidence/ios-snapshot-warm-relaunch-local-71fb2483f.json)
covers warm and relaunch lifecycle cells across all six screens with 20 samples per cell and
includes package-size measurements.
- [`ios-snapshot-proxy-71fb2483f.json`](./evidence/ios-snapshot-proxy-71fb2483f.json) covers
persistent-client and fresh-process CLI cells at RTT 0, 20, and 80 ms with 20 samples per cell.

Each JSON file is the schema-validated raw result from the commit named in its `revision` field;
each has an adjacent Markdown summary. Superseded pre-admission captures are not part of the
published corpus.

## Package-size evidence

`pnpm size --json .tmp/size.json --markdown .tmp/size.md` prepares the same Apple and Android
publish assets used by `pnpm build:package`, then measures bundled JavaScript, packed tarball,
packed unpacked tree, and the package tree after a clean `npm install` into an isolated consumer.
The report rejects packages missing either Android helper or containing benchmark/build scripts.
The iOS harness includes those three package measurements unless `--skip-package-size` is supplied.

## Permanent deep-button control

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.

```sh
pnpm bench:ios-snapshot:deep-button -- --rule invalid-shallow
# expected exit 1:
# AssertionError: changed descendant was omitted by shallow observation; no-effect claim is invalid.
pnpm bench:ios-snapshot:deep-button -- --rule safe-full
# expected exit 0
```

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.
66 changes: 66 additions & 0 deletions scripts/ios-snapshot-benchmark/benchmark-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import assert from 'node:assert/strict';
import path from 'node:path';
import { test } from 'vitest';
import { mkdtempForTest } from '../../src/__tests__/test-utils/tmp-dir.ts';
import { parseConfig } from './benchmark-config.ts';
import { createBenchmarkStateRoot } from './state-ownership.ts';

test('proxy measurements retain the warm sample minimum', async () => {
const stateDir = createBenchmarkStateRoot();
const args = [
'--mode',
'proxy',
'--udid',
'simulator',
'--state',
'cold',
'--state-dir',
stateDir,
];
assert.throws(() => parseConfig([...args, '--samples', '10']), /at least 20/);
assert.equal(parseConfig([...args, '--samples', '20']).samples, 20);
});

test('unknown benchmark flags fail closed', async () => {
const stateDir = createBenchmarkStateRoot();
assert.throws(
() => parseConfig(['--udid', 'simulator', '--state-dir', stateDir, '--unknown']),
/Unknown option: --unknown/,
);
});

test('derived data is confined to the owned benchmark state directory', async () => {
const stateDir = createBenchmarkStateRoot();
assert.doesNotThrow(() =>
parseConfig([
'--udid',
'simulator',
'--state-dir',
stateDir,
'--derived-path',
path.join(stateDir, 'derived-data', 'cell'),
]),
);
assert.throws(
() =>
parseConfig([
'--udid',
'simulator',
'--state-dir',
stateDir,
'--derived-path',
path.join(stateDir, '..', 'unowned-derived-data'),
]),
/descendant of the benchmark state directory/,
);
});

test('caller-supplied state directories must already be benchmark-owned', async () => {
const unmarked = await mkdtempForTest('agent-device-ios-benchmark-unmarked-');
assert.throws(
() => parseConfig(['--udid', 'simulator', '--state-dir', unmarked]),
/not benchmark-owned/,
);
const stateDir = createBenchmarkStateRoot();
assert.equal(parseConfig(['--udid', 'simulator', '--state-dir', stateDir]).stateDir, stateDir);
});
Loading
Loading