# Test Quality + Pre-Prod Sprint Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Phase B — закрыть 3 test-quality debt items (vitest timeout, prettier drift, ProjectFactory unique-name + test:parallel alias); Phase A — закрыть 3 pre-prod P2 gaps из Audit #2 Phase 14 (partitions schedule, runbook, aria-tooltip-name).

**Architecture:** Phase B — чистые config/formatting/factory изменения без behavioral code changes. Phase A — +1 cron entry, новый `RUNBOOK.md`, +1 a11y attribute fix в TenantsTable.vue. Фазы независимы, коммиты атомарны. Итог: 6 content-commits + push.

**Tech Stack:** PHP 8.3 / Laravel 13 / Pest 4, Vue 3 / Vuetify 3 / Vitest 4, Prettier 3.8, axe-core 4.10 (Playwright MCP verification)

**Reference:**

- Quirk #80: `memory/feedback_environment.md` — vitest coverage timeout 5000ms на router.spec.ts
- Quirk #62/#73: sequential Pest cumulative state в `liderra_testing`
- Quirk #77: ProjectFactory birthday-paradox collision в --parallel
- Audit #2 Phase 14 findings: [`docs/superpowers/audits/2026-05-13-portal-full-audit-findings.md`](../audits/2026-05-13-portal-full-audit-findings.md)

---

## Files

### Phase B

- Modify: [`../../../app/vitest.config.ts`](../../../app/vitest.config.ts) — add `testTimeout: 10000`
- Modify: `../../../app/resources/js/**/*.{ts,vue,css}` + `../../../app/tests/Frontend/**/*.ts` — prettier --write (312-file drift)
- Modify: [`../../../app/database/factories/ProjectFactory.php`](../../../app/database/factories/ProjectFactory.php) — `fake()->unique()->words(3, true)`
- Modify: [`../../../app/composer.json`](../../../app/composer.json) — add `"test:parallel"` script alias

### Phase A

- Modify: [`../../../app/routes/console.php`](../../../app/routes/console.php) — add `Schedule::command('partitions:create-months')->daily()`
- Create: `RUNBOOK.md` (repo root, рядом с CLAUDE.md)
- Modify: [`../../../app/resources/js/components/admin/tenants/TenantsTable.vue`](../../../app/resources/js/components/admin/tenants/TenantsTable.vue) — `aria-label` на `v-tooltip`

---

## Quirks / Guardrails

| Quirk | Правило |
|---|---|
| #79 | Bash absolute paths — не стекать `cd app &&` между calls |
| #74 | `npm install` → `--legacy-peer-deps` |
| #76 | Links в plans/ → `../../../` prefix для app/ references |
| #62/#73 | Sequential Pest failures из cumulative `liderra_testing` data → использовать `--recreate-databases` |
| #77 | ProjectFactory `fake()->words(3,true)` → birthday-paradox collision в --parallel |
| #80 | router.spec.ts coverage timeout 5000ms → `testTimeout: 10000` fix |

---

## Task B.1: vitest.config.ts — testTimeout: 10000 (Quirk #80)

**Files:**

- Modify: `app/vitest.config.ts:9` — добавить `testTimeout` внутри `test: {}`

- [ ] **B.1.1: Verify router.spec.ts тайм-аутит под coverage (baseline до фикса)**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
npx vitest run --coverage --coverage.reporter=text-summary --include="tests/Frontend/router.spec.ts" 2>&1
```

Expected: ≥1 timeout failure (`Error: Test timed out after 5000ms`). Показать полный output.

Если 0 failures — фикс уже применён; skip B.1.2, перейти к B.1.3.

- [ ] **B.1.2: Edit vitest.config.ts**

Изменить `app/vitest.config.ts` — добавить `testTimeout: 10000` в блок `test`:

```ts
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
import vuetify from 'vite-plugin-vuetify';

