fix(scripts): forgejo timeout coverage for python oracles and create-readme #45
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/scripts/forgejo-timeout-coverage"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Что сделано
Покрыты все непокрытые call sites к Forgejo API в Python-оракулах и create-readme (issue #42, Волна 1 после PR#41):
pipeline-status.py:_forgejo_request—urlopen(req, timeout=FORGEJO_TIMEOUT_S)+ retry-цикл (3×, 1с sleep, skip на первой попытке). Retry наURLError(timeout/connection) и HTTP 5xx; НЕ на 4xx. При исчерпании —return (0, "", "[forgejo] ... failed after N retries").project-status.py:_forgejo_get— тот же паттерн (urlopen timeout + retry).spec-status.py:_forgejo_get— тот же паттерн.pipeline-status.ts,project-status.ts,spec-status.ts—spawnSync(..., { timeout: 60000 })+ обработкаstatus === null(SIGTERM → "timed out (60s)").create-readme.ts— мигрировал прямойfetch()на локальный_fetchWithRetry(AbortSignal.timeout + retry на timeout/5xx, НЕ на 4xx). GET принимает 200/404 (404 = нет README, не ошибка); PUT принимает 200/201.OPENCODE_FORGEJO_TIMEOUT(10с),OPENCODE_FORGEJO_RETRY(3),OPENCODE_FORGEJO_RETRY_INTERVAL(1) — Python через_env_int/os.environ.get, TS через_envInt.print(..., file=sys.stderr)с[forgejo]prefix (только retry/error). TS —process.stderr.writeс[forgejo]prefix.test_pipeline_status.py— 10 новых тестов (success/no-retry, timeout-retries-fail, timeout-then-success, 5xx-retries-then-success, 4xx-no-retry 404/401, 5xx-exhausted, retry=0, timeout-param).test_pipeline_status_tool.py— 2 новых (timeout=60000 в opts, status=null → readable error).test_create_readme_tool.py— 4 новых (timeout-then-success, 4xx-404-no-retry, 4xx-401-no-retry, 5xx-retry-then-4xx)._ts_loader.mjs— stub поддерживаетthrow: "timeout"иsignalв response.Почему
PR#41 (issue #40) пофиксил hang в
_shared.ts:callForgejo, но аудит выявил непокрытые call sites — тот же класс 5-мин hang'а. Python-оракулы вызывалиurlopenбезtimeout=, TS-обёртки —spawnSyncбезtimeout=. Один hung-вызов побеждал бюджет CI poll loop. Худший кейс теперь ≤32с (10с×3 retry) на любой Forgejo-вызов в оракулах, ≤60с на уровне spawnSync.Watch out
wait_timeout=300НЕ трогал (by design — ожидание CI билда).create-readme.tsиспользует локальный_fetchWithRetryвместоcallForgejoиз_shared.ts:callForgejoвозвращает spawnSync-shape ({status:0|1, stdout, stderr}), несовместимый с HTTP-response семантикой create-readme (нужен 404 как не-ошибка для GET, 200/201 для PUT)._sleepSyncв create-readme использует busy-wait (spawnSync path синхронный, event loop недоступен). Для retry-интервала 1с это приемлемо.AbortSignal.timeout()требует Node 18+ / Bun — среда opencode поддерживает.ProjectType valid-typeв project-status.py — не связаны с этим PR.Pending
Closes #42
Closes #42
Code Review Summary
PR добавляет
urlopen(timeout=)+ retry (3×, 1с sleep, retry на timeout/5xx, no-retry на 4xx) в 3 Python-оракула,spawnSync timeout: 60000+ обработкуstatus === null(SIGTERM) в 3 TS-обёртках, и локальный_fetchWithRetryвcreate-readme.ts. 16 новых тестов покрывают timeout/retry/4xx-no-retry. Худший кейс latency снижен с 5-мин hang до ≤32с (Python) / ≤60с (spawnSync). Качество реализации высокое, решения задокументированы в PR body.Положительные моменты
urlopen(req, timeout=FORGEJO_TIMEOUT_S)+ циклfor attempt in range(1, max_attempts + 1)сtime.sleep(skip на первой попытке). Retry наURLError(timeout/connection) и HTTP 5xx; 4xx → immediate return. Порядок except-блоков правильный:HTTPError→URLError→OSError(от конкретного к общему).isinstance(e.reason, socket.timeout)корректно различает timeout от connection error.max(1, FORGEJO_RETRY)/Math.max(1, FORGEJO_RETRY)— консистентность между Python и TS:RETRY=0→ 1 попытка, без infinite loop.timeout: 60000+if (r.status === null)с проверкойr.signal === "SIGTERM"→ читаемое "timed out (60s)". Проверкаstatus === nullстоит ДОstatus !== 0— порядок правильный. 60s имеет headroom над Python-внутренним 10с×3+2×1=32с.OPENCODE_FORGEJO_TIMEOUT/RETRY/RETRY_INTERVALчитаются во всех 4 файлах (3 Python + create-readme.ts)..env.exampleдокументирует worst-case latency 32с.print(..., file=sys.stderr)/process.stderr.writeс[forgejo]prefix, только retry/error (не на success). Не засоряют stdout.wait_timeout=300не тронут — by design (ожидание CI билда), корректно._forgejo_request/_forgejo_getне изменились ((status, body, err)), существующие вызовы работают без правок._ts_loader.mjsрасширен поддержкойthrow: "timeout"/"abort"иsignalв response.## Что сделано/## Почему/## Watch out/## Pendingзаполнены осмысленно. Решение по локальному_fetchWithRetryвместоcallForgejoявно обосновано (несовместимость возвращаемого значения: spawnSync-shape vs HTTP-response семантика с 404 как не-ошибкой).Замечания (info, не блокирующие)
.opencode/tools/create-readme.ts:397[dead code]else if (!getRes.ok && getRes.status === 0)— недостижимая ветка. При timeoutgetRes.status === 0, и предыдущая проверкаgetRes.status !== 404(строка 395) перехватывает его первой, возвращая"HTTP 0: <error text>"вместо более понятного"Forgejo GET failed: <error text>"из строки 398. Ошибка всё равно возвращается (не блокирующее), но сообщение вводит в заблуждение ("HTTP 0" — не HTTP-статус). Fix: переставить проверкуstatus === 0ДОstatus !== 404, или удалить dead code и принять "HTTP 0" как есть.Дублирование retry-логики в 3 Python-оракулах [duplication]
pipeline-status.py,project-status.py,spec-status.pyсодержат идентичный retry-цикл ~40 строк (×3 = ~120 строк). Это сознательный компромисс для независимости оракулов (нет общего модуля, только stdlib imports). Можно вынести в общий_forgejo_requesthelper, но это требует изменения архитектуры (общий модуль или sys.path манипуляция). Не блокирующее — оракулы намеренно standalone.create-readme.tsне переиспользуетcallForgejoиз_shared.ts[duplication] Локальный_fetchWithRetry(~50 строк) дублируетcallForgejo(timeout, retry, 4xx-no-retry,[forgejo]логи). PR body объясняет причину:callForgejoвозвращает spawnSync-shape{status:0|1, stdout, stderr}, несовместимый с HTTP-response семантикой create-readme (нужен 404 как не-ошибка для GET, 200/201 для PUT). Альтернатива — расширитьcallForgejoопциейokStatuses: number[]вместо одиночногоokStatus. Не блокирующее — решение задокументировано,_sleepSyncbusy-wait приемлем для 1с интервала..opencode/scripts/pipeline-status.py:446-448[style]FORGEJO_TIMEOUT_S/FORGEJO_RETRY/FORGEJO_RETRY_INTERVAL_Sопределены после_forgejo_request(строка 93), которая их использует. Работает (функция читает globals при вызове, не при определении), но вproject-status.py:206иspec-status.py:131константы определены ДО_forgejo_get— более чистый top-down порядок. Не блокирующее — стилистическое.tests/test_pipeline_status.py:1462[test fragility]assert ps.FORGEJO_TIMEOUT_S == 10зависит от того, чтоOPENCODE_FORGEJO_TIMEOUTне задан в env pytest (или равен 10). CI не задаёт (docker-compose.yml не содержит), но тест хрупкий. Fix:monkeypatch.delenv("OPENCODE_FORGEJO_TIMEOUT", raising=False)перед вызовом, или assert через_env_intнапрямую.Нет теста "все попытки timeout" для
create-readme[test coverage] Ветка ошибки timeout (status=0, все retry исчерпаны) в_fetchWithRetryне покрыта. Тестtest_remote_create_timeout_then_successпроверяет timeout→retry→success, но не timeout→timeout→timeout→fail. Не блокирующее — основной happy/sad path покрыт.Verdict: APPROVE