Hourly smoke watchdog + auto-fix loop + dashboard + multi-tenant guard + perf regression + cleanup job + README badge. 1. ~/quality-watchdog.sh (cron 5 * * * *) — 8 checks (~60s): /health/ready, signup→login→/api/me, GET products, Playwright UI smoke (3.1 product CRUD), /metrics format, /hubs/notifications negotiate with token, multi-tenant isolation, perf p95. 2. Auto-fix loop: 2× consecutive red → ~/.fm-watchdog/incident-*.txt + queue/0000-incident-* to bump it ahead of Server-Claude's sprint queue. fm-watchdog.sh sees prefix 0000- as next. 3. scripts/quality-dashboard.py — renders docs/quality-status.md (current emoji, 8-step table, perf baseline, 7-day history, 24-run sparkline) + injects README badge 🟢/🟡/🔴. 4. Multi-tenant smoke: signup 2 orgs `quality-{epoch}-A/B`, create product in A, verify B sees 404/403 + total=0. 5. Perf regression: p95 over 10 reqs for /api/me, products, sales/retail/stats. Baseline = median of last 10 samples (robust to noise). >50% from baseline → alert. First 5 runs always green (warm-up). 6. HousekeepingJobs.PruneQualityTestOrgsAsync (cron 30 2 * * * UTC): finds orgs `quality-%` older than 24h, dynamically scans information_schema for tables with OrganizationId, iteratively DELETEs with FK-violation retry (up to 10 passes), then cleans AspNetUser*/OpenIddict* by email pattern `quality-%@test-fm.local`, finally users + organizations. 7. README badge: <!-- quality-badge --> marker updated each run. Validated: stage deploy ✓, Hangfire job registered ✓, dry-run SQL on 24 stage candidates → 0 remaining ✓, 3 cron-triggered runs all 8/8 green (12:42/12:45/12:48 +05) ✓. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
13 KiB
Sprint 25 — autonomous continuous quality monitoring
Цель: чтобы существующие фичи постоянно проверялись на работоспособность, и при поломке — автоматическая попытка починить через инцидент-задачу в очередь Server-Claude'а.
Старт: 2026-06-08. Исполнитель: Claude Opus 4.7.
Чек-лист
-
1. Hourly smoke watchdog —
~/quality-watchdog.shзапускается cron'ом каждый час в минуту 5. 8 проверок (~60 сек): /health/ready, signup→login→/api/me, GET /api/catalog/products, Playwright UI flow (3.1 product create-list-get), /metrics, /hubs/notifications/negotiate, multi-tenant изоляция, performance p95. Падение → Telegram + лог в~/.fm-watchdog/quality.log. -
2. Auto-fix loop при двукратном падении — при consecutive_fail≥2 watchdog создаёт
~/.fm-watchdog/incident-{ts}-{step}.txt+ копирует в очередь Server-Claude'а какqueue/0000-incident-*(префикс0000-гарантирует сортировку в начало). После завершения spirinta Server-Claude возвращается к текущему. -
3. Quality dashboard —
docs/quality-status.mdрендерится hourly черезscripts/quality-dashboard.py: текущий статус-эмодзи, список 8 шагов с last-green/last-red, perf baseline (median p95 по последним 10 измерениям), история падений за 7 дней, sparkline 24 последних прогонов. -
4. Multi-tenant smoke 24/7 — создаёт TWO org с уникальными именами
quality-{epoch}-A/B, заводит товар в A, проверяет что B не видит его ни через GET-by-id (404/403), ни через search (total=0). Leak в multi-tenant изоляции — самый дорогой баг, поэтому ежечасный контроль. -
5. Performance regression hourly — p95 за 10 повторов для
/api/me,/api/catalog/products,/api/sales/retail/stats. Baseline = median последних 10 измерений (robust к шуму). Регрессия = текущий p95 >50% от baseline → alert + (при consecutive≥2) incident. Первые 5 запусков всегда green (собираем стартовую выборку). -
6. Hangfire job: cleanup quality test orgs —
HousekeepingJobs.PruneQualityTestOrgsAsync(recurring-idprune-quality-test-orgs, default cron30 2 * * *UTC). Удаляет org'иquality-%старше 24h: динамически поinformation_schemaнаходит все tenant-таблицы сOrganizationId, итеративно DELETE'ит с обработкой FK-violation (до 10 проходов), затем чиститAspNetUserRoles/Tokens/Claims/Logins,OpenIddictTokens/Authorizationsпо email-шаблонуquality-%@test-fm.local, и finallyusers+organizations. Threshold переопределяется черезCleanup:QualityTestOrgHours. -
7. Status badge в README —
<!-- quality-badge -->маркер с эмодзи 🟢/🟡/🔴 + ссылкой наdocs/quality-status.md. Обновляется черезscripts/quality-dashboard.py. 🟢 = последний run all green; 🟡 = красные шаги, но <2 consecutive (transient); 🔴 = 2+ consecutive (incident-уровень). -
8. Cron-валидация — временно поменял на
*/3 * * * *, поймал 3 cron-запуска подряд (12:42 / 12:45 / 12:48 +05), все 8/8 green. Вернул на hourly (5 * * * *). См.~/.fm-watchdog/quality-cron.log.
Журнал
2026-06-08 старт
Sprint 24 закрыт (8/8). Поехали по quality watchdog.
2026-06-08 quality-watchdog.sh (#1)
Главный скрипт ~16 КБ:
- shell-helpers
state_get/state_set/mark_red/mark_green/create_incident. - Все шаги логируют в
~/.fm-watchdog/quality.log+ state в~/.fm-watchdog/quality-state.json(per-stepconsecutive_fail,last_green,last_red,last_detail). - Telegram-нотификация на каждый red-шаг. Если token/chat не настроены (default на dev-vm) — просто лог без падения.
- Лог ротируется при превышении 1 МБ.
Подобранные через debug refs:
/api/catalog/units-of-measure(не/api/refs/units)/api/catalog/product-groups(обязательное поле ProductGroupId)- POST product требует
barcodes: [{code, type, isPrimary}]+ price[{priceTypeId, amount, currencyId}]где priceTypeId = isRequired-row из/api/catalog/price-types.
2026-06-08 auto-fix loop (#2)
create_incident пишет 2 файла: один в ~/.fm-watchdog/incident-{ts}-{step}.txt
(для архива/обзора инженером), другой в ~/.fm-watchdog/queue/0000-incident-{ts}-{step}.txt.
Префикс 0000- критичен — fm-watchdog.sh ротирует очередь через
ls | sort | head -1, так что инцидент-задача рулит раньше любой
плановой работы. Проверил через симуляцию (STAGE_URL=https://nonexistent.example.invalid):
1-я итерация — yellow, 2-я — RED + 7 incident-файлов в очереди (по одному
на каждый упавший шаг, кроме ui_flow — там node_modules + playwright всё
равно работают).
2026-06-08 dashboard + history (#3)
scripts/quality-dashboard.py (Python 3.11+, no deps) рендерит markdown
из quality-history.jsonl (1 JSONL-строка на прогон, append-only,
trimmed до 400 последних = ~2 недели hourly). Содержит:
- Текущий статус-эмодзи.
- Таблицу 8 шагов:
статус | last-change | consecutive_fail. - Performance baseline (median p95 по 10 последним измерениям).
- История за 7 дней + green-ratio %.
- Sparkline последних 24 прогонов (🟢/🔴).
updateReadmeBadge инжектит <!-- quality-badge --> ... <!-- /quality-badge -->
маркер в README между h1 и existing badges.
2026-06-08 multi-tenant scenario (#4)
2 signup'a с unique quality-{epoch}-A/B, токены через /connect/token,
POST product в A → GET в B должен вернуть 404/403 + search в B должен
дать total=0. Хитрость: B видит "Все товары" (root group)
своей организации (создан bootstrap'ом при signup), не A's; это
правильно. Leak бы означал, что product создан вне tenant'a A.
2026-06-08 performance baseline (#5)
Первая версия использовала min(old, new) как baseline — после первого
быстрого замера (cold connection) baseline=200ms, и любой warm-replay
295ms давал +47% и срабатывал alert. Переделал на median последних 10
измерений + первые 5 запусков всегда green (warm-up). Это сглаживает
шум и держит alert на действительно sustained регрессиях.
Файл ~/.fm-watchdog/quality-perf-baseline.json теперь содержит:
{
"median": { "_api_me": 245, ... },
"samples": { "_api_me": [216, 245, 297, ...], ... }
}
2026-06-08 PruneQualityTestOrgs job (#6)
Добавил метод в HousekeepingJobs + регистрация в
HangfireJobsConfigurator под id prune-quality-test-orgs,
cron 30 2 * * * UTC.
Реализация:
_db.Organizations.IgnoreQueryFilters().Where(Name LIKE 'quality-%' AND CreatedAt < threshold)→ candidate ids.DO $do$ ... END $do$блок сorg_ids uuid[] := ARRAY[...].- Итеративный FOR pass IN 1..10 LOOP по
information_schema.columns WHERE column_name = 'OrganizationId'— каждая итерация пытается DELETE из каждой tenant-таблицы; приforeign_key_violationпропускает (следующий проход уберёт parent запись). Реально хватает 2 проходов (нашёл employees ↔ employee_roles). - AspNetUserRoles/Tokens/Claims/Logins/OpenIddictTokens/Authorizations
через email-pattern
quality-%@test-fm.local. users+organizationsотдельным шагом в конце.
Dry-run на stage (24 кандидата, ROLLBACK): pass 1 remaining=2, pass 2
remaining=0, итог 0 orgs в quality-% — работает.
2026-06-08 README badge (#7)
updateReadmeBadge инжектит маркированный блок:
<!-- quality-badge --> 🟢 **Quality:** [`docs/quality-status.md`](docs/quality-status.md) <!-- /quality-badge -->
Каждый запуск watchdog'a обновляет эмодзи по результату последнего прогона.
2026-06-08 cron-валидация (#8)
Crontab: 5 * * * * /home/nns/quality-watchdog.sh >> ~/.fm-watchdog/quality-cron.log 2>&1.
Для быстрого подтверждения временно поменял на */3 * * * *, поймал
3 запуска подряд (12:42 / 12:45 / 12:48 +05), все 8/8 green. Восстановил
hourly.
Архитектура
~/quality-watchdog.sh (cron 5 * * * *)
↓ запускает 8 проверок
├── HTTP curl (steps 1, 2, 3, 5, 6, 7, 8)
└── Playwright @smoke (step 4, использует tests/regression/flows/03)
↓ итог
├── ~/.fm-watchdog/quality.log (append-only)
├── ~/.fm-watchdog/quality-state.json (per-step consecutive_fail)
├── ~/.fm-watchdog/quality-history.jsonl (1 line/run, 400 lines max)
├── ~/.fm-watchdog/quality-perf-baseline.json (median + samples)
└── docs/quality-status.md + README badge (через quality-dashboard.py)
↓ если consecutive_fail ≥ 2
├── ~/.fm-watchdog/incident-{ts}-{step}.txt
└── ~/.fm-watchdog/queue/0000-incident-{ts}-{step}.txt (для fm-watchdog.sh ротации)
Hangfire job `prune-quality-test-orgs` (cron 30 2 * * * UTC):
- находит orgs `quality-%` старше 24h
- итеративно DELETE'ит из всех tenant-таблиц
- чистит Identity+OpenIddict по email-pattern
- DELETE users + organizations
Что найдено и зафиксировано
- Stage perf шумит на старте: первые 1-2 запроса к /api/sales/retail/stats доходят до 400ms (cold DB cache), затем 200-250ms steady. Поэтому baseline стал median-based, не min-based.
- POST product требует non-trivial payload: barcodes (массив объектов
с code/type/isPrimary), prices (массив с priceTypeId/amount/currencyId),
productGroupId — все обязательные. Зафиксил в watchdog: вытягивает
refs из дефолтных значений orga через
/api/catalog/{units,product-groups,price-types,currencies}. - FK dependency между employees и employee_roles: первый DELETE на
employee_rolesвалится на employees.RoleId FK. Решил итеративным ретраем (10 проходов) — гибкая стратегия, не привязанная к ручному порядку, новые tenant-таблицы автоматом подцепятся.
Метрики
| До Sprint 25 | После | Δ | |
|---|---|---|---|
| Watchdog scripts | fm-watchdog.sh + nightly-verify.sh + nightly-perf-check.sh |
+ quality-watchdog.sh + quality-dashboard.py |
+2 |
| Cron entries | 2 | 3 | +1 |
| Hangfire recurring jobs | 11 | 12 | +1 (prune-quality-test-orgs) |
| Hourly smoke шаги | (нет hourly smoke) | 8 шагов | new |
Docs (.md в docs/) |
60 | 62 | +2 (quality-status.md, sprint25-progress.md) |
| README badges | 5 | 6 | +1 (quality 🟢/🟡/🔴) |
| Auto-fix integration | (нет) | incident → fm-watchdog queue | new |
Итог
8/8 ✓. 3 cron-trigger'd runs все green. ~/.fm-watchdog/DONE создан
после ratiosit's финального прогона.