Troubleshooting

Common failure modes and how to diagnose them. For an organised debugging playbook (TestRail → backend → frontend → ArgoCD logs), see the Common Pitfalls section of the User Guide.

Where to look first
  1. Screenshot at failure — under the first failing step in the run report. The runner captures the viewport at the first failing step, on every run, by default. One look usually closes the question: wrong portal, menu not rendered, a red backend toast, an empty dropdown, a dialog over the element, or the login screen instead of the target page.

  2. The step list — which step failed, and its judge tag. A red judged Measured is a real finding; an amber INCONCLUSIVE means nothing measured the claim (see A Run Came Back Amber (INCONCLUSIVE)).

  3. MCP Log — every Playwright call with args; great for "why did the click miss". It is saved onto the report, so it is still there days later.

  4. Output — full Claude reasoning, pre-flight / post-flight lines and verifier diagnostics.

  5. Live browser (the button in the header — gateway-proxied noVNC) — what the browser is doing right now, for the run you own.

  6. Live spec — C:\F_Projects\ft-device-network\docs for KPI formulas before assuming a backend bug.

The absence of a screenshot is evidence too. No screenshot plus a whole segment reporting No result from nav executor usually means the page was blocked — a native OS dialog, for instance — so there was no viewport to capture.

A Run Came Back Amber (INCONCLUSIVE)

Symptom: the run is neither green nor red. One or more steps are marked with a description starting [not measured] ….

This is the evidence gate, live since 2026-09-03. An LLM verdict must quote, verbatim, the text from the captured page that decides the claim, and the runner checks that the quote really occurs in what it captured. A PASS that quotes nothing — or that was reached over an empty capture — is not a product failure and is not a pass either, so it is recorded as inconclusive. A FAIL is applied whatever it quoted: a false red is cheap and visible, a false green is neither.

What to do:

  1. Read the [not measured] line — it names the reason (nothing was captured, the quote was not found in the capture, the capture was empty).

  2. Decide whether the claim is measurable from the screen at all. If the expectation names something the portal never renders, the case needs a programmatic *_verify step or a blocked marker — not a green.

  3. If the step should have been measured, check that it carries an expected and lands on a page whose body text was captured.

A deterministic verdict is never downgraded: a description prefixed [verify], [polling] or [empty-state] means a helper measured the claim itself, and the judge having nothing to quote says nothing about that measurement.

Rollback, if the gate itself is ever suspected: set FT_VERIFY_EVIDENCE=0 on the worker. It changes only the consequence — the judge is asked for evidence either way.

A Green Run Says LLM-judged or Unverified

The verdict tag beside a run says what looked at the claim, not whether the claim held. Unverified means nothing asserted anything (a bare navigation, an auto-filled step, a verify helper that was unavailable). LLM-judged means a model read the page and decided.

Neither is an error, but neither is proof. The Test cases list shows the same information per case as a claims chip (3/7 measured), and the run history has a Pass · not measured filter to sweep exactly these. Cases that keep coming back LLM-judged are candidates for a typed *_verify step.

This Red Is Already Known

A failure carrying a known: <bucket> badge is in the known-issues registry — somebody already investigated it and recorded what it is. Read the badge title before spending time on it; GET /api/known-issues returns the whole registry with the bucket, the note and the action for each case.

Run Hangs in Execute

Symptom: the phase stepper sits on Execute for several minutes; MCP Log shows the same browser_snapshot repeating without action.

  1. Open the live view via the Live browser button (gateway-proxied noVNC for the run you own).

  2. Identify what the browser is showing — is the dialog the agent expects there?

  3. If the model in use is Sonnet, force-stop and rerun with Opus. Sonnet occasionally stalls on multi-step interact loops; Opus is the default for this reason.

  4. If Opus also stalls, the test prompt is ambiguous — open Editor, refine the failing step’s action / expected, save, rerun.

401 authentication_error

A Claude CLI auth failure raised mid-run by the agent’s LLM calls.

