@einja/dev-cli 0.1.41 → 0.1.44

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 (83) hide show
  1. package/dist/commands/task-loop/lib/github-client.test.js.map +1 -1
  2. package/dist/commands/task-loop/lib/vibe-kanban-rest-client.js +2 -2
  3. package/dist/commands/task-loop/lib/vibe-kanban-rest-client.js.map +1 -1
  4. package/dist/lib/preset-update/file-copier.js +3 -3
  5. package/dist/lib/preset-update/file-copier.js.map +1 -1
  6. package/dist/lib/sync/marker-processor.js.map +1 -1
  7. package/dist/lib/sync/metadata-manager.js +1 -1
  8. package/dist/lib/sync/metadata-manager.js.map +1 -1
  9. package/dist/lib/sync/metadata-manager.test.js +3 -2
  10. package/dist/lib/sync/metadata-manager.test.js.map +1 -1
  11. package/dist/lib/sync/project-private-synchronizer.d.ts.map +1 -1
  12. package/dist/lib/sync/project-private-synchronizer.js +5 -1
  13. package/dist/lib/sync/project-private-synchronizer.js.map +1 -1
  14. package/package.json +1 -1
  15. package/presets/default/.claude/agents/einja/backend-architect.md +17 -1
  16. package/presets/default/.claude/agents/einja/codex-agent.md +1 -1
  17. package/presets/default/.claude/agents/einja/design-engineer.md +1 -1
  18. package/presets/default/.claude/agents/einja/docs/docs-updater.md +3 -93
  19. package/presets/default/.claude/agents/einja/frontend-architect.md +17 -1
  20. package/presets/default/.claude/agents/einja/frontend-coder.md +1 -1
  21. package/presets/default/.claude/agents/einja/{specs/spec-design-generator.md → issue-specs/design-generator.md} +12 -7
  22. package/presets/default/.claude/agents/einja/{specs/spec-qa-generator.md → issue-specs/qa-generator.md} +6 -4
  23. package/presets/default/.claude/agents/einja/{specs/spec-requirements-generator.md → issue-specs/requirements-generator.md} +5 -5
  24. package/presets/default/.claude/agents/einja/{specs/spec-tasks-generator.md → issue-specs/tasks-generator.md} +13 -14
  25. package/presets/default/.claude/agents/einja/{specs/spec-tasks-validator.md → issue-specs/tasks-validator.md} +9 -9
  26. package/presets/default/.claude/agents/einja/issue-specs/ui-design-generator.md +114 -0
  27. package/presets/default/.claude/agents/einja/task/task-executer.md +9 -3
  28. package/presets/default/.claude/agents/einja/task/task-modification-analyzer.md +2 -2
  29. package/presets/default/.claude/agents/einja/task/task-qa.md +3 -3
  30. package/presets/default/.claude/agents/einja/task/task-reviewer.md +13 -1
  31. package/presets/default/.claude/commands/einja/einja-sync.md +119 -44
  32. package/presets/default/.claude/commands/einja/issue-exec.md +29 -19
  33. package/presets/default/.claude/commands/einja/sync-cursor-commands.md +6 -6
  34. package/presets/default/.claude/commands/einja/{update-docs-by-task-specs.md → update-docs-by-issue-specs.md} +58 -58
  35. package/presets/default/.claude/hooks/einja/plan-mode-skill-loader.sh +5 -1
  36. package/presets/default/.claude/settings.json +14 -4
  37. package/presets/default/.claude/skills/{einja-general-context-loader → _einja-general-context-loader}/SKILL.md +2 -2
  38. package/presets/default/.claude/skills/{einja-output-format → _einja-output-format}/SKILL.md +1 -1
  39. package/presets/default/.claude/skills/_einja-project-overview/SKILL.md +29 -0
  40. package/presets/default/.claude/skills/{einja-spec-context-loader → _einja-spec-context-loader}/SKILL.md +5 -5
  41. package/presets/default/.claude/skills/einja-coding-standards/references/testing-strategy.md +899 -0
  42. package/presets/default/.claude/skills/einja-conflict-resolver/SKILL.md +1 -1
  43. package/presets/default/.claude/skills/einja-create-pr/SKILL.md +138 -0
  44. package/presets/default/.claude/skills/einja-infra-maintenance/SKILL.md +779 -0
  45. package/presets/default/.claude/{commands/einja/spec-create.md → skills/einja-issue-spec-create/SKILL.md} +47 -24
  46. package/presets/default/.claude/skills/einja-issue-spec-generator/SKILL.md +105 -0
  47. package/presets/default/.claude/skills/einja-issue-spec-generator/references/format-rules.md +35 -0
  48. package/presets/default/.claude/skills/einja-issue-spec-validator/SKILL.md +130 -0
  49. package/presets/default/.claude/skills/einja-issue-spec-validator/references/validation-rules.md +52 -0
  50. package/presets/default/.claude/skills/einja-npm-release/SKILL.md +242 -0
  51. package/presets/default/.claude/skills/einja-skill-creator/SKILL.md +68 -12
  52. package/presets/default/.claude/skills/einja-skill-creator/scripts/aggregate_benchmark.py +368 -121
  53. package/presets/default/.claude/skills/einja-skill-creator/scripts/compare_runs.py +154 -0
  54. package/presets/default/.claude/skills/einja-skill-creator/scripts/generate_report.py +14 -7
  55. package/presets/default/.claude/skills/einja-skill-creator/scripts/improve_description.py +2 -7
  56. package/presets/default/.claude/skills/einja-skill-creator/scripts/run_loop.py +263 -183
  57. package/presets/default/.claude/skills/einja-skill-first/SKILL.md +265 -0
  58. package/presets/default/.claude/skills/einja-subagent-question-protocol/SKILL.md +98 -0
  59. package/presets/default/.claude/skills/einja-task-commit/SKILL.md +7 -7
  60. package/presets/default/.claude/{commands/einja/task-exec.md → skills/einja-task-exec/SKILL.md} +3 -78
  61. package/presets/default/.claude/skills/einja-task-qa/SKILL.md +4 -4
  62. package/presets/default/.claude/skills/einja-task-qa/references/troubleshooting.md +1 -1
  63. package/presets/default/.claude/skills/einja-task-qa/references/usage-patterns.md +2 -2
  64. package/presets/default/.claude/skills/einja-team-exec/SKILL.md +165 -0
  65. package/presets/default/CLAUDE.md.template +21 -6
  66. package/presets/default/docs/einja/instructions/deployment-setup.md +1 -1
  67. package/presets/default/docs/einja/instructions/issue-exec-workflow.md +11 -11
  68. package/presets/default/docs/einja/instructions/local-server-environment-and-worktree.md +1 -1
  69. package/presets/default/docs/einja/instructions/setup-flow.md +279 -0
  70. package/presets/default/docs/einja/instructions/task-execute.md +42 -42
  71. package/presets/default/docs/einja/steering/acceptance-criteria-and-qa-guide.md +1 -1
  72. package/presets/default/docs/einja/steering/branch-strategy.md +1 -1
  73. package/presets/default/docs/einja/steering/development-workflow.md +93 -25
  74. package/presets/default/docs/einja/steering/infrastructure/deployment.md +107 -0
  75. package/presets/default/docs/einja/steering/task-management.md +9 -13
  76. package/presets/default/scripts/ensure-serena.sh +2 -2
  77. package/presets/default/scripts/env-rotate-secrets.ts +66 -6
  78. package/presets/default/scripts/init-github.ts +363 -0
  79. package/presets/default/scripts/init.sh +11 -5
  80. package/presets/default/scripts/setup-dev.ts +16 -1
  81. package/presets/default/.claude/agents/einja/git/conflict-resolver.md +0 -152
  82. package/presets/default/.claude/hooks/einja/validate-git-commit.sh +0 -239
  83. package/presets/default/.claude/skills/einja-project-overview/SKILL.md +0 -39
