{ "provenance": { "sourceHead": "5301119aa8f6f681fe3cae3cd229a3263e5419ab", "originalOutcome": "timeout", "originalReviewCount": 5, "paidOutcomeReclassified": false, "sourcePublicProofSHA256": "c903530934f014824cccde4f5dd17cc6c9d92b4966caaf1174772fdad268ab5c", "sourceVisibleSHA256": "1449a832d6a3c79b599bce30a8b89ead5803393536f0a1dae53ba83d46a9ed54", "sourceScreenSHA256": "c5e480ba5231235956f8c71dae152999fa5e3f8623b35e1ad1a24f7fc44e88fe", "projection": "Exact terminal screen and last acknowledged public same-file Edit/result pair. No pending native ID was retained; test hook epochs are explicitly synthetic." }, "screen": " 176 -_Test coverage decision and further sections follow._ \n 176 +### Issue 5 [P1] (confidence: 8/10) PLAN.md:15-18 — no tests for the retry framework \n 177 +**Decision: 5A (approved).** Every GAP in the diagram gets a test in this PR, \n 178 +written alongside the code, not after: \n 179 + \n 180 +| Test file (placeholder names) | Kind | Asserts | \n 181 +|---|---|---| \n 182 +| `jobs/retry/backoffStrategy.test` | unit | delay ∈ [0, min(cap, base·2^n)] for n=0..maxAttempts; never > maxDela\n +yMs; two calls at same attempt differ (jitter); attempt ≥ maxAttempts returns terminal sentinel | \n 183 +| `jobs/retry/classify.test` | unit | timeout, ECONNRESET, 5xx, 429 → retryable; 4xx≠429, validation, unknown → te\n +rminal (table-driven, one row per error type) | \n 184 +| `jobs/retry/withRetry.test` | unit | success → no failure row; retryable → ledger row + rethrow; terminal → Unre\n +coverable and no rethrow-to-retry; ledger write failure → retry still scheduled + error logged | \n 185 +| `jobs/retry/policy.test` | unit | defaults match table in Issue 3; per-worker override validated; invalid values\n + rejected | \n 186 +| `workers/.test` (x5) | unit | handler invoked through withRetry with the expected policy | \n 187 +| `webhooks/ledger.test` | unit | one row per attempt, attempt strictly increasing; concurrent duplicate for same \n +event id → single delivery | \n 188 +| `processWebhookJob.regression.test` | E2E | **CRITICAL** 2xx-then-timeout → no duplicate distinct event, same ev\n +ent id, one `delivered` row; 4xx → one attempt | \n 189 +| `webhooks/delivery.e2e.test` | E2E | transient 5xx then 2xx → one event id, delivered; 4xx → rejected, no retry;\n + 429 → retried | \n 190 +| `jobs/retry/dlq.e2e.test` | E2E | exhaustion → DLQ + exactly one alert; maxAge exceeded → DLQ; replay → new chai\n +n, same event id | \n 191 + \n 192 +E2E tests run against the real job library and a fake HTTP receiver; no \n 193 +mocking of the retry hook. \n 194 + \n 195 +## 4. Performance \n 196 + \n 197 +_Decision follows._ \n╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌\n Do you want to make this edit to gstack-test-plan-eng-batching.md?\n ❯ 1. Yes\n 2. Yes, and switch to accept edits (auto-approve file edits and common file commands) for this session; Yes, and\n always allow access to /tmp/gstack-paid-shard-dY0BU1/tmp/gstack-e2e-plan-eng-batching-PfFS0a for this session\n (shift+tab)\n 3. No\n\n Esc to cancel · Tab to amend", "priorCompletedEdit": [ { "line": 138, "kind": "use", "timestamp": "2026-09-10T19:53:42.858Z", "sessionId": "66c806c5-6230-4227-a9c1-265da2f39ab0", "messageId": "msg_011CevLsKGJurCS3hVnUXESC", "requestId": "req_011CevLsHryRJ5H8QEHxGJ3u", "toolUseId": "toolu_01XkYcRRq69pesUitEzJLF5Q", "name": "Edit", "input": { "replace_all": false, "file_path": "/tmp/gstack-paid-shard-dY0BU1/tmp/gstack-e2e-plan-eng-batching-PfFS0a/gstack-test-plan-eng-batching.md", "old_string": "## 2. Code Quality (decisions)\n\n_Further sections follow as they are agreed._", "new_string": "## 2. Code Quality (decisions)\n\n### Issue 4 [P1] (confidence: 9/10) PLAN.md:11-13 — retry envelope duplicated across 5 workers\n**Decision: 4A (approved).** Extract one `withRetry(policy, handler)` wrapper\nin a shared module (e.g. `jobs/retry/withRetry`) before touching any worker,\nthen convert all 5 workers to thin call sites in this PR. The wrapper owns:\nerror classification (retryable vs terminal), attempt logging, ledger write\n(decision 2A), and hand-off to the library retry hook (decision 1A). Workers\nown only their handler body. Order: wrapper + tests first, then workers one\ncommit each (structural change, then behavioral change; never both at once).\n\n```\n worker file (x5) shared module (x1)\n ┌────────────────────┐ ┌──────────────────────────────────┐\n │ export default │ │ withRetry(policy, handler) │\n │ withRetry( │ ──────► │ try handler(job) │\n │ policy, │ │ catch e: │\n │ handleXJob) │ │ classify(e) → terminal|retry │\n └────────────────────┘ │ ledger.record(job, attempt) │\n │ terminal → throw Unrecoverable│\n │ retry → rethrow (library) │\n └──────────────────────────────────┘\n backoffStrategy(attempt) → delayMs (1A/3A)\n```\n\n## 3. Tests\n\nTest framework: none detectable in this repo (only PLAN.md and CLAUDE.md are\nchecked in). Coverage diagram produced; concrete test file names use the\n`*.test.ts` convention as a placeholder and must match the real repo.\n\n### REGRESSION (mandatory, CRITICAL, no approval needed)\n`processWebhookJob()` is rewritten (PLAN.md:16-17). Its prior at-most-once\nbehavior is existing behavior with no planned test. Per the regression rule\nthis test is added as a critical requirement:\n\n- **`processWebhookJob.regression.test`** — given a receiver that returns 2xx\n then the connection drops before the response is read (or the worker crashes\n after a 2xx), assert the receiver observes exactly one delivery with the\n same `X-Webhook-Event-Id` on any second attempt, and the ledger shows one\n `delivered` row. Also assert that a 4xx (≠429) response produces exactly one\n attempt and no retry. This pins the new contract: at-least-once with a\n stable dedupe key, never a second *distinct* event.\n\n### Coverage diagram\n\n```\nCODE PATHS USER / OPERATOR FLOWS\n[+] jobs/retry/backoffStrategy [+] Webhook receiver\n ├── [GAP] attempt 0..n → delay within [0, cap] ├── [GAP] [→E2E] transient 5xx then 2xx: one event id, delivered\n ├── [GAP] delay never exceeds maxDelayMs ├── [GAP] [→E2E] 4xx: single attempt, marked rejected\n ├── [GAP] jitter: two calls same attempt differ └── [CRITICAL] [→E2E] 2xx then timeout: no duplicate event (regression)\n └── [GAP] attempt ≥ maxAttempts → -1 / terminal\n[+] jobs/retry/withRetry [+] Operator\n ├── [GAP] handler succeeds → no ledger failure row ├── [GAP] job exhausts → lands in DLQ, alert fires once\n ├── [GAP] retryable error → ledger row + rethrow ├── [GAP] job older than maxAge → stops retrying, DLQ\n ├── [GAP] terminal error → Unrecoverable, no retry └── [GAP] replay from DLQ → new attempt chain, same event id\n ├── [GAP] classify(): timeout / conn reset / 5xx / 429\n ├── [GAP] classify(): 4xx ≠ 429 / validation / unknown\n └── [GAP] ledger write fails → retry still scheduled, error logged\n[+] jobs/retry/policy\n └── [GAP] defaults + per-worker override validation\n[+] workers/* (x5)\n └── [GAP] each worker: handler invoked via withRetry with expected policy\n[+] webhook ledger\n ├── [GAP] one row per attempt, monotonically increasing attempt\n └── [GAP] concurrent duplicate job for same event id → single delivery\n\nCOVERAGE: 0/20 paths tested (0%) | Code paths: 0/14 | Flows: 0/6\nQUALITY: none yet | GAPS: 20 (3 E2E, 1 CRITICAL regression, 0 eval)\n```\n\nLegend: ★★★ behavior + edge + error | ★★ happy path | ★ smoke | [→E2E] integration test\n\n_Test coverage decision and further sections follow._" } }, { "line": 139, "kind": "result", "timestamp": "2026-09-10T19:53:43.195Z", "sessionId": "66c806c5-6230-4227-a9c1-265da2f39ab0", "toolUseId": "toolu_01XkYcRRq69pesUitEzJLF5Q", "isError": false, "content": "The file /tmp/gstack-paid-shard-dY0BU1/tmp/gstack-e2e-plan-eng-batching-PfFS0a/gstack-test-plan-eng-batching.md has been updated successfully. (file state is current in your context — no need to Read it back)" } ], "pendingEditId": null, "expectedPath": "/tmp/gstack-paid-shard-dY0BU1/tmp/gstack-e2e-plan-eng-batching-PfFS0a/gstack-test-plan-eng-batching.md", "cwd": "/tmp/gstack-paid-shard-dY0BU1/tmp/gstack-plan-count-g6ojGV", "sessionId": "66c806c5-6230-4227-a9c1-265da2f39ab0", "lastMatchedDisplayCompletion": "⎿ Added 17 lines", "laterNativeCompletionHasDisplayMatch": false }