Troubleshooting
Symptoms, their causes and the fix, for anyone whose test, Studio session or agent session does not do what it should.
Start with npx spart-desk doctor: it reports the platform, the helper (the
Windows backend) and the session as it reads it, wsb.exe for the sandbox
runtime (experimental), the Java runtimes and the resulting capability tier, with a problem list.
Then find the symptom below.
Setup and installation
| Symptom | Cause → fix |
|---|---|
doctor says spart-desk-host.exe cannot run, or the first action fails with BackendError [helper_unavailable] | The helper is missing from the installation (an antivirus may have quarantined it) or an application control policy (Smart App Control, WDAC, AppLocker) blocks it. Reinstall @spartaficial/spart-desk, or allow the signed file by publisher; nothing else has to be installed (windows-security-gates.md). doctor's signature line says whether the file is signed, and by whom. |
ConfigError: …spart-desk.config.ts: the configuration file is an ES module … | The file uses require, module.exports, __dirname or __filename. It is read as an ES module whatever the package's type: use import, export default and import.meta.dirname. |
ConfigError: …spart-desk.config.ts: … | The project configuration does not load or is invalid: the message names the setting, or the syntax Node could not strip (only erasable TypeScript works in it). Desktop.create({ configFile: false }) bypasses it. See configuration.md. |
spart-desk.config.ts is ignored | It is discovered in the working directory of the process (or through SPART_DESK_CONFIG), not next to the script. Run from the project root, set the variable, or pass configFile. |
Locating controls
| Symptom | Cause → fix |
|---|---|
TimeoutError … Last observed state: not found | The locator matches nothing. The last state names a control of the same name under another role, or a name a typo away, as the locator that finds it. Otherwise check the tree with app.snapshot() (or desktop_snapshot for agents): names differ from what is painted, the control lives in another window, or it is custom-drawn (no UIA): find it by its text read off the screen, by a picture, or with getByVision (experimental). |
A TimeoutError or AssertionError with a Why: line | The locator found nothing, and SPART-Desk can tell why. "No radio is in the application's accessibility tree": the toolkit draws that control and reports no element of the role (Tk, Krypton's painted controls, Flutter's trees and menus); find it by its text (getByText), read it off the screen, or assert on its picture. "No control of the application carries an identifier" or "only ones made up for them": getByAutomationId cannot work there (Delphi VCL's window handles, JavaFX's JavaFX27); use the role and the name. "No text "…" is in the application": the label is painted, or named otherwise; look at app.snapshot(). "Reports next to nothing to UI Automation": the window is drawn by its toolkit; the screen and pictures are what there is. A control that is not found is waited for to the timeout all the same, since it may yet appear. |
not found; its window "…" lies on another virtual desktop | Windows cloaks a window on another virtual desktop, and UI Automation shows nothing of it, while app.windows() still lists it. Switch to that desktop (Win+Ctrl+Left or Right), or move the window to the one the run uses: Task View (Win+Tab), right-click the window, Move to. Run unattended suites on a single desktop. |
StrictModeViolationError … resolved to N controls | The same several matches for about a second (transient duplicates, such as a dialog and its successor, are waited out). The error's Hint: lines say how many candidates are hidden and give a locator for each. Add exact: true, a role, a scope (dialog.getBy…), a state ({ checked: true }), filter({ visible: true }) for a control a hidden tab page repeats, filter({ hasText }) for list rows, or pick first()/nth()/last() as a last resort. Asking how many there are is count() / toHaveCount(n), which are never strict. |
getByLabel finds nothing but getByText does | Labels only match input-like roles (text boxes, combo boxes, check boxes, …). Use getByRole with the name. |
getByRole("dialog") matches the main window | UIA does not distinguish dialogs from windows; dialog accepts both by design. Add a name. |
| Resolution is slow (seconds) | A resolution is one read of each window of the application by the helper: tens of milliseconds for a few hundred controls, seconds for a very deep tree or a Windows Forms grid of thousands of rows. Scoping a locator cuts ambiguity, not the read, and inspectDepth cuts what is matched, not the read. Raise timeout rather than shortening intervals; see Tuning. |
| A VS Code tree row is not found by its AutomationId next time | VS Code's rows carry ids like list_id_1_2, which are places: they move when a folder opens. SPART-Desk no longer suggests them; find rows by role and name. See electron-apps.md. |
| Typing into a WebView2 page needed the foreground | Characters are posted to the page's window that has the focus, and in a WebView2 host some never arrive. SPART-Desk reads the value back, posts once more, and then types with real input. Run with the window in front, or fill a field (fill() sets its value in the background). |
| Every locator is slow while a big Windows Forms grid is on screen | The helper reads a DataGridView's rows through its window, about a millisecond a row (5,000 rows in about 5 s), and only for a locator that may reach a row: one for the grid itself, its column headers, a button or the status line reads none, and getByRole("row").filter({ visible: true }) reads the rows shown alone. Name the grid by its AutomationId or name, and ask for the rows shown where that is what the test means. See dotnet-apps.md. |
getByRole("row") finds nothing past the first screenful (WPF) | The grid is virtualised: it has not created those rows. Scope the locator into the grid (grid.getByRole("row").filter(...)) and act, or call scrollIntoView(); both page to the row. Observations do not page; assertions page with { pages: "all" }. |
A control found by getByVision cannot be scrolled by direction | Controls found on the screen have bounds only: click, fill and press act at their place, hover() moves the real mouse there and scroll({ wheel }) turns the wheel there (both through the helper executable); scroll({ direction }) and scroll({ to }) are refused (not_supported). Turn the wheel instead. |
Acting on controls
| Symptom | Cause → fix |
|---|---|
BackendError [foreground_required] | Real input (mouse/keyboard) needs the target window in the foreground of an unlocked, interactive session. SPART-Desk brings the window to the front for such an action; this error means Windows kept it behind (the message says it tried), another window lies over the point, or the desktop was created with foreground: "require". Bring the window to front yourself, run on an interactive runner (ci.md), or use operations that work through UIA patterns. |
| An action record says "brought … to the front (with a click on its title bar …)" | After a flyout of the shell (the notification centre, the calendar) closed, its empty host window kept the foreground, and Windows gives it to no background process. SPART-Desk clicked the target window's title bar, as a user would, where nothing covers it. Nothing to do. |
BackendError [session_locked] | The session is locked (Win+L, an idle timeout, a policy's inactivity limit such as Intune's InactivityTimeoutSecs): no window comes to the front, and nothing takes real input or can be captured. Unlock the session, or keep it from locking while tests run (an organisation's policy may not let you; ask its administrator). Actions through UI Automation and window messages still work on a locked session. desktop.session() tells a test which session it has, and npx spart-desk doctor prints it. When the message says the session is behind a running screen saver (reason: "screen-saver"), turn the screen saver off for the account the tests run as (ci.md). |
BackendError [session_disconnected] | The same on a Remote Desktop session whose client disconnected: nothing is drawn. Reconnect it, or hand the session to the console when you disconnect (tscon, ci.md). |
BackendError [session_non_interactive] | The tests run where no real input arrives at all: session 0 (a CI agent installed as a service) or a window station no display shows (a scheduled task run whether the user is logged on or not). Run the agent as an interactive process in a logged-on desktop session (ci.md); UIA-only tests still work where they are. |
BackendError [input_busy] | Another run held the right to send real input to this desktop (the input lease) for as long as the action could wait; the message names its process. Run the tests that click and type as a user does one after another (parallelism.md). A run that died frees the lease at once. |
| An action's record says "a person used the mouse or the keyboard meanwhile" or "came to the front while the input was sent" | The input was sent, but something else happened on the desktop at the same moment, so it may have gone elsewhere. SPART-Desk does not retry it, since it may also have arrived. Keep hands off the desk during a real-input run, and close what takes the foreground on its own (notifications, an updater). |
BackendError [reserved_key] | press() was given Ctrl+Alt+Del or Win+L, which Windows keeps for itself. Act on what they open (the Task Manager, the lock screen) through its own window. |
click() used input.mouse.click instead of uia.invoke | The control exposes no Invoke/Toggle/SelectionItem pattern; the fallback is recorded (fallbackFrom). Normal for custom controls and panes. |
fill() typed instead of setting the value | No Value pattern; typing needs focus and the foreground. On a web page, a Monaco editor (VS Code) accepts the value without taking it: an empty one is typed into in the background, one holding text needs the foreground. |
| An Electron application launched outside the test exits at once ("bad option") | A runner started from an editor extension inherits ELECTRON_RUN_AS_NODE, which an Electron application takes as an instruction to run as Node. A SPART-Desk launch does not pass it on; launching outside SPART-Desk, remove it from the environment. |
press("Control+S") fails on a background window | A chord that the window's own menu lists as a shortcut sends that item's command in the background (window.menu); any other chord on a Win32 or Windows Forms control is posted with its modifiers held (window.key). A chord with the Windows key, or on WPF or a web page, needs real input (send-input) and the foreground. Plain keys are posted and work in the background (window.key, or UI Automation's focus through the helper). |
| A WPF window comes to the front while a test pages a list or presses a key | WPF activates its own window when UI Automation scrolls or focuses it, if Windows lets it. Paging and keys work either way. |
BackendError [elevation_required], or a snapshot note "runs at a higher integrity level than this test runner" | The window (an application run as administrator, Windows' credential prompt) runs above the test runner's integrity level, and Windows keeps its controls and input out of reach. SPART-Desk never elevates itself: run the test runner elevated. See elevated.md. |
app.credentialPrompt() stays empty: "No credential prompt of … is showing. One credential prompt is showing that Windows does not tie to it" | The prompt on screen belongs to another application, or its caller no longer waits for it (it was raised on a thread that went on). Only a prompt Windows ties to the application counts (elevated.md). |
Tests pass alone but fail with foreground_required (or act on the wrong window) when run in parallel | A session has one foreground window; two tests injecting real input into two applications compete for it. Within one action they take turns (the input lease); between actions another test can take the window away. Run real-input tests with one worker per interactive session and UIA-only tests in parallel. See parallelism.md. |
| The same script clicks the wrong place on a high-DPI monitor | Coordinates are physical pixels; the backend reports bounds in physical pixels too. If you compute points yourself, use CoordinateMapper for logical/DPI conversions. |
BackendError [read_only] | The field shows a value but takes none (a Windows edit with ES_READONLY, or a Value pattern that says it is read-only). Nothing was typed. Read it with toHaveValue(); fill another field, or make it editable first. See desktop-conditions.md. |
| A fill's record says "typed again as the keys that make them" | The field took no characters posted to it: it builds its value from key presses (a PIN or hot-key field). SPART-Desk typed the text again as key presses, in the background. Nothing to do. |
check() changed a WPF or Avalonia check box, but the application did not react | The Toggle pattern raises Checked, not Click, and runs no bound command. A WPF toggle is given Space with its focus first, which runs both; one that did not take the key, and Avalonia's, went through the pattern. Use check({ mouse: true }) (or click({ mouse: true })), which clicks as a user does. See desktop-conditions.md. |
| After a click that opened a dialog, a snapshot notes that the application "holds a UI Automation call open" | The dialog was shown inside a UI Automation call (a WPF list's SelectionChanged); the action record names it. Until it closes, SPART-Desk reads the application through the window manager: a message box's buttons are pressed in the background, a WPF dialog's are read off its picture and clicked with real input, which needs the foreground. click({ mouse: true }) on the item runs the handler outside any UI Automation call, so the dialog is read as any other. See desktop-conditions.md. |
BackendError [app_busy] "The application is busy: … (still so after N ms)" | The application was still loading or computing at the action's timeout: its window took no message within half a second, or the pointer showed the wait cursor over it. The action waited for it and sent nothing. Raise the action's timeout for an application that loads longer, or wait for it first with app.waitForIdle() (auto-waiting.md). A window that appeared under a pointer at rest (WinUI 3's context menu) shows a stale wait cursor until the pointer moves: when the pointer rests where the desktop's own last click left it, SPART-Desk moves it a pixel aside and back once, and asks again. |
BackendError [not_supported] "toBeChecked() needs a checked state; … reports none" (or toBeExpanded, toBeFocused, toHaveValue) | The control cannot report that state or value: it was read off the screen and its picture does not show it, or it has no pattern to read it by (a check box with no Toggle pattern, a combo box with no value). The refusal comes once that has settled, about a second, not at the timeout. A control read off the screen does report a checked square or ring, a chosen row, an open tree indicator, and the focus when the window's caret lies in it. Assert on what the application shows because of it (a status text), or on the control's picture (toHaveScreenshot()). |
BackendError [not_supported] from toBeDisabled() or not.toBeEnabled() | The control was read off the screen, which shows no disabled state SPART-Desk can tell; toBeEnabled() passes, as actions take such a control to be enabled. A widget whose own window is disabled is reported disabled (Krypton's); one a toolkit greys inside an enabled window (Tk's) is not. Assert on the control's picture, or on what clicking it does not do. |
check(), expand() or selectOption() refused with not_supported, a hint saying to click() instead | The control offers no pattern for the action, and SPART-Desk will not guess what a click on it does. Click it, as a user would, and assert on what the application shows. |
toBeVisible() fails, or toBeHidden() passes, on a control of a minimized window | A minimized window shows none of its controls: they are hidden. Actions on them still work in the background, as before. Restore the window (window.restore()) to see them. |
focus() brought the window to the front | The control took no keyboard focus in the background, so SPART-Desk brought its window forward, as press() and type() do. Nothing to do. |
A close(), minimize() or maximize() record says window.system-command | The toolkit draws the window's title bar, with no buttons UI Automation can press (Electron, Krypton, Material Design). SPART-Desk asked the window through Windows' own system command, as its system menu does. Nothing to do. |
A press() or type() record on a field read off the screen says "no click" | The field holds the keyboard from the desktop's last click on it, with nothing but its own keys since, so it was not clicked again: a click would move the caret that Home or End left. Nothing to do. |
| Text read off a field reads a letter more at its end ("Valuel") | The field's caret, read as an "l". SPART-Desk leaves the caret out of what it reads where Windows says where it stands (Tk, Windows' own edits: the thread's caret, in the window with the keyboard focus). An application that draws its own caret and tells Windows nothing of it is read with it: assert on the text with toContainText(), or move the focus away first. |
| A text read off the screen differs from what it shows by a character or two ("Key. r-2" for "Key: F2") | The whole window was read at a size where small text reads poorly. When no line reads what a locator asks for, the lines close to it are read again alone, enlarged, and the locator matches what they read then; "closest text read" in the error names what was read. A text far from what was asked for is not read again: check the locator. |
BackendError [window_blocked] "the dialog "…" is open over it … (still so after N ms)" | A modal dialog blocks the control's window, so no user could act on it either. The action waited for the dialog to close within its timeout. Answer or close the dialog first. "Could not tell whether a dialog blocks the control's window": the helper did not answer the question, and the action was held back rather than sent unchecked. |
BackendError [occluded] "The control is covered at x,y: … lies over it" | A real click's point lies on something else inside the window: a loading overlay, a panel, a popup of the application's own. The action waited for it to go within its timeout. Wait for the application to finish what the cover shows, or act on the control through its pattern (click() without mouse: true). "It moved since it was observed": the control was elsewhere when the click was sent; an application that keeps moving it may need a wait until it settles. A click the check refuses wrongly can be sent at a point with app.window().mouse.click(point). |
| An action's record says "the control under the point was not checked (…)" | The helper could not tell what lies under a real click's point (UI Automation did not answer in time, or there is no helper), and the click was sent with the window check alone. |
A check() record says "a WPF control's Click handler and bound command do not run" | The Toggle pattern changed the state without a click: the application's Checked handler ran, its Click handler and bound command did not. A WPF toggle gets there only when Space changed nothing. Use check({ mouse: true }) where those must run. An Avalonia check box clicks itself when toggled, and its handlers run. |
selectOption() on a combo box says "nothing confirms the choice" | The combo box reports no value, before the choice as after, and holds no field that shows it: the item was clicked with the mouse and the list closed, which is all it tells. Assert what the application did with the choice. A Qt Quick combo box is read through the field it holds. |
toBeEnabled(), toBeDisabled() or isEnabled() fails with not_supported: "its toolkit reporting every control enabled, a disabled one too" | The control is in a Flutter window. Flutter reports every control enabled to UI Automation, so its state cannot be read. Assert what the application shows instead: a text it writes, or the control's picture. Clicks still go ahead. |
press("End") on a slider fails with not_supported: "a slider of a Flutter window hears only its arrow keys" | Flutter's slider ignores Home, End, Page Up and Page Down. Move it with fill("<value>"), which presses its arrow keys, or with press("ArrowRight"). |
toHaveText() on a Qt label fails with "reports its accessible name as its text" | The label's accessible name is set (setAccessibleName, Accessible.name), and Qt reports that name alone, through Qt Quick's Text pattern too. What it shows is on the screen only: read it with read() before the step that finds it, or getByOcr(). |
expand() fails with not_supported: "reports no expanded/collapsed state, and holds no expander to toggle" | The tree item reports no state, and holds no toggle button SPART-Desk could expand it through (an Avalonia tree item holds one, and is expanded through it). Expand it another way the application offers (a double click, or press("Right") on the item), and assert on the children that appear. |
An expand() record says "through the expander inside it" | The tree item reports no state of its own (Avalonia's); its expander button was toggled and its checked state read as the item's. Nothing to do. |
| A wait's last state says "… was still scrolling; looking again" | The list was looked at too soon after a wheel notch, and it had moved on since: the next attempt looks at it again before paging on. Nothing to do. |
selectOption() on a Flutter drop-down times out with "pressed it; no option is …" | The field was pressed, and none of the buttons its menu showed is the option. The last state lists the options it saw; check the option's text. The field is pressed once only, since a second press would close its menu. |
| A timeout whose last state says "the application is not responding" | A window of the application takes no messages: it is busy or hung. SPART-Desk stopped reading it and kept to the timeout. Raise the timeout if the application is known to be busy that long. |
A click's wait ends with disabled on a control that was enabled when the test looked | The control became disabled between the tree read and the act. UI Automation's refusal (element_not_enabled) is retried: the control is observed again and reported disabled at the timeout, or clicked once it is enabled again. It is never clicked with the mouse at its old place. |
BackendError [application_ended] from an action or a wait | None of the application's processes runs any more: it ended, or crashed, after its window had been observed. The wait fails at once rather than timing out on the locator. Between two dialogs an application shows no window for most of a second while it runs on; that is waited out, never reported as ended. |
Runtimes and backends
| Symptom | Cause → fix |
|---|---|
A Java window shows only its frame; the snapshot has a note | Its JVM runs without the Java Access Bridge. Launch the application through SPART-Desk (java: true when it is not recognised), or enable the bridge for that runtime with jabswitch -enable for an application you attach to. See java-apps.md. |
LaunchError: … turned the Java Access Bridge on for this launch … | The runtime that actually ran the application lacks jdk.accessibility and refused to start AWT with the bridge option. Pass java: false. |
LaunchError: … launched as a Java application, but its window cannot be read … | java: true was given, but no Java window could be read (not a Java application, or its runtime ignores JAVA_TOOL_OPTIONS). |
BackendError [java_busy] | A bridge action opened a modal dialog in a background window, and the JVM answers nothing until it closes. Keep the application's window in the foreground when a click opens a dialog, so the click is real input. |
BackendError [unavailable] from java-access-bridge | The bridge host could not start: no WindowsAccessBridge-64.dll (for a 32-bit runtime, WindowsAccessBridge-32.dll) next to the application's Java runtime, in System32 or in JAVA_HOME, or spart-desk-host.exe (for a 32-bit runtime, spart-desk-host-x86.exe) is missing or blocked by an antivirus or application control policy (doctor reports it, and names each runtime's bitness). |
SandboxUnavailableError (wsb-missing) | The sandbox runtime (experimental) needs Windows Sandbox. On Windows Server there is no Windows Sandbox at all, and the message says so. On Windows 10, or Windows 11 before 24H2, there is no wsb.exe to install: the sandbox runtime needs Windows 11 24H2 (build 26100) or later, and the message names this machine's build. On 24H2 and later: enable the Windows Sandbox feature (reboot), then open Windows Sandbox once from the Start menu: that installs the app that brings wsb.exe. doctor reports whether it is found. |
SandboxUnavailableError (start-failed) | Virtualisation is unavailable (nested virtualisation on cloud VMs), or the config XML is invalid; the message carries wsb's output. "The Sandbox client is not ready": open Windows Sandbox once from the Start menu. "An Application Control policy has blocked this file" while the guest is prepared: the guest enforces Smart App Control, which refuses a helper whose signature is not valid (doctor's signature line says whether it is; reinstall the package); createSandboxRuntime({ guestAppControl: "off" }) turns the policy off in the guest for that boot. "The helper's agent in the guest did not answer": the sign-in took longer than 3 minutes, or the guest's firewall rule did not take; stop the sandbox and try again (runtime-windows-sandbox.md). |
SandboxUnavailableError (not-ready) | The agent answers, but the guest's session takes no real input: the sandbox's client window is closed or minimised, so the desktop is not drawn. Connect it (wsb connect --id <id>) and try again; keep it open and not minimised while real input and captures run. |
LaunchError: Failed to launch … in the sandbox: … An Application Control policy has blocked this file. | The guest enforces Smart App Control or an App Control policy, which starts only signed or known programs, and the program is neither. The host's setting does not carry over into the guest; guestAppControl: "off" turns the policy off there for that boot (runtime-windows-sandbox.md). |
LaunchError: … showed no window within … | The application starts slowly (raise windowTimeout), or it hands off to a process it does not start itself (Windows 11 Notepad activates an app running elsewhere): pass target: { processName } (notepad.exe → Notepad). A launcher that starts the application as its own child (launch4j, jpackage, script hosts) is followed without a target. The message names what the launch started; all of it has been stopped. |
LaunchError: … showed no window: the process exited first. The executable exited before a window appeared, and … | The launcher exited, and nothing it started still ran: the application crashed at start, or another program starts it (a service, WMI, ClickOnce) once the launcher has asked. Name the client's process, target: { processName, windowTitle }, whose wait goes on whatever the launcher does. |
LaunchError: … A window of that title was open before the launch (pid N) … | A window of the target's title was open already, and the launch showed none of its own: a single-instance application probably handed the launch over to it. Pass joinExisting: true to automate that instance, or close it first. |
LaunchError: Windows could not start "…": 0x80073CF1 Package was not found | No installed app has that app ID. App IDs are PackageFamilyName!AppId (Microsoft.WindowsCalculator_8wekyb3d8bbwe!App); the Start menu's apps are listed with theirs in the shell:AppsFolder folder, and PowerShell's Get-StartApps prints them. |
| A Store app (Calculator, Settings) shows no controls | The app is minimized: Windows empties its window then. await app.window().getByTitle("Calculator").restore() brings it back (through the helper executable, as the taskbar does). Keys to a Store app need its window in the foreground: its XAML controls ignore many posted keys. |
app.close() left the application open | The launch joined an instance that was already running (single-instance apps, with joinExisting: true), so it created nothing SPART-Desk may terminate; app.attachedToExisting is true then (the agent prints already running: close will not stop it). Close it through the UI, or start without that application open (many single-instance applications have a switch for a separate instance). Attached applications are terminated through the processes owning their windows. |
LaunchError: … was already running (pid N) and the launch started no new instance of it | A single-instance application handed the launch over to the instance you had open, and the launch did not say it may join one. Pass joinExisting: true to automate that instance, or close it first. |
LaunchError: … still shows a window ("…"), and a process with other windows is not stopped | The application was attached to by one window of a process (attach({ processName, windowTitle })); the window was asked to close, and the process keeps other windows (File Explorer). Close the window through the application, or attach by pid to stop the whole process. |
BackendError [ambiguous_target] | attach({ processName, windowTitle }) names several windows of the process with that title, or matching that pattern. Give a longer part of the title or a narrower pattern, or attach by windowHandle. |
LaunchError: Process N was stopped but had not exited 5 s later | The process ignored or survived termination (a hung driver, a process protected by security software). SPART-Desk reports it rather than letting the next launch meet it. |
LaunchError: … drawn by ApplicationFrameHost … | The target is a Store app's frame, and the helper executable could not tell which app it holds (missing or blocked: doctor reports it). SPART-Desk refuses to terminate the frame host because that would close every open Store app. |
| Notepad opens a tab in my own Notepad | Windows 11 Notepad is single-instance; use another application for experiments. |
BackendError [command_timeout] | A one-shot run of the helper did not finish within 60 s and was stopped. Retried during waits; persistent timeouts mean a hung application or an enormous tree. |
BackendError [delivery_unknown] | A call that delivers something (an invoke, a click, keys, a value, through the helper) was stopped before it answered, so whether the application received it cannot be told. It is never sent again: check what the application shows, then repeat the step deliberately if it did not land. A fill whose value arrived whole, read back after the late answer, is reported done instead. The message says when the application had ended or was not answering meanwhile. |
BackendError [helper_unavailable] | spart-desk-host.exe is missing from the installation, Windows will not start it (an antivirus quarantined it; a Smart App Control, WDAC or AppLocker policy blocks it, windows-security-gates.md), or it exits before it is ready. The first action fails at once with this, not at its timeout. npx spart-desk doctor says which. |
BackendError [command_not_found], [command_not_started] | desktop.run named a program that is not on that machine's PATH, or a working folder that is not there. A batch file or a shell's own command (echo, dir) runs through cmd.exe /d /s /c. |
BackendError [command_timeout] from desktop.run | The command ran past its timeout (60 s by default) and was stopped with what it started; it is not retried. A program waiting for input it never gets is the usual cause: give it input, or its own non-interactive switch (/f, -y). |
desktop.run's output shows wrong accented letters | Output that is valid UTF-8 is read as UTF-8, anything else in the console's OEM code page, as Windows' own console programs write. A program that writes ANSI (code page 1252) bytes instead comes out wrong: have it write UTF-8, or for PowerShell [Console]::OutputEncoding=[Text.Encoding]::UTF8; before the command. |
BackendError [automation_unavailable] | app.automation found no running object under that ProgID, or none holding that document, within its timeout. The application may register none, or register it only once a document is open, under that exact path (automation-interfaces.md). |
BackendError [not_application] | The object under that ProgID is served by another process than the application's: a second instance, the user's own. Close the other instance, or name the application's document instead of the ProgID. |
BackendError [helper_exited] | The helper stopped while answering a question, and stopped again when the restarted helper was asked it. Something in the application makes it fail when asked this, most often its UI Automation provider. npx spart-desk support capture records the application and the helper's conversation for a report. |
BackendError [helper_lost] | The helper stopped four times within five minutes and is not started again for this desktop; the trace's helper entries say how each stop happened. An antivirus that ends spart-desk-host.exe (npx spart-desk doctor), or an application whose provider makes it fail (npx spart-desk support capture), are the usual causes. A new Desktop starts the helper afresh. |
BackendError [helper_protocol] | The helper beside the engine is from another build: its ready line names a protocol the engine does not speak. Reinstall @spartaficial/spart-desk so that native/spart-desk-host.exe matches it. |
Reading the screen, pictures and grounding
| Symptom | Cause → fix |
|---|---|
ScreenError [unavailable] or [startup_failed] from windows (the Windows reader) | The helper executable spart-desk-host.exe is missing, was blocked (an antivirus, a WDAC or AppLocker policy: allow the signed file), or could not load the WinRT types. doctor reports whether it runs; ocr: { provider } accepts a reader of your own, or ocr: { enabled: false } turns reading off. |
ScreenError [unavailable] saying no text recognizer for a language is installed | Windows has no recognizer for the language asked for (ocr.languages, or a locator's language); the message lists the ones installed, and npx spart-desk doctor does too. An administrator installs one: Add-WindowsCapability -Online -Name "Language.OCR~~~nl-NL~0.0.1.0". Or name an installed one, or add PaddleOCR as the next reader (provider: ["windows", "paddle"]). |
ScreenError [decode_failed] | The capture was not a PNG the decoder accepts (interlaced, corrupt, or not a PNG). It names which. |
ScreenError [plain_picture] | The picture is one colour, give or take (a lamp cut out of its background, a patch of a panel), so it would match every patch of that colour. Picture something with an edge, a glyph or a border in it, or find the control through the tree or by its text. |
| Read text never appears | Reading runs only when the accessibility tree found nothing, only for locators it could serve (not a bare getByRole), and at most once per resolution.screen.interval. The timeout error lists every resolver that ran and why the others did not. |
Text read wrongly (Eile for File) | The timeout error's closest text read: shows what the reader saw. { fuzzy: true } accepts a misread of a character or two. Text drawn at 100 % is enlarged before it is read (resolution.screen.upscale, default 2); for one control, app.snapshot({ enrich: "text", root }) reads only its area, which reads better still. |
A focused field's last digit is missing from what was read (100004 for 1000042) | Windows' recognizer leaves out the last glyph of some small fields, with or without the caret in the picture. Read it once the field lost the focus, or add PaddleOCR as the next reader (provider: ["windows", "paddle"]), which reads it whole. |
The error says a window is laid out in a way SPART-Desk does not recognise | Each window is read from its own picture, and a window with owned windows was pictured in a way SPART-Desk does not expect. Please report it with a support bundle (npx spart-desk support capture). |
| Every picture is blank, and the note says this is a Remote Desktop session | The Remote Desktop window on the client machine is minimised, and Windows stops drawing a session while it is. Restore it, or set RemoteDesktop_SuppressWhenMinimized to 2 on the client machine (ci.md). |
getByImage finds nothing | Choose the picture in Studio's Pictures library: it shows every place it is found with its score, near misses dashed. The window is drawn at another DPI than the template was captured at: set images.scale, or pass scales explicitly. Or the threshold is too high — images.threshold is 0.9. Correlation ignores brightness, so a colour change is not the cause; a shape change is. A coloured picture must have its colours too: { colour: false } matches the shape alone. |
getByImage finds nothing in a dialog | Every window of the application is looked in, front first, and a match that a window in front covers is not taken: see that nothing of the application covers the dialog. |
images.crm.main.save does not compile, or index.mjs is missing | The typed index is out of date: a picture was renamed, deleted or added by hand. Run npx spart-desk images, or Update index in Studio's Pictures library; the type check then names every test that uses a picture that is gone. |
getByImage finds too many places | Lower the number by raising threshold, or choose a template with more structure: a plain rectangle correlates with every plain rectangle. |
ScreenError [unavailable] naming an image | No images.directory is configured, or the file is not in it. |
CapabilityUnavailableError for getByVision (experimental) | SPART-Desk ships no grounding provider. Pass one through grounding: { provider }; the error says so. |
Last observed state: … below minimum confidence | The only matches were below grounding.minimumConfidence (0.85) or ocr.minimumConfidence (0.5). Lower it deliberately, or make the query more specific. |
Agents (MCP)
| Symptom | Cause → fix |
|---|---|
StaleReferenceError | Refs belong to the latest snapshot of their application; take a new desktop_snapshot after the UI changes. |
PolicyViolationError [policy:…] | The deterministic policy refused: allow-list, sandbox-only, restricted key, raw coordinates/attach disabled, confirmation denied, hook. Adjust the policy, not the agent. |
Snapshot says truncated | More than maxControls; scope with root or maxDepth. |
Tuning
- Keep locators specific (a role with a name, or an AutomationId), so the tree answers at once and ambiguity costs no retries.
- Lower
inspectDepth(default 20) for a large application when nothing deep is targeted. The helper reads a window whole whatever the depth; the depth cuts the tree after the read, so it bounds what is matched and reported (and the size of a snapshot), not what the read costs. - Raise
timeoutfor a slow application instead of shorteningintervals. - Prefer operations that read through UI Automation in bulk (
controls(), assertions) over repeated single reads. - Parallel workers multiply processes: every worker keeps a helper of its own, and every resolution reads a window whole. Bound the worker count by the machine's processors, and keep real-input tests on one worker per interactive session (parallelism.md).
Getting evidence
For an application SPART-Desk does not handle well, or has not met yet,
npx spart-desk support capture --app "<window title or process>" --out bundle --zip
writes one file that holds what a maintainer needs: the tree at every level,
pictures, timings, coverage notes, and how the application is built and
delivered (studio.md).
Failures attach a screenshot and ui-tree.json (diagnostics.directory,
default %TEMP%\spart-desk); desktop.actions.records shows every
operation with resolver, strategy and timings; desktop.trace.pipe(sink)
streams everything to a file. See
assertions-diagnostics.md.