@gallopsystems/agent-skills 1.11.0 → 1.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gallopsystems/agent-skills",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "Gallop Systems agent skills, symlinked into .claude/skills (Claude Code) and .agents/skills (Codex) on install.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -51,6 +51,7 @@ EOF
51
51
  - After merge: `git switch main && git pull --ff-only`, clean up `[gone]` branches, start the next branch from fresh main.
52
52
  - One concern per PR — hotfixes and review findings go in separate PRs unless told otherwise.
53
53
  - Stacked PRs: `gh pr create --base <parent-branch>`; after the parent merges, retarget with `gh pr edit <n> --base main` (and see [getting-unstuck.md](getting-unstuck.md) for rebasing onto main after the parent was squash-merged).
54
+ - If you discover uncommitted work on the wrong branch and the PR must be "off main", do not commit to the wrong branch. With a cleanly applicable worktree, `git fetch origin main && git switch -c feat/<short-description> origin/main` carries the unstaged changes onto a new branch from `origin/main`. Verify with `git status` and tests. If checkout would overwrite/conflict, stash with `-u`. Only resort to worktree if stash gets too complicated.
54
55
 
55
56
  ## Reading PR and CI State
56
57
 
@@ -526,6 +526,19 @@ await flushPromises();
526
526
  expect(replace.mock.calls.some((c) => (c[0] as any)?.query?.q === "foo")).toBe(true);