// Vitest config (фаза 2, Tooling §3.3 #23). Отдельно от vite.config.js
// чтобы не тащить laravel-vite-plugin в JSDOM-тестах.
export default defineConfig({
    plugins: [vue(), vuetify({ autoImport: true })],
    test: {
        globals: true,
        environment: 'jsdom',
        testTimeout: 10000,
        setupFiles: ['./tests/Frontend/setup.ts'],
        include: ['tests/Frontend/**/*.{test,spec}.ts'],
        server: {
            deps: {
                inline: ['vuetify'],
            },
        },
    },
});
```

- [ ] **B.1.3: Verify router.spec.ts passes под coverage**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
npx vitest run --coverage --coverage.reporter=text-summary --include="tests/Frontend/router.spec.ts" 2>&1
```

Expected: **все 9 тестов PASS**, 0 timeouts. Coverage table включает `router/index.ts` row. Показать полный output.

- [ ] **B.1.4: Verify полный Vitest suite не сломан**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
npx vitest run 2>&1 | tail -15
```

Expected: **88 files / 683 passed + 3 skipped**. Показать output.

- [ ] **B.1.5: Commit**

```powershell
cd "c:\моя\проекты\портал crm\Документация"
git add "app/vitest.config.ts"
git commit -m "test(vitest): add testTimeout: 10000 — fix quirk #80 router.spec.ts coverage timeout

v8 coverage instrumentation adds ~10x overhead to router-guard async tests,
pushing past the 5000ms default. Audit #2 Phase 13 finding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```

---

## Task B.2: Prettier drift — format 312 files

**Files:**

- Modify: `app/resources/js/**/*.{ts,vue,css}`, `app/tests/Frontend/**/*.ts`

- [ ] **B.2.1: Check drift scope**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
npm run format:check 2>&1 | tail -10
```

Expected: список ~312 файлов с formatting issues + exit 1.

Если выход 0 / 0 files — drift уже применён; skip B.2.2–B.2.3.

- [ ] **B.2.2: Apply prettier --write**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
npm run format 2>&1
```

Expected: output вида `X files written`. Показать полный output.

- [ ] **B.2.3: Re-verify 0 issues**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
npm run format:check 2>&1 | tail -5
```

Expected: exit 0, 0 formatting issues.

- [ ] **B.2.4: Vitest suite — verify no breakage**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
npx vitest run 2>&1 | tail -10
```

Expected: 88 files / 683 passed + 3 skipped.

- [ ] **B.2.5: ESLint — verify no regressions**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
npm run lint:vue 2>&1 | tail -10
```

Expected: 0 errors.

- [ ] **B.2.6: Stage и commit**

NB: если Vite dev server запущен background → `dev-indices.json` мог auto-regenerate. Стейджить вместе с logical commit.

```powershell
cd "c:\моя\проекты\портал crm\Документация"
git add "app/resources/js/" "app/tests/Frontend/" "app/dev-indices.json"
git status --short | head -20
git commit -m "style: prettier --write all frontend files — eliminate 312-file drift

Audit #2 Phase 1 found 312 files with formatting mismatch vs .prettierrc.
No logic changes — whitespace/quote/trailing-comma normalization only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```

---

## Task B.3: ProjectFactory unique() + test:parallel alias (Quirks #77, #62)

**Files:**

- Modify: `app/database/factories/ProjectFactory.php:23`
- Modify: `app/composer.json` (add `test:parallel` script)
- Test: `app/tests/Feature/Api/ProjectBulkActionsTest.php` (verify --parallel pass)

**Context quirk #77:** `fake()->words(3, true)` на ~100 English слов → ~1M 3-word комбинаций. Birthday paradox для 501 projects в одном tenant → ~12.5% collision probability per test run. `fake()->unique()` держит per-instance трекер использованных значений → 0 дублей внутри одного factory instance.

- [ ] **B.3.1: Verify collision reproducible (baseline до фикса)**

Run 2–3 раза:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
vendor/bin/pest --parallel tests/Feature/Api/ProjectBulkActionsTest.php 2>&1 | tail -20
```

