Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
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>
217 lines
13 KiB
Markdown
217 lines
13 KiB
Markdown
# Sprint 25 — autonomous continuous quality monitoring
|
||
|
||
Цель: чтобы существующие фичи постоянно проверялись на работоспособность,
|
||
и при поломке — автоматическая попытка починить через инцидент-задачу в
|
||
очередь Server-Claude'а.
|
||
|
||
Старт: 2026-06-08. Исполнитель: Claude Opus 4.7.
|
||
|
||
## Чек-лист
|
||
|
||
- [x] **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`.
|
||
|
||
- [x] **2. Auto-fix loop при двукратном падении** — при consecutive_fail≥2
|
||
watchdog создаёт `~/.fm-watchdog/incident-{ts}-{step}.txt` + копирует в
|
||
очередь Server-Claude'а как `queue/0000-incident-*` (префикс `0000-`
|
||
гарантирует сортировку в начало). После завершения spirinta Server-Claude
|
||
возвращается к текущему.
|
||
|
||
- [x] **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
|
||
последних прогонов.
|
||
|
||
- [x] **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 изоляции — самый дорогой баг, поэтому ежечасный
|
||
контроль.
|
||
|
||
- [x] **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 (собираем стартовую выборку).
|
||
|
||
- [x] **6. Hangfire job: cleanup quality test orgs** —
|
||
`HousekeepingJobs.PruneQualityTestOrgsAsync` (recurring-id
|
||
`prune-quality-test-orgs`, default cron `30 2 * * *` UTC). Удаляет
|
||
org'и `quality-%` старше 24h: динамически по `information_schema`
|
||
находит все tenant-таблицы с `OrganizationId`, итеративно DELETE'ит
|
||
с обработкой FK-violation (до 10 проходов), затем чистит
|
||
`AspNetUserRoles/Tokens/Claims/Logins`, `OpenIddictTokens/Authorizations`
|
||
по email-шаблону `quality-%@test-fm.local`, и finally `users` +
|
||
`organizations`. Threshold переопределяется через
|
||
`Cleanup:QualityTestOrgHours`.
|
||
|
||
- [x] **7. Status badge в README** — `<!-- quality-badge -->` маркер с
|
||
эмодзи 🟢/🟡/🔴 + ссылкой на `docs/quality-status.md`. Обновляется
|
||
через `scripts/quality-dashboard.py`. 🟢 = последний run all green;
|
||
🟡 = красные шаги, но <2 consecutive (transient); 🔴 = 2+
|
||
consecutive (incident-уровень).
|
||
|
||
- [x] **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-step `consecutive_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` теперь содержит:
|
||
```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.
|
||
|
||
Реализация:
|
||
1. `_db.Organizations.IgnoreQueryFilters().Where(Name LIKE 'quality-%' AND CreatedAt < threshold)` → candidate ids.
|
||
2. `DO $do$ ... END $do$` блок с `org_ids uuid[] := ARRAY[...]`.
|
||
3. Итеративный FOR pass IN 1..10 LOOP по
|
||
`information_schema.columns WHERE column_name = 'OrganizationId'`
|
||
— каждая итерация пытается DELETE из каждой tenant-таблицы; при
|
||
`foreign_key_violation` пропускает (следующий проход уберёт parent
|
||
запись). Реально хватает 2 проходов (нашёл employees ↔ employee_roles).
|
||
4. AspNetUserRoles/Tokens/Claims/Logins/OpenIddictTokens/Authorizations
|
||
через email-pattern `quality-%@test-fm.local`.
|
||
5. `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` инжектит маркированный блок:
|
||
```html
|
||
<!-- 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 финального прогона.
|