@wrongstack/core 0.9.4 → 0.9.7
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/skills/api-design/SKILL.md +139 -0
- package/skills/audit-log/SKILL.md +87 -14
- package/skills/bug-hunter/SKILL.md +42 -19
- package/skills/docker-deploy/SKILL.md +155 -0
- package/skills/git-flow/SKILL.md +53 -1
- package/skills/multi-agent/SKILL.md +42 -0
- package/skills/node-modern/SKILL.md +57 -1
- package/skills/observability/SKILL.md +134 -0
- package/skills/prompt-engineering/SKILL.md +46 -19
- package/skills/react-modern/SKILL.md +92 -1
- package/skills/refactor-planner/SKILL.md +49 -1
- package/skills/sdd/SKILL.md +12 -1
- package/skills/security-scanner/SKILL.md +46 -1
- package/skills/skill-creator/SKILL.md +49 -52
- package/skills/testing/SKILL.md +170 -0
- package/skills/typescript-strict/SKILL.md +98 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing
|
|
3
|
+
description: |
|
|
4
|
+
Use this skill when writing, reviewing, or improving tests in WrongStack.
|
|
5
|
+
Triggers: user says "test", "unit test", "integration test", "e2e", "mock",
|
|
6
|
+
"vitest", "coverage", "assert", "expect", "test strategy", "write tests".
|
|
7
|
+
version: 1.0.0
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Testing — WrongStack
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Writes and reviews tests for WrongStack TypeScript code. WrongStack uses **vitest** as the test runner, **pnpm workspaces**, and co-located test files (`foo.ts` → `foo.test.ts`). Tests must pass before every commit.
|
|
15
|
+
|
|
16
|
+
## Rules
|
|
17
|
+
|
|
18
|
+
1. Co-locate tests: `src/foo.ts` → `tests/foo.test.ts` (same package).
|
|
19
|
+
2. Always test public API surfaces — don't test internals.
|
|
20
|
+
3. Use `vi.mock()` for external deps; never mock internal modules.
|
|
21
|
+
4. Every async test needs a timeout: `test(..., { timeout: 5000 })`.
|
|
22
|
+
5. Mock time with `vi.useFakeTimers()` for debounce/throttle tests.
|
|
23
|
+
6. Coverage gate: new code must have ≥70% coverage, don't lower existing coverage.
|
|
24
|
+
7. Don't commit test-only deps — test deps go in `devDependencies`.
|
|
25
|
+
8. Tests must be isolated — each test cleans up its mocks/state.
|
|
26
|
+
|
|
27
|
+
## Patterns
|
|
28
|
+
|
|
29
|
+
### Do
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// ✅ Co-located test
|
|
33
|
+
// packages/tools/src/bash.ts → packages/tools/tests/bash.test.ts
|
|
34
|
+
|
|
35
|
+
// ✅ Test the public API
|
|
36
|
+
import { parseArgs } from '../src/arg-parser';
|
|
37
|
+
test('parses --flag value pairs', () => {
|
|
38
|
+
expect(parseArgs(['--name', 'Alice'])).toEqual({ name: 'Alice' });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ✅ Async test with timeout
|
|
42
|
+
test('fetches user data', async () => {
|
|
43
|
+
const user = await fetchUser('123');
|
|
44
|
+
expect(user.name).toBe('Alice');
|
|
45
|
+
}, { timeout: 5000 });
|
|
46
|
+
|
|
47
|
+
// ✅ Mock external deps
|
|
48
|
+
vi.mock('axios');
|
|
49
|
+
const axios = await import('axios');
|
|
50
|
+
vi.mocked(axios.get).mockResolvedValue({ data: { name: 'Alice' } });
|
|
51
|
+
|
|
52
|
+
// ✅ Fake timers for debounce
|
|
53
|
+
vi.useFakeTimers();
|
|
54
|
+
vi.advanceTimersByTime(300);
|
|
55
|
+
expect(handler).toHaveBeenCalledWith('input');
|
|
56
|
+
|
|
57
|
+
// ✅ Isolation — cleanup
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
vi.restoreAllMocks();
|
|
60
|
+
vi.useRealTimers();
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Don't
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// ❌ Mocking internal modules
|
|
68
|
+
vi.mock('../src/internal/helper'); // internal — don't mock
|
|
69
|
+
|
|
70
|
+
// ❌ No timeout on async test
|
|
71
|
+
test('fetches data', async () => {
|
|
72
|
+
// fetch hangs forever in CI — always add timeout
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ❌ Testing implementation details
|
|
76
|
+
test('calls validateEmail() three times', () => {
|
|
77
|
+
// ❌ fragile — test behavior, not implementation
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ❌ Forgotten cleanup
|
|
81
|
+
// Mocked axios persists across tests — always cleanup
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Test types
|
|
85
|
+
|
|
86
|
+
| Type | Scope | When to use |
|
|
87
|
+
|------|-------|-------------|
|
|
88
|
+
| **Unit** | Single function/module | Pure logic, parsing, transformations |
|
|
89
|
+
| **Integration** | Multi-module interaction | API calls, file I/O, tool chains |
|
|
90
|
+
| **E2E** | Full command flow | CLI smoke tests, slash commands |
|
|
91
|
+
|
|
92
|
+
### Unit test structure
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
describe('parseArgs', () => {
|
|
96
|
+
it('parses --flag value', () => {
|
|
97
|
+
expect(parseArgs(['--name', 'Alice'])).toEqual({ name: 'Alice' });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('throws on missing value for --required', () => {
|
|
101
|
+
expect(() => parseArgs(['--required'])).toThrow();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it.each([...])('handles %s input', (input, expected) => {
|
|
105
|
+
expect(parseArgs(input)).toEqual(expected);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Integration test structure
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
test('executes bash tool with timeout', async () => {
|
|
114
|
+
const result = await bash({
|
|
115
|
+
command: 'echo hello',
|
|
116
|
+
cwd: '/tmp',
|
|
117
|
+
signal: AbortSignal.timeout(5000),
|
|
118
|
+
});
|
|
119
|
+
expect(result.stdout.trim()).toBe('hello');
|
|
120
|
+
}, { timeout: 10000 });
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Mocking patterns
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// ✅ Mock node:fs/promises
|
|
127
|
+
vi.mock('node:fs/promises');
|
|
128
|
+
const fs = await import('node:fs/promises');
|
|
129
|
+
vi.mocked(fs.readFile).mockResolvedValue('content');
|
|
130
|
+
|
|
131
|
+
// ✅ Mock process.env
|
|
132
|
+
const originalEnv = process.env;
|
|
133
|
+
beforeEach(() => { process.env = { ...originalEnv }; });
|
|
134
|
+
afterEach(() => { process.env = originalEnv; });
|
|
135
|
+
|
|
136
|
+
// ✅ Mock spawn
|
|
137
|
+
vi.mock('node:child_process');
|
|
138
|
+
const { spawn } = await import('node:child_process');
|
|
139
|
+
vi.mocked(spawn).mockReturnValue({
|
|
140
|
+
on: vi.fn(),
|
|
141
|
+
stdout: { on: vi.fn() },
|
|
142
|
+
stderr: { on: vi.fn() },
|
|
143
|
+
} as any);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Coverage
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Run with coverage
|
|
150
|
+
pnpm test -- --coverage
|
|
151
|
+
|
|
152
|
+
# Coverage thresholds (enforced in CI)
|
|
153
|
+
coverageThreshold: {
|
|
154
|
+
global: { branches: 70, functions: 70, lines: 70, statements: 70 }
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## WrongStack-specific test notes
|
|
159
|
+
|
|
160
|
+
- **Subpath exports**: Some packages use `exports` field in `package.json` — tests must use the public entry point, not `dist/`.
|
|
161
|
+
- **AbortSignal**: Any test involving timeouts must use `AbortSignal.timeout()` not `setTimeout`.
|
|
162
|
+
- **pnpm workspaces**: Run `pnpm test` in the package root, or `pnpm -r test` for all packages.
|
|
163
|
+
- **Vitest config**: Each package has its own `vitest.config.ts`.
|
|
164
|
+
|
|
165
|
+
## Skills in scope
|
|
166
|
+
|
|
167
|
+
- `bug-hunter` — for turning test failures into concrete bugs
|
|
168
|
+
- `typescript-strict` — for type-safe test assertions
|
|
169
|
+
- `node-modern` — for async/test patterns with AbortSignal
|
|
170
|
+
- `git-flow` — for committing tests with the code they test
|
|
@@ -9,6 +9,67 @@ version: 1.1.0
|
|
|
9
9
|
|
|
10
10
|
# TypeScript Strict Mode — WrongStack
|
|
11
11
|
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Strict TypeScript patterns for WrongStack: exhaustive switch, branded types, discriminated unions, and `noUncheckedIndexedAccess`. WrongStack uses `strict: true` with additional strictness flags.
|
|
15
|
+
|
|
16
|
+
## Rules
|
|
17
|
+
|
|
18
|
+
1. Never silence errors with `as any` — use `as unknown as T` only at trust boundaries with a comment.
|
|
19
|
+
2. Don't use `!` non-null assertion — silence the type checker without explanation.
|
|
20
|
+
3. Always annotate return types on exported functions — hides errors otherwise.
|
|
21
|
+
4. Use `Promise<unknown>` or generics instead of `Promise<any>`.
|
|
22
|
+
5. Be specific with types — `Function` and `Object` are too broad.
|
|
23
|
+
6. Enable `noUncheckedIndexedAccess` — always handle the `undefined` case on array/object access.
|
|
24
|
+
|
|
25
|
+
## Patterns
|
|
26
|
+
|
|
27
|
+
### Do
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// ✅ Exhaustive switch with assertNever
|
|
31
|
+
function assertNever(x: never): never {
|
|
32
|
+
throw new Error(`Unhandled: ${JSON.stringify(x)}`);
|
|
33
|
+
}
|
|
34
|
+
switch (block.type) {
|
|
35
|
+
case 'text': return renderText(block);
|
|
36
|
+
case 'tool_use': return renderToolUse(block);
|
|
37
|
+
case 'error': return renderError(block);
|
|
38
|
+
default: return assertNever(block);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ✅ Branded types for invariants
|
|
42
|
+
type UserId = string & { readonly __brand: 'UserId' };
|
|
43
|
+
type SessionId = string & { readonly __brand: 'SessionId' };
|
|
44
|
+
|
|
45
|
+
// ✅ Discriminated union
|
|
46
|
+
type Result =
|
|
47
|
+
| { status: 'success'; data: User }
|
|
48
|
+
| { status: 'error'; error: Error }
|
|
49
|
+
| { status: 'loading' };
|
|
50
|
+
|
|
51
|
+
// ✅ noUncheckedIndexedAccess — always handle undefined
|
|
52
|
+
const first = items.at(0);
|
|
53
|
+
if (first) console.log(first.toUpperCase());
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Don't
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
// ❌ Non-null assertion — silences the type checker
|
|
60
|
+
console.log(name!.toUpperCase());
|
|
61
|
+
|
|
62
|
+
// ❌ Promise<any> — loses type safety
|
|
63
|
+
async function fetchUser(): Promise<any> { ... }
|
|
64
|
+
|
|
65
|
+
// ❌ Too broad
|
|
66
|
+
const handler: Function = () => {};
|
|
67
|
+
const data: Object = {};
|
|
68
|
+
|
|
69
|
+
// ❌ Missing return type on export
|
|
70
|
+
export function processData(data: string) { ... }
|
|
71
|
+
```
|
|
72
|
+
|
|
12
73
|
## Non-negotiable rules
|
|
13
74
|
|
|
14
75
|
```json
|
|
@@ -22,7 +83,43 @@ version: 1.1.0
|
|
|
22
83
|
|
|
23
84
|
Never silence errors with `as any`. Use `as unknown as T` only at trust boundaries with a comment.
|
|
24
85
|
|
|
25
|
-
##
|
|
86
|
+
## Workflow — applying strict TypeScript
|
|
87
|
+
|
|
88
|
+
Apply strict TypeScript in this order:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
1. tsconfig.json → enable strict flags first
|
|
92
|
+
2. Per-file patterns → apply the patterns below
|
|
93
|
+
3. CI gate → tsc --noEmit must pass
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Step 1 — tsconfig.json** (the foundation):
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"compilerOptions": {
|
|
100
|
+
"strict": true,
|
|
101
|
+
"noUncheckedIndexedAccess": true,
|
|
102
|
+
"noImplicitReturns": true,
|
|
103
|
+
"exactOptionalPropertyTypes": true,
|
|
104
|
+
"target": "ES2022",
|
|
105
|
+
"module": "NodeNext",
|
|
106
|
+
"moduleResolution": "NodeNext"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Step 2 — Per-file patterns** (after tsconfig):
|
|
112
|
+
- Add `assertNever` for exhaustive switches
|
|
113
|
+
- Create branded types for invariant strings (UserId, SessionId)
|
|
114
|
+
- Use discriminated unions instead of optional fields
|
|
115
|
+
- Handle `T | undefined` on every array/object access
|
|
116
|
+
|
|
117
|
+
**Step 3 — CI gate**:
|
|
118
|
+
```bash
|
|
119
|
+
pnpm run typecheck # must pass before merge
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Patterns
|
|
26
123
|
|
|
27
124
|
### Exhaustive switch
|
|
28
125
|
|