La mitad del navegador del §6.5, deliberadamente asimétrica: sólo lee. Pregunta `focus.state` (nuevo en puriy-costura, commit 38815b5f3) y pinta tres estados — `foco`, nada, y `?` cuando nadie escribió el estado. Ese tercero es el que importa: si «no sé» se redondeara a «apagado», la insignia afirmaría que no hay foco sin haberlo mirado. No hay verbo para apagarlo, y es la propiedad y no un pendiente: si el navegador pudiera levantar el foco, valdría lo que vale un bloqueador de extensión. En tawasuyu hay un test que lo fija; acá el guardián mide el EFECTO — tras una sesión entera, el fichero de estado quedó igual. `scripts/test-atuq-foco.py` corre los tres estados sobre el path de PRODUCCIÓN (`/etc/takana/focus`), no la escotilla de pruebas: una escotilla mide el código, no el contrato con la imagen. Verde sobre el artefacto vigente (atuq 5d1afc50, puriy-costura 0de6b4ca), y verificado rompiéndolo — con una extensión parcheada que pinta «foco» siempre (en una COPIA del artefacto, vía ATUQ_DIR), falla nombrando la insignia.
163 lines
7.9 KiB
Python
163 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""¿El navegador MUESTRA el modo foco del sistema, y lo deja como estaba? (SDD 26 §6.5)
|
|
|
|
python3 scripts/test-atuq-foco.py
|
|
|
|
── QUÉ SE EJERCITA ───────────────────────────────────────────────────────────────────────────
|
|
Tres corridas, una por estado del fichero que escribe root (`/etc/takana/focus`): `on`, `off` y
|
|
**ausente**. En cada una se mide lo que la extensión DICE y lo que pinta en su insignia, leído del
|
|
`dump` del navegador — no lo que el host contestó, que es la mitad que ya cubren los tests de
|
|
`puriy-costura`.
|
|
|
|
── Y LA PROMESA DEL §6.5, MEDIDA DEL LADO DEL NAVEGADOR ──────────────────────────────────────
|
|
Después de cada corrida se vuelve a leer el fichero DENTRO de la jaula: tiene que estar **igual**.
|
|
El foco lo sostiene un reglaset `nft` que aplicó root, y la única forma de que eso valga algo es que
|
|
el navegador no pueda deshacerlo. Que no exista un verbo para apagarlo está fijado en tawasuyu
|
|
(`no_hay_verbo_para_apagar_el_foco_y_eso_es_la_propiedad`); acá se comprueba el efecto: el estado
|
|
sobrevive intacto a una sesión entera del navegador.
|
|
|
|
`unknown` es el caso que más importa de los tres: si el host redondeara «no sé» a «apagado», la
|
|
insignia diría «sin foco» sin que nadie lo haya mirado.
|
|
|
|
REQUISITOS: el rootfs mínimo de `atuq-nested.sh`, y `atuq` + `puriy-costura` sellados.
|
|
"""
|
|
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"))
|
|
# El path de producción, el mismo que la constante del host. Se mide ÉSTE y no la escotilla de
|
|
# pruebas: una escotilla mide el código, no el contrato con la imagen.
|
|
FOCO = "/etc/takana/focus"
|
|
|
|
|
|
def fatal(msg):
|
|
print(f"✗ {msg}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
|
|
def artefacto(receta, nombre, escotilla=None):
|
|
forzado = os.environ.get(escotilla) if escotilla else None
|
|
if forzado:
|
|
if not os.path.isdir(forzado):
|
|
fatal(f"{escotilla}={forzado} no existe")
|
|
print(f"⚠ {escotilla}: se mide {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")
|
|
if not os.listdir(d):
|
|
fatal(f"{nombre} b3:{h[:8]} está VACÍO: eso no es un artefacto, es un nombre")
|
|
return d
|
|
|
|
|
|
def correr(atuq, costura, salida, estado):
|
|
"""Una sesión del navegador con el fichero de foco en `estado` (None = no existe)."""
|
|
poner = "rm -f " + FOCO if estado is None else (
|
|
f"mkdir -p {os.path.dirname(FOCO)} && printf '%s\\n' '{estado}' > {FOCO}")
|
|
guion = f"""
|
|
mkdir -p /root /salida/hogar
|
|
[ -s /etc/machine-id ] || tr -d - < /proc/sys/kernel/random/uuid > /etc/machine-id
|
|
export HOME=/salida/hogar
|
|
{poner}
|
|
timeout 45 /usr/bin/atuq --profile /salida/perfil --no-remote --headless 'about:blank' \
|
|
> /salida/consola.log 2>&1
|
|
# El estado, RELEÍDO después de que el navegador terminó: es la prueba de que no lo tocó.
|
|
if [ -f {FOCO} ]; then cp {FOCO} /salida/focus-final; else echo AUSENTE > /salida/focus-final; fi
|
|
exit 0
|
|
"""
|
|
subprocess.run([
|
|
"bwrap",
|
|
"--overlay-src", ALPINE, "--overlay-src", RFS,
|
|
"--overlay-src", atuq, "--overlay-src", costura,
|
|
"--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", "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("FOCO "):] for l in log.splitlines() if l.startswith("FOCO ")]
|
|
final = ""
|
|
try:
|
|
final = open(os.path.join(salida, "focus-final"), errors="replace").read().strip()
|
|
except OSError:
|
|
pass
|
|
return lineas, final, log
|
|
|
|
|
|
def main():
|
|
atuq = artefacto("atuq", "atuq", "ATUQ_DIR")
|
|
costura = artefacto("puriy-costura", "puriy-costura")
|
|
print(f"artefactos: {os.path.basename(atuq)}\n {os.path.basename(costura)}")
|
|
if not os.path.isdir(RFS):
|
|
fatal(f"no está el rootfs en {RFS} — corré antes `scripts/atuq-nested.sh`")
|
|
|
|
# estado puesto → (lo que la extensión tiene que decir, la insignia, el fichero tras la sesión)
|
|
casos = [
|
|
("on", "on", "foco", "on"),
|
|
("off", "off", "", "off"),
|
|
(None, "unknown", "?", "AUSENTE"),
|
|
]
|
|
tmp = tempfile.mkdtemp(prefix="atuq-foco-")
|
|
try:
|
|
for puesto, espera, insignia, tras in casos:
|
|
salida = os.path.join(tmp, f"caso-{puesto or 'ausente'}")
|
|
os.makedirs(os.path.join(salida, "perfil"))
|
|
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')
|
|
|
|
print(f"\n-- fichero de foco: {puesto if puesto else '(no existe)'}")
|
|
lineas, final, log = correr(atuq, costura, salida, puesto)
|
|
for l in lineas:
|
|
print(" " + l)
|
|
if not lineas:
|
|
destino = os.path.join(ROOT, "work", f"atuq-foco-{puesto or 'ausente'}.log")
|
|
os.makedirs(os.path.dirname(destino), exist_ok=True)
|
|
shutil.copy(os.path.join(salida, "consola.log"), destino)
|
|
fatal(f"la extensión no dijo nada — log en {destino}")
|
|
|
|
estados = [l for l in lineas if l.startswith("ESTADO ")]
|
|
if not estados:
|
|
fatal(f"la extensión no informó ningún estado: {lineas}")
|
|
if not estados[-1].startswith(f"ESTADO {espera} "):
|
|
fatal(f"con el fichero en {puesto!r} dijo «{estados[-1]}» y tenía que decir «{espera}»")
|
|
if f'INSIGNIA texto="{insignia}"' not in lineas:
|
|
fatal(f"la insignia no quedó en «{insignia}»: {[l for l in lineas if 'INSIGNIA' in l]}")
|
|
# Y la promesa: el navegador no cambió el estado.
|
|
if final != tras:
|
|
fatal(f"el fichero del foco quedó en {final!r} y tenía que quedar en {tras!r}"
|
|
" — el navegador NO debería poder tocarlo")
|
|
print(f" ✓ dijo {espera}, pintó «{insignia}», y dejó el estado igual ({final})")
|
|
|
|
print("\n✓ los tres estados llegan al navegador tal como los dejó el sistema")
|
|
print("✓ «no sé» es una respuesta: sin fichero, la insignia dice «?» y no «sin foco»")
|
|
print("✓ y tras una sesión entera el estado del foco quedó intacto: el navegador no lo apaga")
|
|
return 0
|
|
finally:
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
|
|
|
|
sys.exit(main())
|