@martinffx/atelier 0.1.0

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 (130) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +285 -0
  4. package/agents/architect.md +38 -0
  5. package/agents/oracle.md +40 -0
  6. package/agents/recon.md +39 -0
  7. package/dist/atelier.js +33082 -0
  8. package/package.json +44 -0
  9. package/skills/code-conventional-commit/SKILL.md +134 -0
  10. package/skills/code-conventional-commit/evals/evals.json +23 -0
  11. package/skills/code-conventional-commit/workspace/iteration-1/eval-0/with_skill/outputs/commit-message.txt +4 -0
  12. package/skills/code-conventional-commit/workspace/iteration-1/eval-0/with_skill/outputs/eval_metadata.json +6 -0
  13. package/skills/code-conventional-commit/workspace/iteration-1/eval-1/with_skill/outputs/eval_metadata.json +6 -0
  14. package/skills/code-conventional-commit/workspace/iteration-1/eval-1/with_skill/outputs/validation-result.txt +16 -0
  15. package/skills/code-conventional-commit/workspace/iteration-1/eval-2/with_skill/outputs/commit-message.txt +8 -0
  16. package/skills/code-conventional-commit/workspace/iteration-1/eval-2/with_skill/outputs/eval_metadata.json +6 -0
  17. package/skills/code-conventional-commit/workspace/iteration-1/report.html +1325 -0
  18. package/skills/code-debug/SKILL.md +271 -0
  19. package/skills/code-debug/references/language-tools.md +139 -0
  20. package/skills/code-debug/references/techniques.md +117 -0
  21. package/skills/code-docs/SKILL.md +171 -0
  22. package/skills/code-docs/references/api-docs.md +116 -0
  23. package/skills/code-docs/references/changelog.md +64 -0
  24. package/skills/code-docs/references/readme-structure.md +94 -0
  25. package/skills/code-git-worktrees/SKILL.md +218 -0
  26. package/skills/code-perf/SKILL.md +163 -0
  27. package/skills/code-perf/references/common-bottlenecks.md +124 -0
  28. package/skills/code-perf/references/optimization-patterns.md +167 -0
  29. package/skills/code-perf/references/profiling-tools.md +126 -0
  30. package/skills/code-review/SKILL.md +78 -0
  31. package/skills/code-review/references/output.md +200 -0
  32. package/skills/code-review/references/reviewers.md +719 -0
  33. package/skills/code-review/references/rq.md +357 -0
  34. package/skills/code-review/references/rs.md +30 -0
  35. package/skills/code-security/SKILL.md +183 -0
  36. package/skills/code-security/references/owasp-top-10.md +129 -0
  37. package/skills/code-security/references/security-tools.md +102 -0
  38. package/skills/code-security/references/vulnerability-patterns.md +129 -0
  39. package/skills/code-stacked-commit/SKILL.md +155 -0
  40. package/skills/code-subagents/SKILL.md +184 -0
  41. package/skills/code-subagents/references/code-quality-reviewer-prompt.md +48 -0
  42. package/skills/code-subagents/references/implementor-prompt.md +79 -0
  43. package/skills/code-subagents/references/spec-reviewer-prompt.md +62 -0
  44. package/skills/oracle-architect/SKILL.md +246 -0
  45. package/skills/oracle-architect/references/api-design.md +345 -0
  46. package/skills/oracle-architect/references/data-modeling.md +318 -0
  47. package/skills/oracle-architect/references/ddd-patterns.md +247 -0
  48. package/skills/oracle-challenge/SKILL.md +126 -0
  49. package/skills/oracle-testing/SKILL.md +237 -0
  50. package/skills/oracle-testing/references/anti-patterns.md +618 -0
  51. package/skills/oracle-testing/references/boundaries.md +579 -0
  52. package/skills/oracle-testing/references/core-testing.md +691 -0
  53. package/skills/oracle-testing/references/edge-testing.md +794 -0
  54. package/skills/oracle-testing/references/stub-driven-tdd.md +322 -0
  55. package/skills/oracle-thinkdeep/SKILL.md +164 -0
  56. package/skills/python-architecture/SKILL.md +148 -0
  57. package/skills/python-architecture/references/data-modeling.md +558 -0
  58. package/skills/python-architecture/references/ddd.md +569 -0
  59. package/skills/python-architecture/references/functional-core.md +337 -0
  60. package/skills/python-build-tools/SKILL.md +153 -0
  61. package/skills/python-build-tools/references/basedpyright.md +321 -0
  62. package/skills/python-build-tools/references/ruff.md +225 -0
  63. package/skills/python-build-tools/references/uv.md +257 -0
  64. package/skills/python-fastapi/SKILL.md +184 -0
  65. package/skills/python-fastapi/references/api-design.md +563 -0
  66. package/skills/python-fastapi/references/dependencies.md +479 -0
  67. package/skills/python-fastapi/references/middleware.md +475 -0
  68. package/skills/python-fastapi/references/validation.md +497 -0
  69. package/skills/python-modern-python/SKILL.md +201 -0
  70. package/skills/python-modern-python/references/async.md +76 -0
  71. package/skills/python-modern-python/references/pattern-matching.md +89 -0
  72. package/skills/python-modern-python/references/typing.md +96 -0
  73. package/skills/python-monorepo/SKILL.md +146 -0
  74. package/skills/python-monorepo/references/docker.md +515 -0
  75. package/skills/python-monorepo/references/namespace-packages.md +426 -0
  76. package/skills/python-monorepo/references/workspace-config.md +341 -0
  77. package/skills/python-sqlalchemy/SKILL.md +119 -0
  78. package/skills/python-sqlalchemy/references/async.md +51 -0
  79. package/skills/python-sqlalchemy/references/models.md +67 -0
  80. package/skills/python-sqlalchemy/references/queries.md +57 -0
  81. package/skills/python-temporal/SKILL.md +128 -0
  82. package/skills/python-temporal/references/patterns.md +87 -0
  83. package/skills/python-temporal/references/testing.md +54 -0
  84. package/skills/python-testing/SKILL.md +161 -0
  85. package/skills/python-testing/references/boundaries.md +449 -0
  86. package/skills/python-testing/references/mocking.md +449 -0
  87. package/skills/python-testing/references/pytest.md +524 -0
  88. package/skills/spec-brainstorm/SKILL.md +325 -0
  89. package/skills/spec-finish/SKILL.md +159 -0
  90. package/skills/spec-implement/SKILL.md +296 -0
  91. package/skills/spec-orchestrator/SKILL.md +156 -0
  92. package/skills/spec-plan/SKILL.md +248 -0
  93. package/skills/typescript-api-design/SKILL.md +396 -0
  94. package/skills/typescript-api-design/references/error-responses.md +469 -0
  95. package/skills/typescript-build-tools/SKILL.md +447 -0
  96. package/skills/typescript-build-tools/references/biome.md +436 -0
  97. package/skills/typescript-build-tools/references/bun.md +432 -0
  98. package/skills/typescript-build-tools/references/turborepo.md +426 -0
  99. package/skills/typescript-build-tools/references/vitest.md +518 -0
  100. package/skills/typescript-drizzle-orm/SKILL.md +398 -0
  101. package/skills/typescript-drizzle-orm/references/cloudflare.md +392 -0
  102. package/skills/typescript-drizzle-orm/references/entity-pattern.md +412 -0
  103. package/skills/typescript-drizzle-orm/references/postgresql.md +191 -0
  104. package/skills/typescript-drizzle-orm/references/repository-pattern.md +479 -0
  105. package/skills/typescript-drizzle-orm/references/sqlite.md +291 -0
  106. package/skills/typescript-dynamodb-toolbox/SKILL.md +370 -0
  107. package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +574 -0
  108. package/skills/typescript-dynamodb-toolbox/references/error-handling.md +495 -0
  109. package/skills/typescript-dynamodb-toolbox/references/modeling.md +332 -0
  110. package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +631 -0
  111. package/skills/typescript-dynamodb-toolbox/references/testing.md +718 -0
  112. package/skills/typescript-dynamodb-toolbox/references/transactions.md +423 -0
  113. package/skills/typescript-effect-ts/SKILL.md +341 -0
  114. package/skills/typescript-effect-ts/references/core.md +412 -0
  115. package/skills/typescript-effect-ts/references/error-handling.md +468 -0
  116. package/skills/typescript-effect-ts/references/resources.md +476 -0
  117. package/skills/typescript-effect-ts/references/schema.md +536 -0
  118. package/skills/typescript-effect-ts/references/services.md +461 -0
  119. package/skills/typescript-fastify/SKILL.md +405 -0
  120. package/skills/typescript-fastify/references/plugins.md +329 -0
  121. package/skills/typescript-fastify/references/typeid.md +315 -0
  122. package/skills/typescript-functional-patterns/SKILL.md +402 -0
  123. package/skills/typescript-functional-patterns/references/adts.md +497 -0
  124. package/skills/typescript-functional-patterns/references/branded-types.md +542 -0
  125. package/skills/typescript-functional-patterns/references/migration-guide.md +695 -0
  126. package/skills/typescript-functional-patterns/references/option-result.md +585 -0
  127. package/skills/typescript-testing/SKILL.md +353 -0
  128. package/skills/typescript-testing/references/mocking.md +675 -0
  129. package/skills/typescript-testing/references/msw.md +852 -0
  130. package/skills/typescript-testing/references/snapshot-testing.md +774 -0
