chore: Task 11 dead code cleanup and capability validation unification

This commit is contained in:
Ochenstarik 2026-08-21 19:49:16 +07:00
parent 1883a86402
commit 906148813e
15 changed files with 143 additions and 125 deletions

View file

@ -0,0 +1,71 @@
# Task 11: Удаление мёртвого кода Report
**Date:** 2026-08-21
**Base Commit SHA:** `1883a864023b0b10fe6674a035fbcffa7461c3c8`
**Status:** COMPLETED / PASS
**Assigned Agent:** Antigravity (Implementation Orchestrator)
---
## 1. Executive Summary
Проведена чистка мертвого и дублирующего кода, устранен технический долг без изменения боевых инвариантов:
1. **Удален неиспользуемый импорт:**
- `generateCryptoSecureSeed` удален из `src/app/api/giveaways/[id]/draw/route.ts`.
2. **Ликвидация дублирующей фабрики `ProviderRegistry` (`src/providers/registry.ts`):**
- Файл `src/providers/registry.ts` удален.
- Метод `getProvider(platform)` перенесен в каноническую фабрику `ProviderFactory` (`src/providers/factory.ts`), реализующую строгий fail-closed контроль наличия `VK_SERVICE_TOKEN` в production.
- Все тестовые вызовы (`tests/concurrency.test.ts`, `tests/payload-summary-regression.test.ts`, `tests/winner-count-contract.test.ts`, `tests/security.test.ts`) и документация `docs/ARCHITECTURE.md` переведены на `ProviderFactory`.
3. **Объединение валидации возможностей провайдера (`validateProviderCapabilities`):**
- Проверка `requireSubscription` перенесена в канонический валидатор `validateProviderCapabilities` (`src/core/validation/giveaway-schemas.ts`).
- Дублирующий модуль `src/core/filtering/rule-validation.ts` удален.
- Тестовый набор `tests/provider-capabilities.test.ts` перенастроен на тестирование `validateProviderCapabilities` (все 11 тестов успешны).
4. **Удаление `GiveawayStore.listAll` (`src/lib/giveaway-store.ts`):**
- Неиспользуемый метод `listAll` удален из класса `GiveawayStore`.
5. **Упрощение `getOAuthClient()` (`src/integrations/vk/vk-oauth-client.ts`):**
- Удалены избыточные тождественные ветви `if/else`, возвращавшие одну и ту же переменную `defaultVkOAuthClient`.
6. **Сохранение статусов FSM (`DRAFT`, `FETCHING`, `PUBLISHED`, `CANCELLED`):**
- Статусы сохранены в `GiveawayStatusType` и таблице переходов FSM согласно контракту (являются заделом под фичи публикации итогов в группу VK и отмены конкурса).
---
## 2. Modified & Deleted Files
| File | Status | Description |
|------|--------|-------------|
| `src/core/filtering/rule-validation.ts` | DELETED | Удален дублирующий валидатор правил. |
| `src/providers/registry.ts` | DELETED | Удалена неконсистентная фабрика `ProviderRegistry`. |
| `src/app/api/giveaways/[id]/draw/route.ts` | MODIFIED | Удален неиспользуемый импорт `generateCryptoSecureSeed`. |
| `src/providers/factory.ts` | MODIFIED | Добавлен метод `getProvider(platform: PlatformType)` с fail-fast проверкой неподдерживаемых платформ. |
| `src/core/validation/giveaway-schemas.ts` | MODIFIED | В `validateProviderCapabilities` добавлена проверка `requireSubscription`. |
| `src/lib/giveaway-store.ts` | MODIFIED | Удален неиспользуемый метод `listAll`. |
| `src/integrations/vk/vk-oauth-client.ts` | MODIFIED | Упрощена функция `getOAuthClient()`. |
| `tests/provider-capabilities.test.ts` | MODIFIED | Переведен на `validateProviderCapabilities` и `ProviderFactory`. |
| `tests/concurrency.test.ts` | MODIFIED | Удален импорт и вызовы `ProviderRegistry`. |
| `tests/payload-summary-regression.test.ts` | MODIFIED | Удален импорт и вызовы `ProviderRegistry`. |
| `tests/winner-count-contract.test.ts` | MODIFIED | Удален импорт и вызовы `ProviderRegistry`. |
| `tests/security.test.ts` | MODIFIED | Переведен на `ProviderFactory`. |
| `docs/ARCHITECTURE.md` | MODIFIED | Обновлена ссылка на `ProviderFactory`. |
---
## 3. Предложение следующей задачи (FSM Statuses Lifecycle)
Статусы `DRAFT`, `FETCHING`, `PUBLISHED`, `CANCELLED` сохранены в `src/core/fsm/giveaway-fsm.ts`.
Рекомендуется оформить отдельную задачу на реализацию недостающих пользовательских сценариев:
1. `POST /api/giveaways/[id]/cancel` — отмена активного розыгрыша организатором с фиксацией аудиторного события (`status: 'CANCELLED'`).
2. `POST /api/giveaways/[id]/publish` — автоматическая публикация карточки с итогами и победителями на стену сообщества через VK API (`status: 'PUBLISHED'`).
---
## 4. Verification Evidence
```text
npx prisma generate -> EXIT 0
npx tsc --noEmit -> EXIT 0
npm test -> EXIT 0 (57 suites, 333 tests passed)
npm run lint -> EXIT 0 (0 errors, 6 warnings on no-img-element)
npm run build -> EXIT 0 (All 17 routes compiled successfully)
npm audit --omit=dev -> EXIT 0 (0 vulnerabilities)
```