527
527
  ```
528
528
 
529
+ If the component's source of truth is `useRoute()`/`useRouter()`, `route:` is not
530
+ always the most direct test seam: app middleware, route rules, and redirects still
531
+ run. For component-level behavior, mock the Nuxt imports before mounting:
532
+
533
+ ```typescript
534
+ mockNuxtImport("useRoute", () => () => ({ query: { tab: "activity" }, params: {} }));
535
+ mockNuxtImport("useRouter", () => () => ({ replace: vi.fn(), push: vi.fn() }));
536
+ ```
537
+
538
+ If the component imports router helpers directly from `vue-router`, mock that
539
+ module too. Keep the test focused on what the component reads or writes; use an
540
+ end-to-end/page test when the middleware behavior itself is under test.
541
+
529
542
  ### 9. Testing a composable that needs a component scope
530
543
 
531
544
  For a composable using lifecycle hooks or `provide`/`inject`, mount a throwaway
@@ -541,6 +554,12 @@ Control a VueUse dependency with `vi.mock("@vueuse/core", …)` to return refs y
541
554
  drive. And make sure the vitest `include` glob covers `app/composables/**` — a
542
555
  composables dir is easy to leave out of the frontend config.
543
556
 
557
+ Do this for lifecycle composables even when the return value looks plain:
558
+ `onMounted`, `onUnmounted`, `useEventListener`, `useScrollLock`, and similar
559
+ helpers need a component effect scope to attach and clean up correctly. Calling
560
+ the composable directly in a Vitest test can produce Vue warnings and, worse,
561
+ skip the listener or cleanup path you meant to verify.
562
+
544
563
  ### 10. Reset the shared `useFetch` cache between mounts
545
564
 
546
565
  `useFetch`/`useAsyncData` cache by key, and the cache is **shared across
@@ -553,6 +572,27 @@ import { clearNuxtData } from "#imports"; // not exported from @nuxt/test-utils/
553
572
  beforeEach(() => clearNuxtData());
554
573
  ```
555
574
 
575
+ ### 11. Mock Nuxt fetch behavior at the right layer
576
+
577
+ `mockNuxtImport("$fetch", ...)` is not a reliable target: `$fetch` is not a normal
578
+ Nuxt auto-import in the same way `useRoute` or a composable is, so the transform
579
+ may fail before the test even runs. Prefer `registerEndpoint` when the component
580
+ calls `useFetch`, `useAsyncData`, or `$fetch` against an app route:
581
+
582
+ ```typescript
583
+ registerEndpoint("/api/search", {
584
+ method: "GET",
585
+ handler: (event) => {
586
+ const q = new URL(event.node.req.url!, "http://localhost").searchParams.get("q");
587
+ return [{ id: 1, name: q ?? "" }];
588
+ },
589
+ });
590
+ ```
591
+
592
+ If the component calls a local service/composable that wraps `$fetch`, mock that
593
+ service/composable instead. Mock the boundary you own; use `registerEndpoint` for
594
+ Nuxt's fetch path.
595
+
556
596
  ## File Organization
557
597
 
558
598
  Co-locate tests with source files:
@@ -39,6 +39,11 @@ cross-links to those rather than restating them.
39
39
  - [page-structure.md](./page-structure.md) — keep pages thin: route-param parsing + layout in the page, data/logic/forms in components
40
40
  - [formatters.md](./formatters.md) — never inline a currency/date/number formatter; centralize in `useFormatters`, prefer Intl/date-fns
41
41
 
42
+ Testing note: when a Vue/Nuxt refactor changes component behavior, route/query
43
+ state, or a composable with lifecycle hooks, use the Nuxt frontend testing
44
+ guidance in `nitro-testing`'s [frontend-testing.md](../../../../nitro-testing/skills/nitro-testing/frontend-testing.md)
45
+ instead of testing those pieces as plain Vue functions.
46
+
42
47
  ## Core Principles
43
48
 
44
49
  1. **Lean on auto-imports.** `app/components`, `app/composables`, `app/utils`, and the Vue/Nuxt APIs all auto-import. Add an explicit `import` only for third-party symbols and TS types. A nested component's tag carries its directory as a prefix (`components/customers/ProfileCard.vue` → `<CustomersProfileCard>`).
@@ -24,6 +24,9 @@ export default defineNuxtConfig({ modules: ['@vueuse/nuxt'] })
24
24
 
25
25
  The **`@vueuse/router`** and **`@vueuse/integrations`** add-ons are separate
26
26
  installs and are **not** auto-imported by the module — import them explicitly.
27
+ For URL query sync, install `@vueuse/router` and use its router composables before
28
+ hand-rolling `useRoute()`/`useRouter()` glue; the package exists exactly for that
29
+ boundary.
27
30
 
28
31
  ## The boundary cases (mapped from `watch.md`)
29
32
 
@@ -42,8 +45,9 @@ installs and are **not** auto-imported by the module — import them explicitly.
42
45
 
43
46
  URL sync (`router.replace({ query: { ...route.query, tab } })`) →
44
47
  `useRouteQuery('tab')` from `@vueuse/router` gives a ref bound two-way to the query
45
- param. Nuxt's own `useRoute()` is already reactive for *reads*; reach for
46
- `useRouteQuery` when you want a **writable** ref bound to a single param.
48
+ param. Nuxt's own `useRoute()` is already reactive for *reads*; install and import
49
+ `useRouteQuery` when you want a **writable** ref bound to a single param instead of
50
+ open-coding the same replace/query merge logic.
47
51
 
48
52
  ### `useLocalStorage` reads at setup — guard SSR hydration
49
53
 
@@ -67,9 +71,13 @@ Because the bound ref reads from and writes to the query param, the URL *is* the
67
71
  state — so it **can't represent a ref with two distinct "empty" states** (e.g. a
68
72
  filter that defaults to `"Open"` on load but clears to `null`; both would be "param
69
73
  absent"). When you need that distinction, keep a plain `ref` + a projecting `watch`
70
- (a sanctioned `watch.md` "URL sync" case). And **don't mix `useUrlSearchParams`
71
- (History API) with `useRouteQuery` (vue-router) in the same component** — they write
72
- the URL through different mechanisms and clobber each other's params; pick one.
74
+ (a sanctioned `watch.md` "URL sync" case). The same exception applies when one
75
+ source object fans out to several query params and the projection itself is the
76
+ behavior being tested. Otherwise, adding `@vueuse/router` is preferable to writing
77
+ your own route-query synchronization. And **don't mix `useUrlSearchParams`
78
+ (History API) with `useRouteQuery` (vue-router) in the same component** — they
79
+ write the URL through different mechanisms and clobber each other's params; pick
80
+ one.
73
81
 
74
82
  ## VueUse's `watch` sugar — when a `watch` IS warranted
75
83
 
@@ -69,12 +69,12 @@ Only when the effect crosses **out of** the reactive graph:
69
69
  - **Persist** — a `localStorage`/`useCookie` write, debounced auto-save of a
70
70
  deep-watched form.
71
71
  - **URL sync** — `router.replace({ query: { ...route.query, tab } })`. For a single
72
- ref ↔ one query param, prefer `useRouteQuery` (see below). A write-back watch is
73
- the right tool only when the URL can't model the state: a ref with **two distinct
74
- "empty" states** (e.g. a filter that defaults to `"Open"` on load but clears to
75
- `null` — an absent param can map to only one of them), or a **composite object
76
- fanning out to many params** (a PrimeVue filter object) that a one-ref-per-param
77
- `useRouteQuery` can't express.
72
+ ref ↔ one query param, install `@vueuse/router` and prefer `useRouteQuery` (see
73
+ below). A write-back watch is the right tool only when the URL can't model the
74
+ state: a ref with **two distinct "empty" states** (e.g. a filter that defaults to
75
+ `"Open"` on load but clears to `null` — an absent param can map to only one of
76
+ them), or a **composite object fanning out to many params** (a PrimeVue filter
77
+ object) that a one-ref-per-param `useRouteQuery` can't express.
78
78
  - **Re-seed local state on dialog open** — `watch(visible, (v) => { if (v) initForm() })`.
79
79
  The single most common legit pattern. Its one-liner is `whenever(visible, initForm)`
80
80
  (see `vueuse.md`); a compound guard keeps its inner half —