Skip to content

fix: saldar la deuda técnica registrada (TD-004 a TD-007) - #15

Merged
beyondnetPeru merged 19 commits into
mainfrom
fix/deuda-td004-td007
Aug 10, 2026
Merged

fix: saldar la deuda técnica registrada (TD-004 a TD-007)#15
beyondnetPeru merged 19 commits into
mainfrom
fix/deuda-td004-td007

Conversation

@beyondnetPeru

Copy link
Copy Markdown
Contributor

Resuelve los cuatro puntos de deuda que quedaron registrados al cerrar el PR #14.

TD-004 — El typecheck del front nunca había corrido

tsBuildInfoFile sin incremental hacía que tsc abortara con TS5111 antes de comprobar nada: el proyecto creía tener un typecheck y no lo tenía. La única verificación de tipos era la que hace Vite al construir, que solo mira lo que entra en el bundle — ni las pruebas, ni el código no alcanzado.

Encendida la puerta aparecieron 538 errores (367 de producción, 171 de pruebas). Están los 538 cerrados, existe npm run typecheck y CI lo ejecuta como paso propio antes de las pruebas.

Lo que la puerta llevaba tapando no eran anotaciones. Quince defectos vivos, cada uno con su commit:

Defecto Efecto
SmartConfigInput leía el segundo argumento de M3Switch.onChange Un parámetro booleano guardaba siempre false, marcara lo que marcara el interruptor
permission-cascade mapeaba submenú a 'Page' ExclusiveArcTarget no tiene Page: cascadear un permiso por un submenú devolvía 400
ListToolbar llamaba onSearchSubmit() sin evento Pulsar Enter en cualquier buscador lanzaba Cannot read properties of undefined
IdpPanel arrancaba con provStrategy = 'OIDC' No es miembro del enum del backend: registrar un proveedor sin tocar el desplegable devolvía 400
ProfileForm leía u.userId / u.userName El desplegable de usuarios pintaba undefined y enviaba undefined
ProfileDomainResourcesPanel leía node.items El campo es permissions: el árbol reventaba al primer render
M3DataView referenciaba ChevronsUp/ChevronsDown sin importar El botón de plegar la cabecera lanzaba en sus dos ramas
ProfileForm llamaba logger.error sin importar El manejador de error moría antes de setError: pantalla muda justo cuando había algo que contar
Zod 4 renombró ZodError.errors a .issues Dos sitios iteraban undefined al fallar la validación
NAV_PREFETCH_CONFIG.delegations llamaba getAll No existe: pasar el ratón por el ítem lanzaba TypeError
Ocho paneles pasaban footerElement a DataList La prop es telemetryInfo: el pie de telemetría no se pintaba en ninguno
M3DialogAction no declaraba disabled ni loading El confirmar seguía activo durante el envío y aceptaba un segundo clic
ErrorBoundary pintaba t.errorSupportReference sin llamarla La referencia de soporte —lo único accionable de esa pantalla— no salía nunca
@infrastructure aliasado en Vite pero no en tsconfig Un módulo y su cadena de consumidores no se comprobaban en absoluto
Diez pruebas de GraphQL afirmaban sobre claves getXxx Los resolvers devuelven xxx; el doble devolvía la clave que le dieran, así que no comprobaban nada

Esa última fila es la misma trampa que la deriva menus/nodes del grafo de autorización: una prueba cuyo doble reproduce el error lo confirma en vez de encontrarlo. Los fixtures de plantillas, perfiles, inquilinos, cuentas, suites y flags estaban igual —describían formas que el contrato no tiene— y quedan alineados con su esquema.

TD-005 — Cuerpos de notificación en claro

El adaptador simulado registraba el cuerpo completo. Llevan enlaces de restablecimiento y tokens de aprobación: en el log son credenciales que funcionan durante todo el tiempo de retención. Ahora registra canal, destinatario, asunto y longitud. Fuera de desarrollo se registra UnconfiguredNotificationAdapter, que falla ruidosamente en vez de dar por enviado lo que nadie recibirá.

TD-006 — Migrar una base limpia fallaba

La migración del outbox de MassTransit usa gen_random_uuid() sin crear pgcrypto. Se añade CREATE EXTENSION IF NOT EXISTS pgcrypto al inicio de su Up.

