# Q.DEFER.003 sub-B + sub-C — Coverage Debt Closure Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development for execution (user picked Subagent-Driven precedent). Steps use checkbox (`- [ ]`) syntax.

**Goal:** Закрыть Q.DEFER.003 sub-B (security UI cards 28-0% coverage) + sub-C (router guards 33% Stmts / 7% Funcs coverage) добавлением own-spec файлов для 3 карточек и расширением router.spec.ts integration scenarios.

**Architecture:** TDD per task. Vitest @vue/test-utils mount + Pinia + Vuetify plugins, `vi.mock('../../resources/js/api/auth')` для изоляции от HTTP. Router tests расширяют existing router.spec.ts (62 lines, 5 tests) integration cases на beforeEach guard branches (authenticated user → guestOnly redirect / passing requiresAuth / catch-all 404 / admin redirect).

**Tech Stack:** Vue 3.5 + Vuetify 3.12 + Vue Router 4.6 + Pinia 3.0 + Vitest 4.1 + @vue/test-utils + vi.mock.

---

## File Structure

**Create (test files):**

- `app/tests/Frontend/ChangePasswordCard.spec.ts` — мин ~3 specs (static placeholder)
- `app/tests/Frontend/RecoveryCodesCard.spec.ts` — ~6-8 specs (dialog flow + auth-gated visibility)
- `app/tests/Frontend/TwoFactorCard.spec.ts` — ~7-10 specs (setup wizard 3-step + disable flow + error states)

**Modify (test):**

- `app/tests/Frontend/router.spec.ts` — добавить 5 integration tests на guard branches

**Modify (memory):**

- `memory/MEMORY.md` — sync project_state line с new test count
- `memory/project_full_audit_2026-05-12.md` — Q.DEFER.003 closure status

**NO production code changes** — pure test additions.

---

## Task 1: ChangePasswordCard.spec.ts (placeholder coverage)

**Files:**

- Create: `app/tests/Frontend/ChangePasswordCard.spec.ts`

Component `app/resources/js/components/settings/security/ChangePasswordCard.vue` — 17 lines, чистый static placeholder: `<v-card>` с заголовком «Пароль» + description «Последняя смена: 12.04.2026 (26 дней назад)» + `<v-btn>` «Сменить пароль» без логики.

- [ ] **Step 1: Write failing test**

```typescript
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { createVuetify } from 'vuetify';

import ChangePasswordCard from '../../resources/js/components/settings/security/ChangePasswordCard.vue';

const vuetify = createVuetify();

describe('ChangePasswordCard (Q.DEFER.003 sub-B)', () => {
    const factory = () => mount(ChangePasswordCard, { global: { plugins: [vuetify] } });

    it('renders heading «Пароль»', () => {
        const wrapper = factory();
        expect(wrapper.find('h3').text()).toBe('Пароль');
    });

    it('shows last-change hint text', () => {
        const wrapper = factory();
        expect(wrapper.text()).toContain('Последняя смена: 12.04.2026');
        expect(wrapper.text()).toContain('26 дней назад');
    });

    it('renders «Сменить пароль» button with lock-reset icon', () => {
        const wrapper = factory();
        const btn = wrapper.find('button');
        expect(btn.exists()).toBe(true);
        expect(btn.text()).toContain('Сменить пароль');
        // prepend-icon mdi-lock-reset → check Vuetify renders i.mdi-lock-reset
        expect(wrapper.find('.mdi-lock-reset').exists()).toBe(true);
    });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `cd app && npx vitest run tests/Frontend/ChangePasswordCard.spec.ts --reporter=verbose`
Expected: 3 PASS (compoent already exists — spec is coverage-only, no implementation change).

If FAIL — investigate (icon class might differ — check Vuetify rendering).

- [ ] **Step 3: Run full Vitest для regression check**

Run: `cd app && npx vitest run 2>&1 | tail -10`
Expected: 86 files (85 → +1) / 663 passed (660 → +3) / 3 skipped / 0 failed.

- [ ] **Step 4: Commit**

```bash
git add app/tests/Frontend/ChangePasswordCard.spec.ts
git commit -m "test(security): Q.DEFER.003 sub-B — ChangePasswordCard 3 own-spec tests

