@keber/qa-framework 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -18
- package/README.md +2 -2
- package/docs/architecture.md +30 -26
- package/docs/decisions/lane-heartbeat-caller.md +86 -0
- package/docs/final-report.md +23 -23
- package/docs/folder-structure-guide.md +106 -80
- package/docs/installation.md +35 -7
- package/docs/skills-architecture.md +23 -23
- package/docs/spec-driven-philosophy.md +9 -9
- package/docs/usage-with-agent.md +83 -9
- package/docs/visuals/diag-01-ejecutivo-1.png +0 -0
- package/docs/visuals/diag-02-tactico-1.png +0 -0
- package/docs/visuals/diag-03-tecnico-1.png +0 -0
- package/docs/visuals/info-01-ejecutivo-1.png +0 -0
- package/docs/visuals/info-02-tactico-1.png +0 -0
- package/docs/visuals/info-03-tecnico-1.png +0 -0
- package/docs/visuals/pres-01-ejecutivo-1.png +0 -0
- package/docs/visuals/pres-02-tactico-1.png +0 -0
- package/docs/visuals/pres-03-tecnico-1.png +0 -0
- package/docs/visuals/qa-01-problema-1.png +0 -0
- package/docs/visuals/qa-02-que-es-1.png +0 -0
- package/docs/visuals/qa-03-pipeline-1.png +0 -0
- package/docs/visuals/qa-04-6-archivos-1.png +0 -0
- package/docs/visuals/qa-05-humano-1.png +0 -0
- package/docs/visuals/qa-06-agente-1.png +0 -0
- package/docs/visuals/qa-07-skills-arch-1.png +0 -0
- package/docs/visuals/qa-08-traceability-1.png +0 -0
- package/docs/visuals/qa-en-01-problem-1.png +0 -0
- package/docs/visuals/qa-en-02-what-is-1.png +0 -0
- package/docs/visuals/qa-en-03-pipeline-1.png +0 -0
- package/docs/visuals/qa-en-04-6files-1.png +0 -0
- package/docs/visuals/qa-en-05-human-1.png +0 -0
- package/docs/visuals/qa-en-06-agent-1.png +0 -0
- package/docs/visuals/qa-en-07-skills-arch-1.png +0 -0
- package/docs/visuals/qa-en-08-traceability-1.png +0 -0
- package/examples/module-example/suppliers/00-inventory.md +1 -1
- package/examples/module-example/suppliers/suppliers-create.spec.ts +8 -8
- package/integrations/ado-powershell/README.md +1 -1
- package/integrations/ado-powershell/pipelines/azure-pipeline-qa.yml +92 -25
- package/integrations/ado-powershell/scripts/sync-ado-titles.ps1 +2 -2
- package/integrations/playwright/README.md +128 -4
- package/package.json +9 -6
- package/qa-framework.config.json +24 -3
- package/scripts/check-forbidden-chars.js +187 -0
- package/scripts/check-spanish-accents.js +159 -0
- package/scripts/cli.js +1 -1
- package/scripts/generate.js +2 -2
- package/scripts/init.js +108 -45
- package/scripts/lib/claude-agents.js +78 -0
- package/scripts/lib/claude-commands.js +56 -0
- package/scripts/sync-version.js +36 -0
- package/scripts/upgrade.js +461 -30
- package/scripts/validate.js +17 -17
- package/skills/qa-ado-integration/SKILL.md +19 -19
- package/skills/qa-ado-integration/references/scripts-and-config.md +6 -6
- package/skills/qa-automation/SKILL.md +49 -27
- package/skills/qa-automation/references/config-checklist.md +4 -4
- package/skills/qa-automation/references/dom-inspection-template.js +7 -7
- package/skills/qa-automation/references/patterns.md +20 -20
- package/skills/qa-automation/references/pom-template.md +161 -0
- package/skills/qa-maintenance/SKILL.md +11 -11
- package/skills/qa-maintenance/references/update-rules.md +8 -8
- package/skills/qa-module-analysis/SKILL.md +20 -19
- package/skills/qa-module-analysis/references/exploration-checklist.md +4 -4
- package/skills/qa-module-analysis/references/output-verification.md +69 -0
- package/skills/qa-module-analysis/references/spec-file-formats.md +14 -14
- package/skills/qa-spec-generation/SKILL.md +13 -7
- package/skills/qa-test-cases/SKILL.md +11 -81
- package/skills/qa-test-cases/references/test-case-template.md +5 -5
- package/skills/qa-test-plan/SKILL.md +22 -86
- package/skills/qa-test-plan/references/plan-de-pruebas-template.md +24 -24
- package/skills/qa-test-stabilization/SKILL.md +59 -30
- package/skills/qa-test-stabilization/references/classification-protocol.md +35 -35
- package/templates/agent-next-steps.md +10 -10
- package/templates/agents/qa-analisis.md +142 -0
- package/templates/agents/qa-asesoria.md +48 -0
- package/templates/agents/qa-informe-resultados.md +234 -0
- package/templates/agents/qa-plan.md +131 -0
- package/templates/automation-scaffold/.env.example +52 -3
- package/templates/automation-scaffold/fixtures/auth.ts +27 -5
- package/templates/automation-scaffold/fixtures/base.ts +130 -0
- package/templates/automation-scaffold/fixtures/test-helpers.ts +1 -1
- package/templates/automation-scaffold/global-setup.ts +196 -37
- package/templates/automation-scaffold/playwright.config.ts +74 -10
- package/templates/automation-scaffold/scripts/global-setup-guards.js +184 -0
- package/templates/automation-scaffold/scripts/lane-config.js +269 -0
- package/templates/automation-scaffold/scripts/lane-lock.js +589 -0
- package/templates/defect-report.md +18 -4
- package/templates/execution-report.md +9 -9
- package/templates/integration-scaffold/.env.example +40 -0
- package/templates/integration-scaffold/README.md +77 -0
- package/templates/integration-scaffold/global-setup.ts +119 -0
- package/templates/integration-scaffold/package.json +15 -0
- package/templates/integration-scaffold/playwright.config.ts +92 -0
- package/templates/integration-scaffold/tests/example.spec.ts +43 -0
- package/templates/memory/ci-pipeline-findings.md +36 -0
- package/templates/memory/data-volatility-strategies.md +70 -0
- package/templates/memory/e2e-stabilization-patterns.md +54 -0
- package/templates/qa-framework.instructions.md +16 -7
- package/templates/qa-framework.rules.md +53 -0
- package/templates/qa-readme.md +26 -5
- package/templates/session-summary.md +2 -2
- package/templates/specification/00-inventory.md +3 -3
- package/templates/specification/01-business-rules.md +6 -6
- package/templates/specification/02-workflows.md +1 -1
- package/templates/specification/03-roles-permissions.md +1 -1
- package/templates/specification/04-test-data.md +8 -8
- package/templates/specification/05-test-scenarios.md +5 -5
- package/templates/test-case.md +3 -3
- package/templates/test-data-guidelines.md +7 -7
- package/templates/test-plan-sprint.md +13 -13
- package/templates/test-plan.md +3 -3
- package/skills/qa-test-plan/references/priority-and-feasibility.md +0 -159
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lane table resolution for the parallel QA lanes.
|
|
6
|
+
*
|
|
7
|
+
* WHY LANES EXIST: not for speed. The applications under test invalidate a session
|
|
8
|
+
* when the same account logs in again, so two concurrent workers sharing one account
|
|
9
|
+
* silently destroy each other's session. Each lane is a distinct QA account, and the
|
|
10
|
+
* lock in lane-lock.js is what stops two workers landing on the same one.
|
|
11
|
+
*
|
|
12
|
+
* This module owns the lane TABLE (which lanes exist and what each one is bound to).
|
|
13
|
+
* lane-lock.js owns the lane STATE (which lanes are currently held). Keeping them
|
|
14
|
+
* apart means the table can be parsed and tested without touching the filesystem
|
|
15
|
+
* state, and means playwright.config.ts can resolve a storageState without pulling
|
|
16
|
+
* in the lock machinery.
|
|
17
|
+
*
|
|
18
|
+
* A lane is four coupled values, not just an account. All four come from config
|
|
19
|
+
* because none of them can be reliably inferred:
|
|
20
|
+
*
|
|
21
|
+
* account env var prefix for credentials, e.g. QA_USER4 -> QA_USER4_EMAIL
|
|
22
|
+
* storageState the Playwright storage state file for that account
|
|
23
|
+
* mcpNamespace tool prefix when an agent drives this lane over Playwright MCP
|
|
24
|
+
* runnerProject the playwright.config.ts project name bound to this lane
|
|
25
|
+
*
|
|
26
|
+
* CAPABILITIES ARE PER-LANE, NOT PER-SET. A project can have 6 accounts but only 3
|
|
27
|
+
* MCP servers. Handing lane 5 to a consumer that needs MCP tools lane 5 lacks is a
|
|
28
|
+
* real failure that happened in production use, not a hypothetical. So each lane
|
|
29
|
+
* declares its own capabilities and consumers state what they require; see
|
|
30
|
+
* lanesWithCapability() and assertLaneHasCapability().
|
|
31
|
+
*
|
|
32
|
+
* BACKWARD COMPATIBILITY: a project with no `parallelLanes` block in its config gets
|
|
33
|
+
* the implicit single default lane (lane '1', account QA_USER, storageState
|
|
34
|
+
* .auth/user-default.json) - exactly the shape the pre-N-lane scaffold hardcoded.
|
|
35
|
+
* See DEFAULT_LANE_TABLE below.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const fs = require('fs');
|
|
39
|
+
const path = require('path');
|
|
40
|
+
|
|
41
|
+
// The implicit lane table used when a project declares no `parallelLanes` config.
|
|
42
|
+
// These values match what the single-lane scaffold used before lanes were
|
|
43
|
+
// configurable: one lane, QA_USER credentials, .auth/user-default.json, the
|
|
44
|
+
// 'chromium' project. A project that never opts in must keep behaving exactly as it
|
|
45
|
+
// did, so this table is not merely a nice default - it is the compatibility contract.
|
|
46
|
+
const DEFAULT_LANE_TABLE = Object.freeze({
|
|
47
|
+
'1': Object.freeze({
|
|
48
|
+
account: 'QA_USER',
|
|
49
|
+
storageState: '.auth/user-default.json',
|
|
50
|
+
mcpNamespace: null,
|
|
51
|
+
runnerProject: 'chromium',
|
|
52
|
+
capabilities: Object.freeze([]),
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// A lane id is referenced from shell env (QA_LANE_ONLY), CI variables and JSON keys,
|
|
57
|
+
// so it is restricted to a conservative token: alphanumerics, dash and underscore.
|
|
58
|
+
const LANE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Locates qa-framework.config.json by walking up from `startDir`.
|
|
62
|
+
*
|
|
63
|
+
* The scaffold lives at qa/07-automation/e2e/ while the config lives at either the
|
|
64
|
+
* project root or qa/, so a fixed relative path would be wrong for one of them.
|
|
65
|
+
* Walking up finds it either way and returns undefined rather than throwing, so a
|
|
66
|
+
* project with no config at all falls through to DEFAULT_LANE_TABLE.
|
|
67
|
+
*/
|
|
68
|
+
function findConfigPath(startDir) {
|
|
69
|
+
let dir = path.resolve(startDir || process.cwd());
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
while (!seen.has(dir)) {
|
|
72
|
+
seen.add(dir);
|
|
73
|
+
for (const candidate of [
|
|
74
|
+
path.join(dir, 'qa-framework.config.json'),
|
|
75
|
+
path.join(dir, 'qa', 'qa-framework.config.json'),
|
|
76
|
+
]) {
|
|
77
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
78
|
+
}
|
|
79
|
+
const parent = path.dirname(dir);
|
|
80
|
+
if (parent === dir) break;
|
|
81
|
+
dir = parent;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validates and normalizes a `parallelLanes` config block into a lane table.
|
|
88
|
+
*
|
|
89
|
+
* Throws on any malformed lane rather than dropping it. A silently ignored lane is
|
|
90
|
+
* the worst outcome available here: the project believes it has N lanes of
|
|
91
|
+
* isolation, the lock hands out fewer, and two workers end up on one account - which
|
|
92
|
+
* is the exact failure the whole mechanism exists to prevent.
|
|
93
|
+
*/
|
|
94
|
+
function laneTableFromConfig(config) {
|
|
95
|
+
const block = config && config.parallelLanes;
|
|
96
|
+
|
|
97
|
+
if (block === undefined || block === null) return DEFAULT_LANE_TABLE;
|
|
98
|
+
if (typeof block !== 'object' || Array.isArray(block)) {
|
|
99
|
+
throw new Error('[lane-config] parallelLanes must be an object.');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// `enabled: false` is an explicit opt-out that must land on the same behavior as
|
|
103
|
+
// no config at all, so a project can disable lanes without deleting its lane
|
|
104
|
+
// definitions.
|
|
105
|
+
if (block.enabled === false) return DEFAULT_LANE_TABLE;
|
|
106
|
+
|
|
107
|
+
const lanes = block.lanes;
|
|
108
|
+
if (!Array.isArray(lanes) || lanes.length === 0) {
|
|
109
|
+
throw new Error('[lane-config] parallelLanes.lanes must be a non-empty array.');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const table = {};
|
|
113
|
+
for (const [index, lane] of lanes.entries()) {
|
|
114
|
+
const where = `parallelLanes.lanes[${index}]`;
|
|
115
|
+
if (!lane || typeof lane !== 'object' || Array.isArray(lane)) {
|
|
116
|
+
throw new Error(`[lane-config] ${where} must be an object.`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const id = lane.id;
|
|
120
|
+
if (typeof id !== 'string' || !LANE_ID_PATTERN.test(id)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`[lane-config] ${where}.id must be a non-empty string of letters, digits, "-" or "_". Got: ${JSON.stringify(id)}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (table[id]) {
|
|
126
|
+
throw new Error(`[lane-config] duplicate lane id "${id}" at ${where}.`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const field of ['account', 'storageState']) {
|
|
130
|
+
if (typeof lane[field] !== 'string' || lane[field].length === 0) {
|
|
131
|
+
throw new Error(`[lane-config] ${where}.${field} is required and must be a non-empty string.`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// mcpNamespace and runnerProject are optional: a lane may be CLI-only (no MCP
|
|
136
|
+
// server provisioned for it) or may not be bound to a runner project at all.
|
|
137
|
+
// Both are normalized to null so consumers never have to distinguish "absent"
|
|
138
|
+
// from "explicitly none".
|
|
139
|
+
for (const field of ['mcpNamespace', 'runnerProject']) {
|
|
140
|
+
const value = lane[field];
|
|
141
|
+
if (value !== undefined && value !== null && (typeof value !== 'string' || value.length === 0)) {
|
|
142
|
+
throw new Error(`[lane-config] ${where}.${field} must be a non-empty string when present.`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let capabilities = lane.capabilities;
|
|
147
|
+
if (capabilities === undefined || capabilities === null) {
|
|
148
|
+
capabilities = [];
|
|
149
|
+
} else if (!Array.isArray(capabilities) || capabilities.some((c) => typeof c !== 'string' || !c)) {
|
|
150
|
+
throw new Error(`[lane-config] ${where}.capabilities must be an array of non-empty strings.`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
table[id] = Object.freeze({
|
|
154
|
+
account: lane.account,
|
|
155
|
+
storageState: lane.storageState,
|
|
156
|
+
mcpNamespace: lane.mcpNamespace === undefined ? null : lane.mcpNamespace,
|
|
157
|
+
runnerProject: lane.runnerProject === undefined ? null : lane.runnerProject,
|
|
158
|
+
capabilities: Object.freeze([...capabilities]),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Two lanes sharing a storageState means two "isolated" lanes writing the same
|
|
163
|
+
// session file - they are not isolated at all, and the corruption is silent.
|
|
164
|
+
const byStorageState = new Map();
|
|
165
|
+
for (const [id, lane] of Object.entries(table)) {
|
|
166
|
+
const clash = byStorageState.get(lane.storageState);
|
|
167
|
+
if (clash) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`[lane-config] lanes "${clash}" and "${id}" share storageState "${lane.storageState}". ` +
|
|
170
|
+
'Each lane must have its own storageState file, otherwise the lanes are not isolated.'
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
byStorageState.set(lane.storageState, id);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return Object.freeze(table);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Reads the config file at `configPath` and returns its lane table. */
|
|
180
|
+
function loadLaneTable(configPath) {
|
|
181
|
+
const resolved = configPath || findConfigPath(process.cwd());
|
|
182
|
+
if (!resolved || !fs.existsSync(resolved)) return DEFAULT_LANE_TABLE;
|
|
183
|
+
|
|
184
|
+
let config;
|
|
185
|
+
try {
|
|
186
|
+
config = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
187
|
+
} catch (err) {
|
|
188
|
+
throw new Error(`[lane-config] Could not parse ${resolved}: ${err.message}`);
|
|
189
|
+
}
|
|
190
|
+
return laneTableFromConfig(config);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Maps a QA_LANE_ONLY value to a lane id.
|
|
195
|
+
*
|
|
196
|
+
* 'default' is accepted as an alias for the first declared lane, because the
|
|
197
|
+
* pre-N-lane scaffold and its CI pipelines already used QA_LANE_ONLY=default and
|
|
198
|
+
* those must keep working. Every other value must name a declared lane exactly.
|
|
199
|
+
*
|
|
200
|
+
* Throws on anything unrecognized. Callers must never silently fall back to a
|
|
201
|
+
* guessed lane: guessing is how a run lands on an account it does not hold.
|
|
202
|
+
*/
|
|
203
|
+
function laneIdForQaLaneOnly(qaLaneOnly, laneTable) {
|
|
204
|
+
const table = laneTable || loadLaneTable();
|
|
205
|
+
const ids = Object.keys(table);
|
|
206
|
+
|
|
207
|
+
if (qaLaneOnly === 'default') {
|
|
208
|
+
if (table.default) return 'default';
|
|
209
|
+
return ids[0];
|
|
210
|
+
}
|
|
211
|
+
if (typeof qaLaneOnly === 'string' && table[qaLaneOnly]) return qaLaneOnly;
|
|
212
|
+
|
|
213
|
+
throw new Error(
|
|
214
|
+
`[lane-config] Invalid QA_LANE_ONLY: ${JSON.stringify(qaLaneOnly)}. ` +
|
|
215
|
+
`Allowed values: default, ${ids.join(', ')}.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Single source of truth for the lane -> storageState mapping.
|
|
221
|
+
*
|
|
222
|
+
* playwright.config.ts resolves every project's storageState through this function
|
|
223
|
+
* instead of hardcoding one per project. A parallel hardcoded mapping caused real
|
|
224
|
+
* evidence corruption once already - a run recorded under the wrong account, because
|
|
225
|
+
* the project's storageState was pinned to one lane while QA_LANE_ONLY named
|
|
226
|
+
* another. There must be exactly one place this mapping lives, and this is it.
|
|
227
|
+
*/
|
|
228
|
+
function storageStateForLaneOnly(qaLaneOnly, laneTable) {
|
|
229
|
+
const table = laneTable || loadLaneTable();
|
|
230
|
+
return table[laneIdForQaLaneOnly(qaLaneOnly, table)].storageState;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Lane ids that declare `capability`. */
|
|
234
|
+
function lanesWithCapability(capability, laneTable) {
|
|
235
|
+
const table = laneTable || loadLaneTable();
|
|
236
|
+
return Object.keys(table).filter((id) => table[id].capabilities.includes(capability));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Throws unless lane `laneId` declares `capability`.
|
|
241
|
+
*
|
|
242
|
+
* This is the guard for the "6 accounts but only 3 MCP servers" failure: handing a
|
|
243
|
+
* consumer a lane that lacks a capability it needs fails later, confusingly, and
|
|
244
|
+
* inside the consumer. Failing here names the lane, the capability and what the lane
|
|
245
|
+
* actually has.
|
|
246
|
+
*/
|
|
247
|
+
function assertLaneHasCapability(laneId, capability, laneTable) {
|
|
248
|
+
const table = laneTable || loadLaneTable();
|
|
249
|
+
const lane = table[laneId];
|
|
250
|
+
if (!lane) throw new Error(`[lane-config] Unknown lane: ${laneId}`);
|
|
251
|
+
if (!lane.capabilities.includes(capability)) {
|
|
252
|
+
const have = lane.capabilities.length ? lane.capabilities.join(', ') : 'none';
|
|
253
|
+
throw new Error(
|
|
254
|
+
`[lane-config] Lane ${laneId} does not have capability "${capability}" (declared: ${have}). ` +
|
|
255
|
+
`Lanes with that capability: ${lanesWithCapability(capability, table).join(', ') || 'none'}.`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = {
|
|
261
|
+
DEFAULT_LANE_TABLE,
|
|
262
|
+
findConfigPath,
|
|
263
|
+
laneTableFromConfig,
|
|
264
|
+
loadLaneTable,
|
|
265
|
+
laneIdForQaLaneOnly,
|
|
266
|
+
storageStateForLaneOnly,
|
|
267
|
+
lanesWithCapability,
|
|
268
|
+
assertLaneHasCapability,
|
|
269
|
+
};
|