TD-007 — La cadena de conexión de desarrollo

Apuntaba a un puerto que el compose no publica. Alineados.

TD-008 — Registrado, no resuelto

El typecheck destapó que hay dos vite en el árbol: 6.4.3 fijado por la app —el que construye— y 8.1.5 izado a la raíz por Storybook y vitest. Los tipos del plugin de React resolvían al izado. tsconfig.node.json mapea vite a la copia de la app, que es la que corre; converger en un major es un salto del empaquetador y merece su propio cambio, su propia verificación de build y su propia vuelta atrás.

Verificación

  • npm run typecheck — 0 errores (538 al empezar)
  • vitest — 1697/1697 en 182 ficheros
  • vite build — OK
  • eslint — 0 errores
  • dotnet build Ums.sln — 0 errores

🤖 Generated with Claude Code

beyondnetPeru and others added 19 commits August 10, 2026 07:38
**TD-005 — cuerpos de notificación en claro.** `SimulatedNotificationAdapter` era la única
implementación de `INotificationService` y se registraba sin guarda de entorno, escribiendo el
cuerpo completo en `LogInformation`. Esos cuerpos llevan enlaces de restablecimiento de
contraseña y tokens de aprobación: en el log son credenciales que funcionan, y durante todo el
tiempo de retención, que sobrevive de largo al token.

Ahora el simulador solo se registra fuera de producción, y ya no escribe el cuerpo —canal,
destinatario y asunto bastan para seguir un flujo; la línea informa la longitud, no el
contenido—. Fuera de desarrollo se registra `UnconfiguredNotificationAdapter`, que **falla
ruidosamente** en vez de callar: un no-op silencioso daría por enviada una invitación que nadie
recibe, y eso solo se descubre cuando el usuario reclama. Sigue sin haber entrega real; ese tipo
es precisamente la documentación de que falta.

**TD-006 — una base PostgreSQL limpia no migraba.** `UpdatePostgresMassTransitOutbox` usa
`gen_random_bytes` y la migración que declara pgcrypto va después, así que sobre una base vacía
el migrador abortaba con 42883 y la API no arrancaba. Se añade un `CREATE EXTENSION IF NOT
EXISTS pgcrypto;` idempotente al principio de esa migración, en vez de reordenar: reordenar
reescribiría un historial ya aplicado en entornos vivos.

Verificado de la única forma que vale: una base virgen (solo `plpgsql`) migra las **32**
migraciones, la API arranca y el login contra los datos sembrados funciona.

**TD-007 — el entorno local no coincidía consigo mismo.** La cadena de desarrollo apuntaba al
5433 y el compose publica el 5432; las contraseñas tampoco coincidían. Ahora concuerdan, con el
compose como fuente de verdad porque es lo que ya usan CI y el camino de contenedor.

Validación: build 0 errores; IntegrationTest 314/316.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…poner los tipos del barril

TD-004, primer tramo: 538 -> 346 errores de tipo.

130 claves de traducción se citaban desde los componentes y no existían en ningún
diccionario. 61 pintaban `undefined` en pantalla; el resto se salvaba con un `?? '...'`
en el sitio de uso, que es la razón de que nadie lo notara. 26 existían antes de la
resincronización y se recuperan del historial con su copia original (4a8b940); las 104
restantes nunca existieron —tampoco en la plataforma de origen— y su texto sale del
valor por defecto que el propio componente traía, o de su contexto de uso.

El barril de componentes reexportaba nueve interfaces de props declaradas locales,
más `EntityCardProps` y `RouteLoaderProps`, que no existen: EntityCard tiene dos
modos (Simple y Generic) y RouteLoader no lleva props. `TreeNode` se reexportaba
desde HierarchicalList, que solo lo importa; ahora sale de su origen.

`PrefetchEntry.queryKey` estaba declarado `string[]` y react-query usa
`readonly unknown[]`. Las claves de ese fichero llevan números y `undefined`, y han de
coincidir literalmente con las que arma cada pantalla: con el tipo estrecho, el arreglo
natural es convertir a texto, y entonces el prefetch calienta una entrada de caché que
la pantalla nunca lee.

