@su-record/vibe 2.0.0 → 2.0.2

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.
Files changed (69) hide show
  1. package/.claude/agents/explorer.md +48 -48
  2. package/.claude/agents/implementer.md +53 -53
  3. package/.claude/agents/searcher.md +54 -54
  4. package/.claude/agents/simplifier.md +119 -119
  5. package/.claude/agents/tester.md +49 -49
  6. package/.claude/commands/vibe.analyze.md +239 -239
  7. package/.claude/commands/vibe.continue.md +88 -88
  8. package/.claude/commands/vibe.diagram.md +178 -178
  9. package/.claude/commands/vibe.reason.md +306 -306
  10. package/.claude/commands/vibe.run.md +760 -760
  11. package/.claude/commands/vibe.spec.md +339 -339
  12. package/.claude/commands/vibe.tool.md +153 -153
  13. package/.claude/commands/vibe.ui.md +137 -137
  14. package/.claude/commands/vibe.verify.md +238 -238
  15. package/.claude/settings.json +152 -152
  16. package/.claude/settings.local.json +4 -57
  17. package/.vibe/config.json +9 -0
  18. package/.vibe/constitution.md +184 -184
  19. package/.vibe/rules/core/communication-guide.md +104 -104
  20. package/.vibe/rules/core/development-philosophy.md +52 -52
  21. package/.vibe/rules/core/quick-start.md +120 -120
  22. package/.vibe/rules/quality/bdd-contract-testing.md +388 -388
  23. package/.vibe/rules/quality/checklist.md +276 -276
  24. package/.vibe/rules/quality/testing-strategy.md +437 -437
  25. package/.vibe/rules/standards/anti-patterns.md +369 -369
  26. package/.vibe/rules/standards/code-structure.md +291 -291
  27. package/.vibe/rules/standards/complexity-metrics.md +312 -312
  28. package/.vibe/rules/standards/naming-conventions.md +198 -198
  29. package/.vibe/rules/tools/mcp-hi-ai-guide.md +665 -665
  30. package/.vibe/rules/tools/mcp-workflow.md +51 -51
  31. package/.vibe/setup.sh +31 -31
  32. package/CLAUDE.md +122 -122
  33. package/LICENSE +21 -21
  34. package/README.md +568 -568
  35. package/dist/cli/index.d.ts.map +1 -1
  36. package/dist/cli/index.js +391 -406
  37. package/dist/cli/index.js.map +1 -1
  38. package/dist/lib/MemoryManager.js +92 -92
  39. package/dist/lib/PythonParser.js +108 -108
  40. package/dist/lib/gemini-mcp.js +15 -15
  41. package/dist/lib/gemini-oauth.d.ts.map +1 -1
  42. package/dist/lib/gemini-oauth.js +41 -38
  43. package/dist/lib/gemini-oauth.js.map +1 -1
  44. package/dist/lib/gpt-mcp.js +17 -17
  45. package/dist/lib/gpt-oauth.d.ts.map +1 -1
  46. package/dist/lib/gpt-oauth.js +50 -45
  47. package/dist/lib/gpt-oauth.js.map +1 -1
  48. package/dist/tools/analytics/getUsageAnalytics.js +12 -12
  49. package/dist/tools/memory/createMemoryTimeline.js +10 -10
  50. package/dist/tools/memory/getMemoryGraph.js +12 -12
  51. package/dist/tools/memory/getSessionContext.js +9 -9
  52. package/dist/tools/memory/linkMemories.js +14 -14
  53. package/dist/tools/memory/listMemories.js +4 -4
  54. package/dist/tools/memory/recallMemory.js +4 -4
  55. package/dist/tools/memory/saveMemory.js +4 -4
  56. package/dist/tools/memory/searchMemoriesAdvanced.js +22 -22
  57. package/dist/tools/planning/generatePrd.js +46 -46
  58. package/dist/tools/prompt/enhancePromptGemini.js +160 -160
  59. package/dist/tools/reasoning/applyReasoningFramework.js +56 -56
  60. package/dist/tools/semantic/analyzeDependencyGraph.js +12 -12
  61. package/package.json +67 -67
  62. package/templates/constitution-template.md +184 -184
  63. package/templates/contract-backend-template.md +517 -517
  64. package/templates/contract-frontend-template.md +594 -594
  65. package/templates/feature-template.md +96 -96
  66. package/templates/hooks-template.json +103 -103
  67. package/templates/spec-template.md +199 -199
  68. package/dist/lib/vibe-mcp.d.ts.map +0 -1
  69. package/dist/lib/vibe-mcp.js.map +0 -1
