mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
v1.4.16 phase B5e: ship the POST endpoint that backs the per-rec
thumbs-up / thumbs-down UI. Body is the four rec-attributes the
client just rendered (id, text-snapshot, severity, metric-source +
time-range) plus the boolean verdict; server-fills providerType +
promptVersion via `resolveFeedbackAttribution()` so the client cannot
poison the daily aggregator's slice by claiming a thumbs-down was
attributed to a rival provider.
Idempotent via the existing `withIdempotency()` wrapper — a retry
with the same Idempotency-Key replays the cached response with
`X-Idempotent-Replay: true`. The dedup unique key on the model
(userId, recommendationId, recommendationText) returns 409 when the
same rec-text is re-rated — that's by design, the aggregator wants
per-text signal not per-id signal so a regeneration that rewrites
the same id with new text re-rates cleanly.
The attribution helper prefers `chainProviderType` over `providerType`
in the latest insights.generate audit row so when the B5b multi-
provider chain falls through to a secondary, the feedback row
reflects the working provider not the configured one. Falls back to
"unknown" + the current PROMPT_VERSION constant when no audit row
exists yet — the aggregator buckets that separately.
Audit row: action `insights.recommendation.feedback`, details carry
the feedback id, severity, helpful, providerType, promptVersion.
Tests:
- 7 unit tests for the Zod request schema (valid path, severity
poisoning, time-range poisoning, empty fields, stringified bool,
extra-field stripping)
- 5 unit tests for `pickProviderType()` priority + fallback
- 4 integration tests: 201 happy path with provider attribution,
409 dedup, 422 validation, fallback to unknown when no audit row
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
73 lines
2.4 KiB
TypeScript
73 lines
2.4 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { recommendationFeedbackRequestSchema } from "../recommendation-feedback";
|
|
|
|
describe("recommendationFeedbackRequestSchema", () => {
|
|
const baseValid = {
|
|
recommendationId: "rec-1",
|
|
recommendationText: "Discuss home BP log with your physician.",
|
|
recommendationSeverity: "important" as const,
|
|
metricSourceType: "bloodPressure",
|
|
metricSourceTimeRange: "last7days" as const,
|
|
helpful: true,
|
|
};
|
|
|
|
it("accepts a valid payload", () => {
|
|
const parsed = recommendationFeedbackRequestSchema.safeParse(baseValid);
|
|
expect(parsed.success).toBe(true);
|
|
});
|
|
|
|
it("rejects an unknown severity (defence against poisoning the aggregator)", () => {
|
|
const parsed = recommendationFeedbackRequestSchema.safeParse({
|
|
...baseValid,
|
|
recommendationSeverity: "wat",
|
|
});
|
|
expect(parsed.success).toBe(false);
|
|
});
|
|
|
|
it("rejects an unknown time-range value", () => {
|
|
const parsed = recommendationFeedbackRequestSchema.safeParse({
|
|
...baseValid,
|
|
metricSourceTimeRange: "lastFortnight",
|
|
});
|
|
expect(parsed.success).toBe(false);
|
|
});
|
|
|
|
it("rejects an empty recommendation id", () => {
|
|
const parsed = recommendationFeedbackRequestSchema.safeParse({
|
|
...baseValid,
|
|
recommendationId: "",
|
|
});
|
|
expect(parsed.success).toBe(false);
|
|
});
|
|
|
|
it("rejects an empty recommendation text", () => {
|
|
const parsed = recommendationFeedbackRequestSchema.safeParse({
|
|
...baseValid,
|
|
recommendationText: "",
|
|
});
|
|
expect(parsed.success).toBe(false);
|
|
});
|
|
|
|
it("rejects a non-boolean helpful field (catches stringified-boolean clients)", () => {
|
|
const parsed = recommendationFeedbackRequestSchema.safeParse({
|
|
...baseValid,
|
|
helpful: "true",
|
|
});
|
|
expect(parsed.success).toBe(false);
|
|
});
|
|
|
|
it("rejects extra fields when stripping isn't desired (helpful is the only thumbs verb)", () => {
|
|
// Zod default behaviour: passthrough is OFF; unknown keys are
|
|
// dropped. We don't use .strict() so a future field can be added
|
|
// additively, but the parsed value must not carry the extra field.
|
|
const parsed = recommendationFeedbackRequestSchema.safeParse({
|
|
...baseValid,
|
|
providerType: "codex",
|
|
});
|
|
expect(parsed.success).toBe(true);
|
|
if (parsed.success) {
|
|
expect("providerType" in parsed.data).toBe(false);
|
|
}
|
|
});
|
|
});
|