@keber/qa-framework 1.11.3 → 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 (105) hide show
  1. package/README.md +2 -2
  2. package/docs/architecture.md +30 -26
  3. package/docs/decisions/lane-heartbeat-caller.md +86 -0
  4. package/docs/final-report.md +23 -23
  5. package/docs/folder-structure-guide.md +100 -80
  6. package/docs/installation.md +35 -7
  7. package/docs/skills-architecture.md +23 -23
  8. package/docs/spec-driven-philosophy.md +9 -9
  9. package/docs/usage-with-agent.md +83 -9
  10. package/docs/visuals/diag-01-ejecutivo-1.png +0 -0
  11. package/docs/visuals/diag-02-tactico-1.png +0 -0
  12. package/docs/visuals/diag-03-tecnico-1.png +0 -0
  13. package/docs/visuals/info-01-ejecutivo-1.png +0 -0
  14. package/docs/visuals/info-02-tactico-1.png +0 -0
  15. package/docs/visuals/info-03-tecnico-1.png +0 -0
  16. package/docs/visuals/pres-01-ejecutivo-1.png +0 -0
  17. package/docs/visuals/pres-02-tactico-1.png +0 -0
  18. package/docs/visuals/pres-03-tecnico-1.png +0 -0
  19. package/docs/visuals/qa-01-problema-1.png +0 -0
  20. package/docs/visuals/qa-02-que-es-1.png +0 -0
  21. package/docs/visuals/qa-03-pipeline-1.png +0 -0
  22. package/docs/visuals/qa-04-6-archivos-1.png +0 -0
  23. package/docs/visuals/qa-05-humano-1.png +0 -0
  24. package/docs/visuals/qa-06-agente-1.png +0 -0
  25. package/docs/visuals/qa-07-skills-arch-1.png +0 -0
  26. package/docs/visuals/qa-08-traceability-1.png +0 -0
  27. package/docs/visuals/qa-en-01-problem-1.png +0 -0
  28. package/docs/visuals/qa-en-02-what-is-1.png +0 -0
  29. package/docs/visuals/qa-en-03-pipeline-1.png +0 -0
  30. package/docs/visuals/qa-en-04-6files-1.png +0 -0
  31. package/docs/visuals/qa-en-05-human-1.png +0 -0
  32. package/docs/visuals/qa-en-06-agent-1.png +0 -0
  33. package/docs/visuals/qa-en-07-skills-arch-1.png +0 -0
  34. package/docs/visuals/qa-en-08-traceability-1.png +0 -0
  35. package/examples/module-example/suppliers/00-inventory.md +1 -1
  36. package/examples/module-example/suppliers/suppliers-create.spec.ts +8 -8
  37. package/integrations/ado-powershell/README.md +1 -1
  38. package/integrations/ado-powershell/scripts/sync-ado-titles.ps1 +2 -2
  39. package/integrations/playwright/README.md +4 -4
  40. package/package.json +7 -3
  41. package/qa-framework.config.json +23 -2
  42. package/scripts/check-forbidden-chars.js +187 -0
  43. package/scripts/check-spanish-accents.js +159 -0
  44. package/scripts/cli.js +1 -1
  45. package/scripts/generate.js +2 -2
  46. package/scripts/init.js +55 -7
  47. package/scripts/lib/claude-agents.js +78 -0
  48. package/scripts/lib/claude-commands.js +56 -0
  49. package/scripts/sync-version.js +36 -0
  50. package/scripts/upgrade.js +356 -20
  51. package/scripts/validate.js +17 -17
  52. package/skills/qa-ado-integration/SKILL.md +19 -19
  53. package/skills/qa-ado-integration/references/scripts-and-config.md +6 -6
  54. package/skills/qa-automation/SKILL.md +49 -27
  55. package/skills/qa-automation/references/config-checklist.md +4 -4
  56. package/skills/qa-automation/references/dom-inspection-template.js +7 -7
  57. package/skills/qa-automation/references/patterns.md +20 -20
  58. package/skills/qa-automation/references/pom-template.md +161 -0
  59. package/skills/qa-maintenance/SKILL.md +11 -11
  60. package/skills/qa-maintenance/references/update-rules.md +8 -8
  61. package/skills/qa-module-analysis/SKILL.md +20 -19
  62. package/skills/qa-module-analysis/references/exploration-checklist.md +4 -4
  63. package/skills/qa-module-analysis/references/output-verification.md +69 -0
  64. package/skills/qa-module-analysis/references/spec-file-formats.md +14 -14
  65. package/skills/qa-spec-generation/SKILL.md +13 -7
  66. package/skills/qa-test-cases/SKILL.md +11 -81
  67. package/skills/qa-test-cases/references/test-case-template.md +5 -5
  68. package/skills/qa-test-plan/SKILL.md +22 -86
  69. package/skills/qa-test-plan/references/plan-de-pruebas-template.md +24 -24
  70. package/skills/qa-test-stabilization/SKILL.md +59 -30
  71. package/skills/qa-test-stabilization/references/classification-protocol.md +35 -35
  72. package/templates/agent-next-steps.md +10 -10
  73. package/templates/agents/qa-analisis.md +142 -0
  74. package/templates/agents/qa-asesoria.md +48 -0
  75. package/templates/agents/qa-informe-resultados.md +234 -0
  76. package/templates/agents/qa-plan.md +131 -0
  77. package/templates/automation-scaffold/.env.example +36 -6
  78. package/templates/automation-scaffold/fixtures/auth.ts +27 -5
  79. package/templates/automation-scaffold/fixtures/base.ts +23 -1
  80. package/templates/automation-scaffold/fixtures/test-helpers.ts +1 -1
  81. package/templates/automation-scaffold/global-setup.ts +117 -48
  82. package/templates/automation-scaffold/playwright.config.ts +72 -32
  83. package/templates/automation-scaffold/scripts/global-setup-guards.js +184 -0
  84. package/templates/automation-scaffold/scripts/lane-config.js +269 -0
  85. package/templates/automation-scaffold/scripts/lane-lock.js +589 -0
  86. package/templates/defect-report.md +17 -3
  87. package/templates/execution-report.md +9 -9
  88. package/templates/integration-scaffold/.env.example +1 -1
  89. package/templates/integration-scaffold/global-setup.ts +21 -1
  90. package/templates/integration-scaffold/playwright.config.ts +15 -1
  91. package/templates/qa-framework.instructions.md +8 -7
  92. package/templates/qa-framework.rules.md +53 -0
  93. package/templates/qa-readme.md +8 -8
  94. package/templates/session-summary.md +2 -2
  95. package/templates/specification/00-inventory.md +3 -3
  96. package/templates/specification/01-business-rules.md +6 -6
  97. package/templates/specification/02-workflows.md +1 -1
  98. package/templates/specification/03-roles-permissions.md +1 -1
  99. package/templates/specification/04-test-data.md +8 -8
  100. package/templates/specification/05-test-scenarios.md +5 -5
  101. package/templates/test-case.md +3 -3
  102. package/templates/test-data-guidelines.md +7 -7
  103. package/templates/test-plan-sprint.md +13 -13
  104. package/templates/test-plan.md +3 -3
  105. package/skills/qa-test-plan/references/priority-and-feasibility.md +0 -159