Placeholder card (17 lines, static UI) — add minimal coverage for heading,
last-change hint, and button rendering. Closes coverage debt от 0% Stmts."
```

---

## Task 2: RecoveryCodesCard.spec.ts

**Files:**

- Create: `app/tests/Frontend/RecoveryCodesCard.spec.ts`

Component `app/resources/js/components/settings/security/RecoveryCodesCard.vue:13-87` — auth-gated card: visible только при `auth.user?.totp_enabled === true`; имеет dialog flow regenerate с password input + API call + display new 8 codes.

- [ ] **Step 1: Write failing test**

```typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import { createVuetify } from 'vuetify';

vi.mock('../../resources/js/api/auth', () => ({
    twoFactorRegenerateRecoveryCodes: vi.fn(),
}));

import RecoveryCodesCard from '../../resources/js/components/settings/security/RecoveryCodesCard.vue';
import * as authApi from '../../resources/js/api/auth';
import { useAuthStore } from '../../resources/js/stores/auth';

const vuetify = createVuetify();

describe('RecoveryCodesCard (Q.DEFER.003 sub-B)', () => {
    beforeEach(() => {
        setActivePinia(createPinia());
        vi.clearAllMocks();
    });

    const factory = (totpEnabled: boolean) => {
        const wrapper = mount(RecoveryCodesCard, {
            global: { plugins: [createPinia(), vuetify], stubs: { VDialog: true } },
        });
        // Set auth state after mount
        const auth = useAuthStore();
        auth.user = {
            id: 1,
            email: 'test@demo.local',
            full_name: 'Test User',
            totp_enabled: totpEnabled,
            timezone: 'UTC',
        } as unknown as ReturnType<typeof useAuthStore>['user'];
        return wrapper;
    };

    it('does NOT render button when 2FA disabled (auth.user.totp_enabled=false)', () => {
        const wrapper = factory(false);
        expect(wrapper.find('[data-testid="regen-codes-btn"]').exists()).toBe(false);
    });

    it('renders button when 2FA enabled', async () => {
        const wrapper = factory(true);
        await flushPromises();
        const btn = wrapper.find('[data-testid="regen-codes-btn"]');
        expect(btn.exists()).toBe(true);
        expect(btn.text()).toContain('Перегенерировать резервные коды');
    });

    it('clicking regen-codes-btn opens dialog (sets regenOpen)', async () => {
        const wrapper = factory(true);
        await flushPromises();
        await wrapper.find('[data-testid="regen-codes-btn"]').trigger('click');
        // VDialog is stubbed — check internal state via component instance
        expect((wrapper.vm as unknown as { regenOpen: boolean }).regenOpen).toBe(true);
    });

    it('confirmRegen() with valid password fetches 8 codes and clears password', async () => {
        const newCodes = ['AAAA-1111', 'BBBB-2222', 'CCCC-3333', 'DDDD-4444', 'EEEE-5555', 'FFFF-6666', 'GGGG-7777', 'HHHH-8888'];
        (authApi.twoFactorRegenerateRecoveryCodes as ReturnType<typeof vi.fn>).mockResolvedValue({ recovery_codes: newCodes });
        const wrapper = factory(true);
        await flushPromises();
        const vm = wrapper.vm as unknown as {
            regenOpen: boolean;
            regenPassword: string;
            regenCodes: string[];
            regenError: string;
            confirmRegen: () => Promise<void>;
        };
        vm.regenOpen = true;
        vm.regenPassword = 'correctPass';
        await vm.confirmRegen();
        await flushPromises();
        expect(vm.regenCodes).toEqual(newCodes);
        expect(vm.regenPassword).toBe('');
        expect(vm.regenError).toBe('');
        expect(authApi.twoFactorRegenerateRecoveryCodes).toHaveBeenCalledWith('correctPass');
    });

    it('confirmRegen() with invalid password sets error and keeps regenCodes empty', async () => {
        (authApi.twoFactorRegenerateRecoveryCodes as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('401'));
        const wrapper = factory(true);
        await flushPromises();
        const vm = wrapper.vm as unknown as {
            regenPassword: string;
            regenCodes: string[];
            regenError: string;
            confirmRegen: () => Promise<void>;
        };
        vm.regenPassword = 'wrongPass';
        await vm.confirmRegen();
        await flushPromises();
        expect(vm.regenError).toBe('Неверный пароль.');
        expect(vm.regenCodes).toEqual([]);
    });

    it('closeRegen() clears codes and closes dialog', async () => {
        const wrapper = factory(true);
        await flushPromises();
        const vm = wrapper.vm as unknown as {
            regenOpen: boolean;
            regenCodes: string[];
            closeRegen: () => void;
        };
        vm.regenOpen = true;
        vm.regenCodes = ['AAAA-1111'];
        vm.closeRegen();
        expect(vm.regenOpen).toBe(false);
        expect(vm.regenCodes).toEqual([]);
    });
});
```

- [ ] **Step 2: Run test to verify it passes**

Run: `cd app && npx vitest run tests/Frontend/RecoveryCodesCard.spec.ts --reporter=verbose`
Expected: 6 PASS.

If a test fails — investigate. Possible issues:

- `VDialog` stub might not be необходим (Vuetify тёлепортит overlay в `document.body`); use stubs config from memory Plan 5 frontend learning.
- `auth.user` typing — может potребоваться `as any` cast.

- [ ] **Step 3: Full Vitest regression**

Run: `cd app && npx vitest run 2>&1 | tail -10`
Expected: 87 files / 669 passed / 3 skipped / 0 failed (663 + 6).

- [ ] **Step 4: Commit**

```bash
git add app/tests/Frontend/RecoveryCodesCard.spec.ts
git commit -m "test(security): Q.DEFER.003 sub-B — RecoveryCodesCard 6 own-spec tests