View file

@ -0,0 +1,26 @@
# Task 11: Удаление мёртвого кода
**Assigned to:** Antigravity (Implementation Orchestrator)
**Priority:** LOW (tech debt cleanup)
**Date:** 2026-08-21
**Base SHA:** `1883a864023b0b10fe6674a035fbcffa7461c3c8`
## Scope
1. **Unused import in `src/app/api/giveaways/[id]/draw/route.ts`**:
- Remove unused import `generateCryptoSecureSeed`.
2. **`ProviderRegistry` (`src/providers/registry.ts`)**:
- Delete dead `src/providers/registry.ts` which duplicates `ProviderFactory` with inconsistent token lengths and non-fail-fast behavior.
3. **`rule-validation.ts` (`src/core/filtering/rule-validation.ts`) vs `validateProviderCapabilities`**:
- Inspect `requireSubscription` check in `src/core/filtering/rule-validation.ts`.
- Transfer required capability checks (including `requireSubscription` checking `supportsSubscriptions`) into canonical `validateProviderCapabilities` in `src/core/validation/giveaway-schemas.ts`.
- Remove redundant `src/core/filtering/rule-validation.ts`.
- Update `tests/provider-capabilities.test.ts` to test canonical `validateProviderCapabilities`.
4. **`GiveawayStore.listAll` (`src/lib/giveaway-store.ts`)**:
- Remove unused `listAll` method.
5. **`getOAuthClient()` in `src/integrations/vk/vk-oauth-client.ts`**:
- Clean up redundant branches in `getOAuthClient()`.
6. **FSM unreachable statuses (`DRAFT`, `FETCHING`, `PUBLISHED`, `CANCELLED`)**:
- Per instructions, **DO NOT delete** from `GiveawayStatusType` or FSM transitions. Document them as reserved for future publication & cancellation features.
7. Verification gate:
- `npm test`, `npx prisma generate`, `npx tsc --noEmit`, `npm run lint`, `npm run build`, `npm audit --omit=dev`.
- Save report to `agents/antigravity/done/TASK-2026-08-21-11-dead-code-cleanup.md`.

View file

@ -33,7 +33,7 @@ graph TD
- `checkSubscription(userIds: string[], groupId: string)`: Проверка подписки на сообщество.
- **`VkProvider`**: Боевой клиент к VK API с поддержкой пакетных запросов `execute`.
- **`VkMockProvider`**: Тестовый провайдер для демонстрации, локальной разработки и оффлайн-тестирования.
- **`ProviderRegistry`**: Фабрика для получения провайдера по типу платформы (`vk`, `telegram`, `youtube`).
- **`ProviderFactory`**: Фабрика для получения провайдера по типу платформы (`VK`, `TELEGRAM`, `YOUTUBE`).
### 2.3. Data & Persistence Layer (`prisma/` + `src/lib/`)
- **PostgreSQL** в качестве надежного реляционного хранилища.

View file

