Skip to content

Commit 7e54b33

Browse files
committed
fix(agent): address #597 review — honest TIMEOUT remedy + nits (B1, N1-N4)
Scott's re-review (vs clean main) — one blocker + nits, all in the error-legibility feature this PR ships. - B1 (blocker): the new TIMEOUT classification pointed operators at BUILD_VERIFY_TIMEOUT_S — but NOTHING in agent/ reads it on this branch (verify_build/verify_lint call run_cmd with no timeout=, so they use the 600s default; the env is only SET in CDK). And the anchoring comment's 'live-caught on ABCA-667 via mise run build' narrative is contradicted by the code: the verify path CATCHES TimeoutExpired and returns a plain failure, so the regex never fires for that path. Took Scott's option (b): reworded the comment to describe the real trigger (any UNCAUGHT run_cmd TimeoutExpired — clone/setup), and softened the remedy to a generic 'retry / contact admin' with no dead-knob promise. (The actual BUILD_VERIFY_TIMEOUT_S wiring lives on the ECS-substrate track, not this main-bound branch — out of scope here.) Also reconciled the ecs-agent-cluster.ts comment to say the env is provisioned ahead of its consuming wiring rather than currently effective. - N1: added explicit errorClass: ErrorClass.USER with a comment stating the no-auto-retry intent (re-running an unchanged slow build just times out again; must NOT be 'fixed' to TRANSIENT). - N2: hardened the regex to match float seconds (\d+(?:\.\d+)?). - N3: replaced the false-confidence allowlist test with a real one — a line that hits a marker (error:) AND a noise term (0 errors), placed outside the tail window so it's only reachable via the marker scan; deleting _FAILURE_LINE_NOISE now fails the test. - N4: added a >40-marker-line test that pins the truncation-cap breadcrumb. Gates: cdk 2439 tests + synth + eslint; agent 1278 tests + ruff + ty.
1 parent 05f07e9 commit 7e54b33

3 files changed

Lines changed: 63 additions & 14 deletions

File tree

agent/tests/test_shell.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,44 @@ def test_benign_zero_errors_line_not_surfaced_as_failure(self):
268268
blob = "\n".join(text for _, text in logs)
269269
assert "ok 19" in blob
270270

271+
def test_marker_line_with_noise_term_is_filtered_by_allowlist(self):
272+
# N3: the LOAD-BEARING allowlist case. A line that hits a real marker
273+
# (`error:`) AND a noise term (`0 errors`) must be filtered OUT of the
274+
# surfaced set. Placed in the MIDDLE (before the tail window) so it is
275+
# only reachable via the marker scan — if _FAILURE_LINE_NOISE were
276+
# deleted, this line WOULD be surfaced and the assertion would fail.
277+
# A genuine failure line follows so the scan still produces a match set.
278+
noisy = "error: eslint produced 0 errors after autofix retry" # marker + noise
279+
real = "[//cdk:test] FAIL test/handlers/bar.test.ts — boom"
280+
# 30 trailing passing lines push both lines above out of the tail window,
281+
# so `noisy` is only reachable via the marker scan (where the allowlist runs).
282+
stdout = f"{noisy}\n{real}\n" + "\n".join(f"[//agent:test] passing {i}" for i in range(30))
283+
proc = self._completed(1, stdout=stdout, stderr="")
284+
logs = self._run_capturing_logs(proc)
285+
blob = "\n".join(text for _, text in logs)
286+
assert "FAIL test/handlers/bar.test.ts" in blob # real failure surfaced
287+
assert "0 errors after autofix retry" not in blob # noisy marker filtered by allowlist
288+
289+
def test_surfaced_failure_lines_are_capped_and_truncation_marked(self):
290+
# N4: a genuinely huge red run (more than _MAX_SURFACED_FAILURE_LINES
291+
# marker lines) must be capped, with an explicit truncation breadcrumb —
292+
# so it can't flood CloudWatch. Exercises the cap branch the other tests
293+
# never reach.
294+
from shell import _MAX_SURFACED_FAILURE_LINES
295+
296+
n = _MAX_SURFACED_FAILURE_LINES + 10
297+
stdout = "\n".join(f"FAIL test/case_{i}.test.ts — assertion {i}" for i in range(n))
298+
proc = self._completed(1, stdout=stdout, stderr="")
299+
logs = self._run_capturing_logs(proc)
300+
blob = "\n".join(text for _, text in logs)
301+
# The marker-scan hit the cap and emitted the truncation breadcrumb.
302+
assert "… (more failure lines truncated)" in blob
303+
# The breadcrumb appears within the surfaced set BEFORE the tail divider —
304+
# i.e. the matched-line scan was capped, not the whole output dumped.
305+
head = blob.split("--- (trailing output) ---")[0]
306+
head_cases = sum(1 for i in range(n) if f"case_{i}.test.ts" in head)
307+
assert head_cases <= _MAX_SURFACED_FAILURE_LINES
308+
271309
def test_stdout_is_redacted(self):
272310
proc = self._completed(1, stdout="error: pushing with ghp_supersecrettoken123", stderr="")
273311
logs = self._run_capturing_logs(proc)

