@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.
Files changed (113) hide show
  1. package/CHANGELOG.md +63 -18
  2. package/README.md +2 -2
  3. package/docs/architecture.md +30 -26
  4. package/docs/decisions/lane-heartbeat-caller.md +86 -0
  5. package/docs/final-report.md +23 -23
  6. package/docs/folder-structure-guide.md +106 -80
  7. package/docs/installation.md +35 -7
  8. package/docs/skills-architecture.md +23 -23
  9. package/docs/spec-driven-philosophy.md +9 -9
  10. package/docs/usage-with-agent.md +83 -9
  11. package/docs/visuals/diag-01-ejecutivo-1.png +0 -0
  12. package/docs/visuals/diag-02-tactico-1.png +0 -0
  13. package/docs/visuals/diag-03-tecnico-1.png +0 -0
  14. package/docs/visuals/info-01-ejecutivo-1.png +0 -0
  15. package/docs/visuals/info-02-tactico-1.png +0 -0
  16. package/docs/visuals/info-03-tecnico-1.png +0 -0
  17. package/docs/visuals/pres-01-ejecutivo-1.png +0 -0
  18. package/docs/visuals/pres-02-tactico-1.png +0 -0
  19. package/docs/visuals/pres-03-tecnico-1.png +0 -0
  20. package/docs/visuals/qa-01-problema-1.png +0 -0
  21. package/docs/visuals/qa-02-que-es-1.png +0 -0
  22. package/docs/visuals/qa-03-pipeline-1.png +0 -0
  23. package/docs/visuals/qa-04-6-archivos-1.png +0 -0
  24. package/docs/visuals/qa-05-humano-1.png +0 -0
  25. package/docs/visuals/qa-06-agente-1.png +0 -0
  26. package/docs/visuals/qa-07-skills-arch-1.png +0 -0
  27. package/docs/visuals/qa-08-traceability-1.png +0 -0
  28. package/docs/visuals/qa-en-01-problem-1.png +0 -0
  29. package/docs/visuals/qa-en-02-what-is-1.png +0 -0
  30. package/docs/visuals/qa-en-03-pipeline-1.png +0 -0
  31. package/docs/visuals/qa-en-04-6files-1.png +0 -0
  32. package/docs/visuals/qa-en-05-human-1.png +0 -0
  33. package/docs/visuals/qa-en-06-agent-1.png +0 -0
  34. package/docs/visuals/qa-en-07-skills-arch-1.png +0 -0
  35. package/docs/visuals/qa-en-08-traceability-1.png +0 -0
  36. package/examples/module-example/suppliers/00-inventory.md +1 -1
  37. package/examples/module-example/suppliers/suppliers-create.spec.ts +8 -8
  38. package/integrations/ado-powershell/README.md +1 -1
  39. package/integrations/ado-powershell/pipelines/azure-pipeline-qa.yml +92 -25
  40. package/integrations/ado-powershell/scripts/sync-ado-titles.ps1 +2 -2
  41. package/integrations/playwright/README.md +128 -4
  42. package/package.json +9 -6
  43. package/qa-framework.config.json +24 -3
  44. package/scripts/check-forbidden-chars.js +187 -0
  45. package/scripts/check-spanish-accents.js +159 -0
  46. package/scripts/cli.js +1 -1
  47. package/scripts/generate.js +2 -2
  48. package/scripts/init.js +108 -45
  49. package/scripts/lib/claude-agents.js +78 -0
  50. package/scripts/lib/claude-commands.js +56 -0
  51. package/scripts/sync-version.js +36 -0
  52. package/scripts/upgrade.js +461 -30
  53. package/scripts/validate.js +17 -17
  54. package/skills/qa-ado-integration/SKILL.md +19 -19
  55. package/skills/qa-ado-integration/references/scripts-and-config.md +6 -6
  56. package/skills/qa-automation/SKILL.md +49 -27
  57. package/skills/qa-automation/references/config-checklist.md +4 -4
  58. package/skills/qa-automation/references/dom-inspection-template.js +7 -7
  59. package/skills/qa-automation/references/patterns.md +20 -20
  60. package/skills/qa-automation/references/pom-template.md +161 -0
  61. package/skills/qa-maintenance/SKILL.md +11 -11
  62. package/skills/qa-maintenance/references/update-rules.md +8 -8
  63. package/skills/qa-module-analysis/SKILL.md +20 -19
  64. package/skills/qa-module-analysis/references/exploration-checklist.md +4 -4
  65. package/skills/qa-module-analysis/references/output-verification.md +69 -0
  66. package/skills/qa-module-analysis/references/spec-file-formats.md +14 -14
  67. package/skills/qa-spec-generation/SKILL.md +13 -7
  68. package/skills/qa-test-cases/SKILL.md +11 -81
  69. package/skills/qa-test-cases/references/test-case-template.md +5 -5
  70. package/skills/qa-test-plan/SKILL.md +22 -86
  71. package/skills/qa-test-plan/references/plan-de-pruebas-template.md +24 -24
  72. package/skills/qa-test-stabilization/SKILL.md +59 -30
  73. package/skills/qa-test-stabilization/references/classification-protocol.md +35 -35
  74. package/templates/agent-next-steps.md +10 -10
  75. package/templates/agents/qa-analisis.md +142 -0
  76. package/templates/agents/qa-asesoria.md +48 -0
  77. package/templates/agents/qa-informe-resultados.md +234 -0
  78. package/templates/agents/qa-plan.md +131 -0
  79. package/templates/automation-scaffold/.env.example +52 -3
  80. package/templates/automation-scaffold/fixtures/auth.ts +27 -5
  81. package/templates/automation-scaffold/fixtures/base.ts +130 -0
  82. package/templates/automation-scaffold/fixtures/test-helpers.ts +1 -1
  83. package/templates/automation-scaffold/global-setup.ts +196 -37
  84. package/templates/automation-scaffold/playwright.config.ts +74 -10
  85. package/templates/automation-scaffold/scripts/global-setup-guards.js +184 -0
  86. package/templates/automation-scaffold/scripts/lane-config.js +269 -0
  87. package/templates/automation-scaffold/scripts/lane-lock.js +589 -0
  88. package/templates/defect-report.md +18 -4
  89. package/templates/execution-report.md +9 -9
  90. package/templates/integration-scaffold/.env.example +40 -0
  91. package/templates/integration-scaffold/README.md +77 -0
  92. package/templates/integration-scaffold/global-setup.ts +119 -0
  93. package/templates/integration-scaffold/package.json +15 -0
  94. package/templates/integration-scaffold/playwright.config.ts +92 -0
  95. package/templates/integration-scaffold/tests/example.spec.ts +43 -0
  96. package/templates/memory/ci-pipeline-findings.md +36 -0
  97. package/templates/memory/data-volatility-strategies.md +70 -0
  98. package/templates/memory/e2e-stabilization-patterns.md +54 -0
  99. package/templates/qa-framework.instructions.md +16 -7
  100. package/templates/qa-framework.rules.md +53 -0
  101. package/templates/qa-readme.md +26 -5
  102. package/templates/session-summary.md +2 -2
  103. package/templates/specification/00-inventory.md +3 -3
  104. package/templates/specification/01-business-rules.md +6 -6
  105. package/templates/specification/02-workflows.md +1 -1
  106. package/templates/specification/03-roles-permissions.md +1 -1
  107. package/templates/specification/04-test-data.md +8 -8
  108. package/templates/specification/05-test-scenarios.md +5 -5
  109. package/templates/test-case.md +3 -3
  110. package/templates/test-data-guidelines.md +7 -7
  111. package/templates/test-plan-sprint.md +13 -13
  112. package/templates/test-plan.md +3 -3
  113. 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
+ };