@@ -0,0 +1,899 @@
1
+ <!-- @einja:managed:start -->
2
+ # テスト戦略
3
+
4
+ ## 概要
5
+
6
+ このドキュメントでは、ユニットテスト、統合テスト、E2Eテスト(Playwrightコード)の戦略と実装ガイドラインを説明します。
7
+
8
+ テストカバレッジ目標を達成し、高品質なコードを維持するためのベストプラクティスを提供します。
9
+
10
+ > **Note**: このドキュメントは開発者向けの自動テスト戦略を扱います。QAエージェント(task-qa)が実行するBrowserテスト(Playwright MCP)については、`docs/einja/steering/terminology.md` および `docs/einja/steering/acceptance-criteria-and-qa-guide.md` を参照してください。
11
+
12
+ ---
13
+
14
+ ## 目次
15
+
16
+ 1. [テスト戦略の全体像](#1-テスト戦略の全体像)
17
+ 2. [ユニットテスト](#2-ユニットテスト)
18
+ 3. [統合テスト](#3-統合テスト)
19
+ 4. [E2Eテスト](#4-e2eテスト)
20
+ 5. [テストカバレッジ](#5-テストカバレッジ)
21
+ 6. [テストツール](#6-テストツール)
22
+ 7. [テストの実行](#7-テストの実行)
23
+ 8. [モックとスタブ](#8-モックとスタブ)
24
+
25
+ ---
26
+
27
+ ## 1. テスト戦略の全体像
28
+
29
+ ### テストピラミッド
30
+
31
+ ```mermaid
32
+ graph TB
33
+ subgraph "テストピラミッド"
34
+ E2E[E2Eテスト<br/>完全なユーザーフロー<br/>少ない]
35
+ Integration[統合テスト<br/>パッケージ間連携<br/>中程度]
36
+ Unit[ユニットテスト<br/>個別の関数・クラス<br/>多い]
37
+ end
38
+
39
+ E2E --> Integration
40
+ Integration --> Unit
41
+
42
+ style E2E fill:#ffebee
43
+ style Integration fill:#fff3e0
44
+ style Unit fill:#e8f5e9
45
+ ```
46
+
47
+ ### テスト種別と目的
48
+
49
+ | テスト種別 | 目的 | 対象 | 実行頻度 |
50
+ |-----------|------|------|---------|
51
+ | ユニットテスト | 個別の関数・クラスの動作確認 | Repository, UseCase, Entity, Validator | 毎コミット |
52
+ | 統合テスト | パッケージ間の連携確認 | @repo/server-core ⇔ apps/* | 毎PR |
53
+ | E2Eテスト | 完全なユーザーフローの確認 | Web, Admin, Cron Worker | デプロイ前 |
54
+
55
+ ### TDDワークフロー(Red-Green-Refactor)
56
+
57
+ TDD採用が推奨される場合(ビジネスロジック、データ変換、外部API連携、金銭・認証処理など)、以下のサイクルを実施:
58
+
59
+ 1. **Red**: 失敗するテストを書く(Given-When-Then形式で記述)
60
+ - 期待する振る舞いをテストとして記述
61
+ - テストを実行し、失敗することを確認
62
+
63
+ 2. **Green**: テストを通す最小限の実装
64
+ - テストが通る最小限のコードを書く
65
+ - この段階では美しさより動作を優先
66
+
67
+ 3. **Refactor**: コードを改善
68
+ - テストが通ったまま、コードを改善
69
+ - 重複削除、命名改善、構造改善
70
+
71
+ **補足**: Given-When-ThenとRed-Green-Refactorは**補完関係**:
72
+ - **Given-When-Then**: テストの記述形式(どう書くか)
73
+ - **Red-Green-Refactor**: TDDの実施サイクル(どう進めるか)
74
+ - 組み合わせ: 「Red-Green-RefactorサイクルでGiven-When-Then形式のテストを書く」
75
+
76
+ ### テストコードスタイル
77
+
78
+ テストコードの一貫性と可読性を確保するため、以下のスタイルルールに従ってください。
79
+
80
+ #### describe構造
81
+
82
+ ```typescript
83
+ describe('対象クラス/関数名', () => {
84
+ describe('メソッド名', () => {
85
+ // 正常系テスト
86
+ it('条件を満たすと、期待する結果になる', () => {})
87
+ })
88
+
89
+ describe('メソッド名 - 異常系', () => {
90
+ // 異常系テスト
91
+ it('不正な条件の場合、エラーが発生する', () => {})
92
+ })
93
+ })
94
+ ```
95
+
96
+ #### describe分類ルール
97
+
98
+ | レベル | 分類基準 | 例 |
99
+ |--------|---------|-----|
100
+ | 1階層 | 対象クラス/関数名 | `describe('UserRepository', ...)` |
101
+ | 2階層 | メソッド名 or 機能名 | `describe('create', ...)` |
102
+ | 2階層 | 正常系/異常系 | `describe('create - 異常系', ...)` |
103
+ | 3階層 | 着眼点ごと(任意) | `describe('バリデーション', ...)` |
104
+
105
+ #### it説明文のフォーマット
106
+
107
+ **必須形式**: 「〜すると、〜になる」(条件 → 結果)
108
+
109
+ ```typescript
110
+ // ✅ 良い例
111
+ it('有効なメールアドレスを渡すと、ユーザーが作成される', () => {})
112
+ it('存在しないIDで検索すると、nullが返る', () => {})
113
+ it('重複したメールアドレスの場合、エラーが返る', () => {})
114
+
115
+ // ❌ 悪い例
116
+ it('ユーザー作成', () => {}) // 条件・結果が不明
117
+ it('should create user', () => {}) // 英語(日本語プロジェクト)
118
+ it('createが動作する', () => {}) // 結果が曖昧
119
+ ```
120
+
121
+ #### Given-When-Thenコメント
122
+
123
+ **必須**: テストコード内部は `// Given:` `// When:` `// Then:` コメントで構造化する
124
+
125
+ ```typescript
126
+ it('有効なユーザーデータを渡すと、ユーザーが作成される', async () => {
127
+ // Given: 有効なユーザーデータ
128
+ const userData = {
129
+ email: 'test@example.com',
130
+ name: 'Test User',
131
+ }
132
+
133
+ // When: createメソッドを呼び出す
134
+ const result = await userRepository.create(userData)
135
+
136
+ // Then: 成功結果が返る
137
+ expect(result.isSuccess).toBe(true)
138
+ })
139
+ ```
140
+
141
+ #### チェックリスト
142
+
143
+ 新しいテストを書く際の確認事項:
144
+
145
+ - [ ] describe が適切に階層化されている
146
+ - [ ] it 説明文が「〜すると、〜になる」形式
147
+ - [ ] Given-When-Then コメントで構造化されている
148
+ - [ ] 正常系と異常系が分離されている
149
+
150
+ ---
151
+
152
+ ## 2. ユニットテスト
153
+
154
+ ### 対象コンポーネント
155
+
156
+ 1. **Repository**(Infrastructure層)
157
+ 2. **UseCase**(Application層)
158
+ 3. **Entity**(Domain層)
159
+ 4. **Validator**(Domain層)
160
+ 5. **Mapper**(Infrastructure層)
161
+
162
+ ### 正常系テストケース
163
+
164
+ #### Repositoryのテスト
165
+
166
+ **Given-When-Then形式**:
167
+
168
+ ```typescript
169
+ // packages/server-core/src/infrastructure/database/repositories/__tests__/UserRepository.test.ts
170
+ import { userRepository } from '../UserRepository'
171
+ import { prisma } from '../../client'
172
+
173
+ describe('UserRepository', () => {
174
+ beforeEach(async () => {
175
+ // テストデータのクリーンアップ
176
+ await prisma.user.deleteMany()
177
+ })
178
+
179
+ describe('create', () => {
180
+ it('有効なユーザーデータを渡すと、データベースにユーザーが作成され、ドメインエンティティが返る', async () => {
181
+ // Given: 有効なユーザーデータ
182
+ const userData = {
183
+ email: 'test@example.com',
184
+ name: 'Test User',
185
+ }
186
+
187
+ // When: createメソッドを呼び出す
188
+ const result = await userRepository.create(userData)
189
+
190
+ // Then: 成功結果が返り、ユーザーが作成される
191
+ expect(result.isSuccess).toBe(true)
192
+ if (result.isSuccess) {
193
+ expect(result.value.email).toBe('test@example.com')
194
+ expect(result.value.name).toBe('Test User')
195
+ expect(result.value.id).toBeDefined()
196
+ }
197
+
198
+ // データベースに保存されているか確認
199
+ const savedUser = await prisma.user.findUnique({
200
+ where: { email: 'test@example.com' }
201
+ })
202
+ expect(savedUser).toBeDefined()
203
+ })
204
+ })
205
+
206
+ describe('find', () => {
207
+ it('存在するユーザーのメールアドレスで検索すると、ユーザーが返る', async () => {
208
+ // Given: ユーザーが存在する
209
+ await prisma.user.create({
210
+ data: {
211
+ email: 'existing@example.com',
212
+ name: 'Existing User',
213
+ }
214
+ })
215
+
216
+ // When: メールアドレスで検索
217
+ const result = await userRepository.find({ email: 'existing@example.com' })
218
+
219
+ // Then: ユーザーが見つかる
220
+ expect(result.isSuccess).toBe(true)
221
+ if (result.isSuccess) {
222
+ expect(result.value).not.toBeNull()
223
+ expect(result.value?.email).toBe('existing@example.com')
224
+ }
225
+ })
226
+ })
227
+ })
228
+ ```
229
+
230
+ #### バリデーションのテスト
231
+
232
+ ```typescript
233
+ // packages/server-core/src/domain/validators/__tests__/user.test.ts
234
+ import { createUserSchema, updateUserSchema } from '../user'
235
+
236
+ describe('User Validators', () => {
237
+ describe('createUserSchema', () => {
238
+ it('有効なメールアドレスと名前が渡されると、バリデーションが成功する', () => {
239
+ // Given: 有効なユーザーデータ
240
+ const validData = {
241
+ email: 'test@example.com',
242
+ name: 'Test User',
243
+ }
244
+
245
+ // When: バリデーションを実行
246
+ const result = createUserSchema.safeParse(validData)
247
+
248
+ // Then: バリデーション成功
249
+ expect(result.success).toBe(true)
250
+ if (result.success) {
251
+ expect(result.data.email).toBe('test@example.com')
252
+ expect(result.data.name).toBe('Test User')
253
+ }
254
+ })
255
+
256
+ it('不正な形式のメールアドレスが渡されると、バリデーションが失敗する', () => {
257
+ // Given: 不正なメールアドレス
258
+ const invalidData = {
259
+ email: 'invalid-email',
260
+ name: 'Test User',
261
+ }
262
+
263
+ // When: バリデーションを実行
264
+ const result = createUserSchema.safeParse(invalidData)
265
+
266
+ // Then: バリデーション失敗
267
+ expect(result.success).toBe(false)
268
+ if (!result.success) {
269
+ expect(result.error.issues[0].path).toContain('email')
270
+ }
271
+ })
272
+ })
273
+ })
274
+ ```
275
+
276
+ #### エンティティのテスト
277
+
278
+ ```typescript
279
+ // packages/server-core/src/domain/entities/__tests__/Post.test.ts
280
+ import { Post } from '../Post'
281
+
282
+ describe('Post Entity', () => {
283
+ describe('publish', () => {
284
+ it('下書き状態の投稿をpublishすると、公開状態になり、公開日時が設定される', () => {
285
+ // Given: 下書き状態の投稿
286
+ const post = new Post({
287
+ id: '1',
288
+ userId: 'user1',
289
+ title: 'Test Post',
290
+ content: 'Content',
291
+ status: 'draft',
292
+ publishedAt: null,
293
+ createdAt: new Date(),
294
+ updatedAt: new Date(),
295
+ })
296
+
297
+ // When: publish メソッドを呼び出す
298
+ post.publish()
299
+
300
+ // Then: 公開状態になる
301
+ expect(post.status).toBe('published')
302
+ expect(post.publishedAt).toBeInstanceOf(Date)
303
+ expect(post.isPublished()).toBe(true)
304
+ })
305
+ })
306
+
307
+ describe('isOwnedBy', () => {
308
+ it('投稿の所有者IDと一致するユーザーIDを渡すと、trueが返る', () => {
309
+ // Given: ユーザー'user1'が作成した投稿
310
+ const post = new Post({
311
+ id: '1',
312
+ userId: 'user1',
313
+ title: 'Test Post',
314
+ content: 'Content',
315
+ status: 'draft',
316
+ publishedAt: null,
317
+ createdAt: new Date(),
318
+ updatedAt: new Date(),
319
+ })
320
+
321
+ // When: 所有者チェック
322
+ const isOwned = post.isOwnedBy('user1')
323
+
324
+ // Then: trueが返る
325
+ expect(isOwned).toBe(true)
326
+ })
327
+
328
+ it('投稿の所有者IDと異なるユーザーIDを渡すと、falseが返る', () => {
329
+ // Given: ユーザー'user1'が作成した投稿
330
+ const post = new Post({
331
+ id: '1',
332
+ userId: 'user1',
333
+ title: 'Test Post',
334
+ content: 'Content',
335
+ status: 'draft',
336
+ publishedAt: null,
337
+ createdAt: new Date(),
338
+ updatedAt: new Date(),
339
+ })
340
+
341
+ // When: 別のユーザーIDで所有者チェック
342
+ const isOwned = post.isOwnedBy('user2')
343
+
344
+ // Then: falseが返る
345
+ expect(isOwned).toBe(false)
346
+ })
347
+ })
348
+ })
349
+ ```
350
+
351
+ ### 異常系テストケース
352
+
353
+ #### 無効な入力値のテスト
354
+
355
+ ```typescript
356
+ describe('createUserSchema - 異常系', () => {
357
+ it('メールアドレスが空文字の場合、バリデーションエラーが発生する', () => {
358
+ // Given: 空のメールアドレス
359
+ const invalidData = {
360
+ email: '',
361
+ name: 'Test User',
362
+ }
363
+
364
+ // When: バリデーション実行
365
+ const result = createUserSchema.safeParse(invalidData)
366
+
367
+ // Then: エラーが返る
368
+ expect(result.success).toBe(false)
369
+ if (!result.success) {
370
+ expect(result.error.issues[0].message).toContain('email')
371
+ }
372
+ })
373
+
374
+ it('名前が100文字を超える場合、バリデーションエラーが発生する', () => {
375
+ // Given: 100文字を超える名前
376
+ const invalidData = {
377
+ email: 'test@example.com',
378
+ name: 'a'.repeat(101),
379
+ }
380
+
381
+ // When: バリデーション実行
382
+ const result = createUserSchema.safeParse(invalidData)
383
+
384
+ // Then: エラーが返る
385
+ expect(result.success).toBe(false)
386
+ })
387
+ })
388
+ ```
389
+
390
+ #### データベースエラーのテスト
391
+
392
+ ```typescript
393
+ describe('UserRepository - データベースエラー', () => {
394
+ it('重複したメールアドレスでユーザーを作成すると、エラーが返る', async () => {
395
+ // Given: 既に存在するメールアドレス
396
+ await prisma.user.create({
397
+ data: {
398
+ email: 'duplicate@example.com',
399
+ name: 'Existing User',
400
+ }
401
+ })
402
+
403
+ // When: 同じメールアドレスでユーザー作成
404
+ const result = await userRepository.create({
405
+ email: 'duplicate@example.com',
406
+ name: 'New User',
407
+ })
408
+
409
+ // Then: エラーが返る
410
+ expect(result.isSuccess).toBe(false)
411
+ if (!result.isSuccess) {
412
+ expect(result.error.code).toBe('CONFLICT')
413
+ }
414
+ })
415
+ })
416
+ ```
417
+
418
+ ---
419
+
420
+ ## 3. 統合テスト
421
+
422
+ ### パッケージ間の連携テスト
423
+
424
+ #### webから@repo/server-coreの使用
425
+
426
+ ```typescript
427
+ // apps/web/__tests__/integration/post-api.test.ts
428
+ import { apiClient } from '@/lib/api-client'
429
+ import { prisma } from '@repo/server-core/infrastructure/database/client'
430
+
431
+ describe('Post API統合テスト', () => {
432
+ beforeEach(async () => {
433
+ // テストデータのクリーンアップ
434
+ await prisma.post.deleteMany()
435
+ await prisma.user.deleteMany()
436
+ })
437
+
438
+ it('投稿一覧APIを呼び出すと、@repo/server-coreのRepositoryが使用され、データが取得できる', async () => {
439
+ // Given: ユーザーと投稿が存在する
440
+ const user = await prisma.user.create({
441
+ data: {
442
+ email: 'test@example.com',
443
+ name: 'Test User',
444
+ }
445
+ })
446
+
447
+ await prisma.post.create({
448
+ data: {
449
+ userId: user.id,
450
+ title: 'Test Post',
451
+ content: 'Content',
452
+ status: 'published',
453
+ }
454
+ })
455
+
456
+ // When: API呼び出し
457
+ const response = await apiClient.posts.$get({
458
+ query: { page: '1', limit: '10' }
459
+ })
460
+
461
+ // Then: データが取得できる
462
+ expect(response.ok).toBe(true)
463
+ const data = await response.json()
464
+ expect(data.posts).toHaveLength(1)
465
+ expect(data.posts[0].title).toBe('Test Post')
466
+ })
467
+
468
+ it('Repositoryパターンが正しく動作し、型定義が適切に共有される', async () => {
469
+ // Given: ユーザーが存在する
470
+ const user = await prisma.user.create({
471
+ data: {
472
+ email: 'test@example.com',
473
+ name: 'Test User',
474
+ }
475
+ })
476
+
477
+ // When: 投稿作成APIを呼び出し
478
+ const response = await apiClient.posts.$post({
479
+ json: {
480
+ title: 'New Post',
481
+ content: 'Content',
482
+ status: 'draft',
483
+ }
484
+ })
485
+
486
+ // Then: 作成成功
487
+ expect(response.ok).toBe(true)
488
+ const data = await response.json()
489
+ expect(data.post.title).toBe('New Post')
490
+ expect(data.post.status).toBe('draft')
491
+
492
+ // 型推論が正しく機能しているか確認(TypeScriptコンパイルエラーが出ないこと)
493
+ const title: string = data.post.title
494
+ const status: 'draft' | 'published' | 'archived' = data.post.status
495
+ expect(title).toBeDefined()
496
+ expect(status).toBeDefined()
497
+ })
498
+ })
499
+ ```
500
+
501
+ ### ビルドパイプラインのテスト
502
+
503
+ ```typescript
504
+ // __tests__/integration/build.test.ts
505
+ import { execSync } from 'child_process'
506
+
507
+ describe('ビルドパイプライン統合テスト', () => {
508
+ it('turbo run buildで全アプリケーションがビルド成功する', () => {
509
+ // When: ビルド実行
510
+ expect(() => {
511
+ execSync('pnpm turbo run build', { stdio: 'inherit' })
512
+ }).not.toThrow()
513
+ })
514
+
515
+ it('依存関係の順序が正しく解決される', () => {
516
+ // Given: @repo/server-coreがwebより先にビルドされる必要がある
517
+ // When: ビルド実行
518
+ const output = execSync('pnpm turbo run build --dry-run', { encoding: 'utf-8' })
519
+
520
+ // Then: 依存関係の順序が正しい
521
+ expect(output).toContain('@repo/server-core')
522
+ expect(output.indexOf('@repo/server-core')).toBeLessThan(output.indexOf('web'))
523
+ })
524
+ })
525
+ ```
526
+
527
+ ---
528
+
529
+ ## 4. E2Eテスト(Playwrightコード)
530
+
531
+ > **用語の明確化**: このセクションで説明する「E2Eテスト」は、`pnpm test:e2e` で実行されるPlaywrightコードベースの自動テストを指します。QAエージェントがPlaywright MCPを使用して実行する「Browserテスト」とは異なります。詳細は `docs/einja/steering/terminology.md` を参照してください。
532
+
533
+ ### Playwright設定
534
+
535
+ **playwright.config.ts**:
536
+
537
+ ```typescript
538
+ import { defineConfig, devices } from '@playwright/test'
539
+
540
+ export default defineConfig({
541
+ testDir: './e2e',
542
+ fullyParallel: true,
543
+ forbidOnly: !!process.env.CI,
544
+ retries: process.env.CI ? 2 : 0,
545
+ workers: process.env.CI ? 1 : undefined,
546
+ reporter: 'html',
547
+ use: {
548
+ baseURL: 'http://localhost:3000',
549
+ trace: 'on-first-retry',
550
+ },
551
+
552
+ projects: [
553
+ {
554
+ name: 'chromium',
555
+ use: { ...devices['Desktop Chrome'] },
556
+ },
557
+ {
558
+ name: 'firefox',
559
+ use: { ...devices['Desktop Firefox'] },
560
+ },
561
+ ],
562
+
563
+ webServer: {
564
+ command: 'pnpm dev',
565
+ url: 'http://localhost:3000',
566
+ reuseExistingServer: !process.env.CI,
567
+ },
568
+ })
569
+ ```
570
+
571
+ ### 完全なユーザーフロー
572
+
573
+ ```typescript
574
+ // e2e/post-creation.spec.ts
575
+ import { test, expect } from '@playwright/test'
576
+
577
+ test.describe('投稿作成フロー', () => {
578
+ test('ユーザーがwebにアクセスし、ログインして投稿を作成すると、データベースに保存され、一覧ページに表示される', async ({ page }) => {
579
+ // Given: ユーザーがログインページにアクセス
580
+ await page.goto('/auth/login')
581
+
582
+ // When: ログイン
583
+ await page.fill('[data-testid="email-input"]', 'test@example.com')
584
+ await page.fill('[data-testid="password-input"]', 'password123')
585
+ await page.click('[data-testid="login-button"]')
586
+
587
+ // Then: ダッシュボードにリダイレクト
588
+ await expect(page).toHaveURL('/dashboard')
589
+
590
+ // When: 投稿作成ページに移動
591
+ await page.click('[data-testid="create-post-button"]')
592
+ await expect(page).toHaveURL('/posts/new')
593
+
594
+ // When: 投稿フォームを入力
595
+ await page.fill('[data-testid="title-input"]', 'E2E Test Post')
596
+ await page.fill('[data-testid="content-input"]', 'This is E2E test content')
597
+ await page.selectOption('[data-testid="status-select"]', 'published')
598
+ await page.click('[data-testid="submit-button"]')
599
+
600
+ // Then: 投稿一覧ページにリダイレクト
601
+ await expect(page).toHaveURL('/posts')
602
+
603
+ // Then: 作成した投稿が一覧に表示される
604
+ await expect(page.locator('text=E2E Test Post')).toBeVisible()
605
+ })
606
+ })
607
+ ```
608
+
609
+ ### 管理画面フロー
610
+
611
+ ```typescript
612
+ // e2e/admin-user-management.spec.ts
613
+ import { test, expect } from '@playwright/test'
614
+
615
+ test.describe('管理画面ユーザー管理フロー', () => {
616
+ test('管理者がadminにログインし、ユーザー一覧を表示し、ユーザーを削除すると、データベースから削除され、webに反映される', async ({ page, context }) => {
617
+ // Given: 管理者がログイン
618
+ await page.goto('/admin/login')
619
+ await page.fill('[data-testid="email-input"]', 'admin@example.com')
620
+ await page.fill('[data-testid="password-input"]', 'admin123')
621
+ await page.click('[data-testid="login-button"]')
622
+
623
+ // Then: 管理画面ダッシュボードにリダイレクト
624
+ await expect(page).toHaveURL('/admin/dashboard')
625
+
626
+ // When: ユーザー一覧ページに移動
627
+ await page.click('[data-testid="users-link"]')
628
+ await expect(page).toHaveURL('/admin/users')
629
+
630
+ // Then: ユーザー一覧が表示される
631
+ await expect(page.locator('table[data-testid="users-table"]')).toBeVisible()
632
+
633
+ // When: ユーザーを削除
634
+ await page.click('[data-testid="delete-user-button-1"]')
635
+ await page.click('[data-testid="confirm-delete-button"]')
636
+
637
+ // Then: ユーザーが一覧から削除される
638
+ await expect(page.locator('[data-testid="user-row-1"]')).not.toBeVisible()
639
+
640
+ // When: webアプリで確認(別タブ)
641
+ const webPage = await context.newPage()
642
+ await webPage.goto('http://localhost:3000/users')
643
+
644
+ // Then: 削除されたユーザーが表示されない
645
+ await expect(webPage.locator('[data-testid="user-1"]')).not.toBeVisible()
646
+ })
647
+ })
648
+ ```
649
+
650
+ ### Cronジョブフロー
651
+
652
+ ```typescript
653
+ // e2e/cron-cleanup.spec.ts
654
+ import { test, expect } from '@playwright/test'
655
+ import { execSync } from 'child_process'
656
+ import { prisma } from '@repo/server-core/infrastructure/database/client'
657
+
658
+ test.describe('Cronジョブフロー', () => {
659
+ test('クリーンアップジョブが実行されると、期限切れセッションが削除される', async () => {
660
+ // Given: 期限切れセッションが存在する
661
+ const expiredSession = await prisma.session.create({
662
+ data: {
663
+ userId: 'user1',
664
+ token: 'expired-token',
665
+ expiresAt: new Date(Date.now() - 1000 * 60 * 60), // 1時間前
666
+ }
667
+ })
668
+
669
+ // When: クリーンアップジョブを実行
670
+ execSync('pnpm job:cleanup', { stdio: 'inherit' })
671
+
672
+ // Then: 期限切れセッションが削除される
673
+ const deletedSession = await prisma.session.findUnique({
674
+ where: { id: expiredSession.id }
675
+ })
676
+ expect(deletedSession).toBeNull()
677
+ })
678
+ })
679
+ ```
680
+
681
+ ---
682
+
683
+ ## 5. テストカバレッジ
684
+
685
+ ### カバレッジ目標
686
+
687
+ | 対象 | カバレッジ目標 | 優先度 |
688
+ |------|--------------|--------|
689
+ | 共有パッケージ (@repo/server-core) | 80%以上 | 最優先 |
690
+ | アプリケーション (web, admin, cron-worker) | 70%以上 | 高 |
691
+ | 統合テスト | 主要フロー100% | 高 |
692
+ | E2Eテスト | クリティカルパス100% | 中 |
693
+
694
+ ### カバレッジレポートの生成
695
+
696
+ ```bash
697
+ # カバレッジレポート生成
698
+ pnpm test:coverage
699
+
700
+ # カバレッジレポートの確認
701
+ open coverage/index.html
702
+ ```
703
+
704
+ ### カバレッジ確認コマンド
705
+
706
+ ```json
707
+ {
708
+ "scripts": {
709
+ "test": "vitest",
710
+ "test:coverage": "vitest run --coverage",
711
+ "test:watch": "vitest watch"
712
+ }
713
+ }
714
+ ```
715
+
716
+ ---
717
+
718
+ ## 6. テストツール
719
+
720
+ ### テストフレームワーク
721
+
722
+ | ツール | 用途 | 対象 |
723
+ |--------|------|------|
724
+ | Vitest | ユニットテスト、統合テスト | @repo/server-core, apps/* |
725
+ | Playwright (コード) | E2Eテスト(`pnpm test:e2e`で実行) | web, admin |
726
+ | Playwright MCP | Browserテスト(task-qaが実行) | 画面フロー確認 |
727
+ | Testing Library | Reactコンポーネントテスト | web, admin |
728
+
729
+ ### Vitest設定
730
+
731
+ **vitest.config.ts**:
732
+
733
+ ```typescript
734
+ import { defineConfig } from 'vitest/config'
735
+
736
+ export default defineConfig({
737
+ test: {
738
+ globals: true,
739
+ environment: 'node',
740
+ setupFiles: ['./vitest.setup.ts'],
741
+ coverage: {
742
+ provider: 'v8',
743
+ reporter: ['text', 'html', 'json'],
744
+ exclude: [
745
+ 'node_modules/',
746
+ '**/*.test.ts',
747
+ '**/*.spec.ts',
748
+ '**/dist/**',
749
+ '**/.next/**',
750
+ ],
751
+ },
752
+ },
753
+ })
754
+ ```
755
+
756
+ **vitest.setup.ts**:
757
+
758
+ ```typescript
759
+ import { beforeEach } from 'vitest'
760
+ import { prisma } from '@repo/server-core/infrastructure/database/client'
761
+
762
+ // 各テスト前にデータベースをクリーンアップ
763
+ beforeEach(async () => {
764
+ await prisma.post.deleteMany()
765
+ await prisma.session.deleteMany()
766
+ await prisma.account.deleteMany()
767
+ await prisma.user.deleteMany()
768
+ })
769
+ ```
770
+
771
+ ---
772
+
773
+ ## 7. テストの実行
774
+
775
+ ### ローカルでのテスト実行
776
+
777
+ ```bash
778
+ # すべてのユニットテストを実行
779
+ pnpm test
780
+
781
+ # 特定のファイルのみテスト
782
+ pnpm test UserRepository.test.ts
783
+
784
+ # ウォッチモード
785
+ pnpm test:watch
786
+
787
+ # カバレッジ付きでテスト
788
+ pnpm test:coverage
789
+
790
+ # E2Eテスト実行
791
+ pnpm test:e2e
792
+
793
+ # E2Eテスト(UI付き)
794
+ pnpm test:e2e:ui
795
+ ```
796
+
797
+ ### CI/CDでのテスト実行
798
+
799
+ ```yaml
800
+ # .github/workflows/ci.yml
801
+ - name: Run tests
802
+ run: pnpm turbo run test
803
+ env:
804
+ DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
805
+
806
+ - name: Run E2E tests
807
+ run: pnpm test:e2e
808
+ env:
809
+ DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
810
+
811
+ - name: Upload coverage
812
+ uses: codecov/codecov-action@v3
813
+ with:
814
+ files: ./coverage/coverage-final.json
815
+ ```
816
+
817
+ ---
818
+
819
+ ## 8. モックとスタブ
820
+
821
+ ### Repositoryのモック
822
+
823
+ ```typescript
824
+ // __tests__/mocks/userRepository.mock.ts
825
+ import type { IUserRepository } from '@repo/server-core/domain/repository-interfaces/IUserRepository'
826
+ import type { User } from '@repo/server-core/domain/entities/User'
827
+
828
+ export const createMockUserRepository = (): IUserRepository => ({
829
+ find: vi.fn().mockResolvedValue({
830
+ isSuccess: true,
831
+ value: {
832
+ id: '1',
833
+ email: 'test@example.com',
834
+ name: 'Test User',
835
+ createdAt: new Date(),
836
+ updatedAt: new Date(),
837
+ } as User,
838
+ }),
839
+
840
+ create: vi.fn().mockResolvedValue({
841
+ isSuccess: true,
842
+ value: {
843
+ id: '1',
844
+ email: 'test@example.com',
845
+ name: 'Test User',
846
+ createdAt: new Date(),
847
+ updatedAt: new Date(),
848
+ } as User,
849
+ }),
850
+
851
+ // 他のメソッドも同様にモック
852
+ })
853
+ ```
854
+
855
+ ### UseCaseでのモック使用
856
+
857
+ ```typescript
858
+ // __tests__/use-cases/UserUseCase.test.ts
859
+ import { createMockUserRepository } from '../mocks/userRepository.mock'
860
+ import { userUseCase } from '@repo/server-core/application/use-cases/UserUseCase'
861
+
862
+ describe('UserUseCase', () => {
863
+ it('createメソッドが正しく動作する', async () => {
864
+ // Given: モックRepository
865
+ const mockRepo = createMockUserRepository()
866
+
867
+ // When: UseCase実行(依存性注入)
868
+ const result = await userUseCase.create({
869
+ email: 'test@example.com',
870
+ name: 'Test User',
871
+ })
872
+
873
+ // Then: Repository呼び出しを確認
874
+ expect(mockRepo.create).toHaveBeenCalledWith({
875
+ email: 'test@example.com',
876
+ name: 'Test User',
877
+ })
878
+ })
879
+ })
880
+ ```
881
+
882
+ ---
883
+
884
+ ## まとめ
885
+
886
+ このテスト戦略に従うことで、以下を実現できます:
887
+
888
+ 1. **高品質**: カバレッジ目標達成により、バグの早期発見
889
+ 2. **自動化**: CI/CDパイプラインでの自動テスト実行
890
+ 3. **保守性**: モックとスタブによるテストの独立性確保
891
+ 4. **信頼性**: E2Eテストによる完全なユーザーフロー検証
892
+ 5. **効率性**: Vitestによる高速なテスト実行
893
+
894
+ すべてのテストは、このガイドラインに従って実装してください。
895
+ <!-- @einja:managed:end -->
896
+
897
+ <!-- @einja:seed:start id="testing-strategy-project" -->
898
+ <!-- プロジェクト固有の情報を記入 -->
899
+ <!-- @einja:seed:end -->