@@ -0,0 +1,718 @@
1
+ # Testing Patterns for DynamoDB
2
+
3
+ Comprehensive testing strategies for DynamoDB repositories using DynamoDB Local and Jest.
4
+
5
+ ## Test Architecture
6
+
7
+ ### Three Testing Layers
8
+
9
+ 1. **Repository Tests** - Data access, CRUD, queries, transactions
10
+ 2. **Service Tests** - Business logic, orchestration
11
+ 3. **Router Tests** - HTTP handling, validation, error responses
12
+
13
+ **Do NOT test:**
14
+ - Entity transformations in isolation (tested through Repository layer)
15
+ - Schema definitions (validated by DynamoDB Toolbox)
16
+
17
+ ### Why This Approach?
18
+
19
+ - **Repository tests** verify DynamoDB operations work correctly
20
+ - **Entity transformations** tested implicitly (full round-trip)
21
+ - **Service tests** verify business logic without database overhead
22
+ - **Router tests** verify HTTP layer without business logic
23
+
24
+ ## DynamoDB Local Setup
25
+
26
+ ### Installation
27
+
28
+ ```bash
29
+ # Using Docker (recommended)
30
+ docker pull amazon/dynamodb-local
31
+
32
+ # Run DynamoDB Local
33
+ docker run -p 8000:8000 amazon/dynamodb-local
34
+ ```
35
+
36
+ **Or use docker-compose:**
37
+
38
+ ```yaml
39
+ # docker-compose.yml
40
+ version: '3.8'
41
+ services:
42
+ dynamodb-local:
43
+ image: amazon/dynamodb-local
44
+ ports:
45
+ - "8000:8000"
46
+ command: ["-jar", "DynamoDBLocal.jar", "-sharedDb", "-inMemory"]
47
+ ```
48
+
49
+ ### Test Configuration
50
+
51
+ ```typescript
52
+ // src/test/config.ts
53
+ export const testConfig = {
54
+ database: {
55
+ endpoint: "http://localhost:8000",
56
+ region: "local",
57
+ credentials: {
58
+ accessKeyId: "dummy",
59
+ secretAccessKey: "dummy",
60
+ },
61
+ tableName: "test-github-table",
62
+ },
63
+ }
64
+ ```
65
+
66
+ ### Test Setup Pattern
67
+
68
+ ```typescript
69
+ import { DynamoDBClient, CreateTableCommand } from "@aws-sdk/client-dynamodb"
70
+ import { createTableParams, initializeSchema } from "./schema"
71
+
72
+ describe("UserRepository", () => {
73
+ let client: DynamoDBClient
74
+ let userRepo: UserRepository
75
+
76
+ // Unique ID per test run for isolation
77
+ const testRunId = Date.now()
78
+ const testUsername = `testuser-${testRunId}`
79
+
80
+ beforeAll(async () => {
81
+ // 1. Initialize DynamoDB client
82
+ client = new DynamoDBClient({
83
+ endpoint: testConfig.database.endpoint,
84
+ region: testConfig.database.region,
85
+ credentials: testConfig.database.credentials,
86
+ })
87
+
88
+ // 2. Create unique table for this test suite
89
+ const tableName = `test-user-repo-${testRunId}`
90
+ await client.send(new CreateTableCommand(createTableParams(tableName)))
91
+
92
+ // 3. Initialize schema with table name
93
+ const schema = initializeSchema(tableName, client)
94
+
95
+ // 4. Initialize repository
96
+ userRepo = new UserRepository(schema.user)
97
+ }, 15000) // 15s timeout for table creation
98
+
99
+ afterAll(async () => {
100
+ // Cleanup client connection
101
+ client.destroy()
102
+ })
103
+
104
+ // Tests...
105
+ })
106
+ ```
107
+
108
+ **Key Points:**
109
+ - Use unique table names per test suite (prevents collisions)
110
+ - Use 15s timeout for `beforeAll` (table creation is slow)
111
+ - Clean up client in `afterAll`
112
+ - Don't delete table (in-memory or cleaned by DynamoDB Local restart)
113
+
114
+ ## Fixture Factories
115
+
116
+ ### Entity Factory Pattern
117
+
118
+ ```typescript
119
+ // src/services/entities/fixtures.ts
120
+
121
+ let userCounter = 0
122
+
123
+ export function createUserEntity(
124
+ overrides: Partial<UserEntityOpts> = {}
125
+ ): UserEntity {
126
+ const count = ++userCounter
127
+ return new UserEntity({
128
+ username: `testuser${count}`,
129
+ email: `test${count}@example.com`,
130
+ bio: "Test user bio",
131
+ ...overrides, // Allow customization
132
+ })
133
+ }
134
+
135
+ let repoCounter = 0
136
+
137
+ export function createRepoEntity(
138
+ overrides: Partial<RepositoryEntityOpts> = {}
139
+ ): RepositoryEntity {
140
+ const count = ++repoCounter
141
+ return new RepositoryEntity({
142
+ owner: "testowner",
143
+ repoName: `repo${count}`,
144
+ description: "Test repository",
145
+ isPrivate: false,
146
+ ...overrides,
147
+ })
148
+ }
149
+ ```
150
+
151
+ **Usage in tests:**
152
+
153
+ ```typescript
154
+ it("should create user", async () => {
155
+ const user = createUserEntity({
156
+ username: "alice", // Override username
157
+ })
158
+
159
+ const created = await userRepo.create(user)
160
+
161
+ expect(created.username).toBe("alice")
162
+ expect(created.email).toBe(user.email)
163
+
164
+ // Cleanup
165
+ await userRepo.delete(user.username)
166
+ })
167
+ ```
168
+
169
+ ### Shared DynamoDB Client
170
+
171
+ ```typescript
172
+ // src/test/helpers.ts
173
+
174
+ let client: DynamoDBClient | undefined
175
+
176
+ export async function getDDBClient(): Promise<DynamoDBClient> {
177
+ if (!client) {
178
+ client = new DynamoDBClient({
179
+ endpoint: testConfig.database.endpoint,
180
+ region: testConfig.database.region,
181
+ credentials: testConfig.database.credentials,
182
+ })
183
+ }
184
+ return client
185
+ }
186
+
187
+ export async function cleanupDDBClient(): Promise<void> {
188
+ if (client !== undefined) {
189
+ client.destroy()
190
+ client = undefined
191
+ }
192
+ }
193
+ ```
194
+
195
+ ## CRUD Operation Tests
196
+
197
+ ### Create Tests
198
+
199
+ ```typescript
200
+ describe("create", () => {
201
+ it("should create user successfully", async () => {
202
+ const user = createUserEntity()
203
+
204
+ const created = await userRepo.create(user)
205
+
206
+ expect(created.username).toBe(user.username)
207
+ expect(created.email).toBe(user.email)
208
+ expect(created.created).toBeDefined()
209
+ expect(created.modified).toBeDefined()
210
+
211
+ // Cleanup
212
+ await userRepo.delete(user.username)
213
+ })
214
+
215
+ it("should throw DuplicateEntityError for duplicate username", async () => {
216
+ const user = createUserEntity({ username: "duplicate" })
217
+
218
+ await userRepo.create(user)
219
+
220
+ await expect(userRepo.create(user)).rejects.toThrow(DuplicateEntityError)
221
+ await expect(userRepo.create(user)).rejects.toThrow("already exists")
222
+
223
+ // Cleanup
224
+ await userRepo.delete(user.username)
225
+ })
226
+ })
227
+ ```
228
+
229
+ ### Read Tests
230
+
231
+ ```typescript
232
+ describe("get", () => {
233
+ it("should return user when exists", async () => {
234
+ const user = createUserEntity()
235
+ await userRepo.create(user)
236
+
237
+ const retrieved = await userRepo.get(user.username)
238
+
239
+ expect(retrieved).toBeDefined()
240
+ expect(retrieved?.username).toBe(user.username)
241
+
242
+ // Cleanup
243
+ await userRepo.delete(user.username)
244
+ })
245
+
246
+ it("should return undefined when not found", async () => {
247
+ const retrieved = await userRepo.get("nonexistent")
248
+
249
+ expect(retrieved).toBeUndefined()
250
+ })
251
+ })
252
+ ```
253
+
254
+ ### Update Tests
255
+
256
+ ```typescript
257
+ describe("update", () => {
258
+ it("should update user successfully", async () => {
259
+ const user = createUserEntity()
260
+ await userRepo.create(user)
261
+
262
+ const updated = user.updateUser({
263
+ email: "newemail@example.com",
264
+ })
265
+
266
+ const result = await userRepo.update(updated)
267
+
268
+ expect(result.email).toBe("newemail@example.com")
269
+ expect(result.username).toBe(user.username)
270
+
271
+ // Cleanup
272
+ await userRepo.delete(user.username)
273
+ })
274
+
275
+ it("should throw EntityNotFoundError when updating non-existent user", async () => {
276
+ const user = createUserEntity({ username: "nonexistent" })
277
+
278
+ await expect(userRepo.update(user)).rejects.toThrow(EntityNotFoundError)
279
+ await expect(userRepo.update(user)).rejects.toThrow("not found")
280
+ })
281
+ })
282
+ ```
283
+
284
+ ### Delete Tests
285
+
286
+ ```typescript
287
+ describe("delete", () => {
288
+ it("should delete user successfully", async () => {
289
+ const user = createUserEntity()
290
+ await userRepo.create(user)
291
+
292
+ await userRepo.delete(user.username)
293
+
294
+ const retrieved = await userRepo.get(user.username)
295
+ expect(retrieved).toBeUndefined()
296
+ })
297
+
298
+ it("should succeed silently when deleting non-existent user", async () => {
299
+ await expect(
300
+ userRepo.delete("nonexistent")
301
+ ).resolves.not.toThrow()
302
+ })
303
+ })
304
+ ```
305
+
306
+ ## Query and Pagination Tests
307
+
308
+ ### Basic Query Test
309
+
310
+ ```typescript
311
+ describe("listByOwner", () => {
312
+ it("should return all repos for owner", async () => {
313
+ const user = createUserEntity({ username: "alice" })
314
+ await userRepo.create(user)
315
+
316
+ const repo1 = createRepoEntity({ owner: "alice", repoName: "repo1" })
317
+ const repo2 = createRepoEntity({ owner: "alice", repoName: "repo2" })
318
+
319
+ await repoRepo.create(repo1)
320
+ await repoRepo.create(repo2)
321
+
322
+ const result = await repoRepo.listByOwner("alice")
323
+
324
+ expect(result.items).toHaveLength(2)
325
+ expect(result.items.map(r => r.repoName)).toContain("repo1")
326
+ expect(result.items.map(r => r.repoName)).toContain("repo2")
327
+
328
+ // Cleanup
329
+ await repoRepo.delete("alice", "repo1")
330
+ await repoRepo.delete("alice", "repo2")
331
+ await userRepo.delete("alice")
332
+ })
333
+ })
334
+ ```
335
+
336
+ ### Pagination Test
337
+
338
+ ```typescript
339
+ describe("pagination", () => {
340
+ it("should paginate results correctly", async () => {
341
+ const user = createUserEntity({ username: "alice" })
342
+ await userRepo.create(user)
343
+
344
+ // Create 5 repos
345
+ for (let i = 1; i <= 5; i++) {
346
+ const repo = createRepoEntity({
347
+ owner: "alice",
348
+ repoName: `repo${i}`,
349
+ })
350
+ await repoRepo.create(repo)
351
+ }
352
+
353
+ // First page (limit 2)
354
+ const page1 = await repoRepo.listByOwner("alice", 2)
355
+ expect(page1.items).toHaveLength(2)
356
+ expect(page1.offset).toBeDefined()
357
+
358
+ // Second page
359
+ const page2 = await repoRepo.listByOwner("alice", 2, page1.offset)
360
+ expect(page2.items).toHaveLength(2)
361
+ expect(page2.offset).toBeDefined()
362
+
363
+ // Third page (last page)
364
+ const page3 = await repoRepo.listByOwner("alice", 2, page2.offset)
365
+ expect(page3.items).toHaveLength(1)
366
+ expect(page3.offset).toBeUndefined() // No more pages
367
+
368
+ // Cleanup
369
+ for (let i = 1; i <= 5; i++) {
370
+ await repoRepo.delete("alice", `repo${i}`)
371
+ }
372
+ await userRepo.delete("alice")
373
+ })
374
+ })
375
+ ```
376
+
377
+ ### Temporal Sorting Test
378
+
379
+ ```typescript
380
+ describe("temporal sorting", () => {
381
+ it("should return repos in newest-first order", async () => {
382
+ const user = createUserEntity({ username: "alice" })
383
+ await userRepo.create(user)
384
+
385
+ // Create repos with delays to ensure different timestamps
386
+ const repo1 = createRepoEntity({ owner: "alice", repoName: "old" })
387
+ await repoRepo.create(repo1)
388
+
389
+ // Wait 100ms
390
+ await new Promise(resolve => setTimeout(resolve, 100))
391
+
392
+ const repo2 = createRepoEntity({ owner: "alice", repoName: "new" })
393
+ await repoRepo.create(repo2)
394
+
395
+ const result = await repoRepo.listByOwner("alice")
396
+
397
+ expect(result.items).toHaveLength(2)
398
+ expect(result.items[0].repoName).toBe("new") // Newest first
399
+ expect(result.items[1].repoName).toBe("old")
400
+
401
+ // Verify timestamps
402
+ expect(result.items[0].modified.toMillis()).toBeGreaterThan(
403
+ result.items[1].modified.toMillis()
404
+ )
405
+
406
+ // Cleanup
407
+ await repoRepo.delete("alice", "old")
408
+ await repoRepo.delete("alice", "new")
409
+ await userRepo.delete("alice")
410
+ })
411
+ })
412
+ ```
413
+
414
+ ## Concurrency Tests
415
+
416
+ ### Atomic Counter Test
417
+
418
+ ```typescript
419
+ describe("atomic counter", () => {
420
+ it("should handle concurrent increments without duplicates", async () => {
421
+ const orgId = "testorg"
422
+ const repoId = "testrepo"
423
+
424
+ // Execute 5 increments in parallel
425
+ const increments = Array.from({ length: 5 }, () =>
426
+ counterRepo.incrementAndGet(orgId, repoId)
427
+ )
428
+
429
+ const results = await Promise.all(increments)
430
+
431
+ // All results should be unique
432
+ const uniqueResults = new Set(results)
433
+ expect(uniqueResults.size).toBe(5)
434
+
435
+ // Results should be 1, 2, 3, 4, 5 (order may vary)
436
+ expect(results.sort()).toEqual([1, 2, 3, 4, 5])
437
+ })
438
+ })
439
+ ```
440
+
441
+ ### Concurrent Issue Creation Test
442
+
443
+ ```typescript
444
+ describe("concurrent issue creation", () => {
445
+ it("should assign unique sequential numbers", async () => {
446
+ const user = createUserEntity({ username: "alice" })
447
+ await userRepo.create(user)
448
+
449
+ const repo = createRepoEntity({ owner: "alice", repoName: "test" })
450
+ await repoRepo.create(repo)
451
+
452
+ // Create 5 issues concurrently
453
+ const issues = Array.from({ length: 5 }, (_, i) =>
454
+ IssueEntity.fromRequest({
455
+ owner: "alice",
456
+ repo_name: "test",
457
+ title: `Concurrent Issue ${i}`,
458
+ author: "alice",
459
+ })
460
+ )
461
+
462
+ const created = await Promise.all(
463
+ issues.map(issue => issueRepo.create(issue))
464
+ )
465
+
466
+ // All should have unique numbers
467
+ const numbers = created.map(i => i.issueNumber)
468
+ const uniqueNumbers = new Set(numbers)
469
+ expect(uniqueNumbers.size).toBe(5)
470
+
471
+ // Numbers should be sequential (1, 2, 3, 4, 5)
472
+ expect(numbers.sort()).toEqual([1, 2, 3, 4, 5])
473
+
474
+ // Cleanup
475
+ for (const issue of created) {
476
+ await issueRepo.delete(issue.owner, issue.repoName, issue.issueNumber)
477
+ }
478
+ await repoRepo.delete("alice", "test")
479
+ await userRepo.delete("alice")
480
+ })
481
+ })
482
+ ```
483
+
484
+ ## Transaction Tests
485
+
486
+ ### Parent Validation Test
487
+
488
+ ```typescript
489
+ describe("transaction validation", () => {
490
+ it("should throw EntityNotFoundError when creating issue for non-existent repo", async () => {
491
+ const issue = IssueEntity.fromRequest({
492
+ owner: "alice",
493
+ repo_name: "nonexistent",
494
+ title: "Test Issue",
495
+ author: "alice",
496
+ })
497
+
498
+ await expect(issueRepo.create(issue)).rejects.toThrow(EntityNotFoundError)
499
+ await expect(issueRepo.create(issue)).rejects.toThrow("RepositoryEntity")
500
+ })
501
+
502
+ it("should create issue when repo exists", async () => {
503
+ const user = createUserEntity({ username: "alice" })
504
+ await userRepo.create(user)
505
+
506
+ const repo = createRepoEntity({ owner: "alice", repoName: "test" })
507
+ await repoRepo.create(repo)
508
+
509
+ const issue = IssueEntity.fromRequest({
510
+ owner: "alice",
511
+ repo_name: "test",
512
+ title: "Test Issue",
513
+ author: "alice",
514
+ })
515
+
516
+ const created = await issueRepo.create(issue)
517
+
518
+ expect(created.issueNumber).toBe(1)
519
+ expect(created.title).toBe("Test Issue")
520
+
521
+ // Cleanup
522
+ await issueRepo.delete("alice", "test", 1)
523
+ await repoRepo.delete("alice", "test")
524
+ await userRepo.delete("alice")
525
+ })
526
+ })
527
+ ```
528
+
529
+ ## Test Isolation
530
+
531
+ ### Unique Test IDs
532
+
533
+ ```typescript
534
+ // Use timestamps for unique IDs
535
+ const testRunId = Date.now()
536
+ const username = `testuser-${testRunId}`
537
+
538
+ // Or use counters
539
+ let testCounter = 0
540
+ const getUniqueUsername = () => `testuser-${++testCounter}`
541
+ ```
542
+
543
+ ### Cleanup Pattern
544
+
545
+ ```typescript
546
+ describe("UserRepository", () => {
547
+ const createdUsers: string[] = []
548
+
549
+ afterEach(async () => {
550
+ // Clean up all created users after each test
551
+ for (const username of createdUsers) {
552
+ await userRepo.delete(username).catch(() => {}) // Ignore errors
553
+ }
554
+ createdUsers.length = 0 // Clear array
555
+ })
556
+
557
+ it("should create user", async () => {
558
+ const user = createUserEntity()
559
+ createdUsers.push(user.username) // Track for cleanup
560
+
561
+ const created = await userRepo.create(user)
562
+
563
+ expect(created.username).toBe(user.username)
564
+ })
565
+ })
566
+ ```
567
+
568
+ ## Error Handling Tests
569
+
570
+ ### Duplicate Entity Test
571
+
572
+ ```typescript
573
+ it("should throw DuplicateEntityError", async () => {
574
+ const user = createUserEntity({ username: "duplicate" })
575
+
576
+ await userRepo.create(user)
577
+
578
+ await expect(userRepo.create(user)).rejects.toThrow(DuplicateEntityError)
579
+ await expect(userRepo.create(user)).rejects.toThrow("already exists")
580
+ await expect(userRepo.create(user)).rejects.toMatchObject({
581
+ entityType: "UserEntity",
582
+ entityKey: "duplicate",
583
+ })
584
+
585
+ await userRepo.delete("duplicate")
586
+ })
587
+ ```
588
+
589
+ ### Not Found Test
590
+
591
+ ```typescript
592
+ it("should throw EntityNotFoundError", async () => {
593
+ const user = createUserEntity({ username: "nonexistent" })
594
+
595
+ await expect(userRepo.update(user)).rejects.toThrow(EntityNotFoundError)
596
+ await expect(userRepo.update(user)).rejects.toThrow("not found")
597
+ })
598
+ ```
599
+
600
+ ## Test Organization Best Practices
601
+
602
+ ### ✓ Do
603
+
604
+ - Use DynamoDB Local for development/CI
605
+ - Create unique table per test suite
606
+ - Use fixture factories for test data
607
+ - Clean up created entities after tests
608
+ - Test error paths explicitly
609
+ - Use unique IDs per test run
610
+ - Test pagination and sorting
611
+ - Test concurrent operations
612
+ - Use 15s timeout for `beforeAll`
613
+
614
+ ### ✗ Don't
615
+
616
+ - Don't test entity transformations in isolation
617
+ - Don't skip cleanup (causes test pollution)
618
+ - Don't use shared test data across tests
619
+ - Don't test schema definitions (trust DynamoDB Toolbox)
620
+ - Don't use production DynamoDB for tests
621
+ - Don't rely on test execution order
622
+
623
+ ## Testing Checklist
624
+
625
+ For each repository:
626
+
627
+ - [ ] **CRUD tests** - Create, read, update, delete
628
+ - [ ] **Error tests** - Duplicate, not found, validation
629
+ - [ ] **Query tests** - Main table and GSI queries
630
+ - [ ] **Pagination tests** - Multiple pages, last page
631
+ - [ ] **Sorting tests** - Temporal or status-based sorting
632
+ - [ ] **Concurrency tests** - Atomic operations, race conditions
633
+ - [ ] **Transaction tests** - Parent validation, multi-entity
634
+ - [ ] **Cleanup** - Delete all created test data
635
+ - [ ] **Isolation** - Unique IDs, independent tests
636
+
637
+ ## Example: Complete Test Suite
638
+
639
+ ```typescript
640
+ import { DynamoDBClient, CreateTableCommand } from "@aws-sdk/client-dynamodb"
641
+ import { createTableParams, initializeSchema } from "./schema"
642
+ import { createUserEntity } from "./fixtures"
643
+
644
+ describe("UserRepository", () => {
645
+ let client: DynamoDBClient
646
+ let userRepo: UserRepository
647
+ const testRunId = Date.now()
648
+ const createdUsers: string[] = []
649
+
650
+ beforeAll(async () => {
651
+ client = new DynamoDBClient({
652
+ endpoint: "http://localhost:8000",
653
+ region: "local",
654
+ credentials: { accessKeyId: "dummy", secretAccessKey: "dummy" },
655
+ })
656
+
657
+ const tableName = `test-user-repo-${testRunId}`
658
+ await client.send(new CreateTableCommand(createTableParams(tableName)))
659
+
660
+ const schema = initializeSchema(tableName, client)
661
+ userRepo = new UserRepository(schema.user)
662
+ }, 15000)
663
+
664
+ afterAll(async () => {
665
+ client.destroy()
666
+ })
667
+
668
+ afterEach(async () => {
669
+ for (const username of createdUsers) {
670
+ await userRepo.delete(username).catch(() => {})
671
+ }
672
+ createdUsers.length = 0
673
+ })
674
+
675
+ describe("create", () => {
676
+ it("should create user successfully", async () => {
677
+ const user = createUserEntity()
678
+ createdUsers.push(user.username)
679
+
680
+ const created = await userRepo.create(user)
681
+
682
+ expect(created.username).toBe(user.username)
683
+ expect(created.email).toBe(user.email)
684
+ })
685
+
686
+ it("should throw DuplicateEntityError", async () => {
687
+ const user = createUserEntity({ username: "duplicate" })
688
+ createdUsers.push(user.username)
689
+
690
+ await userRepo.create(user)
691
+
692
+ await expect(userRepo.create(user)).rejects.toThrow(DuplicateEntityError)
693
+ })
694
+ })
695
+
696
+ describe("get", () => {
697
+ it("should return user when exists", async () => {
698
+ const user = createUserEntity()
699
+ createdUsers.push(user.username)
700
+
701
+ await userRepo.create(user)
702
+
703
+ const retrieved = await userRepo.get(user.username)
704
+
705
+ expect(retrieved).toBeDefined()
706
+ expect(retrieved?.username).toBe(user.username)
707
+ })
708
+
709
+ it("should return undefined when not found", async () => {
710
+ const retrieved = await userRepo.get("nonexistent")
711
+
712
+ expect(retrieved).toBeUndefined()
713
+ })
714
+ })
715
+
716
+ // ... more tests
717
+ })
718
+ ```