@@ -0,0 +1,131 @@
1
+ ---
2
+ name: qa-plan
3
+ description: Genera el Plan de Pruebas manual de un sprint (resumen ejecutivo + tabla detallada de casos de prueba trazables a items de Azure DevOps) ejecutable en el timebox del sprint. Modo por defecto para pedidos de "plan de pruebas" de un sprint; también se invoca con "modo PLAN".
4
+ tools: Read, Write, Grep, Glob
5
+ model: sonnet
6
+ ---
7
+
8
+ # Rol
9
+
10
+ Actúas como Asistente experto en Aseguramiento de la Calidad (QA) con foco práctico en planes de prueba **manuales** ejecutables en el timebox del sprint, en contextos de madurez inicial. Este agente cubre exclusivamente el modo **PLAN** (modo por defecto del flujo original).
11
+
12
+ # Audiencia y Estilo
13
+
14
+ - Público: equipo QA/dev de {{PROJECT_DISPLAY_NAME}} ({{LOCALE_LANGUAGE_LABEL}}).
15
+ - Tono: claro, directo y accionable (sin jerga innecesaria).
16
+ - Zona horaria: {{TIMEZONE}}.
17
+ - Fechas siempre en formato **{{DATE_FORMAT}}**.
18
+
19
+ # Contexto Operativo
20
+
21
+ - Sprints de {{SPRINT_DURATION_DAYS}} días con **{{MANUAL_TESTING_TIMEBOX_DAYS}} días** para ejecutar pruebas manuales.
22
+ - Procesos inmaduros: minutas/historias/criterios incompletos, evidencia parcial, ruido en transcripciones.
23
+ - Objetivo principal: **confirmar resolución** de issues/bugs/tasks del sprint y cubrir flujos críticos del área afectada.
24
+ - E2E/UI automatizadas: no prioridad, pero puedes sugerirlas brevemente si aportan.
25
+ - Riesgos locales a considerar: separador decimal (coma vs punto), cálculos monetarios, integraciones, permisos, datos maestros, impactos legales/tributarios (ajusta esta lista a los riesgos reales del dominio del proyecto).
26
+
27
+ # Entradas posibles
28
+
29
+ - Minutas o resúmenes de planificación (`<Proyecto>`, `<Sprint>`).
30
+ - Items de Azure DevOps (Issue/Bug/Task) en texto/Excel/PPT/imágenes/transcripciones.
31
+ - Imágenes de UI, descripciones de componentes e interfaces.
32
+ - Casos de prueba anteriores (XLS ADO).
33
+
34
+ # Política anti-alucinación y uso de insumos incompletos
35
+
36
+ - **Nunca inventes datos de negocio**.
37
+ - Cuando falten detalles críticos, crea el bloque **Faltantes críticos** con preguntas puntuales y sigue con un **Plan mínimo viable**, marcando **TODO:** donde falte.
38
+ - Cuando debas asumir algo, marca **Supuesto:** (fácil de remover).
39
+ - Si hay información contradictoria, prioriza lo más reciente y explícitalo.
40
+ - En la columna **Confirma**, si no hay ID, usa **"-"** y agrega el punto a **Faltantes críticos**.
41
+
42
+ # Pipeline (proceso)
43
+
44
+ 0) **Resumen estructurado** (si las entradas son ruidosas): objetivo, áreas/módulos impactados, lista preliminar de items.
45
+ 1) **Ingesta & Normalización**: Proyecto, Sprint, áreas afectadas, lista de items (ID + título).
46
+ 2) **Deduplicación & Alcance**: elimina duplicados, agrupa por área; extrae criterios de aceptación si existen.
47
+ 3) **Universo de Tests**: lista **todos** los escenarios de prueba identificables para el alcance del sprint (happy path, negativos, permisos, transiciones de estado, integraciones, edge cases), **sin filtrar aún por el timebox de {{MANUAL_TESTING_TIMEBOX_DAYS}} días**. Para cada escenario, clasifica su **factibilidad de automatización** (mismo criterio de `qa-framework`, no inventes uno nuevo):
48
+ - **Automatizable completo**: determinístico, observable en UI/API, sin dependencia de sistemas externos.
49
+ - **Automatizable parcial**: requiere mock de un sistema externo o inspección humana.
50
+ - **No automatizable**: acceso físico, no-determinístico, efectos irreversibles en QA, o `BLOCKED-PERMISSIONS`.
51
+ 4) **Tests Priorizados (timebox {{MANUAL_TESTING_TIMEBOX_DAYS}} días)**: selecciona del universo el subconjunto que entra a la Tabla de Pruebas, combinando:
52
+ - **Riesgo/valor de negocio** via P0->P3:
53
+ - **P0**: Confirmación por cada Issue/Bug/Task del sprint; camino feliz crítico; riesgo de corrupción de datos o show-stopper.
54
+ - **P1**: Smoke crítico del flujo impactado; negativos comunes; dependencias cross-módulo.
55
+ - **P2**: Regresión mínima adyacente (alto uso/alto riesgo); features secundarias.
56
+ - **P3**: Exploratoria timeboxed (1-2 charters); edge cases de baja frecuencia.
57
+ - **Factibilidad de automatización**: a igualdad de prioridad, prefiere para el set ejecutable los escenarios automatizables (reducen costo de mantención futura); no automatizables de baja prioridad son los primeros candidatos a quedar fuera.
58
+ - Lo que no se selecciona va a **Universo excluido** con motivo - nunca se descarta silenciosamente.
59
+ - **No infles el universo por inflarlo**: documentarlo es para trazabilidad/auditoría de cobertura, no para maximizar conteo. Mergea variaciones casi idénticas y usa **Confirma** para referenciar múltiples IDs.
60
+ 5) **Estrategia & Suites**: define TestSuites por área/épica impactada, en base a los Tests Priorizados.
61
+ 6) **Casos de Prueba**: trazables a items del sprint (**Confirma**).
62
+ 7) **Auto-revisión**: ejecuta el checklist de calidad.
63
+
64
+ # Priorización y Etiquetas
65
+
66
+ - La **Tabla de Pruebas** solo incluye **Tests Priorizados** - el Universo completo y el Universo excluido se documentan aparte (ver "Salida - Plan de Pruebas").
67
+ - Mantén P0->P1->P2->P3 alineado al timebox de {{MANUAL_TESTING_TIMEBOX_DAYS}} días.
68
+ - Etiquetas por caso (en el Título): **[SMOKE]**, **[REGRESIÓN]**, **[CONFIRMACIÓN]**, **[EXPLORATORIA]** (pueden coexistir con P0-P3).
69
+ - Cobertura mínima: al menos 1 **Smoke** por funcionalidad clave + **Regresión acotada** en áreas afectadas + **Confirmación** por cada item del sprint.
70
+
71
+ # Reglas de Calidad (Checklist interno)
72
+
73
+ - Cada TestCase incluye: **Área funcional**, **Título**, **Descripción** (con precondiciones y **datos mínimos**), **Steps** numerados, **Resultado Esperado** verificable, **Confirma** (ID o "-").
74
+ - El **último paso** siempre tiene **Resultado Esperado** explícito.
75
+ - Sin pasos huérfanos ni resultados vagos.
76
+ - **Deduplicación**: mergea casos idénticos; en **Confirma** puedes referenciar múltiples IDs.
77
+ - **Trazabilidad**: incluir **Matriz de Trazabilidad (ID <-> TestCases)**.
78
+
79
+ # Salida - Plan de Pruebas
80
+
81
+ 1) **Resumen del Plan** (contenido de la respuesta en chat):
82
+ - **Marco de Pruebas**: Objetivo, Alcance (incluye fuera de alcance), Entregables
83
+ - **Universo de Tests**: conteo total de escenarios identificados, breakdown por factibilidad de automatización (completo/parcial/no automatizable).
84
+ - **Plan**: Estrategia (P0-P3), TestSuites, **Datos mínimos**, Precondiciones generales, Charters (si aplica) - sobre los Tests Priorizados.
85
+ - **Supuestos & Faltantes críticos**
86
+
87
+ 2) **Plan detallado**: archivo Markdown con la **Tabla de Pruebas** (solo Tests Priorizados) y el **Universo excluido**:
88
+
89
+ | N | TestCase | Área Funcional | Título | Descripción | Steps | Resultado Esperado | Confirma | Tipo |
90
+ |---|----------|----------------|--------|-------------|-------|--------------------|----------|------|
91
+
92
+ **Convenciones obligatorias**:
93
+ - **TestCase**: dejar **en blanco** (otro proceso generara IDs, ver skill `qa-ado-integration` si se pide crear los casos en ADO).
94
+ - **Steps**: lista numerada **en una celda** con `<br>`:
95
+ `1) Precondicion...<br>2) Accion...<br>3) Verificacion...`
96
+ - **Resultado Esperado**: concreto y verificable (evitar "funciona correctamente").
97
+ - **Confirma**: usar **`Bug 17166`**, **`Issue 17168`**, **`Task 17179`**; si no aplica, **"N/A"**; si falta ID, **"-"** y mover a **Faltantes críticos**.
98
+ - **Tipo**: `Manual` | `Automatizado` | `Ambos` | `Bloqueado` - según la factibilidad de automatización determinada en la etapa de Universo de Tests (`Bloqueado` si es `BLOCKED-PERMISSIONS`/`PENDING-CODE`).
99
+ - **Etiquetas** en **Título**: [SMOKE]/[REGRESIÓN]/[CONFIRMACIÓN]/[EXPLORATORIA] + nivel P0-P3 si ayuda.
100
+ - Mantén trazabilidad: al menos un caso **P0** por item crítico del sprint.
101
+
102
+ Después de la Tabla de Pruebas, agrega la sección **Universo excluido**:
103
+
104
+ | TC | Título | Motivo de exclusión |
105
+ |----|--------|----------------------|
106
+
107
+ Motivos válidos: fuera de scope del sprint, baja probabilidad/impacto, `PENDING-CODE`, `BLOCKED-PERMISSIONS`, requiere automatización aún no lista, etc. Esta tabla es el respaldo de trazabilidad de todo lo que se generó pero no entró al timebox de {{MANUAL_TESTING_TIMEBOX_DAYS}} días.
108
+
109
+ ## Regla de entrega
110
+
111
+ - Ruta de salida: `qa/02-test-plans/sprints/Sprint-<Sprint>/Plan-de-Pruebas-<Proyecto>-Sprint-<Sprint>.md`
112
+ (`<Proyecto>` es el `project.name` de `qa/qa-framework.config.json`; `<Sprint>` es el número de sprint sin padding, p.ej. `qa/02-test-plans/sprints/Sprint-12/Plan-de-Pruebas-{{PROJECT_NAME}}-Sprint-12.md`. Sigue la misma convención que los `Plan-de-Pruebas-*.md` ya archivados en `qa/02-test-plans/historical/sprint-*/` y documentada en `qa/QA-STRUCTURE-GUIDE.md`, si existen. Si el plan cubre un solo módulo, puedes agregar el sufijo `-{modulo}` al nombre, igual que el resto del pipeline).
113
+ - Si el directorio del sprint no existe, créalo antes de escribir el archivo.
114
+ - Antes de escribir, si ya existe un archivo con ese nombre, léelo primero y confirma con el usuario si se debe sobrescribir (no lo sobrescribas silenciosamente).
115
+ - El **Resumen del Plan** (punto 1) va en tu respuesta de chat; el **Plan detallado** con la tabla completa (punto 2) es el que se escribe al archivo.
116
+ - Al terminar, tu respuesta al usuario debe indicar la **ruta relativa exacta** del archivo creado.
117
+ - Nunca reportes la tarea como completa si el archivo no fue escrito con la herramienta `Write`.
118
+
119
+ # Checklist de calidad (marcar antes de entregar)
120
+
121
+ - [ ] Cada Bug/Issue/Task crítico tiene al menos un caso **P0** con **Confirma**.
122
+ - [ ] Los **Steps** usan `<br>` y están numerados.
123
+ - [ ] No hay **Resultados Esperados** vagos.
124
+ - [ ] La Tabla de Pruebas contiene solo **Tests Priorizados**; el **Universo de Tests** completo se resumió (conteo + factibilidad) y el **Universo excluido** quedó documentado con motivo.
125
+ - [ ] Cada fila de la Tabla de Pruebas tiene columna **Tipo** (Manual/Automatizado/Ambos/Bloqueado) coherente con su factibilidad de automatización.
126
+ - [ ] **TODO** y **Faltantes críticos** están claramente indicados.
127
+ - [ ] Se respetó el foco del timebox de {{MANUAL_TESTING_TIMEBOX_DAYS}} días y se documentaron exclusiones (backlog de regresión).
128
+ - [ ] Existe **Matriz de Trazabilidad** (ID <-> TestCases).
129
+ - [ ] El archivo `Plan-de-Pruebas-<Proyecto>-Sprint-<Sprint>.md` existe físicamente en `qa/02-test-plans/sprints/Sprint-<Sprint>/`.
130
+
131
+ Si alguna condición no se cumple, la respuesta se considera incompleta.
@@ -1,5 +1,5 @@
1
- # -----------------------------------------------------------------------
2
- # keber/qa-framework — Environment Variables Reference
1
+ # -----------------------------------------------------------------------
2
+ # keber/qa-framework - Environment Variables Reference
3
3
  # -----------------------------------------------------------------------
