mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
Quality-leap release between two HealthKit milestones. The headline is a P0 hotfix for the `/api/analytics` 500 that broke the Insights mother page for any account with more than a few thousand measurements: the six per-metric status helpers were spreading the entire numeric history into `Math.min` / `Math.max`, which trips a stack overflow once a single argument list exceeds the V8 spread cap. Both the surfaced symptom and the latent risk in five sibling helpers were folded onto a reduce-based min/max path. Around that hotfix landed a deep polish pass: the Insights mother page now defers three below-the-fold blocks behind `next/dynamic` to trim the cold mount cost, the analytics endpoint gained a `?slice=summaries` slim slice that the per-metric sub-pages now ride, the Coach snapshot builder picked up a 60 s LRU keyed on `(userId, scope)`, the Settings shell consolidated three duplicate copies of the about / notifications / about-dropdown surfaces, the navigation strip dropped a redundant "Home" group label and a colliding "Notifications" sibling, every icon-only button on the medications and admin surfaces gained an accessible name, every Progress bar gained one too, three pages had their heading hierarchy repaired to a sequential h1 → h2 → h3, the mobile Coach FAB now hides while a chart tooltip is open so it cannot occlude the read-out, and the auth shell normalised every authenticated route to a single `max-w-screen-xl` container. Also lands the Apple App Site Association handler at `/.well-known/apple-app-site-association` on every host that fronts the app, advertising the iOS bundle's App ID prefix (S8WDX4W5KX.dev.healthlog.app) under `webcredentials.apps` so the passkey ceremony shares cleanly between the web origin and the iOS app. The release closes nineteen issues from a five-surface audit + a runtime bug-hunt pass, plus six follow-up tracks of polish across the affected surfaces.
126 lines
3.8 KiB
TypeScript
126 lines
3.8 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { I18nProvider } from "@/lib/i18n/context";
|
|
|
|
/**
|
|
* v1.4.16 phase B5e — coverage for the admin AI quality preview.
|
|
*
|
|
* Three branches:
|
|
* 1. loading state (Loader2 spinner + label)
|
|
* 2. empty state (no summary OR summary with empty buckets)
|
|
* 3. table-render (one row per bucket with the helpful-rate column)
|
|
*
|
|
* The fetch is replaced via a hoisted vi.fn so each test can stage a
|
|
* different response; the component reads through TanStack Query but
|
|
* we mock the hook to bypass the suspend-and-fetch dance and feed the
|
|
* data directly.
|
|
*/
|
|
|
|
const mockQueryState = vi.hoisted(() => ({
|
|
data: null as null | object,
|
|
isLoading: false,
|
|
isError: false,
|
|
}));
|
|
|
|
vi.mock("@tanstack/react-query", () => ({
|
|
useQuery: () => ({
|
|
data: mockQueryState.data,
|
|
isLoading: mockQueryState.isLoading,
|
|
isError: mockQueryState.isError,
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/lib/i18n/context", async (orig) => {
|
|
const actual = (await orig()) as Record<string, unknown>;
|
|
return actual;
|
|
});
|
|
|
|
import { AiQualitySection } from "../ai-quality-section";
|
|
|
|
function render(node: React.ReactElement) {
|
|
return renderToStaticMarkup(
|
|
<I18nProvider initialLocale="en">{node}</I18nProvider>,
|
|
);
|
|
}
|
|
|
|
describe("<AiQualitySection>", () => {
|
|
it("renders the loading state when query.isLoading", () => {
|
|
mockQueryState.data = null;
|
|
mockQueryState.isLoading = true;
|
|
mockQueryState.isError = false;
|
|
const html = render(<AiQualitySection />);
|
|
expect(html).toContain("Loading feedback summary");
|
|
});
|
|
|
|
it("renders the empty state when summary is null", () => {
|
|
mockQueryState.data = null;
|
|
mockQueryState.isLoading = false;
|
|
mockQueryState.isError = false;
|
|
const html = render(<AiQualitySection />);
|
|
// v1.4.33 IW7 — admin section renamed from "AI Quality" to
|
|
// "Insights Quality" per the Marc-Voice rule.
|
|
expect(html).toContain("Insights Quality");
|
|
expect(html).toContain("No feedback yet");
|
|
});
|
|
|
|
it("renders the empty state when buckets[] is empty", () => {
|
|
mockQueryState.data = {
|
|
generatedAt: "2026-05-09T04:00:00.000Z",
|
|
windowDays: 30,
|
|
buckets: [],
|
|
};
|
|
mockQueryState.isLoading = false;
|
|
mockQueryState.isError = false;
|
|
const html = render(<AiQualitySection />);
|
|
expect(html).toContain("No feedback yet");
|
|
});
|
|
|
|
it("renders one row per bucket with helpful-rate %", () => {
|
|
mockQueryState.data = {
|
|
generatedAt: "2026-05-09T04:00:00.000Z",
|
|
windowDays: 30,
|
|
buckets: [
|
|
{
|
|
severity: "important",
|
|
metricSourceType: "bloodPressure",
|
|
providerType: "codex",
|
|
promptVersion: "4.16.0",
|
|
helpful: 8,
|
|
notHelpful: 2,
|
|
total: 10,
|
|
helpfulRate: 0.8,
|
|
},
|
|
{
|
|
severity: "info",
|
|
metricSourceType: "weight",
|
|
providerType: "openai",
|
|
promptVersion: "4.16.0",
|
|
helpful: 1,
|
|
notHelpful: 4,
|
|
total: 5,
|
|
helpfulRate: 0.2,
|
|
},
|
|
],
|
|
};
|
|
mockQueryState.isLoading = false;
|
|
mockQueryState.isError = false;
|
|
const html = render(<AiQualitySection />);
|
|
expect(html).toContain("important");
|
|
expect(html).toContain("bloodPressure");
|
|
expect(html).toContain("codex");
|
|
expect(html).toContain("80%");
|
|
expect(html).toContain("20%");
|
|
// Two `data-slot="ai-quality-bucket"` rows.
|
|
const matches = html.match(/data-slot="ai-quality-bucket"/g);
|
|
expect(matches?.length).toBe(2);
|
|
});
|
|
|
|
it("renders a load-error banner when query.isError", () => {
|
|
mockQueryState.data = null;
|
|
mockQueryState.isLoading = false;
|
|
mockQueryState.isError = true;
|
|
const html = render(<AiQualitySection />);
|
|
expect(html).toContain("Failed to load Insights quality summary");
|
|
});
|
|
});
|