mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
t3code/cleanup-claim-tests
82 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1e50f9672a |
fix(daemon): take a foreign device claim the device's own reboot invalidated (#2570)
* fix(daemon): take a foreign device claim the device's own reboot invalidated An open that found a claim belonging to another session gave up even when the device had rebooted since that claim was taken, leaving the surface unreachable for every session. A reboot already took the app and the runner away, so the claim guarded nothing. Ask the device when its current boot began and release a foreign claim whose stamp predates it. The stamp is the last instant the owner vouched for the device, renewed by every open that reaches it, including the one that boots the device on the way in, so an owner that boots the device for its own work keeps it and only an owner that never came back loses it. Co-Authored-By: Claude <noreply@anthropic.com> * chore(gates): classify the daemon edge that asks a device when it booted Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
4f5a87b6b3 |
refactor(command-registry): move CLI flag grammar, text and command aliases down (#2561)
Move the vocabulary that both the CLI and commands read but no command's runtime depends on into the package below both: flag types, registry, groups and the four flag-definitions files, command-text, and cli-command-aliases. These are pure moves; only their import specifiers change. No compat re-export at the old paths — every consumer switches to the owning subpath. The per-command defaults stay where they are for now (the daemon's edge into the facet resolver is the harder cut and belongs with the daemon-closure work). Part of #2545 / #2543. |
||
|
|
da76aa4f1e |
refactor(commands): declare project-config admission and recorder sanitization on the flag declaration (#2453)
* refactor(commands): declare project-config admission and recorder sanitization on the flag declaration Move the two fail-closed flag properties — may a key be set from a project `agent-device.json`, and does the session recorder copy it into `SessionAction.flags` — off the hand-maintained allowlists and onto each `FlagDefinition` as required `projectConfig` / `recorded` fields. Omitting either is now a type error, so the compiler holds the fail-closed property a list held by omission. - 156 declarations carry both fields; the 6 screenshot-specific definitions carry them too. Populated to match the old sets exactly (one-off diff empty: 85 project-config and 39 recorded keys, byte-for-byte). - `cli-config.ts` and `session-action-recorder.ts` derive their sets from the registry and no longer list keys; `RECORDED`/`PROJECT_CONFIG` derivations recomputed per call so a consumer builds its set at its own module load. Recorder reaches the derivation through the `cli-schema/command-schema.ts` seam (daemon may not import `commands/`). - Planted-divergence tests, per #2421: flipping one declaration's field moves the admission/sanitization outcome through the production derivation, plus a compile-time pin that an incomplete declaration does not build. - `docs/agents/cli-flags.md` now points at the declaration fields, not the allowlist. Refs #2445 * refactor(commands): return the recorded keys as a set, matching project-config Both derivations answer the same question — the set of flag keys a surface admits — so both return ReadonlySet<FlagKey>. Drops a needless set-then-spread on the recorder path; consumers already iterate the value. Refs #2445 * fix(commands): keep the CommandFlags guard on recorded flag declarations The deleted `SANITIZED_FLAG_KEYS` was `satisfies readonly (keyof CommandFlags)[]`, so every recorded key had to be a `CommandFlags` key. The derived set returns `FlagKey` and the recorder indexed it through a cast, so `recorded: true` on a CLI-only key (`daemonAuthToken`, `help`, …) compiled and could leak an uncarrable value into a recorded action. State the constraint on the declaration: `FlagDefinition` is a union that locks `recorded` to `false` for a `NonRecordableFlagKey = Exclude<FlagKey, keyof CommandFlags>`. `recordedFlagKeys()` returns `ReadonlySet<RecordableFlagKey>` via a narrowing predicate, so `sanitizeFlags` drops its cast. Adds a `@ts-expect-error` test that a CLI-only key cannot opt into recording. Refs #2445 |
||
|
|
dbfebd4e8f |
refactor(cli-schema): derive the flag tail of usage synopses (#2456)
A command that hand-wrote its synopsis had to restate every option it accepts inside that string, which is the last restatement left on the help surface after #2421 made the flag declaration own the option itself. A synopsis is now grammar plus a generated `[label]` tail, and the two rendering rules live on the declaration rather than per command: - the tail names an option with its declared `usageLabel`, alias included, the token the `Command flags:` section already shows; - `usageHidden: true` keeps a cross-cutting opt-in out of every synopsis; `--record` is the one today, and it stays under `Command flags:`. `usageFlags` is where a command states that its synopsis names fewer options than it accepts: `[]` for a synopsis that is pure grammar or writes its own mutually-exclusive brackets, otherwise the subset it names. `Command flags:` still documents everything in `allowedFlags`. Adding an option to a command therefore updates `--help` on its own, except where the command said its synopsis stays short. `snapshot` and `proxy` drop their override; `daemon`, `device`, `doctor`, `prepare`, `tv-remote`, `scroll` and `artifacts` drop the flag brackets from theirs. Guards fail a tail that names an option the command does not accept, or one the hand-written grammar already wrote. Every synopsis except `snapshot` and `is` is byte-identical; those two move exactly per the rules above, and the canonical `snapshot` docs line follows the generator. Closes #2444 |
||
|
|
fef0b12cc5 |
chore: hoist shared snapshot/selector test fixtures into a single canonical location (#2419)
* chore: hoist shared snapshot/selector test fixtures into @agent-device/selectors PR #2397 left two copies of the snapshot-state builder and duplicated geometry/touch-point arbitraries (root's src/__tests__/test-utils/ and the package's internal/__tests__/), because packages cannot import root src/. Move the canonical versions into a new @agent-device/selectors/test-fixtures subpath and have both root and the selectors package import from it, leaving buildNodes and the root-only replay/gesture arbitraries in place. Fixes #2402 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USabYbQjD16A2UkkvMpf5x * chore: exempt test-fixtures.ts's test-only arbitraries from dead-code check PROPERTY_RUNS, scrollingContainerTypeArb, distinctRectPairArb, and interactionTouchPointScenarioArb are consumed only by *.test.ts files, which Fallow's --production analysis does not see, matching the existing pattern for other workspace-package symbols reached only from the test tree. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USabYbQjD16A2UkkvMpf5x * chore: consolidate makeSnapshotState into capture-kit, rename fixtures file An adversarial review of the #2402 fixture-hoisting change found a third copy of makeSnapshotState in packages/capture-kit/src/snapshot-state.fixtures.ts, predating PR #2397. Since @agent-device/selectors already depends on capture-kit, make capture-kit's copy canonical (exported as ./snapshot-state-fixtures) and have the selectors package's fixtures module re-export it instead of duplicating it a third time. Also rename packages/selectors/src/test-fixtures.ts to snapshot-geometry.fixtures.ts (subpath ./snapshot-geometry-fixtures) to match every other test-fixture module's *.fixtures.ts convention in this repo, which lets it fall under .fallowrc.json's existing blanket **/*.fixtures.ts dead-code exemption instead of needing a bespoke per-symbol entry. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USabYbQjD16A2UkkvMpf5x --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
0da105e3c3 |
docs: simplify agent context and resolve conflicting guidance (#2287)
* docs: clarify agent task scope and validation * docs: remove redundant and conflicting agent guidance |
||
|
|
e882cf9723 |
feat(runtime): add managed-local ownership and the exact-only managed runtime (#2258)
* docs: trim the CONTEXT.md glossary within the guidance byte budget
CONTEXT.md sat at 11,992 of its 12,000-byte guidance budget, so no new domain term could be added
without first paying for it.
- Condense eighteen definitions that had grown past one line (platform leaf, command surface,
runtime use, runner command traits, interactor, coordinate-first resolved element activation,
parent-owned touch point, guarantee cell, delegation-on-error, ref frame, snapshot producer,
snapshot policy facet, capture hint, regular presented-depth frontier, clip fold,
AX-unavailable target invalidation, Maestro program, Maestro observation generation). The
definitions keep their meaning; only the elaboration is gone.
- Move the five test-harness terms of 'Providers and tests' (provider-backed integration
scenario, provider transcript, scenario transcript, in-process provider scenario harness, HTTP
contract test) to docs/agents/domain.md, which AGENTS.md already routes to for domain
vocabulary. None of them names a concept a command or a wire shape carries, and none appears in
a test name.
CONTEXT.md is 10,517 bytes after this pass.
* feat(runtime): add the managed-local owner kind, device-claim rule, and managed binding fence
ADR 0021 foundations, unit 1. Nothing registers a managed local owner yet, so every arm below is
reached from tests only; the point of the unit is that the arms exist and fail closed.
- `RuntimeOwnerRef` gains `{ kind: 'managed-local'; instance }` with `managedLocalRuntimeOwner`:
one owner per allocator instance, family-agnostic because the device carries its family. Every
owner-kind discrimination becomes an exhaustive switch, so a fourth kind is a type error at each
site: the owner key, the unavailable-facts provider mode, the durable envelope decode, and the
gateway's provider-mode acceptance and exact-owner selection.
- `deviceClaimRuleForOwner` ('ordinary' | 'allocator-held' | 'none') in the new leaf
src/daemon/device-claim-rule.ts replaces the boolean `isLocalDeviceClaimTarget`. Both claim
gates switch on it, and the admission gate now evaluates it under every device-claim policy: the
`transient-exclusive` condition moved inside the ordinary arm, so a managed owner is verified
where an ordinary owner would never have touched the store.
- `requireAllocatorHeldDeviceClaim` (src/daemon/device-claim-allocator.ts) is the one read-only
verifier both gates consult. It never acquires, never locks and never clears; in this unit it
can only answer `binding-invalid`, `missing`, or `conflict`, because no allocator-held claim
kind exists until unit 2. `allocatorHeldAdmissionError` answers each outcome with its own
refusal through an exhaustive switch, so an outcome the verifier learns to produce is a
compile error until it is answered.
- A missing allocator-held claim refuses with COMMAND_FAILED / `allocator-claim-missing`,
`retriable: false`. It is deliberately not a `DeviceClaimConflictReason`: replay retries every
conflict reason as infrastructure, and a managed identity no allocator activated is permanent.
- `managedBindingFence` / `decodeManagedBindingFence` encode `[requesterId, identityIncarnationId]`
as the fence token and the request generation as its generation, so two requesters on one
identity incarnation never share a fence. The ids are fenced verbatim, and the decoder accepts a
token only if it re-encodes to itself.
- Claim admission now receives the binding intent the gateway bound, so an exact-owner fence
reaches the gate unchanged. Session open still binds ordinarily and passes an ordinary intent:
a managed local owner is therefore refused there structurally, and the Host open route replaces
that intent when it lands.
- CONTEXT.md: managed local owner, device-claim rule, managed binding fence, request generation,
identity incarnation.
* fix(daemon): decide allocator-held admission totally instead of by an optional error
`allocatorHeldAdmissionError` returned `AppError | undefined`, so its switch without a default
was never exhaustiveness-checked: TS2366 fires only when the return type excludes `undefined`,
`noImplicitReturns` is off, and oxlint has no exhaustiveness rule. A verifier outcome nobody
answered would therefore fall out as `undefined`, which both gates read as an admission — claim
admission throws nothing and session open proceeds to open the session on a device it never
verified.
Replace it with `decideAllocatorHeldAdmission`, returning
`{ admitted: true } | { admitted: false; error }`. The return type excludes `undefined`, so
dropping an arm is now a compile error at the switch, and a gate asks whether the outcome was
admitted rather than whether an error happened to come back. `buildAllocatorHeldRefusal` and the
admission gate are projections of that one decision.
* docs: restore the meaning five CONTEXT.md definitions lost in the trim
The condensing pass shortened these five past the point where they still said what they meant:
- Capture hint said 'presented depth' where the term is 'regular presented depth', which is what
Regular presented-depth frontier is measured against; the short form read as a different axis.
- Clip fold lost both that the interpreter runs inside presentation for every backend and that a
platform difference may not enter as a backend exception. Those are the whole rule.
- Snapshot policy facet lost the process boundary that makes it host-side at all: runner-side
Swift presentation stays separate.
- Runner command traits lost 'independently of the public command surface', which is what
distinguishes them from the command surface.
- Delegation-on-error said 'settles', and Settled observation makes 'settle' a term of its own.
CONTEXT.md is 11,674 of its 12,000-byte budget.
* docs(daemon): correct the claim-gate and managed-owner comments
- The claim-gate docstring claimed there is no other way to obtain device operations. That is
true of command handlers, but two daemon-owned recovery paths bind outside the seam:
application-lifecycle-recovery.ts (ordinary intent, daemon shutdown) and
durable-capture-runtime-recovery.ts (exact-owner intent read back from a durable envelope,
which this unit makes able to carry a managed local owner). Name them instead of claiming
coverage the seam does not have.
- The open path's comment described a session executing under an allocator-held claim, a state
this route cannot produce. Say what the `{ kind: 'ordinary' }` literal actually is: the truth
of a route that binds ordinarily, which the Host open route replaces with the request's exact
intent when it lands.
- Name U3 as the unit that fills the exact-owner selection arm, rather than the whole ADR.
* fix(runtime): accept transport-composed facts for a managed owner
providerModeMatchesOwner's managed-local arm accepted mode === 'local' only, but
selectExactOwner's managed-local arm loads the device's local family owner through the same
loadLocal a local-family owner uses, so it inherits that owner's provider modes verbatim. A
managed binding over a transport-composed local device (e.g. a remote ADB or web-provider
transport) would fail bindingContractFailure's facts check and be rejected as an owner/facts
mismatch. Accept the same local-family modes the local-family arm already does; still
unreachable until U3 registers the exact-only owner, which is where the binding regression
test that pins this lives.
* feat(runtime): register the managed local owner as an exact-only wrapper and add the neutral allocator port (#2259)
* feat(runtime): register the managed local owner as an exact-only wrapper and add the neutral allocator port
ADR 0021 foundations, unit 3. Unit 1 added the `managed-local` owner kind and left the gateway's
exact-owner arm for it failing closed; this unit gives that arm a registry and the owner it selects.
Nothing in production registers a managed owner yet, so both are reached from tests only.
- `createComposedPlatformRuntimeGateway` gains a `managedOwners` list that only the `managed-local`
arm of `selectExactOwner` reads. `selectOrdinaryProvider`, `inspectFacts` and the ordinary `bind`
arm never see it, and `providerModules` pairs one provider-runtime owner with one
`ProviderDeviceRuntime`, so ordinary selection cannot reach a managed owner by construction
rather than by a check. A duplicate instance is refused at composition.
- The wrapper (src/platform-runtime-managed-owner.ts, root zone, no platform imports) binds only
under an exact-owner intent naming itself, loads the device's own family owner through the
gateway's loader, delegates with an ordinary intent — a family owner refuses a foreign exact
owner — and republishes the binding under the managed owner. It does not read the fence: what a
managed binding fence proves is the device-claim gate's business. `ownsDevice` returns false.
- Twenty cells are withheld as `owner-capability-missing`, enumerated by mechanics rather than by
catalog group: the four device-lifecycle cells, the four application cells that boot or shut the
device down (`prepareApplicationOpen`, `prepareAppleRunner`, `closeApplication`,
`finalizeApplicationClose`), and the twelve durable-capture cells, which a managed binding could
never reattach because the family runtime stamps envelopes with its own local owner. The
operations are then filtered by those facts, so an operation cannot outlive its own fact.
- `@agent-device/contracts/managed-device-allocation` is agent-device's own allocator port: lease
request, lookup, supersession, cancellation, renewal, release, activation confirmation, identity
status, removal acknowledgement, and the typed environment projection. Types only, named to match
the allocator's published contract so the two sides cannot drift, with no dependency on any
allocator package. Its only implementation is a scripted fake under `*.fixtures.ts`.
- Budgets: the new contracts entry surface is a one-module closure; the `src/platform-runtime.ts`
hub moves 47 -> 48 for the wrapper, whose own value imports were already in that closure.
* fix(runtime): withhold the deployment cells from a managed binding and trim the allocator port
Review findings on the managed local owner.
- `deployApp` and `deployMaterializedApp` join the lifecycle group. Both family deployment runtimes
ensure device readiness before installing, and `deployAppUse` requires `deployApp` alone — so
`install` on a managed binding would have booted the allocator's device with nothing to refuse
it. Twenty withheld cells become twenty-two, and the refused-uses test covers `deployAppUse`.
- The wrapper's doc comment no longer implies that withholding cells is a complete lifecycle
exclusion: several retained Apple cells (screenshot capture, settings, clipboard, application
launch) boot the simulator lazily inside the family runtime, where cell selection cannot reach.
That is the same class as the pre-binding readiness path, and closing it is a family-runtime
change.
- `readLeaseEnvironment` leaves the allocator port. It was beyond the vocabulary the contract
fixes, and it made the scripted fake carry a real parser whose only test passed with every
production line reverted. `ManagedLeaseEnvironment`, `ManagedLeaseEnvironmentKey` and
`LeaseEnvironmentError` stay as types; the reader that produces them lands with the unit that
first turns a grant into a device.
- CONTEXT.md drops an operation enumeration that was already incomplete.
* fix(runtime): withhold the lazily-booting Apple system and screenshot cells
Screenshot capture, settings, clipboard and application launch were retained on a managed
binding even though their Apple family-runtime implementations can boot the simulator lazily
below cell-selection granularity (screenshot's shutdown-failure retry boot; settings, clipboard
and application launch each resolve a local interactor the same way). That preserves rather than
blocks the exact bypass ADR-0021 section 3's hard boundary names: managed lifecycle/readiness
belongs to the allocator, and no handler path may fall back to direct lifecycle tooling.
Withhold captureScreenshot, setSetting, readClipboard, writeClipboard and openApplication
alongside the existing withheld groups. The wrapper's doc comment now names the pre-binding
readiness gap explicitly as the same class of follow-up, rather than folding it into a retained-
cells caveat that no longer applies. MANAGED_RETAINED_OPERATION moves to tapPoint, the cell the
fixture-based regression tests now use to prove something survives the wrapper.
* chore: retrigger CI (stale synchronize event after rebase)
* fix(runtime): lazy-load the managed owner wrapper to satisfy the eager-closure no-growth gate
Main's eager-closure budget gate (the merge-base ratchet) replaced the hand-tracked
HUB_BUDGETS map with an automatic no-growth-vs-merge-base check: src/platform-runtime.ts
is a hub with no growth allowed at all, not a number bumped by hand with a justifying
comment. The static import of createManagedLocalRuntimeOwner in platform-runtime-gateway.ts
added one module to that hub's closure (47 -> 48), which now fails
scripts/__tests__/eager-closure-budgets.test.ts outright rather than needing a manual bump.
Move the value import into loadManaged's dynamic `await import`, matching how the rest of
this file's owner loaders defer their leaf modules. Only the managed-local arm reaches this
path, so an ordinary bind never pays for it, same as before -- the wrapper module itself was
simply the wrong side of the eager/lazy line.
|
||
|
|
9941330dcc |
docs(agents): PR diff budget, move PRs, gates commit, validation lifecycle (#2247)
* docs(agents): PR diff budget, move PRs, gates commit, validation lifecycle * docs(agents): merge-base diff budget, subprocess lane facts, gate-stage validation * docs(agents): keep volatile test topology at its declaration |
||
|
|
fbf6097700 |
chore(gates): drop the test-file size pin map, keep the merge-base ratchet (#2238)
The exact-length pin map duplicated what the merge-base already records and made every shrink a two-file edit. The gate now has one rule: a test file over the 1,000-line tripwire may be no longer than at the merge-base with origin/main, and no new test file may cross the tripwire. |
||
|
|
db08548026 | refactor: enforce src/utils retirement (#2149) (#2229) | ||
|
|
b643d0f761 | docs: clarify technical issue requirements (#2226) | ||
|
|
34e8cbb7a2 |
docs+ux: make device ownership discoverable end to end (#2165)
* docs+ux: make device ownership discoverable end to end Complete the #1320 agent experience so 'busy? -> inspect -> choose or release' is discoverable from every surface an agent actually reads: - devices now projects the blocking claim owner per row (claimedBy with session and workspace, observe-policy projection; provably dead owners are excluded because the next open replaces them automatically), so an agent told a device is busy can pick a free one from the same listing. - help debugging gains a 'Device busy and ownership' section separating the two DEVICE_IN_USE flavors and their exact recoveries. - AGENTS.md documents both flavors; docs/agents/device-verification.md retires the last ps/kill recovery guidance in favor of device status, daemon stop --state-dir, and device release --stale (Stage 5 of #1320). - ADR-0010 no longer calls DEVICE_IN_USE 'the only retriable code' without naming the claim path's non-retriable override. - The rendered cross-worktree claim error gains a help-conformance quiz case binding (sample-output-device-claim-inspects-owner). - README points at device status / device release --stale. Part of #1320. * fix: key ownership projection by canonical device identity end to end Review findings on #2165: - blockingClaimOwnersByDevice keyed claims and inventory rows by bare device.id, so a live Android claim could project claimedBy onto an unrelated same-id Apple/Harmony/Vega row, with scan order picking the displayed owner. Both sides now use the canonical local device key (claim.deviceKey against canonicalLocalDeviceKey of the row's claim identity). The cross-family same-id regression was observed red against the bare-id keying. - The projection is now asserted across every hop the PR promises: client normalization preserves well-formed claimedBy and drops malformed ones, and the devices CLI formatter carries it through JSON data and renders the text line (MCP shares the same serialization). |
||
|
|
e832325e87 |
refactor(substrate): split host mechanics into @agent-device/host-kit capability ports (#2088)
* refactor: split generic host mechanics into @agent-device/host-kit (#2082 W1) The shared src/utils closure that blocked the platform-family moves lands on declared owners: generic host mechanics form a new private @agent-device/host-kit package between kernel and capture-kit, and capture-kit keeps capture, snapshot, and recording behavior, depending on host-kit for the mechanics it needs. tar-stream and yauzl move with the archive code. Every seam's exported subpaths are pinned in package-boundaries.test.ts, the layering model ranks the new zone, R13's allow-list names it, and each seam carries an exact eager-closure row. ADR-0019's substrate amendment describes the layout. Tests that mocked two of the moved modules separately became duplicate same-seam vi.mock factories, where the second silently replaced the first; those are merged, and the mocks that production code reaches past are pinned at their injection points instead. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * refactor(host-kit): one narrow capability port per export The four technical barrels (exec/fs/values/request) grouped by category rather than by capability, so a consumer needing one mechanic evaluated unrelated ones. Each export is now a single capability over the host machine: command, process, diagnostics, retry, archive, file, request, version. A port re-exports only what a consumer of that capability uses, and every port carries its own eager-closure row. Most of the old values barrel was never host mechanics. Pure record readers, config-source values, result text, memoization, async scoping, coordinate validation, and device-scope parsing touch no process, file, or environment, so they join kernel's other primitives instead. Closures fall accordingly: capture-kit's png-worker-client from 20 to 10, png-resize from 28 to 18, session-teardown from 79 to 68, and the CLI from 386 to 380. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * chore: drop the migration inventories and trim the touched comments Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * docs: trim the touched host-kit and mutation-lane comments Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * docs: keep tool directives only in the touched files Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * docs: keep tool directives only across the touched tree Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * fix: point the Swift parity comment at the real TS twin and test The W1 move rewrote this citation to packages/contracts/src/mobile-snapshot-semantics.ts, which does not exist: the module went to capture-kit while isTapPointInsideViewport itself went to packages/contracts/src/snapshot-visibility.ts. The TS test line was left pointing at the pre-move path. Both now resolve. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH * fix: repoint comment citations at the homes this refactor moved them to The W1 move left ~20 comment citations pointing at src/utils/*.ts and src/request/*.ts paths that no longer exist. Each now names the capability port that owns the symbol, which survives further file moves: exec -> host-kit/command host-process, owner-identity -> host-kit/process diagnostics -> host-kit/diagnostics atomic-file, process-lock -> host-kit/file retry -> host-kit/retry request progress/cancel -> host-kit/request version -> host-kit/version ttl-memo, source-value, parsing, device-isolation, keyed-lock, success-text -> kernel subpaths Comment-only; no closure, budget, or behavior change. ADR citations are left as written, being dated records of the decision rather than live references. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VngeKZH6zBuJzNBk5YzUH --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
7b48531d3b | refactor: retire ADR-0019 cutover scaffolding (#2081) | ||
|
|
bf26ab14d6 |
refactor(commands): one audience table for common input fields (#2074)
"Who may write this input field, on which surface" was expressed three times,
each a separate name-keyed mechanism: `retiredField()` in the command field
maps, `ALWAYS_HIDDEN_FIELDS` in the AI SDK adapter, and
`OPERATOR_INPUT_GUIDANCE` / `CONFIG_LOADER_GUIDANCE` at the MCP admission
boundary -- twelve hand-written refusal sentences keyed by name, far from the
fields they govern.
The root cause was that the ~19 shared common fields existed only as parallel
enumerations by name -- `commonProperties()`, `readCommonInput()`,
`commonToClientOptions()`, and the `CommonCommandInput` type -- carrying no
metadata, so any policy about a field forced a new name-keyed map elsewhere.
Declare each common field once, in `commands/common-input-fields.ts`, keyed by
its input key and carrying `{ schema?, read?, clientKey?, audience? }`. The JSON
schema, the readers, the client-options projection, and the model-facing
audience boundary all derive from that one table, and `satisfies Record<keyof
CommonCommandInput | 'target', ...>` makes a row without a field, or a field
without a row, a type error in both directions.
`audience` is the unified vocabulary (`commands/input-audience.ts`): `operator`
keys stay in the CLI and Node schemas but are hidden from and refused by every
model-facing tool schema; `retired` keys are absent from every schema yet still
recognized, so they answer with migration guidance. `retiredField()` now sets
`audience: 'retired'`, metro's `bearerToken`/`proxyBaseUrl` declare
`audience: 'operator'` at the field, and `stateDir` declares it in the new
`mcp/tool-control-fields.ts` beside the other MCP-only tool arguments. Refusal
guidance is rendered from each declaration's operator path -- env var names via
`buildPrimaryEnvVarName`, the operator config file, or an explicit sentence --
rather than hand-written per key, and `OperatorInputSource` is shaped so a
declaration naming no path at all does not typecheck.
`#2076`'s nested-step admission recurses through the same derived
`findInadmissibleInput`, so a batch step's refusals come from this audience map
rather than a second filter; its suite passes against this unchanged.
A field-level audience only reaches the boundaries through its command's
metadata, so that wiring is closed structurally rather than by convention:
`inputAudience` is required on `CommandMetadata`, and
`defineFieldCommandMetadata` -- which now takes an optional custom reader, so
`batch` and `gesture` go through it too -- is the only construction path for a
field-map command. At the boundary, a command's own audiences merge before the
global operator classifications, so an `operator` key outranks a colliding
per-command `retired` one and a name collision fails closed.
`command-input.ts` was 705 lines and over the 300-line target; the record
readers move to `commands/input-readers.ts` so the table can use them without an
import cycle. `click`/`press`/`fill` move onto `defineFieldCommandMetadata` --
they were that helper inlined.
`COMMON_COMMAND_SUPPORTED_FLAG_KEYS` stays hand-maintained: it is the CLI
parser's axis, and 25 of its 42 keys never become structured command input while
the table's `cwd` and `debug` are not flags. The reasoning is recorded above the
constant.
Purely internal: `listCommandTools()`, the CLI command schemas, and every
command `inputSchema` are byte-identical, verified by diffing the serialized
surfaces before and after.
Refs #2027
|
||
|
|
71214e11da |
refactor(runtime): close residue execution units (#2054)
* refactor(runtime): close residue execution units * refactor(runtime): address residue ownership review * fix(runtime): preserve viewport diagnostic log path |
||
|
|
a904ef0d5d |
fix(fuzz): run parser cases in a worker process, not the runner's thread (#2053) (#2055)
The unit-lane corpus replay executed adversarial parser cases on worker threads of the Vitest worker running the test file. A fault in a worker thread ends its whole process, so a case that faulted killed the test runner: `[vitest-pool]: Worker forks emitted error / Worker exited unexpectedly`, with no test, file, or case named. Six of six Coverage deaths before #1994's split were this one file out of ~1100, and the uninstrumented second leg it created then lost the same file six more times in three days. Cases now run in a worker *process*. The two faults a case cannot report about itself are both classified from outside it: a case that never returns is a `hang` (unchanged), and one that ends the process it runs in is a new `crash` failure carrying the exit code or signal and the tail of the worker's stderr — the death certificate the lane used to lose. A sixth self-check target seeds that kind, so a regression in reporting it fails the harness self-check like every other kind. |
||
|
|
80a3fdc79c | perf: raise local vitest worker cap to four (#2049) | ||
|
|
67b813c55b |
fix(web): launch npm and the managed backend through node, not .cmd shims (#2033)
On Windows every `--platform web` command failed with `spawn EINVAL`: the managed backend resolved to `node_modules/.bin/agent-browser.cmd` and was spawned with `shell: false`, which Node refuses for `.bat`/`.cmd` since the CVE-2024-27980 fix. `web setup` failed earlier still — a bare `npm` is not spawnable on Windows, where npm ships as `npm.cmd`. `runManagedAgentBrowser` is now the only path that executes the backend. Entry resolution, the Node runtime, the managed environment, and the spawn all live behind it, so setup, doctor, and the provider cannot reintroduce the shim. The entry comes from the installed package's declared `bin` rather than a hard-coded path, which is the part of this worth being precise about. npm is untouched on macOS and Linux, which were never broken: setup still spawns `npm` from PATH. Only Windows resolves npm's own `npm-cli.js` — from an `npm_execpath` that really is npm's launcher, else the copy bundled beside `node` — and fails with the existing actionable TOOL_MISSING when neither is there. Setup also pins `--no-global` so an ambient `npm_config_global` cannot redirect the install out of the managed prefix. The published status shape is unchanged: `binaryPath` still names npm's console shim, now informational rather than the spawned command, and `entryScript` plus `packageDir` are additive. Closes #2022 Claude-Session: https://claude.ai/code/session_01LMS3BidXb3F4HSr26vvQmG Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
dbc4f2f955 |
chore(test): start the subprocess-stub kill-criterion experiment (#1823) (#2007)
Deletes the serialized `subprocess-stub` Vitest project and drops SUBPROCESS_STUB_TESTS from unit-core's exclude, so its two real spawners (client-metro.test.ts, harness.test.ts — corpus-replay.test.ts already left for fuzz-worker in #1994) run un-serialized in the default forks pool per #1823's own kill criterion. Revert if a timeout-shaped failure shows up before 20 consecutive CI runs pass clean. The files stay excluded from the mutation lane (SERIALIZED_TESTS): that exclusion is about mutant-rerun cost, independent of Vitest project structure. Updated the comments/docs/scripts that described the old project by name so none of them assert a project that no longer exists. Claude-Session: https://claude.ai/code/session_015YPgKE1xmjdqh7T1q987DA Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
104fe75248 |
fix(ci): run the fuzz corpus replay outside the coverage lane (#1994)
The Coverage job intermittently ends with no failing test and one file's
results missing:
Test Files 1070 passed (1071)
Errors 1 error
Error: [vitest-pool]: Worker forks emitted error.
Caused by: Error: Worker exited unexpectedly
This is shape (B) of #1824 — the half #1854 did not fix. Scanning every
failed Coverage job across the 120 CI runs since #1854 merged finds the
signature five times, and the vanished file is
scripts/fuzz/corpus-replay.test.ts all five (six for six with #1866's
occurrence): 23% of Coverage failures in that window, ~4% of all CI runs.
The ~40s gap before the error is coverage report generation, not test
time — the pool surfaces its AggregateError only once every task settles.
Control, from a green attempt of the same run: the file passes in 3152ms
at 09:37:35.9 and the summary prints at 09:38:12.5. So the file is not
slow in CI, nothing else is in flight when it dies, and neither a missed
per-case budget nor STARTUP_BUDGET_MS is implicated. Partial test counts
(3/11 and 9/11 reported) place the death mid-file, inside runCases.
So the corpus replay gets its own serialized project that the coverage
run skips, and a second uninstrumented Vitest invocation in
`test:coverage:ci` runs it, keeping the tests on every PR. Measured
against two full runs, this costs zero coverage: the cases execute in
worker threads, a separate isolate the fork's inspector never
instruments, so the lines reported are identical with and without it.
Membership is by demonstrated failure, not by a property of the code:
`session-replay-runtime-maestro.test.ts` also constructs a
node:worker_threads Worker and stays in unit-core, instrumented and
green, so "nests a Worker" is explicitly not the criterion.
The second leg goes through `test:fuzz-worker`, which blanks
AGENT_DEVICE_COVERAGE_SHARD and AGENT_DEVICE_COVERAGE_MERGE. ci.yml sets
those as job-level env over a single `gate: unit-ci` step, so both legs
would otherwise inherit them and the shard would die: Vitest refuses
`--shard=1/2` over this one-file project, and the blob reporter
overwrites the instrumented shard's report on its way out. Verified on
the merged tree — shard 1/2 (549 files), shard 2/2 (548), and the merge
job (1097 files, 90.38% lines) all pass, and the leg still fails without
the blanking.
Refs #1824
|
||
|
|
d713988c5a |
docs(agents): simplify testing and pull-request guidance wording (#1997)
* refactor(lint): replace the facade import scan with a lint rule The surviving half of `contracts-entry-closure.test.ts` walked ~490 candidate files and parsed each one to assert that nothing value-imports the two wide contracts facades. `eslint/no-restricted-imports` already states exactly that, and `allowTypeImports` already draws the one distinction that made the walker seem necessary: `import type` is erased, so it stays legal. Verified rather than assumed, because the override semantics are not additive: a same-rule override REPLACES the parent, so a top-level rule would have been silently dropped for `src/**`, and the existing `"off"` entry for `exec.ts` and the test tree would have exempted the files that carried most of the cost #1959 removed. So the paths are added per zone, and the blanket `"off"` becomes a facade-only config that keeps the `node:child_process` exemption it existed for. Planted red in all three zones — `src/core/capabilities.ts`, a `src/__tests__` file, and `packages/capture-kit/src` — each flagged, while a type-only import in the same probe file was not. A first probe read as a pass because the sed that built it produced a type-only import; the zone was re-probed with a real value import rather than trusting the green. Misconfiguration fails loudly, which is why this is safe to rely on: a typo'd rule name makes oxlint exit 1 with "Rule not found in plugin", not pass silently (the failure mode #1976 records for the `rg` assertions). What a linter cannot replace, and stays: the eager-closure budgets. Those are a transitive-weight property — a module already imported grows an import, and the cost arrives without any single file's import list changing. Per-file rules cannot see that, and `no-restricted-imports` can only ban specifiers named in advance, which is precisely what #1950/#1956/#1959 could not have named. * docs(agents): simplify testing and pull-request guidance wording testing.md sat 15 bytes under the 10k per-doc check:agent-guidance cap. Rewrite both docs in shorter, plainer sentences without dropping any fact, threshold, or identifier (backtick-identifier sets verified unchanged against the previous revision). Also fix testing.md's gate catalog sentence being separated from its code block and the missing blank line before pull-requests.md's Reviewing section. |
||
|
|
c4b1a6131e |
dx(test): opt-in worker-count override for solo local vitest runs (#1964)
* dx(test): opt-in worker-count override for solo local vitest runs resolveVitestMaxWorkers() caps local runs at 2 workers so parallel worktrees and spawn-heavy tests keep headroom, but a solo run that owns the machine pays 6x on a 12-core host for no benefit. Add AGENT_DEVICE_VITEST_MAX_WORKERS to opt in to a higher cap. It is clamped to os.cpus().length so a runaway value can't oversubscribe the host, and it is a no-op in CI (CI already derives its own worker count). A missing, blank, non-numeric, non-integer, or non-positive value falls through to the existing default cap rather than throwing. Default (unset) behavior is unchanged. Closes #1962 * docs: tighten the worker-override note to fit the agent-guidance budget docs/agents/testing.md sits at a 10,000-byte per-file ceiling enforced by check:agent-guidance, and the first phrasing pushed it to 10,065. Restate the override in one tighter bullet that leads with the "solo run only" caveat, which is the constraint a reader most needs. * fix(test): clamp the worker override with os.availableParallelism() Node documents cpus().length as unfit for sizing application parallelism: it ignores CPU affinity and cgroup limits, so it can report a pool wider than the process may actually use. Clamping against it would inflate the very ceiling this override's safety clamp exists to enforce. availableParallelism() honors those constraints, so the clamp now means what it claims on constrained hosts. Test updated to match. * test: keep the resolver cases in the already-included setup test Review feedback: a new test file beside the resolver, plus its entry in vitest.config.ts's unit-core include list, is a change to test discovery that the mutation lane's `vitest related` graph reads. Fold the override cases into src/__tests__/hermetic-env-setup.test.ts, which is already in the unit suite and already imports the resolver, and drop the config edit entirely so this PR no longer touches test discovery at all. Same six assertions, no coverage lost. |
||
|
|
cb65d6ca1f |
refactor(tests): replace the test-utils barrel with direct module imports (#1956)
* refactor(tests): replace the test-utils barrel with direct module imports The barrel re-exported 13 modules, so every importer evaluated all of them (store-factory alone drags 16 daemon session-store files; property-arbitraries drags fast-check). Importing the backing modules directly cuts the unit suite's aggregate eager module evaluations from 153,401 to 144,344 (-5.9%), measured with the eager-import-closure walker. Deleting the barrel makes the tax unrepresentable instead of pinning it with a guard test. * docs(testing): point fixture guidance at the test-utils modules, not the deleted barrel * test: extract replay session fixture |
||
|
|
aed00aef42 | docs: capture gesture verification lessons (#1913) | ||
|
|
40e4b0dd3e |
docs(agents): restore and enforce progressive disclosure (#1888)
* docs(agents): restore and enforce progressive disclosure * test(maestro): pin typed selector fallback signal * docs(agents): address progressive disclosure review * docs(agents): restore orphaned traps and close guidance-gate bypasses - AGENTS.md: skills carry a minimal start/routing card; command semantics stay in versioned CLI help (the skills contract enumerates two skills by hand, so prose retains ownership for the rest) - testing.md: restore the two local-only XCTest snags CI never hits (unsigned-bundle policy refusal signature + first-run automation permission) - scripts/gate/routing.ts: record GitHub's 300-changed-file path-filter limit at the paths-ignore assertion it bounds - agent-guidance-contract.test.ts: recurse docs/agents so nested guidance cannot evade the byte budgets while the gate stays green |
||
|
|
dd2a18ed4d |
perf(check-affected): stop running coverage locally, CI stays authoritative (#1908)
* perf(check-affected): stop running coverage locally, CI stays authoritative The `coverage` gate re-ran the affected Vitest suite under instrumentation on every `check:affected --run`, adding real overhead for signal the dedicated `Coverage` CI job already enforces on every PR. Mark it GitHub-authoritative and let `vitest-related`/`unit`/`provider-integration` run locally on their own instead of being folded into a coverage pass. Also removes the now-dead dedupe machinery in run.ts that existed only to support the local coverage-instrumented run. * fix: keep affected tests fast and bounded |
||
|
|
80b4769230 |
test(fuzz): structured CLI/Maestro generators that reach command validation and assert error codes (#1781 B2) (#1866)
* test(fuzz): structured CLI/Maestro generators that reach command validation and assert error codes (#1781 B2) * test(fuzz): pin the rediscovered #1433 excess-positional case and keep numeric flag samples inside their range * style: apply oxfmt to the new fuzz modules * perf(fuzz): derive the CLI validation surface lazily so unrelated harness paths keep their startup * test(fuzz): resolve validation generators in the run path so corpus replay keeps its small module graph * test(fuzz): weight the CLI budget toward command validation, pin the finite classes as seeds, guard lazy surface derivation * docs(testing): describe the validation lane's layer split, seed-pinned classes, and PR-time gates * refactor(fuzz): split the validation generator into CLI and Maestro modules, mirrored in tests * refactor(fuzz): collapse the flag-shaped mutation classes and seed literals, derive class coverage from declarations * fix(fuzz): hash every case-generation module in configHash, guarded by an import-closure test * test(fuzz): assert CLI command and flag-key coverage against the registry, and close the six gaps it found |
||
|
|
393eb30a28 |
ci: give check:affected real Apple ownership rules and route ios.yml on them (#1781 A9-2) (#1857)
* ci: give check:affected real Apple ownership rules and route ios.yml on them (#1781 A9-2) Device-lane ownership by platform family in the affected selector (scripts/check-affected/device-lanes.ts): a TypeScript-only Apple change now carries replay-ios/replay-ios-device/replay-macos in a narrow plan, other families own only their own lanes, shared runtime surface owns every lane, unit tests own none. Golden tables (contracts/fixtures) own the parity unit test and both runner builds instead of failing open. ios.yml pull_request paths-ignore is routed on that ownership; the gate manifest asserts the list against the selector over every tracked path both ways (scripts/gate/routing.ts, ROUTED_LANES). push to main is unfiltered. Path coverage exempts declared manual-only checks the way owned does. * ci: tighten routing assertion shape (fallow: unused exports, complexity) * ci: name parked checks in check:affected --run skips * ci: bound the routed-lane exemption to sibling workflows (review of #1857) The exact-name .github exemption was unbounded: naming the lane's own setup-apple-runner-build or boot-ios-test-simulator action skipped the lane that runs them and the manifest stayed green. Lane now carries the transitive composite-action closure plus its own workflow file (Lane.uses, same walk declaredGates does), and the exemption refuses anything in it. Also: an unowned path under an ignored root (a non-TS fixture under a family root) asked for the ignore entry to be removed, which would un-route every sibling in that tree; it now asks for a selector owner. Both cases pinned, both proven red against the pre-fix code. Documents GitHub's 300-changed-file path-filter limit in docs/agents/testing.md. * ci: close the routed-lane exemption over composite-action support files Lane.uses recorded only each composite action's action.yml, so a support file the descriptor executes was exemptible as if it were an unrelated sibling workflow: ios.yml uses setup-fixture-app, whose action.yml runs "$GITHUB_ACTION_PATH/fetch-artifact.sh", and that script runs its siblings resolve-artifact-name.sh and trusted-artifact.mjs — references that exist only inside shell, one level past anything YAML parsing sees. The closure unit is the action's directory now. It needs no shell model and cannot miss a file however deep the reference chain runs; the coarseness is harmless because a file in an action's own directory belongs to that action. All three files pinned, red against the descriptor-only closure. |
||
|
|
ef2094c9d7 |
docs: keep size review in CI and local feedback fast (#1842)
* feat(size): measure a base ref in one command (pnpm size --base <ref>) The Size workflow already compares base and PR builds; locally that needed a manual checkout, install, build, --json, and --compare dance, so budgets were negotiated late. --base <ref> does the workflow's recipe in a detached worktree under .tmp/size-base/<sha> (kept for reuse, other bases pruned) and compares against it: first run ~1-2 min, later runs against the same base ~3s. Documents the local caveat: npm tarball/unpacked rows compare a fresh base against a working tree that may carry locally built helper artifacts. * feat(tooling): pnpm pr:evidence — one paste-ready, SHA-stamped evidence block for PR bodies Composes what the repo already measures instead of hand-transcribing it after every rebase: exact merge-base and head, changed-file areas, the affected selector's plan (local vs GitHub-authoritative, fail-open summarized), the layering guard verdict, depgraph counts with a real delta against the base (a throwaway git worktree, no install — the script analyzes its cwd while its imports resolve from this checkout), and, behind flags, the changed-line coverage table and pnpm size --base. It claims nothing about CI: the last line links the head's checks. ~20s default tier. The pure model (grouping, report parsing, rendering) has node:test coverage registered as the pr-evidence-model gate, run in the Affected-check Selector job next to the selector it reads. * fix(tooling): pr:evidence measures pristine head/base worktrees from an os.tmpdir scratch; size --base gets a per-SHA lock, completion stamp, and non-destructive eviction Review (three P1s): - pr:evidence created its scratch under an untracked .tmp/ that a fresh checkout lacks (ENOENT). Scratch now lives under os.tmpdir(), which exists by construction; a real entrypoint regression runs the whole pipeline with --base HEAD (no origin/main needed) and asserts JSON shape plus cleanup of both worktrees and the scratch. - Untracked or uncommitted production files could move the layering/depgraph numbers the block labels as HEAD's. Head is now measured from a pristine worktree of the head commit exactly like base, and the affected plan takes the head SHA (the literal HEAD folds the working tree in). The dirty flag now counts untracked files and says they are not in the block. - size --base force-pruned other cached bases without locking and trusted a dist/src that could be half-built. Per-SHA .lock (pid, O_EXCL) held from before the worktree exists until the base report is read; a live lock on the same base fails fast, a stale one is replaced; eviction skips worktrees whose lock owner is alive; dist/.size-base-complete marks a finished build. Orchestration tests run the real script against a throwaway git repo with pnpm/npm shimmed on PATH (build once, reuse, live lock, stale lock, interrupted build, guarded vs idle eviction). Also fixes the /tmp → /private/tmp realpath mismatch those tests surfaced (git lists worktrees by real path, so the registration check removed a live worktree). * fix(tooling): symlink-identity locks with compare-then-unlink; evict under the victim's lock; pr:evidence registers worktrees on add and cleans up exhaustively Review (three P1s): - Lock creation/takeover races: the lock is now a symlink whose target is the owner identity (pid:nonce), created with its identity in one syscall (no empty-file window), taken over only by compare-then-unlink on the exact identity judged stale, and verified after creation; release unlinks only a link that still names this run. Real overlapping-process tests: two runs on one base (exactly one builds, the other fails fast), and a takeover race against a simulated other taker across delays straddling the acquire window (a live lock is never unlinked, both never proceed). - Cross-base eviction: a victim is removed only while holding its own lock, acquired through the same path, so a run wanting it after the check finds it locked rather than half-removed; a live-locked victim is skipped. - pr:evidence worktrees: withWorktrees registers each worktree the moment its add succeeds and sweeps every resource on the way out, collecting failures instead of stopping at the first; planted reds for both (second add fails → first removed; removal of the middle one throws → the others still go). * test(size): serialize the size --base orchestration file with the other real spawners Caught running the full unit suite on the rebased branch: the file passed in isolation but intermittently failed under broad file parallelism, where it took 14s versus ~5.5s alone. It spawns node scripts/size-report.mjs per case, which spawns git and the shimmed package managers under it — the SUBPROCESS_STUB_TESTS class exactly (starved spawns surface as a vitest test timeout instead of the orchestration assertion the case is about), so it joins that serialized project with its spawn named at the entry, per docs/agents/testing.md. No rerun layer is involved: the flake is removed, not retried. Two full-suite runs green after. * refactor(size): extract the base-cache claim protocol and make stale takeover atomic Review (P1 + architecture): Stale-claim removal was compare-then-unlink (readlink then unlink; lstat then rm for a stray file), so another taker could replace the observed entry with its live claim between the two syscalls and this run would delete the replacement. Removal now happens only while holding the entry's takeover mutex — an atomically created directory — and re-verifies the claim inside it. A replacement can appear only by creating one on a free path (the abandoned claim occupies it until the unlink) or by another takeover (needs the mutex), so removal cannot delete a replacement. A mutex leaked by a process killed inside its sub-millisecond critical section is reclaimed by age, and even a wrong reclamation is contained: both takers re-verify inside, and the winner is still decided by the atomic symlink() that follows. The protocol moves out of size-report.mjs into scripts/size-base-cache.mjs (AGENTS.md: extract past 500 LOC) — 719 → 536, with the entry lifecycle (claim → evict others → ensure worktree → build if unstamped → measure → release) owned by the module behind withPreparedBaseWorktree. Mirrored tests in scripts/__tests__/size-base-cache.test.ts plant every dangerous interleaving directly on the filesystem: replacement-after-observation, a takeover held by another run, age reclamation, release-after-retarget, and a stray non-symlink. They need no subprocess and run in 9ms, so the raced single-process case was dropped from the orchestration file, which keeps only what real processes can show. Planted red: removing the mutex makes the contended case delete the claim it must not touch. * ci(size): preserve the reporter's whole module graph, and gate that it stays whole The Size workflow measures the base commit with the PR's reporter, so it copies the reporter out of the tree before checking the base out. Extracting size-base-cache.mjs made the reporter a two-file graph while the step still copied one file, and the base measurement died with ERR_MODULE_NOT_FOUND — after every deterministic gate had passed, because nothing local reproduces that copy. The step now copies the scripts directory, so a further split cannot leave an import behind, and size-report-preserved-closure.test.ts holds it to the reporter's real relative-import closure and to running the preserved copy rather than the checked-out tree. Planted red: restoring the single-file copy fails both cases, naming scripts/size-base-cache.mjs. Verified by running the reporter from a copied directory exactly as the workflow does. * fix(size): the takeover mutex has one holder for life; split report publishing out of the reporter Review (P1 + architecture): Age-based reclamation of the takeover mutex reintroduced the split ownership the mutex exists to prevent: a holder that is merely slow — paused or SIGSTOPed past any threshold — could have its mutex force-removed and replaced, putting two takers inside the supposedly exclusive section, where either could unlink the claim the other had just created; the unconditional pathname-based release could also delete the replacement mutex. The mutex is now a symlink naming its holder, created in one syscall, never reclaimed at any age, and released only by the run that owns it. A mutex leaked by a process killed inside a three-syscall critical section wedges one cache entry with the path to clear in the message, rather than silently deleting another run's live claim. Planted red: restoring age reclamation displaces a day-old delayed holder, which the new case pins. Publishing the report to a PR is a separate question from measuring and formatting it, so it moves to scripts/size-report-comment.mjs with the marker and retry policy it owns; its existing regression drives it through the real script unchanged. scripts/size-report.mjs is 386 LOC — under the 500 tripwire and below the 512 it had on base. * test: prove delayed size cache holder is preserved * docs: keep size review in CI |
||
|
|
d07b837621 |
test: classify the runner XCTests — pure decisions to a macOS host lane, simulator semantics gated os(iOS) (#1781 A7) (#1861)
Every declared AgentDeviceRunnerUITests method now belongs to a lane, and the #if guard is the classification: AGENT_DEVICE_RUNNER_UNIT_TESTS alone means a pure runner decision (runs on the macOS host on every PR — ci.yml's existing compile job now executes the bundle it builds), '&& os(iOS)' means runner/XCTest semantics (simulator lanes only). check:xctest-selection evaluates the guards per platform, derives each lane's reach, and fails on a flagged identifier that is undeclared or uncompiled on that lane, on a declared test no lane reaches (found the two tvOS-only tests, dark since birth — widened to os(tvOS) || os(macOS)), and on testCommand reaching any lane. The host and nightly lanes assert executed == derived reach, so a missing -D flag or a guard that compiles a file out reads red, not as a smaller green. One duplicate test deleted (sparse-verdict assertions folded into its twin). |
||
|
|
e4c3b420a4 |
test: refuse foreign-pid signals from unit-test workers (Coverage fork death, #1824) (#1854)
* ci: w3-1824 experiment — trace fork signals and plant pid sentinels in the Coverage job
Temporary instrumentation for #1824. Every vitest fork logs each real
process.kill it sends to a foreign pid (and every kill/pkill it spawns);
the Coverage job parks sentinel processes on the pids the Apple runner
tests fabricate (4141/4242/4343/4444) and reports which of them survive
the run. Reverted before this PR leaves draft.
* test: refuse foreign-pid signals from unit-test workers
A vitest worker may signal only itself and the processes it spawned.
src/__tests__/hermetic-signal-setup.ts records any other process.kill,
answers it with ESRCH (so best-effort kill paths proceed as if the pid
were dead), and fails the sending test by name in afterEach.
The senders this catches today are the Apple runner tests, which
fabricate runner child pids (4242, 4141, 4343, 4444) and mocked the
liveness reads in host-process.ts but not the signal writes:
killRunnerProcessTree delivered real SIGINT/SIGTERM/SIGKILL to those
pids and their process groups — 146 signals per run of
runner-session.test.ts. On the CI runner the sibling vitest forks live
in that pid band, so the Coverage job periodically lost one fork
mid-file with no test attributed (issue #1824, 6 of the last 40 red CI
runs).
The group-signal write moves behind signalProcessGroupBestEffort in
host-process.ts, next to signalPidsBestEffort, so the runner tests mock
the signal seam in the same place they already mock the liveness reads.
Refs #1824
* Revert "ci: w3-1824 experiment — trace fork signals and plant pid sentinels in the Coverage job"
This reverts commit
|
||
|
|
f3d5b3d92c |
refactor(daemon): admit-before-bind as an admitted-plan token; retire the R32 syntax policy (#1841)
* refactor(daemon): admit-before-bind as an identity-keyed admitted-plan token; retire the R32 syntax policy admitRuntimePlan (was inspectRequiredRuntimeUse) takes the plan and, on success, mints an AdmittedRuntimePlan: a nominal class instance with nothing readable on it. Its payload — a frozen copy of the device the facts were read for, and the plan — lives in a module-private WeakMap keyed by the token's exact identity, and the only way to read it is unwrapAdmittedRuntimePlan, which refuses anything not minted here. The snapshot owning interface (resolveBoundSnapshotCaptureRuntime, #1847) admits through it and its private binder takes only the token: no bare plan, no separate device, and no look-alike — a spread lacks the #private member (not assignable), a Proxy around a real token types as the token but is a different identity (refused at unwrap), Object.assign/defineProperty throw on the frozen instance, and the class value is not exported so its constructor is not nameable. That retires scripts/layering/runtime-command-cutover-snapshot.ts — R32's per-command AST policy (call-shape recognition of the admission and a text sniff for a local admission) — and the source-regex test beside the descriptor tests. The generic row keeps retirement, narrowing, and singular execution; the manufactured-proof column now also rejects casts to AdmittedRuntimePlan. Planted reds: token degraded to a plain public shape → 2 unused @ts-expect-error directives; unwrap reading the token surface via getters → the Proxy regression fails; getter-based branded literal → the runtime retarget test fails. * docs(agents): the ADR 0019 unit checklist teaches the shipped admission API #1836 documented inspectRequiredRuntimeUse with a forward note pointing here; this PR makes admitRuntimePlan real, so the row now teaches it plus the identity-keyed unwrap the binder uses, and points at the shared snapshot/diff owning interface as the model. |
||
|
|
b12a3e3cb3 |
test: pin test files over 1,000 lines at their exact length so they can only shrink (#1843)
* test: pin test files over 1,000 lines at their exact length so they can only shrink AGENTS.md has said for a while that past 1,000 lines is architecture debt and tests are not exempt; nothing enforced it, and the second-largest test file gained 55 lines in the PR before this one. This is the slow-test ratchet's shape for a reader's context instead of wall clock: the 26 test files over the tripwire are pinned at their exact length (R9-style equality pin, #1781 A6); growth fails, shrink fails until the pin is lowered in the same PR, a file that drops under the line leaves the list, and a new file may not cross it. One directory walk per unit run, ~250ms; the pin list emptying deletes it. * test(ratchet): hold giant test files to their merge-base length so pin edits cannot admit growth Review (P1): the equality pin compared measured lengths only against the pin map in the same checkout, so growing a file and raising its pin, or adding a new >1,000-line file with a pin, stayed green. The gate is now history-backed: every test file over the tripwire may be no longer than at the merge-base with origin/main (renames followed; new files may not cross the line), and no pin may exceed its file's base length — one git cat-file --batch spawn, parsed by bytes because the sizes are bytes. Both bypasses planted red against real git on a pinned file and on a fresh 1,001-line file with a pin added. * test(ratchet): a pin on a file at or under the tripwire is itself a finding Review: a new pin for an unchanged sub-tripwire file (900 pinned at 900) passed equality and history and grew the map. Pins now exist only for files over the tripwire — any other pin is red with 'remove it' — which also subsumes the old shrink-under-the-line message. Planted red in-file and against real git (a 186-line test pinned at 186). The android snapshot test pin bootstraps 1636→1660: main grew that file in #1846 before this gate exists, and history agrees (1660 at the merge-base). |
||
|
|
72d421fe36 |
docs(agents): ADR 0019 unit checklist, owning-seam mock rule, worktree and rebase guidance (#1836)
* docs(agents): ADR 0019 unit checklist, owning-seam mock rule, worktree and rebase guidance Retro follow-up (item 2). Adds docs/agents/adr-0019-unit.md — the order of operations for one command unit with the declaration site for each step, the evidence a unit review must carry, and what 'done' is not — so the pattern rediscovered during the snapshot unit (#1779) is written down once. testing.md: mock the seam the code under test consumes (fake inspectFacts / bindDevice), not the generic dispatchCommand mock; a migrating command moves its tests off the dispatch mock in the same PR. AGENTS.md: fresh-worktree preflight (pnpm install + build in the worktree; layering scan reads tracked files only) and concurrent-agent hygiene (one full gate per host, verify subagent edits with git -C, one PR per worktree). pull-requests.md: two readiness claims (published-and-reported vs merge-ready) and the rebase rule — main has no up-to-date protection; rebase on conflict or when `check:affected --base <merge-base> --head origin/main` names your surface. * docs(agents): name the admitted-plan token in the ADR 0019 unit checklist (#1841) * docs(agents): merge-ready owes live evidence only for changed device-facing paths * docs(agents): the unit checklist documents the admission API on main; #1841 updates the row when it lands |
||
|
|
6a8beb653e |
feat(mcp): compact server instructions in both eras + MCP-only help tool (#1839)
* feat(mcp): compact server instructions in both eras + MCP-only help tool (#1833) MCP-only clients got no workflow guidance: server/discover carried two sentences, legacy initialize carried nothing, and the CLI guides (agent-device --help, help <topic>) were unreachable over MCP. - MCP_SERVER_INSTRUCTIONS: one MCP-phrased workflow card (<2 KB, the Claude Code truncation limit) returned by server/discover and legacy initialize alike. - help tool, router-owned (not a command descriptor): no topic -> the CLI decision card; topic -> agent-device help <topic|command> text, prefixed with the one-line CLI->tool-property mapping; unknown topic -> isError listing the topics. listCommandTools() stays descriptor-only for the AI SDK; the router composes descriptors + help. - Move src/cli/parser/cli-help{,-overview}.ts to src/cli-schema/ so src/mcp (rank 3) can import the renderers without a layering back-edge into src/cli (rank 6). * fix(mcp): name terminal-only commands in help guides; colocate cli-help tests with their sources - The MCP guide preamble claimed every `agent-device <command>` line is a tool of that name; `help web` tells the reader to run `web setup` / `web doctor` and no `web` tool exists. The preamble now lists the exact CLI-only set (listCliCommandNames minus listMcpExposedCommandNames) — derived, not scanned out of prose where `device`/`web` are ordinary words. Regression: help web names `web` as terminal-only, and the listed set equals the registry difference. - cli-help-*.test.ts move from src/cli/parser/__tests__ to src/cli-schema/ to mirror the moved sources. * perf(mcp): tighten the guide card, tool description, and preamble Instructions card 1572 -> 1378 bytes (paid every session), tool description and preamble trimmed, HELP_TOOL built once as a const. Bundle delta vs main 3189 -> 2715 bytes; the remainder is the guide text itself, which the bundle carried in no MCP-phrased form before. |
||
|
|
0fb38f1da2 |
test: prune abandoned test-run tmp directories at run setup (#1834)
* test: prune abandoned test-run tmp directories at run setup A run killed before its teardown (tool-timeout SIGKILL, OOM, cancelled job) left /tmp/agent-device-test-run-<pid>-* behind, and check:tmpdir-leaks — which runs after test:unit in check:unit — flagged every dead-pid directory it found. It could not tell this run's leak from a historical one, so one killed run made every later, otherwise-green gate on the host fail. Both TMPDIR redirection entry points (the Vitest global setup and the node --test wrapper) now prune dead-pid run directories before creating their own, printing one [tmpdir] line when they did; the post-run check keeps its semantics and can now only ever name the run that just finished. Live owners (a concurrent run in another worktree) are never touched. The root/prefix constants move into check-tmpdir-leaks-model.ts, next to the liveness classification, so the setup can import the prune without a cycle. * test(tmpdir): a run directory is live while any process still holds it as TMPDIR, not only while its owner runs Review (P1): owner-pid liveness alone would prune a directory out from under the orphaned children of a SIGKILLed run — the node --test chain, Vitest forks, or a daemon a test spawned all keep running with that TMPDIR. The liveness model now reads every process's TMPDIR (ps -E on macOS, /proc/<pid>/environ on Linux) and treats a run directory as live while its owner pid is alive OR any process's TMPDIR points into it; both the prune and the post-run leak check use it. Regression: a wrapped probe spawns a detached long-lived child, only the wrapper is SIGKILLed, the next prune preserves the directory; after every consumer exits, the next prune removes it. Planted red with owner-only liveness: the orphaned directory is pruned. |
||
|
|
423927fdd8 |
chore(mutation): shrink to report-only — drop the ratchet, baseline and graduation (#1457, #1781) (#1828)
* chore(mutation): shrink the lane to report-only (#1457, #1781 wave 2) The mutation harness's two real catches (#1474, #1475) both came from humans reading the weekly score report. The ratchet half never operated: the baseline was committed exactly twice ( |
||
|
|
4b44c1c53a |
chore(test): remove the contention retry and shrink the subprocess-stub project (#1781 A4) (#1827)
The enumerated single-retry policy (#1419) has fired zero times since it landed on 2026-07-29: 0 of 234 sampled Coverage-job lane envelopes (2026-08-11 to 2026-08-18) have retryCount > 0, and none of 17 recent failed runs was retried (5 refused "outside the enumerated retry list", 4 refused "unhandled error"). All three trackers its entries pointed at (#1098, #1414, #1419) are closed. It cost ~1,454 LOC, a per-run secret marker threaded through a setup file on every Vitest project, and a standing obligation for every future gate reporter to call the blocker bus. Delete the scripts, tests and fixtures, the check:contention-retry script and gate, the envelope artifact upload, and the runner-timeout setup file; test:coverage:ci is a plain `vitest run --coverage` again. lane-envelope.ts stays: the mutation, fuzz and concurrency-torture lanes build their envelopes from it. run-blocker-bus.ts goes: its only consumer was the retry's failure sink, and its only publisher already fails the run by setting process.exitCode. Keep the subprocess-stub project for the three files that really spawn (client-metro, fuzz harness, fuzz corpus-replay) and drop the three that run in 31/212/277ms in CI, which cannot contend for anything. The list is now a plain array in vitest.config.ts with the reason at each entry. Membership and the project's kill criterion live in #1823. Because test:coverage:ci is a bare vitest run, the gate manifest reads its projects directly, so OPAQUE_RUNNERS no longer needs it and an unrun Vitest project becomes unrepresentable rather than detected; the audit test now constructs that state by project-scoping the script. |
||
|
|
142d156338 |
ci(ios): run the full XCTest suite nightly and check the PR test list (#1781 A7) (#1789)
* ci(ios): run the full XCTest suite nightly and check the PR test list (#1781 A7) * fix(ci): skip the runner server entry point in the nightly and validate both test flags * docs(ci): restate the nightly lane cost and timeout honestly * docs(ci): stop quoting XCTest counts that drift between commits * ci(ios): tighten the nightly timeout to the measured suite duration |
||
|
|
ccf64f6797 |
ci: move parked device replay suites to a dispatch-only workflow (#1781 A1) (#1794)
* ci: move parked device replay suites to a dispatch-only workflow (#1781 A1) Both full-tier device jobs have failed every scheduled run since 2026-07-24: the Android suite inside full-tier scenarios that had never executed end to end, the iOS suite on varying steps. They move to .github/workflows/replays-manual.yml, which has no `schedule:`, so the schedule stops emitting a guaranteed failure while the suites stay runnable on demand. A job-level `if: github.event_name == 'workflow_dispatch'` would have looked the same and lied: `workflowLanes()` decides `qualifying` per workflow FILE and never reads job-level `if:`, so the manifest kept reporting replay-android, replay-ios, and replay-ios-device as scheduled-lane owners — the silent-owner-loss failure the manifest exists to catch. A separate file is what the file-level model already reads correctly. Those three checks now have no pull_request/schedule owner, so they are declared as MANUAL_ONLY_OWNERS rather than folded into UNPROVABLE_OWNERS, whose claim ("it runs, this loader cannot see it") is no longer true for replay-android. check:gate-manifest drops from 48 to 46 wired checks and names the three on every run. Two tests pin it: a dispatch-only lane is non-qualifying however many gates it declares, and every manual-only declaration must name a registered check that no qualifying lane owns, so a re-scheduled lane cannot keep a stale exemption. * ci: attest manual-only checks against their dispatch lane (#1781 A1) Review P1: MANUAL_ONLY_OWNERS was a negative allowlist — it proved each entry named a registered check no qualifying lane owned, but nothing tied the entry to a lane that can still run it. Deleting a parked job, or its run-gate step, would have left the manifest green and still printing the check as manual-only: parked coverage silently becoming deleted coverage. Each entry now names its dispatch lane, and a new 'manual-only' audit assertion resolves that name against the derived model: the lane must exist, must still be dispatch-only, and must still declare the gate. replay-android carries an explicit `opaque` flag because its gate sits inside the third-party emulator action's `script:` (#1429), so the job's existence is the whole attestation the model can make — and the flag says so rather than letting an unreadable lane look like a declaring one. Four regressions pin both directions: deleting a declaration reports the check as unowned; deleting the parked job fails with 'no workflow defines'; re-scheduling the lane fails until the entry is dropped; and a parked lane that loses its run-gate step fails unless the entry is opaque. * ci: make manual-only mean dispatch-only, not merely non-qualifying (#1781 A1) Review follow-up: the attestation checked `qualifying === false`, which is true of any lane that is not pull_request/schedule. Swapping `workflow_dispatch` for `push` in replays-manual.yml would have kept the audit green and the checks printed as manual-only, while the runs nobody starts by hand quietly started themselves on every push. The lane model now keeps the trigger names instead of collapsing them into that one bit, and the manual-only assertion requires `workflow_dispatch` and nothing else. Three planted regressions cover the gap the review named: a parked lane re-triggered by `push` fails, a parked lane with no trigger at all fails, and the loader test pins that trigger kinds survive into the model (a push lane reads `[push]`, the nightly reads `[schedule, workflow_dispatch]`). |
||
|
|
9c22467832 |
refactor(ci): make gate ownership structural (#1429) (#1753)
* test(ci): prove every registered gate is owned and reachable (#1429) A check that silently stops running looks exactly like a green build. Two suites had already stopped: `check:tmpdir-leaks` (with its model tests) and `test:fixture-cache` are real package scripts that no workflow ran, reachable only through the `check:unit` aggregate CI never invokes. `CHECK_CATALOG` becomes the registry of every check and `pnpm gate <id>` the only way CI runs one, so finding what a lane runs is a scan for `pnpm gate` rather than an attempt to interpret shell. `pnpm check:gate-manifest` then asserts against the real workflows that every registered check is run by some qualifying lane (per unit, not per script name), that every check the real selector activates for a path is run by a lane that path would start (#1420's class), and that every Vitest project and suite script belongs to a check. The wiring that keeps those honest is asserted too: a gate id must name a registered check, an `if:` must be ruled on in GATE_CONDITIONS so `if: false` unowns what it guards, an action declared to run a gate is proven to, and a job whose steps the loader cannot open fails closed. It deliberately does not try to prove CI runs project code only through `pnpm gate`. Whether a shell block executes project code is not decidable from its text, so shell this model does not recognise earns no ownership credit — the failure direction is a check reported unowned, never one waved through. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * test(ci): update the two suites that assert on rewired workflow text `scripts/mutation/workflow.test.ts` and `test/ci/trusted-fixture-artifact.test.mjs` read the workflow and action files and assert on their command text, so routing those steps through `pnpm gate <id>` moved what they were matching. They are the two suites the manifest cannot help with: it proves a gate is still run, not that a test asserting on how CI spells a command was updated with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * fix(ci): credit gates by execution shape, and keep every guard Three ways the manifest could report a gate as owned when it does not run. 1. Crediting was a substring scan over `run:`, which #1429 explicitly rules out — "do not infer reachability from a command name merely appearing in workflow text". `false && pnpm gate x`, a gate inside `if false; then … fi`, one named in a heredoc, and `echo pnpm gate x` all credited it. There is a live instance: conformance-regenerate.yml's "Fail if regeneration changed anything" step names `pnpm gate maestro-regenerate` inside an error message telling a human to run it, and that credited the gate. A gate now counts only as the first command segment of a line, and a body carrying shell structure earns nothing. Reachability inside a script is not decidable, so this does not try: unrecognised shape means no credit and the check reports unowned. `VAR=$(pnpm gate x …)` is read, since the assignment form is unambiguous and the gate runs. 2. Job-level `if:` was not modelled at all, though six live jobs carry one, so a job that cannot run still credited every gate inside it. Two conditions on the mutation lanes are now declared. 3. A caller's `if:` REPLACED the guard on a nested composite-action step (`guard[0] ?? step.condition`), so an outer `always()` erased an inner `if: false`. Steps carry every guard between the lane and the step. Also corrects two source comments that still claimed project code run outside the runner fails the manifest. It does not: such a step earns no credit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * ci: add the run-gate action that names a gate structurally The seam the ownership proof will read instead of shell. A lane says which gate it runs in `with.gate`, a typed input the manifest reads straight out of the YAML and validates against CHECK_CATALOG. Nothing here is wired yet — the ~60 call sites and the model change follow. Added first so the target of that conversion is reviewable on its own. `args` cannot select which gate runs; it is appended after the id, so the worst a wrong value does is fail the gate it already named. There is no `|| true` and no output capture: the gate's exit code is the step's exit code, so a gate cannot run without being able to fail its lane. Part of #1429. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * merge: main (#1770) and route its three new steps through the runner #1770 landed the orphan-check fix on main, wiring `check:tmpdir-leaks`, `check:tmpdir-leaks:test` and `test:fixture-cache` into Coverage, Layering Guard and Integration Tests. This branch had wired the same three through `pnpm gate`, so the merge produced two steps per check rather than a conflict — each check ran twice. Kept main's steps, with the placement and reasoning reviewed on #1770, and changed only their `run:` line to the canonical runner. Dropped this branch's duplicates. Net effect on CI is unchanged: the same three checks, in the same three lanes, once each. Gate manifest green after the merge: 47 checks wired across 33 lanes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * fix(ci): address review — suite detection, freerange, glob, vacuous skip-list Six review findings plus the mutation blocker. [bug] `registered` was shape-only, so a `test:*` script running `node src/bin.ts test <dir>` resolved to a `script:` leaf and was invisible. Four `test:replay:*` scripts were owned only because someone hand-registered them; `test:replay:android` was neither registered nor reported while the nightly ran the same six .ad files by inlining them. A `test:*` script is now a suite by name. `replay-android` is registered, and the nightly runs the script instead of re-listing its files so the two cannot drift. The nightly invokes it inside `reactivecircus/android-emulator-runner`'s `script:` input — shell handed to a third-party action this loader does not read — so the suite executes but cannot be credited. Recorded in UNPROVABLE_OWNERS with that exact reason rather than assumed. The fixed detector also found a second orphan the review did not name: `test:integration:progress`. That one is a reporter whose `--check` sibling is the registered gate, so it is declared in REPORTING_SCRIPTS — a declaration that itself fails when inert. [bug] `freerange` defaulted to localRunnable, so fail-open ran `fr` (a Bun binary) on the pre-push path. Now false. [suggestion] The `--run` skip-list asserted `build:android-snapshot-helper`, a name `android-helpers` no longer uses, so it could not fail. Derived from the catalog instead. [suggestion] `matchesGlob` joined `**` splits with `.*`, making the adjacent slash mandatory — GitHub's `**` matches zero directories, so `src/**/*.test.ts` did not match `src/a.test.ts`. Pinned against `packages/*/src/**/*.test.ts`. [suggestion] Deleted the unwired `run-gate` action. It had no callers, was absent from GATE_ACTIONS, and its comment described a system that had not shipped. It returns with the rewiring, not before. [suggestion] Collapsed the module headers that narrated discarded designs. Mutation: `daemon entrypoint publishes HTTP metadata and cleans up on shutdown` is the only test here that spawns a real daemon process. It takes ~1.1s alone but exceeds Vitest's 5s default inside Stryker's dry run, which aborts the sweep before a single mutant runs. Given 30s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * fix(mutation): order sandbox aliases longest-first so subpaths resolve Every shard of the mutation sweep aborted in Stryker's dry run with: Cannot find package '@agent-device/selectors/engine' imported from .tmp/stryker/sandbox-*/src/core/selector-pipeline.ts The alias was generated correctly; it just never won. Vite matches a STRING alias by prefix and takes the first hit, and `workspaceSpecifierTargets` emitted the bare `@agent-device/selectors` ahead of the subpath entries. The bare entry therefore captured `@agent-device/selectors/engine` and rewrote it to `…/src/index.ts/engine`, which does not exist; Node fell back to real package resolution, could not find the subpath inside the sandbox, and the dry run failed before a single mutant ran — so the shard uploaded an empty envelope instead of a report and the ratchet failed for want of one. Sorting longest specifier first makes the most specific alias win: @agent-device/selectors/engine -> packages/selectors/src/engine.ts @agent-device/selectors/ast -> packages/selectors/src/ast.ts @agent-device/selectors -> packages/selectors/src/index.ts `/ast` never tripped this because nothing in a related test set imported it; `selector-pipeline.ts` introduced the first subpath import that mattered (#1744), so the mutation lane has been unable to run since that landed. Any PR touching `scripts/mutation/**` — which fails open into the full sweep — would have hit it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SkS4S8XXrfkJ8TD1VBKkvJ * refactor: derive gate ownership from workflow structure * fix: run gates without optional arguments * fix: resolve mutation workspace subpaths exactly --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
f5d9789764 |
feat: enforce local device claims and reconcile stale owners (#1735)
* feat: enforce local device claims * fix: address device claim review feedback * fix: persist canonical daemon claim state directory |
||
|
|
fa9a350361 |
docs: prefer design fixes over regression-only guards (#1722)
* ci: require simplicity review for large tooling changes * docs: prefer design constraints over regression-only fixes * docs: simplify design-first guidance |
||
|
|
05a1d76f2e |
test: add daemon RPC wire-surface compatibility gate (#1717)
* test: gate daemon RPC wire compatibility against the last released tag (#1432) ADR 0006 fixes exactly when DAEMON_RPC_PROTOCOL_VERSION must be bumped, and nothing checked that it was. The runtime guard (readRemoteDaemonHealth) refuses a mismatched peer, but only fires when someone remembered the bump — a wire change that skipped it left both sides advertising protocol 2 while parsing different payloads, which is the failure ADR 0006 exists to prevent. Local daemons cannot skew (isReusableDaemonInfo takes over on any package version mismatch). Cross-machine is skewed by design — proxy, cloud/limrun, a remote macOS host — and ADR 0006 explicitly rules package version out as the compatibility gate there, so the one boundary where skew is intended was the one boundary with no gate. test/wire-compat/surface.ts declares the wire surface grouped by the ADR bullet each group serves, quoting it, with an `uncovered` note where a bullet is only partly digestible (the /health and /rpc literals inside http-server.ts stay reviewer-owned: a moved route 404s at connect time rather than misparsing). ledger.json records what each declaration hashes to, at which protocol version. Two gates, split for the same reason the replay-compat corpus splits: - unit-core holds the ledger to its source and prints the digest to paste; - Released-Surface Compatibility reads the ledger at the last RELEASED tag and requires the drift since then to carry a bump or a compatibleChanges ack. From one commit a bumped ledger and an unbumped one are both just an edited file, so only a released baseline can tell them apart. Acks are keyed by the digest they cover, so one "added an optional field" cannot launder later changes. Digests ignore comments and formatting; the manifest's closure is derived from the AST, so a field typed by an unlisted sibling fails rather than sitting outside the gate. CI cost: one added job (checkout + toolchain + two node scripts, ~1 min), mirroring the existing full-history replay-compat job. * test: close wire-surface overclaim and make the closure fail closed (#1432) Addresses both review P1s on #1717. P1 — the manifest materially overclaimed ADR 0006 coverage. It quoted all four bullets while digesting only the payload TYPES, so the producer and consumer seams could break a skewed peer without moving a listed digest. Now listed on both sides of every boundary: JSON-RPC method sets and the projections that turn each method's params into a DaemonRequest, createRpcError/sendJson/ writeRpcResponseEnvelope, resolveToken and the auth-hook types, upload preflight/finalize/308 handlers and the resumable ticket shape, artifact route and download/inventory framing, REST error mapping, and the client's own payload builder, lease-method mapping, response parser and error projection. 57 -> 117 declarations. What stays out is now named rather than implied: createDaemonHttpServer's dispatch wiring and the /health and /rpc literals inside it. Everything it dispatches WITH is digested individually, and a moved route 404s at connect time rather than misparsing — the loud failure, not the silent one. P1 — imported and re-exported payload shapes escaped the closure. declarationHomes() scanned only the manifest's own files and the walk continued silently when a name could not be placed, so a listed type could gain foo?: ImportedShape from a new module and stay green. Resolution is now explicit and fails closed: relative imports, workspace specifiers (through the owning package's own exports map, so a re-pointed export cannot drop a type), and facade re-export chains. Every referenced name must land on a listed declaration, a waiver with a written reason, a declared external module, or the TS/Node global set. Fixed two extractor blind spots the walk exposed: a declaration's own generic parameters and `as const` were being reported as references. Planted-red proofs (wire-mutations.test.ts): 13 cases independently mutate method naming, response serialization, response parsing, auth projection, upload ticket shape, 308 framing, artifact framing, REST error mapping, and progress framing, each asserting the digest moves; 3 probes prove the closure really reaches across a package boundary, a facade re-export, and a plain relative import. Mutations apply inside the declaration's own span — a whole-file replace silently hit a sibling sharing the substring, which is how the first draft of one case passed vacuously. The largest waiver pair (InternalRequestOptions, CommandFlags) rests on ADR 0006's own additive rule: they reach the peer inside DaemonRequest's untyped flags/input bags, and the decision says a new flag needs no bump. Digesting them would fire the gate on every new CLI flag and train reviewers to rubber-stamp acks. * test: list the consumer half of the auxiliary HTTP boundaries (#1432) Addresses the remaining review P1 on #1717. The manifest claimed both sides of response/upload/artifact framing while listing nothing from upload-client.ts, daemon-artifacts.ts, or the health consumer in daemon-client-transport.ts, so those parsers could narrow without moving a listed digest or protocol 2. Now listed (117 -> 141 declarations): - /health consumer: RemoteDaemonHealth, readHealthPayload, readDaemonHttpHealth, readRemoteDaemonHealth. This is the sharpest of the three — narrowing the reader or the comparison disables the very refusal ADR 0006 exists to guarantee, and nothing else in the repo would notice. - /upload consumer: UploadResponse, UploadPreflightResponse, UploadPreflightResult, parseUploadPreflightResult, requestUploadPreflight, uploadDirectArtifact, tryDirectUploadWithResume, shouldRetryDirectUpload, finalizeDirectUpload, uploadLegacyArtifact, ARTIFACT_HASH_ALGORITHM, isStringRecord, and PreparedUploadArtifact — whose sha256/sizeBytes/fileName/artifactType/ contentType fields ARE the preflight body the daemon parses. - /artifacts/* consumer: DaemonArtifactEndpoint, buildDaemonArtifactUrl, isRemoteDaemon, DownloadRemoteArtifactParams, downloadRemoteArtifact, materializeRemoteArtifacts, resolveMaterializedArtifactPath. Running the closure fail-closed over the new files surfaced three more stops, each decided rather than skipped: PreparedUploadArtifact listed (it is payload), UploadProgressSink waived (client-local rendering, never leaves the process), and src/daemon/types.ts#DaemonArtifact waived as a re-export alias of the listed kernel type, matching its DaemonRequest/DaemonResponse siblings. 10 more planted-red mutations cover the new seams: health version-read and mismatch-refusal defeated, RemoteDaemonHealth field dropped, preflight parser narrowed, preflight/legacy response shapes narrowed, finalize body key renamed, ticket field renamed, artifact tenant header dropped, artifact URL moved. A fourth closure probe proves the upload-consumer files are genuinely reached by the walk rather than merely listed. 22 -> 33 tests. The README now states the coverage as a producer/consumer table per boundary, so the claim is checkable at a glance instead of asserted in prose. * test: list the client half of the resumable 308 contract (#1432) Addresses the third review P1 on #1717. Listing the daemon's handleResumableUpload proved it still PRODUCES 308; nothing proved the client still CONSUMES the released one. src/remote/upload-stream.ts owns that half and was entirely outside the manifest, so a newer client could stop accepting `upload-offset`, change how it reads `Range: bytes=0-N`, or emit a different resumed `Content-Range` without moving one of the 141 listed digests. Now listed (141 -> 151): UploadStreamResponse, streamFileToHttpRequest, streamFileToHttpRequestAttempt, buildUploadRequestHeaders, isUploadResumeStatus, isUploadRedirectStatus, parseUploadResumeOffset, parseNonNegativeIntegerHeader, firstHeaderValue, MAX_UPLOAD_REDIRECTS. streamFileToHttpRequestAttempt is listed despite its size, unlike createDaemonHttpServer which stays in `uncovered`. The distinction is stated at the declaration: the HTTP server only dispatches to handlers that are each digested, while the attempt loop IS the resume state machine — it decides whether a 308 continues the upload and what the next request carries, so its sequencing alone can break a released daemon while every helper keeps its digest. 6 new planted-red mutations prove the client half moves the ledger: a dropped `upload-offset` fallback, narrowed Range parsing, a changed resumed Content-Range, 308 no longer treated as continue, a narrowed UploadStreamResponse, and dropped header-value coercion. 33 -> 39 tests. Closure fail-closed surfaced two more stops: UploadStreamProgressOptions waived (local byte-progress rendering) and URL/URLSearchParams added to the global set. README now carries a `/upload` resume row in the producer/consumer table, and names the pattern behind three rounds of review: the coverage sentence kept getting written ahead of the coverage, so the table and the `uncovered` notes are the claims to trust — they are checkable against surface.ts, prose is not. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
c06bed9f77 |
refactor: extract platform device inventory runtime (#1699)
* refactor: extract platform inventory runtime * fix: preserve scoped Apple inventory tooling * fix: preserve Apple tool cancellation * refactor: tighten platform inventory boundaries |
||
|
|
4b432fb59b |
feat: add HarmonyOS support (#1683)
* feat: add HarmonyOS device automation foundation Add HDC-backed discovery, snapshots, application lifecycle, and core mobile interactions. Route HarmonyOS through the platform registry and client contracts. Cover parsing and capability parity with focused tests. * feat: support HarmonyOS HAP deployment Install and reinstall signed HAP archives through HDC. Resolve bundle identities from module metadata and relaunch after package replacement. Extend deploy routing and capability coverage for HarmonyOS. * feat: add HarmonyOS single-pointer gestures Execute pan, fling, and swipe plans through HDC uiInput primitives. Derive scroll coordinates from the live ArkUI viewport. Keep unsupported multi-touch gestures explicitly rejected. * refactor: split session inventory command handling Separate session, device, capability, and app inventory response paths. Preserve the public inventory response contract while reducing handler complexity. * feat: support HarmonyOS keyboard actions Route HarmonyOS enter, return, and dismiss through HDC key events. Expose supported keyboard actions through the system command metadata. Keep keyboard visibility inspection explicitly unsupported. * fix: reject unsupported HarmonyOS drag gestures Keep drag unavailable until HDC can preserve source and destination hold semantics. * feat: add HarmonyOS app log streaming 1. Stream HarmonyOS app logs through PID-scoped hilog sessions.\n2. Record HarmonyOS app identity during bundle-id opens for app-scoped commands.\n3. Cover backend routing and bundle identity resolution. * feat: report HarmonyOS foreground app state 1. Read the foreground HarmonyOS mission through aa dump.\n2. Expose HarmonyOS appstate with package and ability metadata.\n3. Add parser coverage for foreground and missing-state cases. * fix: advertise appstate through capabilities 1. Classify appstate in the command descriptor capability matrix.\n2. Surface supported appstate commands in capability inventory.\n3. Cover the advertised Android capability contract. * feat: sample HarmonyOS process performance 1. Sample HarmonyOS process CPU and resident memory through HDC.\n2. Expose the verified metrics through the shared perf command.\n3. Keep frame and memory snapshot collection explicitly unavailable. * feat: clear HarmonyOS app state 1. Add HarmonyOS settings clear-app-state through bundle cleanup.\n2. Force stop the app before clearing data and cache.\n3. Reject all unverified HarmonyOS settings explicitly. * docs: document HarmonyOS support 1. Describe HarmonyOS HDC prerequisites and HAP installation.\n2. Add HarmonyOS to platform discovery and product documentation.\n3. Document verified performance limits for the public HDC surface. * fix: preserve HarmonyOS deploy session identity 1. Bind a resolved HarmonyOS bundle after install or reinstall.\n2. Keep app-scoped logs and observability available after deployment.\n3. Cover session identity preservation for HarmonyOS reinstall. * test: lock HarmonyOS capability boundary 1. Add an independent HarmonyOS capability-matrix oracle and exact advertised-command regression test. 2. Document current HDC-backed support and evidence-based unsupported command boundaries. * refactor: simplify HarmonyOS shared platform boundaries 1. Split device selection and settings dispatch into focused helpers without changing behavior. 2. Keep HarmonyOS serial selection and lock-policy classification covered by regression tests. 3. Remove Fallow complexity findings from the HarmonyOS diff against upstream main. * fix: bound default HarmonyOS HDC commands 1. Apply a 15 second timeout to ordinary HDC operations. 2. Preserve operation-specific timeout budgets for installation and capture paths. 3. Add regression coverage for default and overridden HDC timeouts. * feat: add HarmonyOS screen recording Implement physical-device whole-screen recording through the system recorder and HDC media transfer. Reject unsupported HarmonyOS recording scopes and export flags. Cover capability routing, media retrieval, cleanup, and simulator rejection. * feat: report HarmonyOS HDC readiness Add an HDC version check to the HarmonyOS doctor flow. Document HarmonyOS as a supported doctor platform and cover the result. * refactor: simplify HarmonyOS recording checks Reduce recording validation and test complexity without changing behavior. * test: cover HarmonyOS platform contracts Synchronize public platform expectations across CLI, MCP, replay, and inventory tests. Mock HarmonyOS inventory probes to preserve concurrent test behavior. * test: model HarmonyOS recording capability Require a physical HarmonyOS device in the independent capability parity oracle. * test: cover HarmonyOS input and lifecycle paths Exercise HDC input, lifecycle, installation, and relaunch command sequences. * test: cover HarmonyOS device observability paths Exercise discovery, screenshot validation, and process performance sampling. * docs: define HarmonyOS CI hardware policy Keep HDC hardware validation local and require mocked CI contract tests. * fix: honor HarmonyOS app inventory filters * fix: bound HarmonyOS app inventory classification 1. 限制应用元数据分类并发并为默认清单设置整体时限. 2. 将请求取消信号传递给 HarmonyOS 应用清单读取. 3. 补充失败时中止在飞读取且不继续排队的回归测试. * fix: preserve HarmonyOS inventory failure causes 1. 保留触发应用元数据分类失败的原始错误, 避免被取消同级任务覆盖. 2. 补充总时限中止在飞读取且不启动排队任务的回归测试. 3. 验证后序任务失败时保留默认筛选的恢复提示. |
||
|
|
ac9e4d0f04 |
test: measure oracle liveness suite-wide; pin the one dead-path oracle (#1679)
* test: measure oracle liveness suite-wide; pin the one dead-path oracle - docs/agents/oracle-negation-spike.md: assertion-negation sweep over 677 test files (5,687 verdicts). Zero vacuous tests: all 150 negation survivors decompose into assert.rejects-validator artifacts (112), helper-oracles (31), in-file-fake breakage (6), and one conditional oracle. Records the companion mock-coupled coverage-uniqueness numbers and the follow-ups they motivate (diff-scoped mutation gate, provider seam closures, transcript provenance). - watchos-sentinel: the non-watchOS test's only assertion sat in a catch block that never fires (tvOS interactor creation succeeds), so no assertion executed on the observed path. Pin creation success instead. Red-run proof: the old shape survived the negation sweep; the new shape fails under it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019S5sZnmPn4A9Ct7sTJdfAf * test(android): inject fake adb through the provider scope, not PATH stubs Adds withFakeAdb to test-utils: a scripted in-process AndroidAdbProvider installed through the production withAndroidAdbProvider seam — the same scope the daemon installs per request — replacing PATH-stub shell scripts that spawn a real subprocess per adb call. No PATH mutation, no spawns, no real subprocess waits. Converts settings.test.ts (15 tests, 23ms; waiver said "waits real settings-apply poll time") and notifications.test.ts (2 tests, 9ms). Assertions move from args-log regex greps to structural checks on the recorded call list; the fake receives device-scoped args with the -s serial pair stripped, so serial routing is enforced by the scoped provider matching device.id instead of asserted per call. Remaining PATH-stub files convert next; their contention-retry waiver entries lift together with the conversions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019S5sZnmPn4A9Ct7sTJdfAf * test(android): convert device-input-state to fake adb provider injection 10 PATH-stub cases move to withFakeAdb through the production provider scope; the 2 tests that already inject an executor directly are unchanged. Cross-invocation shell STATE_FILE state becomes a closure boolean; args-log regex asserts become structural checks on recorded calls. 12/12 green at 386ms — the residue is dismissAndroidKeyboard's two fixed 120ms retry sleeps, not stub subprocess waits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019S5sZnmPn4A9Ct7sTJdfAf * test(android): convert app-lifecycle-install adb stubbing to fake provider The adb half of every case moves to withFakeAdb through the production provider scope; installs take the documented exec-shaped fallback (exec(['install','-r',...])), matching what the PATH stub saw minus the serial pair. bundletool/zip/unzip stay real or PATH-stubbed — they run via runCmd outside the adb seam, so this file remains in the serialized subprocess-stub lane with its waiver reason to be corrected from adb to bundletool. 13/13 green at ~130ms; no case enters a retry/poll loop. Conversion note: manifest identity's `unzip -p` failure is silently swallowed (readZipEntry catch -> undefined, aapt fallback) — an invisible degradation path worth a future explicit diagnostic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019S5sZnmPn4A9Ct7sTJdfAf * test(android): convert input-actions adb stubbing to fake provider 9 PATH-stub cases move to withFakeAdb; the 3 tests already injecting providers directly are unchanged. Chunked shell-input assertions become ordered deepEqual on the recorded calls; never-called negatives and call-count checks preserved 1:1. 12/12 green. File time drops to 2.2s, all of it production sleeps: verifyAndroidFilledText unconditionally waits its [0,150,350]ms verification cadence even when the first inspection matches, so each fill verification pass costs ~500ms with an instant fake. A budget-derived cadence there (testing.md pattern 1) would put this file near 25ms; flagged as follow-up rather than changed here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019S5sZnmPn4A9Ct7sTJdfAf * test(android): extract shared oracles; make fake adb failure-faithful Three test-utils extractions applied across the six converted files: - assertRejectsAppError collapses the hand-rolled AppError code+message rejection validator (10 sites here; ~30 more repo-wide can adopt it incrementally). Validators asserting details or multiple differently- flagged regexes stay explicit on purpose. - withFakeAdb gains a `provider` option for extra capabilities (snapshotHelperArtifact, reverse, ...), replacing input-actions' nested re-scoping bridge. - withFakeAdb now mirrors the local executor's contract: a scripted nonzero exit throws androidAdbResultError unless the call site passed allowFailure. Provider-scoped exec bypasses exec.ts's throw-on-close- failure, so returning {exitCode:1} took a different production path than the PATH-stub `exit 1` these fakes replaced. All 75 tests hold under the corrected semantics. Also swaps settings' inline emulator DeviceInfo literals for the shared ANDROID_EMULATOR fixture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019S5sZnmPn4A9Ct7sTJdfAf * test: lift five converted Android files from the contention-retry waiver settings, notifications, device-input-state, input-actions, and app-lifecycle-open no longer stub binaries on PATH or spawn subprocesses, so their contention mechanism is gone: they leave CONTENTION_RETRY_FILES and, through the derived SUBPROCESS_STUB_TESTS constant, the serialized subprocess-stub project (17 -> 12 files). app-lifecycle-install stays with its reason corrected: adb is now in-process, but bundletool stays PATH-stubbed and zip/unzip spawn for .aab packaging paths. Full unit suite green at the new membership: 638 files, 5,724 tests, with the five files running at unit-core's default parallelism. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019S5sZnmPn4A9Ct7sTJdfAf * test: apply review findings to the fake-adb conversion batch - app-lifecycle-open: the missing-package launch failure returns {stderr, exitCode: 1} and lets withFakeAdb's throw path produce the production-shaped androidAdbResultError instead of hand-modeling the thrown AppError — the drift the helper exists to eliminate. - withScriptedAdb deleted: the six converted files were its only callers, and a live PATH-stub export invites new tests back into the serialized lane this batch shrank. withMockedAdb stays (dispatch and runtime-hints tests still stub other binaries). - android-snapshot-helper gains androidSnapshotHelperScriptResponse so the version-probe detection and versionCode reply have one source of truth; input-actions' local copy delegates to it. - withFakeAdb's provider option becomes a distributed Omit over the AndroidAdbProvider union, so touch without gestureViewport is a compile error at the fake's boundary (planted and verified) instead of a TypeError inside production gesture planning. - spike-doc re-run checklist restores wider than the codemod globs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019S5sZnmPn4A9Ct7sTJdfAf * test: drop the consumer-less FakeAdbScript barrel re-export Fallow's dead-code gate flagged it: scripts are always passed as inline lambdas, so only FakeAdbResponse needs a name at the barrel. The type stays exported from fake-adb.ts where the withFakeAdb signature uses it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019S5sZnmPn4A9Ct7sTJdfAf * test(apple): inject fake xcrun through the tool-provider scope, not PATH stubs withFakeAppleTool mirrors withFakeAdb for the Apple seam: a scripted provider installed via the production withAppleToolProvider scope, flat simctl/devicectl invocations recorded exactly as the PATH-stub shell scripts saw them, throw-on-nonzero fidelity matching exec.ts unless the call site passed allowFailure, and the canned `simctl privacy help` listing served by default (the block withMockedXcrun injected into every script). screenshot-status-bar.test.ts converts as the exemplar: 3/3 green at 9ms with deepEqual call-sequence assertions replacing the args-log regexes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019S5sZnmPn4A9Ct7sTJdfAf * test(apple): convert apps.test.ts xcrun stubbing to fake tool provider All withMockedXcrun scripts and hand-rolled PATH stubs move to withFakeAppleTool; args-log regexes become structural call assertions (exact deepEqual where order is deterministic, presence checks where the 5s simulatorBootedMemo TTL makes boot-probe order test-dependent). 12 hand-rolled AppError validators collapse into assertRejectsAppError. 54/54 green; file test time 1172ms -> ~400ms with no test over 201ms. Five .ipa install tests keep a minimal PATH stub for unzip only: install-artifact.ts:112 and install-source.ts:438 call runCmd('unzip') directly, outside the Apple tool provider seam — the file therefore stays in the serialized subprocess-stub lane with its waiver reason corrected from xcrun to unzip. Also observed: getSimctlPrivacyServices caches per PATH+simulatorSetPath and simulatorBootedMemo keys on deviceId|setPath, so neither cache accounts for the provider scope — worked around per test, follow-up worthy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019S5sZnmPn4A9Ct7sTJdfAf * test: lift six Apple waivers; fix format and fallow findings from CI - interactions, simulator, screenshot, physical-device-screenshot, devicectl, and screenshot-status-bar leave CONTENTION_RETRY_FILES: the first five stopped stubbing PATH binaries in earlier refactors (measured 3-64ms per file, no subprocess activity), and screenshot-status-bar now injects through the fake tool provider. apps.test.ts stays with its reason corrected to the unzip PATH stub (xcrun is in-process; install-artifact.ts:112 / install-source.ts:438 call runCmd('unzip') outside the Apple seam). Serialized lane 12 -> 6. - oxfmt: fake-apple-tool.ts and contention-retry.ts were pushed unformatted (local check piped through tail masked the failure). - fallow complexity: the three fake-script arrows in apps.test.ts drop under threshold via shared predicates (isSimctlMainScreenScale, isSimctlScreenshot, isDevicectlDevice), which also deduplicate the screenshot pair. Full unit suite green at the new membership: 638 files, 5,724 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019S5sZnmPn4A9Ct7sTJdfAf --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
ac52281448 |
fix(test): deterministic temp-dir cleanup across node --test lanes (#1661)
* fix(test): deterministic temp-dir cleanup across node --test lanes node --test has no global setup/teardown hook, so unlike Vitest (#1593) every node --test package.json script (maestro:conformance, mutation:test, check:affected:test, check:coverage-changed:test, check:layering, depgraph:test, check:tmpdir-leaks:test, check:contention-retry, test:fixture-cache, test:smoke(:web), test:integration:node, test:concurrency-torture) still created scratch directories against the real, unredirected os.tmpdir(), with cleanup only as reliable as each call site's own try/finally — which a crash, OOM, or timeout kill bypasses entirely. Add scripts/node-test-tmpdir.ts: it wraps the whole `node --test` invocation as a child process, redirecting TMPDIR to one disposable, pid-tagged directory (shared root/prefix with the Vitest lane) and removing it from the process 'exit' event, which fires on normal completion, a thrown error, or a forwarded SIGINT/SIGTERM alike. Every node --test script now runs through it. check-tmpdir-leaks.ts already scans by root/prefix, so it covers both mechanisms with no changes to its detection logic. Verified: a node --test process that mkdtemp's then gets SIGKILL'd leaves a directory behind unwrapped; wrapped and SIGTERM'd, TMPDIR is redirected and the directory is gone with no orphaned processes. All 13 wrapped lanes and the full Vitest suite (5,591 tests) pass with zero residual agent-device-test-run-* directories after the run. Fixes #1595 * test(tmpdir): ratchet every node --test script through the wrapper The 13 lanes wrapped in package.json were a one-time hand sweep with nothing enforcing the pattern going forward — a 14th node --test script added later without scripts/node-test-tmpdir.ts would silently reopen #1595 for that one lane. Add a structural check to scripts/node-test-tmpdir.test.ts (now part of check:tmpdir-leaks:test) that reads package.json and fails if any script invokes `node ... --test` without routing through the wrapper. Dumb string matching over the scripts map, no shell parsing, with an explicit (currently empty) NODE_TEST_WRAPPER_BYPASS_ALLOWLIST for any lane that must legitimately bypass it. Verified it both passes on the current package.json and fails when a synthetic unwrapped `node --test` script is added. * fix(test): preserve the Swift cache and close the raw node --test bypasses Review on #1661 found two gaps: 1. The wrapper only overrode TMPDIR, so it discarded and forced a recompile of the durable Swift compiler cache every run instead of mirroring vitest-tmpdir-global-setup.ts's carve-out for it. Read os.tmpdir() before the child's TMPDIR redirect takes effect and set AGENT_DEVICE_SWIFT_CACHE_DIR from that (only when unset), same as the Vitest lane — the two now share one durable cache instead of each discarding and recompiling their own. Added a probe assertion (scripts/node-test-tmpdir.test.ts) that fails without the fix and passes with it (verified both ways). 2. docs/agents/testing.md documented raw `node --test` commands for the iOS smoke files, and the android/ios/conformance-regenerate/nightly workflows invoked `node --test` directly outside package.json. Routed all of them through scripts/node-test-tmpdir.ts so the documented local commands and CI lanes get the same crash/timeout-safe cleanup the package.json scripts already have. |
||
|
|
23a3016e9b |
fix: stop the unit suite from leaking temp directories (#1593)
* fix: stop the unit suite from leaking temp directories ~650 test call sites across the unit suite create scratch directories via fs.mkdtemp(path.join(os.tmpdir(), ...)) or shared factories (makeSessionStore) with no cleanup, ever. Over time this accumulated 1.16M+ orphaned directories in the real system tmpdir, slow enough to make tools that enumerate $TMPDIR at startup (e.g. opencode) take 1-2 minutes to launch. Rather than migrate every call site, redirect os.tmpdir() itself for the lifetime of the whole `vitest run` invocation: scripts/vitest-tmpdir-global-setup.ts wires in as vitest's globalSetup/globalTeardown, points TMPDIR at one /tmp-rooted directory (verified: env mutations here propagate to every forked worker, confirmed empirically), and removes it in one recursive rm after every worker across every project finishes. Since os.tmpdir() reads TMPDIR on every call, this covers all ~650 call sites without touching any of them. Rooted at /tmp rather than nested inside the current (already deep, on macOS) os.tmpdir(): that broke real AF_UNIX socket tests (runner-usbmux.test.ts) by pushing socket paths past the 104-byte sun_path limit. A per-file afterAll hook was tried first but proved unreliable — 5 of 7 workers in one run never ran it before their process was torn down; the global setup/teardown pair (one process, confirmed single execution) is the mechanism that's actually guaranteed to run once. Also adds: - scripts/check-tmpdir-leaks.ts: CI/local guard asserting no agent-device-test-run-* directory survives a run (a leftover one means a worker was killed before cleanup could run). - src/__tests__/test-utils/tmp-dir.ts: documented mkdtempForTest / mkdtempForTestSync helpers, the discoverable way to get a scratch dir going forward (mirrors the src/utils/exec.ts pattern for node:child_process). - scripts/check-test-tmpdir-helper.ts: ratchet guard capping raw fs.mkdtemp/mkdtempSync call sites in test files at today's count (632); it can only shrink as call sites migrate to the helper. * fix: make check-tmpdir-leaks scan the same root the fix actually uses check-tmpdir-leaks.ts was scanning os.tmpdir() for leftover run directories, but vitest-tmpdir-global-setup.ts creates them under a hard-coded /tmp. On macOS those are different paths (TMPDIR is a deep per-user /var/folders/.../T/ directory) — the guard could never find a leak on the exact platform the original leak happened on, only on Linux CI where os.tmpdir() already is /tmp. Export TEST_RUN_TMP_ROOT and TEST_RUN_TMP_PREFIX from the global-setup module and import them in the leak check instead of recomputing a path that can drift. Switched from a fixed pid-based directory name to fs.mkdtempSync so a same-named leftover from a prior killed run (or, on a shared machine, another user) can't collide with a live run. Also fixes two stale comments (in this file and ci.yml) that still described the per-file afterAll hook design that was abandoned in favor of the global setup/teardown pair, and notes the check only covers vitest runs, not the node --test lanes (test:smoke, test:integration:node). Verified live: with the old code the guard reported no leaks even with a real orphaned /tmp/agent-device-test-run-* directory present (left by a command that got killed mid-run); with this fix it correctly found and reported it. * simplify: drop the tmpdir ratchet guard, keep the leak check local-only Two guards were more than this needed: - check:test-tmpdir-helper (ratchet on raw fs.mkdtemp call counts) protects nothing a bug could actually trigger — the leak is already fixed architecturally regardless of call-site count, so this was pure style/discoverability nudging. Dropped the script and its check:tooling/CI wiring; kept mkdtempForTest/mkdtempForTestSync in tmp-dir.ts as the documented option without enforcing it. - check:tmpdir-leaks in CI added little: GitHub-hosted runners are destroyed after each job, so a leftover directory there is harmless by construction, and a worker getting killed mid-run would already surface as a job failure some other way. Its real value is local, on the long-lived dev machines where the original leak actually accumulated — kept it wired into check:unit, dropped the CI step. * fix: don't flag a concurrent vitest run's tmpdir as a leak check-tmpdir-leaks.ts reported every agent-device-test-run-* directory as a leak, but a concurrent vitest run in another worktree legitimately keeps its own directory present until its own teardown finishes. On a machine that regularly runs several worktrees at once, that made check:unit fail on unrelated in-progress work. Embed the owning process's pid in the directory name (still random- suffixed via mkdtempSync, so same-pid reuse across separate runs can't collide) and have the leak check skip any directory whose pid is still alive (process.kill(pid, 0)) — only directories whose owning process already exited without running its globalTeardown are real leaks. Split the pure logic into check-tmpdir-leaks-model.ts (findLeakedRunDirectories, with an injectable liveness check for testing) so it has a real regression suite, including the concurrent-run case, instead of only being exercised by hand. * refactor: migrate raw fs.mkdtemp call sites to mkdtempForTest(Sync) Migrates 629 raw fs.mkdtemp(Sync)(path.join(os.tmpdir(), PREFIX)) call sites across 168 test files to the mkdtempForTest / mkdtempForTestSync helpers (src/__tests__/test-utils/tmp-dir.ts), so there's one documented, discoverable way to get a scratch dir in a test — cleanup already didn't depend on the call-site shape (the global TMPDIR redirect covers any of them), this is purely for consistency and discoverability, same reasoning as src/utils/exec.ts for node:child_process. Existing manual per-test cleanup (fs.rm in finally/afterEach/onTestFinished blocks) is untouched — the global teardown is a fallback for killed workers, not a replacement for tests cleaning up after themselves. Migrated with a one-off AST-based codemod (oxc-parser, since regex mismatched multi-line calls and complex prefix expressions like `options?.tempPrefix ?? 'default-'`) rather than by hand across 168 files. The codemod isn't included — it doesn't need to survive this commit. Caught and fixed one real bug in it during review: a small number of files declare a second import statement later in the file, after some of the matched call sites, which broke a naive "insert after the textually-last ImportDeclaration" placement; fixed to insert after the top contiguous import block instead, plus a self-check that re-parses every generated file before writing it. Also fixes 5 fallow dead-code findings the branch introduced: the vitest globalSetup functions (setup/teardown) are only referenced by the config-string path vitest.config.ts hands to globalSetup, invisible to static analysis — suppressed with the documented convention. isProcessAlive didn't need to be exported (nothing outside the module uses it). And dropped a barrel re-export of the new helpers from test-utils/index.ts: nothing actually imports through the barrel (matching the existing makeSessionStore convention, which is imported directly from store-factory.ts everywhere despite also being barrel-exported), so the re-export was genuinely dead. Documents the convention in docs/agents/testing.md. Verified: full unit suite (5308 tests) passes except the one pre-existing, unrelated package-exports.test.ts failure; typecheck, lint, and format all clean; fallow audit clean against the PR base. * fix: correct fallow suppression token and drop unused barrel re-export These were meant to be part of 397cdd6d7 (verified locally before that commit) but didn't actually get staged — caught by CI's Fallow Code Quality check re-running against the pushed commit, which still had the plural 'unused-exports' token (fallow expects singular 'unused-export') and the dead barrel re-export. * fix: migrate the two mkdtemp call sites the rebase silently reintroduced Rebasing onto main pulled in #1594's two new test cases in this file, added independently of this branch's migration, still using raw fs.mkdtempSync(path.join(os.tmpdir(), ...)). Git's line-based merge found no textual conflict with this branch's removal of the os import (the changes touch non-overlapping regions), so it silently produced a file that doesn't typecheck. Migrated both to mkdtempForTestSync for consistency with the rest of the file, caught by CI's Typecheck, Fallow Code Quality, and FreeRange checks re-running against the pushed commit. * test: pin Vitest tmpdir lifecycle * fix: preserve the Swift cache across test runs |