mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
3bf3ff130a
* diag: instrument Linux CI to gather evidence for #1935 input/a11y defects Temporary — adds a diagnostic step that dumps raw AT-SPI interfaces/actions for gnome-calculator's digit buttons, tests a raw xdotool click at a button's own rect (bypassing our promotion logic), and isolates the typed '=' character in several configurations. Will be removed once the real fixes land. * diag: harden diagnostic step against bash -e and AT-SPI registration races The prior version crashed 7s in: GH Actions runs steps under bash -e, and an unguarded python3 heredoc threw (iterating a dict instead of a list when the app wasn't found yet), aborting the rest of the script silently under continue-on-error. Guards every fallible command, and replaces the fixed 2s sleep with inspect.py's own poll-until-found loop. * diag: test WINDOW coordtype and static Text.get_text call (round 3) Round 2 proved Component.get_extents(SCREEN) returns (0,0) for every non-toplevel widget (real click miss confirmed on-screen), and Text.get_text() throws — a documented PyGObject binding collision with the deprecated 1-arg Accessible.get_text(). This narrows to the two candidate fixes before writing them: does CoordType.WINDOW give usable relative offsets, and does Atspi.Text.get_text(accessible, ...) (static call) return the real typed text. * fix(linux): resolve click-miss, dropped '=', and GTK4 text exposure defects Three defects surfaced by CI on #1935 (Linux Smoke lane), all confirmed live via instrumented CI runs before being fixed here: 1. Click misses its target: Component.get_extents(Atspi.CoordType.SCREEN) returns (0, 0) as the origin for every non-toplevel widget under this GTK4 build — confirmed by a raw click at the computed rect center landing on the window's own header-bar button instead of the intended digit button. CoordType.WINDOW gives correct, distinct per-widget offsets, so get_rect() now computes screen-absolute rects as that offset plus the enclosing top-level frame's own (correct) screen origin, threaded through traverse_node() alongside the existing window-title tracking. Complementary hardening: role "label" is now excluded from `hittable`, since GTK4 wraps every button's caption in a same-rect "label" child, and the shared cross-platform promotion logic in interaction-targeting.ts would otherwise retarget a click from the button onto that non-interactive label. 2. Typed '=' never arrives: a single isolated synthetic keystroke sent right after a focus change is unreliably delivered — confirmed live, both `xdotool type -- "="` and `xdotool key equal` sent alone produced no character at all, while multi-character bursts always landed in full. typeLinux and sendKey now wait a short settle margin before dispatching to xdotool/ydotool, absorbing the race regardless of which action last changed focus. 3. GTK4 apps expose no editable text: accessible.get_text_iface().get_text() throws "Atspi.Accessible.get_text() takes exactly 1 argument (3 given)" — a documented PyGObject binding collision between Text.get_text and the deprecated 1-argument Accessible.get_text, silently swallowed as "no text" by the broad exception handler. get_text_value() now calls the unbound Atspi.Text.get_text(accessible, ...) form, which correctly returns the real content. The Linux smoke replay is restored to exercise all three fixes together (click a resolved digit button, type a full calculation including the '=' keystroke, wait on the computed result through the tree) instead of staying at the weakened, contract-tier assertions the defects had forced. The coverage manifest promotes click and type from command-contract to live accordingly. * fix(linux): drop unproven keyboard-settle and hittable changes per review Addresses thymikee's review on #1949 (both points correct): P1: the keyboard settle (typeLinux/sendKey) was unjustified. The cited diagnostic evidence for a dropped '=' actually shows the opposite — "100+55=" and "5=5" both computed correctly with zero settle, proving '=' was delivered in every multi-character burst tested. Sending '=' alone to an empty entry showing a blank display is normal calculator semantics (nothing to evaluate), not a lost keystroke. The likelier explanation for the original "100+55" screenshot (run 32487868346) is that its attempt-3 hit the already-fixed mousemove --sync hang, not an independent keyboard-dispatch defect. Reverted; no keyboard-dispatch change was needed. P2: the `role_name != "label"` hittable narrowing was extra surface beyond what the click-miss fix required. The corrected AT-SPI coordinates alone fix the observed miss — the button and its same-rect label child resolve to nearly identical centers, so descendant promotion still lands inside the button either way, and the replay can't distinguish which node it actually targeted. Reverted; only the coordinate fix remains.
247 lines
10 KiB
TypeScript
247 lines
10 KiB
TypeScript
import { PUBLIC_COMMANDS } from '../../../src/command-catalog.ts';
|
|
import { buildCoverageClassificationSummary } from '../support/coverage-classification.ts';
|
|
|
|
type PublicCommand = (typeof PUBLIC_COMMANDS)[keyof typeof PUBLIC_COMMANDS];
|
|
|
|
type RepositoryEvidence = {
|
|
path: string;
|
|
test: string;
|
|
};
|
|
|
|
type CapabilityDeclarationEvidence = RepositoryEvidence & {
|
|
declaration: string;
|
|
};
|
|
|
|
export type LinuxPlatformCoverageEntry =
|
|
| {
|
|
assertion: string;
|
|
level: 'live' | 'command-contract';
|
|
owner: RepositoryEvidence;
|
|
}
|
|
| {
|
|
assertion: string;
|
|
level: 'capability-denial';
|
|
owner: CapabilityDeclarationEvidence;
|
|
}
|
|
| {
|
|
assertion: string;
|
|
level: 'known-gap';
|
|
trackingIssue: number;
|
|
};
|
|
|
|
export const LINUX_COVERAGE_GAP_ISSUE = 1915;
|
|
|
|
export const LINUX_REPLAY_EVIDENCE: RepositoryEvidence = {
|
|
path: 'test/integration/replays/linux/01-desktop-smoke.ad',
|
|
test: '# Smoke test for Linux desktop automation on CI.',
|
|
};
|
|
|
|
const LINUX_PROVIDER_EVIDENCE: RepositoryEvidence = {
|
|
path: 'test/integration/provider-scenarios/linux-desktop.test.ts',
|
|
test: 'Provider-backed integration Linux desktop flow uses semantic desktop and input providers',
|
|
};
|
|
|
|
const LINUX_RUNTIME_EVIDENCE: RepositoryEvidence = {
|
|
path: 'packages/platform-linux/src/runtime.test.ts',
|
|
test: 'classifies the Linux $name lifecycle denominator against the legacy dispatch cell',
|
|
};
|
|
|
|
const LINUX_CAPABILITY_DECLARATION_PATH = 'src/core/command-descriptor/registry.ts';
|
|
const LINUX_CAPABILITY_DECLARATION = 'linux: LINUX_NONE';
|
|
|
|
const C = PUBLIC_COMMANDS;
|
|
const live = (assertion: string): LinuxPlatformCoverageEntry => ({
|
|
assertion,
|
|
level: 'live',
|
|
owner: LINUX_REPLAY_EVIDENCE,
|
|
});
|
|
const contract = (path: string, test: string, assertion: string): LinuxPlatformCoverageEntry => ({
|
|
assertion,
|
|
level: 'command-contract',
|
|
owner: { path, test },
|
|
});
|
|
const denial = (command: string, assertion: string): LinuxPlatformCoverageEntry => ({
|
|
assertion,
|
|
level: 'capability-denial',
|
|
owner: {
|
|
path: LINUX_CAPABILITY_DECLARATION_PATH,
|
|
test: `name: '${command}'`,
|
|
declaration: LINUX_CAPABILITY_DECLARATION,
|
|
},
|
|
});
|
|
const gap = (assertion: string): LinuxPlatformCoverageEntry => ({
|
|
assertion,
|
|
level: 'known-gap',
|
|
trackingIssue: LINUX_COVERAGE_GAP_ISSUE,
|
|
});
|
|
|
|
/**
|
|
* One primary, observable owner for every public command on the Linux desktop.
|
|
*
|
|
* Live rows are limited to the existing Linux replay. Contract rows cite the
|
|
* existing provider scenario or dedicated Linux unit/runtime evidence; they do
|
|
* not turn mocked provider calls into live desktop claims. Capability denials
|
|
* are derived from the owning command-descriptor matrix. Known gaps are
|
|
* explicit follow-up work, not an implicit claim that a generic command works.
|
|
*/
|
|
export const LINUX_PLATFORM_COVERAGE = {
|
|
[C.artifacts]: gap('No Linux-specific artifact inventory command evidence exists yet'),
|
|
[C.devices]: contract(
|
|
LINUX_PROVIDER_EVIDENCE.path,
|
|
LINUX_PROVIDER_EVIDENCE.test,
|
|
'Linux provider scenario inventories the selected desktop device through the daemon client',
|
|
),
|
|
[C.capabilities]: gap('No Linux-specific capabilities command evidence exists yet'),
|
|
[C.doctor]: gap('No Linux-specific doctor command evidence exists yet'),
|
|
[C.apps]: contract(
|
|
LINUX_RUNTIME_EVIDENCE.path,
|
|
LINUX_RUNTIME_EVIDENCE.test,
|
|
'Linux runtime facts explicitly report native app inventory unavailable',
|
|
),
|
|
[C.boot]: contract(
|
|
LINUX_RUNTIME_EVIDENCE.path,
|
|
LINUX_RUNTIME_EVIDENCE.test,
|
|
'Linux runtime facts explicitly report boot unavailable for the desktop owner',
|
|
),
|
|
[C.shutdown]: gap('No Linux-specific shutdown command evidence exists yet'),
|
|
[C.appState]: contract(
|
|
LINUX_RUNTIME_EVIDENCE.path,
|
|
LINUX_RUNTIME_EVIDENCE.test,
|
|
'Linux runtime facts explicitly report app state unavailable',
|
|
),
|
|
[C.perf]: denial('perf', 'Linux capability declaration rejects native performance inspection'),
|
|
[C.logs]: gap('No Linux-specific app-log command evidence exists yet'),
|
|
[C.events]: gap('No Linux-specific session event command evidence exists yet'),
|
|
[C.network]: contract(
|
|
LINUX_RUNTIME_EVIDENCE.path,
|
|
LINUX_RUNTIME_EVIDENCE.test,
|
|
'Linux runtime facts explicitly report network capture unavailable',
|
|
),
|
|
[C.audio]: denial('audio', 'Linux capability declaration rejects native audio probing'),
|
|
[C.replay]: gap('No Linux-specific replay command evidence exists yet'),
|
|
[C.test]: gap('No Linux-specific test-suite command evidence exists yet'),
|
|
[C.clipboard]: contract(
|
|
'src/platforms/linux/__tests__/clipboard.test.ts',
|
|
'writeLinuxClipboard uses xclip with stdin on X11',
|
|
'Linux clipboard writes through the supported X11 host-tool seam',
|
|
),
|
|
[C.keyboard]: denial('keyboard', 'Linux capability declaration rejects native keyboard control'),
|
|
[C.install]: gap('No Linux-specific application installation command evidence exists yet'),
|
|
[C.reinstall]: gap('No Linux-specific application reinstallation command evidence exists yet'),
|
|
[C.push]: gap('No Linux-specific push delivery command evidence exists yet'),
|
|
[C.triggerAppEvent]: denial(
|
|
'trigger-app-event',
|
|
'Linux capability declaration rejects native application event delivery',
|
|
),
|
|
[C.open]: live('the existing Linux replay opens gnome-calculator'),
|
|
[C.prepare]: contract(
|
|
LINUX_RUNTIME_EVIDENCE.path,
|
|
LINUX_RUNTIME_EVIDENCE.test,
|
|
'Linux runtime facts explicitly report Apple runner preparation unavailable',
|
|
),
|
|
[C.batch]: gap('No Linux-specific batch command evidence exists yet'),
|
|
[C.close]: contract(
|
|
LINUX_PROVIDER_EVIDENCE.path,
|
|
LINUX_PROVIDER_EVIDENCE.test,
|
|
'Linux provider scenario closes the calculator and observes the desktop close call',
|
|
),
|
|
[C.snapshot]: live('the existing Linux replay captures the calculator accessibility tree'),
|
|
[C.diff]: gap('No Linux-specific snapshot diff command evidence exists yet'),
|
|
[C.wait]: live('the existing Linux replay waits for an observable calculator landmark'),
|
|
[C.alert]: denial('alert', 'Linux capability declaration rejects native alert operations'),
|
|
[C.settings]: denial(
|
|
'settings',
|
|
'Linux capability declaration rejects native device settings operations',
|
|
),
|
|
[C.reactNative]: denial(
|
|
'react-native',
|
|
'Linux capability declaration rejects React Native inspection',
|
|
),
|
|
[C.record]: gap('No Linux-specific recording command evidence exists yet'),
|
|
[C.trace]: gap('No Linux-specific trace command evidence exists yet'),
|
|
[C.find]: gap('No Linux-specific find command evidence exists yet'),
|
|
// Promoted from command-contract to live: the desktop replay now clicks a resolved digit
|
|
// button on real Linux hardware and the downstream wait only passes if the click landed
|
|
// (formerly missed — AT-SPI extents were computed screen-absolute-wrong under GTK4; see
|
|
// linux/atspi-dump.py).
|
|
[C.click]: live('the Linux desktop replay clicks a resolved calculator digit button'),
|
|
[C.fill]: contract(
|
|
LINUX_PROVIDER_EVIDENCE.path,
|
|
LINUX_PROVIDER_EVIDENCE.test,
|
|
'Linux provider scenario fills both a snapshot ref and coordinate target',
|
|
),
|
|
[C.longPress]: contract(
|
|
LINUX_PROVIDER_EVIDENCE.path,
|
|
LINUX_PROVIDER_EVIDENCE.test,
|
|
'Linux provider scenario executes a coordinate long press',
|
|
),
|
|
[C.hover]: denial('hover', 'Linux capability declaration rejects pointer-only hover input'),
|
|
[C.press]: contract(
|
|
LINUX_PROVIDER_EVIDENCE.path,
|
|
LINUX_PROVIDER_EVIDENCE.test,
|
|
'Linux provider scenario presses a snapshot ref and coordinate target',
|
|
),
|
|
// Promoted from command-contract to live: GTK4 gnome-calculator's entry previously exposed no
|
|
// Text-interface content to selectors (a PyGObject binding call-pattern bug — see
|
|
// linux/atspi-dump.py), so no tree-level assertion could hold. Fixed, so the desktop replay's
|
|
// typed calculation now has a real wait assertion on the computed result.
|
|
[C.type]: live('the Linux desktop replay types a calculation and its result is selectable'),
|
|
[C.get]: contract(
|
|
LINUX_PROVIDER_EVIDENCE.path,
|
|
LINUX_PROVIDER_EVIDENCE.test,
|
|
'Linux provider scenario reads the pressed snapshot ref text',
|
|
),
|
|
[C.is]: live('the existing Linux replay verifies a calculator landmark exists'),
|
|
[C.back]: contract(
|
|
LINUX_PROVIDER_EVIDENCE.path,
|
|
LINUX_PROVIDER_EVIDENCE.test,
|
|
'Linux provider scenario dispatches Alt+Left through the semantic input provider',
|
|
),
|
|
[C.gesture]: contract(
|
|
LINUX_PROVIDER_EVIDENCE.path,
|
|
LINUX_PROVIDER_EVIDENCE.test,
|
|
'Linux provider scenario executes a single-pointer pan through the semantic drag provider',
|
|
),
|
|
[C.home]: contract(
|
|
LINUX_PROVIDER_EVIDENCE.path,
|
|
LINUX_PROVIDER_EVIDENCE.test,
|
|
'Linux provider scenario dispatches Super+D through the semantic input provider',
|
|
),
|
|
[C.tvRemote]: denial('tv-remote', 'Linux capability declaration rejects TV remote input'),
|
|
[C.orientation]: denial(
|
|
'orientation',
|
|
'Linux capability declaration rejects native orientation changes',
|
|
),
|
|
[C.scroll]: contract(
|
|
'src/platforms/linux/__tests__/input-actions.test.ts',
|
|
'scrollLinux uses ydotool mousemove --wheel for vertical scroll',
|
|
'Linux scroll dispatch uses the Wayland ydotool wheel primitive',
|
|
),
|
|
[C.swipe]: gap('No Linux-specific public swipe command evidence exists yet'),
|
|
// Promoted from command-contract to live by #1925: the desktop replay now runs a coordinate
|
|
// focus on real Linux hardware, so the migrated `focusPoint` path has live changed-path
|
|
// evidence rather than only the provider scenario at LINUX_PROVIDER_EVIDENCE.
|
|
[C.focus]: live('the Linux desktop replay focuses a coordinate on real hardware'),
|
|
[C.screenshot]: live('the existing Linux replay creates a screenshot artifact'),
|
|
[C.viewport]: contract(
|
|
LINUX_RUNTIME_EVIDENCE.path,
|
|
LINUX_RUNTIME_EVIDENCE.test,
|
|
'Linux runtime facts explicitly report viewport changes unavailable',
|
|
),
|
|
[C.appSwitcher]: denial(
|
|
'app-switcher',
|
|
'Linux capability declaration rejects native app-switcher navigation',
|
|
),
|
|
[C.installFromSource]: gap('No Linux-specific source-install command evidence exists yet'),
|
|
} satisfies Record<PublicCommand, LinuxPlatformCoverageEntry>;
|
|
|
|
export const LINUX_PLATFORM_COVERAGE_CLASSIFICATION_SUMMARY = buildCoverageClassificationSummary(
|
|
Object.values(LINUX_PLATFORM_COVERAGE),
|
|
);
|
|
|
|
export function liveCommandsForLinuxReplay(): PublicCommand[] {
|
|
return Object.entries(LINUX_PLATFORM_COVERAGE)
|
|
.filter(([, entry]) => entry.level === 'live')
|
|
.map(([command]) => command as PublicCommand);
|
|
}
|