Expected: ≥1 из 3 прогонов → failure `SQLSTATE[23505] projects_tenant_id_name_key`. Показать output каждого прогона.

- [ ] **B.3.2: Применить фикс в ProjectFactory.php**

Изменить `app/database/factories/ProjectFactory.php` строку 23 с:

```php
            'name' => fake()->words(3, true),
```

на:

```php
            'name' => fake()->unique()->words(3, true),
```

- [ ] **B.3.3: Run affected test 5 раз — verify 0 collisions**

Run 5 раз:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
vendor/bin/pest --parallel tests/Feature/Api/ProjectBulkActionsTest.php 2>&1 | tail -5
```

Expected: **14/14 passed** все 5 прогонов. Показать output каждого.

- [ ] **B.3.4: Добавить test:parallel alias в composer.json**

В `app/composer.json` добавить строку после `"pint:test"`:

```json
        "test:parallel": "vendor/bin/pest --parallel --recreate-databases",
```

Результирующий блок scripts (фрагмент):

```json
        "test": [
            "@php artisan config:clear --ansi @no_additional_args",
            "@php artisan test"
        ],
        "pint": "@php vendor/bin/pint",
        "pint:test": "@php vendor/bin/pint --test",
        "test:parallel": "vendor/bin/pest --parallel --recreate-databases",
```

- [ ] **B.3.5: Validate composer.json + smoke-run test:parallel**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
composer validate 2>&1 | tail -3
```

Expected: `./composer.json is valid`

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
composer test:parallel 2>&1 | tail -10
```

Expected: 742 passed, ≤1 failed (квирк 72 sporadic). Показать полный tail.

- [ ] **B.3.6: Commit**

```powershell
cd "c:\моя\проекты\портал crm\Документация"
git add "app/database/factories/ProjectFactory.php" "app/composer.json"
git commit -m "fix(test): ProjectFactory unique() name + test:parallel alias (quirks #77, #62)

fake()->unique()->words() prevents birthday-paradox collision on 501 projects
in one tenant (1M combos, ~12.5% parallel-run failure rate). Quirk #77.

Adds composer test:parallel alias (--parallel --recreate-databases) for
accumulated-state cleanup in sequential Pest runs. Quirks #62/#73.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```

---

## Task A.1: Register partitions:create-months в schedule (Audit #2 Phase 14 P2)

**Files:**

- Modify: `app/routes/console.php`

**Context:** `PartitionsCreateMonths` (Command signature: `partitions:create-months`) создаёт ежемесячные партиции для `deals` и `supplier_lead_costs` на 2 месяца вперёд. Idempotent (CREATE TABLE IF NOT EXISTS via `pg_class` check). Команда существует, но НЕ зарегистрирована в `routes/console.php` → partition lifecycle не automated. Audit #2 Phase 14 P2.

На dev (Windows): запускается вручную / Windows Task Scheduler.
На prod (YC Linux): через `php artisan schedule:run` каждую минуту cron.

- [ ] **A.1.1: Verify команда отсутствует в schedule**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
php artisan schedule:list 2>&1
```

Expected: `partitions:create-months` **отсутствует** в списке. Показать полный output.

- [ ] **A.1.2: Добавить Schedule entry в routes/console.php**

В `app/routes/console.php` добавить в конце файла (после блока CsvReconcileJob):

```php

// Partition maintenance: create next 2 monthly partitions for `deals` + `supplier_lead_costs`.
// Replaces pg_partman (unavailable on native Windows; see project_phase1_strategy.md).
// Idempotent: CREATE TABLE IF NOT EXISTS via pg_class check in PartitionsCreateMonths::partitionExists().
// On prod: triggered daily via `php artisan schedule:run` (cron every minute).
// On dev (Windows): run manually or via Windows Task Scheduler.
Schedule::command('partitions:create-months')
    ->daily()
    ->timezone('Europe/Moscow');
```

- [ ] **A.1.3: Verify команда появилась в schedule:list**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
php artisan schedule:list 2>&1
```

Expected: строка с `partitions:create-months` и частотой `Daily`. Показать полный output.

