test(uix): cover announce contracts and record attach timer leak

astra
dev 2 weeks ago
parent aa0e0a069c
commit 85ff2eaaf5

@ -978,7 +978,8 @@ depends on who builds the engine:
- **The composition roots** (`createActiveUix`, `attachActiveUix`) — default
ON: the root registers the channel on its shared sink after the engine is
built, unless the engine was constructed with `announce: false`
(`createActiveUix({ events: { announce: false } })`; in attach, the app's own
(`createActiveUix({ events: { announce: false } })`; in attach,
`defineUixServices({ events: { announce: false } })` or the app's own
`defineEngineSemantic({ announce: false })`). An explicitly passed `announce`
wins: the root never overwrites an existing channel. Why: the S-19(ii) note
below.

@ -195,7 +195,7 @@ Queda una condición del autor: el push (L-123).
| L-152 | El defecto de A-106 en masa: wrappers de eidos pasan `...rest` a una parte de soma con `ref = $bindable` sin `bind:ref` ⇒ `bind:ref` del consumidor recibe null | F4 censo estático (adb680899) | — | medio | CONFIRMADO | 532 ficheros en 99 componentes (top: media-player 24, sidebar 17, color-picker 16); muestra runtime 3/3. La DOCTRINA ya está escrita (`eidos/components/README.md` §Reglas duras, regla 8) y el Button reparado; los otros wrappers siguen sin reenviar |
| L-153 | `applyOverride` (resolver.ts:343-346) clona un DeltaOp LITERAL como firma cuando la familia no tiene slice haptic base (`emerge` + `channels:['haptic']` + `{ intensity: { op: 'add' } }`) ⇒ HapticChannel.handle lanza `TypeError … 'pattern'`, 0 vibraciones | F4 adversarial L-ADV-3 | — | pequeño | CONFIRMADO | bug de runtime de sema; fuera del lote |
| L-154 | «opt-in» sin calificar para announce en `docs/decisions/book-deviations.md:508` y `src/uix/sema/chans/announce.ts:13` (cierto solo para el motor desnudo) | F4 adversarial L-ADV-4 | — | trivial | ARREGLADO | pasada de documentación 2026-09-17: `book-deviations.md`, `announce.ts` y `theming/channels.md:58` califican el opt-in (motor desnudo) frente al cableado por defecto de las raíces |
| L-155 | Announce sin pin directo en attach (default ON) ni en motor desnudo (opt-in); la viñeta de attach de sema.md nombra solo `defineEngineSemantic` y omite `defineUixServices({ events: { announce: false } })`, que también sirve | F4 adversarial L-ADV-5/6 | — | pequeño | DIFERIDO | hoy ciertas (medido); solo contracts.test.ts:670-702 pinea standalone |
| L-155 | Announce sin pin directo en attach (default ON) ni en motor desnudo (opt-in); la viñeta de attach de sema.md nombra solo `defineEngineSemantic` y omite `defineUixServices({ events: { announce: false } })`, que también sirve | F4 adversarial L-ADV-5/6 | — | pequeño | ARREGLADO | 2026-09-23: pruebas permanentes de attach default ON, opt-out en defineUixServices y motor desnudo opt-in; guía sema.md corregida. Q01-b/c siguen abiertos |
| L-156 | Siete handoffs de julio en minúscula (`docs/process/continue-*.md`: agente, arts-docs-reconciliation, audit-fixes, cleanroom-fixes, proof-of-human, runed-tabbable-port, with-stumbles-s2-s6) quedaron fuera del censo del plan | F6 | — | — | DATO | no llevan cabecera CONGELADO ni están transcritos aquí: son históricos (varios se declaran cerrados en su cabecera) |
| L-157 | `docs-check` I10 cosecha los identificadores del código LÍNEA A LÍNEA (`identifiersOf`), así que el conjunto de verdad sigue a cómo prettier parte o junta las listas de import/export: 219 de 267 READMEs cambiaron su conjunto (55 crecen, 76 encogen) | F5 medición de guards (8a7a31fb9) | — | pequeño | ARREGLADO | `docs-check` I10 usa ahora AST de TypeScript para declaraciones e imports/exports de `.ts` y bloques `<script>` de `.svelte`; prueba de formato compacto/multilínea 2/2, inyección negativa de prop fantasma rechazada y `docs:check` 0/0 tras restaurar el README. Sin cambio de runtime. |
| L-158 | `pack-census.test.ts` detecta emisiones con `source.includes("'evento'")`: solo la comilla SIMPLE. Una emisión escrita con comilla doble o backtick produce un falso «nadie lo emite» | F5 medición de guards | — | trivial | ARREGLADO | El censo reconoce literales de evento con comillas simples, dobles y backticks; prueba de las tres formas y rechazo de nombre distinto/sin comillas. Sin cambio de runtime. |

@ -513,6 +513,23 @@ ocho decisiones mecánicas reducidas a una. Antes y después pasan 2 archivos y
El gate completo pasa 474/474 archivos y 5.486/5.486 pruebas, incluido el build y
el smoke 27/27 de `apps/base`.
### ActiveUix · cierre de reproducción Q01 y cobertura L-155 (2026-09-23)
La tercera arista de Q01 queda reproducida: en attach, `announce()` programa
`uix:announce:polite` sobre el scheduler prestado y `uix.dispose()` deja esa
clave pendiente. La aserción temporal de cancelación falló en Chromium; se retiró
sin cambiar producción. Q01-a y Q01-b ya estaban reproducidos. La corrección
de las tres aristas exige una política de propiedad del canal y los timers, y
permanece separada de las extracciones neutrales.
Para L-155 se añadieron tres pruebas permanentes: attach anuncia por defecto
mediante una sola región accesible, `defineUixServices` respeta `announce: false`
y `EngineSemantic` desnudo mantiene el opt-in. Los dos archivos dirigidos pasan
104/104 pruebas frente a 101/101 antes de la cobertura. Se corrigió también la
guía de Sema para mostrar el opt-out con `defineUixServices`.
El gate completo pasa 474/474 archivos y 5.489/5.489 pruebas, incluido el build
y el smoke 27/27 de `apps/base`.
### Pre-flight de Soma para las partes de ítem de los dos menús (2026-09-22)
El usuario autorizó la deduplicación interna sin cambios de comportamiento, API
@ -802,15 +819,17 @@ Evidencia: [ActiveUix](../../src/uix/active-uix/active-uix.svelte.ts), líneas
- El constructor registra en el motor compartido una closure que captura esa
instancia de ActiveUix. En attach, `dispose()` deja vivo el motor y no retira
esa conexión. Una segunda conexión encuentra el canal existente y conserva
el destino anterior. Confirmado por lectura; falta test de attach → dispose → attach.
el destino anterior. Reproducido en Vitest client con attach → dispose → attach.
- `announce()` programa `uix:announce:${priority}`, pero una llamada posterior
con `timeout: 0` no cancela el temporizador anterior. Este puede borrar el
mensaje nuevo, pese a que la API promete desactivar el borrado automático.
- En attach no se cancelan esos temporizadores al destruir la instancia. Los
IDs de nodos y las claves de temporizadores son fijos, por lo que varias
instancias sobre el mismo DOM requieren una política explícita de compartición.
Reproducido: la clave `uix:announce:polite` permanece en `app.timers` tras
`uix.dispose()`.
Actuación: probar los tres escenarios; definir propietario y duración del sink;
Actuación: las tres aristas están reproducidas; definir propietario y duración del sink;
extraer su implementación; hacer simétrica la conexión/desconexión sin destruir
servicios prestados. Mantener **un sink accesible compartido**; no resolverlo
creando regiones duplicadas por componente. Determinar explícitamente si múltiples
@ -1081,18 +1100,18 @@ Ninguna autoriza alterar contratos bajo una limpieza interna.
El estado de estas filas se consulta en el [ledger](./LEDGER-cierre-2026-09.md);
los números siguientes son evidencia documental previa, no mediciones nuevas.
| Filas | Efecto en este plan |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| L-126 | Verificar destrucción de una raíz Eidos antigua tras crear la siguiente antes de extraer su aplicación DOM |
| L-130 | Medir la doble foundation de apps/base antes de prometer ahorro por modularizar recetas; conservar CSS del primer paint |
| L-138 y L-140 | Priorizar retorno efectivo del foco y valor visible de Select; comprobar resultados en navegador, no solo llamadas a métodos |
| L-152 | Auditar reenvío real de bindables en wrappers; el censo previo de 532 archivos/99 componentes orienta, no reemplaza una comprobación actual |
| L-153 | Reproducir fallo de override háptico sin firma base antes de optimizar el resolver |
| L-141 y L-144 | Langs: cobertura del idioma configurado y tipado del catálogo de app antes de cambios de carga |
| L-155 | Completar cobertura del announce de attach/motor desnudo al investigar Q01; no confundirlo con un canal aún por activar |
| L-157–L-159 y L-165 | Guards: sustituir análisis textual frágil donde proceda y comprobar que el corpus inspeccionado no está vacío |
| L-007, L-023 y L-026 | Attach sin consumidor/preboot, holds bajo WIP y escrituras de Eidos pendientes de decisión: mantener sus límites explícitos |
| L-034–L-037 | Incorporar deuda de harnesses, auditoría de Blocks, D13/D14 y theming; conservar IDs y disposiciones previas |
| Filas | Efecto en este plan |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| L-126 | Verificar destrucción de una raíz Eidos antigua tras crear la siguiente antes de extraer su aplicación DOM |
| L-130 | Medir la doble foundation de apps/base antes de prometer ahorro por modularizar recetas; conservar CSS del primer paint |
| L-138 y L-140 | Priorizar retorno efectivo del foco y valor visible de Select; comprobar resultados en navegador, no solo llamadas a métodos |
| L-152 | Auditar reenvío real de bindables en wrappers; el censo previo de 532 archivos/99 componentes orienta, no reemplaza una comprobación actual |
| L-153 | Reproducir fallo de override háptico sin firma base antes de optimizar el resolver |
| L-141 y L-144 | Langs: cobertura del idioma configurado y tipado del catálogo de app antes de cambios de carga |
| L-155 | Cobertura de attach y motor desnudo añadida el 2026-09-23; conservar la distinción default ON / opt-in y resolver por separado el ownership Q01 |
| L-157–L-159 y L-165 | Guards: sustituir análisis textual frágil donde proceda y comprobar que el corpus inspeccionado no está vacío |
| L-007, L-023 y L-026 | Attach sin consumidor/preboot, holds bajo WIP y escrituras de Eidos pendientes de decisión: mantener sus límites explícitos |
| L-034–L-037 | Incorporar deuda de harnesses, auditoría de Blocks, D13/D14 y theming; conservar IDs y disposiciones previas |
Las filas diferidas se evalúan dentro de su familia; no se convierten en encargos
aprobados ni se dan por cerradas porque un guard pase. Los defectos registrados

@ -20,6 +20,7 @@ oficial de cada fila.
| ----- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Q01-a | ActiveUix announce | `announce()` sólo reemplaza el timer si `timeout > 0`; `timeout: 0` conserva el timer anterior | Reproducido en Vitest client (Chromium) | Corregir en un lote de defectos con prueba permanente |
| Q01-b | ActiveUix attach | La closure de `AnnounceChannel` captura la primera instancia; el canal prestado sobrevive a `dispose()` | Reproducido en Vitest client (Chromium) | Definir ownership del canal antes de corregirlo |
| Q01-c | ActiveUix attach | `announce()` programa el borrado en `app.timers`, pero `uix.dispose()` deja pendiente ese temporizador prestado | Reproducido en Vitest client (Chromium) | Definir cancelación por instancia junto con Q01-b |
| Q05 | ActiveUix boot | `createActiveUix()` registra listeners antes de validar Sema y lee la dirección del documento global aunque ActiveDom apunte a otro `targetWindow` | Ambos casos reproducidos en Vitest client (Chromium) | Garantizar rollback en error y alinear la lectura inicial de preferencias con el destino configurado |
| L-126 | Eidos lifecycle | `dispose()` borra IDs de estilo y atributos sin marca de la instancia que escribió después | Reproducido en Vitest server | Corregir ownership de DOM y estilos en un lote de defectos |
| L-130 | Eidos CSS | `apps/base` importa `index.css`, que incluye `generated/base.css`, y crea `ActiveEidos` con `applyDom:true`, que escribe foundation runtime | Medido en el ledger; la alternativa afecta primer paint | Comparar SSR, hidratación, CSS y first paint antes de variar el entrypoint |
@ -65,6 +66,20 @@ el runtime: corregir cualquiera de los dos casos altera el comportamiento de
anuncios y pertenece a un lote de defectos distinto de las extracciones
neutrales autorizadas.
### Reproducción de Q01-c y cobertura de L-155 (2026-09-23)
Una prueba temporal de Vitest client (Chromium) adjuntó `ActiveUix` a un
`ActiveApp`, llamó a `announce('First root', 'polite', 10_000)` y comprobó que
`app.timers` contenía `uix:announce:polite`. Tras `uix.dispose()`, la aserción
de que la clave hubiera desaparecido falló: el temporizador seguía pendiente.
La prueba temporal se retiró y no se cambió el runtime. Este resultado fija la
deuda de limpieza de attach; no demuestra que el callback borre un nodo de otra
instancia, ya que conserva la referencia al nodo anterior.
La cobertura permanente de L-155 ahora comprueba attach con anuncio por defecto
en el sink compartido, el opt-out de `defineUixServices` y el opt-in del motor
Sema desnudo. La suite dirigida pasó de 101/101 a 104/104 pruebas.
### Reproducción de Q05
Una prueba temporal de Vitest client (Chromium) creó ActiveUix con

@ -47,8 +47,9 @@ si se solicitaron, pertenecen a ActiveUix; y los motores de motion, scene y soun
creados porque el host no los declaró son fallbacks que ActiveUix debe destruir.
La cabecera de implementación y la suite ya reconocen esa tercera categoría.
Los tests fijan que los tres motores prestados sobreviven a `uix.dispose()` y
que los tres fallbacks se destruyen. Sigue pendiente la reconexión del sink de
anuncios tras attach → dispose → attach (Q01).
que los tres fallbacks se destruyen. La reconexión del sink de anuncios tras
attach → dispose → attach ya se reprodujo como defecto (Q01-b); su corrección
sigue pendiente de una decisión de ownership.
El coste de la representación actual es acotado: cinco getters bifurcan por modo
para servicios o core, y los casts de `ActiveApp` se concentran en la frontera de
@ -404,7 +405,7 @@ lote se detiene en la prueba: no se sustituye por una deducción visual.
| L-153 de Sema | `npx vitest run src/uix/sema/resolver.test.ts src/uix/sema/chans/haptic.test.ts` más la señal `emerge` con haptic sin `kind` | La misma orden y una decisión explícita sobre rechazo, silencio o completado de firma |
| Morfo de menús | `npx vitest run src/uix/morfo/compile.test.ts src/uix/soma/components/context-menu/context-menu-provider.svelte.test.ts src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.test.ts` | Misma orden y guards de Morfo que cubran el contrato compilado |
| Overlays | `npx vitest run src/uix/soma/components/dialog/dialog-provider.svelte.test.ts src/uix/soma/components/alert-dialog/alert-dialog-provider.svelte.test.ts` y nueva prueba de foco | Misma suite en los tres navegadores exigidos por L-138 |
| ActiveUix | `npx vitest run src/uix/active-uix/active-uix.svelte.test.ts`; los motores prestados/fallbacks están caracterizados; falta reconexión de announce tras remount | Misma suite, SSR y matriz standalone/attach/dispose |
| ActiveUix | `npx vitest run src/uix/active-uix/active-uix.svelte.test.ts`; motores prestados/fallbacks caracterizados; Q01-b/c reproducidos con pruebas temporales; falta fijar ownership antes de corregir | Misma suite, SSR y matriz standalone/attach/dispose |
| CSS de Eidos | `npx vitest run src/uix/eidos/recipe-css-contract.test.ts src/uix/eidos/generated-css.test.ts src/uix/eidos/active-eidos-ssr.test.ts` | Comparación de salida CSS antes/después y comprobación de aplicación |
Q03 quedó corregido el 2026-09-20: `npm run check:gate` ya interpreta la salida

@ -703,6 +703,47 @@ describe('UIX layer contracts', () => {
}
});
it('attaches the default announce channel to the shared live region', async () => {
const app = createActiveApp({ services: defineUixServices({ langs: minimalLang }) });
const uix = attachActiveUix(app as unknown as ActiveApp, {
registerDefaultLangs: false
});
const target = document.createElement('div');
document.body.appendChild(target);
try {
expect(uix.events?.getChannel('announce')).toBeDefined();
await uix.events!.emit({
target,
name: 'signal-attach-announce',
family: 'signal',
intent: 'threat',
message: 'Atención',
channels: ['announce']
}).settled;
expect(document.getElementById('uix-announce-assertive')?.textContent).toContain('Atención');
expect(document.querySelectorAll("[role='alert']")).toHaveLength(1);
} finally {
target.remove();
uix.dispose();
app.dispose();
}
});
it('honours announce opt-out in the attached app service', () => {
const app = createActiveApp({
services: defineUixServices({ langs: minimalLang, events: { announce: false } })
});
const uix = attachActiveUix(app as unknown as ActiveApp, {
registerDefaultLangs: false
});
try {
expect(uix.events?.getChannel('announce')).toBeUndefined();
} finally {
uix.dispose();
app.dispose();
}
});
it('pins ActiveUix public service names and rejects legacy aliases', () => {
const uix = createActiveUix({ langs: minimalLang });
try {

@ -177,6 +177,21 @@ describe('EngineSemantic', () => {
expect(engine.getChannel('visual')).toBeUndefined();
});
it('bare engines leave announce opt-in', () => {
const bare = new EngineSemantic({ visual: false });
const enabled = new EngineSemantic({ visual: false, announce: true });
const disabled = new EngineSemantic({ visual: false, announce: false });
try {
expect(bare.getChannel('announce')).toBeUndefined();
expect(enabled.getChannel('announce')).toBeDefined();
expect(disabled.getChannel('announce')).toBeUndefined();
} finally {
bare.dispose();
enabled.dispose();
disabled.dispose();
}
});
it('visual accepts options and applies them to the built-in channel', () => {
const engine = new EngineSemantic({ dom: TEST_DOM, visual: { defaultHold: 999 } });
expect(engine.getChannel('visual')).toBeDefined();

Loading…
Cancel
Save

Powered by TurnKey Linux.