mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
chore(deps): Renovate config, packageManager-derived pnpm in CI, repo-wide format (#1444)
* chore(deps): add Renovate config and enforce packageManager pnpm version in CI Refs #1422 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: bump pnpm to 11.17.0 and format the whole repo with oxfmt format/format:check drop their hand-maintained path list: oxfmt already skips node_modules and honors .gitignore, so the only exclusion list is .oxfmtrc.json ignorePatterns. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(mutation): accept either quote style in the affected-lane path filter Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore(deps): keep fixture-app runtime deps as individual Renovate PRs Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
e89ef9e69b
commit
edca35d122
+1
-1
@@ -1,2 +1,2 @@
|
||||
[features]
|
||||
collaboration_modes = true
|
||||
collaboration_modes = true
|
||||
|
||||
+9
-33
@@ -33,16 +33,12 @@
|
||||
"scripts/write-xcuitest-cache-metadata.mjs",
|
||||
"scripts/help-conformance-sample-outputs.d.mts"
|
||||
],
|
||||
"ignoreDependencies": [
|
||||
"@theme"
|
||||
],
|
||||
"ignoreDependencies": ["@theme"],
|
||||
"ignoreExports": [
|
||||
{
|
||||
"comment": "Authoritative supported-command list; sole consumer is the conformance oracle (scripts/maestro-conformance, an ignored tooling tree).",
|
||||
"file": "src/compat/maestro/program-ir-command-parser.ts",
|
||||
"exports": [
|
||||
"SUPPORTED_MAESTRO_COMMAND_NAMES"
|
||||
]
|
||||
"exports": ["SUPPORTED_MAESTRO_COMMAND_NAMES"]
|
||||
},
|
||||
{
|
||||
"comment": "Daemon route handlers are reached only through the dynamic `import()` table in request-handler-chain.ts, which --production analysis cannot follow to a consumer.",
|
||||
@@ -60,16 +56,12 @@
|
||||
{
|
||||
"comment": "Published package surface (see package.json#exports); consumed by SDK users, not internally.",
|
||||
"file": "src/sdk/*.ts",
|
||||
"exports": [
|
||||
"*"
|
||||
]
|
||||
"exports": ["*"]
|
||||
},
|
||||
{
|
||||
"comment": "Tool config default exports, loaded by the tool rather than imported.",
|
||||
"file": "{tsdown.config.ts,vitest.mutation.config.ts,website/rspress.config.ts,test/skillgym/skillgym.config.ts,test/skillgym/suites/*.ts}",
|
||||
"exports": [
|
||||
"default"
|
||||
]
|
||||
"exports": ["default"]
|
||||
},
|
||||
{
|
||||
"comment": "Type-level `AssertTrue<...>` registry-totality guards. Exported only so `noUnusedLocals` keeps them alive; a consumer would defeat the point.",
|
||||
@@ -84,34 +76,20 @@
|
||||
{
|
||||
"comment": "Mutation-lane seam: readTestScope is consumed by vitest.mutation.config.ts, a tool config outside --production analysis.",
|
||||
"file": "scripts/mutation/test-scope.ts",
|
||||
"exports": [
|
||||
"readTestScope"
|
||||
]
|
||||
"exports": ["readTestScope"]
|
||||
},
|
||||
{
|
||||
"comment": "Mutation-lane completeness gate: ownedTestFiles enumerates the whole derived ownership map, which only the assertion in ownership.test.ts consumes — a production consumer would defeat the point.",
|
||||
"file": "scripts/mutation/ownership.ts",
|
||||
"exports": [
|
||||
"ownedTestFiles"
|
||||
]
|
||||
"exports": ["ownedTestFiles"]
|
||||
},
|
||||
{
|
||||
"comment": "Help-benchmark conformance seams: helpTopicIds feeds the topic-coverage gate and PRIVATE_AX_RECOVERY_SAMPLE feeds the skillgym suite; both consumers are test-tree files outside --production analysis.",
|
||||
"file": "{src/cli/parser/cli-help.ts,scripts/help-conformance-sample-outputs.mjs}",
|
||||
"exports": [
|
||||
"helpTopicIds",
|
||||
"PRIVATE_AX_RECOVERY_SAMPLE"
|
||||
]
|
||||
"exports": ["helpTopicIds", "PRIVATE_AX_RECOVERY_SAMPLE"]
|
||||
}
|
||||
],
|
||||
"usedClassMembers": [
|
||||
"name",
|
||||
"listActiveLeases",
|
||||
"delete",
|
||||
"values",
|
||||
"elapsedMs",
|
||||
"isExpired"
|
||||
],
|
||||
"usedClassMembers": ["name", "listActiveLeases", "delete", "values", "elapsedMs", "isExpired"],
|
||||
"rules": {
|
||||
"unused-types": "error",
|
||||
"duplicate-exports": "off"
|
||||
@@ -119,9 +97,7 @@
|
||||
"production": {
|
||||
"dupes": true
|
||||
},
|
||||
"publicPackages": [
|
||||
"agent-device"
|
||||
],
|
||||
"publicPackages": ["agent-device"],
|
||||
"audit": {
|
||||
"deadCodeBaseline": "fallow-baselines/dead-code.json",
|
||||
"healthBaseline": "fallow-baselines/health.json"
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
name: "Boot iOS Test Simulator"
|
||||
description: "Resolve and boot the preferred iOS simulator for replay tests"
|
||||
name: 'Boot iOS Test Simulator'
|
||||
description: 'Resolve and boot the preferred iOS simulator for replay tests'
|
||||
|
||||
inputs:
|
||||
runtime-version:
|
||||
description: "Preferred iOS runtime version"
|
||||
description: 'Preferred iOS runtime version'
|
||||
required: true
|
||||
preferred-device-name:
|
||||
description: "Preferred simulator device name"
|
||||
description: 'Preferred simulator device name'
|
||||
required: false
|
||||
default: "iPhone 17 Pro"
|
||||
default: 'iPhone 17 Pro'
|
||||
boot-timeout-seconds:
|
||||
description: "Maximum time to wait for simulator bootstatus"
|
||||
description: 'Maximum time to wait for simulator bootstatus'
|
||||
required: false
|
||||
default: "300"
|
||||
default: '300'
|
||||
|
||||
outputs:
|
||||
simulator-udid:
|
||||
description: "UDID of the booted iOS simulator"
|
||||
description: 'UDID of the booted iOS simulator'
|
||||
value: ${{ steps.boot.outputs.simulator-udid }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Resolve and boot iOS test simulator
|
||||
id: boot
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
name: "Setup Android Replay Host"
|
||||
description: "Prepare the Android CI host for replay tests and expose the agent-device home path"
|
||||
name: 'Setup Android Replay Host'
|
||||
description: 'Prepare the Android CI host for replay tests and expose the agent-device home path'
|
||||
|
||||
outputs:
|
||||
agent-home-dir:
|
||||
description: "Resolved agent-device home directory"
|
||||
description: 'Resolved agent-device home directory'
|
||||
value: ${{ steps.agent-home.outputs.dir }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Resolve agent-device home
|
||||
id: agent-home
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
name: "Setup Apple Replay"
|
||||
description: "Resolve shared Apple replay cache/build state and expose the agent-device home path"
|
||||
name: 'Setup Apple Replay'
|
||||
description: 'Resolve shared Apple replay cache/build state and expose the agent-device home path'
|
||||
|
||||
inputs:
|
||||
derived-path:
|
||||
description: "Derived data path used by the replay runner"
|
||||
description: 'Derived data path used by the replay runner'
|
||||
required: true
|
||||
cache-key-prefix:
|
||||
description: "Prefix for the cache key"
|
||||
description: 'Prefix for the cache key'
|
||||
required: true
|
||||
cache-key-suffix:
|
||||
description: "Optional suffix for the cache key"
|
||||
description: 'Optional suffix for the cache key'
|
||||
required: false
|
||||
default: ""
|
||||
default: ''
|
||||
build-command:
|
||||
description: "Command used to refresh replay artifacts before use"
|
||||
description: 'Command used to refresh replay artifacts before use'
|
||||
required: true
|
||||
xcuitest-platform:
|
||||
description: "Optional AGENT_DEVICE_XCUITEST_PLATFORM value"
|
||||
description: 'Optional AGENT_DEVICE_XCUITEST_PLATFORM value'
|
||||
required: false
|
||||
default: ""
|
||||
default: ''
|
||||
xcuitest-destination:
|
||||
description: "Optional AGENT_DEVICE_XCUITEST_DESTINATION value"
|
||||
description: 'Optional AGENT_DEVICE_XCUITEST_DESTINATION value'
|
||||
required: false
|
||||
default: ""
|
||||
default: ''
|
||||
build-before-use:
|
||||
description: "Whether this setup action should refresh replay artifacts after restoring the cache"
|
||||
description: 'Whether this setup action should refresh replay artifacts after restoring the cache'
|
||||
required: false
|
||||
default: "true"
|
||||
default: 'true'
|
||||
|
||||
outputs:
|
||||
agent-home-dir:
|
||||
description: "Resolved agent-device home directory"
|
||||
description: 'Resolved agent-device home directory'
|
||||
value: ${{ steps.agent-home.outputs.dir }}
|
||||
cache-hit:
|
||||
description: "Whether a replay cache was restored before any build refresh"
|
||||
description: 'Whether a replay cache was restored before any build refresh'
|
||||
value: ${{ steps.restore-prebuilt.outputs.cache-hit }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Resolve Xcode cache key
|
||||
id: xcode
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: "Setup Fixture App"
|
||||
description: "Install a ready Release build of examples/test-app on the booted iOS simulator, from the shared build cache"
|
||||
name: 'Setup Fixture App'
|
||||
description: 'Install a ready Release build of examples/test-app on the booted iOS simulator, from the shared build cache'
|
||||
|
||||
# Any job that needs a controlled app to drive can use this instead of an Apple
|
||||
# system app. It fetches the Release binary that test-app-build-cache.yml
|
||||
@@ -18,17 +18,17 @@ description: "Install a ready Release build of examples/test-app on the booted i
|
||||
|
||||
inputs:
|
||||
device-name:
|
||||
description: "Simulator device name used for the inline-build fallback"
|
||||
description: 'Simulator device name used for the inline-build fallback'
|
||||
required: false
|
||||
default: "iPhone 17 Pro"
|
||||
default: 'iPhone 17 Pro'
|
||||
install:
|
||||
description: "Install the app onto the booted simulator"
|
||||
description: 'Install the app onto the booted simulator'
|
||||
required: false
|
||||
default: "true"
|
||||
default: 'true'
|
||||
|
||||
outputs:
|
||||
app-path:
|
||||
description: "Path to the ready .app bundle"
|
||||
description: 'Path to the ready .app bundle'
|
||||
value: ${{ steps.locate.outputs.app-path }}
|
||||
app-id:
|
||||
description: "Bundle identifier read from the app's Info.plist"
|
||||
@@ -38,7 +38,7 @@ outputs:
|
||||
value: ${{ steps.fetch.outputs.source }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
using: 'composite'
|
||||
steps:
|
||||
# Installed even on a hit, because the fingerprint is derived from the
|
||||
# dependency graph and cannot be computed without it.
|
||||
|
||||
@@ -6,10 +6,6 @@ inputs:
|
||||
description: 'Node.js version'
|
||||
required: false
|
||||
default: '24.13'
|
||||
pnpm-version:
|
||||
description: 'pnpm version'
|
||||
required: false
|
||||
default: '11.1.2'
|
||||
install-deps:
|
||||
description: 'Whether to install dependencies with pnpm install --frozen-lockfile'
|
||||
required: false
|
||||
@@ -18,10 +14,28 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
# package.json `packageManager` is the single source of truth for the pnpm
|
||||
# version. This action carries no default of its own: a second default is
|
||||
# how local (corepack) and CI pnpm versions drift apart silently.
|
||||
- name: Resolve pnpm version from packageManager
|
||||
id: package-manager
|
||||
run: |
|
||||
set -euo pipefail
|
||||
spec="$(node -p "require('$GITHUB_WORKSPACE/package.json').packageManager ?? ''")"
|
||||
case "$spec" in
|
||||
pnpm@[0-9]*) ;;
|
||||
*)
|
||||
echo "package.json packageManager must be pnpm@<version>, got '${spec}'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo "pnpm-version=${spec#pnpm@}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
with:
|
||||
version: ${{ inputs.pnpm-version }}
|
||||
version: ${{ steps.package-manager.outputs.pnpm-version }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
@@ -32,6 +46,21 @@ runs:
|
||||
# miss (any lockfile-changing PR) the post-job cache save fails the job.
|
||||
cache: ${{ inputs.install-deps == 'true' && 'pnpm' || '' }}
|
||||
|
||||
# Corepack resolves the same `packageManager` field locally, so a mismatch
|
||||
# here means CI is running a pnpm nobody develops against.
|
||||
- name: Assert pnpm matches packageManager
|
||||
env:
|
||||
EXPECTED_PNPM_VERSION: ${{ steps.package-manager.outputs.pnpm-version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
actual="$(pnpm --version)"
|
||||
if [ "$actual" != "$EXPECTED_PNPM_VERSION" ]; then
|
||||
echo "pnpm ${actual} is installed but package.json pins pnpm@${EXPECTED_PNPM_VERSION}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "pnpm ${actual} matches package.json packageManager"
|
||||
shell: bash
|
||||
|
||||
- name: Install dependencies
|
||||
if: ${{ inputs.install-deps == 'true' }}
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
name: "Upload Agent Device Artifacts"
|
||||
description: "Upload shared agent-device diagnostics, session logs, and replay artifacts"
|
||||
name: 'Upload Agent Device Artifacts'
|
||||
description: 'Upload shared agent-device diagnostics, session logs, and replay artifacts'
|
||||
|
||||
inputs:
|
||||
artifact-name:
|
||||
description: "Artifact name"
|
||||
description: 'Artifact name'
|
||||
required: true
|
||||
agent-home-dir:
|
||||
description: "Resolved agent-device daemon state directory"
|
||||
description: 'Resolved agent-device daemon state directory'
|
||||
required: true
|
||||
runner-derived-path:
|
||||
description: "Optional Apple runner derived data path to upload cache metadata and Xcode logs"
|
||||
description: 'Optional Apple runner derived data path to upload cache metadata and Xcode logs'
|
||||
required: false
|
||||
default: ".agent-device-no-runner-derived"
|
||||
default: '.agent-device-no-runner-derived'
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":semanticCommitTypeAll(chore)",
|
||||
"helpers:pinGitHubActionDigests"
|
||||
],
|
||||
"timezone": "Etc/UTC",
|
||||
"schedule": ["before 6am on monday"],
|
||||
"labels": ["dependencies"],
|
||||
"dependencyDashboard": true,
|
||||
"automerge": false,
|
||||
"prConcurrentLimit": 5,
|
||||
"prHourlyLimit": 2,
|
||||
"rebaseWhen": "conflicted",
|
||||
"minimumReleaseAge": "7 days",
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"schedule": ["before 6am on monday"],
|
||||
"automerge": false,
|
||||
"commitMessageAction": "refresh"
|
||||
},
|
||||
"vulnerabilityAlerts": {
|
||||
"enabled": true,
|
||||
"labels": ["dependencies", "security"],
|
||||
"schedule": ["at any time"],
|
||||
"minimumReleaseAge": null,
|
||||
"automerge": false
|
||||
},
|
||||
"osvVulnerabilityAlerts": true,
|
||||
"packageRules": [
|
||||
{
|
||||
"matchDepTypes": ["devDependencies"],
|
||||
"groupName": "devDependencies",
|
||||
"groupSlug": "dev-dependencies",
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"matchDepTypes": ["dependencies", "optionalDependencies", "peerDependencies"],
|
||||
"groupName": null,
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"groupName": "github actions",
|
||||
"groupSlug": "github-actions",
|
||||
"pinDigests": true,
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"matchFileNames": ["examples/test-app/package.json"],
|
||||
"matchDepTypes": ["devDependencies"],
|
||||
"groupName": "test-app fixture devDependencies",
|
||||
"groupSlug": "test-app-dev",
|
||||
"automerge": false
|
||||
},
|
||||
{
|
||||
"matchDepTypes": ["packageManager"],
|
||||
"groupName": "pnpm (packageManager)",
|
||||
"groupSlug": "package-manager",
|
||||
"automerge": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,13 +3,13 @@ name: Android
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "website/**"
|
||||
- "README.md"
|
||||
- ".github/actions/build-docs/action.yml"
|
||||
- ".github/workflows/deploy.yml"
|
||||
- ".github/workflows/pr-preview.yml"
|
||||
- ".github/workflows/pr-preview-cleanup.yml"
|
||||
- 'docs/**'
|
||||
- 'website/**'
|
||||
- 'README.md'
|
||||
- '.github/actions/build-docs/action.yml'
|
||||
- '.github/workflows/deploy.yml'
|
||||
- '.github/workflows/pr-preview.yml'
|
||||
- '.github/workflows/pr-preview-cleanup.yml'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
+10
-10
@@ -3,13 +3,13 @@ name: CI
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "website/**"
|
||||
- "README.md"
|
||||
- ".github/actions/build-docs/action.yml"
|
||||
- ".github/workflows/deploy.yml"
|
||||
- ".github/workflows/pr-preview.yml"
|
||||
- ".github/workflows/pr-preview-cleanup.yml"
|
||||
- 'docs/**'
|
||||
- 'website/**'
|
||||
- 'README.md'
|
||||
- '.github/actions/build-docs/action.yml'
|
||||
- '.github/workflows/deploy.yml'
|
||||
- '.github/workflows/pr-preview.yml'
|
||||
- '.github/workflows/pr-preview-cleanup.yml'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
- name: Setup Node.js 22.12
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: "22.12"
|
||||
node-version: '22.12'
|
||||
|
||||
- name: Install and run global CLI on Node.js 22.12
|
||||
run: |
|
||||
@@ -328,7 +328,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
AGENT_DEVICE_WEB_E2E: "1"
|
||||
AGENT_DEVICE_WEB_E2E: '1'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -336,7 +336,7 @@ jobs:
|
||||
- name: Setup toolchain
|
||||
uses: ./.github/actions/setup-node-pnpm
|
||||
with:
|
||||
node-version: "24.13"
|
||||
node-version: '24.13'
|
||||
|
||||
- name: Run live web smoke
|
||||
run: |
|
||||
|
||||
@@ -15,11 +15,11 @@ name: Conformance Differential
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 5 * * *"
|
||||
- cron: '0 5 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
only:
|
||||
description: "Single scenario id to run (default: all)"
|
||||
description: 'Single scenario id to run (default: all)'
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
@@ -32,11 +32,11 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
AGENT_DEVICE_CLI: "--experimental-strip-types src/bin.ts"
|
||||
AGENT_DEVICE_CLI: '--experimental-strip-types src/bin.ts'
|
||||
DIFFERENTIAL_ONLY: ${{ github.event.inputs.only || '' }}
|
||||
# CI should not phone home, and it keeps `maestro --version` to just the
|
||||
# version instead of prefixing an analytics notice.
|
||||
MAESTRO_CLI_NO_ANALYTICS: "1"
|
||||
MAESTRO_CLI_NO_ANALYTICS: '1'
|
||||
|
||||
jobs:
|
||||
differential-ios:
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 90
|
||||
env:
|
||||
IOS_RUNTIME_VERSION: "26.2"
|
||||
IOS_RUNTIME_VERSION: '26.2'
|
||||
AGENT_DEVICE_STATE_DIR: ${{ github.workspace }}/.tmp/agent-device-state
|
||||
AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: ${{ github.workspace }}/.tmp/ios-runner-derived
|
||||
steps:
|
||||
|
||||
@@ -15,7 +15,7 @@ name: Conformance Regenerate Verify
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 6 * * *"
|
||||
- cron: '0 6 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
java-version: '17'
|
||||
|
||||
# Verifies the pinned jar SHA-256s against pinned-upstream.json, runs the
|
||||
# harness over the corpus, and rewrites fixtures/ + corpus/manifest.json.
|
||||
|
||||
+10
-10
@@ -3,13 +3,13 @@ name: Linux
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "website/**"
|
||||
- "README.md"
|
||||
- ".github/actions/build-docs/action.yml"
|
||||
- ".github/workflows/deploy.yml"
|
||||
- ".github/workflows/pr-preview.yml"
|
||||
- ".github/workflows/pr-preview-cleanup.yml"
|
||||
- 'docs/**'
|
||||
- 'website/**'
|
||||
- 'README.md'
|
||||
- '.github/actions/build-docs/action.yml'
|
||||
- '.github/workflows/deploy.yml'
|
||||
- '.github/workflows/pr-preview.yml'
|
||||
- '.github/workflows/pr-preview-cleanup.yml'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -29,12 +29,12 @@ jobs:
|
||||
env:
|
||||
# Force X11 mode (Xvfb) — no Wayland on CI.
|
||||
XDG_SESSION_TYPE: x11
|
||||
DISPLAY: ":99"
|
||||
DISPLAY: ':99'
|
||||
# Headless GTK: avoid dconf issues, enable accessibility bridge.
|
||||
GSETTINGS_BACKEND: memory
|
||||
NO_AT_BRIDGE: "0"
|
||||
NO_AT_BRIDGE: '0'
|
||||
GTK_A11Y: atspi
|
||||
GTK_MODULES: "gail:atk-bridge"
|
||||
GTK_MODULES: 'gail:atk-bridge'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
@@ -3,13 +3,13 @@ name: macOS
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "website/**"
|
||||
- "README.md"
|
||||
- ".github/actions/build-docs/action.yml"
|
||||
- ".github/workflows/deploy.yml"
|
||||
- ".github/workflows/pr-preview.yml"
|
||||
- ".github/workflows/pr-preview-cleanup.yml"
|
||||
- 'docs/**'
|
||||
- 'website/**'
|
||||
- 'README.md'
|
||||
- '.github/actions/build-docs/action.yml'
|
||||
- '.github/workflows/deploy.yml'
|
||||
- '.github/workflows/pr-preview.yml'
|
||||
- '.github/workflows/pr-preview-cleanup.yml'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
@@ -25,17 +25,17 @@ on:
|
||||
# Kernel sources, every src test (ownership is derived, so any test may own
|
||||
# a kernel — `select` decides, not this filter), and the lane's own tooling.
|
||||
# scripts/mutation/workflow.test.ts asserts this covers the registry.
|
||||
- "src/kernel/errors.ts"
|
||||
- "src/daemon/ref-frame.ts"
|
||||
- "src/commands/interaction/runtime/settle.ts"
|
||||
- "src/utils/scroll-edge-state.ts"
|
||||
- "src/selectors/**"
|
||||
- "src/**/*.test.ts"
|
||||
- "scripts/mutation/**"
|
||||
- "scripts/lib/**"
|
||||
- "stryker.config.json"
|
||||
- "mutation-baselines/**"
|
||||
- ".github/workflows/mutation-affected.yml"
|
||||
- 'src/kernel/errors.ts'
|
||||
- 'src/daemon/ref-frame.ts'
|
||||
- 'src/commands/interaction/runtime/settle.ts'
|
||||
- 'src/utils/scroll-edge-state.ts'
|
||||
- 'src/selectors/**'
|
||||
- 'src/**/*.test.ts'
|
||||
- 'scripts/mutation/**'
|
||||
- 'scripts/lib/**'
|
||||
- 'stryker.config.json'
|
||||
- 'mutation-baselines/**'
|
||||
- '.github/workflows/mutation-affected.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -19,7 +19,7 @@ name: Mutation Weekly
|
||||
on:
|
||||
schedule:
|
||||
# Sundays 05:00 UTC — after the nightly lanes, before the working week.
|
||||
- cron: "0 5 * * 0"
|
||||
- cron: '0 5 * * 0'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -8,13 +8,13 @@ name: Perf Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 4 * * *"
|
||||
- cron: '0 4 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
rounds:
|
||||
description: "Measured rounds per command (samples)"
|
||||
description: 'Measured rounds per command (samples)'
|
||||
required: false
|
||||
default: "5"
|
||||
default: '5'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -24,7 +24,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
AGENT_DEVICE_PERF_CLI: "--experimental-strip-types src/bin.ts"
|
||||
AGENT_DEVICE_PERF_CLI: '--experimental-strip-types src/bin.ts'
|
||||
PERF_ROUNDS: ${{ github.event.inputs.rounds || '5' }}
|
||||
|
||||
jobs:
|
||||
@@ -33,10 +33,10 @@ jobs:
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 80
|
||||
env:
|
||||
IOS_RUNTIME_VERSION: "26.2"
|
||||
IOS_RUNTIME_VERSION: '26.2'
|
||||
AGENT_DEVICE_STATE_DIR: ${{ github.workspace }}/.tmp/agent-device-state
|
||||
AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: ${{ github.workspace }}/.tmp/ios-runner-derived
|
||||
AGENT_DEVICE_IOS_PREPARE_TIMEOUT_MS: "420000"
|
||||
AGENT_DEVICE_IOS_PREPARE_TIMEOUT_MS: '420000'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
@@ -7,13 +7,13 @@ on:
|
||||
- reopened
|
||||
- synchronize
|
||||
paths:
|
||||
- "website/**"
|
||||
- ".github/actions/build-docs/action.yml"
|
||||
- ".github/actions/setup-node-pnpm/action.yml"
|
||||
- ".github/workflows/pr-preview.yml"
|
||||
- "package.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- 'website/**'
|
||||
- '.github/actions/build-docs/action.yml'
|
||||
- '.github/actions/setup-node-pnpm/action.yml'
|
||||
- '.github/workflows/pr-preview.yml'
|
||||
- 'package.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
|
||||
concurrency: preview-${{ github.ref }}
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ name: Size
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "docs/**"
|
||||
- "website/**"
|
||||
- "README.md"
|
||||
- ".github/actions/build-docs/action.yml"
|
||||
- ".github/workflows/deploy.yml"
|
||||
- ".github/workflows/pr-preview.yml"
|
||||
- ".github/workflows/pr-preview-cleanup.yml"
|
||||
- 'docs/**'
|
||||
- 'website/**'
|
||||
- 'README.md'
|
||||
- '.github/actions/build-docs/action.yml'
|
||||
- '.github/workflows/deploy.yml'
|
||||
- '.github/workflows/pr-preview.yml'
|
||||
- '.github/workflows/pr-preview-cleanup.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -20,9 +20,9 @@ on:
|
||||
- main
|
||||
pull_request:
|
||||
paths:
|
||||
- "examples/test-app/**"
|
||||
- ".github/workflows/test-app-build-cache.yml"
|
||||
- ".github/actions/setup-fixture-app/**"
|
||||
- 'examples/test-app/**'
|
||||
- '.github/workflows/test-app-build-cache.yml'
|
||||
- '.github/actions/setup-fixture-app/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
+9
-1
@@ -4,5 +4,13 @@
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"sortPackageJson": false,
|
||||
"ignorePatterns": ["dist/**", "node_modules/**", "**/.skillgym-results/**"]
|
||||
"ignorePatterns": [
|
||||
"dist/**",
|
||||
"node_modules/**",
|
||||
"**/.skillgym-results/**",
|
||||
"**/*.md",
|
||||
"scripts/maestro-conformance/corpus/**",
|
||||
"fallow-baselines/**",
|
||||
"mutation-baselines/**"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -155,9 +155,10 @@ one question so `rg` → read-whole-file stays one cheap bounded read.
|
||||
`tsconfig.lib.json` (it needs an explicit `rootDir: "./src"`) and `tsdown.config.ts` first, and run
|
||||
`pnpm check:tooling` for any build-tooling edit.
|
||||
- Prefer the aggregate `package.json` scripts; they encode the expected validation bundles better
|
||||
than ad hoc command lists — including which paths get formatted: run `pnpm format`, never
|
||||
`oxfmt <path>`, or you will reformat files the repo deliberately leaves alone (`scripts/`,
|
||||
every `.md`).
|
||||
than ad hoc command lists. `pnpm format` formats the whole repo (`oxfmt` with no path list), and
|
||||
the only exclusion list is `.oxfmtrc.json` `ignorePatterns` — Markdown, the Maestro conformance
|
||||
corpus, and generated baselines. Run `pnpm format`, never `oxfmt <path>`: a path argument
|
||||
reformats a subset and hides whatever else drifted.
|
||||
- Before pushing, the aggregate is **`pnpm check`** (`check:tooling && check:fallow &&
|
||||
check:unit`). `pnpm check:tooling` is a *subset*: it stops before the Fallow audit, so dead
|
||||
exports and complexity findings your diff introduces still fail CI after it passes clean.
|
||||
|
||||
+22
-1
@@ -7,16 +7,22 @@ Thanks for your interest in contributing to agent-device.
|
||||
Requirements:
|
||||
|
||||
- Node.js 22+
|
||||
- pnpm
|
||||
- pnpm, activated from `package.json` `packageManager` (`corepack enable pnpm`)
|
||||
- Android SDK tools (`adb`) for Android support
|
||||
- Xcode (`simctl`/`devicectl`) for iOS support
|
||||
|
||||
Setup:
|
||||
|
||||
```bash
|
||||
corepack enable pnpm
|
||||
pnpm install
|
||||
```
|
||||
|
||||
`package.json` `packageManager` is the single source of truth for the pnpm version. Corepack
|
||||
activates that exact version locally, and `.github/actions/setup-node-pnpm` reads the same field
|
||||
and fails the job when the installed `pnpm --version` disagrees — so the CI and local package
|
||||
managers cannot drift apart silently, and the "Ignoring...pnpm" drift warning stays quiet locally.
|
||||
|
||||
Build all CLIs and Xcode projects:
|
||||
|
||||
```bash
|
||||
@@ -162,6 +168,21 @@ Examples:
|
||||
// root. Revisit only if external artifacts get an ownership marker that makes cleanup safe.
|
||||
```
|
||||
|
||||
## Dependency Updates
|
||||
|
||||
Renovate (`.github/renovate.json`) proposes dependency updates: weekly lockfile maintenance, one
|
||||
grouped PR for devDependencies, one PR per runtime dependency, and digest bumps for GitHub Actions.
|
||||
Security updates are enabled explicitly (`vulnerabilityAlerts` plus `osvVulnerabilityAlerts`) and
|
||||
are the only updates exempt from the 7-day minimum release age.
|
||||
|
||||
Renovate PRs are gated exactly like human PRs: `automerge` is off everywhere, so every branch needs
|
||||
a green CI run and a human review before merge. Review one the way you would review any dependency
|
||||
change — read the release notes in the PR body, and check that the affected-check plan for the diff
|
||||
(`pnpm check:affected --base origin/main --run`, which fails open to the full set for lockfile and
|
||||
workflow changes) is green. A green Renovate PR is a merge candidate, not a merge: no rubber-stamp
|
||||
automerge path exists, and none should be added without also deciding which gate is trusted to
|
||||
replace the reviewer.
|
||||
|
||||
## Issue Labels
|
||||
|
||||
Issue labels describe workflow state, not who will do the work. See
|
||||
|
||||
@@ -86,7 +86,10 @@ export function AudioScreen() {
|
||||
function playNativeSample() {
|
||||
stopNativeSample();
|
||||
try {
|
||||
const player = createAudioPlayer({ uri: createNativeBeepDataUri(), name: 'Agent Device beep' });
|
||||
const player = createAudioPlayer({
|
||||
uri: createNativeBeepDataUri(),
|
||||
name: 'Agent Device beep',
|
||||
});
|
||||
nativePlayerRef.current = player;
|
||||
player.play();
|
||||
setPlaybackState('playing');
|
||||
@@ -304,7 +307,8 @@ function base64Encode(bytes: Uint8Array): string {
|
||||
const third = bytes[index + 2];
|
||||
output += alphabet[first >> 2];
|
||||
output += alphabet[((first & 0x03) << 4) | ((second ?? 0) >> 4)];
|
||||
output += index + 1 < bytes.length ? alphabet[((second & 0x0f) << 2) | ((third ?? 0) >> 6)] : '=';
|
||||
output +=
|
||||
index + 1 < bytes.length ? alphabet[((second & 0x0f) << 2) | ((third ?? 0) >> 6)] : '=';
|
||||
output += index + 2 < bytes.length ? alphabet[(third ?? 0) & 0x3f] : '=';
|
||||
}
|
||||
return output;
|
||||
|
||||
@@ -36,8 +36,8 @@ export function InertScreen() {
|
||||
Tapping this text changes nothing on screen.
|
||||
</Text>
|
||||
<Text style={styles.footnote}>
|
||||
Nothing here reacts to touch, loads, or animates, so a tap leaves the
|
||||
hierarchy byte-identical and retryTapIfNoChange must re-tap.
|
||||
Nothing here reacts to touch, loads, or animates, so a tap leaves the hierarchy
|
||||
byte-identical and retryTapIfNoChange must re-tap.
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@
|
||||
"url": "https://github.com/callstack/agent-device/issues"
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@11.1.2",
|
||||
"packageManager": "pnpm@11.17.0",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"exports": {
|
||||
@@ -114,8 +114,8 @@
|
||||
"mutation:affected": "node --experimental-strip-types scripts/mutation/run.ts --affected",
|
||||
"mutation:test": "node --experimental-strip-types --test scripts/mutation/*.test.ts",
|
||||
"lint": "oxlint . --deny-warnings",
|
||||
"format": "node ./node_modules/oxfmt/bin/oxfmt --write src test skills scripts/fuzz scripts/mutation scripts/lib scripts/help-conformance-bench.mjs scripts/help-conformance-case-checks.mjs scripts/help-conformance-command-validator.ts scripts/help-conformance-expectations.mjs scripts/help-conformance-plan-validator.mjs scripts/help-conformance-runner-output.mjs scripts/help-conformance-summary.mjs scripts/help-conformance-cases.mjs scripts/help-conformance-sample-outputs.mjs scripts/__tests__/help-conformance-bench.test.ts scripts/__tests__/help-conformance-sample-outputs.test.ts scripts/__tests__/help-conformance-topic-coverage.test.ts package.json tsconfig.json tsconfig.lib.json tsdown.config.ts vitest.config.ts vitest.mutation.config.ts .github/actions/setup-node-pnpm/action.yml .oxlintrc.json .oxfmtrc.json '!test/skillgym/.skillgym-results/**'",
|
||||
"format:check": "node ./node_modules/oxfmt/bin/oxfmt --check src test skills scripts/fuzz scripts/mutation scripts/lib scripts/help-conformance-bench.mjs scripts/help-conformance-case-checks.mjs scripts/help-conformance-command-validator.ts scripts/help-conformance-expectations.mjs scripts/help-conformance-plan-validator.mjs scripts/help-conformance-runner-output.mjs scripts/help-conformance-summary.mjs scripts/help-conformance-cases.mjs scripts/help-conformance-sample-outputs.mjs scripts/__tests__/help-conformance-bench.test.ts scripts/__tests__/help-conformance-sample-outputs.test.ts scripts/__tests__/help-conformance-topic-coverage.test.ts package.json tsconfig.json tsconfig.lib.json tsdown.config.ts vitest.config.ts vitest.mutation.config.ts .github/actions/setup-node-pnpm/action.yml .oxlintrc.json .oxfmtrc.json '!test/skillgym/.skillgym-results/**'",
|
||||
"format": "node ./node_modules/oxfmt/bin/oxfmt --write",
|
||||
"format:check": "node ./node_modules/oxfmt/bin/oxfmt --check",
|
||||
"fuzz:parsers": "node --experimental-strip-types scripts/fuzz/run.ts",
|
||||
"fallow": "fallow audit --base origin/main",
|
||||
"fallow:all": "fallow --summary",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
packages:
|
||||
- "website"
|
||||
- 'website'
|
||||
# Keep non-interactive checks from downloading tooling or reinstalling dependencies implicitly.
|
||||
pmOnFail: warn
|
||||
verifyDepsBeforeRun: warn
|
||||
|
||||
@@ -47,7 +47,9 @@ for (const tag of REPLAY_COMPAT_RELEASED_TAGS) {
|
||||
|
||||
for (const entry of REPLAY_COMPAT_CORPUS) {
|
||||
if (!REPLAY_COMPAT_RELEASED_TAGS.includes(entry.recordedBy)) {
|
||||
failures.push(`${entry.id}: recordedBy ${entry.recordedBy} is not in REPLAY_COMPAT_RELEASED_TAGS`);
|
||||
failures.push(
|
||||
`${entry.id}: recordedBy ${entry.recordedBy} is not in REPLAY_COMPAT_RELEASED_TAGS`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (entry.provenance.kind !== 'mined') continue;
|
||||
|
||||
@@ -37,7 +37,7 @@ type Payload = {
|
||||
lvl: number;
|
||||
cyc: number;
|
||||
}[];
|
||||
/**
|
||||
/**
|
||||
* `[fromIndex, toIndex, kind, flags]`; kind 0=value 1=type 2=dynamic,
|
||||
* flags bit0=R5 back-edge, bit1=target also reachable at distance >= 2, bit2=R6 type inversion.
|
||||
*/
|
||||
@@ -71,9 +71,7 @@ function edgeKindCode(kind: GraphData['edges'][number]['kind']): number {
|
||||
*/
|
||||
function edgeFlags(edge: GraphData['edges'][number]): number {
|
||||
return (
|
||||
(edge.backEdge ? 1 : 0) |
|
||||
(edge.transitivelyReachable ? 2 : 0) |
|
||||
(edge.typeInversion ? 4 : 0)
|
||||
(edge.backEdge ? 1 : 0) | (edge.transitivelyReachable ? 2 : 0) | (edge.typeInversion ? 4 : 0)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -213,7 +213,11 @@ test('no tracked TypeScript source contains a raw NUL byte', () => {
|
||||
// output path, JSON shape or summary without anything failing. These run it as a subprocess, which
|
||||
// is the only way to cover argument handling and the file it actually writes.
|
||||
|
||||
function runBuild(args: readonly string[]): { status: number | null; stdout: string; stderr: string } {
|
||||
function runBuild(args: readonly string[]): {
|
||||
status: number | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
} {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
['--experimental-strip-types', 'scripts/depgraph/build.ts', ...args],
|
||||
@@ -243,7 +247,10 @@ test('build.ts writes the default path and a summary consistent with the JSON',
|
||||
|
||||
// The printed summary must agree with the payload it was derived from.
|
||||
const inversions = Object.values(payload.typeInversions).reduce((sum, n) => sum + n, 0);
|
||||
assert.match(stdout, new RegExp(`${payload.generated.files} files, ${payload.generated.edges} edges`));
|
||||
assert.match(
|
||||
stdout,
|
||||
new RegExp(`${payload.generated.files} files, ${payload.generated.edges} edges`),
|
||||
);
|
||||
assert.match(stdout, new RegExp(`type-only spine inversions \\(R6\\): ${inversions}`));
|
||||
const reachable = payload.edges.filter(([, , , flags]) => (flags & 2) !== 0).length;
|
||||
assert.match(stdout, new RegExp(`reachable at distance >= 2: ${reachable}`));
|
||||
|
||||
@@ -66,7 +66,6 @@ export type GraphData = {
|
||||
typeInversions: Record<string, number>;
|
||||
};
|
||||
|
||||
|
||||
function countLines(source: string): number {
|
||||
let lines = 1;
|
||||
for (let index = 0; index < source.length; index++) {
|
||||
@@ -317,9 +316,7 @@ function aggregateZones(nodes: ReadonlyMap<string, GraphNode>): GraphData['zones
|
||||
* but the count feeding a CI equality check must not be able to drift for a reason unrelated to
|
||||
* layering.
|
||||
*/
|
||||
export function typeInversionsByPair(
|
||||
edges: readonly ResolvedImportEdge[],
|
||||
): Record<string, number> {
|
||||
export function typeInversionsByPair(edges: readonly ResolvedImportEdge[]): Record<string, number> {
|
||||
const seen = new Set<string>();
|
||||
const byPair = new Map<string, number>();
|
||||
for (const edge of edges) {
|
||||
|
||||
@@ -83,7 +83,9 @@ function readSources(files: readonly string[]): Map<string, string> {
|
||||
}
|
||||
|
||||
function fileExists(file: string): boolean {
|
||||
return fs.existsSync(path.join(repoRoot, file)) && fs.statSync(path.join(repoRoot, file)).isFile();
|
||||
return (
|
||||
fs.existsSync(path.join(repoRoot, file)) && fs.statSync(path.join(repoRoot, file)).isFile()
|
||||
);
|
||||
}
|
||||
|
||||
function readSourceOrNull(file: string): string | null {
|
||||
@@ -443,7 +445,10 @@ function checkSessionStateOwnership(sources: ReadonlyMap<string, string>): Viola
|
||||
// `node_modules` is always present — which is why this needs a gate rather than a convention.
|
||||
function checkZeroDepJobs(): Violation[] {
|
||||
const workflows = readSources(
|
||||
execFileSync('git', ['ls-files', '.github/workflows/*.yml'], { cwd: repoRoot, encoding: 'utf8' })
|
||||
execFileSync('git', ['ls-files', '.github/workflows/*.yml'], {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
.split('\n')
|
||||
.filter(Boolean),
|
||||
);
|
||||
@@ -480,9 +485,7 @@ function checkZeroDepJobs(): Violation[] {
|
||||
}
|
||||
|
||||
function sessionStateFieldCount(): number {
|
||||
return (
|
||||
Object.keys(SESSION_STATE_FIELD_OWNERS).length + STORE_OWNED_SESSION_STATE_FIELDS.size
|
||||
);
|
||||
return Object.keys(SESSION_STATE_FIELD_OWNERS).length + STORE_OWNED_SESSION_STATE_FIELDS.size;
|
||||
}
|
||||
|
||||
/** R9 is growth-only, so a shrunk tree is reported rather than failed — see checkTypeCycleGrowth. */
|
||||
|
||||
@@ -395,11 +395,18 @@ test("the repo's own zero-dep jobs resolve without node_modules", () => {
|
||||
};
|
||||
const exists = (file: string): boolean => read(file) !== null;
|
||||
|
||||
const jobs = zeroDepJobs(new Map([['.github/workflows/ci.yml', read('.github/workflows/ci.yml')!]]), exists);
|
||||
const jobs = zeroDepJobs(
|
||||
new Map([['.github/workflows/ci.yml', read('.github/workflows/ci.yml')!]]),
|
||||
exists,
|
||||
);
|
||||
assert.ok(jobs.length > 0, 'expected ci.yml to still declare at least one zero-dep job');
|
||||
for (const job of jobs) {
|
||||
assert.ok(job.entries.length > 0, `${job.job} must name an entry script`);
|
||||
assert.deepEqual(uninstallableImports(job, read, exists), [], `${job.job} must reach no package`);
|
||||
assert.deepEqual(
|
||||
uninstallableImports(job, read, exists),
|
||||
[],
|
||||
`${job.job} must reach no package`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -475,20 +482,24 @@ test('classification drift is reported in all three directions', () => {
|
||||
test('largestTypeCycleSize counts type-only cycles and ignores dynamic ones', () => {
|
||||
// Acyclic: every component is a single file, so the largest is 1 (not 0).
|
||||
const acyclic = resolveImportEdges(
|
||||
new Map(Object.entries({
|
||||
'src/core/a.ts': "import type { B } from '../contracts/b.ts';",
|
||||
'src/contracts/b.ts': 'export type B = 1;',
|
||||
})),
|
||||
new Map(
|
||||
Object.entries({
|
||||
'src/core/a.ts': "import type { B } from '../contracts/b.ts';",
|
||||
'src/contracts/b.ts': 'export type B = 1;',
|
||||
}),
|
||||
),
|
||||
);
|
||||
assert.equal(largestTypeCycleSize(acyclic), 1);
|
||||
|
||||
// A three-file loop closed by type-only imports is exactly what R4 permits and R9 measures.
|
||||
const typeCycle = resolveImportEdges(
|
||||
new Map(Object.entries({
|
||||
'src/core/a.ts': "import type { B } from './b.ts';",
|
||||
'src/core/b.ts': "import type { C } from './c.ts';\nexport type B = 1;",
|
||||
'src/core/c.ts': "import type { A } from './a.ts';\nexport type C = 1;",
|
||||
})),
|
||||
new Map(
|
||||
Object.entries({
|
||||
'src/core/a.ts': "import type { B } from './b.ts';",
|
||||
'src/core/b.ts': "import type { C } from './c.ts';\nexport type B = 1;",
|
||||
'src/core/c.ts': "import type { A } from './a.ts';\nexport type C = 1;",
|
||||
}),
|
||||
),
|
||||
);
|
||||
assert.equal(largestTypeCycleSize(typeCycle), 3);
|
||||
|
||||
@@ -496,20 +507,24 @@ test('largestTypeCycleSize counts type-only cycles and ignores dynamic ones', ()
|
||||
// comprehension barrier, and R3 relies on dynamic imports existing. With no non-dynamic edge at
|
||||
// all no file enters the walk, so the floor here is 0 rather than 1 — specified, not incidental.
|
||||
const dynamicCycle = resolveImportEdges(
|
||||
new Map(Object.entries({
|
||||
'src/core/a.ts': "void import('./b.ts');",
|
||||
'src/core/b.ts': "void import('./a.ts');",
|
||||
})),
|
||||
new Map(
|
||||
Object.entries({
|
||||
'src/core/a.ts': "void import('./b.ts');",
|
||||
'src/core/b.ts': "void import('./a.ts');",
|
||||
}),
|
||||
),
|
||||
);
|
||||
assert.equal(largestTypeCycleSize(dynamicCycle), 0);
|
||||
|
||||
// A value cycle counts too — R4 rejects it separately, so R9 must not be the thing that
|
||||
// notices, but it must not under-report either.
|
||||
const valueCycle = resolveImportEdges(
|
||||
new Map(Object.entries({
|
||||
'src/core/a.ts': "export { b } from './b.ts';",
|
||||
'src/core/b.ts': "export { a } from './a.ts';",
|
||||
})),
|
||||
new Map(
|
||||
Object.entries({
|
||||
'src/core/a.ts': "export { b } from './b.ts';",
|
||||
'src/core/b.ts': "export { a } from './a.ts';",
|
||||
}),
|
||||
),
|
||||
);
|
||||
assert.equal(largestTypeCycleSize(valueCycle), 2);
|
||||
});
|
||||
|
||||
@@ -134,9 +134,7 @@ export type FieldClassificationDrift = {
|
||||
* Where the two ownership tables disagree with `SessionState` itself. Empty means every declared
|
||||
* field is classified exactly once and neither table names a field that no longer exists.
|
||||
*/
|
||||
export function fieldClassificationDrift(
|
||||
fields: readonly string[],
|
||||
): FieldClassificationDrift[] {
|
||||
export function fieldClassificationDrift(fields: readonly string[]): FieldClassificationDrift[] {
|
||||
const declared = new Set(fields);
|
||||
const owned = new Set(Object.keys(SESSION_STATE_FIELD_OWNERS));
|
||||
const drift: FieldClassificationDrift[] = [];
|
||||
|
||||
@@ -54,7 +54,9 @@ function runBlockEntries(run: string): string[] {
|
||||
function stepsOf(job: unknown): Record<string, unknown>[] {
|
||||
if (job === null || typeof job !== 'object') return [];
|
||||
const steps = (job as Record<string, unknown>)['steps'];
|
||||
return Array.isArray(steps) ? steps.filter((step) => step !== null && typeof step === 'object') : [];
|
||||
return Array.isArray(steps)
|
||||
? steps.filter((step) => step !== null && typeof step === 'object')
|
||||
: [];
|
||||
}
|
||||
|
||||
function skipsInstall(step: Record<string, unknown>): boolean {
|
||||
@@ -96,7 +98,8 @@ export function zeroDepJobs(
|
||||
}
|
||||
}
|
||||
return jobs.sort(
|
||||
(left, right) => left.workflow.localeCompare(right.workflow) || left.job.localeCompare(right.job),
|
||||
(left, right) =>
|
||||
left.workflow.localeCompare(right.workflow) || left.job.localeCompare(right.job),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,23 +17,37 @@ const UPSTREAM_REPO_DIR = 'maestro-test/src/test/resources';
|
||||
|
||||
const NOTES = {
|
||||
'bug-classes/percent-decimal-swipe': 'Bug class 1: decimal percentage rejection at parse.',
|
||||
'bug-classes/target-swipe-missing-direction': 'Bug class 2: target swipe requires an explicit direction.',
|
||||
'bug-classes/retry-over-cap': 'Bug class 3: retry maxRetries parses verbatim; the clamp is a layer-2 vector.',
|
||||
'bug-classes/settle-after-tap': 'Bug class 4: plain tap; settle ordering is a layer-3 differential.',
|
||||
'bug-classes/target-swipe-missing-direction':
|
||||
'Bug class 2: target swipe requires an explicit direction.',
|
||||
'bug-classes/retry-over-cap':
|
||||
'Bug class 3: retry maxRetries parses verbatim; the clamp is a layer-2 vector.',
|
||||
'bug-classes/settle-after-tap':
|
||||
'Bug class 4: plain tap; settle ordering is a layer-3 differential.',
|
||||
'authored/runscript': 'Coverage: runScript file command (no self-contained upstream flow).',
|
||||
'authored/runflow-main': 'Coverage: runFlow file include + provenance.',
|
||||
'authored/runflow-child': 'Include target for runflow-main (not parsed as a top-level flow).',
|
||||
'authored/doubletap': 'Coverage: doubleTapOn (upstream 101 uses the unsupported retryTapIfNoChange option).',
|
||||
'authored/scroll-until-visible': 'Coverage: scrollUntilVisible (upstream 079 uses unsupported speed/visibilityPercentage).',
|
||||
'authored/extended-wait': 'Coverage: extendedWaitUntil (upstream 042 interpolates ${TIMEOUT} from a flow env block).',
|
||||
'authored/repeat': 'Coverage: repeat.times (upstream 053 also uses the unsupported evalScript command).',
|
||||
'authored/presskey': 'Coverage: pressKey supported keys (upstream 034 exercises many unsupported keycodes).',
|
||||
'authored/numeric-variable-tap': 'Coverage: tapOn repeat/delay/index numeric option fields accept ${VAR} tokens (upstream rejects repeat/delay as integer-typed; agent-device is lenient).',
|
||||
'authored/numeric-variable-doubletap': 'Coverage: doubleTapOn delay accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).',
|
||||
'authored/numeric-variable-swipe': 'Coverage: swipe duration accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).',
|
||||
'authored/numeric-variable-erase': 'Coverage: eraseText charactersToErase accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).',
|
||||
'authored/numeric-variable-tap-index': 'Coverage: tapOn index accepts a ${VAR} token and projects identically through the canonical model.',
|
||||
'authored/numeric-variable-wait': 'Coverage: waitForAnimationToEnd timeout accepts a ${VAR} token and projects identically through the canonical model.',
|
||||
'authored/doubletap':
|
||||
'Coverage: doubleTapOn (upstream 101 uses the unsupported retryTapIfNoChange option).',
|
||||
'authored/scroll-until-visible':
|
||||
'Coverage: scrollUntilVisible (upstream 079 uses unsupported speed/visibilityPercentage).',
|
||||
'authored/extended-wait':
|
||||
'Coverage: extendedWaitUntil (upstream 042 interpolates ${TIMEOUT} from a flow env block).',
|
||||
'authored/repeat':
|
||||
'Coverage: repeat.times (upstream 053 also uses the unsupported evalScript command).',
|
||||
'authored/presskey':
|
||||
'Coverage: pressKey supported keys (upstream 034 exercises many unsupported keycodes).',
|
||||
'authored/numeric-variable-tap':
|
||||
'Coverage: tapOn repeat/delay/index numeric option fields accept ${VAR} tokens (upstream rejects repeat/delay as integer-typed; agent-device is lenient).',
|
||||
'authored/numeric-variable-doubletap':
|
||||
'Coverage: doubleTapOn delay accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).',
|
||||
'authored/numeric-variable-swipe':
|
||||
'Coverage: swipe duration accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).',
|
||||
'authored/numeric-variable-erase':
|
||||
'Coverage: eraseText charactersToErase accepts a ${VAR} token (upstream rejects as integer-typed; agent-device is lenient).',
|
||||
'authored/numeric-variable-tap-index':
|
||||
'Coverage: tapOn index accepts a ${VAR} token and projects identically through the canonical model.',
|
||||
'authored/numeric-variable-wait':
|
||||
'Coverage: waitForAnimationToEnd timeout accepts a ${VAR} token and projects identically through the canonical model.',
|
||||
'invalid/bad-swipe-direction': 'Lenient-guard: unknown SwipeDirection enum value.',
|
||||
'invalid/unknown-command': 'Lenient-guard: unknown command name (tapOn typo).',
|
||||
'invalid/malformed-selector': 'Lenient-guard: selector given as a sequence.',
|
||||
@@ -46,7 +60,10 @@ const sha256 = (file) => createHash('sha256').update(fs.readFileSync(file)).dige
|
||||
|
||||
const yamlIn = (dir) =>
|
||||
fs.existsSync(path.join(CORPUS_DIR, dir))
|
||||
? fs.readdirSync(path.join(CORPUS_DIR, dir)).filter((n) => n.endsWith('.yaml')).sort()
|
||||
? fs
|
||||
.readdirSync(path.join(CORPUS_DIR, dir))
|
||||
.filter((n) => n.endsWith('.yaml'))
|
||||
.sort()
|
||||
: [];
|
||||
|
||||
function entriesFor(dir, kind) {
|
||||
|
||||
@@ -41,7 +41,10 @@ test('a tap that burns the full settle budget violates the invariant (bug class
|
||||
});
|
||||
|
||||
test('the invariant reports the slowest matching step, not the first', () => {
|
||||
const result = evaluateInvariant([stop('tapOn', 300, 1), stop('tapOn', 2117, 2)], SETTLE_INVARIANT);
|
||||
const result = evaluateInvariant(
|
||||
[stop('tapOn', 300, 1), stop('tapOn', 2117, 2)],
|
||||
SETTLE_INVARIANT,
|
||||
);
|
||||
assert.equal(result.status, 'violated');
|
||||
assert.match(result.detail, /2117ms/);
|
||||
});
|
||||
|
||||
@@ -22,7 +22,10 @@ test('every differential scenario references an existing corpus flow with a uniq
|
||||
|
||||
test('the settle-loop bug class (4) is covered by a differential scenario', () => {
|
||||
const settle = DIFFERENTIAL_SCENARIOS.find((scenario) => scenario.bugClass === 4);
|
||||
assert.ok(settle, 'bug class 4 (settle ordering) has no reflectable constant; it must be a differential scenario');
|
||||
assert.ok(
|
||||
settle,
|
||||
'bug class 4 (settle ordering) has no reflectable constant; it must be a differential scenario',
|
||||
);
|
||||
assert.equal(settle?.id, 'settle-after-tap');
|
||||
});
|
||||
|
||||
@@ -75,14 +78,22 @@ test('every knownDivergence carries a tracking issue', () => {
|
||||
// swallow upstream regressing or a different invariant breaking — hiding the
|
||||
// next bug behind the last one.
|
||||
describe('knownDivergence signature matching', () => {
|
||||
const sig: DivergenceSignature = { maestro: 'pass', agentDevice: 'fail', invariants: ['no-data'] };
|
||||
const sig: DivergenceSignature = {
|
||||
maestro: 'pass',
|
||||
agentDevice: 'fail',
|
||||
invariants: ['no-data'],
|
||||
};
|
||||
const engine = (outcome: 'pass' | 'fail') => ({
|
||||
engine: 'maestro' as const,
|
||||
outcome,
|
||||
exitCode: outcome === 'pass' ? 0 : 1,
|
||||
});
|
||||
const inv = (status: 'held' | 'violated' | 'no-data') =>
|
||||
({ invariant: { kind: 'stepDurationBelow', command: 'tapOn', maxMs: 1, because: 'x' }, status, detail: '' }) as never;
|
||||
({
|
||||
invariant: { kind: 'stepDurationBelow', command: 'tapOn', maxMs: 1, because: 'x' },
|
||||
status,
|
||||
detail: '',
|
||||
}) as never;
|
||||
|
||||
test('matches the declared failure exactly', () => {
|
||||
assert.equal(matchesSignature(sig, engine('pass'), engine('fail'), [inv('no-data')]), true);
|
||||
@@ -113,7 +124,10 @@ describe('knownDivergence signature matching', () => {
|
||||
for (const scenario of DIFFERENTIAL_SCENARIOS) {
|
||||
const declared = scenario.knownDivergence;
|
||||
if (!declared) continue;
|
||||
assert.ok(declared.expected, `${scenario.id}: knownDivergence must declare an expected signature`);
|
||||
assert.ok(
|
||||
declared.expected,
|
||||
`${scenario.id}: knownDivergence must declare an expected signature`,
|
||||
);
|
||||
assert.ok(
|
||||
['pass', 'fail'].includes(declared.expected.maestro) &&
|
||||
['pass', 'fail'].includes(declared.expected.agentDevice),
|
||||
|
||||
@@ -68,17 +68,18 @@ export function validateScenarios(): void {
|
||||
if (ids.has(scenario.id)) throw new Error(`Duplicate scenario id: ${scenario.id}`);
|
||||
ids.add(scenario.id);
|
||||
const flowPath = path.join(CONFORMANCE_DIR, scenario.flow);
|
||||
if (!fs.existsSync(flowPath)) throw new Error(`Scenario ${scenario.id} flow not found: ${scenario.flow}`);
|
||||
if (!fs.existsSync(flowPath))
|
||||
throw new Error(`Scenario ${scenario.id} flow not found: ${scenario.flow}`);
|
||||
}
|
||||
}
|
||||
|
||||
type EngineResult = { engine: 'maestro' | 'agent-device'; outcome: 'pass' | 'fail'; exitCode: number };
|
||||
type EngineResult = {
|
||||
engine: 'maestro' | 'agent-device';
|
||||
outcome: 'pass' | 'fail';
|
||||
exitCode: number;
|
||||
};
|
||||
|
||||
function runEngine(
|
||||
engine: EngineResult['engine'],
|
||||
command: string,
|
||||
args: string[],
|
||||
): EngineResult {
|
||||
function runEngine(engine: EngineResult['engine'], command: string, args: string[]): EngineResult {
|
||||
const [bin = '', ...rest] = command.split(' ').filter(Boolean);
|
||||
const result = spawnSync(bin, [...rest, ...args], { stdio: 'inherit', cwd: process.cwd() });
|
||||
const exitCode = result.status ?? 1;
|
||||
@@ -255,7 +256,9 @@ function main(argv: readonly string[]): void {
|
||||
// Keep declared gaps visible: a green run must still say what it is not proving.
|
||||
const known = reports.filter((report) => report.status === 'known-divergence');
|
||||
if (known.length > 0) {
|
||||
console.log(`\n${known.length} declared divergence(s), not enforced: ${known.map((r) => r.id).join(', ')}`);
|
||||
console.log(
|
||||
`\n${known.length} declared divergence(s), not enforced: ${known.map((r) => r.id).join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const failed = reports.filter((report) => report.failed);
|
||||
|
||||
@@ -32,7 +32,8 @@ export const FLOW_DIVERGENCES: Record<string, FlowDivergence> = {
|
||||
},
|
||||
'upstream/062_copy_paste_text': {
|
||||
classification: 'we-reject',
|
||||
reason: 'Clipboard commands are unsupported; pasteText is de-advertised (takes clipboard, not inline text).',
|
||||
reason:
|
||||
'Clipboard commands are unsupported; pasteText is de-advertised (takes clipboard, not inline text).',
|
||||
unsupported: ['copyTextFrom', 'pasteText'],
|
||||
},
|
||||
'upstream/067_assertTrue_pass': {
|
||||
@@ -58,7 +59,8 @@ export const FLOW_DIVERGENCES: Record<string, FlowDivergence> = {
|
||||
},
|
||||
'upstream/053_repeat_times': {
|
||||
classification: 'we-reject',
|
||||
reason: 'repeat is supported, but the flow also uses evalScript and a ${output.list.length} times expression.',
|
||||
reason:
|
||||
'repeat is supported, but the flow also uses evalScript and a ${output.list.length} times expression.',
|
||||
unsupported: ['evalScript'],
|
||||
},
|
||||
// --- Deliberately stricter than upstream ---
|
||||
@@ -83,23 +85,27 @@ export const FLOW_DIVERGENCES: Record<string, FlowDivergence> = {
|
||||
},
|
||||
'upstream/076_optional_assertion': {
|
||||
classification: 'we-reject',
|
||||
reason: 'assertTrue is outside the supported subset; optional is now supported on scrollUntilVisible and extendedWaitUntil.',
|
||||
reason:
|
||||
'assertTrue is outside the supported subset; optional is now supported on scrollUntilVisible and extendedWaitUntil.',
|
||||
unsupported: ['assertTrue'],
|
||||
tracking: 'https://github.com/callstack/agent-device/issues/1295',
|
||||
},
|
||||
'upstream/079_scroll_until_visible': {
|
||||
classification: 'we-reject',
|
||||
reason: 'scrollUntilVisible is supported; the flow sets the unsupported speed and visibilityPercentage options.',
|
||||
reason:
|
||||
'scrollUntilVisible is supported; the flow sets the unsupported speed and visibilityPercentage options.',
|
||||
unsupported: ['scrollUntilVisible.speed', 'scrollUntilVisible.visibilityPercentage'],
|
||||
},
|
||||
'upstream/101_doubleTapOn': {
|
||||
classification: 'we-reject',
|
||||
reason: 'doubleTapOn is supported; the flow sets the unsupported retryTapIfNoChange option on it.',
|
||||
reason:
|
||||
'doubleTapOn is supported; the flow sets the unsupported retryTapIfNoChange option on it.',
|
||||
unsupported: ['doubleTapOn.retryTapIfNoChange'],
|
||||
},
|
||||
'upstream/119_retry_commands': {
|
||||
classification: 'we-reject',
|
||||
reason: 'retry is supported; the flow uses the unsupported per-command waitToSettleTimeoutMs option on a nested tapOn.',
|
||||
reason:
|
||||
'retry is supported; the flow uses the unsupported per-command waitToSettleTimeoutMs option on a nested tapOn.',
|
||||
unsupported: ['tapOn.waitToSettleTimeoutMs'],
|
||||
},
|
||||
// --- agent-device supports ${VAR} in integer-typed numeric option fields that the pinned upstream parser rejects ---
|
||||
|
||||
@@ -33,7 +33,12 @@ export type CanonicalTarget = {
|
||||
|
||||
export type CanonicalGesture =
|
||||
| { mode: 'direction'; direction: string; duration?: number | string }
|
||||
| { mode: 'coordinates'; start?: CanonicalPoint; end?: CanonicalPoint; duration?: number | string }
|
||||
| {
|
||||
mode: 'coordinates';
|
||||
start?: CanonicalPoint;
|
||||
end?: CanonicalPoint;
|
||||
duration?: number | string;
|
||||
}
|
||||
| { mode: 'element'; from: CanonicalSelector; direction?: string; duration?: number | string };
|
||||
|
||||
export type CanonicalCommand =
|
||||
@@ -42,14 +47,31 @@ export type CanonicalCommand =
|
||||
// COUNT is the canonical field on both sides rather than a `double` variant on
|
||||
// one — that keeps our distinct tapOn/doubleTapOn kinds comparable to upstream
|
||||
// and preserves conformance signal for tapOn.repeat/delay.
|
||||
| { kind: 'tap'; longPress: boolean; repeat: number | string; delay?: number | string; target: CanonicalTarget }
|
||||
| { kind: 'assert'; mode: 'visible' | 'notVisible'; timed: boolean; timeout?: number | string; selector?: CanonicalSelector }
|
||||
| {
|
||||
kind: 'tap';
|
||||
longPress: boolean;
|
||||
repeat: number | string;
|
||||
delay?: number | string;
|
||||
target: CanonicalTarget;
|
||||
}
|
||||
| {
|
||||
kind: 'assert';
|
||||
mode: 'visible' | 'notVisible';
|
||||
timed: boolean;
|
||||
timeout?: number | string;
|
||||
selector?: CanonicalSelector;
|
||||
}
|
||||
| { kind: 'swipe'; gesture: CanonicalGesture }
|
||||
| { kind: 'inputText'; text?: string }
|
||||
| { kind: 'eraseText'; count?: number | string }
|
||||
| { kind: 'openLink'; link?: string }
|
||||
| { kind: 'scroll' }
|
||||
| { kind: 'scrollUntilVisible'; direction?: string; selector?: CanonicalSelector; timeout?: number | string }
|
||||
| {
|
||||
kind: 'scrollUntilVisible';
|
||||
direction?: string;
|
||||
selector?: CanonicalSelector;
|
||||
timeout?: number | string;
|
||||
}
|
||||
| { kind: 'pressKey'; key: string }
|
||||
| { kind: 'back' }
|
||||
| { kind: 'hideKeyboard' }
|
||||
@@ -141,7 +163,10 @@ function canonicalizeUpstreamCommand(command: UpstreamCommand): CanonicalCommand
|
||||
case 'InputTextCommand':
|
||||
return dropUndefined({ kind: 'inputText', text: str(f.text) });
|
||||
case 'EraseTextCommand':
|
||||
return dropUndefined({ kind: 'eraseText', count: numLike(f.charactersToErase) ?? str(f.charactersToErase) });
|
||||
return dropUndefined({
|
||||
kind: 'eraseText',
|
||||
count: numLike(f.charactersToErase) ?? str(f.charactersToErase),
|
||||
});
|
||||
case 'OpenLinkCommand':
|
||||
return dropUndefined({ kind: 'openLink', link: str(f.link) });
|
||||
case 'PressKeyCommand':
|
||||
@@ -153,13 +178,19 @@ function canonicalizeUpstreamCommand(command: UpstreamCommand): CanonicalCommand
|
||||
case 'TakeScreenshotCommand':
|
||||
return { kind: 'takeScreenshot' };
|
||||
case 'WaitForAnimationToEndCommand':
|
||||
return dropUndefined({ kind: 'waitForAnimationToEnd', timeout: numLike(f.timeout) ?? str(f.timeout) });
|
||||
return dropUndefined({
|
||||
kind: 'waitForAnimationToEnd',
|
||||
timeout: numLike(f.timeout) ?? str(f.timeout),
|
||||
});
|
||||
case 'StopAppCommand':
|
||||
return { kind: 'stopApp' };
|
||||
case 'RepeatCommand':
|
||||
return { kind: 'repeat', times: numLike(f.times) ?? str(f.times) ?? '' };
|
||||
case 'RetryCommand':
|
||||
return dropUndefined({ kind: 'retry', maxRetries: numLike(f.maxRetries) ?? str(f.maxRetries) });
|
||||
return dropUndefined({
|
||||
kind: 'retry',
|
||||
maxRetries: numLike(f.maxRetries) ?? str(f.maxRetries),
|
||||
});
|
||||
case 'RunFlowCommand':
|
||||
return { kind: 'runFlow', source: f.sourceDescription != null ? 'file' : 'commands' };
|
||||
case 'RunScriptCommand':
|
||||
@@ -335,7 +366,9 @@ function canonicalizeAgentCommand(
|
||||
return canonicalTap({
|
||||
longPress: false,
|
||||
repeat,
|
||||
delay: repeatIsNumber ? (numLike(command.delay) ?? AGENT_REPEAT_DELAY_MS) : numLike(command.delay),
|
||||
delay: repeatIsNumber
|
||||
? (numLike(command.delay) ?? AGENT_REPEAT_DELAY_MS)
|
||||
: numLike(command.delay),
|
||||
target: agentTarget(command.target, numLike(command.index), command.childOf),
|
||||
});
|
||||
}
|
||||
@@ -403,7 +436,10 @@ function canonicalizeAgentCommand(
|
||||
case 'repeat':
|
||||
return { kind: 'repeat', times: numLike(command.times) ?? str(command.times) };
|
||||
case 'retry':
|
||||
return dropUndefined({ kind: 'retry', maxRetries: numLike(command.maxRetries) ?? str(command.maxRetries) });
|
||||
return dropUndefined({
|
||||
kind: 'retry',
|
||||
maxRetries: numLike(command.maxRetries) ?? str(command.maxRetries),
|
||||
});
|
||||
case 'runFlow':
|
||||
return { kind: 'runFlow', source: command.include.kind === 'file' ? 'file' : 'commands' };
|
||||
case 'runScript':
|
||||
@@ -429,7 +465,9 @@ function agentTarget(
|
||||
}),
|
||||
};
|
||||
}
|
||||
return { point: { x: target.x, y: target.y, unit: target.space === 'percent' ? 'percent' : 'px' } };
|
||||
return {
|
||||
point: { x: target.x, y: target.y, unit: target.space === 'percent' ? 'percent' : 'px' },
|
||||
};
|
||||
}
|
||||
|
||||
function agentSelector(
|
||||
@@ -468,6 +506,14 @@ function agentGesture(gesture: MaestroSwipeGesture): CanonicalGesture {
|
||||
}
|
||||
}
|
||||
|
||||
function agentPoint(coordinate: { space: 'absolute' | 'percent'; x: number; y: number }): CanonicalPoint {
|
||||
return { x: coordinate.x, y: coordinate.y, unit: coordinate.space === 'percent' ? 'percent' : 'px' };
|
||||
function agentPoint(coordinate: {
|
||||
space: 'absolute' | 'percent';
|
||||
x: number;
|
||||
y: number;
|
||||
}): CanonicalPoint {
|
||||
return {
|
||||
x: coordinate.x,
|
||||
y: coordinate.y,
|
||||
unit: coordinate.space === 'percent' ? 'percent' : 'px',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,12 +37,16 @@ function gradle(args, options = {}) {
|
||||
const [cmd, baseArgs] = override
|
||||
? [override, []]
|
||||
: [process.platform === 'win32' ? 'gradlew.bat' : './gradlew', []];
|
||||
return execFileSync(cmd, [...baseArgs, '-p', HARNESS_DIR, '--no-daemon', '--console=plain', '-q', ...args], {
|
||||
cwd: HARNESS_DIR,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'inherit'],
|
||||
...options,
|
||||
});
|
||||
return execFileSync(
|
||||
cmd,
|
||||
[...baseArgs, '-p', HARNESS_DIR, '--no-daemon', '--console=plain', '-q', ...args],
|
||||
{
|
||||
cwd: HARNESS_DIR,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'inherit'],
|
||||
...options,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function sha256(filePath) {
|
||||
@@ -58,7 +62,8 @@ function verifyArtifacts(pin) {
|
||||
}
|
||||
for (const artifact of pin.artifacts) {
|
||||
const jarPath = resolved.get(artifact.coordinate);
|
||||
if (!jarPath) throw new Error(`Pinned artifact ${artifact.coordinate} was not resolved by Gradle.`);
|
||||
if (!jarPath)
|
||||
throw new Error(`Pinned artifact ${artifact.coordinate} was not resolved by Gradle.`);
|
||||
const actual = sha256(jarPath);
|
||||
if (actual !== artifact.sha256) {
|
||||
throw new Error(
|
||||
@@ -92,7 +97,9 @@ function writeFixture(name, pin, content) {
|
||||
|
||||
function main() {
|
||||
const pin = readPin();
|
||||
console.log(`Regenerating Maestro ${pin.version} conformance fixtures (${pin.commit.slice(0, 12)}).`);
|
||||
console.log(
|
||||
`Regenerating Maestro ${pin.version} conformance fixtures (${pin.commit.slice(0, 12)}).`,
|
||||
);
|
||||
|
||||
// Refresh corpus provenance first so a newly added flow is picked up.
|
||||
const manifest = writeManifest(pin);
|
||||
@@ -110,7 +117,9 @@ function main() {
|
||||
} finally {
|
||||
fs.rmSync(outDir, { recursive: true, force: true });
|
||||
}
|
||||
console.log('Done. Review the diff, then run `node --experimental-strip-types scripts/maestro-conformance/verify.test.ts`.');
|
||||
console.log(
|
||||
'Done. Review the diff, then run `node --experimental-strip-types scripts/maestro-conformance/verify.test.ts`.',
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -63,11 +63,16 @@ test('the seal rejects an edited capture (proof the check has teeth)', () => {
|
||||
|
||||
test('fixtures pin the reviewed upstream Maestro artifacts', () => {
|
||||
for (const fixture of [loadLayer1(), loadLayer2()] as Array<{ upstream?: unknown }>) {
|
||||
const upstream = (fixture as { upstream: { version: string; commit: string; artifacts: unknown } })
|
||||
.upstream;
|
||||
const upstream = (
|
||||
fixture as { upstream: { version: string; commit: string; artifacts: unknown } }
|
||||
).upstream;
|
||||
assert.equal(upstream.version, PIN.version, 'fixture must pin the reviewed version');
|
||||
assert.equal(upstream.commit, PIN.commit, 'fixture must pin the reviewed commit');
|
||||
assert.deepEqual(upstream.artifacts, PIN.artifacts, 'fixture jar SHAs must match pinned-upstream.json');
|
||||
assert.deepEqual(
|
||||
upstream.artifacts,
|
||||
PIN.artifacts,
|
||||
'fixture jar SHAs must match pinned-upstream.json',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -104,21 +109,33 @@ test('layer 1: every divergence is declared (no silent drift)', () => {
|
||||
for (const flow of flows) {
|
||||
const declared = FLOW_DIVERGENCES[flow.id];
|
||||
if (flow.classification === 'identical' || flow.classification === 'both-reject') {
|
||||
if (declared) problems.push(`${flow.id}: declared divergence but classified ${flow.classification}`);
|
||||
if (declared)
|
||||
problems.push(`${flow.id}: declared divergence but classified ${flow.classification}`);
|
||||
continue;
|
||||
}
|
||||
if (!declared) {
|
||||
problems.push(`${flow.id}: undeclared ${flow.classification}${flow.detail ? `\n ${flow.detail}` : ''}`);
|
||||
problems.push(
|
||||
`${flow.id}: undeclared ${flow.classification}${flow.detail ? `\n ${flow.detail}` : ''}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (declared.classification !== flow.classification) {
|
||||
problems.push(`${flow.id}: declared ${declared.classification} but classified ${flow.classification}`);
|
||||
problems.push(
|
||||
`${flow.id}: declared ${declared.classification} but classified ${flow.classification}`,
|
||||
);
|
||||
}
|
||||
if (flow.classification === 'we-reject' && !(declared.unsupported && declared.unsupported.length > 0)) {
|
||||
if (
|
||||
flow.classification === 'we-reject' &&
|
||||
!(declared.unsupported && declared.unsupported.length > 0)
|
||||
) {
|
||||
problems.push(`${flow.id}: we-reject entries must list the unsupported command(s)/option(s)`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(problems, [], `Undeclared or mismatched divergences:\n ${problems.join('\n ')}`);
|
||||
assert.deepEqual(
|
||||
problems,
|
||||
[],
|
||||
`Undeclared or mismatched divergences:\n ${problems.join('\n ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('layer 1: no stale divergence declarations', () => {
|
||||
@@ -138,7 +155,9 @@ test('layer 2: generated semantic vectors match live engine constants', () => {
|
||||
// Every reference-only vector must be an on-the-record deviation.
|
||||
for (const result of results) {
|
||||
if (result.status === 'reference-only') {
|
||||
const documented = DOCUMENTED_DEVIATIONS.some((d) => d.description.includes(result.id) || LAYER2_REFERENCE_ONLY.has(result.id));
|
||||
const documented = DOCUMENTED_DEVIATIONS.some(
|
||||
(d) => d.description.includes(result.id) || LAYER2_REFERENCE_ONLY.has(result.id),
|
||||
);
|
||||
assert.ok(documented, `reference-only vector ${result.id} must be a documented deviation`);
|
||||
}
|
||||
}
|
||||
@@ -181,7 +200,9 @@ test('bug class 4: settle default parses identically; ordering is a layer-3 diff
|
||||
// sleep-after-capture ordering is verified by the layer-3 differential scenario.
|
||||
const layer2 = loadLayer2();
|
||||
assert.ok(
|
||||
!layer2.constants.some((constant) => /settle/i.test(constant.id) && constant.id !== 'iosScreenSettleTimeoutMs'),
|
||||
!layer2.constants.some(
|
||||
(constant) => /settle/i.test(constant.id) && constant.id !== 'iosScreenSettleTimeoutMs',
|
||||
),
|
||||
'no upstream settle-loop constant exists to cross-check; keep this as layer 3',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -112,7 +112,10 @@ function agentParseProgram(file: string): MaestroProgram | null {
|
||||
}
|
||||
}
|
||||
|
||||
function agentParse(file: string): { status: 'parsed' | 'rejected'; commands?: CanonicalCommand[] } {
|
||||
function agentParse(file: string): {
|
||||
status: 'parsed' | 'rejected';
|
||||
commands?: CanonicalCommand[];
|
||||
} {
|
||||
const program = agentParseProgram(file);
|
||||
if (!program) return { status: 'rejected' };
|
||||
return { status: 'parsed', commands: canonicalizeAgentCommands(program) };
|
||||
@@ -121,7 +124,12 @@ function agentParse(file: string): { status: 'parsed' | 'rejected'; commands?: C
|
||||
function classifyFlow(fixtureFlow: Layer1Fixture['flows'][number]): FlowResult {
|
||||
const agent = agentParse(fixtureFlow.file);
|
||||
const upstreamStatus = fixtureFlow.status;
|
||||
const base = { id: fixtureFlow.id, file: fixtureFlow.file, upstreamStatus, agentStatus: agent.status };
|
||||
const base = {
|
||||
id: fixtureFlow.id,
|
||||
file: fixtureFlow.file,
|
||||
upstreamStatus,
|
||||
agentStatus: agent.status,
|
||||
};
|
||||
|
||||
if (upstreamStatus === 'rejected') {
|
||||
return {
|
||||
@@ -208,7 +216,10 @@ export function agentKindsByCorpus(): Set<string> {
|
||||
return kinds;
|
||||
}
|
||||
|
||||
function collectKinds(commands: Array<{ kind: string; commands?: unknown }>, into: Set<string>): void {
|
||||
function collectKinds(
|
||||
commands: Array<{ kind: string; commands?: unknown }>,
|
||||
into: Set<string>,
|
||||
): void {
|
||||
for (const command of commands) {
|
||||
into.add(command.kind);
|
||||
const nested = (command as { commands?: Array<{ kind: string }> }).commands;
|
||||
@@ -246,7 +257,9 @@ function report(): void {
|
||||
}
|
||||
console.log('\n### layer 2');
|
||||
for (const result of checkLayer2()) {
|
||||
console.log(` ${result.status.padEnd(14)} ${result.id} upstream=${result.upstream} agent=${result.agent ?? '-'}`);
|
||||
console.log(
|
||||
` ${result.status.padEnd(14)} ${result.id} upstream=${result.upstream} agent=${result.agent ?? '-'}`,
|
||||
);
|
||||
}
|
||||
console.log('\n### coverage gaps');
|
||||
for (const result of checkCoverage()) {
|
||||
|
||||
@@ -56,9 +56,11 @@ test('no mutation shard is allowed to exceed the 30-minute budget', () => {
|
||||
});
|
||||
|
||||
test('every kernel path a PR can touch selects the affected mutation job', () => {
|
||||
const paths = [...workflow('mutation-affected.yml').matchAll(/^ {6}- "(?<glob>[^"]+)"$/gm)].map(
|
||||
(match) => match.groups!.glob,
|
||||
);
|
||||
// Quote style is the formatter's business (oxfmt formats the workflow tree), so
|
||||
// accept either spelling of the same scalar rather than pinning this gate to it.
|
||||
const paths = [
|
||||
...workflow('mutation-affected.yml').matchAll(/^ {6}- (?<q>['"])(?<glob>[^'"]+)\k<q>$/gm),
|
||||
].map((match) => match.groups!.glob);
|
||||
for (const module of KERNEL_MODULES) {
|
||||
for (const owned of module.owns) {
|
||||
const selected = paths.some(
|
||||
|
||||
@@ -33,12 +33,16 @@ function toMarkdown(run: RunResult): string {
|
||||
lines.push(`- **Finished**: ${run.finishedAt}`);
|
||||
lines.push('');
|
||||
lines.push('All times in milliseconds. `wall-clock` includes process spawn + socket overhead;');
|
||||
lines.push('`daemon` is the batch step round-trip (spawn overhead ≈ wall-median − daemon-median).');
|
||||
lines.push(
|
||||
'`daemon` is the batch step round-trip (spawn overhead ≈ wall-median − daemon-median).',
|
||||
);
|
||||
lines.push('`elements` = node count in the snapshot payload (tree-size proxy).');
|
||||
lines.push('An untimed warmup interaction runs after each open/relaunch, so measured commands');
|
||||
lines.push('do not pay the one-time iOS-runner startup or post-relaunch first-AX-query cost.');
|
||||
lines.push('');
|
||||
lines.push('| command | cli | mode | n | wall min | wall median | wall p95 | wall max | daemon median | elements | notes |');
|
||||
lines.push(
|
||||
'| command | cli | mode | n | wall min | wall median | wall p95 | wall max | daemon median | elements | notes |',
|
||||
);
|
||||
lines.push('|---|---|---|---|---|---|---|---|---|---|---|');
|
||||
for (const m of run.measurements) lines.push(measurementRow(m));
|
||||
lines.push('');
|
||||
@@ -49,7 +53,9 @@ function toMarkdown(run: RunResult): string {
|
||||
lines.push('');
|
||||
for (const m of failed) {
|
||||
const sample = m.samples.find((s) => !s.ok);
|
||||
lines.push(`- **${m.label}** — ${m.notes.join('; ')}${sample?.errorMessage ? ` — ${sample.errorMessage}` : ''}`);
|
||||
lines.push(
|
||||
`- **${m.label}** — ${m.notes.join('; ')}${sample?.errorMessage ? ` — ${sample.errorMessage}` : ''}`,
|
||||
);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
+6
-1
@@ -1,7 +1,12 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { parseConfig, REPO_ROOT, usesSourceCli } from './config.ts';
|
||||
import { runScenario, setupIsolation, teardownIsolation, type IsolationContext } from './harness.ts';
|
||||
import {
|
||||
runScenario,
|
||||
setupIsolation,
|
||||
teardownIsolation,
|
||||
type IsolationContext,
|
||||
} from './harness.ts';
|
||||
import { writeReports } from './report.ts';
|
||||
import type { RunResult } from './types.ts';
|
||||
|
||||
|
||||
@@ -19,7 +19,10 @@ if (typeof expectedName !== 'string' || expectedName.length === 0) {
|
||||
if (typeof expectedVersion !== 'string' || expectedVersion.length === 0) {
|
||||
fail('package.json must define version.');
|
||||
}
|
||||
if (typeof server.description === 'string' && server.description.length > registryDescriptionMaxLength) {
|
||||
if (
|
||||
typeof server.description === 'string' &&
|
||||
server.description.length > registryDescriptionMaxLength
|
||||
) {
|
||||
fail(`server.json description must be ${registryDescriptionMaxLength} characters or fewer.`);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,10 +53,7 @@ export function classifySlowTest(params: {
|
||||
}
|
||||
|
||||
/** Render the gate outcome; returns true when the run must fail. */
|
||||
export function reportSlowTests(
|
||||
offenders: Offender[],
|
||||
write: (message: string) => void,
|
||||
): boolean {
|
||||
export function reportSlowTests(offenders: Offender[], write: (message: string) => void): boolean {
|
||||
if (offenders.length === 0) return false;
|
||||
const sorted = [...offenders].sort((a, b) => b.durationMs - a.durationMs);
|
||||
const line = (o: Offender): string =>
|
||||
@@ -71,7 +68,7 @@ export function reportSlowTests(
|
||||
}
|
||||
if (failing.length === 0) return false;
|
||||
write(
|
||||
`\nSlow-test gate: ${failing.length} test(s) exceeded ${ENFORCE_FACTOR}x the wall-clock budget.\n` +
|
||||
`\nSlow-test gate: ${failing.length} test(s) exceeded ${ENFORCE_FACTOR}x the wall-clock budget.\n` +
|
||||
`Tests must not wait real time — inject the timeout/poll budget or assert the budget is\n` +
|
||||
`wired instead of waiting it out (docs/agents/testing.md). If the runtime cost is genuinely\n` +
|
||||
`irreducible, document the reason in the owning test or move it out of the unit lane.\n` +
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
runtime: "typescript"
|
||||
runtime: 'typescript'
|
||||
|
||||
@@ -23,8 +23,9 @@ export default withCallstackPreset(
|
||||
theme: {
|
||||
content: {
|
||||
outlineCTAHeadline: 'Curious about developing mobile apps with AI agents?',
|
||||
outlineCTADescription: 'We can help you take your agentic workflows to the next level and ship faster.',
|
||||
outlineCTAButtonText: "Book a call",
|
||||
outlineCTADescription:
|
||||
'We can help you take your agentic workflows to the next level and ship faster.',
|
||||
outlineCTAButtonText: 'Book a call',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user