@su-record/vibe 2.0.0 → 2.0.1

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 (65) 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.js +35 -35
  42. package/dist/lib/gpt-mcp.js +17 -17
  43. package/dist/lib/gpt-oauth.js +44 -44
  44. package/dist/tools/analytics/getUsageAnalytics.js +12 -12
  45. package/dist/tools/memory/createMemoryTimeline.js +10 -10
  46. package/dist/tools/memory/getMemoryGraph.js +12 -12
  47. package/dist/tools/memory/getSessionContext.js +9 -9
  48. package/dist/tools/memory/linkMemories.js +14 -14
  49. package/dist/tools/memory/listMemories.js +4 -4
  50. package/dist/tools/memory/recallMemory.js +4 -4
  51. package/dist/tools/memory/saveMemory.js +4 -4
  52. package/dist/tools/memory/searchMemoriesAdvanced.js +22 -22
  53. package/dist/tools/planning/generatePrd.js +46 -46
  54. package/dist/tools/prompt/enhancePromptGemini.js +160 -160
  55. package/dist/tools/reasoning/applyReasoningFramework.js +56 -56
  56. package/dist/tools/semantic/analyzeDependencyGraph.js +12 -12
  57. package/package.json +67 -67
  58. package/templates/constitution-template.md +184 -184
  59. package/templates/contract-backend-template.md +517 -517
  60. package/templates/contract-frontend-template.md +594 -594
  61. package/templates/feature-template.md +96 -96
  62. package/templates/hooks-template.json +103 -103
  63. package/templates/spec-template.md +199 -199
  64. package/dist/lib/vibe-mcp.d.ts.map +0 -1
  65. package/dist/lib/vibe-mcp.js.map +0 -1