Since 2026-07-13 the workers authenticate with a long-lived CLAUDE_CODE_OAUTH_TOKEN set in the Portainer stack environment (from claude setup-token, valid about a year). It overrides any stale .credentials.json on the bind mount, so there is no periodic refresh to run and no laptop-side scheduled task — the old one was removed. A 401 here means that token was revoked, rotated, or never set on this stack.

  1. Check GET /api/admin/credentials-status (admin session).

  2. Fix the stack env and redeploy. POST /api/admin/refresh-credentials still exists for a manual push of a local credentials file.

The runner scores a CLI auth failure as a run error, not as a test verdict — a stand problem must never look like a product failure.
This is the Claude OAuth error, raised mid-run by the agent’s LLM calls. A 401 returned by the UI login is a different problem — see Login Fails / 401 on the UI (Not Claude OAuth) below.

Login Fails / 401 on the UI (Not Claude OAuth)

Symptom: the :8083 UI rejects your sign-in, or API calls come back 401 Unauthorized from the gateway (not from a running test).

  • 401 on login means a wrong username/password, a disabled account, or an expired bearer-token session — log in again.

  • After ~5 failed attempts within a minute the gateway rate-limits you and returns 429 Too many attempts; wait a minute and retry.

  • On a fresh deployment the only account is the bootstrap admin admin / admin. If that was changed and lost, an admin must reset it from the Users tab (or re-bootstrap the gateway).

This is unrelated to the Claude 401 authentication_error above, which is about the agent’s LLM OAuth token, not your UI session. See Accounts, Roles & Audit.

"Env Is Busy" / 409 When Starting a Run

Symptom: starting a run returns 409 with a message like the emulator for env '<env>' is busy.

Cause: another run already holds that environment. Exclusivity is keyed on the env’s physical emulator backend (emulator_url), so two profiles that point at the same emulator block each other, while envs on distinct emulators run in parallel.

Fix: wait for the in-flight run to finish, or pick another environment in the first dropdown of the Test cases run bar. See gateway.adoc#selecting-env.

Not to Be Confused With a Blocked Case

A case whose infrastructure precondition is missing on this stand no longer returns 409. It is recorded as a finished run with the grey BLOCKED status and a reason, so the gap is visible in the history and in the batch report instead of vanishing. To run it anyway, use Run anyway (force) in the report panel (or ?force=true on POST /api/run).

I Can’t See My Run / Someone Else’s Run Is Hidden

Run history and active runs are owner-scoped: each account sees only the runs it started. Admins are the exception — they see all runs. If your history looks empty or a colleague’s run is missing, you are simply logged in as a non-owning, non-admin account. See Accounts & Roles.

Two things are deliberately not owner-scoped, and they are the right way to find out whether a stand is free:

  • the Wave in progress card above Overview and Runs — every batch in flight on any worker, whoever started it (env, worker, owner, model, progress, ETA);

  • the Busy column in the admin Workers section — what each worker is running right now.

GET /api/runs/active is owner-scoped and will happily report "nothing running" while a colleague’s wave occupies the stand. Never use it to decide whether it is safe to redeploy — read the wave card, the Busy column, or the worker’s own access log.

A Section Is Missing From the Sidebar

Two independent reasons, and only the first is about roles:

  1. Admin-only. Settings, Users, Workers and Activity are hidden for editor and viewer roles. Ask an admin to raise your role or to perform the action.

  2. Hidden while empty. Learning, Procedures and Drafts were taken out of the sidebar on 2026-09-05 because they were carrying no content. This applies to everyone, admins included — it is not a permission problem. Their panels and their routes are untouched, so a direct link still opens them: /#learning, /#knowledge, /#drafts.

The always-visible sections are Overview, Test cases, Runs and Editor.

Live Browser Blank / noVNC Won’t Connect