@ -1,6 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { GiveawayStore } from '@/lib/giveaway-store';
import { generateCryptoSecureSeed } from '@/core/randomizer/hasher';
import { executeDeterministicDrawV1 } from '@/core/randomizer/deterministic';
import { executeDrawSchema } from '@/core/validation/giveaway-schemas';
import {

View file

@ -1,43 +0,0 @@
import { FilterRules } from '../types/giveaway';
import { ProviderCapabilities } from '../../providers/types';
export interface RuleValidationResult {
valid: boolean;
errors: string[];
}
/**
* Validates that the requested filter rules can actually be verified by the
* selected social-media provider. This prevents organizers from configuring
* giveaways with conditions (e.g. reposts) that the provider cannot check.
*/
export function validateFilterRulesAgainstProviderCapabilities(
rules: FilterRules,
capabilities: ProviderCapabilities
): RuleValidationResult {
const errors: string[] = [];
if (rules.requireRepost && !capabilities.reposts) {
errors.push(
`requireRepost is not supported by the ${capabilities.repostsNote || 'current provider'}`
);
}
if (rules.requireSubscription && !capabilities.subscriptions) {
errors.push('requireSubscription is not supported by the current provider');
}
if (rules.excludeAdmins && !capabilities.adminDetection) {
errors.push(
`excludeAdmins is not supported by the ${capabilities.adminDetectionNote || 'current provider'}`
);
}
// likes/comments are considered universally supported by providers that have
// them declared; the engine itself still needs a provider to fetch them.
return {
valid: errors.length === 0,
errors,
};
}

View file

@ -80,6 +80,13 @@ export function validateProviderCapabilities(
);
}
if (rules.requireSubscription && !capabilities.subscriptions) {
throw new ValidationError(
'Subscription verification is not supported by the current provider',
{ condition: 'requireSubscription' }
);
}
if (rules.excludeAdmins && !capabilities.adminDetection) {
throw new ValidationError(
'Admin detection requires VK ID organizer authorization',

View file

@ -271,8 +271,5 @@ export function setOAuthClient(client: IVkOAuthClient): void {
}
export function getOAuthClient(): IVkOAuthClient {
if (process.env.USE_VK_MOCK === 'true' || (process.env.NODE_ENV === 'test' && !process.env.VK_APP_ID)) {
return defaultVkOAuthClient;
}
return defaultVkOAuthClient;
}

View file

@ -44,10 +44,6 @@ export class GiveawayStore {
return await activeRepository.getGiveawayById(id);
}
static async listAll(organizerId?: string): Promise<StoredGiveaway[]> {
return await activeRepository.listGiveaways(organizerId);
}
static async listSummaries(organizerId?: string): Promise<GiveawaySummary[]> {
return await activeRepository.listGiveawaysSummary(organizerId);
}

View file

@ -1,7 +1,8 @@
import { PlatformType } from '../core/types/giveaway';
import { SocialMediaProvider } from './types';
import { VkProvider } from './vk/vk-provider';
import { VkMockProvider } from './vk/vk-mock-provider';
import { DependencyUnavailableError } from '../core/errors/http-errors';
import { DependencyUnavailableError, ValidationError } from '../core/errors/http-errors';
export class ProviderFactory {
public static getVkProvider(): SocialMediaProvider {
@ -26,4 +27,16 @@ export class ProviderFactory {
'VK provider credentials are not configured. Configure VK_SERVICE_TOKEN or set USE_VK_MOCK=true for staging/test.'
);
}
public static getProvider(platform: PlatformType): SocialMediaProvider {
switch (platform) {
case 'VK':
return this.getVkProvider();
case 'TELEGRAM':
case 'YOUTUBE':
throw new ValidationError(`Platform "${platform}" is not currently supported`);
default:
throw new ValidationError(`Unknown platform: ${platform}`);
}
}
}

View file

@ -1,41 +0,0 @@
import { PlatformType } from '../core/types/giveaway';
import { SocialMediaProvider } from './types';
import { VkMockProvider } from './vk/vk-mock-provider';
import { VkProvider } from './vk/vk-provider';
export class ProviderRegistry {
private static providers: Map<PlatformType, SocialMediaProvider> = new Map();
static {
// Determine whether to use real VK provider or mock provider
const useRealVk = Boolean(process.env.VK_SERVICE_TOKEN && process.env.VK_SERVICE_TOKEN.trim().length > 10);
const vkProvider = useRealVk ? new VkProvider() : new VkMockProvider();
this.providers.set('VK', vkProvider);
}
/**
* Register or override a provider for a platform
*/
static registerProvider(platform: PlatformType, provider: SocialMediaProvider): void {
this.providers.set(platform, provider);
}
/**
* Get provider by platform type
*/
static getProvider(platform: PlatformType): SocialMediaProvider {
const provider = this.providers.get(platform);
if (!provider) {
throw new Error(`Provider for platform "${platform}" is not registered`);
}
return provider;
}
/**
* Force set mock provider for testing purposes
*/
static useMockVk(): void {
this.providers.set('VK', new VkMockProvider());
}
}

View file

@ -2,7 +2,6 @@ import { describe, it, expect, beforeEach } from 'vitest';
import { NextRequest } from 'next/server';
import { GiveawayStore } from '../src/lib/giveaway-store';
import { MemoryGiveawayRepository } from '../src/lib/repository/memory-repository';
import { ProviderRegistry } from '../src/providers/registry';
import { POST as drawPost } from '../src/app/api/giveaways/[id]/draw/route';
import { POST as participantsPost } from '../src/app/api/giveaways/[id]/participants/route';
import { POST as snapshotPost } from '../src/app/api/giveaways/[id]/snapshot/route';
@ -55,7 +54,6 @@ async function createReadyGiveaway() {
describe('Concurrency analysis', () => {
beforeEach(async () => {
GiveawayStore.setRepository(new MemoryGiveawayRepository());
ProviderRegistry.useMockVk();
defaultSessionStore.clear();
sessionId = await defaultSessionStore.createSession(testUser);
});

View file

@ -4,7 +4,6 @@ import { GiveawayStore } from '../src/lib/giveaway-store';
import { MemoryGiveawayRepository } from '../src/lib/repository/memory-repository';
import { POST as participantsPost } from '../src/app/api/giveaways/[id]/participants/route';
import { DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
import { ProviderRegistry } from '../src/providers/registry';
import { defaultSessionStore, SESSION_COOKIE_NAME } from '../src/lib/auth/session';
describe('POST /participants Payload Summary Regression Test', () => {
@ -13,7 +12,6 @@ describe('POST /participants Payload Summary Regression Test', () => {
beforeEach(async () => {
GiveawayStore.setRepository(new MemoryGiveawayRepository());
ProviderRegistry.useMockVk();
defaultSessionStore.clear();
sessionId = await defaultSessionStore.createSession(user);
});

View file

@ -1,14 +1,14 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { validateFilterRulesAgainstProviderCapabilities } from '../src/core/filtering/rule-validation';
import { validateProviderCapabilities } from '../src/core/validation/giveaway-schemas';
import { VkMockProvider } from '../src/providers/vk/vk-mock-provider';
import { VkProvider } from '../src/providers/vk/vk-provider';
import { ProviderRegistry } from '../src/providers/registry';
import { FilterRules } from '../src/core/types/giveaway';
import { NextRequest } from 'next/server';
import { POST as participantsPost } from '../src/app/api/giveaways/[id]/participants/route';
import { GiveawayStore } from '../src/lib/giveaway-store';
import { MemoryGiveawayRepository } from '../src/lib/repository/memory-repository';
import { defaultSessionStore, SESSION_COOKIE_NAME } from '../src/lib/auth/session';
import { ValidationError } from '../src/core/errors/http-errors';
const testUser = { id: 'usr_capabilities_tester', vkUserId: '77777' };
let sessionId: string;
@ -34,7 +34,6 @@ async function createGiveaway(store: typeof GiveawayStore) {
requireSubscription: false,
excludeAdmins: false,
excludeBlacklistedIds: [],
excludeDuplicateComments: true,
},
winnersCount: 1,
reserveWinnersCount: 0,
@ -56,7 +55,6 @@ function buildReq(id: string, body: object): NextRequest {
describe('Provider capabilities', () => {
beforeEach(async () => {
GiveawayStore.setRepository(new MemoryGiveawayRepository());
ProviderRegistry.useMockVk();
defaultSessionStore.clear();
sessionId = await defaultSessionStore.createSession(testUser);
});
@ -95,12 +93,10 @@ describe('Provider capabilities', () => {
requireSubscription: false,
excludeAdmins: false,
excludeBlacklistedIds: [],
excludeDuplicateComments: true,
};
const provider = new VkMockProvider();
const result = validateFilterRulesAgainstProviderCapabilities(rules, provider.capabilities);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('requireRepost'))).toBe(true);
expect(() => validateProviderCapabilities(rules, provider.capabilities)).toThrow(ValidationError);
expect(() => validateProviderCapabilities(rules, provider.capabilities)).toThrow(/repost/i);
});
it('validation rejects excludeAdmins when provider cannot detect admins', () => {
@ -111,12 +107,24 @@ describe('Provider capabilities', () => {
requireSubscription: false,
excludeAdmins: true,
excludeBlacklistedIds: [],
excludeDuplicateComments: true,
};
const provider = new VkMockProvider();
const result = validateFilterRulesAgainstProviderCapabilities(rules, provider.capabilities);
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('excludeAdmins'))).toBe(true);
expect(() => validateProviderCapabilities(rules, provider.capabilities)).toThrow(ValidationError);
expect(() => validateProviderCapabilities(rules, provider.capabilities)).toThrow(/admin/i);
});
it('validation rejects requireSubscription when provider does not support subscriptions', () => {
const rules: FilterRules = {
requireLike: false,
requireComment: false,
requireRepost: false,
requireSubscription: true,
excludeAdmins: false,
excludeBlacklistedIds: [],
};
const capabilities = { ...new VkMockProvider().capabilities, subscriptions: false };
expect(() => validateProviderCapabilities(rules, capabilities)).toThrow(ValidationError);
expect(() => validateProviderCapabilities(rules, capabilities)).toThrow(/subscription/i);
});
it('validation accepts supported combinations', () => {
@ -128,12 +136,9 @@ describe('Provider capabilities', () => {
targetGroupId: '-100',
excludeAdmins: false,
excludeBlacklistedIds: [],
excludeDuplicateComments: true,
};
const provider = new VkMockProvider();
const result = validateFilterRulesAgainstProviderCapabilities(rules, provider.capabilities);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
expect(() => validateProviderCapabilities(rules, provider.capabilities)).not.toThrow();
});
it('participants route returns 400 when requireRepost is requested for VK', async () => {
@ -146,7 +151,6 @@ describe('Provider capabilities', () => {
requireSubscription: false,
excludeAdmins: false,
excludeBlacklistedIds: [],
excludeDuplicateComments: true,
},
});
@ -167,7 +171,6 @@ describe('Provider capabilities', () => {
requireSubscription: false,
excludeAdmins: true,
excludeBlacklistedIds: [],
excludeDuplicateComments: true,
},
});
@ -189,7 +192,6 @@ describe('Provider capabilities', () => {
targetGroupId: '-100',
excludeAdmins: false,
excludeBlacklistedIds: [],
excludeDuplicateComments: true,
},
});

View file

@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { ProviderRegistry } from '../src/providers/registry';
import { ProviderFactory } from '../src/providers/factory';
import { VkProvider } from '../src/providers/vk/vk-provider';
import { GiveawayStore } from '../src/lib/giveaway-store';
import { MemoryGiveawayRepository } from '../src/lib/repository/memory-repository';
@ -14,13 +14,11 @@ describe('Security: VK_SERVICE_TOKEN handling', () => {
beforeEach(() => {
GiveawayStore.setRepository(new MemoryGiveawayRepository());
ProviderRegistry.useMockVk();
});
it('ProviderRegistry does not expose VK_SERVICE_TOKEN in public API', () => {
it('ProviderFactory does not expose VK_SERVICE_TOKEN in public API', () => {
process.env.VK_SERVICE_TOKEN = secretToken;
// Re-initialize registry (static block already ran, but we can inspect provider)
const provider = ProviderRegistry.getProvider('VK');
const provider = ProviderFactory.getProvider('VK');
expect(provider.platform).toBe('VK');
expect(provider).not.toHaveProperty('serviceToken');
delete process.env.VK_SERVICE_TOKEN;
@ -63,7 +61,6 @@ describe('Security: VK_SERVICE_TOKEN handling', () => {
it('Post preview response does not contain VK_SERVICE_TOKEN', async () => {
process.env.VK_SERVICE_TOKEN = secretToken;
ProviderRegistry.useMockVk(); // mock so no real API call
const req = new NextRequest('http://localhost/api/posts/preview', {
method: 'POST',
body: JSON.stringify({ url: 'https://vk.com/wall-1_1' }),

View file

@ -2,7 +2,6 @@ import { describe, it, expect, beforeEach } from 'vitest';
import { NextRequest } from 'next/server';
import { GiveawayStore } from '../src/lib/giveaway-store';
import { MemoryGiveawayRepository } from '../src/lib/repository/memory-repository';
import { ProviderRegistry } from '../src/providers/registry';
import { POST as drawPost } from '../src/app/api/giveaways/[id]/draw/route';
import { DEFAULT_FILTER_RULES } from '../src/core/types/giveaway';
import { FilteredParticipant } from '../src/core/types/participant';
@ -15,7 +14,6 @@ describe('Winner Count Contract & Draw Retry Invariants', () => {
beforeEach(async () => {
GiveawayStore.setRepository(new MemoryGiveawayRepository());
ProviderRegistry.useMockVk();
defaultSessionStore.clear();
sessionId = await defaultSessionStore.createSession(testUser);
});