Verificado: vitest 1697/1697, build OK, eslint 0 errores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ps que los componentes ignoraban

TD-004, segundo tramo: 346 -> 313 errores de tipo.

Cuatro identificadores se usaban sin existir en el ámbito. No son avisos de tipos:
son excepciones en cuanto se ejecuta esa línea.

- M3DataView referenciaba ChevronsUp/ChevronsDown sin importarlos de lucide-react:
  el botón de plegar la cabecera rompe el render en cualquiera de sus dos ramas.
- ProfileForm llamaba `logger.error` en su `catch` sin importar el logger, así que
  el manejador de error moría antes de llamar a `setError` y la pantalla se quedaba
  muda justo cuando había algo que contar.
- ProfileDomainResourcesPanel usaba `itemEffect` y `SystemSuiteCrudOperation` sin
  importarlos; DomainResourcesPanel, lo segundo.
- Las pruebas usaban `global`, que es de Node y no está declarado en un tsconfig de
  navegador; `globalThis` vale en ambos.

Props declaradas de menos, que se descartaban en silencio:

- M3DialogAction no admitía `disabled` ni `loading` y el render no los reenviaba a
  M3Button, que sí los soporta. Las pantallas ya los pasaban: el confirmar seguía
  activo mientras se enviaba y aceptaba un segundo clic.
- IconButton heredaba `size` de ButtonHTMLAttributes, donde no existe (es de
  input/select), así que los 17 `size="sm"` acababan en el DOM como atributo
  inválido y el botón conservaba su tamaño normal.
- StatusBadge no hace spread de props: su `size` se perdía sin dejar rastro.

QueryStateApi y PaginationStateApi sustituyen a los `ReturnType<typeof …>` que cada
panel escribía por su cuenta. Chocaban (TS2719) porque el hook infiere el filtro
estrecho y el panel lo declaraba `string`. `activeFilter` se lee estrecho y
`setActiveFilter` se escribe ancho: la asimetría es lo que hace la asignación válida.

Verificado: vitest 1697/1697, build OK, eslint 0 errores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…activo

TD-004, tercer tramo: 313 -> 255 errores de tipo.

Nueve genéricos exigían `T extends Record<string, unknown>` y ninguno usa la firma de
índice: todos se limitan a `keyof T` y `T[K]`. La restricción rechazaba cualquier
interfaz —TS no les da firma de índice implícita, solo a los alias de tipo—, así que
DataGrid, HierarchicalList, useInlineEdit, useTreeNodes y useLocalOverrides no
aceptaban los propios modelos del dominio. `object` es lo que de verdad necesitan.

`Translations` estaba definido como `typeof translations.es`. Con `as const` cada
idioma tiene sus propios tipos literales, así que el diccionario inglés no era
asignable y toda función que recibiera `t` rechazaba la mitad de las llamadas. Es la
unión de ambos idiomas: quien recibe `t` lo pinta, y 'Categoría' | 'Category' se pinta
igual de bien.

De ahí salía el `Record<string, string>` de tenant-list-renders, que además era falso:
el diccionario tiene entradas que son funciones. Y el panel pasaba `t` a los dos
renderizadores de fila, que ni lo declaran ni lo usan —solo pintan datos—, así que
JavaScript lo descartaba en cada render.

Verificado: vitest 1697/1697, build OK, eslint 0 errores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…, flags, suites)

TD-004, cuarto tramo: 255 -> 227 errores de tipo.

Las pruebas de plantillas de permisos y de perfiles construían sus datos con
`code`, `name`, `status` y `createdAt`, y afirmaban sobre `.name`. Ninguno de los
cuatro campos existe en el contrato: una plantilla se identifica por rol y suite y
lleva `version`; un perfil es la terna inquilino-usuario-rol dentro de una suite. Las
pruebas pasaban porque el mock devolvía exactamente la forma inventada, así que
confirmaban la invención en vez de comprobar el contrato. Es la misma trampa que la
deriva `menus`/`nodes` del grafo de autorización.

Los payloads de creación iban por el mismo camino: `create` recibe
{tenantId, roleId, systemSuiteId}, no {code, name}; y `addItem` recibe el arco
exclusivo completo (destino, acción y el par permitir/denegar).

