#!/usr/bin/env python3 """¿La página de inicio y la pestaña nueva de `atuq` son las NUESTRAS? (SDD 26 §4.e) python3 scripts/test-atuq-inicio.py # sale 0 si las dos son de atuq python3 scripts/test-atuq-inicio.py --negative-control # sin el XPI: exige que sean las de Firefox La v0.3 puso la página de inicio y la pestaña nueva **por extensión de sistema**, con `chrome_url_overrides.newtab` y `chrome_settings_overrides.homepage`. Que el XPI esté en el artefacto no dice nada de eso: un override lo puede rechazar el gestor de extensiones, lo puede pisar una política, y —el modo de fallo propio de este proyecto— el navegador puede arrancar con el chrome VIEJO cacheado y enseñar la página de Firefox como si nada. Ninguna de las tres cosas falla ruidosamente: se ve una pestaña nueva perfectamente normal, que es de otro. ── CÓMO SE MIDE, SIN ABRIR PESTAÑAS NI MIRAR PÍXELES ───────────────────────────────────────── Una extensión sonda con permiso `browserSettings` le pregunta al propio navegador cuál es su `homepageOverride` y su `newTabPageOverride`. Es la respuesta del motor, no una inferencia: si el override de `inicio` agarró, las dos son `moz-extension://…/inicio.html`; si no, son las de fábrica (`about:home` / `about:newtab`). Mirar el XPI en el disco, o el `manifest.json` dentro del XPI, mediría lo declarado — que es justo lo que ya sabíamos. El control negativo BORRA el XPI de `inicio` dentro del overlay temporal (el rootfs real no se toca) y exige el resultado contrario. Sin él, una sonda rota que siempre dijera «es la nuestra» se vería idéntica a una que funciona. REQUISITOS: el rootfs de `scripts/atuq-nested.sh` (que lo hidrata) y `atuq` sellado. """ import json import os import shutil import subprocess import sys import tempfile import zipfile ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) RFS = os.environ.get("RFS", "/mnt/cosecha/escritorios/atuq-rootfs") ALPINE = os.path.join(ROOT, ".dev-fs/alpine") HAMMER = os.path.join(ROOT, "target/release/takana") STORE = os.environ.get("HAMMER_STORE", os.path.join(ROOT, "store")) EXT_ID = "sonda-inicio@atuq.tawasuyu" XPI_INICIO = "/usr/lib/atuq/distribution/extensions/inicio@atuq.tawasuyu.xpi" MANIFIESTO = { "manifest_version": 2, "name": "sonda-inicio", "version": "1.0", "browser_specific_settings": {"gecko": {"id": EXT_ID}}, "permissions": ["browserSettings"], "background": {"scripts": ["fondo.js"]}, } FONDO = """ function di(s) { try { dump("INICIO-TEST " + s + "\\n"); } catch (e) {} } async function mirar() { try { const h = await browser.browserSettings.homepageOverride.get({}); const n = await browser.browserSettings.newTabPageOverride.get({}); di("HOMEPAGE " + h.value); di("NEWTAB " + n.value); } catch (e) { di("EXCEPCIÓN " + e.message); } } mirar(); """ def fatal(msg): print(f"✗ {msg}", file=sys.stderr) raise SystemExit(1) def artefacto(receta, nombre): """El artefacto VIGENTE, por `hammer hash` y nunca por glob.""" # ⚠ ESCOTILLA `ATUQ_DIR`, y con su aviso puesto. El corpus es compartido: otro frente puede # mover una receta de arriba —el 2026-09-07 el perfil PGO v2 re-hasheó `firefox` y con él # `atuq`— y entonces el artefacto VIGENTE no existe en ningún store hasta que alguien pague un # build de horas. Sin escotilla, ninguna prueba de atuq se puede correr en esa ventana. Con # escotilla, se puede correr contra un artefacto viejo A SABIENDAS: por eso grita, y por eso el # resultado no se puede citar como «atuq de hoy pasa». forzado = os.environ.get("ATUQ_DIR") if forzado and nombre == "atuq": if not os.path.isdir(forzado): fatal(f"ATUQ_DIR={forzado} no existe") print(f"⚠ ATUQ_DIR: se prueba {os.path.basename(forzado)}, que NO es necesariamente el vigente") return forzado r = subprocess.run([HAMMER, "--store", STORE, "hash", f"recipes/{receta}.toml"], cwd=ROOT, capture_output=True, text=True) if r.returncode != 0: fatal(f"no pude hashear {receta}: {r.stderr.strip()}") h = r.stdout.strip().splitlines()[-1].removeprefix("b3:") d = os.path.join(STORE, f"{h}-{nombre}") if not os.path.isdir(d): fatal(f"{nombre} b3:{h[:8]} no está sellado — construilo antes") return d def main(): negativo = "--negative-control" in sys.argv[1:] if negativo: print("== MODO CONTROL NEGATIVO: se BORRA el XPI de inicio y se exige la página de Firefox") if not os.path.isdir(RFS): fatal(f"no está el rootfs en {RFS} — corré antes `scripts/atuq-nested.sh`, que lo hidrata") atuq = artefacto("atuq", "atuq") print(f"artefacto: {os.path.basename(atuq)}") tmp = tempfile.mkdtemp(prefix="atuq-inicio-") salida = os.path.join(tmp, "salida") extdir = os.path.join(tmp, "capa", "usr", "lib", "atuq", "distribution", "extensions") os.makedirs(os.path.join(salida, "perfil")) os.makedirs(extdir) try: with zipfile.ZipFile(os.path.join(extdir, f"{EXT_ID}.xpi"), "w") as z: z.writestr("manifest.json", json.dumps(MANIFIESTO, indent=1)) z.writestr("fondo.js", FONDO) with open(os.path.join(salida, "perfil", "user.js"), "w") as fh: fh.write('user_pref("browser.dom.window.dump.enabled", true);\n') fh.write('user_pref("browser.shell.checkDefaultBrowser", false);\n') borrar = f"rm -f {XPI_INICIO}" if negativo else "true" guion = f""" mkdir -p /root [ -s /etc/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > /etc/machine-id {borrar} timeout 50 /usr/bin/atuq --profile /salida/perfil --no-remote --headless about:blank \ > /salida/consola.log 2>&1 exit 0 """ subprocess.run([ "bwrap", "--overlay-src", ALPINE, "--overlay-src", RFS, "--overlay-src", atuq, "--overlay-src", os.path.join(tmp, "capa"), "--tmp-overlay", "/", "--dev-bind", "/dev", "/dev", "--proc", "/proc", "--bind", "/sys", "/sys", "--tmpfs", "/tmp", "--tmpfs", "/run", "--unshare-pid", "--bind", salida, "/salida", "--uid", "0", "--gid", "0", "--setenv", "PATH", "/usr/bin:/bin:/usr/sbin:/sbin", "--setenv", "XDG_RUNTIME_DIR", "/run", "--setenv", "HOME", "/root", "--setenv", "XDG_DATA_DIRS", "/usr/share", "--setenv", "MOZ_HEADLESS", "1", "--setenv", "MOZ_DISABLE_CONTENT_SANDBOX", "1", "--setenv", "MOZ_DISABLE_GMP_SANDBOX", "1", "--setenv", "MOZ_DISABLE_RDD_SANDBOX", "1", "--setenv", "MOZ_DISABLE_SOCKET_PROCESS_SANDBOX", "1", "--setenv", "MOZ_DISABLE_UTILITY_SANDBOX", "1", "sh", "-c", guion, ], check=False) log = open(os.path.join(salida, "consola.log"), errors="replace").read() lineas = [l for l in log.splitlines() if l.startswith("INICIO-TEST")] for l in lineas: print(" " + l) if not lineas: shutil.copy(os.path.join(salida, "consola.log"), os.path.join(ROOT, "work", "atuq-inicio.log")) fatal("la sonda no dijo nada — ¿no se instaló? log en work/atuq-inicio.log") def valor(clave): for l in lineas: if l.startswith(f"INICIO-TEST {clave} "): return l.split(" ", 2)[2] return None home, newtab = valor("HOMEPAGE"), valor("NEWTAB") nuestras = [v for v in (home, newtab) if v and "inicio.html" in v] if negativo: if nuestras: fatal("SIN el XPI de inicio las páginas siguen siendo las nuestras" " — esta prueba no distingue nada") print("✓ control negativo: sin el XPI son las de Firefox (la prueba SÍ lo vería)") return 0 if len(nuestras) != 2: shutil.copy(os.path.join(salida, "consola.log"), os.path.join(ROOT, "work", "atuq-inicio.log")) fatal(f"la página de inicio y/o la pestaña nueva NO son de atuq" f" (homepage={home} newtab={newtab}) — log en work/atuq-inicio.log") print("✓ la página de inicio y la pestaña nueva son las de atuq") return 0 finally: shutil.rmtree(tmp, ignore_errors=True) sys.exit(main())