@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.
@@ -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
- ## Common patterns
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