The live view is now gateway-proxied and owner-gated — the old direct noVNC host port was removed. You can only watch the browser of a run you currently own on that stand; the gateway closes the noVNC socket otherwise. Make sure a run of yours is actually active, that you are logged in as its owner, and then open the Live browser button.

Run Fails With [tree-coverage] CRITICAL

Symptom: the run is marked error immediately after start; the Output pane shows a line like:

[tree-coverage] CRITICAL: tree=181.xml kpi=wlan-health-de-mesh
  | missing required prefixes: Device.WiFi.DataElements

Cause: the YAML’s kpi_type requires TR-181 paths that the configured tree_file does not contain. The pre-flight (see architecture.adoc#preflight) refuses to start a run that is guaranteed to fail later in verify because the data simply isn’t there.

Fixes, in order of preference:

  1. Switch tree_file to a template that covers the required data model (e.g. 181-mesh-de.xml for wlan-health-de-mesh).

  2. If no template fits, click Generate Tree in the Editor with the test text in place and let bbf-tree-mcp produce a fresh tree.

  3. As a last resort, hand-edit the XML under /app/templates/setups/ to add the missing object instances. Keep the tree minimal — extra paths can surface accidental defaults that change scoring.

A second, non-fatal validator writes [tree-coverage:intent] lines to the same pane. Those describe what the case’s intent needs and never stop a run.

If the same line is [tree-coverage] WARN instead of CRITICAL, the run proceeds but emulator_setup.parameters reference paths that aren’t in the tree — the emulator will accept them but the parameter will not be visible to the backend until a corresponding object instance is created.

The validator is schema-aware — it normalises any concrete instance index (AccessPoint.2.AssociatedDevice.3.*) to .{i}. and matches against the schema-level path set extracted from the tree, so dynamic instances created at runtime by the emulator no longer trigger false-positive WARNs. For neighboring_diag tests the validator also auto-swaps tree_file to a template that already declares NeighboringWiFiDiagnostic.Result.{i} if the configured tree lacks those instances.

Switched Settings to a New Env, Tests Fail / Device Not Found

Symptom: after changing the URLs in Settings to point at a different environment (e.g. from test-agent to dev-mysql), runs go to the new portal but the device the test expects is missing — the runner can’t find it in the device list, or the portal shows no parameters.

Root cause: portal navigation uses base_url_* and direct emulator REST queries use emulator_url — both honour Settings immediately. But the device-creation path goes through the mcp-tr-emul sidecar, which historically had its target ACS hard-coded.

Fix: set both Emulator MCP URL and ACS URL in Settings so they match the new environment:

After saving Settings, no restart is needed — the next run picks up the new URLs.

Imported Test Has Empty emulator_setup

The TestRail case has no XML tree attached and the converter fell back to a template that had no parameter requirements.

  1. Open Editor, click Generate Tree with the test text in place.

  2. Or set tree_file: 181.xml (or another template) and add parameters manually for the values the test expects to read.

Step Fails With "Expected value not found"

Programmatic verifier scanned the DOM and didn’t see the expected text. Check:

  1. Did the runner navigate to the right page? Use MCP Log — the last browser_navigate URL should match the widget being verified.

  2. Is the widget rendering the value you expect? Open VNC after a failed run to confirm.

  3. Does the backend actually have the data? Hit the emulator API:

    curl 'http://tr069-emulator.test-agent.rd.friendly-tech.com/apiV2/parametersByPrefix?serial=test-runner-001&prefix=Device.WiFi.AccessPoint.1.AssociatedDevice.1.'
  4. Or query the device tree from the portal via FtApiClient. The portal tree reflects what the backend received over CWMP — if a parameter is in the emulator API but missing in the portal tree, the ACS didn’t pick it up.

Backend Score Doesn’t Match TestRail Expected

The widget value computed by the live backend differs from what the TestRail test expects. Order of investigation:

  1. Spec docs — C:\F_Projects\ft-device-network\docs\src\modules\ROOT\pages\ (mesh/client-health-score.adoc, network-score.adoc, etc.). Source of truth for KPI formulas, data-model paths, weights, fallback paths.

  2. Backend Java sources — NetworkScoreProcessor and *ScoreCalculator`s in `C:\F_Projects\ft-device-network. Implementation of the formulas with exact border values.

  3. Frontend — C:\F_Projects\oneiot_angular. How the value is rendered (rounding, transformation).

  4. ArgoCD logs — ft-device-network pod. Look for NetworkScoreProcessor: TH: …​, INT: …​, NH: …​ lines for WLAN; tail to see what the backend computed and whether the ACS delivered the new state.

The expected value in TestRail is the source of truth — never edit it. Fix the converter or emulator parameters.

Tree Pushed But Portal Shows Old State

The portal shows whatever the ACS has cached from the last CWMP Inform. If the emulator was reset but no Inform was triggered, the portal lags.

  1. Check the MCP Log for invoke_event after the tree push.

  2. Manually trigger via the emulator API /apiV2/invokeEvent?serial=…​&type=2 PERIODIC.

  3. Or restart the emulator instance via start_protocol_emulator.

Sidebar / Tab Click Times Out

Selector text doesn’t match what the portal shows. Two common reasons:

  1. Wrong portal — the YAML says portal: support but the case is for management. Check the top-level field.

  2. Localisation / casing — nav_target is matched case-sensitively. Open VNC, copy the exact label, paste into nav_target.

An Import Won’t Start

POST /api/testrail/import answers 409 import already running (job …). Only one import may be in flight per worker. Poll GET /api/testrail/import/jobs/{job_id} for the running one and wait for it — the modal does this for you and shows Created / Skipped / Errors when it lands.

Force Stop Didn’t Clean Up

In rare cases the Claude CLI subprocess survives Force Stop. The next run will queue but never start because Playwright is still bound to the previous session.

  1. docker exec test-runner pkill -f claude (via Portainer terminal).

  2. Or restart the stack from Portainer.

A force-stopped case is recorded with status error and the reason "Stopped by user". There is no run status stopped — stopped exists only as a batch status and as a failure category.
Never redeploy a stack to clear a stuck run without first checking that no batch is in flight on it. Recreating the container cuts the running case in half: its pre-flight has already created domains, users and moved devices, and the post-flight that would clean them up never executes.

TestRail Modal Shows "Disconnected"

  1. Open Settings → TestRail.

  2. Verify URL, Email, API Key.

  3. Save. The pill should turn green within a couple of seconds.

  4. If still red, check the agent logs for HTTP errors against TestRail — often a stale API key after rotation.

Reading the Batch Report Failure Categories

Each failure in a batch report is bucketed into a category by the post-run analyser. This drives the doughnut chart in the Batch Report dialog and the per-row label in the results table.

A run whose status is inconclusive is given no category at all, so it never appears in the breakdown or in the failure counts. Read the Inconclusive tile, not the doughnut — a batch can look almost clean there and still have measured almost nothing. See A Run Came Back Amber (INCONCLUSIVE).
Category Meaning

setup

emulator_setup failed before any UI step ran (tree push, parameter write, ACS Inform). Typically a missing tree file, malformed parameter map, or the emulator container being down.

mcp_timeout

The Playwright MCP didn’t respond within the per-call timeout. Usually the browser hung on a snapshot or a long network call. Retry or restart the runner if it persists.

nav

A nav step couldn’t find its target. Wrong nav_target text, wrong portal, or the page didn’t load before the click.

verify

Steps executed but the verifier (DOM scan or LLM fallback) didn’t see the expected value. Usually a real backend / converter mismatch — start with the debugging order in user-guide.adoc#common-pitfalls.

converter

The Smart Converter raised an error on this YAML before the run could start (missing required field, malformed parameters map, etc.). Reimport the case after fixing.

stopped

The user pressed Force stop (or the Force Stop API was called). Not a real failure.

unknown

Couldn’t classify. Check the run’s Output pane for the raw error.