mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
e8b779cb32
* fix(daemon): keep close-time script-save failures from leaking the session/device claim A close-time script write (implicit from `open --save-script`, or this close's own `--save-script`) that refuses to publish (e.g. a no-clobber target-exists AppError) threw uncaught out of `handleCloseCommand`, skipping lease release, device-claim release, and `sessionStore.delete` entirely — while the `close` action had already been recorded with no rollback. Live-repro'd over the real CLI against an Android emulator: this single gap explained both symptoms split out of #1384 into #1391 — a lingering `DEVICE_IN_USE` claim after a failed `close`, and a published `.ad` rewritten with duplicated trailing `close` lines when the same close was retried (each attempt re-recorded a `close` action on top of the one never rolled back from the prior failure). Catch the write failure, roll back the just-recorded `close` action (mirroring the existing repair-armed commit-failure pattern), and let teardown (lease release, device-claim clear, session delete) complete regardless — exactly as an ordinary platform-close failure already doesn't block them. The failure is still surfaced to the caller, but after teardown, with a corrected hint: retrying the same close is no longer meaningful since the session is now gone. Fixes #1391 * refactor(daemon): shrink handleCloseCommand/runSessionCloseTeardown under fallow's complexity gate CI's fallow code-quality check flagged handleCloseCommand (126 lines, 19 cyclomatic / 16 cognitive) and runSessionCloseTeardown (73 lines) as exceeding the large-function/high-complexity thresholds after the prior commit's fix. Extract runCloseTeardownAndRelease (teardown + lease release + claim clear + delete + ordered error surfacing) and buildCloseSuccessResponse (final response shaping) out of handleCloseCommand, and finalizeOrdinaryCloseScript out of runSessionCloseTeardown. No behavior change — same control flow, split into named, independently-readable steps; fallow now reports 0 complexity findings for this diff. * fix(daemon): preserve the write error's structured details in the close-time save failure Review feedback on #1392 (thymikee): toOrdinaryCloseSaveScriptFailure rebuilt the AppError from only the original message, dropping its machine-readable details.reason ("script_target_exists"), details.path, and cause. A caller dispatching on those fields (or reading the CLI's --json error.details) lost them even though the underlying write failure carried them. Preserve the original error's details/cause, overriding only the close-specific hint and retriable:false. Extends the #1391 regression test to assert the routed close response still carries reason/path. * refactor(daemon): drop the vestigial close-time rollback, add router-level #1391 coverage Review feedback on #1392 (thymikee), P2 items: - The close-time save-script failure's session.actions rollback (finalizeOrdinaryCloseScript) was left over from an earlier design where a failed save could keep the session alive for retry. It never does now — runCloseTeardownAndRelease always tears the session down regardless of the outcome — so there is no surviving session for a later write to duplicate the close action on. Drop the rollback; the durable events.ndjson entry (which the rollback never touched anyway) and the in-memory action now agree, both accurately recording that the close happened. - Add a request-router-level regression (request-router-typed-error.test.ts, alongside the existing repair-close BLOCKER 2 test it mirrors) proving the normalized JSON error shape a real client sees: top-level retriable:false, details.reason/path preserved, and the session torn down — not just that handleCloseCommand throws the right AppError when called directly. * test(daemon): assert the durable close event survives a failed close-time save Review feedback on #1392 (thymikee), final P2 item: the previous commit removed the actions rollback because there's no surviving session to duplicate the close action on, but nothing actually asserted the durable events.ndjson action.recorded:close event stays put. Flush and read it back so a future rollback or event-order change can't silently recreate the in-memory/durable mismatch the removed rollback used to paper over asymmetrically. * test(daemon): assert the retained session's in-memory close action, not just the durable event Review feedback on #1392 (thymikee): the durable-event assertion alone doesn't catch a reintroduced session.actions.length = actionsBeforeClose rollback, because that event is queued (and durable) before the write even attempts — a regression there would leave the assertion passing while silently reintroducing the in-memory/durable mismatch. Retain the session object past handleCloseCommand (store.delete only drops the map entry, not the object a local variable still points at) and assert its actions array contains exactly one close entry, matching the durable event count. Verified by temporarily reintroducing the old rollback locally: this assertion fails (0 !== 1) where the prior durable-only check did not, then reverted. * refactor(daemon): model repair close retry as receipt * refactor(daemon): merge blockingError to state, not explain, the save-script exclusion Following up on the comment-trimming pass already on this branch: the device-claim condition (!platformCloseError && !cleanupAggregate) and the two-line throw sequence right below it both needed a paragraph explaining why saveScriptError is excluded from one but not the other. Merge platformCloseError and cleanupAggregate into a single named blockingError — its name now states the exclusion the comment used to argue for, and the throw sequence collapses from two ifs to one. Trimmed the remaining long docblocks in this file the same way: state what's non-obvious in 1-3 lines instead of re-deriving it in prose. * refactor(daemon): clarify close script finalization
462 lines
18 KiB
JavaScript
462 lines
18 KiB
JavaScript
import {
|
|
AMBIGUOUS_MATCH_SAMPLE,
|
|
APP_NOT_INSTALLED_SAMPLE,
|
|
DEVICE_IN_USE_SAMPLE,
|
|
NOT_SETTLED_SAMPLE,
|
|
SETTLE_DIFF_SAMPLE,
|
|
SETTLE_DIFF_SAMPLE_NOTES,
|
|
SETTLE_TAIL_SAMPLE,
|
|
STALE_REF_SAMPLE,
|
|
sampleText,
|
|
} from './help-conformance-sample-outputs.mjs';
|
|
|
|
// Raw-coordinate fallback the quiz cases forbid: a click/fill/press targeting
|
|
// bare numbers instead of a ref or selector.
|
|
const RAW_COORDINATE_TARGET =
|
|
/(?:^|\n)(?:agent-device\s+)?(?:click|fill|press)\s+-?\d+(?:\.\d+)?\s+-?\d+(?:\.\d+)?/i;
|
|
|
|
function quiz(sample, question) {
|
|
return `Read this previous agent-device output, then plan the next command:
|
|
|
|
${sampleText(sample)}
|
|
|
|
${question}`;
|
|
}
|
|
|
|
// Case docs reference help topic ids from src/cli/parser/cli-help.ts plus the
|
|
// synthetic '--help:first30' first-screen slice. Topic coverage is enforced by
|
|
// scripts/__tests__/help-conformance-topic-coverage.test.ts: a new help topic
|
|
// needs a case here or an explicit waiver there.
|
|
export const CASES = [
|
|
{
|
|
id: 'raw-first-screen-bluesky',
|
|
docs: ['--help:first30'],
|
|
task: 'Plan commands to open an already installed Bluesky app, search "callstack", open the @callstack.com account, press Follow or Following, and close.',
|
|
expectations: [
|
|
'validPlanCommands',
|
|
'fullPrefix',
|
|
'usesSnapshotI',
|
|
'usesSettleOnMutations',
|
|
'noWaitStable',
|
|
],
|
|
},
|
|
{
|
|
id: 'metamorphic-community-search',
|
|
docs: ['--help:first30'],
|
|
task: 'Plan commands to open the already installed app com.example.community, open the visible Discover destination, fill the People search field with "react native", open the @react.dev account, press Connect or Connected, and close.',
|
|
expectations: [
|
|
'validPlanCommands',
|
|
'fullPrefix',
|
|
'usesSnapshotI',
|
|
'usesSettleOnMutations',
|
|
'noWaitStable',
|
|
'opensAndCloses',
|
|
],
|
|
matchers: [
|
|
{
|
|
id: 'opensKnownCommunityApp',
|
|
pattern: /\bagent-device\s+open\s+com\.example\.community\b/i,
|
|
},
|
|
{
|
|
id: 'fillsExpectedSearch',
|
|
pattern: /\bagent-device\s+fill\b[^\n]*(?:"react native"|'react native')[^\n]*--settle\b/i,
|
|
},
|
|
{
|
|
id: 'usesLiteralHandleSelector',
|
|
pattern:
|
|
/\bagent-device\s+(?:press|click|tap)\b[^\n]*(?:label|text)=@react\.dev\b[^\n]*--settle\b/i,
|
|
},
|
|
],
|
|
forbidden: [
|
|
{
|
|
id: 'noBlueskyLeakage',
|
|
pattern: /(?:bluesky|callstack|@e64|@callstack\.com)/i,
|
|
},
|
|
{ id: 'noRawCoordinateTarget', pattern: RAW_COORDINATE_TARGET },
|
|
],
|
|
},
|
|
{
|
|
id: 'manual-qa-bluesky-script',
|
|
docs: ['--help:first30', 'manual-qa'],
|
|
task: 'You are following a manual QA script: on Bluesky, open Search, search "callstack", open @callstack.com, press Follow or Following, verify the button state changed, then close. Plan commands only.',
|
|
expectations: [
|
|
'validPlanCommands',
|
|
'fullPrefix',
|
|
'usesSnapshotI',
|
|
'usesSettleOnMutations',
|
|
'verifiesNamedExpectation',
|
|
'noWaitStable',
|
|
],
|
|
},
|
|
{
|
|
id: 'dogfood-mode',
|
|
docs: ['--help:first30', 'dogfood'],
|
|
task: 'Plan a short dogfood pass for the logged-in iOS shop app com.example.shop. Exercise the visible Home, Search, and Cart destinations and capture reproducible evidence for any issue found.',
|
|
allowedExternalCommands: ['mkdir'],
|
|
expectations: [
|
|
'validPlanCommands',
|
|
'fullPrefix',
|
|
'usesSnapshotI',
|
|
'usesSettleOnMutations',
|
|
'usesDogfoodEvidence',
|
|
'opensAndCloses',
|
|
],
|
|
matchers: [
|
|
{ id: 'opensKnownDogfoodApp', pattern: /\bagent-device\s+open\s+com\.example\.shop\b/i },
|
|
{
|
|
id: 'capturesStrongIssueEvidence',
|
|
pattern: /\b(?:screenshot\b[^\n]*--overlay-refs|record\s+start\b|logs\s+mark\b)/i,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'engineering-validate-mode',
|
|
docs: ['--help:first30', 'validate'],
|
|
task: 'Plan commands to validate a TypeScript-only CLI/runtime change to settled press output against the already installed iOS Settings app. Use the known General control, prove current built output is running, and clean up. Swift runner code did not change.',
|
|
allowedExternalCommands: ['pnpm'],
|
|
expectations: [
|
|
'validPlanCommands',
|
|
'fullPrefix',
|
|
'usesSnapshotI',
|
|
'usesSettleOnMutations',
|
|
'usesValidationPrep',
|
|
'opensAndCloses',
|
|
],
|
|
matchers: [
|
|
{
|
|
id: 'opensSettings',
|
|
pattern: /\bagent-device\s+open\s+(?:settings|com\.apple\.Preferences)\b/i,
|
|
},
|
|
],
|
|
forbidden: [
|
|
{
|
|
id: 'avoidsUnrelatedPlatformBuild',
|
|
pattern: /\bpnpm\s+(?:run\s+)?build:(?:android|xcuitest)\b/i,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'tv-focus-first-remote',
|
|
docs: ['--help:first30', 'tv'],
|
|
task: 'On an Android TV emulator, open the installed app com.example.tvhub, move focus to the "Continue watching" tile two positions to the right of the initially focused tile, activate it, verify the player screen appeared, and close. Plan commands only.',
|
|
expectations: ['validPlanCommands', 'fullPrefix', 'usesSnapshotI', 'opensAndCloses'],
|
|
matchers: [
|
|
{ id: 'movesFocusWithRemote', pattern: /\btv-remote\s+press\s+right\b/i },
|
|
{ id: 'activatesWithSelect', pattern: /\btv-remote\s+press\s+select\b/i },
|
|
{ id: 'verifiesOutcome', pattern: /\b(?:is\s+focused|wait\b|find\b)/i },
|
|
],
|
|
forbidden: [
|
|
// Focus-first surface: activation goes through tv-remote select, not a
|
|
// coordinate/element tap (help tv "Do not assume press/click @ref works").
|
|
{
|
|
id: 'noDirectTapActivation',
|
|
pattern: /(?:^|\n)agent-device\s+(?:press|click|tap)\s/i,
|
|
},
|
|
{ id: 'noRawAdbKeyevent', pattern: /\badb\s+shell\s+input\b/i },
|
|
],
|
|
},
|
|
{
|
|
id: 'ios-system-ui-widget-flow',
|
|
docs: ['--help:first30', 'ios-system-ui'],
|
|
task: 'On an iOS simulator, add the Calendar widget from SpringBoard, capture visual evidence of the placed widget, return to the installed app com.example.calendar, and close. Plan commands only.',
|
|
expectations: ['validPlanCommands', 'fullPrefix', 'usesSnapshotI', 'opensAndCloses'],
|
|
matchers: [
|
|
{
|
|
id: 'bindsSessionToSpringBoard',
|
|
pattern: /\bagent-device\s+open\s+com\.apple\.springboard\b[^\n]*--platform\s+ios\b/i,
|
|
},
|
|
{
|
|
id: 'entersEditModeWithCoordinateLongpress',
|
|
pattern: /\bagent-device\s+longpress\s+-?\d+(?:\.\d+)?\s+-?\d+(?:\.\d+)?\b/i,
|
|
},
|
|
{
|
|
id: 'refreshesSnapshotAfterEnteringEditMode',
|
|
pattern: /\blongpress\b[\s\S]*\n[^\n]*\bsnapshot\s+-i\b/i,
|
|
},
|
|
{
|
|
id: 'usesScreenshotForSparseGalleryResult',
|
|
pattern:
|
|
/\bagent-device\s+screenshot\b[\s\S]*\n[^\n]*\bagent-device\s+press\s+-?\d+(?:\.\d+)?\s+-?\d+(?:\.\d+)?\b/i,
|
|
},
|
|
{
|
|
id: 'returnsToAppUnderTest',
|
|
pattern: /\bagent-device\s+open\s+com\.example\.calendar\b[^\n]*--platform\s+ios\b/i,
|
|
},
|
|
],
|
|
forbidden: [
|
|
{
|
|
id: 'noRawSimulatorControl',
|
|
pattern: /(?:^|\n)(?:xcrun\s+simctl|idb|maestro)\b/i,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'web-managed-backend-loop',
|
|
docs: ['--help:first30', 'web'],
|
|
task: 'On a fresh machine that has never run web automation, plan commands to set up and verify the managed web backend, open https://shop.example/login, fill the Email field with "qa@example.com", press the "Sign in" button, verify the "Welcome back" text appears, capture a screenshot to ./artifacts/web-login.png, and close. Plan commands only.',
|
|
expectations: ['validPlanCommands', 'fullPrefix', 'usesSnapshotI', 'opensAndCloses'],
|
|
matchers: [
|
|
{
|
|
id: 'setsUpBackendBeforeOpen',
|
|
pattern: /\bagent-device\s+web\s+setup\b[\s\S]*\n[^\n]*\bopen\s+https:\/\//i,
|
|
},
|
|
{ id: 'verifiesBackendWithDoctor', pattern: /\bagent-device\s+web\s+doctor\b/i },
|
|
{ id: 'usesWebPlatform', pattern: /--platform\s+web\b/i },
|
|
{ id: 'verifiesWelcomeText', pattern: /\b(?:wait|is|find)\b[^\n]*welcome/i },
|
|
],
|
|
forbidden: [
|
|
// help web: native mobile/desktop setup commands are out of scope for
|
|
// --platform web sessions.
|
|
{
|
|
id: 'noNativeSetupCommands',
|
|
pattern: /(?:^|\n)agent-device\s+(?:boot|apps|install|alert|keyboard|perf|logs)\b/i,
|
|
},
|
|
{ id: 'noStandaloneAgentBrowser', pattern: /(?:^|\n)agent-browser\b/i },
|
|
],
|
|
},
|
|
{
|
|
id: 'react-native-overlay-before-tap',
|
|
docs: ['--help:first30', 'react-native'],
|
|
task: 'An Expo dev-client app on the iOS simulator shows a React Native warning overlay in the latest snapshot. Plan the commands that safely get past it and then press the control with id "submit-order". Plan commands only.',
|
|
expectations: ['validPlanCommands', 'fullPrefix', 'usesSettleOnMutations'],
|
|
matchers: [
|
|
{
|
|
id: 'usesDismissOverlayCommand',
|
|
pattern: /(?:^|\n)agent-device\s+react-native\s+dismiss-overlay\b/i,
|
|
},
|
|
{
|
|
id: 'refreshesRefsAfterDismiss',
|
|
pattern: /dismiss-overlay\b[\s\S]*\n[^\n]*\bsnapshot\s+-i\b/i,
|
|
},
|
|
{ id: 'pressesSubmitTarget', pattern: /(?:^|\n)agent-device\s+press\s+[^\n]*submit-order/i },
|
|
],
|
|
forbidden: [
|
|
// help react-native: never press warning/error overlay text manually;
|
|
// the dismiss-overlay command owns LogBox/RedBox targeting.
|
|
{
|
|
id: 'noManualOverlayPress',
|
|
pattern: /(?:^|\n)agent-device\s+(?:press|click)\s+[^\n]*(?:warning|error|logbox|redbox)/i,
|
|
},
|
|
{ id: 'noPlainReloadCommand', pattern: /(?:^|\n)agent-device\s+reload\b/i },
|
|
],
|
|
},
|
|
{
|
|
id: 'debugging-small-log-window',
|
|
docs: ['--help:first30', 'debugging'],
|
|
task: 'The "Load diagnostics" control (id "load-diagnostics") in the already-open iOS app intermittently fails. Plan commands to capture a small fresh log window plus request/response metadata around one reproduction. Plan commands only.',
|
|
expectations: ['validPlanCommands', 'fullPrefix'],
|
|
matchers: [
|
|
{
|
|
id: 'clearsAndRestartsLogs',
|
|
pattern: /(?:^|\n)agent-device\s+logs\s+clear\s+--restart\b/i,
|
|
},
|
|
{ id: 'marksBeforeRepro', pattern: /\blogs\s+mark\b/i },
|
|
{
|
|
id: 'reproducesTargetPress',
|
|
pattern: /(?:^|\n)agent-device\s+press\s+[^\n]*load-diagnostics/i,
|
|
},
|
|
{ id: 'readsLogPath', pattern: /\blogs\s+path\b/i },
|
|
{ id: 'dumpsNetworkMetadata', pattern: /\bnetwork\s+dump\b/i },
|
|
],
|
|
forbidden: [
|
|
{ id: 'noSessionReopen', pattern: /(?:^|\n)agent-device\s+open\b/i },
|
|
{ id: 'noSplitLogRestart', pattern: /\blogs\s+stop\b/i },
|
|
],
|
|
},
|
|
{
|
|
id: 'workflow-install-artifact-before-open',
|
|
docs: ['--help:first30', 'workflow'],
|
|
task: 'A local Android build artifact ./dist/app-release.apk contains the app com.example.orders, which is not yet on the emulator. Plan commands to get it running with fresh state and confirm its first screen shows "Orders". Plan commands only.',
|
|
expectations: ['validPlanCommands', 'fullPrefix', 'usesSnapshotI'],
|
|
matchers: [
|
|
{
|
|
id: 'installsIdThenArtifact',
|
|
pattern: /(?:^|\n)agent-device\s+install\s+com\.example\.orders\s+\S*app-release\.apk/i,
|
|
},
|
|
{
|
|
id: 'opensFreshAfterInstall',
|
|
pattern: /\binstall\b[\s\S]*\n[^\n]*\bopen\s+com\.example\.orders\b[^\n]*--relaunch\b/i,
|
|
},
|
|
{ id: 'verifiesFirstScreen', pattern: /\b(?:wait|find|is|get)\b[^\n]*orders/i },
|
|
],
|
|
forbidden: [
|
|
// help workflow: install for a first install; reinstall only when
|
|
// explicitly requested; never open an artifact path.
|
|
{ id: 'noReinstall', pattern: /(?:^|\n)agent-device\s+reinstall\b/i },
|
|
{ id: 'noOpenArtifactPath', pattern: /(?:^|\n)agent-device\s+open\s+[^\n]*\.apk\b/i },
|
|
],
|
|
},
|
|
// Next-command quiz cases: captured output (pinned to the real renderer by
|
|
// scripts/__tests__/help-conformance-sample-outputs.test.ts) plus a task,
|
|
// scored by regex instead of the named expectation scorers above.
|
|
{
|
|
id: 'settle-diff-is-observation',
|
|
docs: ['--help:first30'],
|
|
task: `You already ran this command and observed its settled output:
|
|
|
|
${sampleText(SETTLE_TAIL_SAMPLE)}
|
|
|
|
Use the output already shown to determine whether the feed-search UI is present, then close the session. What command should run next?`,
|
|
expectations: ['validPlanCommands', 'fullPrefix'],
|
|
matchers: [{ id: 'plansClose', pattern: /(?:^|\n)(?:agent-device\s+)?close\b/i }],
|
|
forbidden: [
|
|
{ id: 'noSnapshot', pattern: /\bsnapshot\b/i },
|
|
{ id: 'noWait', pattern: /\bwait\b/i },
|
|
{ id: 'noFind', pattern: /\bfind\b/i },
|
|
{ id: 'noGet', pattern: /\bget\b/i },
|
|
{ id: 'noIs', pattern: /\bis\b/i },
|
|
{ id: 'noPressOrClick', pattern: /\b(?:press|click)\b/i },
|
|
],
|
|
},
|
|
{
|
|
id: 'sample-output-settled-diff-next-target',
|
|
docs: ['--help:first30'],
|
|
task: quiz(
|
|
SETTLE_DIFF_SAMPLE,
|
|
'The task is to open the matching account result. What command should run next?',
|
|
),
|
|
expectations: ['validPlanCommands', 'fullPrefix'],
|
|
matchers: [
|
|
{ id: 'pressOrClickOrTap', pattern: /\b(?:press|click|tap)\b/i },
|
|
{ id: 'usesE64RefOrLabel', pattern: /@e64\b|label=(?:["']?@callstack\.com["']?)/i },
|
|
{ id: 'usesSettleFlag', pattern: /--settle\b/i },
|
|
],
|
|
forbidden: [
|
|
{ id: 'noSnapshot', pattern: /\bsnapshot\b/i },
|
|
{ id: 'noWaitStable', pattern: /wait\s+stable/i },
|
|
{ id: 'noFill', pattern: /\bfill\b/i },
|
|
{ id: 'noRawCoordinateTarget', pattern: RAW_COORDINATE_TARGET },
|
|
],
|
|
},
|
|
{
|
|
id: 'metamorphic-settled-diff-next-target-notes',
|
|
docs: ['--help:first30'],
|
|
task: quiz(
|
|
SETTLE_DIFF_SAMPLE_NOTES,
|
|
'The task is to open the matching list result. What command should run next?',
|
|
),
|
|
expectations: ['validPlanCommands', 'fullPrefix'],
|
|
matchers: [
|
|
{ id: 'pressOrClickOrTap', pattern: /\b(?:press|click|tap)\b/i },
|
|
{ id: 'usesE21RefOrLabel', pattern: /@e21\b|label=(?:["']?groceries list["']?)/i },
|
|
{ id: 'usesSettleFlag', pattern: /--settle\b/i },
|
|
],
|
|
forbidden: [
|
|
{ id: 'noSnapshot', pattern: /\bsnapshot\b/i },
|
|
{ id: 'noWaitStable', pattern: /wait\s+stable/i },
|
|
{ id: 'noFill', pattern: /\bfill\b/i },
|
|
{ id: 'noCallstackLeakage', pattern: /(?:callstack|@e64)/i },
|
|
{ id: 'noRawCoordinateTarget', pattern: RAW_COORDINATE_TARGET },
|
|
],
|
|
},
|
|
{
|
|
id: 'sample-output-not-settled-needs-observe',
|
|
docs: ['--help:first30'],
|
|
task: quiz(
|
|
NOT_SETTLED_SAMPLE,
|
|
'The next target is not known yet. What command should run next?',
|
|
),
|
|
expectations: ['validPlanCommands', 'fullPrefix'],
|
|
matchers: [
|
|
{
|
|
id: 'observesBeforeActing',
|
|
pattern: /(?:^|\n)(?:agent-device\s+)?(?:wait\b|snapshot\b[^\n]*-i\b)/i,
|
|
},
|
|
],
|
|
forbidden: [
|
|
{
|
|
id: 'noBareRefMutation',
|
|
pattern: /(?:^|\n)(?:agent-device\s+)?(?:press|click|fill|longpress)\s+@e\d+/i,
|
|
},
|
|
{ id: 'noRawCoordinateTarget', pattern: RAW_COORDINATE_TARGET },
|
|
],
|
|
},
|
|
{
|
|
id: 'sample-output-device-in-use-reuses-session',
|
|
docs: ['--help:first30'],
|
|
task: quiz(
|
|
DEVICE_IN_USE_SAMPLE,
|
|
'You are continuing the checkout flow that the "checkout" session was already running on this device. What command should run next?',
|
|
),
|
|
expectations: ['validPlanCommands', 'fullPrefix'],
|
|
matchers: [
|
|
{
|
|
id: 'retriesWithOwningSession',
|
|
pattern: /(?:^|\n)agent-device\s+press\b[^\n]*--session\s+checkout\b/i,
|
|
},
|
|
{ id: 'keepsSettle', pattern: /--settle\b/i },
|
|
],
|
|
forbidden: [
|
|
{ id: 'noClose', pattern: /(?:^|\n)agent-device\s+close\b/i },
|
|
{ id: 'noReopen', pattern: /(?:^|\n)agent-device\s+open\b/i },
|
|
{ id: 'noRawCoordinateTarget', pattern: RAW_COORDINATE_TARGET },
|
|
],
|
|
},
|
|
{
|
|
id: 'sample-output-stale-ref-resnapshots',
|
|
docs: ['--help:first30'],
|
|
task: quiz(
|
|
STALE_REF_SAMPLE,
|
|
'The Continue control this ref pointed at may have moved. What command should run next?',
|
|
),
|
|
expectations: ['validPlanCommands', 'fullPrefix'],
|
|
matchers: [
|
|
{ id: 'refreshesInteractiveRefs', pattern: /(?:^|\n)agent-device\s+snapshot\s+-i\b/i },
|
|
],
|
|
forbidden: [
|
|
{
|
|
id: 'noBareRefRetry',
|
|
pattern: /(?:^|\n)agent-device\s+(?:press|click|fill|longpress)\s+@e\d/i,
|
|
},
|
|
{ id: 'noReopen', pattern: /(?:^|\n)agent-device\s+open\b/i },
|
|
{ id: 'noRawCoordinateTarget', pattern: RAW_COORDINATE_TARGET },
|
|
],
|
|
},
|
|
{
|
|
id: 'sample-output-ambiguous-match-reobserves',
|
|
docs: ['--help:first30'],
|
|
task: quiz(
|
|
AMBIGUOUS_MATCH_SAMPLE,
|
|
'The intent is to follow the @callstack.com account row. The candidate refs were not shown. What command should run next?',
|
|
),
|
|
expectations: ['validPlanCommands', 'fullPrefix'],
|
|
matchers: [
|
|
{
|
|
id: 'reobservesOrNarrows',
|
|
pattern:
|
|
/(?:^|\n)agent-device\s+(?:snapshot\s+-i\b|(?:find|press|click)\s+[^\n]*(?:role=|id=|label="?@callstack\.com))/i,
|
|
},
|
|
],
|
|
forbidden: [
|
|
// The candidates live in error details the human output never printed,
|
|
// so a ref-targeting command here would be a guess.
|
|
{ id: 'noGuessedRef', pattern: /(?:^|\n)agent-device\s+(?:press|click)\s+@e\d/i },
|
|
{
|
|
id: 'noVerbatimRetry',
|
|
pattern: /(?:^|\n)agent-device\s+find\s+text\s+"?follow"?\s+press\b/i,
|
|
},
|
|
{ id: 'noRawCoordinateTarget', pattern: RAW_COORDINATE_TARGET },
|
|
],
|
|
},
|
|
{
|
|
id: 'sample-output-app-not-installed-discovers-first',
|
|
docs: ['--help:first30'],
|
|
task: quiz(
|
|
APP_NOT_INSTALLED_SAMPLE,
|
|
'The goal is still to open the shop app on this simulator; no build artifact was provided. What command should run next?',
|
|
),
|
|
expectations: ['validPlanCommands', 'fullPrefix'],
|
|
matchers: [{ id: 'discoversInstalledApps', pattern: /(?:^|\n)agent-device\s+apps\b/i }],
|
|
forbidden: [
|
|
{ id: 'noBlindReopen', pattern: /(?:^|\n)agent-device\s+open\s+"?shoply\b/i },
|
|
// No artifact exists to install; inventing one is the failure mode help
|
|
// workflow forbids ("Do not open artifact paths or invent package ids").
|
|
{
|
|
id: 'noInventedInstall',
|
|
pattern: /(?:^|\n)agent-device\s+(?:install|install-from-source)\b/i,
|
|
},
|
|
{ id: 'noRawCoordinateTarget', pattern: RAW_COORDINATE_TARGET },
|
|
],
|
|
},
|
|
];
|