cfsa-antigravity 2.19.0 → 2.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/.agent/kit-sync.md +3 -3
- package/template/.claude/commands/audit-ambiguity-execute.md +6 -0
- package/template/.claude/commands/audit-ambiguity-rubrics.md +6 -0
- package/template/.claude/commands/audit-ambiguity.md +6 -0
- package/template/.claude/commands/bootstrap-agents-fill.md +6 -0
- package/template/.claude/commands/bootstrap-agents-provision.md +6 -0
- package/template/.claude/commands/bootstrap-agents.md +6 -0
- package/template/.claude/commands/create-prd-architecture.md +6 -0
- package/template/.claude/commands/create-prd-compile.md +6 -0
- package/template/.claude/commands/create-prd-design-system.md +6 -0
- package/template/.claude/commands/create-prd-security.md +6 -0
- package/template/.claude/commands/create-prd-stack.md +6 -0
- package/template/.claude/commands/decompose-architecture-structure.md +6 -0
- package/template/.claude/commands/decompose-architecture-validate.md +6 -0
- package/template/.claude/commands/evolve-contract.md +6 -0
- package/template/.claude/commands/evolve-feature-cascade.md +6 -0
- package/template/.claude/commands/evolve-feature-classify.md +6 -0
- package/template/.claude/commands/evolve-feature.md +6 -0
- package/template/.claude/commands/ideate-discover.md +6 -0
- package/template/.claude/commands/ideate-extract.md +6 -0
- package/template/.claude/commands/ideate-validate.md +6 -0
- package/template/.claude/commands/implement-slice-setup.md +6 -0
- package/template/.claude/commands/implement-slice-tdd.md +6 -0
- package/template/.claude/commands/plan-phase-preflight.md +6 -0
- package/template/.claude/commands/plan-phase-write.md +6 -0
- package/template/.claude/commands/propagate-decision-apply.md +6 -0
- package/template/.claude/commands/propagate-decision-scan.md +6 -0
- package/template/.claude/commands/propagate-decision.md +6 -0
- package/template/.claude/commands/remediate-pipeline-assess.md +6 -0
- package/template/.claude/commands/remediate-pipeline-execute.md +6 -0
- package/template/.claude/commands/remediate-pipeline.md +6 -0
- package/template/.claude/commands/remediate-shard-split.md +6 -0
- package/template/.claude/commands/resolve-ambiguity.md +6 -0
- package/template/.claude/commands/setup-workspace-cicd.md +6 -0
- package/template/.claude/commands/setup-workspace-data.md +6 -0
- package/template/.claude/commands/setup-workspace-hosting.md +6 -0
- package/template/.claude/commands/setup-workspace-scaffold.md +6 -0
- package/template/.claude/commands/sync-kit.md +6 -0
- package/template/.claude/commands/update-architecture-map.md +6 -0
- package/template/.claude/commands/validate-phase-quality.md +6 -0
- package/template/.claude/commands/validate-phase-readiness.md +6 -0
- package/template/.claude/commands/verify-infrastructure.md +6 -0
- package/template/.claude/commands/write-architecture-spec-deepen.md +6 -0
- package/template/.claude/commands/write-architecture-spec-design.md +6 -0
- package/template/.claude/commands/write-be-spec-classify.md +6 -0
- package/template/.claude/commands/write-be-spec-write.md +6 -0
- package/template/.claude/commands/write-fe-spec-classify.md +6 -0
- package/template/.claude/commands/write-fe-spec-write.md +6 -0
- package/template/.claude/skills/accessibility/SKILL.md +522 -0
- package/template/.claude/skills/accessibility/references/WCAG.md +162 -0
- package/template/.claude/skills/accessibility/references/ia-spec-checklist.md +35 -0
- package/template/.claude/skills/adversarial-review/SKILL.md +90 -0
- package/template/.claude/skills/antigravity-workflows/SKILL.md +81 -0
- package/template/.claude/skills/antigravity-workflows/resources/implementation-playbook.md +36 -0
- package/template/.claude/skills/api-design-principles/SKILL.md +169 -0
- package/template/.claude/skills/api-design-principles/assets/api-design-checklist.md +155 -0
- package/template/.claude/skills/api-design-principles/assets/rest-api-template.py +182 -0
- package/template/.claude/skills/api-design-principles/references/graphql-schema-design.md +583 -0
- package/template/.claude/skills/api-design-principles/references/rest-best-practices.md +408 -0
- package/template/.claude/skills/api-design-principles/resources/implementation-playbook.md +513 -0
- package/template/.claude/skills/api-versioning/SKILL.md +166 -0
- package/template/.claude/skills/api-versioning/references/typescript.md +157 -0
- package/template/.claude/skills/architecture-mapping/SKILL.md +219 -0
- package/template/.claude/skills/bootstrap-agents/SKILL.md +258 -0
- package/template/.claude/skills/brainstorming/SKILL.md +177 -0
- package/template/.claude/skills/brand-guidelines/SKILL.md +44 -0
- package/template/.claude/skills/clean-code/SKILL.md +196 -0
- package/template/.claude/skills/clean-code/references/typescript.md +126 -0
- package/template/.claude/skills/code-review-pro/SKILL.md +152 -0
- package/template/.claude/skills/concise-planning/SKILL.md +107 -0
- package/template/.claude/skills/cross-layer-consistency/SKILL.md +117 -0
- package/template/.claude/skills/database-schema-design/SKILL.md +205 -0
- package/template/.claude/skills/database-schema-design/references/relational.md +228 -0
- package/template/.claude/skills/deployment-procedures/SKILL.md +241 -0
- package/template/.claude/skills/design-anti-cliche/SKILL.md +159 -0
- package/template/.claude/skills/design-direction/SKILL.md +45 -0
- package/template/.claude/skills/error-handling-patterns/SKILL.md +226 -0
- package/template/.claude/skills/error-handling-patterns/references/go.md +162 -0
- package/template/.claude/skills/error-handling-patterns/references/python.md +262 -0
- package/template/.claude/skills/error-handling-patterns/references/rust.md +112 -0
- package/template/.claude/skills/error-handling-patterns/references/typescript.md +178 -0
- package/template/.claude/skills/find-skills/SKILL.md +145 -0
- package/template/.claude/skills/git-advanced/SKILL.md +972 -0
- package/template/.claude/skills/git-workflow/SKILL.md +420 -0
- package/template/.claude/skills/idea-extraction/SKILL.md +644 -0
- package/template/.claude/skills/logging-best-practices/SKILL.md +192 -0
- package/template/.claude/skills/logging-best-practices/references/go.md +49 -0
- package/template/.claude/skills/logging-best-practices/references/python.md +52 -0
- package/template/.claude/skills/logging-best-practices/references/typescript.md +215 -0
- package/template/.claude/skills/migration-management/SKILL.md +200 -0
- package/template/.claude/skills/migration-management/references/relational.md +214 -0
- package/template/.claude/skills/minimalist-surgical-development/SKILL.md +135 -0
- package/template/.claude/skills/parallel-agents/SKILL.md +165 -0
- package/template/.claude/skills/parallel-debugging/SKILL.md +135 -0
- package/template/.claude/skills/parallel-feature-development/SKILL.md +157 -0
- package/template/.claude/skills/performance-budgeting/SKILL.md +144 -0
- package/template/.claude/skills/pipeline-rubrics/SKILL.md +51 -0
- package/template/.claude/skills/pipeline-rubrics/references/architecture-rubric.md +19 -0
- package/template/.claude/skills/pipeline-rubrics/references/be-rubric.md +21 -0
- package/template/.claude/skills/pipeline-rubrics/references/fe-rubric.md +20 -0
- package/template/.claude/skills/pipeline-rubrics/references/ia-rubric.md +19 -0
- package/template/.claude/skills/pipeline-rubrics/references/scoring.md +32 -0
- package/template/.claude/skills/pipeline-rubrics/references/vision-rubric.md +12 -0
- package/template/.claude/skills/prd-templates/SKILL.md +107 -0
- package/template/.claude/skills/prd-templates/references/architecture-completeness-checklist.md +28 -0
- package/template/.claude/skills/prd-templates/references/architecture-design-template.md +88 -0
- package/template/.claude/skills/prd-templates/references/be-spec-classification.md +41 -0
- package/template/.claude/skills/prd-templates/references/be-spec-template.md +107 -0
- package/template/.claude/skills/prd-templates/references/bootstrap-verification-protocol.md +50 -0
- package/template/.claude/skills/prd-templates/references/constraint-exploration.md +41 -0
- package/template/.claude/skills/prd-templates/references/data-placement-template.md +74 -0
- package/template/.claude/skills/prd-templates/references/decision-confirmation-protocol.md +68 -0
- package/template/.claude/skills/prd-templates/references/decision-propagation.md +121 -0
- package/template/.claude/skills/prd-templates/references/decomposition-templates.md +226 -0
- package/template/.claude/skills/prd-templates/references/deep-ideation-loading-protocol.md +114 -0
- package/template/.claude/skills/prd-templates/references/design-system-decisions.md +198 -0
- package/template/.claude/skills/prd-templates/references/design-system-prerequisite-check.md +18 -0
- package/template/.claude/skills/prd-templates/references/domain-exhaustion-criteria.md +37 -0
- package/template/.claude/skills/prd-templates/references/engagement-tier-protocol.md +58 -0
- package/template/.claude/skills/prd-templates/references/engineering-standards-template.md +126 -0
- package/template/.claude/skills/prd-templates/references/evolution-layer-guidance.md +91 -0
- package/template/.claude/skills/prd-templates/references/expansion-modes.md +27 -0
- package/template/.claude/skills/prd-templates/references/fe-classification-procedures.md +47 -0
- package/template/.claude/skills/prd-templates/references/fe-spec-template.md +90 -0
- package/template/.claude/skills/prd-templates/references/feature-ledger-protocol.md +149 -0
- package/template/.claude/skills/prd-templates/references/folder-seeding-protocol.md +77 -0
- package/template/.claude/skills/prd-templates/references/fractal-cx-template.md +58 -0
- package/template/.claude/skills/prd-templates/references/fractal-feature-template.md +93 -0
- package/template/.claude/skills/prd-templates/references/fractal-node-index-template.md +55 -0
- package/template/.claude/skills/prd-templates/references/gate-applicability.md +92 -0
- package/template/.claude/skills/prd-templates/references/ideation-crosscut-template.md +36 -0
- package/template/.claude/skills/prd-templates/references/ideation-index-template.md +111 -0
- package/template/.claude/skills/prd-templates/references/ideation-meta-template.md +126 -0
- package/template/.claude/skills/prd-templates/references/infrastructure-report-template.md +71 -0
- package/template/.claude/skills/prd-templates/references/input-classification.md +23 -0
- package/template/.claude/skills/prd-templates/references/map-guard-protocol.md +65 -0
- package/template/.claude/skills/prd-templates/references/operational-templates.md +116 -0
- package/template/.claude/skills/prd-templates/references/persona-completeness-gate.md +20 -0
- package/template/.claude/skills/prd-templates/references/placeholder-guard-template.md +21 -0
- package/template/.claude/skills/prd-templates/references/placeholder-workflow-mapping.md +50 -0
- package/template/.claude/skills/prd-templates/references/shard-boundary-analysis.md +103 -0
- package/template/.claude/skills/prd-templates/references/shard-split-remediation.md +157 -0
- package/template/.claude/skills/prd-templates/references/skill-loading-protocol.md +36 -0
- package/template/.claude/skills/prd-templates/references/slice-completion-gates.md +29 -0
- package/template/.claude/skills/prd-templates/references/spec-coverage-sweep.md +44 -0
- package/template/.claude/skills/prd-templates/references/surface-model.md +61 -0
- package/template/.claude/skills/prd-templates/references/tdd-testing-policy.md +39 -0
- package/template/.claude/skills/prd-templates/references/vision-template.md +57 -0
- package/template/.claude/skills/prd-templates/references/workflow-checkpoint-protocol.md +112 -0
- package/template/.claude/skills/prd-templates/references/write-verification-protocol.md +57 -0
- package/template/.claude/skills/prompt-engineer/SKILL.md +203 -0
- package/template/.claude/skills/regex-patterns/SKILL.md +333 -0
- package/template/.claude/skills/regex-patterns/references/go.md +44 -0
- package/template/.claude/skills/regex-patterns/references/javascript.md +63 -0
- package/template/.claude/skills/regex-patterns/references/python.md +77 -0
- package/template/.claude/skills/regex-patterns/references/rust.md +43 -0
- package/template/.claude/skills/resolve-ambiguity/SKILL.md +278 -0
- package/template/.claude/skills/security-scanning-security-hardening/SKILL.md +231 -0
- package/template/.claude/skills/session-continuity/SKILL.md +732 -0
- package/template/.claude/skills/session-continuity/protocols/01-session-resumption.md +38 -0
- package/template/.claude/skills/session-continuity/protocols/02-progress-generation.md +85 -0
- package/template/.claude/skills/session-continuity/protocols/03-progress-update.md +70 -0
- package/template/.claude/skills/session-continuity/protocols/04-pattern-extraction.md +60 -0
- package/template/.claude/skills/session-continuity/protocols/05-session-close.md +37 -0
- package/template/.claude/skills/session-continuity/protocols/06-decision-analysis.md +84 -0
- package/template/.claude/skills/session-continuity/protocols/07-spec-pipeline-generation.md +48 -0
- package/template/.claude/skills/session-continuity/protocols/08-spec-pipeline-update.md +55 -0
- package/template/.claude/skills/session-continuity/protocols/09-parallel-claim.md +122 -0
- package/template/.claude/skills/session-continuity/protocols/10-placeholder-verification-gate.md +83 -0
- package/template/.claude/skills/session-continuity/protocols/11-parallel-synthesis.md +21 -0
- package/template/.claude/skills/session-continuity/protocols/ambiguity-gates.md +48 -0
- package/template/.claude/skills/skill-creator/SKILL.md +203 -0
- package/template/.claude/skills/skill-creator/references/.gitkeep +0 -0
- package/template/.claude/skills/skill-creator/scripts/.gitkeep +0 -0
- package/template/.claude/skills/spec-writing/SKILL.md +110 -0
- package/template/.claude/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/template/.claude/skills/systematic-debugging/SKILL.md +297 -0
- package/template/.claude/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/template/.claude/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/template/.claude/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/template/.claude/skills/systematic-debugging/find-polluter.sh +63 -0
- package/template/.claude/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/template/.claude/skills/systematic-debugging/test-academic.md +14 -0
- package/template/.claude/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/template/.claude/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/template/.claude/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/template/.claude/skills/tdd-workflow/SKILL.md +186 -0
- package/template/.claude/skills/tdd-workflow/references/typescript.md +231 -0
- package/template/.claude/skills/tech-stack-catalog/SKILL.md +49 -0
- package/template/.claude/skills/tech-stack-catalog/references/constraint-questions.md +237 -0
- package/template/.claude/skills/tech-stack-catalog/references/dev-tooling-decisions.md +37 -0
- package/template/.claude/skills/tech-stack-catalog/references/surface-decision-tables.md +69 -0
- package/template/.claude/skills/technical-writer/SKILL.md +242 -0
- package/template/.claude/skills/testing-strategist/SKILL.md +319 -0
- package/template/.claude/skills/testing-strategist/references/typescript.md +328 -0
- package/template/.claude/skills/verification-before-completion/SKILL.md +97 -0
- package/template/.claude/skills/workflow-automation/SKILL.md +166 -0
- package/template/.claude/skills/workflow-automation/references/inngest.md +88 -0
- package/template/.claude/skills/workflow-automation/references/temporal.md +64 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# TypeScript Testing Patterns
|
|
2
|
+
|
|
3
|
+
Language-specific patterns for the `testing-strategist` skill. Read `SKILL.md` first for universal methodology.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Framework Stack
|
|
8
|
+
|
|
9
|
+
| Level | Tool | Purpose |
|
|
10
|
+
|-------|------|---------|
|
|
11
|
+
| Unit | **Jest** or **Vitest** | Test runner, assertions, mocking |
|
|
12
|
+
| Component | **React Testing Library** | User-centric component testing |
|
|
13
|
+
| Integration | **Supertest** | HTTP API testing |
|
|
14
|
+
| E2E | **Playwright** | Browser automation |
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm test # Run tests
|
|
20
|
+
npm test -- --watch # Watch mode
|
|
21
|
+
npm run test:coverage # Coverage report
|
|
22
|
+
npx playwright test # E2E tests
|
|
23
|
+
open coverage/lcov-report/index.html # View HTML report
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Unit Test: Business Logic
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// src/lib/pricing.ts
|
|
32
|
+
export function calculateTotal(items: { price: number; quantity: number }[]) {
|
|
33
|
+
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function applyDiscount(total: number, discountPercent: number) {
|
|
37
|
+
if (discountPercent < 0 || discountPercent > 100) {
|
|
38
|
+
throw new Error('Invalid discount percentage')
|
|
39
|
+
}
|
|
40
|
+
return total * (1 - discountPercent / 100)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/lib/pricing.test.ts
|
|
44
|
+
import { calculateTotal, applyDiscount } from './pricing'
|
|
45
|
+
|
|
46
|
+
describe('calculateTotal', () => {
|
|
47
|
+
it('calculates total for single item', () => {
|
|
48
|
+
const items = [{ price: 10, quantity: 2 }]
|
|
49
|
+
expect(calculateTotal(items)).toBe(20)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('calculates total for multiple items', () => {
|
|
53
|
+
const items = [
|
|
54
|
+
{ price: 10, quantity: 2 },
|
|
55
|
+
{ price: 5, quantity: 3 }
|
|
56
|
+
]
|
|
57
|
+
expect(calculateTotal(items)).toBe(35)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('returns 0 for empty array', () => {
|
|
61
|
+
expect(calculateTotal([])).toBe(0)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('applyDiscount', () => {
|
|
66
|
+
it('applies discount correctly', () => {
|
|
67
|
+
expect(applyDiscount(100, 20)).toBe(80)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('throws error for invalid discount', () => {
|
|
71
|
+
expect(() => applyDiscount(100, -10)).toThrow('Invalid discount')
|
|
72
|
+
expect(() => applyDiscount(100, 150)).toThrow('Invalid discount')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Unit Test: React Components
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
81
|
+
import { Button } from './Button'
|
|
82
|
+
|
|
83
|
+
describe('Button', () => {
|
|
84
|
+
it('renders with correct text', () => {
|
|
85
|
+
render(<Button>Click me</Button>)
|
|
86
|
+
expect(screen.getByText('Click me')).toBeInTheDocument()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('applies primary variant by default', () => {
|
|
90
|
+
render(<Button>Click me</Button>)
|
|
91
|
+
expect(screen.getByRole('button')).toHaveClass('btn-primary')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('calls onClick when clicked', () => {
|
|
95
|
+
const handleClick = jest.fn()
|
|
96
|
+
render(<Button onClick={handleClick}>Click me</Button>)
|
|
97
|
+
|
|
98
|
+
fireEvent.click(screen.getByRole('button'))
|
|
99
|
+
expect(handleClick).toHaveBeenCalledTimes(1)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Unit Test: Custom Hooks
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { renderHook, act } from '@testing-library/react'
|
|
108
|
+
import { useCounter } from './useCounter'
|
|
109
|
+
|
|
110
|
+
describe('useCounter', () => {
|
|
111
|
+
it('initializes with default value', () => {
|
|
112
|
+
const { result } = renderHook(() => useCounter())
|
|
113
|
+
expect(result.current.count).toBe(0)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('increments count', () => {
|
|
117
|
+
const { result } = renderHook(() => useCounter())
|
|
118
|
+
act(() => { result.current.increment() })
|
|
119
|
+
expect(result.current.count).toBe(1)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('resets to initial value', () => {
|
|
123
|
+
const { result } = renderHook(() => useCounter(10))
|
|
124
|
+
act(() => {
|
|
125
|
+
result.current.increment()
|
|
126
|
+
result.current.reset()
|
|
127
|
+
})
|
|
128
|
+
expect(result.current.count).toBe(10)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Integration Test: API Routes
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { testClient } from '@/lib/test-utils'
|
|
139
|
+
|
|
140
|
+
describe('POST /api/posts', () => {
|
|
141
|
+
beforeEach(async () => {
|
|
142
|
+
await db.post.deleteMany()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('creates a new post', async () => {
|
|
146
|
+
const response = await testClient
|
|
147
|
+
.post('/api/posts')
|
|
148
|
+
.set('Authorization', `Bearer ${authToken}`)
|
|
149
|
+
.send({ title: 'Test Post', content: 'This is a test post' })
|
|
150
|
+
|
|
151
|
+
expect(response.status).toBe(201)
|
|
152
|
+
expect(response.body).toMatchObject({
|
|
153
|
+
title: 'Test Post',
|
|
154
|
+
content: 'This is a test post'
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// Verify in database
|
|
158
|
+
const posts = await db.post.findMany()
|
|
159
|
+
expect(posts).toHaveLength(1)
|
|
160
|
+
expect(posts[0].title).toBe('Test Post')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('returns 400 for invalid data', async () => {
|
|
164
|
+
const response = await testClient
|
|
165
|
+
.post('/api/posts')
|
|
166
|
+
.set('Authorization', `Bearer ${authToken}`)
|
|
167
|
+
.send({ title: '' })
|
|
168
|
+
|
|
169
|
+
expect(response.status).toBe(400)
|
|
170
|
+
expect(response.body.errors).toBeDefined()
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('returns 401 for unauthenticated request', async () => {
|
|
174
|
+
const response = await testClient.post('/api/posts')
|
|
175
|
+
.send({ title: 'Test', content: 'Test' })
|
|
176
|
+
|
|
177
|
+
expect(response.status).toBe(401)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Integration Test: Database Operations
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
describe('userRepository', () => {
|
|
186
|
+
beforeEach(async () => { await db.user.deleteMany() })
|
|
187
|
+
afterAll(async () => { await db.$disconnect() })
|
|
188
|
+
|
|
189
|
+
describe('createUser', () => {
|
|
190
|
+
it('creates user with hashed password', async () => {
|
|
191
|
+
const user = await createUser({
|
|
192
|
+
email: 'test@example.com',
|
|
193
|
+
password: 'password123'
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
expect(user.email).toBe('test@example.com')
|
|
197
|
+
expect(user.password).not.toBe('password123')
|
|
198
|
+
expect(user.password).toMatch(/^\$2[aby]/) // bcrypt hash
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('throws error for duplicate email', async () => {
|
|
202
|
+
await createUser({ email: 'test@example.com', password: 'pass' })
|
|
203
|
+
await expect(createUser({ email: 'test@example.com', password: 'pass' }))
|
|
204
|
+
.rejects.toThrow()
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## E2E Test: Playwright
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { test, expect } from '@playwright/test'
|
|
216
|
+
|
|
217
|
+
test.describe('Authentication', () => {
|
|
218
|
+
test('user can sign up and log in', async ({ page }) => {
|
|
219
|
+
await page.goto('/signup')
|
|
220
|
+
await page.fill('input[name="email"]', 'test@example.com')
|
|
221
|
+
await page.fill('input[name="password"]', 'SecurePass123!')
|
|
222
|
+
await page.fill('input[name="confirmPassword"]', 'SecurePass123!')
|
|
223
|
+
await page.click('button[type="submit"]')
|
|
224
|
+
|
|
225
|
+
await expect(page).toHaveURL('/dashboard')
|
|
226
|
+
await expect(page.locator('h1')).toContainText('Welcome')
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
test('shows error for invalid credentials', async ({ page }) => {
|
|
230
|
+
await page.goto('/login')
|
|
231
|
+
await page.fill('input[name="email"]', 'wrong@example.com')
|
|
232
|
+
await page.fill('input[name="password"]', 'wrongpassword')
|
|
233
|
+
await page.click('button[type="submit"]')
|
|
234
|
+
|
|
235
|
+
await expect(page.locator('[role="alert"]')).toContainText('Invalid credentials')
|
|
236
|
+
await expect(page).toHaveURL('/login')
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Mocking
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// Mock external API
|
|
247
|
+
import { fetchUserData } from '@/lib/api'
|
|
248
|
+
|
|
249
|
+
jest.mock('@/lib/api')
|
|
250
|
+
const mockFetchUserData = fetchUserData as jest.MockedFunction<typeof fetchUserData>
|
|
251
|
+
|
|
252
|
+
it('displays user data', async () => {
|
|
253
|
+
mockFetchUserData.mockResolvedValue({
|
|
254
|
+
id: '1', name: 'John Doe', email: 'john@example.com'
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
render(<UserProfile userId="1" />)
|
|
258
|
+
|
|
259
|
+
await waitFor(() => {
|
|
260
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument()
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
// Mock Date
|
|
265
|
+
beforeAll(() => {
|
|
266
|
+
jest.useFakeTimers()
|
|
267
|
+
jest.setSystemTime(new Date('2024-01-01'))
|
|
268
|
+
})
|
|
269
|
+
afterAll(() => { jest.useRealTimers() })
|
|
270
|
+
|
|
271
|
+
// Mock Math.random
|
|
272
|
+
const mockRandom = jest.spyOn(Math, 'random')
|
|
273
|
+
mockRandom.mockReturnValue(0.5)
|
|
274
|
+
// ... assertions ...
|
|
275
|
+
mockRandom.mockRestore()
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Coverage Configuration
|
|
281
|
+
|
|
282
|
+
```javascript
|
|
283
|
+
// jest.config.js
|
|
284
|
+
module.exports = {
|
|
285
|
+
collectCoverageFrom: [
|
|
286
|
+
'src/**/*.{ts,tsx}',
|
|
287
|
+
'!src/**/*.d.ts',
|
|
288
|
+
'!src/**/*.stories.tsx',
|
|
289
|
+
'!src/types/**'
|
|
290
|
+
],
|
|
291
|
+
coverageThresholds: {
|
|
292
|
+
global: {
|
|
293
|
+
branches: 70, functions: 70, lines: 70, statements: 70
|
|
294
|
+
},
|
|
295
|
+
'./src/lib/auth/**': {
|
|
296
|
+
branches: 90, functions: 90, lines: 90
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## CI/CD
|
|
303
|
+
|
|
304
|
+
```yaml
|
|
305
|
+
- name: Run Tests
|
|
306
|
+
run: npm test -- --coverage
|
|
307
|
+
- name: Upload Coverage
|
|
308
|
+
uses: codecov/codecov-action@v3
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Assertion Examples
|
|
312
|
+
|
|
313
|
+
### ❌ Shallow (BANNED)
|
|
314
|
+
```typescript
|
|
315
|
+
expect(result).toBeDefined()
|
|
316
|
+
expect(result).toBeTruthy()
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### ✅ Deep (REQUIRED)
|
|
320
|
+
```typescript
|
|
321
|
+
expect(result).toEqual({
|
|
322
|
+
id: expect.any(String),
|
|
323
|
+
name: 'Test',
|
|
324
|
+
items: expect.arrayContaining([
|
|
325
|
+
expect.objectContaining({ amount: 100 })
|
|
326
|
+
])
|
|
327
|
+
})
|
|
328
|
+
```
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verification-before-completion
|
|
3
|
+
description: "Use when about to claim work is complete, fixed, or passing — requires running verification commands and confirming output before making any success claims. Evidence before claims, always."
|
|
4
|
+
version: 2.0.0
|
|
5
|
+
source: self
|
|
6
|
+
date_added: "2026-02-27"
|
|
7
|
+
date_rewritten: "2026-03-14"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Verification Before Completion
|
|
11
|
+
|
|
12
|
+
## The Iron Law
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
If you haven't run the verification command in this response, you cannot claim it passes. Period.
|
|
19
|
+
|
|
20
|
+
## When to Apply
|
|
21
|
+
|
|
22
|
+
**Before ANY of these:**
|
|
23
|
+
- Claiming tests pass, build succeeds, or linter is clean
|
|
24
|
+
- Expressing satisfaction ("Great!", "Done!", "All good!")
|
|
25
|
+
- Committing, pushing, or creating a PR
|
|
26
|
+
- Moving to the next task or slice
|
|
27
|
+
- Reporting completion to the user
|
|
28
|
+
|
|
29
|
+
## The Gate
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
1. IDENTIFY → What command proves this claim?
|
|
33
|
+
2. RUN → Execute it. Fresh. Complete. Not a cached result.
|
|
34
|
+
3. READ → Full output. Check exit code. Count failures.
|
|
35
|
+
4. VERIFY → Does the output support the claim?
|
|
36
|
+
5. CLAIM → Only now may you state the result.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Skip any step = the claim is unverified.
|
|
40
|
+
|
|
41
|
+
## Evidence Standards
|
|
42
|
+
|
|
43
|
+
| Claim | Required Evidence | Not Sufficient |
|
|
44
|
+
|-------|-------------------|----------------|
|
|
45
|
+
| "Tests pass" | Full test output showing 0 failures | Previous run, "should pass", partial suite |
|
|
46
|
+
| "Build succeeds" | Build command output, exit code 0 | Linter passing (linter ≠ compiler) |
|
|
47
|
+
| "Lint clean" | Linter output showing 0 errors/warnings | Build passing (build ≠ lint) |
|
|
48
|
+
| "Bug fixed" | Reproduce original symptom → now passes | "I changed the code" |
|
|
49
|
+
| "Regression test works" | Red-green cycle: fail → fix → pass | Test passes once without verifying it can fail |
|
|
50
|
+
| "Requirements met" | Line-by-line checklist against acceptance criteria | "Tests pass" (tests may not cover requirements) |
|
|
51
|
+
| "Migration works" | Run migration up AND down successfully | "Schema looks correct" |
|
|
52
|
+
|
|
53
|
+
## Red-Green Verification (TDD)
|
|
54
|
+
|
|
55
|
+
When claiming a regression test is valid:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
1. Write test
|
|
59
|
+
2. Run test → MUST FAIL (Red)
|
|
60
|
+
3. Implement fix
|
|
61
|
+
4. Run test → MUST PASS (Green)
|
|
62
|
+
5. Revert fix temporarily
|
|
63
|
+
6. Run test → MUST FAIL AGAIN (proves test is meaningful)
|
|
64
|
+
7. Restore fix
|
|
65
|
+
8. Run test → Passes (final confirmation)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
If the test never fails, it proves nothing.
|
|
69
|
+
|
|
70
|
+
## Red Flags — Stop and Verify
|
|
71
|
+
|
|
72
|
+
Catch yourself if you're about to say:
|
|
73
|
+
|
|
74
|
+
| Phrase | What to Do Instead |
|
|
75
|
+
|--------|-------------------|
|
|
76
|
+
| "Should work now" | Run the command |
|
|
77
|
+
| "I'm confident this is correct" | Confidence ≠ evidence |
|
|
78
|
+
| "Looks like it should pass" | Looks ≠ verified |
|
|
79
|
+
| "Just this once I'll skip" | No exceptions |
|
|
80
|
+
| "The linter passed so the build should too" | They check different things |
|
|
81
|
+
| "I changed the right file" | Run the test |
|
|
82
|
+
| "The agent said it succeeded" | Verify independently |
|
|
83
|
+
|
|
84
|
+
## Integration with Pipeline
|
|
85
|
+
|
|
86
|
+
This skill reinforces the pipeline's `completion-checklist` rule:
|
|
87
|
+
|
|
88
|
+
1. **Code** — implementation meets the contract ← verified by tests
|
|
89
|
+
2. **Tests** — all pass ← verified by running them RIGHT NOW
|
|
90
|
+
3. **Tracking** — progress files updated ← verified by reading them
|
|
91
|
+
4. **Locks** — claims removed ← verified by checking
|
|
92
|
+
|
|
93
|
+
## The Bottom Line
|
|
94
|
+
|
|
95
|
+
**Run the command. Read the output. Count the failures. THEN make the claim.**
|
|
96
|
+
|
|
97
|
+
This is non-negotiable. The cost of verification is 30 seconds. The cost of a false claim is broken trust, shipped bugs, and wasted debugging time downstream.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workflow-automation
|
|
3
|
+
description: "Architect durable, event-driven workflows using step functions, retry strategies, idempotency, fan-out patterns, and the critical differences between orchestration approaches."
|
|
4
|
+
version: 2.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Workflow Automation
|
|
8
|
+
|
|
9
|
+
You are a workflow architect who understands the fundamental tradeoff: **simplicity vs. correctness**. Every workflow platform sits somewhere on that spectrum. Your job is to pick the right point for the user's situation and implement it correctly.
|
|
10
|
+
|
|
11
|
+
## When to Use
|
|
12
|
+
|
|
13
|
+
- Background jobs that must complete even if the server restarts
|
|
14
|
+
- Multi-step processes where partial completion is worse than failure (payments, onboarding)
|
|
15
|
+
- Event-driven architectures where one event triggers multiple independent reactions
|
|
16
|
+
- Scheduled/recurring tasks that need observability and retry guarantees
|
|
17
|
+
- Any process where "it probably ran" is not acceptable
|
|
18
|
+
|
|
19
|
+
## When NOT to Use
|
|
20
|
+
|
|
21
|
+
- Simple fire-and-forget tasks (use a queue or a delay)
|
|
22
|
+
- Synchronous request-response flows
|
|
23
|
+
- Tasks under 100ms that don't involve external services
|
|
24
|
+
|
|
25
|
+
## Ecosystem-Specific References
|
|
26
|
+
|
|
27
|
+
After reading the methodology below, read the reference matching your orchestration platform:
|
|
28
|
+
|
|
29
|
+
| Platform | Reference | Best For |
|
|
30
|
+
|----------|-----------|----------|
|
|
31
|
+
| Inngest | `references/inngest.md` | Serverless, event-driven, fast shipping |
|
|
32
|
+
| Temporal | `references/temporal.md` | Complex orchestration, human-in-the-loop |
|
|
33
|
+
| BullMQ | `references/bullmq.md` | Simple job queues, rate limiting, Redis stack |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Core Concept: Durable Execution
|
|
38
|
+
|
|
39
|
+
**Wrap each unit of work in a step, so the system can retry, resume, and checkpoint independently.**
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Without durable execution:
|
|
43
|
+
fetch data → process → save → send email
|
|
44
|
+
↑ server crashes here
|
|
45
|
+
↓ entire pipeline reruns from scratch
|
|
46
|
+
↓ customer gets duplicate email
|
|
47
|
+
|
|
48
|
+
With durable execution:
|
|
49
|
+
step("fetch") ✓ saved
|
|
50
|
+
step("process") ✓ saved
|
|
51
|
+
step("save") ← server crashes here
|
|
52
|
+
step("save") ← retries ONLY this step
|
|
53
|
+
step("email") ✓ runs once
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Platform Decision Matrix
|
|
59
|
+
|
|
60
|
+
| Factor | Inngest | Temporal | BullMQ |
|
|
61
|
+
|--------|---------|----------|--------|
|
|
62
|
+
| **Complexity** | Low (serverless functions) | High (workers + server) | Medium (Redis-backed) |
|
|
63
|
+
| **Durability** | Steps checkpointed | Full workflow replay | Job-level retry |
|
|
64
|
+
| **Infrastructure** | Managed or self-hosted | Requires Temporal Server | Requires Redis |
|
|
65
|
+
| **Best for** | Serverless, event-driven | Complex orchestration | Simple job queues |
|
|
66
|
+
| **Learning curve** | 1-2 days | 1-2 weeks | 1-2 days |
|
|
67
|
+
| **Language support** | TS, Python, Go | TS, Go, Java, Python, .NET | TS/JS only |
|
|
68
|
+
|
|
69
|
+
### Decision Rules
|
|
70
|
+
|
|
71
|
+
1. **Serverless + event-driven?** → Inngest
|
|
72
|
+
2. **Complex orchestration, human-in-the-loop, multi-day?** → Temporal
|
|
73
|
+
3. **Simple job queue, Redis already in stack?** → BullMQ
|
|
74
|
+
4. **Team of 1-3, shipping fast?** → Inngest
|
|
75
|
+
5. **Enterprise, compliance-heavy, Java/.NET stack?** → Temporal
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Pattern 1: Sequential Steps
|
|
80
|
+
|
|
81
|
+
Each step depends on the previous result. Steps checkpoint independently.
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
step("create-db-record") → returns user
|
|
85
|
+
step("create-stripe-customer") → uses user.email
|
|
86
|
+
step("send-welcome-email") → uses user.email
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If step 2 fails, only step 2 retries. Steps 1 and 3 are not re-executed.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Pattern 2: Fan-Out (Parallel Execution)
|
|
94
|
+
|
|
95
|
+
One event triggers multiple independent workflows. Each runs and retries independently.
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
Event: user/signup.completed
|
|
99
|
+
→ Workflow A: send-welcome-email (independent)
|
|
100
|
+
→ Workflow B: create-stripe-trial (independent)
|
|
101
|
+
→ Workflow C: provision-workspace (independent)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
If Workflow B fails, Workflows A and C are unaffected.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Pattern 3: Idempotency
|
|
109
|
+
|
|
110
|
+
Prevent duplicate execution when the same event fires twice.
|
|
111
|
+
|
|
112
|
+
**Rule:** Idempotency at the workflow level AND the external call level. Belt and suspenders.
|
|
113
|
+
|
|
114
|
+
- **Workflow level:** Deduplicate by event ID or business key (e.g., order ID)
|
|
115
|
+
- **External call level:** Use idempotency keys on payment APIs, email sends, etc.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Pattern 4: Scheduled/Recurring
|
|
120
|
+
|
|
121
|
+
Cron-triggered workflows with observability:
|
|
122
|
+
- Scheduled trigger (e.g., daily at 9am)
|
|
123
|
+
- Steps checkpoint independently
|
|
124
|
+
- Failed runs are visible, retriable, and alertable
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Pattern 5: Human-in-the-Loop
|
|
129
|
+
|
|
130
|
+
Workflows that pause and wait for external input — approval flows, multi-day processes.
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
workflow: purchaseWorkflow(amount)
|
|
134
|
+
if amount > threshold:
|
|
135
|
+
pause and wait for approval signal (up to 72h)
|
|
136
|
+
if no approval → deny
|
|
137
|
+
continue with purchase
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Sharp Edges
|
|
143
|
+
|
|
144
|
+
| Issue | Severity | Rule |
|
|
145
|
+
|-------|----------|------|
|
|
146
|
+
| No idempotency keys on external calls | Critical | ALWAYS use idempotency keys for payments, emails, mutations |
|
|
147
|
+
| Side effects in workflow code (Temporal) | Critical | Workflows must be deterministic — no I/O, no random, no clock |
|
|
148
|
+
| Giant payloads in step results | High | Steps serialize data — keep under 256KB |
|
|
149
|
+
| No timeouts on activities | High | ALWAYS set timeouts — default infinite hangs forever |
|
|
150
|
+
| Linear retry without backoff | Medium | ALWAYS use exponential backoff |
|
|
151
|
+
| Missing dead letter handling | High | Failed-after-all-retries jobs need a destination |
|
|
152
|
+
| Monolithic workflows | Medium | Break workflows >10 steps into child workflows |
|
|
153
|
+
|
|
154
|
+
## Anti-Patterns
|
|
155
|
+
|
|
156
|
+
| Don't | Do |
|
|
157
|
+
|-------|-----|
|
|
158
|
+
| `setTimeout(fn, 3600000)` for delays | Platform sleep — survives restarts |
|
|
159
|
+
| Global variables for workflow state | Step results and explicit state passing |
|
|
160
|
+
| Retry loops in application code | Platform-level retry policies |
|
|
161
|
+
| Polling in a loop for completion | Event-driven triggers or workflow signals |
|
|
162
|
+
| One giant function with all logic | Decompose into steps/activities |
|
|
163
|
+
|
|
164
|
+
## Related Skills
|
|
165
|
+
|
|
166
|
+
Works with: `error-handling-patterns`, `deployment-procedures`
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Inngest Workflow Patterns
|
|
2
|
+
|
|
3
|
+
Ecosystem-specific patterns for the `workflow-automation` skill. Read `SKILL.md` first for universal methodology.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Sequential Steps
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
const syncUser = inngest.createFunction(
|
|
11
|
+
{ id: "sync-user-data" },
|
|
12
|
+
{ event: "user/signup.completed" },
|
|
13
|
+
async ({ event, step }) => {
|
|
14
|
+
const user = await step.run("create-db-record", async () => {
|
|
15
|
+
return db.users.create({ email: event.data.email });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const stripeCustomer = await step.run("create-stripe-customer", async () => {
|
|
19
|
+
return stripe.customers.create({ email: user.email });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
await step.run("send-welcome-email", async () => {
|
|
23
|
+
return email.send({ to: user.email, template: "welcome" });
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Fan-Out
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// Each function subscribes to the same event — runs independently
|
|
33
|
+
const sendWelcome = inngest.createFunction(
|
|
34
|
+
{ id: "send-welcome-email" },
|
|
35
|
+
{ event: "user/signup.completed" },
|
|
36
|
+
async ({ event, step }) => {
|
|
37
|
+
await step.run("send", async () => {
|
|
38
|
+
await email.send({ to: event.data.email, template: "welcome" });
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const startTrial = inngest.createFunction(
|
|
44
|
+
{ id: "start-stripe-trial" },
|
|
45
|
+
{ event: "user/signup.completed" },
|
|
46
|
+
async ({ event, step }) => {
|
|
47
|
+
await step.run("create-trial", async () => {
|
|
48
|
+
await stripe.subscriptions.create({
|
|
49
|
+
customer: event.data.stripeId,
|
|
50
|
+
trial_period_days: 14,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Idempotency
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
const processPayment = inngest.createFunction(
|
|
61
|
+
{
|
|
62
|
+
id: "process-payment",
|
|
63
|
+
idempotency: "event.data.orderId", // Dedup within 24h
|
|
64
|
+
},
|
|
65
|
+
{ event: "order/payment.requested" },
|
|
66
|
+
async ({ event, step }) => {
|
|
67
|
+
await step.run("charge", async () => {
|
|
68
|
+
return stripe.charges.create({
|
|
69
|
+
amount: event.data.amount,
|
|
70
|
+
idempotencyKey: event.data.orderId, // Stripe-level too
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Scheduled/Recurring
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const dailyReport = inngest.createFunction(
|
|
81
|
+
{ id: "daily-metrics-report" },
|
|
82
|
+
{ cron: "0 9 * * *" }, // 9am daily
|
|
83
|
+
async ({ step }) => {
|
|
84
|
+
const metrics = await step.run("fetch-metrics", fetchDailyMetrics);
|
|
85
|
+
await step.run("send-report", async () => sendSlackReport(metrics));
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
```
|