Investigate (find the cause, not a symptom)
Discipline beats guessing. Do not propose a fix until you can point at the line that's wrong and say why. Never edit a test to make a real failure pass.
Method
- Reproduce. Get the exact failing command and run it:
mix test path:line, the LiveView action, the MCP call, the failing context function iniex -S mix. No repro → you're guessing. - Read the WHOLE error. The full stacktrace, the changeset errors, the SQL. The first frame in our code (not a dep) is usually the spot. Don't skim to the summary line.
- Locate + read the code. Open the failing function and the data it touched.
Confirm the contract of anything you're unsure of (
/tooling-verify-api) — half of "bugs" are an assumed return shape or arg that was never real. - One hypothesis, then confirm it against the code/data before fixing. If the evidence doesn't match, the hypothesis is wrong — don't fix anyway.
- Minimal fix at the cause. Then add a regression test that fails before / passes after (and the denial / cross-account variant if it's a context bug, §7).
emisar error catalogue (where to look first)
{:error, %Ecto.Changeset{}}— readchangeset.errors. A*_constrainterror (unique/foreign_key) means the DB rejected it: the matchingunique_constraint/foreign_key_constraintis missing from the Changeset, or the migration lacks the index/FK. Avalidate_*error is just invalid input.{:error, :unauthorized}— the subject lacks the permission (Authorizer role list), orensure_has_permissionsis checking the wrong one.{:error, :not_found}— genuinely missing, ORAuthorizer.for_subjectscoped it out (cross-account) — check the subject's account vs the row's.- Recurrent job keeps failing — read the
execute/1error and the durable rows it polls; a job that isn't idempotent (IL-13) corrupts state when a tick repeats. - LiveView — value missing/doubled on load →
mountruns twice (IL-18); a silent form failure → check the{:error, changeset}branch is handled, not the UI. - MCP — a doubled action → the operation-id recovery path
(
get_operation/MCPOperations.fetch_recovery), not an idempotency key: that column and module were dropped in migration20260811000000. An auth error → the subject built at the MCP boundary. - Runner socket — state not updating → the
Runners.apply_state/mark_*path (§1.4); a crash shouldn't take down other runners (IL-17). - Flaky test — a cross-process race:
$callersnot inherited, or async side effects not made sync in test (thenotify_approvers_async?flag pattern). NeverProcess.sleepit away —assert_receivewith a timeout.
Output
Cause: <file:line — what's wrong and why> → Fix: <the minimal change> →
Regression test: <what it asserts>. If you can't isolate it, say what you ruled
out and what you'd instrument next — don't ship a guess.