From d0947d852cccfbbd6f380a9bcc3ae3b8f8f1ae61 Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 3 May 2026 11:02:01 +0700 Subject: [PATCH] fix(runner): use errgroup-bound ctx so siblings cancel on failure The errgroup's bound ctx was discarded; goroutines closed over the outer ctx, so neither a sibling failure nor the future ability to propagate per-step cancellation reached the V8 extractor's CDP round-trip. Switch closures to gctx and document why Wait()'s error is intentionally discarded. --- internal/runner/runner.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/internal/runner/runner.go b/internal/runner/runner.go index de668f3..a3e0619 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -74,25 +74,29 @@ func Run(ctx context.Context, options Options) (Summary, error) { var metrics *trace.Metrics var logs []verifier.LogEntry - g, _ := errgroup.WithContext(ctx) + // gctx is bound to the errgroup so a returned error (or outer + // cancellation) propagates to siblings - notably the V8 extractor + // goroutine, whose CDP round-trip can otherwise outrun the step + // budget on a hung tab. + g, gctx := errgroup.WithContext(ctx) g.Go(func() error { - tree, hierarchyErr = fetchHierarchy(ctx, options.Driver) + tree, hierarchyErr = fetchHierarchy(gctx, options.Driver) return nil }) si := stepIndex g.Go(func() error { - metrics = captureMetrics(ctx, options, logger, si) + metrics = captureMetrics(gctx, options, logger, si) return nil }) logSince := lastLogTime g.Go(func() error { - logs = collectLogs(ctx, options.Driver, logSince) + logs = collectLogs(gctx, options.Driver, logSince) return nil }) var v8Overrides map[int]json.RawMessage if web, ok := options.Driver.(driver.WebDriver); ok { g.Go(func() error { - overrides, err := web.EvaluateExtractors(ctx) + overrides, err := web.EvaluateExtractors(gctx) if err != nil { logger.Warn("v8 extractor evaluation failed", "step", si, "err", err) return nil @@ -104,12 +108,14 @@ func Run(ctx context.Context, options Options) (Summary, error) { if pendingPostScreenshot { postStep := pendingPostScreenshotStep g.Go(func() error { - captureScreenshot(ctx, options, logger, postStep, true) + captureScreenshot(gctx, options, logger, postStep, true) return nil }) pendingPostScreenshot = false } - g.Wait() + // All goroutines write to local variables and return nil, so the Wait + // error is always nil; ignored intentionally. + _ = g.Wait() if hierarchyErr != nil { if isWDADrop(hierarchyErr) {