navori 0.4.2 → 0.5.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.
@@ -33,9 +33,9 @@ set -euo pipefail
33
33
  base={{shq:branchBase}}
34
34
 
35
35
  block() {
36
- echo "[navori] BLOQUEADO por guard-destructive: $1" >&2
37
- echo "[navori] comando: $cmd" >&2
38
- echo "[navori] si es intencional, corre el comando mismo fuera del agente." >&2
36
+ echo "[navori] BLOCKED by guard-destructive: $1" >&2
37
+ echo "[navori] command: $cmd" >&2
38
+ echo "[navori] if intentional, run the command yourself outside the agent." >&2
39
39
  exit 2
40
40
  }
41
41
 
@@ -75,7 +75,7 @@ git_cp='(^|[[:space:]]|[;&|])git([[:space:]]+-[a-zA-Z-]+(=[^[:space:]]+)?([[:spa
75
75
  # (`git commit -qn`/`-nq`) without a hyphen-word in a message tripping it.
76
76
  if printf '%s' "$scan" | grep -qE "${git_cp}([[:space:]]|.)*--no-verify" \
77
77
  || printf '%s' "$scan_flags" | grep -qE "${git_cp}([[:space:]]|.)*[[:space:]]-[a-zA-Z]*n[a-zA-Z]*([[:space:]]|\$)"; then
78
- block "git commit/push con --no-verify (saltarse los hooks/gates)"
78
+ block "git commit/push with --no-verify (skipping hooks/gates)"
79
79
  fi
80
80
 
81
81
  # 2. Force-push to the base branch. force-with-lease is allowed (safe rebase
@@ -87,13 +87,13 @@ if printf '%s' "$scan" | grep -qE '(^|[[:space:]]|[;&|])git([[:space:]]+-[a-zA-Z
87
87
  && printf '%s' "$scan" | grep -qE '(--force([[:space:]]|$)|[[:space:]]-f([[:space:]]|$)|[[:space:]]\+)' \
88
88
  && ! printf '%s' "$scan" | grep -qE 'force-with-lease' \
89
89
  && printf '%s' "$scan" | grep -qE "(^|[[:space:]+/])${base}([[:space:]]|\$)"; then
90
- block "force-push a la rama base '${base}'"
90
+ block "force-push to the base branch '${base}'"
91
91
  fi
92
92
 
93
93
  # 3. rm -rf with variable indirection or absolute/home roots that static deny
94
94
  # globs miss (e.g. PATH=/; rm -rf $PATH).
95
95
  if printf '%s' "$cmd" | grep -qE '(^|[[:space:]])rm[[:space:]]+(-[a-zA-Z]*r[a-zA-Z]*[[:space:]]+|-[a-zA-Z]*f[a-zA-Z]*[[:space:]]+)*-?[a-zA-Z]*[rf][a-zA-Z]*[[:space:]]+("?\$|/[[:space:]]*$|~[[:space:]]*$)'; then
96
- block "rm recursivo sobre variable / raíz / home"
96
+ block "recursive rm over a variable / root / home"
97
97
  fi
98
98
 
99
99
  # 4. Fork bomb.
@@ -103,13 +103,13 @@ fi
103
103
 
104
104
  # 5. Writing to a raw block device (wipes a disk/partition).
105
105
  if printf '%s' "$cmd" | grep -qE '(of=/dev/(sd|nvme|disk|hd)|>[[:space:]]*/dev/(sd|nvme|disk|hd))'; then
106
- block "escritura directa a un dispositivo de bloque"
106
+ block "direct write to a block device"
107
107
  fi
108
108
 
109
109
  # navori:user-section
110
- # user: agrega guards adicionales acá. `$cmd` ya tiene el comando completo
111
- # (incluye comandos compuestos) y `block "<motivo>"` aborta con exit 2.
112
- # Ejemplo:
110
+ # user: add extra guards here. `$cmd` already holds the full command (compound
111
+ # commands included) and `block "<reason>"` aborts with exit 2.
112
+ # Example:
113
113
  #
114
114
  # if printf '%s' "$cmd" | grep -qE 'drop[[:space:]]+(table|database)'; then
115
115
  # block "DROP TABLE/DATABASE"
@@ -132,19 +132,19 @@ if is_scan_trigger "$cmd"; then
132
132
  # `exit 0` that handed a contributor zero quality gate without a word.
133
133
  detected_pm="$(detect_pm)"
134
134
  if is_pm "$gate_bin" && [ -n "$detected_pm" ] && [ "$detected_pm" != "$gate_bin" ] && command -v "$detected_pm" >/dev/null 2>&1; then
135
- echo "[navori] '$gate_bin' no está en PATH; uso el package manager detectado por lockfile: '$detected_pm'." >&2
135
+ echo "[navori] '$gate_bin' is not on PATH; using the lockfile-detected package manager: '$detected_pm'." >&2
136
136
  run_gate "$detected_pm ${gate#* }"
137
137
  else