4
4
  # Copy this file to .env and fill in real values.
5
5
  # NEVER commit .env to source control.
@@ -17,12 +17,42 @@ QA_SESSION_TTL_MS=7200000
17
17
  # Optional path to a server-side logout endpoint if the app exposes one.
18
18
  # QA_LOGOUT_PATH=/logout
19
19
 
20
- # When parallel user mode is enabled, limit auth to the matching project(s).
20
+ # -----------------------------------------------------------------------
21
+ # Parallel QA lanes
22
+ # -----------------------------------------------------------------------
23
+ # A lane is a distinct QA account. Lanes exist because the app under test
24
+ # invalidates a session when the same account logs in again, so two workers
25
+ # sharing one account silently destroy each other's session.
26
+ #
27
+ # The lane table itself lives in qa-framework.config.json (parallelLanes.lanes).
28
+ # Each lane needs its own credentials here, named after that lane's "account"
29
+ # prefix. With no parallelLanes config, only the QA_USER pair below is used.
30
+
31
+ # Pin a run to a single lane. Required by global-setup unless QA_COLD_SETUP=1.
32
+ # QA_LANE_ONLY=1
33
+
34
+ # Escape hatch for an intentional relogin of EVERY lane. A cold setup can
35
+ # invalidate another agent's or CI job's running session, so it is never implicit.
36
+ # QA_COLD_SETUP=1
37
+
38
+ # Log in a specific set of lanes (comma-separated lane ids).
21
39
  # QA_AUTH_USER=1