@@ -1,594 +1,594 @@
1
- # Frontend Contract Tests: {기능명}
2
-
3
- **Generated from**: `specs/{기능명}.md` (Section 6: API 계약)
4
- **Framework**: {Flutter | React | React Native | Vue}
5
- **Language**: {Dart | TypeScript | JavaScript}
6
- **Priority**: {HIGH | MEDIUM | LOW}
7
-
8
- ---
9
-
10
- ## Overview
11
-
12
- Frontend Contract Testing은 **Consumer 관점에서 API 계약을 검증**합니다:
13
- - ✅ API 요청이 계약에 맞게 전송되는지
14
- - ✅ API 응답이 예상 스키마를 따르는지
15
- - ✅ 에러 처리가 계약대로 동작하는지
16
- - ✅ Mock 서버로 독립적 테스트 가능
17
-
18
- **Consumer-Driven Contract Testing** (Pact 패턴)
19
-
20
- ---
21
-
22
- ## API Contracts (Consumer View)
23
-
24
- ### Contract 1: Create Resource
25
-
26
- **Consumer Expectation**:
27
- ```json
28
- {
29
- "request": {
30
- "method": "POST",
31
- "path": "/api/v1/resource",
32
- "headers": {
33
- "Authorization": "Bearer {token}",
34
- "Content-Type": "application/json"
35
- },
36
- "body": {
37
- "field1": "string",
38
- "field2": "integer"
39
- }
40
- },
41
- "response": {
42
- "status": 201,
43
- "body": {
44
- "id": "uuid",
45
- "field1": "string",
46
- "field2": "integer",
47
- "createdAt": "datetime"
48
- }
49
- }
50
- }
51
- ```
52
-
53
- ---
54
-
55
- ## Implementation
56
-
57
- ### Flutter (Dart + http_mock_adapter)
58
-
59
- **File**: `test/contract/{기능명}_contract_test.dart`
60
-
61
- ```dart
62
- import 'package:flutter_test/flutter_test.dart';
63
- import 'package:dio/dio.dart';
64
- import 'package:http_mock_adapter/http_mock_adapter.dart';
65
- import 'package:your_app/services/api_service.dart';
66
- import 'package:your_app/models/resource.dart';
67
-
68
- void main() {
69
- late Dio dio;
70
- late DioAdapter dioAdapter;
71
- late ApiService apiService;
72
-
73
- setUp(() {
74
- dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
75
- dioAdapter = DioAdapter(dio: dio);
76
- apiService = ApiService(dio: dio);
77
- });
78
-
79
- group('Create Resource Contract', () {
80
- test('should match request contract', () async {
81
- // Arrange: Expected request contract
82
- final requestBody = {
83
- 'field1': 'test value',
84
- 'field2': 42,
85
- };
86
-
87
- // Arrange: Mock response matching contract
88
- final responseBody = {
89
- 'id': '123e4567-e89b-12d3-a456-426614174000',
90
- 'field1': 'test value',
91
- 'field2': 42,
92
- 'createdAt': '2025-01-17T10:00:00Z',
93
- };
94
-
95
- dioAdapter.onPost(
96
- '/api/v1/resource',
97
- (server) => server.reply(201, responseBody),
98
- data: requestBody,
99
- headers: {
100
- 'Authorization': 'Bearer test-token',
101
- 'Content-Type': 'application/json',
102
- },
103
- );
104
-
105
- // Act: Call API service
106
- final result = await apiService.createResource(
107
- field1: 'test value',
108
- field2: 42,
109
- token: 'test-token',
110
- );
111
-
112
- // Assert: Response matches contract
113
- expect(result, isA<Resource>());
114
- expect(result.id, isNotEmpty);
115
- expect(result.field1, equals('test value'));
116
- expect(result.field2, equals(42));
117
- expect(result.createdAt, isA<DateTime>());
118
- });
119
-
120
- test('should handle error response contract', () async {
121
- // Arrange: Error response contract
122
- final errorBody = {
123
- 'error': 'ValidationError',
124
- 'message': 'field1 is required',
125
- 'details': ['field1 must not be empty'],
126
- };
127
-
128
- dioAdapter.onPost(
129
- '/api/v1/resource',
130
- (server) => server.reply(400, errorBody),
131
- );
132
-
133
- // Act & Assert: Error handling matches contract
134
- expect(
135
- () async => await apiService.createResource(
136
- field1: '',
137
- field2: 42,
138
- token: 'test-token',
139
- ),
140
- throwsA(isA<ApiException>().having(
141
- (e) => e.statusCode,
142
- 'status code',
143
- equals(400),
144
- )),
145
- );
146
- });
147
-
148
- test('should validate response schema', () async {
149
- // Arrange: Response with invalid schema
150
- final invalidResponse = {
151
- 'id': 'not-a-uuid', // Invalid UUID format
152
- 'field1': 123, // Wrong type
153
- // Missing field2
154
- };
155
-
156
- dioAdapter.onPost(
157
- '/api/v1/resource',
158
- (server) => server.reply(201, invalidResponse),
159
- );
160
-
161
- // Act & Assert: Schema validation fails
162
- expect(
163
- () async => await apiService.createResource(
164
- field1: 'test',
165
- field2: 42,
166
- token: 'test-token',
167
- ),
168
- throwsA(isA<SchemaValidationException>()),
169
- );
170
- });
171
- });
172
-
173
- group('Response Schema Validation', () {
174
- test('validates UUID format', () {
175
- final validUuid = '123e4567-e89b-12d3-a456-426614174000';
176
- expect(isValidUuid(validUuid), isTrue);
177
-
178
- final invalidUuid = 'not-a-uuid';
179
- expect(isValidUuid(invalidUuid), isFalse);
180
- });
181
-
182
- test('validates DateTime format (ISO 8601)', () {
183
- final validDateTime = '2025-01-17T10:00:00Z';
184
- expect(() => DateTime.parse(validDateTime), returnsNormally);
185
-
186
- final invalidDateTime = '2025-01-17'; // Missing time
187
- expect(() => DateTime.parse(invalidDateTime), throwsFormatException);
188
- });
189
- });
190
- }
191
-
192
- // Helper function
193
- bool isValidUuid(String uuid) {
194
- final uuidRegex = RegExp(
195
- r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
196
- caseSensitive: false,
197
- );
198
- return uuidRegex.hasMatch(uuid);
199
- }
200
- ```
201
-
202
- **Run**:
203
- ```bash
204
- flutter test test/contract/{기능명}_contract_test.dart
205
- ```
206
-
207
- ---
208
-
209
- ### React (TypeScript + MSW + Zod)
210
-
211
- **File**: `tests/contract/{기능명}.contract.test.ts`
212
-
213
- ```typescript
214
- import { rest } from 'msw';
215
- import { setupServer } from 'msw/node';
216
- import { z } from 'zod';
217
- import { createResource, ApiService } from '@/services/api';
218
-
219
- // Zod schemas for contract validation
220
- const CreateResourceRequestSchema = z.object({
221
- field1: z.string().min(1).max(100),
222
- field2: z.number().int().nonnegative(),
223
- field3: z.boolean().optional(),
224
- });
225
-
226
- const CreateResourceResponseSchema = z.object({
227
- id: z.string().uuid(),
228
- field1: z.string(),
229
- field2: z.number().int(),
230
- field3: z.boolean().optional(),
231
- createdAt: z.string().datetime(),
232
- });
233
-
234
- const ErrorResponseSchema = z.object({
235
- error: z.string(),
236
- message: z.string(),
237
- details: z.array(z.string()).optional(),
238
- });
239
-
240
- // Mock server
241
- const server = setupServer();
242
-
243
- beforeAll(() => server.listen());
244
- afterEach(() => server.resetHandlers());
245
- afterAll(() => server.close());
246
-
247
- describe('Create Resource Contract', () => {
248
- it('should send request matching contract', async () => {
249
- let capturedRequest: any;
250
-
251
- server.use(
252
- rest.post('/api/v1/resource', async (req, res, ctx) => {
253
- capturedRequest = await req.json();
254
-
255
- // Validate request matches contract
256
- const result = CreateResourceRequestSchema.safeParse(capturedRequest);
257
- expect(result.success).toBe(true);
258
-
259
- return res(
260
- ctx.status(201),
261
- ctx.json({
262
- id: '123e4567-e89b-12d3-a456-426614174000',
263
- field1: capturedRequest.field1,
264
- field2: capturedRequest.field2,
265
- field3: capturedRequest.field3 ?? false,
266
- createdAt: new Date().toISOString(),
267
- })
268
- );
269
- })
270
- );
271
-
272
- const result = await createResource({
273
- field1: 'test value',
274
- field2: 42,
275
- field3: true,
276
- });
277
-
278
- // Verify request contract
279
- expect(capturedRequest).toMatchObject({
280
- field1: 'test value',
281
- field2: 42,
282
- field3: true,
283
- });
284
-
285
- // Verify response contract
286
- const responseValidation = CreateResourceResponseSchema.safeParse(result);
287
- expect(responseValidation.success).toBe(true);
288
- });
289
-
290
- it('should handle error response contract', async () => {
291
- server.use(
292
- rest.post('/api/v1/resource', (req, res, ctx) => {
293
- return res(
294
- ctx.status(400),
295
- ctx.json({
296
- error: 'ValidationError',
297
- message: 'field1 is required',
298
- details: ['field1 must not be empty'],
299
- })
300
- );
301
- })
302
- );
303
-
304
- await expect(
305
- createResource({
306
- field1: '',
307
- field2: 42,
308
- })
309
- ).rejects.toThrow();
310
-
311
- // Verify error response matches contract
312
- try {
313
- await createResource({ field1: '', field2: 42 });
314
- } catch (error: any) {
315
- const errorValidation = ErrorResponseSchema.safeParse(error.response.data);
316
- expect(errorValidation.success).toBe(true);
317
- expect(error.response.status).toBe(400);
318
- }
319
- });
320
-
321
- it('should reject response with invalid schema', async () => {
322
- server.use(
323
- rest.post('/api/v1/resource', (req, res, ctx) => {
324
- return res(
325
- ctx.status(201),
326
- ctx.json({
327
- id: 'not-a-uuid', // Invalid UUID
328
- field1: 123, // Wrong type
329
- // Missing field2
330
- })
331
- );
332
- })
333
- );
334
-
335
- await expect(
336
- createResource({
337
- field1: 'test',
338
- field2: 42,
339
- })
340
- ).rejects.toThrow('Schema validation failed');
341
- });
342
-
343
- it('validates response headers', async () => {
344
- let responseHeaders: Headers;
345
-
346
- server.use(
347
- rest.post('/api/v1/resource', (req, res, ctx) => {
348
- return res(
349
- ctx.status(201),
350
- ctx.set('Content-Type', 'application/json'),
351
- ctx.json({
352
- id: '123e4567-e89b-12d3-a456-426614174000',
353
- field1: 'test',
354
- field2: 42,
355
- createdAt: new Date().toISOString(),
356
- })
357
- );
358
- })
359
- );
360
-
361
- const response = await fetch('/api/v1/resource', {
362
- method: 'POST',
363
- body: JSON.stringify({ field1: 'test', field2: 42 }),
364
- });
365
-
366
- expect(response.headers.get('Content-Type')).toBe('application/json');
367
- });
368
- });
369
-
370
- describe('Schema Validation Utilities', () => {
371
- it('validates UUID format', () => {
372
- const validUuid = '123e4567-e89b-12d3-a456-426614174000';
373
- const result = z.string().uuid().safeParse(validUuid);
374
- expect(result.success).toBe(true);
375
-
376
- const invalidUuid = 'not-a-uuid';
377
- const invalidResult = z.string().uuid().safeParse(invalidUuid);
378
- expect(invalidResult.success).toBe(false);
379
- });
380
-
381
- it('validates ISO 8601 datetime', () => {
382
- const validDate = '2025-01-17T10:00:00Z';
383
- const result = z.string().datetime().safeParse(validDate);
384
- expect(result.success).toBe(true);
385
-
386
- const invalidDate = '2025-01-17'; // Missing time
387
- const invalidResult = z.string().datetime().safeParse(invalidDate);
388
- expect(invalidResult.success).toBe(false);
389
- });
390
- });
391
- ```
392
-
393
- **Run**:
394
- ```bash
395
- npm test -- tests/contract/{기능명}.contract.test.ts
396
- ```
397
-
398
- ---
399
-
400
- ### React Native (TypeScript + Axios + MockAdapter)
401
-
402
- **File**: `__tests__/contract/{기능명}.contract.test.ts`
403
-
404
- ```typescript
405
- import axios from 'axios';
406
- import MockAdapter from 'axios-mock-adapter';
407
- import { z } from 'zod';
408
- import { ApiService } from '@/services/api';
409
-
410
- const mock = new MockAdapter(axios);
411
-
412
- const ResponseSchema = z.object({
413
- id: z.string().uuid(),
414
- field1: z.string(),
415
- field2: z.number(),
416
- createdAt: z.string().datetime(),
417
- });
418
-
419
- describe('Create Resource Contract (React Native)', () => {
420
- beforeEach(() => {
421
- mock.reset();
422
- });
423
-
424
- it('should match API contract', async () => {
425
- const requestBody = {
426
- field1: 'test value',
427
- field2: 42,
428
- };
429
-
430
- const responseBody = {
431
- id: '123e4567-e89b-12d3-a456-426614174000',
432
- field1: 'test value',
433
- field2: 42,
434
- createdAt: '2025-01-17T10:00:00Z',
435
- };
436
-
437
- mock.onPost('/api/v1/resource', requestBody).reply(201, responseBody);
438
-
439
- const apiService = new ApiService(axios);
440
- const result = await apiService.createResource(requestBody);
441
-
442
- // Validate response schema
443
- const validation = ResponseSchema.safeParse(result);
444
- expect(validation.success).toBe(true);
445
- });
446
- });
447
- ```
448
-
449
- **Run**:
450
- ```bash
451
- npm test -- __tests__/contract/
452
- ```
453
-
454
- ---
455
-
456
- ## Pact Consumer Tests
457
-
458
- ### Flutter (dart_pact)
459
-
460
- **File**: `test/pact/{기능명}_pact_test.dart`
461
-
462
- ```dart
463
- import 'package:pact_consumer_dart/pact_consumer_dart.dart';
464
- import 'package:test/test.dart';
465
-
466
- void main() {
467
- late PactMockService mockService;
468
-
469
- setUpAll(() async {
470
- mockService = PactMockService(
471
- consumer: 'FrontendApp',
472
- provider: 'BackendAPI',
473
- port: 1234,
474
- );
475
- await mockService.start();
476
- });
477
-
478
- tearDownAll(() async {
479
- await mockService.stop();
480
- });
481
-
482
- test('create resource contract', () async {
483
- await mockService
484
- .given('user is authenticated')
485
- .uponReceiving('a request to create resource')
486
- .withRequest(
487
- method: 'POST',
488
- path: '/api/v1/resource',
489
- headers: {'Authorization': 'Bearer token'},
490
- body: {
491
- 'field1': 'test value',
492
- 'field2': 42,
493
- },
494
- )
495
- .willRespondWith(
496
- status: 201,
497
- body: {
498
- 'id': Matchers.uuid,
499
- 'field1': Matchers.string('test value'),
500
- 'field2': Matchers.integer(42),
501
- 'createdAt': Matchers.iso8601DateTime,
502
- },
503
- );
504
-
505
- await mockService.run((config) async {
506
- // Test your API service against mock
507
- final apiService = ApiService(baseUrl: config.baseUrl);
508
- final result = await apiService.createResource(
509
- field1: 'test value',
510
- field2: 42,
511
- );
512
-
513
- expect(result.id, isNotEmpty);
514
- });
515
-
516
- // Pact file generated: pacts/FrontendApp-BackendAPI.json
517
- });
518
- }
519
- ```
520
-
521
- ---
522
-
523
- ## CI/CD Integration
524
-
525
- ```yaml
526
- # .github/workflows/contract-tests.yml
527
- name: Frontend Contract Tests
528
-
529
- on: [pull_request]
530
-
531
- jobs:
532
- contract-tests:
533
- runs-on: ubuntu-latest
534
-
535
- steps:
536
- - uses: actions/checkout@v2
537
-
538
- - name: Setup Flutter
539
- uses: subosito/flutter-action@v2
540
- with:
541
- flutter-version: '3.24.0'
542
-
543
- - name: Run contract tests
544
- run: flutter test test/contract/
545
-
546
- - name: Run Pact tests
547
- run: flutter test test/pact/
548
-
549
- - name: Publish Pact
550
- if: success()
551
- run: |
552
- flutter pub global activate pact_broker_cli
553
- pact-broker publish pacts/ \
554
- --consumer-app-version=${{ github.sha }} \
555
- --broker-base-url=${{ secrets.PACT_BROKER_URL }}
556
- ```
557
-
558
- ---
559
-
560
- ## Best Practices
561
-
562
- 1. **Mock 서버 활용**
563
- - ✅ 백엔드 없이 독립적 테스트
564
- - ✅ 계약 위반 시 즉시 감지
565
-
566
- 2. **Schema Validation**
567
- - ✅ Zod, JSON Schema로 응답 검증
568
- - ✅ 타입 안정성 보장
569
-
570
- 3. **Consumer-Driven**
571
- - ✅ Frontend 요구사항 먼저 정의
572
- - ✅ Pact 파일로 백엔드 팀과 공유
573
-
574
- 4. **CI/CD 자동화**
575
- - ✅ PR마다 Contract 검증
576
- - ✅ Pact Broker로 중앙 관리
577
-
578
- ---
579
-
580
- ## Next Steps
581
-
582
- ```bash
583
- # 1. Contract 테스트 작성
584
- vibe contract "{기능명}" --frontend
585
-
586
- # 2. Mock 서버로 개발
587
- flutter test test/contract/ --watch
588
-
589
- # 3. Pact 생성 및 발행
590
- flutter test test/pact/
591
-
592
- # 4. 백엔드와 계약 검증
593
- vibe verify "{기능명}" --contract
594
- ```
1
+ # Frontend Contract Tests: {기능명}
2
+
3
+ **Generated from**: `specs/{기능명}.md` (Section 6: API 계약)
4
+ **Framework**: {Flutter | React | React Native | Vue}
5
+ **Language**: {Dart | TypeScript | JavaScript}
6
+ **Priority**: {HIGH | MEDIUM | LOW}
7
+
8
+ ---
9
+
10
+ ## Overview
11
+
12
+ Frontend Contract Testing은 **Consumer 관점에서 API 계약을 검증**합니다:
13
+ - ✅ API 요청이 계약에 맞게 전송되는지
14
+ - ✅ API 응답이 예상 스키마를 따르는지
15
+ - ✅ 에러 처리가 계약대로 동작하는지
16
+ - ✅ Mock 서버로 독립적 테스트 가능
17
+
18
+ **Consumer-Driven Contract Testing** (Pact 패턴)
19
+
20
+ ---
21
+
22
+ ## API Contracts (Consumer View)
23
+
24
+ ### Contract 1: Create Resource
25
+
26
+ **Consumer Expectation**:
27
+ ```json
28
+ {
29
+ "request": {
30
+ "method": "POST",
31
+ "path": "/api/v1/resource",
32
+ "headers": {
33
+ "Authorization": "Bearer {token}",
34
+ "Content-Type": "application/json"
35
+ },
36
+ "body": {
37
+ "field1": "string",
38
+ "field2": "integer"
39
+ }
40
+ },
41
+ "response": {
42
+ "status": 201,
43
+ "body": {
44
+ "id": "uuid",
45
+ "field1": "string",
46
+ "field2": "integer",
47
+ "createdAt": "datetime"
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Implementation
56
+
57
+ ### Flutter (Dart + http_mock_adapter)
58
+
59
+ **File**: `test/contract/{기능명}_contract_test.dart`
60
+
61
+ ```dart
62
+ import 'package:flutter_test/flutter_test.dart';
63
+ import 'package:dio/dio.dart';
64
+ import 'package:http_mock_adapter/http_mock_adapter.dart';
65
+ import 'package:your_app/services/api_service.dart';
66
+ import 'package:your_app/models/resource.dart';
67
+
68
+ void main() {
69
+ late Dio dio;
70
+ late DioAdapter dioAdapter;
71
+ late ApiService apiService;
72
+
73
+ setUp(() {
74
+ dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
75
+ dioAdapter = DioAdapter(dio: dio);
76
+ apiService = ApiService(dio: dio);
77
+ });
78
+
79
+ group('Create Resource Contract', () {
80
+ test('should match request contract', () async {
81
+ // Arrange: Expected request contract
82
+ final requestBody = {
83
+ 'field1': 'test value',
84
+ 'field2': 42,
85
+ };
86
+
87
+ // Arrange: Mock response matching contract
88
+ final responseBody = {
89
+ 'id': '123e4567-e89b-12d3-a456-426614174000',
90
+ 'field1': 'test value',
91
+ 'field2': 42,
92
+ 'createdAt': '2025-01-17T10:00:00Z',
93
+ };
94
+
95
+ dioAdapter.onPost(
96
+ '/api/v1/resource',
97
+ (server) => server.reply(201, responseBody),
98
+ data: requestBody,
99
+ headers: {
100
+ 'Authorization': 'Bearer test-token',
101
+ 'Content-Type': 'application/json',
102
+ },
103
+ );
104
+
105
+ // Act: Call API service
106
+ final result = await apiService.createResource(
107
+ field1: 'test value',
108
+ field2: 42,
109
+ token: 'test-token',
110
+ );
111
+
112
+ // Assert: Response matches contract
113
+ expect(result, isA<Resource>());
114
+ expect(result.id, isNotEmpty);
115
+ expect(result.field1, equals('test value'));
116
+ expect(result.field2, equals(42));
117
+ expect(result.createdAt, isA<DateTime>());
118
+ });
119
+
120
+ test('should handle error response contract', () async {
121
+ // Arrange: Error response contract
122
+ final errorBody = {
123
+ 'error': 'ValidationError',
124
+ 'message': 'field1 is required',
125
+ 'details': ['field1 must not be empty'],
126
+ };
127
+
128
+ dioAdapter.onPost(
129
+ '/api/v1/resource',
130
+ (server) => server.reply(400, errorBody),
131
+ );
132
+
133
+ // Act & Assert: Error handling matches contract
134
+ expect(
135
+ () async => await apiService.createResource(
136
+ field1: '',
137
+ field2: 42,
138
+ token: 'test-token',
139
+ ),
140
+ throwsA(isA<ApiException>().having(
141
+ (e) => e.statusCode,
142
+ 'status code',
143
+ equals(400),
144
+ )),
145
+ );
146
+ });
147
+
148
+ test('should validate response schema', () async {
149
+ // Arrange: Response with invalid schema
150
+ final invalidResponse = {
151
+ 'id': 'not-a-uuid', // Invalid UUID format
152
+ 'field1': 123, // Wrong type
153
+ // Missing field2
154
+ };
155
+
156
+ dioAdapter.onPost(
157
+ '/api/v1/resource',
158
+ (server) => server.reply(201, invalidResponse),
159
+ );
160
+
161
+ // Act & Assert: Schema validation fails
162
+ expect(
163
+ () async => await apiService.createResource(
164
+ field1: 'test',
165
+ field2: 42,
166
+ token: 'test-token',
167
+ ),
168
+ throwsA(isA<SchemaValidationException>()),
169
+ );
170
+ });
171
+ });
172
+
173
+ group('Response Schema Validation', () {
174
+ test('validates UUID format', () {
175
+ final validUuid = '123e4567-e89b-12d3-a456-426614174000';
176
+ expect(isValidUuid(validUuid), isTrue);
177
+
178
+ final invalidUuid = 'not-a-uuid';
179
+ expect(isValidUuid(invalidUuid), isFalse);
180
+ });
181
+
182
+ test('validates DateTime format (ISO 8601)', () {
183
+ final validDateTime = '2025-01-17T10:00:00Z';
184
+ expect(() => DateTime.parse(validDateTime), returnsNormally);
185
+
186
+ final invalidDateTime = '2025-01-17'; // Missing time
187
+ expect(() => DateTime.parse(invalidDateTime), throwsFormatException);
188
+ });
189
+ });
190
+ }
191
+
192
+ // Helper function
193
+ bool isValidUuid(String uuid) {
194
+ final uuidRegex = RegExp(
195
+ r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
196
+ caseSensitive: false,
197
+ );
198
+ return uuidRegex.hasMatch(uuid);
199
+ }
200
+ ```
201
+
202
+ **Run**:
203
+ ```bash
204
+ flutter test test/contract/{기능명}_contract_test.dart
205
+ ```
206
+
207
+ ---
208
+
209
+ ### React (TypeScript + MSW + Zod)
210
+
211
+ **File**: `tests/contract/{기능명}.contract.test.ts`
212
+
213
+ ```typescript
214
+ import { rest } from 'msw';
215
+ import { setupServer } from 'msw/node';
216
+ import { z } from 'zod';
217
+ import { createResource, ApiService } from '@/services/api';
218
+
219
+ // Zod schemas for contract validation
220
+ const CreateResourceRequestSchema = z.object({
221
+ field1: z.string().min(1).max(100),
222
+ field2: z.number().int().nonnegative(),
223
+ field3: z.boolean().optional(),
224
+ });
225
+
226
+ const CreateResourceResponseSchema = z.object({
227
+ id: z.string().uuid(),
228
+ field1: z.string(),
229
+ field2: z.number().int(),
230
+ field3: z.boolean().optional(),
231
+ createdAt: z.string().datetime(),
232
+ });
233
+
234
+ const ErrorResponseSchema = z.object({
235
+ error: z.string(),
236
+ message: z.string(),
237
+ details: z.array(z.string()).optional(),
238
+ });
239
+
240
+ // Mock server
241
+ const server = setupServer();
242
+
243
+ beforeAll(() => server.listen());
244
+ afterEach(() => server.resetHandlers());
245
+ afterAll(() => server.close());
246
+
247
+ describe('Create Resource Contract', () => {
248
+ it('should send request matching contract', async () => {
249
+ let capturedRequest: any;
250
+
251
+ server.use(
252
+ rest.post('/api/v1/resource', async (req, res, ctx) => {
253
+ capturedRequest = await req.json();
254
+
255
+ // Validate request matches contract
256
+ const result = CreateResourceRequestSchema.safeParse(capturedRequest);
257
+ expect(result.success).toBe(true);
258
+
259
+ return res(
260
+ ctx.status(201),
261
+ ctx.json({
262
+ id: '123e4567-e89b-12d3-a456-426614174000',
263
+ field1: capturedRequest.field1,
264
+ field2: capturedRequest.field2,
265
+ field3: capturedRequest.field3 ?? false,
266
+ createdAt: new Date().toISOString(),
267
+ })
268
+ );
269
+ })
270
+ );
271
+
272
+ const result = await createResource({
273
+ field1: 'test value',
274
+ field2: 42,
275
+ field3: true,
276
+ });
277
+
278
+ // Verify request contract
279
+ expect(capturedRequest).toMatchObject({
280
+ field1: 'test value',
281
+ field2: 42,
282
+ field3: true,
283
+ });
284
+
285
+ // Verify response contract
286
+ const responseValidation = CreateResourceResponseSchema.safeParse(result);
287
+ expect(responseValidation.success).toBe(true);
288
+ });
289
+
290
+ it('should handle error response contract', async () => {
291
+ server.use(
292
+ rest.post('/api/v1/resource', (req, res, ctx) => {
293
+ return res(
294
+ ctx.status(400),
295
+ ctx.json({
296
+ error: 'ValidationError',
297
+ message: 'field1 is required',
298
+ details: ['field1 must not be empty'],
299
+ })
300
+ );
301
+ })
302
+ );
303
+
304
+ await expect(
305
+ createResource({
306
+ field1: '',
307
+ field2: 42,
308
+ })
309
+ ).rejects.toThrow();
310
+
311
+ // Verify error response matches contract
312
+ try {
313
+ await createResource({ field1: '', field2: 42 });
314
+ } catch (error: any) {
315
+ const errorValidation = ErrorResponseSchema.safeParse(error.response.data);
316
+ expect(errorValidation.success).toBe(true);
317
+ expect(error.response.status).toBe(400);
318
+ }
319
+ });
320
+
321
+ it('should reject response with invalid schema', async () => {
322
+ server.use(
323
+ rest.post('/api/v1/resource', (req, res, ctx) => {
324
+ return res(
325
+ ctx.status(201),
326
+ ctx.json({
327
+ id: 'not-a-uuid', // Invalid UUID
328
+ field1: 123, // Wrong type
329
+ // Missing field2
330
+ })
331
+ );
332
+ })
333
+ );
334
+
335
+ await expect(
336
+ createResource({
337
+ field1: 'test',
338
+ field2: 42,
339
+ })
340
+ ).rejects.toThrow('Schema validation failed');
341
+ });
342
+
343
+ it('validates response headers', async () => {
344
+ let responseHeaders: Headers;
345
+
346
+ server.use(
347
+ rest.post('/api/v1/resource', (req, res, ctx) => {
348
+ return res(
349
+ ctx.status(201),
350
+ ctx.set('Content-Type', 'application/json'),
351
+ ctx.json({
352
+ id: '123e4567-e89b-12d3-a456-426614174000',
353
+ field1: 'test',
354
+ field2: 42,
355
+ createdAt: new Date().toISOString(),
356
+ })
357
+ );
358
+ })
359
+ );
360
+
361
+ const response = await fetch('/api/v1/resource', {
362
+ method: 'POST',
363
+ body: JSON.stringify({ field1: 'test', field2: 42 }),
364
+ });
365
+
366
+ expect(response.headers.get('Content-Type')).toBe('application/json');
367
+ });
368
+ });
369
+
370
+ describe('Schema Validation Utilities', () => {
371
+ it('validates UUID format', () => {
372
+ const validUuid = '123e4567-e89b-12d3-a456-426614174000';
373
+ const result = z.string().uuid().safeParse(validUuid);
374
+ expect(result.success).toBe(true);
375
+
376
+ const invalidUuid = 'not-a-uuid';
377
+ const invalidResult = z.string().uuid().safeParse(invalidUuid);
378
+ expect(invalidResult.success).toBe(false);
379
+ });
380
+
381
+ it('validates ISO 8601 datetime', () => {
382
+ const validDate = '2025-01-17T10:00:00Z';
383
+ const result = z.string().datetime().safeParse(validDate);
384
+ expect(result.success).toBe(true);
385
+
386
+ const invalidDate = '2025-01-17'; // Missing time
387
+ const invalidResult = z.string().datetime().safeParse(invalidDate);
388
+ expect(invalidResult.success).toBe(false);
389
+ });
390
+ });
391
+ ```
392
+
393
+ **Run**:
394
+ ```bash
395
+ npm test -- tests/contract/{기능명}.contract.test.ts
396
+ ```
397
+
398
+ ---
399
+
400
+ ### React Native (TypeScript + Axios + MockAdapter)
401
+
402
+ **File**: `__tests__/contract/{기능명}.contract.test.ts`
403
+
404
+ ```typescript
405
+ import axios from 'axios';
406
+ import MockAdapter from 'axios-mock-adapter';
407
+ import { z } from 'zod';
408
+ import { ApiService } from '@/services/api';
409
+
410
+ const mock = new MockAdapter(axios);
411
+
412
+ const ResponseSchema = z.object({
413
+ id: z.string().uuid(),
414
+ field1: z.string(),
415
+ field2: z.number(),
416
+ createdAt: z.string().datetime(),
417
+ });
418
+
419
+ describe('Create Resource Contract (React Native)', () => {
420
+ beforeEach(() => {
421
+ mock.reset();
422
+ });
423
+
424
+ it('should match API contract', async () => {
425
+ const requestBody = {
426
+ field1: 'test value',
427
+ field2: 42,
428
+ };
429
+
430
+ const responseBody = {
431
+ id: '123e4567-e89b-12d3-a456-426614174000',
432
+ field1: 'test value',
433
+ field2: 42,
434
+ createdAt: '2025-01-17T10:00:00Z',
435
+ };
436
+
437
+ mock.onPost('/api/v1/resource', requestBody).reply(201, responseBody);
438
+
439
+ const apiService = new ApiService(axios);
440
+ const result = await apiService.createResource(requestBody);
441
+
442
+ // Validate response schema
443
+ const validation = ResponseSchema.safeParse(result);
444
+ expect(validation.success).toBe(true);
445
+ });
446
+ });
447
+ ```
448
+
449
+ **Run**:
450
+ ```bash
451
+ npm test -- __tests__/contract/
452
+ ```
453
+
454
+ ---
455
+
456
+ ## Pact Consumer Tests
457
+
458
+ ### Flutter (dart_pact)
459
+
460
+ **File**: `test/pact/{기능명}_pact_test.dart`
461
+
462
+ ```dart
463
+ import 'package:pact_consumer_dart/pact_consumer_dart.dart';
464
+ import 'package:test/test.dart';
465
+
466
+ void main() {
467
+ late PactMockService mockService;
468
+
469
+ setUpAll(() async {
470
+ mockService = PactMockService(
471
+ consumer: 'FrontendApp',
472
+ provider: 'BackendAPI',
473
+ port: 1234,
474
+ );
475
+ await mockService.start();
476
+ });
477
+
478
+ tearDownAll(() async {
479
+ await mockService.stop();
480
+ });
481
+
482
+ test('create resource contract', () async {
483
+ await mockService
484
+ .given('user is authenticated')
485
+ .uponReceiving('a request to create resource')
486
+ .withRequest(
487
+ method: 'POST',
488
+ path: '/api/v1/resource',
489
+ headers: {'Authorization': 'Bearer token'},
490
+ body: {
491
+ 'field1': 'test value',
492
+ 'field2': 42,
493
+ },
494
+ )
495
+ .willRespondWith(
496
+ status: 201,
497
+ body: {
498
+ 'id': Matchers.uuid,
499
+ 'field1': Matchers.string('test value'),
500
+ 'field2': Matchers.integer(42),
501
+ 'createdAt': Matchers.iso8601DateTime,
502
+ },
503
+ );
504
+
505
+ await mockService.run((config) async {
506
+ // Test your API service against mock
507
+ final apiService = ApiService(baseUrl: config.baseUrl);
508
+ final result = await apiService.createResource(
509
+ field1: 'test value',
510
+ field2: 42,
511
+ );
512
+
513
+ expect(result.id, isNotEmpty);
514
+ });
515
+
516
+ // Pact file generated: pacts/FrontendApp-BackendAPI.json
517
+ });
518
+ }
519
+ ```
520
+
521
+ ---
522
+
523
+ ## CI/CD Integration
524
+
525
+ ```yaml
526
+ # .github/workflows/contract-tests.yml
527
+ name: Frontend Contract Tests
528
+
529
+ on: [pull_request]
530
+
531
+ jobs:
532
+ contract-tests:
533
+ runs-on: ubuntu-latest
534
+
535
+ steps:
536
+ - uses: actions/checkout@v2
537
+
538
+ - name: Setup Flutter
539
+ uses: subosito/flutter-action@v2
540
+ with:
541
+ flutter-version: '3.24.0'
542
+
543
+ - name: Run contract tests
544
+ run: flutter test test/contract/
545
+
546
+ - name: Run Pact tests
547
+ run: flutter test test/pact/
548
+
549
+ - name: Publish Pact
550
+ if: success()
551
+ run: |
552
+ flutter pub global activate pact_broker_cli
553
+ pact-broker publish pacts/ \
554
+ --consumer-app-version=${{ github.sha }} \
555
+ --broker-base-url=${{ secrets.PACT_BROKER_URL }}
556
+ ```
557
+
558
+ ---
559
+
560
+ ## Best Practices
561
+
562
+ 1. **Mock 서버 활용**
563
+ - ✅ 백엔드 없이 독립적 테스트
564
+ - ✅ 계약 위반 시 즉시 감지
565
+
566
+ 2. **Schema Validation**
567
+ - ✅ Zod, JSON Schema로 응답 검증
568
+ - ✅ 타입 안정성 보장
569
+
570
+ 3. **Consumer-Driven**
571
+ - ✅ Frontend 요구사항 먼저 정의
572
+ - ✅ Pact 파일로 백엔드 팀과 공유
573
+
574
+ 4. **CI/CD 자동화**
575
+ - ✅ PR마다 Contract 검증
576
+ - ✅ Pact Broker로 중앙 관리
577
+
578
+ ---
579
+
580
+ ## Next Steps
581
+
582
+ ```bash
583
+ # 1. Contract 테스트 작성
584
+ vibe contract "{기능명}" --frontend
585
+
586
+ # 2. Mock 서버로 개발
587
+ flutter test test/contract/ --watch
588
+
589
+ # 3. Pact 생성 및 발행
590
+ flutter test test/pact/
591
+
592
+ # 4. 백엔드와 계약 검증
593
+ vibe verify "{기능명}" --contract
594
+ ```