@ai-support-agent/cli 0.1.32-beta.1 → 0.1.33-beta.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/dist/commands/claude-code-args.d.ts +1 -0
- package/dist/commands/claude-code-args.d.ts.map +1 -1
- package/dist/commands/claude-code-args.js +3 -0
- package/dist/commands/claude-code-args.js.map +1 -1
- package/dist/commands/claude-code-runner.d.ts.map +1 -1
- package/dist/commands/claude-code-runner.js +2 -1
- package/dist/commands/claude-code-runner.js.map +1 -1
- package/dist/commands/plugin-dir.d.ts +20 -0
- package/dist/commands/plugin-dir.d.ts.map +1 -0
- package/dist/commands/plugin-dir.js +71 -0
- package/dist/commands/plugin-dir.js.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +9 -0
- package/dist/plugin/LICENSE +26 -0
- package/dist/plugin/README.md +86 -0
- package/dist/plugin/SYNC.md +113 -0
- package/dist/plugin/agents/build-error-resolver.md +159 -0
- package/dist/plugin/agents/code-reviewer.md +277 -0
- package/dist/plugin/agents/django-reviewer.md +159 -0
- package/dist/plugin/agents/infra-reviewer.md +172 -0
- package/dist/plugin/agents/investigator.md +75 -0
- package/dist/plugin/agents/nextjs-reviewer.md +191 -0
- package/dist/plugin/agents/php-reviewer.md +167 -0
- package/dist/plugin/agents/planner.md +184 -0
- package/dist/plugin/agents/python-reviewer.md +161 -0
- package/dist/plugin/agents/react-reviewer.md +150 -0
- package/dist/plugin/agents/silent-failure-hunter.md +158 -0
- package/dist/plugin/agents/typescript-reviewer.md +179 -0
- package/dist/plugin/agents/ui-reviewer.md +203 -0
- package/dist/plugin/commands/add-feature.md +301 -0
- package/dist/plugin/commands/build-fix.md +47 -0
- package/dist/plugin/commands/code-review.md +228 -0
- package/dist/plugin/commands/fix-defect.md +393 -0
- package/dist/plugin/commands/learn-eval.md +94 -0
- package/dist/plugin/commands/learn.md +84 -0
- package/dist/plugin/commands/plan.md +211 -0
- package/dist/plugin/commands/test-coverage.md +64 -0
- package/dist/plugin/commands/update-docs.md +98 -0
- package/dist/plugin/hooks/hooks.json +59 -0
- package/dist/plugin/hooks/scripts/auto-format.sh +63 -0
- package/dist/plugin/hooks/scripts/check-secrets-before-commit.sh +50 -0
- package/dist/plugin/hooks/scripts/guard-dangerous-commands.sh +55 -0
- package/dist/plugin/hooks/scripts/on-command-resume.sh +112 -0
- package/dist/plugin/hooks/scripts/on-command-stop.sh +200 -0
- package/dist/plugin/hooks/scripts/protect-sensitive-files.sh +58 -0
- package/dist/plugin/rules/common/coding-guidelines.md +73 -0
- package/dist/plugin/rules/documentation/api-docs.md +46 -0
- package/dist/plugin/rules/documentation/docs-site.md +60 -0
- package/dist/plugin/rules/documentation/source-docs.md +89 -0
- package/dist/plugin/rules/documentation/test-docs.md +39 -0
- package/dist/plugin/rules/logging/logging-rules.md +83 -0
- package/dist/plugin/rules/php/coding-rules.md +40 -0
- package/dist/plugin/rules/python/coding-rules.md +40 -0
- package/dist/plugin/rules/typescript/coding-rules.md +45 -0
- package/dist/plugin/skills/api-design/SKILL.md +269 -0
- package/dist/plugin/skills/backend-patterns/SKILL.md +312 -0
- package/dist/plugin/skills/database-migrations/SKILL.md +323 -0
- package/dist/plugin/skills/docker-patterns/SKILL.md +308 -0
- package/dist/plugin/skills/docs-site/SKILL.md +341 -0
- package/dist/plugin/skills/e2e-testing/SKILL.md +334 -0
- package/dist/plugin/skills/frontend-patterns/SKILL.md +318 -0
- package/dist/plugin/skills/integration-testing/SKILL.md +273 -0
- package/package.json +2 -2
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-patterns
|
|
3
|
+
description: A pattern collection for component design, state management, performance, forms, error handling, and accessibility in React (Next.js App Router) and Vue.js (Composition API). Use when creating or refactoring frontend components, deciding on a state management or data-fetching approach, or checking what to look for when reviewing frontend code.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Frontend Development Patterns
|
|
7
|
+
|
|
8
|
+
This guide centers on React (Next.js App Router), with the equivalent Vue.js (Composition API)
|
|
9
|
+
pattern noted alongside each topic. When in doubt, follow the principle of "start simple, add
|
|
10
|
+
complexity only when you actually need it."
|
|
11
|
+
|
|
12
|
+
## 1. Component Design
|
|
13
|
+
|
|
14
|
+
### Splitting responsibilities
|
|
15
|
+
|
|
16
|
+
Aim for one responsibility per component. If you can't describe what a component does in a
|
|
17
|
+
single sentence, consider splitting it.
|
|
18
|
+
|
|
19
|
+
- Separate presentation (display) from data fetching and state management (container).
|
|
20
|
+
- In the Next.js App Router, fetch data in Server Components and carve out only the
|
|
21
|
+
interactive parts into small `"use client"` Client Components.
|
|
22
|
+
- In Vue, once the logic inside `setup` starts spanning multiple areas of the screen, split it
|
|
23
|
+
into child components or composables.
|
|
24
|
+
|
|
25
|
+
### Signs a component has grown too large
|
|
26
|
+
|
|
27
|
+
If several of these apply, it's a signal to split the component:
|
|
28
|
+
|
|
29
|
+
- The file is over 300 lines, or the JSX/template is over 100 lines.
|
|
30
|
+
- Seven or more `useState`/`ref` calls are lined up.
|
|
31
|
+
- Boolean props (`isCompact`, `showHeader`, etc.) keep growing and their conditional branches
|
|
32
|
+
start interacting with each other.
|
|
33
|
+
- The component performs multiple unrelated data fetches.
|
|
34
|
+
- Writing a test for it requires mocking a pile of unrelated functionality.
|
|
35
|
+
|
|
36
|
+
### Composing with children / slots
|
|
37
|
+
|
|
38
|
+
Express variants through composition rather than piling on flag props.
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
// Bad: variants expressed as a growing set of flag props
|
|
42
|
+
<Card title="Sales" showIcon iconType="chart" footerText="Details" footerLink="/sales" />
|
|
43
|
+
|
|
44
|
+
// Good: composition via children lets the caller decide the structure
|
|
45
|
+
<Card>
|
|
46
|
+
<Card.Header icon={<ChartIcon />}>Sales</Card.Header>
|
|
47
|
+
<Card.Body>{children}</Card.Body>
|
|
48
|
+
<Card.Footer><Link href="/sales">Details</Link></Card.Footer>
|
|
49
|
+
</Card>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```vue
|
|
53
|
+
<!-- In Vue, named slots express the same structure -->
|
|
54
|
+
<Card>
|
|
55
|
+
<template #header>Sales</template>
|
|
56
|
+
<template #default>...</template>
|
|
57
|
+
<template #footer><RouterLink to="/sales">Details</RouterLink></template>
|
|
58
|
+
</Card>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 2. Reusing Logic (Custom Hooks / Composables)
|
|
62
|
+
|
|
63
|
+
### When to extract
|
|
64
|
+
|
|
65
|
+
- The same combination of state and effects shows up in two or more places.
|
|
66
|
+
- There's logic you want to unit test independently of the component.
|
|
67
|
+
- You can give it a name that describes what it does (`useDebounce`, `usePagination`, etc.).
|
|
68
|
+
|
|
69
|
+
Conversely, don't force an extraction for logic that's used in only one place and is hard to
|
|
70
|
+
name meaningfully.
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
// Good: extract the state + effect boilerplate into a hook (React)
|
|
74
|
+
function useDebouncedValue<T>(value: T, delayMs = 300): T {
|
|
75
|
+
const [debounced, setDebounced] = useState(value);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
const id = setTimeout(() => setDebounced(value), delayMs);
|
|
78
|
+
return () => clearTimeout(id);
|
|
79
|
+
}, [value, delayMs]);
|
|
80
|
+
return debounced;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// Good: the same logic implemented as a Vue composable
|
|
86
|
+
export function useDebouncedValue<T>(source: Ref<T>, delayMs = 300) {
|
|
87
|
+
const debounced = ref(source.value) as Ref<T>;
|
|
88
|
+
let id: ReturnType<typeof setTimeout>;
|
|
89
|
+
watch(source, (v) => {
|
|
90
|
+
clearTimeout(id);
|
|
91
|
+
id = setTimeout(() => (debounced.value = v), delayMs);
|
|
92
|
+
});
|
|
93
|
+
onUnmounted(() => clearTimeout(id));
|
|
94
|
+
return debounced;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- Hooks/composables should not return UI. If the reusable unit involves UI, make it a
|
|
99
|
+
component instead.
|
|
100
|
+
- Return an object so callers can destructure only what they need.
|
|
101
|
+
|
|
102
|
+
## 3. State Management
|
|
103
|
+
|
|
104
|
+
### Where state should live: priority order
|
|
105
|
+
|
|
106
|
+
1. Local state (`useState` / `ref`). Always start here.
|
|
107
|
+
2. Lift state up to a parent, once it needs to be shared between siblings.
|
|
108
|
+
3. Context (`createContext` / `provide` and `inject`), once passing props down deeply becomes
|
|
109
|
+
painful.
|
|
110
|
+
4. A global store (Zustand / Pinia), once it meets the criteria below.
|
|
111
|
+
|
|
112
|
+
### When a global store is warranted
|
|
113
|
+
|
|
114
|
+
- State that outlives a single screen (authenticated user, theme, cart, etc.).
|
|
115
|
+
- State that must survive route changes without being discarded.
|
|
116
|
+
- State that's read and written by many far-apart components.
|
|
117
|
+
|
|
118
|
+
If the only issue is "props get passed through 2-3 levels," component composition or context
|
|
119
|
+
can usually solve it — that alone isn't a reason to introduce a store.
|
|
120
|
+
|
|
121
|
+
### Let a data-fetching library own server state
|
|
122
|
+
|
|
123
|
+
Don't hand-roll caching, refetching, and loading-state management for API responses. Delegate
|
|
124
|
+
to SWR or TanStack Query (React Query / Vue Query).
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
// Bad: manually managing server data with useEffect + a global store
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
fetch("/api/users").then((r) => r.json()).then((d) => store.setUsers(d));
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
// Good: delegate to a data-fetching library (caching, revalidation, and dedup included)
|
|
133
|
+
const { data, error, isLoading } = useQuery({
|
|
134
|
+
queryKey: ["users"],
|
|
135
|
+
queryFn: fetchUsers,
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- Reserve the global store for genuinely client-only state.
|
|
140
|
+
- Copying server data into a store means you end up reimplementing freshness tracking and
|
|
141
|
+
invalidation yourself.
|
|
142
|
+
|
|
143
|
+
## 4. Derived State
|
|
144
|
+
|
|
145
|
+
Values that can be computed from existing state should be derived during render (React) or via
|
|
146
|
+
`computed` (Vue) — don't duplicate them into a separate piece of state via an effect or watcher.
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
// Bad: duplicating a derived value into separate state via useEffect
|
|
150
|
+
// (lags a render behind and is a common source of bugs)
|
|
151
|
+
const [filtered, setFiltered] = useState<Item[]>([]);
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
setFiltered(items.filter((i) => i.name.includes(query)));
|
|
154
|
+
}, [items, query]);
|
|
155
|
+
|
|
156
|
+
// Good: compute it during render; add useMemo only if profiling shows it's expensive
|
|
157
|
+
const filtered = items.filter((i) => i.name.includes(query));
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// Bad: duplicating into a separate ref via watch (Vue)
|
|
162
|
+
watch([items, query], () => {
|
|
163
|
+
filtered.value = items.value.filter((i) => i.name.includes(query.value));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Good: derive it declaratively with computed
|
|
167
|
+
const filtered = computed(() =>
|
|
168
|
+
items.value.filter((i) => i.name.includes(query.value))
|
|
169
|
+
);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- Whenever you see "an effect that updates state B when state A changes," ask first whether B
|
|
173
|
+
can just be a derived value instead.
|
|
174
|
+
- Effects/watchers are legitimately for syncing with external systems (DOM manipulation,
|
|
175
|
+
subscriptions, sending logs, etc.) — not for mirroring state.
|
|
176
|
+
|
|
177
|
+
## 5. Performance
|
|
178
|
+
|
|
179
|
+
### Memoization discipline: measure first
|
|
180
|
+
|
|
181
|
+
- Check actual re-render cost with React DevTools Profiler / Vue DevTools before optimizing.
|
|
182
|
+
- Don't reflexively slap `useMemo` / `useCallback` / `memo` on everything — it costs
|
|
183
|
+
readability, and a wrong dependency array turns into a bug.
|
|
184
|
+
- It's worth adding when a computation is measurably expensive, or when referential stability
|
|
185
|
+
matters for a `memo`-ized child or a dependency array.
|
|
186
|
+
- In environments where the React Compiler or Vue's built-in optimizations are effective,
|
|
187
|
+
manual memoization can often be reduced further.
|
|
188
|
+
|
|
189
|
+
### Code splitting and lazy loading
|
|
190
|
+
|
|
191
|
+
- Route-level splitting is handled automatically by Next.js / Vue Router — rely on that first.
|
|
192
|
+
- Lazy-load heavy components that aren't needed on initial render (modals, charts, editors)
|
|
193
|
+
with `next/dynamic` or `defineAsyncComponent`.
|
|
194
|
+
|
|
195
|
+
```tsx
|
|
196
|
+
// Good: lazily load a heavy component that's only needed once it's opened
|
|
197
|
+
const ChartPanel = dynamic(() => import("./ChartPanel"), { ssr: false });
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Virtualizing long lists
|
|
201
|
+
|
|
202
|
+
Don't render every row of a list with hundreds of items — render only the visible range with
|
|
203
|
+
`@tanstack/react-virtual` / `@tanstack/vue-virtual` or similar. Before reaching for
|
|
204
|
+
virtualization, first consider whether pagination or a "load more" pattern can simply reduce
|
|
205
|
+
the item count.
|
|
206
|
+
|
|
207
|
+
## 6. Forms
|
|
208
|
+
|
|
209
|
+
### Schema-based validation
|
|
210
|
+
|
|
211
|
+
Define validation rules as a schema (Zod, Valibot, etc.) and wire it into your form library
|
|
212
|
+
(React Hook Form / VeeValidate). Re-validate with the same schema on the server.
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
// Good: the schema is the single source of truth for validation rules
|
|
216
|
+
const schema = z.object({
|
|
217
|
+
email: z.string().email("Please enter a valid email address"),
|
|
218
|
+
age: z.coerce.number().int().min(18, "Must be 18 or older"),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const { register, handleSubmit, formState } = useForm({
|
|
222
|
+
resolver: zodResolver(schema),
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- Display error messages near the relevant field and associate them with the input via
|
|
227
|
+
`aria-describedby`.
|
|
228
|
+
- Don't hardcode validation logic inline in JSX or templates.
|
|
229
|
+
|
|
230
|
+
### Submission state and preventing double submits
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
// Bad: no submission-state guard, so rapid clicks trigger duplicate POSTs
|
|
234
|
+
<button onClick={() => submit(values)}>Submit</button>
|
|
235
|
+
|
|
236
|
+
// Good: disable the button while submitting and show the state to the user
|
|
237
|
+
const { isSubmitting } = formState;
|
|
238
|
+
<button type="submit" disabled={isSubmitting}>
|
|
239
|
+
{isSubmitting ? "Submitting..." : "Submit"}
|
|
240
|
+
</button>
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
- Using `useMutation` from React Query / Vue Query for mutations standardizes handling of
|
|
244
|
+
`isPending`, errors, and retries.
|
|
245
|
+
- After a successful submission, reset the form or navigate away so the user isn't left in a
|
|
246
|
+
state where they could resubmit the same data.
|
|
247
|
+
|
|
248
|
+
## 7. Error Handling
|
|
249
|
+
|
|
250
|
+
### Error boundaries / onErrorCaptured
|
|
251
|
+
|
|
252
|
+
- React: place an error boundary around each layout region so one failure doesn't blank out
|
|
253
|
+
the whole screen. In the Next.js App Router, add an `error.tsx` per segment.
|
|
254
|
+
- Vue: catch descendant rendering errors in a parent's `onErrorCaptured` and switch to a
|
|
255
|
+
fallback UI.
|
|
256
|
+
- Never swallow the error silently in the boundary — always report it to your error monitoring
|
|
257
|
+
service.
|
|
258
|
+
|
|
259
|
+
### The three states of a data-fetching UI
|
|
260
|
+
|
|
261
|
+
Always explicitly handle the three states: loading, error, and empty (zero results).
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
// Good: handle all three states before rendering the real content
|
|
265
|
+
if (isLoading) return <Spinner />;
|
|
266
|
+
if (error) return <ErrorMessage error={error} onRetry={refetch} />;
|
|
267
|
+
if (data.length === 0) return <EmptyState message="No data available" />;
|
|
268
|
+
return <UserList users={data} />;
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
- Give the error state a retry action (a retry button).
|
|
272
|
+
- Word the empty state so it clearly reads as "not an error," and point toward a next action
|
|
273
|
+
(e.g., create new).
|
|
274
|
+
- To avoid flicker, consider a skeleton UI or `placeholderData` (keeping the previous data
|
|
275
|
+
visible while refetching).
|
|
276
|
+
|
|
277
|
+
## 8. Accessibility
|
|
278
|
+
|
|
279
|
+
### Semantic elements
|
|
280
|
+
|
|
281
|
+
- Use `button` / `a` for anything clickable. Don't attach `onClick` to a `div`.
|
|
282
|
+
- Use `a` (`Link`) for navigation/URL changes and `button` for in-place actions.
|
|
283
|
+
- Keep heading levels in order starting from `h1`; don't pick a heading level for its visual
|
|
284
|
+
size.
|
|
285
|
+
- Use landmarks (`main`, `nav`, `header`, `footer`) to convey document structure.
|
|
286
|
+
|
|
287
|
+
```tsx
|
|
288
|
+
// Bad: onClick on a div (unreachable via keyboard or assistive technology)
|
|
289
|
+
<div onClick={save}>Save</div>
|
|
290
|
+
|
|
291
|
+
// Good: use a native button (focus and Enter/Space work out of the box)
|
|
292
|
+
<button type="button" onClick={save}>Save</button>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Labels
|
|
296
|
+
|
|
297
|
+
- Associate every form input with a `label` (`htmlFor` / `for`).
|
|
298
|
+
- Add `aria-label` to icon-only buttons.
|
|
299
|
+
- Image `alt` text should describe the content; use `alt=""` for purely decorative images.
|
|
300
|
+
|
|
301
|
+
### Keyboard operation basics
|
|
302
|
+
|
|
303
|
+
- Everything should be reachable via Tab, operable with Enter/Space, and modals should close
|
|
304
|
+
with Esc.
|
|
305
|
+
- Don't remove the focus ring via CSS. If you do, provide an alternative visible style.
|
|
306
|
+
- When a modal opens, move focus into it, and restore focus to the triggering element when it
|
|
307
|
+
closes.
|
|
308
|
+
- Before building custom UI, consider whether a native element or a proven headless UI library
|
|
309
|
+
already covers the need.
|
|
310
|
+
|
|
311
|
+
## Related
|
|
312
|
+
|
|
313
|
+
- For reviews, use the react-reviewer / typescript-reviewer subagents, or `/code-review`.
|
|
314
|
+
- For Next.js-specific concerns (Server/Client boundaries, Server Actions, caching, etc.), use
|
|
315
|
+
the nextjs-reviewer.
|
|
316
|
+
- For usability and consistency (UI/UX) concerns, use the ui-reviewer.
|
|
317
|
+
- Treat this skill as the decision criteria to apply during implementation and refactoring;
|
|
318
|
+
delegate the actual application of review criteria to the subagents listed above.
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: integration-testing
|
|
3
|
+
description: A collection of integration-testing patterns for verifying actual database reads and writes. Covers the limits of mocking, isolating the test database, transaction rollback, and how to verify constraint violations, optimistic locking, and N+1 queries, for NestJS (TypeORM/Prisma), Django, Flask, Laravel, and CakePHP. Use this when writing or reviewing tests for DB-touching features, when you want to avoid a "green only because it's mocked" false sense of safety, or when designing how DB tests run in CI.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# integration-testing: Integration Testing (DB Read/Write Verification) Patterns
|
|
7
|
+
|
|
8
|
+
## 1. Why Integration Tests Are Necessary (The Limits of Mocking)
|
|
9
|
+
|
|
10
|
+
Unit tests that mock the repository or ORM never verify any of the following:
|
|
11
|
+
|
|
12
|
+
- Correctness of SQL or query-builder output (typo'd column names, wrong JOIN conditions)
|
|
13
|
+
- Consistency with the schema (missing migrations, type mismatches)
|
|
14
|
+
- DB constraint behavior (unique constraints, foreign keys, NOT NULL, CHECK)
|
|
15
|
+
- Transaction boundaries (commit/rollback timing)
|
|
16
|
+
- Optimistic locking and concurrency conflicts
|
|
17
|
+
|
|
18
|
+
### The Classic Trap: Tests Are Green, but the DB Breaks
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// Bad: the repository is mocked, so a unique-constraint violation is never caught
|
|
22
|
+
it('can register a user', async () => {
|
|
23
|
+
const repo = { save: jest.fn().mockResolvedValue({ id: 1 }) };
|
|
24
|
+
const service = new UserService(repo as any);
|
|
25
|
+
await service.register('taro@example.com');
|
|
26
|
+
await service.register('taro@example.com'); // the mock silently succeeds
|
|
27
|
+
expect(repo.save).toHaveBeenCalledTimes(2); // green, but in production the
|
|
28
|
+
}); // second call hits a unique-constraint 500 error
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// Good: register against a real DB and verify the behavior on duplicates
|
|
33
|
+
it('throws ConflictException when the email address is duplicated', async () => {
|
|
34
|
+
await service.register('taro@example.com');
|
|
35
|
+
await expect(service.register('taro@example.com'))
|
|
36
|
+
.rejects.toThrow(ConflictException); // confirms constraint-violation handling against a real DB
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
As long as the mocked `save` keeps returning success, the test stays green forever — and a unit test will never catch "the second registration fails on the DB's unique constraint" or "the service layer never converts that exception into the right HTTP error." Those are exactly the failures that show up in production.
|
|
41
|
+
|
|
42
|
+
## 2. Dividing Responsibilities Across Test Layers
|
|
43
|
+
|
|
44
|
+
| Layer | Target | DB | Responsibility |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| Unit | Pure logic (calculations, transforms, validation) | Not used | Branch coverage, edge cases |
|
|
47
|
+
| Integration | Repository/service layer, queries | Real DB | Correctness of DB-touching code |
|
|
48
|
+
| E2E | Business flows through the UI/API | Real DB (production-like) | User-facing scenarios |
|
|
49
|
+
|
|
50
|
+
- Verifying DB-touching code is the responsibility of integration tests. Pushing that burden onto E2E tests makes them slow and flaky.
|
|
51
|
+
- For E2E test structure and waiting strategies, see the e2e-testing skill.
|
|
52
|
+
|
|
53
|
+
## 3. Core Principles for DB Testing
|
|
54
|
+
|
|
55
|
+
### 3.1 Isolate the Test Database
|
|
56
|
+
|
|
57
|
+
Never share the development database. Test runs will destroy data if you do, so use a dedicated database (e.g. `myapp_test`) or a disposable container. Switch the connection target via environment variables, and verify "this is really the test connection" at test startup to prevent accidents.
|
|
58
|
+
|
|
59
|
+
### 3.2 Keep Tests Independent of Each Other
|
|
60
|
+
|
|
61
|
+
No test may depend on data left behind by a previous test. There are two cleanup strategies:
|
|
62
|
+
|
|
63
|
+
- **Transaction rollback**: wrap each test in a transaction and roll it back at the end. Fast — but it can't verify code paths where the code under test explicitly commits, uses a separate connection, or relies on nested transactions.
|
|
64
|
+
- **Truncate/recreate**: truncate all tables between tests. Slower, but it also works for logic that involves an actual commit (i.e. testing the transaction boundary itself).
|
|
65
|
+
|
|
66
|
+
Rule of thumb: default to the rollback strategy, and switch to truncation only for the specific tests that verify transaction behavior or post-commit hooks.
|
|
67
|
+
|
|
68
|
+
### 3.3 Use the Same DB Engine as Production (the SQLite-Substitute Trap)
|
|
69
|
+
|
|
70
|
+
"Let's just use SQLite for speed" is a reliable source of both false positives and false negatives.
|
|
71
|
+
|
|
72
|
+
- **Loose typing**: SQLite will happily store 100 characters in a `VARCHAR(10)`. Length-overflow bugs go undetected.
|
|
73
|
+
- **Constraints**: foreign keys are disabled by default, and CHECK constraints and some unique-constraint behaviors differ from production engines.
|
|
74
|
+
- **Functions and SQL dialect**: differences in `ILIKE`, `ON CONFLICT`, window functions, and date functions mean things break only in production, or only in tests.
|
|
75
|
+
- **Locking behavior**: SQLite locks the whole database rather than individual rows, so concurrency and deadlock tests can't meaningfully run against it.
|
|
76
|
+
|
|
77
|
+
Use Testcontainers or Docker Compose to spin up the same PostgreSQL/MySQL you run in production. The few seconds of startup cost is easily absorbed by reusing one container per test suite.
|
|
78
|
+
|
|
79
|
+
### 3.4 Mock External APIs, but Never Mock the Database
|
|
80
|
+
|
|
81
|
+
The boundary for an integration test is: "things we own are real, things we don't own can be mocked." Payment gateways, email delivery, and external SaaS can reasonably be mocked (or stubbed). The database, however, is the very thing whose schema and constraints you're trying to verify — so it must never be mocked.
|
|
82
|
+
|
|
83
|
+
## 4. What You Must Always Test
|
|
84
|
+
|
|
85
|
+
1. **Read-back after write**: read the saved value back through a separate path (a repository `find`, or raw SQL) and confirm what was actually persisted. Checking only the return value of `save` will miss values that never made it to the DB — lost conversions, missing column mappings, and the like.
|
|
86
|
+
2. **Behavior on constraint violations**: deliberately trigger unique-constraint, foreign-key, and NOT NULL violations, and confirm the application layer converts them into the right exception or error response.
|
|
87
|
+
3. **Transaction atomicity**: force a failure partway through a multi-table update (e.g. the second of two writes violates a constraint), then read back the first write to confirm it was rolled back too.
|
|
88
|
+
4. **Concurrency and optimistic locking**: fetch a versioned entity in two separate contexts, update one first, then update the other, and confirm a conflict exception is raised.
|
|
89
|
+
5. **Complex queries**: for JOINs, aggregations, and pagination, insert real data that includes boundary conditions and verify the results (zero rows, exact page boundaries, stable sort ordering on duplicate keys).
|
|
90
|
+
6. **N+1 detection**: assert on the query count, confirming that listing endpoints don't issue a number of queries proportional to the row count.
|
|
91
|
+
|
|
92
|
+
## 5. Stack-Specific Implementation Patterns
|
|
93
|
+
|
|
94
|
+
### 5.1 NestJS + TypeORM (Testcontainers)
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// Good: spin up a real PostgreSQL via Testcontainers and verify against the real repository
|
|
98
|
+
import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
|
|
99
|
+
|
|
100
|
+
let container: StartedPostgreSqlContainer;
|
|
101
|
+
let module: TestingModule;
|
|
102
|
+
|
|
103
|
+
beforeAll(async () => {
|
|
104
|
+
container = await new PostgreSqlContainer('postgres:16').start();
|
|
105
|
+
module = await Test.createTestingModule({
|
|
106
|
+
imports: [
|
|
107
|
+
TypeOrmModule.forRoot({
|
|
108
|
+
type: 'postgres',
|
|
109
|
+
url: container.getConnectionUri(),
|
|
110
|
+
entities: [User, Order],
|
|
111
|
+
migrations: ['migrations/*.ts'],
|
|
112
|
+
migrationsRun: true, // apply real migrations instead of `synchronize`
|
|
113
|
+
}),
|
|
114
|
+
UserModule, // use the real repository, not a mock
|
|
115
|
+
],
|
|
116
|
+
}).compile();
|
|
117
|
+
}, 60_000);
|
|
118
|
+
|
|
119
|
+
afterEach(async () => {
|
|
120
|
+
// truncate strategy: needed because this suite tests transaction boundaries
|
|
121
|
+
await module.get(DataSource).query('TRUNCATE "user", "order" CASCADE');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('rolls back the stock decrement when order confirmation fails partway through', async () => {
|
|
125
|
+
const service = module.get(OrderService);
|
|
126
|
+
await expect(service.confirm(orderWithInvalidItem)).rejects.toThrow();
|
|
127
|
+
const stock = await module.get(DataSource).getRepository(Stock).findOneBy({ sku: 'A' });
|
|
128
|
+
expect(stock.quantity).toBe(10); // the first decrement was rolled back too
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 5.2 NestJS + Prisma
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// Good: point DATABASE_URL at the container and run `prisma migrate deploy`
|
|
136
|
+
beforeAll(async () => {
|
|
137
|
+
container = await new PostgreSqlContainer('postgres:16').start();
|
|
138
|
+
process.env.DATABASE_URL = container.getConnectionUri();
|
|
139
|
+
execSync('npx prisma migrate deploy'); // build the schema from real migrations
|
|
140
|
+
prisma = new PrismaClient();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('reads back a saved value', async () => {
|
|
144
|
+
await service.createUser({ email: 'taro@example.com', name: 'Taro' });
|
|
145
|
+
// read back directly from the DB instead of trusting the return value of save
|
|
146
|
+
const row = await prisma.user.findUnique({ where: { email: 'taro@example.com' } });
|
|
147
|
+
expect(row?.name).toBe('Taro');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('catches a unique-constraint violation as P2002 and handles it', async () => {
|
|
151
|
+
await service.createUser({ email: 'a@example.com', name: 'A' });
|
|
152
|
+
await expect(service.createUser({ email: 'a@example.com', name: 'B' }))
|
|
153
|
+
.rejects.toThrow(ConflictException);
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 5.3 Django + pytest-django
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
# Good: the django_db mark uses a real DB; by default each test is rolled back afterward
|
|
161
|
+
import pytest
|
|
162
|
+
|
|
163
|
+
@pytest.mark.django_db
|
|
164
|
+
def test_read_back_after_save():
|
|
165
|
+
Order.objects.create(code="A-001", total=1000)
|
|
166
|
+
row = Order.objects.get(code="A-001")
|
|
167
|
+
assert row.total == 1000
|
|
168
|
+
|
|
169
|
+
# Use transaction=True (equivalent to TransactionTestCase) when testing the
|
|
170
|
+
# transaction boundary itself — select_for_update and on_commit hooks only work here
|
|
171
|
+
@pytest.mark.django_db(transaction=True)
|
|
172
|
+
def test_full_rollback_on_partial_failure():
|
|
173
|
+
with pytest.raises(IntegrityError):
|
|
174
|
+
place_order_with_invalid_item()
|
|
175
|
+
assert Stock.objects.get(sku="A").quantity == 10
|
|
176
|
+
|
|
177
|
+
# N+1 detection: assert on the query count
|
|
178
|
+
def test_listing_uses_2_queries_regardless_of_row_count(django_assert_num_queries):
|
|
179
|
+
OrderFactory.create_batch(20)
|
|
180
|
+
with django_assert_num_queries(2): # pins down the effect of select_related / prefetch_related
|
|
181
|
+
list_orders_with_items()
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Rule of thumb: default to `@pytest.mark.django_db` (the fast rollback strategy). `transaction=True` actually commits, so it's slower — reserve it for the specific tests that need it.
|
|
185
|
+
|
|
186
|
+
### 5.4 Flask + pytest (SQLAlchemy)
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# Good: share one PostgreSQL container for the whole session, and roll back after each test
|
|
190
|
+
@pytest.fixture(scope="session")
|
|
191
|
+
def engine():
|
|
192
|
+
with PostgresContainer("postgres:16") as pg:
|
|
193
|
+
engine = create_engine(pg.get_connection_url())
|
|
194
|
+
run_migrations(engine) # apply the real schema via Alembic
|
|
195
|
+
yield engine
|
|
196
|
+
|
|
197
|
+
@pytest.fixture
|
|
198
|
+
def db_session(engine):
|
|
199
|
+
conn = engine.connect()
|
|
200
|
+
trans = conn.begin()
|
|
201
|
+
session = Session(bind=conn, join_transaction_mode="create_savepoint")
|
|
202
|
+
yield session # even if the code under test commits, it stays inside the savepoint
|
|
203
|
+
session.close()
|
|
204
|
+
trans.rollback() # the independence between tests is guaranteed by the outer transaction
|
|
205
|
+
conn.close()
|
|
206
|
+
|
|
207
|
+
def test_foreign_key_violation_becomes_400(client, db_session):
|
|
208
|
+
res = client.post("/orders", json={"user_id": 9999}) # a user that doesn't exist
|
|
209
|
+
assert res.status_code == 400 # confirms IntegrityError is converted, not swallowed
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### 5.5 Laravel + PHPUnit
|
|
213
|
+
|
|
214
|
+
```php
|
|
215
|
+
// Good: RefreshDatabase wraps each test in a transaction and rolls it back quickly
|
|
216
|
+
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
217
|
+
|
|
218
|
+
class OrderTest extends TestCase
|
|
219
|
+
{
|
|
220
|
+
use RefreshDatabase;
|
|
221
|
+
|
|
222
|
+
public function test_saved_value_is_persisted_to_the_database(): void
|
|
223
|
+
{
|
|
224
|
+
$user = User::factory()->create();
|
|
225
|
+
$this->postJson('/api/orders', ['user_id' => $user->id, 'total' => 1000])
|
|
226
|
+
->assertCreated();
|
|
227
|
+
// Good: use assertDatabaseHas to verify the actual data in the DB
|
|
228
|
+
$this->assertDatabaseHas('orders', ['user_id' => $user->id, 'total' => 1000]);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public function test_duplicate_registration_returns_409(): void
|
|
232
|
+
{
|
|
233
|
+
User::factory()->create(['email' => 'taro@example.com']);
|
|
234
|
+
$this->postJson('/api/users', ['email' => 'taro@example.com'])
|
|
235
|
+
->assertStatus(409);
|
|
236
|
+
$this->assertDatabaseCount('users', 1); // also confirm no extra row was created
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Note: pin the connection in `phpunit.xml` to the test database. Don't swap in SQLite's `:memory:` — that reintroduces the trap described in 3.3. If production runs MySQL, test against a MySQL container.
|
|
242
|
+
|
|
243
|
+
### 5.6 CakePHP + PHPUnit
|
|
244
|
+
|
|
245
|
+
```php
|
|
246
|
+
// Good: manage tables and data via fixtures against the `test` connection
|
|
247
|
+
class OrdersTableTest extends TestCase
|
|
248
|
+
{
|
|
249
|
+
protected array $fixtures = ['app.Orders', 'app.Users']; // loaded into the test connection
|
|
250
|
+
|
|
251
|
+
public function test_total_aggregation_query_is_correct(): void
|
|
252
|
+
{
|
|
253
|
+
$orders = $this->getTableLocator()->get('Orders');
|
|
254
|
+
$total = $orders->find()->where(['user_id' => 1])->sumOf('total');
|
|
255
|
+
$this->assertSame(3000, $total); // expected value is derived from the fixture data
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Define a dedicated database under `Datasources.test` in `config/app_local.php`. It's worth verifying in your test bootstrap that this isn't accidentally pointing at the same database as the default connection.
|
|
261
|
+
|
|
262
|
+
## 6. Running in CI
|
|
263
|
+
|
|
264
|
+
- **Starting the DB container**: on GitHub Actions, either define it under `services:` (PostgreSQL/MySQL) or run Testcontainers directly (requires a runner with Docker available). Wait for a health check (e.g. `pg_isready`) before starting the tests.
|
|
265
|
+
- **Applying migrations**: always run real migrations before the test suite. Relying on `synchronize` or schema auto-generation means you can't detect missing migrations.
|
|
266
|
+
- **DB isolation under parallel execution**: assign each worker its own database. For Jest, suffix the DB name with `JEST_WORKER_ID`; for pytest-xdist, split by worker ID such as `gw0` (pytest-django appends a suffix automatically). If you can't isolate databases, fall back to serial execution (e.g. `--runInBand`).
|
|
267
|
+
- **Debugging CI-only failures**: if a test only fails in CI, suspect insufficient container-startup waiting, database sharing under parallel execution, or timezone/locale differences.
|
|
268
|
+
|
|
269
|
+
## 7. Related
|
|
270
|
+
|
|
271
|
+
- Overall test-layer picture and how to write E2E tests: the e2e-testing skill
|
|
272
|
+
- Measuring coverage and finding gaps: `/test-coverage`
|
|
273
|
+
- Reviewing test code: the code-reviewer subagent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-support-agent/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33-beta.0",
|
|
4
4
|
"description": "AI Support Agent CLI client",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
32
|
"dev": "node --env-file-if-exists=.env --require ts-node/register src/index.ts",
|
|
33
|
-
"build": "tsc && cp -r src/locales dist/locales",
|
|
33
|
+
"build": "tsc && cp -r src/locales dist/locales && cp -r src/plugin dist/plugin && chmod +x dist/plugin/hooks/scripts/*.sh",
|
|
34
34
|
"prepublishOnly": "npm run build && npm test",
|
|
35
35
|
"test": "jest",
|
|
36
36
|
"test:cov": "jest --coverage",
|