# Triage Integration Test Failures

> Use when a CI integration, unbounded, or container test job fails intermittently, passes on a rerun, fails identically across every matrix variant, or shows an infra-shaped error (connection timeout, account lock, native crash, docker daemon not ready) rather than a clear assertion diff -- the cause may be environmental, tooling, or a real product defect exposed only sometimes. Classify the failure before proposing any fix.

- Skill: `newrelic/triage-integration-test-failures` (Agent Skill)
- Install (CLI): `npx skillmds@latest add newrelic/triage-integration-test-failures`
- Raw SKILL.md: https://api.skillmd.com/api/skills/newrelic/triage-integration-test-failures/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: newrelic (https://skillmd.com/u/newrelic)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/newrelic/triage-integration-test-failures

---


# Triage integration test failures

## Overview

A failure investigation ends with one of five remedies, not a patch guessed
under time pressure. Classify the failure first. The category decides the
remedy; the wrong category produces a fix that treats a symptom.

**REQUIRED BACKGROUND:** superpowers:systematic-debugging governs root-cause
work in general. This skill adds the classification step and the toolkit
specific to this repo's CI, agent, and test infrastructure.

## Hard rule: delegate the reading

A CI job log, an `az monitor`/`kubectl` dump, and an agent log are evidence,
not the deliverable. None of them belongs in the main session.

