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 815Rust 56Shell 37Markdown 291229 Definitions

⎇ 534014c3ec69daea789a1d0523e1d00a69c6ed65 scripts/test-atuq-inicio.py Python · 186 lines · 8.4 KB History Who touched what raw

takana etapa 3b: las 49 invocaciones pasan a ./target/release/takana 8730aad34 · Sergio ·

1#!/usr/bin/env python3
2"""¿La página de inicio y la pestaña nueva de `atuq` son las NUESTRAS? (SDD 26 §4.e)
3
4    python3 scripts/test-atuq-inicio.py                     # sale 0 si las dos son de atuq
5    python3 scripts/test-atuq-inicio.py --negative-control  # sin el XPI: exige que sean las de Firefox
6
7La v0.3 puso la página de inicio y la pestaña nueva **por extensión de sistema**, con
8`chrome_url_overrides.newtab` y `chrome_settings_overrides.homepage`. Que el XPI esté en el
9artefacto no dice nada de eso: un override lo puede rechazar el gestor de extensiones, lo puede
10pisar una política, y —el modo de fallo propio de este proyecto— el navegador puede arrancar con el
11chrome VIEJO cacheado y enseñar la página de Firefox como si nada. Ninguna de las tres cosas
12falla ruidosamente: se ve una pestaña nueva perfectamente normal, que es de otro.
13
14── CÓMO SE MIDE, SIN ABRIR PESTAÑAS NI MIRAR PÍXELES ─────────────────────────────────────────
15Una extensión sonda con permiso `browserSettings` le pregunta al propio navegador cuál es su
16`homepageOverride` y su `newTabPageOverride`. Es la respuesta del motor, no una inferencia: si el
17override de `inicio` agarró, las dos son `moz-extension://…/inicio.html`; si no, son las de fábrica
18(`about:home` / `about:newtab`). Mirar el XPI en el disco, o el `manifest.json` dentro del XPI,
19mediría lo declarado — que es justo lo que ya sabíamos.
20
21El control negativo BORRA el XPI de `inicio` dentro del overlay temporal (el rootfs real no se toca)
22y exige el resultado contrario. Sin él, una sonda rota que siempre dijera «es la nuestra» se vería
23idéntica a una que funciona.
24
25REQUISITOS: el rootfs de `scripts/atuq-nested.sh` (que lo hidrata) y `atuq` sellado.
26"""
27import json
28import os
29import shutil
30import subprocess
31import sys
32import tempfile
33import zipfile
34
35ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
36RFS = os.environ.get("RFS", "/mnt/cosecha/escritorios/atuq-rootfs")
37ALPINE = os.path.join(ROOT, ".dev-fs/alpine")
38HAMMER = os.path.join(ROOT, "target/release/takana")
39STORE = os.environ.get("HAMMER_STORE", os.path.join(ROOT, "store"))
40
41EXT_ID = "sonda-inicio@atuq.tawasuyu"
42XPI_INICIO = "/usr/lib/atuq/distribution/extensions/inicio@atuq.tawasuyu.xpi"
43
44MANIFIESTO = {
45    "manifest_version": 2,
46    "name": "sonda-inicio",
47    "version": "1.0",
48    "browser_specific_settings": {"gecko": {"id": EXT_ID}},
49    "permissions": ["browserSettings"],
50    "background": {"scripts": ["fondo.js"]},
51}
52
53FONDO = """
54function di(s) { try { dump("INICIO-TEST " + s + "\\n"); } catch (e) {} }
55async function mirar() {
56  try {
57    const h = await browser.browserSettings.homepageOverride.get({});
58    const n = await browser.browserSettings.newTabPageOverride.get({});
59    di("HOMEPAGE " + h.value);
60    di("NEWTAB " + n.value);
61  } catch (e) {
62    di("EXCEPCIÓN " + e.message);
63  }
64}
65mirar();
66"""
67
68
69def fatal(msg):
70    print(f"✗ {msg}", file=sys.stderr)
71    raise SystemExit(1)
72
73
74def artefacto(receta, nombre):
75    """El artefacto VIGENTE, por `hammer hash` y nunca por glob."""
76    # ⚠ ESCOTILLA `ATUQ_DIR`, y con su aviso puesto. El corpus es compartido: otro frente puede
77    # mover una receta de arriba —el 2026-09-07 el perfil PGO v2 re-hasheó `firefox` y con él
78    # `atuq`— y entonces el artefacto VIGENTE no existe en ningún store hasta que alguien pague un
79    # build de horas. Sin escotilla, ninguna prueba de atuq se puede correr en esa ventana. Con
80    # escotilla, se puede correr contra un artefacto viejo A SABIENDAS: por eso grita, y por eso el
81    # resultado no se puede citar como «atuq de hoy pasa».
82    forzado = os.environ.get("ATUQ_DIR")
83    if forzado and nombre == "atuq":
84        if not os.path.isdir(forzado):
85            fatal(f"ATUQ_DIR={forzado} no existe")
86        print(f"⚠ ATUQ_DIR: se prueba {os.path.basename(forzado)}, que NO es necesariamente el vigente")
87        return forzado
88
89    r = subprocess.run([HAMMER, "--store", STORE, "hash", f"recipes/{receta}.toml"],
90                       cwd=ROOT, capture_output=True, text=True)
91    if r.returncode != 0:
92        fatal(f"no pude hashear {receta}: {r.stderr.strip()}")
93    h = r.stdout.strip().splitlines()[-1].removeprefix("b3:")
94    d = os.path.join(STORE, f"{h}-{nombre}")
95    if not os.path.isdir(d):
96        fatal(f"{nombre} b3:{h[:8]} no está sellado — construilo antes")
97    return d
98
99
100def main():
101    negativo = "--negative-control" in sys.argv[1:]
102    if negativo:
103        print("== MODO CONTROL NEGATIVO: se BORRA el XPI de inicio y se exige la página de Firefox")
104    if not os.path.isdir(RFS):
105        fatal(f"no está el rootfs en {RFS} — corré antes `scripts/atuq-nested.sh`, que lo hidrata")
106    atuq = artefacto("atuq", "atuq")
107    print(f"artefacto: {os.path.basename(atuq)}")
108
109    tmp = tempfile.mkdtemp(prefix="atuq-inicio-")
110    salida = os.path.join(tmp, "salida")
111    extdir = os.path.join(tmp, "capa", "usr", "lib", "atuq", "distribution", "extensions")
112    os.makedirs(os.path.join(salida, "perfil"))
113    os.makedirs(extdir)
114    try:
115        with zipfile.ZipFile(os.path.join(extdir, f"{EXT_ID}.xpi"), "w") as z:
116            z.writestr("manifest.json", json.dumps(MANIFIESTO, indent=1))
117            z.writestr("fondo.js", FONDO)
118
119        with open(os.path.join(salida, "perfil", "user.js"), "w") as fh:
120            fh.write('user_pref("browser.dom.window.dump.enabled", true);\n')
121            fh.write('user_pref("browser.shell.checkDefaultBrowser", false);\n')
122
123        borrar = f"rm -f {XPI_INICIO}" if negativo else "true"
124        guion = f"""
125        mkdir -p /root
126        [ -s /etc/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > /etc/machine-id
127        {borrar}
128        timeout 50 /usr/bin/atuq --profile /salida/perfil --no-remote --headless about:blank \
129            > /salida/consola.log 2>&1
130        exit 0
131        """
132        subprocess.run([
133            "bwrap",
134            "--overlay-src", ALPINE, "--overlay-src", RFS, "--overlay-src", atuq,
135            "--overlay-src", os.path.join(tmp, "capa"), "--tmp-overlay", "/",
136            "--dev-bind", "/dev", "/dev", "--proc", "/proc", "--bind", "/sys", "/sys",
137            "--tmpfs", "/tmp", "--tmpfs", "/run", "--unshare-pid",
138            "--bind", salida, "/salida",
139            "--uid", "0", "--gid", "0",
140            "--setenv", "PATH", "/usr/bin:/bin:/usr/sbin:/sbin",
141            "--setenv", "XDG_RUNTIME_DIR", "/run", "--setenv", "HOME", "/root",
142            "--setenv", "XDG_DATA_DIRS", "/usr/share", "--setenv", "MOZ_HEADLESS", "1",
143            "--setenv", "MOZ_DISABLE_CONTENT_SANDBOX", "1",
144            "--setenv", "MOZ_DISABLE_GMP_SANDBOX", "1",
145            "--setenv", "MOZ_DISABLE_RDD_SANDBOX", "1",
146            "--setenv", "MOZ_DISABLE_SOCKET_PROCESS_SANDBOX", "1",
147            "--setenv", "MOZ_DISABLE_UTILITY_SANDBOX", "1",
148            "sh", "-c", guion,
149        ], check=False)
150
151        log = open(os.path.join(salida, "consola.log"), errors="replace").read()
152        lineas = [l for l in log.splitlines() if l.startswith("INICIO-TEST")]
153        for l in lineas:
154            print("   " + l)
155        if not lineas:
156            shutil.copy(os.path.join(salida, "consola.log"),
157                        os.path.join(ROOT, "work", "atuq-inicio.log"))
158            fatal("la sonda no dijo nada — ¿no se instaló? log en work/atuq-inicio.log")
159
160        def valor(clave):
161            for l in lineas:
162                if l.startswith(f"INICIO-TEST {clave} "):
163                    return l.split(" ", 2)[2]
164            return None
165
166        home, newtab = valor("HOMEPAGE"), valor("NEWTAB")
167        nuestras = [v for v in (home, newtab) if v and "inicio.html" in v]
168
169        if negativo:
170            if nuestras:
171                fatal("SIN el XPI de inicio las páginas siguen siendo las nuestras"
172                      " — esta prueba no distingue nada")
173            print("✓ control negativo: sin el XPI son las de Firefox (la prueba SÍ lo vería)")
174            return 0
175        if len(nuestras) != 2:
176            shutil.copy(os.path.join(salida, "consola.log"),
177                        os.path.join(ROOT, "work", "atuq-inicio.log"))
178            fatal(f"la página de inicio y/o la pestaña nueva NO son de atuq"
179                  f" (homepage={home} newtab={newtab}) — log en work/atuq-inicio.log")
180        print("✓ la página de inicio y la pestaña nueva son las de atuq")
181        return 0
182    finally:
183        shutil.rmtree(tmp, ignore_errors=True)
184
185
186sys.exit(main())