mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* test(mobile): record the golden corpus through the page bridge (OTA phase C, C0.5) `ScriptedRpcTransport` gains one seam: an optional `wrapClient`, applied between the recorder's own instrumentation and the logical client, so a recording can be driven through another transport without the runner learning which one. `runRecording` passes it through. The instrumentation moves outside the seam. `ordinal` orders a logical `sendRequest` against the recording's device writes and physical payloads, and the operation makes that call at the same moment with or without a wrapper; stamping it under one times the wrapper's forwarded send instead, which the unwrapped recording has no counterpart for. Measured: 131 goldens move their sender ordinal under the seam, none above it. The name a physical send is filed by therefore becomes a queue rather than one slot, taken on the inside of the wrapper — a wrapper that forwards on a microtask arrives after the next logical call has been made, and one slot hands both sends the second name. Underflow throws; a wrapper that drops a send is a finding. `rpc-recording-through-bridge.test.ts` is the harness: every golden recorded again with `BridgeRpcClient` over an in-memory port pair to a `createBridgeHost` holding the scripted client, compared body for body against the committed file. One FIFO per direction, delivery on a microtask, and the `init` handshake delivered in place before anything mounts. It is opt-in behind `RPC_FOUNDATION_BRIDGE=1` and does not pass yet. 391 of 787 goldens diverge, for four causes that are findings about the bridge rather than about the corpus; the suite's header names each one with its count, and flipping the gate is one line once they close. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * chore(mobile): repin the recording corpus and refresh its headers (OTA phase C, C0.5) The C0.5 seam edits `scripted-rpc-transport.ts` and `run-recording.ts`, both inside `RECORDER_DIRECTORY`, so `recorderSha256` moves and every golden's header names an engine that no longer exists. `baseline` moves with it, from1e3795de99toddbb194585, which is main's tip and the tree these were recorded against; the recorder's fence is clean at that commit, tracked and untracked alike. Recorded whole rather than in part, as the README requires: a partial refresh would leave the corpus pinned to two different trees. The delta is header-only, and that is checked rather than asserted. All 787 goldens changed exactly two lines each, and across the corpus and the manifest the only keys that moved are `baseline` and `recorderSha256`, to one value each. No observation moved, which is the claim the pair of digests exists to make: the engine changed, what it observes did not. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): correct the bridged-parity counts to what the landed tree measures The counts in the harness's header were read off an intermediate run: 391 is the divergence with the reply schema's `_meta` requirement already widened, not the divergence this tree has. Against the tree as it lands, 763 of 787 goldens diverge and 24 replay byte-identically; widening `_meta` is worth 372 of them and takes the count to 391. The same intermediate run is behind a wrong sentence in this lane's first commit message, which says no sender ordinal moves with the instrumentation above the seam. Seven do, plus six payload ordinals, and they are the subscribe reorder cause 3 now names. 131 was the count below the seam. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): repartition the bridged-parity classes from a measured run The header's last two counts were read off an intermediate measurement. Classified from the failure text of a run with the `_meta` arms widened, the 391 that remain partition 345 / 33 / 13, not 340 / 13, and the ordinal class is not a reorder on the wire: the page posts its frames in call order and they publish in that order, while the writes the operation makes above the bridge land a delivery earlier. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): let the shared bridge port pair carry any shell client The golden recorder needs the pair the bridge tests already have, holding its own scripted client rather than the fake, delivering the handshake in place so a screen can mount in the same turn, and able to answer one counterfactual: what the page would have done had the shell posted a field it does not. Every other test keeps the shape it had under `createFakeBridgePortPair`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): catch a wrapper that swallows a send instead of mislabelling the next The seam took the next name off the queue without checking it belonged to the method now on the wire, so a wrapper that rejected without forwarding left its name behind and the following payload was filed under it. Underflow already threw; this is the other half, and it is the half that is silent. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin the bridged-parity partition by rule instead of by reading it Counting the classes by eye is how two of them drifted. Every diverging golden is now named by a rule over the frames and the scenario, each name is disjoint, and a golden that fits none of them fails the run. The first class needs a second replay with `_meta` supplied, because nearly every golden is refused some reply for that field and only supplying it says which ones the field explains: 372 / 338 / 7 / 33 / 13 over 787, with 24 byte-identical. CI runs it so the counts cannot drift again. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * chore(mobile): repin the recording corpus and refresh its headers The seam's name check and the export the parity suite reads both sit inside `recorderSha256`, and the port pair the suite now borrows sits inside the recorder's fence, so the pin moves to this branch's tip and all 787 headers follow. Two lines per golden, both of them header: baseline and recorderSha256, and no body byte anywhere. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep the recorder's own boundary ratchets green Two of them were red, and the first has been since the seam landed, because that commit was checked against the recorder's directory instead of the whole mobile suite. The engine may not import a suite, so the parity classifier moves beside the recorder rather than inside it, where the golden digest would also stop claiming a recording's provenance for a rule that only reads failures. And the raw-port ceiling for the scripted transport rises from five to seven: the seam needs one layer between the operation's call and the logical client, and its two references are what that layer costs. A named argument type takes back the third. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * chore(mobile): repin the corpus onto the ratchet fixes and refresh its headers Supersedes the refresh two commits back: the scripted transport moved again, so both the digest and the pin do. Two lines per golden, both header, no body byte anywhere. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): stamp the counterfactual on replies only, not on events An `event` frame carries a `payload` too, so keying off the key rather than the type put `_meta` into subscription bytes. The page reads an event payload as `z.unknown()`, so nothing refused it and the classes did not move, but the tool that names a divergence must not author one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
197 lines
8.3 KiB
YAML
197 lines
8.3 KiB
YAML
name: Mobile Checks
|
|
|
|
on:
|
|
pull_request:
|
|
types:
|
|
- opened
|
|
- synchronize
|
|
- reopened
|
|
- ready_for_review
|
|
paths:
|
|
- 'mobile/**'
|
|
# Mobile launch contracts exercise the real host dispatcher and durable receipt store.
|
|
- 'src/main/agent-launch/**'
|
|
- 'src/main/runtime/rpc/**'
|
|
- 'src/main/runtime/runtime-rpc/**'
|
|
- 'src/main/runtime/runtime-rpc.ts'
|
|
- 'src/main/runtime/device-registry.ts'
|
|
- 'src/main/runtime/orca-runtime.ts'
|
|
- 'src/main/runtime/agent-session-*.ts'
|
|
- 'src/main/native-chat/agent-session-wire/**'
|
|
- 'src/shared/agent-launch-*.ts'
|
|
- 'src/shared/agent-session-*.ts'
|
|
- 'src/shared/new-workspace/worktree-create-collision.ts'
|
|
# Why: the mobile terminal link parsers are conformance-tested against
|
|
# these shared fixtures; desktop-side fixture edits must re-run this suite.
|
|
- 'src/shared/terminal-file-link-conformance.ts'
|
|
# Why: mobile imports the negotiated capability names directly and records
|
|
# the whole capability read verbatim in its goldens, so a capability added
|
|
# desktop-side rewrites a mobile fixture and must re-run this suite.
|
|
- 'src/shared/protocol-version.ts'
|
|
# Why: mobile's rpc-params-contract.ts is a type-only re-export of the
|
|
# generated params catalog, and mobile/tsconfig.json includes **/*.ts. A
|
|
# schema edit anywhere under here changes mobile's types, so a desktop-only
|
|
# change can break mobile's typecheck with no other mobile signal.
|
|
- 'src/shared/rpc-contract/**'
|
|
# Why: this job holds the only checks that load the Fastfile, so edits to
|
|
# it or to the release workflow it guards must re-run them.
|
|
- '.github/workflows/mobile.yml'
|
|
- '.github/actions/install-node-dependencies/**'
|
|
- '.github/workflows/mobile-ios-release.yml'
|
|
# Why main too: a behaviour-change branch legitimately pins its own last fenced commit, and that
|
|
# commit only stops being reachable when the branch squash-merges. The pull_request run cannot
|
|
# see that; this one is where the pin guard finds it.
|
|
push:
|
|
branches:
|
|
- main
|
|
paths:
|
|
- 'mobile/**'
|
|
- '.github/workflows/mobile.yml'
|
|
|
|
concurrency:
|
|
# Per commit on main, not per branch. GitHub cancels any PENDING run in a group when a new one
|
|
# queues, whatever `cancel-in-progress` says, so one shared main group drops the middle merge of
|
|
# three -- and a pin that breaks there is exactly what this workflow now checks for.
|
|
group: mobile-${{ github.event.pull_request.number || github.sha }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
verify:
|
|
if: github.event_name == 'pull_request'
|
|
runs-on: ubuntu-latest
|
|
|
|
env:
|
|
# Why: an unfrozen bundler silently re-resolves when Gemfile.lock drifts
|
|
# from the Gemfile, which is how the release jobs could land on different
|
|
# fastlane versions in the first place. Fail here instead.
|
|
BUNDLE_FROZEN: 'true'
|
|
|
|
defaults:
|
|
run:
|
|
working-directory: mobile
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
with:
|
|
cache-dependency-path: |
|
|
pnpm-lock.yaml
|
|
mobile/pnpm-lock.yaml
|
|
|
|
# bundler-cache installs mobile/Gemfile.lock, so this job is also what
|
|
# proves the pinned fastlane the release workflow depends on still
|
|
# resolves — before a release run finds out.
|
|
- name: Setup Ruby and fastlane
|
|
uses: ruby/setup-ruby@v1
|
|
with:
|
|
ruby-version: '3.3'
|
|
bundler-cache: true
|
|
working-directory: mobile
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
- name: Typecheck
|
|
run: pnpm typecheck
|
|
|
|
# Why a ratchet and not the raw typecheck: mobile/tsconfig.json excludes test files, so until
|
|
# tsconfig.test.json existed nothing checked them, and at introduction 127 of the 632 had
|
|
# drifted. This fails when a test file that checks today stops checking, when a test leaves
|
|
# the program, and on @ts-nocheck; the baseline may only shrink.
|
|
- name: Typecheck tests (ratchet)
|
|
run: pnpm run check:tests-typecheck
|
|
|
|
- name: Test
|
|
run: pnpm test
|
|
|
|
# Why a second run of the same corpus: `pnpm test` leaves this suite off, because it replays
|
|
# every golden through the page bridge and the per-class counts it pins are the only thing
|
|
# that says how far that bridge is from byte-identical. It fails when a class grows or when a
|
|
# divergence lands in no class at all, so a change that widens the gap cannot land quietly.
|
|
# ~2.5 min locally, because a golden that diverges is replayed a second time with `_meta`
|
|
# supplied and that counterfactual is what separates the reader's share of the gap from the
|
|
# rest. Ungated on purpose: unlike the pin guard's reproduce, this verdict moves on any change
|
|
# to the bridge, which no path filter on the corpus would catch.
|
|
- name: Replay the recording corpus through the page bridge
|
|
env:
|
|
RPC_FOUNDATION_BRIDGE: '1'
|
|
run: pnpm exec vitest run src/test-support/rpc-recording/rpc-recording-through-bridge.test.ts
|
|
|
|
- name: Test iOS release version resolution
|
|
run: ruby fastlane/ios_release_version_test.rb
|
|
|
|
- name: Test TestFlight lane arguments
|
|
run: ruby fastlane/fastfile_testflight_arguments_test.rb
|
|
|
|
# Why: nothing else in CI loads the Fastfile, so a syntax error, a broken
|
|
# require, or an undefined constant only surfaces mid-release — the
|
|
# ios-distribute job failed every run for six days that way. `lanes` just
|
|
# loads and lists, so it needs no App Store Connect credentials and makes
|
|
# no network calls to Apple.
|
|
- name: Smoke-check the Fastfile
|
|
env:
|
|
FASTLANE_SKIP_UPDATE_CHECK: '1'
|
|
FASTLANE_OPT_OUT_USAGE: '1'
|
|
run: bundle exec fastlane lanes
|
|
|
|
- name: Lint
|
|
run: pnpm lint
|
|
|
|
- name: Check formatting
|
|
run: pnpm format:check
|
|
|
|
recording-pin:
|
|
name: RPC recording pin
|
|
runs-on: ubuntu-latest
|
|
|
|
defaults:
|
|
run:
|
|
working-directory: mobile
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
with:
|
|
# The ancestry verdict is read straight off history. On a shallow checkout
|
|
# `git merge-base --is-ancestor` answers from grafted parents, so the guard refuses to
|
|
# answer at all rather than reporting a pass it has no evidence for -- and the pinned tree
|
|
# below has to be checkable out.
|
|
fetch-depth: 0
|
|
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
with:
|
|
cache-dependency-path: |
|
|
pnpm-lock.yaml
|
|
mobile/pnpm-lock.yaml
|
|
|
|
- name: Install dependencies
|
|
run: pnpm install --frozen-lockfile
|
|
|
|
# Seconds. No `--ref`, so the pin is judged against the same tree it was read out of. On a
|
|
# pull request that is the merge preview, which already carries main's repins; judging the
|
|
# branch head instead fails every branch cut before the day's repin, and its instruction would
|
|
# tell the author to pin their own head -- creating the break this guard exists to catch. A
|
|
# branch that pins its own commit passes here and fails on the push after the squash, which is
|
|
# where the pin actually leaves the history.
|
|
- name: Check the recording pin is reachable
|
|
shell: bash
|
|
run: pnpm exec tsx scripts/rpc-recording-pin-guard.mts ancestry
|
|
|
|
# ~2 min locally for the record itself, so it is gated rather than run twice over. A pull
|
|
# request that moves none of the corpus, the manifest or the recorder cannot move this
|
|
# verdict away from the one the base commit already published, and `verify` replays the
|
|
# corpus against the branch tree in the meantime. A push to main has no `verify` job and is
|
|
# where a squash lands a spliced corpus, so there it always runs.
|
|
- name: Reproduce the corpus from the pinned tree
|
|
shell: bash
|
|
env:
|
|
PIN_GUARD_BASE: ${{ github.event.pull_request.base.sha }}
|
|
run: |
|
|
if [ -n "$PIN_GUARD_BASE" ]; then
|
|
pnpm exec tsx scripts/rpc-recording-pin-guard.mts reproduce --if-changed-since "$PIN_GUARD_BASE"
|
|
else
|
|
pnpm exec tsx scripts/rpc-recording-pin-guard.mts reproduce
|
|
fi
|