Los mocks compartidos omitían campos obligatorios: los flags, el código y el nombre
de su suite; los recursos de dominio, sus operaciones CRUD y sus acciones propias
—que es justo lo que pintan los paneles del árbol—. El servidor simulado servía
formas que el esquema real rechazaría.

El mock del store de sesión declaraba `selector?: never`, lo que hacía inllamable al
propio selector que la prueba le pasa.

Verificado: vitest 1697/1697, build OK, eslint 0 errores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… de menos

TD-004, quinto tramo: 227 -> 201 errores de tipo.

`ListToolbar` invoca `onSearchSubmit()` sin argumento —ya hizo `preventDefault` por su
cuenta— y los paneles le pasan `handleQuerySubmit`, que empieza con `e.preventDefault()`.
Pulsar Enter en el buscador lanzaba «Cannot read properties of undefined». El evento
pasa a ser opcional, que es lo que ya exigían sus dos formas de uso: `onSubmit` de un
formulario y callback pelado.

Ocho paneles pasaban `footerElement` a DataList, que su prop llama `telemetryInfo`:
el pie de telemetría no se pintaba en ninguno de los ocho.

ApiErrorBanner descartaba el `className` del llamante, así que sus márgenes eran
siempre los de dentro. DataViewShell exigía `title` y varios paneles no tienen uno
propio —la cuenta y la etiqueta las lleva ListToolbar—, de modo que pintaba un `<h2>`
vacío; ahora se omite la barra si no hay título. ConfigValueDisplay recortaba con
constantes fijas de 30 y 40 caracteres e ignoraba el `truncateAt` que ya le pasaban.

Verificado: vitest 1697/1697, build OK, eslint 0 errores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…alse

TD-004, sexto tramo: 201 -> 184 errores de tipo.

`M3Switch.onChange` entrega el valor como PRIMER argumento y SmartConfigInput leía el
segundo: `checked` era siempre `undefined`, así que el interruptor de un parámetro
booleano escribía 'false' estuviera donde estuviera. No lo veía nadie porque el
componente devuelve el valor por la prop `checked`, que sí llegaba bien: la pantalla
se pintaba correcta y lo que se guardaba no.

Convivían tres vocabularios de tamaño: 'xs' (36 sitios), 'sm' (19), 'small' (7),
'md' (1) y 'large' (1). Como los componentes no declaraban `size`, los de la segunda
familia se descartaban sin ruido. Se estandariza en el de la casa —'xs' | 'sm' | 'md',
el que ya usaba CodeBadge— y se reescriben los ocho sitios sueltos. Spinner nunca tuvo
`size`: se dimensiona por className. Y M3TextField hereda el `size` de <input>, que es
el ancho EN CARACTERES: 'sm' allí no significaba nada.

El usuario sustituto de ProfileScreen (sesión ausente) omitía `id` y `profileId`, que
el panel de diagnóstico pinta: salían vacíos justo en el modo en que sirven para
depurar.

Verificado: vitest 1697/1697, build OK, eslint 0 errores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TD-004, séptimo tramo: 184 -> 172 errores de tipo.

`userOptions` leía `u.userId` y `u.userName`, y `UserAccount` tiene `userAccountId` y
`displayName`: cada opción del desplegable de usuarios se pintaba «undefined (correo)»
y llevaba `undefined` como valor. Crear un perfil desde esa pantalla mandaba un
`userId` vacío.

`FieldSelect` no declaraba `disabled` y cuatro sitios se lo pasaban, así que los
selectores de usuario, rol y suite quedaban activos aunque faltara su prerequisito:
se podía elegir rol antes que suite y enviar la combinación.

`resolvePermissionEffect` devolvía `string` para un valor que va al servidor y solo
admite allow/deny/neutral. `ProfileModulePermissionsPanel` recibía `onNodeSelect` y
`selectedNodeId`, que no existen en su interfaz y no hacen nada: el panel no tiene
noción de selección.

Verificado: vitest 1697/1697, build OK, eslint 0 errores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…módulo entero

TD-004, octavo tramo: 172 -> 155 errores de tipo.

