diff --git a/docs/audit/tracker-gap-reference-catalog.md b/docs/audit/tracker-gap-reference-catalog.md index d48b2287..be273506 100644 --- a/docs/audit/tracker-gap-reference-catalog.md +++ b/docs/audit/tracker-gap-reference-catalog.md @@ -3934,6 +3934,22 @@ Por tanto, «no hay rate limiting configurado» es **falso**; lo correcto es «e - **Resolution / Next step:** Added `Tracker.Infrastructure/Services/Tenancy/DevTenantSeedHostedService.cs`, mirroring `GeoCatalogSeedHostedService` (scope, log-and-swallow so the API still boots if the DB is down). Registered in `Infrastructure/DependencyInjection.cs`; **gated Development-only** — `if (!environment.IsDevelopment()) return;` is the first line of `StartAsync`, **before any DB access**, so Testing/Prod and the integration-test host (env `Testing`) no-op → clean DB. **Idempotent** (skips if any tenant exists). Seeds 3 **bare** demo tenants (`acme`/`globex`/`initech`) via the real `Tenant.Create` + repository + unit-of-work — no raw SQL, no geo localization (left for the UI, per LV-04..07). Build stays 0 warnings. - **Status:** `DONE` +#### LV-26 + +**Title:** An upstream 401 reaches the caller as 502, with the specific code overwritten. + +- **Purpose / Problem:** `AgentRuntimeGateway` throws `AgentRuntimeGatewayException(code: "AgentRuntime.HttpError", statusCode: )` when the runtime answers non-2xx. `AgentRuntimeTurnExecutor` catches it and flattens it to a STRING (`Result.Failure($"{ex.Code}: {ex.Message}")`) — deliberately, so the failed turn is still recorded, and that intent is right. But the status code does not survive the flattening: `AssistantEndpoints.Translate` re-derives one from the string, matches no case, and falls through `_ => ("AgentRuntime.Failed", 502)`. Two things are lost at once — the **upstream status** (401) and the **specific code**, rewritten from `AgentRuntime.HttpError` to the generic `AgentRuntime.Failed`. +- **What it means:** a CREDENTIAL problem is reported as an AVAILABILITY problem. `502 Bad Gateway` sends the operator to check whether the runtime is up; it was up, and answering 401 in 88 ms. The endpoint's own comment already recognises that collapsing to an HTTP code erases the story and that the trace keeps it — but the caller has no trace, and the caller is who has to act. +- **Example, observed:** on 2026-08-04 the `core-integration` robot reported `POST /assistant/converse → 502` against a live two-cluster stack. The cause was `AGENT_RUNTIME_API_KEY` differing between the Tracker's `tracker-runtime-auth` secret and the runtime's own. Only the tracker-api log revealed the 401; the API surface said bad gateway. Aligning the key turned the step green, so the diagnosis is confirmed and the mis-mapping is what cost the time. +- **Component:** `Backend` · **Module:** Assistant / Agent Runtime integration · **Type:** LV +- **Criticality:** P2 · **Complexity:** XS +- **Discovery:** Found while running `core-integration` against a live stack for the FIRST time — the robot is excluded from the default RoboSoft list and had never executed. The same run found a real Core defect (the MCP chart's `runAsUser`, beyondnetcode/evolith_arch32#425) and one defect in the robot itself (#139). +- **Proposed fix:** Carry the upstream status and code through the `Result` rather than re-deriving them from a string — e.g. a typed failure carrying `(code, upstreamStatus)`, so `Translate` maps `AgentRuntime.HttpError` + 401 to a 502 whose BODY names the upstream status and code. The HTTP status arguably stays 502 (the caller's own auth did succeed; the failure is upstream), so the defect to fix is the ERASURE, not the number. +- **Acceptance criteria:** + - [ ] A non-2xx from the runtime yields a response body naming the upstream status and the specific code, not the generic `AgentRuntime.Failed`. + - [ ] A test drives a 401 from a stubbed runtime and asserts the body distinguishes it from an unreachable runtime. +- **Status:** `PENDING` + #### LV-25 **Title:** Operator tenant switch serves stale, wrong-tenant lists (react-query cache not scoped to the acting tenant). diff --git a/docs/audit/tracker-gap-tracking.md b/docs/audit/tracker-gap-tracking.md index 5571b0db..f145d87a 100644 --- a/docs/audit/tracker-gap-tracking.md +++ b/docs/audit/tracker-gap-tracking.md @@ -87,6 +87,7 @@ This board is the single source of truth for Tracker technical debt, gaps, oppor | [`LV-11`](./tracker-gap-reference-catalog.md#lv-11) | Backend build warnings: 64 × CS0108 (per-aggregate `Id` hiding the Shell.Ddd `Entity` base) + 84 × CS8618 (non-nullable uninitialized) + 2 × CS8620. **Fixed:** 150→0 warnings with real fixes (explicit `new` on intentional `Guid Id` hides, `required` on Props records, element-wise nullability widening at 1 call site); no suppression, 428 tests green. | | | `Backend` | Cross | P2 | M | `DONE` | | [`LV-12`](./tracker-gap-reference-catalog.md#lv-12) | Frontend lint debt: 39 problems (5 errors + 34 warnings). **Fixed:** `--fix` + manual → **0 errors** (dead code / unused-symbol removal, an equivalent if/else, removed 2 stale eslint-disable comments); 8 `any`/non-null-assertion warnings left by design (fixing = typing refactor). Typecheck still 0. | | | `WEB` | Cross | P3 | S | `DONE` | | [`LV-13`](./tracker-gap-reference-catalog.md#lv-13) | Dev-seed papercut: the DevBypass platform-root tenant (`11111111…`) is a phantom — no seeder creates it, so local dev must create tenants by hand before tenant-scoped screens work. **Fixed:** `DevTenantSeedHostedService` (Development-only gate before any DB access, idempotent, seeds 3 bare demo tenants via `Tenant.Create` + repo — no geo, left for the UI). Testing/Prod no-op. | | | `Backend/WEB` | Cross | P3 | S | `DONE` | +| [`LV-26`](./tracker-gap-reference-catalog.md#lv-26) | **Un 401 del agent-runtime llega al llamante como 502, y con el código específico sobrescrito.** El gateway lanza `AgentRuntimeGatewayException` con el status real; el executor lo aplana a string a propósito —para que el turno fallido quede asentado, y eso está bien— pero `Translate` vuelve a derivar el status desde ese string, no casa con ningún caso y cae en `_ => ("AgentRuntime.Failed", 502)`. Se pierden a la vez el **status de origen** (401) y el **código específico** (`AgentRuntime.HttpError` → genérico). | Un problema de CREDENCIAL se reporta como problema de DISPONIBILIDAD: el 502 manda a comprobar si el runtime está en pie, y estaba en pie respondiendo 401 en 88 ms. | **Observado 2026-08-04:** el robot `core-integration` dio `POST /assistant/converse → 502` contra el stack real; la causa era `AGENT_RUNTIME_API_KEY` distinta entre el secreto del Tracker y la del runtime. Solo el log de tracker-api mostraba el 401. Alineando la clave el paso pasó a verde. | `Backend` | Cross | P2 | XS | `PENDING` | | [`LV-25`](./tracker-gap-reference-catalog.md#lv-25) | **Hallado por el nuevo harness UI-E2E (Playwright, `apps/tracker-web-e2e`) — Winston 2026-07-24.** Un platform-operator que cambia el tenant operado seguía viendo las listas tenant-scoped del tenant anterior: varias query-keys de react-query no incluyen el tenant (p.ej. `qk.initiatives = ['initiatives']`, `api/hooks.ts:110`) y `setActingTenant` (`store/auth.store.ts:309`) sólo hacía `set({actingTenantId})` sin invalidar el caché → datos del tenant equivocado hasta `staleTime` (30s), o **indefinidamente** al alternar entre dos tenants con datos. El backend SÍ aísla (robosoft `tenant-isolation` 10/10) — es correctness de **caché de cliente**, no fuga de datos. **Fixed:** efecto en `app/app.tsx` que hace `queryClient.invalidateQueries()` al cambiar `actingTenantId` (guarda de primer render), forzando refetch de toda query activa bajo el nuevo scope. Verificado por el propio E2E: switch a «Acme» → 9 iniciativas + drill-down a las 5 compuertas (`05-processes` 5/5). | Un operador de plataforma cambia de organización y la pantalla sigue mostrando las iniciativas de la anterior | Alternar entre dos tenants con datos mostraba las filas del tenant equivocado hasta 30 s | `WEB` | Cross | P2 | S | `DONE` | | [`LV-14`](./tracker-gap-reference-catalog.md#lv-14) | La pantalla de Soporte sigue mostrando datos inventados porque no existe modelo de tickets | La sección se ve completa y funcional, pero lo que muestra no corresponde a nada real del negocio | Resuelto por DESCARTE (decisión PO): Soporte no es contexto acotado ni está en la visión; pantalla huérfana del prototipo retirada por completo (pantalla+nav+ruta+mock+permisos). Web typecheck+build verde | `Backend/WEB` | Local | P3 | M | `DONE` | | [`LV-15`](./tracker-gap-reference-catalog.md#lv-15) | **Hecho — roll-out de [ADR T-034](../adrs/T-034-config-hub-vs-monitor.md)** (Config-hub vs Monitor) completo. Los **monitores** migrados a solo-lectura con la edición movida a zonas inline en Tenant configuration: Gate policies & criteria, Custom fields / artifact schemas (Gate governance 100% solo-lectura) y Tenant intelligence (`intel` solo-lectura). **Connectors (PPM intake)** y **Products** quedan como pantallas de **registro/gestión** — exentas por diseño (ADR T-034 §2.1: los registros no son monitores); sus cards de Config enlazan a su área de gestión ("Manage …"). | | | `WEB` | Cross | P2 | L | `DONE` | @@ -221,7 +222,7 @@ This board is the single source of truth for Tracker technical debt, gaps, oppor | [`GT-480`](./tracker-gap-reference-catalog.md#gt-480) | El job de despliegue corría también para cambios de sólo documentación | ~14 min de CI para publicar dos ficheros markdown | Sale a su propio workflow con `paths-ignore`; lista negra y no blanca, porque la blanca se queda obsoleta en silencio | `Infra` | Cross | P3 | XS | `DONE` | | [`GT-481`](./tracker-gap-reference-catalog.md#gt-481) | El despliegue se comprobaba dos veces sobre el mismo árbol | ~7 min de clúster Kubernetes repetidos sobre contenido idéntico | Deja de correr en push a `main`; y las esperas fijas pasan a sondeo por hecho observable | `Infra` | Cross | P3 | XS | `DONE` | -**Progress:** 174 / 203 done · 17 pending · 0 in progress · 3 blocked · 7 deferred · 1 superseded · 1 wontfix +**Progress:** 174 / 204 done · 18 pending · 0 in progress · 3 blocked · 7 deferred · 1 superseded · 1 wontfix *(Conteos reconciliados contra `python3 .harness/scripts/check-gap-registry.py` el 2026-08-01 al cerrar `CP-01` y `CP-08`: 203 fichas / 203 filas; estados `{PENDING: 17, DONE: 174, DEFERRED: 7, BLOCKED: 3, SUPERSEDED: 1, WONTFIX: 1}`.)* **Wave 2026-06-07 → 2026-06-14 (BMAD audit + coherence):** Items `GAP-*`, `COH-*`, `OPP-*` from the PROMPT MAESTRO functional/technical/documentary audit and the source-coherence analysis (106 items: 81 resolved, 24 open, 1 blocked, 1 deferred at import time). diff --git a/robosoft/robots/core-integration.robot.mjs b/robosoft/robots/core-integration.robot.mjs index 47904d34..2e59e34d 100644 --- a/robosoft/robots/core-integration.robot.mjs +++ b/robosoft/robots/core-integration.robot.mjs @@ -127,8 +127,18 @@ export default { // may be "failed" (the probe manifest has findings) — that is a REAL Core // evaluation, which is exactly what proves the round-trip. const data = unwrap(r.body); - check('Core returns a real evaluation verdict (gates evaluated)', r.body?.success === true && Array.isArray(data.gates), { - detail: Array.isArray(data.gates) ? summarizeVerdict(data) : r.body?.error?.message || 'no gates', + // `results.gate`, NOT `gates`. The Core's canonical EvaluationResult nests + // per-kind results under `results` (`gate`, `artifact`, `compliance`); it is + // the TRACKER's own DTO that flattens them to `gates`, and this robot talks + // to the Core through the gateway, so it gets the canonical shape. + // + // Asserting the flattened key made this step report "no gates" against a + // Core that had evaluated six of them — a defect in the instrument that read + // as a defect in the product. Measured against a captured REST response: + // `data.gates` is undefined, `data.results.gate` has 6 entries. + const gates = gatesOf(data); + check('Core returns a real evaluation verdict (gates evaluated)', r.body?.success === true && Array.isArray(gates), { + detail: Array.isArray(gates) ? summarizeVerdict(data) : r.body?.error?.message || 'no gates', }); } @@ -177,8 +187,16 @@ function parseMcpInner(body) { } /** One-line summary of an evaluation verdict from the Core's `data` payload. */ +/** The canonical EvaluationResult nests gates under `results.gate`. */ +function gatesOf(data) { + return Array.isArray(data?.results?.gate) ? data.results.gate : undefined; +} + function summarizeVerdict(data) { - const gates = Array.isArray(data.gates) ? data.gates : []; - const failed = gates.filter((g) => String(g.verdict).toLowerCase() === 'failed').length; + const gates = gatesOf(data) ?? []; + // The Core emits `FAIL`, not `failed`. Comparing against 'failed' reported + // zero failures on a run where all six gates had failed — a green-looking + // summary over a red result, which is worse than no summary. + const failed = gates.filter((g) => /^fail/i.test(String(g.verdict))).length; return `gates=${gates.length} · failed=${failed}`; }