Coverage uplift от 28% to 70%+ (auth-gated visibility / dialog flow /
confirmRegen success+error / closeRegen reset). vi.mock authApi
для изоляции; VDialog stub'аем для DOM unit-test (избегаем teleport)."
```

---

## Task 3: TwoFactorCard.spec.ts

**Files:**

- Create: `app/tests/Frontend/TwoFactorCard.spec.ts`

Component `app/resources/js/components/settings/security/TwoFactorCard.vue:1-218` — самый сложный: 3-step setup wizard (`setupStep: 'init' | 'confirm' | 'codes'`) с API calls `twoFactorInit` + `twoFactorConfirm`; отдельный disable flow с password.

- [ ] **Step 1: Write failing test**

```typescript
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import { createVuetify } from 'vuetify';

vi.mock('../../resources/js/api/auth', () => ({
    twoFactorInit: vi.fn(),
    twoFactorConfirm: vi.fn(),
    twoFactorDisable: vi.fn(),
}));

import TwoFactorCard from '../../resources/js/components/settings/security/TwoFactorCard.vue';
import * as authApi from '../../resources/js/api/auth';
import { useAuthStore } from '../../resources/js/stores/auth';

const vuetify = createVuetify();

type CardVm = {
    setupOpen: boolean;
    setupStep: 'init' | 'confirm' | 'codes';
    setupSecret: string;
    setupQrUrl: string;
    setupCode: string;
    setupRecoveryCodes: string[];
    setupError: string;
    setupBusy: boolean;
    disableOpen: boolean;
    disablePassword: string;
    disableError: string;
    openSetup: () => Promise<void>;
    confirmSetup: () => Promise<void>;
    closeSetup: () => void;
    confirmDisable: () => Promise<void>;
};