`@infrastructure` estaba aliasado en vite.config.ts pero no en el tsconfig, y un
módulo lo usaba: para el empaquetador resolvía y para el comprobador no, así que ese
fichero y su cadena de consumidores jamás se comprobaron. Al normalizarlo a `@infra`
—el alias de la casa— aparecieron los errores que llevaba tapando.

Detrás había tres fallos vivos:

- Zod 4 renombró `ZodError.errors` a `.issues`. Dos sitios iteraban `undefined`, así
  que el formulario del catálogo de parámetros y la validación compartida lanzaban
  TypeError justo al fallar la validación, que es cuando tienen que informar.
- El catálogo leía `pageData.totalPages`, que ese endpoint no devuelve: siempre valía
  1 y la lista se quedaba en una sola página hubiera las que hubiera. Ahora se deriva
  de `totalItems` y el tamaño de página.
- `asApiError` se usaba sin importar en app-configuration.service: el `catch` moría
  con ReferenceError antes de registrar nada.

Además, `securityInterceptor` se reexportaba un tipo a sí mismo, `useRef` sin valor
inicial (React 19 lo exige) y `setSortBy` repetía la asimetría de varianza que ya
tenía `setActiveFilter`: ancho al escribir, estrecho al leer.

Verificado: vitest 1697/1697, build OK, eslint 0 errores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TD-004, noveno tramo: 155 -> 138 errores de tipo.

`computeNodeState` y `countEffects` se copiaron del panel de plantillas sin adaptar el
nombre del campo: leían `node.items`, y el nodo del árbol de perfiles guarda
`permissions`. `undefined.map` en la primera llamada.

M3Button hereda `size` de ButtonHTMLAttributes, donde no existe: los 16 sitios que
pasaban `sm` o `xs` lo mandaban al DOM como atributo inválido y el botón conservaba su
altura de 40 dp. SectionHeader recibía `badge` sin declararlo, así que la insignia de
estado no se pintaba.

`useFormValidation` indexaba a ciegas tres estructuras que TypeScript ve como `{}`:
`flatten().fieldErrors` (Zod no infiere las claves desde un ZodTypeAny genérico),
`schema.shape` (solo existe en ZodObject) y las claves de `useFormDirty`.

DetailPanelShell exige `isLoading` e `isEmpty` porque son los que deciden si pinta su
propio esqueleto; tres pantallas pintan el suyo como hijo y los omitían.

Verificado: vitest 1697/1697, build OK, eslint 0 errores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…gia por defecto

TD-004, décimo tramo: 138 -> 122 errores de tipo.

`IdpPanel` arrancaba con `provStrategy = 'OIDC'`, y el backend valida el nombre EXACTO
del enum: la lista es 'GenericOidc', 'Saml2', 'AzureAd'… Quien no tocara el desplegable
recibía «Invalid identity provider strategy». Lo advierte el propio comentario de
idp.constants (G-155); lo que faltaba era que el comprobador lo hiciera cumplir.

`ErrorBoundary` pintaba `{t.errorSupportReference}` sin llamarla: es una función del
diccionario, y React rechaza una función como hijo. La referencia de soporte —lo único
accionable de esa pantalla— no salía nunca.

Tres hooks devolvían miembros que su propia firma no declaraba (`blockReason`,
`handleRestoreRequest`, `requiresFilter`), así que las pantallas que los leen veían
`undefined`. Y `ConsoleTab` no incluía 'branding': la pestaña de identidad visual se
reenganchó al panel pero no al tipo que enumera las pestañas.

`_retry`, la marca propia sobre la configuración de axios que evita el doble reintento,
ahora se declara en vez de colarse.

Verificado: vitest 1697/1697, build OK, eslint 0 errores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TD-004, undécimo tramo: 122 -> 111 errores de tipo (23 en producción).

`useQueryState` solo podía inferir `'all'` como tipo del filtro, así que tras la guarda
`activeFilter !== 'all'` TypeScript lo estrechaba a `never` y la comparación siguiente
—`d.status === activeFilter`— quedaba muerta. Anotando el filtro como
`'all' | DelegationStatus` la comparación vuelve a tener sentido.

`GraphNodeAction.effect` estaba declarado `string` en el lector del grafo, y el contrato
lo define como el enum `AccessEffect`: `evaluateEffect` recibía cualquier cadena sin que
nadie la comprobara. Es el mismo fichero que corrige la deriva `menus`/`nodes`, y el
tipo suelto era el resto de aquella herida.

