socialseed-e2e 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl

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 (29) hide show
  1. socialseed_e2e/__init__.py +184 -20
  2. socialseed_e2e/__version__.py +2 -2
  3. socialseed_e2e/cli.py +353 -190
  4. socialseed_e2e/core/base_page.py +368 -49
  5. socialseed_e2e/core/config_loader.py +15 -3
  6. socialseed_e2e/core/headers.py +11 -4
  7. socialseed_e2e/core/loaders.py +6 -4
  8. socialseed_e2e/core/test_orchestrator.py +2 -0
  9. socialseed_e2e/core/test_runner.py +487 -0
  10. socialseed_e2e/templates/agent_docs/AGENT_GUIDE.md.template +412 -0
  11. socialseed_e2e/templates/agent_docs/EXAMPLE_TEST.md.template +152 -0
  12. socialseed_e2e/templates/agent_docs/FRAMEWORK_CONTEXT.md.template +55 -0
  13. socialseed_e2e/templates/agent_docs/WORKFLOW_GENERATION.md.template +182 -0
  14. socialseed_e2e/templates/data_schema.py.template +111 -70
  15. socialseed_e2e/templates/e2e.conf.template +19 -0
  16. socialseed_e2e/templates/service_page.py.template +82 -27
  17. socialseed_e2e/templates/test_module.py.template +21 -7
  18. socialseed_e2e/templates/verify_installation.py +192 -0
  19. socialseed_e2e/utils/__init__.py +29 -0
  20. socialseed_e2e/utils/ai_generator.py +463 -0
  21. socialseed_e2e/utils/pydantic_helpers.py +392 -0
  22. socialseed_e2e/utils/state_management.py +312 -0
  23. {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/METADATA +64 -27
  24. socialseed_e2e-0.1.2.dist-info/RECORD +38 -0
  25. socialseed_e2e-0.1.0.dist-info/RECORD +0 -29
  26. {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/WHEEL +0 -0
  27. {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/entry_points.txt +0 -0
  28. {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/licenses/LICENSE +0 -0
  29. {socialseed_e2e-0.1.0.dist-info → socialseed_e2e-0.1.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,412 @@
1
+ # 📘 Guía para Agentes de IA - SocialSeed E2E Framework
2
+
3
+ > **Versión 2.0 - Actualizado para evitar errores comunes**
4
+
5
+ Esta guía te permite generar tests E2E funcionales sin errores de importación, serialización o configuración.
6
+
7
+ ---
8
+
9
+ ## 🚨 REGLAS DE ORO (Lee esto primero)
10
+
11
+ ### 1. **Imports SIEMPRE Absolutos**
12
+ ```python
13
+ # ❌ NUNCA uses imports relativos
14
+ from ..data_schema import RegisterRequest # PROHIBIDO
15
+
16
+ # ✅ SIEMPRE uses imports absolutos
17
+ from services.auth_service.data_schema import RegisterRequest
18
+ from services.auth_service.auth_page import AuthServicePage
19
+ ```
20
+
21
+ ### 2. **Serialización con Aliases**
22
+ ```python
23
+ # ❌ NUNCA sin by_alias=True (envía refresh_token en lugar de refreshToken)
24
+ data = request.model_dump()
25
+
26
+ # ✅ SIEMPRE con by_alias=True (envía refreshToken como espera Java)
27
+ data = request.model_dump(by_alias=True)
28
+ ```
29
+
30
+ ### 3. **Modelos Pydantic con Config**
31
+ ```python
32
+ class RefreshTokenRequest(BaseModel):
33
+ # SIEMPRE incluye esto
34
+ model_config = {"populate_by_name": True}
35
+
36
+ # Campos compuestos SIEMPRE con alias camelCase
37
+ refresh_token: str = Field(
38
+ ...,
39
+ alias="refreshToken",
40
+ serialization_alias="refreshToken"
41
+ )
42
+ ```
43
+
44
+ ### 4. **Manejo Manual de Headers**
45
+ ```python
46
+ # ❌ NO existe update_headers()
47
+ self.update_headers({"Authorization": f"Bearer {token}"})
48
+
49
+ # ✅ Implementa _get_headers() manualmente
50
+ def _get_headers(self, extra=None):
51
+ headers = {"Content-Type": "application/json"}
52
+ if self.access_token:
53
+ headers["Authorization"] = f"Bearer {self.access_token}"
54
+ return headers
55
+ ```
56
+
57
+ ### 5. **Nombres de Métodos sin Conflicto**
58
+ ```python
59
+ # ❌ Conflicto: atributo refresh_token vs método refresh_token()
60
+ self.refresh_token = token # Atributo
61
+
62
+ # ✅ Usa prefijo do_ para métodos
63
+ self.refresh_token = token # Atributo
64
+ self.do_refresh_token() # Método
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 📦 Dependencias Requeridas
70
+
71
+ El archivo `requirements.txt` debe incluir:
72
+
73
+ ```
74
+ pydantic>=2.0.0
75
+ email-validator>=2.0.0 # REQUERIDO para EmailStr
76
+ dnspython>=2.0.0 # Para validación de email
77
+ ```
78
+
79
+ **Si falta email-validator:**
80
+ ```
81
+ ImportError: email-validator is not installed
82
+ ```
83
+
84
+ **Solución:**
85
+ ```bash
86
+ pip install -r requirements.txt
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 🎯 Templates Actualizados
92
+
93
+ ### data_schema.py - Estructura Base
94
+
95
+ ```python
96
+ """Data schema for <service> API.
97
+
98
+ ⚠️ IMPORTANTE: Todos los campos compuestos usan camelCase aliases.
99
+ """
100
+ from pydantic import BaseModel, Field, EmailStr
101
+ from typing import Optional
102
+
103
+
104
+ class LoginRequest(BaseModel):
105
+ """Login request."""
106
+ model_config = {"populate_by_name": True}
107
+
108
+ email: EmailStr
109
+ password: str
110
+
111
+
112
+ class RefreshTokenRequest(BaseModel):
113
+ """Refresh token request."""
114
+ model_config = {"populate_by_name": True}
115
+
116
+ # ⚠️ camelCase alias requerido para backend Java
117
+ refresh_token: str = Field(
118
+ ...,
119
+ alias="refreshToken",
120
+ serialization_alias="refreshToken"
121
+ )
122
+ ```
123
+
124
+ ### service_page.py - Estructura Base
125
+
126
+ ```python
127
+ """Page object for <service> API."""
128
+ from typing import Optional, Dict, Any
129
+ from playwright.sync_api import APIResponse
130
+ from socialseed_e2e.core.base_page import BasePage
131
+
132
+ from .data_schema import (
133
+ ENDPOINTS,
134
+ LoginRequest,
135
+ RefreshTokenRequest,
136
+ )
137
+
138
+
139
+ class AuthPage(BasePage):
140
+ """Page for auth service."""
141
+
142
+ def __init__(self, base_url: str, **kwargs):
143
+ super().__init__(base_url=base_url, **kwargs)
144
+ self.access_token: Optional[str] = None
145
+ self.refresh_token: Optional[str] = None
146
+
147
+ def _get_headers(self, extra: Optional[Dict] = None) -> Dict[str, str]:
148
+ headers = {"Content-Type": "application/json"}
149
+ if self.access_token:
150
+ headers["Authorization"] = f"Bearer {self.access_token}"
151
+ if extra:
152
+ headers.update(extra)
153
+ return headers
154
+
155
+ def do_login(self, request: LoginRequest) -> APIResponse:
156
+ """Login and store tokens."""
157
+ response = self.post(
158
+ ENDPOINTS["login"],
159
+ data=request.model_dump(by_alias=True) # ✅ SIEMPRE by_alias=True
160
+ )
161
+ if response.ok:
162
+ data = response.json()["data"]
163
+ self.access_token = data.get("token")
164
+ self.refresh_token = data.get("refreshToken")
165
+ return response
166
+
167
+ def do_refresh_token(self) -> APIResponse:
168
+ """Refresh access token."""
169
+ if not self.refresh_token:
170
+ raise ValueError("No refresh token")
171
+
172
+ request = RefreshTokenRequest(refresh_token=self.refresh_token)
173
+ response = self.post(
174
+ ENDPOINTS["refresh"],
175
+ data=request.model_dump(by_alias=True) # ✅ SIEMPRE by_alias=True
176
+ )
177
+ if response.ok:
178
+ data = response.json()["data"]
179
+ self.access_token = data.get("token")
180
+ self.refresh_token = data.get("refreshToken")
181
+ return response
182
+ ```
183
+
184
+ ### modules/01_login_flow.py - Estructura Base
185
+
186
+ ```python
187
+ """Test module 01: Login flow."""
188
+ from services.auth_service.data_schema import LoginRequest, TEST_USER
189
+
190
+ from typing import TYPE_CHECKING
191
+ if TYPE_CHECKING:
192
+ from services.auth_service.auth_page import AuthPage
193
+
194
+
195
+ def run(page: "AuthPage"):
196
+ """Execute login test."""
197
+ print("STEP 01: Testing Login")
198
+
199
+ login_data = LoginRequest(
200
+ email=TEST_USER["email"],
201
+ password=TEST_USER["password"]
202
+ )
203
+
204
+ response = page.do_login(login_data)
205
+
206
+ assert response.ok, f"Login failed: {response.status}"
207
+ assert page.access_token is not None, "Token not stored"
208
+
209
+ print(f"✓ Login successful")
210
+ print(f"✓ Token: {page.access_token[:20]}...")
211
+ ```
212
+
213
+ ---
214
+
215
+ ## 🔧 Solución de Problemas
216
+
217
+ ### Problema 1: `ImportError: email-validator is not installed`
218
+
219
+ **Causa:** Falta la dependencia email-validator.
220
+
221
+ **Solución:**
222
+ 1. Asegúrate de que `requirements.txt` incluya `email-validator>=2.0.0`
223
+ 2. Ejecuta: `pip install -r requirements.txt`
224
+
225
+ ### Problema 2: `cannot import name '_01_register_flow'`
226
+
227
+ **Causa:** Los módulos con nombres numéricos no pueden importarse normalmente.
228
+
229
+ **Solución - Tres métodos:**
230
+
231
+ **Método A: importlib (recomendado para scripts)**
232
+ ```python
233
+ import importlib.util
234
+ import importlib.machinery
235
+
236
+ def load_module(name, path):
237
+ loader = importlib.machinery.SourceFileLoader(name, path)
238
+ spec = importlib.util.spec_from_loader(name, loader)
239
+ module = importlib.util.module_from_spec(spec)
240
+ loader.exec_module(module)
241
+ return module
242
+
243
+ register_module = load_module(
244
+ "register_flow",
245
+ "/path/to/services/auth/modules/01_register_flow.py"
246
+ )
247
+ register_module.run(page)
248
+ ```
249
+
250
+ **Método B: Import directo**
251
+ ```python
252
+ from services.auth.modules._01_register_flow import run
253
+ run(page)
254
+ ```
255
+
256
+ **Método C: Import función**
257
+ ```python
258
+ from services.auth.modules import _01_register_flow
259
+ _01_register_flow.run(page)
260
+ ```
261
+
262
+ ### Problema 3: `Refresh token is required` (400 error)
263
+
264
+ **Causa:** Pydantic serializa `refresh_token` en lugar de `refreshToken`.
265
+
266
+ **Solución:**
267
+ 1. En el modelo, usa alias camelCase:
268
+ ```python
269
+ refresh_token: str = Field(
270
+ ...,
271
+ alias="refreshToken",
272
+ serialization_alias="refreshToken"
273
+ )
274
+ ```
275
+
276
+ 2. Al serializar, usa by_alias=True:
277
+ ```python
278
+ data = request.model_dump(by_alias=True)
279
+ ```
280
+
281
+ ### Problema 4: `Method declaration "logout" is obscured`
282
+
283
+ **Causa:** Conflicto entre atributo y método con mismo nombre.
284
+
285
+ **Solución:**
286
+ ```python
287
+ # Atributo
288
+ self.refresh_token: Optional[str] = None
289
+
290
+ # Método con prefijo do_
291
+ def do_refresh_token(self):
292
+ pass
293
+
294
+ def do_logout(self):
295
+ pass
296
+ ```
297
+
298
+ ### Problema 5: `update_headers not found`
299
+
300
+ **Causa:** El método `update_headers()` no existe en `BasePage`.
301
+
302
+ **Solución:** Implementa manejo manual:
303
+ ```python
304
+ def _get_headers(self, extra=None):
305
+ headers = {"Content-Type": "application/json"}
306
+ if self.access_token:
307
+ headers["Authorization"] = f"Bearer {self.access_token}"
308
+ if extra:
309
+ headers.update(extra)
310
+ return headers
311
+ ```
312
+
313
+ ---
314
+
315
+ ## ✅ Checklist Pre-Entrega
316
+
317
+ Antes de decir "terminado", verifica:
318
+
319
+ - [ ] `email-validator` está en requirements.txt
320
+ - [ ] Todos los modelos tienen `model_config = {"populate_by_name": True}`
321
+ - [ ] Campos compuestos tienen `alias` y `serialization_alias` en camelCase
322
+ - [ ] En todos los métodos de page: `request.model_dump(by_alias=True)`
323
+ - [ ] NINGÚN import relativo (`from ..x`)
324
+ - [ ] Todos los imports son absolutos (`from services.xxx`)
325
+ - [ ] Métodos conflictivos usan prefijo `do_` (do_refresh_token, do_logout)
326
+ - [ ] Headers manejados manualmente (no update_headers)
327
+ - [ ] Funciones `run(page)` bien definidas en módulos
328
+ - [ ] Type hints usando `TYPE_CHECKING` para evitar imports circulares
329
+
330
+ ---
331
+
332
+ ## 🎯 Flujo de Trabajo Recomendado
333
+
334
+ ### Para Agentes de IA:
335
+
336
+ 1. **Lee los controladores Java** del servicio target
337
+ 2. **Identifica endpoints**: rutas, métodos HTTP, body params
338
+ 3. **Crea data_schema.py**:
339
+ - Modelos Pydantic con aliases camelCase
340
+ - Constantes de endpoints
341
+ - Datos de test
342
+ 4. **Crea service_page.py**:
343
+ - Hereda de BasePage
344
+ - Implementa _get_headers()
345
+ - Métodos para cada endpoint
346
+ - Usa by_alias=True siempre
347
+ 5. **Crea modules/**:
348
+ - 01_*_flow.py: Crear/Login
349
+ - 02_*_flow.py: Operaciones CRUD
350
+ - 99_*_flow.py: Cleanup/Logout
351
+ 6. **Verifica** ejecutando: `python verify_installation.py`
352
+
353
+ ---
354
+
355
+ ## 📚 Recursos Adicionales
356
+
357
+ - **Ejemplo Completo:** Ver `.agent/EXAMPLE_TEST.md`
358
+ - **Flujo de Generación:** Ver `.agent/WORKFLOW_GENERATION.md`
359
+ - **Documentación Framework:** https://daironpf.github.io/socialseed-e2e/
360
+ - **Pydantic v2:** https://docs.pydantic.dev/latest/
361
+
362
+ ---
363
+
364
+ ## 🎓 Tips para Éxito
365
+
366
+ 1. **Siempre verifica serialización:**
367
+ ```python
368
+ print(request.model_dump(by_alias=True))
369
+ # Debe mostrar camelCase: {'refreshToken': 'xxx'}
370
+ # NO snake_case: {'refresh_token': 'xxx'}
371
+ ```
372
+
373
+ 2. **Test incremental:**
374
+ - Ejecuta test 01 primero
375
+ - Verifica que el estado se guarda en `page`
376
+ - Luego ejecuta test 02
377
+
378
+ 3. **Manejo de errores:**
379
+ ```python
380
+ assert response.ok, f"Failed: {response.status} - {response.text()[:100]}"
381
+ ```
382
+
383
+ 4. **Compartir estado:**
384
+ - Guarda en `page` (ej: `page.user_id = data["id"]`)
385
+ - Recupera en siguiente test
386
+
387
+ 5. **Ejecución Flexible:**
388
+ Si ejecutas desde el root del proyecto y el config está en una subcarpeta:
389
+ ```bash
390
+ e2e run -c otrotest/e2e.conf
391
+ ```
392
+ El framework encontrará los servicios automáticamente.
393
+
394
+ ---
395
+
396
+ ## 🚀 Resultado Esperado
397
+
398
+ Siguiendo esta guía, los agentes de IA pueden generar:
399
+
400
+ - ✅ Tests funcionales sin errores de importación
401
+ - ✅ Serialización correcta camelCase ↔ snake_case
402
+ - ✅ Manejo de autenticación entre tests
403
+ - ✅ Tests secuenciales con estado compartido
404
+
405
+ **Tiempo estimado:** 2-5 minutos por servicio
406
+ **Intervención humana:** Mínima o ninguna
407
+
408
+ ---
409
+
410
+ **Versión:** 2.0
411
+ **Última actualización:** 2026-02-03
412
+ **Framework:** socialseed-e2e v0.1.0+
@@ -0,0 +1,152 @@
1
+ # Ejemplo Completo de Test Generado
2
+
3
+ Este archivo sirve como referencia "Gold Standard" de lo que se espera generar.
4
+
5
+ **IMPORTANT PRINCIPLES:**
6
+ - NO relative imports (from ..x import y) - use absolute imports from `services.xxx.data_schema`
7
+ - Always use `by_alias=True` when serializing Pydantic models
8
+ - Handle authentication headers manually (no `update_headers` method)
9
+ - Use `do_*` prefix for methods to avoid name conflicts with attributes
10
+
11
+ Supongamos una API de Tareas (TODOs).
12
+
13
+ ## 1. services/todo_app/data_schema.py
14
+
15
+ ```python
16
+ from pydantic import BaseModel, Field
17
+ from typing import Optional
18
+
19
+ class TodoItem(BaseModel):
20
+ id: Optional[str] = None
21
+ title: str
22
+ completed: bool = False
23
+
24
+ class Config:
25
+ populate_by_name = True
26
+
27
+ ENDPOINTS = {
28
+ "list": "/todos",
29
+ "create": "/todos",
30
+ "get": "/todos/{id}",
31
+ "update": "/todos/{id}",
32
+ "delete": "/todos/{id}"
33
+ }
34
+ ```
35
+
36
+ ## 2. services/todo_app/todo_app_page.py
37
+
38
+ ```python
39
+ from socialseed_e2e.core.base_page import BasePage
40
+ from playwright.sync_api import APIResponse, APIRequestContext
41
+ from services.todo_app.data_schema import TodoItem, ENDPOINTS
42
+
43
+ class TodoAppPage(BasePage):
44
+ def __init__(self, base_url: str, **kwargs):
45
+ super().__init__(base_url=base_url, **kwargs)
46
+ self.created_todo_id = None # Estado compartido
47
+ self.auth_token = None
48
+
49
+ def do_create_todo(self, item: TodoItem) -> APIResponse:
50
+ """Create a new todo item."""
51
+ return self.post(
52
+ ENDPOINTS["create"],
53
+ data=item.model_dump(by_alias=True, exclude={"id"})
54
+ )
55
+
56
+ def do_get_todo(self, todo_id: str) -> APIResponse:
57
+ """Retrieve a todo by ID."""
58
+ path = ENDPOINTS["get"].format(id=todo_id)
59
+ return self.get(path)
60
+
61
+ def do_delete_todo(self, todo_id: str) -> APIResponse:
62
+ """Delete a todo by ID."""
63
+ path = ENDPOINTS["delete"].format(id=todo_id)
64
+ return self.delete(path)
65
+
66
+ def do_authenticate(self, token: str) -> None:
67
+ """Set authentication token manually."""
68
+ self.auth_token = token
69
+ # Manually handle headers - don't use update_headers()
70
+ self.headers = {**self.headers, "Authorization": f"Bearer {token}"}
71
+ ```
72
+
73
+ ## 3. services/todo_app/modules/01_create_todo.py
74
+
75
+ ```python
76
+ from services.todo_app.data_schema import TodoItem
77
+ from typing import TYPE_CHECKING
78
+ if TYPE_CHECKING:
79
+ from services.todo_app.todo_app_page import TodoAppPage
80
+
81
+ def run(page: 'TodoAppPage'):
82
+ print("STEP: Create Todo")
83
+
84
+ new_todo = TodoItem(title="Learn SocialSeed E2E", completed=False)
85
+
86
+ response = page.do_create_todo(new_todo)
87
+ assert response.ok, f"Failed to create todo: {response.status} - {response.text()}"
88
+
89
+ data = response.json()
90
+ assert data["title"] == new_todo.title, f"Title mismatch: expected {new_todo.title}, got {data.get('title')}"
91
+ assert "id" in data, "No 'id' field in response"
92
+
93
+ # Guardar ID para el siguiente paso
94
+ page.created_todo_id = data["id"]
95
+ print(f"✓ Created Todo with ID: {page.created_todo_id}")
96
+ ```
97
+
98
+ ## 4. services/todo_app/modules/02_get_todo.py
99
+
100
+ ```python
101
+ from typing import TYPE_CHECKING
102
+ if TYPE_CHECKING:
103
+ from services.todo_app.todo_app_page import TodoAppPage
104
+
105
+ def run(page: 'TodoAppPage'):
106
+ print("STEP: Get Todo")
107
+
108
+ # Recuperar ID del paso anterior
109
+ todo_id = page.created_todo_id
110
+ assert todo_id, "No created_todo_id found from previous step"
111
+
112
+ response = page.do_get_todo(todo_id)
113
+ assert response.ok, f"Failed to get todo: {response.status} - {response.text()}"
114
+
115
+ data = response.json()
116
+ assert data["id"] == todo_id, f"ID mismatch: expected {todo_id}, got {data.get('id')}"
117
+ print("✓ Todo retrieved successfully")
118
+ ```
119
+
120
+ ## 5. services/todo_app/modules/03_authenticated_request.py
121
+
122
+ ```python
123
+ from services.todo_app.data_schema import TodoItem
124
+ from typing import TYPE_CHECKING
125
+ if TYPE_CHECKING:
126
+ from services.todo_app.todo_app_page import TodoAppPage
127
+
128
+ def run(page: 'TodoAppPage'):
129
+ print("STEP: Authenticated Request Example")
130
+
131
+ # Perform authentication and store token
132
+ auth_response = page.post("/auth/login", data={
133
+ "username": "test@example.com",
134
+ "password": "password123"
135
+ })
136
+ assert auth_response.ok, f"Authentication failed: {auth_response.status}"
137
+
138
+ token = auth_response.json().get("token")
139
+ assert token, "No token in authentication response"
140
+
141
+ # Manually set the authentication token
142
+ page.do_authenticate(token)
143
+
144
+ # Now make authenticated request
145
+ new_todo = TodoItem(title="Authenticated Todo", completed=False)
146
+
147
+ # Use by_alias=True when serializing
148
+ response = page.do_create_todo(new_todo)
149
+ assert response.ok, f"Failed to create todo: {response.status} - {response.text()}"
150
+
151
+ print("✓ Authenticated request successful")
152
+ ```
@@ -0,0 +1,55 @@
1
+ # Contexto del Framework socialseed-e2e para Agentes de IA
2
+
3
+ Este documento te proporciona el contexto necesario para trabajar con el framework `socialseed-e2e`. Tu objetivo es ayudar al desarrollador a crear pruebas end-to-end (E2E) para APIs REST de manera autónoma.
4
+
5
+ ## 1. Arquitectura del Framework
6
+
7
+ El framework sigue una arquitectura hexagonal simplificada para desacoplar la lógica de prueba de la lógica del servicio.
8
+
9
+ ### Estructura de Directorios Clave
10
+
11
+ ```
12
+ .
13
+ ├── e2e.conf # Configuración principal (URLs, timeouts)
14
+ ├── services/ # Directorio de definición de servicios
15
+ │ └── <service-name>/ # Cada API/Microservicio tiene su carpeta
16
+ │ ├── __init__.py
17
+ │ ├── config.py # Configuración específica del servicio
18
+ │ ├── data_schema.py # DTOs (Pydantic), Endpoints y Constantes
19
+ │ ├── <name>_page.py # Page Object Model (hereda de BasePage)
20
+ │ └── modules/ # Tests individuales
21
+ │ ├── 01_register_flow.py
22
+ │ ├── 02_login_flow.py
23
+ │ └── ...
24
+ └── tests/ # Tests generales o de integración del propio framework
25
+ ```
26
+
27
+ ## 2. Conceptos Principales
28
+
29
+ ### Service Page (Page Object)
30
+ Cada servicio tiene una clase "Page" (ej. `UsersApiPage`) que hereda de `BasePage`.
31
+ - **Responsabilidad**: Abstraer las llamadas HTTP.
32
+ - **Ubicación**: `services/<service-name>/<name>_page.py`.
33
+ - **Métodos**: Deben corresponder a acciones de negocio (ej. `register_user`, `get_profile`) y retornar `APIResponse` de Playwright.
34
+
35
+ ### Test Modules
36
+ Los tests se dividen en archivos pequeños y numerados dentro de `services/<service-name>/modules/`.
37
+ - **Naming**: `XX_description_flow.py` (ej. `01_register_flow.py`).
38
+ - **Estructura**: Cada módulo debe tener una función `run(context)` o `run(page)`.
39
+ - **Orden**: Se ejecutan secuencialmente según su numeración.
40
+ - **Estado**: El estado se comparte a través de la instancia de la `Page`. Si el test 01 guarda un token en `page.auth_token`, el test 02 puede usarlo.
41
+
42
+ ### Data Schema
43
+ - **Ubicación**: `services/<service-name>/data_schema.py`.
44
+ - **Contenido**: Modelos Pydantic para request/response bodies y costantes de rutas.
45
+
46
+ ## 3. Tu Rol como Agente
47
+
48
+ Cuando el usuario te pida "generar tests", debes:
49
+ 1. **Analizar** el código fuente del controlador REST que te provea el usuario.
50
+ 2. **Identificar** los endpoints, métodos HTTP, y estructuras de datos.
51
+ 3. **Mapear** estos endpoints a métodos en la `ServicePage`.
52
+ 4. **Generar** módulos de prueba secuenciales que cubran flujos lógicos (crear -> obtener -> actualizar -> borrar).
53
+
54
+ ---
55
+ Lee `WORKFLOW_GENERATION.md` para instrucciones paso a paso sobre cómo generar código.