@mrciphersmith/keryx 0.3.0 → 0.3.2
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/README.md +4 -1
- package/dist/cli.js +13362 -7078
- package/dist/core.js +11706 -11330
- package/package.json +1 -1
- package/src/gdskills/bundled/agents/go-code-auditor.md +1 -1
- package/src/gdskills/bundled/agents/python-code-auditor.md +1 -1
- package/src/gdskills/bundled/install-manifest.json +271 -4
- package/src/gdskills/bundled/rules/core/model-selection.mdc +51 -0
- package/src/gdskills/bundled/skills/review/review-jev-comments/SKILL.md +184 -0
- package/src/gdskills/bundled/skills/review/review-jev-docs/SKILL.md +189 -0
- package/src/gdskills/bundled/skills/review/review-jev-risk/SKILL.md +190 -0
- package/src/gdskills/bundled/skills/review/review-jev-rules/SKILL.md +267 -0
- package/src/gdskills/bundled/skills/review/review-jev-scenarios/SKILL.md +187 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.detail.md +39 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +1 -1
- package/src/gdskills/bundled/stacks/angular/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/angular/governance/eval.json +1751 -0
- package/src/gdskills/bundled/stacks/angular/governance/scout.json +32 -0
- package/src/gdskills/bundled/stacks/angular/pack.json +55 -0
- package/src/gdskills/bundled/stacks/angular/rules/coding-style.mdc +82 -0
- package/src/gdskills/bundled/stacks/angular/rules/patterns.mdc +84 -0
- package/src/gdskills/bundled/stacks/angular/rules/security.mdc +70 -0
- package/src/gdskills/bundled/stacks/angular/rules/testing.mdc +73 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-build-fix/SKILL.md +127 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-build-fix/evals.json +72 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-code-review/SKILL.md +98 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-code-review/evals.json +73 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-implementation/SKILL.md +112 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-implementation/evals.json +74 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-testing/SKILL.md +102 -0
- package/src/gdskills/bundled/stacks/angular/skills/angular-testing/evals.json +71 -0
- package/src/gdskills/bundled/stacks/mobx/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/mobx/governance/eval.json +904 -0
- package/src/gdskills/bundled/stacks/mobx/governance/scout.json +18 -0
- package/src/gdskills/bundled/stacks/mobx/pack.json +28 -0
- package/src/gdskills/bundled/stacks/mobx/rules/coding-style.mdc +91 -0
- package/src/gdskills/bundled/stacks/mobx/rules/patterns.mdc +122 -0
- package/src/gdskills/bundled/stacks/mobx/rules/security.mdc +56 -0
- package/src/gdskills/bundled/stacks/mobx/rules/testing.mdc +63 -0
- package/src/gdskills/bundled/stacks/mobx/skills/mobx-observable-testing/SKILL.md +124 -0
- package/src/gdskills/bundled/stacks/mobx/skills/mobx-observable-testing/evals.json +73 -0
- package/src/gdskills/bundled/stacks/mobx/skills/mobx-store-implementation/SKILL.md +149 -0
- package/src/gdskills/bundled/stacks/mobx/skills/mobx-store-implementation/evals.json +74 -0
- package/src/gdskills/bundled/stacks/nestjs/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/nestjs/governance/eval.json +1308 -0
- package/src/gdskills/bundled/stacks/nestjs/governance/scout.json +34 -0
- package/src/gdskills/bundled/stacks/nestjs/pack.json +53 -0
- package/src/gdskills/bundled/stacks/nestjs/rules/coding-style.mdc +70 -0
- package/src/gdskills/bundled/stacks/nestjs/rules/patterns.mdc +83 -0
- package/src/gdskills/bundled/stacks/nestjs/rules/security.mdc +73 -0
- package/src/gdskills/bundled/stacks/nestjs/rules/testing.mdc +69 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-build-fix/SKILL.md +157 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-build-fix/evals.json +70 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-implementation/SKILL.md +129 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-implementation/evals.json +71 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-testing/SKILL.md +143 -0
- package/src/gdskills/bundled/stacks/nestjs/skills/nestjs-testing/evals.json +69 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/governance/eval.json +2413 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/governance/scout.json +42 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/pack.json +42 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/coding-style.mdc +69 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/patterns.mdc +88 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/security.mdc +72 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/rules/testing.mdc +64 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-build-fix/SKILL.md +147 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-build-fix/evals.json +75 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-code-review/SKILL.md +118 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-code-review/evals.json +76 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-implementation/SKILL.md +135 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-implementation/evals.json +78 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-testing/SKILL.md +116 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-testing/evals.json +75 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-upgrade-migration/SKILL.md +134 -0
- package/src/gdskills/bundled/stacks/nextjs-nuxt/skills/nextjs-nuxt-upgrade-migration/evals.json +76 -0
- package/src/gdskills/bundled/stacks/vue/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/vue/governance/eval.json +2215 -0
- package/src/gdskills/bundled/stacks/vue/governance/scout.json +42 -0
- package/src/gdskills/bundled/stacks/vue/pack.json +42 -0
- package/src/gdskills/bundled/stacks/vue/rules/coding-style.mdc +73 -0
- package/src/gdskills/bundled/stacks/vue/rules/patterns.mdc +84 -0
- package/src/gdskills/bundled/stacks/vue/rules/security.mdc +60 -0
- package/src/gdskills/bundled/stacks/vue/rules/testing.mdc +69 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-build-fix/SKILL.md +137 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-build-fix/evals.json +72 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-code-review/SKILL.md +120 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-code-review/evals.json +71 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-implementation/SKILL.md +122 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-implementation/evals.json +72 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-testing/SKILL.md +115 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue-testing/evals.json +72 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue2-to-vue3-migration/SKILL.md +135 -0
- package/src/gdskills/bundled/stacks/vue/skills/vue2-to-vue3-migration/evals.json +71 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vue-code-review
|
|
3
|
+
description: "Use when reviewing changed Vue 3 Single File Component or composable code (.vue) for reactivity bugs -- lost reactivity from destructuring reactive()/props, missing watch cleanup, prop mutation instead of emit, v-for key misuse, v-html injection risk, and Pinia store boundary violations. Read-only, no repository convention-doc lookup and no MobX/React store review. Not for authoring or fixing the component (use vue-implementation or vue-build-fix)."
|
|
4
|
+
triggers:
|
|
5
|
+
- "review this vue component diff for reactivity bugs"
|
|
6
|
+
- "check this vue pull request for prop mutation"
|
|
7
|
+
- "does this vue composable leak a watcher"
|
|
8
|
+
- "review this pinia store change"
|
|
9
|
+
- "check for v-html injection in this vue template"
|
|
10
|
+
- "review this vue component's v-for keys"
|
|
11
|
+
metadata:
|
|
12
|
+
origin: authored
|
|
13
|
+
category: review
|
|
14
|
+
version: "1.0.0"
|
|
15
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
16
|
+
license: "MIT"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Vue code review
|
|
20
|
+
|
|
21
|
+
Review changed Vue 3 Single File Component or composable code for
|
|
22
|
+
reactivity and structural bugs. Read-only: report findings, do not edit
|
|
23
|
+
the code under review. See `rules/patterns.mdc` for the reactivity/
|
|
24
|
+
composable rules a diff should follow and `rules/security.mdc` for
|
|
25
|
+
`v-html`/routing risks.
|
|
26
|
+
|
|
27
|
+
## Scope
|
|
28
|
+
|
|
29
|
+
- Reviews `.vue` files and the composables/Pinia stores a changed
|
|
30
|
+
component pulls in.
|
|
31
|
+
- Not for the `<script>`-only TypeScript/Node.js parts of a project with
|
|
32
|
+
no Vue rendering surface (use `nodejs-code-review`), and not for a
|
|
33
|
+
React or MobX store diff.
|
|
34
|
+
- Does not read the repository's own convention docs -- flags what the
|
|
35
|
+
diff itself shows against the rules below, not against local style
|
|
36
|
+
guides.
|
|
37
|
+
|
|
38
|
+
## Workflow
|
|
39
|
+
|
|
40
|
+
### Step 1: Read the diff for reactivity boundary breaks
|
|
41
|
+
|
|
42
|
+
- Any place a `reactive()` object, a Pinia store's state, or (on
|
|
43
|
+
pre-3.5 Vue) `defineProps()`'s return value is destructured into a
|
|
44
|
+
loose local variable and later expected to stay live.
|
|
45
|
+
- Any `watch`/`watchEffect` created outside `setup`/`<script setup>`
|
|
46
|
+
scope (inside a manually managed subscription, an event listener
|
|
47
|
+
registered once at module scope) with no stored stop handle to clean
|
|
48
|
+
it up.
|
|
49
|
+
- A `watch` whose source is broader than what actually changed (a whole
|
|
50
|
+
reactive object with `{ deep: true }` when only one field matters).
|
|
51
|
+
|
|
52
|
+
### Step 2: Check prop/emit and component boundaries
|
|
53
|
+
|
|
54
|
+
- A prop mutated in place inside the component that receives it
|
|
55
|
+
(`props.foo = ...`, `props.items.push(...)`) instead of emitting an
|
|
56
|
+
event for the parent to act on.
|
|
57
|
+
- `defineProps`/`defineEmits` using the untyped runtime-object/string-
|
|
58
|
+
array form where the surrounding file is otherwise TypeScript, losing
|
|
59
|
+
compile-time checking on the component's own public contract.
|
|
60
|
+
- A `v-for` list with no `:key`, or `:key` bound to the array index on a
|
|
61
|
+
list whose order or membership can change (reordering, filtering,
|
|
62
|
+
insertion in the middle) -- index keys silently misattribute component
|
|
63
|
+
state across re-orders.
|
|
64
|
+
|
|
65
|
+
### Step 3: Check security-relevant sinks
|
|
66
|
+
|
|
67
|
+
- `v-html` bound to anything that can carry user-controlled content with
|
|
68
|
+
no sanitization step immediately before the binding.
|
|
69
|
+
- A dynamic `:href`/`:src` bound to user-controlled input with no
|
|
70
|
+
scheme allowlist.
|
|
71
|
+
- A route param (`route.params.*`) used directly in a redirect target,
|
|
72
|
+
query, or `v-html` value with no validation.
|
|
73
|
+
|
|
74
|
+
### Step 4: Check Pinia store boundaries
|
|
75
|
+
|
|
76
|
+
- An action in one store mutating another store's state directly
|
|
77
|
+
instead of calling that store's own exported action.
|
|
78
|
+
- Store state read or mutated from outside any component/setup
|
|
79
|
+
reactivity scope (e.g. a plain module-level side effect) in a way that
|
|
80
|
+
will not trigger the expected reactive updates.
|
|
81
|
+
|
|
82
|
+
### Step 5: Report findings
|
|
83
|
+
|
|
84
|
+
For each finding: file:line, what the diff does, why it is a bug (which
|
|
85
|
+
rule it violates), and the concrete fix -- as a suggestion, not an edit
|
|
86
|
+
you apply yourself.
|
|
87
|
+
|
|
88
|
+
## Rules
|
|
89
|
+
|
|
90
|
+
- Follow `rules/patterns.mdc` for reactivity/composable/store boundary
|
|
91
|
+
rules and `rules/security.mdc` for `v-html`/routing/store-persistence
|
|
92
|
+
risks.
|
|
93
|
+
- Read-only: report findings and suggested fixes; do not edit the
|
|
94
|
+
reviewed files.
|
|
95
|
+
- Do not flag a destructured `defineProps()` on a confirmed Vue 3.5+
|
|
96
|
+
project as broken reactivity -- it is reactive there by design; check
|
|
97
|
+
the `vue` dependency version before flagging this pattern.
|
|
98
|
+
|
|
99
|
+
## Red Flags
|
|
100
|
+
|
|
101
|
+
| Rationalization | Why it is wrong |
|
|
102
|
+
|---|---|
|
|
103
|
+
| "The `v-for` has a `:key`, so the missing-key rule doesn't apply, even though it's `:key=\"index\"`" | An index key on a reorderable/filterable list still causes Vue to misattribute component state across items after a reorder; the rule is about a *stable* key, not merely the presence of one |
|
|
104
|
+
| "This destructured `const { count } = store` looks fine, JS objects are always live" | A plain destructure of Pinia/reactive state captures a snapshot value, not a live binding -- flag it unless `storeToRefs`/`toRefs` wraps it first |
|
|
105
|
+
| "The prop mutation is inside a `watch`, so it's not really 'the component' mutating it" | A `watch` handler runs as part of the component's own reactive scope; mutating `props.x` there is the same violation as doing it in a click handler |
|
|
106
|
+
| "`v-html` is bound to a value from our own API, so it's safe" | Content originating from an API is only as trustworthy as whatever produced it upstream (e.g. user-submitted text stored and echoed back); flag it unless a sanitize step sits immediately before the binding |
|
|
107
|
+
|
|
108
|
+
## Verification
|
|
109
|
+
|
|
110
|
+
Before finishing the review, confirm:
|
|
111
|
+
|
|
112
|
+
- Every reactivity-boundary finding names the exact line and the
|
|
113
|
+
specific pattern violated (not a vague "check reactivity here").
|
|
114
|
+
- Every `v-for`/`v-html`/prop-mutation finding includes the concrete
|
|
115
|
+
fix (the corrected `:key` expression, the sanitize call, or the emit
|
|
116
|
+
to add), not just "this looks wrong".
|
|
117
|
+
- No suggested fix was applied to the files under review -- the skill
|
|
118
|
+
only reports.
|
|
119
|
+
- The report distinguishes a genuine Vue 3.5+ destructured-props usage
|
|
120
|
+
(fine) from a pre-3.5 or reactive()/store destructure (a real bug).
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"triggers": {
|
|
3
|
+
"positive": [
|
|
4
|
+
"can you scan this vue component's diff before it merges for anything that could break reactivity",
|
|
5
|
+
"check this pinia store change for state mutation across stores",
|
|
6
|
+
"does this vue composable's watch() call ever get cleaned up before the component unmounts, or is it leaking",
|
|
7
|
+
"is there an XSS risk from how this vue template renders raw HTML into the DOM",
|
|
8
|
+
"in this pull request's diff, take a look at whether this vue component's list rendering picks safe keys",
|
|
9
|
+
"review this vue diff for prop mutation instead of emit"
|
|
10
|
+
],
|
|
11
|
+
"negative": [
|
|
12
|
+
"review this react component diff for hook bugs",
|
|
13
|
+
"build a new vue component with a pinia store",
|
|
14
|
+
"fix this vue-tsc build error in this component",
|
|
15
|
+
"write a vitest test for this vue component",
|
|
16
|
+
"review this node.js service diff for floating promises",
|
|
17
|
+
"review this mobx store diff for action boundary violations"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"scenarios": [
|
|
21
|
+
{
|
|
22
|
+
"id": "index-key-on-reorderable-list",
|
|
23
|
+
"prompt": "Review this part of a Vue component diff:\n```vue\n<li v-for=\"(task, index) in tasks\" :key=\"index\">{{ task.title }}</li>\n```\nThe list supports drag-to-reorder and delete-in-the-middle. Any issue?",
|
|
24
|
+
"strictness": "high",
|
|
25
|
+
"expected_behavior": [
|
|
26
|
+
{ "grader": "regex", "value": ":key" },
|
|
27
|
+
{
|
|
28
|
+
"grader": "judge",
|
|
29
|
+
"rubric": "A correct review flags that keying by array index on a list that can reorder or delete from the middle causes Vue to misattribute per-item state across re-renders, and recommends keying by a stable identifier from the task itself instead.",
|
|
30
|
+
"pass_criteria": [
|
|
31
|
+
"Identifies that `:key=\"index\"` is unsafe specifically because the list reorders/deletes in the middle, not merely that a key is present",
|
|
32
|
+
"Names the concrete fix: key by a stable identifier from the item, e.g. `:key=\"task.id\"`"
|
|
33
|
+
],
|
|
34
|
+
"fail_criteria": [
|
|
35
|
+
"Concludes the key is fine because a `:key` binding exists at all, without addressing that an index key breaks under reorder/delete-in-the-middle"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"calibration": {
|
|
40
|
+
"known_right": "Flag this: `:key=\"index\"` is a problem here specifically because the list supports drag-to-reorder and delete-in-the-middle. Vue uses the key to decide which existing DOM node/component instance to reuse across a re-render; when the key is the array index, reordering or removing a middle item shifts every subsequent item's index, so Vue reuses the wrong node for the wrong task -- component-local state (an open edit field, a checked checkbox, an input's value) can end up attached to the wrong row. Fix: key by a stable id that travels with the task, not its position:\n```vue\n<li v-for=\"task in tasks\" :key=\"task.id\">{{ task.title }}</li>\n```\n(dropped `index` from the loop var since it's unused now). If `task` genuinely has no stable id, that's the actual bug to fix upstream -- generate one when the task is created, don't fall back to index.",
|
|
41
|
+
"known_wrong": "This is fine -- there's already a `:key` bound on the `<li>`, which is exactly what Vue's v-for warning asks for. The list has a key, so this satisfies the requirement.",
|
|
42
|
+
"vague": "Using the index as a key can cause issues with dynamic lists, might be worth using something else.",
|
|
43
|
+
"subtle_wrong": "Worth a second look, but since `task.title` is already unique across the list in practice, I'd key on that instead of the index: `:key=\"task.title\"`. That avoids the index issue and doesn't require adding an id field to the data."
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "action-mutates-other-store-state-directly",
|
|
48
|
+
"prompt": "Review this Pinia action:\n```ts\n// in useCartStore\nfunction checkout() {\n const userStore = useUserStore()\n userStore.recentOrders.push(buildOrder(items.value))\n items.value = []\n}\n```\nAny concerns?",
|
|
49
|
+
"strictness": "high",
|
|
50
|
+
"expected_behavior": [
|
|
51
|
+
{
|
|
52
|
+
"grader": "judge",
|
|
53
|
+
"rubric": "A correct review flags that the cart store's action reaches into another store (userStore) and mutates its state array directly instead of calling a dedicated action on that store, and recommends adding/calling a userStore action (e.g. addRecentOrder) instead of pushing into its state from outside.",
|
|
54
|
+
"pass_criteria": [
|
|
55
|
+
"Identifies that `userStore.recentOrders.push(...)` mutates another store's state directly from outside that store, bypassing its own action layer",
|
|
56
|
+
"Recommends the concrete fix: call (or add) a userStore action such as `userStore.addRecentOrder(order)` instead of pushing into its state array from the cart store"
|
|
57
|
+
],
|
|
58
|
+
"fail_criteria": [
|
|
59
|
+
"Treats the direct cross-store state mutation as acceptable because both stores are part of the same app/Pinia instance"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"calibration": {
|
|
64
|
+
"known_right": "Flag `userStore.recentOrders.push(buildOrder(items.value))`: this mutates `userStore`'s state array directly from inside `useCartStore`'s `checkout` action, bypassing `userStore`'s own action layer entirely. Now there are two places `recentOrders` can change -- through `userStore`'s own actions, and through any other store that happens to reach in like this -- which makes it impossible to reason about where a change to that state actually came from, and any validation/side effect `userStore` might want on adding an order gets skipped here. Fix: give `userStore` an action for this and call it instead:\n```ts\n// in useUserStore\nfunction addRecentOrder(order: Order) {\n recentOrders.push(order)\n}\n\n// in useCartStore.checkout\nconst userStore = useUserStore()\nuserStore.addRecentOrder(buildOrder(items.value))\nitems.value = []\n```\nNow `userStore` owns every mutation to its own state through one discoverable entry point.",
|
|
65
|
+
"known_wrong": "No concern -- both stores are part of the same Pinia instance in this app, so one store reaching into another's reactive state and pushing to it is just normal cross-store communication; Pinia doesn't enforce store boundaries anyway.",
|
|
66
|
+
"vague": "Might be better for the cart store to not touch the user store's data directly, worth reconsidering that.",
|
|
67
|
+
"subtle_wrong": "I'd tighten this slightly by having the cart store read `userStore.recentOrders` through a computed instead of accessing it live, but the push itself from `checkout` is a reasonable enough way to record the order without adding a whole new action just for this."
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vue-implementation
|
|
3
|
+
description: "Use when building or changing a Vue 3 Single File Component, composable, or Pinia store -- covers <script setup> with type-based defineProps/defineEmits, choosing ref vs reactive, computed vs watch/watchEffect, composable extraction, and Pinia store/action design. Not for Options API legacy components (use vue2-to-vue3-migration) or Node.js service/CLI code with no .vue file (use nodejs-implementation)."
|
|
4
|
+
triggers:
|
|
5
|
+
- "build a vue component"
|
|
6
|
+
- "add a composable to this vue app"
|
|
7
|
+
- "wire up a pinia store"
|
|
8
|
+
- "add props and emits to this vue component"
|
|
9
|
+
- "should this be ref or reactive"
|
|
10
|
+
- "implement this vue form with v-model"
|
|
11
|
+
- "extract this logic into a use composable"
|
|
12
|
+
metadata:
|
|
13
|
+
origin: authored
|
|
14
|
+
category: implement
|
|
15
|
+
version: "1.0.0"
|
|
16
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
17
|
+
license: "MIT"
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Vue implementation (Composition API)
|
|
21
|
+
|
|
22
|
+
Build or change a Vue 3 Single File Component, composable, or Pinia store
|
|
23
|
+
using the Composition API. See `rules/coding-style.mdc` for `<script
|
|
24
|
+
setup>` typing conventions, `rules/patterns.mdc` for reactivity and
|
|
25
|
+
composable design, and `rules/security.mdc` for `v-html`/routing risks a
|
|
26
|
+
new component must avoid.
|
|
27
|
+
|
|
28
|
+
## Workflow
|
|
29
|
+
|
|
30
|
+
### Step 1: Discover the project's own conventions
|
|
31
|
+
|
|
32
|
+
Before writing anything, check:
|
|
33
|
+
|
|
34
|
+
- Does the project use `<script setup lang="ts">` everywhere, or does it
|
|
35
|
+
still have Options API components? Match the file's neighbors in the
|
|
36
|
+
same directory, not a mix.
|
|
37
|
+
- Is state management Pinia, Vuex, or ad hoc `provide`/`inject` +
|
|
38
|
+
composables? Check `package.json` and an existing store file.
|
|
39
|
+
- Is the project on Vue 3.5+ (destructured `defineProps()` stays
|
|
40
|
+
reactive) or an earlier 3.x (destructuring breaks reactivity)? Check
|
|
41
|
+
`package.json`'s `vue` dependency version before choosing which prop
|
|
42
|
+
access style to write.
|
|
43
|
+
- Is `eslint-plugin-vue` configured, and with which rule preset
|
|
44
|
+
(`recommended`, `strongly-recommended`)? Its rule set decides template
|
|
45
|
+
formatting details (self-closing tags, attribute order) worth matching.
|
|
46
|
+
|
|
47
|
+
### Step 2: Design the component/composable boundary
|
|
48
|
+
|
|
49
|
+
- A piece of state or behavior used by more than one component becomes a
|
|
50
|
+
composable (`useX()`), not copy-pasted `ref`/`watch` blocks.
|
|
51
|
+
- State shared across unrelated branches of the component tree (not just
|
|
52
|
+
parent-to-child) is a Pinia store, not `provide`/`inject` bolted onto an
|
|
53
|
+
unrelated ancestor.
|
|
54
|
+
- A component that only renders based on props and emits events, with no
|
|
55
|
+
store/composable dependency of its own, stays "dumb" -- push data
|
|
56
|
+
fetching and derived state up into a parent or a composable it calls.
|
|
57
|
+
|
|
58
|
+
### Step 3: Declare props, emits, and reactive state
|
|
59
|
+
|
|
60
|
+
- Type-only `defineProps<{ ... }>()` / `defineEmits<{ ... }>()`, not the
|
|
61
|
+
runtime-object form -- see `rules/coding-style.mdc`.
|
|
62
|
+
- Choose `ref()` for a value reassigned wholesale, `reactive()` for a
|
|
63
|
+
fixed-shape object mutated in place -- see `rules/patterns.mdc`. Never
|
|
64
|
+
destructure a `reactive()` object or a pre-3.5 props object into loose
|
|
65
|
+
variables; that breaks the reactive link.
|
|
66
|
+
- Choose `computed()` for a pure re-derivation, `watch`/`watchEffect`
|
|
67
|
+
only for an actual side effect (fetch, DOM measurement, external sync).
|
|
68
|
+
|
|
69
|
+
### Step 4: Wire the store (when the feature needs one)
|
|
70
|
+
|
|
71
|
+
- Define Pinia state, getters, and actions in `defineStore()`; keep
|
|
72
|
+
mutation inside the store's own actions -- a component calls an action,
|
|
73
|
+
it does not reach in and mutate `store.someField` directly from outside.
|
|
74
|
+
- One store owns one another store's state through that store's own
|
|
75
|
+
actions, not by importing and mutating it directly.
|
|
76
|
+
|
|
77
|
+
### Step 5: Verify
|
|
78
|
+
|
|
79
|
+
Run the project's own scripts (they may wrap these with extra flags):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx vue-tsc --noEmit
|
|
83
|
+
npx eslint .
|
|
84
|
+
npm test
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Confirm the new component renders without a runtime prop-mutation warning
|
|
88
|
+
and that events fire with the expected payload (check in a quick manual
|
|
89
|
+
mount or the existing dev server) before calling the feature done.
|
|
90
|
+
|
|
91
|
+
## Rules
|
|
92
|
+
|
|
93
|
+
- Follow `rules/coding-style.mdc` for `<script setup>`/prop/emit typing.
|
|
94
|
+
- Follow `rules/patterns.mdc` for `ref`/`reactive` choice, composable
|
|
95
|
+
extraction, and `provide`/`inject` vs. Pinia store decisions.
|
|
96
|
+
- Follow `rules/security.mdc` before adding any `v-html` binding or a
|
|
97
|
+
dynamic `:href`/`:src` from user-controlled input.
|
|
98
|
+
- NEVER mutate a prop in place inside the child; emit an event
|
|
99
|
+
(`emit('update:modelValue', v)`) and let the parent own the state.
|
|
100
|
+
- NEVER destructure a `reactive()` object into loose variables expecting
|
|
101
|
+
them to stay reactive.
|
|
102
|
+
|
|
103
|
+
## Red Flags
|
|
104
|
+
|
|
105
|
+
| Rationalization | Why it is wrong |
|
|
106
|
+
|---|---|
|
|
107
|
+
| "I'll just mutate `props.items.push(...)` directly, it's simpler than emitting" | Vue warns at runtime; the parent's data silently drifts from what it thinks it owns, and the mutation is invisible to anyone reading the parent |
|
|
108
|
+
| "I'll destructure `const { count } = state` from this `reactive()` object for convenience" | `count` is now a frozen copy at destructure time; later mutations to `state.count` never update it, producing a component that silently stops re-rendering |
|
|
109
|
+
| "I'll skip the composable and just copy this fetch+loading+error block into the second component" | Two independent copies of the same stateful logic drift apart the next time either one is bugfixed |
|
|
110
|
+
| "`watch(props, ...)` with `deep: true` is easier than picking the one field I actually need" | A whole-object deep watch fires on every unrelated field change and is expensive on any non-trivial object |
|
|
111
|
+
|
|
112
|
+
## Verification
|
|
113
|
+
|
|
114
|
+
Do not report the feature done until all of the following hold:
|
|
115
|
+
|
|
116
|
+
- `vue-tsc --noEmit` (or the project's own type-check script) exits 0.
|
|
117
|
+
- `eslint .` exits 0 with no new `eslint-disable` added.
|
|
118
|
+
- No prop is mutated in place; every parent-visible change goes through
|
|
119
|
+
an emitted event or a store action.
|
|
120
|
+
- Every `defineProps`/`defineEmits` is the type-only form, not the
|
|
121
|
+
runtime-object form, unless the surrounding file is still Options API.
|
|
122
|
+
- `git status` shows changes confined to the files the feature required.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"triggers": {
|
|
3
|
+
"positive": [
|
|
4
|
+
"I need a new vue single file component that takes some typed props from its parent and emits an event back up when something happens",
|
|
5
|
+
"add a pinia store action to update the cart",
|
|
6
|
+
"for this piece of local state in a vue component, is ref() or reactive() the better call here",
|
|
7
|
+
"two of my vue components need the same piece of reactive logic -- how do I pull it out into a composable so they can both use it",
|
|
8
|
+
"I want typing into this vue form's input to update the parent component's data automatically through v-model -- how do I wire that up",
|
|
9
|
+
"implement this vue 3 script setup component"
|
|
10
|
+
],
|
|
11
|
+
"negative": [
|
|
12
|
+
"write a react component with props and state",
|
|
13
|
+
"fix this vue-tsc type error blocking the build",
|
|
14
|
+
"review this vue component diff for reactivity bugs",
|
|
15
|
+
"write a vitest test for this vue component",
|
|
16
|
+
"migrate this vue 2 options api component to vue 3",
|
|
17
|
+
"implement this node.js CLI command"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"scenarios": [
|
|
21
|
+
{
|
|
22
|
+
"id": "prop-mutation-vs-emit",
|
|
23
|
+
"prompt": "A Vue child component has an 'items' prop for a todo list. A button handler currently does `props.items.push(newItem)` to add an item. What should this do instead so the parent's data stays in sync?",
|
|
24
|
+
"strictness": "high",
|
|
25
|
+
"expected_behavior": [
|
|
26
|
+
{
|
|
27
|
+
"grader": "judge",
|
|
28
|
+
"rubric": "A correct answer explains that props are one-way and must not be mutated in the child (e.g. via `props.items.push(...)`), and instead emits an event so the parent owns the state change -- naming the defineEmits declaration and the emit() call with the new item as payload.",
|
|
29
|
+
"pass_criteria": [
|
|
30
|
+
"Explicitly says not to mutate the prop directly (e.g. `props.items.push(...)`) since props flow one-way from parent to child",
|
|
31
|
+
"Shows the fix as code: a defineEmits<{...}>() declaration for an event (e.g. 'add-item' or 'update:items') and the emit(...) call passing the new item, not just a description of emitting something"
|
|
32
|
+
],
|
|
33
|
+
"fail_criteria": [
|
|
34
|
+
"Recommends keeping `props.items.push(newItem)` (or an equivalent direct mutation of the prop) as the actual fix. Naming `props.items.push` only to explain why it is wrong does not count as this failure."
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"calibration": {
|
|
39
|
+
"known_right": "Don't call `props.items.push(newItem)` -- props flow one-way from parent to child, and mutating the array in place changes the parent's underlying data through a reference the child was never supposed to write to, which Vue also warns about at runtime. Instead, declare a typed emit and let the parent decide how to update its own state:\n\n```ts\nconst emit = defineEmits<{\n 'add-item': [item: TodoItem]\n}>()\n\nfunction onAdd(newItem: TodoItem) {\n emit('add-item', newItem)\n}\n```\n\nIn the parent, listen for `@add-item` and push into its own `items` ref/reactive array there. Now the child never touches the prop directly, and the parent has one clear place where the list actually changes.",
|
|
40
|
+
"known_wrong": "Easiest fix: just keep doing `props.items.push(newItem)` inside the click handler. Since `items` is an array, JavaScript objects are passed by reference, so pushing into it from the child updates the same array the parent has -- it already works, no need to add an emit for something this simple.",
|
|
41
|
+
"vague": "Don't mutate the prop directly, emit an event instead so the parent controls the data update.",
|
|
42
|
+
"subtle_wrong": "Instead of mutating the prop, I'd clone it into local state so the child owns its own copy: `const localItems = ref([...props.items]); function onAdd(newItem) { localItems.value.push(newItem) }`, then render from `localItems` in the template. This avoids touching the prop directly and keeps the child's list working, and the parent can re-sync later if it needs to."
|
|
43
|
+
},
|
|
44
|
+
"anti_patterns": ["props.items.push"]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "reactive-destructure-breaks-tracking",
|
|
48
|
+
"prompt": "I destructured a Pinia store's reactive state like `const { count } = store` inside a component so I don't have to write `store.count` everywhere. The template stopped updating when count changes elsewhere. What's wrong and how do I fix it?",
|
|
49
|
+
"strictness": "high",
|
|
50
|
+
"expected_behavior": [
|
|
51
|
+
{
|
|
52
|
+
"grader": "judge",
|
|
53
|
+
"rubric": "A correct answer identifies that destructuring a Pinia store's reactive state directly captures a frozen snapshot value at that moment, breaking the reactive connection, and fixes it with storeToRefs(store) (or toRefs()/toRef() for a plain reactive object) before destructuring.",
|
|
54
|
+
"pass_criteria": [
|
|
55
|
+
"Identifies that destructuring the reactive state directly copies a snapshot value, breaking the reactive connection to the store",
|
|
56
|
+
"Names the concrete fix as code: `storeToRefs(store)` for a Pinia store (or `toRefs()`/`toRef()` for a plain reactive object) used before destructuring, e.g. `const { count } = storeToRefs(store)`"
|
|
57
|
+
],
|
|
58
|
+
"fail_criteria": [
|
|
59
|
+
"Concludes that plain destructuring of the store's state should already work and looks for the bug elsewhere (e.g. a re-fetch or a missing watcher) instead of identifying the destructuring itself as the cause",
|
|
60
|
+
"Proposes a fix that does not use storeToRefs/toRefs/toRef at all, such as only wrapping the read in a manual watch-and-copy workaround"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"calibration": {
|
|
65
|
+
"known_right": "The problem is the destructuring itself: `const { count } = store` reads `store.count`'s current value once and assigns it to a plain local variable -- it's a snapshot, not a live binding, so later changes to `store.count` never touch your local `count`. Pinia's reactive state only stays reactive through the store object itself (or through a ref/computed derived from it), not through a plain destructured value.\n\nFix it with `storeToRefs`, which converts the store's reactive properties into individual refs before you destructure them:\n\n```ts\nimport { storeToRefs } from 'pinia'\n\nconst store = useCounterStore()\nconst { count } = storeToRefs(store)\n```\n\nNow `count` is a `Ref<number>` that stays linked to `store.count`, so the template updates when it changes elsewhere. (Actions like `store.increment` stay destructured directly from `store`, not through `storeToRefs` -- functions don't need reactive wrapping.)",
|
|
66
|
+
"known_wrong": "That should already work -- JavaScript destructuring just reads the current property values, and since Pinia stores are reactive objects, `count` should update along with `store.count` automatically. If the template isn't updating, it's probably not related to the destructuring at all; check whether the action that changes `count` elsewhere is actually being called, or whether the component is even re-rendering.",
|
|
67
|
+
"vague": "The destructuring probably broke reactivity somewhere. Try to keep it wired to the store more directly instead of pulling it out as a plain variable.",
|
|
68
|
+
"subtle_wrong": "Rather than changing how you read `count`, keep a local ref synced manually with a watcher: `const count = ref(store.count); watch(() => store.count, (v) => { count.value = v })`. That way `count` stays a reactive ref in the component and updates whenever the store's value changes, without needing to change the original destructuring line."
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vue-testing
|
|
3
|
+
description: "Use when a Vue 3 component or composable's test suite needs writing, extending, or fixing with Vue Test Utils and Vitest -- covers mount vs. shallowMount, querying by test id/role, awaiting trigger()/nextTick for async DOM updates, asserting wrapper.emitted() payloads, and Pinia test-store isolation with createTestingPinia. Not for plain Node.js/TypeScript unit tests with no .vue mount (use nodejs-testing) or React Testing Library tests (use react-testing)."
|
|
4
|
+
triggers:
|
|
5
|
+
- "write a vitest test for this vue component"
|
|
6
|
+
- "test this vue composable"
|
|
7
|
+
- "fix this failing vue test utils test"
|
|
8
|
+
- "mock the api call in this vue component test"
|
|
9
|
+
- "assert the emitted event payload in this test"
|
|
10
|
+
- "test this pinia store"
|
|
11
|
+
metadata:
|
|
12
|
+
origin: authored
|
|
13
|
+
category: test
|
|
14
|
+
version: "1.0.0"
|
|
15
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
16
|
+
license: "MIT"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Vue testing (Vue Test Utils / Vitest)
|
|
20
|
+
|
|
21
|
+
Write, extend, or fix a Vue 3 component or composable test using Vue Test
|
|
22
|
+
Utils and Vitest. See `rules/testing.mdc` for the layout, querying, and
|
|
23
|
+
mocking conventions a correct test follows.
|
|
24
|
+
|
|
25
|
+
## Workflow
|
|
26
|
+
|
|
27
|
+
### Step 1: Discover the project's own test setup
|
|
28
|
+
|
|
29
|
+
- Read an existing `*.spec.ts`/`*.test.ts` next to a `.vue` file in the
|
|
30
|
+
same directory (or the project's shared `__tests__` folder) to match
|
|
31
|
+
its layout, its DOM environment (`happy-dom` vs `jsdom`), and whether it
|
|
32
|
+
uses raw Vue Test Utils assertions or `@testing-library/vue` on top.
|
|
33
|
+
- Check whether the project uses Pinia and, if so, whether
|
|
34
|
+
`@pinia/testing`'s `createTestingPinia()` is already a dependency --
|
|
35
|
+
use it if present rather than hand-rolling store setup.
|
|
36
|
+
- Check the project's network-mocking convention (MSW, a mocked
|
|
37
|
+
service/composable module, or `vi.mock()` on the fetch wrapper) and
|
|
38
|
+
mock at that same boundary.
|
|
39
|
+
|
|
40
|
+
### Step 2: Choose mount depth and render the component
|
|
41
|
+
|
|
42
|
+
- `mount()` when the test cares about the full render tree, including
|
|
43
|
+
child components' own output.
|
|
44
|
+
- `shallowMount()` only when child components are irrelevant noise for
|
|
45
|
+
this test and stubbing them meaningfully clarifies the assertions.
|
|
46
|
+
- Pass every prop the assertions actually depend on explicitly through
|
|
47
|
+
`mount(Component, { props: { ... } })` -- do not lean on a default
|
|
48
|
+
silently covering a value the test cares about.
|
|
49
|
+
|
|
50
|
+
### Step 3: Interact and wait for reactivity to settle
|
|
51
|
+
|
|
52
|
+
- Trigger interaction with `await wrapper.find(selector).trigger('click')`
|
|
53
|
+
(or `@testing-library/vue`'s `fireEvent`) -- always `await` it, since
|
|
54
|
+
Vue's DOM updates are asynchronous (`flush: 'pre'` by default).
|
|
55
|
+
- After an async composable/store call the interaction kicks off, `await
|
|
56
|
+
flushPromises()` (or the project's existing helper) plus `await
|
|
57
|
+
nextTick()` before asserting on DOM that depends on it -- never an
|
|
58
|
+
arbitrary `setTimeout`.
|
|
59
|
+
|
|
60
|
+
### Step 4: Assert behavior, not internals
|
|
61
|
+
|
|
62
|
+
- Assert on rendered text/attributes and `wrapper.emitted('eventName')`
|
|
63
|
+
payloads, not on `wrapper.vm`'s internal reactive state -- `vm` access
|
|
64
|
+
couples the test to implementation details that can change without
|
|
65
|
+
changing behavior.
|
|
66
|
+
- When asserting an emitted event, check the payload array
|
|
67
|
+
(`wrapper.emitted('update')![0]`), not just that the event key exists --
|
|
68
|
+
a wrong payload with the right event name is still a real bug.
|
|
69
|
+
|
|
70
|
+
### Step 5: Verify
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx vitest run <path-to-spec>
|
|
74
|
+
npx vitest run
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Run the full suite once the target file passes, to confirm the change did
|
|
78
|
+
not affect an unrelated test through shared Pinia/module state.
|
|
79
|
+
|
|
80
|
+
## Rules
|
|
81
|
+
|
|
82
|
+
- Follow `rules/testing.mdc` for layout, querying, Pinia isolation, and
|
|
83
|
+
mocking boundaries.
|
|
84
|
+
- ALWAYS `await` a `trigger()`/`fireEvent` call and any subsequent DOM
|
|
85
|
+
assertion that depends on it.
|
|
86
|
+
- ALWAYS give a component under test that reads a Pinia store a fresh
|
|
87
|
+
test store instance (`createTestingPinia()` or a fresh `createPinia()` +
|
|
88
|
+
`setActivePinia()`) -- never reuse one Pinia instance across tests.
|
|
89
|
+
- NEVER delete or loosen a failing assertion to make the suite green;
|
|
90
|
+
fix the component if behavior regressed, or fix the assertion if it
|
|
91
|
+
was asserting stale behavior, and say which.
|
|
92
|
+
- NEVER assert through `wrapper.vm.someInternalRef` when the same fact is
|
|
93
|
+
observable through rendered output or an emitted event.
|
|
94
|
+
|
|
95
|
+
## Red Flags
|
|
96
|
+
|
|
97
|
+
| Rationalization | Why it is wrong |
|
|
98
|
+
|---|---|
|
|
99
|
+
| "I'll skip the `await` on `trigger()`, the click is synchronous anyway" | Vue batches DOM updates asynchronously by default; the assertion right after an un-awaited trigger reads stale DOM and the test can pass for the wrong reason |
|
|
100
|
+
| "I'll just check `wrapper.emitted('update')` exists, not the payload" | A component that emits the right event with the wrong value still passes this assertion, hiding a real bug |
|
|
101
|
+
| "This test keeps failing on Pinia state from the previous test, I'll just reset it inside this one test" | The root cause is a shared store instance across tests; reset the store per-test globally (`beforeEach`) or use `createTestingPinia()`, not a one-off patch in the test that happened to notice |
|
|
102
|
+
| "I'll assert `wrapper.vm.count` directly, it's faster than finding the rendered text" | Couples the test to an internal variable name; a refactor that keeps behavior identical but renames the ref breaks the test for no real reason |
|
|
103
|
+
|
|
104
|
+
## Verification
|
|
105
|
+
|
|
106
|
+
Do not report the test done until all of the following hold:
|
|
107
|
+
|
|
108
|
+
- `npx vitest run` for the target file (and the full suite) exits 0.
|
|
109
|
+
- Every `trigger()`/`fireEvent` call is `await`ed, and no arbitrary
|
|
110
|
+
`setTimeout` was used to wait for a DOM update.
|
|
111
|
+
- Every emitted-event assertion checks the payload, not just presence.
|
|
112
|
+
- A component reading a Pinia store gets its own fresh store instance in
|
|
113
|
+
this test, not one shared with another test file.
|
|
114
|
+
- `git status` shows changes confined to the test file(s) the fix or new
|
|
115
|
+
coverage required.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"triggers": {
|
|
3
|
+
"positive": [
|
|
4
|
+
"write a vue test utils spec that clicks this component's button and asserts the result",
|
|
5
|
+
"I want a vitest spec that verifies the values this vue composable returns are correct",
|
|
6
|
+
"debug why this vue test utils test fails intermittently",
|
|
7
|
+
"assert what payload this vue component's update event actually carries in its vue test utils spec",
|
|
8
|
+
"this vue component's vue test utils wrapper still triggers a real fetch call during its vitest spec -- how do I mock that out",
|
|
9
|
+
"how do I set up a test for this component that depends on a pinia store without hitting the real store"
|
|
10
|
+
],
|
|
11
|
+
"negative": [
|
|
12
|
+
"add React Testing Library coverage for this React component's onClick handler",
|
|
13
|
+
"write vitest tests for this node.js service function",
|
|
14
|
+
"review this vue component diff for bugs",
|
|
15
|
+
"implement this vue composable for fetching orders",
|
|
16
|
+
"fix this vue-tsc build error",
|
|
17
|
+
"migrate this vuex store to pinia"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"scenarios": [
|
|
21
|
+
{
|
|
22
|
+
"id": "await-trigger-before-assert",
|
|
23
|
+
"prompt": "This Vue Test Utils test clicks a button and immediately asserts the counter text updated, but it fails intermittently:\n```ts\nwrapper.find('button').trigger('click')\nexpect(wrapper.find('span').text()).toBe('1')\n```\nWhat's wrong and how do I fix it?",
|
|
24
|
+
"strictness": "high",
|
|
25
|
+
"expected_behavior": [
|
|
26
|
+
{ "grader": "regex", "value": "await" },
|
|
27
|
+
{
|
|
28
|
+
"grader": "judge",
|
|
29
|
+
"rubric": "A correct answer explains that trigger() is asynchronous because Vue batches DOM updates, so the assertion right after an un-awaited trigger() can read stale DOM, and fixes it by awaiting the trigger() call (and, where relevant, nextTick) before asserting.",
|
|
30
|
+
"pass_criteria": [
|
|
31
|
+
"Explains that Vue's reactive DOM updates are asynchronous/batched, so trigger() must be awaited before the DOM reflects the click",
|
|
32
|
+
"Shows the fix as code: `await wrapper.find('button').trigger('click')` before the assertion"
|
|
33
|
+
],
|
|
34
|
+
"fail_criteria": [
|
|
35
|
+
"Attributes the flakiness to something other than the missing await (e.g. a slow CI machine, a random test order) and proposes retries or a fixed setTimeout delay instead of awaiting trigger()"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"calibration": {
|
|
40
|
+
"known_right": "The test is flaky because `trigger('click')` is asynchronous -- Vue Test Utils' `trigger()` returns a promise that resolves after Vue has flushed the resulting reactive DOM update (by default on the 'pre' flush timing). Calling it without `await` fires the click, but the assertion right after can run before the DOM has actually updated, so it sometimes reads the old text and sometimes the new one depending on timing. The fix is to await it:\n\n```ts\nawait wrapper.find('button').trigger('click')\nexpect(wrapper.find('span').text()).toBe('1')\n```\n\nIf the counter update itself depends on an async composable or store call beyond the synchronous reactive update, also await `flushPromises()` before the assertion.",
|
|
41
|
+
"known_wrong": "Test flakiness like this is usually just CI being slow under load -- I'd add a short `await new Promise(r => setTimeout(r, 100))` after the trigger to give it time to settle, or mark the test as retry: 2 in the Vitest config so a transient failure doesn't block the run.",
|
|
42
|
+
"vague": "trigger() is async so you need to wait for it before checking the result.",
|
|
43
|
+
"subtle_wrong": "Since `trigger()` needs time to propagate, I'd wrap the assertion in `await vi.waitFor(() => expect(wrapper.find('span').text()).toBe('1'))` so it polls until the DOM catches up, without changing the `trigger('click')` call itself."
|
|
44
|
+
},
|
|
45
|
+
"anti_patterns": ["setTimeout"]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "assert-emitted-payload-not-presence",
|
|
49
|
+
"prompt": "My component test checks that clicking save fires the 'submit' event: `expect(wrapper.emitted('submit')).toBeTruthy()`. The test passes, but a teammate says it wouldn't catch a bug where the wrong data is submitted. What should the assertion actually check?",
|
|
50
|
+
"strictness": "high",
|
|
51
|
+
"expected_behavior": [
|
|
52
|
+
{
|
|
53
|
+
"grader": "judge",
|
|
54
|
+
"rubric": "A correct answer explains that checking only that the event was emitted proves the event fired but not that it carried the right data, and fixes it by asserting the emitted event's actual payload array.",
|
|
55
|
+
"pass_criteria": [
|
|
56
|
+
"States that `toBeTruthy()` on `wrapper.emitted('submit')` only proves the event fired, not that its payload was correct",
|
|
57
|
+
"Shows the concrete fix: asserting the payload, e.g. `expect(wrapper.emitted('submit')![0]).toEqual([expectedData])`"
|
|
58
|
+
],
|
|
59
|
+
"fail_criteria": [
|
|
60
|
+
"Recommends leaving the assertion as just `toBeTruthy()`/presence-only and relying on a different, unrelated test to cover the payload instead of fixing this assertion"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"calibration": {
|
|
65
|
+
"known_right": "`expect(wrapper.emitted('submit')).toBeTruthy()` only proves the event fired at least once -- it says nothing about what was passed with it. A component that emits `submit` with completely wrong or stale data still passes that check. `wrapper.emitted('eventName')` returns an array of argument arrays, one per emit call, so assert against the actual payload:\n\n```ts\nawait wrapper.find('form').trigger('submit')\nexpect(wrapper.emitted('submit')).toBeTruthy()\nexpect(wrapper.emitted('submit')![0]).toEqual([{ name: 'Ada', email: 'ada@example.com' }])\n```\nNow a bug that emits the right event with the wrong data actually fails the test.",
|
|
66
|
+
"known_wrong": "toBeTruthy() is fine here -- the point of this test is just to confirm the submit flow wires up the event at all. Checking the exact payload is really the job of a separate unit test on the form's own validation logic, not this component test.",
|
|
67
|
+
"vague": "You should check what data was actually emitted, not just that the event happened.",
|
|
68
|
+
"subtle_wrong": "I'd strengthen it by checking the emitted count instead: `expect(wrapper.emitted('submit')!.length).toBe(1)`, so at least we know it only fired once and isn't double-submitting."
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|