@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,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,86 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SUELO FIJO de la forma `juego-terminal` (fijación del catálogo de Arketum: se
4
+ copia tal cual en `arketum/fijaciones/suelo/` y no se edita; el cierre compara su
5
+ checksum). Las comprobaciones de uso que no escribe quien construye: que el
6
+ programa vive, no que el juego esté bien.
7
+
8
+ Arranca el artefacto por su contrato de arranque: `sh arrancar.sh` desde su raíz.
9
+
10
+ arranca a 80x24, a los 3 s sigue vivo, pinta algo y sin traza
11
+ se_mueve sin tocar nada, en 2.4 s se ven al menos 5 fotogramas distintos (un juego en tiempo real anima)
12
+ aguanta 8 s más con flechas pulsadas, vivo y sin traza
13
+ sale_q con `q` termina
14
+ pequena a 30x10 no saca traza (avisa o sale limpio)
15
+
16
+ Uso: python3 arketum/fijaciones/suelo/suelo.py <raíz del artefacto> → JSON; código 0 si pasa, 1 si no.
17
+ Necesita `pyte` (`python3 -m pip install -r arketum/fijaciones/suelo/requirements.txt`).
18
+ """
19
+
20
+ import faulthandler, json, os, sys
21
+
22
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
23
+ from terminal_prueba import Terminal # noqa: E402
24
+
25
+ ARRANQUE = ['sh', 'arrancar.sh']
26
+
27
+
28
+ def pintadas(t):
29
+ """Lo más pintado en unas cuantas muestras: un juego que borra y redibuja puede pillarse a medias."""
30
+ n = 0
31
+ for _ in range(5):
32
+ t.esperar(0.15)
33
+ n = max(n, t.pintadas())
34
+ return n
35
+
36
+
37
+ def evaluar(raiz):
38
+ if not os.path.isfile(os.path.join(raiz, 'arrancar.sh')):
39
+ return {'ok': False, 'fallo': 'no hay `arrancar.sh` en la raíz del artefacto: es su contrato de arranque', 'pantalla': ''}
40
+ r = {'comando': ' '.join(ARRANQUE)}
41
+ with Terminal(ARRANQUE, cwd=raiz, columnas=80, filas=24) as t:
42
+ t.tecla('ENTER') # por si hay una pantalla de inicio que espera una tecla
43
+ t.esperar(3)
44
+ n = pintadas(t)
45
+ texto = t.pantalla()
46
+ r['arranca'] = t.vivo() and 'Traceback' not in texto and n >= 10
47
+ if not r['arranca']:
48
+ return {**r, 'ok': False, 'fallo': 'a 80x24 no arranca: muere, saca una traza o no pinta nada', 'pantalla': texto}
49
+ # Movimiento: muchos fotogramas distintos. Un juego que borra y redibuja parpadea entre dos
50
+ # estados (completo y vacío), y el parpadeo no es movimiento.
51
+ fotos = set()
52
+ for _ in range(12):
53
+ t.esperar(0.2)
54
+ fotos.add(t.foto())
55
+ r['se_mueve'] = len(fotos) >= 5
56
+ if not r['se_mueve']:
57
+ return {**r, 'ok': False, 'fallo': 'la pantalla no cambia sola: no hay nada en movimiento', 'pantalla': t.pantalla()}
58
+ for i in range(32):
59
+ t.tecla('ARRIBA' if (i // 4) % 2 else 'ABAJO')
60
+ t.esperar(0.25)
61
+ if not t.vivo():
62
+ break
63
+ texto = t.pantalla()
64
+ r['aguanta'] = t.vivo() and 'Traceback' not in texto
65
+ if not r['aguanta']:
66
+ return {**r, 'ok': False, 'fallo': 'jugando 8 s con flechas, el programa muere o saca una traza', 'pantalla': texto}
67
+ t.tecla('q')
68
+ t.esperar(3)
69
+ r['sale_q'] = not t.vivo()
70
+ if not r['sale_q']:
71
+ return {**r, 'ok': False, 'fallo': 'con `q` el programa no termina', 'pantalla': t.pantalla()}
72
+ with Terminal(ARRANQUE, cwd=raiz, columnas=30, filas=10) as t:
73
+ t.esperar(2.5)
74
+ texto = t.pantalla()
75
+ r['pequena'] = 'Traceback' not in texto and (t.vivo() or t.codigo == 0)
76
+ if not r['pequena']:
77
+ return {**r, 'ok': False, 'fallo': 'a 30x10 saca una traza o sale con error en vez de avisar', 'pantalla': texto}
78
+ return {**r, 'ok': True}
79
+
80
+
81
+ if __name__ == '__main__':
82
+ # Un suelo nunca se queda colgado: a los 150 s escribe dónde estaba y termina con error (1).
83
+ faulthandler.dump_traceback_later(150, exit=True)
84
+ resultado = evaluar(os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else '.'))
85
+ print(json.dumps(resultado, ensure_ascii=False, indent=1))
86
+ sys.exit(0 if resultado['ok'] else 1)
@@ -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,74 @@
1
+ # Stack: Go.
2
+ #
3
+ # Toma las ubicaciones de `servicio-con-integracion` (1.1.0): cada paquete en
4
+ # `internal/`, con las pruebas `<nombre>_test.go` junto a lo que prueban; cada
5
+ # ubicación de código las excluye, para que solo las reclame `prueba`.
6
+ #
7
+ # Cambios:
8
+ # - 1.1.0: las fijaciones de `juego-terminal` y de `aplicacion-por-capas` (los suelos y la ayuda e2e por
9
+ # terminal, de caja negra en Python), sus comandos y `arrancar.sh` como
10
+ # arranque; las pruebas e2e en `tests/e2e/`; lo que genera Python.
11
+ # - 1.2.0: `noEjecutan`, los argumentos con que el ejecutor de la suite e2e
12
+ # (pytest) no ejecuta ninguna prueba, que antes sabía Arketum. TRANSICIÓN: la
13
+ # huella del stack y la del efectivo cambian; los descensos certificados con la
14
+ # anterior dan `stale_policy` hasta recertificarlos.
15
+ name: go
16
+ version: 1.2.0
17
+ description: >-
18
+ Go: un módulo por artefacto, con sus paquetes en `internal/` y las pruebas `<nombre>_test.go` junto al código; `go.mod` y `go.sum` como módulo, `cmd/<binario>/main.go` y `arrancar.sh` como arranque, y las pruebas de extremo a extremo, de caja negra, en `tests/e2e/`.
19
+
20
+ ubicaciones:
21
+ modulo: { pattern: "go.<fichero>" }
22
+ dominio: { pattern: "internal/dominio/<nombre>.go", excluding: ["internal/dominio/<nombre>_test.go"] }
23
+ almacen: { pattern: "internal/almacen/<nombre>.go", excluding: ["internal/almacen/<nombre>_test.go"] }
24
+ servicio: { pattern: "internal/servicios/<nombre>.go", excluding: ["internal/servicios/<nombre>_test.go"] }
25
+ interfaz: { pattern: "internal/interfaz/<nombre>.go", excluding: ["internal/interfaz/<nombre>_test.go"] }
26
+ api: { pattern: "internal/api/<nombre>.go", excluding: ["internal/api/<nombre>_test.go"] }
27
+ cliente: { pattern: "internal/clientes/<nombre>.go", excluding: ["internal/clientes/<nombre>_test.go"] }
28
+ arranque: { pattern: ["cmd/<binario>/main.go", "arrancar.sh"] }
29
+ prueba: { pattern: ["internal/<paquete>/<nombre>_test.go", "tests/e2e/<nombre>.py"] }
30
+
31
+ muestras:
32
+ modulo: [go.mod, go.sum]
33
+ dominio: [internal/dominio/pedido.go]
34
+ almacen: [internal/almacen/sqlite.go]
35
+ servicio: [internal/servicios/pedidos.go]
36
+ interfaz: [internal/interfaz/web.go]
37
+ api: [internal/api/rutas.go]
38
+ cliente: [internal/clientes/catalogo.go]
39
+ arranque: [cmd/tienda/main.go, arrancar.sh]
40
+ prueba: [internal/servicios/pedidos_test.go, internal/dominio/pedido_test.go, tests/e2e/test_partida.py, tests/e2e/conftest.py]
41
+
42
+ # Lo que generan las pruebas e2e y las fijaciones, que son de Python.
43
+ generados: [__pycache__/, .pytest_cache/]
44
+
45
+ oficio:
46
+ - Toda frontera con otro sistema es una interfaz que el servicio declara tal como la necesita, con un doble para probar sin el sistema real.
47
+ - 'Los errores se devuelven y se envuelven con contexto (`fmt.Errorf("…: %w", err)`); nunca se ignoran ni se convierten en `panic`.'
48
+ - Ningún paquete de `internal/` importa `cmd/`; el cableado vive en `cmd/<binario>/main.go`.
49
+ - El contexto (`context.Context`) viaja como primer parámetro en lo que habla con fuera.
50
+ - '`arrancar.sh` compila y lanza el binario (`go run ./cmd/<binario> "$@"`, o el binario ya construido): es lo que lanzan el suelo y las pruebas e2e.'
51
+ - Las pruebas e2e (`tests/e2e/`, de caja negra, con pytest) arrancan el programa con el lanzador `app` (o `juego`) de la fijación e2e y lo usan por la terminal, como una persona. Las fijaciones necesitan `python3` con `pyte` y `pytest` (`python3 -m pip install -r arketum/fijaciones/e2e/requirements.txt`), que son de las pruebas, no del producto.
52
+
53
+ # Fijas del catálogo: se copian tal cual en `arketum/fijaciones/<nombre>/` del
54
+ # artefacto y no se editan (el cierre compara su checksum).
55
+ fijaciones:
56
+ - { de: stacks/go/suelo-juego-terminal, nombre: suelo, para: suelo, formas: [juego-terminal] }
57
+ - { de: stacks/go/suelo-aplicacion-por-capas, nombre: suelo, para: suelo, formas: [aplicacion-por-capas] }
58
+ - { de: stacks/go/e2e-terminal, nombre: e2e, para: e2e, formas: [juego-terminal, aplicacion-por-capas] }
59
+
60
+ # Los argumentos con los que su ejecutor de pruebas de la suite e2e (pytest, por la ayuda e2e de caja negra) no ejecuta ninguna: una
61
+ # ejecución de la suite e2e con uno de ellos no cuenta como e2e.
62
+ noEjecutan: [--co, --collect-only, --collectonly, --trace-config, --help, -h, --version, -V, --fixtures, --fixtures-per-test, --markers, --setup-plan, --setup-only]
63
+
64
+ comandos:
65
+ pruebas: go test ./...
66
+ suelo: python3 -B arketum/fijaciones/suelo/suelo.py .
67
+ e2e: PYTHONPATH=arketum/fijaciones/e2e python3 -B -m pytest -q -c arketum/fijaciones/e2e/pytest.ini --rootdir=. tests/e2e
68
+
69
+
70
+ selfReview:
71
+ criteria:
72
+ - check: La cobertura de pruebas del código propio es al menos del 80%
73
+ how: Corre `go test -cover ./...` y lee el total.
74
+ evidence: medida
@@ -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