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.
- package/dist/assets/core/core-assets/hooks/guard-destructive.sh +10 -10
- package/dist/assets/core/core-assets/hooks/quality-gate-pre-commit.sh +5 -5
- package/dist/assets/core/core-assets/lib-skills/citty.md +74 -0
- package/dist/assets/core/core-assets/lib-skills/clack.md +81 -0
- package/dist/assets/core/core-assets/lib-skills/jest.md +87 -0
- package/dist/assets/core/core-assets/lib-skills/playwright.md +66 -0
- package/dist/assets/core/core-assets/lib-skills/supertest.md +69 -0
- package/dist/assets/core/core-assets/lib-skills/testing-library.md +66 -0
- package/dist/assets/core/core-assets/lib-skills/vitest.md +67 -0
- package/dist/assets/core/core-assets/managed/formato-respuesta.md +1 -1
- package/dist/assets/plugins/acli/plugin.json +1 -2
- package/dist/assets/plugins/codegraph/plugin.json +1 -2
- package/dist/assets/plugins/codegraph/skills/codegraph-rung.md +2 -2
- package/dist/assets/plugins/engram/plugin.json +1 -2
- package/dist/assets/plugins/gh/plugin.json +0 -1
- package/dist/assets/plugins/jscpd/managed/jscpd-protocol.md +1 -1
- package/dist/assets/plugins/jscpd/scripts/check-jscpd.sh +2 -2
- package/dist/assets/plugins/semgrep/managed/semgrep-protocol.md +3 -2
- package/dist/assets/plugins/semgrep/plugin.json +1 -2
- package/dist/assets/plugins/semgrep/scripts/check-semgrep.sh +2 -2
- package/dist/index.js +289 -286
- package/package.json +3 -2
|
@@ -33,9 +33,9 @@ set -euo pipefail
|
|
|
33
33
|
base={{shq:branchBase}}
|
|
34
34
|
|
|
35
35
|
block() {
|
|
36
|
-
echo "[navori]
|
|
37
|
-
echo "[navori]
|
|
38
|
-
echo "[navori]
|
|
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
|
|
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
|
|
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
|
|
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 "
|
|
106
|
+
block "direct write to a block device"
|
|
107
107
|
fi
|
|
108
108
|
|
|
109
109
|
# navori:user-section
|
|
110
|
-
# user:
|
|
111
|
-
#
|
|
112
|
-
#
|
|
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'
|
|
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
|
|
139
|
-
echo "[navori] Commit
|
|
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:
|
|
147
|
-
#
|
|
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): ...`),
|
|
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
|
|
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
|
|
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.
|