@rlabs-cl/arketum 0.0.0-stage → 1.0.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 (77) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/LICENSE +48 -0
  3. package/README.md +67 -2
  4. package/THIRD-PARTY-NOTICES +2428 -0
  5. package/catalogo-de-ejemplo/agentes.yaml +11 -0
  6. package/catalogo-de-ejemplo/arquetipos/ajuste.yaml +54 -0
  7. package/catalogo-de-ejemplo/arquetipos/arquetipo-autor.yaml +72 -0
  8. package/catalogo-de-ejemplo/arquetipos/arquitectura-de-sistema.yaml +72 -0
  9. package/catalogo-de-ejemplo/arquetipos/despliegue-compose.yaml +104 -0
  10. package/catalogo-de-ejemplo/arquetipos/digestion.yaml +97 -0
  11. package/catalogo-de-ejemplo/arquetipos/documento-academico.yaml +131 -0
  12. package/catalogo-de-ejemplo/arquetipos/inventario-funcional.yaml +42 -0
  13. package/catalogo-de-ejemplo/arquetipos/levantamiento-de-infraestructura.yaml +117 -0
  14. package/catalogo-de-ejemplo/arquetipos/plan-de-migracion.yaml +40 -0
  15. package/catalogo-de-ejemplo/arquetipos/prd.yaml +62 -0
  16. package/catalogo-de-ejemplo/arquetipos/radio-de-alcance.yaml +141 -0
  17. package/catalogo-de-ejemplo/arquetipos/rca.yaml +68 -0
  18. package/catalogo-de-ejemplo/arquetipos/servicio-con-integracion.yaml +154 -0
  19. package/catalogo-de-ejemplo/carriles/ajuste.yaml +14 -0
  20. package/catalogo-de-ejemplo/carriles/arquetipo.yaml +13 -0
  21. package/catalogo-de-ejemplo/carriles/comprobacion.yaml +23 -0
  22. package/catalogo-de-ejemplo/carriles/correccion.yaml +25 -0
  23. package/catalogo-de-ejemplo/carriles/despliegue.yaml +36 -0
  24. package/catalogo-de-ejemplo/carriles/digestion.yaml +14 -0
  25. package/catalogo-de-ejemplo/carriles/funcionalidad.yaml +34 -0
  26. package/catalogo-de-ejemplo/carriles/migracion.yaml +27 -0
  27. package/catalogo-de-ejemplo/carriles/nuevo.yaml +30 -0
  28. package/catalogo-de-ejemplo/formas/aplicacion-por-capas.yaml +185 -0
  29. package/catalogo-de-ejemplo/formas/juego-terminal.yaml +161 -0
  30. package/catalogo-de-ejemplo/formas/monorepo-de-aplicaciones.yaml +207 -0
  31. package/catalogo-de-ejemplo/incluido.yaml +3 -0
  32. package/catalogo-de-ejemplo/stacks/go/e2e-terminal/e2e_http.py +36 -0
  33. package/catalogo-de-ejemplo/stacks/go/e2e-terminal/pytest.ini +5 -0
  34. package/catalogo-de-ejemplo/stacks/go/e2e-terminal/requirements.txt +2 -0
  35. package/catalogo-de-ejemplo/stacks/go/e2e-terminal/terminal_e2e.py +112 -0
  36. package/catalogo-de-ejemplo/stacks/go/e2e-terminal/terminal_prueba.py +118 -0
  37. package/catalogo-de-ejemplo/stacks/go/suelo-aplicacion-por-capas/requirements.txt +1 -0
  38. package/catalogo-de-ejemplo/stacks/go/suelo-aplicacion-por-capas/suelo.py +102 -0
  39. package/catalogo-de-ejemplo/stacks/go/suelo-aplicacion-por-capas/terminal_prueba.py +118 -0
  40. package/catalogo-de-ejemplo/stacks/go/suelo-juego-terminal/requirements.txt +1 -0
  41. package/catalogo-de-ejemplo/stacks/go/suelo-juego-terminal/suelo.py +86 -0
  42. package/catalogo-de-ejemplo/stacks/go/suelo-juego-terminal/terminal_prueba.py +118 -0
  43. package/catalogo-de-ejemplo/stacks/go.yaml +74 -0
  44. package/catalogo-de-ejemplo/stacks/python/e2e-terminal/e2e_http.py +36 -0
  45. package/catalogo-de-ejemplo/stacks/python/e2e-terminal/pytest.ini +5 -0
  46. package/catalogo-de-ejemplo/stacks/python/e2e-terminal/requirements.txt +2 -0
  47. package/catalogo-de-ejemplo/stacks/python/e2e-terminal/terminal_e2e.py +112 -0
  48. package/catalogo-de-ejemplo/stacks/python/e2e-terminal/terminal_prueba.py +118 -0
  49. package/catalogo-de-ejemplo/stacks/python/suelo-aplicacion-por-capas/requirements.txt +1 -0
  50. package/catalogo-de-ejemplo/stacks/python/suelo-aplicacion-por-capas/suelo.py +102 -0
  51. package/catalogo-de-ejemplo/stacks/python/suelo-aplicacion-por-capas/terminal_prueba.py +118 -0
  52. package/catalogo-de-ejemplo/stacks/python/suelo-juego-terminal/requirements.txt +1 -0
  53. package/catalogo-de-ejemplo/stacks/python/suelo-juego-terminal/suelo.py +86 -0
  54. package/catalogo-de-ejemplo/stacks/python/suelo-juego-terminal/terminal_prueba.py +118 -0
  55. package/catalogo-de-ejemplo/stacks/python.yaml +167 -0
  56. package/catalogo-de-ejemplo/stacks/typescript/e2e-node/app.mjs +182 -0
  57. package/catalogo-de-ejemplo/stacks/typescript/e2e-node/requirements.txt +1 -0
  58. package/catalogo-de-ejemplo/stacks/typescript/e2e-node/terminal_prueba.py +118 -0
  59. package/catalogo-de-ejemplo/stacks/typescript/e2e-node/terminal_puente.py +51 -0
  60. package/catalogo-de-ejemplo/stacks/typescript/e2e-node/vitest.config.mjs +12 -0
  61. package/catalogo-de-ejemplo/stacks/typescript/suelo-aplicacion-por-capas/requirements.txt +1 -0
  62. package/catalogo-de-ejemplo/stacks/typescript/suelo-aplicacion-por-capas/suelo.py +102 -0
  63. package/catalogo-de-ejemplo/stacks/typescript/suelo-aplicacion-por-capas/terminal_prueba.py +118 -0
  64. package/catalogo-de-ejemplo/stacks/typescript/suelo-juego-terminal/requirements.txt +1 -0
  65. package/catalogo-de-ejemplo/stacks/typescript/suelo-juego-terminal/suelo.py +86 -0
  66. package/catalogo-de-ejemplo/stacks/typescript/suelo-juego-terminal/terminal_prueba.py +118 -0
  67. package/catalogo-de-ejemplo/stacks/typescript.yaml +88 -0
  68. package/dist/mcp/arketum.js +3 -0
  69. package/dist/mcp/ci-cli.js +2 -0
  70. package/dist/mcp/cli.js +2 -0
  71. package/dist/mcp/comun-ZEXDDERM.js +1 -0
  72. package/dist/mcp/comun-ZTEHRDHC.js +1 -0
  73. package/dist/mcp/panel-cli.js +2 -0
  74. package/dist/mcp/telemetria-cli.js +1 -0
  75. package/package.json +33 -4
  76. package/server.json +15 -0
  77. package/telemetria.json +27 -0
