Skip to content

Commit b8366d2

Browse files
committed
test(skillgym): make SpringBoard widget case assertions sequence-aware
Presence-only matchers let an out-of-order or invented-ref plan pass: one early unrelated snapshot, a selector-based longpress instead of the required empty-space coordinate long-press, or a coordinate press standing in for any ref-driven step could all satisfy the old case. Add a small sequentialSteps mechanism to makeCase and bind each fresh snapshot, ref (@e30/@e31/@e35), and coordinate fallback to its documented SpringBoard step in the order the workflow actually requires. Verified locally against six synthetic finalOutput plans, including the reviewer's exact described bad pattern (early snapshot + selector-based longpress + invented refs + unrelated coordinate action), which now fails as expected; two valid orderings (with and without an extra leading snapshot) still pass.
1 parent fbbcd35 commit b8366d2

1 file changed

Lines changed: 94 additions & 13 deletions

File tree

test/skillgym/suites/agent-device-smoke-suite.ts

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,43 @@ const IOS_TEST_APP_DEV_BUILD_OPEN = new RegExp(
451451
'i',
452452
);
453453

454+
interface SequentialCommandStep {
455+
label: string;
456+
regex: RegExp;
457+
}
458+
459+
/**
460+
* Enforces that each step's pattern appears, in order, at or after the position where the
461+
* previous step matched — unlike the presence-only OutputMatcher checks above, this catches a
462+
* plan that satisfies every individual requirement out of order (for example an early,
463+
* unrelated snapshot standing in for the one that must follow a specific action).
464+
*/
465+
function assertSequentialCommandSteps(
466+
finalOutput: string,
467+
steps: readonly SequentialCommandStep[],
468+
caseLabel: string,
469+
) {
470+
const lines = normalizedFinalOutput(finalOutput)
471+
.split('\n')
472+
.map((line) => line.trim())
473+
.filter(Boolean);
474+
let cursor = 0;
475+
for (const step of steps) {
476+
let matchIndex = -1;
477+
for (let i = cursor; i < lines.length; i++) {
478+
if (step.regex.test(lines[i]!)) {
479+
matchIndex = i;
480+
break;
481+
}
482+
}
483+
assert.ok(
484+
matchIndex !== -1,
485+
`${caseLabel}: expected step "${step.label}" at or after position ${cursor}. Observed commands:\n${lines.join('\n')}`,
486+
);
487+
cursor = matchIndex + 1;
488+
}
489+
}
490+
454491
function makeCase(options: {
455492
id: string;
456493
contract: string[];
@@ -461,6 +498,7 @@ function makeCase(options: {
461498
strictFinalOutput?: boolean;
462499
allowOnlyLocalCliHelpCommands?: boolean;
463500
finalOutputInstructions?: string;
501+
sequentialSteps?: readonly SequentialCommandStep[];
464502
}): Case {
465503
return {
466504
id: options.id,
@@ -484,6 +522,9 @@ function makeCase(options: {
484522
if (options.allowOnlyLocalCliHelpCommands) {
485523
assertOnlyLocalCliHelpCommands(report);
486524
}
525+
if (options.sequentialSteps) {
526+
assertSequentialCommandSteps(ctx.finalOutput(), options.sequentialSteps, options.id);
527+
}
487528
},
488529
};
489530
}
@@ -2641,24 +2682,64 @@ Do not combine final commands with shell operators such as &&, ||, pipes, or sem
26412682
'SpringBoard control labels vary by iOS version and locale; do not assume fixed English strings such as "Add Widget" or "Edit" — use the refs above, sourced from a fresh snapshot -i, instead',
26422683
],
26432684
task: 'Plan the commands to open SpringBoard, enter edit mode, open the widget gallery with @e30, search it for "Calendar" using @e31, select the result using the documented coordinate fallback for the unlabeled search-result row, place the widget with @e35, reopen Agent Device Tester to resume app automation, and close the session.',
2644-
outputs: [
2645-
plannedCommand('open com.apple.springboard'),
2646-
/--platform ios/i,
2647-
/snapshot\b[^\n]*-i\b/i,
2648-
plannedCommand('longpress'),
2649-
/(?:^|\n)(?:agent-device\s+)?(?:press|click)\s+@e30\b/i,
2650-
/(?:^|\n)(?:agent-device\s+)?fill\s+@e31\b[^\n]*Calendar/i,
2651-
/(?:^|\n)(?:agent-device\s+)?screenshot\b/i,
2652-
RAW_COORDINATE_TARGET,
2653-
/(?:^|\n)(?:agent-device\s+)?(?:press|click)\s+@e35\b/i,
2654-
plannedCommand('open com.callstack.agentdevicelab'),
2655-
plannedCommand('close'),
2656-
],
2685+
outputs: [/--platform ios/i],
26572686
forbiddenOutputs: [
26582687
plannedCommandAlternatives(['widget', 'widget add']),
26592688
/widget\s+add/i,
26602689
/label=["'](?:Add Widget|Edit|Done)["']/i,
26612690
],
2691+
// Presence-only matchers would let an out-of-order or invented-ref plan pass (an early
2692+
// unrelated snapshot, a selector-based longpress instead of the required empty-space
2693+
// coordinate long-press, or a coordinate press standing in for any of the ref-driven
2694+
// steps). Bind each fresh snapshot, ref, and coordinate fallback to its documented
2695+
// SpringBoard step in the order the workflow actually requires.
2696+
sequentialSteps: [
2697+
{
2698+
label: 'open SpringBoard',
2699+
regex: /^(?:agent-device\s+)?open\s+com\.apple\.springboard\b/i,
2700+
},
2701+
{
2702+
label: 'empty-space long-press to enter edit mode (raw coordinates, not a selector)',
2703+
regex: /^(?:agent-device\s+)?longpress\s+-?\d+(?:\.\d+)?\s+-?\d+(?:\.\d+)?\b/i,
2704+
},
2705+
{
2706+
label: 'fresh snapshot -i exposing @e30',
2707+
regex: /^(?:agent-device\s+)?snapshot\b.*-i\b/i,
2708+
},
2709+
{
2710+
label: 'press/click @e30 to open the widget gallery',
2711+
regex: /^(?:agent-device\s+)?(?:press|click)\s+@e30\b/i,
2712+
},
2713+
{
2714+
label: 'fresh snapshot -i exposing @e31',
2715+
regex: /^(?:agent-device\s+)?snapshot\b.*-i\b/i,
2716+
},
2717+
{
2718+
label: 'fill @e31 with "Calendar" to search the gallery',
2719+
regex: /^(?:agent-device\s+)?fill\s+@e31\b.*calendar/i,
2720+
},
2721+
{
2722+
label: 'screenshot before the coordinate fallback',
2723+
regex: /^(?:agent-device\s+)?screenshot\b/i,
2724+
},
2725+
{
2726+
label: 'raw-coordinate press on the unlabeled search-result row',
2727+
regex: /^(?:agent-device\s+)?(?:press|click)\s+-?\d+(?:\.\d+)?\s+-?\d+(?:\.\d+)?\b/i,
2728+
},
2729+
{
2730+
label: 'fresh snapshot -i exposing @e35',
2731+
regex: /^(?:agent-device\s+)?snapshot\b.*-i\b/i,
2732+
},
2733+
{
2734+
label: 'press/click @e35 to place the widget',
2735+
regex: /^(?:agent-device\s+)?(?:press|click)\s+@e35\b/i,
2736+
},
2737+
{
2738+
label: 'reopen Agent Device Tester',
2739+
regex: /^(?:agent-device\s+)?open\s+com\.callstack\.agentdevicelab\b/i,
2740+
},
2741+
{ label: 'close the session', regex: /^(?:agent-device\s+)?close\b/i },
2742+
],
26622743
}),
26632744
makeCase({
26642745
id: 'replay-maintenance-update',

0 commit comments

Comments
 (0)