cdk/src/constructs/ecs-agent-cluster.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,13 @@ export class EcsAgentCluster extends Construct {
184184
LOG_GROUP_NAME: logGroup.logGroupName,
185185
GITHUB_TOKEN_SECRET_ARN: props.githubTokenSecret.secretArn,
186186
// Heavy CI-parity builds on this big-box substrate legitimately run
187-
// longer than the 1800s default (ABCA's own `mise run build` is
188-
// ~50 min cold). Raise the post-agent build-verify cap so a slow-but-
189-
// healthy build isn't mis-reported as a timeout (see post_hooks.py
190-
// BUILD_VERIFY_TIMEOUT_S). ECS-only: AgentCore repos keep the default.
187+
// long (ABCA's own `mise run build` is ~50 min cold), so set a generous
188+
// post-agent build-verify cap here for the ECS-only path. NOTE: the
189+
// consuming side (verify_build/verify_lint reading BUILD_VERIFY_TIMEOUT_S
190+
// and passing it as the subprocess timeout) ships with the ECS-substrate
191+
// work; on `main` today the verify subprocess still uses run_cmd's
192+
// default cap, so this env is provisioned ahead of the wiring rather
193+
// than currently effective. ECS-only: AgentCore repos don't set it.
191194
BUILD_VERIFY_TIMEOUT_S: '3600',
192195
...(props.memoryId && { MEMORY_ID: props.memoryId }),
193196
// #502: the payload bucket name so the orchestrator-issued

cdk/src/handlers/shared/error-classifier.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -434,21 +434,29 @@ const PATTERNS: readonly ErrorPattern[] = [
434434

435435
// --- Timeout ---
436436
{
437-
// The build/verify command shelled out and was KILLED at the wall-clock cap
438-
// (Python subprocess `TimeoutExpired … timed out after N seconds`). Live-caught
439-
// on ABCA-667: the fork's full `mise run build` (~2800 tests) exceeded the
440-
// 600s default and surfaced as a bare "Unexpected error". This is NOT a code
441-
// failure — the build didn't fail, it ran too long — so name it precisely and
442-
// point at the timeout, not the diff. On a big repo the fix is a higher
443-
// BUILD_VERIFY_TIMEOUT_S (or the ECS build box), which an admin sets — but a
444-
// one-off may just be slow, so it's a user-actionable "retry / raise the cap".
445-
pattern: /TimeoutExpired.*timed out after \d+ ?s(econds)?|Command .*build.* timed out/i,
437+
// An ``agent/`` subprocess (``run_cmd``) hit its wall-clock cap and Python
438+
// raised an uncaught ``TimeoutExpired … timed out after N seconds``, which
439+
// otherwise surfaces as a bare "Unexpected error". This is NOT a code
440+
// failure — the command didn't fail, it ran too long — so name it precisely
441+
// and point at "slow, retry", not the diff. NOTE: this matches only
442+
// UNCAUGHT ``run_cmd`` timeouts (clone/setup/etc.); the post-agent build/lint
443+
// VERIFY path catches ``TimeoutExpired`` itself and returns a plain build
444+
// failure, so it does not reach here. The remedy is deliberately generic
445+
// ("retry / it may be slow") rather than naming a specific env knob — on
446+
// ``main`` the verify timeout is not operator-tunable, so promising a lever
447+
// here would misdirect (the tunable ``BUILD_VERIFY_TIMEOUT_S`` + larger ECS
448+
// build compute live on the ECS-substrate track, not this branch).
449+
pattern: /TimeoutExpired.*timed out after \d+(?:\.\d+)? ?s(econds)?|Command .*build.* timed out/i,
446450
classification: {
447451
category: ErrorCategory.TIMEOUT,
448452
title: 'Build/tests didn\'t finish in time (timed out)',
449453
description: 'The configured build/verify command was still running when it hit the time limit and was stopped — it did not fail, it ran too long.',
450-
remedy: 'This is usually a slow build, not broken code. Retry (a one-off may just be slow); if this repo\'s build is legitimately long, an admin can raise BUILD_VERIFY_TIMEOUT_S or move it to the larger ECS build compute.',
454+
remedy: 'This is usually a slow build, not broken code — retry, since a one-off may just have been slow. If a repo\'s build is legitimately long, its build environment likely needs more time or capacity; contact your ABCA admin.',
451455
retryable: true,
456+
// Intentionally USER (no auto-retry): re-running an unchanged slow build
457+
// just times out again, so this must NOT be TRANSIENT — a human decides
458+
// whether to retry or right-size the build. Do not "fix" this to TRANSIENT.
459+
errorClass: ErrorClass.USER,
452460
},
453461
},
454462
{

0 commit comments

Comments
 (0)