@@ -0,0 +1,207 @@
1
+ # Forma de sistema: dos o tres aplicaciones que se despliegan por separado, en
2
+ # un mismo repositorio, con frontera.
3
+ #
4
+ # Cambios:
5
+ # - 1.2.0: los papeles de la fase 2 (`implementador` y `revisor`) en las
6
+ # dos variantes. Sin salidas: el suelo y el e2e de varios despliegues
7
+ # levantan el sistema entero, y eso no cabe en las salidas de un artefacto.
8
+ # - 1.1.0: señales `actores`, `consumidores` y `volumen`; variantes (`simple` y
9
+ # `completa`, con los componentes `duena` y `consumidora`) y los escalones
10
+ # `varios-actores`, `consumido-por-sistemas`, `mucho-volumen` e
11
+ # `integraciones`. Un artefacto con capas declara `stack` y `componente`.
12
+ name: monorepo-de-aplicaciones
13
+ version: 1.2.0
14
+ description: >-
15
+ Dos o tres aplicaciones que se despliegan por separado en un mismo repositorio, con frontera: la que mantiene el dato principal es su dueña y las demás lo leen por su API.
16
+ orden: 3
17
+ senales:
18
+ despliegues: numero
19
+ integraciones: numero
20
+ actores: numero
21
+ consumidores: texto
22
+ volumen: numero
23
+ aplica:
24
+ - [despliegues, '>=', 2]
25
+ - [despliegues, '<=', 3]
26
+ - [integraciones, '<=', 3]
27
+ diseno:
28
+ - Son dos aplicaciones que se despliegan por separado; la que mantiene el dato principal es su dueña.
29
+ - La dueña del dato es la única que lo escribe; las demás (y cualquier otro sistema) solo lo leen, y solo a través de su API.
30
+ - Cada aplicación que no es dueña tiene que poder cambiarse o sustituirse sin tocar a la dueña.
31
+ - Lo que cada aplicación guarda por su cuenta es suyo y está separado del dato principal.
32
+ - El dato principal y lo que guarda cada aplicación sobreviven a reinicios; los datos de partida se cargan una vez desde lo que se usa hoy.
33
+ huecos:
34
+ - Cuáles son las aplicaciones (lo que usa cada tipo de persona o sistema) y si cada una tiene que poder cambiarse o sustituirse sin tocar la otra.
35
+ - De quién es cada dato (quién lo escribe y lo corrige) y quién solo lo lee.
36
+ - Qué otros sistemas usan alguna de las aplicaciones, qué pueden hacer con ellas y qué no.
37
+ - Qué guarda cada aplicación por su cuenta, además del dato principal.
38
+ - De dónde salen los datos de hoy, cuántos son, cómo se pasan al sistema y qué tiene que sobrevivir a un reinicio.
39
+ - Cuándo tiene que verse en una aplicación un cambio hecho en la otra.
40
+
41
+ variantes:
42
+ - nombre: simple
43
+ aplica:
44
+ - [despliegues, '<=', 2]
45
+ - [actores, '<=', 1]
46
+ papeles: &papeles
47
+ - nombre: implementador
48
+ para_que: construye el sistema en el paso de sistema (el código, sus pruebas y las salidas que exige el paso de evidencia)
49
+ texto: >-
50
+ PAPEL: IMPLEMENTADOR (fase 2). Construyes lo acordado, nada más: los requisitos que trae la unidad y los técnicos vigentes, con el oficio del arquetipo.
51
+ Desciendes cada artefacto tocado nivel a nivel y el código se queda como lo pide su stack; no lo deformas para encajar.
52
+ Cada funcional que construyes lleva su prueba de extremo a extremo, que arranca el artefacto por su contrato de arranque y lo usa como una persona:
53
+ una prueba que llama por dentro a una función que el camino real no usa no demuestra nada.
54
+ Copias las fijaciones del catálogo tal cual y no las editas. Corres tú las pruebas, el suelo y la suite e2e entera, y vinculas cada ejecución
55
+ a sus requisitos con `satisfies`. No rebajas ni borras una prueba para que pase. Si algo que funcionaba se rompe, lo arreglas;
56
+ si no puedes, lo reportas con `solicitar` (`pide: defecto`) y decide la persona. Nunca lo escondes.
57
+ - nombre: revisor
58
+ para_que: revisa en el paso de evidencia que lo construido funciona por el camino real, antes de entregar la evidencia
59
+ texto: >-
60
+ PAPEL: REVISOR (fase 2). No escribes el código: lo revisas como lo usaría la persona. Compruebas que cada funcional construido tiene su prueba e2e,
61
+ que arranca el artefacto por su contrato de arranque y no lo llama por dentro, y que pasa; que las fijaciones del catálogo no se tocaron;
62
+ y miras las pruebas modificadas o borradas desde la foto (las lista el resumen del paso): si alguna se rebajó para que pase, lo dices.
63
+ Corres el suelo y la suite e2e entera con el comando compuesto exacto y entregas esa evidencia. Lo que falla no lo das por bueno:
64
+ se arregla, o se reporta como defecto con `solicitar` y decide la persona. Si un funcional no admite una prueba automática
65
+ (un correo, algo visual, una integración externa), lo pides con `solicitar` (`pide: sin_e2e`), con el motivo.
66
+ componentes:
67
+ - nombre: duena
68
+ description: >-
69
+ La aplicación dueña del dato principal, en tres peldaños: lo que se pide, sus piezas y el código. Es la única que lo escribe, y lo ofrece por su API.
70
+ kinds:
71
+ - { name: objetivo, describes: "pieza de la solicitud (L0), sin ubicación en el árbol", ubicado: false }
72
+ - { name: modulo, describes: la definición del paquete o del módulo, ubicado: true }
73
+ - { name: dominio, describes: "las entidades del dato principal y sus reglas, puras", ubicado: true }
74
+ - { name: almacen, describes: guardar y cargar el dato principal, ubicado: true }
75
+ - { name: servicio, describes: "un caso de uso por acción, sobre el dominio y el almacén, sin HTTP", ubicado: true }
76
+ - { name: api, describes: "la API con que las demás leen el dato; solo traduce peticiones a servicios", ubicado: true }
77
+ - { name: arranque, describes: el punto de entrada de la aplicación, ubicado: true }
78
+ - { name: prueba, describes: fichero de prueba, ubicado: true }
79
+ coRequirements:
80
+ - { when: servicio, requires: [prueba] }
81
+ levels: &niveles-simples
82
+ - id: L0
83
+ requiredRoles: []
84
+ title: SOLICITUD
85
+ instruction: Declara qué tiene que hacer esta aplicación, para quién, y qué datos son suyos.
86
+ - id: L1
87
+ requiredRoles: []
88
+ title: PIEZAS
89
+ instruction: Propón sus piezas, con la frontera con la otra aplicación a la vista.
90
+ fanout: { min: 3, max: 16 }
91
+ - id: L2
92
+ requiredRoles: []
93
+ title: IMPLEMENTACIÓN
94
+ instruction: Escribe el código; lo que habla con la otra aplicación lo hace solo por su API.
95
+ fanout: { min: 1, max: 60 }
96
+ - nombre: consumidora
97
+ description: >-
98
+ Una aplicación que lee el dato principal por la API de la dueña, en tres peldaños. Se puede cambiar o sustituir sin tocar a la dueña.
99
+ kinds:
100
+ - { name: objetivo, describes: "pieza de la solicitud (L0), sin ubicación en el árbol", ubicado: false }
101
+ - { name: modulo, describes: la definición del paquete o del módulo, ubicado: true }
102
+ - { name: cliente, describes: el único sitio que llama a la API de la dueña, ubicado: true }
103
+ - { name: dominio, describes: "lo que la aplicación hace con lo que lee, puro", ubicado: true }
104
+ - { name: almacen, describes: "lo que guarda por su cuenta, separado del dato principal", ubicado: true }
105
+ - { name: interfaz, describes: lo que ve quien la usa, ubicado: true }
106
+ - { name: arranque, describes: el punto de entrada de la aplicación, ubicado: true }
107
+ - { name: prueba, describes: fichero de prueba, ubicado: true }
108
+ coRequirements:
109
+ - { when: cliente, requires: [prueba] }
110
+ levels: *niveles-simples
111
+ - nombre: completa
112
+ aplica: []
113
+ papeles: *papeles
114
+ componentes:
115
+ - nombre: duena
116
+ description: >-
117
+ La aplicación dueña del dato principal, en cinco peldaños: lo que se pide, sus piezas, sus contratos, el cableado y el código.
118
+ kinds:
119
+ - { name: objetivo, describes: "pieza de la solicitud (L0), sin ubicación en el árbol", ubicado: false }
120
+ - { name: modulo, describes: la definición del paquete o del módulo, ubicado: true }
121
+ - { name: dominio, describes: "las entidades del dato principal y sus reglas, puras", ubicado: true }
122
+ - { name: almacen, describes: guardar y cargar el dato principal, ubicado: true }
123
+ - { name: servicio, describes: "un caso de uso por acción, sobre el dominio y el almacén, sin HTTP", ubicado: true }
124
+ - { name: api, describes: "la API con que las demás leen el dato; solo traduce peticiones a servicios", ubicado: true }
125
+ - { name: arranque, describes: el punto de entrada de la aplicación, ubicado: true }
126
+ - { name: prueba, describes: fichero de prueba, ubicado: true }
127
+ - { name: dependencia, describes: "arista del cableado (L3) entre dos piezas declaradas", ubicado: false }
128
+ coRequirements:
129
+ - { when: servicio, requires: [prueba] }
130
+ - { when: almacen, requires: [prueba] }
131
+ levels: &niveles-completos
132
+ - id: L0
133
+ requiredRoles: []
134
+ title: SOLICITUD
135
+ instruction: Declara qué tiene que hacer esta aplicación, para quién, y qué datos son suyos.
136
+ - id: L1
137
+ requiredRoles: []
138
+ title: PIEZAS
139
+ instruction: Propón sus piezas, con la frontera con la otra aplicación a la vista.
140
+ fanout: { min: 3, max: 20 }
141
+ - id: L2
142
+ requiredRoles: []
143
+ title: CONTRATOS
144
+ instruction: Declara cada operación de la API (o cada llamada a ella) con lo que recibe, lo que devuelve y sus errores.
145
+ fanout: { min: 1, max: 40 }
146
+ - id: L3
147
+ requiredRoles: []
148
+ title: CABLEADO
149
+ instruction: Declara qué depende de qué; nada de una aplicación importa la otra.
150
+ fanout: { min: 1, max: 50 }
151
+ - id: L4
152
+ requiredRoles: []
153
+ title: IMPLEMENTACIÓN
154
+ instruction: Escribe el código; lo que habla con la otra aplicación lo hace solo por su API.
155
+ fanout: { min: 1, max: 90 }
156
+ - nombre: consumidora
157
+ description: >-
158
+ Una aplicación que lee el dato principal por la API de la dueña, en cinco peldaños. Se puede cambiar o sustituir sin tocar a la dueña.
159
+ kinds:
160
+ - { name: objetivo, describes: "pieza de la solicitud (L0), sin ubicación en el árbol", ubicado: false }
161
+ - { name: modulo, describes: la definición del paquete o del módulo, ubicado: true }
162
+ - { name: cliente, describes: el único sitio que llama a la API de la dueña, ubicado: true }
163
+ - { name: dominio, describes: "lo que la aplicación hace con lo que lee, puro", ubicado: true }
164
+ - { name: almacen, describes: "lo que guarda por su cuenta, separado del dato principal", ubicado: true }
165
+ - { name: interfaz, describes: lo que ve quien la usa, ubicado: true }
166
+ - { name: arranque, describes: el punto de entrada de la aplicación, ubicado: true }
167
+ - { name: prueba, describes: fichero de prueba, ubicado: true }
168
+ - { name: dependencia, describes: "arista del cableado (L3) entre dos piezas declaradas", ubicado: false }
169
+ coRequirements:
170
+ - { when: cliente, requires: [prueba] }
171
+ levels: *niveles-completos
172
+
173
+ escalones:
174
+ - nombre: varios-actores
175
+ cuando:
176
+ - [actores, '>=', 2]
177
+ diseno:
178
+ - Cada actor tiene su vía; mantener el dato se hace del lado de la aplicación dueña, y la que usa el público nunca escribe en él.
179
+ huecos:
180
+ - Quién mantiene el dato principal y desde dónde.
181
+ oficio:
182
+ - Mantener el dato principal se hace del lado de la dueña; la consumidora nunca escribe en él.
183
+ - nombre: consumido-por-sistemas
184
+ cuando:
185
+ - [consumidores, 'en', [sistemas, ambos]]
186
+ diseno:
187
+ - La API de la aplicación dueña es un contrato estable para otros sistemas.
188
+ huecos:
189
+ - Qué otros sistemas usan la API, qué pueden hacer con ella y cómo se enteran de un cambio.
190
+ oficio:
191
+ - La API es un contrato - respuestas estables, errores con código y mensaje, y cada ruta descrita con un ejemplo.
192
+ coRequirements:
193
+ - { when: api, requires: [prueba], componentes: [duena] }
194
+ - nombre: mucho-volumen
195
+ cuando:
196
+ - [volumen, '>=', 1000]
197
+ oficio:
198
+ - Con miles de registros nada recorre ni descarga el dato entero en cada petición; los listados se paginan.
199
+ - nombre: integraciones
200
+ cuando:
201
+ - [integraciones, '>=', 1]
202
+ diseno:
203
+ - Cada sistema ajeno se usa a través de un solo sitio, que se puede sustituir por un doble para probar.
204
+ oficio:
205
+ - Cada llamada a otro sistema tiene un doble para probar sin el sistema real, que cubre el fallo y la demora.
206
+ coRequirements:
207
+ - { when: servicio, requires: [prueba] }
@@ -0,0 +1,3 @@
1
+ # El marcador del catálogo que trae Arketum (ds-f06): lo reconoce por sí mismo, se llame como se llame el
2
+ # paquete donde está instalado. No es política: el lector de catálogos no lo lee.
3
+ nombre: ejemplo
@@ -0,0 +1,36 @@
1
+ """Peticiones HTTP para las pruebas de extremo a extremo (fijación del catálogo de Arketum: no se edita). Solo la biblioteca estándar."""
2
+ import json, urllib.error, urllib.parse, urllib.request
3
+
4
+
5
+ def _hacer(req, timeout=15):
6
+ try:
7
+ with urllib.request.urlopen(req, timeout=timeout) as r:
8
+ return r.status, r.read().decode('utf8', 'replace')
9
+ except urllib.error.HTTPError as e:
10
+ return e.code, e.read().decode('utf8', 'replace')
11
+
12
+
13
+ def get(url, **params):
14
+ if params:
15
+ url += ('&' if '?' in url else '?') + urllib.parse.urlencode(params)
16
+ return _hacer(url)
17
+
18
+
19
+ def get_json(url, **params):
20
+ st, t = get(url, **params)
21
+ try:
22
+ return st, json.loads(t)
23
+ except ValueError:
24
+ return st, t
25
+
26
+
27
+ def post_json(url, datos, metodo='POST'):
28
+ st, t = _hacer(urllib.request.Request(url, data=json.dumps(datos).encode(), method=metodo, headers={'Content-Type': 'application/json'}))
29
+ try:
30
+ return st, json.loads(t)
31
+ except ValueError:
32
+ return st, t
33
+
34
+
35
+ def post_form(url, datos):
36
+ return _hacer(urllib.request.Request(url, data=urllib.parse.urlencode(datos).encode(), headers={'Content-Type': 'application/x-www-form-urlencoded'}))
@@ -0,0 +1,5 @@
1
+ # Fijación del catálogo de Arketum: no se edita. La suite e2e se corre desde la
2
+ # raíz del artefacto con:
3
+ # PYTHONPATH=arketum/fijaciones/e2e python3 -m pytest -q -c arketum/fijaciones/e2e/pytest.ini --rootdir=. tests/e2e
4
+ [pytest]
5
+ addopts = -p terminal_e2e -p no:cacheprovider --confcutdir=.
@@ -0,0 +1,112 @@
1
+ """
2
+ AYUDA E2E (fijación del catálogo de Arketum: se copia tal cual en
3
+ `arketum/fijaciones/e2e/` y no se edita; el cierre compara su checksum). Es un
4
+ complemento de pytest que carga `pytest.ini` (`-p terminal_e2e`), con la carpeta en `PYTHONPATH`:
5
+
6
+ PYTHONPATH=arketum/fijaciones/e2e python3 -m pytest -q -c arketum/fijaciones/e2e/pytest.ini --rootdir=. tests/e2e
7
+
8
+ Arranca el artefacto por el camino real, con su contrato de arranque
9
+ (`sh arrancar.sh` desde su raíz), con datos nuevos: `HOME` y `DATOS_DIR` en una
10
+ carpeta temporal propia y `PUERTO` libre. Las pruebas de `tests/e2e/` usan:
11
+
12
+ - `app` (o `juego`): un lanzador.
13
+ - `app()` (o `app(columnas, filas)`): la aplicación en una terminal emulada
14
+ (`Terminal` de `terminal_prueba`: `tecla`, `esperar`, `pantalla`, `vivo`…).
15
+ - `app.reabrir()`: cierra lo abierto y vuelve a arrancar con los mismos datos.
16
+ - `app.servidor()`: la arranca sin terminal (para una aplicación que sirve
17
+ HTTP en `PUERTO`) y espera a que responda; devuelve su URL base.
18
+ - `app.url`, `app.datos`: la URL base (`http://127.0.0.1:<PUERTO>`) y la
19
+ carpeta de datos.
20
+ - `e2e_http` (módulo): `get`, `get_json`, `post_json`, `post_form`.
21
+
22
+ Una prueba e2e recorre lo que haría una persona por la interfaz real: nunca
23
+ importa el código del artefacto ni llama a sus funciones por dentro.
24
+ """
25
+
26
+ import os, signal, socket, subprocess, sys, tempfile, time, urllib.error, urllib.request
27
+
28
+ import pytest
29
+
30
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
31
+
32
+ ARRANQUE = ['sh', 'arrancar.sh']
33
+
34
+
35
+ def _libre():
36
+ with socket.socket() as s:
37
+ s.bind(('127.0.0.1', 0))
38
+ return s.getsockname()[1]
39
+
40
+
41
+ def _responde(url):
42
+ try:
43
+ urllib.request.urlopen(url, timeout=2)
44
+ return True
45
+ except urllib.error.HTTPError:
46
+ return True
47
+ except Exception: # noqa: BLE001
48
+ return False
49
+
50
+
51
+ class Lanzador:
52
+ def __init__(self, raiz):
53
+ self.raiz = raiz
54
+ self.casa = tempfile.mkdtemp(prefix='e2e-')
55
+ self.datos = os.path.join(self.casa, 'datos')
56
+ self.puerto = _libre()
57
+ self.url = f'http://127.0.0.1:{self.puerto}'
58
+ self.abiertas = []
59
+ self.procesos = []
60
+
61
+ def entorno(self):
62
+ return {'HOME': self.casa, 'DATOS_DIR': self.datos, 'PUERTO': str(self.puerto), 'PYTHONUNBUFFERED': '1'}
63
+
64
+ def __call__(self, columnas=80, filas=24):
65
+ from terminal_prueba import Terminal # necesita pyte; solo si se usa la terminal
66
+
67
+ t = Terminal(ARRANQUE, cwd=self.raiz, columnas=columnas, filas=filas, env=self.entorno())
68
+ self.abiertas.append(t)
69
+ return t
70
+
71
+ def reabrir(self, columnas=80, filas=24):
72
+ self.cerrar()
73
+ return self(columnas, filas)
74
+
75
+ def servidor(self, ruta='/', segundos=25):
76
+ p = subprocess.Popen(ARRANQUE, cwd=self.raiz, env=dict(os.environ, **self.entorno()), stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
77
+ self.procesos.append(p)
78
+ fin = time.time() + segundos
79
+ while time.time() < fin:
80
+ if p.poll() is not None:
81
+ raise AssertionError(f'`sh arrancar.sh` terminó con código {p.returncode} antes de responder en {self.url}')
82
+ if _responde(self.url + ruta):
83
+ return self.url
84
+ time.sleep(0.2)
85
+ raise AssertionError(f'`sh arrancar.sh` no respondió en {self.url}{ruta} en {segundos} s')
86
+
87
+ def cerrar(self):
88
+ for t in self.abiertas:
89
+ t.cerrar()
90
+ self.abiertas = []
91
+ for p in self.procesos:
92
+ try:
93
+ os.killpg(p.pid, signal.SIGTERM)
94
+ p.wait(timeout=5)
95
+ except Exception: # noqa: BLE001
96
+ try:
97
+ os.killpg(p.pid, signal.SIGKILL)
98
+ except Exception: # noqa: BLE001
99
+ pass
100
+ self.procesos = []
101
+
102
+
103
+ @pytest.fixture
104
+ def app(request):
105
+ l = Lanzador(str(request.config.rootpath))
106
+ yield l
107
+ l.cerrar()
108
+
109
+
110
+ @pytest.fixture
111
+ def juego(app):
112
+ return app
@@ -0,0 +1,118 @@
1
+ """
2
+ TERMINAL DE PRUEBA (fijación del catálogo de Arketum: se copia tal cual y no se
3
+ edita). Prueba un programa de terminal como lo usaría una persona, en una
4
+ pseudo-terminal emulada con pyte (`python3 -m pip install pyte`).
5
+
6
+ from terminal_prueba import Terminal
7
+
8
+ with Terminal(['sh', 'arrancar.sh'], cwd='.', columnas=80, filas=24) as t:
9
+ t.esperar(2) # deja correr el programa
10
+ t.tecla('ARRIBA') # ARRIBA, ABAJO, IZQUIERDA, DERECHA, ENTER, ESC o un carácter
11
+ t.esperar(0.5)
12
+ texto = t.pantalla() # lo que se ve, una cadena con una línea por fila
13
+ assert t.vivo() # sigue corriendo
14
+ assert 'Traceback' not in texto
15
+
16
+ `t.lineas()` da la lista de filas y `t.codigo` el código de salida cuando termina.
17
+ """
18
+
19
+ import fcntl, os, pty, select, signal, struct, termios, time
20
+
21
+ import pyte
22
+
23
+ TECLAS = {'ARRIBA': b'\x1b[A', 'ABAJO': b'\x1b[B', 'DERECHA': b'\x1b[C', 'IZQUIERDA': b'\x1b[D', 'ENTER': b'\r', 'ESC': b'\x1b'}
24
+
25
+
26
+ class Terminal:
27
+ def __init__(self, comando, cwd='.', columnas=80, filas=24, env=None):
28
+ self._pantalla = pyte.Screen(columnas, filas)
29
+ self._flujo = pyte.ByteStream(self._pantalla)
30
+ pid, fd = pty.fork()
31
+ if pid == 0:
32
+ entorno = dict(os.environ, LANG='en_US.UTF-8', LC_ALL='en_US.UTF-8', TERM='xterm-256color', COLUMNS=str(columnas), LINES=str(filas))
33
+ entorno.update(env or {})
34
+ os.chdir(cwd)
35
+ os.execvpe(comando[0], comando, entorno)
36
+ fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack('HHHH', filas, columnas, 0, 0))
37
+ self.pid, self.fd, self._vivo, self.codigo = pid, fd, True, None
38
+
39
+ def __enter__(self):
40
+ return self
41
+
42
+ def __exit__(self, *_):
43
+ self.cerrar()
44
+
45
+ def esperar(self, segundos):
46
+ fin = time.time() + segundos
47
+ while time.time() < fin:
48
+ r, _, _ = select.select([self.fd], [], [], 0.05)
49
+ if r:
50
+ try:
51
+ datos = os.read(self.fd, 65536)
52
+ except OSError:
53
+ datos = b''
54
+ if datos:
55
+ self._flujo.feed(datos)
56
+ else:
57
+ time.sleep(0.05)
58
+ self._comprobar()
59
+ self._comprobar()
60
+
61
+ def tecla(self, nombre):
62
+ datos = TECLAS.get(nombre, nombre.encode() if isinstance(nombre, str) else nombre)
63
+ # Como un terminal real: si el programa activó el modo de cursor de aplicación (DECCKM, el modo privado 1, que pyte guarda como 1 << 5; curses
64
+ # con keypad), las flechas van como ESC O x en vez de ESC [ x.
65
+ if datos[:2] == b'\x1b[' and len(datos) == 3 and datos[2:] in b'ABCD' and (1 << 5) in self._pantalla.mode:
66
+ datos = b'\x1bO' + datos[2:]
67
+ try:
68
+ os.write(self.fd, datos)
69
+ except OSError:
70
+ pass
71
+
72
+ def pantalla(self):
73
+ return '\n'.join(self._pantalla.display)
74
+
75
+ def lineas(self):
76
+ return list(self._pantalla.display)
77
+
78
+ def pintadas(self):
79
+ """Cuántas celdas se ven: con carácter, en vídeo inverso o con un fondo distinto del
80
+ dominante (al borrar, pyte rellena con el fondo del cursor: ese no cuenta)."""
81
+ from collections import Counter
82
+ filas, cols = self._pantalla.lines, self._pantalla.columns
83
+ celdas = [self._pantalla.buffer[y][x] for y in range(filas) for x in range(cols)]
84
+ fondo = Counter(c.bg for c in celdas).most_common(1)[0][0]
85
+ return sum(1 for c in celdas if c.data.strip() or c.reverse or c.bg != fondo)
86
+
87
+ def foto(self):
88
+ """Lo que se ve, con forma: el texto y dónde hay celdas en vídeo inverso o con fondo distinto del dominante."""
89
+ from collections import Counter
90
+ filas, cols = self._pantalla.lines, self._pantalla.columns
91
+ fondo = Counter(self._pantalla.buffer[y][x].bg for y in range(filas) for x in range(cols)).most_common(1)[0][0]
92
+ marcas = tuple((y, x) for y in range(filas) for x in range(cols) if self._pantalla.buffer[y][x].reverse or self._pantalla.buffer[y][x].bg != fondo)
93
+ return (self.pantalla(), marcas)
94
+
95
+ def vivo(self):
96
+ self._comprobar()
97
+ return self._vivo
98
+
99
+ def _comprobar(self):
100
+ if not self._vivo:
101
+ return
102
+ p, estado = os.waitpid(self.pid, os.WNOHANG)
103
+ if p != 0:
104
+ self._vivo = False
105
+ self.codigo = os.waitstatus_to_exitcode(estado)
106
+
107
+ def cerrar(self):
108
+ if self._vivo:
109
+ try:
110
+ os.kill(self.pid, signal.SIGKILL)
111
+ os.waitpid(self.pid, 0)
112
+ except OSError:
113
+ pass
114
+ self._vivo = False
115
+ try:
116
+ os.close(self.fd)
117
+ except OSError:
118
+ pass
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SUELO FIJO de la forma `aplicacion-por-capas` (fijación del catálogo de Arketum:
4
+ se copia tal cual en `arketum/fijaciones/suelo/` y no se edita; el cierre compara
5
+ su checksum). Las comprobaciones de uso que no escribe quien construye: que la
6
+ aplicación vive, se puede usar y cerrar, y vuelve a arrancar con sus datos; no
7
+ que haga bien lo que pide el encargo.
8
+
9
+ Arranca el artefacto por su contrato de arranque: `sh arrancar.sh` desde su raíz,
10
+ en una terminal, con `DATOS_DIR` (la carpeta de sus datos), `HOME` y `PUERTO` (un
11
+ puerto libre, por si sirve HTTP) en una carpeta temporal propia.
12
+
13
+ arranca a 80x24, a los 3 s pinta algo y sin traza (vivo, o esperando órdenes)
14
+ responde tras Enter y unas flechas sigue sin traza
15
+ sale termina limpio (código 0) con `q`, con la orden `salir`/`q`/`exit` + Enter, con la opción de salir del menú, o con fin de entrada
16
+ rearranca con los mismos datos (mismo HOME y DATOS_DIR) vuelve a arrancar sin traza
17
+
18
+ Uso: python3 arketum/fijaciones/suelo/suelo.py <raíz del artefacto> → JSON; código 0 si pasa, 1 si no.
19
+ Necesita `pyte` (`python3 -m pip install -r arketum/fijaciones/suelo/requirements.txt`).
20
+ """
21
+
22
+ import faulthandler, json, os, re, socket, sys, tempfile
23
+
24
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
25
+ from terminal_prueba import Terminal # noqa: E402
26
+
27
+ ARRANQUE = ['sh', 'arrancar.sh']
28
+ SALIDAS = [b'q', b'\r', b'salir\r', b'q\r', b'exit\r']
29
+ # Una opción de menú para salir, la que se vea en pantalla: «5. Salir», «[0] Exit»…
30
+ OPCION_DE_SALIR = re.compile(r'[\[(]?\s*(\w)\s*[\])\.:-]\s*(?:salir|exit|quit|terminar|cerrar)\b', re.I)
31
+
32
+
33
+ def libre():
34
+ with socket.socket() as s:
35
+ s.bind(('127.0.0.1', 0))
36
+ return s.getsockname()[1]
37
+
38
+
39
+ def pintadas(t):
40
+ n = 0
41
+ for _ in range(5):
42
+ t.esperar(0.15)
43
+ n = max(n, t.pintadas())
44
+ return n
45
+
46
+
47
+ def evaluar(raiz):
48
+ if not os.path.isfile(os.path.join(raiz, 'arrancar.sh')):
49
+ return {'ok': False, 'fallo': 'no hay `arrancar.sh` en la raíz del artefacto: es su contrato de arranque', 'pantalla': ''}
50
+ r = {'comando': ' '.join(ARRANQUE)}
51
+ casa = tempfile.mkdtemp(prefix='suelo-')
52
+ env = {'HOME': casa, 'DATOS_DIR': os.path.join(casa, 'datos'), 'PUERTO': str(libre())}
53
+ with Terminal(ARRANQUE, cwd=raiz, columnas=80, filas=24, env=env) as t:
54
+ t.esperar(3)
55
+ n = pintadas(t)
56
+ texto = t.pantalla()
57
+ r['arranca'] = 'Traceback' not in texto and n >= 10 and (t.vivo() or t.codigo == 0)
58
+ if not r['arranca'] or not t.vivo():
59
+ return {**r, 'ok': False, 'fallo': 'a 80x24 no arranca: muere, saca una traza o no pinta nada', 'pantalla': texto}
60
+ t.tecla('ENTER')
61
+ for k in ('ABAJO', 'ARRIBA'):
62
+ t.tecla(k)
63
+ t.esperar(0.3)
64
+ t.esperar(1)
65
+ texto = t.pantalla()
66
+ r['responde'] = 'Traceback' not in texto and (t.vivo() or t.codigo == 0)
67
+ if not r['responde']:
68
+ return {**r, 'ok': False, 'fallo': 'tras Enter y unas flechas saca una traza o muere con error', 'pantalla': texto}
69
+ for b in SALIDAS:
70
+ if not t.vivo():
71
+ break
72
+ t.tecla(b)
73
+ t.esperar(1.5)
74
+ for _ in range(2): # la opción de salir del menú que se vea, y como último recurso, fin de entrada
75
+ m = OPCION_DE_SALIR.search(t.pantalla())
76
+ if not t.vivo() or m is None:
77
+ break
78
+ t.tecla(f'{m.group(1)}\r')
79
+ t.esperar(1.5)
80
+ if t.vivo():
81
+ t.tecla(b'\x04')
82
+ t.esperar(1.5)
83
+ texto = t.pantalla()
84
+ r['sale'] = not t.vivo() and t.codigo == 0 and 'Traceback' not in texto
85
+ if not r['sale']:
86
+ return {**r, 'ok': False, 'fallo': 'no termina limpio con `q`, ni con `salir`/`q`/`exit` + Enter, ni con fin de entrada' if t.vivo() else 'al salir saca una traza o termina con error', 'pantalla': texto}
87
+ env['PUERTO'] = str(libre())
88
+ with Terminal(ARRANQUE, cwd=raiz, columnas=80, filas=24, env=env) as t:
89
+ t.esperar(3)
90
+ texto = t.pantalla()
91
+ r['rearranca'] = 'Traceback' not in texto and (t.vivo() or t.codigo == 0)
92
+ if not r['rearranca']:
93
+ return {**r, 'ok': False, 'fallo': 'con los datos de la primera vez, al volver a abrir saca una traza o muere', 'pantalla': texto}
94
+ return {**r, 'ok': True}
95
+
96
+
97
+ if __name__ == '__main__':
98
+ # Un suelo nunca se queda colgado: a los 150 s escribe dónde estaba y termina con error (1).
99
+ faulthandler.dump_traceback_later(150, exit=True)
100
+ resultado = evaluar(os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else '.'))
101
+ print(json.dumps(resultado, ensure_ascii=False, indent=1))
102
+ sys.exit(0 if resultado['ok'] else 1)