TenantSelect ignoraba `disabled` en tres formularios; el interruptor de solo lectura de
ConfigValueDisplay no pasaba manejador; `PaginationData.pageSize` exigía una opción del
selector (10/25/50) y las bandejas fijan 6, que es legítimo; y el barril exportaba
`SpinnerProps`, que no existe, mientras omitía `DetailTab`, que sí.

Verificado: vitest 1697/1697, build OK, eslint 0 errores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
El commit anterior añadió la prop a la interfaz sin cablearla al render, que es
exactamente el defecto que venía a corregir.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TD-004, duodécimo tramo: 111 -> 95 errores de tipo (7 en producción).

`NAV_PREFETCH_CONFIG.delegations` invocaba `delegation.service.getAll`, que no existe:
las delegaciones se consultan por administrador delegante o delegado y este fichero no
tiene a quién. Pasar el ratón por ese ítem de navegación lanzaba TypeError. La entrada
se retira con la razón escrita en su lugar. La del catálogo de parámetros calentaba
'parameter-catalogs' con ocho elementos de paginación, y ese endpoint no pagina ni usa
esa clave: se alinea con ['parameter-definitions', filter], que es la que lee la
pantalla.

`UserAccountPasswordPanel` pasaba `isLoading` a M3Button, que expone `loading`: el
botón de generar contraseña temporal no giraba y seguía pulsable durante la mutación.
`ConnectedUserDrawer` pasaba `position` y `width`, y M3Drawer no tiene la primera
—siempre abre a la derecha— y llama `maxWidth` a la segunda.

`AddButtonInline` declaraba `onClick: () => void` y su único uso necesita el evento
para frenar la propagación dentro de una cabecera plegable. `DelegationForm.handleSubmit`
exigía evento y el diálogo lo llama sin él. Y `UserAccountProfileCard` mantenía una copia
local de `UserCategory`: si el enum del dominio crece, la tarjeta se queda atrás sin que
nadie lo note.

Verificado: vitest 1697/1697, build OK, eslint 0 errores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ue el servidor rechaza

TD-004: código de PRODUCCIÓN a cero errores de tipo (367 al empezar).

`permission-cascade` mapeaba el submenú a `'Page'`, y ExclusiveArcTarget
(Ums.Domain/Enums/ExclusiveArcTarget.cs) solo tiene SystemSuite, Module, Submodule,
Option, Aggregate y Entity. AddTemplateItemCommandValidator valida el nombre contra ese
enum, así que habilitar la jerarquía superior de una opción bajo un submenú respondía
400. Menú y submenú van ahora los dos a 'Submodule': el árbol recursivo de MenuNode
(ADR-0090) ya no fija cuántos niveles hay, y distinguirlos no aportaba nada que el
servidor supiera leer.

La jerarquía de recursos de dominio (Agregado → Entidad → DomainMethod) estaba a medias:
`HierarchyType` y el payload de `useAddDomainResource` se quedaron en Aggregate|Entity, y
ese último tampoco admitía `parentResourceId`, que es justo lo que el formulario pide
para un método de dominio.

Además: los iconos de lucide no aceptan `title` —el tooltip de estado del árbol no
salía—, `rowVersion` es anulable en el esquema y obligatorio en el mutador, y el tipo
de `SystemSuiteNode` declaraba `metadata` solo anulable cuando el esquema la hace
también opcional, que era lo que impedía casar el ZodLazy con su anotación.

Verificado: vitest 1697/1697, build OK, eslint 0 errores, tsc 0 errores en producción
(quedan 88 en ficheros de prueba).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… contrato no tiene

TD-004: 88 -> 64 errores de tipo, todos ya en ficheros de prueba.

Las pruebas del store de sesión creaban usuarios sin `authorizationGraph`, que es
obligatorio en AuthUser: un usuario así no es uno que el login pudiera producir.

Las de feature flags construían el flag con code/name/description/sortOrder y
`flagType: 'boolean'` en minúscula. El contrato es flagCode/flagType/flagTargets/status
más el código y el nombre de su suite, y el servidor compara el tipo con el nombre del
enum, así que la minúscula sería un 400.