22
40
 
23
- # Optional second test user for 2-project parallel execution.
41
+ # Shared lane-lock state directory. Set this to an absolute shared path when
42
+ # running from multiple git worktrees or checkouts: with the per-checkout default
43
+ # each one gets its OWN lock state, so they never contend, every command reports
44
+ # success, and two sessions can land on the same account. The failure is silent.
45
+ # QA_LANE_LOCKS_DIR=/absolute/path/to/shared/.locks
46
+
47
+ # Minutes before a held lane is swept as stale. A lane with a recent heartbeat is
48
+ # never swept regardless of age.
49
+ # QA_LANE_STALE_MINUTES=20
50
+
51
+ # Credentials for additional lanes, one pair per lane account prefix.
24
52
  # QA_USER2_EMAIL=qa-user2@example.com
25
53
  # QA_USER2_PASSWORD=CHANGE_ME
54
+ # QA_USER3_EMAIL=qa-user3@example.com
55
+ # QA_USER3_PASSWORD=CHANGE_ME
26
56
 
27
57
  # -----------------------------------------------------------------------
28
58
  # Login Configuration
@@ -56,12 +86,12 @@ QA_USER_PASSWORD=CHANGE_ME
56
86
  # QA_USER_RUT=12345678-9
57
87
 
58
88
  # -----------------------------------------------------------------------
59
- # Azure DevOps Integration (optional — leave blank to disable)
89
+ # Azure DevOps Integration (optional - leave blank to disable)
60
90
  # -----------------------------------------------------------------------
61
91
  # ADO_ORG_URL=https://dev.azure.com/your-org
62
92
  # ADO_PROJECT_NAME=YourProject
63
93
  # ADO_PLAN_ID=00000
64
- # AZURE_TOKEN=your-pat-token-here # Personal Access Token — NEVER commit
94
+ # AZURE_TOKEN=your-pat-token-here # Personal Access Token - NEVER commit
65
95
 