- [ ] **A.1.4: Verify команда выполняется без ошибок**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
php artisan partitions:create-months 2>&1
```

Expected: `Done: 0 created, 6 skipped (ahead=2).` (все партиции уже существуют от migrate:fresh). Exit code 0. Показать полный output.

- [ ] **A.1.5: Pest regression check**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
vendor/bin/pest --parallel --recreate-databases 2>&1 | tail -10
```

Expected: 742 passed, ≤1 failed (квирк 72).

- [ ] **A.1.6: Commit**

```powershell
cd "c:\моя\проекты\портал crm\Документация"
git add "app/routes/console.php"
git commit -m "fix(scheduler): register partitions:create-months in schedule — close Audit #2 P2

Missing from routes/console.php left partition lifecycle not automated.
Audit #2 Phase 14 P2 finding. Idempotent daily at Europe/Moscow timezone.
On dev: run manually. On prod: via schedule:run cron.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```

---

## Task A.2: Create RUNBOOK.md (Audit #2 Phase 14 P2)

**Files:**

- Create: `RUNBOOK.md` (repo root, рядом с `CLAUDE.md`)

**Context:** Audit #2 Phase 14 P2 — deployment runbook отсутствует. Статус: pre-prod (Б-1 ООО регистрация pending; prod credentials/infra не finalized). Runbook покрывает local dev setup и production deploy на YC Linux VM.

- [ ] **A.2.1: Create RUNBOOK.md**

Создать файл `RUNBOOK.md` в корне репозитория (`c:\моя\проекты\портал crm\Документация\RUNBOOK.md`):

```markdown
# Лидерра — Runbook развёртывания

> **Версия:** 1.0 от 13.05.2026. Статус: 🟡 PRE-PROD (Б-1 ООО регистрация pending — prod credentials/infra не finalized).
> **Стек:** Laravel 13 + Vue 3 + Vuetify 3 + PostgreSQL 16 (ICU) + Redis 7 + Yandex Cloud.

---

## Содержание

1. [Системные требования](#1-системные-требования)
2. [Клонирование и зависимости](#2-клонирование-и-зависимости)
3. [Конфигурация .env](#3-конфигурация-env)
4. [База данных](#4-база-данных)
5. [Сборка frontend](#5-сборка-frontend)
6. [Очереди (Queue Worker)](#6-очереди-queue-worker)
7. [Планировщик (Scheduler)](#7-планировщик-scheduler)
8. [Проверка работоспособности](#8-проверка-работоспособности)
9. [Типовые проблемы](#9-типовые-проблемы)
10. [Первый запуск — Dev seed](#10-первый-запуск--dev-seed)

---

## 1. Системные требования

| Компонент | Версия | Примечания |
|-----------|--------|------------|
| PHP | 8.3+ | Extensions: pdo_pgsql, pgsql, redis, bcmath, mbstring, openssl, fileinfo |
| Composer | 2.x | |
| Node.js | 20+ | |
| npm | 10+ | |
| PostgreSQL | 16+ | ICU collation support обязателен (ILIKE кириллица) |
| Redis | 7+ | Memurai на Windows-dev |
| Git | 2.x | |

**Linux prod (Yandex Cloud Ubuntu 22.04):**

```bash
# PHP 8.3
sudo apt install php8.3-fpm php8.3-pgsql php8.3-redis php8.3-bcmath \
  php8.3-mbstring php8.3-xml php8.3-curl php8.3-zip

# PostgreSQL 16 client
sudo apt install libpq-dev postgresql-client-16

# Node.js 20
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
sudo apt install nodejs
```

---

## 2. Клонирование и зависимости

```bash
git clone git@github.com:CoralMinister/lidpotok.git liderra
cd liderra/app

# PHP-зависимости (prod)
composer install --no-dev --optimize-autoloader

