gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1543Shell 203Rust 109Markdown 993728 Definitions

⎇ 551a103e2dd119ec47b28d831074a214c8a080d2 scripts/test-atuq-foco.py Python · 162 lines · 7.9 KB History Who touched what raw

atuq §6.5.bis: la extensión `foco` — muestra el foco del sistema y no tiene con qué apagarlo 10c499777 · Sergio ·

1#!/usr/bin/env python3
2"""¿El navegador MUESTRA el modo foco del sistema, y lo deja como estaba? (SDD 26 §6.5)
3
4    python3 scripts/test-atuq-foco.py
5
6── QUÉ SE EJERCITA ───────────────────────────────────────────────────────────────────────────
7Tres corridas, una por estado del fichero que escribe root (`/etc/takana/focus`): `on`, `off` y
8**ausente**. En cada una se mide lo que la extensión DICE y lo que pinta en su insignia, leído del
9`dump` del navegador — no lo que el host contestó, que es la mitad que ya cubren los tests de
10`puriy-costura`.
11
12── Y LA PROMESA DEL §6.5, MEDIDA DEL LADO DEL NAVEGADOR ──────────────────────────────────────
13Después de cada corrida se vuelve a leer el fichero DENTRO de la jaula: tiene que estar **igual**.
14El foco lo sostiene un reglaset `nft` que aplicó root, y la única forma de que eso valga algo es que
15el navegador no pueda deshacerlo. Que no exista un verbo para apagarlo está fijado en tawasuyu
16(`no_hay_verbo_para_apagar_el_foco_y_eso_es_la_propiedad`); acá se comprueba el efecto: el estado
17sobrevive intacto a una sesión entera del navegador.
18
19`unknown` es el caso que más importa de los tres: si el host redondeara «no sé» a «apagado», la
20insignia diría «sin foco» sin que nadie lo haya mirado.
21
22REQUISITOS: el rootfs mínimo de `atuq-nested.sh`, y `atuq` + `puriy-costura` sellados.
23"""
24import os
25import shutil
26import subprocess
27import sys
28import tempfile
29
30ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
31RFS = os.environ.get("RFS", "/mnt/cosecha/escritorios/atuq-rootfs")
32ALPINE = os.path.join(ROOT, ".dev-fs/alpine")
33HAMMER = os.path.join(ROOT, "target/release/takana")
34STORE = os.environ.get("HAMMER_STORE", os.path.join(ROOT, "store"))
35# El path de producción, el mismo que la constante del host. Se mide ÉSTE y no la escotilla de
36# pruebas: una escotilla mide el código, no el contrato con la imagen.
37FOCO = "/etc/takana/focus"
38
39
40def fatal(msg):
41    print(f"✗ {msg}", file=sys.stderr)
42    raise SystemExit(1)
43
44
45def artefacto(receta, nombre, escotilla=None):
46    forzado = os.environ.get(escotilla) if escotilla else None
47    if forzado:
48        if not os.path.isdir(forzado):
49            fatal(f"{escotilla}={forzado} no existe")
50        print(f"⚠ {escotilla}: se mide {os.path.basename(forzado)}, que NO es necesariamente el vigente")
51        return forzado
52    r = subprocess.run([HAMMER, "--store", STORE, "hash", f"recipes/{receta}.toml"],
53                       cwd=ROOT, capture_output=True, text=True)
54    if r.returncode != 0:
55        fatal(f"no pude hashear {receta}: {r.stderr.strip()}")
56    h = r.stdout.strip().splitlines()[-1].removeprefix("b3:")
57    d = os.path.join(STORE, f"{h}-{nombre}")
58    if not os.path.isdir(d):
59        fatal(f"{nombre} b3:{h[:8]} no está sellado — construilo antes")
60    if not os.listdir(d):
61        fatal(f"{nombre} b3:{h[:8]} está VACÍO: eso no es un artefacto, es un nombre")
62    return d
63
64
65def correr(atuq, costura, salida, estado):
66    """Una sesión del navegador con el fichero de foco en `estado` (None = no existe)."""
67    poner = "rm -f " + FOCO if estado is None else (
68        f"mkdir -p {os.path.dirname(FOCO)} && printf '%s\\n' '{estado}' > {FOCO}")
69    guion = f"""
70    mkdir -p /root /salida/hogar
71    [ -s /etc/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > /etc/machine-id
72    export HOME=/salida/hogar
73    {poner}
74    timeout 45 /usr/bin/atuq --profile /salida/perfil --no-remote --headless 'about:blank' \
75        > /salida/consola.log 2>&1
76    # El estado, RELEÍDO después de que el navegador terminó: es la prueba de que no lo tocó.
77    if [ -f {FOCO} ]; then cp {FOCO} /salida/focus-final; else echo AUSENTE > /salida/focus-final; fi
78    exit 0
79    """
80    subprocess.run([
81        "bwrap",
82        "--overlay-src", ALPINE, "--overlay-src", RFS,
83        "--overlay-src", atuq, "--overlay-src", costura,
84        "--tmp-overlay", "/",
85        "--dev-bind", "/dev", "/dev", "--proc", "/proc", "--bind", "/sys", "/sys",
86        "--tmpfs", "/tmp", "--tmpfs", "/run", "--unshare-pid",
87        "--bind", salida, "/salida",
88        "--uid", "0", "--gid", "0",
89        "--setenv", "PATH", "/usr/bin:/bin:/usr/sbin:/sbin",
90        "--setenv", "XDG_RUNTIME_DIR", "/run",
91        "--setenv", "XDG_DATA_DIRS", "/usr/share", "--setenv", "MOZ_HEADLESS", "1",
92        "--setenv", "MOZ_DISABLE_CONTENT_SANDBOX", "1",
93        "--setenv", "MOZ_DISABLE_GMP_SANDBOX", "1",
94        "--setenv", "MOZ_DISABLE_RDD_SANDBOX", "1",
95        "--setenv", "MOZ_DISABLE_SOCKET_PROCESS_SANDBOX", "1",
96        "--setenv", "MOZ_DISABLE_UTILITY_SANDBOX", "1",
97        "sh", "-c", guion,
98    ], check=False)
99    log = open(os.path.join(salida, "consola.log"), errors="replace").read()
100    lineas = [l[len("FOCO "):] for l in log.splitlines() if l.startswith("FOCO ")]
101    final = ""
102    try:
103        final = open(os.path.join(salida, "focus-final"), errors="replace").read().strip()
104    except OSError:
105        pass
106    return lineas, final, log
107
108
109def main():
110    atuq = artefacto("atuq", "atuq", "ATUQ_DIR")
111    costura = artefacto("puriy-costura", "puriy-costura")
112    print(f"artefactos: {os.path.basename(atuq)}\n            {os.path.basename(costura)}")
113    if not os.path.isdir(RFS):
114        fatal(f"no está el rootfs en {RFS} — corré antes `scripts/atuq-nested.sh`")
115
116    # estado puesto → (lo que la extensión tiene que decir, la insignia, el fichero tras la sesión)
117    casos = [
118        ("on",  "on",      "foco", "on"),
119        ("off", "off",     "",     "off"),
120        (None,  "unknown", "?",    "AUSENTE"),
121    ]
122    tmp = tempfile.mkdtemp(prefix="atuq-foco-")
123    try:
124        for puesto, espera, insignia, tras in casos:
125            salida = os.path.join(tmp, f"caso-{puesto or 'ausente'}")
126            os.makedirs(os.path.join(salida, "perfil"))
127            with open(os.path.join(salida, "perfil", "user.js"), "w") as fh:
128                fh.write('user_pref("browser.dom.window.dump.enabled", true);\n')
129                fh.write('user_pref("browser.shell.checkDefaultBrowser", false);\n')
130
131            print(f"\n-- fichero de foco: {puesto if puesto else '(no existe)'}")
132            lineas, final, log = correr(atuq, costura, salida, puesto)
133            for l in lineas:
134                print("   " + l)
135            if not lineas:
136                destino = os.path.join(ROOT, "work", f"atuq-foco-{puesto or 'ausente'}.log")
137                os.makedirs(os.path.dirname(destino), exist_ok=True)
138                shutil.copy(os.path.join(salida, "consola.log"), destino)
139                fatal(f"la extensión no dijo nada — log en {destino}")
140
141            estados = [l for l in lineas if l.startswith("ESTADO ")]
142            if not estados:
143                fatal(f"la extensión no informó ningún estado: {lineas}")
144            if not estados[-1].startswith(f"ESTADO {espera} "):
145                fatal(f"con el fichero en {puesto!r} dijo «{estados[-1]}» y tenía que decir «{espera}»")
146            if f'INSIGNIA texto="{insignia}"' not in lineas:
147                fatal(f"la insignia no quedó en «{insignia}»: {[l for l in lineas if 'INSIGNIA' in l]}")
148            # Y la promesa: el navegador no cambió el estado.
149            if final != tras:
150                fatal(f"el fichero del foco quedó en {final!r} y tenía que quedar en {tras!r}"
151                      " — el navegador NO debería poder tocarlo")
152            print(f"   ✓ dijo {espera}, pintó «{insignia}», y dejó el estado igual ({final})")
153
154        print("\n✓ los tres estados llegan al navegador tal como los dejó el sistema")
155        print("✓ «no sé» es una respuesta: sin fichero, la insignia dice «?» y no «sin foco»")
156        print("✓ y tras una sesión entera el estado del foco quedó intacto: el navegador no lo apaga")
157        return 0
158    finally:
159        shutil.rmtree(tmp, ignore_errors=True)
160
161
162sys.exit(main())