@schalkneethling/toolkit 0.5.1 → 0.6.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.
Files changed (28) hide show
  1. package/dist/index.mjs.map +1 -1
  2. package/hooks/auto-approve-safe-commands/hook.mjs +5 -1
  3. package/hooks/auto-approve-safe-commands/hook.mts +7 -6
  4. package/hooks/block-dangerous-commands/hook.mjs +3 -3
  5. package/hooks/block-dangerous-commands/hook.mts +10 -22
  6. package/package.json +1 -1
  7. package/skills/css-tokens/SKILL.md +1 -1
  8. package/skills/css-tokens/references/tokens.css +6 -10
  9. package/skills/frontend-security/SKILL.md +3 -0
  10. package/skills/frontend-security/references/csp-configuration.md +68 -51
  11. package/skills/frontend-security/references/csrf-protection.md +74 -70
  12. package/skills/frontend-security/references/dom-security.md +36 -29
  13. package/skills/frontend-security/references/file-upload-security.md +101 -69
  14. package/skills/frontend-security/references/framework-patterns.md +42 -40
  15. package/skills/frontend-security/references/input-validation.md +36 -31
  16. package/skills/frontend-security/references/jwt-security.md +68 -84
  17. package/skills/frontend-security/references/nodejs-npm-security.md +63 -55
  18. package/skills/frontend-security/references/xss-prevention.md +38 -36
  19. package/skills/frontend-testing/SKILL.md +31 -38
  20. package/skills/frontend-testing/references/accessibility-testing.md +56 -62
  21. package/skills/frontend-testing/references/aria-snapshots.md +35 -34
  22. package/skills/frontend-testing/references/locator-strategies.md +37 -40
  23. package/skills/frontend-testing/references/visual-regression.md +29 -23
  24. package/skills/more-secure-dependabot-config/SKILL.md +120 -0
  25. package/skills/more-secure-dependabot-config/references/ecosystem.md +35 -0
  26. package/skills/npm-publishing-best-practices/SKILL.md +316 -0
  27. package/skills/semantic-html/SKILL.md +5 -21
  28. package/skills/semantic-html/references/heading-patterns.md +1 -5
@@ -54,33 +54,35 @@ vm.runInThisContext(userInput);
54
54
  require(userInput);
55
55
 
56
56
  // DANGEROUS - setTimeout/setInterval with strings
57
- setTimeout(userInput, 1000); // Executes as code
57
+ setTimeout(userInput, 1000); // Executes as code
58
58
 
59
59
  // SAFE - pass functions instead
60
- setTimeout(() => { /* code */ }, 1000);
60
+ setTimeout(() => {
61
+ /* code */
62
+ }, 1000);
61
63
  ```
62
64
 
63
65
  ### Child Process Injection
64
66
 
65
67
  ```javascript
66
68
  // DANGEROUS - command injection
67
- const { exec } = require('child_process');
68
- exec(`ls ${userInput}`); // Shell injection
69
+ const { exec } = require("child_process");
70
+ exec(`ls ${userInput}`); // Shell injection
69
71
 
70
72
  // SAFER - use execFile with arguments array
71
- const { execFile } = require('child_process');
72
- execFile('ls', [userInput], callback); // Arguments not interpreted by shell
73
+ const { execFile } = require("child_process");
74
+ execFile("ls", [userInput], callback); // Arguments not interpreted by shell
73
75
 
74
76
  // SAFEST - use spawn with shell: false
75
- const { spawn } = require('child_process');
76
- spawn('ls', [userInput], { shell: false });
77
+ const { spawn } = require("child_process");
78
+ spawn("ls", [userInput], { shell: false });
77
79
  ```
78
80
 
79
81
  ### File System
80
82
 
81
83
  ```javascript
82
- const path = require('path');
83
- const fs = require('fs');
84
+ const path = require("path");
85
+ const fs = require("fs");
84
86
 
85
87
  // DANGEROUS - path traversal
86
88
  const filePath = `/uploads/${userInput}`;
@@ -92,8 +94,8 @@ function safeReadFile(userInput, baseDir) {
92
94
  const relativePath = path.relative(basePath, safePath);
93
95
 
94
96
  // Verify path is within allowed directory
95
- if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
96
- throw new Error('Invalid file path');
97
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
98
+ throw new Error("Invalid file path");
97
99
  }
