[QA-trace][#184 7/7 финал] Верификация autonomous-runs: инвариант ДЕРЖИТСЯ (real z.ai) + 1 medium TOCTOU-гонка → чиню #241

Closed
opened 2026-06-27 19:08:48 +03:00 by Ghost · 0 comments

Прогон 7/7 (финал) · верификация фичи PR #234 (#184 phase 1, autonomous detached runs) · web-test-orchestrator · РЕАЛЬНЫЙ провайдер z.ai (glm-5.2) · 14 находок → 2 подтв / 0 ложных.
Вердикт фичи (кратко): инвариант ДЕРЖИТСЯ (ход переживает закрытие браузера, переоткрытый чат рендерит ответ, stop≠disconnect), + 1 реальная брешь под гонкой (TOCTOU обход 409). Полный вердикт — ниже.

ИНВАРИАНТ ДЕРЖИТСЯ С ОДНОЙ ОГОВОРКОЙ. Ядро #184 подтверждено на реальном пути (браузер → /api/ai-chat/stream, провайдер z.ai glm-5.2): ход переживает закрытие браузера в середине выполнения, доезжает до succeeded и персистится; реконнект через POST /api/ai-chat/run возвращает сохранённый run + спроецированное assistant-сообщение из БД; явный stop (status=aborted + stop_requested_at) корректно отличается от простого дисконнекта (continues → succeeded, stop_requested_at=NULL). Оговорка: гарантия «один активный run на чат» держится в последовательном сценарии (409 срабатывает, частичный уникальный индекс никогда не допускает >1 активной строки), но ПОД ГОНКОЙ (два одновременных stream на один chatId) 409 обходится TOCTOU-окном: оба хода реально отрабатывают, проигравший INSERT падает на уникальном индексе, ошибка глотается, и второй ход идёт untracked/без runId/неостанавливаемым — нарушение рекламируемого «409 или очередь» (medium). Плюс одно diagnostic-only наблюдение: INFO-строка «run continues server-side» не пишется при дисконнекте (функционально безвредно).

Процесс-трейс верификации #184 «durable detached agent runs» (feat/184-autonomous-agent-runs)

Отчёт для владельца скилла web-test-orchestrator. Фокус — на ПРОЦЕССЕ верификации, а не на коде фичи. Особенность стенда: это серверная фича без клиентского UI (в phase 1 нет новых экранов; runId живёт только в метаданных стрима, /stop принимает chatId) — поэтому оракулом служили API + БД, а не визуал.


1. Сводка и ВЕРДИКТ ФИЧИ

Прогон закрыл главный вопрос: переживает ли ход закрытие браузера, персистится ли, возвращает ли его реконнект, и отличается ли явный stop от дисконнекта. Ответ — да, инвариант держится, проверено на реальном платном провайдере (z.ai glm-5.2), а не на заглушке.

  • Durability: ход стартован через реальный чат-UI, браузерный контекст закрыт пока ai_chat_runs.status='running' (step=0). Ран продолжил выполнение на сервере, settled succeeded, step_count=1, finished_at проставлен, stop_requested_at=NULL. Переоткрытие ТОГО ЖЕ чата в свежем контексте отрисовало полный ответ (не спиннер/не пусто).
  • Reconnect: POST /api/ai-chat/run {chatId} → 200 {run:{полная строка}, message:{assistant, content, status:completed, usage}}; чужой/несуществующий chatId → 403 (owner-gated).
  • Stop vs disconnect: stop по chatId на живом ране → {stopped:true}, ран aborted + stop_requested_at + finished_at, assistant-сообщение финализировано aborted (воспроизведено дважды). Дисконнект без /stop → ран доезжает до succeeded, stop_requested_at=NULL. Поздний /stop на settled-ране → {stopped:false}, штамп не ставится. Пустое тело /stop → 400.
  • One-active-run: 409 срабатывает в последовательном сценарии; БД-инвариант (≤1 активной строки) НИКОГДА не нарушался. НО под гонкой 409 обходится (см. раздел 3) — единственный реальный дефект (medium).

ВЕРДИКТ: фича работает, headline-инвариант держится; одна реальная брешь в «один активный run» под конкурентной гонкой + одно диагностическое наблюдение по логам.


2. Агенты и инструменты — кто и как работал

Сработали 4 роли + независимый верификатор. Все драйвили реальный путь приложения (browser → /api/ai-chat/stream через aiFetch-обёртку), что критично: на этом стенде была явная установка не подменять путь сырым curl/openai-sdk.

  • recon (6 находок) — разметка швов. Прочитал контроллер / run-repo / run-service / клиентский transport / chatStreamMetadata; через Playwright залогинился и выполнил in-browser fetch ровно тем телом, что шлёт useChat, получил живой SSE с runId в start.messageMetadata; psql как ground-truth по ai_chat_runs/ai_chat_messages. Заложил карту эндпоинтов и lifecycle (beginRun/finalizeRun) для остальных.
  • durable-detached (2 находки)ядро инварианта. Двухфазный Playwright e2e: контекст A стартует ход и закрывается в середине; контекст B переоткрывает чат. psql на двух таблицах как оракул, authed curl на /run, full-page screenshot отрисованного ответа. Именно здесь подтверждён «закрыл браузер → доехало → реконнект вернул».
  • stop-vs-disconnect (3 находки) — семантика abort. Playwright стартует ходы через реальный UI (header AI-кнопка, composer Enter), curl c authToken-cookie бьёт /stop, psql + чтение серверного лога (/proc/<pid>/fd → q184-server.log) как оракул. Развёл Case A (stop→aborted) и Case B (disconnect→succeeded).
  • web-test-orchestrator (3 находки)состязательная конкуренция. curl с authToken-cookie (JWT несёт sessionId — ровно то, что требует /stream, т.е. тот же путь, что у DefaultChatTransport), Python http.client + threading/Barrier для одновременных/таймированных запросов, 30–50ms watcher по psql на активные строки. Нашёл TOCTOU-гонку и одновременно подтвердил, что БД-инвариант (≤1 строки) держится.
  • VERIFIER — пересобрал с нуля 2 небанальных находки через реальный stream-путь: race (Barrier-синхронизация двух потоков на свежевставленный chatId, кросс-чек метаданных/psql/лога) и observability-лог (curl --max-time 3 чтобы дропнуть клиента до первого чанка провайдера; проверил, что в бинаре лог-вызов реально есть и флаг=true, исключив stale-build/flag-off).

Как справились с отсутствием клиентского UI. Ни один из проверяемых аспектов не имел экрана. Агенты осознанно сделали БД и API оракулом: ai_chat_runs (status, stop_requested_at, started/finished_at, assistant_message_id, step_count) и ai_chat_messages (роль/контент/status) давали объективную истину состояния, а /run и /stop — наблюдаемое поведение. UI использовался ТОЛЬКО там, где он нужен по существу — стартануть настоящий ход и отрисовать переоткрытый чат (durable-detached). Это не ограничило покрытие: все ветки lifecycle, реконнекта и stop проверены через API+DB точнее, чем дал бы UI. Единственная честная брешь — наблюдаемость INFO-лога (его нет в UI и его отсутствие функционально безвредно).


3. Находки по классу (что / как / кем / этап)

feature-works (подтверждено):

  • Реальный ход через app-path: 200 text/event-stream, живой ответ glm-5.2, usage — recon, in-browser fetch + psql.
  • runId в start-метаданных (только при включённом флаге) — recon, парс SSE.
  • Lifecycle running→succeeded, поля заполнены, assistant_message_id связан — recon, psql сразу после хода.
  • /run реконнект возвращает run+message, owner-gating 403 — recon.
  • /stop shaping и owner-gating (400 на пусто, 200 {stopped:false} на неактивное) — recon, 5 проб.
  • Durable run переживает закрытие браузера, переоткрытый чат рендерит ответdurable-detached, двухфазный e2e (ядро).
  • Дисконнект ≠ stop (onClose в autonomous-режиме не зовёт abort) — durable-detached + чтение контроллера.
  • Stop vs disconnect разведены (aborted+штамп против succeeded+NULL) — stop-vs-disconnect.
  • 409 срабатывает, частичный уникальный индекс не допускает 2 активных строки — web-test-orchestrator, таймированный тест.
  • Нет регрессии: обычный чат при флаге ON (send/reply, history, reconnect, stop, lifecycle) — web-test-orchestrator.

feature-broken (1, medium):

  • Гонка двойного сабмита обходит 409: два одновременных /stream на один chatId оба → 200 и оба стримят реальный ответ; runId только у победителя; проигравший beginRun INSERT падает на уникальном индексе, ошибка глотается (ai-chat.service.ts ~L397/L388-405), ход идёт untracked, без runId, не таргетируется /stop, и т.к. autonomousRuns=on — onClose его тоже не abort'ит → orphan-ход. БД-инвариант (≤1 строки) при этом держится. Этап: execution (adversarial concurrency)web-test-orchestrator; подтверждено независимым верификатором (Barrier-репро, лог q184-server.log:314 со swallowed-ошибкой).

observability (1, diagnostic-only):

  • INFO «run continues server-side» не пишется при дисконнекте; функционально безвредно (abort в autonomous завязан на run-signal, не на сокет). Reported likely → верификатор поднял до verified (репро с --max-time 3 до первого чанка; grep всего лога = 0; проверено, что вызов лога есть в бинаре, флаг=true) — stop-vs-disconnect → VERIFIER.

env-limitation (2, не дефекты фичи):

  • scratchpad/token.py теневит stdlib token → Playwright-импорт виснет из scratchpad-каталога; + установленному playwright 1.60 нужен executable_path на chromium-1148. Обойдено запуском из чистого подкаталога — recon.
  • Частичный-ответ-на-stop не удалось проверить: высокий TTFB z.ai (abort попадал в PRE-RESPONSE окно, 0 символов partial). Код onAbort (flushAssistant, ai-chat.service.ts:828) корректен — нечего было персистить — stop-vs-disconnect.

4. Фолс-позитивы

0 ложных срабатываний. Верификатор независимо пересобрал обе небанальные находки (race и observability-лог) — обе подтверждены, ни одна не опровергнута и не понижена. Наоборот, неуверенная находка (likely) была повышена до verified. Позитивные верификации (feature-works) не отправлялись на повторную проверку, т.к. опирались на прямой БД/API-оракул, а не на скриншот-«похоже работает» — то есть классический источник success-hallucination здесь был структурно исключён.


5. Где скилл сработал, а где спотыкался (server-only / async-durability / реальный AI)

Сработало:

  • Async-durability «закрыл браузер → доехало» взято честно. Ключевой манёвр — закрывать контекст ИМЕННО пока status='running'/step=0, а потом поллить psql до settled. Это поймало настоящий инвариант, а не «успело до закрытия».
  • БД+лог как оракул вместо UI. Для фичи без экранов скилл правильно сместил оракул на ai_chat_runs/ai_chat_messages + серверный лог через /proc/<pid>/fd. Это дало детерминированную истину, недоступную через UI.
  • Реальный app-path, а не подмена. Везде использован путь browser→/stream (in-browser fetch либо curl с sessionId-JWT, эквивалентным DefaultChatTransport). Это прямой урок из прошлых факапов (aiFetch-обёртку нельзя обходить) — здесь не нарушен.
  • Doer-Verifier добил неуверенность. Независимый репро с проверкой «а есть ли вообще лог-вызов в бинаре и включён ли флаг» снял ложные объяснения (stale-build / flag-off) — образцовая дисциплина.
  • Состязательный этап нашёл то, что последовательный пропустил. Без Barrier-синхронизации TOCTOU-гонка не воспроизводится; скилл не ограничился happy-path 409.

Спотыкалось / ограничения:

  • Реальный платный провайдер диктовал темп. Высокий и нестабильный TTFB z.ai (firstChunkLatency 5–66s) сделал НЕвоспроизводимым сценарий «partial-ответ на stop» — abort стабильно попадал в pre-response окно. Это не баг фичи, но дыра в покрытии: персист частичного текста при aborted не проверен реально.
  • Бюджет токенов как жёсткий лимит. Установка «1 короткий промпт на тест, ~6 ходов всего» ограничивает число повторов гонки/конкуренции — race поймана, но статистики устойчивости (как часто обходится 409) нет.
  • Тулинговое трение на старте. token.py-теневание stdlib и рассинхрон версии playwright/chromium сожгли время на bisect до того, как пошла собственно верификация.
  • Наблюдаемость дисконнекта непокрыта по дизайну. Отсутствие INFO-лога поймано grep'ом, но под headless-chromium 'close'-хендлер вёл себя нестабильно — скилл не смог детерминированно отделить «лог не пишется» от «сокет-событие не пришло», оставив находку диагностической.

6. Предложения по скиллу (как тестировать server-only / async-durability фичи)

  1. Явный паттерн «durability-окно». Зашить в скилл шаг: дождаться по оракулу (БД/лог), что асинхронная работа РЕАЛЬНО в состоянии running (а не pending/уже done), и только тогда рвать клиента. Без этой синхронизации тест проверяет не то.
  2. БД/лог-оракул как первоклассный режим для фич без UI. Формализовать выбор оракула: если у фичи нет экрана — оракулом по умолчанию становятся таблицы состояния + серверный лог + API-проекция, а UI используется только для «настоящего входа» (старт хода) и финальной отрисовки.
  3. Состязательный этап обязателен для инвариантов «ровно один / только один». Любую гарантию уникальности/единственности гонять через Barrier-синхронизированные параллельные запросы — последовательный happy-path систематически пропускает TOCTOU. Сделать это чек-листным пунктом.
  4. Контроль «свежести бинаря и флага» перед negative-находкой. Прежде чем рапортовать «лог/поведение отсутствует», верификатор должен подтвердить, что код реально в запущенном бинаре и фича-флаг включён (как и сделано здесь) — внести в обязательный verifier-протокол.
  5. Детерминизм против латентности реального провайдера. Для сценариев, чувствительных к таймингу (partial-on-stop), предусмотреть либо медленный/контролируемый стаб как ВТОРОЙ прогон рядом с реальным (реальный — что путь работает; стаб — что персист частичного текста корректен), либо явно помечать такие сценарии как «непокрыто из-за TTFB», а не «ок».
  6. Преднастройка окружения. Добавить guard: запускать python из чистого изолированного каталога (защита от теневания stdlib вроде token.py) и фиксировать executable_path/версию chromium — чтобы тулинговое трение не съедало бюджет до начала верификации.
  7. Разделять класс «observability/diagnostic-only» от «feature-broken». Скилл уже сделал это правильно (лог-миссинг не раздут до дефекта) — закрепить это как явную категорию в оракуле, чтобы безвредные диагностические пробелы не шумели в вердикте.

📌 Итог серии 7/7. Все 7 прогонов с процесс-трейс-отчётами: #235 #236 (#119 git-sync ×2), #237 #238 (#120 offline ×2), #239 (#221 captions — работает), #240 (#228 footnotes — была сломана → починил fa929c9e), этот (#184 — работает + 1 medium гонка). Скилл-фидбэк по серии: verifier незаменим (убивал ложные контрольными экспериментами); главные провалы — нулевая межролевая дедупликация (1 баг = N находок), недо-сэмплирование verifier'а на all-pass фичах, и (отдельно) тот случай с зависанием на bash-промпте (не баг скилла — окружение).

Найденную гонку (TOCTOU обход 409) чиню в #234 — второй конкурентный stream должен отклоняться (409), а не идти untracked.

🤖 web-test-orchestrator (process-trace, feature-verify)

> **Прогон 7/7 (финал)** · верификация фичи **PR #234 (#184 phase 1, autonomous detached runs)** · web-test-orchestrator · РЕАЛЬНЫЙ провайдер z.ai (glm-5.2) · 14 находок → 2 подтв / 0 ложных. > **Вердикт фичи (кратко):** инвариант ДЕРЖИТСЯ (ход переживает закрытие браузера, переоткрытый чат рендерит ответ, stop≠disconnect), + 1 реальная брешь под гонкой (TOCTOU обход 409). Полный вердикт — ниже. > > ИНВАРИАНТ ДЕРЖИТСЯ С ОДНОЙ ОГОВОРКОЙ. Ядро #184 подтверждено на реальном пути (браузер → /api/ai-chat/stream, провайдер z.ai glm-5.2): ход переживает закрытие браузера в середине выполнения, доезжает до succeeded и персистится; реконнект через POST /api/ai-chat/run возвращает сохранённый run + спроецированное assistant-сообщение из БД; явный stop (status=aborted + stop_requested_at) корректно отличается от простого дисконнекта (continues → succeeded, stop_requested_at=NULL). Оговорка: гарантия «один активный run на чат» держится в последовательном сценарии (409 срабатывает, частичный уникальный индекс никогда не допускает >1 активной строки), но ПОД ГОНКОЙ (два одновременных stream на один chatId) 409 обходится TOCTOU-окном: оба хода реально отрабатывают, проигравший INSERT падает на уникальном индексе, ошибка глотается, и второй ход идёт untracked/без runId/неостанавливаемым — нарушение рекламируемого «409 или очередь» (medium). Плюс одно diagnostic-only наблюдение: INFO-строка «run continues server-side» не пишется при дисконнекте (функционально безвредно). # Процесс-трейс верификации #184 «durable detached agent runs» (feat/184-autonomous-agent-runs) Отчёт для владельца скилла **web-test-orchestrator**. Фокус — на ПРОЦЕССЕ верификации, а не на коде фичи. Особенность стенда: это **серверная фича без клиентского UI** (в phase 1 нет новых экранов; runId живёт только в метаданных стрима, /stop принимает chatId) — поэтому оракулом служили API + БД, а не визуал. --- ## 1. Сводка и ВЕРДИКТ ФИЧИ Прогон закрыл главный вопрос: **переживает ли ход закрытие браузера, персистится ли, возвращает ли его реконнект, и отличается ли явный stop от дисконнекта.** Ответ — **да, инвариант держится**, проверено на реальном платном провайдере (z.ai glm-5.2), а не на заглушке. - **Durability**: ход стартован через реальный чат-UI, браузерный контекст закрыт пока `ai_chat_runs.status='running'` (step=0). Ран продолжил выполнение на сервере, settled `succeeded`, `step_count=1`, `finished_at` проставлен, `stop_requested_at=NULL`. Переоткрытие ТОГО ЖЕ чата в свежем контексте отрисовало полный ответ (не спиннер/не пусто). - **Reconnect**: `POST /api/ai-chat/run {chatId}` → 200 `{run:{полная строка}, message:{assistant, content, status:completed, usage}}`; чужой/несуществующий chatId → 403 (owner-gated). - **Stop vs disconnect**: stop по chatId на живом ране → `{stopped:true}`, ран `aborted` + `stop_requested_at` + `finished_at`, assistant-сообщение финализировано `aborted` (воспроизведено дважды). Дисконнект без /stop → ран доезжает до `succeeded`, `stop_requested_at=NULL`. Поздний /stop на settled-ране → `{stopped:false}`, штамп не ставится. Пустое тело /stop → 400. - **One-active-run**: 409 срабатывает в последовательном сценарии; БД-инвариант (≤1 активной строки) НИКОГДА не нарушался. **НО** под гонкой 409 обходится (см. раздел 3) — единственный реальный дефект (medium). **ВЕРДИКТ: фича работает, headline-инвариант держится; одна реальная брешь в «один активный run» под конкурентной гонкой + одно диагностическое наблюдение по логам.** --- ## 2. Агенты и инструменты — кто и как работал Сработали 4 роли + независимый верификатор. Все драйвили **реальный путь приложения** (browser → /api/ai-chat/stream через aiFetch-обёртку), что критично: на этом стенде была явная установка не подменять путь сырым curl/openai-sdk. - **recon (6 находок)** — разметка швов. Прочитал контроллер / run-repo / run-service / клиентский transport / chatStreamMetadata; через Playwright залогинился и выполнил **in-browser fetch** ровно тем телом, что шлёт `useChat`, получил живой SSE с runId в `start.messageMetadata`; psql как ground-truth по `ai_chat_runs`/`ai_chat_messages`. Заложил карту эндпоинтов и lifecycle (beginRun/finalizeRun) для остальных. - **durable-detached (2 находки)** — **ядро инварианта**. Двухфазный Playwright e2e: контекст A стартует ход и закрывается в середине; контекст B переоткрывает чат. psql на двух таблицах как оракул, authed curl на /run, full-page screenshot отрисованного ответа. Именно здесь подтверждён «закрыл браузер → доехало → реконнект вернул». - **stop-vs-disconnect (3 находки)** — семантика abort. Playwright стартует ходы через реальный UI (header AI-кнопка, composer Enter), curl c authToken-cookie бьёт /stop, psql + чтение серверного лога (`/proc/<pid>/fd` → q184-server.log) как оракул. Развёл Case A (stop→aborted) и Case B (disconnect→succeeded). - **web-test-orchestrator (3 находки)** — **состязательная конкуренция**. curl с authToken-cookie (JWT несёт sessionId — ровно то, что требует /stream, т.е. тот же путь, что у DefaultChatTransport), Python `http.client` + `threading`/Barrier для одновременных/таймированных запросов, 30–50ms watcher по psql на активные строки. Нашёл TOCTOU-гонку и одновременно подтвердил, что БД-инвариант (≤1 строки) держится. - **VERIFIER** — пересобрал с нуля **2** небанальных находки через реальный stream-путь: race (Barrier-синхронизация двух потоков на свежевставленный chatId, кросс-чек метаданных/psql/лога) и observability-лог (curl --max-time 3 чтобы дропнуть клиента до первого чанка провайдера; проверил, что в бинаре лог-вызов реально есть и флаг=true, исключив stale-build/flag-off). **Как справились с отсутствием клиентского UI.** Ни один из проверяемых аспектов не имел экрана. Агенты осознанно сделали **БД и API оракулом**: `ai_chat_runs` (status, stop_requested_at, started/finished_at, assistant_message_id, step_count) и `ai_chat_messages` (роль/контент/status) давали объективную истину состояния, а /run и /stop — наблюдаемое поведение. UI использовался ТОЛЬКО там, где он нужен по существу — стартануть настоящий ход и отрисовать переоткрытый чат (durable-detached). Это не ограничило покрытие: все ветки lifecycle, реконнекта и stop проверены через API+DB точнее, чем дал бы UI. Единственная честная брешь — наблюдаемость INFO-лога (его нет в UI и его отсутствие функционально безвредно). --- ## 3. Находки по классу (что / как / кем / этап) **feature-works (подтверждено):** - Реальный ход через app-path: 200 text/event-stream, живой ответ glm-5.2, usage — *recon, in-browser fetch + psql*. - runId в start-метаданных (только при включённом флаге) — *recon, парс SSE*. - Lifecycle `running→succeeded`, поля заполнены, assistant_message_id связан — *recon, psql сразу после хода*. - /run реконнект возвращает run+message, owner-gating 403 — *recon*. - /stop shaping и owner-gating (400 на пусто, 200 {stopped:false} на неактивное) — *recon, 5 проб*. - **Durable run переживает закрытие браузера, переоткрытый чат рендерит ответ** — *durable-detached, двухфазный e2e* (ядро). - Дисконнект ≠ stop (onClose в autonomous-режиме не зовёт abort) — *durable-detached + чтение контроллера*. - Stop vs disconnect разведены (aborted+штамп против succeeded+NULL) — *stop-vs-disconnect*. - 409 срабатывает, частичный уникальный индекс не допускает 2 активных строки — *web-test-orchestrator, таймированный тест*. - Нет регрессии: обычный чат при флаге ON (send/reply, history, reconnect, stop, lifecycle) — *web-test-orchestrator*. **feature-broken (1, medium):** - **Гонка двойного сабмита обходит 409**: два одновременных /stream на один chatId оба → 200 и оба стримят реальный ответ; runId только у победителя; проигравший beginRun INSERT падает на уникальном индексе, ошибка глотается (ai-chat.service.ts ~L397/L388-405), ход идёт untracked, без runId, не таргетируется /stop, и т.к. autonomousRuns=on — onClose его тоже не abort'ит → orphan-ход. БД-инвариант (≤1 строки) при этом держится. Этап: **execution (adversarial concurrency)** — *web-test-orchestrator*; **подтверждено независимым верификатором** (Barrier-репро, лог q184-server.log:314 со swallowed-ошибкой). **observability (1, diagnostic-only):** - INFO «run continues server-side» не пишется при дисконнекте; функционально безвредно (abort в autonomous завязан на run-signal, не на сокет). Reported `likely` → верификатор **поднял до `verified`** (репро с --max-time 3 до первого чанка; grep всего лога = 0; проверено, что вызов лога есть в бинаре, флаг=true) — *stop-vs-disconnect → VERIFIER*. **env-limitation (2, не дефекты фичи):** - scratchpad/`token.py` теневит stdlib `token` → Playwright-импорт виснет из scratchpad-каталога; + установленному playwright 1.60 нужен `executable_path` на chromium-1148. Обойдено запуском из чистого подкаталога — *recon*. - Частичный-ответ-на-stop не удалось проверить: высокий TTFB z.ai (abort попадал в PRE-RESPONSE окно, 0 символов partial). Код onAbort (flushAssistant, ai-chat.service.ts:828) корректен — нечего было персистить — *stop-vs-disconnect*. --- ## 4. Фолс-позитивы **0 ложных срабатываний.** Верификатор независимо пересобрал обе небанальные находки (race и observability-лог) — **обе подтверждены**, ни одна не опровергнута и не понижена. Наоборот, неуверенная находка (`likely`) была повышена до `verified`. Позитивные верификации (feature-works) не отправлялись на повторную проверку, т.к. опирались на прямой БД/API-оракул, а не на скриншот-«похоже работает» — то есть классический источник success-hallucination здесь был структурно исключён. --- ## 5. Где скилл сработал, а где спотыкался (server-only / async-durability / реальный AI) **Сработало:** - **Async-durability «закрыл браузер → доехало» взято честно.** Ключевой манёвр — закрывать контекст ИМЕННО пока `status='running'`/step=0, а потом поллить psql до settled. Это поймало настоящий инвариант, а не «успело до закрытия». - **БД+лог как оракул вместо UI.** Для фичи без экранов скилл правильно сместил оракул на `ai_chat_runs`/`ai_chat_messages` + серверный лог через `/proc/<pid>/fd`. Это дало детерминированную истину, недоступную через UI. - **Реальный app-path, а не подмена.** Везде использован путь browser→/stream (in-browser fetch либо curl с sessionId-JWT, эквивалентным DefaultChatTransport). Это прямой урок из прошлых факапов (aiFetch-обёртку нельзя обходить) — здесь не нарушен. - **Doer-Verifier добил неуверенность.** Независимый репро с проверкой «а есть ли вообще лог-вызов в бинаре и включён ли флаг» снял ложные объяснения (stale-build / flag-off) — образцовая дисциплина. - **Состязательный этап нашёл то, что последовательный пропустил.** Без Barrier-синхронизации TOCTOU-гонка не воспроизводится; скилл не ограничился happy-path 409. **Спотыкалось / ограничения:** - **Реальный платный провайдер диктовал темп.** Высокий и нестабильный TTFB z.ai (firstChunkLatency 5–66s) сделал НЕвоспроизводимым сценарий «partial-ответ на stop» — abort стабильно попадал в pre-response окно. Это не баг фичи, но **дыра в покрытии**: персист частичного текста при aborted не проверен реально. - **Бюджет токенов как жёсткий лимит.** Установка «1 короткий промпт на тест, ~6 ходов всего» ограничивает число повторов гонки/конкуренции — race поймана, но статистики устойчивости (как часто обходится 409) нет. - **Тулинговое трение на старте.** `token.py`-теневание stdlib и рассинхрон версии playwright/chromium сожгли время на bisect до того, как пошла собственно верификация. - **Наблюдаемость дисконнекта непокрыта по дизайну.** Отсутствие INFO-лога поймано grep'ом, но под headless-chromium 'close'-хендлер вёл себя нестабильно — скилл не смог детерминированно отделить «лог не пишется» от «сокет-событие не пришло», оставив находку диагностической. --- ## 6. Предложения по скиллу (как тестировать server-only / async-durability фичи) 1. **Явный паттерн «durability-окно».** Зашить в скилл шаг: дождаться по оракулу (БД/лог), что асинхронная работа РЕАЛЬНО в состоянии `running` (а не `pending`/уже `done`), и только тогда рвать клиента. Без этой синхронизации тест проверяет не то. 2. **БД/лог-оракул как первоклассный режим для фич без UI.** Формализовать выбор оракула: если у фичи нет экрана — оракулом по умолчанию становятся таблицы состояния + серверный лог + API-проекция, а UI используется только для «настоящего входа» (старт хода) и финальной отрисовки. 3. **Состязательный этап обязателен для инвариантов «ровно один / только один».** Любую гарантию уникальности/единственности гонять через Barrier-синхронизированные параллельные запросы — последовательный happy-path систематически пропускает TOCTOU. Сделать это чек-листным пунктом. 4. **Контроль «свежести бинаря и флага» перед negative-находкой.** Прежде чем рапортовать «лог/поведение отсутствует», верификатор должен подтвердить, что код реально в запущенном бинаре и фича-флаг включён (как и сделано здесь) — внести в обязательный verifier-протокол. 5. **Детерминизм против латентности реального провайдера.** Для сценариев, чувствительных к таймингу (partial-on-stop), предусмотреть либо медленный/контролируемый стаб как ВТОРОЙ прогон рядом с реальным (реальный — что путь работает; стаб — что персист частичного текста корректен), либо явно помечать такие сценарии как «непокрыто из-за TTFB», а не «ок». 6. **Преднастройка окружения.** Добавить guard: запускать python из чистого изолированного каталога (защита от теневания stdlib вроде `token.py`) и фиксировать `executable_path`/версию chromium — чтобы тулинговое трение не съедало бюджет до начала верификации. 7. **Разделять класс «observability/diagnostic-only» от «feature-broken».** Скилл уже сделал это правильно (лог-миссинг не раздут до дефекта) — закрепить это как явную категорию в оракуле, чтобы безвредные диагностические пробелы не шумели в вердикте. --- 📌 **Итог серии 7/7.** Все 7 прогонов с процесс-трейс-отчётами: #235 #236 (#119 git-sync ×2), #237 #238 (#120 offline ×2), #239 (#221 captions — работает), #240 (#228 footnotes — была сломана → починил `fa929c9e`), этот (#184 — работает + 1 medium гонка). **Скилл-фидбэк по серии:** verifier незаменим (убивал ложные контрольными экспериментами); главные провалы — нулевая межролевая дедупликация (1 баг = N находок), недо-сэмплирование verifier'а на all-pass фичах, и (отдельно) тот случай с зависанием на bash-промпте (не баг скилла — окружение). **Найденную гонку (TOCTOU обход 409) чиню** в #234 — второй конкурентный stream должен отклоняться (409), а не идти untracked. 🤖 web-test-orchestrator (process-trace, feature-verify)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#241