138
- echo "[navori] quality-gate NO ejecutado: '$gate_bin' no está en PATH y no hay un package manager alternativo detectado que pueda correrlo." >&2
139
- echo "[navori] Commit BLOQUEADO para no saltarnos el gate en silencio. Instala '$gate_bin', o si de verdad quieres saltártelo corre el commit mismo fuera del agente." >&2
138
+ echo "[navori] quality-gate NOT run: '$gate_bin' is not on PATH and no alternative package manager was detected that could run it." >&2
139
+ echo "[navori] Commit BLOCKED to avoid skipping the gate silently. Install '$gate_bin', or if you really want to skip it run the commit yourself outside the agent." >&2
140
140
  exit 2
141
141
  fi
142
142
  fi
143
143
  fi
144
144
 
145
145
  # navori:user-section
146
- # user: agrega checks adicionales acá. `$cmd` ya está parseado del input
147
- # de la tool. Ejemplo:
146
+ # user: add extra checks here. `$cmd` is already parsed from the tool input.
147
+ # Example:
148
148
  #
149
149
  # case "$cmd" in
150
150
  # 'git push'*)
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: citty
3
+ description: Use when adding or editing a CLI command with citty — defineCommand, runMain, typed args (positional/string/boolean/enum), subCommands, and run/setup/cleanup hooks.
4
+ type: reference
5
+ ---
6
+
7
+ # Citty — command definitions
8
+
9
+ ## When to use this skill
10
+
11
+ When adding a subcommand, declaring typed args, wiring `run`/`setup`/`cleanup`, or debugging an unparsed flag. Citty is UnJS's zero-dependency builder over Node's `util.parseArgs`; the tree is `runMain` → root `defineCommand` → `subCommands`.
12
+
13
+ ## The pattern
14
+
15
+ Each command is a `defineCommand` object: `meta` for help, `args` for typed input, `run` for the body. Nest `subCommands`; run the root once with `runMain`.
16
+
17
+ ```ts
18
+ import { defineCommand, runMain } from "citty";
19
+
20
+ const build = defineCommand({
21
+ meta: { name: "build", description: "Build the project" },
22
+ args: {
23
+ entry: { type: "positional", required: true, description: "Entry file" },
24
+ mode: { type: "enum", options: ["dev", "prod"], default: "dev" },
25
+ minify: { type: "boolean", description: "Minify output" },
26
+ out: { type: "string", alias: ["o"], valueHint: "dir" },
27
+ },
28
+ run({ args }) {
29
+ // args.entry, args.mode, args.minify, args.out (kebab also → camelCase)
30
+ },
31
+ });
32
+
33
+ const main = defineCommand({
34
+ meta: { name: "cli", version: "1.0.0" },
35
+ subCommands: { build },
36
+ });
37
+ runMain(main);
38
+ ```
39
+
40
+ ## Gotchas that bite
41
+
42
+ - **A custom `-v`/`-h` shadows the built-ins.** Citty auto-handles `--version`/`-v` and `--help`/`-h`, but declaring an arg with the same name or alias silently disables them. Don't alias `verbose` to `v`.
43
+ - **Only the run command's hooks fire.** `setup`/`cleanup` run for the executing command, not its parents — a root `setup` won't run before a subcommand. Put shared init in the leaf.
44
+ - **`cleanup` runs even on throw.** It's your `finally`; keep it idempotent and side-effect-safe.
45
+ - **Positionals take no `alias`.** Alias is for flags. A missing `required` positional throws before `run`.
46
+ - **Kebab args have two names.** `--out-dir` lands as both `args["out-dir"]` and `args.outDir`; pick one.
47
+ - **Unset optional string/enum is `undefined`, not `""`.** Give a `default` or guard before use.
48
+
49
+ ## Hard rules
50
+
51
+ 1. One `defineCommand` per file, exported; wire the tree in the entrypoint, `runMain` once.
52
+ 2. Declare every input in `args` with an explicit `type` and `description` — never read `rawArgs` by hand.
53
+ 3. `enum` args always carry `options`; give user flags a `default` so `run` never sees `undefined`.
54
+ 4. Lazy-load heavy subcommands: `sub: () => import("./sub.ts").then((m) => m.default)`.
55
+ 5. Side effects (fs, network) live in `run`, never at module top level — imports stay pure.
56
+ 6. Use `required: true` for mandatory input; let citty throw rather than validating presence yourself.
57
+
58
+ ## Quick table
59
+
60
+ | Need | Use |
61
+ |---|---|
62
+ | Named flag | `{ type: "string", alias: ["o"] }` |
63
+ | Yes/no flag | `{ type: "boolean" }` (`--no-x` negates) |
64
+ | Constrained choice | `{ type: "enum", options: [...] }` |
65
+ | Required arg | `{ type: "positional", required: true }` |
66
+ | Nested command | `subCommands: { build }` |
67
+ | Lazy subcommand | `() => import("./x.ts").then((m) => m.default)` |
68
+
69
+ ## Before declaring done
70
+
71
+ - Every arg has a `type` and `description`; enums carry `options`; user flags have defaults.
72
+ - Built-in `--help`/`--version` still work (no shadowing alias).
73
+ - No side effects at import time; `cleanup` is idempotent.
74
+ - `{{qualityGate.fast}}` green.
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: clack
3
+ description: Use when building interactive CLI prompts with @clack/prompts — intro/outro, text/select/confirm/multiselect, spinner, isCancel, and group flows.
4
+ type: reference
5
+ ---
6
+
7
+ # Clack prompts — interactive CLI
8
+
9
+ ## When to use this skill
10
+
11
+ When building interactive flows: prompting the user (`text`/`select`/`confirm`/`multiselect`), bracketing a session with `intro`/`outro`, showing progress with `spinner`, or bundling steps with `group`. `@clack/prompts` is the UI layer; every prompt is `await`ed and can be cancelled.
12
+
13
+ ## The pattern
14
+
15
+ Bracket the flow with `intro`/`outro`. `await` each prompt, then immediately check `isCancel` — on cancel, emit `cancel(...)` and exit. Import the namespace as `p`.
16
+
17
+ ```ts
18
+ import * as p from "@clack/prompts";
19
+
20
+ p.intro("navori init");
21
+
22
+ const name = await p.text({
23
+ message: "Project name?",
24
+ placeholder: "my-app",
25
+ validate: (v) => (v.length === 0 ? "Required" : undefined),
26
+ });
27
+ if (p.isCancel(name)) {
28
+ p.cancel("Cancelled.");
29
+ process.exit(0);
30
+ }
31
+
32
+ const kind = await p.select({
33
+ message: "Preset?",
34
+ options: [
35
+ { value: "spa", label: "SPA" },
36
+ { value: "api", label: "API", hint: "backend" },
37
+ ],
38
+ });
39
+ if (p.isCancel(kind)) {
40
+ p.cancel("Cancelled.");
41
+ process.exit(0);
42
+ }
43
+
44
+ p.outro("Done");
45
+ ```
46
+
47
+ ## Gotchas that bite
48
+
49
+ - **A prompt returns `value | symbol`.** Cancel is a symbol, not `null`. You MUST `isCancel` after every prompt — it narrows the type so `value` is usable afterward. Skip it and a cancelled `text` is treated as a string.
50
+ - **Forget the check and the flow misbehaves.** Nothing throws on cancel; handle it or downstream code runs with a symbol.
51
+ - **`spinner` needs both `start` and `stop`.** `const s = p.spinner(); s.start("…"); …; s.stop("done")` — an unstopped spinner leaves the terminal spinning; wrap work in try/finally.
52
+ - **`confirm` returns a boolean**, `multiselect` an array, `select` the chosen `value` — not the label.
53
+ - **`group` centralizes cancel.** Its `onCancel` fires if any step cancels, so per-step `isCancel` isn't needed inside a group.
54
+ - **`validate` returns a string (error) or `undefined` (ok)** — returning `""` still reads as valid.
55
+
56
+ ## Hard rules
57
+
58
+ 1. `intro` opens and `outro` closes every flow — matched pair.
59
+ 2. `isCancel` after every standalone prompt; on cancel, `p.cancel(msg)` then `process.exit(0)`.
60
+ 3. Prefer `group({...}, { onCancel })` for multi-step flows — one cancel handler, typed `results`.
61
+ 4. Every `spinner().start()` has a matching `.stop()`, even on error (try/finally).
62
+ 5. `select`/`multiselect` options are `{ value, label, hint? }`; consume `value`, never the label.
63
+ 6. `validate` returns an error string or `undefined`; keep prompts `await`ed — they're all async.
64
+
65
+ ## Quick table
66
+
67
+ | Need | Use |
68
+ |---|---|
69
+ | Free text | `await p.text({ message })` |
70
+ | One of many | `await p.select({ message, options })` |
71
+ | Several | `await p.multiselect({ message, options })` |
72
+ | Yes/no | `await p.confirm({ message })` |
73
+ | Progress | `const s = p.spinner(); s.start(); s.stop()` |
74
+ | Multi-step | `await p.group({...}, { onCancel })` |
75
+
76
+ ## Before declaring done
77
+
78
+ - Every prompt is `await`ed and followed by `isCancel` (or inside a `group` with `onCancel`).
79
+ - Every spinner is stopped; `intro`/`outro` bracket the flow.
80
+ - `select`/`multiselect` consume `value`, not label.
81
+ - `{{qualityGate.fast}}` green.
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: jest
3
+ description: Use when testing React Native/Expo apps or the Medusa backend — jest-expo preset, transformIgnorePatterns, jest.mock hoisting, medusaIntegrationTestRunner.
4
+ type: reference
5
+ ---
6
+
7
+ # Jest — conventions
8
+
9
+ ## When to use this skill
10
+
11
+ When the code under test is **React Native / Expo** (the mobile app) or the **Medusa backend** — those two run on Jest, not Vitest. Reach for the right preset/runner: `jest-expo` for RN/Expo, `medusaIntegrationTestRunner`/`moduleIntegrationTestRunner` from `@medusajs/test-utils` for Medusa. Everything else in the stack uses Vitest; do not introduce Jest into a Vitest project.
12
+
13
+ ## The pattern
14
+
15
+ Preset-driven config, `jest.mock` at the top, RN queries via `@testing-library/react-native`.
16
+
17
+ ```js
18
+ // jest.config.js (Expo) — the preset alone is often enough
19
+ module.exports = {
20
+ preset: 'jest-expo',
21
+ transformIgnorePatterns: [
22
+ 'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native)',
23
+ ],
24
+ };
25
+ ```
26
+
27
+ ```tsx
28
+ import { render, screen, fireEvent } from '@testing-library/react-native';
29
+ import { LoginScreen } from './LoginScreen';
30
+
31
+ jest.mock('./auth-api', () => ({ login: jest.fn() }));
32
+
33
+ it('shows an error on empty submit', () => {
34
+ render(<LoginScreen />);
35
+ fireEvent.press(screen.getByText('Sign in'));
36
+ expect(screen.getByText('Email is required')).toBeOnTheScreen();
37
+ });
38
+ ```
39
+
40
+ Medusa boots a real framework + test DB:
41
+
42
+ ```ts
43
+ import { medusaIntegrationTestRunner } from '@medusajs/test-utils';
44
+
45
+ medusaIntegrationTestRunner({
46
+ testSuite: ({ api, getContainer }) => {
47
+ it('lists posts', async () => {
48
+ const res = await api.get('/store/posts');
49
+ expect(res.status).toBe(200);
50
+ });
51
+ },
52
+ });
53
+ ```
54
+
55
+ ## Gotchas that bite
56
+
57
+ - **`SyntaxError: Unexpected token 'export'` from node_modules** is the RN classic: an untranspiled ESM package. Whitelist the offender in `transformIgnorePatterns` (`node_modules/(?!(pkg)/)`), never transform all of `node_modules`; pnpm/Bun prepend `.pnpm`/`.bun`.
58
+ - **`jest.mock` is hoisted above imports** like `vi.mock`. The factory can't close over later variables — declare mock fns inside it, or `jest.mock('m'); const m = jest.mocked(require('m'))`.
59
+ - **jest-native is deprecated.** RNTL v12.4+ ships built-in matchers (`toBeOnTheScreen`/`toBeVisible`/`toHaveTextContent`) — no `@testing-library/jest-native`, no `extend-expect`. Older RNTL: add `@testing-library/jest-native/extend-expect` to `setupFilesAfterEnv`.
60
+ - **Wrong runner for Medusa.** A plain `it` calling the service fails on the missing DI container. Single-module tests use `moduleIntegrationTestRunner<Service>({ moduleName, moduleModels, resolve, testSuite })`. Both need `testEnvironment: 'node'`, an `@swc/jest` transform, and `--runInBand --forceExit`.
61
+ - **State leaks between tests.** Set `clearMocks: true` (call history); add `resetMocks: true` only when a mock's implementation must not carry over.
62
+ - **Fake timers + animations.** RN animations and `setTimeout` UI need `jest.useFakeTimers()` + `act(() => jest.advanceTimersByTime(ms))`, or updates never flush.
63
+
64
+ ## Hard rules
65
+
66
+ 1. Use the preset/runner for the target: `jest-expo` for RN/Expo, `@medusajs/test-utils` runners for Medusa.
67
+ 2. Fix RN "unexpected token" via a targeted `transformIgnorePatterns` whitelist — never blanket-transform `node_modules`.
68
+ 3. `jest.mock` at top level; keep factories self-contained (hoisting).
69
+ 4. Query RN trees with `@testing-library/react-native` matchers, not `.props`.
70
+ 5. `clearMocks: true` in config; add `resetMocks` only when implementations must not persist.
71
+
72
+ ## Quick table
73
+
74
+ | Target | Preset / runner |
75
+ |---|---|
76
+ | React Native / Expo | `preset: 'jest-expo'` |
77
+ | Medusa API + full app | `medusaIntegrationTestRunner` |
78
+ | Medusa single module | `moduleIntegrationTestRunner` |
79
+ | RN matchers | built-in (RNTL ≥ 12.4) |
80
+ | ESM in node_modules | whitelist in `transformIgnorePatterns` |
81
+
82
+ ## Before declaring done
83
+
84
+ - Correct preset/runner (RN vs Medusa); no blanket node_modules transform.
85
+ - Mocks hoisted safely and cleared between tests; RN matchers available.
86
+ - Medusa tests boot the runner (`testEnvironment: 'node'`), not the bare service.
87
+ - `{{qualityGate.fast}}` green.
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: playwright
3
+ description: Use when writing browser E2E tests with Playwright — @playwright/test runner, web-first assertions with auto-wait, role locators, projects, traces on failure.
4
+ type: reference
5
+ ---
6
+
7
+ # Playwright — conventions
8
+
9
+ ## When to use this skill
10
+
11
+ When writing end-to-end browser tests that drive a real page (storefront, dashboard, Medusa e2e). Playwright is E2E only — it complements, never replaces, the Vitest/Jest unit suite. Two official pillars: **test user-visible behavior** (what the page renders, not CSS classes) and **web-first, auto-waiting assertions** — `await expect(locator).toBeVisible()` retries until the condition holds or times out, so you never sleep.
12
+
13
+ ## The pattern
14
+
15
+ `test`/`expect` from `@playwright/test`, role/label locators, awaited web-first assertions.
16
+
17
+ ```ts
18
+ import { test, expect } from '@playwright/test';
19
+
20
+ test('user can log in', async ({ page }) => {
21
+ await page.goto('/login');
22
+ await page.getByLabel('Email').fill('ada@x.com');
23
+ await page.getByLabel('Password').fill('secret');
24
+ await page.getByRole('button', { name: 'Sign in' }).click();
25
+
26
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
27
+ await expect(page).toHaveURL(/\/dashboard/);
28
+ });
29
+ ```
30
+
31
+ ## Gotchas that bite
32
+
33
+ - **The `expect` is Playwright's, not the runner's.** Import `test` and `expect` from `@playwright/test`. Pulling in Vitest/Jest `expect` loses auto-retry and every web-first matcher.
34
+ - **`waitForTimeout` is a code smell.** Never sleep a fixed number of ms. Use web-first assertions (`toBeVisible`, `toHaveText`, `toHaveURL`) or `locator.waitFor()`; they retry until ready and kill flake.
35
+ - **Assert on the locator, not a captured value.** `await expect(locator).toHaveText('x')` re-queries and retries; `expect(await locator.textContent()).toBe('x')` reads once and flakes. Keep `await` on `expect`, not inside it.
36
+ - **Prefer user-facing locators.** Official order: `getByRole` → `getByLabel` → `getByPlaceholder` → `getByText`. Drop to `getByTestId` only when nothing user-visible works; never CSS/XPath. Narrow with `.filter({ hasText })`.
37
+ - **Tests must be isolated.** Each test gets a fresh `page`/context; don't share auth or state through module scope. Reuse login via `storageState`/a fixture. Mock external calls with `page.route()`; don't hit servers you don't control.
38
+ - **Multi-browser lives in `projects`.** Chromium/Firefox/WebKit are `projects` in `playwright.config.ts`, not `if` branches. Same test, matrixed by config.
39
+ - **Debug with artifacts, not console.log.** Set `trace: 'on-first-retry'` and `screenshot: 'only-on-failure'`; open the trace viewer. To scaffold or heal tests, use the official agents (`npx playwright init-agents`) and MCP (`@playwright/mcp`).
40
+
41
+ ## Hard rules
42
+
43
+ 1. Import `test`/`expect` from `@playwright/test`; never the unit runner's `expect`.
44
+ 2. Assert user-visible behavior with web-first assertions and `locator.waitFor()`; `waitForTimeout` is banned.
45
+ 3. `await expect(locator)...` — assert on the live locator, not a captured value.
46
+ 4. Prefer role/label locators; `getByTestId` is the fallback, CSS/XPath the last resort.
47
+ 5. Keep tests independent; share auth via `storageState`/fixtures, mock third parties, matrix browsers through `projects` with trace + screenshot on failure.
48
+
49
+ ## Quick table
50
+
51
+ | Goal | API |
52
+ |---|---|
53
+ | Find by role | `page.getByRole('button', { name: 'Save' })` |
54
+ | Find by label | `page.getByLabel('Email')` |
55
+ | Fallback locator | `page.getByTestId('cart')` |
56
+ | Assert visible | `await expect(locator).toBeVisible()` |
57
+ | Assert URL | `await expect(page).toHaveURL(/x/)` |
58
+ | Multi-browser | `projects` in config |
59
+ | Failure debug | `trace: 'on-first-retry'` |
60
+
61
+ ## Before declaring done
62
+
63
+ - All waits are web-first assertions or `waitFor`; zero `waitForTimeout`.
64
+ - `test`/`expect` imported from `@playwright/test`; tests run isolated in parallel; external calls mocked.
65
+ - Locators are user-facing (role/label first); trace + screenshot on failure; browsers matrixed via `projects`.
66
+ - `{{qualityGate.fast}}` green.
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: supertest
3
+ description: Use when integration-testing HTTP APIs — supertest against an Express/Medusa app without binding a port, status/body/header assertions, auth headers.
4
+ type: reference
5
+ ---
6
+
7
+ # Supertest — conventions
8
+
9
+ ## When to use this skill
10
+
11
+ When integration-testing an HTTP API — real routes end to end through the middleware stack, no running server or network. Pass the **app object** (or an `http.Server`) to `request()`; if it is not listening, supertest binds an ephemeral port and tears it down — no `listen()`, no port bookkeeping. Runner-agnostic (Vitest, Jest, Mocha). Tests the app, not a deployed service.
12
+
13
+ ## The pattern
14
+
15
+ `request(app)`, chain verb + path, assert status/headers inline via `.expect(...)`, then the body via the runner's `expect(res.body)`.
16
+
17
+ ```ts
18
+ import request from 'supertest';
19
+ import { app } from '../src/app';
20
+
21
+ describe('POST /users', () => {
22
+ it('creates a user and returns 201', async () => {
23
+ const res = await request(app)
24
+ .post('/users')
25
+ .set('Authorization', `Bearer ${token}`)
26
+ .send({ email: 'ada@x.com', name: 'Ada' })
27
+ .expect('Content-Type', /json/)
28
+ .expect(201);
29
+
30
+ expect(res.body).toMatchObject({ email: 'ada@x.com' });
31
+ });
32
+ });
33
+ ```
34
+
35
+ ## Gotchas that bite
36
+
37
+ - **Pass the app, not a URL.** `request(app)` lets supertest manage the port; `request('http://localhost:3000')` ties the test to a manually-started server and defeats isolation.
38
+ - **Don't `listen()` yourself.** Export the app separately from `app.listen()`. If the entry file both builds and starts the server, tests leak a port — split creation from startup.
39
+ - **The request is lazy — `await` it or nothing runs.** A chain fires only when you `await`, `return`, `.then()`, or `.end()` it. Forget all of them and the assertion never runs (false green), or the failure floats as an unhandled rejection outside the test. With `.end((err, res) => …)`, a failed `.expect(...)` returns as `err` rather than throwing — forward it.
40
+ - **`.expect(...)` vs the runner's `expect`.** `.expect()` (supertest) checks status, headers, or an exact body/regex inline, plus a custom `res => { … }` assertor; partial/deep body shape belongs to the runner's `expect(res.body)`. `.expect(res.body.id)` checks nothing.
41
+ - **Auth is just a header.** Set tokens with `.set('Authorization', …)`; no session magic. Multi-step cookie/session flows: `request.agent(app)`, which persists cookies across calls.
42
+ - **Assert body shape, not the whole object.** Use `toMatchObject`/specific fields; snapshotting `res.body` breaks on every timestamp/id and adds noise.
43
+
44
+ ## Hard rules
45
+
46
+ 1. `request(app)` with the app/server instance — never a live URL/port.
47
+ 2. Export the app separately from `listen()`; tests never bind a real port.
48
+ 3. `await` (or `return`/`.end()`) every `request(...)` chain — unawaited chains do not run.
49
+ 4. Status/headers via `.expect(...)`; body shape via the runner's `expect(res.body)`.
50
+ 5. Auth via `.set('Authorization', …)`; multi-step cookie flows via `request.agent(app)`.
51
+
52
+ ## Quick table
53
+
54
+ | Goal | API |
55
+ |---|---|
56
+ | Call a route | `request(app).get('/x')` |
57
+ | Assert status | `.expect(200)` |
58
+ | Assert header | `.expect('Content-Type', /json/)` |
59
+ | Custom assertion | `.expect(res => { … })` |
60
+ | Send JSON body | `.send({ email: 'a@x.com' })` |
61
+ | Auth header | `.set('Authorization', 'Bearer …')` |
62
+ | Persist cookies | `request.agent(app)` |
63
+
64
+ ## Before declaring done
65
+
66
+ - `request(app)`, app export split from `listen()`.
67
+ - Every chain awaited/returned; status via `.expect`, body via the runner's `expect`.
68
+ - Body assertions target fields, not snapshots.
69
+ - `{{qualityGate.fast}}` green.
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: testing-library
3
+ description: Use when querying rendered UI in tests — Testing Library query priority (getByRole first), get/query/find, user-event over fireEvent, React Native variant.
4
+ type: reference
5
+ ---
6
+
7
+ # Testing Library — conventions
8
+
9
+ ## When to use this skill
10
+
11
+ When a test renders a component and needs to find nodes and assert on them — React, Vue, Svelte, or React Native. The whole library encodes one rule: **query the way a user perceives the UI** (role, label, text), not the way it's built (class names, component internals, test ids as a first resort). It works under both Vitest and Jest; it's the query layer, not the runner.
12
+
13
+ ## The pattern
14
+
15
+ Render, query by accessible role/text via `screen`, drive interaction with `user-event`, assert with jest-dom matchers.
16
+
17
+ ```tsx
18
+ import { render, screen } from '@testing-library/react';
19
+ import userEvent from '@testing-library/user-event';
20
+ import { LoginForm } from './LoginForm';
21
+
22
+ it('submits the email', async () => {
23
+ const user = userEvent.setup();
24
+ const onSubmit = vi.fn();
25
+ render(<LoginForm onSubmit={onSubmit} />);
26
+
27
+ await user.type(screen.getByRole('textbox', { name: /email/i }), 'ada@x.com');
28
+ await user.click(screen.getByRole('button', { name: /sign in/i }));
29
+
30
+ expect(onSubmit).toHaveBeenCalledWith({ email: 'ada@x.com' });
31
+ });
32
+ ```
33
+
34
+ ## Gotchas that bite
35
+
36
+ - **`getByTestId` is the last resort.** Priority: `getByRole` → `getByLabelText` (forms) → `getByPlaceholderText`/`getByText` → `getByTestId`. Reaching for a testid usually means the markup lacks an accessible name — fix the markup instead.
37
+ - **`getBy` throws, `queryBy` returns null, `findBy` awaits.** Assert presence with `getBy*`; assert **absence** only with `queryBy*` (`expect(screen.queryByText('x')).not.toBeInTheDocument()`); wait for async UI with `await findBy*` — never poll a `getBy` inside `waitFor` for something not yet rendered.
38
+ - **`fireEvent` skips real user behavior.** `user-event` fires the full event sequence (focus, keydown, input) and is async — `await user.click(...)`. Prefer it; keep `fireEvent` only for events user-event can't model (e.g. `scroll`).
39
+ - **`act` warnings mean an un-awaited update.** Almost always a missing `await` on a `user.*` call or a `findBy`. Add the `await`; don't wrap things in manual `act`.
40
+ - **jest-dom matchers must be imported.** `toBeInTheDocument`/`toBeVisible`/`toHaveValue` come from `@testing-library/jest-dom` (loaded in setup). Without it you fall back to weak truthiness checks.
41
+ - **React Native shares the same queries now.** `@testing-library/react-native` (v12.4+) supports `getByRole` (matches the `role`/`accessibilityRole` prop) with the same priority — role first, then `getByLabelText`/`getByText`, `getByTestId` last. Its matchers (`toBeOnTheScreen`, `toBeDisabled`) are built in; do not install `@testing-library/jest-native` (deprecated).
42
+
43
+ ## Hard rules
44
+
45
+ 1. Query by role/label/text first; `getByTestId` only when no accessible name exists.
46
+ 2. `getBy` for presence, `queryBy` for absence, `findBy` (awaited) for async.
47
+ 3. Prefer `user-event` (awaited) over `fireEvent`.
48
+ 4. Load jest-dom matchers in setup (web); React Native's are built in.
49
+ 5. Test observable behavior and output, never component internals or state.
50
+
51
+ ## Quick table
52
+
53
+ | Goal | Query |
54
+ |---|---|
55
+ | Interactive element | `getByRole('button', { name: /save/i })` |
56
+ | Form field | `getByLabelText(/email/i)` |
57
+ | Assert absence | `queryByText('x')` + `.not.toBeInTheDocument()` |
58
+ | Async appearance | `await findByText('Loaded')` |
59
+ | React Native | `getByRole` (v12.4+); built-in matchers |
60
+
61
+ ## Before declaring done
62
+
63
+ - Queries follow priority (role/label before testid); absence uses `queryBy`, async uses `findBy`.
64
+ - Interactions go through awaited `user-event`; no stray `act` warnings.
65
+ - jest-dom loaded (web); React Native's matchers are built in.
66
+ - `{{qualityGate.fast}}` green.
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: vitest
3
+ description: Use when writing or fixing unit/integration tests with Vitest — describe/it/expect, vi.mock hoisting, jsdom vs node env, coverage, fake timers.
4
+ type: reference
5
+ ---
6
+
7
+ # Vitest — conventions
8
+
9
+ ## When to use this skill
10
+
11
+ When authoring or debugging Vitest unit/integration tests: asserting behavior, mocking with `vi`, choosing an environment, or fixing flaky async/timer tests. Vitest is the default runner except React Native and the Medusa backend (those use Jest). It shares Jest's `expect` surface, but the mock API lives on `vi`, and `vi.mock` is hoisted above imports.
12
+
13
+ ## The pattern
14
+
15
+ Arrange–act–assert, one behavior per `it`, mock at the module boundary, `await` every async assertion. Prefer the `import()` form of `vi.mock`.
16
+
17
+ ```ts
18
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
19
+ import { getUser } from './user-service';
20
+ import { db } from './db';
21
+
22
+ vi.mock(import('./db'), () => ({ db: { findUser: vi.fn() } }));
23
+
24
+ beforeEach(() => vi.clearAllMocks());
25
+
26
+ describe('getUser', () => {
27
+ it('returns the mapped user', async () => {
28
+ vi.mocked(db.findUser).mockResolvedValue({ id: 1, name: 'Ada' });
29
+ await expect(getUser(1)).resolves.toMatchObject({ id: 1, name: 'Ada' });
30
+ });
31
+ });
32
+ ```
33
+
34
+ ## Gotchas that bite
35
+
36
+ - **`vi.mock` is hoisted above imports.** It runs before any `import`, so the factory cannot reference file-scope variables, and `vi` must come from `vitest`. Share a fn via `vi.hoisted`: `const { send } = vi.hoisted(() => ({ send: vi.fn() }))`.
37
+ - **Wrong `environment` = `document is not defined` or a slow suite.** Default is `node`; use `environment: 'jsdom'` (or `'happy-dom'`) for DOM, or `// @vitest-environment jsdom` per file.
38
+ - **Unawaited async assertions pass silently.** `expect(p).resolves.toBe(x)` without `await`/`return` is a false green. Always `await expect(...).resolves` / `.rejects`.
39
+ - **Fake timers must be paired.** `vi.useFakeTimers()` in setup, `vi.useRealTimers()` in teardown; advance with `await vi.advanceTimersByTimeAsync(ms)`, or callbacks never flush.
40
+ - **`clearAllMocks` ≠ `resetAllMocks` ≠ `restoreAllMocks`.** `clear` wipes call history; `reset` also restores the `vi.fn(impl)` implementation; `restore` reverts `vi.spyOn` spies only, not automocks. Prefer `clearAllMocks` in `beforeEach`.
41
+ - **Snapshots rot.** Reserve for small, stable output; never a whole component tree or an object with dates/ids — assert the fields that matter with `toMatchObject`.
42
+
43
+ ## Hard rules
44
+
45
+ 1. Run with `vitest run` (or `--no-watch`) so it exits under CI and agents.
46
+ 2. Mock at top level; share fns via `vi.hoisted`, never a bare outer variable.
47
+ 3. `await` (or `return`) every `.resolves`/`.rejects` and every async timer advance.
48
+ 4. Pick `environment`: `jsdom`/`happy-dom` for DOM, `node` for backend.
49
+ 5. Reset between tests (`clearAllMocks` or `clearMocks: true`); restore only what you spied on.
50
+ 6. Coverage via `@vitest/coverage-v8` (install it); v4 dropped `coverage.all`, so set `coverage.include`. Assert behavior, not a line target.
51
+
52
+ ## Quick table
53
+
54
+ | Need | Use |
55
+ |---|---|
56
+ | Mock a module | `vi.mock(import('./m'), () => ({ db: vi.fn() }))` |
57
+ | Share a mock fn into a factory | `vi.hoisted(() => ({ fn: vi.fn() }))` |
58
+ | DOM component test | `environment: 'jsdom'` |
59
+ | Async assert | `await expect(p).resolves.toBe(x)` |
60
+ | Advance timers | `await vi.advanceTimersByTimeAsync(ms)` |
61
+
62
+ ## Before declaring done
63
+
64
+ - Every async assertion is awaited; no unhandled-rejection warnings.
65
+ - Mocks reset between tests; environment matches the code under test (DOM vs node).
66
+ - No sprawling snapshots; assertions target real values.
67
+ - `{{qualityGate.fast}}` green.
@@ -16,4 +16,4 @@ CAUSA: <1 línea> / ARCHIVO: <path>:<línea> / FIX: <diff mínimo>
16
16
  [MEDIO] ... # legibilidad, naming