# Node-зависимости (prod)
# --legacy-peer-deps обязателен (Histoire 1.0-beta.1 peerDep conflict)
npm install --legacy-peer-deps --omit=dev
```

---

## 3. Конфигурация .env

```bash
cp .env.example .env
php artisan key:generate
```

Обязательные переменные:

```ini
APP_ENV=production
APP_DEBUG=false
APP_URL=https://your-domain.ru

DB_CONNECTION=pgsql
DB_HOST=<managed-pg-host>
DB_PORT=5432
DB_DATABASE=liderra
DB_USERNAME=crm_app_user
DB_PASSWORD=<strong-password>

REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=<redis-password>

MAIL_MAILER=smtp
MAIL_HOST=smtp.unisender.ru
MAIL_PORT=587
MAIL_USERNAME=<unisender-login>
MAIL_PASSWORD=<unisender-api-key>
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=noreply@liderra.ru
MAIL_FROM_NAME="Лидерра"

SESSION_DRIVER=redis
CACHE_STORE=redis
QUEUE_CONNECTION=redis
```

> **Sentry (pending Б-1):** `SENTRY_LARAVEL_DSN=https://...@sentry.your-domain.ru/...`

---

## 4. База данных

### 4.1 Создание БД с ICU collation

```sql
-- От суперпользователя PostgreSQL
CREATE DATABASE liderra
  WITH ENCODING 'UTF8'
  LOCALE_PROVIDER icu
  ICU_LOCALE 'und'
  LC_COLLATE 'C'
  LC_CTYPE 'C'
  TEMPLATE template0;
```

