feat: pre-check DWH reachability before creating a session (local dev only)

New session now refuses to spawn a Pi runtime that would only die in bootstrap
retrieval when the DWH/vector host is unreachable (e.g. a dropped VPN). Before
`session new`, POST /sessions probes the DWH via `tht db ping`; if it is down it
returns 503 {code:"dwh_unreachable"} with a clear message and creates nothing.

- Gated behind the THT_DWH_PRECHECK flag (default off), enabled only by the local
  dev launcher (run-stack.sh) — containers/CI never pay the probe, and existing
  tests that don't set it are unaffected.
- ThtRunner.dbPing() runs `tht db ping` with a 10s timeout (run() gains an optional
  timeout that SIGKILLs a hung child).
- Frontend: apiFetch throws a typed ApiError (status + parsed payload); the new-
  session composer shows the specific alert on `dwh_unreachable` instead of the
  generic retry hint, keeping the question for retry.

Verified live on an isolated backend (precheck on + broken DWH host → 503
dwh_unreachable, no session created) and via unit tests (backend 228, frontend 308).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 12:08:47 +02:00
co-authored by Claude Opus 4.8
parent 84da3b149b
commit 3453f3ae23
10 changed files with 158 additions and 8 deletions
@@ -95,6 +95,32 @@ test("a failed create restores the landing view and preserves the question for r
expect(FakeEventSource.instances).toHaveLength(0);
});
test("a DWH-unreachable precheck shows a specific alert and preserves the question", async () => {
server.use(
http.post("http://localhost:8787/sessions", () =>
HttpResponse.json(
{
error: "Cannot start a session: the data warehouse is unreachable. Check the VPN connection and try again.",
code: "dwh_unreachable",
},
{ status: 503 },
)),
);
renderShell();
const composer = screen.getByRole("textbox", { name: /new question/i });
await userEvent.type(composer, "quanti pazienti?");
await userEvent.click(screen.getByRole("button", { name: /send/i }));
// Specific alert, not the generic "failed to create session" hint.
expect(await screen.findByText(/data warehouse is unreachable/i)).toBeInTheDocument();
expect(screen.queryByText(/failed to create session/i)).not.toBeInTheDocument();
// No session was created: landing view stays and the question is kept for retry.
expect(screen.getByText(/type your question/i)).toBeInTheDocument();
expect(composer).toHaveValue("quanti pazienti?");
expect(FakeEventSource.instances).toHaveLength(0);
});
test("model selector shows the three Pi-enabled models and persists the selected provider", async () => {
let saved: unknown;
server.use(