@roopesh.yadava/qa-pack 1.2.0 → 1.4.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/README.md +16 -3
- package/bin/postinstall.js +31 -0
- package/claude/skills/SKILLS_CONTEXT.md +10 -0
- package/claude/skills/k6-framework-scaffold/SKILL.md +58 -0
- package/claude/skills/k6-framework-scaffold/scaffold.sh +388 -0
- package/claude/skills/manual-testing/SKILL.md +15 -1
- package/claude/skills/qa-agent/SKILL.md +17 -0
- package/package.json +5 -2
- package/templates/.env.example +4 -0
- package/templates/settings.local.json.example +1 -5
package/README.md
CHANGED
|
@@ -31,6 +31,13 @@ After installing:
|
|
|
31
31
|
(plus `JIRA_BASE_URL` / `JIRA_EMAIL` / `JIRA_API_TOKEN` if you want bug screenshots attached).
|
|
32
32
|
2. Open `CLAUDE.md` and fill in the non-secret project facts (Jira key, environment, auth method).
|
|
33
33
|
|
|
34
|
+
`.claude/settings.local.json` (seeded from `settings.local.json.example`, and re-checked on
|
|
35
|
+
every install/update) auto-approves Playwright MCP tool calls, since those run constantly
|
|
36
|
+
during test execution and are scoped to the app under test. Every Atlassian/Jira MCP call —
|
|
37
|
+
reading a card, filing a bug, commenting, transitioning — and any action needing your input
|
|
38
|
+
(filing a bug, publishing a charter, overwriting product context) still prompts for
|
|
39
|
+
confirmation.
|
|
40
|
+
|
|
34
41
|
Then open the repo in Claude Code and run:
|
|
35
42
|
|
|
36
43
|
```
|
|
@@ -43,8 +50,13 @@ Every run follows the same structured flow:
|
|
|
43
50
|
|
|
44
51
|
1. **Card input** — give a Jira card ID (`run qa PROJ-123`)
|
|
45
52
|
2. **Product context** — saved context + the Jira card are loaded automatically
|
|
46
|
-
3. **
|
|
47
|
-
|
|
53
|
+
3. **Model recommendation** — a one-line tier suggestion based on the card's Jira issue type
|
|
54
|
+
(Story/Feature/Epic → default high-capability model, Task/Bug/Sub-task → a lighter model
|
|
55
|
+
like Haiku). Informational only — switch with `/model` if you want, or ignore it; set
|
|
56
|
+
`QA_MODEL_ROUTING=off` in `.env` to skip it. See `SKILLS_CONTEXT.md` for the opt-in
|
|
57
|
+
subagent-override pattern.
|
|
58
|
+
4. **Gap questions** — you're asked only what isn't already known (one message)
|
|
59
|
+
5. **Phase selection**:
|
|
48
60
|
- **Phase 1 — Manual testing** (UI/Figma diff → browser tests → bugs → charter)
|
|
49
61
|
- **Phase 2 — Write automation** (reuse audit → Gherkin → steps → POM → real run + self-heal)
|
|
50
62
|
- **Phase 3 — Both** (manual first — its findings feed the automation)
|
|
@@ -79,6 +91,7 @@ and `/write-acceptance-criteria`, which are also available as explicit slash com
|
|
|
79
91
|
| `delete files` / `clean up outputs` | delete-files | Prompts to delete/keep files in `outputs/` |
|
|
80
92
|
| `/write-acceptance-criteria PROJ-123` | write-acceptance-criteria | Generates AC, appends to the Jira card description |
|
|
81
93
|
| `/impacted-tests` / `which tests are impacted by this pull` | impacted-tests | After pulling dev changes into a test branch, reports which Cucumber feature files are at risk — report-only, no card needed |
|
|
94
|
+
| `set up k6` / `scaffold performance tests` | k6-framework-scaffold | Scaffolds a `k6-performance-tests/` framework (Grafana Cloud, protocol + optional browser layers) with commented templates to fill in — no card needed |
|
|
82
95
|
|
|
83
96
|
## What postinstall does
|
|
84
97
|
|
|
@@ -91,7 +104,7 @@ and `/write-acceptance-criteria`, which are also available as explicit slash com
|
|
|
91
104
|
| `.claude/settings.json` | Created once, never overwritten |
|
|
92
105
|
| `CLAUDE.md`, `.mcp.json`, `cucumber.cjs`, `.env` | Created once, never overwritten |
|
|
93
106
|
| `.env.example` | Always refreshed (shows latest env keys) |
|
|
94
|
-
| `.claude/settings.local.json` | Created once from example |
|
|
107
|
+
| `.claude/settings.local.json` | Created once from example. Every subsequent install/update also checks for the Playwright MCP auto-approve rule (`mcp__playwright`) and adds it if missing — merged in without touching any other key you've set in the file. If the file isn't valid JSON, this merge is skipped with a warning and the file is left untouched. |
|
|
95
108
|
| `.claude/settings.local.json.example` | Always refreshed (shows latest options) |
|
|
96
109
|
| `.gitignore` | Managed `# >>> qa-pack` block regenerated on every install — ignores all pack-installed skills/commands plus `outputs/`, session files, and local settings |
|
|
97
110
|
|
package/bin/postinstall.js
CHANGED
|
@@ -104,6 +104,37 @@ copyFile(
|
|
|
104
104
|
{ overwrite: true }
|
|
105
105
|
);
|
|
106
106
|
|
|
107
|
+
// ── 4b. Force-ensure the Playwright MCP auto-approve rule ─────────────────────
|
|
108
|
+
// Runs on every install/update, whether settings.local.json was just created above,
|
|
109
|
+
// already existed from an older pack version that predates this rule, or was hand-
|
|
110
|
+
// edited. Merges in only this one permissions.allow entry — every other key/value in
|
|
111
|
+
// the file (including any other allow/deny/ask rules) is left exactly as it was.
|
|
112
|
+
const PLAYWRIGHT_MCP_RULE = 'mcp__playwright';
|
|
113
|
+
if (fs.existsSync(localSettingsDest)) {
|
|
114
|
+
try {
|
|
115
|
+
const settings = JSON.parse(fs.readFileSync(localSettingsDest, 'utf8'));
|
|
116
|
+
if (typeof settings.permissions !== 'object' || settings.permissions === null) {
|
|
117
|
+
settings.permissions = {};
|
|
118
|
+
}
|
|
119
|
+
if (!Array.isArray(settings.permissions.allow)) {
|
|
120
|
+
settings.permissions.allow = [];
|
|
121
|
+
}
|
|
122
|
+
const hasRule = settings.permissions.allow.some(
|
|
123
|
+
(rule) => rule === PLAYWRIGHT_MCP_RULE || rule === `${PLAYWRIGHT_MCP_RULE}__*`
|
|
124
|
+
);
|
|
125
|
+
if (!hasRule) {
|
|
126
|
+
settings.permissions.allow.push(PLAYWRIGHT_MCP_RULE);
|
|
127
|
+
fs.writeFileSync(localSettingsDest, `${JSON.stringify(settings, null, 2)}\n`);
|
|
128
|
+
log.updated.push(`${path.relative(PROJECT_ROOT, localSettingsDest)} (added Playwright MCP auto-approve rule)`);
|
|
129
|
+
}
|
|
130
|
+
} catch (e) {
|
|
131
|
+
console.warn(
|
|
132
|
+
` Warning: could not parse ${path.relative(PROJECT_ROOT, localSettingsDest)} as JSON — left untouched.\n` +
|
|
133
|
+
` Add "${PLAYWRIGHT_MCP_RULE}" to its permissions.allow array manually to auto-approve Playwright MCP calls.`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
107
138
|
// ── 5. One-time templates (never overwrite — user fills these in) ─────────────
|
|
108
139
|
const templates = [
|
|
109
140
|
['templates/CLAUDE.md', 'CLAUDE.md'],
|
|
@@ -210,6 +210,16 @@ Also check `token_usage_log.json` for `cache_read_input_tokens` per phase to see
|
|
|
210
210
|
Target: `cache_efficiency_pct` ≥ 70 after the first run on a given product.
|
|
211
211
|
First run will always be 0% (cold cache). Second and subsequent runs should cache the stable block (product context + skill instructions).
|
|
212
212
|
|
|
213
|
+
## Model Routing (optional)
|
|
214
|
+
|
|
215
|
+
`QA_MODEL_ROUTING` in `.env` controls how qa-agent responds to Jira issue type at Step 1c:
|
|
216
|
+
|
|
217
|
+
| Value | Behavior |
|
|
218
|
+
|-------|----------|
|
|
219
|
+
| unset or `recommend` (default) | qa-agent prints a one-line model-tier recommendation (Story/Feature/Epic → high-capability, Task/Bug/Sub-task → lighter model) after fetching the card, then continues on whatever model is already active. Portable — depends on no special tooling. |
|
|
220
|
+
| `off` | Skip the recommendation line entirely. |
|
|
221
|
+
| `subagent` (advanced, opt-in — not wired up by the shipped skills) | For Claude Code setups where phase dispatch runs through a subagent/Task-style tool that accepts a `model` parameter: adapt qa-agent's Step 4 dispatch to pass `model: haiku` for Task/Bug and the default model for Story/Feature when invoking `manual-testing` / `automation`. This is a pattern to wire into your own environment's actual subagent tool — qa-pack itself doesn't assume one exists, since not every Claude Code setup qa-pack installs into exposes the same dispatch mechanism. |
|
|
222
|
+
|
|
213
223
|
## Artifact Locations
|
|
214
224
|
|
|
215
225
|
| Artifact | Path |
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: k6-framework-scaffold
|
|
3
|
+
description: Scaffold a ready-to-use k6 load-testing framework (Grafana Cloud) in a repo. Creates the k6-performance-tests/ tree — shared config/ + lib/, a protocol/ (API load) layer with api/flows/tests, and an OPTIONAL browser/ (UI) layer — with commented template files that testers then fill in. Use when someone asks to "set up k6", "create the k6 load testing structure/framework", "scaffold performance tests", or start load testing a new app.
|
|
4
|
+
compatibility: >
|
|
5
|
+
Standalone skill — not part of the qa-agent pipeline. The bundled scaffold.sh only writes
|
|
6
|
+
files; the k6 CLI (and a Grafana Cloud account for `k6 cloud run`) is only needed later,
|
|
7
|
+
when the tester actually runs the generated tests.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# k6 Framework Scaffold
|
|
11
|
+
|
|
12
|
+
Generates a standard, reusable k6 performance-testing framework so a tester can start from a
|
|
13
|
+
ready structure and just fill in their app's endpoints, weights, and tokens.
|
|
14
|
+
|
|
15
|
+
## What it creates
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
k6-performance-tests/
|
|
19
|
+
├── config/ env.js · secrets.js · profiles.js (shared: URLs, tokens, load shapes)
|
|
20
|
+
├── lib/ http.js · rand.js · browser.js (shared helpers)
|
|
21
|
+
├── protocol/ api/{userTypeA,userTypeB}/ · flows/ · tests/{smoke,load,spike,breakpoint,endurance}/
|
|
22
|
+
│ ── API / HTTP load — the CORE, needed for every app
|
|
23
|
+
├── browser/ pages/ · flows/ · tests/
|
|
24
|
+
│ ── real-browser UX — OPTIONAL, only for UI-heavy apps (delete if unneeded)
|
|
25
|
+
└── README.md
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Design principle: **separate WHAT we test (api → flows) from HOW MUCH load (profiles → tests)**,
|
|
29
|
+
so the same endpoints/journeys are reused across every test type. All files are commented
|
|
30
|
+
templates with `TODO`s.
|
|
31
|
+
|
|
32
|
+
## How to run it
|
|
33
|
+
|
|
34
|
+
From the target repo root, run the bundled script:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bash .claude/skills/k6-framework-scaffold/scaffold.sh
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- It creates `k6-performance-tests/` in the current directory.
|
|
41
|
+
- It is **safe**: if `k6-performance-tests/` already exists it aborts (won't overwrite).
|
|
42
|
+
Pass `--force` to scaffold into an existing folder (only adds missing files; still never
|
|
43
|
+
overwrites an existing file).
|
|
44
|
+
|
|
45
|
+
After it runs, tell the tester the next steps:
|
|
46
|
+
1. `config/env.js` — set `BASE` (API URL), `UI_BASE`s, `PROJECT_ID`, scale numbers, test data.
|
|
47
|
+
2. `config/secrets.js` — paste auth tokens/credentials (git-ignored pattern).
|
|
48
|
+
3. `protocol/api/**` — replace the sample endpoints with the real ones (one function per call).
|
|
49
|
+
4. `protocol/flows/**` — set the weighted action table (feature-usage %) + thresholds (SLA).
|
|
50
|
+
5. Browser layer is **optional** — keep it only for UI-heavy apps; otherwise delete `browser/`.
|
|
51
|
+
6. Install k6, `k6 cloud login --token <token>`, then run from inside `k6-performance-tests/`:
|
|
52
|
+
`k6 run protocol/tests/smoke/example.smoke.js` (local) or `k6 cloud run ...`.
|
|
53
|
+
|
|
54
|
+
## Notes
|
|
55
|
+
- The script only writes files; it does not install k6 or run tests.
|
|
56
|
+
- Keep real tokens out of version control (the scaffold's `.gitignore` covers `secrets.local.*`).
|
|
57
|
+
- If the user wants the structure tailored (their user types, real endpoints from a Postman
|
|
58
|
+
collection), scaffold first, then edit the generated templates to match.
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# k6-framework-scaffold — create a ready-to-use k6 load-testing framework in the CURRENT repo.
|
|
3
|
+
# Usage: bash .claude/skills/k6-framework-scaffold/scaffold.sh [--force]
|
|
4
|
+
# (no flag) aborts if k6-performance-tests/ already exists.
|
|
5
|
+
# --force scaffolds into an existing folder; still never overwrites an existing file.
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
ROOT="k6-performance-tests"
|
|
9
|
+
FORCE="${1:-}"
|
|
10
|
+
|
|
11
|
+
if [ -d "$ROOT" ] && [ "$FORCE" != "--force" ]; then
|
|
12
|
+
echo "ERROR: $ROOT/ already exists. Re-run with --force to add only missing files." >&2
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# w <relative-path> : write heredoc (stdin) to $ROOT/<path>, only if the file doesn't exist.
|
|
17
|
+
w() {
|
|
18
|
+
local path="$1"
|
|
19
|
+
if [ -f "$ROOT/$path" ]; then echo " skip (exists): $path"; cat >/dev/null; return; fi
|
|
20
|
+
mkdir -p "$ROOT/$(dirname "$path")"
|
|
21
|
+
cat > "$ROOT/$path"
|
|
22
|
+
echo " created: $path"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
echo "Scaffolding $ROOT/ ..."
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------- config/
|
|
28
|
+
w config/env.js <<'EOF'
|
|
29
|
+
// config/env.js — shared configuration. Fill these in for your app + environment.
|
|
30
|
+
|
|
31
|
+
export const BASE = 'https://api.example.com/v1'; // TODO: API base URL
|
|
32
|
+
export const UI_BASE = 'https://app.example.com'; // TODO: web-app URL (browser tests only)
|
|
33
|
+
|
|
34
|
+
export const PROJECT_ID = 0; // TODO: Grafana Cloud project id
|
|
35
|
+
|
|
36
|
+
// Target concurrency — the production peak you reproduce. Rescale by editing these numbers.
|
|
37
|
+
export const CONCURRENT = { userTypeA: 100, userTypeB: 100 };
|
|
38
|
+
export const TOTAL = {
|
|
39
|
+
userTypeAVUs: CONCURRENT.userTypeA,
|
|
40
|
+
userTypeBVUs: CONCURRENT.userTypeB,
|
|
41
|
+
totalVUs: CONCURRENT.userTypeA + CONCURRENT.userTypeB,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Credentials + ids used by auth and parameterised endpoints.
|
|
45
|
+
export const CREDS = { username: 'TODO', password: 'TODO', otp: '000000' };
|
|
46
|
+
export const TEST_DATA = { sampleId: 'TODO' };
|
|
47
|
+
|
|
48
|
+
// Include mutating (write) endpoints in the mix? They modify real data on the target env.
|
|
49
|
+
export const ENABLE_WRITES = false;
|
|
50
|
+
|
|
51
|
+
// Long-lived auth token(s) live in secrets.js (rotate per run).
|
|
52
|
+
export { ACCESS_TOKEN } from './secrets.js';
|
|
53
|
+
EOF
|
|
54
|
+
|
|
55
|
+
w config/secrets.js <<'EOF'
|
|
56
|
+
// config/secrets.js — auth material, refreshed before each run.
|
|
57
|
+
// ⚠️ Keep production tokens OUT of version control. Use short-lived non-prod tokens.
|
|
58
|
+
export const ACCESS_TOKEN = 'Bearer TODO';
|
|
59
|
+
EOF
|
|
60
|
+
|
|
61
|
+
w config/profiles.js <<'EOF'
|
|
62
|
+
// config/profiles.js — reusable LOAD PROFILES (the "how much"). A test wires a flow to one.
|
|
63
|
+
|
|
64
|
+
export function loadProfile({ exec, target, rampUp = '2m', hold = '15m', rampDown = '2m', startVUs = 0 }) {
|
|
65
|
+
return { executor: 'ramping-vus', exec, startVUs, stages: [
|
|
66
|
+
{ duration: rampUp, target }, { duration: hold, target }, { duration: rampDown, target: 0 },
|
|
67
|
+
] };
|
|
68
|
+
}
|
|
69
|
+
export function arrivalRateProfile({ exec, rate, timeUnit = '1m', duration = '15m', preAllocatedVUs = 50, maxVUs = 100 }) {
|
|
70
|
+
return { executor: 'constant-arrival-rate', exec, rate, timeUnit, duration, preAllocatedVUs, maxVUs };
|
|
71
|
+
}
|
|
72
|
+
export function spikeProfile({ exec, target, rampUp = '30s', hold = '1m', rampDown = '30s', startVUs = 0 }) {
|
|
73
|
+
return { executor: 'ramping-vus', exec, startVUs, stages: [
|
|
74
|
+
{ duration: rampUp, target }, { duration: hold, target }, { duration: rampDown, target: 0 },
|
|
75
|
+
] };
|
|
76
|
+
}
|
|
77
|
+
export function breakpointProfile({ exec, maxRate, ramp = '30m', timeUnit = '1m', preAllocatedVUs = 50, maxVUs = 200 }) {
|
|
78
|
+
return { executor: 'ramping-arrival-rate', exec, startRate: 0, timeUnit, preAllocatedVUs, maxVUs,
|
|
79
|
+
stages: [{ duration: ramp, target: maxRate }] };
|
|
80
|
+
}
|
|
81
|
+
export function smokeProfile({ exec, vus = 1, iterations = 1 }) {
|
|
82
|
+
return { executor: 'per-vu-iterations', exec, vus, iterations, maxDuration: '1m' };
|
|
83
|
+
}
|
|
84
|
+
export function enduranceProfile({ exec, target, rampUp = '2m', hold = '2h', rampDown = '2m', startVUs = 0 }) {
|
|
85
|
+
return { executor: 'ramping-vus', exec, startVUs, stages: [
|
|
86
|
+
{ duration: rampUp, target }, { duration: hold, target }, { duration: rampDown, target: 0 },
|
|
87
|
+
] };
|
|
88
|
+
}
|
|
89
|
+
EOF
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------- lib/
|
|
92
|
+
w lib/http.js <<'EOF'
|
|
93
|
+
// lib/http.js — shared request headers + response handling.
|
|
94
|
+
import { check } from 'k6';
|
|
95
|
+
|
|
96
|
+
// `token` is the full Bearer string. Each flow passes its own token.
|
|
97
|
+
export function authHeaders(token, extra = {}) {
|
|
98
|
+
return { Authorization: token, 'Content-Type': 'application/json', ...extra };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2xx check; logs 4xx (client-side) with a short body snippet. 5xx tracked by error-rate metric.
|
|
102
|
+
export function checkResponse(res, name) {
|
|
103
|
+
check(res, { [`${name} ok`]: (r) => r.status >= 200 && r.status < 300 });
|
|
104
|
+
if (res.status >= 400 && res.status < 500) {
|
|
105
|
+
const body = String(res.body || '').replace(/\s+/g, ' ').slice(0, 300);
|
|
106
|
+
console.error(`FAIL ${name} | ${res.status} ${res.request.method} ${res.request.url} | ${body}`);
|
|
107
|
+
}
|
|
108
|
+
return res;
|
|
109
|
+
}
|
|
110
|
+
EOF
|
|
111
|
+
|
|
112
|
+
w lib/rand.js <<'EOF'
|
|
113
|
+
// lib/rand.js — deterministic pseudo-random for load distribution (NOT cryptographic; and
|
|
114
|
+
// intentionally not the JS built-in RNG, which trips security scans). Reproducible runs.
|
|
115
|
+
|
|
116
|
+
// xorshift32 -> fraction in [0,1). seed = any integer (e.g. the iteration index).
|
|
117
|
+
export function frac(seed) {
|
|
118
|
+
let x = (seed >>> 0) || 1;
|
|
119
|
+
x ^= x << 13; x >>>= 0; x ^= x >>> 17; x ^= x << 5; x >>>= 0;
|
|
120
|
+
return (x >>> 0) / 4294967296;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Pick a weighted action given the total weight and a value u in [0,1).
|
|
124
|
+
export function weightedPick(actions, totalWeight, u) {
|
|
125
|
+
const target = u * totalWeight; let acc = 0;
|
|
126
|
+
for (let i = 0; i < actions.length; i++) { acc += actions[i].weight; if (target <= acc) return actions[i]; }
|
|
127
|
+
return actions[actions.length - 1];
|
|
128
|
+
}
|
|
129
|
+
EOF
|
|
130
|
+
|
|
131
|
+
w lib/browser.js <<'EOF'
|
|
132
|
+
// lib/browser.js — checkpoints for browser/ (real-Chromium) tests.
|
|
133
|
+
import { check } from 'k6';
|
|
134
|
+
|
|
135
|
+
// Element visible within timeout ms (a "stuck UI" surfaces here as a timeout -> failure).
|
|
136
|
+
export async function expectVisible(page, selector, name, timeout = 10000) {
|
|
137
|
+
let ok = false;
|
|
138
|
+
try { await page.locator(selector).waitFor({ state: 'visible', timeout }); ok = true; } catch (e) { ok = false; }
|
|
139
|
+
check(ok, { [`${name} visible`]: (v) => v === true });
|
|
140
|
+
return ok;
|
|
141
|
+
}
|
|
142
|
+
// Element gone within timeout ms (e.g. a spinner cleared).
|
|
143
|
+
export async function expectHidden(page, selector, name, timeout = 15000) {
|
|
144
|
+
let ok = false;
|
|
145
|
+
try { await page.locator(selector).waitFor({ state: 'hidden', timeout }); ok = true; } catch (e) { ok = false; }
|
|
146
|
+
check(ok, { [`${name} settled`]: (v) => v === true });
|
|
147
|
+
return ok;
|
|
148
|
+
}
|
|
149
|
+
// Best-effort screenshot (never fails the iteration).
|
|
150
|
+
export async function shot(page, path) { try { await page.screenshot({ path }); } catch (e) {} }
|
|
151
|
+
EOF
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------- protocol/api/
|
|
154
|
+
w protocol/api/exampleUser/auth.js <<'EOF'
|
|
155
|
+
// protocol/api/exampleUser/auth.js — token-issuing login. Adapt to your auth, or delete if
|
|
156
|
+
// you only use a pre-issued static token (config/secrets.js).
|
|
157
|
+
import http from 'k6/http';
|
|
158
|
+
import { BASE, CREDS } from '../../../config/env.js';
|
|
159
|
+
import { checkResponse } from '../../../lib/http.js';
|
|
160
|
+
|
|
161
|
+
const JSON_HEADERS = { 'Content-Type': 'application/json' };
|
|
162
|
+
|
|
163
|
+
// Example: POST /auth/sign-in -> returns a session (for a two-step OTP flow) or a token.
|
|
164
|
+
export function signIn(tags) {
|
|
165
|
+
const res = http.post(`${BASE}/auth/sign-in`,
|
|
166
|
+
JSON.stringify({ username: CREDS.username, password: CREDS.password }),
|
|
167
|
+
{ headers: JSON_HEADERS, tags: { name: 'auth-sign-in', ...tags } });
|
|
168
|
+
checkResponse(res, 'auth-sign-in');
|
|
169
|
+
try { return res.json('data.session'); } catch (e) { return null; }
|
|
170
|
+
}
|
|
171
|
+
// If login is two-step (OTP): read the session from signIn() and feed it into a verify() here.
|
|
172
|
+
EOF
|
|
173
|
+
|
|
174
|
+
w protocol/api/exampleUser/sample.js <<'EOF'
|
|
175
|
+
// protocol/api/exampleUser/sample.js — ENDPOINTS: one function per API call. Copy this shape
|
|
176
|
+
// for each endpoint (add a unique `name` tag so it shows per-endpoint in Grafana).
|
|
177
|
+
import http from 'k6/http';
|
|
178
|
+
import { BASE, TEST_DATA } from '../../../config/env.js';
|
|
179
|
+
import { authHeaders, checkResponse } from '../../../lib/http.js';
|
|
180
|
+
|
|
181
|
+
export function list(token, tags) {
|
|
182
|
+
const res = http.get(`${BASE}/sample/list?page=1&limit=10`,
|
|
183
|
+
{ headers: authHeaders(token), tags: { name: 'sample-list', ...tags } });
|
|
184
|
+
return checkResponse(res, 'sample-list');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function get(token, tags) {
|
|
188
|
+
const res = http.get(`${BASE}/sample/get/${TEST_DATA.sampleId}`,
|
|
189
|
+
{ headers: authHeaders(token), tags: { name: 'sample-get', ...tags } });
|
|
190
|
+
return checkResponse(res, 'sample-get');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ⚠️ WRITE example — mark write:true in the flow so ENABLE_WRITES can gate it.
|
|
194
|
+
// export function create(token, tags) {
|
|
195
|
+
// const res = http.post(`${BASE}/sample`, JSON.stringify({ /* ... */ }),
|
|
196
|
+
// { headers: authHeaders(token), tags: { name: 'sample-create', ...tags } });
|
|
197
|
+
// return checkResponse(res, 'sample-create');
|
|
198
|
+
// }
|
|
199
|
+
EOF
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------- protocol/flows/
|
|
202
|
+
w protocol/flows/exampleUserFlow.js <<'EOF'
|
|
203
|
+
// protocol/flows/exampleUserFlow.js — JOURNEY: a weighted API traffic-split for one user
|
|
204
|
+
// population. Each iteration a VU performs ONE weighted action; over the run the request mix
|
|
205
|
+
// converges to these proportions (= the client's feature-usage %).
|
|
206
|
+
import { sleep } from 'k6';
|
|
207
|
+
import exec from 'k6/execution';
|
|
208
|
+
import { ACCESS_TOKEN, ENABLE_WRITES } from '../../config/env.js';
|
|
209
|
+
import { frac, weightedPick } from '../../lib/rand.js';
|
|
210
|
+
import * as sample from '../api/exampleUser/sample.js';
|
|
211
|
+
|
|
212
|
+
// Pass/fail thresholds (from the client's SLA).
|
|
213
|
+
export const THRESHOLDS = {
|
|
214
|
+
http_req_duration: ['p(95)<2000'], // TODO: SLA
|
|
215
|
+
http_req_failed: ['rate<0.01'],
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// weight = share of total traffic (sum ~ 1.0). write:true = mutating (gated by ENABLE_WRITES).
|
|
219
|
+
const ACTIONS = [
|
|
220
|
+
{ weight: 0.7, run: (t) => sample.list(t) },
|
|
221
|
+
{ weight: 0.3, run: (t) => sample.get(t) },
|
|
222
|
+
// { weight: 0.1, write: true, run: (t) => sample.create(t) },
|
|
223
|
+
];
|
|
224
|
+
const ACTIVE = ENABLE_WRITES ? ACTIONS : ACTIONS.filter((a) => !a.write);
|
|
225
|
+
const TOTAL_WEIGHT = ACTIVE.reduce((s, a) => s + a.weight, 0);
|
|
226
|
+
|
|
227
|
+
export function exampleUserFlow() {
|
|
228
|
+
const token = ACCESS_TOKEN;
|
|
229
|
+
const seed = exec.scenario.iterationInTest + 1;
|
|
230
|
+
weightedPick(ACTIVE, TOTAL_WEIGHT, frac(seed)).run(token);
|
|
231
|
+
sleep(1 + frac(seed ^ 0x5bd1e995) * 2); // think time 1-3s
|
|
232
|
+
}
|
|
233
|
+
EOF
|
|
234
|
+
|
|
235
|
+
# ---------------------------------------------------------------- protocol/tests/
|
|
236
|
+
w protocol/tests/smoke/example.smoke.js <<'EOF'
|
|
237
|
+
// protocol/tests/smoke/example.smoke.js — quick "does it work?" before a big run.
|
|
238
|
+
import { PROJECT_ID } from '../../../config/env.js';
|
|
239
|
+
import { smokeProfile } from '../../../config/profiles.js';
|
|
240
|
+
import { THRESHOLDS } from '../../flows/exampleUserFlow.js';
|
|
241
|
+
export { exampleUserFlow } from '../../flows/exampleUserFlow.js';
|
|
242
|
+
|
|
243
|
+
export const options = {
|
|
244
|
+
cloud: { projectID: PROJECT_ID, name: 'MyApp API — Smoke' },
|
|
245
|
+
scenarios: { smoke: smokeProfile({ exec: 'exampleUserFlow', vus: 1, iterations: 1 }) },
|
|
246
|
+
thresholds: THRESHOLDS,
|
|
247
|
+
};
|
|
248
|
+
EOF
|
|
249
|
+
|
|
250
|
+
w protocol/tests/load/example.load.js <<'EOF'
|
|
251
|
+
// protocol/tests/load/example.load.js — the expected-peak run.
|
|
252
|
+
import { PROJECT_ID, TOTAL } from '../../../config/env.js';
|
|
253
|
+
import { loadProfile } from '../../../config/profiles.js';
|
|
254
|
+
import { THRESHOLDS } from '../../flows/exampleUserFlow.js';
|
|
255
|
+
export { exampleUserFlow } from '../../flows/exampleUserFlow.js';
|
|
256
|
+
|
|
257
|
+
export const options = {
|
|
258
|
+
cloud: { projectID: PROJECT_ID, name: 'MyApp API — Load' },
|
|
259
|
+
scenarios: {
|
|
260
|
+
users: loadProfile({ exec: 'exampleUserFlow', target: TOTAL.userTypeAVUs, hold: '15m' }),
|
|
261
|
+
},
|
|
262
|
+
thresholds: THRESHOLDS,
|
|
263
|
+
};
|
|
264
|
+
EOF
|
|
265
|
+
|
|
266
|
+
w protocol/tests/spike/README.md <<'EOF'
|
|
267
|
+
# Spike tests
|
|
268
|
+
Copy a load test here and swap the profile to `spikeProfile` (config/profiles.js) — a sudden
|
|
269
|
+
burst then a quick drop, to check the system survives/recovers from a surge.
|
|
270
|
+
EOF
|
|
271
|
+
|
|
272
|
+
w protocol/tests/breakpoint/README.md <<'EOF'
|
|
273
|
+
# Breakpoint tests
|
|
274
|
+
Ramp load upward until it breaks (find capacity). Use `breakpointProfile` and pair with an
|
|
275
|
+
`abortOnFail` threshold so the run stops at the breaking point.
|
|
276
|
+
EOF
|
|
277
|
+
|
|
278
|
+
w protocol/tests/endurance/README.md <<'EOF'
|
|
279
|
+
# Endurance (soak) tests
|
|
280
|
+
Moderate, sustained load for hours (memory leaks, resource growth). Use `enduranceProfile`.
|
|
281
|
+
EOF
|
|
282
|
+
|
|
283
|
+
# ---------------------------------------------------------------- browser/ (OPTIONAL)
|
|
284
|
+
w browser/pages/exampleApp/loginPage.js <<'EOF'
|
|
285
|
+
// browser/pages/exampleApp/loginPage.js — PAGE OBJECT (selectors + actions). Fill selectors
|
|
286
|
+
// from the real DOM/source. SPA-safe: wait on the next screen's element, not full navigation.
|
|
287
|
+
import { UI_BASE, CREDS } from '../../../config/env.js';
|
|
288
|
+
|
|
289
|
+
const SEL = {
|
|
290
|
+
username: '#username', // TODO
|
|
291
|
+
password: '#password', // TODO
|
|
292
|
+
submit: '[data-testid="submit"]', // TODO
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
export async function login(page) {
|
|
296
|
+
await page.goto(`${UI_BASE}/login`, { waitUntil: 'networkidle' });
|
|
297
|
+
await page.locator(SEL.username).fill(CREDS.username);
|
|
298
|
+
await page.locator(SEL.password).fill(CREDS.password);
|
|
299
|
+
await page.locator(SEL.submit).click();
|
|
300
|
+
}
|
|
301
|
+
EOF
|
|
302
|
+
|
|
303
|
+
w browser/pages/exampleApp/samplePage.js <<'EOF'
|
|
304
|
+
// browser/pages/exampleApp/samplePage.js — checkpoints for a screen.
|
|
305
|
+
import { expectVisible } from '../../../lib/browser.js';
|
|
306
|
+
const SEL = { root: '[data-testid="page-root"]' }; // TODO
|
|
307
|
+
export async function assertLoaded(page) {
|
|
308
|
+
return expectVisible(page, SEL.root, 'sample-page', 15000);
|
|
309
|
+
}
|
|
310
|
+
EOF
|
|
311
|
+
|
|
312
|
+
w browser/flows/exampleUiFlow.js <<'EOF'
|
|
313
|
+
// browser/flows/exampleUiFlow.js — UI JOURNEY (real browser): login -> a screen renders.
|
|
314
|
+
import { browser } from 'k6/browser';
|
|
315
|
+
import * as loginPage from '../pages/exampleApp/loginPage.js';
|
|
316
|
+
import * as samplePage from '../pages/exampleApp/samplePage.js';
|
|
317
|
+
import { shot } from '../../lib/browser.js';
|
|
318
|
+
|
|
319
|
+
export const UI_THRESHOLDS = {
|
|
320
|
+
checks: ['rate>0.95'],
|
|
321
|
+
browser_web_vital_lcp: ['p(90)<2500'], // TODO: tune to UX SLA
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
export async function exampleUiJourney() {
|
|
325
|
+
const page = await browser.newPage();
|
|
326
|
+
try {
|
|
327
|
+
await loginPage.login(page);
|
|
328
|
+
const ok = await samplePage.assertLoaded(page);
|
|
329
|
+
await shot(page, `screenshots/example-${ok ? 'ok' : 'FAIL'}.png`);
|
|
330
|
+
} finally {
|
|
331
|
+
await page.close();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
EOF
|
|
335
|
+
|
|
336
|
+
w browser/tests/exampleUi.browser.js <<'EOF'
|
|
337
|
+
// browser/tests/exampleUi.browser.js — run a FEW real-browser VUs (each is a real Chromium,
|
|
338
|
+
// so keep counts small). Headless by default; K6_BROWSER_HEADLESS=false to watch locally.
|
|
339
|
+
import { PROJECT_ID } from '../../config/env.js';
|
|
340
|
+
import { UI_THRESHOLDS } from '../flows/exampleUiFlow.js';
|
|
341
|
+
export { exampleUiJourney } from '../flows/exampleUiFlow.js';
|
|
342
|
+
|
|
343
|
+
export const options = {
|
|
344
|
+
cloud: { projectID: PROJECT_ID, name: 'MyApp UI — Example Journey (browser)' },
|
|
345
|
+
scenarios: {
|
|
346
|
+
ui: {
|
|
347
|
+
executor: 'per-vu-iterations', exec: 'exampleUiJourney', vus: 1, iterations: 1,
|
|
348
|
+
maxDuration: '3m', options: { browser: { type: 'chromium' } },
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
thresholds: UI_THRESHOLDS,
|
|
352
|
+
};
|
|
353
|
+
EOF
|
|
354
|
+
|
|
355
|
+
# ---------------------------------------------------------------- root files
|
|
356
|
+
w .gitignore <<'EOF'
|
|
357
|
+
config/secrets.local.js
|
|
358
|
+
.env
|
|
359
|
+
*.k6-summary.json
|
|
360
|
+
summary.json
|
|
361
|
+
screenshots/
|
|
362
|
+
EOF
|
|
363
|
+
|
|
364
|
+
w README.md <<'EOF'
|
|
365
|
+
# k6 Performance & UX Test Framework
|
|
366
|
+
|
|
367
|
+
Two layers, sharing `config/` + `lib/`:
|
|
368
|
+
- **protocol/** — API / HTTP load (the CORE, every app). api/ (endpoints) -> flows/ (weighted
|
|
369
|
+
traffic-split + thresholds) -> tests/ (wire a flow to a load profile).
|
|
370
|
+
- **browser/** — real-browser UX (OPTIONAL; UI-heavy apps only). Delete this folder if unneeded.
|
|
371
|
+
|
|
372
|
+
## Fill in
|
|
373
|
+
1. `config/env.js` — BASE / UI_BASE / PROJECT_ID / scale numbers / test data.
|
|
374
|
+
2. `config/secrets.js` — auth token(s) (keep out of VCS).
|
|
375
|
+
3. `protocol/api/**` — one function per real endpoint.
|
|
376
|
+
4. `protocol/flows/**` — weighted action table (feature-usage %) + thresholds (SLA).
|
|
377
|
+
|
|
378
|
+
## Run (from inside this folder)
|
|
379
|
+
```
|
|
380
|
+
k6 cloud login --token <GRAFANA_CLOUD_TOKEN>
|
|
381
|
+
k6 run protocol/tests/smoke/example.smoke.js # local sanity
|
|
382
|
+
k6 cloud run protocol/tests/load/example.load.js # on Grafana Cloud
|
|
383
|
+
# browser (optional): k6 run browser/tests/exampleUi.browser.js
|
|
384
|
+
```
|
|
385
|
+
EOF
|
|
386
|
+
|
|
387
|
+
echo "Done. Structure created under $ROOT/"
|
|
388
|
+
echo "Next: fill config/env.js + config/secrets.js, then replace the example endpoints/flows."
|
|
@@ -27,6 +27,14 @@ user-invocable: true
|
|
|
27
27
|
**After the ONE allowed login snapshot — never call `browser_snapshot()` again in this run.**
|
|
28
28
|
Use `browser_evaluate` with a specific CSS selector or JS expression for all DOM inspection.
|
|
29
29
|
|
|
30
|
+
## Test Data & Entity Selection Rules
|
|
31
|
+
|
|
32
|
+
| Situation | Forbidden | Do instead |
|
|
33
|
+
|-----------|-----------|------------|
|
|
34
|
+
| A test step needs a value you don't have — a user record, a transaction PIN, a one-time user code | Calling a user-list endpoint or any other app API to fetch/guess it | Ask the user once, in the normal gap-question flow ("This test needs a valid user code — please provide one, or a test user's ID"), or pull it from `.env` / product context if already recorded there |
|
|
35
|
+
| A test requires two specifically related entities (e.g. a parent user + a specific child user, an account + a specific linked card) and the AC/COS doesn't name the exact pair | Picking one at random and proceeding as if it passed | Ask the user to specify the pair once. If they can't provide it inline, mark that test `🔒 BLOCKED` with the note "entity relationship not specified — needs manual verification" and continue with the rest of the plan — don't stop the whole run over one test |
|
|
36
|
+
| The AC/COS doesn't say how to reach the feature under test (menu path, URL, entry point) | Reading or grepping the `src/` tree to reverse-engineer the route | Ask once: "AC doesn't say how to reach {feature} — what's the navigation path (menu/page) to get there?" |
|
|
37
|
+
|
|
30
38
|
## Token Tracking
|
|
31
39
|
|
|
32
40
|
Optional and silent — follow the **Token Tracking** pattern in `SKILLS_CONTEXT.md`
|
|
@@ -152,13 +160,19 @@ HINTS = {
|
|
|
152
160
|
|
|
153
161
|
### 3a — Generate Test Plan
|
|
154
162
|
|
|
163
|
+
Before drafting test ideas, check each AC item for a stated entry point (menu path, URL,
|
|
164
|
+
button/link name). Per the **Test Data & Entity Selection Rules** above, do not scan `src/`
|
|
165
|
+
to infer a missing one — collect a single navigation question per missing entry point and
|
|
166
|
+
fold it into the confirmation prompt below instead of guessing.
|
|
167
|
+
|
|
155
168
|
From the AC, generate numbered test ideas:
|
|
156
169
|
- 1–3 tests per AC item
|
|
157
170
|
- 2+ negative/edge case tests
|
|
158
171
|
- 1+ error state test
|
|
159
172
|
|
|
160
173
|
Show as a compact table (T-01, T-02 ... with name and expected outcome).
|
|
161
|
-
Ask: `"Ready to run these tests? (yes / no or edit)"`
|
|
174
|
+
Ask: `"Ready to run these tests? (yes / no or edit)"` — include any missing navigation
|
|
175
|
+
questions in this same message.
|
|
162
176
|
Proceed on confirmation.
|
|
163
177
|
|
|
164
178
|
### 3b — Setup Browser + Capture Login Selectors
|
|
@@ -95,6 +95,23 @@ Criteria / COS**, **Figma link**, **issue type**, **project name** (for Step 6 f
|
|
|
95
95
|
If the description exceeds ~800 words, keep only Title + AC/Given-When-Then blocks +
|
|
96
96
|
Figma links; discard prose, comment threads, embedded images. Do not mention the truncation.
|
|
97
97
|
|
|
98
|
+
### 1c — Model Recommendation
|
|
99
|
+
|
|
100
|
+
Using the issue type fetched in 1b, print one line — informational only, never blocking:
|
|
101
|
+
|
|
102
|
+
| Issue type | Recommendation |
|
|
103
|
+
|---|---|
|
|
104
|
+
| Story / Feature / Epic | high-capability model (current default) |
|
|
105
|
+
| Task / Bug / Sub-task | lighter model (e.g. Haiku) |
|
|
106
|
+
|
|
107
|
+
> "Issue type: {type} → {tier} recommended for this run. Switch now with `/model` if you'd
|
|
108
|
+
> like, or continue on the current model."
|
|
109
|
+
|
|
110
|
+
Skip this line entirely if `.env` sets `QA_MODEL_ROUTING=off`. This is a recommendation
|
|
111
|
+
only — qa-pack has no way to force a mid-run model switch, so proceed on whatever model is
|
|
112
|
+
active regardless of the user's choice. See `SKILLS_CONTEXT.md` → "Model Routing" for the
|
|
113
|
+
opt-in subagent-override pattern some environments can wire up instead.
|
|
114
|
+
|
|
98
115
|
---
|
|
99
116
|
|
|
100
117
|
## Step 2 — Ask Relevant Questions (gaps only)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roopesh.yadava/qa-pack",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "AI-powered QA agent skills for Claude Code — manual testing, BDD automation, accessibility, UI/Figma diff, bug reporting",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"postinstall": "node bin/postinstall.js"
|
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
"claude",
|
|
24
24
|
"playwright",
|
|
25
25
|
"cucumber",
|
|
26
|
-
"bdd"
|
|
26
|
+
"bdd",
|
|
27
|
+
"k6",
|
|
28
|
+
"performance-testing",
|
|
29
|
+
"load-testing"
|
|
27
30
|
],
|
|
28
31
|
"license": "MIT"
|
|
29
32
|
}
|
package/templates/.env.example
CHANGED
|
@@ -20,3 +20,7 @@ JIRA_API_TOKEN=
|
|
|
20
20
|
# When unset or the scripts are missing, skills skip all tracking silently.
|
|
21
21
|
# QA_TRACKING_DIR=
|
|
22
22
|
# QA_TRACKING_MODEL=
|
|
23
|
+
|
|
24
|
+
# ── Optional: model-tier recommendation for qa-agent (see SKILLS_CONTEXT.md) ──
|
|
25
|
+
# recommend (default) | off | subagent (advanced — see "Model Routing" in SKILLS_CONTEXT.md)
|
|
26
|
+
# QA_MODEL_ROUTING=recommend
|
|
@@ -5,11 +5,7 @@
|
|
|
5
5
|
"Bash(npx cucumber-js *)",
|
|
6
6
|
"Bash(echo \"Exit: $?\")",
|
|
7
7
|
"Bash(playwright --version)",
|
|
8
|
-
"
|
|
9
|
-
"mcp__claude_ai_Atlassian__getJiraIssue",
|
|
10
|
-
"mcp__claude_ai_Atlassian__createJiraIssue",
|
|
11
|
-
"mcp__claude_ai_Atlassian__createIssueLink",
|
|
12
|
-
"mcp__claude_ai_Atlassian__addCommentToJiraIssue"
|
|
8
|
+
"mcp__playwright"
|
|
13
9
|
]
|
|
14
10
|
},
|
|
15
11
|
"enableAllProjectMcpServers": true,
|