Los dobles de UserAccountListPanel se habían quedado cortos frente a QueryStateApi y
PaginationStateApi —faltaban seis miembros—, y sus inquilinos y cuentas omitían
`isManagementOwner`, `displayName` y `hasActivePassword`. Un doble incompleto prueba una
pantalla que no existe: ahora se declaran con el tipo, que es lo que impide que vuelvan
a quedarse atrás.

Verificado: vitest 1697/1697, eslint 0 errores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lta no devuelve

TD-004: 64 -> 54 errores de tipo.

Diez pruebas simulaban la respuesta con `getDelegationById`, `getTenantBranding`,
`getFeatureFlags`… y afirmaban sobre esa misma clave. Las consultas reales devuelven el
campo sin el prefijo (`delegationById`, `tenantBranding`, `featureFlags`), que es el
nombre del resolver en el esquema. Pasaban porque el doble devuelve exactamente la clave
que se le da: si producción leyera la clave equivocada, estas pruebas seguirían verdes.
Es la misma trampa que el fixture del grafo de autorización.

`permission-template.graphql` declaraba sus dos respuestas como `{ campo: unknown }`, así
que nada comprobaba la forma y la prueba podía leer `.templateId` de un `unknown`. Ahora
se declaran los DTO que la propia consulta pide campo por campo.

Verificado: 51/51 en las nueve pruebas de consultas GraphQL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TD-004: 54 -> 26 errores de tipo.

Los tipos de estado de los stores de notificaciones e i18n no se exportaban, así que
cada prueba tipaba su selector como `never` —lo que vuelve inllamable el propio selector
que le pasa— y lo salvaba con castings sueltos. Ahora se exportan y el doble declara qué
está simulando.

`TopAppBar.test` mantenía una copia local del tipo de marca sin `shortName`, justo el
campo que una de sus pruebas comprueba. Se usa `SystemSettings['brand']`.

Fixtures alineados con su contrato: inquilinos (faltaban `type`, `parentTenantId`,
`companyReference`, `isManagementOwner` y sobraba `createdAt`; el payload de creación
exige `type`), cuentas (branchId, displayName, identityReference, hasActivePassword,
passwordUpdatedAtUtc) y suites (tenantId, description y las tres colecciones).

Verificado: vitest 1697/1697, eslint 0 errores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
538 -> 0. `npm run typecheck` existe y CI lo ejecuta como paso propio, antes de las
pruebas: un error de tipo es más barato de leer que el fallo en cascada que provoca.

Hasta aquí, `tsBuildInfoFile` sin `incremental` hacía que tsc abortara con TS5111 antes
de comprobar nada, y la única verificación de tipos era la que hace Vite al construir,
que solo mira lo que entra en el bundle. Lo que la puerta llevaba tapando —además de
las 538 anotaciones— son quince defectos vivos, cada uno con su commit en esta rama:
el interruptor booleano que siempre guardaba `false`, el Enter del buscador que
reventaba, la cascada de permisos que devolvía 400 por un `targetType` inexistente, el
alta de proveedor de identidad que devolvía 400 con su valor por defecto, el árbol de
recursos que no llegaba a pintar, y diez pruebas de GraphQL que afirmaban sobre claves
que ningún resolver devuelve.

`tsconfig.node.json` destapó de paso que hay dos vite en el árbol: 6.4.3 fijado por la
app —el que construye— y 8.1.5 izado a la raíz por Storybook y vitest. Los tipos del
plugin de React resolvían al izado. Se mapea `vite` a la copia de la app, que es la que
corre, y la duplicación queda registrada como TD-008: converger es un salto de major del
empaquetador y merece su propio cambio.

Verificado: typecheck 0, vitest 1697/1697, build OK, eslint 0 errores.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_logger.LogError(
"[NOTIFICACIÓN NO ENTREGADA] Canal={Channel} | Para={Recipient} | Asunto={Subject}",
notification.Channel,
notification.Recipient,
@beyondnetPeru
beyondnetPeru merged commit 2e5c59b into main Aug 10, 2026
23 of 24 checks passed
@beyondnetPeru
beyondnetPeru deleted the fix/deuda-td004-td007 branch August 10, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants