@comfanion/workflow 4.38.4-dev.1 → 4.39.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.
- package/package.json +1 -1
- package/src/build-info.json +2 -2
- package/src/opencode/agents/architect.md +8 -3
- package/src/opencode/agents/pm.md +9 -3
- package/src/opencode/gitignore +1 -0
- package/src/opencode/skills/acceptance-criteria/SKILL.md +58 -176
- package/src/opencode/skills/architecture-design/SKILL.md +86 -576
- package/src/opencode/skills/archiving/SKILL.md +60 -140
- package/src/opencode/skills/coding-standards/SKILL.md +113 -434
- package/src/opencode/skills/coding-standards/what-to-document.md +512 -0
- package/src/opencode/skills/database-design/SKILL.md +94 -778
- package/src/opencode/skills/database-design/indexing.md +187 -0
- package/src/opencode/skills/database-design/migrations.md +239 -0
- package/src/opencode/skills/database-design/schema-design.md +319 -0
- package/src/opencode/skills/doc-todo/SKILL.md +35 -27
- package/src/opencode/skills/epic-writing/SKILL.md +156 -244
- package/src/opencode/skills/epic-writing/template.md +11 -1
- package/src/opencode/skills/methodologies/SKILL.md +91 -354
- package/src/opencode/skills/methodologies/define.md +336 -0
- package/src/opencode/skills/methodologies/diagnose.md +374 -0
- package/src/opencode/skills/methodologies/empathize.md +253 -0
- package/src/opencode/skills/methodologies/ideate.md +458 -0
- package/src/opencode/skills/prd-writing/SKILL.md +162 -366
- package/src/opencode/skills/prd-writing/template.md +178 -48
- package/src/opencode/skills/requirements-gathering/SKILL.md +102 -117
- package/src/opencode/skills/requirements-gathering/template.md +97 -17
- package/src/opencode/skills/sprint-planning/SKILL.md +76 -225
- package/src/opencode/skills/sprint-planning/template.yaml +8 -0
- package/src/opencode/skills/story-writing/SKILL.md +76 -210
- package/src/opencode/skills/story-writing/template.md +10 -1
- package/src/opencode/skills/test-design/SKILL.md +78 -84
- package/src/opencode/skills/test-design/test-strategy.md +279 -0
- package/src/opencode/skills/test-design/unit-tests-mocking.md +247 -0
- package/src/opencode/skills/test-design/unit-tests-patterns.md +181 -0
- package/src/opencode/skills/test-design/unit-tests.md +117 -0
- package/src/opencode/skills/unit-writing/SKILL.md +119 -377
- package/src/opencode/skills/module-documentation/SKILL.md +0 -224
- package/src/opencode/skills/module-documentation/template.md +0 -139
- /package/src/opencode/skills/test-design/{template-integration.md → templates/template-integration.md} +0 -0
- /package/src/opencode/skills/test-design/{template-module.md → templates/template-module.md} +0 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Unit Testing Patterns
|
|
2
|
+
|
|
3
|
+
Common patterns for writing effective unit tests.
|
|
4
|
+
|
|
5
|
+
## Table-Driven Tests
|
|
6
|
+
|
|
7
|
+
Test multiple scenarios with same logic.
|
|
8
|
+
|
|
9
|
+
**Go Example:**
|
|
10
|
+
```go
|
|
11
|
+
func TestValidateEmail(t *testing.T) {
|
|
12
|
+
tests := []struct {
|
|
13
|
+
name string
|
|
14
|
+
email string
|
|
15
|
+
wantErr bool
|
|
16
|
+
}{
|
|
17
|
+
{"valid", "test@example.com", false},
|
|
18
|
+
{"missing @", "testexample.com", true},
|
|
19
|
+
{"empty", "", true},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for _, tt := range tests {
|
|
23
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
24
|
+
err := ValidateEmail(tt.email)
|
|
25
|
+
if (err != nil) != tt.wantErr {
|
|
26
|
+
t.Errorf("got error = %v, want %v", err, tt.wantErr)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**TypeScript Example:**
|
|
34
|
+
```typescript
|
|
35
|
+
describe('validateEmail', () => {
|
|
36
|
+
test.each([
|
|
37
|
+
{ email: 'test@example.com', valid: true },
|
|
38
|
+
{ email: 'testexample.com', valid: false },
|
|
39
|
+
{ email: '', valid: false },
|
|
40
|
+
])('validates $email', ({ email, valid }) => {
|
|
41
|
+
expect(validateEmail(email).isValid).toBe(valid);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## State Transitions
|
|
49
|
+
|
|
50
|
+
Test state machine behavior.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
describe('Order state transitions', () => {
|
|
54
|
+
it('transitions DRAFT → PENDING on submit', () => {
|
|
55
|
+
const order = new Order(); // DRAFT
|
|
56
|
+
|
|
57
|
+
order.submit();
|
|
58
|
+
|
|
59
|
+
expect(order.status).toBe('PENDING');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('throws error on invalid transition', () => {
|
|
63
|
+
const order = new Order();
|
|
64
|
+
order.submit(); // DRAFT → PENDING
|
|
65
|
+
|
|
66
|
+
expect(() => order.submit()).toThrow('Invalid state');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Validation Testing
|
|
74
|
+
|
|
75
|
+
Test input validation rules.
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
describe('Order validation', () => {
|
|
79
|
+
it('rejects empty items', () => {
|
|
80
|
+
const order = new Order();
|
|
81
|
+
|
|
82
|
+
const result = order.validate();
|
|
83
|
+
|
|
84
|
+
expect(result.isValid).toBe(false);
|
|
85
|
+
expect(result.errors).toContain('Order must have items');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('rejects negative quantities', () => {
|
|
89
|
+
const order = new Order();
|
|
90
|
+
order.addItem({ price: 100, quantity: -1 });
|
|
91
|
+
|
|
92
|
+
const result = order.validate();
|
|
93
|
+
|
|
94
|
+
expect(result.errors).toContain('Quantity must be positive');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Error Handling
|
|
102
|
+
|
|
103
|
+
Test error scenarios and recovery.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
describe('OrderService error handling', () => {
|
|
107
|
+
it('handles database errors', async () => {
|
|
108
|
+
const mockDb = {
|
|
109
|
+
save: jest.fn().mockRejectedValue(new Error('DB error'))
|
|
110
|
+
};
|
|
111
|
+
const service = new OrderService(mockDb);
|
|
112
|
+
|
|
113
|
+
await expect(service.createOrder(order))
|
|
114
|
+
.rejects.toThrow('Failed to create order');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('retries on transient errors', async () => {
|
|
118
|
+
const mockDb = {
|
|
119
|
+
save: jest.fn()
|
|
120
|
+
.mockRejectedValueOnce(new Error('Timeout'))
|
|
121
|
+
.mockResolvedValueOnce({ id: '123' })
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const result = await service.createOrder(order);
|
|
125
|
+
|
|
126
|
+
expect(result.id).toBe('123');
|
|
127
|
+
expect(mockDb.save).toHaveBeenCalledTimes(2);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Boundary Testing
|
|
135
|
+
|
|
136
|
+
Test edge cases and limits.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
describe('boundary conditions', () => {
|
|
140
|
+
it('handles empty input', () => {
|
|
141
|
+
expect(calculateTotal([])).toBe(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('handles single item', () => {
|
|
145
|
+
expect(calculateTotal([{ price: 100 }])).toBe(100);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('handles maximum items', () => {
|
|
149
|
+
const items = Array(1000).fill({ price: 1 });
|
|
150
|
+
expect(calculateTotal(items)).toBe(1000);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('handles zero price', () => {
|
|
154
|
+
expect(calculateTotal([{ price: 0 }])).toBe(0);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Tips
|
|
162
|
+
|
|
163
|
+
**Table-driven tests:**
|
|
164
|
+
- Use for multiple input/output combinations
|
|
165
|
+
- Name each case clearly
|
|
166
|
+
- Include edge cases
|
|
167
|
+
|
|
168
|
+
**State transitions:**
|
|
169
|
+
- Test all valid transitions
|
|
170
|
+
- Test invalid transitions (should error)
|
|
171
|
+
- Verify state after transition
|
|
172
|
+
|
|
173
|
+
**Validation:**
|
|
174
|
+
- Test each validation rule separately
|
|
175
|
+
- Test combinations of errors
|
|
176
|
+
- Verify error messages
|
|
177
|
+
|
|
178
|
+
**Error handling:**
|
|
179
|
+
- Test error paths
|
|
180
|
+
- Test recovery/retry logic
|
|
181
|
+
- Verify error messages
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Unit Testing Guide
|
|
2
|
+
|
|
3
|
+
Unit tests verify individual components in isolation.
|
|
4
|
+
|
|
5
|
+
## What to Test
|
|
6
|
+
|
|
7
|
+
### ✅ Always Test
|
|
8
|
+
- **Public API** - Public methods, exported interfaces
|
|
9
|
+
- **Business Logic** - Domain rules, calculations, validations
|
|
10
|
+
- **Error Handling** - Invalid input, exceptions
|
|
11
|
+
- **Edge Cases** - Empty/null/zero, min/max, boundaries
|
|
12
|
+
|
|
13
|
+
### ❌ Don't Test
|
|
14
|
+
- **Private methods** - Test through public API
|
|
15
|
+
- **Trivial code** - Getters/setters, pass-through
|
|
16
|
+
- **Framework internals** - Third-party libraries
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## AAA Pattern
|
|
21
|
+
|
|
22
|
+
**Structure:** Arrange → Act → Assert
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
it('calculates order total', () => {
|
|
26
|
+
// Arrange - setup
|
|
27
|
+
const order = new Order();
|
|
28
|
+
order.addItem({ price: 100, quantity: 2 });
|
|
29
|
+
|
|
30
|
+
// Act - execute
|
|
31
|
+
const total = order.calculateTotal();
|
|
32
|
+
|
|
33
|
+
// Assert - verify
|
|
34
|
+
expect(total).toBe(200);
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Test Naming
|
|
41
|
+
|
|
42
|
+
**Format:** `Test{Component}_{Method}_{Scenario}_{Expected}`
|
|
43
|
+
|
|
44
|
+
**Examples:**
|
|
45
|
+
```
|
|
46
|
+
TestOrder_CalculateTotal_WithItems_ReturnsSum
|
|
47
|
+
TestOrder_CalculateTotal_EmptyOrder_ReturnsZero
|
|
48
|
+
TestOrder_AddItem_DuplicateSKU_MergesQuantity
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**TypeScript/Jest:**
|
|
52
|
+
```typescript
|
|
53
|
+
describe('Order', () => {
|
|
54
|
+
describe('calculateTotal', () => {
|
|
55
|
+
it('returns sum when order has items', () => {});
|
|
56
|
+
it('returns zero when order is empty', () => {});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Coverage Targets
|
|
64
|
+
|
|
65
|
+
| Layer | Target | Focus |
|
|
66
|
+
|-------|--------|-------|
|
|
67
|
+
| **Domain** | 80%+ | Business logic, rules |
|
|
68
|
+
| **Application** | 70%+ | Use cases |
|
|
69
|
+
| **Infrastructure** | 50%+ | Adapters |
|
|
70
|
+
|
|
71
|
+
**Don't chase 100%** - Focus on critical paths.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Advanced Topics
|
|
76
|
+
|
|
77
|
+
**For more details, see:**
|
|
78
|
+
- [unit-tests-patterns.md](unit-tests-patterns.md) - Table-driven tests, state transitions, validation
|
|
79
|
+
- [unit-tests-mocking.md](unit-tests-mocking.md) - Mocking, dependency injection, test isolation
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Quick Example
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
describe('validateEmail', () => {
|
|
87
|
+
it('accepts valid email', () => {
|
|
88
|
+
expect(validateEmail('test@example.com').isValid).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('rejects invalid email', () => {
|
|
92
|
+
expect(validateEmail('invalid').isValid).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('rejects empty email', () => {
|
|
96
|
+
expect(validateEmail('').isValid).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Tips
|
|
104
|
+
|
|
105
|
+
**Keep tests simple:**
|
|
106
|
+
- One assertion per test
|
|
107
|
+
- Clear test names
|
|
108
|
+
- Minimal setup
|
|
109
|
+
|
|
110
|
+
**Test behavior, not implementation:**
|
|
111
|
+
- Test what it does, not how
|
|
112
|
+
- Refactor shouldn't break tests
|
|
113
|
+
|
|
114
|
+
**Make tests fast:**
|
|
115
|
+
- No real I/O (mock it)
|
|
116
|
+
- No sleep/delays
|
|
117
|
+
- Parallel execution
|