mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
Replace the fixed three-beat assessment skeleton with a menu of moves the model orders freely, and reframe the daily briefing from per-signal enumeration to a verdict-first read that weaves the few salient signals into one associative story. Score cards now name both the helped and the hurt contributors, translate the band into a forward read for the day, and close on one grounded nudge; the deterministic fallback gains the same band-meaning sentence so provider-less and demo accounts read as well. Add a hash-seeded opener-archetype rotation (deterministic, testable) so consecutive cards and days no longer open the same way, a day-rotated sampling seed for the period narrative in place of the fixed reference seed, and a shared paragraph formatting contract. Add a score-number grounding gate and extend the briefing grounding allow-set to the glucose, labs, preventive-care and workout aggregate blocks so real figures can be cited without opening the gate. Gate the daily score warm on an input fingerprint so a quiet day skips the model call.
147 lines
4.8 KiB
TypeScript
147 lines
4.8 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
aiInsightResponseSchema,
|
|
trendAnnotationsSchema,
|
|
type AIInsightResponse,
|
|
} from "../schema";
|
|
import {
|
|
PROMPT_VERSION,
|
|
getStrictInsightsSystemPrompt,
|
|
} from "../prompts/insight-generator";
|
|
|
|
/**
|
|
* v1.4.20 phase B3 — Trend Annotations schema + prompt validation.
|
|
*
|
|
* Acceptance covered here:
|
|
* 1. PROMPT_VERSION matches `^4\.20\.\d+$` and is `4.20.1` (B3 bump).
|
|
* 2. trendAnnotationsSchema accepts any combination of bp/weight/mood
|
|
* strings, each capped at 200 chars; empty strings rejected.
|
|
* 3. aiInsightResponseSchema treats trendAnnotations as nullable +
|
|
* optional so legacy 4.20.0 caches round-trip.
|
|
* 4. EN + DE prompts both contain GROUND RULE 9 (trendAnnotations)
|
|
* with the conservative-phrasing + ≤200 char constraint.
|
|
*/
|
|
|
|
const baseResponse: AIInsightResponse = {
|
|
summary: "Things are trending well this week.",
|
|
recommendations: [],
|
|
citations: [],
|
|
warnings: [],
|
|
};
|
|
|
|
describe("PROMPT_VERSION (B3 bump)", () => {
|
|
it("stays on the 4.x / 5.x train", () => {
|
|
// v1.22 (W6) bumped to 5.0.0 for the verdict-first briefing rewrite.
|
|
expect(PROMPT_VERSION).toMatch(/^[45]\.\d+\.\d+$/);
|
|
});
|
|
|
|
it("is bumped past 4.20.0 to signal the trendAnnotations format change", () => {
|
|
expect(PROMPT_VERSION).not.toBe("4.20.0");
|
|
});
|
|
});
|
|
|
|
describe("trendAnnotationsSchema", () => {
|
|
it("accepts an empty object — every field is independently optional", () => {
|
|
expect(trendAnnotationsSchema.safeParse({}).success).toBe(true);
|
|
});
|
|
|
|
it("accepts a single metric annotation", () => {
|
|
expect(
|
|
trendAnnotationsSchema.safeParse({ bp: "Systolic trending down." })
|
|
.success,
|
|
).toBe(true);
|
|
});
|
|
|
|
it("accepts all three metric annotations", () => {
|
|
const input = {
|
|
bp: "Systolic settling into target — a pattern worth watching.",
|
|
weight: "Weight down 1.4 kg over 30 days, linear rate.",
|
|
mood: "Mood stable, scoring 4 of 5 on most days this month.",
|
|
};
|
|
expect(trendAnnotationsSchema.safeParse(input).success).toBe(true);
|
|
});
|
|
|
|
it("rejects an annotation > 200 chars", () => {
|
|
const tooLong = "x".repeat(201);
|
|
expect(trendAnnotationsSchema.safeParse({ bp: tooLong }).success).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it("accepts an annotation at the 200-char boundary", () => {
|
|
const boundary = "x".repeat(200);
|
|
expect(trendAnnotationsSchema.safeParse({ bp: boundary }).success).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("rejects an empty string for any metric", () => {
|
|
expect(trendAnnotationsSchema.safeParse({ bp: "" }).success).toBe(false);
|
|
expect(trendAnnotationsSchema.safeParse({ weight: "" }).success).toBe(
|
|
false,
|
|
);
|
|
expect(trendAnnotationsSchema.safeParse({ mood: "" }).success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("aiInsightResponseSchema — trendAnnotations integration", () => {
|
|
it("legacy payload (no trendAnnotations field) still parses", () => {
|
|
expect(aiInsightResponseSchema.safeParse(baseResponse).success).toBe(true);
|
|
});
|
|
|
|
it("payload with trendAnnotations=null still parses", () => {
|
|
const result = aiInsightResponseSchema.safeParse({
|
|
...baseResponse,
|
|
trendAnnotations: null,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it("payload with a partial trendAnnotations parses round-trip", () => {
|
|
const result = aiInsightResponseSchema.safeParse({
|
|
...baseResponse,
|
|
trendAnnotations: { bp: "Systolic trending down." },
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.trendAnnotations?.bp).toContain("trending down");
|
|
}
|
|
});
|
|
|
|
it("payload with a > 200 char annotation is rejected", () => {
|
|
const tooLong = "x".repeat(201);
|
|
const result = aiInsightResponseSchema.safeParse({
|
|
...baseResponse,
|
|
trendAnnotations: { weight: tooLong },
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("system prompt — GROUND RULE 9 (trendAnnotations)", () => {
|
|
it("English prompt declares the trendAnnotations rule", () => {
|
|
const prompt = getStrictInsightsSystemPrompt("en");
|
|
expect(prompt).toContain("trendAnnotations");
|
|
expect(prompt).toMatch(/200 characters/);
|
|
expect(prompt).toMatch(/observational/i);
|
|
expect(prompt).toMatch(/causal/i);
|
|
});
|
|
|
|
it("German prompt declares the trendAnnotations rule", () => {
|
|
const prompt = getStrictInsightsSystemPrompt("de");
|
|
expect(prompt).toContain("trendAnnotations");
|
|
expect(prompt).toMatch(/200 Zeichen/);
|
|
expect(prompt).toMatch(/beobachtend/i);
|
|
expect(prompt).toMatch(/kausal/i);
|
|
});
|
|
|
|
it("both prompts spell the JSON shape with bp / weight / mood keys", () => {
|
|
for (const locale of ["en", "de"] as const) {
|
|
const prompt = getStrictInsightsSystemPrompt(locale);
|
|
expect(prompt).toMatch(/"bp":/);
|
|
expect(prompt).toMatch(/"weight":/);
|
|
expect(prompt).toMatch(/"mood":/);
|
|
}
|
|
});
|
|
});
|