@mmerterden/multi-agent-pipeline 20.36.0 → 20.37.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.
- package/CHANGELOG.md +37 -0
- package/docs/facts.json +2 -2
- package/docs/features.md +1 -1
- package/docs/recovery-guide.md +1 -1
- package/install/catalog-history.json +1 -1
- package/manifest.json +225 -148
- package/package.json +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +33 -154
- package/pipeline/commands/multi-agent/analysis/SKILL.md +14 -68
- package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +5 -8
- package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +38 -109
- package/pipeline/commands/multi-agent/channels/SKILL.md +38 -167
- package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +37 -144
- package/pipeline/commands/multi-agent/design-check/SKILL.md +27 -131
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +22 -48
- package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +37 -180
- package/pipeline/commands/multi-agent/jira/SKILL.md +16 -46
- package/pipeline/commands/multi-agent/manual-test/SKILL.md +12 -7
- package/pipeline/commands/multi-agent/refactor/SKILL.md +23 -184
- package/pipeline/commands/multi-agent/resume/SKILL.md +30 -136
- package/pipeline/commands/multi-agent/review/SKILL.md +24 -199
- package/pipeline/commands/multi-agent/setup/SKILL.md +25 -181
- package/pipeline/commands/multi-agent/stack/SKILL.md +9 -53
- package/pipeline/commands/multi-agent/status/SKILL.md +13 -2
- package/pipeline/commands/multi-agent/store-ready/SKILL.md +27 -226
- package/pipeline/commands/multi-agent/sync/SKILL.md +35 -164
- package/pipeline/commands/multi-agent/test-gen/SKILL.md +2 -4
- package/pipeline/commands/multi-agent/update/SKILL.md +18 -60
- package/pipeline/contract/CHANGELOG.md +9 -0
- package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
- package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
- package/pipeline/contract/fixtures/error-unsigned.json +1 -1
- package/pipeline/contract/fixtures/runs-awaiting-question.json +1 -1
- package/pipeline/contract/fixtures/runs-empty.json +1 -1
- package/pipeline/contract/fixtures/runs-failed.json +1 -1
- package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +1 -1
- package/pipeline/contract/fixtures/runs-pr-opened.json +1 -1
- package/pipeline/contract/fixtures/runs-running.json +1 -1
- package/pipeline/contract/manifest.json +5 -5
- package/pipeline/contract/types/index.d.ts +3 -2
- package/pipeline/multi-agent-refs/audit-guide.md +20 -0
- package/pipeline/multi-agent-refs/autopilot-on/details.md +148 -0
- package/pipeline/multi-agent-refs/channels/generator.md +14 -0
- package/pipeline/multi-agent-refs/channels/preview.md +13 -0
- package/pipeline/multi-agent-refs/channels/reference.md +46 -0
- package/pipeline/multi-agent-refs/complaint-analysis/phases.md +159 -0
- package/pipeline/multi-agent-refs/design-check/export.md +1 -0
- package/pipeline/multi-agent-refs/design-check/init-inventory.md +1 -1
- package/pipeline/multi-agent-refs/design-check/scope-and-ledger.md +64 -0
- package/pipeline/multi-agent-refs/features/analysis-command-notes.md +53 -0
- package/pipeline/multi-agent-refs/features/commit-multi-repo.md +63 -0
- package/pipeline/multi-agent-refs/features/dev-multi-repo.md +51 -0
- package/pipeline/multi-agent-refs/features/test-gap.md +40 -0
- package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
- package/pipeline/multi-agent-refs/garbage-collect/phases.md +60 -0
- package/pipeline/multi-agent-refs/ios-coding-standard/plan.md +71 -0
- package/pipeline/multi-agent-refs/ios-coding-standard/resolve-and-scan.md +135 -0
- package/pipeline/multi-agent-refs/jira/picker-detail.md +71 -0
- package/pipeline/multi-agent-refs/orchestrator/copilot-pipeline.md +88 -0
- package/pipeline/multi-agent-refs/orchestrator/dispatcher.md +96 -1
- package/pipeline/multi-agent-refs/orchestrator/phase-3-user-test.md +12 -9
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +4 -4
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +8 -47
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +21 -65
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +9 -59
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +0 -31
- package/pipeline/multi-agent-refs/refactor/sweep.md +190 -0
- package/pipeline/multi-agent-refs/resume/details.md +115 -2
- package/pipeline/multi-agent-refs/review/inputs.md +33 -1
- package/pipeline/multi-agent-refs/review/module-guides.md +9 -1
- package/pipeline/multi-agent-refs/review/panel.md +75 -0
- package/pipeline/multi-agent-refs/review/valven.md +5 -1
- package/pipeline/multi-agent-refs/rules.md +1 -1
- package/pipeline/multi-agent-refs/setup/hooks-and-plugins.md +4 -0
- package/pipeline/multi-agent-refs/setup/token-save-flow.md +13 -0
- package/pipeline/multi-agent-refs/stack/background.md +72 -0
- package/pipeline/multi-agent-refs/store-ready/gates.md +139 -0
- package/pipeline/multi-agent-refs/store-ready/report.md +40 -2
- package/pipeline/multi-agent-refs/sync/office.md +10 -0
- package/pipeline/multi-agent-refs/sync/overview.md +61 -0
- package/pipeline/multi-agent-refs/sync/repo.md +91 -0
- package/pipeline/multi-agent-refs/update/details.md +83 -0
- package/pipeline/office/index.html +6 -3
- package/pipeline/office/toolkit.json +1 -1
- package/pipeline/rules/figma-pipeline.md +6 -3
- package/pipeline/schemas/prefs.schema.json +1 -1
- package/pipeline/schemas/runs-index.schema.json +2 -1
- package/pipeline/scripts/_memory-policy.mjs +27 -5
- package/pipeline/scripts/checkpoint-fold.sh +59 -0
- package/pipeline/scripts/contract-server.mjs +1 -1
- package/pipeline/scripts/gate-ledger.mjs +1 -1
- package/pipeline/scripts/office-control.mjs +41 -15
- package/pipeline/scripts/offload-ref.sh +1 -1
- package/pipeline/scripts/rework-rulings.mjs +5 -3
- package/pipeline/scripts/runs-index.mjs +34 -7
- package/pipeline/scripts/scope-check-gate.mjs +15 -2
- package/pipeline/scripts/status-overview.mjs +401 -0
- package/pipeline/scripts/user-test-handoff.sh +203 -0
- package/pipeline/scripts/validate-planning.mjs +5 -3
- package/pipeline/scripts/validate-triage.mjs +2 -1
- package/pipeline/scripts/write-state.mjs +13 -4
- package/pipeline/skills/.skill-manifest.json +64 -64
- package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +7 -70
- package/pipeline/skills/shared/core/apple-archive-compliance/references/examples.md +67 -0
- package/pipeline/skills/shared/core/apple-archive-compliance/references/version-history.md +8 -0
- package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +10 -72
- package/pipeline/skills/shared/core/google-play-compliance/references/examples.md +69 -0
- package/pipeline/skills/shared/core/google-play-compliance/references/notes.md +7 -0
- package/pipeline/skills/shared/core/google-play-compliance/references/version-history.md +5 -0
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +39 -262
- package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +35 -73
- package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +37 -337
- package/pipeline/skills/shared/core/multi-agent-design-check/SKILL.md +27 -122
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +8 -340
- package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +37 -180
- package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +12 -7
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +23 -184
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +14 -82
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +25 -277
- package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +13 -2
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +73 -217
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +11 -162
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-code.md +257 -0
- package/pipeline/skills/shared/external/android-architecture/SKILL.md +7 -58
- package/pipeline/skills/shared/external/android-architecture/references/code-examples.md +68 -0
- package/pipeline/skills/shared/external/app-intents/SKILL.md +3 -45
- package/pipeline/skills/shared/external/app-intents/references/code-examples.md +54 -0
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +5 -81
- package/pipeline/skills/shared/external/app-store-optimization/references/engagement-and-localization.md +96 -0
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +6 -112
- package/pipeline/skills/shared/external/app-store-review/references/monetization-and-submission.md +138 -0
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +10 -117
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models-code.md +186 -0
- package/pipeline/skills/shared/external/authentication/SKILL.md +3 -50
- package/pipeline/skills/shared/external/authentication/references/code-examples.md +59 -0
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +3 -84
- package/pipeline/skills/shared/external/callkit-voip/references/code-examples.md +93 -0
- package/pipeline/skills/shared/external/ci-cd-pipelines/SKILL.md +7 -263
- package/pipeline/skills/shared/external/ci-cd-pipelines/references/pipeline-examples.md +281 -0
- package/pipeline/skills/shared/external/clean-code/SKILL.md +8 -41
- package/pipeline/skills/shared/external/clean-code/references/code-examples.md +56 -0
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +8 -146
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-code.md +214 -0
- package/pipeline/skills/shared/external/compose-components/SKILL.md +6 -80
- package/pipeline/skills/shared/external/compose-components/references/code-examples.md +89 -0
- package/pipeline/skills/shared/external/compose-navigation/SKILL.md +5 -64
- package/pipeline/skills/shared/external/compose-navigation/references/code-examples.md +70 -0
- package/pipeline/skills/shared/external/core-data/SKILL.md +9 -157
- package/pipeline/skills/shared/external/core-data/references/core-data-code.md +221 -0
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +3 -89
- package/pipeline/skills/shared/external/core-nfc/references/code-examples.md +98 -0
- package/pipeline/skills/shared/external/coreml/SKILL.md +17 -133
- package/pipeline/skills/shared/external/coreml/references/coreml-code.md +208 -0
- package/pipeline/skills/shared/external/css-modern/SKILL.md +8 -131
- package/pipeline/skills/shared/external/css-modern/references/code-examples.md +142 -0
- package/pipeline/skills/shared/external/database-patterns/SKILL.md +6 -103
- package/pipeline/skills/shared/external/database-patterns/references/orm-examples.md +112 -0
- package/pipeline/skills/shared/external/html-semantic/SKILL.md +7 -48
- package/pipeline/skills/shared/external/html-semantic/references/code-examples.md +54 -0
- package/pipeline/skills/shared/external/humanizer/SKILL.md +6 -19
- package/pipeline/skills/shared/external/humanizer/references/voice.md +24 -0
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +13 -170
- package/pipeline/skills/shared/external/ios-accessibility/references/assistive-tech-review.md +224 -0
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +2 -24
- package/pipeline/skills/shared/external/ios-localization/references/code-examples.md +30 -0
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +3 -58
- package/pipeline/skills/shared/external/ios-networking/references/code-examples.md +67 -0
- package/pipeline/skills/shared/external/ios-security/SKILL.md +4 -64
- package/pipeline/skills/shared/external/ios-security/references/code-examples.md +74 -0
- package/pipeline/skills/shared/external/live-activities/SKILL.md +4 -90
- package/pipeline/skills/shared/external/live-activities/references/code-examples.md +108 -0
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +9 -23
- package/pipeline/skills/shared/external/localization-reuse-map/references/project-settings.md +22 -0
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +5 -95
- package/pipeline/skills/shared/external/mapkit-location/references/code-examples.md +117 -0
- package/pipeline/skills/shared/external/nextjs-app-router/SKILL.md +7 -106
- package/pipeline/skills/shared/external/nextjs-app-router/references/app-router-examples.md +124 -0
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +2 -31
- package/pipeline/skills/shared/external/permissionkit/references/code-examples.md +37 -0
- package/pipeline/skills/shared/external/play-store-review/SKILL.md +6 -74
- package/pipeline/skills/shared/external/play-store-review/references/signing-and-policies.md +83 -0
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +8 -125
- package/pipeline/skills/shared/external/push-notifications/references/notification-code.md +176 -0
- package/pipeline/skills/shared/external/python-patterns/SKILL.md +7 -158
- package/pipeline/skills/shared/external/python-patterns/references/python-examples.md +176 -0
- package/pipeline/skills/shared/external/rest-api-design/SKILL.md +7 -157
- package/pipeline/skills/shared/external/rest-api-design/references/api-examples.md +175 -0
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +11 -230
- package/pipeline/skills/shared/external/swift-architecture/references/pattern-examples.md +295 -0
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +17 -149
- package/pipeline/skills/shared/external/swift-codable/references/codable-recipes.md +207 -0
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +12 -99
- package/pipeline/skills/shared/external/swift-concurrency/references/code-examples.md +100 -0
- package/pipeline/skills/shared/external/swift-concurrency/references/swift-6-2-additions.md +18 -0
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +9 -78
- package/pipeline/skills/shared/external/swift-formatstyle/references/formatstyle-examples.md +113 -0
- package/pipeline/skills/shared/external/swift-language/SKILL.md +3 -56
- package/pipeline/skills/shared/external/swift-language/references/code-examples.md +65 -0
- package/pipeline/skills/shared/external/swift-testing-pro/SKILL.md +1 -0
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +6 -122
- package/pipeline/skills/shared/external/swiftdata/references/code-examples.md +140 -0
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +12 -139
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/interop-code.md +198 -0
- package/pipeline/skills/shared/external/tailwind-css/SKILL.md +9 -81
- package/pipeline/skills/shared/external/tailwind-css/references/code-examples.md +91 -0
- package/pipeline/skills/shared/external/testing-backend/SKILL.md +7 -207
- package/pipeline/skills/shared/external/testing-backend/references/testing-examples.md +225 -0
- package/pipeline/skills/shared/external/typescript-patterns/SKILL.md +8 -89
- package/pipeline/skills/shared/external/typescript-patterns/references/code-examples.md +100 -0
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +2 -20
- package/pipeline/skills/shared/external/vision-framework/references/code-examples.md +26 -0
- package/pipeline/skills/shared/external/vue-composition/SKILL.md +7 -169
- package/pipeline/skills/shared/external/vue-composition/references/vue-examples.md +187 -0
- package/pipeline/skills/shared/external/web-accessibility/SKILL.md +7 -174
- package/pipeline/skills/shared/external/web-accessibility/references/a11y-examples.md +192 -0
- package/pipeline/skills/shared/external/web-performance/SKILL.md +8 -66
- package/pipeline/skills/shared/external/web-performance/references/code-examples.md +77 -0
- package/pipeline/skills/shared/external/web-testing/SKILL.md +11 -186
- package/pipeline/skills/shared/external/web-testing/references/testing-examples.md +213 -0
- /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/format-and-output.md +0 -0
- /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/publish-and-snapshot.md +0 -0
- /package/pipeline/skills/shared/external/localization-reuse-map/{reference → references}/sources-and-recipes.md +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Tailwind CSS Code Examples
|
|
2
|
+
|
|
3
|
+
Full code samples for the SKILL.md sections that link here. The rules and the decision guidance stay in SKILL.md; each heading below matches the SKILL.md section the code belongs to.
|
|
4
|
+
|
|
5
|
+
## Form
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<form class="space-y-6 max-w-md mx-auto">
|
|
9
|
+
<div>
|
|
10
|
+
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
11
|
+
Email
|
|
12
|
+
</label>
|
|
13
|
+
<input
|
|
14
|
+
type="email"
|
|
15
|
+
id="email"
|
|
16
|
+
class="
|
|
17
|
+
mt-1 block w-full rounded-lg border border-gray-300
|
|
18
|
+
px-4 py-2.5 text-gray-900
|
|
19
|
+
shadow-sm
|
|
20
|
+
placeholder:text-gray-400
|
|
21
|
+
focus:border-brand focus:ring-2 focus:ring-brand/20 focus:outline-none
|
|
22
|
+
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100
|
|
23
|
+
dark:focus:border-brand-light
|
|
24
|
+
"
|
|
25
|
+
placeholder="you@example.com"
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div>
|
|
30
|
+
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
31
|
+
Password
|
|
32
|
+
</label>
|
|
33
|
+
<input
|
|
34
|
+
type="password"
|
|
35
|
+
id="password"
|
|
36
|
+
class="
|
|
37
|
+
mt-1 block w-full rounded-lg border border-gray-300
|
|
38
|
+
px-4 py-2.5 text-gray-900
|
|
39
|
+
shadow-sm
|
|
40
|
+
focus:border-brand focus:ring-2 focus:ring-brand/20 focus:outline-none
|
|
41
|
+
dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100
|
|
42
|
+
"
|
|
43
|
+
/>
|
|
44
|
+
<p class="mt-1 text-xs text-gray-500">Must be at least 8 characters.</p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<button
|
|
48
|
+
type="submit"
|
|
49
|
+
class="
|
|
50
|
+
w-full rounded-lg bg-brand px-4 py-2.5
|
|
51
|
+
text-sm font-semibold text-white
|
|
52
|
+
shadow-sm
|
|
53
|
+
hover:bg-brand-dark focus:ring-2 focus:ring-brand/50 focus:outline-none
|
|
54
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
55
|
+
transition-colors
|
|
56
|
+
"
|
|
57
|
+
>
|
|
58
|
+
Sign in
|
|
59
|
+
</button>
|
|
60
|
+
</form>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Navigation
|
|
64
|
+
|
|
65
|
+
```html
|
|
66
|
+
<nav class="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
|
|
67
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
68
|
+
<div class="flex h-16 items-center justify-between">
|
|
69
|
+
<div class="flex items-center gap-8">
|
|
70
|
+
<a href="/" class="text-xl font-bold text-brand">Logo</a>
|
|
71
|
+
<div class="hidden md:flex gap-6">
|
|
72
|
+
<a href="#" class="text-sm font-medium text-gray-900 dark:text-gray-100 hover:text-brand">
|
|
73
|
+
Home
|
|
74
|
+
</a>
|
|
75
|
+
<a href="#" class="text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-brand">
|
|
76
|
+
Products
|
|
77
|
+
</a>
|
|
78
|
+
<a href="#" class="text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-brand">
|
|
79
|
+
About
|
|
80
|
+
</a>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
<button class="md:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800">
|
|
84
|
+
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
85
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
|
86
|
+
</svg>
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</nav>
|
|
91
|
+
```
|
|
@@ -38,70 +38,7 @@ tests/
|
|
|
38
38
|
|
|
39
39
|
### Fixtures and Factories
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
# tests/conftest.py
|
|
43
|
-
import pytest
|
|
44
|
-
from httpx import AsyncClient, ASGITransport
|
|
45
|
-
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
46
|
-
from app.main import app
|
|
47
|
-
from app.database import Base, get_db
|
|
48
|
-
|
|
49
|
-
TEST_DB_URL = "postgresql+asyncpg://test:test@localhost:5432/test_db"
|
|
50
|
-
|
|
51
|
-
@pytest.fixture(scope="session")
|
|
52
|
-
def anyio_backend():
|
|
53
|
-
return "asyncio"
|
|
54
|
-
|
|
55
|
-
@pytest.fixture(scope="session")
|
|
56
|
-
async def engine():
|
|
57
|
-
engine = create_async_engine(TEST_DB_URL)
|
|
58
|
-
async with engine.begin() as conn:
|
|
59
|
-
await conn.run_sync(Base.metadata.create_all)
|
|
60
|
-
yield engine
|
|
61
|
-
async with engine.begin() as conn:
|
|
62
|
-
await conn.run_sync(Base.metadata.drop_all)
|
|
63
|
-
await engine.dispose()
|
|
64
|
-
|
|
65
|
-
@pytest.fixture
|
|
66
|
-
async def db_session(engine):
|
|
67
|
-
async with AsyncSession(engine) as session:
|
|
68
|
-
async with session.begin():
|
|
69
|
-
yield session
|
|
70
|
-
await session.rollback() # Rollback after each test
|
|
71
|
-
|
|
72
|
-
@pytest.fixture
|
|
73
|
-
async def client(db_session):
|
|
74
|
-
async def override_get_db():
|
|
75
|
-
yield db_session
|
|
76
|
-
|
|
77
|
-
app.dependency_overrides[get_db] = override_get_db
|
|
78
|
-
transport = ASGITransport(app=app)
|
|
79
|
-
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
80
|
-
yield client
|
|
81
|
-
app.dependency_overrides.clear()
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
```python
|
|
85
|
-
# tests/factories.py
|
|
86
|
-
from dataclasses import dataclass, field
|
|
87
|
-
import uuid
|
|
88
|
-
|
|
89
|
-
@dataclass
|
|
90
|
-
class UserFactory:
|
|
91
|
-
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
92
|
-
name: str = "Test User"
|
|
93
|
-
email: str = field(default_factory=lambda: f"user-{uuid.uuid4().hex[:8]}@test.com")
|
|
94
|
-
role: str = "user"
|
|
95
|
-
|
|
96
|
-
def build(self) -> dict:
|
|
97
|
-
return {"id": self.id, "name": self.name, "email": self.email, "role": self.role}
|
|
98
|
-
|
|
99
|
-
async def create(self, db: AsyncSession) -> User:
|
|
100
|
-
user = User(**self.build())
|
|
101
|
-
db.add(user)
|
|
102
|
-
await db.flush()
|
|
103
|
-
return user
|
|
104
|
-
```
|
|
41
|
+
Code for this section: [Backend testing examples](references/testing-examples.md).
|
|
105
42
|
|
|
106
43
|
### Unit Tests
|
|
107
44
|
|
|
@@ -154,152 +91,11 @@ class TestUserService:
|
|
|
154
91
|
|
|
155
92
|
### Integration Tests
|
|
156
93
|
|
|
157
|
-
|
|
158
|
-
# tests/integration/test_api.py
|
|
159
|
-
import pytest
|
|
160
|
-
from httpx import AsyncClient
|
|
161
|
-
|
|
162
|
-
class TestUserAPI:
|
|
163
|
-
async def test_create_user(self, client: AsyncClient):
|
|
164
|
-
response = await client.post("/api/v1/users", json={
|
|
165
|
-
"name": "Alice",
|
|
166
|
-
"email": "alice@example.com",
|
|
167
|
-
"role": "user",
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
assert response.status_code == 201
|
|
171
|
-
data = response.json()["data"]
|
|
172
|
-
assert data["name"] == "Alice"
|
|
173
|
-
assert data["email"] == "alice@example.com"
|
|
174
|
-
assert "id" in data
|
|
175
|
-
|
|
176
|
-
async def test_create_user_duplicate_email(self, client: AsyncClient, db_session):
|
|
177
|
-
# Arrange: create existing user
|
|
178
|
-
from tests.factories import UserFactory
|
|
179
|
-
await UserFactory(email="taken@example.com").create(db_session)
|
|
180
|
-
|
|
181
|
-
# Act
|
|
182
|
-
response = await client.post("/api/v1/users", json={
|
|
183
|
-
"name": "Bob",
|
|
184
|
-
"email": "taken@example.com",
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
# Assert
|
|
188
|
-
assert response.status_code == 409
|
|
189
|
-
assert response.json()["error"]["code"] == "DUPLICATE_EMAIL"
|
|
190
|
-
|
|
191
|
-
async def test_list_users_pagination(self, client: AsyncClient, db_session):
|
|
192
|
-
from tests.factories import UserFactory
|
|
193
|
-
for i in range(25):
|
|
194
|
-
await UserFactory(name=f"User {i}").create(db_session)
|
|
195
|
-
|
|
196
|
-
response = await client.get("/api/v1/users?per_page=10&page=1")
|
|
197
|
-
|
|
198
|
-
assert response.status_code == 200
|
|
199
|
-
body = response.json()
|
|
200
|
-
assert len(body["data"]) == 10
|
|
201
|
-
assert body["meta"]["total"] == 25
|
|
202
|
-
assert body["meta"]["has_next"] is True
|
|
203
|
-
|
|
204
|
-
async def test_get_user_not_found(self, client: AsyncClient):
|
|
205
|
-
response = await client.get("/api/v1/users/nonexistent-id")
|
|
206
|
-
assert response.status_code == 404
|
|
207
|
-
```
|
|
94
|
+
Code for this section: [Backend testing examples](references/testing-examples.md).
|
|
208
95
|
|
|
209
96
|
## Jest Patterns (Node.js)
|
|
210
97
|
|
|
211
|
-
|
|
212
|
-
// tests/setup.ts
|
|
213
|
-
import { PrismaClient } from '@prisma/client';
|
|
214
|
-
|
|
215
|
-
const prisma = new PrismaClient();
|
|
216
|
-
|
|
217
|
-
beforeEach(async () => {
|
|
218
|
-
// Clean tables in correct order (respect foreign keys)
|
|
219
|
-
await prisma.$transaction([
|
|
220
|
-
prisma.postTag.deleteMany(),
|
|
221
|
-
prisma.post.deleteMany(),
|
|
222
|
-
prisma.user.deleteMany(),
|
|
223
|
-
]);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
afterAll(async () => {
|
|
227
|
-
await prisma.$disconnect();
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
export { prisma };
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
```typescript
|
|
234
|
-
// tests/unit/user-service.test.ts
|
|
235
|
-
import { UserService } from '@/services/user-service';
|
|
236
|
-
import { UserRepository } from '@/repositories/user-repository';
|
|
237
|
-
|
|
238
|
-
jest.mock('@/repositories/user-repository');
|
|
239
|
-
|
|
240
|
-
describe('UserService', () => {
|
|
241
|
-
let service: UserService;
|
|
242
|
-
let mockRepo: jest.Mocked<UserRepository>;
|
|
243
|
-
|
|
244
|
-
beforeEach(() => {
|
|
245
|
-
mockRepo = new UserRepository() as jest.Mocked<UserRepository>;
|
|
246
|
-
service = new UserService(mockRepo);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
describe('getUserById', () => {
|
|
250
|
-
it('returns user when found', async () => {
|
|
251
|
-
const expected = { id: '1', name: 'Alice', email: 'alice@test.com' };
|
|
252
|
-
mockRepo.findById.mockResolvedValue(expected);
|
|
253
|
-
|
|
254
|
-
const result = await service.getUserById('1');
|
|
255
|
-
|
|
256
|
-
expect(result).toEqual(expected);
|
|
257
|
-
expect(mockRepo.findById).toHaveBeenCalledWith('1');
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('throws NotFoundError when user does not exist', async () => {
|
|
261
|
-
mockRepo.findById.mockResolvedValue(null);
|
|
262
|
-
|
|
263
|
-
await expect(service.getUserById('999')).rejects.toThrow('User not found');
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
```typescript
|
|
270
|
-
// tests/integration/user-api.test.ts
|
|
271
|
-
import request from 'supertest';
|
|
272
|
-
import { app } from '@/app';
|
|
273
|
-
import { prisma } from '../setup';
|
|
274
|
-
|
|
275
|
-
describe('POST /api/v1/users', () => {
|
|
276
|
-
it('creates a user and returns 201', async () => {
|
|
277
|
-
const response = await request(app)
|
|
278
|
-
.post('/api/v1/users')
|
|
279
|
-
.send({ name: 'Alice', email: 'alice@example.com' })
|
|
280
|
-
.expect(201);
|
|
281
|
-
|
|
282
|
-
expect(response.body.data).toMatchObject({
|
|
283
|
-
name: 'Alice',
|
|
284
|
-
email: 'alice@example.com',
|
|
285
|
-
});
|
|
286
|
-
expect(response.body.data.id).toBeDefined();
|
|
287
|
-
|
|
288
|
-
// Verify in database
|
|
289
|
-
const user = await prisma.user.findUnique({ where: { email: 'alice@example.com' } });
|
|
290
|
-
expect(user).not.toBeNull();
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it('returns 422 for invalid email', async () => {
|
|
294
|
-
const response = await request(app)
|
|
295
|
-
.post('/api/v1/users')
|
|
296
|
-
.send({ name: 'Bob', email: 'invalid' })
|
|
297
|
-
.expect(422);
|
|
298
|
-
|
|
299
|
-
expect(response.body.error.details[0].field).toBe('email');
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
```
|
|
98
|
+
Code for this section: [Backend testing examples](references/testing-examples.md).
|
|
303
99
|
|
|
304
100
|
## Database Testing
|
|
305
101
|
|
|
@@ -394,3 +190,7 @@ describe('User API Contract', () => {
|
|
|
394
190
|
| Factory creates duplicate data | Unique constraints violated | Use random values in factories (UUID, random email) |
|
|
395
191
|
| Mock not applied | Wrong import path or scope | Verify mock target matches actual import path |
|
|
396
192
|
| DB schema out of sync | Migrations not run in test DB | Run migrations before test suite in CI |
|
|
193
|
+
|
|
194
|
+
## References
|
|
195
|
+
|
|
196
|
+
- [Backend testing examples](references/testing-examples.md): open when writing pytest fixtures, integration tests against a real database, or Jest tests for a Node.js service.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Backend testing examples
|
|
2
|
+
|
|
3
|
+
<!-- toc -->
|
|
4
|
+
- [Fixtures and Factories](#fixtures-and-factories)
|
|
5
|
+
- [Integration Tests](#integration-tests)
|
|
6
|
+
- [Jest Patterns (Node.js)](#jest-patterns-nodejs)
|
|
7
|
+
<!-- /toc -->
|
|
8
|
+
|
|
9
|
+
Full code for pytest fixtures and factories, integration tests and Jest patterns. SKILL.md keeps the pyramid, the rules and troubleshooting.
|
|
10
|
+
|
|
11
|
+
## Fixtures and Factories
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
# tests/conftest.py
|
|
15
|
+
import pytest
|
|
16
|
+
from httpx import AsyncClient, ASGITransport
|
|
17
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
18
|
+
from app.main import app
|
|
19
|
+
from app.database import Base, get_db
|
|
20
|
+
|
|
21
|
+
TEST_DB_URL = "postgresql+asyncpg://test:test@localhost:5432/test_db"
|
|
22
|
+
|
|
23
|
+
@pytest.fixture(scope="session")
|
|
24
|
+
def anyio_backend():
|
|
25
|
+
return "asyncio"
|
|
26
|
+
|
|
27
|
+
@pytest.fixture(scope="session")
|
|
28
|
+
async def engine():
|
|
29
|
+
engine = create_async_engine(TEST_DB_URL)
|
|
30
|
+
async with engine.begin() as conn:
|
|
31
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
32
|
+
yield engine
|
|
33
|
+
async with engine.begin() as conn:
|
|
34
|
+
await conn.run_sync(Base.metadata.drop_all)
|
|
35
|
+
await engine.dispose()
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
async def db_session(engine):
|
|
39
|
+
async with AsyncSession(engine) as session:
|
|
40
|
+
async with session.begin():
|
|
41
|
+
yield session
|
|
42
|
+
await session.rollback() # Rollback after each test
|
|
43
|
+
|
|
44
|
+
@pytest.fixture
|
|
45
|
+
async def client(db_session):
|
|
46
|
+
async def override_get_db():
|
|
47
|
+
yield db_session
|
|
48
|
+
|
|
49
|
+
app.dependency_overrides[get_db] = override_get_db
|
|
50
|
+
transport = ASGITransport(app=app)
|
|
51
|
+
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
|
52
|
+
yield client
|
|
53
|
+
app.dependency_overrides.clear()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# tests/factories.py
|
|
58
|
+
from dataclasses import dataclass, field
|
|
59
|
+
import uuid
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class UserFactory:
|
|
63
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
64
|
+
name: str = "Test User"
|
|
65
|
+
email: str = field(default_factory=lambda: f"user-{uuid.uuid4().hex[:8]}@test.com")
|
|
66
|
+
role: str = "user"
|
|
67
|
+
|
|
68
|
+
def build(self) -> dict:
|
|
69
|
+
return {"id": self.id, "name": self.name, "email": self.email, "role": self.role}
|
|
70
|
+
|
|
71
|
+
async def create(self, db: AsyncSession) -> User:
|
|
72
|
+
user = User(**self.build())
|
|
73
|
+
db.add(user)
|
|
74
|
+
await db.flush()
|
|
75
|
+
return user
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Integration Tests
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# tests/integration/test_api.py
|
|
82
|
+
import pytest
|
|
83
|
+
from httpx import AsyncClient
|
|
84
|
+
|
|
85
|
+
class TestUserAPI:
|
|
86
|
+
async def test_create_user(self, client: AsyncClient):
|
|
87
|
+
response = await client.post("/api/v1/users", json={
|
|
88
|
+
"name": "Alice",
|
|
89
|
+
"email": "alice@example.com",
|
|
90
|
+
"role": "user",
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
assert response.status_code == 201
|
|
94
|
+
data = response.json()["data"]
|
|
95
|
+
assert data["name"] == "Alice"
|
|
96
|
+
assert data["email"] == "alice@example.com"
|
|
97
|
+
assert "id" in data
|
|
98
|
+
|
|
99
|
+
async def test_create_user_duplicate_email(self, client: AsyncClient, db_session):
|
|
100
|
+
# Arrange: create existing user
|
|
101
|
+
from tests.factories import UserFactory
|
|
102
|
+
await UserFactory(email="taken@example.com").create(db_session)
|
|
103
|
+
|
|
104
|
+
# Act
|
|
105
|
+
response = await client.post("/api/v1/users", json={
|
|
106
|
+
"name": "Bob",
|
|
107
|
+
"email": "taken@example.com",
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
# Assert
|
|
111
|
+
assert response.status_code == 409
|
|
112
|
+
assert response.json()["error"]["code"] == "DUPLICATE_EMAIL"
|
|
113
|
+
|
|
114
|
+
async def test_list_users_pagination(self, client: AsyncClient, db_session):
|
|
115
|
+
from tests.factories import UserFactory
|
|
116
|
+
for i in range(25):
|
|
117
|
+
await UserFactory(name=f"User {i}").create(db_session)
|
|
118
|
+
|
|
119
|
+
response = await client.get("/api/v1/users?per_page=10&page=1")
|
|
120
|
+
|
|
121
|
+
assert response.status_code == 200
|
|
122
|
+
body = response.json()
|
|
123
|
+
assert len(body["data"]) == 10
|
|
124
|
+
assert body["meta"]["total"] == 25
|
|
125
|
+
assert body["meta"]["has_next"] is True
|
|
126
|
+
|
|
127
|
+
async def test_get_user_not_found(self, client: AsyncClient):
|
|
128
|
+
response = await client.get("/api/v1/users/nonexistent-id")
|
|
129
|
+
assert response.status_code == 404
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Jest Patterns (Node.js)
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// tests/setup.ts
|
|
136
|
+
import { PrismaClient } from '@prisma/client';
|
|
137
|
+
|
|
138
|
+
const prisma = new PrismaClient();
|
|
139
|
+
|
|
140
|
+
beforeEach(async () => {
|
|
141
|
+
// Clean tables in correct order (respect foreign keys)
|
|
142
|
+
await prisma.$transaction([
|
|
143
|
+
prisma.postTag.deleteMany(),
|
|
144
|
+
prisma.post.deleteMany(),
|
|
145
|
+
prisma.user.deleteMany(),
|
|
146
|
+
]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
afterAll(async () => {
|
|
150
|
+
await prisma.$disconnect();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
export { prisma };
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// tests/unit/user-service.test.ts
|
|
158
|
+
import { UserService } from '@/services/user-service';
|
|
159
|
+
import { UserRepository } from '@/repositories/user-repository';
|
|
160
|
+
|
|
161
|
+
jest.mock('@/repositories/user-repository');
|
|
162
|
+
|
|
163
|
+
describe('UserService', () => {
|
|
164
|
+
let service: UserService;
|
|
165
|
+
let mockRepo: jest.Mocked<UserRepository>;
|
|
166
|
+
|
|
167
|
+
beforeEach(() => {
|
|
168
|
+
mockRepo = new UserRepository() as jest.Mocked<UserRepository>;
|
|
169
|
+
service = new UserService(mockRepo);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('getUserById', () => {
|
|
173
|
+
it('returns user when found', async () => {
|
|
174
|
+
const expected = { id: '1', name: 'Alice', email: 'alice@test.com' };
|
|
175
|
+
mockRepo.findById.mockResolvedValue(expected);
|
|
176
|
+
|
|
177
|
+
const result = await service.getUserById('1');
|
|
178
|
+
|
|
179
|
+
expect(result).toEqual(expected);
|
|
180
|
+
expect(mockRepo.findById).toHaveBeenCalledWith('1');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('throws NotFoundError when user does not exist', async () => {
|
|
184
|
+
mockRepo.findById.mockResolvedValue(null);
|
|
185
|
+
|
|
186
|
+
await expect(service.getUserById('999')).rejects.toThrow('User not found');
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// tests/integration/user-api.test.ts
|
|
194
|
+
import request from 'supertest';
|
|
195
|
+
import { app } from '@/app';
|
|
196
|
+
import { prisma } from '../setup';
|
|
197
|
+
|
|
198
|
+
describe('POST /api/v1/users', () => {
|
|
199
|
+
it('creates a user and returns 201', async () => {
|
|
200
|
+
const response = await request(app)
|
|
201
|
+
.post('/api/v1/users')
|
|
202
|
+
.send({ name: 'Alice', email: 'alice@example.com' })
|
|
203
|
+
.expect(201);
|
|
204
|
+
|
|
205
|
+
expect(response.body.data).toMatchObject({
|
|
206
|
+
name: 'Alice',
|
|
207
|
+
email: 'alice@example.com',
|
|
208
|
+
});
|
|
209
|
+
expect(response.body.data.id).toBeDefined();
|
|
210
|
+
|
|
211
|
+
// Verify in database
|
|
212
|
+
const user = await prisma.user.findUnique({ where: { email: 'alice@example.com' } });
|
|
213
|
+
expect(user).not.toBeNull();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('returns 422 for invalid email', async () => {
|
|
217
|
+
const response = await request(app)
|
|
218
|
+
.post('/api/v1/users')
|
|
219
|
+
.send({ name: 'Bob', email: 'invalid' })
|
|
220
|
+
.expect(422);
|
|
221
|
+
|
|
222
|
+
expect(response.body.error.details[0].field).toBe('email');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
```
|
|
@@ -211,100 +211,15 @@ function handleInput(input: string | number | { value: string }) {
|
|
|
211
211
|
|
|
212
212
|
## Discriminated Unions
|
|
213
213
|
|
|
214
|
-
|
|
215
|
-
// API response pattern
|
|
216
|
-
type ApiResult<T> =
|
|
217
|
-
| { status: 'success'; data: T }
|
|
218
|
-
| { status: 'error'; error: { code: string; message: string } }
|
|
219
|
-
| { status: 'loading' };
|
|
220
|
-
|
|
221
|
-
function handleResult<T>(result: ApiResult<T>): string {
|
|
222
|
-
switch (result.status) {
|
|
223
|
-
case 'success':
|
|
224
|
-
return JSON.stringify(result.data);
|
|
225
|
-
case 'error':
|
|
226
|
-
return `Error ${result.error.code}: ${result.error.message}`;
|
|
227
|
-
case 'loading':
|
|
228
|
-
return 'Loading...';
|
|
229
|
-
}
|
|
230
|
-
}
|
|
214
|
+
Model API results and events as unions keyed on a literal field (`status`, `type`), switch on that field, and end the switch with an `exhaustiveCheck(value: never)` helper so a new variant fails to compile.
|
|
231
215
|
|
|
232
|
-
|
|
233
|
-
type AppEvent =
|
|
234
|
-
| { type: 'USER_LOGIN'; payload: { userId: string } }
|
|
235
|
-
| { type: 'USER_LOGOUT' }
|
|
236
|
-
| { type: 'ITEM_ADDED'; payload: { itemId: string; quantity: number } }
|
|
237
|
-
| { type: 'ITEM_REMOVED'; payload: { itemId: string } };
|
|
238
|
-
|
|
239
|
-
function handleEvent(event: AppEvent): void {
|
|
240
|
-
switch (event.type) {
|
|
241
|
-
case 'USER_LOGIN':
|
|
242
|
-
console.log(`User ${event.payload.userId} logged in`);
|
|
243
|
-
break;
|
|
244
|
-
case 'USER_LOGOUT':
|
|
245
|
-
console.log('User logged out');
|
|
246
|
-
break;
|
|
247
|
-
case 'ITEM_ADDED':
|
|
248
|
-
console.log(`Added ${event.payload.quantity} of ${event.payload.itemId}`);
|
|
249
|
-
break;
|
|
250
|
-
case 'ITEM_REMOVED':
|
|
251
|
-
console.log(`Removed ${event.payload.itemId}`);
|
|
252
|
-
break;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Exhaustive check helper
|
|
257
|
-
function exhaustiveCheck(value: never): never {
|
|
258
|
-
throw new Error(`Unhandled value: ${JSON.stringify(value)}`);
|
|
259
|
-
}
|
|
260
|
-
```
|
|
216
|
+
Code: [Discriminated Unions](references/code-examples.md#discriminated-unions).
|
|
261
217
|
|
|
262
218
|
## Zod for Runtime Validation
|
|
263
219
|
|
|
264
|
-
|
|
265
|
-
import { z } from 'zod';
|
|
266
|
-
|
|
267
|
-
// Define schema
|
|
268
|
-
const UserSchema = z.object({
|
|
269
|
-
id: z.string().uuid(),
|
|
270
|
-
name: z.string().min(1).max(100),
|
|
271
|
-
email: z.string().email(),
|
|
272
|
-
role: z.enum(['admin', 'user', 'viewer']),
|
|
273
|
-
age: z.number().int().min(0).max(150).optional(),
|
|
274
|
-
tags: z.array(z.string()).default([]),
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// Infer TypeScript type from schema
|
|
278
|
-
type User = z.infer<typeof UserSchema>;
|
|
279
|
-
|
|
280
|
-
// Validate at runtime
|
|
281
|
-
function createUser(input: unknown): User {
|
|
282
|
-
return UserSchema.parse(input); // throws ZodError on failure
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Safe parse (no throw)
|
|
286
|
-
function validateUser(input: unknown): ApiResult<User> {
|
|
287
|
-
const result = UserSchema.safeParse(input);
|
|
288
|
-
if (result.success) {
|
|
289
|
-
return { status: 'success', data: result.data };
|
|
290
|
-
}
|
|
291
|
-
return {
|
|
292
|
-
status: 'error',
|
|
293
|
-
error: { code: 'VALIDATION', message: result.error.message },
|
|
294
|
-
};
|
|
295
|
-
}
|
|
220
|
+
Define a schema once, infer the type with `z.infer`, use `parse` where failure should throw and `safeParse` where it should return a result, and derive create/update schemas with `omit`, `partial` and `required`.
|
|
296
221
|
|
|
297
|
-
|
|
298
|
-
const CreateUserSchema = UserSchema.omit({ id: true });
|
|
299
|
-
const UpdateUserSchema = UserSchema.partial().required({ id: true });
|
|
300
|
-
|
|
301
|
-
// API request validation
|
|
302
|
-
const PaginationSchema = z.object({
|
|
303
|
-
page: z.coerce.number().int().min(1).default(1),
|
|
304
|
-
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
305
|
-
sort: z.enum(['asc', 'desc']).default('desc'),
|
|
306
|
-
});
|
|
307
|
-
```
|
|
222
|
+
Code: [Zod for Runtime Validation](references/code-examples.md#zod-for-runtime-validation).
|
|
308
223
|
|
|
309
224
|
## Do's
|
|
310
225
|
|
|
@@ -337,3 +252,7 @@ const PaginationSchema = z.object({
|
|
|
337
252
|
| "Argument of type 'string' is not assignable to parameter of type 'X'" | Widened literal type | Use `as const` or `satisfies` to preserve literals |
|
|
338
253
|
| Circular type reference | Self-referencing generic | Use `interface` instead of `type` for recursive structures |
|
|
339
254
|
| Slow TypeScript compiler | Complex conditional types | Simplify type computations; avoid deeply nested generics |
|
|
255
|
+
|
|
256
|
+
## References
|
|
257
|
+
|
|
258
|
+
- [Code examples](references/code-examples.md): open for the full code behind Discriminated Unions, Zod for Runtime Validation.
|