- Run every command in the toolkit below inside a subagent (Agent tool --
  general-purpose or Explore), not in the main session. Give the subagent
  the exact command and the exact question ("did this job fail on a
  connection error before any agent code ran, or on an assertion diff?");
  it returns a short verdict with the one or two evidence lines that prove
  it, never the raw log or the full command output.
- This applies even to a "quick look" -- a CI log or agent log runs to
  hundreds of KB or has single lines tens of KB wide, and once that text
  lands in the main session it is re-billed on every later turn for the
  rest of the investigation.
- For agent-log evidence specifically, dispatch a subagent that follows the
  **analyze-dotnet-agent-logs** skill; do not hand-parse the log yourself
  even inside that subagent.
- The main session's job is Steps 2-4: collect verdicts, classify, decide,
  and record the decision. It holds conclusions, not evidence bytes.

## Step 1: is it even a flake?

- Same PR, same test, one run failed and a rerun passed with no code change:
  flake. Continue.
- Fails on every run, or only after a real code change: this is a
  regression, not a flake. Use superpowers:systematic-debugging instead.
- Check `gh run list` for the workflow: has this exact test or job failed
  before on unrelated PRs? Count occurrences of the exact signature across
  recent scheduled runs, and state the rate as per-run, not per-call -- one
  occurrence in 100 runs is not proof of absence. A recurring signature
  across unrelated changes is strong evidence of an environmental cause, not
  a code bug.

## Step 2: classify by evidence, not guess

Before matching a row, trace the failing message or exception string to the
code that emits it, and note whether the assertion sits in the test body or
in fixture setup/teardown (e.g. `RemoteApplicationFixture.TestForKnownProblems`
is a health check, not the test's own logic) -- this decides which row
applies.

| Category | Signature | Evidence to pull |
|---|---|---|
| **Infra / cluster** | Whole class of tests fails identically across every framework/OS variant in the same run; error is a connection, timeout, or account/lock error thrown before any agent code runs | `az monitor metrics list` on the AKS node and load balancer; `kubectl get pods -n unbounded-services -o wide` for restarts/uptime; does a bare retry pass (transient) or fail identically (persistent server state)? |
| **Timing / race** | Assertion is a count or an event that "usually" arrives, on a harvest or aggregator cycle; failure shows the right data, late or short by one cycle | The agent log, via **analyze-dotnet-agent-logs** -- confirm the exact interleaving (e.g. Seen vs Sent counts) before touching the test |
| **Tooling / dependency bug** | Crash signature is inside a third-party tool's native code, not in test or agent code; all tests report `Passed`, only the collector/runner process dies | The crash stack (module name, access-violation code); the tool's own changelog/issue tracker for a fix version already in flight |
| **External / upstream** | Error surfaces in CI infrastructure setup (runner boot, docker daemon, VM image), not in the app or the agent at all | Search the CI vendor's own issue tracker (e.g. `actions/runner-images`) for the exact error string before assuming it's local |
| **Product defect, exposed nondeterministically** | The traced assertion is a health check or invariant (fixture setup/teardown), or the emitting code shows a structural hole (missing try/finally, unguarded async race) | The suspect code path traced above; a repro run that proves the path executed (see Toolkit) |

Never conclude "just flaky" without pulling at least one row's evidence. See
[[feedback_no_guessing]] -- a root cause claim needs a log line, a metric, or
a matching upstream issue behind it, not a hunch. An assertion that exists to
catch a product defect is never reclassified as timing/race and its
threshold is never widened -- trace and fix the defect instead.

## Step 3: pick the remedy for the category

| Category | Remedy | Do NOT |
|---|---|---|
| Infra, transient (network blip) | CI-level retry-once on the job, plus raise the client-side timeout that actually stalled; document as an accepted risk | Silently widen unrelated timeouts repo-wide |
| Infra, persistent server state (account lock, stuck pod) | Operational fix (restart the pod/service); decide explicitly whether to harden the exposure, and record that decision even if the answer is "no, too costly" | Write code to retry around infra state that a retry cannot clear |
| Timing / race in event-harvest assertions | A race-free wait keyed on the **exact asserted evidence** (e.g. an `AgentLogBase.WaitForMetricAggregateCallCount`-style helper polling the real aggregate) | Bump a harvest-cycle magic number again -- each prior bump on the same test is evidence the race is still there, only the window changed |
| Tooling bug with no correctness impact | Wait for the upstream version bump already in flight; record the specific version and re-check after it lands | Change unrelated project settings (e.g. PDB format) as a workaround before confirming the bump doesn't already fix it |
| External/upstream CI regression | Apply the vendor's own recommended workaround inside the repo's CI composite action/step; link the upstream issue in a comment; remove the workaround once fixed upstream | Invent a bespoke workaround when the vendor already published one |
| Product defect, exposed nondeterministically | Trace the code path, file a ticket, leave the check alone; hand off to superpowers:systematic-debugging with the gathered evidence as the starting hypothesis and stop triage | Reclassify as timing/race, widen the assertion or its timeout, or suppress it to keep CI green |

## Step 4: record the decision

Every one of the categories above ends in a decision someone will ask about
again ("why don't we just fix the LB exposure", "did coverlet ever get
bumped"). Capture, in a project memory or handoff note:

1. The failure signature and the run/job that showed it.
2. The evidence that ruled out the other categories.
3. The remedy chosen, and what was explicitly rejected and why.
4. Anything left open (a version to watch for, a follow-up not started).

## Toolkit quick reference

Each row runs inside a subagent per the hard rule above; the main session
sees only the verdict it returns.

- Both `gh run view --job <id> --log-failed` and `--log` truncate silently
  past ~889 lines / 168KB, cutting off mid agent-log dump. For the complete
  text: `gh api repos/<org>/<repo>/actions/jobs/<jobid>/logs`. Copy any log to
  a file before a step that clears it -- lost evidence cannot be re-created.
  The pristine agent log also survives as CI artifact
  `integration-test-results-<matrix>` (`all_solutions.yml` uploads
  `C:\IntegrationTestWorkingDirectory\**\*.log` unconditionally).
- `az monitor metrics list --resource <aks-or-lb-id> --metric <name>` --
  cluster/LB health at the failure timestamp (`MSYS_NO_PATHCONV=1` needed for
  the slash-prefixed resource ID in Git Bash).
- `kubectl get pods -n unbounded-services -o wide` -- pod restarts/uptime
  after `az aks get-credentials`.
- **analyze-dotnet-agent-logs** skill -- agent-side evidence (harvest
  timing, Seen/Sent counts, connect sequence) from the test's own log.
- **run-integration-tests** skill -- reproduce the failing test locally
  before trusting a fix. A null result only counts if the code path is
  proven to have executed (log entry/exit counts); when the agent itself is
  a suspect, rerun once with the agent detached to isolate the confound.
  Delegate the run itself, per that skill's own subagent guidance; take back
  the summary, not the console output.

## Common mistakes

- Treating "all variants failed the same way" as proof of a code bug -- it
  is usually the opposite: identical failure across unrelated variants
  points at a shared external dependency, not the code under test.
- This pattern covers whole-class failures across unrelated variants only --
  a single test failing once, or a fixture health-check assertion firing, is
  not covered by it and may be the product-defect row instead.
- Fixing the assertion's timeout/retry count instead of the mechanism that
  makes the wait racy. If a similar bump has already happened once on this
  test, that is the signal to stop bumping and find the mechanism.
- Closing the investigation without writing down why the infra issue was
  not hardened -- the same question comes back next time the flake recurs.
- Reading a CI log or agent log directly in the main session "just this
  once" -- delegate it every time; a single raw read is how a multi-step
  investigation quietly fills the context window.