describe('TwoFactorCard (Q.DEFER.003 sub-B)', () => {
    beforeEach(() => {
        setActivePinia(createPinia());
        vi.clearAllMocks();
    });

    const factory = (totpEnabled: boolean) => {
        const wrapper = mount(TwoFactorCard, {
            global: { plugins: [createPinia(), vuetify], stubs: { VDialog: true } },
        });
        const auth = useAuthStore();
        auth.user = {
            id: 1,
            email: 'test@demo.local',
            full_name: 'Test User',
            totp_enabled: totpEnabled,
            timezone: 'UTC',
        } as unknown as ReturnType<typeof useAuthStore>['user'];
        return wrapper;
    };

    it('shows «Включить 2FA» button when 2FA disabled', async () => {
        const wrapper = factory(false);
        await flushPromises();
        const btn = wrapper.find('[data-testid="enable-2fa-btn"]');
        expect(btn.exists()).toBe(true);
        expect(btn.text()).toContain('Включить 2FA');
    });

    it('shows «Отключить 2FA» button when 2FA enabled', async () => {
        const wrapper = factory(true);
        await flushPromises();
        const btn = wrapper.find('[data-testid="disable-2fa-btn"]');
        expect(btn.exists()).toBe(true);
        expect(btn.text()).toContain('Отключить 2FA');
    });

    it('shows «включена» chip when 2FA enabled', async () => {
        const wrapper = factory(true);
        await flushPromises();
        expect(wrapper.text()).toContain('включена');
    });

    it('openSetup() initializes wizard and transitions init→confirm with QR secret', async () => {
        (authApi.twoFactorInit as ReturnType<typeof vi.fn>).mockResolvedValue({
            secret: 'JBSWY3DPEHPK3PXP',
            qr_url: 'otpauth://totp/test',
        });
        const wrapper = factory(false);
        await flushPromises();
        const vm = wrapper.vm as unknown as CardVm;
        await vm.openSetup();
        await flushPromises();
        expect(vm.setupOpen).toBe(true);
        expect(vm.setupStep).toBe('confirm');
        expect(vm.setupSecret).toBe('JBSWY3DPEHPK3PXP');
        expect(vm.setupQrUrl).toBe('otpauth://totp/test');
        expect(authApi.twoFactorInit).toHaveBeenCalled();
    });

    it('confirmSetup() valid code → step=codes + 8 recovery codes + auth.totp_enabled=true', async () => {
        const codes = ['A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8'];
        (authApi.twoFactorConfirm as ReturnType<typeof vi.fn>).mockResolvedValue({ recovery_codes: codes });
        const wrapper = factory(false);
        await flushPromises();
        const vm = wrapper.vm as unknown as CardVm;
        vm.setupCode = '123456';
        await vm.confirmSetup();
        await flushPromises();
        expect(vm.setupStep).toBe('codes');
        expect(vm.setupRecoveryCodes).toEqual(codes);
        const auth = useAuthStore();
        expect(auth.user?.totp_enabled).toBe(true);
        expect(authApi.twoFactorConfirm).toHaveBeenCalledWith('123456');
    });

    it('confirmSetup() invalid code → setupError set, step stays confirm', async () => {
        (authApi.twoFactorConfirm as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('422'));
        const wrapper = factory(false);
        await flushPromises();
        const vm = wrapper.vm as unknown as CardVm;
        vm.setupStep = 'confirm';
        vm.setupCode = '999999';
        await vm.confirmSetup();
        await flushPromises();
        expect(vm.setupError).toBe('Неверный код. Проверьте время на устройстве.');
        expect(vm.setupStep).toBe('confirm');
    });

    it('closeSetup() resets all setup state', async () => {
        const wrapper = factory(false);
        await flushPromises();
        const vm = wrapper.vm as unknown as CardVm;
        vm.setupOpen = true;
        vm.setupSecret = 'SECRET';
        vm.setupQrUrl = 'url';
        vm.setupCode = '123';
        vm.closeSetup();
        expect(vm.setupOpen).toBe(false);
        expect(vm.setupSecret).toBe('');
        expect(vm.setupQrUrl).toBe('');
        expect(vm.setupCode).toBe('');
    });

    it('confirmDisable() valid password → auth.totp_enabled=false + dialog closes', async () => {
        (authApi.twoFactorDisable as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
        const wrapper = factory(true);
        await flushPromises();
        const vm = wrapper.vm as unknown as CardVm;
        vm.disableOpen = true;
        vm.disablePassword = 'correctPass';
        await vm.confirmDisable();
        await flushPromises();
        const auth = useAuthStore();
        expect(auth.user?.totp_enabled).toBe(false);
        expect(vm.disableOpen).toBe(false);
        expect(vm.disablePassword).toBe('');
        expect(authApi.twoFactorDisable).toHaveBeenCalledWith('correctPass');
    });

    it('confirmDisable() invalid password → disableError set, totp stays enabled', async () => {
        (authApi.twoFactorDisable as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('401'));
        const wrapper = factory(true);
        await flushPromises();
        const vm = wrapper.vm as unknown as CardVm;
        vm.disablePassword = 'wrongPass';
        await vm.confirmDisable();
        await flushPromises();
        expect(vm.disableError).toBe('Неверный пароль.');
        const auth = useAuthStore();
        expect(auth.user?.totp_enabled).toBe(true);
    });
});
```

- [ ] **Step 2: Run test to verify**

Run: `cd app && npx vitest run tests/Frontend/TwoFactorCard.spec.ts --reporter=verbose`
Expected: 9 PASS.

- [ ] **Step 3: Full Vitest regression**

Run: `cd app && npx vitest run 2>&1 | tail -10`
Expected: 88 files / 678 passed / 3 skipped / 0 failed (669 + 9).

- [ ] **Step 4: Commit**

```bash
git add app/tests/Frontend/TwoFactorCard.spec.ts
git commit -m "test(security): Q.DEFER.003 sub-B — TwoFactorCard 9 own-spec tests

Coverage uplift от 28% to 80%+: enable button visibility / disable button
visibility / chip status / setup wizard openSetup→confirm→codes / invalid
code error / disable flow valid+invalid password / closeSetup state reset.
vi.mock authApi для 3 endpoint'ов (init/confirm/disable)."
```

---

## Task 4: router.spec.ts integration tests (sub-C)

**Files:**

- Modify: `app/tests/Frontend/router.spec.ts:62` — append 5 integration tests.

Existing 5 tests verify route definition. Add tests на guard branches (lines 283-302 of router/index.ts):

- `to.meta.requiresAuth && !auth.isAuthenticated` → redirect /login (existing)
- `to.meta.guestOnly && auth.isAuthenticated` → redirect /dashboard (NEW)
- requiresAuth + authenticated → passes (NEW)
- catch-all `/:pathMatch(.*)*` → 404 (NEW)
- /admin → redirect /admin/tenants (NEW)
- /reset/:token → resolves with token param (NEW)

- [ ] **Step 1: Augment router.spec.ts**

Edit `app/tests/Frontend/router.spec.ts` — изменить existing `vi.mock` для `me()` (теперь conditional rejection), append 5 tests after existing «гость, идущий на /dashboard» (line 61).

```typescript
// Existing vi.mock — refactor to support both resolved and rejected mock per test
let meResolves = false;
let meResolvedUser: { id: number; email: string; full_name: string; totp_enabled: boolean; timezone: string } | null = null;

vi.mock('../../resources/js/api/auth', () => ({
    me: vi.fn(() => {
        if (meResolves && meResolvedUser) {
            return Promise.resolve({ user: meResolvedUser });
        }
        return Promise.reject(new Error('not authenticated'));
    }),
    login: vi.fn(),
    register: vi.fn(),
    logout: vi.fn(),
}));
```

И добавить tests после line 61:

```typescript
    it('authenticated user attempting /login (guestOnly) → redirected to /dashboard', async () => {
        meResolves = true;
        meResolvedUser = { id: 1, email: 'test@demo.local', full_name: 'Test', totp_enabled: false, timezone: 'UTC' };
        // Force re-init authInitialized — workaround: reset by calling fetchMe through guard
        // (router module-level authInitialized flag persists between tests; new router instance
        // requires module re-import. Use direct push instead.)
        await router.push('/login');
        await router.isReady();
        expect(router.currentRoute.value.path).toBe('/dashboard');
    });

    it('authenticated user navigating to /dashboard passes requiresAuth guard', async () => {
        meResolves = true;
        meResolvedUser = { id: 1, email: 'test@demo.local', full_name: 'Test', totp_enabled: false, timezone: 'UTC' };
        await router.push('/dashboard');
        await router.isReady();
        expect(router.currentRoute.value.path).toBe('/dashboard');
        expect(router.currentRoute.value.name).toBe('dashboard');
    });

    it('unknown path /no-such-path resolves to 404 ErrorView', async () => {
        await router.push('/no-such-path-xyz');
        await router.isReady();
        expect(router.currentRoute.value.name).toBe('not-found');
        expect(router.currentRoute.value.meta.errorCode).toBe('404');
        expect(router.currentRoute.value.meta.layout).toBe('error');
    });

    it('/admin redirects to /admin/tenants', async () => {
        meResolves = true;
        meResolvedUser = { id: 1, email: 'test@demo.local', full_name: 'Test', totp_enabled: false, timezone: 'UTC' };
        await router.push('/admin');
        await router.isReady();
        expect(router.currentRoute.value.path).toBe('/admin/tenants');
        expect(router.currentRoute.value.name).toBe('admin-tenants');
    });

    it('/reset/:token resolves with token param exposed', async () => {
        await router.push('/reset/abc123-token-xyz');
        await router.isReady();
        expect(router.currentRoute.value.name).toBe('reset-password');
        expect(router.currentRoute.value.params.token).toBe('abc123-token-xyz');
    });
```

**IMPORTANT NOTE:** `authInitialized` flag — module-level in router/index.ts:281; once `fetchMe()` ran on first router.push, it's `true` для всех subsequent tests. This means subsequent tests' auth state must be modified via direct `auth.user = {...}` mutation, NOT через meResolves flag (which only fires first time). Refactor test setup if needed: import `useAuthStore` and set state directly in beforeEach for authenticated-user tests.

Adjusted approach (more robust):

```typescript
import { useAuthStore } from '../../resources/js/stores/auth';

beforeEach(() => {
    setActivePinia(createPinia());
    // Note: authInitialized module-level flag persists; first test will trigger fetchMe.
    // For tests requiring authenticated state, set auth.user directly after setActivePinia.
});

// In authenticated tests:
const auth = useAuthStore();
auth.user = { id: 1, email: 'test@demo.local', ... } as unknown as ReturnType<typeof useAuthStore>['user'];
```

This bypasses the fetchMe path. May need to combine с meResolves для first test (which still triggers fetchMe).

- [ ] **Step 2: Run target spec**

Run: `cd app && npx vitest run tests/Frontend/router.spec.ts --reporter=verbose`
Expected: 10 PASS (5 existing + 5 new). Если fail на «authenticated user → /dashboard» — investigate authInitialized flag persistence.

- [ ] **Step 3: Full Vitest regression**

Run: `cd app && npx vitest run 2>&1 | tail -10`
Expected: 88 files / 683 passed / 3 skipped / 0 failed (678 + 5).

- [ ] **Step 4: Commit**

```bash
git add app/tests/Frontend/router.spec.ts
git commit -m "test(router): Q.DEFER.003 sub-C — 5 integration tests for guard branches

Coverage uplift router/index.ts от 33% Stmts / 7% Funcs к ~85% Funcs:
- authenticated /login (guestOnly) → /dashboard redirect
- authenticated /dashboard passes requiresAuth
- /no-such-path → 404 catch-all
- /admin → /admin/tenants redirect
- /reset/:token param exposure

Refactored vi.mock me() для conditional resolve/reject per test."
```

---

## Task 5: Coverage verification (optional sanity check)

**Files:** read-only. Output — Vitest --coverage report.

- [ ] **Step 1: Run Vitest --coverage**

```bash
cd app && npx vitest run --coverage 2>&1 | tail -40
```

Expected: coverage report showing:

- `ChangePasswordCard.vue`: 100% (single button + h3)
- `RecoveryCodesCard.vue`: 70%+ (от 28%)
- `TwoFactorCard.vue`: 80%+ (от 28%)
- `router/index.ts` Funcs: 80%+ (от 7%)

Если coverage не уходит вверх — investigate test files правильно exercise functions.

- [ ] **Step 2: Document in plan**

Append actual coverage delta numbers в этот файл (или в blocked.md если open Q-item) для closure status.

- [ ] **Step 3: NO COMMIT** — pure verification.

---

## Task 6: Memory + push (closure)

**Files:**

- Modify: `memory/MEMORY.md`
- Modify: `memory/project_full_audit_2026-05-12.md` (Q.DEFER.003 sub-B+C closure)

- [ ] **Step 1: Update MEMORY.md project_state description**

Replace `Vitest 85 files / 660/3 skipped` line с new count после Tasks 1-4 (88 files / 683 passed / 3 skipped).

- [ ] **Step 2: Update Q.DEFER.003 status в project_full_audit memory**

Add closure line после existing Q.DEFER.003 mention: "✅ sub-B+C CLOSED 13.05.2026 day — security cards (Change/Recovery/TwoFactor) + router integration tests added."

- [ ] **Step 3: Update blocked.md Q.DEFER.003 section (audit file)**

Modify `docs/superpowers/audits/2026-05-12-portal-full-audit-blocked.md` — sub-B status «Sub-B (security cards 28%) ... remain DEFERRED» → «✅ sub-B+C CLOSED 13.05.2026 day (commits TBD)».

- [ ] **Step 4: Pre-push hooks + push**

```bash
git add docs/superpowers/audits/2026-05-12-portal-full-audit-blocked.md
git commit -m "docs(audit): Q.DEFER.003 sub-B+C CLOSED 13.05.2026 — security cards + router guards"
git push origin plan5-frontend-projects
```

Expected hooks: gitleaks-full-history 0 leaks; lychee 0 errors; pint/larastan skip (no PHP changes); eslint-vue skip (only .spec.ts staged — check if eslint runs on test files); pest skip (no PHP changes).

---

## Self-Review Checklist

- [ ] Spec coverage: 3 security cards + router — все 4 task'а закрыты?
- [ ] No regression: Vitest baseline preserved (660 + 23 new = 683).
- [ ] No production code changed — pure test additions + memory + docs.
- [ ] vue-tsc clean (no new type errors).
- [ ] ESLint clean (test files conform to existing patterns).
- [ ] Commits атомарные: 1 task = 1 commit (4 spec commits + 1 docs commit).

---

## Известные риски

1. **Pinia store reactivity в Vitest:** v-mock auth-store через direct `auth.user = {...}` assignment может не trigger reactive watchers. Workaround: use `setActivePinia(createPinia())` в `beforeEach`, mount component AFTER setting auth state. Если test fails с «button не reactive» — switch to mounting AFTER auth state set.

2. **`authInitialized` module-level flag** в router/index.ts:281 — persistent между tests в same module. Может cause test order dependence. Workaround: для tests требующих re-init, либо direct `auth.user = ...` (bypass fetchMe), либо принять что first test fires fetchMe и last 4 don't.

3. **VDialog stub** (per memory Plan 5 frontend learning) — `stubs: { VDialog: true }` нужен чтобы dialog content рендерился inline без teleport. Если test fails on `wrapper.find('[data-testid="regen-dialog"]')` — добавить stub.

4. **Vuetify плагины в test setup** — autoImport может потребоваться. Existing AdminSupplierPricesView.spec.ts line 13 использует `createVuetify()` без auto-import — verify это работает для VBtn/VCard/VDialog/VTextField/VSwitch.

5. **TwoFactorCard `auth.user = { ...auth.user, totp_enabled: true }`** (lines 56, 80) — Pinia state mutation pattern. Tests должны verify через `useAuthStore().user?.totp_enabled` после async confirmSetup/confirmDisable.

---

## Estimated effort

- Task 1 (ChangePasswordCard): 15 мин (placeholder, тривиальный)
- Task 2 (RecoveryCodesCard): 30 мин (dialog flow + auth-gated)
- Task 3 (TwoFactorCard): 45 мин (самый сложный — 3-step wizard + disable)
- Task 4 (router integration): 30 мин (vi.mock refactor + 5 tests)
- Task 5 (coverage verify): 10 мин
- Task 6 (memory + push): 10 мин

**Total: ~2 часа 20 минут.**
