chrometools-mcp 3.3.8 → 3.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3101 @@
1
+ # Спецификация: Swagger/OpenAPI -> API-модели и API-клиент
2
+
3
+ ## Чеклист прогресса
4
+
5
+ ### Фаза 1 (текущая)
6
+ - [ ] **1.1** OpenAPI парсер (`utils/openapi/parser.js`) - единый для 2.0 и 3.x
7
+ - [ ] **1.2** $ref резолвер (`utils/openapi/ref-resolver.js`) - рекурсивное разрешение ссылок
8
+ - [ ] **1.3** Маппинг типов (`utils/openapi/type-mapper.js`) - OpenAPI -> TypeScript/Python типы
9
+ - [ ] **1.4** Тул `loadSwagger` - чтение и парсинг спецификации (URL или файл, JSON или YAML)
10
+ - [ ] **1.5** Генератор TypeScript моделей (`utils/api-generators/api-models-typescript.js`)
11
+ - [ ] **1.6** Генератор Python моделей (`utils/api-generators/api-models-python.js`)
12
+ - [ ] **1.7** Тул `generateApiModels` - генерация моделей из swagger
13
+ - [ ] **1.8** Схемы и описания тулов (`tool-schemas.js`, `tool-definitions.js`)
14
+ - [ ] **1.9** Хендлеры в `index.js`
15
+ - [ ] **1.10** Документация в `README.md`
16
+
17
+ ### Фаза 2 (следующая итерация)
18
+ - [ ] **2.1** Генератор методов из endpoints (`utils/openapi/method-generator.js`) — operationId → имя метода, path/query/body параметры
19
+ - [ ] **2.2** Генератор auth-конфигурации (`utils/openapi/auth-generator.js`) — constructor params, header setup для всех типов auth
20
+ - [ ] **2.3** Генератор API-клиента TypeScript (`utils/api-generators/api-client-typescript.js`) — Playwright APIRequestContext
21
+ - [ ] **2.4** Генератор API-клиента Python (`utils/api-generators/api-client-python.js`) — requests.Session
22
+ - [ ] **2.5** Тул `generateApiClient` — Zod schema, описание
23
+ - [ ] **2.6** Схема в `tool-schemas.js` (`GenerateApiClientSchema`)
24
+ - [ ] **2.7** Описание в `tool-definitions.js`
25
+ - [ ] **2.8** Хендлер в `index.js`
26
+ - [ ] **2.9** Документация в `README.md`
27
+ - [ ] **2.10** Верификация — Petstore 2.0/3.0, auth types, path params, query params, request body
28
+
29
+ ### Фаза 3 (будущее)
30
+ - [ ] **3.1** Генератор тестовых скаффолдов (`utils/openapi/test-scaffold-generator.js`) — CRUD detection, happy path + error cases
31
+ - [ ] **3.2** Генератор API-тестов TypeScript (`utils/api-generators/api-tests-typescript.js`) — Playwright test
32
+ - [ ] **3.3** Генератор API-тестов Python (`utils/api-generators/api-tests-python.js`) — pytest + requests
33
+ - [ ] **3.4** Тул `generateApiTests` — Zod schema, описание
34
+ - [ ] **3.5** Схема в `tool-schemas.js` (`GenerateApiTestsSchema`)
35
+ - [ ] **3.6** Описание в `tool-definitions.js`
36
+ - [ ] **3.7** Хендлер в `index.js`
37
+ - [ ] **3.8** Документация в `README.md`
38
+ - [ ] **3.9** Интеграция API-клиента с API-тестами (аналог POM-интеграции: опциональное использование клиента вместо raw HTTP)
39
+ - [ ] **3.10** Верификация — Petstore CRUD, auth тесты, error case scaffolds, grouping by tags
40
+
41
+ ---
42
+
43
+ ## Контекст
44
+
45
+ QA-автоматизаторы хотят быстро создавать API-тесты из Swagger/OpenAPI спецификации. Сейчас процесс ручной: открыть Swagger UI, скопировать эндпоинты, вручную написать типы и клиент.
46
+
47
+ **Цель**: MCP-тулы, которые читают Swagger-спецификацию и генерируют:
48
+ 1. Типизированные модели данных (TypeScript interfaces / Python dataclasses)
49
+ 2. API-клиент с методами для каждого endpoint (фаза 2)
50
+ 3. Тестовые скаффолды (фаза 3)
51
+
52
+ ---
53
+
54
+ ## Фаза 1: loadSwagger + generateApiModels
55
+
56
+ ### Тул 1: `loadSwagger`
57
+
58
+ **Назначение**: загрузить и распарсить OpenAPI-спецификацию, вернуть структурированную сводку.
59
+
60
+ **Параметры**:
61
+ ```js
62
+ LoadSwaggerSchema = z.object({
63
+ source: z.string().describe("URL (http/https) or local file path to swagger.json / openapi.yaml"),
64
+ format: z.enum(['auto', 'json', 'yaml']).optional()
65
+ .describe("Spec format. 'auto' (default) detects from extension/content"),
66
+ })
67
+ ```
68
+
69
+ **Алгоритм**:
70
+ 1. Определить тип источника (URL vs файл) по наличию `http://` / `https://`
71
+ 2. Загрузить контент:
72
+ - URL: HTTP GET через `fetch()` (Node 18+ built-in)
73
+ - Файл: `fs.readFileSync()`
74
+ 3. Определить формат (auto): попробовать `JSON.parse()`, если fail — `yaml.load()`
75
+ 4. Определить версию: `spec.swagger === '2.0'` → OpenAPI 2.0, `spec.openapi?.startsWith('3.')` → OpenAPI 3.x
76
+ 5. Нормализовать структуру (внутренне привести 2.0 к формату 3.x)
77
+ 6. Разрезолвить все `$ref` ссылки
78
+ 7. Вернуть сводку
79
+
80
+ **Возвращаемое значение**:
81
+ ```json
82
+ {
83
+ "success": true,
84
+ "version": "3.0.3",
85
+ "title": "Pet Store API",
86
+ "description": "A sample API",
87
+ "baseUrl": "https://petstore.swagger.io/v2",
88
+ "auth": [
89
+ { "name": "bearerAuth", "type": "http", "scheme": "bearer" },
90
+ { "name": "apiKey", "type": "apiKey", "in": "header", "paramName": "X-API-Key" }
91
+ ],
92
+ "endpoints": [
93
+ {
94
+ "method": "GET",
95
+ "path": "/pets",
96
+ "operationId": "listPets",
97
+ "summary": "List all pets",
98
+ "tags": ["pets"],
99
+ "parameters": [
100
+ { "name": "limit", "in": "query", "type": "integer", "required": false }
101
+ ],
102
+ "requestBody": null,
103
+ "responses": {
104
+ "200": { "description": "OK", "schema": "PetList" },
105
+ "400": { "description": "Bad Request", "schema": "Error" }
106
+ }
107
+ },
108
+ {
109
+ "method": "POST",
110
+ "path": "/pets",
111
+ "operationId": "createPet",
112
+ "summary": "Create a pet",
113
+ "tags": ["pets"],
114
+ "parameters": [],
115
+ "requestBody": { "schema": "CreatePetRequest", "required": true },
116
+ "responses": {
117
+ "201": { "description": "Created", "schema": "Pet" }
118
+ }
119
+ }
120
+ ],
121
+ "schemas": {
122
+ "Pet": {
123
+ "type": "object",
124
+ "required": ["id", "name"],
125
+ "properties": {
126
+ "id": { "type": "integer", "format": "int64" },
127
+ "name": { "type": "string" },
128
+ "status": { "type": "string", "enum": ["available", "pending", "sold"] },
129
+ "tags": { "type": "array", "items": { "$ref": "Tag" } }
130
+ }
131
+ },
132
+ "Tag": {
133
+ "type": "object",
134
+ "properties": {
135
+ "id": { "type": "integer" },
136
+ "name": { "type": "string" }
137
+ }
138
+ }
139
+ },
140
+ "endpointCount": 15,
141
+ "schemaCount": 8,
142
+ "instruction": "Use generateApiModels to generate typed models, or generateApiClient to generate API client class."
143
+ }
144
+ ```
145
+
146
+ ### Тул 2: `generateApiModels`
147
+
148
+ **Назначение**: сгенерировать типизированные модели данных из OpenAPI schemas.
149
+
150
+ **Параметры**:
151
+ ```js
152
+ GenerateApiModelsSchema = z.object({
153
+ source: z.string().describe("URL or file path to OpenAPI spec"),
154
+ language: z.enum(['typescript', 'python']).describe("Target language for models"),
155
+ format: z.enum(['auto', 'json', 'yaml']).optional()
156
+ .describe("Spec format (default: auto)"),
157
+ style: z.enum(['interface', 'type']).optional()
158
+ .describe("TypeScript only: 'interface' (default) or 'type' aliases"),
159
+ pythonStyle: z.enum(['dataclass', 'pydantic', 'typeddict']).optional()
160
+ .describe("Python only: 'dataclass' (default), 'pydantic' BaseModel, or TypedDict"),
161
+ includeEnums: z.boolean().optional()
162
+ .describe("Generate separate enum types (default: true)"),
163
+ includeValidation: z.boolean().optional()
164
+ .describe("Include validation constraints as comments/decorators (default: false)"),
165
+ schemas: z.array(z.string()).optional()
166
+ .describe("Generate only these schemas (default: all). E.g. ['User', 'Pet']"),
167
+ })
168
+ ```
169
+
170
+ **Алгоритм**:
171
+ 1. Загрузить и распарсить спеку (переиспользует парсер из loadSwagger)
172
+ 2. Отфильтровать schemas если указан параметр `schemas`
173
+ 3. Топологически отсортировать schemas по зависимостям ($ref), чтобы зависимости шли первыми
174
+ 4. Для каждой schema сгенерировать код модели
175
+ 5. Вернуть единый файл с кодом + suggested filename
176
+
177
+ **TypeScript выход** (interface):
178
+ ```typescript
179
+ // Generated from Pet Store API (https://petstore.swagger.io/v2)
180
+ // OpenAPI 3.0.3 | Generated at 2024-01-15T12:00:00Z
181
+
182
+ /** Pet status in the store */
183
+ export enum PetStatus {
184
+ Available = 'available',
185
+ Pending = 'pending',
186
+ Sold = 'sold',
187
+ }
188
+
189
+ export interface Tag {
190
+ id?: number;
191
+ name?: string;
192
+ }
193
+
194
+ export interface Pet {
195
+ /** Pet ID */
196
+ id: number;
197
+ /** Pet name */
198
+ name: string;
199
+ status?: PetStatus;
200
+ tags?: Tag[];
201
+ }
202
+
203
+ export interface CreatePetRequest {
204
+ name: string;
205
+ status?: PetStatus;
206
+ }
207
+
208
+ export interface Error {
209
+ code: number;
210
+ message: string;
211
+ }
212
+ ```
213
+
214
+ **Python выход** (dataclass):
215
+ ```python
216
+ """
217
+ Generated from Pet Store API (https://petstore.swagger.io/v2)
218
+ OpenAPI 3.0.3 | Generated at 2024-01-15T12:00:00Z
219
+ """
220
+
221
+ from __future__ import annotations
222
+ from dataclasses import dataclass, field
223
+ from enum import Enum
224
+ from typing import Optional, List
225
+
226
+
227
+ class PetStatus(str, Enum):
228
+ """Pet status in the store"""
229
+ AVAILABLE = 'available'
230
+ PENDING = 'pending'
231
+ SOLD = 'sold'
232
+
233
+
234
+ @dataclass
235
+ class Tag:
236
+ id: Optional[int] = None
237
+ name: Optional[str] = None
238
+
239
+
240
+ @dataclass
241
+ class Pet:
242
+ """Pet object"""
243
+ id: int = 0
244
+ name: str = ''
245
+ status: Optional[PetStatus] = None
246
+ tags: List[Tag] = field(default_factory=list)
247
+
248
+
249
+ @dataclass
250
+ class CreatePetRequest:
251
+ name: str = ''
252
+ status: Optional[PetStatus] = None
253
+
254
+
255
+ @dataclass
256
+ class Error:
257
+ code: int = 0
258
+ message: str = ''
259
+ ```
260
+
261
+ **Python выход** (pydantic):
262
+ ```python
263
+ from pydantic import BaseModel, Field
264
+ from enum import Enum
265
+ from typing import Optional, List
266
+
267
+
268
+ class PetStatus(str, Enum):
269
+ AVAILABLE = 'available'
270
+ PENDING = 'pending'
271
+ SOLD = 'sold'
272
+
273
+
274
+ class Tag(BaseModel):
275
+ id: Optional[int] = None
276
+ name: Optional[str] = None
277
+
278
+
279
+ class Pet(BaseModel):
280
+ id: int
281
+ name: str
282
+ status: Optional[PetStatus] = None
283
+ tags: List[Tag] = Field(default_factory=list)
284
+ ```
285
+
286
+ **Возвращаемое значение**:
287
+ ```json
288
+ {
289
+ "action": "create_new_file",
290
+ "suggestedFileName": "pet-store-api.models.ts",
291
+ "code": "// ...generated code...",
292
+ "schemaCount": 5,
293
+ "enumCount": 1,
294
+ "language": "typescript",
295
+ "source": "https://petstore.swagger.io/v2/swagger.json",
296
+ "instruction": "Create file 'pet-store-api.models.ts' with the code."
297
+ }
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Изменения по файлам (Фаза 1)
303
+
304
+ ### Новые файлы
305
+
306
+ #### 1. `utils/openapi/parser.js`
307
+
308
+ Единый парсер для OpenAPI 2.0 и 3.x.
309
+
310
+ ```js
311
+ import yaml from 'js-yaml';
312
+ import { resolveAllRefs } from './ref-resolver.js';
313
+
314
+ export class OpenAPIParser {
315
+ constructor(rawSpec) {
316
+ this.raw = rawSpec;
317
+ this.version = this.detectVersion();
318
+ this.spec = this.normalize(); // внутренне -> формат 3.x
319
+ }
320
+
321
+ /** Загрузить спеку из URL или файла */
322
+ static async load(source, format = 'auto') { ... }
323
+
324
+ /** Определить версию */
325
+ detectVersion() {
326
+ if (this.raw.swagger === '2.0') return '2.0';
327
+ if (this.raw.openapi?.startsWith('3.')) return this.raw.openapi;
328
+ throw new Error('Unsupported OpenAPI version');
329
+ }
330
+
331
+ /** Нормализовать 2.0 → 3.x структуру */
332
+ normalize() {
333
+ if (this.version === '2.0') return this.normalize2to3(this.raw);
334
+ return this.raw;
335
+ }
336
+
337
+ /** Конвертация 2.0 → 3.x */
338
+ normalize2to3(spec) {
339
+ return {
340
+ openapi: '3.0.0',
341
+ info: spec.info,
342
+ servers: [{
343
+ url: `${spec.schemes?.[0] || 'https'}://${spec.host || 'localhost'}${spec.basePath || ''}`
344
+ }],
345
+ paths: this.normalizePaths2to3(spec.paths),
346
+ components: {
347
+ schemas: spec.definitions || {},
348
+ securitySchemes: this.normalizeSecurityDefs(spec.securityDefinitions || {})
349
+ },
350
+ security: spec.security
351
+ };
352
+ }
353
+
354
+ /** Нормализация путей 2.0 → 3.x (body params → requestBody) */
355
+ normalizePaths2to3(paths) { ... }
356
+
357
+ /** Нормализация securityDefinitions → securitySchemes */
358
+ normalizeSecurityDefs(defs) { ... }
359
+
360
+ /** Получить все schemas */
361
+ getSchemas() {
362
+ return this.spec.components?.schemas || {};
363
+ }
364
+
365
+ /** Получить security schemes */
366
+ getSecuritySchemes() {
367
+ return this.spec.components?.securitySchemes || {};
368
+ }
369
+
370
+ /** Получить base URL */
371
+ getBaseUrl() {
372
+ return this.spec.servers?.[0]?.url || '';
373
+ }
374
+
375
+ /** Получить все endpoints как плоский список */
376
+ getEndpoints() {
377
+ const endpoints = [];
378
+ for (const [path, methods] of Object.entries(this.spec.paths || {})) {
379
+ for (const [method, operation] of Object.entries(methods)) {
380
+ if (['get','post','put','patch','delete','head','options'].includes(method)) {
381
+ endpoints.push({
382
+ method: method.toUpperCase(),
383
+ path,
384
+ operationId: operation.operationId || this.generateOperationId(method, path),
385
+ summary: operation.summary || '',
386
+ description: operation.description || '',
387
+ tags: operation.tags || [],
388
+ parameters: this.extractParameters(operation, methods.parameters),
389
+ requestBody: this.extractRequestBody(operation),
390
+ responses: this.extractResponses(operation),
391
+ security: operation.security || this.spec.security || [],
392
+ deprecated: operation.deprecated || false
393
+ });
394
+ }
395
+ }
396
+ }
397
+ return endpoints;
398
+ }
399
+
400
+ /** Генерация operationId из метода и пути: GET /users/{id} -> getUser */
401
+ generateOperationId(method, path) { ... }
402
+
403
+ /** Извлечение параметров (path, query, header) */
404
+ extractParameters(operation, pathParams) { ... }
405
+
406
+ /** Извлечение request body */
407
+ extractRequestBody(operation) { ... }
408
+
409
+ /** Извлечение responses с именами схем */
410
+ extractResponses(operation) { ... }
411
+
412
+ /** Полная сводка для loadSwagger */
413
+ getSummary() {
414
+ return {
415
+ version: this.version,
416
+ title: this.spec.info?.title || '',
417
+ description: this.spec.info?.description || '',
418
+ baseUrl: this.getBaseUrl(),
419
+ auth: this.getAuthSummary(),
420
+ endpoints: this.getEndpoints(),
421
+ schemas: this.getSchemasSummary(),
422
+ endpointCount: this.getEndpoints().length,
423
+ schemaCount: Object.keys(this.getSchemas()).length
424
+ };
425
+ }
426
+
427
+ /** Сводка auth для удобного потребления */
428
+ getAuthSummary() { ... }
429
+
430
+ /** Сводка schemas (без полного тела, только имена + поля верхнего уровня) */
431
+ getSchemasSummary() { ... }
432
+ }
433
+ ```
434
+
435
+ #### 2. `utils/openapi/ref-resolver.js`
436
+
437
+ Рекурсивное разрешение `$ref` ссылок.
438
+
439
+ ```js
440
+ /**
441
+ * Разрешить все $ref в объекте спецификации
442
+ * @param {Object} spec - полная спецификация
443
+ * @returns {Object} - спецификация с разрешёнными $ref
444
+ */
445
+ export function resolveAllRefs(spec) { ... }
446
+
447
+ /**
448
+ * Разрешить одну $ref ссылку
449
+ * "#/components/schemas/Pet" -> объект Pet
450
+ * "#/definitions/Pet" -> объект Pet (OpenAPI 2.0)
451
+ *
452
+ * @param {string} ref - $ref строка
453
+ * @param {Object} spec - корневой объект спецификации
454
+ * @param {Set} visited - отслеживание циклических ссылок
455
+ * @returns {Object} - разрешённый объект
456
+ */
457
+ export function resolveRef(ref, spec, visited = new Set()) { ... }
458
+ ```
459
+
460
+ **Обработка $ref**:
461
+ - Внутренние: `#/components/schemas/Pet` → разрешить по JSON path
462
+ - Циклические: отслеживать через `visited` Set, при обнаружении цикла — остановиться и вернуть `{ $circularRef: 'SchemaName' }`
463
+ - `allOf`: мержить все элементы в один объект (properties + required)
464
+ - `oneOf` / `anyOf`: сохранять как union type
465
+ - Внешние `$ref` (другие файлы): **НЕ поддерживаем в фазе 1**, бросаем warning
466
+
467
+ #### 3. `utils/openapi/type-mapper.js`
468
+
469
+ Маппинг OpenAPI типов в TypeScript и Python.
470
+
471
+ ```js
472
+ /**
473
+ * Конвертировать OpenAPI тип в целевой язык
474
+ */
475
+ export class TypeMapper {
476
+ static toTypeScript(schema, schemaName = null) {
477
+ // string -> string
478
+ // string + format:date-time -> string (ISO)
479
+ // string + format:binary -> Blob
480
+ // integer / number -> number
481
+ // boolean -> boolean
482
+ // array + items -> ItemType[]
483
+ // object + properties -> inline { ... } или имя схемы
484
+ // enum -> ссылка на enum type
485
+ // oneOf/anyOf -> Union type (A | B)
486
+ // allOf -> Intersection type (A & B)
487
+ // nullable: true -> Type | null
488
+ }
489
+
490
+ static toPython(schema, schemaName = null) {
491
+ // string -> str
492
+ // string + format:date-time -> datetime
493
+ // string + format:date -> date
494
+ // integer -> int
495
+ // number -> float
496
+ // boolean -> bool
497
+ // array + items -> List[ItemType]
498
+ // object -> Dict[str, Any] или имя схемы
499
+ // enum -> ссылка на Enum class
500
+ // oneOf/anyOf -> Union[A, B]
501
+ // nullable: true -> Optional[Type]
502
+ }
503
+ }
504
+ ```
505
+
506
+ Таблица маппинга:
507
+
508
+ | OpenAPI type | format | TypeScript | Python |
509
+ |---|---|---|---|
510
+ | `string` | — | `string` | `str` |
511
+ | `string` | `date-time` | `string` | `datetime` |
512
+ | `string` | `date` | `string` | `date` |
513
+ | `string` | `binary` | `Blob` | `bytes` |
514
+ | `string` | `uuid` | `string` | `str` |
515
+ | `string` | enum [...] | `EnumName` | `EnumName` |
516
+ | `integer` | `int32` | `number` | `int` |
517
+ | `integer` | `int64` | `number` | `int` |
518
+ | `number` | `float` | `number` | `float` |
519
+ | `number` | `double` | `number` | `float` |
520
+ | `boolean` | — | `boolean` | `bool` |
521
+ | `array` | items: T | `T[]` | `List[T]` |
522
+ | `object` | properties | `InterfaceName` | `ClassName` |
523
+ | `object` | additionalProperties: T | `Record<string, T>` | `Dict[str, T]` |
524
+ | — | `nullable: true` | `T \| null` | `Optional[T]` |
525
+ | — | `oneOf: [A, B]` | `A \| B` | `Union[A, B]` |
526
+ | — | `allOf: [A, B]` | `A & B` | наследование / merge |
527
+
528
+ #### 4. `utils/api-generators/api-models-typescript.js`
529
+
530
+ Генератор TypeScript моделей.
531
+
532
+ ```js
533
+ export class ApiModelsTypeScriptGenerator {
534
+ constructor(schemas, options = {}) {
535
+ this.schemas = schemas; // resolved schemas object
536
+ this.options = {
537
+ style: 'interface', // 'interface' | 'type'
538
+ includeEnums: true,
539
+ includeValidation: false, // validation constraints as JSDoc comments
540
+ ...options
541
+ };
542
+ }
543
+
544
+ /** Сгенерировать весь файл */
545
+ generate(metadata = {}) {
546
+ const lines = [];
547
+ lines.push(...this.generateHeader(metadata));
548
+ lines.push('');
549
+
550
+ // 1. Сначала enums
551
+ if (this.options.includeEnums) {
552
+ lines.push(...this.generateAllEnums());
553
+ }
554
+
555
+ // 2. Затем interfaces/types в порядке зависимостей
556
+ const sorted = this.topologicalSort();
557
+ for (const name of sorted) {
558
+ lines.push(...this.generateModel(name, this.schemas[name]));
559
+ lines.push('');
560
+ }
561
+
562
+ return lines.join('\n');
563
+ }
564
+
565
+ /** Генерировать один interface/type */
566
+ generateModel(name, schema) { ... }
567
+
568
+ /** Генерировать enum из string + enum [...] */
569
+ generateEnum(name, values, description) { ... }
570
+
571
+ /** Обработка allOf — merge properties */
572
+ mergeAllOf(allOfSchemas) { ... }
573
+
574
+ /** Обработка oneOf/anyOf — union type */
575
+ generateUnionType(name, schemas) { ... }
576
+
577
+ /** Топологическая сортировка по зависимостям */
578
+ topologicalSort() { ... }
579
+
580
+ /** Генерировать заголовок файла */
581
+ generateHeader(metadata) { ... }
582
+ }
583
+ ```
584
+
585
+ #### 5. `utils/api-generators/api-models-python.js`
586
+
587
+ Генератор Python моделей.
588
+
589
+ ```js
590
+ export class ApiModelsPythonGenerator {
591
+ constructor(schemas, options = {}) {
592
+ this.schemas = schemas;
593
+ this.options = {
594
+ style: 'dataclass', // 'dataclass' | 'pydantic' | 'typeddict'
595
+ includeEnums: true,
596
+ includeValidation: false,
597
+ ...options
598
+ };
599
+ }
600
+
601
+ /** Сгенерировать весь файл */
602
+ generate(metadata = {}) {
603
+ const lines = [];
604
+ lines.push(...this.generateHeader(metadata));
605
+ lines.push(...this.generateImports());
606
+ lines.push('');
607
+
608
+ // 1. Enums
609
+ if (this.options.includeEnums) {
610
+ lines.push(...this.generateAllEnums());
611
+ }
612
+
613
+ // 2. Models в порядке зависимостей
614
+ const sorted = this.topologicalSort();
615
+ for (const name of sorted) {
616
+ lines.push('');
617
+ lines.push(...this.generateModel(name, this.schemas[name]));
618
+ }
619
+
620
+ return lines.join('\n');
621
+ }
622
+
623
+ /** Генерировать импорты в зависимости от стиля */
624
+ generateImports() {
625
+ if (this.options.style === 'dataclass') {
626
+ return [
627
+ 'from __future__ import annotations',
628
+ 'from dataclasses import dataclass, field',
629
+ 'from enum import Enum',
630
+ 'from typing import Optional, List, Dict, Any, Union',
631
+ 'from datetime import datetime, date'
632
+ ];
633
+ }
634
+ if (this.options.style === 'pydantic') {
635
+ return [
636
+ 'from __future__ import annotations',
637
+ 'from pydantic import BaseModel, Field',
638
+ 'from enum import Enum',
639
+ 'from typing import Optional, List, Dict, Any, Union',
640
+ 'from datetime import datetime, date'
641
+ ];
642
+ }
643
+ // typeddict
644
+ return [
645
+ 'from typing import TypedDict, Optional, List, Dict, Any, Union',
646
+ 'from enum import Enum',
647
+ 'from datetime import datetime, date'
648
+ ];
649
+ }
650
+
651
+ generateModel(name, schema) { ... }
652
+ generateEnum(name, values, description) { ... }
653
+ mergeAllOf(allOfSchemas) { ... }
654
+ topologicalSort() { ... }
655
+ generateHeader(metadata) { ... }
656
+ }
657
+ ```
658
+
659
+ ### Изменяемые файлы
660
+
661
+ #### 6. `server/tool-schemas.js`
662
+
663
+ ```js
664
+ export const LoadSwaggerSchema = z.object({
665
+ source: z.string().describe("URL (http/https) or local file path to swagger.json / openapi.yaml"),
666
+ format: z.enum(['auto', 'json', 'yaml']).optional()
667
+ .describe("Spec format. 'auto' (default) detects from extension/content"),
668
+ });
669
+
670
+ export const GenerateApiModelsSchema = z.object({
671
+ source: z.string().describe("URL or file path to OpenAPI spec"),
672
+ language: z.enum(['typescript', 'python']).describe("Target language for models"),
673
+ format: z.enum(['auto', 'json', 'yaml']).optional()
674
+ .describe("Spec format (default: auto)"),
675
+ style: z.enum(['interface', 'type']).optional()
676
+ .describe("TypeScript only: 'interface' (default) or 'type' aliases"),
677
+ pythonStyle: z.enum(['dataclass', 'pydantic', 'typeddict']).optional()
678
+ .describe("Python only: 'dataclass' (default), 'pydantic', or 'typeddict'"),
679
+ includeEnums: z.boolean().optional()
680
+ .describe("Generate enum types (default: true)"),
681
+ includeValidation: z.boolean().optional()
682
+ .describe("Include validation constraints as comments (default: false)"),
683
+ schemas: z.array(z.string()).optional()
684
+ .describe("Generate only these schemas (default: all)"),
685
+ });
686
+ ```
687
+
688
+ #### 7. `server/tool-definitions.js`
689
+
690
+ Добавить определения обоих тулов в массив tools с описаниями и inputSchema.
691
+
692
+ #### 8. `index.js`
693
+
694
+ Добавить хендлеры для `loadSwagger` и `generateApiModels`:
695
+
696
+ **loadSwagger**:
697
+ ```js
698
+ if (name === "loadSwagger") {
699
+ const parser = await OpenAPIParser.load(args.source, args.format);
700
+ const summary = parser.getSummary();
701
+ return {
702
+ content: [{
703
+ type: 'text',
704
+ text: JSON.stringify({ success: true, ...summary }, null, 2)
705
+ }]
706
+ };
707
+ }
708
+ ```
709
+
710
+ **generateApiModels**:
711
+ ```js
712
+ if (name === "generateApiModels") {
713
+ const parser = await OpenAPIParser.load(args.source, args.format);
714
+ const schemas = parser.getSchemas();
715
+ // Отфильтровать если указан args.schemas
716
+ // Выбрать генератор по языку
717
+ // Сгенерировать код
718
+ // Вернуть JSON с кодом и suggestedFileName
719
+ }
720
+ ```
721
+
722
+ #### 9. `README.md`
723
+
724
+ Документировать оба тула в секции Recorder/API Tools.
725
+
726
+ ---
727
+
728
+ ## Обработка Auth типов
729
+
730
+ Все типы auth из OpenAPI spec парсятся и возвращаются в `loadSwagger`:
731
+
732
+ ### HTTP Bearer
733
+ ```yaml
734
+ securitySchemes:
735
+ bearerAuth:
736
+ type: http
737
+ scheme: bearer
738
+ bearerFormat: JWT
739
+ ```
740
+ → `{ name: "bearerAuth", type: "http", scheme: "bearer", bearerFormat: "JWT" }`
741
+
742
+ ### API Key
743
+ ```yaml
744
+ securitySchemes:
745
+ apiKey:
746
+ type: apiKey
747
+ in: header
748
+ name: X-API-Key
749
+ ```
750
+ → `{ name: "apiKey", type: "apiKey", in: "header", paramName: "X-API-Key" }`
751
+
752
+ ### Basic Auth
753
+ ```yaml
754
+ securitySchemes:
755
+ basicAuth:
756
+ type: http
757
+ scheme: basic
758
+ ```
759
+ → `{ name: "basicAuth", type: "http", scheme: "basic" }`
760
+
761
+ ### OAuth2
762
+ ```yaml
763
+ securitySchemes:
764
+ oauth2:
765
+ type: oauth2
766
+ flows:
767
+ authorizationCode:
768
+ authorizationUrl: https://example.com/oauth/authorize
769
+ tokenUrl: https://example.com/oauth/token
770
+ scopes:
771
+ read: Read access
772
+ write: Write access
773
+ ```
774
+ → `{ name: "oauth2", type: "oauth2", flows: { authorizationCode: { ... } } }`
775
+
776
+ ### OpenAPI 2.0 → 3.x маппинг auth
777
+
778
+ | OpenAPI 2.0 securityDefinitions | OpenAPI 3.x securitySchemes |
779
+ |---|---|
780
+ | `type: "apiKey"` | `type: "apiKey"` (без изменений) |
781
+ | `type: "basic"` | `type: "http", scheme: "basic"` |
782
+ | `type: "oauth2", flow: "implicit"` | `type: "oauth2", flows: { implicit: {...} }` |
783
+ | `type: "oauth2", flow: "password"` | `type: "oauth2", flows: { password: {...} }` |
784
+ | `type: "oauth2", flow: "application"` | `type: "oauth2", flows: { clientCredentials: {...} }` |
785
+ | `type: "oauth2", flow: "accessCode"` | `type: "oauth2", flows: { authorizationCode: {...} }` |
786
+
787
+ ---
788
+
789
+ ## Обработка $ref
790
+
791
+ ### Алгоритм разрешения
792
+
793
+ ```
794
+ resolveRef("#/components/schemas/Pet", spec):
795
+ 1. Разбить путь: ["components", "schemas", "Pet"]
796
+ 2. Пройти по spec: spec.components.schemas.Pet
797
+ 3. Если результат содержит $ref — рекурсия
798
+ 4. Если уже в visited — вернуть { $circularRef: "Pet" }
799
+ 5. Кэшировать в resolvedRefs Map
800
+ ```
801
+
802
+ ### allOf обработка
803
+ ```yaml
804
+ NewPet:
805
+ allOf:
806
+ - $ref: '#/components/schemas/Pet'
807
+ - type: object
808
+ properties:
809
+ extraField:
810
+ type: string
811
+ ```
812
+ → Мержить properties и required из всех элементов allOf.
813
+ Для TypeScript: `interface NewPet extends Pet { extraField?: string; }`
814
+ Для Python: `class NewPet(Pet): extra_field: Optional[str] = None`
815
+
816
+ ### oneOf/anyOf обработка
817
+ ```yaml
818
+ Response:
819
+ oneOf:
820
+ - $ref: '#/components/schemas/Cat'
821
+ - $ref: '#/components/schemas/Dog'
822
+ ```
823
+ → TypeScript: `type Response = Cat | Dog;`
824
+ → Python: `Response = Union[Cat, Dog]`
825
+
826
+ ### Циклические ссылки
827
+ ```yaml
828
+ TreeNode:
829
+ properties:
830
+ children:
831
+ type: array
832
+ items:
833
+ $ref: '#/components/schemas/TreeNode' # цикл!
834
+ ```
835
+ → TypeScript: `children?: TreeNode[]` (TS нативно поддерживает)
836
+ → Python: `children: List['TreeNode'] = field(default_factory=list)` (forward reference)
837
+
838
+ ---
839
+
840
+ ## Именование файлов
841
+
842
+ | Спека | TypeScript | Python |
843
+ |---|---|---|
844
+ | Pet Store API | `pet-store-api.models.ts` | `pet_store_api_models.py` |
845
+ | My Service | `my-service.models.ts` | `my_service_models.py` |
846
+
847
+ ---
848
+
849
+ ## Структура файлов проекта (после реализации)
850
+
851
+ ```
852
+ utils/
853
+ openapi/
854
+ parser.js # OpenAPIParser class
855
+ ref-resolver.js # $ref resolution
856
+ type-mapper.js # OpenAPI → TS/Python type mapping
857
+ api-generators/
858
+ api-models-typescript.js # TS interface/type generator
859
+ api-models-python.js # Python dataclass/pydantic generator
860
+ code-generators/
861
+ ... (existing POM/test generators)
862
+ ```
863
+
864
+ ---
865
+
866
+ ## Верификация
867
+
868
+ ### loadSwagger
869
+ 1. Загрузить Petstore 2.0: `https://petstore.swagger.io/v2/swagger.json`
870
+ 2. Загрузить Petstore 3.0: `https://petstore3.swagger.io/api/v3/openapi.json`
871
+ 3. Загрузить из YAML-файла
872
+ 4. Проверить количество endpoints и schemas
873
+ 5. Проверить auth types
874
+ 6. Проверить $ref resolution
875
+
876
+ ### generateApiModels
877
+ 1. Сгенерировать TypeScript interfaces из Petstore
878
+ 2. Сгенерировать Python dataclasses из Petstore
879
+ 3. Сгенерировать Python pydantic из Petstore
880
+ 4. Проверить enum-ы (PetStatus)
881
+ 5. Проверить nested objects ($ref)
882
+ 6. Проверить required vs optional поля
883
+ 7. Проверить allOf merge
884
+ 8. Проверить массивы (tags: Tag[])
885
+ 9. Фильтрация по schemas: только ['Pet', 'Tag']
886
+
887
+ ---
888
+
889
+ ## Фаза 2: generateApiClient
890
+
891
+ ### Тул 3: `generateApiClient`
892
+
893
+ **Назначение**: сгенерировать класс API-клиента с типизированными методами для каждого endpoint из OpenAPI спецификации.
894
+
895
+ **Параметры**:
896
+ ```js
897
+ GenerateApiClientSchema = z.object({
898
+ source: z.string().describe("URL or file path to OpenAPI spec"),
899
+ language: z.enum(['typescript', 'python']).describe("Target language"),
900
+ format: z.enum(['auto', 'json', 'yaml']).optional()
901
+ .describe("Spec format (default: auto)"),
902
+ modelsImportPath: z.string().optional()
903
+ .describe("Import path for models file. E.g. './pet-store-api.models' (default: auto from spec title)"),
904
+ includeAuth: z.boolean().optional()
905
+ .describe("Generate auth configuration in constructor (default: true)"),
906
+ includeComments: z.boolean().optional()
907
+ .describe("Include JSDoc/docstring comments on methods (default: true)"),
908
+ tags: z.array(z.string()).optional()
909
+ .describe("Generate methods only for these tags (default: all). E.g. ['pets', 'users']"),
910
+ groupByTags: z.boolean().optional()
911
+ .describe("Group methods by tag with section comments (default: true)"),
912
+ responseStyle: z.enum(['raw', 'typed', 'both']).optional()
913
+ .describe("Return type: 'raw' returns Response object, 'typed' returns parsed model, 'both' has overloads (default: 'raw')"),
914
+ })
915
+ ```
916
+
917
+ **Алгоритм**:
918
+ 1. Загрузить и распарсить спеку (переиспользует `OpenAPIParser`)
919
+ 2. Извлечь все endpoints через `parser.getEndpoints()`
920
+ 3. Отфильтровать endpoints по `tags` если указан
921
+ 4. Извлечь auth schemes через `parser.getSecuritySchemes()`
922
+ 5. Для каждого endpoint сгенерировать метод:
923
+ a. Определить имя метода из `operationId` (или сгенерировать из method+path)
924
+ b. Извлечь path params → параметры функции
925
+ c. Извлечь query params → опциональный params объект
926
+ d. Извлечь request body → body параметр с типом из моделей
927
+ e. Определить response type из `responses.200` (или 201) schema
928
+ f. Сгенерировать URL-строку с подстановкой path params
929
+ g. Сгенерировать query string building
930
+ 6. Сгенерировать constructor с auth configuration
931
+ 7. Собрать файл: imports → class → constructor → методы
932
+ 8. Вернуть код + suggested filename
933
+
934
+ **Возвращаемое значение**:
935
+ ```json
936
+ {
937
+ "action": "create_new_file",
938
+ "suggestedFileName": "PetStoreApi.ts",
939
+ "modelsFileName": "pet-store-api.models.ts",
940
+ "code": "// ...generated code...",
941
+ "methodCount": 12,
942
+ "tagGroups": ["pets", "store", "user"],
943
+ "authTypes": ["bearer", "apiKey"],
944
+ "language": "typescript",
945
+ "source": "https://petstore.swagger.io/v2/swagger.json",
946
+ "instruction": "Create file 'PetStoreApi.ts'. Make sure models file exists (use generateApiModels if not). Set auth credentials via constructor options."
947
+ }
948
+ ```
949
+
950
+ ---
951
+
952
+ ### Генерация имён методов (method-generator)
953
+
954
+ #### operationId → имя метода
955
+
956
+ | operationId | TypeScript method | Python method |
957
+ |---|---|---|
958
+ | `listPets` | `listPets` | `list_pets` |
959
+ | `createPet` | `createPet` | `create_pet` |
960
+ | `getPetById` | `getPetById` | `get_pet_by_id` |
961
+ | `updatePetWithForm` | `updatePetWithForm` | `update_pet_with_form` |
962
+ | `deletePet` | `deletePet` | `delete_pet` |
963
+
964
+ **Fallback (нет operationId)**:
965
+ Генерация из HTTP method + path:
966
+ | method | path | TypeScript | Python |
967
+ |---|---|---|---|
968
+ | `GET` | `/pets` | `getPets` | `get_pets` |
969
+ | `POST` | `/pets` | `createPets` | `create_pets` |
970
+ | `GET` | `/pets/{petId}` | `getPetsByPetId` | `get_pets_by_pet_id` |
971
+ | `PUT` | `/pets/{petId}` | `updatePetsByPetId` | `update_pets_by_pet_id` |
972
+ | `DELETE` | `/pets/{petId}` | `deletePetsByPetId` | `delete_pets_by_pet_id` |
973
+ | `GET` | `/users/{id}/orders` | `getUsersByIdOrders` | `get_users_by_id_orders` |
974
+
975
+ **Алгоритм генерации operationId из method+path**:
976
+ ```
977
+ 1. Маппинг HTTP метода → префикс: GET→get, POST→create, PUT→update, PATCH→patch, DELETE→delete
978
+ 2. Разбить path по '/': /pets/{petId}/tags → ['pets', '{petId}', 'tags']
979
+ 3. Для каждого сегмента:
980
+ - Обычный сегмент: capitalize ('pets' → 'Pets')
981
+ - Path param {x}: 'By' + capitalize(x) ('By' + 'PetId' → 'ByPetId')
982
+ 4. Склеить: 'get' + 'Pets' + 'ByPetId' + 'Tags' → 'getPetsByPetIdTags'
983
+ ```
984
+
985
+ #### Параметры метода
986
+
987
+ **Path params** → обязательные параметры функции:
988
+ ```typescript
989
+ // GET /pets/{petId}/toys/{toyId}
990
+ async getPetToy(petId: number, toyId: number)
991
+ ```
992
+ ```python
993
+ # GET /pets/{petId}/toys/{toyId}
994
+ def get_pet_toy(self, pet_id: int, toy_id: int)
995
+ ```
996
+
997
+ **Query params** → опциональный params объект:
998
+ ```typescript
999
+ // GET /pets?limit=10&status=available&tags=cat,dog
1000
+ async listPets(params?: { limit?: number; status?: PetStatus; tags?: string[] })
1001
+ ```
1002
+ ```python
1003
+ # GET /pets?limit=10&status=available&tags=cat,dog
1004
+ def list_pets(self, limit: Optional[int] = None, status: Optional[PetStatus] = None, tags: Optional[List[str]] = None)
1005
+ ```
1006
+
1007
+ **Request body** → body параметр:
1008
+ ```typescript
1009
+ // POST /pets
1010
+ async createPet(body: CreatePetRequest)
1011
+ ```
1012
+ ```python
1013
+ # POST /pets
1014
+ def create_pet(self, body: CreatePetRequest)
1015
+ ```
1016
+
1017
+ **Комбинированные** (path + query + body):
1018
+ ```typescript
1019
+ // PUT /pets/{petId}?notify=true body: UpdatePetRequest
1020
+ async updatePet(petId: number, body: UpdatePetRequest, params?: { notify?: boolean })
1021
+ ```
1022
+ ```python
1023
+ # PUT /pets/{petId}?notify=true body: UpdatePetRequest
1024
+ def update_pet(self, pet_id: int, body: UpdatePetRequest, notify: Optional[bool] = None)
1025
+ ```
1026
+
1027
+ #### Маппинг типов параметров
1028
+
1029
+ | OpenAPI parameter type | TypeScript | Python |
1030
+ |---|---|---|
1031
+ | `integer` (in: path) | `number` | `int` |
1032
+ | `integer` (in: query) | `number` | `int` |
1033
+ | `string` (in: query) | `string` | `str` |
1034
+ | `boolean` (in: query) | `boolean` | `bool` |
1035
+ | `string` + enum (in: query) | `EnumType` | `EnumType` |
1036
+ | `array` (in: query) | `string[]` | `List[str]` |
1037
+ | `$ref` (requestBody) | `ModelName` | `ModelName` |
1038
+
1039
+ ---
1040
+
1041
+ ### Генерация Auth конфигурации
1042
+
1043
+ #### Constructor auth params
1044
+
1045
+ Из `securitySchemes` спецификации генерируется конструктор с соответствующими параметрами.
1046
+
1047
+ **HTTP Bearer**:
1048
+ ```typescript
1049
+ constructor(request: APIRequestContext, options: {
1050
+ baseUrl?: string;
1051
+ token?: string; // Bearer token
1052
+ } = {}) {
1053
+ this.request = request;
1054
+ this.baseUrl = options.baseUrl || 'https://petstore.swagger.io/v2';
1055
+ this.headers = {};
1056
+ if (options.token) this.headers['Authorization'] = `Bearer ${options.token}`;
1057
+ }
1058
+ ```
1059
+ ```python
1060
+ def __init__(self, base_url: str = 'https://petstore.swagger.io/v2',
1061
+ token: Optional[str] = None):
1062
+ self.session = requests.Session()
1063
+ self.base_url = base_url
1064
+ if token:
1065
+ self.session.headers['Authorization'] = f'Bearer {token}'
1066
+ ```
1067
+
1068
+ **API Key (header)**:
1069
+ ```typescript
1070
+ // securitySchemes: { apiKey: { type: apiKey, in: header, name: X-API-Key } }
1071
+ constructor(request: APIRequestContext, options: {
1072
+ baseUrl?: string;
1073
+ apiKey?: string; // X-API-Key header
1074
+ } = {}) {
1075
+ // ...
1076
+ if (options.apiKey) this.headers['X-API-Key'] = options.apiKey;
1077
+ }
1078
+ ```
1079
+ ```python
1080
+ def __init__(self, base_url: str = '...', api_key: Optional[str] = None):
1081
+ # ...
1082
+ if api_key:
1083
+ self.session.headers['X-API-Key'] = api_key
1084
+ ```
1085
+
1086
+ **API Key (query)**:
1087
+ ```typescript
1088
+ // securitySchemes: { apiKey: { type: apiKey, in: query, name: api_key } }
1089
+ // → сохраняется в this.queryAuth и добавляется ко всем запросам
1090
+ constructor(request: APIRequestContext, options: {
1091
+ baseUrl?: string;
1092
+ apiKey?: string; // api_key query parameter
1093
+ } = {}) {
1094
+ // ...
1095
+ this.queryAuth = {};
1096
+ if (options.apiKey) this.queryAuth['api_key'] = options.apiKey;
1097
+ }
1098
+ ```
1099
+ ```python
1100
+ def __init__(self, base_url: str = '...', api_key: Optional[str] = None):
1101
+ # ...
1102
+ self.query_auth = {}
1103
+ if api_key:
1104
+ self.query_auth['api_key'] = api_key
1105
+ ```
1106
+
1107
+ **Basic Auth**:
1108
+ ```typescript
1109
+ constructor(request: APIRequestContext, options: {
1110
+ baseUrl?: string;
1111
+ username?: string;
1112
+ password?: string;
1113
+ } = {}) {
1114
+ // ...
1115
+ if (options.username && options.password) {
1116
+ const creds = Buffer.from(`${options.username}:${options.password}`).toString('base64');
1117
+ this.headers['Authorization'] = `Basic ${creds}`;
1118
+ }
1119
+ }
1120
+ ```
1121
+ ```python
1122
+ def __init__(self, base_url: str = '...', username: Optional[str] = None, password: Optional[str] = None):
1123
+ # ...
1124
+ if username and password:
1125
+ self.session.auth = (username, password)
1126
+ ```
1127
+
1128
+ **OAuth2** (все flows):
1129
+ ```typescript
1130
+ // OAuth2 — генерируем приём готового access token (flow настраивается снаружи)
1131
+ constructor(request: APIRequestContext, options: {
1132
+ baseUrl?: string;
1133
+ accessToken?: string; // OAuth2 access token
1134
+ } = {}) {
1135
+ // ...
1136
+ if (options.accessToken) this.headers['Authorization'] = `Bearer ${options.accessToken}`;
1137
+ }
1138
+ ```
1139
+ ```python
1140
+ def __init__(self, base_url: str = '...', access_token: Optional[str] = None):
1141
+ # ...
1142
+ if access_token:
1143
+ self.session.headers['Authorization'] = f'Bearer {access_token}'
1144
+ ```
1145
+
1146
+ **Комбинированные auth** (несколько schemes в одной спеке):
1147
+ ```typescript
1148
+ constructor(request: APIRequestContext, options: {
1149
+ baseUrl?: string;
1150
+ token?: string; // Bearer auth
1151
+ apiKey?: string; // X-API-Key header
1152
+ username?: string; // Basic auth
1153
+ password?: string; // Basic auth
1154
+ } = {}) {
1155
+ this.request = request;
1156
+ this.baseUrl = options.baseUrl || '...';
1157
+ this.headers = {};
1158
+ if (options.token) this.headers['Authorization'] = `Bearer ${options.token}`;
1159
+ else if (options.username && options.password) {
1160
+ const creds = Buffer.from(`${options.username}:${options.password}`).toString('base64');
1161
+ this.headers['Authorization'] = `Basic ${creds}`;
1162
+ }
1163
+ if (options.apiKey) this.headers['X-API-Key'] = options.apiKey;
1164
+ }
1165
+ ```
1166
+
1167
+ #### Per-endpoint security overrides
1168
+
1169
+ Если endpoint имеет собственный `security` блок (отличный от глобального), генерируется комментарий:
1170
+ ```typescript
1171
+ /** GET /admin/users - List admin users
1172
+ * Security: bearerAuth (overrides global apiKey) */
1173
+ async listAdminUsers() { ... }
1174
+ ```
1175
+
1176
+ ---
1177
+
1178
+ ### URL Construction
1179
+
1180
+ #### Path params substitution
1181
+ ```typescript
1182
+ // Template: /pets/{petId}/toys/{toyId}
1183
+ // Generated: `${this.baseUrl}/pets/${petId}/toys/${toyId}`
1184
+ ```
1185
+ ```python
1186
+ # Template: /pets/{petId}/toys/{toyId}
1187
+ # Generated: f'{self.base_url}/pets/{pet_id}/toys/{toy_id}'
1188
+ ```
1189
+
1190
+ #### Query string building
1191
+
1192
+ **TypeScript**:
1193
+ ```typescript
1194
+ async listPets(params?: { limit?: number; status?: PetStatus; tags?: string[] }) {
1195
+ const query = new URLSearchParams();
1196
+ if (params?.limit !== undefined) query.set('limit', String(params.limit));
1197
+ if (params?.status !== undefined) query.set('status', params.status);
1198
+ if (params?.tags !== undefined) params.tags.forEach(v => query.append('tags', v));
1199
+ // Добавить query auth params
1200
+ for (const [k, v] of Object.entries(this.queryAuth)) query.set(k, v);
1201
+ const qs = query.toString();
1202
+ const url = `${this.baseUrl}/pets${qs ? '?' + qs : ''}`;
1203
+ return this.request.get(url, { headers: this.headers });
1204
+ }
1205
+ ```
1206
+
1207
+ **Python**:
1208
+ ```python
1209
+ def list_pets(self, limit: Optional[int] = None, status: Optional[PetStatus] = None,
1210
+ tags: Optional[List[str]] = None):
1211
+ """GET /pets - List all pets"""
1212
+ params = {**self.query_auth}
1213
+ if limit is not None: params['limit'] = limit
1214
+ if status is not None: params['status'] = status.value
1215
+ if tags is not None: params['tags'] = ','.join(tags)
1216
+ return self.session.get(f'{self.base_url}/pets', params=params)
1217
+ ```
1218
+
1219
+ #### Request body serialization
1220
+
1221
+ **TypeScript** (Playwright request context handles JSON automatically):
1222
+ ```typescript
1223
+ async createPet(body: CreatePetRequest) {
1224
+ return this.request.post(`${this.baseUrl}/pets`, {
1225
+ headers: { ...this.headers, 'Content-Type': 'application/json' },
1226
+ data: body
1227
+ });
1228
+ }
1229
+ ```
1230
+
1231
+ **Python**:
1232
+ ```python
1233
+ def create_pet(self, body: CreatePetRequest):
1234
+ """POST /pets - Create a pet"""
1235
+ # dataclass
1236
+ from dataclasses import asdict
1237
+ return self.session.post(f'{self.base_url}/pets', json=asdict(body))
1238
+ ```
1239
+
1240
+ Для `pydantic`:
1241
+ ```python
1242
+ return self.session.post(f'{self.base_url}/pets', json=body.model_dump())
1243
+ ```
1244
+
1245
+ Для `typeddict`:
1246
+ ```python
1247
+ return self.session.post(f'{self.base_url}/pets', json=body)
1248
+ ```
1249
+
1250
+ ---
1251
+
1252
+ ### Изменения по файлам (Фаза 2)
1253
+
1254
+ #### Новые файлы
1255
+
1256
+ ##### 1. `utils/openapi/method-generator.js`
1257
+
1258
+ Генерация метаданных методов из endpoints.
1259
+
1260
+ ```js
1261
+ /**
1262
+ * Генератор метаданных методов из OpenAPI endpoints
1263
+ */
1264
+ export class MethodGenerator {
1265
+ /**
1266
+ * Сгенерировать метаданные метода для endpoint
1267
+ * @param {Object} endpoint - endpoint из OpenAPIParser.getEndpoints()
1268
+ * @param {string} language - 'typescript' | 'python'
1269
+ * @returns {Object} - { methodName, pathParams, queryParams, bodyParam, returnType, comment }
1270
+ */
1271
+ static generateMethod(endpoint, language) {
1272
+ return {
1273
+ methodName: this.getMethodName(endpoint.operationId, endpoint.method, endpoint.path, language),
1274
+ httpMethod: endpoint.method.toLowerCase(),
1275
+ path: endpoint.path,
1276
+ pathParams: this.extractPathParams(endpoint.parameters, language),
1277
+ queryParams: this.extractQueryParams(endpoint.parameters, language),
1278
+ bodyParam: this.extractBodyParam(endpoint.requestBody, language),
1279
+ returnType: this.extractReturnType(endpoint.responses, language),
1280
+ comment: this.buildComment(endpoint),
1281
+ tags: endpoint.tags,
1282
+ deprecated: endpoint.deprecated,
1283
+ security: endpoint.security
1284
+ };
1285
+ }
1286
+
1287
+ /**
1288
+ * Получить имя метода из operationId или сгенерировать из method+path
1289
+ * @param {string|null} operationId
1290
+ * @param {string} method - HTTP method
1291
+ * @param {string} path - URL path
1292
+ * @param {string} language - target language
1293
+ * @returns {string} - method name in target language convention
1294
+ */
1295
+ static getMethodName(operationId, method, path, language) {
1296
+ const camelName = operationId || this.generateOperationId(method, path);
1297
+ if (language === 'python') {
1298
+ return this.camelToSnake(camelName);
1299
+ }
1300
+ return camelName;
1301
+ }
1302
+
1303
+ /**
1304
+ * Генерация operationId из HTTP method + path
1305
+ * GET /pets/{petId} → getPetsByPetId
1306
+ */
1307
+ static generateOperationId(method, path) { ... }
1308
+
1309
+ /**
1310
+ * camelCase → snake_case
1311
+ */
1312
+ static camelToSnake(name) {
1313
+ return name.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`).replace(/^_/, '');
1314
+ }
1315
+
1316
+ /**
1317
+ * Извлечь path параметры
1318
+ * @returns {Array<{name, type, required}>}
1319
+ */
1320
+ static extractPathParams(parameters, language) {
1321
+ return (parameters || [])
1322
+ .filter(p => p.in === 'path')
1323
+ .map(p => ({
1324
+ name: language === 'python' ? this.camelToSnake(p.name) : p.name,
1325
+ originalName: p.name,
1326
+ type: TypeMapper.mapParamType(p, language),
1327
+ required: true
1328
+ }));
1329
+ }
1330
+
1331
+ /**
1332
+ * Извлечь query параметры
1333
+ * @returns {Array<{name, type, required, isArray, enumType}>}
1334
+ */
1335
+ static extractQueryParams(parameters, language) {
1336
+ return (parameters || [])
1337
+ .filter(p => p.in === 'query')
1338
+ .map(p => ({
1339
+ name: language === 'python' ? this.camelToSnake(p.name) : p.name,
1340
+ originalName: p.name,
1341
+ type: TypeMapper.mapParamType(p, language),
1342
+ required: p.required || false,
1343
+ isArray: p.type === 'array',
1344
+ enumType: p.enum ? p.name : null
1345
+ }));
1346
+ }
1347
+
1348
+ /**
1349
+ * Извлечь body параметр
1350
+ * @returns {Object|null} - { type, required } или null
1351
+ */
1352
+ static extractBodyParam(requestBody, language) { ... }
1353
+
1354
+ /**
1355
+ * Определить return type из responses
1356
+ * Приоритет: 200 → 201 → 204(void) → первый 2xx
1357
+ * @returns {string|null} - type name или null
1358
+ */
1359
+ static extractReturnType(responses, language) { ... }
1360
+
1361
+ /**
1362
+ * Построить JSDoc / docstring комментарий
1363
+ * @returns {string} - "METHOD /path - summary"
1364
+ */
1365
+ static buildComment(endpoint) {
1366
+ const deprecated = endpoint.deprecated ? ' [DEPRECATED]' : '';
1367
+ return `${endpoint.method} ${endpoint.path} - ${endpoint.summary || 'No description'}${deprecated}`;
1368
+ }
1369
+ }
1370
+ ```
1371
+
1372
+ ##### 2. `utils/openapi/auth-generator.js`
1373
+
1374
+ Генерация auth-конфигурации для constructor.
1375
+
1376
+ ```js
1377
+ /**
1378
+ * Генератор auth-конфигурации для API-клиента
1379
+ */
1380
+ export class AuthGenerator {
1381
+ /**
1382
+ * Сгенерировать auth metadata из securitySchemes
1383
+ * @param {Object} securitySchemes - из spec.components.securitySchemes
1384
+ * @returns {Object} - { constructorParams, headerSetup, querySetup }
1385
+ */
1386
+ static generateAuthConfig(securitySchemes) {
1387
+ const constructorParams = [];
1388
+ const headerSetup = [];
1389
+ const querySetup = [];
1390
+
1391
+ for (const [name, scheme] of Object.entries(securitySchemes || {})) {
1392
+ const config = this.processScheme(name, scheme);
1393
+ constructorParams.push(...config.params);
1394
+ headerSetup.push(...config.headers);
1395
+ querySetup.push(...config.query);
1396
+ }
1397
+
1398
+ return { constructorParams, headerSetup, querySetup };
1399
+ }
1400
+
1401
+ /**
1402
+ * Обработать одну security scheme
1403
+ */
1404
+ static processScheme(name, scheme) {
1405
+ switch (scheme.type) {
1406
+ case 'http':
1407
+ if (scheme.scheme === 'bearer') return this.bearerAuth(name);
1408
+ if (scheme.scheme === 'basic') return this.basicAuth(name);
1409
+ break;
1410
+ case 'apiKey':
1411
+ if (scheme.in === 'header') return this.apiKeyHeader(name, scheme.name);
1412
+ if (scheme.in === 'query') return this.apiKeyQuery(name, scheme.name);
1413
+ break;
1414
+ case 'oauth2':
1415
+ return this.oauth2Auth(name);
1416
+ }
1417
+ return { params: [], headers: [], query: [] };
1418
+ }
1419
+
1420
+ static bearerAuth(name) {
1421
+ return {
1422
+ params: [{ name: 'token', type: 'string', description: 'Bearer token', optional: true }],
1423
+ headers: [{ condition: 'token', header: 'Authorization', value: 'Bearer ${token}' }],
1424
+ query: []
1425
+ };
1426
+ }
1427
+
1428
+ static basicAuth(name) {
1429
+ return {
1430
+ params: [
1431
+ { name: 'username', type: 'string', description: 'Basic auth username', optional: true },
1432
+ { name: 'password', type: 'string', description: 'Basic auth password', optional: true }
1433
+ ],
1434
+ headers: [{ condition: 'username && password', header: 'Authorization', value: 'Basic(username, password)' }],
1435
+ query: []
1436
+ };
1437
+ }
1438
+
1439
+ static apiKeyHeader(name, headerName) {
1440
+ return {
1441
+ params: [{ name: 'apiKey', type: 'string', description: `${headerName} header`, optional: true }],
1442
+ headers: [{ condition: 'apiKey', header: headerName, value: '${apiKey}' }],
1443
+ query: []
1444
+ };
1445
+ }
1446
+
1447
+ static apiKeyQuery(name, paramName) {
1448
+ return {
1449
+ params: [{ name: 'apiKey', type: 'string', description: `${paramName} query param`, optional: true }],
1450
+ headers: [],
1451
+ query: [{ condition: 'apiKey', param: paramName, value: '${apiKey}' }]
1452
+ };
1453
+ }
1454
+
1455
+ static oauth2Auth(name) {
1456
+ return {
1457
+ params: [{ name: 'accessToken', type: 'string', description: 'OAuth2 access token', optional: true }],
1458
+ headers: [{ condition: 'accessToken', header: 'Authorization', value: 'Bearer ${accessToken}' }],
1459
+ query: []
1460
+ };
1461
+ }
1462
+ }
1463
+ ```
1464
+
1465
+ ##### 3. `utils/api-generators/api-client-typescript.js`
1466
+
1467
+ Генератор TypeScript API-клиента (Playwright APIRequestContext).
1468
+
1469
+ ```js
1470
+ import { MethodGenerator } from '../openapi/method-generator.js';
1471
+ import { AuthGenerator } from '../openapi/auth-generator.js';
1472
+
1473
+ export class ApiClientTypeScriptGenerator {
1474
+ constructor(endpoints, securitySchemes, options = {}) {
1475
+ this.endpoints = endpoints; // из parser.getEndpoints()
1476
+ this.securitySchemes = securitySchemes;
1477
+ this.options = {
1478
+ includeAuth: true,
1479
+ includeComments: true,
1480
+ groupByTags: true,
1481
+ responseStyle: 'raw', // 'raw' | 'typed' | 'both'
1482
+ modelsImportPath: null, // auto-generated if null
1483
+ ...options
1484
+ };
1485
+ }
1486
+
1487
+ /**
1488
+ * Сгенерировать весь файл API-клиента
1489
+ * @param {Object} metadata - { title, baseUrl, version }
1490
+ * @returns {string} - полный TypeScript код
1491
+ */
1492
+ generate(metadata = {}) {
1493
+ const lines = [];
1494
+
1495
+ // 1. Imports
1496
+ lines.push(...this.generateImports(metadata));
1497
+ lines.push('');
1498
+
1499
+ // 2. Class declaration
1500
+ const className = this.getClassName(metadata.title);
1501
+ lines.push(`export class ${className} {`);
1502
+
1503
+ // 3. Fields
1504
+ lines.push(' private request: APIRequestContext;');
1505
+ lines.push(' private baseUrl: string;');
1506
+ lines.push(' private headers: Record<string, string>;');
1507
+ if (this.hasQueryAuth()) {
1508
+ lines.push(' private queryAuth: Record<string, string>;');
1509
+ }
1510
+ lines.push('');
1511
+
1512
+ // 4. Constructor
1513
+ lines.push(...this.generateConstructor(metadata));
1514
+ lines.push('');
1515
+
1516
+ // 5. Methods — grouped by tag or flat
1517
+ const methods = this.endpoints.map(ep => MethodGenerator.generateMethod(ep, 'typescript'));
1518
+ if (this.options.groupByTags) {
1519
+ lines.push(...this.generateGroupedMethods(methods));
1520
+ } else {
1521
+ for (const m of methods) {
1522
+ lines.push(...this.generateMethod(m));
1523
+ lines.push('');
1524
+ }
1525
+ }
1526
+
1527
+ // 6. Close class
1528
+ lines.push('}');
1529
+
1530
+ return lines.join('\n');
1531
+ }
1532
+
1533
+ /** Генерировать import блок */
1534
+ generateImports(metadata) {
1535
+ const lines = [
1536
+ "import { APIRequestContext } from '@playwright/test';",
1537
+ ];
1538
+
1539
+ // Собрать все используемые типы моделей
1540
+ const usedTypes = this.collectUsedTypes();
1541
+ if (usedTypes.length > 0) {
1542
+ const importPath = this.options.modelsImportPath || this.getModelsImportPath(metadata.title);
1543
+ lines.push(`import { ${usedTypes.join(', ')} } from '${importPath}';`);
1544
+ }
1545
+
1546
+ return lines;
1547
+ }
1548
+
1549
+ /** Собрать все типы используемые в параметрах и return types */
1550
+ collectUsedTypes() { ... }
1551
+
1552
+ /** Сгенерировать constructor */
1553
+ generateConstructor(metadata) { ... }
1554
+
1555
+ /** Сгенерировать один метод */
1556
+ generateMethod(methodMeta) {
1557
+ const lines = [];
1558
+
1559
+ // JSDoc comment
1560
+ if (this.options.includeComments) {
1561
+ lines.push(` /** ${methodMeta.comment} */`);
1562
+ }
1563
+
1564
+ // Method signature
1565
+ const params = this.buildMethodSignature(methodMeta);
1566
+ lines.push(` async ${methodMeta.methodName}(${params}) {`);
1567
+
1568
+ // Query string building (if has query params)
1569
+ if (methodMeta.queryParams.length > 0 || this.hasQueryAuth()) {
1570
+ lines.push(...this.generateQueryBuilding(methodMeta));
1571
+ }
1572
+
1573
+ // URL construction
1574
+ const urlExpr = this.buildUrlExpression(methodMeta);
1575
+
1576
+ // HTTP call
1577
+ lines.push(...this.generateHttpCall(methodMeta, urlExpr));
1578
+
1579
+ lines.push(' }');
1580
+ return lines;
1581
+ }
1582
+
1583
+ /** Сгенерировать методы сгруппированные по тегам */
1584
+ generateGroupedMethods(methods) {
1585
+ const lines = [];
1586
+ const grouped = this.groupByTag(methods);
1587
+ for (const [tag, tagMethods] of Object.entries(grouped)) {
1588
+ lines.push(` // ======== ${tag} ========`);
1589
+ lines.push('');
1590
+ for (const m of tagMethods) {
1591
+ lines.push(...this.generateMethod(m));
1592
+ lines.push('');
1593
+ }
1594
+ }
1595
+ return lines;
1596
+ }
1597
+
1598
+ /** Построить сигнатуру метода */
1599
+ buildMethodSignature(methodMeta) { ... }
1600
+
1601
+ /** Построить URL expression с path params */
1602
+ buildUrlExpression(methodMeta) { ... }
1603
+
1604
+ /** Сгенерировать query string building code */
1605
+ generateQueryBuilding(methodMeta) { ... }
1606
+
1607
+ /** Сгенерировать HTTP call */
1608
+ generateHttpCall(methodMeta, urlExpr) { ... }
1609
+
1610
+ /** Имя класса из title: "Pet Store API" → "PetStoreApi" */
1611
+ getClassName(title) { ... }
1612
+
1613
+ /** Import path из title: "Pet Store API" → "./pet-store-api.models" */
1614
+ getModelsImportPath(title) { ... }
1615
+
1616
+ /** Есть ли apiKey auth в query */
1617
+ hasQueryAuth() { ... }
1618
+
1619
+ /** Группировка методов по первому тегу */
1620
+ groupByTag(methods) { ... }
1621
+ }
1622
+ ```
1623
+
1624
+ ##### 4. `utils/api-generators/api-client-python.js`
1625
+
1626
+ Генератор Python API-клиента (requests.Session).
1627
+
1628
+ ```js
1629
+ import { MethodGenerator } from '../openapi/method-generator.js';
1630
+ import { AuthGenerator } from '../openapi/auth-generator.js';
1631
+
1632
+ export class ApiClientPythonGenerator {
1633
+ constructor(endpoints, securitySchemes, options = {}) {
1634
+ this.endpoints = endpoints;
1635
+ this.securitySchemes = securitySchemes;
1636
+ this.options = {
1637
+ includeAuth: true,
1638
+ includeComments: true,
1639
+ groupByTags: true,
1640
+ responseStyle: 'raw',
1641
+ modelsImportPath: null,
1642
+ pythonStyle: 'dataclass', // для определения метода сериализации body
1643
+ ...options
1644
+ };
1645
+ }
1646
+
1647
+ /**
1648
+ * Сгенерировать весь файл API-клиента
1649
+ */
1650
+ generate(metadata = {}) {
1651
+ const lines = [];
1652
+
1653
+ // 1. Imports
1654
+ lines.push(...this.generateImports(metadata));
1655
+ lines.push('');
1656
+ lines.push('');
1657
+
1658
+ // 2. Class
1659
+ const className = this.getClassName(metadata.title);
1660
+ lines.push(`class ${className}:`);
1661
+
1662
+ // 3. Constructor (__init__)
1663
+ lines.push(...this.generateInit(metadata));
1664
+ lines.push('');
1665
+
1666
+ // 4. Methods
1667
+ const methods = this.endpoints.map(ep => MethodGenerator.generateMethod(ep, 'python'));
1668
+ if (this.options.groupByTags) {
1669
+ lines.push(...this.generateGroupedMethods(methods));
1670
+ } else {
1671
+ for (const m of methods) {
1672
+ lines.push(...this.generateMethod(m));
1673
+ lines.push('');
1674
+ }
1675
+ }
1676
+
1677
+ return lines.join('\n');
1678
+ }
1679
+
1680
+ /** Imports */
1681
+ generateImports(metadata) {
1682
+ const lines = [
1683
+ 'import requests',
1684
+ 'from typing import Optional, List, Dict, Any',
1685
+ ];
1686
+
1687
+ const usedTypes = this.collectUsedTypes();
1688
+ if (usedTypes.length > 0) {
1689
+ const importPath = this.options.modelsImportPath || this.getModelsImportPath(metadata.title);
1690
+ lines.push(`from ${importPath} import ${usedTypes.join(', ')}`);
1691
+ }
1692
+
1693
+ return lines;
1694
+ }
1695
+
1696
+ /** __init__ с auth */
1697
+ generateInit(metadata) {
1698
+ // Аналогично TypeScript, но с Python синтаксисом:
1699
+ // def __init__(self, base_url: str = '...', token: Optional[str] = None, ...)
1700
+ // self.session = requests.Session()
1701
+ // self.base_url = base_url
1702
+ // if token: self.session.headers['Authorization'] = f'Bearer {token}'
1703
+ ...
1704
+ }
1705
+
1706
+ /** Один метод */
1707
+ generateMethod(methodMeta) {
1708
+ const lines = [];
1709
+
1710
+ // Method signature
1711
+ const params = this.buildMethodSignature(methodMeta);
1712
+ lines.push(` def ${methodMeta.methodName}(self, ${params}):`);
1713
+
1714
+ // Docstring
1715
+ if (this.options.includeComments) {
1716
+ lines.push(` """${methodMeta.comment}"""`);
1717
+ }
1718
+
1719
+ // Query params building
1720
+ if (methodMeta.queryParams.length > 0 || this.hasQueryAuth()) {
1721
+ lines.push(...this.generateQueryBuilding(methodMeta));
1722
+ }
1723
+
1724
+ // URL
1725
+ const urlExpr = this.buildUrlExpression(methodMeta);
1726
+
1727
+ // HTTP call
1728
+ lines.push(...this.generateHttpCall(methodMeta, urlExpr));
1729
+
1730
+ return lines;
1731
+ }
1732
+
1733
+ /** Сериализация body в зависимости от pythonStyle */
1734
+ serializeBody(varName) {
1735
+ switch (this.options.pythonStyle) {
1736
+ case 'dataclass': return `from dataclasses import asdict; json=asdict(${varName})`;
1737
+ case 'pydantic': return `json=${varName}.model_dump()`;
1738
+ case 'typeddict': return `json=${varName}`;
1739
+ default: return `json=${varName}`;
1740
+ }
1741
+ }
1742
+
1743
+ buildMethodSignature(methodMeta) { ... }
1744
+ buildUrlExpression(methodMeta) { ... }
1745
+ generateQueryBuilding(methodMeta) { ... }
1746
+ generateHttpCall(methodMeta, urlExpr) { ... }
1747
+ getClassName(title) { ... }
1748
+ getModelsImportPath(title) { ... }
1749
+ hasQueryAuth() { ... }
1750
+ generateGroupedMethods(methods) { ... }
1751
+ collectUsedTypes() { ... }
1752
+ }
1753
+ ```
1754
+
1755
+ #### Изменяемые файлы
1756
+
1757
+ ##### 5. `server/tool-schemas.js`
1758
+
1759
+ Добавить:
1760
+ ```js
1761
+ export const GenerateApiClientSchema = z.object({
1762
+ source: z.string().describe("URL or file path to OpenAPI spec"),
1763
+ language: z.enum(['typescript', 'python']).describe("Target language"),
1764
+ format: z.enum(['auto', 'json', 'yaml']).optional()
1765
+ .describe("Spec format (default: auto)"),
1766
+ modelsImportPath: z.string().optional()
1767
+ .describe("Import path for models file (default: auto from spec title)"),
1768
+ includeAuth: z.boolean().optional()
1769
+ .describe("Generate auth config in constructor (default: true)"),
1770
+ includeComments: z.boolean().optional()
1771
+ .describe("Include JSDoc/docstring comments (default: true)"),
1772
+ tags: z.array(z.string()).optional()
1773
+ .describe("Generate only for these tags (default: all)"),
1774
+ groupByTags: z.boolean().optional()
1775
+ .describe("Group methods by tag with section comments (default: true)"),
1776
+ responseStyle: z.enum(['raw', 'typed', 'both']).optional()
1777
+ .describe("Response type style (default: 'raw')"),
1778
+ pythonStyle: z.enum(['dataclass', 'pydantic', 'typeddict']).optional()
1779
+ .describe("Python only: model style for body serialization (default: 'dataclass')"),
1780
+ });
1781
+ ```
1782
+
1783
+ ##### 6. `server/tool-definitions.js`
1784
+
1785
+ Добавить в массив tools:
1786
+ ```js
1787
+ {
1788
+ name: "generateApiClient",
1789
+ description: "Generate typed API client class from OpenAPI/Swagger spec. Creates a class with methods for each endpoint, typed parameters, and auth configuration. Use after loadSwagger to understand the API. Requires models file from generateApiModels.",
1790
+ inputSchema: GenerateApiClientSchema
1791
+ }
1792
+ ```
1793
+
1794
+ ##### 7. `index.js`
1795
+
1796
+ Добавить хендлер:
1797
+ ```js
1798
+ if (name === "generateApiClient") {
1799
+ const parser = await OpenAPIParser.load(args.source, args.format);
1800
+ const endpoints = parser.getEndpoints();
1801
+ const securitySchemes = parser.getSecuritySchemes();
1802
+ const metadata = {
1803
+ title: parser.spec.info?.title || '',
1804
+ baseUrl: parser.getBaseUrl(),
1805
+ version: parser.version
1806
+ };
1807
+
1808
+ // Фильтрация по тегам
1809
+ let filteredEndpoints = endpoints;
1810
+ if (args.tags) {
1811
+ filteredEndpoints = endpoints.filter(ep =>
1812
+ ep.tags.some(tag => args.tags.includes(tag))
1813
+ );
1814
+ }
1815
+
1816
+ // Выбрать генератор
1817
+ let generator;
1818
+ if (args.language === 'typescript') {
1819
+ generator = new ApiClientTypeScriptGenerator(filteredEndpoints, securitySchemes, {
1820
+ includeAuth: args.includeAuth ?? true,
1821
+ includeComments: args.includeComments ?? true,
1822
+ groupByTags: args.groupByTags ?? true,
1823
+ responseStyle: args.responseStyle || 'raw',
1824
+ modelsImportPath: args.modelsImportPath,
1825
+ });
1826
+ } else {
1827
+ generator = new ApiClientPythonGenerator(filteredEndpoints, securitySchemes, {
1828
+ includeAuth: args.includeAuth ?? true,
1829
+ includeComments: args.includeComments ?? true,
1830
+ groupByTags: args.groupByTags ?? true,
1831
+ responseStyle: args.responseStyle || 'raw',
1832
+ modelsImportPath: args.modelsImportPath,
1833
+ pythonStyle: args.pythonStyle || 'dataclass',
1834
+ });
1835
+ }
1836
+
1837
+ const code = generator.generate(metadata);
1838
+ const className = generator.getClassName(metadata.title);
1839
+ const suggestedFileName = args.language === 'typescript'
1840
+ ? `${className}.ts`
1841
+ : `${generator.getModelsImportPath(metadata.title).replace(/\./g, '_')}_client.py`;
1842
+
1843
+ return {
1844
+ content: [{
1845
+ type: 'text',
1846
+ text: JSON.stringify({
1847
+ action: 'create_new_file',
1848
+ suggestedFileName,
1849
+ modelsFileName: args.language === 'typescript'
1850
+ ? `${generator.getModelsImportPath(metadata.title).replace('./', '')}.ts`
1851
+ : `${generator.getModelsImportPath(metadata.title)}.py`,
1852
+ code,
1853
+ methodCount: filteredEndpoints.length,
1854
+ tagGroups: [...new Set(filteredEndpoints.flatMap(ep => ep.tags))],
1855
+ authTypes: Object.values(securitySchemes || {}).map(s => s.type),
1856
+ language: args.language,
1857
+ source: args.source,
1858
+ instruction: `Create file '${suggestedFileName}'. Make sure models file exists (use generateApiModels if not). Set auth credentials via constructor options.`
1859
+ }, null, 2)
1860
+ }]
1861
+ };
1862
+ }
1863
+ ```
1864
+
1865
+ ##### 8. `README.md`
1866
+
1867
+ Добавить в секцию API Tools:
1868
+ ```markdown
1869
+ ### generateApiClient
1870
+
1871
+ Generate typed API client class from OpenAPI/Swagger spec.
1872
+
1873
+ | Parameter | Type | Default | Description |
1874
+ |-----------|------|---------|-------------|
1875
+ | `source` | string | required | URL or file path to OpenAPI spec |
1876
+ | `language` | 'typescript' \| 'python' | required | Target language |
1877
+ | `format` | 'auto' \| 'json' \| 'yaml' | 'auto' | Spec format |
1878
+ | `modelsImportPath` | string | auto | Import path for models file |
1879
+ | `includeAuth` | boolean | true | Generate auth in constructor |
1880
+ | `includeComments` | boolean | true | Include method comments |
1881
+ | `tags` | string[] | all | Filter by endpoint tags |
1882
+ | `groupByTags` | boolean | true | Group methods by tag |
1883
+ | `responseStyle` | 'raw' \| 'typed' \| 'both' | 'raw' | Response type style |
1884
+ | `pythonStyle` | string | 'dataclass' | Python body serialization style |
1885
+
1886
+ **Example — TypeScript API client:**
1887
+ ```json
1888
+ {
1889
+ "source": "https://petstore.swagger.io/v2/swagger.json",
1890
+ "language": "typescript",
1891
+ "includeAuth": true
1892
+ }
1893
+ ```
1894
+ → Returns `PetStoreApi.ts` with methods for all endpoints.
1895
+
1896
+ **Example — Python client, filtered by tags:**
1897
+ ```json
1898
+ {
1899
+ "source": "./openapi.yaml",
1900
+ "language": "python",
1901
+ "tags": ["pets"],
1902
+ "pythonStyle": "pydantic"
1903
+ }
1904
+ ```
1905
+ → Returns Python client with only pet-related methods, pydantic body serialization.
1906
+ ```
1907
+
1908
+ ---
1909
+
1910
+ ### Именование файлов (Фаза 2)
1911
+
1912
+ | Спека | TypeScript client | Python client |
1913
+ |---|---|---|
1914
+ | Pet Store API | `PetStoreApi.ts` | `pet_store_api_client.py` |
1915
+ | My Service | `MyServiceApi.ts` | `my_service_api_client.py` |
1916
+
1917
+ ---
1918
+
1919
+ ### Структура файлов проекта (после Фазы 2)
1920
+
1921
+ ```
1922
+ utils/
1923
+ openapi/
1924
+ parser.js # OpenAPIParser (Фаза 1)
1925
+ ref-resolver.js # $ref resolution (Фаза 1)
1926
+ type-mapper.js # type mapping (Фаза 1)
1927
+ method-generator.js # endpoint → method metadata (Фаза 2)
1928
+ auth-generator.js # auth config generation (Фаза 2)
1929
+ api-generators/
1930
+ api-models-typescript.js # TS models (Фаза 1)
1931
+ api-models-python.js # Python models (Фаза 1)
1932
+ api-client-typescript.js # TS API client (Фаза 2)
1933
+ api-client-python.js # Python API client (Фаза 2)
1934
+ ```
1935
+
1936
+ ---
1937
+
1938
+ ### Верификация (Фаза 2)
1939
+
1940
+ #### Функциональные тесты
1941
+
1942
+ 1. **Petstore 2.0**: загрузить `https://petstore.swagger.io/v2/swagger.json`, сгенерировать TS клиент
1943
+ - Проверить: все endpoints из спеки стали методами
1944
+ - Проверить: path params (`{petId}`) → параметры функции
1945
+ - Проверить: query params (`limit`, `status`) → опциональный params
1946
+ - Проверить: request body (`CreatePetRequest`) → body параметр
1947
+ - Проверить: импорт моделей из models файла
1948
+
1949
+ 2. **Petstore 3.0**: загрузить `https://petstore3.swagger.io/api/v3/openapi.json`, сгенерировать Python клиент
1950
+ - Проверить: все endpoints
1951
+ - Проверить: groupByTags разделяет методы по секциям
1952
+
1953
+ 3. **Auth types**: использовать спеку с разными auth schemes
1954
+ - Bearer → `token` в конструкторе, `Authorization: Bearer ...`
1955
+ - API Key (header) → `apiKey` в конструкторе, кастомный header
1956
+ - API Key (query) → `apiKey` → добавляется во все запросы
1957
+ - Basic → `username + password` → `Authorization: Basic ...`
1958
+ - OAuth2 → `accessToken` → `Authorization: Bearer ...`
1959
+ - Комбинированные → все параметры в одном конструкторе
1960
+
1961
+ 4. **Tag filtering**: `tags: ['pets']` → только методы для тега 'pets'
1962
+
1963
+ 5. **operationId fallback**: спека без operationId → имена генерируются из method+path
1964
+
1965
+ 6. **Python styles**: `pythonStyle: 'pydantic'` → body сериализуется через `.model_dump()`
1966
+
1967
+ 7. **Custom import path**: `modelsImportPath: './models/api-types'` → используется в import
1968
+
1969
+ #### Boundary тесты
1970
+
1971
+ 8. **Пустая спека** (нет endpoints): клиент с пустым классом, не падает
1972
+ 9. **Endpoint без параметров**: `GET /health` → метод без параметров
1973
+ 10. **Endpoint только с path params**: `GET /pets/{id}` → метод с одним обязательным param
1974
+ 11. **Deprecated endpoint**: комментарий `[DEPRECATED]`
1975
+ 12. **Конфликт имён методов**: два endpoint'а с одинаковым operationId → добавить суффикс
1976
+
1977
+ ---
1978
+
1979
+ ## Фаза 3: generateApiTests
1980
+
1981
+ ### Тул 4: `generateApiTests`
1982
+
1983
+ **Назначение**: сгенерировать тестовые скаффолды для API, использующие API-клиент (или raw HTTP calls). Тесты покрывают: happy path, validation errors, auth, status codes.
1984
+
1985
+ **Параметры**:
1986
+ ```js
1987
+ GenerateApiTestsSchema = z.object({
1988
+ source: z.string().describe("URL or file path to OpenAPI spec"),
1989
+ language: z.enum(['typescript', 'python']).describe("Target language/framework"),
1990
+ format: z.enum(['auto', 'json', 'yaml']).optional()
1991
+ .describe("Spec format (default: auto)"),
1992
+ useApiClient: z.boolean().optional()
1993
+ .describe("Use generated API client class (default: true). If false, generates raw HTTP calls"),
1994
+ clientImportPath: z.string().optional()
1995
+ .describe("Import path for API client (default: auto from spec title)"),
1996
+ modelsImportPath: z.string().optional()
1997
+ .describe("Import path for models (default: auto). Only used when useApiClient=false"),
1998
+ tags: z.array(z.string()).optional()
1999
+ .describe("Generate tests only for these tags (default: all)"),
2000
+ testStyle: z.enum(['crud', 'per-endpoint', 'smoke']).optional()
2001
+ .describe("Test generation style (default: 'per-endpoint'). 'crud' groups related CRUD endpoints, 'per-endpoint' one test per endpoint, 'smoke' minimal happy-path only"),
2002
+ includeNegative: z.boolean().optional()
2003
+ .describe("Generate negative test cases: 401, 403, 404, 422 (default: true)"),
2004
+ includeAuth: z.boolean().optional()
2005
+ .describe("Generate auth setup in beforeAll/fixtures (default: true)"),
2006
+ authFromEnv: z.boolean().optional()
2007
+ .describe("Read auth from environment variables (default: true)"),
2008
+ envPrefix: z.string().optional()
2009
+ .describe("Prefix for env vars. E.g. 'PETSTORE' → PETSTORE_TOKEN (default: from spec title)"),
2010
+ })
2011
+ ```
2012
+
2013
+ **Алгоритм**:
2014
+ 1. Загрузить и распарсить спеку
2015
+ 2. Извлечь endpoints, отфильтровать по tags
2016
+ 3. Определить стиль тестов:
2017
+ - `per-endpoint`: один `test()`/`def test_()` на каждый endpoint
2018
+ - `crud`: определить CRUD-группы (resource → GET list, GET by id, POST, PUT, DELETE), сгенерировать последовательные тесты
2019
+ - `smoke`: только happy path для каждого endpoint (минимальные assertions)
2020
+ 4. Для каждого endpoint/группы:
2021
+ a. Сгенерировать happy-path тест (200/201)
2022
+ b. Если `includeNegative`:
2023
+ - 401 Unauthorized (если есть security)
2024
+ - 404 Not Found (для endpoints с path params)
2025
+ - 422 Validation Error (для POST/PUT с request body)
2026
+ 5. Сгенерировать setup (beforeAll / fixture): auth configuration
2027
+ 6. Сгенерировать файл
2028
+
2029
+ **Возвращаемое значение**:
2030
+ ```json
2031
+ {
2032
+ "action": "create_new_file",
2033
+ "suggestedFileName": "pet-store-api.spec.ts",
2034
+ "code": "// ...generated test code...",
2035
+ "testCount": 24,
2036
+ "endpointsCovered": 8,
2037
+ "testBreakdown": {
2038
+ "happyPath": 8,
2039
+ "unauthorized": 6,
2040
+ "notFound": 4,
2041
+ "validationError": 3,
2042
+ "crud": 3
2043
+ },
2044
+ "language": "typescript",
2045
+ "source": "https://petstore.swagger.io/v2/swagger.json",
2046
+ "instruction": "Create file 'pet-store-api.spec.ts'. Make sure API client and models files exist. Set environment variables for auth: PETSTORE_TOKEN, PETSTORE_API_KEY."
2047
+ }
2048
+ ```
2049
+
2050
+ ---
2051
+
2052
+ ### CRUD Detection Algorithm
2053
+
2054
+ #### Определение CRUD-групп
2055
+
2056
+ CRUD-группа — это набор endpoints, работающих с одним ресурсом (одинаковый base path):
2057
+
2058
+ ```
2059
+ Ресурс: /pets
2060
+ - GET /pets → list (Read many)
2061
+ - POST /pets → create (Create)
2062
+ - GET /pets/{petId} → getById (Read one)
2063
+ - PUT /pets/{petId} → update (Update)
2064
+ - DELETE /pets/{petId} → delete (Delete)
2065
+ ```
2066
+
2067
+ **Алгоритм**:
2068
+ ```
2069
+ 1. Для каждого endpoint извлечь base resource:
2070
+ /pets → 'pets'
2071
+ /pets/{petId} → 'pets'
2072
+ /pets/{petId}/tags → 'pets_tags' (sub-resource)
2073
+ /users/{id}/orders → 'users_orders'
2074
+
2075
+ 2. Группировать endpoints по base resource
2076
+
2077
+ 3. Для каждой группы определить CRUD-операции:
2078
+ - Есть GET без path params → list
2079
+ - Есть POST без path params → create
2080
+ - Есть GET с path params → getById
2081
+ - Есть PUT/PATCH с path params → update
2082
+ - Есть DELETE с path params → delete
2083
+
2084
+ 4. Если группа содержит ≥2 CRUD операции → CRUD-группа
2085
+ Иначе → per-endpoint тесты
2086
+ ```
2087
+
2088
+ #### CRUD test sequence
2089
+
2090
+ Для CRUD-группы тесты идут в логическом порядке:
2091
+
2092
+ ```typescript
2093
+ test.describe('Pets CRUD', () => {
2094
+ let createdId: number;
2095
+
2096
+ test('POST /pets - create pet', async () => {
2097
+ const response = await api.createPet({ name: 'Buddy', status: 'available' });
2098
+ expect(response.status()).toBe(201);
2099
+ const body = await response.json();
2100
+ createdId = body.id;
2101
+ expect(body.name).toBe('Buddy');
2102
+ });
2103
+
2104
+ test('GET /pets/{id} - get created pet', async () => {
2105
+ const response = await api.getPet(createdId);
2106
+ expect(response.status()).toBe(200);
2107
+ const body = await response.json();
2108
+ expect(body.id).toBe(createdId);
2109
+ expect(body.name).toBe('Buddy');
2110
+ });
2111
+
2112
+ test('PUT /pets/{id} - update pet', async () => {
2113
+ const response = await api.updatePet(createdId, { name: 'Max', status: 'sold' });
2114
+ expect(response.status()).toBe(200);
2115
+ });
2116
+
2117
+ test('GET /pets - list pets contains updated', async () => {
2118
+ const response = await api.listPets();
2119
+ expect(response.status()).toBe(200);
2120
+ const body = await response.json();
2121
+ expect(body.some(p => p.id === createdId)).toBe(true);
2122
+ });
2123
+
2124
+ test('DELETE /pets/{id} - delete pet', async () => {
2125
+ const response = await api.deletePet(createdId);
2126
+ expect(response.ok()).toBe(true);
2127
+ });
2128
+
2129
+ test('GET /pets/{id} - deleted pet returns 404', async () => {
2130
+ const response = await api.getPet(createdId);
2131
+ expect(response.status()).toBe(404);
2132
+ });
2133
+ });
2134
+ ```
2135
+
2136
+ ```python
2137
+ class TestPetsCrud:
2138
+ created_id = None
2139
+
2140
+ def test_create_pet(self, api):
2141
+ response = api.create_pet(CreatePetRequest(name='Buddy', status=PetStatus.AVAILABLE))
2142
+ assert response.status_code == 201
2143
+ body = response.json()
2144
+ TestPetsCrud.created_id = body['id']
2145
+ assert body['name'] == 'Buddy'
2146
+
2147
+ def test_get_created_pet(self, api):
2148
+ response = api.get_pet(TestPetsCrud.created_id)
2149
+ assert response.status_code == 200
2150
+ body = response.json()
2151
+ assert body['id'] == TestPetsCrud.created_id
2152
+
2153
+ def test_update_pet(self, api):
2154
+ response = api.update_pet(TestPetsCrud.created_id,
2155
+ UpdatePetRequest(name='Max', status=PetStatus.SOLD))
2156
+ assert response.status_code == 200
2157
+
2158
+ def test_list_pets_contains_updated(self, api):
2159
+ response = api.list_pets()
2160
+ assert response.status_code == 200
2161
+ ids = [p['id'] for p in response.json()]
2162
+ assert TestPetsCrud.created_id in ids
2163
+
2164
+ def test_delete_pet(self, api):
2165
+ response = api.delete_pet(TestPetsCrud.created_id)
2166
+ assert response.ok
2167
+
2168
+ def test_deleted_pet_returns_404(self, api):
2169
+ response = api.get_pet(TestPetsCrud.created_id)
2170
+ assert response.status_code == 404
2171
+ ```
2172
+
2173
+ ---
2174
+
2175
+ ### Negative Test Generation
2176
+
2177
+ #### 401 Unauthorized
2178
+
2179
+ Генерируется для endpoints с security requirements:
2180
+ ```typescript
2181
+ test('GET /pets - 401 without auth', async ({ request }) => {
2182
+ const noAuthApi = new PetStoreApi(request, { baseUrl: BASE_URL });
2183
+ const response = await noAuthApi.listPets();
2184
+ expect(response.status()).toBe(401);
2185
+ });
2186
+ ```
2187
+
2188
+ ```python
2189
+ def test_list_pets_unauthorized(self):
2190
+ no_auth_api = PetStoreApi(base_url=BASE_URL)
2191
+ response = no_auth_api.list_pets()
2192
+ assert response.status_code == 401
2193
+ ```
2194
+
2195
+ #### 404 Not Found
2196
+
2197
+ Генерируется для endpoints с path params:
2198
+ ```typescript
2199
+ test('GET /pets/{id} - 404 non-existent', async () => {
2200
+ const response = await api.getPet(999999);
2201
+ expect(response.status()).toBe(404);
2202
+ });
2203
+ ```
2204
+
2205
+ ```python
2206
+ def test_get_pet_not_found(self, api):
2207
+ response = api.get_pet(999999)
2208
+ assert response.status_code == 404
2209
+ ```
2210
+
2211
+ #### 422 Validation Error
2212
+
2213
+ Генерируется для POST/PUT endpoints с required body fields:
2214
+ ```typescript
2215
+ test('POST /pets - 422 missing required fields', async () => {
2216
+ const response = await api.createPet({} as any);
2217
+ expect([400, 422]).toContain(response.status());
2218
+ });
2219
+ ```
2220
+
2221
+ ```python
2222
+ def test_create_pet_validation_error(self, api):
2223
+ response = api.create_pet(CreatePetRequest()) # пустой объект
2224
+ assert response.status_code in (400, 422)
2225
+ ```
2226
+
2227
+ ---
2228
+
2229
+ ### Auth Setup Generation
2230
+
2231
+ #### TypeScript (Playwright)
2232
+
2233
+ ```typescript
2234
+ import { test, expect } from '@playwright/test';
2235
+ import { PetStoreApi } from './PetStoreApi';
2236
+
2237
+ const BASE_URL = process.env.PETSTORE_BASE_URL || 'https://petstore.swagger.io/v2';
2238
+
2239
+ test.describe('Pet Store API', () => {
2240
+ let api: PetStoreApi;
2241
+
2242
+ test.beforeAll(async ({ request }) => {
2243
+ api = new PetStoreApi(request, {
2244
+ baseUrl: BASE_URL,
2245
+ token: process.env.PETSTORE_TOKEN,
2246
+ apiKey: process.env.PETSTORE_API_KEY,
2247
+ });
2248
+ });
2249
+
2250
+ // ... tests ...
2251
+ });
2252
+ ```
2253
+
2254
+ #### Python (pytest)
2255
+
2256
+ ```python
2257
+ import pytest
2258
+ import os
2259
+ from pet_store_api_client import PetStoreApi
2260
+
2261
+ BASE_URL = os.environ.get('PETSTORE_BASE_URL', 'https://petstore.swagger.io/v2')
2262
+
2263
+
2264
+ @pytest.fixture(scope='session')
2265
+ def api():
2266
+ return PetStoreApi(
2267
+ base_url=BASE_URL,
2268
+ token=os.environ.get('PETSTORE_TOKEN'),
2269
+ api_key=os.environ.get('PETSTORE_API_KEY'),
2270
+ )
2271
+
2272
+
2273
+ class TestPetsApi:
2274
+ # ... tests using 'api' fixture ...
2275
+ ```
2276
+
2277
+ #### Environment variable naming
2278
+
2279
+ ```
2280
+ envPrefix (из spec title или параметра) + '_' + AUTH_PARAM
2281
+
2282
+ Pet Store API (envPrefix='PETSTORE'):
2283
+ bearer → PETSTORE_TOKEN
2284
+ apiKey → PETSTORE_API_KEY
2285
+ basic → PETSTORE_USERNAME, PETSTORE_PASSWORD
2286
+ oauth2 → PETSTORE_ACCESS_TOKEN
2287
+ baseUrl → PETSTORE_BASE_URL
2288
+ ```
2289
+
2290
+ ---
2291
+
2292
+ ### Интеграция с API-клиентом (аналог POM)
2293
+
2294
+ #### `useApiClient: true` (default)
2295
+
2296
+ Тесты используют API-клиент:
2297
+ ```typescript
2298
+ const response = await api.listPets({ limit: 10 });
2299
+ expect(response.status()).toBe(200);
2300
+ ```
2301
+
2302
+ #### `useApiClient: false`
2303
+
2304
+ Тесты используют raw HTTP calls (Playwright `request` / Python `requests`):
2305
+ ```typescript
2306
+ const response = await request.get(`${BASE_URL}/pets?limit=10`, {
2307
+ headers: { 'Authorization': `Bearer ${process.env.PETSTORE_TOKEN}` }
2308
+ });
2309
+ expect(response.status()).toBe(200);
2310
+ ```
2311
+
2312
+ ```python
2313
+ response = requests.get(f'{BASE_URL}/pets', params={'limit': 10},
2314
+ headers={'Authorization': f'Bearer {os.environ["PETSTORE_TOKEN"]}'})
2315
+ assert response.status_code == 200
2316
+ ```
2317
+
2318
+ Переключение контролируется параметром `useApiClient`.
2319
+
2320
+ ---
2321
+
2322
+ ### Изменения по файлам (Фаза 3)
2323
+
2324
+ #### Новые файлы
2325
+
2326
+ ##### 1. `utils/openapi/test-scaffold-generator.js`
2327
+
2328
+ Генератор тестовых скаффолдов — определяет какие тесты генерировать.
2329
+
2330
+ ```js
2331
+ /**
2332
+ * Генератор тестовых скаффолдов из OpenAPI endpoints
2333
+ */
2334
+ export class TestScaffoldGenerator {
2335
+ /**
2336
+ * Сгенерировать план тестов для endpoint
2337
+ * @param {Object} endpoint - endpoint из parser
2338
+ * @param {Object} options - { includeNegative, testStyle }
2339
+ * @returns {Array<Object>} - массив тестовых кейсов
2340
+ */
2341
+ static generateTestCases(endpoint, options = {}) {
2342
+ const cases = [];
2343
+
2344
+ // 1. Happy path
2345
+ cases.push(this.happyPathCase(endpoint));
2346
+
2347
+ if (options.includeNegative) {
2348
+ // 2. 401 если есть security
2349
+ if (endpoint.security && endpoint.security.length > 0) {
2350
+ cases.push(this.unauthorizedCase(endpoint));
2351
+ }
2352
+
2353
+ // 3. 404 если есть path params
2354
+ if (endpoint.parameters?.some(p => p.in === 'path')) {
2355
+ cases.push(this.notFoundCase(endpoint));
2356
+ }
2357
+
2358
+ // 4. 422 если POST/PUT/PATCH с required body
2359
+ if (['POST', 'PUT', 'PATCH'].includes(endpoint.method) && endpoint.requestBody?.required) {
2360
+ cases.push(this.validationErrorCase(endpoint));
2361
+ }
2362
+ }
2363
+
2364
+ return cases;
2365
+ }
2366
+
2367
+ /**
2368
+ * Определить CRUD-группы из списка endpoints
2369
+ * @param {Array} endpoints
2370
+ * @returns {Map<string, Object>} - resourceName → { list, create, getById, update, delete }
2371
+ */
2372
+ static detectCrudGroups(endpoints) {
2373
+ const groups = new Map();
2374
+
2375
+ for (const ep of endpoints) {
2376
+ const resource = this.extractResource(ep.path);
2377
+ if (!groups.has(resource)) {
2378
+ groups.set(resource, { resource, endpoints: {}, path: '' });
2379
+ }
2380
+ const group = groups.get(resource);
2381
+ const hasPathParam = ep.parameters?.some(p => p.in === 'path');
2382
+
2383
+ switch (ep.method) {
2384
+ case 'GET':
2385
+ if (hasPathParam) group.endpoints.getById = ep;
2386
+ else group.endpoints.list = ep;
2387
+ break;
2388
+ case 'POST':
2389
+ if (!hasPathParam) group.endpoints.create = ep;
2390
+ break;
2391
+ case 'PUT':
2392
+ case 'PATCH':
2393
+ if (hasPathParam) group.endpoints.update = ep;
2394
+ break;
2395
+ case 'DELETE':
2396
+ if (hasPathParam) group.endpoints.delete = ep;
2397
+ break;
2398
+ }
2399
+ }
2400
+
2401
+ // Фильтровать: только группы с ≥2 CRUD операциями
2402
+ const crudGroups = new Map();
2403
+ for (const [name, group] of groups) {
2404
+ if (Object.keys(group.endpoints).length >= 2) {
2405
+ crudGroups.set(name, group);
2406
+ }
2407
+ }
2408
+ return crudGroups;
2409
+ }
2410
+
2411
+ /**
2412
+ * Извлечь имя ресурса из path
2413
+ * /pets → 'pets'
2414
+ * /pets/{petId} → 'pets'
2415
+ * /users/{id}/orders → 'users_orders'
2416
+ */
2417
+ static extractResource(path) { ... }
2418
+
2419
+ /**
2420
+ * Сгенерировать happy-path test case
2421
+ */
2422
+ static happyPathCase(endpoint) {
2423
+ const successCode = this.getSuccessCode(endpoint);
2424
+ return {
2425
+ type: 'happy_path',
2426
+ name: `${endpoint.method} ${endpoint.path} - ${successCode} success`,
2427
+ endpoint,
2428
+ expectedStatus: successCode,
2429
+ sampleParams: this.generateSampleParams(endpoint),
2430
+ sampleBody: this.generateSampleBody(endpoint),
2431
+ assertions: this.generateAssertions(endpoint, successCode)
2432
+ };
2433
+ }
2434
+
2435
+ /** Определить success status code из responses */
2436
+ static getSuccessCode(endpoint) {
2437
+ if (endpoint.responses['200']) return 200;
2438
+ if (endpoint.responses['201']) return 201;
2439
+ if (endpoint.responses['204']) return 204;
2440
+ const first2xx = Object.keys(endpoint.responses).find(s => s.startsWith('2'));
2441
+ return first2xx ? parseInt(first2xx) : 200;
2442
+ }
2443
+
2444
+ /** Сгенерировать sample параметры для тестов */
2445
+ static generateSampleParams(endpoint) {
2446
+ const params = {};
2447
+ for (const p of endpoint.parameters || []) {
2448
+ if (p.in === 'path') {
2449
+ params[p.name] = p.type === 'integer' ? 1 : 'test';
2450
+ } else if (p.in === 'query') {
2451
+ if (p.required) {
2452
+ params[p.name] = this.getSampleValue(p);
2453
+ }
2454
+ }
2455
+ }
2456
+ return params;
2457
+ }
2458
+
2459
+ /** Сгенерировать sample body для POST/PUT */
2460
+ static generateSampleBody(endpoint) { ... }
2461
+
2462
+ /** Сгенерировать assertions */
2463
+ static generateAssertions(endpoint, status) { ... }
2464
+
2465
+ /** Получить sample value для типа */
2466
+ static getSampleValue(param) {
2467
+ if (param.enum) return param.enum[0];
2468
+ switch (param.type) {
2469
+ case 'integer': return 10;
2470
+ case 'number': return 10.5;
2471
+ case 'boolean': return true;
2472
+ case 'string': return 'test';
2473
+ default: return 'test';
2474
+ }
2475
+ }
2476
+
2477
+ static unauthorizedCase(endpoint) { ... }
2478
+ static notFoundCase(endpoint) { ... }
2479
+ static validationErrorCase(endpoint) { ... }
2480
+ }
2481
+ ```
2482
+
2483
+ ##### 2. `utils/api-generators/api-tests-typescript.js`
2484
+
2485
+ Генератор TypeScript API-тестов (Playwright test).
2486
+
2487
+ ```js
2488
+ import { TestScaffoldGenerator } from '../openapi/test-scaffold-generator.js';
2489
+ import { MethodGenerator } from '../openapi/method-generator.js';
2490
+
2491
+ export class ApiTestsTypeScriptGenerator {
2492
+ constructor(endpoints, securitySchemes, options = {}) {
2493
+ this.endpoints = endpoints;
2494
+ this.securitySchemes = securitySchemes;
2495
+ this.options = {
2496
+ useApiClient: true,
2497
+ clientImportPath: null,
2498
+ modelsImportPath: null,
2499
+ includeNegative: true,
2500
+ includeAuth: true,
2501
+ authFromEnv: true,
2502
+ envPrefix: null,
2503
+ testStyle: 'per-endpoint', // 'crud' | 'per-endpoint' | 'smoke'
2504
+ includeComments: true,
2505
+ ...options
2506
+ };
2507
+ }
2508
+
2509
+ /**
2510
+ * Сгенерировать весь тестовый файл
2511
+ */
2512
+ generate(metadata = {}) {
2513
+ const lines = [];
2514
+ const envPrefix = this.options.envPrefix || this.titleToEnvPrefix(metadata.title);
2515
+
2516
+ // 1. Imports
2517
+ lines.push(...this.generateImports(metadata));
2518
+ lines.push('');
2519
+
2520
+ // 2. Constants
2521
+ lines.push(`const BASE_URL = process.env.${envPrefix}_BASE_URL || '${metadata.baseUrl}';`);
2522
+ lines.push('');
2523
+
2524
+ // 3. Test describe block
2525
+ lines.push(`test.describe('${metadata.title || 'API'} Tests', () => {`);
2526
+
2527
+ // 4. Setup
2528
+ if (this.options.useApiClient) {
2529
+ lines.push(...this.generateApiClientSetup(metadata, envPrefix));
2530
+ }
2531
+ lines.push('');
2532
+
2533
+ // 5. Tests based on style
2534
+ if (this.options.testStyle === 'crud') {
2535
+ lines.push(...this.generateCrudTests(metadata));
2536
+ } else if (this.options.testStyle === 'smoke') {
2537
+ lines.push(...this.generateSmokeTests());
2538
+ } else {
2539
+ lines.push(...this.generatePerEndpointTests());
2540
+ }
2541
+
2542
+ // 6. Close describe
2543
+ lines.push('});');
2544
+
2545
+ return lines.join('\n');
2546
+ }
2547
+
2548
+ /** Imports */
2549
+ generateImports(metadata) {
2550
+ const lines = ["import { test, expect } from '@playwright/test';"];
2551
+ if (this.options.useApiClient) {
2552
+ const clientPath = this.options.clientImportPath || this.getClientImportPath(metadata.title);
2553
+ const className = this.getClientClassName(metadata.title);
2554
+ lines.push(`import { ${className} } from '${clientPath}';`);
2555
+ }
2556
+ return lines;
2557
+ }
2558
+
2559
+ /** beforeAll с API client setup */
2560
+ generateApiClientSetup(metadata, envPrefix) {
2561
+ const className = this.getClientClassName(metadata.title);
2562
+ const varName = className.charAt(0).toLowerCase() + className.slice(1);
2563
+ const lines = [
2564
+ ` let ${varName}: ${className};`,
2565
+ '',
2566
+ ' test.beforeAll(async ({ request }) => {',
2567
+ ` ${varName} = new ${className}(request, {`,
2568
+ ` baseUrl: BASE_URL,`,
2569
+ ];
2570
+
2571
+ // Auth env vars
2572
+ for (const scheme of Object.values(this.securitySchemes || {})) {
2573
+ if (scheme.type === 'http' && scheme.scheme === 'bearer') {
2574
+ lines.push(` token: process.env.${envPrefix}_TOKEN,`);
2575
+ } else if (scheme.type === 'apiKey') {
2576
+ lines.push(` apiKey: process.env.${envPrefix}_API_KEY,`);
2577
+ } else if (scheme.type === 'http' && scheme.scheme === 'basic') {
2578
+ lines.push(` username: process.env.${envPrefix}_USERNAME,`);
2579
+ lines.push(` password: process.env.${envPrefix}_PASSWORD,`);
2580
+ } else if (scheme.type === 'oauth2') {
2581
+ lines.push(` accessToken: process.env.${envPrefix}_ACCESS_TOKEN,`);
2582
+ }
2583
+ }
2584
+
2585
+ lines.push(' });');
2586
+ lines.push(' });');
2587
+ return lines;
2588
+ }
2589
+
2590
+ /** Тесты per-endpoint */
2591
+ generatePerEndpointTests() {
2592
+ const lines = [];
2593
+ for (const ep of this.endpoints) {
2594
+ const cases = TestScaffoldGenerator.generateTestCases(ep, {
2595
+ includeNegative: this.options.includeNegative
2596
+ });
2597
+ for (const tc of cases) {
2598
+ lines.push(...this.generateTest(tc));
2599
+ lines.push('');
2600
+ }
2601
+ }
2602
+ return lines;
2603
+ }
2604
+
2605
+ /** CRUD тесты */
2606
+ generateCrudTests(metadata) {
2607
+ const lines = [];
2608
+
2609
+ // Определить CRUD-группы
2610
+ const crudGroups = TestScaffoldGenerator.detectCrudGroups(this.endpoints);
2611
+
2612
+ // Для каждой CRUD-группы
2613
+ for (const [resource, group] of crudGroups) {
2614
+ lines.push(` test.describe('${this.capitalize(resource)} CRUD', () => {`);
2615
+ lines.push(` let createdId: number | string;`);
2616
+ lines.push('');
2617
+
2618
+ // Create → Read → Update → List → Delete → Verify Deleted
2619
+ if (group.endpoints.create) {
2620
+ lines.push(...this.generateCrudCreate(group.endpoints.create));
2621
+ }
2622
+ if (group.endpoints.getById) {
2623
+ lines.push(...this.generateCrudGetById(group.endpoints.getById));
2624
+ }
2625
+ if (group.endpoints.update) {
2626
+ lines.push(...this.generateCrudUpdate(group.endpoints.update));
2627
+ }
2628
+ if (group.endpoints.list) {
2629
+ lines.push(...this.generateCrudList(group.endpoints.list));
2630
+ }
2631
+ if (group.endpoints.delete) {
2632
+ lines.push(...this.generateCrudDelete(group.endpoints.delete));
2633
+ // Verify deleted
2634
+ if (group.endpoints.getById) {
2635
+ lines.push(...this.generateCrudVerifyDeleted(group.endpoints.getById));
2636
+ }
2637
+ }
2638
+
2639
+ lines.push(' });');
2640
+ lines.push('');
2641
+ }
2642
+
2643
+ // Endpoints вне CRUD-групп — per-endpoint
2644
+ const crudEndpoints = new Set();
2645
+ for (const group of crudGroups.values()) {
2646
+ for (const ep of Object.values(group.endpoints)) {
2647
+ crudEndpoints.add(`${ep.method} ${ep.path}`);
2648
+ }
2649
+ }
2650
+ const remaining = this.endpoints.filter(ep => !crudEndpoints.has(`${ep.method} ${ep.path}`));
2651
+ if (remaining.length > 0) {
2652
+ for (const ep of remaining) {
2653
+ const cases = TestScaffoldGenerator.generateTestCases(ep, { includeNegative: this.options.includeNegative });
2654
+ for (const tc of cases) {
2655
+ lines.push(...this.generateTest(tc));
2656
+ lines.push('');
2657
+ }
2658
+ }
2659
+ }
2660
+
2661
+ return lines;
2662
+ }
2663
+
2664
+ /** Smoke тесты (только happy path) */
2665
+ generateSmokeTests() {
2666
+ const lines = [];
2667
+ for (const ep of this.endpoints) {
2668
+ const tc = TestScaffoldGenerator.happyPathCase(ep);
2669
+ lines.push(...this.generateTest(tc));
2670
+ lines.push('');
2671
+ }
2672
+ return lines;
2673
+ }
2674
+
2675
+ /** Генерировать один test() */
2676
+ generateTest(testCase) { ... }
2677
+
2678
+ /** CRUD helpers */
2679
+ generateCrudCreate(endpoint) { ... }
2680
+ generateCrudGetById(endpoint) { ... }
2681
+ generateCrudUpdate(endpoint) { ... }
2682
+ generateCrudList(endpoint) { ... }
2683
+ generateCrudDelete(endpoint) { ... }
2684
+ generateCrudVerifyDeleted(endpoint) { ... }
2685
+
2686
+ /** Utilities */
2687
+ getClientClassName(title) { ... }
2688
+ getClientImportPath(title) { ... }
2689
+ titleToEnvPrefix(title) { ... }
2690
+ capitalize(str) { ... }
2691
+ }
2692
+ ```
2693
+
2694
+ ##### 3. `utils/api-generators/api-tests-python.js`
2695
+
2696
+ Генератор Python API-тестов (pytest + requests).
2697
+
2698
+ ```js
2699
+ import { TestScaffoldGenerator } from '../openapi/test-scaffold-generator.js';
2700
+ import { MethodGenerator } from '../openapi/method-generator.js';
2701
+
2702
+ export class ApiTestsPythonGenerator {
2703
+ constructor(endpoints, securitySchemes, options = {}) {
2704
+ this.endpoints = endpoints;
2705
+ this.securitySchemes = securitySchemes;
2706
+ this.options = {
2707
+ useApiClient: true,
2708
+ clientImportPath: null,
2709
+ modelsImportPath: null,
2710
+ includeNegative: true,
2711
+ includeAuth: true,
2712
+ authFromEnv: true,
2713
+ envPrefix: null,
2714
+ testStyle: 'per-endpoint',
2715
+ includeComments: true,
2716
+ pythonStyle: 'dataclass',
2717
+ ...options
2718
+ };
2719
+ }
2720
+
2721
+ /**
2722
+ * Сгенерировать весь тестовый файл
2723
+ */
2724
+ generate(metadata = {}) {
2725
+ const lines = [];
2726
+ const envPrefix = this.options.envPrefix || this.titleToEnvPrefix(metadata.title);
2727
+
2728
+ // 1. Imports
2729
+ lines.push(...this.generateImports(metadata));
2730
+ lines.push('');
2731
+
2732
+ // 2. Constants
2733
+ lines.push(`BASE_URL = os.environ.get('${envPrefix}_BASE_URL', '${metadata.baseUrl}')`);
2734
+ lines.push('');
2735
+ lines.push('');
2736
+
2737
+ // 3. Fixture
2738
+ if (this.options.useApiClient) {
2739
+ lines.push(...this.generateFixture(metadata, envPrefix));
2740
+ lines.push('');
2741
+ lines.push('');
2742
+ }
2743
+
2744
+ // 4. Tests based on style
2745
+ if (this.options.testStyle === 'crud') {
2746
+ lines.push(...this.generateCrudTests(metadata));
2747
+ } else if (this.options.testStyle === 'smoke') {
2748
+ lines.push(...this.generateSmokeTests());
2749
+ } else {
2750
+ lines.push(...this.generatePerEndpointTests());
2751
+ }
2752
+
2753
+ return lines.join('\n');
2754
+ }
2755
+
2756
+ /** Imports */
2757
+ generateImports(metadata) {
2758
+ const lines = [
2759
+ 'import pytest',
2760
+ 'import os',
2761
+ ];
2762
+ if (this.options.useApiClient) {
2763
+ const importPath = this.options.clientImportPath || this.getClientImportPath(metadata.title);
2764
+ const className = this.getClientClassName(metadata.title);
2765
+ lines.push(`from ${importPath} import ${className}`);
2766
+ } else {
2767
+ lines.push('import requests');
2768
+ }
2769
+ return lines;
2770
+ }
2771
+
2772
+ /** pytest fixture */
2773
+ generateFixture(metadata, envPrefix) {
2774
+ const className = this.getClientClassName(metadata.title);
2775
+ const lines = [
2776
+ "@pytest.fixture(scope='session')",
2777
+ 'def api():',
2778
+ ` return ${className}(`,
2779
+ ` base_url=BASE_URL,`,
2780
+ ];
2781
+
2782
+ for (const scheme of Object.values(this.securitySchemes || {})) {
2783
+ if (scheme.type === 'http' && scheme.scheme === 'bearer') {
2784
+ lines.push(` token=os.environ.get('${envPrefix}_TOKEN'),`);
2785
+ } else if (scheme.type === 'apiKey') {
2786
+ lines.push(` api_key=os.environ.get('${envPrefix}_API_KEY'),`);
2787
+ } else if (scheme.type === 'http' && scheme.scheme === 'basic') {
2788
+ lines.push(` username=os.environ.get('${envPrefix}_USERNAME'),`);
2789
+ lines.push(` password=os.environ.get('${envPrefix}_PASSWORD'),`);
2790
+ } else if (scheme.type === 'oauth2') {
2791
+ lines.push(` access_token=os.environ.get('${envPrefix}_ACCESS_TOKEN'),`);
2792
+ }
2793
+ }
2794
+
2795
+ lines.push(' )');
2796
+ return lines;
2797
+ }
2798
+
2799
+ /** Per-endpoint тесты (класс на тег или flat) */
2800
+ generatePerEndpointTests() {
2801
+ const lines = [];
2802
+ // Группировка по тегам → class TestPetsApi / class TestUsersApi
2803
+ const grouped = this.groupByTag(this.endpoints);
2804
+
2805
+ for (const [tag, endpoints] of Object.entries(grouped)) {
2806
+ const className = `Test${this.capitalize(tag)}Api`;
2807
+ lines.push(`class ${className}:`);
2808
+
2809
+ for (const ep of endpoints) {
2810
+ const cases = TestScaffoldGenerator.generateTestCases(ep, {
2811
+ includeNegative: this.options.includeNegative
2812
+ });
2813
+ for (const tc of cases) {
2814
+ lines.push(...this.generateTest(tc));
2815
+ lines.push('');
2816
+ }
2817
+ }
2818
+ lines.push('');
2819
+ }
2820
+ return lines;
2821
+ }
2822
+
2823
+ /** CRUD тесты */
2824
+ generateCrudTests(metadata) {
2825
+ // Аналогично TypeScript, но с Python синтаксисом
2826
+ // Используется class TestResourceCrud: с class-level переменными
2827
+ ...
2828
+ }
2829
+
2830
+ /** Smoke тесты */
2831
+ generateSmokeTests() { ... }
2832
+
2833
+ /** Генерировать один def test_() */
2834
+ generateTest(testCase) { ... }
2835
+
2836
+ /** Utilities */
2837
+ getClientClassName(title) { ... }
2838
+ getClientImportPath(title) { ... }
2839
+ titleToEnvPrefix(title) { ... }
2840
+ capitalize(str) { ... }
2841
+ groupByTag(endpoints) { ... }
2842
+ }
2843
+ ```
2844
+
2845
+ #### Изменяемые файлы
2846
+
2847
+ ##### 4. `server/tool-schemas.js`
2848
+
2849
+ Добавить:
2850
+ ```js
2851
+ export const GenerateApiTestsSchema = z.object({
2852
+ source: z.string().describe("URL or file path to OpenAPI spec"),
2853
+ language: z.enum(['typescript', 'python']).describe("Target language/framework"),
2854
+ format: z.enum(['auto', 'json', 'yaml']).optional()
2855
+ .describe("Spec format (default: auto)"),
2856
+ useApiClient: z.boolean().optional()
2857
+ .describe("Use generated API client (default: true). False = raw HTTP calls"),
2858
+ clientImportPath: z.string().optional()
2859
+ .describe("Import path for API client (default: auto)"),
2860
+ modelsImportPath: z.string().optional()
2861
+ .describe("Import path for models (default: auto). Used when useApiClient=false"),
2862
+ tags: z.array(z.string()).optional()
2863
+ .describe("Test only these tags (default: all)"),
2864
+ testStyle: z.enum(['crud', 'per-endpoint', 'smoke']).optional()
2865
+ .describe("'crud' groups CRUD ops, 'per-endpoint' one test each, 'smoke' happy-path only (default: 'per-endpoint')"),
2866
+ includeNegative: z.boolean().optional()
2867
+ .describe("Generate 401/404/422 tests (default: true)"),
2868
+ includeAuth: z.boolean().optional()
2869
+ .describe("Generate auth setup (default: true)"),
2870
+ authFromEnv: z.boolean().optional()
2871
+ .describe("Auth from env vars (default: true)"),
2872
+ envPrefix: z.string().optional()
2873
+ .describe("Env var prefix, e.g. 'PETSTORE' → PETSTORE_TOKEN (default: from title)"),
2874
+ pythonStyle: z.enum(['dataclass', 'pydantic', 'typeddict']).optional()
2875
+ .describe("Python only: model style for test data (default: 'dataclass')"),
2876
+ });
2877
+ ```
2878
+
2879
+ ##### 5. `server/tool-definitions.js`
2880
+
2881
+ Добавить:
2882
+ ```js
2883
+ {
2884
+ name: "generateApiTests",
2885
+ description: "Generate API test scaffolds from OpenAPI/Swagger spec. Creates test files with happy-path and negative tests for each endpoint. Supports CRUD grouping, auth setup, and both API-client and raw HTTP styles. Use after generateApiClient for best results.",
2886
+ inputSchema: GenerateApiTestsSchema
2887
+ }
2888
+ ```
2889
+
2890
+ ##### 6. `index.js`
2891
+
2892
+ Добавить хендлер:
2893
+ ```js
2894
+ if (name === "generateApiTests") {
2895
+ const parser = await OpenAPIParser.load(args.source, args.format);
2896
+ const endpoints = parser.getEndpoints();
2897
+ const securitySchemes = parser.getSecuritySchemes();
2898
+ const metadata = {
2899
+ title: parser.spec.info?.title || '',
2900
+ baseUrl: parser.getBaseUrl(),
2901
+ version: parser.version
2902
+ };
2903
+
2904
+ // Фильтрация по тегам
2905
+ let filteredEndpoints = endpoints;
2906
+ if (args.tags) {
2907
+ filteredEndpoints = endpoints.filter(ep =>
2908
+ ep.tags.some(tag => args.tags.includes(tag))
2909
+ );
2910
+ }
2911
+
2912
+ // Выбрать генератор
2913
+ let generator;
2914
+ const genOptions = {
2915
+ useApiClient: args.useApiClient ?? true,
2916
+ clientImportPath: args.clientImportPath,
2917
+ modelsImportPath: args.modelsImportPath,
2918
+ includeNegative: args.includeNegative ?? true,
2919
+ includeAuth: args.includeAuth ?? true,
2920
+ authFromEnv: args.authFromEnv ?? true,
2921
+ envPrefix: args.envPrefix,
2922
+ testStyle: args.testStyle || 'per-endpoint',
2923
+ };
2924
+
2925
+ if (args.language === 'typescript') {
2926
+ generator = new ApiTestsTypeScriptGenerator(filteredEndpoints, securitySchemes, genOptions);
2927
+ } else {
2928
+ generator = new ApiTestsPythonGenerator(filteredEndpoints, securitySchemes, {
2929
+ ...genOptions,
2930
+ pythonStyle: args.pythonStyle || 'dataclass',
2931
+ });
2932
+ }
2933
+
2934
+ const code = generator.generate(metadata);
2935
+ const envPrefix = genOptions.envPrefix || generator.titleToEnvPrefix(metadata.title);
2936
+
2937
+ // Подсчитать тесты
2938
+ let testCount = 0;
2939
+ const breakdown = { happyPath: 0, unauthorized: 0, notFound: 0, validationError: 0 };
2940
+ for (const ep of filteredEndpoints) {
2941
+ const cases = TestScaffoldGenerator.generateTestCases(ep, { includeNegative: genOptions.includeNegative });
2942
+ testCount += cases.length;
2943
+ for (const c of cases) {
2944
+ if (c.type === 'happy_path') breakdown.happyPath++;
2945
+ else if (c.type === 'unauthorized') breakdown.unauthorized++;
2946
+ else if (c.type === 'not_found') breakdown.notFound++;
2947
+ else if (c.type === 'validation_error') breakdown.validationError++;
2948
+ }
2949
+ }
2950
+
2951
+ const suggestedFileName = args.language === 'typescript'
2952
+ ? `${generator.titleToKebab(metadata.title)}.spec.ts`
2953
+ : `test_${generator.titleToSnake(metadata.title)}.py`;
2954
+
2955
+ // Собрать env vars для instruction
2956
+ const envVars = [`${envPrefix}_BASE_URL`];
2957
+ for (const scheme of Object.values(securitySchemes || {})) {
2958
+ if (scheme.type === 'http' && scheme.scheme === 'bearer') envVars.push(`${envPrefix}_TOKEN`);
2959
+ else if (scheme.type === 'apiKey') envVars.push(`${envPrefix}_API_KEY`);
2960
+ else if (scheme.type === 'http' && scheme.scheme === 'basic') {
2961
+ envVars.push(`${envPrefix}_USERNAME`, `${envPrefix}_PASSWORD`);
2962
+ }
2963
+ else if (scheme.type === 'oauth2') envVars.push(`${envPrefix}_ACCESS_TOKEN`);
2964
+ }
2965
+
2966
+ return {
2967
+ content: [{
2968
+ type: 'text',
2969
+ text: JSON.stringify({
2970
+ action: 'create_new_file',
2971
+ suggestedFileName,
2972
+ code,
2973
+ testCount,
2974
+ endpointsCovered: filteredEndpoints.length,
2975
+ testBreakdown: breakdown,
2976
+ language: args.language,
2977
+ source: args.source,
2978
+ instruction: `Create file '${suggestedFileName}'. ${genOptions.useApiClient ? 'Make sure API client and models files exist. ' : ''}Set environment variables: ${envVars.join(', ')}.`
2979
+ }, null, 2)
2980
+ }]
2981
+ };
2982
+ }
2983
+ ```
2984
+
2985
+ ##### 7. `README.md`
2986
+
2987
+ Добавить в секцию API Tools:
2988
+ ```markdown
2989
+ ### generateApiTests
2990
+
2991
+ Generate API test scaffolds from OpenAPI/Swagger spec.
2992
+
2993
+ | Parameter | Type | Default | Description |
2994
+ |-----------|------|---------|-------------|
2995
+ | `source` | string | required | URL or file path to OpenAPI spec |
2996
+ | `language` | 'typescript' \| 'python' | required | Target language |
2997
+ | `format` | 'auto' \| 'json' \| 'yaml' | 'auto' | Spec format |
2998
+ | `useApiClient` | boolean | true | Use API client class vs raw HTTP |
2999
+ | `clientImportPath` | string | auto | Import path for API client |
3000
+ | `tags` | string[] | all | Filter by endpoint tags |
3001
+ | `testStyle` | 'crud' \| 'per-endpoint' \| 'smoke' | 'per-endpoint' | Test grouping style |
3002
+ | `includeNegative` | boolean | true | Generate 401/404/422 tests |
3003
+ | `includeAuth` | boolean | true | Generate auth setup |
3004
+ | `authFromEnv` | boolean | true | Auth from env vars |
3005
+ | `envPrefix` | string | from title | Env var prefix |
3006
+
3007
+ **Example — Full TypeScript test suite:**
3008
+ ```json
3009
+ {
3010
+ "source": "https://petstore.swagger.io/v2/swagger.json",
3011
+ "language": "typescript",
3012
+ "testStyle": "crud",
3013
+ "includeNegative": true
3014
+ }
3015
+ ```
3016
+ → Returns `pet-store-api.spec.ts` with CRUD test sequences and negative cases.
3017
+
3018
+ **Example — Python smoke tests:**
3019
+ ```json
3020
+ {
3021
+ "source": "./openapi.yaml",
3022
+ "language": "python",
3023
+ "testStyle": "smoke",
3024
+ "includeNegative": false,
3025
+ "envPrefix": "MYAPI"
3026
+ }
3027
+ ```
3028
+ → Returns `test_my_api.py` with minimal happy-path tests.
3029
+
3030
+ **Example — Raw HTTP tests (no API client):**
3031
+ ```json
3032
+ {
3033
+ "source": "./openapi.yaml",
3034
+ "language": "typescript",
3035
+ "useApiClient": false,
3036
+ "tags": ["pets"]
3037
+ }
3038
+ ```
3039
+ → Returns tests using raw `request.get()`/`request.post()` instead of API client methods.
3040
+ ```
3041
+
3042
+ ---
3043
+
3044
+ ### Именование файлов (Фаза 3)
3045
+
3046
+ | Спека | TypeScript tests | Python tests |
3047
+ |---|---|---|
3048
+ | Pet Store API | `pet-store-api.spec.ts` | `test_pet_store_api.py` |
3049
+ | My Service | `my-service.spec.ts` | `test_my_service.py` |
3050
+
3051
+ ---
3052
+
3053
+ ### Структура файлов проекта (после всех 3 фаз)
3054
+
3055
+ ```
3056
+ utils/
3057
+ openapi/
3058
+ parser.js # OpenAPIParser (Фаза 1)
3059
+ ref-resolver.js # $ref resolution (Фаза 1)
3060
+ type-mapper.js # type mapping (Фаза 1)
3061
+ method-generator.js # endpoint → method metadata (Фаза 2)
3062
+ auth-generator.js # auth config generation (Фаза 2)
3063
+ test-scaffold-generator.js # test case planning (Фаза 3)
3064
+ api-generators/
3065
+ api-models-typescript.js # TS models (Фаза 1)
3066
+ api-models-python.js # Python models (Фаза 1)
3067
+ api-client-typescript.js # TS API client (Фаза 2)
3068
+ api-client-python.js # Python API client (Фаза 2)
3069
+ api-tests-typescript.js # TS API tests (Фаза 3)
3070
+ api-tests-python.js # Python API tests (Фаза 3)
3071
+ ```
3072
+
3073
+ ---
3074
+
3075
+ ### Верификация (Фаза 3)
3076
+
3077
+ #### Функциональные тесты
3078
+
3079
+ 1. **Per-endpoint TypeScript**: Petstore → тест на каждый endpoint, проверить happy path + negative cases
3080
+ 2. **Per-endpoint Python**: Petstore → pytest с fixture, class-based group by tags
3081
+ 3. **CRUD TypeScript**: Petstore → CRUD-группа для `/pets`, последовательные create→read→update→list→delete→verify
3082
+ 4. **CRUD Python**: аналогично, class-level переменные, pytest
3083
+ 5. **Smoke**: только happy path, минимальные assertions
3084
+ 6. **Auth setup**: проверить env var names для всех auth types
3085
+ 7. **Tag filtering**: `tags: ['pets']` → тесты только для pet endpoints
3086
+ 8. **useApiClient=false**: raw HTTP calls, без import клиента
3087
+
3088
+ #### Boundary тесты
3089
+
3090
+ 9. **Спека без security**: нет auth setup, нет 401 тестов
3091
+ 10. **Endpoint без path params**: нет 404 теста
3092
+ 11. **GET endpoint**: нет validation error теста (нет body)
3093
+ 12. **Пустая спека**: файл с пустым describe/class
3094
+ 13. **Custom envPrefix**: `envPrefix: 'MYAPP'` → `MYAPP_TOKEN`, `MYAPP_BASE_URL`
3095
+ 14. **Custom import paths**: `clientImportPath`, `modelsImportPath` используются в import
3096
+
3097
+ #### Интеграционные тесты
3098
+
3099
+ 15. **Полный пайплайн**: loadSwagger → generateApiModels → generateApiClient → generateApiTests → все файлы совместимы по imports
3100
+ 16. **Python pydantic**: `pythonStyle: 'pydantic'` → body в тестах использует `.model_dump()`
3101
+ 17. **Python typeddict**: body в тестах передаётся как dict