17
17
 
18
18
  **Generación**: diff si modifica; archivo completo solo si es nuevo.
19
- **Commits**: Conventional (`feat(scope): ...`), español MX, atómicos.
19
+ **Commits**: Conventional (`feat(scope): ...`), atómicos, en el idioma que define `commits` en la config.
@@ -15,8 +15,7 @@
15
15
  "checkBinary": "acli",
16
16
  "install": {
17
17
  "darwin": "brew install atlassian-labs/acli/acli",
18
- "linux": "curl -fsSL https://acli-releases.atlassian.com/install.sh | bash",
19
- "win32": "(descarga acli para Windows y agrégalo al PATH — https://developer.atlassian.com/cloud/acli/guides/install-acli/)"
18
+ "linux": "curl -fsSL https://acli-releases.atlassian.com/install.sh | bash"
20
19
  }
21
20
  },
22
21
  "settingsFragment": {
@@ -15,8 +15,7 @@
15
15
  "checkBinary": "codegraph",
16
16
  "install": {
17
17
  "darwin": "curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh",
18
- "linux": "curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh",
19
- "win32": "(codegraph no tiene instalador nativo para Windows; instálalo vía WSL — https://github.com/colbymchenry/codegraph)"
18
+ "linux": "curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh"
20
19
  },
21
20
  "postInstall": "codegraph install && codegraph init"
22
21
  },
