Los llamadores EJECUTABLES: scripts/ (incluida toda la granja), los runbooks y CLAUDE.md. Seguro porque la 3a ya garantiza que el worker emite los dos binarios, y porque en farm-lab-sync.sh el cargo build remoto precede a la invocación remota en el mismo script. Verificado: sintaxis de los 49 (bash -n / py_compile — ojo que why-differs-barrido.sh es Python con extensión .sh) y `takana hash` devuelve hash real sobre el store. NO se toca en esta etapa, a propósito: - La variable de entorno HAMMER=. Es interfaz de los scripts entre sí y hay llamadores que la fijan; renombrarla va con la etapa 4. - docs/evidencia/ y los HANDOFF: son REGISTRO de lo que se corrió ese día. Reescribir un comando dentro de una evidencia la falsifica. - docs/state/: es generado, se regenera solo. - Los ADR y los docs de diseño: texto, y `hammer` sigue funcionando. Van con la etapa 5, que es la de churn de texto.
187 lines
8.4 KiB
Python
187 lines
8.4 KiB
Python
#!/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())
|