98
100
 
99
101
  return fs.readFileSync(safePath);
@@ -105,14 +107,14 @@ function safeReadFile(userInput, baseDir) {
105
107
  ### Rate Limiting
106
108
 
107
109
  ```javascript
108
- const rateLimit = require('express-rate-limit');
110
+ const rateLimit = require("express-rate-limit");
109
111
 
110
112
  const limiter = rateLimit({
111
113
  windowMs: 15 * 60 * 1000, // 15 minutes
112
114
  max: 100, // Limit each IP to 100 requests per window
113
- message: 'Too many requests',
115
+ message: "Too many requests",
114
116
  standardHeaders: true,
115
- legacyHeaders: false
117
+ legacyHeaders: false,
116
118
  });
117
119
 
118
120
  app.use(limiter);
@@ -121,28 +123,28 @@ app.use(limiter);
121
123
  const authLimiter = rateLimit({
122
124
  windowMs: 60 * 60 * 1000, // 1 hour
123
125
  max: 5, // 5 attempts per hour
124
- message: 'Too many login attempts'
126
+ message: "Too many login attempts",
125
127
  });
126
128
 
127
- app.use('/api/login', authLimiter);
129
+ app.use("/api/login", authLimiter);
128
130
  ```
129
131
 
130
132
  ### Request Size Limits
131
133
 
132
134
  ```javascript
133
- const express = require('express');
135
+ const express = require("express");
134
136
  const app = express();
135
137
 
136
138
  // Limit JSON body size
137
- app.use(express.json({ limit: '100kb' }));
139
+ app.use(express.json({ limit: "100kb" }));
138
140
 
139
141
  // Limit URL-encoded body
140
- app.use(express.urlencoded({ extended: true, limit: '100kb' }));
142
+ app.use(express.urlencoded({ extended: true, limit: "100kb" }));
141
143
 
142
144
  // Limit file uploads
143
- const multer = require('multer');
145
+ const multer = require("multer");
144
146
  const upload = multer({
145
- limits: { fileSize: 5 * 1024 * 1024 } // 5MB
147
+ limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
146
148
  });
147
149
  ```
148
150
 
@@ -160,27 +162,29 @@ server.headersTimeout = 66000;
160
162
  ## Secure Headers
161
163
 
162
164
  ```javascript
163
- const helmet = require('helmet');
164
-
165
- app.use(helmet({
166
- contentSecurityPolicy: {
167
- directives: {
168
- defaultSrc: ["'self'"],
169
- scriptSrc: ["'self'"],
170
- styleSrc: ["'self'", "'unsafe-inline'"],
171
- imgSrc: ["'self'", "data:", "https:"],
172
- objectSrc: ["'none'"],
173
- upgradeInsecureRequests: []
174
- }
175
- },
176
- hsts: {
177
- maxAge: 31536000,
178
- includeSubDomains: true,
179
- preload: true
180
- },
181
- referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
182
- frameguard: { action: 'deny' }
183
- }));
165
+ const helmet = require("helmet");
166
+
167
+ app.use(
168
+ helmet({
169
+ contentSecurityPolicy: {
170
+ directives: {
171
+ defaultSrc: ["'self'"],
172
+ scriptSrc: ["'self'"],
173
+ styleSrc: ["'self'", "'unsafe-inline'"],
174
+ imgSrc: ["'self'", "data:", "https:"],
175
+ objectSrc: ["'none'"],
176
+ upgradeInsecureRequests: [],
177
+ },
178
+ },
179
+ hsts: {
180
+ maxAge: 31536000,
181
+ includeSubDomains: true,
182
+ preload: true,
183
+ },
184
+ referrerPolicy: { policy: "strict-origin-when-cross-origin" },
185
+ frameguard: { action: "deny" },
186
+ }),
187
+ );
184
188
  ```
185
189
 
186
190
  ## Error Handling
@@ -193,19 +197,22 @@ app.use((err, req, res, next) => {
193
197
 
194
198
  // Send generic message to client
195
199
  res.status(500).json({
196
- error: 'An unexpected error occurred'
200
+ error: "An unexpected error occurred",
197
201
  });
198
202
  });
199
203
 
200
204
  // Async error wrapper
201
- const asyncHandler = fn => (req, res, next) => {
205
+ const asyncHandler = (fn) => (req, res, next) => {
202
206
  Promise.resolve(fn(req, res, next)).catch(next);
203
207
  };
204
208
 
205
- app.get('/data', asyncHandler(async (req, res) => {
206
- const data = await fetchData();
207
- res.json(data);
208
- }));
209
+ app.get(
210
+ "/data",
211
+ asyncHandler(async (req, res) => {
212
+ const data = await fetchData();
213
+ res.json(data);
214
+ }),
215
+ );
209
216
  ```
210
217
 
211
218
  ## Environment Variables
@@ -216,8 +223,8 @@ app.get('/data', asyncHandler(async (req, res) => {
216
223
  const apiKey = process.env.API_KEY;
217
224
 
218
225
  // Validate required env vars at startup
219
- const required = ['API_KEY', 'DB_URL', 'SESSION_SECRET'];
220
- required.forEach(varName => {
226
+ const required = ["API_KEY", "DB_URL", "SESSION_SECRET"];
227
+ required.forEach((varName) => {
221
228
  if (!process.env[varName]) {
222
229
  console.error(`Missing required env var: ${varName}`);
223
230
  process.exit(1);
@@ -230,16 +237,16 @@ required.forEach(varName => {
230
237
  ```javascript
231
238
  // DANGEROUS - evil regex (catastrophic backtracking)
232
239
  const evilRegex = /^(a+)+$/;
233
- evilRegex.test('aaaaaaaaaaaaaaaaaaaaaaaaaaa!'); // Hangs
240
+ evilRegex.test("aaaaaaaaaaaaaaaaaaaaaaaaaaa!"); // Hangs
234
241
 
235
242
  // Heuristic only: safe-regex can have false positives/negatives for ReDoS
236
- const safe = require('safe-regex');
243
+ const safe = require("safe-regex");
237
244
  if (!safe(userProvidedRegex)) {
238
- throw new Error('Unsafe regex pattern');
245
+ throw new Error("Unsafe regex pattern");
239
246
  }
240
247
 
241
248
  // Preferred for untrusted patterns: use RE2 for guaranteed linear time
242
- const RE2 = require('re2');
249
+ const RE2 = require("re2");
243
250
  const pattern = new RE2(userProvidedRegex);
244
251
  ```
245
252
 
@@ -257,5 +264,6 @@ const pattern = new RE2(userProvidedRegex);
257
264
  - [ ] Use `npm-shrinkwrap.json` for published packages
258
265
 
259
266
  OWASP References:
267
+
260
268
  - https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html
261
269
  - https://cheatsheetseries.owasp.org/cheatsheets/NPM_Security_Cheat_Sheet.html
@@ -4,13 +4,13 @@
4
4
 
5
5
  Apply context-appropriate encoding for all untrusted data:
6
6
 
7
- | Context | Encoding Method | Example |
8
- |---------|-----------------|---------|
9
- | HTML Body | HTML Entity Encoding | `<script>` |
10
- | HTML Attribute | Attribute Encoding | `"onclick"` |
11
- | JavaScript | JavaScript Encoding | `\x3cscript\x3e` |
12
- | CSS | CSS Encoding | `\3c script\3e` |
13
- | URL Parameter | URL Encoding | `%3Cscript%3E` |
7
+ | Context | Encoding Method | Example |
8
+ | -------------- | -------------------- | --------------------- |
9
+ | HTML Body | HTML Entity Encoding | `<script>` |
10
+ | HTML Attribute | Attribute Encoding | `"onclick"` |
11
+ | JavaScript | JavaScript Encoding | `\x3cscript\x3e` |
12
+ | CSS | CSS Encoding | `\3c script\3e` |
13
+ | URL Parameter | URL Encoding | `%3Cscript%3E` |
14
14
 
15
15
  ## Safe vs Unsafe Sinks
16
16
 
@@ -18,37 +18,37 @@ Apply context-appropriate encoding for all untrusted data:
18
18
 
19
19
  ```javascript
20
20
  // Execution sinks - NEVER use with user input
21
- element.innerHTML = userInput; // XSS
22
- element.outerHTML = userInput; // XSS
23
- document.write(userInput); // XSS
24
- document.writeln(userInput); // XSS
21
+ element.innerHTML = userInput; // XSS
22
+ element.outerHTML = userInput; // XSS
23
+ document.write(userInput); // XSS
24
+ document.writeln(userInput); // XSS
25
25
 
26
26
  // JavaScript execution sinks
27
- eval(userInput); // XSS
28
- new Function(userInput); // XSS
29
- setTimeout(userInput, time); // XSS if string
30
- setInterval(userInput, time); // XSS if string
27
+ eval(userInput); // XSS
28
+ new Function(userInput); // XSS
29
+ setTimeout(userInput, time); // XSS if string
30
+ setInterval(userInput, time); // XSS if string
31
31
 
32
32
  // URL sinks
33
- location.href = userInput; // XSS
34
- location.assign(userInput); // XSS
35
- location.replace(userInput); // XSS
36
- window.open(userInput); // XSS
33
+ location.href = userInput; // XSS
34
+ location.assign(userInput); // XSS
35
+ location.replace(userInput); // XSS
36
+ window.open(userInput); // XSS
37
37
  ```
38
38
 
39
39
  ### Safe Alternatives
40
40
 
41
41
  ```javascript
42
42
  // Safe text insertion
43
- element.textContent = userInput; // Safe
44
- element.innerText = userInput; // Safe
43
+ element.textContent = userInput; // Safe
44
+ element.innerText = userInput; // Safe
45
45
 
46
46
  // Safe attribute setting (for safe attributes)
47
- element.setAttribute('title', userInput); // Safe for non-event attributes
47
+ element.setAttribute("title", userInput); // Safe for non-event attributes
48
48
 
49
49
  // Safe URL handling
50
50
  const url = new URL(userInput, window.location.origin);
51
- if (url.protocol === 'https:') {
51
+ if (url.protocol === "https:") {
52
52
  location.href = url.href;
53
53
  }
54
54
  ```
@@ -59,19 +59,19 @@ When HTML must be rendered, use sanitization:
59
59
 
60
60
  ```javascript
61
61
  // Using DOMPurify (recommended)
62
- import DOMPurify from 'dompurify';
62
+ import DOMPurify from "dompurify";
63
63
  element.innerHTML = DOMPurify.sanitize(userInput);
64
64
 
65
65
  // With configuration
66
66
  const clean = DOMPurify.sanitize(dirty, {
67
- ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
68
- ALLOWED_ATTR: ['href', 'title']
67
+ ALLOWED_TAGS: ["b", "i", "em", "strong", "a"],
68
+ ALLOWED_ATTR: ["href", "title"],
69
69
  });
70
70
 
71
71
  // Browser Sanitizer API (when available)
72
72
  const sanitizer = new Sanitizer({
73
- allowElements: ['b', 'i', 'em', 'strong', 'a'],
74
- allowAttributes: { 'href': ['a'] }
73
+ allowElements: ["b", "i", "em", "strong", "a"],
74
+ allowAttributes: { href: ["a"] },
75
75
  });
76
76
  element.setHTML(userInput, { sanitizer });
77
77
  ```
@@ -131,7 +131,7 @@ const safeUrl = userInput.startsWith('https://') ? userInput : '#';
131
131
  function isValidUrl(input) {
132
132
  try {
133
133
  const url = new URL(input);
134
- return ['http:', 'https:'].includes(url.protocol);
134
+ return ["http:", "https:"].includes(url.protocol);
135
135
  } catch {
136
136
  return false;
137
137
  }
@@ -139,12 +139,14 @@ function isValidUrl(input) {
139
139
 
140
140
  // Prevent javascript: URLs
141
141
  function sanitizeHref(input) {
142
- if (!input) return '#';
142
+ if (!input) return "#";
143
143
  const trimmed = input.trim().toLowerCase();
144
- if (trimmed.startsWith('javascript:') ||
145
- trimmed.startsWith('data:') ||
146
- trimmed.startsWith('vbscript:')) {
147
- return '#';
144
+ if (
145
+ trimmed.startsWith("javascript:") ||
146
+ trimmed.startsWith("data:") ||
147
+ trimmed.startsWith("vbscript:")
148
+ ) {
149
+ return "#";
148
150
  }
149
151
  return input;
150
152
  }
@@ -156,8 +158,8 @@ Always set appropriate Content-Type headers:
156
158
 
157
159
  ```javascript
158
160
  // Express.js
159
- res.setHeader('Content-Type', 'application/json');
160
- res.setHeader('X-Content-Type-Options', 'nosniff');
161
+ res.setHeader("Content-Type", "application/json");
162
+ res.setHeader("X-Content-Type-Options", "nosniff");
161
163
  ```
162
164
 
163
165
  OWASP Reference: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
@@ -16,7 +16,7 @@ This principle guides testing decisions, but isn't the whole picture:
16
16
  - **Acceptance criteria tests** verify the system does what users/stakeholders need. These should be stable across refactors.
17
17
  - **Implementation tests** verify the pieces are robust — edge cases, error handling, complex logic. These may change when you refactor.
18
18
 
19
- Both have value. The anti-pattern to avoid is tests that *only* mirror implementation without validating meaningful behavior.
19
+ Both have value. The anti-pattern to avoid is tests that _only_ mirror implementation without validating meaningful behavior.
20
20
 
21
21
  ## When to Load References
22
22
 
@@ -36,6 +36,7 @@ Load reference files based on test type:
36
36
  Before writing any test, identify what the code should do from the user's perspective.
37
37
 
38
38
  Ask for or extract criteria from:
39
+
39
40
  - Ticket description or user story
40
41
  - Figma annotations
41
42
  - Functional requirements
@@ -48,11 +49,13 @@ Document criteria as a checklist. These become your first tests.
48
49
  ### Step 2: Map Criteria to Test Cases
49
50
 
50
51
  For each criterion, identify:
52
+
51
53
  - **Happy path**: Normal expected behavior
52
54
  - **Edge cases**: Boundary conditions
53
55
  - **Error cases**: Invalid inputs, failures
54
56
 
55
57
  Example mapping:
58
+
56
59
  ```text
57
60
  Criterion: "User can filter products by category"
58
61
  ├─ Happy path: Select category, products filter correctly
@@ -85,14 +88,14 @@ The distinction: acceptance tests should rarely change on refactor; implementati
85
88
 
86
89
  ### Step 4: Choose Test Type
87
90
 
88
- | Scenario | Test Type | Tool |
89
- |----------|-----------|------|
90
- | Pure logic (no DOM) | Unit test | Vitest |
91
- | Component behavior | Unit test with DOM | Vitest + Testing Library |
92
- | User flows, real browser | E2E test | Playwright |
93
- | Semantic structure validation | ARIA snapshot | Playwright `toMatchAriaSnapshot` |
94
- | Visual appearance | VRT | Playwright screenshots |
95
- | Accessibility compliance | A11y test | Playwright + axe-core |
91
+ | Scenario | Test Type | Tool |
92
+ | ----------------------------- | ------------------ | -------------------------------- |
93
+ | Pure logic (no DOM) | Unit test | Vitest |
94
+ | Component behavior | Unit test with DOM | Vitest + Testing Library |
95
+ | User flows, real browser | E2E test | Playwright |
96
+ | Semantic structure validation | ARIA snapshot | Playwright `toMatchAriaSnapshot` |
97
+ | Visual appearance | VRT | Playwright screenshots |
98
+ | Accessibility compliance | A11y test | Playwright + axe-core |
96
99
 
97
100
  **ARIA snapshots** are particularly valuable for E2E tests. A single snapshot can replace multiple individual assertions while validating the accessibility tree structure.
98
101
 
@@ -114,10 +117,10 @@ describe("calculateDiscount", () => {
114
117
  it("applies 20% discount to order total", () => {
115
118
  // Arrange - Set up test data matching criterion
116
119
  const order = { total: 100, membership: "premium" };
117
-
120
+
118
121
  // Act - Call the function
119
122
  const result = calculateDiscount(order);
120
-
123
+
121
124
  // Assert - Verify expected outcome from requirements
122
125
  expect(result).toBe(80);
123
126
  });
@@ -136,24 +139,14 @@ describe("LoginForm", () => {
136
139
  it("displays error message to user", async () => {
137
140
  const user = userEvent.setup();
138
141
  render(<LoginForm />);
139
-
142
+
140
143
  // Interact using accessible queries
141
- await user.type(
142
- screen.getByLabelText(/email/i),
143
- "invalid@test.com"
144
- );
145
- await user.type(
146
- screen.getByLabelText(/password/i),
147
- "wrong"
148
- );
149
- await user.click(
150
- screen.getByRole("button", { name: /sign in/i })
151
- );
152
-
144
+ await user.type(screen.getByLabelText(/email/i), "invalid@test.com");
145
+ await user.type(screen.getByLabelText(/password/i), "wrong");
146
+ await user.click(screen.getByRole("button", { name: /sign in/i }));
147
+
153
148
  // Assert on user-visible outcome
154
- expect(
155
- await screen.findByRole("alert")
156
- ).toHaveTextContent(/invalid credentials/i);
149
+ expect(await screen.findByRole("alert")).toHaveTextContent(/invalid credentials/i);
157
150
  });
158
151
  });
159
152
  });
@@ -168,10 +161,10 @@ test.describe("Product Catalog", () => {
168
161
  test.describe("filtering by category", () => {
169
162
  test("shows only matching products", async ({ page }) => {
170
163
  await page.goto("/products");
171
-
164
+
172
165
  // Use semantic locators
173
166
  await page.getByRole("combobox", { name: /category/i }).selectOption("Electronics");
174
-
167
+
175
168
  // Assert count, then spot-check first/last
176
169
  const products = page.getByRole("article");
177
170
  await expect(products).toHaveCount(5);
@@ -188,14 +181,12 @@ When you need to verify all items, use `Promise.all` for parallel assertions:
188
181
  test("all products match filter", async ({ page }) => {
189
182
  await page.goto("/products");
190
183
  await page.getByRole("combobox", { name: /category/i }).selectOption("Electronics");
191
-
184
+
192
185
  const products = await page.getByRole("article").all();
193
-
186
+
194
187
  // Parallel assertions — faster than sequential await in a loop
195
188
  await Promise.all(
196
- products.map(product =>
197
- expect(product.getByText(/electronics/i)).toBeVisible()
198
- )
189
+ products.map((product) => expect(product.getByText(/electronics/i)).toBeVisible()),
199
190
  );
200
191
  });
201
192
  ```
@@ -208,7 +199,7 @@ ARIA snapshots consolidate multiple assertions into one, validating semantic str
208
199
  test.describe("Login Page", () => {
209
200
  test("has correct form structure", async ({ page }) => {
210
201
  await page.goto("/login");
211
-
202
+
212
203
  // One snapshot replaces 5+ individual assertions
213
204
  await expect(page.getByRole("main")).toMatchAriaSnapshot(`
214
205
  - heading "Sign In" [level=1]
@@ -218,11 +209,11 @@ test.describe("Login Page", () => {
218
209
  - link "Forgot password?"
219
210
  `);
220
211
  });
221
-
212
+
222
213
  test("shows validation errors on empty submit", async ({ page }) => {
223
214
  await page.goto("/login");
224
215
  await page.getByRole("button", { name: /sign in/i }).click();
225
-
216
+
226
217
  await expect(page.getByRole("form")).toMatchAriaSnapshot(`
227
218
  - textbox "Email"
228
219
  - text "Email is required"
@@ -279,7 +270,7 @@ it("returns validation errors for malformed email", () => {
279
270
  });
280
271
  ```
281
272
 
282
- The key distinction: implementation tests should verify *meaningful* behavior of units, not just that code paths execute.
273
+ The key distinction: implementation tests should verify _meaningful_ behavior of units, not just that code paths execute.
283
274
 
284
275
  ### Circular Validation
285
276
 
@@ -324,11 +315,13 @@ page.getByRole("button", { name: /submit order/i });
324
315
  When a test fails, the investigation depends on the test type:
325
316
 
326
317
  **Acceptance test fails:**
318
+
327
319
  1. Check the code under test first — likely a real bug
328
320
  2. Verify the test still matches current requirements — requirements may have changed
329
321
  3. Only update the test after confirming the new behavior is correct
330
322
 
331
323
  **Implementation test fails:**
324
+
332
325
  1. If you're refactoring, the test may legitimately need updating
333
326
  2. If you're not refactoring, check the code — likely a bug
334
327
  3. Consider whether the test is too tightly coupled to implementation