Files
main/audits/2026-10-11/reports/08-test-quality-and-effectiveness-report.md
T
codex-bot d07fbfabb1 docs: archive technical audit 2026-10-11 in develop (#36)
Переносит архив `audits/2026-10-11/` (19 файлов) из codex/audit-remediation-2026-10-11 в develop и добавляет точечный allowlist в .gitignore.
2026-10-17 19:38:03 +03:00

431 lines
92 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Аудит 08 — Test Quality & Effectiveness
## 1. Audit metadata
- **Дата:** 2026-10-11.
- **Задача:** проверить, какие тесты различают сохранённые бизнес-инварианты и реальные регрессии, а какие проверяют другой сценарий, слишком раннее состояние или устаревший контракт.
- **Протокол:** `audit-prompts/08-test-quality-and-effectiveness.md`.
- **Workspace:** `/Users/ksv741/Project/vibe/WealthLedger`; это родительская папка четырёх самостоятельных репозиториев. Единственный создаваемый artifact — этот отчёт вне них.
| Checkout | Абсолютный путь | Точный HEAD | Начальное состояние |
| --- | --- | --- | --- |
| root | `/Users/ksv741/Project/vibe/WealthLedger` | `dc030f84c979cedb2628ef4bd97c62d6c63a9256` | `git status --short`: пусто |
| frontend | `/Users/ksv741/Project/vibe/WealthLedger/frontend` | `c8821200ce699dab1ce65360d1968d5877a2e78b` | чисто |
| backend | `/Users/ksv741/Project/vibe/WealthLedger/backend` | `0ad84ff4f0b4712abc7e69dc299856f14af6eadd` | чисто |
| specs | `/Users/ksv741/Project/vibe/WealthLedger/specs` | `fcdc805f6c1b3be6a20a7e858e3e80cbd83a9121` | чисто |
| docs | `/Users/ksv741/Project/vibe/WealthLedger/docs` | `e6fd4f64b81dd73b33959b845a1babcfac1891c8` | чисто |
**Инструкции и skills.** Прочитаны локальные `AGENTS.md`, engineering/testing conventions и необходимые data/auth/integration правила. Установленный `ponytail-audit` загружен и применён к стоимости и кандидатам удаления тестов. Его фактический scope — over-engineering; корректность assertions исследована по протоколу аудита 08, а не приписана этому skill. `graphify` загружен; существующего `graphify-out/graph.json` нет. Построение графа не выполнялось, поскольку оно создаёт дополнительные artifacts и противоречит read-only контракту. Subagents не использовались.
**Предыдущие сведения.** `audit-results/technical-audit-2026-10-11.md` прочитан как реестр гипотез. Его ссылки на race tests, Portfolio и MOEX перепроверены по текущему вызовному пути. Совпадение HEAD с предыдущим аудитом не использовано как доказательство.
**Выполнено:** статическое чтение, поиски имён/consumers/assertions, `git rev-parse HEAD`, `git status --short`, одноразовый inventory через `rg --files`, проверка отчёта и diff. Дополнительно выполнен один целевой запуск двух чистых денежных suites:
```sh
source "$HOME/.nvm/nvm.sh"
nvm use --silent
npm test -- --runInBand --no-cache --coverage=false --runTestsByPath src/integrations/tbank/decimal.spec.ts src/modules/banking/bank-cards/domain/rub-money.spec.ts
```
Working directory: `backend`. **Результат: 2 suites PASS, 28 tests PASS, 0 snapshots; Jest сообщил 0.342 s.** Это время одного локального запуска, не измерение CI или общей эффективности. npm выдал предупреждение о пользовательской настройке `//registry`; выполнение завершилось успешно. Эти suites импортируют денежные функции, не открывают DB и не обращаются к upstream.
**Не выполнялось:** install, codegen, migrations, build, full-suite, verify, SQLite/HTTP suites, browser suites, coverage collection, mutation/removal исходников, mutation framework, обращения к брокерским/банковским API, commit/PR. Runtime-прохождение остальных тестов и предполагаемые исходы мутантов обозначаются отдельно. Исходники, тесты, конфигурации и четыре checkout не изменены.
## 2. Executive verdict
1. **Факт: сильная тестовая основа существует.** Денежные cases используют независимо заданные строки и границы `>2^53`/signed64; auth concurrency проходит реальный SQLite CAS; operations rollback выполняет часть записи до отказа следующего интервала. Их нельзя заменять одними mocks или удалять как «дублирующие coverage».
2. **Главный риск: новый владелец обновления почти не проверен как выполняемый flow.** В inventory не найдены прямые проверки `tick`, `reserveHourly`, `settleHourly`; три integration suites явно заменяют scheduler. Проверки готового `meta` не доказывают запуск, сроки, завершение и результат реального обхода. Это единственный P1 finding о пробеле защиты.
3. **Факт: backend late-portfolio test не создаёт заявленную гонку.** Deferred установлен на RPC, но stimulus — локальный GET; PUT тоже не запускает Portfolio owner. Тест не проверяет отклонение поздней публикации. Отдельные соседние тесты также сохранили названия failed refresh/recovery при прямой подстановке DB-state.
4. **Риск: frontend race assertions не всегда стоят после завершения проигравшего.** StocksPage проверяет уже видимый новый результат сразу после освобождения старого ответа. В resource tests ожидается promise затвора, не всего HTTP/query lifecycle. Отсутствие надёжного completion barrier подтверждено; фактический false-green мутанта не запускался.
5. **Факт: часть test oracles противоречит действующим поведенческим требованиям.** Portfolio tests требуют отсутствия предупреждения/retry; Events tests закрепляют фильтры и отсутствие retry; backend Events ожидает `data: null` для непересекающегося сохранённого прогноза. Это не просто недостаток покрытия: такие ожидания защищают другую модель UX/данных.
6. **Факт: важные negative boundaries остаются неразличимыми.** Тест «publish fails» отклоняет upstream до начала публикации, а не запись внутри transaction. Auth cases не ловят различие UTF-8 bytes/characters и не проверяют refresh token как Bearer. Отсутствие теста не доказывает production-эксплуатацию.
7. **Непроверенная семантика: отсутствующий блок MOEX.** Код и tests считают его failure, spec — нормальным отсутствием данных. Конфликт подтверждён, но без согласования provider semantics нельзя просто ослабить malformed-response guards. Решение — INVESTIGATE.
**Итог:** 10 actionable findings: 7 FIX, 1 SIMPLIFY, 1 условный DELETE и 1 INVESTIGATE; отдельно 3 KEEP записи о критических гарантиях. P0 findings нет. Цель — заменить слабые стимулы и неверные ожидания, а не увеличить число tests или coverage threshold.
## 3. Inventory & coverage
### 3.1. Метод и границы подсчёта
Inventory выполнен один раз по исходным test-file patterns; уточняющие команды разделили первоначальные суммы по директориям:
```sh
# backend
rg --files src -g '*.spec.ts' | rg -c '\.spec\.ts$'
rg --files test -g '*.e2e-spec.ts' | rg -c '\.e2e-spec\.ts$'
# frontend
rg --files src -g '*.test.ts' -g '*.test.tsx' | rg -c '\.test\.tsx?$'
rg --files packages scripts -g '*.test.ts' -g '*.test.tsx' -g '*test*.mjs'
```
| Область | Inventory | Что означает число |
| --- | --- | --- |
| backend `src` | 39 `*.spec.ts` | файлы colocated unit/component tests |
| backend `test` | 20 `*.e2e-spec.ts` | HTTP/persistence/migration suites, включая отдельный read benchmark |
| frontend `src` | 57 `*.test.ts(x)` | test files, включая `position-layout.browser.test.tsx` |
| frontend `packages` | 6 `*.test.ts(x)` | UI/icons tests, включая `EntryCard.browser.test.tsx` |
| frontend `scripts` | 2 `*test*.mjs` | tooling tests; только инвентаризированы, вне глубокого анализа 08 |
Поиск учитывает ignore rules `rg`, исключает dependencies/generated/build outputs. Рабочие деревья были чистыми. Это **число файлов, не число сценариев**: `it.each` и loops не развёртывались для метрик. Test-case total и проценты покрытия не вычислялись. 28 PASS относятся только к двум явно запущенным suites.
Конфигурации уровней сверены по `backend/package.json:123–139` и `frontend/vitest.config.ts:26–65`: Jest Node для backend unit, отдельные SQLite/HTTP suites, frontend JSDOM и настоящий Chromium для browser layout. Здесь не оценивались стоимость runner/setup, CI minutes или целесообразность их архитектуры.
### 3.2. Risk-based sampling matrix
| Flow | Исследованные тестовые owners | Production/contract boundary | Почему выбрано |
| --- | --- | --- | --- |
| Exact money | decimal, RUB parser, bank-card HTTP, bank summary sorting | BigInt decimal → application → SQLite/HTTP → frontend comparator | финансовая точность и независимо заданные edge values |
| Auth/CAS/replay | AuthService unit, auth HTTP, frontend client/AuthProvider | JWT verification, conditional update, cookie response, session generation | серьёзный эффект удаления одного guard; сильные и слабые proofs |
| Connection publication | local reads, connection HTTP, value history | local GET vs source request; generation/FK/conditional publish | отделить реальные fences от несостоявшихся races |
| Async/read lifecycle | StocksPage, resource query, screen sync, initial availability | HTTP promises, query cancellation, request identity, timer boundaries | completion ordering, retries и ложные negative assertions |
| Forecast/calendar | upcoming-events HTTP, ForecastRepository, MOEX parsing | saved forecast/window, dependency versions, complete source publication | неверный oracle, malformed-vs-empty и реальные stale-data guarantees |
| Frontend screens | AccountPage/VM, EventsPage/filters, RouteErrorBoundary | route → VM → view, cached snapshot/error/recovery | проверить видимый пользовательский исход и integration seam |
| API boundaries | configured auth/events HTTP, DTO pipe, OpenAPI, TbankClient mapping | guards/validation/status/schema/proto dates | различить wire contract и простое mock wiring |
| Scheduler | все test paths по names + metadata cases | `tick → reserveHourly → walk → settleHourly` | новый критический owner, явный пробел executable coverage |
| Browser fit | position-layout browser test + PositionSection | настоящий layout 375/1280 px | полезный тест геометрии, которую JSDOM не доказывает |
Shortlist отбирался по риску денежной/identity регрессии, смене владельца обновления, подозрительной асинхронности и стоимости устаревших ожиданий. Выборка намеренно включает хорошие примеры. Она не статистическая; findings не экстраполируются на каждый файл inventory. Большие suites прочитаны в релевантных диапазонах, с callers и setup, влияющими на stimulus.
**Вне глубокого охвата:** полный набор банковских UI-сценариев, все instrument/candle/ledger permutations, весь NCR parser, все theme/icons/package tests, scripts, миграционная совместимость/durability, browser cookie enforcement, actual gRPC queue scheduling/deadlines и длительность CI. Для них не заявляется отсутствие дефектов. Specs/docs использованы как независимые требования/решения, а не как исполняемые гарантии или автоматически правильные conventions.
### 3.3. Test Value Matrix
В этой таблице B = `backend/`, F = `frontend/`. Остальная часть каждого пути точная. «Ловит» означает вывод из stimulus/assertions, **не результат запущенной мутации**. Метка PASS используется только для реального целевого запуска.
| Test case / evidence | Claimed invariant | Given → executed stimulus | Observable assertion | Regression caught? | Action |
| --- | --- | --- | --- | --- | --- |
| B `src/integrations/tbank/decimal.spec.ts:5–22` | signed nano и большие суммы точны | `9007199254740993` + nano; отрицательная дробь; subtraction/multiply | точные ожидаемые decimal strings | да: Number coercion/sign/scale; **PASS** | KEEP TQ-012 |
| B `src/modules/banking/bank-cards/domain/rub-money.spec.ts:4–57` | storage bounds, no rounding, positive input | independently заданные крайние kopecks и invalid strings → parse/format | точный BigInt/строка или throw | да: off-by-one signed64, дробная копейка, zero-positive; **PASS** | KEEP TQ-012 |
| B `test/bank-cards.e2e-spec.ts:146–165` | точность проходит storage и HTTP | create/read карты `>2^53`, topup за край storage value, expenses 0.10/0.20 | точные balance strings в HTTP | да: serialization/storage/application truncation | KEEP TQ-012 |
| F `src/entities/bank-card/api/summary.test.ts:9–37` | сортировка не теряет копейки и source order | huge amounts с разницей 0.01, ties, negatives → `sortBankSummary` | IDs в независимом порядке; source unchanged | да: Number comparator, mutation массива | KEEP TQ-012 |
| B `src/modules/auth/application/auth.service.spec.ts:95–148` | один refresh winner, loser не отзывает session | два callers читают old hash через barrier → refresh; mock CAS | один fulfillment/401; expected hash передан; no revoke | да: application CAS/error wiring; не DB atomicity | KEEP TQ-011 |
| B `test/app.e2e-spec.ts:161–213` | реальный CAS, replay rejection, usable winner | два HTTP refresh блокируются до CAS; `Promise.all` обоих | statuses 200/401, no loser cookie, старый token 401, winner 200, одна session | да: unconditional update/revocation/cookie clobber | KEEP TQ-011 |
| F `src/shared/api/client.test.ts:92–123` | old identity retry не возвращает чужие данные | дождаться второго GET → generation change → release → await rejection | итоговый Promise отклонён `AbortError` | да: удаление post-retry generation check | KEEP |
| F `src/features/auth/model/auth.test.tsx:189–275` | auth-cookie writes сериализованы в этой вкладке | pending refresh → logout/login → release → await всех операций | итоговая identity и порядок реальных MSW endpoints | да: late refresh overwrites login/logout; не browser cookie attributes | KEEP |
| B `test/tbank-local-reads.e2e-spec.ts:489–514` | late portfolio fenced after replacement | deferred RPC установлен → только local GET → PUT → release неиспользованного promise | data null, DB count 0 | **нет stimulus публикации**, тест этого fence не различает | FIX TQ-001 |
| B `test/tbank-local-reads.e2e-spec.ts:299–325` | failed refresh сохраняет old totals | seed успешного owner result → ручной failed work-state → local GET | old data; lastAttempt none; RPC count 1 | ловит часть read projection, не failed owner refresh | уточнить scope / заменить stimulus; TQ-001 |
| B `test/tbank-local-reads.e2e-spec.ts:328–379` | recovery/coalescing concurrent discovery | abandoned row → 20 local GET; RPC forbidden; work завершён самим тестом | accounts видимы; no RPC | ловит passive reads, не recovery producer и не coalescing работы | переименовать/убрать unused deferred; TQ-001 |
| F `src/pages/stocks/ui/StocksPage/StocksPage.test.tsx:175–236` | новый search переживает late page | page GET + новый search → новый результат → release old | новый текст уже существует; старого ещё нет | completion loser не доказан; false-green mutant **UNVERIFIED** | FIX TQ-002 |
| F `src/shared/api/resource-query.test.tsx:139–172` | observed result побеждает older GET | old handler started → await observe → release; await только затвора | current payload равен snapshot | часть cancellation path выполнена; end-to-end settlement old HTTP **UNVERIFIED** | FIX barrier TQ-002 |
| F `src/shared/api/resource-query.test.tsx:174–209` | access loss очищает pending sibling | initial a loaded → a 404 → b release; await затвора | a/b data undefined; a isError | initial access reset различает; late sibling application не доказана | FIX barrier TQ-002 |
| F `src/features/tbank-connection/model/screen-sync.test.tsx:57–79` | resources refresh together | один accounts resource → manual refresh | GET count 2 | ловит manual refetch одного owner; не multiple resources/timer/unmount | KEEP узкую гарантию; wider claim не подтверждён |
| F `src/shared/lib/initial-availability.test.tsx:7–37,76–101` | wait budget и identity reset | fake time 99+1; rerender new identity | loading/unavailable/ready | да: deadline off-by-one, stale identity selection | KEEP |
| F `src/pages/account/ui/AccountPage/AccountPage.test.tsx:751–855` | snapshot сохраняется «тихо» после 503 | initial data → invalidation → request count → следующий invalidation | суммы остаются, warning/retry отсутствуют | да для totals; неправильный oracle для recovery; timestamp selector устарел | FIX TQ-004 |
| B `test/upcoming-events.e2e-spec.ts:321–340` | иной forecast range не выдаётся за прежний | saved forecast Feb → query Mar | `newRange.data === null` | различает range, но отвергает required successful empty fallback | FIX TQ-005 |
| B `test/upcoming-events.e2e-spec.ts:619–640` | narrow filter не обновляет calculation time | saved Feb window → Feb 1–10 query | тот же calculatedAt; одна event; no RPC | да для overlap; не non-overlap/latest-success/shifted-window publication | KEEP и дополнить TQ-005 |
| B `test/upcoming-events.e2e-spec.ts:344–368` | dependency CAS отказывает stale candidate | qty=3 candidate → реальный qty=9 write в seam перед resolve | first result old; next result `90.000000009` | да: удаление portfolio revision fence | KEEP TQ-013 |
| B `src/integrations/moex/moex.client.spec.ts:121–191` | malformed calendar не становится complete | valid empty blocks + `coupons: undefined`/bad columns/rows → parser | MOEX_UNAVAILABLE | да, но отсутствие блока спорно объединено с malformed | INVESTIGATE TQ-006; malformed KEEP |
| B `src/integrations/moex/moex.client.spec.ts:223–274` | все cursor pages, no repeated index | controlled response pages/index → реальный parser | две точные events или failure; starts 0/1 | да: потерянная page/repeated cursor | KEEP |
| B `test/upcoming-events.e2e-spec.ts:540–599` | source failure сохраняет complete forecast | сохранить реальный parsed calendar → force invalid payload → await durable works | DB calendar/forecast и time прежние; failure observable | да для persistence fallback; семантика missing block спорна | KEEP malformed arm; TQ-006 |
| B `src/common/dto-validation.spec.ts:13–29` | credentials required, email normalized | missing fields / ASCII-valid password → configured local ValidationPipe | reject или normalized DTO | да для этих inputs; не UTF-8 minimum/maximum semantics | FIX gap TQ-007 |
| F `src/features/auth/ui/AuthForm/AuthForm.test.tsx:9–31` | confirmation mismatch не отправляется | заполнить несовпадающие пароли → click submit | видимая field error и onSubmit не вызван | да: removal confirmation guard; callback — допустимый seam | KEEP; TQ-007 не закрывает |
| B `test/tbank-value-history.e2e-spec.ts:316–333` | «publish fails» сохраняет snapshot/points | existing points → `getPortfolio` rejects → await owner done | points прежние; snapshot не asserted | да для upstream failure; **не transaction rollback** | FIX TQ-008 |
| B `test/tbank-operations-persistence.e2e-spec.ts:186–246` | group publication atomic | successful a/b → изменить staged a, оставить b incomplete → publish | rejection; прежние rows и coverage times; later success/revision stability | да: first interval writes rollback после incomplete b | KEEP TQ-013 |
| B `test/tbank-operations-persistence.e2e-spec.ts:74–126` | separate reader видит old/new group | real parallel publish + 20 reads; reconnect reader | только 10/10 или 20/20; reopened 20/20 | полезно, но reader не гарантированно попадает внутрь любого write interleaving | KEEP вместе с deterministic rollback, не считать exhaustive race proof |
| B `test/bank-cards.e2e-spec.ts:391–422` | replay не дублирует деньги/не оживляет tombstone | same intent дважды + другой expense → edit/delete/replay | balance 700/800, 409 stale version, 410 deleted replay | да: duplicate debit, lost independent write, resurrected operation | KEEP TQ-013 |
| B `test/bank-cards.e2e-spec.ts:622–653` | refunds limited, rejected locks rollback | HTTP refund vs expense reduction; после settlement invalid refund date | один успех; исходная version после rejection; refund sum ≤ expense | да: race unsafe limit/transaction removal | KEEP TQ-013 |
| B `src/modules/investments/tbank-sync/tbank-screen.spec.ts:4–73` | manual screen normalization/validation | pure normalizeScreen с descriptors/dates | normalized object или throw | да для функции, но у текущего request-flow нет запускающего consumer | conditional DELETE TQ-009 |
| B `src/modules/investments/upcoming-events/upcoming-events.controller.spec.ts:8–18` | authenticated sub/account/query forwarded | прямой controller call с arbitrary sub → mock service | точные service arguments | да для seam mapping; не guards/HTTP/DTO/runtime response | KEEP малую seam-проверку |
| B `src/integrations/tbank/tbank.client.spec.ts:174–239` | bounded retry, queue admission, auth failure | mock transport/queue, real `call` policy + fake 999/1 ms | request count и resolved/rejected result | да: policy bypass/retry excess; не реальный queue scheduler | KEEP, корректно назвать boundary |
| F `src/app/routes/RouteErrorBoundary/RouteErrorBoundary.test.tsx:133–148` | безопасный fallback и navigation reset | throwing route → click healthy navigation | no technical details; healthy content | да: boundary reset/navigation regression | KEEP |
| F `src/pages/account/ui/AccountPage/position-layout.browser.test.tsx:17–95` | stocks/bonds vertical, other responsive | real render at configured 375/1280 px → geometry read | ненулевая геометрия и relative positions | да: ошибочное число колонок; не весь visual/a11y contract | KEEP browser level |
| F `src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.filters.test.tsx:229–290` | filters apply/reset/history | real route/MSW → picker/apply/back/reset | URL, period text, expected GET params | да для старого filter UI, которого current spec не требует | SIMPLIFY TQ-010 |
| F `src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.test.tsx:172–190` | «quiet» error без retry | mocked error resource → rerender | Reset видим, Retry отсутствует | закрепляет противоположное required HTTP recovery | SIMPLIFY/FIX TQ-010 |
### 3.4. Critical Behavior Gaps
| Critical behavior | Что уже проверяется | Что остаётся без надёжного distinguishing proof | Самая дешёвая responsible boundary |
| --- | --- | --- | --- |
| Hourly owner/lifecycle | pure metadata projection; individual source owners | реальные сроки/overlap/restart/partial failure/terminal settlement | controlled `tick` с fake upstream и настоящим repository; SQLite для reservation, TQ-003 |
| Поздний portfolio старого подключения | connection replacement/FK; прямой old-attempt rejection | действительно started owner → replace → complete late owner | existing local-reads suite с await started + `work.done`, TQ-001 |
| Late frontend result | новая search identity; initial access reset | assertion после завершения проигравшей операции, а не его затвора | существующие RTL/MSW suites, TQ-002 |
| Cached Portfolio + visible metadata/retry | сохранение сумм; standalone status widget | route/container не теряет source; 503 warning и HTTP-only retry | существующий AccountPage integration test, TQ-004 |
| Forecast filter/latest result | overlap filtering, same-scope CAS | non-overlap empty fallback и публикация нового полного shifted window | существующая SQLite/HTTP Events suite, TQ-005 |
| Empty vs malformed source | malformed cursor/rows и сохранение старого complete | смысл отсутствующего блока, отдельно от damaged block | unit parser + один integration arm после решения, TQ-006 |
| Auth trust boundary | valid access, absent access, refresh replay/CAS | UTF-8 boundary; refresh-as-Bearer; cookie flags; JWT/DB expiry в актуальном suite | существующий configured auth HTTP и local DTO/schema tests, TQ-007 |
| Portfolio transaction rollback | upstream failure preserves points | ошибка после первого успешного write сохраняет snapshot и points вместе | SQLite repository publication со failure injection внутри transaction, TQ-008 |
| Current events UX | старая форма периода и Reset | forecast calculation time, read-only list и Retry как reread того же resource | RTL/MSW existing Events suite, TQ-010 |
Отдельно: два `screen-sync` tests не продвигают 30-минутный timer и используют только один resource. Нельзя считать interval/multi-resource/unmount поведение проверенным. Это ограничение выборки, а не основание автоматически строить новый scheduler harness или отдельный finding.
## 4. Findings
Для KEEP severity означает ущерб от удаления защищающей гарантии, **не текущий дефект**. Effort для KEEP — стоимость сохранить/правильно классифицировать proof. `Impact unmeasured` означает отсутствие измерения инцидентов, flaky frequency или затрат CI.
### TQ-001 — Late-portfolio test не запускает защищаемую публикацию
- **Decision:** FIX. **Severity:** P2. **Confidence:** High. **Effort:** S. **Related finding IDs:** TQ-003, TQ-013.
- **Evidence:** `backend/test/tbank-local-reads.e2e-spec.ts:489–514,69–76`; `backend/src/modules/investments/portfolio/presentation/portfolio.controller.ts:25–31`; `backend/src/modules/investments/portfolio/application/portfolio.service.ts:55–59,113–231,234–306`; `backend/src/modules/investments/tbank-connection/tbank-connection.service.ts:31–76`; `backend/src/modules/investments/portfolio/infrastructure/portfolio.repository.ts:121–146`.
- **Claim:** deferred `getPortfolio` не потребляется stimulus теста. Нет source-owner request, late candidate и вызова publication fence, который якобы проверяется.
- **Current mechanism:** GET вызывает `portfolio.read`, а тот читает access/saved data/metadata; PUT валидирует token и транзакционно заменяет connection, без запуска PortfolioService. Deferred освобождается, затем ожидается 20 ms и проверяется по-прежнему пустая DB. Scheduler зарегистрирован в этой suite, но тест не вызывает `tick` и не синхронизируется с минутным timer.
- **Impact / actual cost:** удаление portfolio conditional-publication guard не обязано менять outcome этого case: publisher им вообще не вызван. Подтверждён структурный пробел stimulus; запуск такого мутанта не выполнялся. Impact unmeasured.
- **Why now:** suite уже использует явный `portfolioService.request` в других cases; данный тест сохранил предположение, что обычный GET начинает работу.
- **Minimum change:** сохранить отдельный полезный empty-membership assertion. До replacement вызвать existing `request` с текущей revision; потребовать ненулевой work и подтверждённый старт upstream; после replacement освободить RPC и **await `work.done`**. Затем проверить отсутствующую old-generation publication и неизменность new-generation data.
- **Guarantees at risk:** connection isolation, empty ≠ missing, late financial writes. Не заменять локальный GET импортом ради восстановления старого теста.
- **Counterarguments / trade-offs:** FK и old-attempt tests уже защищают некоторые случаи (`tbank-connection.e2e-spec.ts:113–161`, `tbank-value-history.e2e-spec.ts:264–313`). Они не делают этот case полноценной owner race. Если минутный tick случайно случится, это не контролируемый proof, а неявная зависимость от времени suite.
- **Verification or rollback:** targeted existing SQLite suite; порядок started → replace → release → done → assertion обязателен. Откат правки теста не меняет production. Проверка удаления fence в рамках этого аудита запрещена и не выполнялась.
### TQ-002 — Frontend late-response assertions не имеют completion barrier проигравшего
- **Decision:** FIX. **Severity:** P2. **Confidence:** Medium. **Effort:** S. **Related finding IDs:** TQ-001, TQ-011.
- **Evidence:** `frontend/src/pages/stocks/ui/StocksPage/StocksPage.test.tsx:175–236`; `frontend/src/pages/stocks/ui/StocksPage/StocksPage.vm.ts:65–100`; `frontend/src/shared/api/resource-query.test.tsx:139–209`; `frontend/src/shared/api/resource-query.ts:104–126`; `frontend/src/shared/api/client.ts:205–247`.
- **Claim:** освобождение MSW затвора не эквивалентно завершению fetch body, continuation и React/query application. Финальные assertions допускают проверку состояния до этого завершения.
- **Current mechanism:** после «Новый» stock test вызывает `oldPage.resolve`, затем `waitFor` на уже выполненное условие. Нет `old request started` assertion или ожидания завершения `changePage`. Resource tests ждут `blocked`, который разрешён самими tests; generated query/fetch продолжает собственную цепочку. `act` может обработать часть работы, но это не явное ожидание полного сетевого Promise.
- **Impact / actual cost:** stale request guard/cancellation regression может не наблюдаться в момент assertion. Точное false-green расписание и mutant outcome — UNVERIFIED; частота flakiness не измерялась. Impact unmeasured.
- **Why now:** эти cases названы regression tests именно late completion, а production имеет отдельные guards на success/error/finally и собственный response-body lifecycle.
- **Minimum change:** в existing test probe сохранить awaited operations/settled marker для реального вызываемого Promise, либо использовать существующую наблюдаемую lifecycle boundary, подтверждающую завершение. Начало старого handler проверить до нового search; после release ждать old operation settlement и React flush; затем сравнить data/error/loading. MSW handler-return marker сам по себе ещё не гарантирует client application.
- **Guarantees at risk:** request identity, cancellation/access-group cleanup; не добавлять глобальные epochs/HTTP registries ради теста.
- **Counterarguments / trade-offs:** текущие `act`/MSW scheduling иногда доведут response до конца; correctness source guards присутствуют. Поэтому не заявляется, что existing test обязательно false-green. Сильный альтернативный образец — `client.test.ts:115–123`: ожидается итоговый request rejection, не только освобождение затвора.
- **Verification or rollback:** targeted StocksPage/resource-query tests без сборки нового harness; финальное условие должно стать невозможным до settlement old operation. Source mutation не нужна для правки ordering; удаление guard — лишь описанный мысленный regression.
### TQ-003 — Нет executable proof часового координатора и durable reservation policy
- **Decision:** FIX. **Severity:** P1. **Confidence:** High. **Effort:** M. **Related finding IDs:** TQ-001, TQ-005.
- **Evidence:** `backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:48–110,113–189,199–218`; `backend/src/modules/investments/tbank-sync/tbank-sync.repository.ts:174–242`; `backend/test/upcoming-events.e2e-spec.ts:140–147`; `backend/test/tbank-value-history.e2e-spec.ts:47–53`; `backend/test/portfolio-structure.e2e-spec.ts:40–45`; `backend/src/modules/investments/investment-resource.mapper.spec.ts:45–117`; `specs/openspec/specs/hourly-investment-sync/spec.md:8–40`.
- **Claim:** 59 inventoried backend test files не содержат прямого exercise `tick`/`reserveHourly`/`settleHourly` с assertion требуемого lifecycle. Полезные projection tests получают уже готовый hourly result.
- **Current mechanism:** поиск `reserveHourly|settleHourly|\.tick\(|TbankHourlyScheduler` по `src`/`test` test patterns нашёл только три imports/overrides scheduler. Real owner сначала резервирует работу, запускает `walk` через setImmediate, затем ждёт источники и публикует result. Ordinary read/source tests проверяют другую boundary.
- **Impact / actual cost:** регрессии часовых сроков, overlap, missed term, first launch и неверного successful settlement не имеют прямого regression oracle. Например, общий catch вызывает settle с прежним failures, а reserve policy сравнивает start с now: это конкретные ветви, которые нужен отличающий stimulus. Их production-исправление относится к другому аудиту. Impact unmeasured.
- **Why now:** этот owner заменяет read-driven refresh для всех личных investment resources, то есть пробел влияет на основной способ обновления данных, а не опциональный helper.
- **Minimum change:** одна таблица сценариев реального SQLite reservation с управляемым временем: first/due/not-due, running same process, interrupted other process, пропуск срока долгим обходом. Один controlled coordinator scenario: два accounts, успех одного и failure другого, сохранённый успешный snapshot и terminal partial result. Добавить error arm вне локального source catch; дождаться реальной terminal row. Mocks — только upstream/контролируемые внешние owners, не проверяемая reservation.
- **Guarantees at risk:** single attempt, skip/no catch-up, сохранность partial successes, connection ownership, scheduler lifecycle. Не заменять durable CAS mock-map и не убирать isolation overrides в unrelated suites только ради coverage.
- **Counterarguments / trade-offs:** indirect timer может сработать в длинном HTTP suite; отдельные source/metadata tests полезны. Без явных assertions результата обхода это не proof. Не требуется exhaustive scheduler permutation suite, inter-process lease или real upstream.
- **Verification or rollback:** новое различающее поведение исполнять на existing isolated SQLite boundary; fake clock должен контролировать проверяемую eligibility, а work completion — явный terminal barrier. Согласовать long-walk policy до утверждения oracle. Не запускавшаяся в аудите проверка остаётся UNVERIFIED.
### TQ-004 — Portfolio tests защищают отсутствие recovery и не проверяют передачу source metadata
- **Decision:** FIX. **Severity:** P2. **Confidence:** High. **Effort:** S. **Related finding IDs:** TQ-010.
- **Evidence:** `frontend/src/pages/account/ui/AccountPage/AccountPage.test.tsx:751–855,859–895`; `frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:125–135`; `frontend/src/pages/account/ui/AccountPage/AccountPage.tsx:24–34`; `frontend/src/pages/account/ui/AccountPage/components/AccountPageLoaded.view.tsx:55–59`; `frontend/src/shared/ui/SourceSyncStatus/SourceSyncStatus.tsx:31–47`; `frontend/src/shared/ui/SourceSyncStatus/SourceSyncStatus.test.tsx:23–26`; `specs/openspec/specs/frontend-data-resilience/spec.md:11–28`.
- **Claim:** valuable cached-total assertions смешаны с противоположным требованиям oracle: предупреждение и Repeat должны отсутствовать. Route-level proof передачи `source` отсутствует.
- **Current mechanism:** VM возвращает source; container его не передаёт; view умеет показывать timestamp/stale/attempt. Test проверяет отсутствие «Данные получены», тогда как current widget использует «Данные на». Поэтому этот negative regexp даже не различает появление актуального status widget. После 503 ожидание requestCount=2 не доказывает, что query уже перешёл в error.
- **Impact / actual cost:** omission VM → view остаётся незамеченным, а требуемое предупреждение/retry может восприниматься как regression теста. Информация о stale/failed result теряется рядом с денежными суммами. Impact unmeasured.
- **Why now:** существует нужный owner status widget и точная requirement о cached snapshot плюс неблокирующее предупреждение и HTTP-only retry.
- **Minimum change:** переиспользовать existing integration case: fixture с осмысленными `lastSuccessfulAt/freshness/lastAttempt`; positive assertions текущего source label/state. 503 дождаться как terminal query outcome; суммы должны сохраниться, warning и Retry — появиться. Click Retry должен читать тот же GET и обновить data; source sync request недопустим.
- **Guarantees at risk:** snapshot не исчезает и не превращается в initial skeleton; HTTP error не переписывает server `isStale`; identity switch не сохраняет чужие данные.
- **Counterarguments / trade-offs:** «quiet» намерение могло означать отсутствие блокирующего loader или ручного импорта; это разумно. Оно не обосновывает скрытие freshness и HTTP recovery. Standalone widget test не ловит пропущенный prop в container.
- **Verification or rollback:** RTL/MSW на реальном AccountPage route; не ограничиваться VM-only или presentation-only render. Production/UI согласовать с текущим behavioral requirement; исходники в этом аудите не менялись.
### TQ-005 — Forecast range oracle требует missing вместо сохранённого успешного empty result
- **Decision:** FIX. **Severity:** P2. **Confidence:** High. **Effort:** M. **Related finding IDs:** TQ-003, TQ-006, TQ-013.
- **Evidence:** `backend/test/upcoming-events.e2e-spec.ts:321–339,619–640`; `backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:152–173`; `backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:50–79`; `backend/src/modules/investments/portfolio/application/forecast.service.ts:76–93,98–120`; `specs/openspec/specs/brokerage-account-upcoming-events/spec.md:12–24,63–76`.
- **Claim:** тест «never relabels another range» считает null правильным при непересечении сохранённого окна. Требование предписывает успешный пустой data с исходными calculation/source metadata. Сохранить исходную идентичность не означает потерять подтверждённый forecast.
- **Current mechanism:** test сначала сохраняет February forecast, затем спрашивает March и ожидает null. Repository ищет fallback только среди пересекающихся scopes. Scope includes window; branch `!previous && fallback` возвращает fallback и при новом candidate. Narrow-window test проверяет только overlap и тот же timestamp.
- **Impact / actual cost:** исправление non-overlap behavior ломает текущий oracle; новый полный candidate shifted window не имеет отдельного positive proof и может уступить старому. Пользователь получает missing/старую расчётную basis после успешной работы. Impact unmeasured.
- **Why now:** с часовым обновлением default window движется; разные windows — обычный сценарий, а не только ручной date filter.
- **Minimum change:** разделить два сценария на existing SQLite/HTTP boundary: (a) нет нового полного candidate, запрос вне saved window → empty events с прежними calculatedAt/source identity; (b) появился новый полный candidate с другой quantity/source version и shifted window → новый forecast с новой расчётной basis. Не переименовывать старый forecast новым scope/time.
- **Guarantees at risk:** membership/portfolio/source revision CAS, точные amounts, fallback only complete, отсутствие fake recalculation time.
- **Counterarguments / trade-offs:** old test правильно запрещает relabelling; это KEEP гарантия, которую надо сохранить новым expected shape. Выбор «последнего полного» требует явного ordering; внедрение новой repository abstraction не требуется. HTTP route spelling в spec местами старое — finding основан на содержательной semantics окна, не на совпадении имени endpoint.
- **Verification or rollback:** оба сценария должны различать правильный итог независимо от ORM/query order; existing CAS test TQ-013 остаётся. Expected contract уточнить до source fix; mutations в аудите не выполнялись.
### TQ-006 — Missing MOEX block объединён с malformed response в двух уровнях тестов
- **Decision:** INVESTIGATE. **Severity:** P2. **Confidence:** High для конфликта; provider semantics UNVERIFIED. **Effort:** S. **Related finding IDs:** TQ-005, TQ-013.
- **Evidence:** `backend/src/integrations/moex/moex.client.spec.ts:101–118,121–191,223–274`; `backend/src/integrations/moex/moex.client.ts:119–158,182–197`; `backend/test/upcoming-events.e2e-spec.ts:540–599`; `specs/openspec/specs/brokerage-account-upcoming-events/spec.md:78–84`.
- **Claim:** unit table и настоящий-parser integration arm оба требуют failure для отсутствующего блока; spec требует normal no-data. Тесты не являются независимым подтверждением нужности этого правила.
- **Current mechanism:** `parseRows(undefined)` падает так же, как non-array columns/data или damaged row. Calendar owner сохраняет предыдущую DB source/forecast на failure. Empty arrays и 404 уже трактуются отдельно как normal absence.
- **Impact / actual cost:** зелёные suites закрепляют противоречащий spec outcome; обратная механическая правка может ошибочно объявить неполный calendar подтверждённо пустым. Impact unmeasured.
- **Why now:** distinction определяет, можно ли безопасно заменить complete calendar/forecast новым результатом, а не только текст ошибки.
- **Minimum change:** согласовать missing-block semantics на сохранённом provider payload/первичном контракте без real banking calls. После решения выделить отсутствующий block в свой case; malformed columns/rows/cursor оставить failure. Отразить принятое ожидание и unit, и persistence level.
- **Guarantees at risk:** полная pagination, no partial-success, сохранность последнего complete forecast, no fabricated zero payments.
- **Counterarguments / trade-offs:** текущий failure может быть сознательной защитой от изменения ISS shape. Тогда меняется behavioral requirement, а не ослабляются guards. Наличие обоих уровней оправдано: unit доказывает parser, integration — сохранность DB fallback.
- **Verification or rollback:** offline fixture подтверждает форму missing block; targeted unit и один real-parser integration arm. До решения обе защиты KEEP; production не изменён.
### TQ-007 — Auth suite не различает важные входные и token-role boundaries
- **Decision:** FIX. **Severity:** P2. **Confidence:** High. **Effort:** S. **Related finding IDs:** TQ-011.
- **Evidence:** `backend/src/common/dto-validation.spec.ts:13–29`; `backend/test/app.e2e-spec.ts:51–90,161–214,216–261`; `backend/src/modules/auth/application/auth.service.spec.ts:60–148`; `backend/src/modules/auth/presentation/auth.dto.ts:43–54,66–74`; `backend/src/modules/auth/infrastructure/jwt.strategy.ts:13–25`; `backend/src/modules/auth/presentation/auth.controller.ts:113–137`; `frontend/src/features/auth/model/auth.schema.ts:36–56`; `frontend/src/features/auth/ui/AuthForm/AuthForm.test.tsx:9–31`; `specs/openspec/specs/user-authentication/spec.md:10,43–60,83–96`.
- **Claim:** фактический auth shortlist не проверяет UTF-8 byte boundaries, запрещённый refresh-as-Bearer и ключевые cookie attributes; удаление соответствующего application/config guard не различается current assertions.
- **Current mechanism:** credentials case использует ASCII password; frontend case — confirmation mismatch. Backend минимум задан MinLength, maximum — byte validator; frontend обе границы считает TextEncoder. Ровно шесть кириллических букв дают 12 UTF-8 bytes, но не 12 символов. HTTP tests используют valid access и absent access; cookie проверяется для rotation/clear, не HttpOnly/SameSite/path/host-only flags. Поиск auth expiry/token-role/cookie assertions в `src` unit и `test/*.ts` не нашёл соответствующих negative cases.
- **Impact / actual cost:** существующее несовпадение password semantics не имеет regression case. Пропущенные role/expiry/cookie tests — риск будущей регрессии, не доказательство существующего обхода auth: production type guard и expiry config присутствуют. Impact unmeasured.
- **Why now:** это конкретные trust-boundary distinctions, явно требуемые current auth spec; ASCII happy path не различает конкурирующие правила.
- **Minimum change:** добавить маленькую boundary table в existing DTO/schema tests: 11/12 и 72/73 UTF-8 bytes с multibyte fixture и сохранением исходного password. В существующем configured HTTP auth flow использовать настоящий refresh token как Bearer → 401. На уже имеющемся successful response проверить cookie flags/path/no Domain. Следующим risk-driven шагом — signed-expired access/refresh и DB-expired session; не создавать тест каждого decorator.
- **Guarantees at risk:** server runtime validation остаётся authoritative; refresh не становится access; bad refresh не очищает winner cookie; maximum не допускает bcrypt truncation semantics.
- **Counterarguments / trade-offs:** JWT/bcrypt/class-validator имеют собственные tests, но application выбирает role/expiry/options и связывает два независимых validators. Проверять весь framework не нужно; byte-vs-character fixture и application token-role case оправданы именно собственным контрактом. Browser enforcement cookie attributes этим HTTP test полностью не доказывается.
- **Verification or rollback:** local boundary table + targeted configured auth HTTP. Подтверждение wire/runtime ошибки byte minimum доступно самым дешёвым DTO case; в этом аудите он не создавался/не запускался. Production исправляется отдельным изменением с согласованным contract.
### TQ-008 — «Publish fails» отклоняет upstream до transaction и не проверяет snapshot
- **Decision:** FIX. **Severity:** P2. **Confidence:** High. **Effort:** S. **Related finding IDs:** TQ-001, TQ-012, TQ-013.
- **Evidence:** `backend/test/tbank-value-history.e2e-spec.ts:99–111,316–333`; `backend/src/modules/investments/portfolio/application/portfolio.service.ts:265–291`; `backend/src/modules/investments/portfolio/infrastructure/portfolio.repository.ts:121–183`.
- **Claim:** имя утверждает сохранность snapshot и points при publish failure, но публикация вообще не начинается. Assertions сравнивают только value points.
- **Current mechanism:** `getPortfolio` rejects внутри Promise.all; execution переходит в fail branch до normalize/repository.publish. Реальная transaction сначала claim-ит work, пишет Portfolio, затем history/points. Именно частичный write/rejection этот test не осуществляет.
- **Impact / actual cost:** removal transaction wrapper либо failure после Portfolio upsert может повредить связность snapshot/points без изменения этого upstream-failure case. Это мысленная регрессия; actual mutant не запускался. Impact unmeasured.
- **Why now:** value history добавляет несколько persisted outputs к одному publish; тест уже заявляет их совместную сохранность, но наблюдает только один.
- **Minimum change:** оставить и честно назвать existing upstream-failure case. Один новый SQLite publication case должен выполнить первый реальный write и затем вызвать контролируемую ошибку внутри transaction (например, temporary DB trigger, удаляемый в teardown). Сравнить previous Portfolio payload/dataRevision/time, history points и согласованный work outcome. Не mock-ить весь publish.
- **Guarantees at risk:** атомарность snapshot + points, dedup attempt/currency, original source time, fail-state без частично обновлённых данных.
- **Counterarguments / trade-offs:** FK/transaction в production есть; дефект их работы не утверждается. Другие bank/operations rollback tests используют другие repository owners и не заменяют этот proof. Existing upstream failure case полезен и не удаляется.
- **Verification or rollback:** isolated SQLite suite; fault обязан происходить после подтверждённой записи внутри tx, затем independently прочитать DB/reopen при необходимости. Удалить fault в teardown; не использовать application DB и не добавлять audit executable script.
### TQ-009 — Manual-screen normalization tests обслуживают недоступный request-flow
- **Decision:** DELETE после подтверждения retirement владельцем. **Severity:** P3. **Confidence:** High для текущего call graph. **Effort:** S. **Related finding IDs:** TQ-003.
- **Evidence:** `backend/src/modules/investments/tbank-sync/tbank-screen.spec.ts:1–75`; `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:54–78`; `backend/src/modules/investments/tbank-sync/tbank-sync.controller.ts:31–40`; `backend/test/openapi-contract.e2e-spec.ts:27–55`; `specs/openspec/specs/tbank-data-sync/spec.md:9–14`.
- **Claim:** normalizeScreen unit cases проверяют manual-command descriptor boundary, у которой в текущем source/test consumer graph нет запускающего production caller.
- **Current mechanism:** normalizeScreen вызывается только `TbankSyncService.request`; все references самого сервиса — declaration/provider/controller status. Controller предоставляет GET status, не request; OpenAPI test прямо требует отсутствия collection command path. Hourly owner использует другую reservation/flow.
- **Impact / actual cost:** поддерживается тестовая модель запрещённой manual sync capability. Runtime/CI экономия not measured; минимум снижается cognitive/maintenance load при окончательном retirement.
- **Why now:** новые нужные tests должны защищать hourly contract, а не расширять прежний manual request vocabulary.
- **Minimum change:** согласовать retirement с production owner из audit 04/06; удалить именно `tbank-screen.spec.ts`, не заменяя тестами того же недоступного flow. Source retirement — отдельная задача, не выполненная этим аудитом.
- **Guarantees at risk / что перестанет проверяться:** нормализация query/ticker; canonical categories/order; rejection отсутствующих identities/лишних descriptor fields; inclusive event calendar и invalid/default-period rejection **в manual descriptor API**. Это не active HTTP query validation, не auth, не hourly reservation и не Ledger planner.
- **Counterarguments / trade-offs:** если request — явно поддерживаемый programmatic API с consumer вне проверенного дерева, DELETE заменяется KEEP. Такого consumer в `src`/`test` не найдено. Нельзя по этой находке удалять `planInvestmentScreen`, у него иной active consumer.
- **Verification or rollback:** повторить consumer search перед удалением, сохранить existing HTTP/calendar/owner tests; восстановление test file из Git полностью обратимо. До подтверждённого retirement эта рекомендация условная.
### TQ-010 — Events tests закрепляют старую форму периода и отсутствие HTTP retry
- **Decision:** SIMPLIFY. **Severity:** P2. **Confidence:** High для расхождения. **Effort:** M. **Related finding IDs:** TQ-004, TQ-005.
- **Evidence:** `frontend/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.filters.test.tsx:159–291`; `frontend/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.test.tsx:63–79,172–190`; `frontend/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.vm.ts:19–33,62–75,92–123`; `frontend/src/pages/account-events/ui/AccountEventsPage/components/AccountEventsPageLoaded.view.tsx:59–98`; `frontend/src/pages/account-events/ui/AccountEventsPage/components/AccountEventsPageError.view.tsx:13–23`; `specs/openspec/specs/brokerage-account-upcoming-events/spec.md:86–107`.
- **Claim:** наиболее развитые current tests защищают интерактивный filter UI и Reset вместо Retry, тогда как existing spec требует read-only forecast list без фильтров и с HTTP-only recovery.
- **Current mechanism:** VM хранит applied/draft periods и maxDate, отдаёт events, но не exposes calculatedAt/original forecast window; view показывает date picker/Apply/Reset. Error test явно отвергает «Повторить». MSW filter tests доказывают старые URL transitions, но не current forecast presentation.
- **Impact / actual cost:** UX migration удерживается устаревшими assertions; required timestamp/coverage explanation/retry не имеют route-level oracle. Поддерживается периодная state machine, которая не нужна сохранённому current screen contract. Impact unmeasured.
- **Why now:** spec уже различает query window API и read-only screen. Нет необходимости переиспользовать Ledger-подобную форму периода для этого экрана.
- **Minimum change:** вместе с согласованной screen migration заменить filter-interaction сценарии несколькими existing RTL/MSW cases: sorted saved list и calculatedAt/original window; empty complete; missing/error; complete fallback + failed attempt; Retry читает тот же GET. Сохранить semantic event amount/offer/identity и доступное status text. Transport `from/to` tests не удалять автоматически: API filtering остаётся отдельным контрактом.
- **Guarantees at risk:** deep-link account scope, inclusive UTC filtering API, cached full forecast, no manual import, accessible statuses. Удаление формы не должно удалить эти гарантии вместе с test file.
- **Counterarguments / trade-offs:** если продукт сознательно сохраняет filters, сначала согласовать изменение current spec и forecast-source UX. Сам факт слова «MUST» не доказывает оптимальность screen decision; однако code/test противоречие уже факт. Глубокий UI/source redesign относится к audit 05, здесь проверен только test oracle/coverage.
- **Verification or rollback:** existing route/MSW tests должны проверять видимый контент, а не private VM field shape. Reuse existing cases/handlers; новый browser runner не нужен. Ошибка source и transport должны оставаться раздельными.
### TQ-011 — KEEP: реальные auth CAS и session-generation proofs дополняют unit tests
- **Decision:** KEEP. **Severity:** P1. **Confidence:** High. **Effort:** S. **Related finding IDs:** TQ-002, TQ-007.
- **Evidence:** `backend/test/app.e2e-spec.ts:161–213`; `backend/src/modules/auth/application/auth.service.spec.ts:95–148`; `backend/src/modules/auth/application/auth.service.ts:81–114,166–200`; `frontend/src/shared/api/client.test.ts:92–123`; `frontend/src/features/auth/model/auth.test.tsx:189–275`; `frontend/src/features/auth/model/auth.vm.ts:41–115,121–140`.
- **Claim:** unit mock CAS, real SQLite HTTP race и frontend identity/cookie-write ordering защищают разные failure modes; это не бесполезное повторение одного сценария.
- **Current mechanism:** unit barrier обеспечивает два reads и проверяет expected hash/error handling. HTTP barrier пропускает оба real sign/refresh paths к SQLite CAS, ждёт обоих responses и последовательно проверяет replay/winner. Frontend tests ждут реальные operation promises и проверяют identity после late completion.
- **Impact / actual cost:** ловятся unconditional rotation, session revocation loser-ом, loser Set-Cookie и resurrection old user. Стоимость исполнения этих suites not measured; runtime assertions по статике значимы.
- **Why now:** simplification и тестовые gaps TQ-002/TQ-007 не должны вести к замене сильного proof более дешёвым, но неэквивалентным mock wiring.
- **Minimum change:** сохранить cases/барьеры. При переименовании прямо обозначать DB vs orchestration vs frontend generation scope. Не добавлять cross-tab protocol, которого current contract не требует.
- **Guarantees at risk:** один winner, reusable winner session, no cookie clobber, in-memory identity isolation.
- **Counterarguments / trade-offs:** JWT signature boundary — уместный test seam для ordering, не assertion private call order. Это same-process HTTP race, не доказательство multi-process deployment или реального browser cookie enforcement.
- **Verification or rollback:** targeted auth HTTP plus existing frontend cases при изменении этого owner. Аудит их runtime не запускал; hypothetical removing expected-hash predicate изменяет содержательный outcome, а не только mock invocation.
### TQ-012 — KEEP: money tests имеют независимые oracles и правильные complementary levels
- **Decision:** KEEP. **Severity:** P1. **Confidence:** High. **Effort:** S. **Related finding IDs:** TQ-007, TQ-008, TQ-013.
- **Evidence:** `backend/src/integrations/tbank/decimal.spec.ts:5–37`; `backend/src/modules/banking/bank-cards/domain/rub-money.spec.ts:4–57`; `backend/src/common/decimal.ts:19–48,59–83`; `backend/src/integrations/tbank/decimal.ts:9–13`; `backend/src/modules/banking/bank-cards/domain/rub-money.ts:12–38`; `backend/test/bank-cards.e2e-spec.ts:146–165`; `frontend/src/entities/bank-card/api/summary.test.ts:9–37`; `frontend/src/entities/bank-card/api/queries.ts:13–25`.
- **Claim:** финансовые expected results не вычислены тем же production helper; границы точно отличают float/coercion/range regressions. Unit + real DB/HTTP + frontend comparator не взаимозаменяемы.
- **Current mechanism:** explicit BigInt/string fixtures, boundary values ±signed64, forbidden fractional kopecks, zero/negative operation guard, computed sum beyond storage bounds. HTTP проверяет persist/serialize/application, frontend comparator различает копейки больших чисел и non-mutating ties.
- **Impact / actual cost:** сохраняются точные деньги и no rounding/no fake zero; **две pure suites реально прошли, 28 tests**. Стоимость remaining layers not measured.
- **Why now:** эти small tests дают больше защиты, чем рост coverage на passthrough DTOs; сокращать их из-за похожих inputs нельзя.
- **Minimum change:** оставить existing tables. Новую property-testing dependency или suite для каждого arithmetic helper текущие findings не обосновывают. Finance regression добавлять по отличающему risk, не ради 100% coverage.
- **Guarantees at risk:** precision, nanos/sign, storage input bound, вычисления beyond individual stored-value bound, per-currency semantics.
- **Counterarguments / trade-offs:** Number допустим для округляемого display percentage (`frontend/src/entities/portfolio/model/display.ts:7–24`), это не тот же инвариант, что exact money sorting. Длинный fraction case проверяет собственный formatter; его runtime cost отдельно не выделялся.
- **Verification or rollback:** фактический targeted command записан в metadata; он не подтверждает всю финансовую систему. При persistence change сохранить HTTP exact-value proof отдельно от pure unit result.
### TQ-013 — KEEP: deterministic rollback, dependency CAS и replay проверяют реальные данные
- **Decision:** KEEP. **Severity:** P1. **Confidence:** High. **Effort:** S. **Related finding IDs:** TQ-001, TQ-005, TQ-006, TQ-008, TQ-012.
- **Evidence:** `backend/test/tbank-operations-persistence.e2e-spec.ts:186–246,292–350`; `backend/src/modules/investments/operations/infrastructure/brokerage-operations.repository.ts:703–818,820–839,932–944`; `backend/test/upcoming-events.e2e-spec.ts:344–368`; `backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:80–145`; `backend/test/bank-cards.e2e-spec.ts:391–422,622–653`; `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:152–195,214–240`; `backend/src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts:35–59`.
- **Claim:** это tests собственных transaction/publication/idempotency invariants, не просто проверка Prisma/SQLite «работает».
- **Current mechanism:** в operations publish первая изменённая группа реально записывается, затем incomplete marker второй группы throws; independently прочитанные rows/coverage должны остаться старыми. Forecast case меняет DB revision в контролируемом seam перед original resolve. Banking HTTP replay проверяет persisted balance/tombstone, а invalid refund date возникает после version-lock write внутри transaction.
- **Impact / actual cost:** rollback failure, stale calculated publication, duplicate debit/replay resurrection становятся наблюдаемыми. Impact runtime/CI not measured; не утверждается exhaustive coverage всех interleavings.
- **Why now:** похожие «failed refresh» names не делают все failure tests равноценными. Эти cases обеспечивают реальные данные/порядок, которых нет в TQ-001/TQ-008.
- **Minimum change:** сохранить existing cases и asserted financial outcomes. Private seam в forecast допустим как контролируемая интервенция, потому что test вызывает original repository и наблюдает HTTP/DB итог. Не заменять conditional publication проверкой `publish called`.
- **Guarantees at risk:** group atomicity, revision stability, connection/member/source binding, exact reconciliation, replay protection и refund limit.
- **Counterarguments / trade-offs:** отдельный reader loop не гарантирует попасть в середину каждого scheduling interleaving; claim должен быть ограничен. Deterministic post-first-write rollback и controlled revision-change cases дают более сильный distinguishing proof и сохраняются независимо от этого reader sample.
- **Verification or rollback:** existing isolated SQLite suites при соответствующем source change. Их запуск не выполнен в аудите; вывод основан на точном control flow до throw и независимых observed records.
## 5. DELETE / SIMPLIFY / KEEP / INVESTIGATE
| Decision | Объект | Обоснование и условие | Что обязательно сохраняется |
| --- | --- | --- | --- |
| DELETE | `tbank-screen.spec.ts`, TQ-009 | текущий normalization consumer — недоступная manual command; только после retirement confirmation | active HTTP date/access tests, hourly owner и Ledger planner |
| SIMPLIFY | Events filter-interaction tests, TQ-010 | заменить прежний UX oracle current read-only forecast behavior | event meaning/amount, account scope, API date semantics, cached result/retry |
| FIX | race stimulus/barriers, TQ-001/002 | started producer и completed loser вместо unused deferred/early negative | generation/identity guards и real observed data |
| FIX | hourly proof, TQ-003 | actual coordinator/reservation/result boundary | durable reservation, no overlap/catch-up, partial-success preservation |
| FIX | Portfolio/forecast/auth/rollback oracles, TQ-004/005/007/008 | existing suites можно сделать различающими без нового framework | monetary accuracy, HTTP read-only, CAS, validation, transaction |
| KEEP | exact money layers, TQ-012 | разные failure modes: pure math/storage/wire/sorting | большие/отрицательные значения и независимо заданные expected results |
| KEEP | real auth CAS + frontend generations, TQ-011 | две операции реально начаты и дожданы; winner проверен после replay | conditional update/no loser cookie/no resurrected identity |
| KEEP | transaction/CAS/replay cases, TQ-013 | actual DB writes перед failure или изменением dependency | old complete group, work/source versions, tombstones, refund limits |
| KEEP | controller argument seam test | 20-line arbitrary-user mapping проверяет собственный adapter; HTTP guards другим уровнем | authenticated sub/account/query routing |
| KEEP | TbankClient retry/date seam tests | собственная calendar/protocol/retry policy, даже при mock transport | inclusive → half-open UTC, retry bound, no auth retry |
| KEEP | real browser layout case | actual geometry различает колонку/row; JSDOM не даёт эквивалентный oracle | 375/1280 responsive behavior |
| INVESTIGATE | missing MOEX block, TQ-006 | semantic conflict, no trusted provider payload in audit | malformed/cursor/partial-calendar safeguards |
**Применение ponytail-audit:** `delete: manual-screen normalization tests после retirement недоступного command-flow; replacement: ничего для прежней capability. [backend/src/modules/investments/tbank-sync/tbank-screen.spec.ts]`
Размер этого условного кандидата — **75 физических строк**, по numbered Read существующего файла. Для него отдельно: `net: -75 lines, -0 deps possible.` Это размер только удаляемого test file, **не прогноз net diff всех рекомендаций**; source cleanup, replacement tests и CI savings не измерялись. Массовое удаление controller/DTO/mock tests только по внешнему виду не обосновано.
### High-value KEEP examples
- **Auth HTTP race:** обе операции проходят barrier, SQLite выбирает winner, обе завершены до assertion; replay старого cookie и последующий winner refresh различают «одна ошибка» и «повреждена победившая session».
- **Operations group rollback:** incomplete second interval проверяется после обработки первого, поэтому removal transaction меняет visible financial group/coverage, а не только количество calls.
- **Forecast dependency CAS:** DB quantity/revision меняются до original resolve; old fallback и next exact amount отличают отказ stale candidate от просто вызванного repository method.
- **Money:** expected maximum kopecks и decimal strings независимо заданы; roundtrip отдельно проверяет BigInt и output, не только `format(parse(x)) == x`.
- **Bank replay/tombstone:** одинаковый intent и независимый expense одновременно не должны ни двойно списывать деньги, ни терять второй intent; replay после delete не оживляет operation.
- **Browser layout:** реальные ненулевые bounds и относительное положение карточек; это оправданный browser test своего layout, а не тест декларации Mantine.
## 6. Cost & benefit
| Рекомендация | Ожидаемый класс пользы | Измерение | Обязательная regression boundary |
| --- | --- | --- | --- |
| TQ-001/002 | меньше ложной уверенности/недетерминированного расследования races | false-green rate, flaky rate, agent tokens: not measured | started + completed real operation и observed data |
| TQ-003 | предотвращение потери refresh/lifecycle regressions | incidents/CI minutes: not measured; suite может добавить время | DB reservation + terminal hourly result, fake upstream |
| TQ-004/005/010 | меньше поддержки устаревшего поведения; лучше user-visible guarantees | maintenance time/UX impact: not measured | full route/resource result, original calculation metadata, HTTP-only retry |
| TQ-006 | избежать неверного confirmed-empty либо ложного failure | provider failure frequency: not measured | offline payload semantics + real persistence fallback |
| TQ-007/008 | закрытие конкретных trust/storage boundary blind spots | security/storage failure frequency: not measured | configured HTTP/DTO и post-write SQLite rollback |
| TQ-009 | cognitive/maintenance load; удаление orphan scenario | candidate file: 75 физических строк; время исполнения not measured | повторная проверка consumer graph, active boundaries KEEP |
| TQ-011/012/013 | сохранить наиболее ценные distinguishing proofs | только 2 pure suites/28 PASS; остальные runtime UNVERIFIED | complementary unit + true integration; не mock-only replacement |
Runtime production оптимизация этим аудитом не предлагалась. Проценты ускорения/сокращения покрытия/CI или общий net LOC не прогнозируются. Новые dependencies, runner scripts, generic factories и mutation framework не нужны для рекомендованных минимальных изменений.
## 7. Top actions
Максимум десять шагов; порядок — value/risk/effort, а не число замечаний.
### Quick wins
1. **TQ-001:** заменить unused-deferred late-portfolio stimulus existing source request, проверить started и await work.done. Empty membership оставить отдельной observable гарантией.
2. **TQ-002:** добавить completion barrier real old operation в StocksPage/resource race cases; уже true new-result assertion не использовать как barrier.
3. **TQ-004:** обновить existing route/MSW oracle: source metadata + сохранённые totals + warning + click HTTP retry; дождаться actual query error, не request counter.
4. **TQ-007:** добавить independently expected multibyte boundaries и refresh-as-Bearer HTTP case; расширить cookie assertions на существующем success response.
5. **TQ-008:** честно назвать upstream-failure test и добавить один post-first-write rollback case с snapshot + points, используя isolated SQLite.
### Нужны подтверждённый behavioral oracle или решение владельца
6. **TQ-003:** построить минимальный executable proof current hourly owner: reservation time table и один controlled partial/error walk. Сначала закрепить missed-term semantics; новый framework не нужен.
7. **TQ-005:** согласовать latest saved forecast selection; заменить null expectation на successful empty fallback и добавить shifted-window/new-complete candidate distinction. Existing CAS/fallback tests KEEP.
8. **TQ-010:** вместе с screen owner упростить Events test set под read-only current UX. API `from/to`, event meaning и source-error fallback не удалять вместе с filter UI.
9. **TQ-006:** решить missing-block semantics по trusted offline payload/контракту; затем разделить absence/malformed unit cases и сохранить persistence negative proof.
10. **TQ-009:** подтвердить отсутствие внешнего programmatic consumer manual command; после retirement удалить orphan normalization test. При наличии такого consumer — KEEP и явно определить контракт.
Полноценный ADR нужен только если меняется принятое продуктовое решение (например, сохраняются event filters вопреки текущей spec) или provider completeness semantics. Для исправления test stimulus, ожидания Promise и independent boundary fixture отдельный ADR не нужен.
## 8. Cross-audit signals
- **Audit 06/13:** hourly catch/settlement/polling и first-launch/missed-term поведение — `backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:48–110,178–218`, `backend/src/modules/investments/tbank-sync/tbank-sync.repository.ts:174–219`; здесь исследован только отсутствующий regression proof.
- **Audit 05:** Portfolio metadata/recovery и Events filter UI owner — `frontend/src/pages/account/ui/AccountPage/AccountPage.tsx:24–34`, `frontend/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.vm.ts:19–123`; полноценный UI redesign не выполнялся.
- **Audit 04/06:** retirement недоступного manual command — `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:54–78`, `backend/src/modules/investments/tbank-sync/tbank-sync.controller.ts:31–40`; тестовый кандидат TQ-009 зависит от решения owner.
- **Audit 09/11:** отсутствие timer/multi-resource proof не решать изменением runner/CI; `frontend/src/features/tbank-connection/model/screen-sync.test.tsx:21–79` — отдельная quality boundary, стоимость инфраструктуры здесь не оценивалась.
- **Audit 15:** accepted ADR 019 и current behavioral specs расходятся с частью UI/parser/test expectations; `docs/adr/019-hourly-reads-and-online-instrument-reference.md:9–29`, Events/Auth specs — нужна reconciliation решений, не дополнительный обязательный workflow.
## 9. Open questions / verification plan
| Вопрос, меняющий решение | Current evidence | Самый дешёвый способ проверки | Что изменить в выводе |
| --- | --- | --- | --- |
| Есть ли поддерживаемый consumer `TbankSyncService.request` вне checkout? | в `src`/`test` найден только unused request owner; controller status-only | спросить владельца capability / повторить consumer search перед retirement | external consumer → TQ-009 KEEP вместо DELETE |
| Missing ISS block означает supported absence или повреждение/изменение API? | tests/source failure vs spec no-data | trusted сохранённый JSON/первичный provider contract offline; никаких real personal APIs | выбрать unit/integration oracle TQ-006 и при необходимости обновить решение |
| Принят ли current Events read-only UX или filters намеренно остаются? | current spec no filters/retry vs source/tests filters/reset | решение screen owner по existing spec/accepted ADR | TQ-010 simplification либо explicit amended contract; negative retry не оставлять молча |
| Как выбирать последний complete forecast между окнами? | fallback ищет overlap, candidate может уступить old scope | existing SQLite suite с двумя independently distinguishable scopes/amounts/times | TQ-005 positive selection oracle без relabelling |
| Какой long-walk/missed-term outcome действителен? | spec 10:00 → после 11:00 completion → следующий ≥12:00 | table `reserveHourly` на real isolated DB с контролируемыми start/finish/due | TQ-003 expected timing; конфликт source не исправлять по случайному аналогичному test |
| При удалении stale guard текущий frontend test действительно false-green? | completion old operation не ожидается явно | при отдельной test-fix задаче сначала explicit started/settled instrumentation existing test; mutation только по отдельному разрешению | сейчас Medium risk; не превращать статическую гипотезу в измеренный факт |
| Какие suites актуально проходят в runtime? | запущены только две денежные pure suites | owners запускают соответствующие existing targeted suites после изменений | current report не означает all tests PASS |
**Минимальный порядок последующей верификации:** safe pure auth/decimal boundary → targeted RTL/MSW owner tests → isolated SQLite publication/reservation/auth HTTP → browser только для изменяемой browser-specific гарантии. Использовать локальные `.nvmrc` и existing runner safeguards. Полный verify нужен при будущих code changes по affected repository conventions, а не как побочный эффект read-only аудита.
**Проверка завершения аудита:** новый отчёт просмотрен на структуру, exact evidence и границы claims; `git diff --check` в checkout не выявил whitespace errors. Для нового ignored-файла отдельно выполнен `git diff --no-index --check /dev/null audit-results/08-test-quality-and-effectiveness-report.md`; замечаний нет. Подсчёт заголовков `^### TQ-[0-9]{3} —` дал 13 записей, соответствующих итоговой таблице. Повторный `git status --short` в четырёх checkout пустой; их HEAD не изменены. `git check-ignore` подтвердил, что отчёт — ignored локальный artifact в root `audit-results`, не изменение исходников/test/config четырёх репозиториев.
## 10. Машинно-считываемая таблица для консолидации
| ID | Decision | Severity | Confidence | Repo | Files | One-line impact | Effort | Related IDs |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| TQ-001 | FIX | P2 | High | backend | test/tbank-local-reads.e2e-spec.ts | Late-publication case never starts the protected producer | S | TQ-003,TQ-013 |
| TQ-002 | FIX | P2 | Medium | frontend | src/pages/stocks/ui/StocksPage/StocksPage.test.tsx;src/shared/api/resource-query.test.tsx | Late-result assertions lack full loser-completion barrier | S | TQ-001,TQ-011 |
| TQ-003 | FIX | P1 | High | backend | src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts;src/modules/investments/tbank-sync/tbank-sync.repository.ts;test/upcoming-events.e2e-spec.ts | Critical hourly lifecycle and reservation lack direct executable proof | M | TQ-001,TQ-005 |
| TQ-004 | FIX | P2 | High | frontend | src/pages/account/ui/AccountPage/AccountPage.test.tsx;src/pages/account/ui/AccountPage/AccountPage.tsx | Portfolio oracle forbids recovery and misses source-metadata wiring | S | TQ-010 |
| TQ-005 | FIX | P2 | High | backend | test/upcoming-events.e2e-spec.ts;src/modules/investments/portfolio/infrastructure/forecast.repository.ts | Range oracle rejects complete-empty fallback and misses new-window publication | M | TQ-003,TQ-006,TQ-013 |
| TQ-006 | INVESTIGATE | P2 | High | backend | src/integrations/moex/moex.client.spec.ts;test/upcoming-events.e2e-spec.ts | Missing source block semantics conflict with behavioral requirement | S | TQ-005,TQ-013 |
| TQ-007 | FIX | P2 | High | backend,frontend | backend/src/common/dto-validation.spec.ts;backend/test/app.e2e-spec.ts;frontend/src/features/auth/ui/AuthForm/AuthForm.test.tsx | UTF-8 and token-role/cookie boundaries lack distinguishing cases | S | TQ-011 |
| TQ-008 | FIX | P2 | High | backend | test/tbank-value-history.e2e-spec.ts | Publish-failure case fails upstream before transaction and omits snapshot assertion | S | TQ-001,TQ-012,TQ-013 |
| TQ-009 | DELETE | P3 | High | backend | src/modules/investments/tbank-sync/tbank-screen.spec.ts | Orphan manual-command tests removable after owner confirms retirement | S | TQ-003 |
| TQ-010 | SIMPLIFY | P2 | High | frontend | src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.filters.test.tsx;src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.test.tsx | Old filter/reset oracles obstruct current read-only forecast and retry UX | M | TQ-004,TQ-005 |
| TQ-011 | KEEP | P1 | High | backend,frontend | backend/test/app.e2e-spec.ts;backend/src/modules/auth/application/auth.service.spec.ts;frontend/src/shared/api/client.test.ts;frontend/src/features/auth/model/auth.test.tsx | Preserve real refresh CAS, usable winner and late-identity isolation | S | TQ-002,TQ-007 |
| TQ-012 | KEEP | P1 | High | backend,frontend | backend/src/integrations/tbank/decimal.spec.ts;backend/src/modules/banking/bank-cards/domain/rub-money.spec.ts;backend/test/bank-cards.e2e-spec.ts;frontend/src/entities/bank-card/api/summary.test.ts | Preserve independent exact-money unit, persistence and sorting proofs | S | TQ-007,TQ-008,TQ-013 |
| TQ-013 | KEEP | P1 | High | backend | test/tbank-operations-persistence.e2e-spec.ts;test/upcoming-events.e2e-spec.ts;test/bank-cards.e2e-spec.ts | Preserve deterministic rollback, dependency CAS and replay invariants | S | TQ-001,TQ-005,TQ-006,TQ-008,TQ-012 |