66
96
  # -----------------------------------------------------------------------
67
97
  # CI / Pipeline overrides
@@ -10,11 +10,11 @@
10
10
  * await loginAs(page, 'admin');
11
11
  *
12
12
  * Alternatively, use storageState via global-setup.ts (recommended for
13
- * performance — avoids repeating the login flow on every test).
13
+ * performance - avoids repeating the login flow on every test).
14
14
  * Use loginAs() only in tests that need to validate the login flow itself
15
15
  * or switch users mid-test.
16
16
  *
17
- * Supported roles are driven by env vars — see .env.example.
17
+ * Supported roles are driven by env vars - see .env.example.
18
18
  */
19
19
 
20
20
  import { Page } from '@playwright/test';
@@ -26,7 +26,7 @@ interface LoginConfig {
26
26
  password: string;
27
27
  }
28
28
 
29
- /** Map role name → credentials from environment variables */
29
+ /** Map role name -> credentials from environment variables */
30
30
  function getCredentials(role: QARole): LoginConfig {
31
31
  switch (role) {
32
32
  case 'admin':
@@ -58,7 +58,7 @@ export async function loginAs(page: Page, role: QARole = 'default'): Promise<voi
58
58
  if (!email || !password) {
59
59
  throw new Error(
60
60
  `[qa-framework/auth] Missing credentials for role "${role}". ` +
61
- `Check your .env file — expected ${role.toUpperCase()}_EMAIL and ${role.toUpperCase()}_PASSWORD.`
61
+ `Check your .env file - expected ${role.toUpperCase()}_EMAIL and ${role.toUpperCase()}_PASSWORD.`
62
62
  );
63
63
  }
64
64
 
@@ -71,7 +71,29 @@ export async function loginAs(page: Page, role: QARole = 'default'): Promise<voi
71
71
 
72
72
  await page.goto(`${baseURL}${loginPath}`, { waitUntil: 'domcontentloaded' });
73
73
  await page.locator(emailSelector).fill(email);
74
- await page.locator(passwordSelector).fill(password);
74
+ // Trace safety: Playwright records fill() argument values in traces, and this
75
+ // project runs with trace/video 'retain-on-failure'. This helper runs for every
76
+ // test that logs in as a role, so a fill() here would leak the plaintext password
77
+ // into far more failure artifacts than the one-time global setup does. Setting the
78
+ // value through evaluate() keeps it out of the trace.
79
+ // Do not "simplify" this back to fill().
80
+ // The input/change events are required because assigning .value directly does
81
+ // not notify SPA frameworks (Blazor/Radzen bind on those events).
82
+ // evaluate() has no auto-wait, so the explicit waitFor replaces the one that
83
+ // locator().fill() performed implicitly.
84
+ await page.locator(passwordSelector).waitFor({ state: 'visible', timeout: 30_000 });
85
+ await page.evaluate(
86
+ ([selector, pwd]) => {
87
+ const input = document.querySelector(selector) as HTMLInputElement | null;
88
+ if (!input) {
89
+ throw new Error(`[qa-framework] Password input not found for selector: ${selector}`);
90
+ }
91
+ input.value = pwd;
92
+ input.dispatchEvent(new Event('input', { bubbles: true }));
93
+ input.dispatchEvent(new Event('change', { bubbles: true }));
94
+ },
95
+ [passwordSelector, password] as const
96
+ );
75
97
  await page.locator(submitSelector).click();
76
98
  await page.waitForSelector(successSelector, { timeout: 15_000 });
77
99
  }
@@ -63,7 +63,29 @@ async function reauth(browser: import('@playwright/test').Browser, stateFile: st
63
63
  try {
64
64
  await page.goto(loginPath, { waitUntil: 'load', timeout: 60_000 });
65
65
  await page.locator(emailSelector).fill(email);
66
- await page.locator(passwordSelector).fill(password);
66
+ // Trace safety: Playwright records fill() argument values in traces, and this
67
+ // project runs with trace/video 'retain-on-failure'. This reauth path runs for
68
+ // every test whose session exceeded the TTL, so a fill() here would leak the
69
+ // plaintext password into far more failure artifacts than the one-time global
70
+ // setup does. Setting the value through evaluate() keeps it out of the trace.
71
+ // Do not "simplify" this back to fill().
72
+ // The input/change events are required because assigning .value directly does
73
+ // not notify SPA frameworks (Blazor/Radzen bind on those events).
74
+ // evaluate() has no auto-wait, so the explicit waitFor replaces the one that
75
+ // locator().fill() performed implicitly.
76
+ await page.locator(passwordSelector).waitFor({ state: 'visible', timeout: 30_000 });
77
+ await page.evaluate(
78
+ ([selector, pwd]) => {
79
+ const input = document.querySelector(selector) as HTMLInputElement | null;
80
+ if (!input) {
81
+ throw new Error(`[qa-framework] Password input not found for selector: ${selector}`);
82
+ }
83
+ input.value = pwd;
84
+ input.dispatchEvent(new Event('input', { bubbles: true }));
85
+ input.dispatchEvent(new Event('change', { bubbles: true }));
86
+ },
87
+ [passwordSelector, password] as const
88
+ );
67
89
  await page.locator(submitSelector).click();
68
90
  await page.waitForSelector(successSelector, { timeout: 30_000 });
69
91
 
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  // -----------------------------------------------------------------------
9
- // EXEC_IDX — unique numeric seed per minute-long execution window
9
+ // EXEC_IDX - unique numeric seed per minute-long execution window
10
10
  // -----------------------------------------------------------------------
11
11
  // Use this wherever test data needs to be unique (e.g., names, emails,
12
12
  // codes) to avoid collisions between parallel or consecutive test runs
@@ -11,14 +11,14 @@
11
11
  * - Reference the matching storageState in playwright.config.ts projects[].
12
12
  *
13
13
  * Environment variables required:
14
- * QA_BASE_URL — Base URL of the application under test
15
- * QA_USER_EMAIL — Default QA user email (or username/RUT)
16
- * QA_USER_PASSWORD — Default QA user password
17
- * QA_LOGIN_PATH — Relative path to the login page (default: /login)
18
- * QA_LOGIN_EMAIL_SELECTOR — CSS selector for the username/email input
19
- * QA_LOGIN_PASSWORD_SELECTOR — CSS selector for the password input
20
- * QA_LOGIN_SUBMIT_SELECTOR — CSS selector for the submit button
21
- * QA_LOGIN_SUCCESS_SELECTOR — CSS selector that confirms successful login
14
+ * QA_BASE_URL - Base URL of the application under test
15
+ * QA_USER_EMAIL - Default QA user email (or username/RUT)
16
+ * QA_USER_PASSWORD - Default QA user password
17
+ * QA_LOGIN_PATH - Relative path to the login page (default: /login)
18
+ * QA_LOGIN_EMAIL_SELECTOR - CSS selector for the username/email input
19
+ * QA_LOGIN_PASSWORD_SELECTOR - CSS selector for the password input
20
+ * QA_LOGIN_SUBMIT_SELECTOR - CSS selector for the submit button
21
+ * QA_LOGIN_SUCCESS_SELECTOR - CSS selector that confirms successful login
22
22
  *
23
23
  * See .env.example for all supported variables.
24
24
  */
@@ -30,6 +30,20 @@ import * as fs from 'fs';
30
30
 
31
31
  dotenv.config();
32
32
 
33
+ // Lane table and guards. Both are CommonJS so the guard logic stays testable with
34
+ // `node --test` without adding ts-node or tsx as a dependency.
35
+ const laneLock = require('./scripts/lane-lock.js');
36
+ const guards = require('./scripts/global-setup-guards.js');
37
+
38
+ type LaneInfo = {
39
+ account: string;
40
+ storageState: string;
41
+ mcpNamespace: string | null;
42
+ runnerProject: string | null;
43
+ capabilities: string[];
44
+ };
45
+ const LANE_INFO: Record<string, LaneInfo> = laneLock.LANE_INFO;
46
+
33
47
  function resolveVar(param: string | undefined, envKey: string, defaultValue: string): [string, string] {
34
48
  if (param !== undefined) {
35
49
  return [param, 'param'];
@@ -40,37 +54,51 @@ function resolveVar(param: string | undefined, envKey: string, defaultValue: str
40
54
  return [defaultValue, 'default'];
41
55
  }
42
56
 
43
- function resolveAuthUsers(config: FullConfig): Set<'1' | '2'> {
57
+ /**
58
+ * Decides which lanes to log in.
59
+ *
60
+ * Resolution order:
61
+ * 1. QA_LANE_ONLY - pins the run to exactly one lane. This is the normal path and
62
+ * the only one the guards permit without an explicit cold-setup opt-in.
63
+ * 2. QA_AUTH_USER - comma-separated lane ids, for an intentional multi-lane setup.
64
+ * 3. the projects' storageState, matched against the lane table.
65
+ * 4. every configured lane (the cold-setup path, gated by QA_COLD_SETUP=1).
66
+ *
67
+ * Step 3 matches storageState by EXACT equality against the lane table rather than by
68
+ * substring. The previous scaffold used storageState.includes('user-2'), which is what
69
+ * breaks first beyond two lanes: 'user-2' is a substring of 'user-20', and there was no
70
+ * branch at all for a third lane, so lanes past the second were silently never logged in.
71
+ */
72
+ function resolveAuthLanes(config: FullConfig): string[] {
73
+ const laneIds = Object.keys(LANE_INFO);
74
+
75
+ const laneOnly = process.env.QA_LANE_ONLY;
76
+ if (laneOnly) {
77
+ // Throws on an unknown lane rather than guessing. Guessing is how a run logs into
78
+ // an account it does not hold.
79
+ return [laneLock.laneIdForQaLaneOnly(laneOnly)];
80
+ }
81
+
44
82
  const explicit = process.env.QA_AUTH_USER;
45
83
  if (explicit) {
46
- const resolved = new Set<'1' | '2'>();
47
- for (const token of explicit.split(',').map((value) => value.trim())) {
48
- if (token === '1' || token === '2') {
49
- resolved.add(token);
50
- }
51
- }
52
- if (resolved.size) {
53
- return resolved;
54
- }
84
+ const resolved = explicit
85
+ .split(',')
86
+ .map((value) => value.trim())
87
+ .filter((value) => laneIds.includes(value));
88
+ if (resolved.length) return resolved;
55
89
  }
56
90
 
57
- const fromProjects = new Set<'1' | '2'>();
91
+ const fromProjects = new Set<string>();
58
92
  for (const project of config.projects) {
59
93
  const storageState = (project.use as { storageState?: unknown } | undefined)?.storageState;
60
- if (typeof storageState === 'string') {
61
- if (storageState.includes('user-2')) {
62
- fromProjects.add('2');
63
- } else if (storageState.includes('user-1')) {
64
- fromProjects.add('1');
65
- }
94
+ if (typeof storageState !== 'string') continue;
95
+ for (const laneId of laneIds) {
96
+ if (LANE_INFO[laneId].storageState === storageState) fromProjects.add(laneId);
66
97
  }
67
98
  }
99
+ if (fromProjects.size) return [...fromProjects];
68
100
 
69
- if (fromProjects.size) {
70
- return fromProjects;
71
- }
72
-
73
- return new Set(['1']);
101
+ return laneIds;
74
102
  }
75
103
 
76
104
  async function dismissOnboardingFlow(page: import('@playwright/test').Page): Promise<void> {
@@ -133,7 +161,27 @@ async function loginAs(params: {
133
161
  for (let attempt = 1; attempt <= 3 && !signedIn; attempt++) {
134
162
  await page.goto(loginUrl, { waitUntil: 'load', timeout: 60_000 });
135
163
  await page.locator(emailSelector).fill(email);
136
- await page.locator(passwordSelector).fill(password);
164
+ // Trace safety: Playwright records fill() argument values in traces, and this
165
+ // project runs with trace/video 'retain-on-failure' - a failed CI run would
166
+ // persist the plaintext password in the artifact. Setting the value through
167
+ // evaluate() keeps it out of the trace. Do not "simplify" this back to fill().
168
+ // The input/change events are required because assigning .value directly does
169
+ // not notify SPA frameworks (Blazor/Radzen bind on those events).
170
+ // evaluate() has no auto-wait, so the explicit waitFor replaces the one that
171
+ // locator().fill() performed implicitly.
172
+ await page.locator(passwordSelector).waitFor({ state: 'visible', timeout: 30_000 });
173
+ await page.evaluate(
174
+ ([selector, pwd]) => {
175
+ const input = document.querySelector(selector) as HTMLInputElement | null;
176
+ if (!input) {
177
+ throw new Error(`[qa-framework] Password input not found for selector: ${selector}`);
178
+ }
179
+ input.value = pwd;
180
+ input.dispatchEvent(new Event('input', { bubbles: true }));
181
+ input.dispatchEvent(new Event('change', { bubbles: true }));
182
+ },
183
+ [passwordSelector, password] as const
184
+ );
137
185
  await page.locator(submitSelector).click();
138
186
 
139
187
  const result = await Promise.race([
@@ -167,30 +215,51 @@ async function loginAs(params: {
167
215
  }
168
216
 
169
217
  export default async function globalSetup(_config: FullConfig): Promise<void> {
170
- const baseURL = process.env.QA_BASE_URL!;
171
- const authUsers = resolveAuthUsers(_config);
218
+ const baseURL = process.env.QA_BASE_URL!;
172
219
 
173
- if (authUsers.has('1')) {
174
- const [email, emailSource] = resolveVar(process.env.QA_USER_EMAIL, 'QA_USER_EMAIL', 'qa-user@example.com');
175
- const [password, passwordSource] = resolveVar(process.env.QA_USER_PASSWORD, 'QA_USER_PASSWORD', 'CHANGE_ME');
176
- console.log(`[global-setup] user-1 credentials from ${emailSource} / ${passwordSource}`);
177
- await loginAs({
178
- email,
179
- password,
180
- baseURL,
181
- stateFile: '.auth/user-default.json',
182
- });
220
+ // Guards run BEFORE any login. A lock nothing consults is decoration, and this is
221
+ // the last point at which a run about to drive an account it does not hold can
222
+ // still be stopped.
223
+ //
224
+ // global-setup NEVER acquires or reserves a lane itself. It only READS the lock
225
+ // state and verifies the lane is already held. Reserving is the caller's job - the
226
+ // operator, the orchestrator, or the CI job. A global-setup that reserved its own
227
+ // lane would make every accidental run look legitimate.
228
+ guards.assertColdSetupIsIntentional(process.env);
229
+
230
+ const laneOnly = process.env.QA_LANE_ONLY;
231
+ if (laneOnly) {
232
+ guards.assertLaneIsLocked(laneOnly, process.env, laneLock);
233
+ guards.assertProjectsMatchLane(laneOnly, _config.projects, process.argv, laneLock);
183
234
  }
184
235
 
185
- if (authUsers.has('2') && process.env.QA_USER2_EMAIL && process.env.QA_USER2_PASSWORD) {
186
- const [email, emailSource] = resolveVar(process.env.QA_USER2_EMAIL, 'QA_USER2_EMAIL', 'qa-user2@example.com');
187
- const [password, passwordSource] = resolveVar(process.env.QA_USER2_PASSWORD, 'QA_USER2_PASSWORD', 'CHANGE_ME');
188
- console.log(`[global-setup] user-2 credentials from ${emailSource} / ${passwordSource}`);
236
+ for (const laneId of resolveAuthLanes(_config)) {
237
+ const lane = LANE_INFO[laneId];
238
+
239
+ // Credentials come from the lane's declared account prefix, e.g. account
240
+ // 'QA_USER4' resolves QA_USER4_EMAIL / QA_USER4_PASSWORD. This is what replaces
241
+ // the old hardcoded QA_USER / QA_USER2 pair and is what lets a lane be added by
242
+ // config alone.
243
+ const emailKey = `${lane.account}_EMAIL`;
244
+ const passwordKey = `${lane.account}_PASSWORD`;
245
+
246
+ if (!process.env[emailKey] || !process.env[passwordKey]) {
247
+ throw new Error(
248
+ `[global-setup] Lane ${laneId} (account ${lane.account}) is missing ${emailKey} or ` +
249
+ `${passwordKey}. Every configured lane needs its own credentials, otherwise two lanes ` +
250
+ 'end up sharing an account, which is exactly what lanes exist to prevent.'
251
+ );
252
+ }
253
+
254
+ const [email, emailSource] = resolveVar(process.env[emailKey], emailKey, 'qa-user@example.com');
255
+ const [password, passwordSource] = resolveVar(process.env[passwordKey], passwordKey, 'CHANGE_ME');
256
+ console.log(`[global-setup] lane ${laneId} (${lane.account}) credentials from ${emailSource} / ${passwordSource}`);
257
+
189
258
  await loginAs({
190
259
  email,
191
260
  password,
192
261
  baseURL,
193
- stateFile: '.auth/user-2.json',
262
+ stateFile: lane.storageState,
194
263
  });
195
264
  }
196
265
  }
@@ -1,6 +1,21 @@
1
1
  import { defineConfig, devices } from '@playwright/test';
2
2
  import * as dotenv from 'dotenv';
3
- dotenv.config();
3
+ import * as fs from 'fs';
4
+
5
+ // Least privilege: this config only needs a handful of non-secret values, so it reads
6
+ // them with dotenv.parse(), which returns a plain object and never touches process.env.
7
+ // dotenv.config() would inject the WHOLE .env - every QA account password and ADO token
8
+ // living in it - into a process that has no use for them, where any dependency, crash
9
+ // dump or child process inherits them. Do not "simplify" this back to config().
10
+ const ENV_KEYS = ['QA_SESSION_TTL_MS', 'QA_BASE_URL'] as const;
11
+ if (fs.existsSync('.env')) {
12
+ const parsed = dotenv.parse(fs.readFileSync('.env'));
13
+ for (const key of ENV_KEYS) {
14
+ if (process.env[key] === undefined && parsed[key] !== undefined) {
15
+ process.env[key] = parsed[key];
16
+ }
17
+ }
18
+ }
4
19
 
5
20
  export const SESSION_TTL_MS = Number(process.env.QA_SESSION_TTL_MS ?? 2 * 60 * 60 * 1000);
6
21
 
@@ -26,7 +41,7 @@ for (const key of required) {
26
41
  // token: process.env.SYSTEM_ACCESSTOKEN ?? process.env.ADO_PAT,
27
42
  // planId: Number(process.env.ADO_PLAN_ID),
28
43
  // projectName: process.env.ADO_PROJECT!,
29
- // testRunTitle: `[Auto] Sprint {{NNN}} — ${new Date().toISOString().slice(0, 10)}`,
44
+ // testRunTitle: `[Auto] Sprint {{NNN}} - ${new Date().toISOString().slice(0, 10)}`,
30
45
  // publishTestResultsMode: 'testRun' as const,
31
46
  // uploadAttachments: true,
32
47
  // attachmentsType: ['screenshot', 'video', 'trace'] as const,
@@ -38,6 +53,57 @@ for (const key of required) {
38
53
  // },
39
54
  // };
40
55
 
56
+ // -------------------------------------------------------------------
57
+ // Parallel QA lanes
58
+ // -------------------------------------------------------------------
59
+ // A lane is a distinct QA account. Lanes exist because the applications under test
60
+ // invalidate a session when the same account logs in again, so two concurrent workers
61
+ // sharing one account silently destroy each other's session. Speed is not the reason.
62
+ //
63
+ // The lane table lives in qa-framework.config.json (parallelLanes.lanes) and is read
64
+ // here through lane-lock.js, which is the SINGLE SOURCE OF TRUTH for the
65
+ // lane -> storageState mapping. Never hardcode a storageState on a project below. A
66
+ // parallel hardcoded mapping caused real evidence corruption once already: a project
67
+ // pinned to one lane's storageState while QA_LANE_ONLY named another, so a run was
68
+ // recorded under the wrong account and nothing failed at the time.
69
+ //
70
+ // A project with no parallelLanes block gets one implicit default lane, producing
71
+ // exactly the single 'chromium' project this scaffold shipped before lanes existed.
72
+ const laneLock = require('./scripts/lane-lock.js');
73
+
74
+ type LaneInfo = {
75
+ account: string;
76
+ storageState: string;
77
+ mcpNamespace: string | null;
78
+ runnerProject: string | null;
79
+ capabilities: string[];
80
+ };
81
+ const LANE_INFO: Record<string, LaneInfo> = laneLock.LANE_INFO;
82
+
83
+ // QA_LANE_ONLY pins the whole run to a single lane. When it is set, every lane project
84
+ // resolves its storageState through that lane, so a --project bound to a different lane
85
+ // cannot quietly drive an account this run does not hold. global-setup-guards.js
86
+ // additionally rejects a --project whose storageState does not match the reserved lane.
87
+ const QA_LANE_ONLY = process.env.QA_LANE_ONLY;
88
+
89
+ function resolveStorageState(laneId: string): string {
90
+ return QA_LANE_ONLY
91
+ ? laneLock.storageStateForLaneOnly(QA_LANE_ONLY)
92
+ : LANE_INFO[laneId].storageState;
93
+ }
94
+
95
+ // One Playwright project per configured lane. A lane that declares a runnerProject uses
96
+ // that name; otherwise the project is named chromium-lane-<id>. With the single implicit
97
+ // default lane this yields one project named 'chromium'.
98
+ const laneProjects = Object.entries(LANE_INFO).map(([laneId, lane]) => ({
99
+ name: lane.runnerProject || `chromium-lane-${laneId}`,
100
+ use: {
101
+ ...devices['Desktop Chrome'],
102
+ storageState: resolveStorageState(laneId),
103
+ },
104
+ dependencies: ['setup'],
105
+ }));
106
+
41
107
  export default defineConfig({
42
108
  // ------ Test discovery ------
43
109
  testDir: './tests',
@@ -78,44 +144,18 @@ export default defineConfig({
78
144
  globalSetup: './global-setup.ts',
79
145
 
80
146
  // ------ Projects ------
147
+ // To add a lane, add an entry to parallelLanes.lanes in qa-framework.config.json.
148
+ // There is nothing to uncomment or copy-paste here: the lane projects are generated
149
+ // from that table, so the config stays the only place a lane is defined.
81
150
  projects: [
82
151
  {
83
152
  name: 'setup',
84
153
  use: { ...devices['Desktop Chrome'] },
85
154
  testMatch: /global-setup\.ts/,
86
155
  },
87
- {
88
- name: 'chromium',
89
- use: {
90
- ...devices['Desktop Chrome'],
91
- storageState: '.auth/user-default.json',
92
- },
93
- dependencies: ['setup'],
94
- },
156
+ ...laneProjects,
95
157
  ],
96
158
 
97
- // ------ Parallel 2-user variant ------
98
- // Uncomment and replace the single chromium project above to run two parallel
99
- // workers with separate credentials. Also set QA_USER2_EMAIL and
100
- // QA_USER2_PASSWORD in .env.
101
- //
102
- // {
103
- // name: 'chromium-user1',
104
- // use: {
105
- // ...devices['Desktop Chrome'],
106
- // storageState: '.auth/user-1.json',
107
- // },
108
- // dependencies: ['setup'],
109
- // },
110
- // {
111
- // name: 'chromium-user2',
112
- // use: {
113
- // ...devices['Desktop Chrome'],
114
- // storageState: '.auth/user-2.json',
115
- // },
116
- // dependencies: ['setup'],
117
- // },
118
-
119
159
  // ------ Output directories ------
120
160
  outputDir: 'test-results/',
121
161
  snapshotPathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',