atuq §6.5.bis: la extensión foco — muestra el foco del sistema y no tiene con qué apagarlo

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.
This commit is contained in:
Sergio
2026-09-10 23:17:32 +00:00
parent be368df070
commit 10c499777e
8 changed files with 340 additions and 4 deletions
+30 -1
View File
@@ -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.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.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.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.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.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 | | 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 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**. 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) ### 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 Una navegación de PRIMER NIVEL a un medio (`Content-Type: video/*` o `audio/*`) se cancela y la URL se
+27 -1
View File
@@ -29,6 +29,7 @@ el vigente (por eso el ciclo es: editar todo → resellar una vez → medir), y
| `extensions/archivo/` | `<appdir>/distribution/extensions/archivo@atuq.tawasuyu.xpi` | **Archivo personal** (SDD 26 §6.3). | | `extensions/archivo/` | `<appdir>/distribution/extensions/archivo@atuq.tawasuyu.xpi` | **Archivo personal** (SDD 26 §6.3). |
| `extensions/medios/` | `<appdir>/distribution/extensions/medios@atuq.tawasuyu.xpi` | **Medios fuera del navegador** (SDD 26 §6.6). | | `extensions/medios/` | `<appdir>/distribution/extensions/medios@atuq.tawasuyu.xpi` | **Medios fuera del navegador** (SDD 26 §6.6). |
| `extensions/torrent/` | `<appdir>/distribution/extensions/torrent@atuq.tawasuyu.xpi` | **Torrent con identidad** (SDD 26 §6.9). | | `extensions/torrent/` | `<appdir>/distribution/extensions/torrent@atuq.tawasuyu.xpi` | **Torrent con identidad** (SDD 26 §6.9). |
| `extensions/foco/` | `<appdir>/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. | | `native-messaging/*.json` | `/usr/lib/mozilla/native-messaging-hosts/` | El manifiesto del host nativo. **Fuera del appdir** — ver abajo. |
| `bin/puriy-costura-host` | `<appdir>/puriy-costura-host` | El lanzador del host: le pone el `--state` que el manifiesto no puede. | | `bin/puriy-costura-host` | `<appdir>/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 ⚠ 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 `--`. `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) ## `torrent` — el magnet lo toma la suite (§6.9)
La extensión registra un handler para `magnet:` (`protocol_handlers`, que existe en nuestro build), 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. 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 - **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. 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 - **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í 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. las dos raíces pueden volver a ser una.
+4
View File
@@ -55,6 +55,10 @@
"torrent@atuq.tawasuyu": { "torrent@atuq.tawasuyu": {
"installation_mode": "normal_installed", "installation_mode": "normal_installed",
"install_url": "file:///usr/lib/atuq/distribution/extensions/torrent@atuq.tawasuyu.xpi" "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"
} }
} }
} }
+89
View File
@@ -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);
@@ -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"
}
}
@@ -8,6 +8,7 @@
"descargas@atuq.tawasuyu", "descargas@atuq.tawasuyu",
"archivo@atuq.tawasuyu", "archivo@atuq.tawasuyu",
"medios@atuq.tawasuyu", "medios@atuq.tawasuyu",
"torrent@atuq.tawasuyu" "torrent@atuq.tawasuyu",
"foco@atuq.tawasuyu"
] ]
} }
+1 -1
View File
@@ -43,7 +43,7 @@ license = "MIT OR Apache-2.0"
[source] [source]
repo = "gitea@git.tawasuyu.net:tawasuyu/tawasuyu.git" 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. # ⚠ 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 # 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" }`. # async para el compositor), y lo enchufa con `[patch.crates-io] smithay = { path = "vendor/smithay" }`.
+162
View File
@@ -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())