@@ -6,12 +6,12 @@ type: behavior
6
6
 
7
7
  ## Rung -1 — query the code graph first (codegraph)
8
8
 
9
- Before the grep/ast-grep ladder below, if the `codegraph` MCP tool is available, ask the pre-built AST graph where the code lives:
9
+ Before the grep/ast-grep ladder above, if the `codegraph` MCP tool is available, ask the pre-built AST graph where the code lives:
10
10
 
11
11
  - **Locate a symbol / definition:** `codegraph_explore` with the symbol name or a natural-language question ("where is the auth token refreshed?"). One call returns the source span + call paths, instead of a `grep` sweep.
12
12
  - **Trace impact:** ask what calls a function or what a change touches, to size the blast-radius before reading files.
13
13
 
14
- The graph **forms the hypothesis** (which files/symbols matter). The rungs below still **verify** it — never edit off the graph alone:
14
+ The graph **forms the hypothesis** (which files/symbols matter). The rungs above still **verify** it — never edit off the graph alone:
15
15
 
16
16
  - On a stale index or ambiguous name, codegraph can return the wrong symbol while claiming it's exact. Confirm the concrete span with `Grep`/`Read` before writing, especially in critical areas.
17
17
  - Treat its blast-radius / "tests found" as a hint, not a coverage gate — the repo's real tests decide.