From 10c499777efd3e92225a1f189ebdc9775514ba9e Mon Sep 17 00:00:00 2001 From: Sergio Date: Thu, 10 Sep 2026 23:17:32 +0000 Subject: [PATCH] =?UTF-8?q?atuq=20=C2=A76.5.bis:=20la=20extensi=C3=B3n=20`?= =?UTF-8?q?foco`=20=E2=80=94=20muestra=20el=20foco=20del=20sistema=20y=20n?= =?UTF-8?q?o=20tiene=20con=20qu=C3=A9=20apagarlo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/26-atuq-envoltorio-gecko.md | 31 +++- recipes/atuq/README.md | 28 ++- recipes/atuq/distribution/policies.json | 4 + recipes/atuq/extensions/foco/fondo.js | 89 ++++++++++ recipes/atuq/extensions/foco/manifest.json | 25 +++ .../atuq/native-messaging/puriy_costura.json | 3 +- recipes/puriy-costura.toml | 2 +- scripts/test-atuq-foco.py | 162 ++++++++++++++++++ 8 files changed, 340 insertions(+), 4 deletions(-) create mode 100644 recipes/atuq/extensions/foco/fondo.js create mode 100644 recipes/atuq/extensions/foco/manifest.json create mode 100644 scripts/test-atuq-foco.py diff --git a/docs/26-atuq-envoltorio-gecko.md b/docs/26-atuq-envoltorio-gecko.md index 87b9b60b..b31e3416 100644 --- a/docs/26-atuq-envoltorio-gecko.md +++ b/docs/26-atuq-envoltorio-gecko.md @@ -491,7 +491,7 @@ La columna «quién más lo tiene» es lo que evita que nos contemos un cuento. | 6.2 | **Descargas direccionadas por contenido** | nadie | store CAS de takana, `tejido` | bajo | | 6.3 | **Archivo personal + RAG local** | Rewind/Recall (nube, Windows); SingleFile (guarda, no busca) | `khipu`, `rag-motor`, `willay-rag` | medio | | 6.4 | **Historial y perfil sobre `qullqa`** | nadie | `qullqa-core`, `qullqa-pozo` | medio | -| 6.5 | **Foco por `cortafuegos`, no por extensión** | nadie (nadie es dueño del navegador *y* del sistema) | `cortafuegos`, `pacha` | bajo | +| 6.5 | **Foco por `cortafuegos`, no por extensión** ✅ **cadena y navegador; falta quién lo aplica** | nadie (nadie es dueño del navegador *y* del sistema) | `cortafuegos`, `nftables` (nuevo en el corpus) | bajo | | 6.6 | **Medios por fuera del navegador** | extensiones sueltas; de fábrica no | `foreign-ytdlp`, `-platform`, `-dlna` | bajo | | 6.7 | **IA local en la barra lateral** | Chrome/Edge son nube; Zen no tiene | `rimay`, `iniy` | bajo | | 6.8 | **Proxy por contenedor** ✅ **v0.5** | nadie de fábrica | — (API de Firefox) | bajo | @@ -761,6 +761,35 @@ navegador: Mientras eso no exista, lo que `atuq` puede tener —y tiene, §6.5.bis— es la mitad honesta: **ver** el estado del foco y **no poder apagarlo**. +#### 6.5.bis La mitad del navegador: `focus.state`, y la asimetría a propósito (2026-09-10) + +La extensión `foco` **sólo lee**. Pregunta `focus.state` al host cada minuto —el estado lo cambia root +por fuera, no hay evento al que suscribirse— y pinta tres cosas: `foco`, nada, o **`?`**. El tercer +caso es el que importa: el host contesta `unknown` cuando nadie escribió el estado o cuando el +contenido no se entiende, y **eso no se redondea a `off`**; una insignia que dijera «sin foco» sin +haber mirado afirmaría un hecho que no tiene. + +**Y no hay verbo para apagarlo.** `focus.stop` cae en «verbo desconocido», igual que `focus.off` y +`focus.start`. No es una omisión pendiente: es la propiedad entera. Si el navegador pudiera levantar +el foco, esto valdría exactamente lo que vale un bloqueador de extensión —dos clics— que es el +problema que el §6.5 venía a resolver. Encenderlo tampoco está: aplicar un reglaset pide root y este +proceso corre como el usuario. Hay un test en tawasuyu que lo fija por nombre, y se puso rojo a +propósito agregando un `focus.stop` «por comodidad». + +El estado vive en `/etc/takana/focus` (lo escribe root junto con el reglaset). `scripts/test-atuq-foco.py` +corre **el path de producción, no la escotilla de pruebas** —una escotilla mide el código, no el +contrato con la imagen— en tres sesiones del navegador, y en cada una comprueba tres cosas: qué dijo +la extensión, qué pintó, y que el fichero **quedó igual** después de la sesión. Eso último es la +promesa del §6.5 medida del lado del navegador. Verificado rompiéndolo: con una extensión parcheada +que pinta «foco» pase lo que pase (dentro de una COPIA del artefacto, vía `ATUQ_DIR`), el guardián +falla nombrando la insignia. + +``` +-- fichero de foco: on → ESTADO on · INSIGNIA "foco" · quedó on +-- fichero de foco: off → ESTADO off · INSIGNIA "" · quedó off +-- fichero de foco: (no existe) → ESTADO unknown · INSIGNIA "?" · siguió ausente +``` + ### 6.6 Medios fuera del navegador — HECHO (2026-09-10) Una navegación de PRIMER NIVEL a un medio (`Content-Type: video/*` o `audio/*`) se cancela y la URL se diff --git a/recipes/atuq/README.md b/recipes/atuq/README.md index ba41c609..bca0ee85 100644 --- a/recipes/atuq/README.md +++ b/recipes/atuq/README.md @@ -29,6 +29,7 @@ el vigente (por eso el ciclo es: editar todo → resellar una vez → medir), y | `extensions/archivo/` | `/distribution/extensions/archivo@atuq.tawasuyu.xpi` | **Archivo personal** (SDD 26 §6.3). | | `extensions/medios/` | `/distribution/extensions/medios@atuq.tawasuyu.xpi` | **Medios fuera del navegador** (SDD 26 §6.6). | | `extensions/torrent/` | `/distribution/extensions/torrent@atuq.tawasuyu.xpi` | **Torrent con identidad** (SDD 26 §6.9). | +| `extensions/foco/` | `/distribution/extensions/foco@atuq.tawasuyu.xpi` | **Modo foco: lo muestra, no lo apaga** (SDD 26 §6.5). | | `native-messaging/*.json` | `/usr/lib/mozilla/native-messaging-hosts/` | El manifiesto del host nativo. **Fuera del appdir** — ver abajo. | | `bin/puriy-costura-host` | `/puriy-costura-host` | El lanzador del host: le pone el `--state` que el manifiesto no puede. | @@ -240,6 +241,32 @@ sin pestaña es el único resultado inaceptable, y el control negativo del guard ⚠ El binario **no viene del mensaje**: es una constante del host. Lo único que viaja es la URL, sólo `http(s)`, y detrás de un `--`. +## `foco` — el navegador MUESTRA el foco, y no tiene con qué apagarlo (§6.5) + +El foco no es un bloqueador de sitios: es una regla `nft` de **egress por cgroup** que aplicó root, y +mientras está puesta el navegador **no sale a la red** — con todo lo local andando (el archivo del +§6.3 se lee, el CAS del §6.2 está). Medido en una máquina con root: el proceso del cgroup en foco no +abre la conexión y el del cgroup de al lado sí (`scripts/test-foco-egress.sh`). + +Esta extensión es la mitad del navegador, y es **asimétrica a propósito: sólo lee**. Pregunta +`focus.state` al host cada minuto —el estado lo cambia root por fuera, así que no hay evento al que +suscribirse— y pinta tres cosas y ninguna más: `foco` si está puesto, nada si no, y **`?` si nadie +escribió el estado**. Ese tercer caso 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 eso es la propiedad y no un pendiente.** Si desde el navegador se +pudiera levantar, el modo foco valdría lo que vale un bloqueador de extensión: dos clics. En tawasuyu +hay un test que lo fija (`focus.stop` → «verbo desconocido»), y acá el guardián mide el efecto: +después de una sesión entera del navegador, el fichero de estado quedó **igual**. + +`scripts/test-atuq-foco.py` corre los tres estados y comprueba, por corrida, qué dijo la extensión, +qué pintó y que el estado no cambió. Verificado rompiéndolo: con una extensión que pinta «foco» pase +lo que pase, el guardián falla nombrando la insignia. + +Lo que todavía NO existe —y no se inventó de paso— es quién pone a `atuq` en un cgroup y quién aplica +la política: lo primero pide delegación de un subárbol a la sesión (decisión de arje, SDD 10) y lo +segundo pide root. Ver el §6.5 del SDD 26. + ## `torrent` — el magnet lo toma la suite (§6.9) La extensión registra un handler para `magnet:` (`protocol_handlers`, que existe en nuestro build), @@ -324,7 +351,6 @@ pestañas están fijadas borra el wrapper y devuelve `null`. `--negative-control necesita el daemon de embeddings y un backend LLM en la imagen. `willay-rag` es el molde. - **La IA local en la barra lateral (6.7)**, que está bloqueada por algo medido: el corpus no tiene ninguna receta de LLM ni de embeddings. Es también la que traería el motor que le falta al archivo. -- **El foco por `cortafuegos` (6.5)**, el último del §6 que no depende de un modelo ni de un daemon. - **Que una descarga se pueda COMPARTIR con `tejido`**: hoy el objeto está en el CAS de descargas y tejido sirve el del sistema. Falta que el índice de descargas sea una raíz que el GC respete, y ahí las dos raíces pueden volver a ser una. diff --git a/recipes/atuq/distribution/policies.json b/recipes/atuq/distribution/policies.json index 631e37f5..f261d356 100644 --- a/recipes/atuq/distribution/policies.json +++ b/recipes/atuq/distribution/policies.json @@ -55,6 +55,10 @@ "torrent@atuq.tawasuyu": { "installation_mode": "normal_installed", "install_url": "file:///usr/lib/atuq/distribution/extensions/torrent@atuq.tawasuyu.xpi" + }, + "foco@atuq.tawasuyu": { + "installation_mode": "normal_installed", + "install_url": "file:///usr/lib/atuq/distribution/extensions/foco@atuq.tawasuyu.xpi" } } } diff --git a/recipes/atuq/extensions/foco/fondo.js b/recipes/atuq/extensions/foco/fondo.js new file mode 100644 index 00000000..825e84cd --- /dev/null +++ b/recipes/atuq/extensions/foco/fondo.js @@ -0,0 +1,89 @@ +"use strict"; +// foco — el navegador MUESTRA el modo foco; no lo decide y no lo puede apagar (SDD 26 §6.5), v1. +// +// ── QUÉ ES EL FOCO, Y POR QUÉ NO ES UN BLOQUEADOR DE SITIOS ─────────────────── +// El foco lo sostiene el cortafuegos del sistema: una regla `nft` de egress por +// cgroup que aplicó root. Cuando está puesto, el navegador NO SALE A LA RED — y +// todo lo local sigue: el archivo personal (§6.3) se lee, el CAS (§6.2) está ahí. +// No es una lista de dominios: una lista se esquiva con un espejo, y un cgroup sin +// egress no. +// +// ── POR QUÉ ESTA EXTENSIÓN SÓLO MIRA ────────────────────────────────────────── +// Si desde acá se pudiera levantar el foco, valdría exactamente lo que vale un +// bloqueador de extensión: lo apaga en dos clics la misma persona que no quería +// distraerse. El host no expone ningún verbo para apagarlo —`focus.stop` contesta +// «verbo desconocido», y hay un test que lo fija—, así que esta extensión no tiene +// con qué, y eso es la propiedad. Encenderlo tampoco: aplicar un reglaset pide root. +// +// ── Y POR QUÉ «NO SÉ» ES UNA RESPUESTA ──────────────────────────────────────── +// Si nadie escribió el estado, el host contesta `unknown` y acá se muestra tal cual. +// Pintar «sin foco» cuando no se miró sería afirmar un hecho que no se tiene. + +const HOST = "puriy_costura"; +// Cada cuánto se vuelve a preguntar. El estado lo cambia root por fuera del +// navegador, así que no hay evento al que suscribirse: se sondea, y despacio. +const CADA_MS = 60000; + +let puerto = null; +let siguiente = 1; + +function di(s) { + try { dump("FOCO " + s + "\n"); } catch (e) {} +} + +// La insignia es TODA la interfaz: tres estados, tres textos, y ninguno inventado. +function pintar(estado, fichero) { + const texto = estado === "on" ? "foco" : estado === "unknown" ? "?" : ""; + const titulo = + estado === "on" + ? "Modo foco: el sistema tiene al navegador sin salida a la red" + : estado === "off" + ? "Modo foco apagado" + : "Modo foco: no se sabe — nadie escribió el estado en " + (fichero || "el sistema"); + try { + browser.browserAction.setBadgeText({ text: texto }); + browser.browserAction.setTitle({ title: titulo }); + } catch (e) { + di("INSIGNIA falló " + e); + } + di('INSIGNIA texto="' + texto + '"'); +} + +function conectar() { + try { + puerto = browser.runtime.connectNative(HOST); + } catch (e) { + di("SIN HOST " + e); + return; + } + di("CONECTADO " + HOST); + puerto.onMessage.addListener((r) => { + if (!r || r.verb !== "focus.state") return; + if (!r.ok) { + di("ERROR " + (r.error || "sin causa")); + return; + } + di("ESTADO " + r.focus + " fichero=" + (r.focus_file || "?")); + pintar(r.focus, r.focus_file); + }); + puerto.onDisconnect.addListener(() => { + const e = puerto && puerto.error ? puerto.error.message : "sin causa"; + di("DESCONECTADO " + e); + puerto = null; + }); +} + +function preguntar() { + if (!puerto) conectar(); + if (!puerto) return; + try { + puerto.postMessage({ id: siguiente++, verb: "focus.state" }); + } catch (e) { + di("ENVÍO falló " + e); + puerto = null; + } +} + +di("LISTA v1 muestra el foco del sistema; no lo apaga (no tiene con qué)"); +preguntar(); +setInterval(preguntar, CADA_MS); diff --git a/recipes/atuq/extensions/foco/manifest.json b/recipes/atuq/extensions/foco/manifest.json new file mode 100644 index 00000000..be359b72 --- /dev/null +++ b/recipes/atuq/extensions/foco/manifest.json @@ -0,0 +1,25 @@ +{ + "manifest_version": 2, + "name": "atuq — modo foco", + "description": "Muestra si el sistema tiene al navegador en modo foco. No lo apaga: no puede (SDD 26 §6.5).", + "version": "1.0", + "browser_specific_settings": { + "gecko": { + "id": "foco@atuq.tawasuyu", + "strict_min_version": "115.0" + } + }, + "permissions": [ + "nativeMessaging" + ], + "background": { + "scripts": ["fondo.js"] + }, + "browser_action": { + "default_title": "Modo foco", + "default_icon": "atuq128.png" + }, + "icons": { + "128": "atuq128.png" + } +} diff --git a/recipes/atuq/native-messaging/puriy_costura.json b/recipes/atuq/native-messaging/puriy_costura.json index 85df956b..8416e7fc 100644 --- a/recipes/atuq/native-messaging/puriy_costura.json +++ b/recipes/atuq/native-messaging/puriy_costura.json @@ -8,6 +8,7 @@ "descargas@atuq.tawasuyu", "archivo@atuq.tawasuyu", "medios@atuq.tawasuyu", - "torrent@atuq.tawasuyu" + "torrent@atuq.tawasuyu", + "foco@atuq.tawasuyu" ] } diff --git a/recipes/puriy-costura.toml b/recipes/puriy-costura.toml index 24319a6e..70826361 100644 --- a/recipes/puriy-costura.toml +++ b/recipes/puriy-costura.toml @@ -43,7 +43,7 @@ license = "MIT OR Apache-2.0" [source] repo = "gitea@git.tawasuyu.net:tawasuyu/tawasuyu.git" -commit = "066b3761f7a7a18415b0a59cb671355953bb48a1" +commit = "38815b5f3625d7d61dd4005195e3f822d11fc380" # ⚠ SIN ESTO EL BUILD MUERE EN UN SITIO QUE NO NOMBRA LA CAUSA — medido acá el 2026-09-10. # tawasuyu COMMITEA su propio `vendor/`: adentro tiene un `smithay` parcheado a mano (tearing/flip # async para el compositor), y lo enchufa con `[patch.crates-io] smithay = { path = "vendor/smithay" }`. diff --git a/scripts/test-atuq-foco.py b/scripts/test-atuq-foco.py new file mode 100644 index 00000000..10ff07b8 --- /dev/null +++ b/scripts/test-atuq-foco.py @@ -0,0 +1,162 @@ +#!/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())