@complexthings/superpowers-agent 8.1.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/.agents/docs/SUPERPOWERS.md +108 -0
- package/.agents/prompts/001-copilot-tool-mapping-do/001-copilot-tool-mapping-do.md +116 -0
- package/.agents/prompts/001-copilot-tool-mapping-do/SUMMARY.md +72 -0
- package/.agents/prompts/001-copilot-tool-mapping-do/copilot-tool-mapping-do-output.md +83 -0
- package/.agents/prompts/001-release-management-do/001-release-management-do.md +161 -0
- package/.agents/prompts/001-release-management-do/SUMMARY.md +19 -0
- package/.agents/prompts/001-release-management-do/completed/001-release-management-do.md +161 -0
- package/.agents/prompts/002-pull-command-do/SUMMARY.md +28 -0
- package/.agents/prompts/002-pull-command-do/completed/002-pull-command-do.md +122 -0
- package/.agents/prompts/002-pull-command-do/pull-command-do-output.md +174 -0
- package/.agents/prompts/command_updates_prompt.md +69 -0
- package/.agents/prompts/current_prompt.md +8 -0
- package/.agents/prompts/refactor.prompt.md +57 -0
- package/.agents/skills/.gitkeep +0 -0
- package/.agents/superpowers-agent +355 -0
- package/.agents/superpowers-bootstrap.md +42 -0
- package/.agents/templates/AGENTS.md.template +40 -0
- package/.agents/templates/SUPERPOWERS.md.template +108 -0
- package/.agents/templates/TOOLS.md.template +15 -0
- package/.github/workflows/main.yaml +32 -0
- package/AGENTS.md +60 -0
- package/LICENSE +21 -0
- package/README.md +724 -0
- package/hooks/hooks.json +15 -0
- package/hooks/session-start.sh +27 -0
- package/package.json +51 -0
- package/skills/architecture/ABOUT.md +20 -0
- package/skills/architecture/preserving-productive-tensions/SKILL.md +146 -0
- package/skills/architecture/preserving-productive-tensions/skill.json +9 -0
- package/skills/collaboration/brainstorming/SKILL.md +166 -0
- package/skills/collaboration/brainstorming/skill.json +9 -0
- package/skills/collaboration/brainstorming/spec-document-reviewer-prompt.md +50 -0
- package/skills/collaboration/brainstorming/visual-companion.md +277 -0
- package/skills/collaboration/dispatching-parallel-agents/SKILL.md +174 -0
- package/skills/collaboration/dispatching-parallel-agents/skill.json +9 -0
- package/skills/collaboration/executing-plans/SKILL.md +130 -0
- package/skills/collaboration/executing-plans/skill.json +9 -0
- package/skills/collaboration/finishing-a-development-branch/SKILL.md +261 -0
- package/skills/collaboration/finishing-a-development-branch/skill.json +9 -0
- package/skills/collaboration/leveraging-cli-tools/SKILL.md +132 -0
- package/skills/collaboration/leveraging-cli-tools/skill.json +9 -0
- package/skills/collaboration/receiving-code-review/SKILL.md +233 -0
- package/skills/collaboration/receiving-code-review/skill.json +9 -0
- package/skills/collaboration/requesting-code-review/SKILL.md +110 -0
- package/skills/collaboration/requesting-code-review/code-reviewer.md +146 -0
- package/skills/collaboration/requesting-code-review/skill.json +12 -0
- package/skills/collaboration/subagent-driven-development/SKILL.md +255 -0
- package/skills/collaboration/subagent-driven-development/code-quality-reviewer-prompt.md +26 -0
- package/skills/collaboration/subagent-driven-development/implementer-prompt.md +113 -0
- package/skills/collaboration/subagent-driven-development/skill.json +15 -0
- package/skills/collaboration/subagent-driven-development/spec-reviewer-prompt.md +61 -0
- package/skills/collaboration/using-git-worktrees/SKILL.md +366 -0
- package/skills/collaboration/using-git-worktrees/skill.json +9 -0
- package/skills/collaboration/writing-plans/SKILL.md +121 -0
- package/skills/collaboration/writing-plans/plan-document-reviewer-prompt.md +52 -0
- package/skills/collaboration/writing-plans/skill.json +9 -0
- package/skills/debugging/defense-in-depth/SKILL.md +380 -0
- package/skills/debugging/defense-in-depth/skill.json +9 -0
- package/skills/debugging/root-cause-tracing/SKILL.md +361 -0
- package/skills/debugging/root-cause-tracing/find-polluter.sh +63 -0
- package/skills/debugging/root-cause-tracing/skill.json +12 -0
- package/skills/debugging/systematic-debugging/SKILL.md +299 -0
- package/skills/debugging/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/debugging/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/debugging/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/debugging/systematic-debugging/find-polluter.sh +63 -0
- package/skills/debugging/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/debugging/systematic-debugging/skill.json +9 -0
- package/skills/debugging/systematic-debugging/test-academic.md +14 -0
- package/skills/debugging/systematic-debugging/test-pressure-1.md +58 -0
- package/skills/debugging/systematic-debugging/test-pressure-2.md +68 -0
- package/skills/debugging/systematic-debugging/test-pressure-3.md +69 -0
- package/skills/debugging/verification-before-completion/SKILL.md +143 -0
- package/skills/debugging/verification-before-completion/skill.json +9 -0
- package/skills/finding-skills/SKILL.md +101 -0
- package/skills/finding-skills/skill.json +8 -0
- package/skills/meta/create-skill-json/SKILL.md +370 -0
- package/skills/meta/create-skill-json/skill.json +9 -0
- package/skills/meta/create-skill-json/test-scenarios.md +29 -0
- package/skills/meta/creating-prompts/SKILL.md +349 -0
- package/skills/meta/creating-prompts/examples/do-example.md +65 -0
- package/skills/meta/creating-prompts/examples/plan-example.md +75 -0
- package/skills/meta/creating-prompts/examples/refine-example.md +65 -0
- package/skills/meta/creating-prompts/examples/research-example.md +63 -0
- package/skills/meta/creating-prompts/scripts/get-next-number.sh +27 -0
- package/skills/meta/creating-prompts/skill.json +20 -0
- package/skills/meta/creating-prompts/templates/do-template.md +59 -0
- package/skills/meta/creating-prompts/templates/plan-template.md +58 -0
- package/skills/meta/creating-prompts/templates/refine-template.md +54 -0
- package/skills/meta/creating-prompts/templates/research-template.md +56 -0
- package/skills/meta/using-superpowers/SKILL.md +122 -0
- package/skills/meta/using-superpowers/skill.json +5 -0
- package/skills/meta/writing-prompts/SKILL.md +345 -0
- package/skills/meta/writing-prompts/skill.json +9 -0
- package/skills/problem-solving/ABOUT.md +40 -0
- package/skills/problem-solving/collision-zone-thinking/SKILL.md +188 -0
- package/skills/problem-solving/collision-zone-thinking/references/historical-examples.md +393 -0
- package/skills/problem-solving/collision-zone-thinking/skill.json +9 -0
- package/skills/problem-solving/inversion-exercise/SKILL.md +174 -0
- package/skills/problem-solving/inversion-exercise/skill.json +9 -0
- package/skills/problem-solving/meta-pattern-recognition/SKILL.md +116 -0
- package/skills/problem-solving/meta-pattern-recognition/skill.json +9 -0
- package/skills/problem-solving/scale-game/SKILL.md +222 -0
- package/skills/problem-solving/scale-game/skill.json +9 -0
- package/skills/problem-solving/simplification-cascades/SKILL.md +113 -0
- package/skills/problem-solving/simplification-cascades/skill.json +9 -0
- package/skills/problem-solving/when-stuck/SKILL.md +69 -0
- package/skills/problem-solving/when-stuck/skill.json +9 -0
- package/skills/research/ABOUT.md +20 -0
- package/skills/research/tracing-knowledge-lineages/SKILL.md +241 -0
- package/skills/research/tracing-knowledge-lineages/skill.json +9 -0
- package/skills/setup-skills/SKILL.md +47 -0
- package/skills/testing/condition-based-waiting/SKILL.md +359 -0
- package/skills/testing/condition-based-waiting/example.ts +158 -0
- package/skills/testing/condition-based-waiting/skill.json +12 -0
- package/skills/testing/test-driven-development/SKILL.md +434 -0
- package/skills/testing/test-driven-development/skill.json +9 -0
- package/skills/testing/testing-anti-patterns/SKILL.md +298 -0
- package/skills/testing/testing-anti-patterns/skill.json +9 -0
- package/skills/testing/verification-before-completion/SKILL.md +246 -0
- package/skills/testing/verification-before-completion/skill.json +10 -0
- package/skills/using-a-skill/SKILL.md +101 -0
- package/skills/using-a-skill/skill.json +8 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: root-cause-tracing
|
|
3
|
+
description: Systematically trace bugs backward through the call chain to find the original trigger — not just where the error appears. Use this skill when an error surfaces deep in execution, a symptom keeps recurring after fixes, invalid data arrives at a function from an unknown source, or a test fails but the failure point isn't the true cause. Covers synchronous and async traces, cross-process tracing, bisecting regressions, and identifying far-from-origin bug patterns.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Root Cause Tracing
|
|
7
|
+
|
|
8
|
+
## Core Principle
|
|
9
|
+
|
|
10
|
+
Bugs manifest where they surface, not where they're born. Fixing the symptom is fast but fragile — the same root cause will produce a different symptom tomorrow.
|
|
11
|
+
|
|
12
|
+
**Trace backward through the call chain until you find the original trigger, then fix at the source.**
|
|
13
|
+
|
|
14
|
+
## The Tracing Process
|
|
15
|
+
|
|
16
|
+
### Step 1 — Observe the Symptom
|
|
17
|
+
|
|
18
|
+
Write down exactly what failed and where:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Error: ENOENT: no such file or directory, open '/tmp/undefined/output.json'
|
|
22
|
+
at Object.openSync (node:fs:600:3)
|
|
23
|
+
at writeFileSync (src/reporter.ts:42)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Step 2 — Find the Immediate Cause
|
|
27
|
+
|
|
28
|
+
What code directly triggers this? Don't fix it yet — just identify it.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// reporter.ts:42 — writes to outputDir which is somehow 'undefined'
|
|
32
|
+
fs.writeFileSync(path.join(outputDir, 'output.json'), content);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Step 3 — Ask "What Called This?" (trace up one level)
|
|
36
|
+
|
|
37
|
+
Follow the call chain backward. Use your IDE's "Find Usages", grep, or add a stack trace:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// Temporary instrumentation — add before the problem line
|
|
41
|
+
console.error('DEBUG reporter:', { outputDir, stack: new Error().stack });
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
→ reporter.writeOutput(outputDir)
|
|
46
|
+
→ Runner.finish(outputDir)
|
|
47
|
+
→ CLI.run(config.outputDir)
|
|
48
|
+
→ config loaded from parseArgs()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Step 4 — Keep Tracing Until You Find the Source
|
|
52
|
+
|
|
53
|
+
At each level, ask: **"Could this function be correct if all its inputs were valid?"**
|
|
54
|
+
|
|
55
|
+
If yes, the bug is upstream — keep tracing. If no, you've found a broken invariant.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// CLI.run receives config.outputDir — where does that come from?
|
|
59
|
+
const config = parseArgs(process.argv);
|
|
60
|
+
// parseArgs returns outputDir as undefined when --output flag is missing —
|
|
61
|
+
// no default value is set, so any downstream use of outputDir becomes 'undefined'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Step 5 — Confirm You've Found the Root
|
|
65
|
+
|
|
66
|
+
A true root cause meets all of these:
|
|
67
|
+
- [ ] Removing or fixing this code eliminates **all** instances of the symptom
|
|
68
|
+
- [ ] The code is doing "wrong thing" — not just receiving wrong input
|
|
69
|
+
- [ ] A unit test of just this component would expose the bug
|
|
70
|
+
- [ ] There's no caller upstream that's causing this code to behave badly
|
|
71
|
+
|
|
72
|
+
**Root cause found:** `parseArgs()` returns `undefined` for missing optional flags with no defaults.
|
|
73
|
+
|
|
74
|
+
**Fix at the source:** Set a default in `parseArgs()`, not a guard in `reporter.ts`.
|
|
75
|
+
|
|
76
|
+
## When You Can't Trace Manually — Add Instrumentation
|
|
77
|
+
|
|
78
|
+
When the call chain isn't obvious from reading code, add temporary debug logging:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// TypeScript/JavaScript — capture full call chain
|
|
82
|
+
async function riskyOperation(directory: string) {
|
|
83
|
+
console.error('DEBUG riskyOperation:', {
|
|
84
|
+
directory,
|
|
85
|
+
cwd: process.cwd(),
|
|
86
|
+
stack: new Error().stack, // shows who called this
|
|
87
|
+
});
|
|
88
|
+
// ... rest of function
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
# Python — capture locals at each stack frame
|
|
94
|
+
import traceback
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
result = process_data(user_input)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
tb = traceback.TracebackException.from_exception(e, capture_locals=True)
|
|
100
|
+
print(''.join(tb.format()))
|
|
101
|
+
# Shows local variable values at each frame in the traceback
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Key instrumentation rules:**
|
|
105
|
+
- Log **before** the dangerous operation, not after it fails (you won't get there)
|
|
106
|
+
- In tests, use `console.error()` or `stderr` — test runners often suppress `stdout`
|
|
107
|
+
- Include: the value being used, `process.cwd()` or equivalent, and the full stack
|
|
108
|
+
- Remove all instrumentation once the root cause is found
|
|
109
|
+
|
|
110
|
+
## Tracing Async Bugs
|
|
111
|
+
|
|
112
|
+
Async bugs are harder because the call stack at throw-time shows the event loop, not who scheduled the work.
|
|
113
|
+
|
|
114
|
+
### Node.js
|
|
115
|
+
|
|
116
|
+
Node 12+ has async stack traces enabled by default. If stacks are truncated:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
node --stack-trace-limit=50 your-script.js
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Capture context *before* the await — this is when you have the scheduling context:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
async function fetchUser(id: string) {
|
|
126
|
+
console.error('DEBUG fetchUser scheduled by:', {
|
|
127
|
+
id,
|
|
128
|
+
stack: new Error().stack, // who called fetchUser — this is your trace
|
|
129
|
+
});
|
|
130
|
+
return await db.query('SELECT * FROM users WHERE id = ?', [id]);
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Watch for **fire-and-forget** as a common root cause — errors disappear silently:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// BUG: errors from processItem() are swallowed
|
|
138
|
+
items.forEach(item => {
|
|
139
|
+
processItem(item); // not awaited!
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// FIX: await all promises, surface errors
|
|
143
|
+
await Promise.all(items.map(item => processItem(item)));
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Python asyncio
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
PYTHONASYNCIODEBUG=1 python your_script.py
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Debug mode catches: never-awaited coroutines (with traceback of where they were created), silently-failed tasks, and event loop blocking callbacks.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
import asyncio
|
|
156
|
+
asyncio.run(main(), debug=True) # equivalent in code
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Tracing Across Process Boundaries
|
|
160
|
+
|
|
161
|
+
When a bug originates in Service A but manifests in Service B, you need trace IDs to correlate logs across services.
|
|
162
|
+
|
|
163
|
+
**Lightweight approach for debugging sessions:**
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// Service A — inject a trace ID on outbound calls
|
|
167
|
+
const traceId = crypto.randomUUID();
|
|
168
|
+
console.error('DEBUG outbound:', { traceId, payload, stack: new Error().stack });
|
|
169
|
+
|
|
170
|
+
const response = await fetch('https://service-b/api', {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: { 'X-Trace-Id': traceId },
|
|
173
|
+
body: JSON.stringify(payload),
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// Service B — extract and log the trace ID
|
|
179
|
+
app.use((req, res, next) => {
|
|
180
|
+
const traceId = req.headers['x-trace-id'] ?? crypto.randomUUID();
|
|
181
|
+
console.error('DEBUG inbound:', { traceId, path: req.path });
|
|
182
|
+
req.traceId = traceId;
|
|
183
|
+
next();
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Now you can grep both services' logs for the same `traceId` to reconstruct the full chain.
|
|
188
|
+
|
|
189
|
+
For production distributed tracing, use OpenTelemetry — it propagates trace context automatically via the `traceparent` header and lets you reconstruct full call trees across services.
|
|
190
|
+
|
|
191
|
+
## Finding Which Commit Introduced a Bug
|
|
192
|
+
|
|
193
|
+
When you know something broke between two commits but not which one, use `git bisect`:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
git bisect start
|
|
197
|
+
git bisect bad # current state is broken
|
|
198
|
+
git bisect good v2.3.0 # last known good state
|
|
199
|
+
|
|
200
|
+
# Git checks out the midpoint. Run your test, then:
|
|
201
|
+
git bisect good # or: git bisect bad
|
|
202
|
+
# Repeat until Git identifies the culprit commit (log2(N) steps)
|
|
203
|
+
git bisect reset # restore HEAD when done
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Fully automated bisect** — pass any script that exits 0 for good, non-zero for bad:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
git bisect start
|
|
210
|
+
git bisect bad HEAD
|
|
211
|
+
git bisect good v2.3.0
|
|
212
|
+
# For Jest: use --testNamePattern; for Pytest: use -k; adapt to your test runner
|
|
213
|
+
git bisect run npm test -- --testNamePattern="the failing test"
|
|
214
|
+
git bisect reset
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Skip untestable commits (e.g., broken build mid-refactor):
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
git bisect skip
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Finding Which Test Pollutes State
|
|
224
|
+
|
|
225
|
+
When tests pass in isolation but fail in suite, shared mutable state is the cause. To find which test contaminates the environment, run them one-by-one and stop at the first failure:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# Run each test file in isolation until one fails (adapt glob for your project)
|
|
229
|
+
for f in $(npx jest --listTests); do
|
|
230
|
+
npx jest "$f" --silent && continue
|
|
231
|
+
echo "POLLUTER: $f"
|
|
232
|
+
break
|
|
233
|
+
done
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Or use binary search — halve the test list and narrow down:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Get all test files, run the first half, then the second half
|
|
240
|
+
ALL=$(npx jest --listTests)
|
|
241
|
+
HALF=$(echo "$ALL" | head -n $(( $(echo "$ALL" | wc -l) / 2 )))
|
|
242
|
+
echo "$HALF" | xargs npx jest
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Common Far-From-Origin Patterns
|
|
246
|
+
|
|
247
|
+
These patterns cause bugs to manifest far from where they were introduced:
|
|
248
|
+
|
|
249
|
+
### Initialization Order / Top-Level Access
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// BUG: context.tempDir is '' at module load time, populated only in beforeEach
|
|
253
|
+
const context = setupCoreTest();
|
|
254
|
+
const project = Project.create('test', context.tempDir); // '' — runs at define time!
|
|
255
|
+
|
|
256
|
+
// FIX: access inside the test body, after beforeEach has run
|
|
257
|
+
it('creates project', () => {
|
|
258
|
+
const project = Project.create('test', context.tempDir); // populated by now
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Signal:** Value is empty/undefined even though setup exists — check *when* the setup runs vs. when the value is read.
|
|
263
|
+
|
|
264
|
+
### Closure Over Mutable State
|
|
265
|
+
|
|
266
|
+
```javascript
|
|
267
|
+
// BUG: all callbacks capture the same `i` (var is function-scoped)
|
|
268
|
+
for (var i = 0; i < 3; i++) {
|
|
269
|
+
setTimeout(() => console.log(i), 100); // prints 3, 3, 3
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// FIX: use let (block-scoped) so each iteration captures its own i
|
|
273
|
+
for (let i = 0; i < 3; i++) {
|
|
274
|
+
setTimeout(() => console.log(i), 100); // prints 0, 1, 2
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Signal:** Callbacks or async handlers all see the same (final) value — check `var` vs `let`, or whether a reference is being mutated after being captured.
|
|
279
|
+
|
|
280
|
+
### Exhausted Generator / Iterator
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
# BUG: generator is exhausted after first iteration
|
|
284
|
+
items = (x * 2 for x in range(10)) # generator expression
|
|
285
|
+
print(list(items)) # [0, 2, 4, ...] works once
|
|
286
|
+
print(list(items)) # [] — exhausted!
|
|
287
|
+
|
|
288
|
+
# FIX: materialize to a list if you need to iterate more than once
|
|
289
|
+
items = list(x * 2 for x in range(10))
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Signal:** Works the first time, empty/wrong on second use — check generators, iterators, lazy sequences.
|
|
293
|
+
|
|
294
|
+
### Shared Mutable Default Arguments (Python)
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
# BUG: the default list [] is created once and shared across ALL calls
|
|
298
|
+
def append_to(item, lst=[]):
|
|
299
|
+
lst.append(item)
|
|
300
|
+
return lst
|
|
301
|
+
|
|
302
|
+
append_to(1) # [1]
|
|
303
|
+
append_to(2) # [1, 2] — not a fresh list!
|
|
304
|
+
|
|
305
|
+
# FIX: use None as sentinel, create fresh default inside the function
|
|
306
|
+
def append_to(item, lst=None):
|
|
307
|
+
if lst is None:
|
|
308
|
+
lst = []
|
|
309
|
+
lst.append(item)
|
|
310
|
+
return lst
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Signal:** State persists unexpectedly between calls with default arguments — check for mutable defaults.
|
|
314
|
+
|
|
315
|
+
### Object Reference Mutation
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
// BUG: config mutated after being shared with multiple consumers
|
|
319
|
+
const config = { timeout: 5000 };
|
|
320
|
+
serviceA.init(config);
|
|
321
|
+
serviceB.init(config);
|
|
322
|
+
config.timeout = 1000; // retroactively affects both services
|
|
323
|
+
|
|
324
|
+
// FIX: freeze or clone at handoff
|
|
325
|
+
serviceA.init(Object.freeze({ ...config }));
|
|
326
|
+
serviceB.init(Object.freeze({ ...config }));
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**Signal:** Value changes unexpectedly mid-execution — check whether objects are passed by reference and mutated downstream.
|
|
330
|
+
|
|
331
|
+
## Symptom vs. Root Cause Quick Reference
|
|
332
|
+
|
|
333
|
+
| What you see | Likely root cause |
|
|
334
|
+
|---|---|
|
|
335
|
+
| `TypeError: Cannot read property 'x' of undefined` | Caller passed `undefined` — trace who called with bad args |
|
|
336
|
+
| Database query returns wrong results | Filter/sort applied in wrong order, or data corrupted at write time |
|
|
337
|
+
| Test passes alone, fails in suite | Shared mutable state — find the polluting test |
|
|
338
|
+
| Intermittent `ECONNRESET` | Connection pool exhausted upstream; someone not releasing connections |
|
|
339
|
+
| UI component renders blank | Data fetcher returned `null` — trace the fetch, not the render |
|
|
340
|
+
| `ENOENT` on path containing `undefined` | String interpolation of an `undefined` variable — trace the variable |
|
|
341
|
+
|
|
342
|
+
## After Finding the Root Cause
|
|
343
|
+
|
|
344
|
+
Fix at the source, then add defense-in-depth to make the bug impossible at multiple layers:
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
// Root cause fix: set a safe default at the point of origin
|
|
348
|
+
function parseArgs(argv: string[]): Config {
|
|
349
|
+
const args = minimist(argv.slice(2));
|
|
350
|
+
const outputDir = args['output'] ?? './output'; // default instead of undefined
|
|
351
|
+
return { outputDir };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Defense-in-depth: guard at each consumption layer
|
|
355
|
+
function writeOutput(outputDir: string, content: string) {
|
|
356
|
+
if (!outputDir) throw new Error('writeOutput: outputDir is required');
|
|
357
|
+
fs.writeFileSync(path.join(outputDir, 'output.json'), content);
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
See the `defense-in-depth` skill for a full layered validation approach.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Bisection script to find which test creates unwanted files/state
|
|
3
|
+
# Usage: ./find-polluter.sh <file_or_dir_to_check> <test_pattern>
|
|
4
|
+
# Example: ./find-polluter.sh '.git' 'src/**/*.test.ts'
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
if [ $# -ne 2 ]; then
|
|
9
|
+
echo "Usage: $0 <file_to_check> <test_pattern>"
|
|
10
|
+
echo "Example: $0 '.git' 'src/**/*.test.ts'"
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
POLLUTION_CHECK="$1"
|
|
15
|
+
TEST_PATTERN="$2"
|
|
16
|
+
|
|
17
|
+
echo "🔍 Searching for test that creates: $POLLUTION_CHECK"
|
|
18
|
+
echo "Test pattern: $TEST_PATTERN"
|
|
19
|
+
echo ""
|
|
20
|
+
|
|
21
|
+
# Get list of test files
|
|
22
|
+
TEST_FILES=$(find . -path "$TEST_PATTERN" | sort)
|
|
23
|
+
TOTAL=$(echo "$TEST_FILES" | wc -l | tr -d ' ')
|
|
24
|
+
|
|
25
|
+
echo "Found $TOTAL test files"
|
|
26
|
+
echo ""
|
|
27
|
+
|
|
28
|
+
COUNT=0
|
|
29
|
+
for TEST_FILE in $TEST_FILES; do
|
|
30
|
+
COUNT=$((COUNT + 1))
|
|
31
|
+
|
|
32
|
+
# Skip if pollution already exists
|
|
33
|
+
if [ -e "$POLLUTION_CHECK" ]; then
|
|
34
|
+
echo "⚠️ Pollution already exists before test $COUNT/$TOTAL"
|
|
35
|
+
echo " Skipping: $TEST_FILE"
|
|
36
|
+
continue
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
echo "[$COUNT/$TOTAL] Testing: $TEST_FILE"
|
|
40
|
+
|
|
41
|
+
# Run the test
|
|
42
|
+
npm test "$TEST_FILE" > /dev/null 2>&1 || true
|
|
43
|
+
|
|
44
|
+
# Check if pollution appeared
|
|
45
|
+
if [ -e "$POLLUTION_CHECK" ]; then
|
|
46
|
+
echo ""
|
|
47
|
+
echo "🎯 FOUND POLLUTER!"
|
|
48
|
+
echo " Test: $TEST_FILE"
|
|
49
|
+
echo " Created: $POLLUTION_CHECK"
|
|
50
|
+
echo ""
|
|
51
|
+
echo "Pollution details:"
|
|
52
|
+
ls -la "$POLLUTION_CHECK"
|
|
53
|
+
echo ""
|
|
54
|
+
echo "To investigate:"
|
|
55
|
+
echo " npm test $TEST_FILE # Run just this test"
|
|
56
|
+
echo " cat $TEST_FILE # Review test code"
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
done
|
|
60
|
+
|
|
61
|
+
echo ""
|
|
62
|
+
echo "✅ No polluter found - all tests clean!"
|
|
63
|
+
exit 0
|