@@ -1,517 +1,517 @@
1
- # Backend Contract Tests: {기능명}
2
-
3
- **Generated from**: `specs/{기능명}.md` (Section 6: API 계약)
4
- **Framework**: {FastAPI | Django | Express | NestJS}
5
- **Language**: {Python | TypeScript | JavaScript}
6
- **Priority**: {HIGH | MEDIUM | LOW}
7
-
8
- ---
9
-
10
- ## Overview
11
-
12
- Contract Testing은 **API 계약(스키마)을 검증**합니다:
13
- - ✅ Request/Response 스키마 준수
14
- - ✅ 상태 코드 일치
15
- - ✅ 헤더 검증
16
- - ✅ 데이터 타입 및 필수 필드 확인
17
-
18
- **Consumer → Provider 계약 보장** (Pact 패턴)
19
-
20
- ---
21
-
22
- ## API Contracts
23
-
24
- ### Contract 1: {엔드포인트 이름}
25
-
26
- **Endpoint**: `POST /api/v1/{resource}`
27
- **Mapped to**: REQ-001 in SPEC
28
-
29
- #### Request Contract
30
-
31
- ```json
32
- {
33
- "method": "POST",
34
- "path": "/api/v1/{resource}",
35
- "headers": {
36
- "Content-Type": "application/json",
37
- "Authorization": "Bearer {token}"
38
- },
39
- "body": {
40
- "field1": "string (required)",
41
- "field2": "integer (required)",
42
- "field3": "boolean (optional)"
43
- }
44
- }
45
- ```
46
-
47
- **JSON Schema**:
48
- ```json
49
- {
50
- "$schema": "http://json-schema.org/draft-07/schema#",
51
- "type": "object",
52
- "required": ["field1", "field2"],
53
- "properties": {
54
- "field1": {
55
- "type": "string",
56
- "minLength": 1,
57
- "maxLength": 100
58
- },
59
- "field2": {
60
- "type": "integer",
61
- "minimum": 0
62
- },
63
- "field3": {
64
- "type": "boolean",
65
- "default": false
66
- }
67
- },
68
- "additionalProperties": false
69
- }
70
- ```
71
-
72
- #### Response Contract (Success)
73
-
74
- ```json
75
- {
76
- "status": 201,
77
- "headers": {
78
- "Content-Type": "application/json"
79
- },
80
- "body": {
81
- "id": "uuid",
82
- "field1": "string",
83
- "field2": "integer",
84
- "field3": "boolean",
85
- "created_at": "datetime (ISO 8601)"
86
- }
87
- }
88
- ```
89
-
90
- **JSON Schema**:
91
- ```json
92
- {
93
- "$schema": "http://json-schema.org/draft-07/schema#",
94
- "type": "object",
95
- "required": ["id", "field1", "field2", "created_at"],
96
- "properties": {
97
- "id": {
98
- "type": "string",
99
- "format": "uuid"
100
- },
101
- "field1": {
102
- "type": "string"
103
- },
104
- "field2": {
105
- "type": "integer"
106
- },
107
- "field3": {
108
- "type": "boolean"
109
- },
110
- "created_at": {
111
- "type": "string",
112
- "format": "date-time"
113
- }
114
- }
115
- }
116
- ```
117
-
118
- #### Response Contract (Error)
119
-
120
- ```json
121
- {
122
- "status": 400,
123
- "body": {
124
- "error": "string",
125
- "message": "string",
126
- "details": ["array of strings (optional)"]
127
- }
128
- }
129
- ```
130
-
131
- ---
132
-
133
- ## Implementation
134
-
135
- ### Python (FastAPI + Pydantic)
136
-
137
- **File**: `tests/contract/test_{기능명}_contract.py`
138
-
139
- ```python
140
- import pytest
141
- from fastapi.testclient import TestClient
142
- from jsonschema import validate, ValidationError
143
- from app.main import app
144
-
145
- client = TestClient(app)
146
-
147
- # JSON Schema definitions
148
- REQUEST_SCHEMA = {
149
- "$schema": "http://json-schema.org/draft-07/schema#",
150
- "type": "object",
151
- "required": ["field1", "field2"],
152
- "properties": {
153
- "field1": {"type": "string", "minLength": 1, "maxLength": 100},
154
- "field2": {"type": "integer", "minimum": 0},
155
- "field3": {"type": "boolean", "default": False}
156
- },
157
- "additionalProperties": False
158
- }
159
-
160
- RESPONSE_SCHEMA = {
161
- "$schema": "http://json-schema.org/draft-07/schema#",
162
- "type": "object",
163
- "required": ["id", "field1", "field2", "created_at"],
164
- "properties": {
165
- "id": {"type": "string", "format": "uuid"},
166
- "field1": {"type": "string"},
167
- "field2": {"type": "integer"},
168
- "field3": {"type": "boolean"},
169
- "created_at": {"type": "string", "format": "date-time"}
170
- }
171
- }
172
-
173
- class TestCreateResourceContract:
174
- """Contract tests for POST /api/v1/resource"""
175
-
176
- def test_request_schema_valid(self):
177
- """Request body matches contract schema"""
178
- payload = {
179
- "field1": "test value",
180
- "field2": 42,
181
- "field3": True
182
- }
183
- # Should not raise ValidationError
184
- validate(instance=payload, schema=REQUEST_SCHEMA)
185
-
186
- def test_request_schema_invalid_missing_required(self):
187
- """Request with missing required field is rejected"""
188
- payload = {
189
- "field1": "test value"
190
- # Missing field2
191
- }
192
- with pytest.raises(ValidationError):
193
- validate(instance=payload, schema=REQUEST_SCHEMA)
194
-
195
- def test_response_schema_success(self):
196
- """Response body matches contract schema (201 Created)"""
197
- payload = {
198
- "field1": "test value",
199
- "field2": 42,
200
- "field3": True
201
- }
202
- response = client.post(
203
- "/api/v1/resource",
204
- json=payload,
205
- headers={"Authorization": "Bearer test-token"}
206
- )
207
-
208
- # Status code contract
209
- assert response.status_code == 201
210
-
211
- # Response schema contract
212
- response_data = response.json()
213
- validate(instance=response_data, schema=RESPONSE_SCHEMA)
214
-
215
- # Data contract
216
- assert response_data["field1"] == payload["field1"]
217
- assert response_data["field2"] == payload["field2"]
218
- assert response_data["field3"] == payload["field3"]
219
-
220
- def test_response_schema_error(self):
221
- """Error response matches contract schema (400 Bad Request)"""
222
- payload = {
223
- "field1": "", # Invalid: empty string
224
- "field2": -1 # Invalid: negative
225
- }
226
- response = client.post(
227
- "/api/v1/resource",
228
- json=payload,
229
- headers={"Authorization": "Bearer test-token"}
230
- )
231
-
232
- # Status code contract
233
- assert response.status_code == 400
234
-
235
- # Error schema contract
236
- error_data = response.json()
237
- assert "error" in error_data
238
- assert "message" in error_data
239
- assert isinstance(error_data["message"], str)
240
-
241
- def test_headers_contract(self):
242
- """Response headers match contract"""
243
- payload = {
244
- "field1": "test value",
245
- "field2": 42
246
- }
247
- response = client.post(
248
- "/api/v1/resource",
249
- json=payload,
250
- headers={"Authorization": "Bearer test-token"}
251
- )
252
-
253
- assert response.headers["Content-Type"] == "application/json"
254
-
255
- @pytest.mark.parametrize("invalid_payload,expected_error", [
256
- ({"field1": "x" * 101, "field2": 42}, "field1 too long"),
257
- ({"field1": "test", "field2": -1}, "field2 must be positive"),
258
- ({"field2": 42}, "field1 is required"),
259
- ])
260
- def test_validation_errors(self, invalid_payload, expected_error):
261
- """Contract validation errors are properly handled"""
262
- response = client.post(
263
- "/api/v1/resource",
264
- json=invalid_payload,
265
- headers={"Authorization": "Bearer test-token"}
266
- )
267
- assert response.status_code == 400
268
- ```
269
-
270
- **Run**:
271
- ```bash
272
- pytest tests/contract/test_{기능명}_contract.py -v --tb=short
273
- ```
274
-
275
- ---
276
-
277
- ### Python (Pact - Consumer-Driven Contracts)
278
-
279
- **File**: `tests/pact/consumer_test_{기능명}.py`
280
-
281
- ```python
282
- import pytest
283
- from pact import Consumer, Provider, Like, EachLike, Format
284
-
285
- pact = Consumer('FrontendApp').has_pact_with(Provider('BackendAPI'))
286
-
287
- @pytest.fixture(scope='module')
288
- def setup_pact():
289
- pact.start_service()
290
- yield
291
- pact.stop_service()
292
-
293
- def test_create_resource_contract(setup_pact):
294
- """Consumer expects provider to create resource"""
295
- expected = {
296
- 'id': Format().uuid,
297
- 'field1': Like('test value'),
298
- 'field2': Like(42),
299
- 'field3': Like(True),
300
- 'created_at': Format().iso_8601_datetime
301
- }
302
-
303
- (pact
304
- .given('user is authenticated')
305
- .upon_receiving('a request to create resource')
306
- .with_request('POST', '/api/v1/resource',
307
- headers={'Authorization': Like('Bearer token')},
308
- body={
309
- 'field1': 'test value',
310
- 'field2': 42,
311
- 'field3': True
312
- })
313
- .will_respond_with(201, body=expected))
314
-
315
- with pact:
316
- # Test consumer code
317
- result = api_client.create_resource(field1='test value', field2=42)
318
- assert result['id'] is not None
319
- assert result['field1'] == 'test value'
320
- ```
321
-
322
- **Generate Pact file**:
323
- ```bash
324
- pytest tests/pact/ --pact-broker-url=https://your-pact-broker.com
325
- ```
326
-
327
- ---
328
-
329
- ### TypeScript (NestJS + Jest)
330
-
331
- **File**: `test/contract/{기능명}.contract.spec.ts`
332
-
333
- ```typescript
334
- import { Test } from '@nestjs/testing';
335
- import { INestApplication, ValidationPipe } from '@nestjs/common';
336
- import * as request from 'supertest';
337
- import { AppModule } from '../src/app.module';
338
- import Ajv from 'ajv';
339
- import addFormats from 'ajv-formats';
340
-
341
- describe('Create Resource Contract (e2e)', () => {
342
- let app: INestApplication;
343
- const ajv = new Ajv();
344
- addFormats(ajv);
345
-
346
- const requestSchema = {
347
- type: 'object',
348
- required: ['field1', 'field2'],
349
- properties: {
350
- field1: { type: 'string', minLength: 1, maxLength: 100 },
351
- field2: { type: 'integer', minimum: 0 },
352
- field3: { type: 'boolean' }
353
- },
354
- additionalProperties: false
355
- };
356
-
357
- const responseSchema = {
358
- type: 'object',
359
- required: ['id', 'field1', 'field2', 'createdAt'],
360
- properties: {
361
- id: { type: 'string', format: 'uuid' },
362
- field1: { type: 'string' },
363
- field2: { type: 'integer' },
364
- field3: { type: 'boolean' },
365
- createdAt: { type: 'string', format: 'date-time' }
366
- }
367
- };
368
-
369
- beforeAll(async () => {
370
- const moduleFixture = await Test.createTestingModule({
371
- imports: [AppModule],
372
- }).compile();
373
-
374
- app = moduleFixture.createNestApplication();
375
- app.useGlobalPipes(new ValidationPipe());
376
- await app.init();
377
- });
378
-
379
- afterAll(async () => {
380
- await app.close();
381
- });
382
-
383
- it('POST /api/v1/resource - validates request schema', () => {
384
- const payload = {
385
- field1: 'test value',
386
- field2: 42,
387
- field3: true
388
- };
389
-
390
- const validate = ajv.compile(requestSchema);
391
- expect(validate(payload)).toBe(true);
392
- });
393
-
394
- it('POST /api/v1/resource - validates response schema (201)', async () => {
395
- const response = await request(app.getHttpServer())
396
- .post('/api/v1/resource')
397
- .set('Authorization', 'Bearer test-token')
398
- .send({
399
- field1: 'test value',
400
- field2: 42,
401
- field3: true
402
- })
403
- .expect(201)
404
- .expect('Content-Type', /json/);
405
-
406
- const validate = ajv.compile(responseSchema);
407
- expect(validate(response.body)).toBe(true);
408
- });
409
-
410
- it('POST /api/v1/resource - returns 400 for invalid request', async () => {
411
- await request(app.getHttpServer())
412
- .post('/api/v1/resource')
413
- .set('Authorization', 'Bearer test-token')
414
- .send({
415
- field1: '', // Invalid
416
- field2: -1 // Invalid
417
- })
418
- .expect(400);
419
- });
420
- });
421
- ```
422
-
423
- **Run**:
424
- ```bash
425
- npm test -- test/contract/{기능명}.contract.spec.ts
426
- ```
427
-
428
- ---
429
-
430
- ## Contract Testing Strategy
431
-
432
- ### 1. Provider Tests (Backend)
433
- ```bash
434
- # Run all contract tests
435
- pytest tests/contract/ -v
436
-
437
- # Run specific contract
438
- pytest tests/contract/test_{기능명}_contract.py
439
-
440
- # Generate Pact file for consumer
441
- pytest tests/pact/ --pact-broker-url=...
442
- ```
443
-
444
- ### 2. Consumer Tests (Frontend)
445
- ```bash
446
- # Verify against provider contract
447
- npm run test:contract -- --pact-broker-url=...
448
- ```
449
-
450
- ### 3. CI/CD Integration
451
- ```yaml
452
- # .github/workflows/contract-tests.yml
453
- name: Contract Tests
454
-
455
- on: [pull_request]
456
-
457
- jobs:
458
- contract-tests:
459
- runs-on: ubuntu-latest
460
- steps:
461
- - uses: actions/checkout@v2
462
- - name: Run provider contract tests
463
- run: pytest tests/contract/ -v
464
- - name: Publish Pact
465
- run: pytest tests/pact/ --pact-broker-url=${{ secrets.PACT_BROKER_URL }}
466
- ```
467
-
468
- ---
469
-
470
- ## Coverage Mapping
471
-
472
- | Contract | SPEC REQ | Endpoints | Status |
473
- |----------|----------|-----------|--------|
474
- | Create Resource | REQ-001 | POST /api/v1/resource | ⬜ |
475
- | Get Resource | REQ-002 | GET /api/v1/resource/:id | ⬜ |
476
- | Update Resource | REQ-003 | PATCH /api/v1/resource/:id | ⬜ |
477
-
478
- **Coverage**: 0 / {총 계약 수} (0%)
479
-
480
- ---
481
-
482
- ## Best Practices
483
-
484
- 1. **Test Contract, Not Implementation**
485
- - ✅ 스키마 준수 확인
486
- - ❌ 비즈니스 로직 테스트 금지
487
-
488
- 2. **Provider-First vs Consumer-First**
489
- - Provider-First: API 먼저 정의 → Contract 테스트 작성
490
- - Consumer-First: Frontend 요구사항 → Pact 작성 → Provider 구현
491
-
492
- 3. **Version Control**
493
- - API 버전별 Contract 파일 관리
494
- - Breaking Changes 감지
495
-
496
- 4. **Pact Broker 활용**
497
- - Contract 중앙 관리
498
- - Consumer-Provider 매칭
499
- - CI/CD 자동화
500
-
501
- ---
502
-
503
- ## Next Steps
504
-
505
- ```bash
506
- # 1. Contract 테스트 작성
507
- vibe contract "{기능명}"
508
-
509
- # 2. Provider 구현
510
- vibe run "Task 1-1"
511
-
512
- # 3. Contract 검증
513
- vibe test "{기능명}" --contract
514
-
515
- # 4. Pact 발행 (선택)
516
- pytest tests/pact/ --pact-broker-url=...
517
- ```
1
+ # Backend Contract Tests: {기능명}
2
+
3
+ **Generated from**: `specs/{기능명}.md` (Section 6: API 계약)
4
+ **Framework**: {FastAPI | Django | Express | NestJS}
5
+ **Language**: {Python | TypeScript | JavaScript}
6
+ **Priority**: {HIGH | MEDIUM | LOW}
7
+
8
+ ---
9
+
10
+ ## Overview
11
+
12
+ Contract Testing은 **API 계약(스키마)을 검증**합니다:
13
+ - ✅ Request/Response 스키마 준수
14
+ - ✅ 상태 코드 일치
15
+ - ✅ 헤더 검증
16
+ - ✅ 데이터 타입 및 필수 필드 확인
17
+
18
+ **Consumer → Provider 계약 보장** (Pact 패턴)
19
+
20
+ ---
21
+
22
+ ## API Contracts
23
+
24
+ ### Contract 1: {엔드포인트 이름}
25
+
26
+ **Endpoint**: `POST /api/v1/{resource}`
27
+ **Mapped to**: REQ-001 in SPEC
28
+
29
+ #### Request Contract
30
+
31
+ ```json
32
+ {
33
+ "method": "POST",
34
+ "path": "/api/v1/{resource}",
35
+ "headers": {
36
+ "Content-Type": "application/json",
37
+ "Authorization": "Bearer {token}"
38
+ },
39
+ "body": {
40
+ "field1": "string (required)",
41
+ "field2": "integer (required)",
42
+ "field3": "boolean (optional)"
43
+ }
44
+ }
45
+ ```
46
+
47
+ **JSON Schema**:
48
+ ```json
49
+ {
50
+ "$schema": "http://json-schema.org/draft-07/schema#",
51
+ "type": "object",
52
+ "required": ["field1", "field2"],
53
+ "properties": {
54
+ "field1": {
55
+ "type": "string",
56
+ "minLength": 1,
57
+ "maxLength": 100
58
+ },
59
+ "field2": {
60
+ "type": "integer",
61
+ "minimum": 0
62
+ },
63
+ "field3": {
64
+ "type": "boolean",
65
+ "default": false
66
+ }
67
+ },
68
+ "additionalProperties": false
69
+ }
70
+ ```
71
+
72
+ #### Response Contract (Success)
73
+
74
+ ```json
75
+ {
76
+ "status": 201,
77
+ "headers": {
78
+ "Content-Type": "application/json"
79
+ },
80
+ "body": {
81
+ "id": "uuid",
82
+ "field1": "string",
83
+ "field2": "integer",
84
+ "field3": "boolean",
85
+ "created_at": "datetime (ISO 8601)"
86
+ }
87
+ }
88
+ ```
89
+
90
+ **JSON Schema**:
91
+ ```json
92
+ {
93
+ "$schema": "http://json-schema.org/draft-07/schema#",
94
+ "type": "object",
95
+ "required": ["id", "field1", "field2", "created_at"],
96
+ "properties": {
97
+ "id": {
98
+ "type": "string",
99
+ "format": "uuid"
100
+ },
101
+ "field1": {
102
+ "type": "string"
103
+ },
104
+ "field2": {
105
+ "type": "integer"
106
+ },
107
+ "field3": {
108
+ "type": "boolean"
109
+ },
110
+ "created_at": {
111
+ "type": "string",
112
+ "format": "date-time"
113
+ }
114
+ }
115
+ }
116
+ ```
117
+
118
+ #### Response Contract (Error)
119
+
120
+ ```json
121
+ {
122
+ "status": 400,
123
+ "body": {
124
+ "error": "string",
125
+ "message": "string",
126
+ "details": ["array of strings (optional)"]
127
+ }
128
+ }
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Implementation
134
+
135
+ ### Python (FastAPI + Pydantic)
136
+
137
+ **File**: `tests/contract/test_{기능명}_contract.py`
138
+
139
+ ```python
140
+ import pytest
141
+ from fastapi.testclient import TestClient
142
+ from jsonschema import validate, ValidationError
143
+ from app.main import app
144
+
145
+ client = TestClient(app)
146
+
147
+ # JSON Schema definitions
148
+ REQUEST_SCHEMA = {
149
+ "$schema": "http://json-schema.org/draft-07/schema#",
150
+ "type": "object",
151
+ "required": ["field1", "field2"],
152
+ "properties": {
153
+ "field1": {"type": "string", "minLength": 1, "maxLength": 100},
154
+ "field2": {"type": "integer", "minimum": 0},
155
+ "field3": {"type": "boolean", "default": False}
156
+ },
157
+ "additionalProperties": False
158
+ }
159
+
160
+ RESPONSE_SCHEMA = {
161
+ "$schema": "http://json-schema.org/draft-07/schema#",
162
+ "type": "object",
163
+ "required": ["id", "field1", "field2", "created_at"],
164
+ "properties": {
165
+ "id": {"type": "string", "format": "uuid"},
166
+ "field1": {"type": "string"},
167
+ "field2": {"type": "integer"},
168
+ "field3": {"type": "boolean"},
169
+ "created_at": {"type": "string", "format": "date-time"}
170
+ }
171
+ }
172
+
173
+ class TestCreateResourceContract:
174
+ """Contract tests for POST /api/v1/resource"""
175
+
176
+ def test_request_schema_valid(self):
177
+ """Request body matches contract schema"""
178
+ payload = {
179
+ "field1": "test value",
180
+ "field2": 42,
181
+ "field3": True
182
+ }
183
+ # Should not raise ValidationError
184
+ validate(instance=payload, schema=REQUEST_SCHEMA)
185
+
186
+ def test_request_schema_invalid_missing_required(self):
187
+ """Request with missing required field is rejected"""
188
+ payload = {
189
+ "field1": "test value"
190
+ # Missing field2
191
+ }
192
+ with pytest.raises(ValidationError):
193
+ validate(instance=payload, schema=REQUEST_SCHEMA)
194
+
195
+ def test_response_schema_success(self):
196
+ """Response body matches contract schema (201 Created)"""
197
+ payload = {
198
+ "field1": "test value",
199
+ "field2": 42,
200
+ "field3": True
201
+ }
202
+ response = client.post(
203
+ "/api/v1/resource",
204
+ json=payload,
205
+ headers={"Authorization": "Bearer test-token"}
206
+ )
207
+
208
+ # Status code contract
209
+ assert response.status_code == 201
210
+
211
+ # Response schema contract
212
+ response_data = response.json()
213
+ validate(instance=response_data, schema=RESPONSE_SCHEMA)
214
+
215
+ # Data contract
216
+ assert response_data["field1"] == payload["field1"]
217
+ assert response_data["field2"] == payload["field2"]
218
+ assert response_data["field3"] == payload["field3"]
219
+
220
+ def test_response_schema_error(self):
221
+ """Error response matches contract schema (400 Bad Request)"""
222
+ payload = {
223
+ "field1": "", # Invalid: empty string
224
+ "field2": -1 # Invalid: negative
225
+ }
226
+ response = client.post(
227
+ "/api/v1/resource",
228
+ json=payload,
229
+ headers={"Authorization": "Bearer test-token"}
230
+ )
231
+
232
+ # Status code contract
233
+ assert response.status_code == 400
234
+
235
+ # Error schema contract
236
+ error_data = response.json()
237
+ assert "error" in error_data
238
+ assert "message" in error_data
239
+ assert isinstance(error_data["message"], str)
240
+
241
+ def test_headers_contract(self):
242
+ """Response headers match contract"""
243
+ payload = {
244
+ "field1": "test value",
245
+ "field2": 42
246
+ }
247
+ response = client.post(
248
+ "/api/v1/resource",
249
+ json=payload,
250
+ headers={"Authorization": "Bearer test-token"}
251
+ )
252
+
253
+ assert response.headers["Content-Type"] == "application/json"
254
+
255
+ @pytest.mark.parametrize("invalid_payload,expected_error", [
256
+ ({"field1": "x" * 101, "field2": 42}, "field1 too long"),
257
+ ({"field1": "test", "field2": -1}, "field2 must be positive"),
258
+ ({"field2": 42}, "field1 is required"),
259
+ ])
260
+ def test_validation_errors(self, invalid_payload, expected_error):
261
+ """Contract validation errors are properly handled"""
262
+ response = client.post(
263
+ "/api/v1/resource",
264
+ json=invalid_payload,
265
+ headers={"Authorization": "Bearer test-token"}
266
+ )
267
+ assert response.status_code == 400
268
+ ```
269
+
270
+ **Run**:
271
+ ```bash
272
+ pytest tests/contract/test_{기능명}_contract.py -v --tb=short
273
+ ```
274
+
275
+ ---
276
+
277
+ ### Python (Pact - Consumer-Driven Contracts)
278
+
279
+ **File**: `tests/pact/consumer_test_{기능명}.py`
280
+
281
+ ```python
282
+ import pytest
283
+ from pact import Consumer, Provider, Like, EachLike, Format
284
+
285
+ pact = Consumer('FrontendApp').has_pact_with(Provider('BackendAPI'))
286
+
287
+ @pytest.fixture(scope='module')
288
+ def setup_pact():
289
+ pact.start_service()
290
+ yield
291
+ pact.stop_service()
292
+
293
+ def test_create_resource_contract(setup_pact):
294
+ """Consumer expects provider to create resource"""
295
+ expected = {
296
+ 'id': Format().uuid,
297
+ 'field1': Like('test value'),
298
+ 'field2': Like(42),
299
+ 'field3': Like(True),
300
+ 'created_at': Format().iso_8601_datetime
301
+ }
302
+
303
+ (pact
304
+ .given('user is authenticated')
305
+ .upon_receiving('a request to create resource')
306
+ .with_request('POST', '/api/v1/resource',
307
+ headers={'Authorization': Like('Bearer token')},
308
+ body={
309
+ 'field1': 'test value',
310
+ 'field2': 42,
311
+ 'field3': True
312
+ })
313
+ .will_respond_with(201, body=expected))
314
+
315
+ with pact:
316
+ # Test consumer code
317
+ result = api_client.create_resource(field1='test value', field2=42)
318
+ assert result['id'] is not None
319
+ assert result['field1'] == 'test value'
320
+ ```
321
+
322
+ **Generate Pact file**:
323
+ ```bash
324
+ pytest tests/pact/ --pact-broker-url=https://your-pact-broker.com
325
+ ```
326
+
327
+ ---
328
+
329
+ ### TypeScript (NestJS + Jest)
330
+
331
+ **File**: `test/contract/{기능명}.contract.spec.ts`
332
+
333
+ ```typescript
334
+ import { Test } from '@nestjs/testing';
335
+ import { INestApplication, ValidationPipe } from '@nestjs/common';
336
+ import * as request from 'supertest';
337
+ import { AppModule } from '../src/app.module';
338
+ import Ajv from 'ajv';
339
+ import addFormats from 'ajv-formats';
340
+
341
+ describe('Create Resource Contract (e2e)', () => {
342
+ let app: INestApplication;
343
+ const ajv = new Ajv();
344
+ addFormats(ajv);
345
+
346
+ const requestSchema = {
347
+ type: 'object',
348
+ required: ['field1', 'field2'],
349
+ properties: {
350
+ field1: { type: 'string', minLength: 1, maxLength: 100 },
351
+ field2: { type: 'integer', minimum: 0 },
352
+ field3: { type: 'boolean' }
353
+ },
354
+ additionalProperties: false
355
+ };
356
+
357
+ const responseSchema = {
358
+ type: 'object',
359
+ required: ['id', 'field1', 'field2', 'createdAt'],
360
+ properties: {
361
+ id: { type: 'string', format: 'uuid' },
362
+ field1: { type: 'string' },
363
+ field2: { type: 'integer' },
364
+ field3: { type: 'boolean' },
365
+ createdAt: { type: 'string', format: 'date-time' }
366
+ }
367
+ };
368
+
369
+ beforeAll(async () => {
370
+ const moduleFixture = await Test.createTestingModule({
371
+ imports: [AppModule],
372
+ }).compile();
373
+
374
+ app = moduleFixture.createNestApplication();
375
+ app.useGlobalPipes(new ValidationPipe());
376
+ await app.init();
377
+ });
378
+
379
+ afterAll(async () => {
380
+ await app.close();
381
+ });
382
+
383
+ it('POST /api/v1/resource - validates request schema', () => {
384
+ const payload = {
385
+ field1: 'test value',
386
+ field2: 42,
387
+ field3: true
388
+ };
389
+
390
+ const validate = ajv.compile(requestSchema);
391
+ expect(validate(payload)).toBe(true);
392
+ });
393
+
394
+ it('POST /api/v1/resource - validates response schema (201)', async () => {
395
+ const response = await request(app.getHttpServer())
396
+ .post('/api/v1/resource')
397
+ .set('Authorization', 'Bearer test-token')
398
+ .send({
399
+ field1: 'test value',
400
+ field2: 42,
401
+ field3: true
402
+ })
403
+ .expect(201)
404
+ .expect('Content-Type', /json/);
405
+
406
+ const validate = ajv.compile(responseSchema);
407
+ expect(validate(response.body)).toBe(true);
408
+ });
409
+
410
+ it('POST /api/v1/resource - returns 400 for invalid request', async () => {
411
+ await request(app.getHttpServer())
412
+ .post('/api/v1/resource')
413
+ .set('Authorization', 'Bearer test-token')
414
+ .send({
415
+ field1: '', // Invalid
416
+ field2: -1 // Invalid
417
+ })
418
+ .expect(400);
419
+ });
420
+ });
421
+ ```
422
+
423
+ **Run**:
424
+ ```bash
425
+ npm test -- test/contract/{기능명}.contract.spec.ts
426
+ ```
427
+
428
+ ---
429
+
430
+ ## Contract Testing Strategy
431
+
432
+ ### 1. Provider Tests (Backend)
433
+ ```bash
434
+ # Run all contract tests
435
+ pytest tests/contract/ -v
436
+
437
+ # Run specific contract
438
+ pytest tests/contract/test_{기능명}_contract.py
439
+
440
+ # Generate Pact file for consumer
441
+ pytest tests/pact/ --pact-broker-url=...
442
+ ```
443
+
444
+ ### 2. Consumer Tests (Frontend)
445
+ ```bash
446
+ # Verify against provider contract
447
+ npm run test:contract -- --pact-broker-url=...
448
+ ```
449
+
450
+ ### 3. CI/CD Integration
451
+ ```yaml
452
+ # .github/workflows/contract-tests.yml
453
+ name: Contract Tests
454
+
455
+ on: [pull_request]
456
+
457
+ jobs:
458
+ contract-tests:
459
+ runs-on: ubuntu-latest
460
+ steps:
461
+ - uses: actions/checkout@v2
462
+ - name: Run provider contract tests
463
+ run: pytest tests/contract/ -v
464
+ - name: Publish Pact
465
+ run: pytest tests/pact/ --pact-broker-url=${{ secrets.PACT_BROKER_URL }}
466
+ ```
467
+
468
+ ---
469
+
470
+ ## Coverage Mapping
471
+
472
+ | Contract | SPEC REQ | Endpoints | Status |
473
+ |----------|----------|-----------|--------|
474
+ | Create Resource | REQ-001 | POST /api/v1/resource | ⬜ |
475
+ | Get Resource | REQ-002 | GET /api/v1/resource/:id | ⬜ |
476
+ | Update Resource | REQ-003 | PATCH /api/v1/resource/:id | ⬜ |
477
+
478
+ **Coverage**: 0 / {총 계약 수} (0%)
479
+
480
+ ---
481
+
482
+ ## Best Practices
483
+
484
+ 1. **Test Contract, Not Implementation**
485
+ - ✅ 스키마 준수 확인
486
+ - ❌ 비즈니스 로직 테스트 금지
487
+
488
+ 2. **Provider-First vs Consumer-First**
489
+ - Provider-First: API 먼저 정의 → Contract 테스트 작성
490
+ - Consumer-First: Frontend 요구사항 → Pact 작성 → Provider 구현
491
+
492
+ 3. **Version Control**
493
+ - API 버전별 Contract 파일 관리
494
+ - Breaking Changes 감지
495
+
496
+ 4. **Pact Broker 활용**
497
+ - Contract 중앙 관리
498
+ - Consumer-Provider 매칭
499
+ - CI/CD 자동화
500
+
501
+ ---
502
+
503
+ ## Next Steps
504
+
505
+ ```bash
506
+ # 1. Contract 테스트 작성
507
+ vibe contract "{기능명}"
508
+
509
+ # 2. Provider 구현
510
+ vibe run "Task 1-1"
511
+
512
+ # 3. Contract 검증
513
+ vibe test "{기능명}" --contract
514
+
515
+ # 4. Pact 발행 (선택)
516
+ pytest tests/pact/ --pact-broker-url=...
517
+ ```