#!/usr/bin/env python3 """¿Corre JS NUESTRO en el chrome de `atuq`, y funciona la vista dividida? (SDD 26 §4) python3 scripts/test-atuq-chrome.py # exige las dos cosas python3 scripts/test-atuq-chrome.py --negative-control # con las pestañas FIJADAS: no debe partir ── LAS DOS PREGUNTAS, Y POR QUÉ VAN JUNTAS ─────────────────────────────────────────────────── El README de atuq decía que «el chrome de verdad» —split view, dientes propios— exige entrar a `omni.ja` y poner contenido nuestro adentro. Las dos mitades de esa frase resultaron falsas, y esta sonda mide cada una: 1. **El gancho.** `atuq.cfg` ya corre con privilegios (de ahí sale `nsIStyleSheetService`), así que desde ahí se puede observar `browser-delayed-startup-finished` y tocar el `gBrowser` de CADA ventana. O sea: **el chrome se programa sin abrir el zip**, que además es lo que hay que hacer, porque re-empacar `omni.ja` pelea con el orden del `jarlog` del PGO (por eso `jarlog` está aparcado). 2. **La vista dividida.** Firefox 154 la trae HECHA y PRENDIDA de fábrica: `browser/omni.ja` lleva `tabbrowser/tabsplitview.js` (492 líneas, el custom element), `opentabs-splitview.mjs` y `split-view-footer.js`, y `defaults/preferences/firefox.js` dice `pref("browser.tabs.splitView.enabled", true)`. Eso no es «lo que falta escribir»: es una función que ya tenemos y que nadie había ejercitado. Lo que la sonda hace en la ventana real: abre dos pestañas, llama a `gBrowser.addTabSplitView`, y después pregunta al motor por `gBrowser.activeSplitView` y `gBrowser.splitViewBrowsers`. Son hechos del DOM del chrome, no píxeles — corre headless. ── EL CONTROL NEGATIVO SALE DEL PROPIO MOTOR ───────────────────────────────────────────────── `addTabSplitView` tiene un `bail out` documentado: si TODAS las pestañas están fijadas, borra el wrapper y devuelve `null` (no se pueden agrupar). `--negative-control` fija las dos pestañas y exige justamente eso. Sin él, una sonda que dijera «partió» pase lo que pase se vería idéntica a una que funciona. REQUISITOS: el rootfs de `scripts/atuq-nested.sh` (que lo hidrata) y `atuq` sellado. """ import os import shutil import subprocess import sys import tempfile 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")) APPDIR = "usr/lib/atuq" # El bloque se APENDA al `atuq.cfg` del artefacto (no lo reemplaza): así se mide el fichero real de # atuq con la sonda encima, y no una copia que podría haber divergido. La capa de overlay pisa el # fichero entero, por eso hay que reescribirlo completo con el original adentro. SONDA = """ // ── SONDA DE PRUEBA — NO VIAJA EN EL ARTEFACTO (scripts/test-atuq-chrome.py) ─────────────────── try { const S = (typeof Services !== "undefined") ? Services : ChromeUtils.importESModule("resource://gre/modules/Services.sys.mjs").Services; const lineas = []; const di = function (s) { lineas.push(s); try { dump("CHROME-TEST " + s + "\\n"); } catch (e) {} // El fichero es la vía FIABLE: `dump()` desde el proceso padre depende de prefs y de cómo se // compiló. Se usan las dos y el guión lee la que haya. try { IOUtils.writeUTF8("/salida/chrome.txt", lineas.join("\\n") + "\\n"); } catch (e) {} }; di("VIVO el JS de atuq.cfg corre"); S.obs.addObserver(function observador(win) { S.obs.removeObserver(observador, "browser-delayed-startup-finished"); try { di("VENTANA " + (win.gBrowser ? "con gBrowser" : "SIN gBrowser")); di("PREF splitView.enabled=" + S.prefs.getBoolPref("browser.tabs.splitView.enabled", false)); const principal = S.scriptSecurityManager.getSystemPrincipal(); const t1 = win.gBrowser.addTab("about:blank", { triggeringPrincipal: principal }); const t2 = win.gBrowser.addTab("about:blank", { triggeringPrincipal: principal }); if (FIJADAS) { win.gBrowser.pinTab(t1); win.gBrowser.pinTab(t2); } const sv = win.gBrowser.addTabSplitView([t1, t2]); di("WRAPPER " + (sv ? ("tabs=" + sv.tabs.length) : "null")); if (sv && sv.tabs.length) { win.gBrowser.selectedTab = sv.tabs[0]; } win.setTimeout(function () { di("ACTIVA " + (win.gBrowser.activeSplitView ? "si" : "no")); di("BROWSERS " + win.gBrowser.splitViewBrowsers.length); di("FIN"); }, 2000); } catch (e) { di("EXCEPCIÓN " + e); } }, "browser-delayed-startup-finished"); } catch (e) { try { dump("CHROME-TEST EXCEPCIÓN-EXTERIOR " + e + "\\n"); } catch (e2) {} } """ def fatal(msg): print(f"✗ {msg}", file=sys.stderr) raise SystemExit(1) def artefacto(): """El artefacto VIGENTE de atuq, por `takana hash` y nunca por glob. ⚠ ESCOTILLA `ATUQ_DIR`, que grita: el corpus es compartido y otro frente puede re-hashear `firefox` dejando el atuq vigente sin sellar durante las horas del build. Un resultado obtenido con la escotilla NO se cita como «el atuq de hoy pasa». """ forzado = os.environ.get("ATUQ_DIR") if forzado: if not os.path.isdir(forzado): fatal(f"ATUQ_DIR={forzado} no existe") print(f"⚠ ATUQ_DIR: se mide {os.path.basename(forzado)}, que NO es necesariamente el vigente") return forzado r = subprocess.run([HAMMER, "--store", STORE, "hash", "recipes/atuq.toml"], cwd=ROOT, capture_output=True, text=True) if r.returncode != 0: fatal(f"no pude hashear atuq: {r.stderr.strip()}") h = r.stdout.strip().splitlines()[-1].removeprefix("b3:") d = os.path.join(STORE, f"{h}-atuq") if not os.path.isdir(d): fatal(f"atuq 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: las dos pestañas van FIJADAS; el motor tiene que negarse") 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() print(f"artefacto: {os.path.basename(atuq)}") tmp = tempfile.mkdtemp(prefix="atuq-chrome-") salida = os.path.join(tmp, "salida") capa_app = os.path.join(tmp, "capa", APPDIR) os.makedirs(os.path.join(salida, "perfil")) os.makedirs(capa_app) try: original = open(os.path.join(atuq, APPDIR, "atuq.cfg"), encoding="utf8").read() with open(os.path.join(capa_app, "atuq.cfg"), "w", encoding="utf8") as fh: fh.write(original) fh.write(SONDA.replace("FIJADAS", "true" if negativo else "false")) 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') guion = """ mkdir -p /root [ -s /etc/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > /etc/machine-id timeout 60 /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[len("CHROME-TEST "):] for l in log.splitlines() if l.startswith("CHROME-TEST ")] fichero = os.path.join(salida, "chrome.txt") if os.path.exists(fichero): del_fichero = open(fichero, errors="replace").read().splitlines() # El fichero es la fuente autorizada si dice MÁS que el stdout. if len(del_fichero) > len(lineas): lineas = del_fichero for l in lineas: print(" " + l) def valor(clave): for l in lineas: if l.startswith(clave + " "): return l.split(" ", 1)[1] return None if not lineas: destino = os.path.join(ROOT, "work", "atuq-chrome.log") os.makedirs(os.path.dirname(destino), exist_ok=True) open(destino, "w").write(log) fatal("la sonda no dijo NADA: el JS de `atuq.cfg` no corrió, o no llegó a la ventana." f" Log en {destino}") if valor("VIVO") is None: fatal("no salió la línea VIVO — el bloque de la sonda no se ejecutó") if "FIN" not in lineas and valor("EXCEPCIÓN"): fatal(f"el JS del chrome se rompió: {valor('EXCEPCIÓN')}") wrapper, activa, browsers = valor("WRAPPER"), valor("ACTIVA"), valor("BROWSERS") if negativo: if wrapper != "null": fatal(f"con las dos pestañas FIJADAS el motor partió igual (WRAPPER={wrapper})" " — esta prueba no distingue nada") if activa != "no": fatal(f"control negativo: ACTIVA={activa}, se esperaba «no»") print("✓ control negativo: con las pestañas fijadas NO hay vista dividida" " (la sonda sí lo vería)") return 0 if valor("VENTANA") != "con gBrowser": fatal("el observador corrió pero la ventana no traía `gBrowser`") if valor("PREF") != "splitView.enabled=true": fatal(f"la vista dividida viene APAGADA en este build ({valor('PREF')})" " — habría que prenderla en atuq.cfg") if wrapper != "tabs=2": fatal(f"`addTabSplitView` no armó la vista de dos pestañas (WRAPPER={wrapper})") if activa != "si": fatal(f"la vista se creó pero el motor no la reporta ACTIVA (ACTIVA={activa})") if browsers != "2": fatal(f"el motor no reporta dos navegadores en la vista (BROWSERS={browsers})") print("✓ el JS de `atuq.cfg` llega al `gBrowser` de la ventana:" " el chrome se programa SIN abrir `omni.ja`") print("✓ la vista dividida del motor funciona en nuestro build:" " dos pestañas, vista ACTIVA y dos navegadores") return 0 finally: shutil.rmtree(tmp, ignore_errors=True) sys.exit(main())