> **Важно:** ICU collation обязателен для корректного `ILIKE` на кириллице. БД без ICU → поиск по русским строкам возвращает 0 результатов (Quirk #61).

### 4.2 Роли БД (только prod)

```bash
# На dev используется postgres-superuser; на prod создать роли:
psql -U postgres -f db/00_create_roles.sql
```

### 4.3 Миграции

```bash
php artisan migrate --force
```

Единственная миграция `load_initial_schema.php` загружает `db/schema.sql` через `DB::unprepared()`. Ожидается ~800ms.

### 4.4 Grants (prod)

```bash
psql -U postgres -d liderra -f db/02_grants.sql
```

### 4.5 Первичное создание партиций

```bash
# Создать ежемесячные партиции deals + supplier_lead_costs на 2 мес. вперёд
php artisan partitions:create-months
```

Далее планировщик создаёт их автоматически ежедневно.

---

## 5. Сборка frontend

```bash
npm run build
```

Ожидается ~3–5 сек. Артефакты в `public/build/`.

> Bundle analyzer: `BUILD_ANALYZE=1 npm run build` → `storage/bundle-analyze.html`

---

## 6. Очереди (Queue Worker)

**Dev:**

```bash
php artisan queue:work redis --tries=3 --timeout=60
```

**Prod (Supervisor)** — `/etc/supervisor/conf.d/liderra-worker.conf`:

```ini
[program:liderra-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/liderra/app/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/liderra/worker.log
```

```bash
sudo supervisorctl reread && sudo supervisorctl update
sudo supervisorctl start liderra-worker:*
```

---

## 7. Планировщик (Scheduler)

Добавить в crontab на prod:

```bash
* * * * * cd /var/www/liderra/app && php artisan schedule:run >> /dev/null 2>&1
```

Расписание (`routes/console.php`):

| Команда | Частота | Назначение |
|---------|---------|------------|
| `projects:reset-delivered-today` | Ежедневно 00:00 МСК | Сброс daily-счётчика лидов |
| `projects:reset-monthly` | 1-го числа 00:00 МСК | Сброс месячных счётчиков |
| `partitions:create-months` | Ежедневно | Партиции PG на 2 мес. вперёд |
| `supplier:retry-failed` | Ежечасно | Повтор failed supplier jobs |
| Queue jobs | см. console.php | Supplier sync + CSV reconcile |

Проверить расписание:

```bash
php artisan schedule:list
```

---

## 8. Проверка работоспособности

```bash
# PHP + Laravel
php artisan about

# БД соединение
php artisan db:show

# Redis
php artisan tinker --execute="echo cache()->get('healthcheck', 'redis-ok');"

# HTTP ответ (ожидается 200)
curl -o /dev/null -s -w "%{http_code}" http://localhost/login

# Frontend build артефакты
ls public/build/assets/*.js | head -5
```

---

## 9. Типовые проблемы

### ILIKE не находит кириллицу

**Симптом:** поиск по русским строкам → 0 результатов.
**Причина:** БД создана с `LC_CTYPE=C` без ICU (Quirk #61).
**Решение:** пересоздать БД через раздел 4.1 + повторить миграции.

### Pest sequential тесты падают (LookupsTest / ProjectExtensionsTest)

**Симптом:** `composer test` → 3 failures «N matches 2».
**Причина:** накопленные данные в `liderra_testing` (Quirk #62/#73).
**Решение:** `composer test:parallel` (использует `--recreate-databases`).

### partitions:create-months — ошибка доступа к pg_class

**Симптом:** `SELECT 1 FROM pg_class WHERE ...` → permission error.
**Причина:** ограниченная роль без доступа к pg_class.
**Решение:** запускать от `crm_migrator` роли или postgres superuser.

### Queue worker зависает на RefreshSupplierSessionJob

**Симптом:** job в статусе `processing` >60 сек.
**Причина:** PlaywrightBridge не может войти в портал поставщика.
**Решение:** `php artisan queue:failed` → проверить error message. Обновить credentials в таблице `system_settings`.

### Vite hot-reload не работает в staging

**Симптом:** изменения Vue-компонентов не отражаются без hard-reload.
**Причина:** `VITE_HMR_*` env vars не настроены для staging hostname.
**Решение:** в `.env` добавить `VITE_HMR_HOST=your-staging-host`. Или использовать production build (`npm run build`).

---

## 10. Первый запуск — Dev seed

Только для локальной разработки:

```bash
php artisan db:seed --class=DemoSeeder
```

Создаёт: `admin@demo.local` / `password`, 1 тенант, 3 проекта, 14 сделок.

---

*Обновлять при изменении инфраструктуры, деплой-процедур или списка scheduled-команд.*

```

- [ ] **A.2.2: Verify markdownlint**

Run:
```powershell
cd "c:\моя\проекты\портал crm\Документация"
npx markdownlint-cli2 RUNBOOK.md 2>&1
```

Expected: exit 0, 0 errors. При ошибках — исправить inline (trailing spaces, heading levels, bare URLs).

- [ ] **A.2.3: Commit**

```powershell
cd "c:\моя\проекты\портал crm\Документация"
git add "RUNBOOK.md"
git commit -m "docs: add RUNBOOK.md — deployment runbook (Audit #2 Phase 14 P2)

Covers system requirements (PHP 8.3/PG 16 ICU/Redis), DB setup with ICU
collation, migrations, roles, partition maintenance, queue worker (Supervisor),
scheduler crontab, health checks, common issues.
Status: pre-prod (B-1 pending; prod credentials TBD).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```

---

## Task A.3: Fix aria-tooltip-name — VTooltip /admin/tenants (Audit #2 Phase 10.2 P2)

**Files:**

- Modify: `app/resources/js/components/admin/tenants/TenantsTable.vue:81`

**Context:** axe-core 4.10 violation `aria-tooltip-name` (impact: serious) на `/admin/tenants`. `v-tooltip` рендерит `<div role="tooltip" id="v-tooltip-v-N">` без явно вычислимого accessible name. Vuetify VOverlay передаёт `$attrs` к overlay root div → добавление `aria-label` на `<v-tooltip>` даёт explicit accessible name для axe.

Текущий код `TenantsTable.vue:81–95`:

```html
<template #[`item.actions`]="{ item }: { item: AdminTenant }">
    <v-tooltip text="Войти как клиент (impersonation)" location="top">
        <template #activator="{ props: tipProps }">
            <v-btn
                v-bind="tipProps"
                icon="mdi-account-switch"
                variant="text"
                size="small"
                density="comfortable"
                :aria-label="`Войти как клиент (impersonation) для ${item.name}`"
                :disabled="item.status === 'suspended'"
                :data-testid="`impersonate-btn-${item.id}`"
                @click.stop="emit('impersonate', item)"
            />
        </template>
    </v-tooltip>
</template>
```

- [ ] **A.3.1: Baseline axe-core на /admin/tenants (pre-fix)**

Через Playwright MCP — убедиться что dev servers запущены:

```powershell
curl -s -o NUL -w "%{http_code}" http://127.0.0.1:8000/login
```

Expected: `200`. Если нет → запустить `php artisan serve --port=8000` + `npm run dev` в background.

Login (если сессия не активна):

```javascript
// browser_navigate → http://127.0.0.1:8000/login
// browser_evaluate:
document.querySelector('input[type="email"]').value = 'admin@demo.local';
document.querySelector('input[type="email"]').dispatchEvent(new Event('input', { bubbles: true }));
document.querySelector('input[type="password"]').value = 'password';
document.querySelector('input[type="password"]').dispatchEvent(new Event('input', { bubbles: true }));
// browser_click → button[type="submit"]
// verify redirect → /dashboard
```

Baseline axe scan:

```javascript
// browser_navigate → http://127.0.0.1:8000/admin/tenants
// browser_evaluate:
await new Promise(r => setTimeout(r, 500));
const s = document.createElement('script');
s.src = 'https://cdn.jsdelivr.net/npm/axe-core@4.10.0/axe.min.js';
document.head.appendChild(s);
await new Promise(r => setTimeout(r, 2000));
const res = await axe.run({ runOnly: { type: 'rule', values: ['aria-tooltip-name'] } });
return res.violations.map(v => ({ id: v.id, nodes: v.nodes.length, help: v.help }));
```

Expected pre-fix: `[{ id: 'aria-tooltip-name', nodes: 1, ... }]`. Если `[]` — violation исчезла сама (e.g. tooltip рендерится скрытым); зафиксировать в findings и skip A.3.2–A.3.4.

- [ ] **A.3.2: Apply fix в TenantsTable.vue**

Изменить `app/resources/js/components/admin/tenants/TenantsTable.vue` строку 81–82 с:

```html
            <template #[`item.actions`]="{ item }: { item: AdminTenant }">
                <v-tooltip text="Войти как клиент (impersonation)" location="top">
```

на:

```html
            <template #[`item.actions`]="{ item }: { item: AdminTenant }">
                <v-tooltip
                    text="Войти как клиент (impersonation)"
                    location="top"
                    aria-label="Войти как клиент (impersonation)"
                >
```

- [ ] **A.3.3: Run Vitest AdminTenantsView spec — no regression**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
npx vitest run tests/Frontend/AdminTenantsView.spec.ts 2>&1
```

Expected: все тесты pass. Показать полный output.

- [ ] **A.3.4: Verify fix via axe-core post-fix**

Через Playwright MCP:

```javascript
// browser_navigate → http://127.0.0.1:8000/admin/tenants (hard reload)
// browser_evaluate (та же axe-инъекция + axe.run):
await new Promise(r => setTimeout(r, 500));
// [inject axe-core script как в A.3.1]
await new Promise(r => setTimeout(r, 2000));
const res = await axe.run({ runOnly: { type: 'rule', values: ['aria-tooltip-name'] } });
return res.violations.map(v => ({ id: v.id, nodes: v.nodes.length }));
```

Expected: **`[]`** — 0 `aria-tooltip-name` violations.

**Если не `[]` — ≥3 гипотезы:**

- **H1:** Vuetify не форвардит `aria-label` к overlay root div (attrs идут в content div, не overlay). Фикс: попробовать `:content-props="{ 'aria-label': 'Войти как клиент (impersonation)' }"` вместо прямого `aria-label`.
- **H2:** Tooltip overlay рендерится в `display:none` во время axe scan — accessible name не вычислима. Фикс: hover кнопки перед scan (`browser_hover` на `[data-testid="impersonate-btn-1"]`) чтобы tooltip открылся, затем axe.run.
- **H3:** axe CDN кэш прогрузил old version (4.9.x вместо 4.10). Проверить: `axe.version` в evaluate. Если не `4.10.0` → принудительный cache-bust `?v=${Date.now()}` в src URL.

Если H1+H2+H3 не дают `[]` → эскалировать как «VTooltip overlay teleport не доступен axe — требует отдельного исследования».

- [ ] **A.3.5: Commit**

```powershell
cd "c:\моя\проекты\портал crm\Документация"
git add "app/resources/js/components/admin/tenants/TenantsTable.vue"
git commit -m "fix(a11y): aria-label on VTooltip overlay /admin/tenants — close Audit #2 P2

axe-core 4.10 aria-tooltip-name: role=tooltip overlay had no accessible name.
aria-label forwarded via VOverlay attrs. Audit #2 Phase 10.2 finding.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
```

---

## Task I: Финальная регрессия + Pre-push + Push

- [ ] **I.1: Pest полный suite**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
vendor/bin/pest --parallel --recreate-databases 2>&1 | tail -15
```

Expected: **742 passed**, ≤1 failed (квирк 72 sporadic). Показать полный tail.

- [ ] **I.2: Vitest полный suite**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
npx vitest run 2>&1 | tail -15
```

Expected: **88 files / 683 passed + 3 skipped. 0 failures.** Показать полный tail.

- [ ] **I.3: Pint + Larastan**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация\app"
composer pint:test 2>&1 | tail -5
composer stan 2>&1 | tail -10
```

Expected: Pint 0 issues; Larastan 0 errors. Показать output обоих.

- [ ] **I.4: lefthook pre-push**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация"
npx lefthook run pre-push 2>&1
```

Expected: gitleaks-full-history 0 leaks + lychee 0 broken links. Показать **полный** output.

- [ ] **I.5: Push origin/main**

Run:

```powershell
cd "c:\моя\проекты\портал crm\Документация"
git push origin main 2>&1
```

Expected: exit 0. Показать полный output.

- [ ] **I.6: Verify remote HEAD**

Run:

```powershell
git log origin/main --oneline -8
```

Expected: 6 новых коммитов (B.1, B.2, B.3, A.1, A.2, A.3) на вершине после `9e175a1`.

---

## Self-Review

**1. Spec coverage:**

| Требование | Task |
|---|---|
| Quirk #80 testTimeout router.spec.ts | B.1 ✅ |
| Prettier drift 312 files | B.2 ✅ |
| ProjectFactory unique() (quirk #77) | B.3 ✅ |
| Quirk #62/#73 test:parallel alias | B.3 ✅ |
| partitions:create-months в schedule | A.1 ✅ |
| RUNBOOK.md deployment runbook | A.2 ✅ |
| aria-tooltip-name VTooltip fix | A.3 ✅ |
| Финальная регрессия + push | I ✅ |

**2. Placeholder scan:** нет TBD/TODO/"implement later" в step bodies. A.3.4 H-гипотезы — contingency debugging, не placeholder (конкретные действия с конкретными ожидаемыми результатами). A.2 RUNBOOK.md `<managed-pg-host>` / `<strong-password>` — это actual env-var placeholders, not plan placeholders (правильно — юзер заполняет).

**3. Type consistency:** PHP factory — `string` (возврат `fake()->unique()->words(3, true)` тот же type). Vue template — `aria-label` string attr (стандартный HTML attr). composer.json — корректный JSON script entry. Нет новых типов/методов.

**Gaps found и закрыты:**

- B.2.6: Vite dev server в background может auto-regenerate `dev-indices.json` → добавлено в stage-команду ✅
- A.3.4: H-гипотезы охватывают 3 failure-mode'а VTooltip axe ✅
- lychee проверка RUNBOOK.md: файл в корне, links к `app/` корректны без `../../../` (не в plans/) ✅
