test-first-bugfix — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited test-first-bugfix (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
When a user reports a bug, the instinct is to jump straight to reading code and patching it. Resist that. A fix without a reproduction test is a guess — you can't prove it works, and you can't prevent the bug from coming back. This skill enforces a disciplined sequence: understand → reproduce → fix → prove.
Before touching any code, make sure you understand what's actually broken:
Before writing the reproduction test, understand how this project tests things:
jest.config, vitest.config, pytest.ini, Package.swift test targets, .test. / .spec. / _test. files, etc.)This matters because a test that doesn't follow project conventions is a test the team won't maintain.
Write a test that fails right now because of the bug. This is the most important step.
Guidelines:
test_login_fails_when_email_has_plus_sign, not test_login_bug)beforeEach/afterEach (or before/after) for setup and cleanup instead of inline try/finally blocks. This keeps test bodies focused on assertions and ensures cleanup runs even if the test throws. For Node.js node:test, use describe with before/after or beforeEach/afterEach hooks.After writing the test, run it and confirm it fails. Show the user the failing output. If the test passes, the reproduction is wrong — the test must fail before proceeding.
Do NOT move to the next step until you have a failing test that the user can see.
Now spawn 2-3 subagents in parallel, each attempting a different fix strategy. The reproduction test is their proof — a fix is only valid if the test passes.
Each subagent gets this prompt structure:
You are fixing a bug. Here is the context:
**Bug description**: [what the user reported]
**Reproduction test**: [path to the test file, and the specific test name]
**Test command**: [exact command to run just this test]
**Fix strategy**: [the specific approach this subagent should try]
Your job:
1. Read the reproduction test to understand what's expected
2. Read the relevant source code
3. Apply your fix strategy
4. Run the reproduction test — it must pass
5. Run the full test suite in the affected area — nothing else should break
6. If your fix works, save your changes. If not, explain why this strategy didn't work.
IMPORTANT: Do not modify the reproduction test. The test defines the contract. Fix the code, not the test.Choosing fix strategies: Think about 2-3 genuinely different approaches. For example:
If the bug is simple enough that there's really only one sensible fix, use 2 subagents with the same strategy as a consistency check.
Run all subagents using the Agent tool with isolation: "worktree" so they don't conflict with each other.
When subagents complete, evaluate their results:
Apply the winning fix to the main working tree. If multiple fixes passed, briefly tell the user which approaches worked and why you picked the one you did.
After applying the winning fix:
Only then report the bug as fixed. Show what changed and why.
When investigating a bug, check whether it falls into one of these known regression categories. If it does, the fix should address the pattern, not just the symptom.
A Set, enum, or array of recognized values is missing entries. Classic example: INFRA_ERROR_CODES had 6 errno codes but missed EAGAIN — the most common resource exhaustion error. The code looked complete because it had ENOMEM, EMFILE, ENFILE, but the author didn't systematically enumerate all POSIX resource exhaustion codes.
How to detect: When a bug report describes "X is not handled" or "Y is treated as Z instead of W", check if there's a hardcoded list that should include X. Then check what OTHER values are also missing — don't just add the one from the bug report.
How to prevent:
assert.equal(set.size, 6) breaks when you add an entry)A fix is applied to 2 of 14 call sites. Classic example: LC_ALL=C was added to 2 git exec calls but 12 others still used the system locale. The bug reporter's specific reproduction was fixed, but the systemic issue persisted.
How to detect: When a prior fix exists for the same class of bug, search for ALL call sites that need the same treatment, not just the one in the bug report. Use grep to find every instance of the pattern.
How to prevent:
execFileSync that references a git command must include GIT_NO_PROMPT_ENV")A function has branches for known cases but no handler for a new case, so it falls through to a default that produces wrong behavior silently. Classic example: deriveState had handlers for complete, pre-planning, and executing phases but not needs-discussion, which fell through to auto-mode dispatch and looped.
How to detect: When a bug describes "X gets stuck" or "Y loops forever", check if there's a switch/if-else chain that should handle the case but doesn't. Look for missing else branches, incomplete switch statements without default, or early returns that skip new code paths.
How to prevent:
satisfies Record<Phase, Handler>)default: throw new Error("unhandled case") to switch statementsAn existing test asserts the wrong behavior because it was written to match the bug, not the spec. Classic example: a test asserting getApiKey("unknown") returns undefined when the correct behavior is to throw. The test passed because it encoded the bug.
How to detect: When investigating a bug, read existing tests for the area. If a test explicitly asserts the buggy behavior, the test itself is wrong — it was written (or updated) to match the code without questioning whether the code was correct.
How to prevent:
Before pushing test changes, run these checks locally. These catch the exact failures that have repeatedly broken CI:
# 1. TypeScript compilation — use BOTH tsconfigs
npx tsc --noEmit # root (src/tests/)
npx tsc --noEmit --project tsconfig.extensions.json # extensions (gsd/tests/)
# 2. Node type-stripping syntax check (catches what tsc misses)
for f in $(git diff --name-only HEAD~ -- '*.test.ts'); do
node --experimental-strip-types -e "import('./$f')" 2>&1 | grep -q 'SyntaxError' && echo "FAIL: $f"
done
# 3. Run affected tests
npm run test:unit -- --test-name-pattern "pattern"1. `t.after()` requires `(t)` parameter
// WRONG — ReferenceError: t is not defined
test("name", async () => {
t.after(() => cleanup()); // ❌ no t in scope
});
// RIGHT
test("name", async (t) => {
t.after(() => cleanup()); // ✅ t from parameter
});2. `t.after()` with try/catch needs block braces
// WRONG — arrow function can't contain statements without braces
t.after(() => try { rmSync(tmp) } catch { }); // ❌ SyntaxError
// RIGHT
t.after(() => { try { rmSync(tmp, { recursive: true, force: true }); } catch { } }); // ✅3. Helper functions CANNOT use `t.after()` — keep try/finally
// WRONG — helpers don't have test context
function withEnv(env, fn) {
t.after(() => restore()); // ❌ t not in scope
}
// RIGHT — helpers keep try/finally
function withEnv(env, fn) {
const original = { ...process.env };
try { fn(); } finally { Object.assign(process.env, original); } // ✅
}4. Double braces from refactoring
// WRONG — extra brace from try removal
test("name", (t) => {{ // ❌ double brace
// code
}});
// RIGHT
test("name", (t) => { // ✅ single brace
// code
});5. Orphaned catch/finally without try
// WRONG — try was removed but catch left behind
const result = doSomething();
} catch { // ❌ orphaned catch
// handle
}6. Two tsconfigs catch different errors
tsconfig.json excludes src/resources/ — so npx tsc --noEmit won't catch errors in extension test filestsconfig.extensions.json which includes src/resources/7. Missing semicolons in refactored code When moving code between blocks (e.g., extracting from try/finally into t.after), ensure statement terminators are preserved. Node's ASI (Automatic Semicolon Insertion) doesn't always save you — especially before ( or [ on the next line.
t.after() vs try/finally| Scenario | Use | Reason |
|---|---|---|
| Test body cleanup (temp dirs, env vars) | t.after() | Cleaner, runs after test regardless |
| Helper function cleanup | try/finally | No access to test context t |
| Cleanup that assertions depend on | try/finally | t.after() runs AFTER assertions |
| Multi-step cleanup with ordering | try/finally | Explicit ordering guaranteed |
The #1 cause of CI failures (45% in our analysis) is stale base branches. When a PR sits open for more than a few hours on an active repo, main evolves and the PR's tests may fail against the new code.
# Always rebase onto latest main before pushing
git fetch origin main
git rebase origin/main
# Resolve any conflicts, then verify
npx tsc --noEmit
npx tsc --noEmit --project tsconfig.extensions.json # if touching extensions# Check if main has evolved since the PR was created
git log --oneline origin/main..HEAD | wc -l # commits behind
git log --oneline HEAD..origin/main | wc -l # commits ahead on main
# If main is ahead, rebase first — the failure may resolve itselfWhen test fixtures use template literals inside describe/test blocks, every line inherits the block's indentation. Regex anchors like ^### fail because the content starts with spaces.
// WRONG — 2-space indent breaks ^### regex
describe("parser", () => {
test("parses heading", () => {
const content = `
### R001 --- Title
- Description here
`; // ❌ Every line has leading spaces from indentation
});
});
// RIGHT — array.join preserves exact whitespace
test("parses heading", () => {
const content = [
"### R001 --- Title",
"- Description here",
].join("\n"); // ✅ No unintended indentation
});node:test serializes registered test() calls, but top-level await runs during module evaluation — before test registration completes. If a top-level block and a test() both use a shared singleton (like a DB connection), they race.
// WRONG — top-level block runs concurrently with test() calls
console.log("── my test ──");
{
const db = openDatabase(path); // Races with test() DB access
await doSomething(db);
closeDatabase();
}
// RIGHT — wrap in test() for serialized execution
test("my test", async () => {
const db = openDatabase(path);
await doSomething(db);
closeDatabase();
});"The bug is in untested code": That's fine — you're adding the first test for this area. Follow the project's test patterns as closely as possible.
"I can't reproduce it with a test": Some bugs (race conditions, environment-specific issues, UI rendering) are genuinely hard to capture in an automated test. If after a reasonable attempt you can't write a failing test, tell the user and ask how they'd like to proceed. Don't silently skip the test step.
"The user says just fix it, skip the test": Acknowledge their preference, but explain briefly why the test matters ("the test will prevent this bug from coming back"). If they insist on skipping, respect that — they're the boss. But suggest adding the test after the fix at minimum.
"Multiple bugs in one report": Treat each bug separately. Write a reproduction test for each, fix them one at a time. Don't try to fix everything in one pass — that's how things get missed.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.