diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G01-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G01-anonymous.json.snap new file mode 100644 index 000000000..694a6f447 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G01-anonymous.json.snap @@ -0,0 +1,32 @@ +{ + "status": 401, + "body": { + "data": null, + "error": "Not authenticated", + "meta": { + "errorCode": "auth.missing", + "requestId": "" + } + }, + "events": [ + { + "status": 401, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "HttpError", + "message": "Not authenticated" + } + } + ], + "effects": [], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G02-opted-out.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G02-opted-out.json.snap new file mode 100644 index 000000000..b234a3fe3 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G02-opted-out.json.snap @@ -0,0 +1,35 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "This AI feature is turned off in your settings", + "meta": { + "capability": "briefing", + "reason": "user_disabled", + "errorCode": "module.disabled", + "module": "insights", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "AiUnavailableError", + "message": "This AI feature is turned off in your settings" + } + } + ], + "effects": [], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G03-empty.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G03-empty.json.snap new file mode 100644 index 000000000..0ef9af169 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G03-empty.json.snap @@ -0,0 +1,52 @@ +{ + "status": 200, + "body": { + "data": { + "insights": null, + "cached": false, + "legacyPayload": false, + "revalidating": true, + "hasProvider": true, + "generationFailed": false, + "generationFailureClass": null, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": false, + "hasProvider": true, + "languageMismatch": false, + "readerLocale": "en", + "revalidating": true + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueForceWarm", + "args": [ + { + "userId": "", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G04-fresh.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G04-fresh.json.snap new file mode 100644 index 000000000..0798c38b0 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G04-fresh.json.snap @@ -0,0 +1,92 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+11:00:00" + }, + "cached": true, + "cachedAt": "anchor+0d+11:00:00", + "legacyPayload": false, + "revalidating": false, + "hasProvider": true, + "generationFailed": false, + "generationFailureClass": null, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": true, + "hasProvider": true, + "legacyPayload": false, + "revalidating": false + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "insightsCachedAt": "anchor+0d+11:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+11:00:00" + }, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G05-stale.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G05-stale.json.snap new file mode 100644 index 000000000..fe446ac21 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G05-stale.json.snap @@ -0,0 +1,90 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor-1d+16:00:00" + }, + "cached": true, + "cachedAt": "anchor+0d+10:00:00", + "legacyPayload": false, + "revalidating": true, + "hasProvider": true, + "generationFailed": false, + "generationFailureClass": null, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": true, + "hasProvider": true, + "legacyPayload": false, + "revalidating": true + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueForceWarm", + "args": [ + { + "userId": "", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+10:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor-1d+16:00:00" + }, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G06-other-language.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G06-other-language.json.snap new file mode 100644 index 000000000..5e2b774d1 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G06-other-language.json.snap @@ -0,0 +1,77 @@ +{ + "status": 200, + "body": { + "data": { + "insights": null, + "cached": false, + "legacyPayload": false, + "revalidating": true, + "hasProvider": true, + "generationFailed": false, + "generationFailureClass": null, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": false, + "hasProvider": true, + "languageMismatch": true, + "readerLocale": "en", + "revalidating": true + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueForceWarm", + "args": [ + { + "userId": "", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+11:00:00", + "insightsCachedLocale": "de", + "cachedPayload": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+11:00:00" + }, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G07-unparseable.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G07-unparseable.json.snap new file mode 100644 index 000000000..c09a6729d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G07-unparseable.json.snap @@ -0,0 +1,52 @@ +{ + "status": 200, + "body": { + "data": { + "insights": null, + "cached": false, + "legacyPayload": false, + "revalidating": true, + "hasProvider": true, + "generationFailed": false, + "generationFailureClass": null, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": false, + "hasProvider": true, + "languageMismatch": false, + "readerLocale": "en", + "revalidating": true + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueForceWarm", + "args": [ + { + "userId": "", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+11:00:00", + "insightsCachedLocale": null, + "cachedPayload": "{not json", + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G08-legacy.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G08-legacy.json.snap new file mode 100644 index 000000000..96cf8ba74 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G08-legacy.json.snap @@ -0,0 +1,62 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Legacy", + "recommendations": [ + "Drink water." + ] + }, + "cached": true, + "cachedAt": "anchor+0d+11:00:00", + "legacyPayload": true, + "revalidating": true, + "hasProvider": true, + "generationFailed": false, + "generationFailureClass": null, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": true, + "hasProvider": true, + "legacyPayload": true, + "revalidating": true + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueForceWarm", + "args": [ + { + "userId": "", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+11:00:00", + "insightsCachedLocale": null, + "cachedPayload": { + "summary": "Legacy", + "recommendations": [ + "Drink water." + ] + }, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G09-held-after-failure.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G09-held-after-failure.json.snap new file mode 100644 index 000000000..bb82dddbc --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G09-held-after-failure.json.snap @@ -0,0 +1,102 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+09:00:00" + }, + "cached": true, + "cachedAt": "anchor+0d+09:00:00", + "legacyPayload": false, + "revalidating": false, + "hasProvider": true, + "generationFailed": true, + "generationFailureClass": "rate-limit", + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": true, + "hasProvider": true, + "legacyPayload": false, + "revalidating": false + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "insightsCachedAt": "anchor+0d+09:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+09:00:00" + }, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "reason": "all-providers-failed", + "httpStatus": 429, + "failureKind": null + }, + "ipAddress": null + } + ], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G10-failed-empty.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G10-failed-empty.json.snap new file mode 100644 index 000000000..446fc6618 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G10-failed-empty.json.snap @@ -0,0 +1,71 @@ +{ + "status": 200, + "body": { + "data": { + "insights": null, + "cached": false, + "legacyPayload": false, + "revalidating": true, + "hasProvider": true, + "generationFailed": true, + "generationFailureClass": "format", + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": false, + "hasProvider": true, + "languageMismatch": false, + "readerLocale": "en", + "revalidating": true + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "enqueueForceWarm", + "args": [ + { + "userId": "", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "reason": "provider-error", + "httpStatus": null, + "failureKind": null + }, + "ipAddress": null + }, + { + "action": "insights.briefing-failure", + "details": { + "reason": "invalid-json", + "httpStatus": null, + "failureKind": null + }, + "ipAddress": null + } + ], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.G11-withheld.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.G11-withheld.json.snap new file mode 100644 index 000000000..82ae9ed31 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.G11-withheld.json.snap @@ -0,0 +1,78 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+10:00:00" + }, + "cached": true, + "cachedAt": "anchor+0d+10:00:00", + "legacyPayload": false, + "revalidating": false, + "hasProvider": true, + "generationFailed": false, + "generationFailureClass": null, + "briefingOmittedReason": "ungrounded" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate.read" + }, + "meta": { + "cached": true, + "hasProvider": true, + "legacyPayload": false, + "revalidating": false + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "insightsCachedAt": "anchor+0d+10:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+10:00:00" + }, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "reason": "briefing-ungrounded", + "httpStatus": null, + "failureKind": null + }, + "ipAddress": null + } + ], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P01-anonymous.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P01-anonymous.json.snap new file mode 100644 index 000000000..c8614fbac --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P01-anonymous.json.snap @@ -0,0 +1,55 @@ +{ + "status": 401, + "body": { + "data": null, + "error": "Not authenticated", + "meta": { + "errorCode": "auth.missing", + "requestId": "" + } + }, + "events": [ + { + "status": 401, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "HttpError", + "message": "Not authenticated" + } + } + ], + "effects": [], + "stored": { + "insightsCachedAt": "anchor+0d+10:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+10:00:00" + }, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "reason": "briefing-ungrounded", + "httpStatus": null, + "failureKind": null + }, + "ipAddress": null + } + ], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P02-opted-out.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P02-opted-out.json.snap new file mode 100644 index 000000000..b234a3fe3 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P02-opted-out.json.snap @@ -0,0 +1,35 @@ +{ + "status": 403, + "body": { + "data": null, + "error": "This AI feature is turned off in your settings", + "meta": { + "capability": "briefing", + "reason": "user_disabled", + "errorCode": "module.disabled", + "module": "insights", + "requestId": "" + } + }, + "events": [ + { + "status": 403, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "AiUnavailableError", + "message": "This AI feature is turned off in your settings" + } + } + ], + "effects": [], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P03-body-too-large.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P03-body-too-large.json.snap new file mode 100644 index 000000000..83788011a --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P03-body-too-large.json.snap @@ -0,0 +1,29 @@ +{ + "status": 413, + "body": { + "data": null, + "error": "Request body exceeds 16384 bytes", + "meta": { + "errorCode": "http.payload_too_large", + "requestId": "" + } + }, + "events": [ + { + "status": 413, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P04-cache-hit.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P04-cache-hit.json.snap new file mode 100644 index 000000000..13f227223 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P04-cache-hit.json.snap @@ -0,0 +1,86 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+11:00:00" + }, + "cached": true, + "cachedAt": "anchor+0d+11:00:00", + "legacyPayload": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "briefingless": false, + "cached": true, + "legacyPayload": false + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "insightsCachedAt": "anchor+0d+11:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+11:00:00" + }, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P05-yesterday-regenerated.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P05-yesterday-regenerated.json.snap new file mode 100644 index 000000000..5e7bd73dd --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P05-yesterday-regenerated.json.snap @@ -0,0 +1,276 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "locale": "en", + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P06-other-language-regenerated.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P06-other-language-regenerated.json.snap new file mode 100644 index 000000000..57099b957 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P06-other-language-regenerated.json.snap @@ -0,0 +1,275 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P07-quota-cached-fallback.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P07-quota-cached-fallback.json.snap new file mode 100644 index 000000000..0a33c3fee --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P07-quota-cached-fallback.json.snap @@ -0,0 +1,62 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+11:00:00" + }, + "cached": true, + "cachedAt": "anchor+0d+11:00:00", + "legacyPayload": false + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "cached": true, + "locale": "en", + "rate_limited_fallback": true + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "insightsCachedAt": "anchor+0d+11:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Cached summary.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+11:00:00" + }, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P08-quota-refused.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P08-quota-refused.json.snap new file mode 100644 index 000000000..ba22d1ef5 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P08-quota-refused.json.snap @@ -0,0 +1,29 @@ +{ + "status": 429, + "body": { + "data": null, + "error": "Maximum 10 insight generations per hour.", + "meta": { + "errorCode": "http.rate_limited", + "requestId": "" + } + }, + "events": [ + { + "status": 429, + "action": null, + "meta": null, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P09-unreadable-key.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P09-unreadable-key.json.snap new file mode 100644 index 000000000..a0924a58b --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P09-unreadable-key.json.snap @@ -0,0 +1,32 @@ +{ + "status": 500, + "body": { + "data": null, + "error": "Interner Serverfehler", + "meta": { + "errorCode": "http.internal", + "requestId": "" + } + }, + "events": [ + { + "status": 500, + "action": null, + "meta": null, + "warnings": null, + "error": { + "type": "TypeError", + "message": "Invalid authentication tag length: 0" + } + } + ], + "effects": [], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P10-generated.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P10-generated.json.snap new file mode 100644 index 000000000..3ba9663b0 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P10-generated.json.snap @@ -0,0 +1,275 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 2100 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 2100, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 2100, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P11-strict-reply.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P11-strict-reply.json.snap new file mode 100644 index 000000000..646d8ce72 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P11-strict-reply.json.snap @@ -0,0 +1,251 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "disclaimer": "Consult your doctor.", + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "disclaimer": "Consult your doctor.", + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P12-generated-de-short.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P12-generated-de-short.json.snap new file mode 100644 index 000000000..bc8dbcbf4 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P12-generated-de-short.json.snap @@ -0,0 +1,271 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "off", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_usage_level": "frugal", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000 + }, + "promptChars": 39012, + "promptSha256": "9b74ece939faf8f6643d894cc718bd3e0925133e2c6c12d98dfb3f9d68c82d61" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "de" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "de" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "de", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "c98e2d8e9f112318293af870eaa7c15d342f7f37a12047b0fd4e2a7f66d84676", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P13-downgraded.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P13-downgraded.json.snap new file mode 100644 index 000000000..1e52a6832 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P13-downgraded.json.snap @@ -0,0 +1,278 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "insights_features_downgraded": true, + "insights_features_hard_downgraded": false, + "insights_features_oversize_bytes": 6100000, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34783, + "promptSha256": "2aeca4de2fc6397815465ac4de27a2267fdfc69cd4a607b2e19d7b06502bc040" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "raw", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P14-hard-downgraded.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P14-hard-downgraded.json.snap new file mode 100644 index 000000000..77bdb63ba --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P14-hard-downgraded.json.snap @@ -0,0 +1,278 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "insights_features_downgraded": true, + "insights_features_hard_downgraded": true, + "insights_features_oversize_bytes": 5400000, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34193, + "promptSha256": "f11bae5de41f1344113977970c832df38427bcdf4b24a6f7fb8b05cef899c5af" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "11a4fb0ae0188e5a95cbe69124afaccdb326b76ea8e8ad73affbc819255a01cc", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "raw", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P15-too-large.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P15-too-large.json.snap new file mode 100644 index 000000000..6f9e66788 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P15-too-large.json.snap @@ -0,0 +1,32 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "Your data exceeds the AI payload size limit. Reduce the data window or exclude optional blocks in Settings > AI.", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": null, + "meta": { + "insights_payload_oversize_bytes": 5200000, + "insights_payload_too_large": true + }, + "warnings": null, + "error": null + } + ], + "effects": [], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P16-budget-exceeded.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P16-budget-exceeded.json.snap new file mode 100644 index 000000000..63e94ff84 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P16-budget-exceeded.json.snap @@ -0,0 +1,58 @@ +{ + "status": 429, + "body": { + "data": null, + "error": "Daily AI token budget reached. Insights will be available again tomorrow.", + "meta": { + "errorCode": "insights.generate.budgetExceeded", + "requestId": "" + } + }, + "events": [ + { + "status": 429, + "action": { + "name": "insights.briefing.budget_exceeded" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "cap": 1600000, + "include_raw": false, + "limit": "owner-cap", + "operatorAfter": 0, + "owner": "user", + "since_days": 400, + "stage": "generate", + "surface": "job", + "totalAfter": 5000000 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 5000000, + "operatorTokens": 0, + "messageCount": 0 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P17-chain-auth.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P17-chain-auth.json.snap new file mode 100644 index 000000000..2b6a467d1 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P17-chain-auth.json.snap @@ -0,0 +1,103 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "AI provider rejected the request. Check your API key in Settings > AI", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_chain_attempts": 2, + "insights_chain_first_provider": "anthropic", + "insights_chain_first_status": 401, + "insights_chain_outcome": "all-failed", + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "all-providers-failed", + "locale": "en", + "httpStatus": 401, + "failureKind": null + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "all-providers-failed", + "httpStatus": 401, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P18-chain-rate-limited.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P18-chain-rate-limited.json.snap new file mode 100644 index 000000000..96934ea41 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P18-chain-rate-limited.json.snap @@ -0,0 +1,103 @@ +{ + "status": 429, + "body": { + "data": null, + "error": "AI provider rate-limited the request", + "meta": { + "errorCode": "http.rate_limited", + "requestId": "" + } + }, + "events": [ + { + "status": 429, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_chain_attempts": 1, + "insights_chain_first_provider": "anthropic", + "insights_chain_first_status": 429, + "insights_chain_outcome": "all-failed", + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "all-providers-failed", + "locale": "en", + "httpStatus": 429, + "failureKind": null + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "all-providers-failed", + "httpStatus": 429, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P19-chain-5xx.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P19-chain-5xx.json.snap new file mode 100644 index 000000000..3a50c4531 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P19-chain-5xx.json.snap @@ -0,0 +1,103 @@ +{ + "status": 503, + "body": { + "data": null, + "error": "AI provider temporarily unavailable, try again in a moment", + "meta": { + "errorCode": "http.unavailable", + "requestId": "" + } + }, + "events": [ + { + "status": 503, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_chain_attempts": 2, + "insights_chain_first_provider": "anthropic", + "insights_chain_first_status": 401, + "insights_chain_outcome": "all-failed", + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "all-providers-failed", + "locale": "en", + "httpStatus": 401, + "failureKind": null + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "all-providers-failed", + "httpStatus": 401, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P20-chain-transport.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P20-chain-transport.json.snap new file mode 100644 index 000000000..500f35359 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P20-chain-transport.json.snap @@ -0,0 +1,103 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "AI provider connection failed. Check your AI settings", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_chain_attempts": 2, + "insights_chain_first_provider": "anthropic", + "insights_chain_first_status": null, + "insights_chain_outcome": "all-failed", + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "all-providers-failed", + "locale": "en", + "httpStatus": null, + "failureKind": null + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "all-providers-failed", + "httpStatus": null, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P21-chain-mixed.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P21-chain-mixed.json.snap new file mode 100644 index 000000000..a249fe477 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P21-chain-mixed.json.snap @@ -0,0 +1,103 @@ +{ + "status": 503, + "body": { + "data": null, + "error": "AI provider temporarily unavailable, try again in a moment", + "meta": { + "errorCode": "http.unavailable", + "requestId": "" + } + }, + "events": [ + { + "status": 503, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_chain_attempts": 2, + "insights_chain_first_provider": "anthropic", + "insights_chain_first_status": 404, + "insights_chain_outcome": "all-failed", + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "all-providers-failed", + "locale": "en", + "httpStatus": 404, + "failureKind": null + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "all-providers-failed", + "httpStatus": 404, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-400.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-400.json.snap new file mode 100644 index 000000000..8bd4eb339 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-400.json.snap @@ -0,0 +1,102 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "AI provider connection failed. Check your AI settings", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_provider_body_excerpt": "{\"error\":\"status 400\"}", + "insights_provider_error": "upstream answered 400", + "insights_provider_status": 400, + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "provider-error", + "locale": "en", + "httpStatus": 400, + "failureKind": "http" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "provider-error", + "httpStatus": 400, + "failureKind": "http", + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-401.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-401.json.snap new file mode 100644 index 000000000..6175d24a4 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-401.json.snap @@ -0,0 +1,102 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "AI provider rejected the request. Check your API key in Settings > AI", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_provider_body_excerpt": "{\"error\":\"status 401\"}", + "insights_provider_error": "upstream answered 401", + "insights_provider_status": 401, + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "provider-error", + "locale": "en", + "httpStatus": 401, + "failureKind": "http" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "provider-error", + "httpStatus": 401, + "failureKind": "http", + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-429.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-429.json.snap new file mode 100644 index 000000000..c220e2f05 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-429.json.snap @@ -0,0 +1,102 @@ +{ + "status": 429, + "body": { + "data": null, + "error": "AI provider rate-limited the request", + "meta": { + "errorCode": "http.rate_limited", + "requestId": "" + } + }, + "events": [ + { + "status": 429, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_provider_body_excerpt": "{\"error\":\"status 429\"}", + "insights_provider_error": "upstream answered 429", + "insights_provider_status": 429, + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "provider-error", + "locale": "en", + "httpStatus": 429, + "failureKind": "http" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "provider-error", + "httpStatus": 429, + "failureKind": "http", + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-500.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-500.json.snap new file mode 100644 index 000000000..d8648f6fa --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P22-provider-500.json.snap @@ -0,0 +1,102 @@ +{ + "status": 503, + "body": { + "data": null, + "error": "AI provider temporarily unavailable, try again in a moment", + "meta": { + "errorCode": "http.unavailable", + "requestId": "" + } + }, + "events": [ + { + "status": 503, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_provider_body_excerpt": "{\"error\":\"status 500\"}", + "insights_provider_error": "upstream answered 500", + "insights_provider_status": 500, + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "provider-error", + "locale": "en", + "httpStatus": 500, + "failureKind": "http" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "provider-error", + "httpStatus": 500, + "failureKind": "http", + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 0, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P23-invalid-json.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P23-invalid-json.json.snap new file mode 100644 index 000000000..a5781ea92 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P23-invalid-json.json.snap @@ -0,0 +1,97 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "AI response was not valid JSON", + "meta": { + "errorCode": "http.invalid", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "invalid-json", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "invalid-json", + "httpStatus": null, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P24-truncated.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P24-truncated.json.snap new file mode 100644 index 000000000..77c805581 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P24-truncated.json.snap @@ -0,0 +1,98 @@ +{ + "status": 422, + "body": { + "data": null, + "error": "AI response was cut off before the JSON completed. Raise the token limit (INSIGHTS_MAX_TOKENS)", + "meta": { + "errorCode": "ai_response_truncated", + "requestId": "" + } + }, + "events": [ + { + "status": 422, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "insights_response_truncated": true, + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "invalid-json", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "invalid-json", + "httpStatus": null, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P25-grounding-retry-kept.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P25-grounding-retry-kept.json.snap new file mode 100644 index 000000000..d02342f18 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P25-grounding-retry-kept.json.snap @@ -0,0 +1,295 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 900, + "ungroundedCount": 1 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000 + }, + "promptChars": 35302, + "promptSha256": "2b3978fcf41077f56835864a67264bea403c0dc31a6bd211ecc469afa16d8a7d" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Your readings held steady this week, with no surprises.", + "signalsOfDay": [ + { + "sourceMetric": "weight", + "tone": "info", + "headline": "Weight is steady", + "nudge": "Keep weighing in at the same time each morning.", + "delta": null + } + ], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 900, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 2100, + "operatorTokens": 0, + "messageCount": 2 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P26-grounding-previous.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P26-grounding-previous.json.snap new file mode 100644 index 000000000..39f29a9ce --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P26-grounding-previous.json.snap @@ -0,0 +1,280 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Yesterday's briefing, grounded when it was written.", + "signalsOfDay": [], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": null + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "briefing_fallback": "previous-cached", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200, + "ungroundedCount": 1 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000 + }, + "promptChars": 35302, + "promptSha256": "2b3978fcf41077f56835864a67264bea403c0dc31a6bd211ecc469afa16d8a7d" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": { + "paragraph": "Yesterday's briefing, grounded when it was written.", + "signalsOfDay": [], + "keyFindings": [] + }, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 2400, + "operatorTokens": 0, + "messageCount": 2 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P27-grounding-stripped.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P27-grounding-stripped.json.snap new file mode 100644 index 000000000..6b9b73e81 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P27-grounding-stripped.json.snap @@ -0,0 +1,294 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": "ungrounded" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "briefing_fallback": "stripped", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200, + "ungroundedCount": 1 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000 + }, + "promptChars": 35302, + "promptSha256": "2b3978fcf41077f56835864a67264bea403c0dc31a6bd211ecc469afa16d8a7d" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "briefing-ungrounded", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "briefing-ungrounded", + "httpStatus": null, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + }, + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 2400, + "operatorTokens": 0, + "messageCount": 2 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P28-grounding-retry-unparseable.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P28-grounding-retry-unparseable.json.snap new file mode 100644 index 000000000..6b9b73e81 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P28-grounding-retry-unparseable.json.snap @@ -0,0 +1,294 @@ +{ + "status": 200, + "body": { + "data": { + "insights": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "cached": false, + "legacyPayload": false, + "briefingOmittedReason": "ungrounded" + }, + "error": null + }, + "events": [ + { + "status": 200, + "action": { + "name": "insights.generate" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "briefing_fallback": "stripped", + "cached": false, + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "include_raw": false, + "model": "claude-parity", + "providerType": "anthropic", + "scopes": 13, + "since_days": 400, + "tokensUsed": 1200, + "ungroundedCount": 1 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000 + }, + "promptChars": 35302, + "promptSha256": "2b3978fcf41077f56835864a67264bea403c0dc31a6bd211ecc469afa16d8a7d" + } + ] + }, + { + "effect": "recordBriefingFailure", + "args": [ + { + "userId": "", + "reason": "briefing-ungrounded", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "blood-pressure", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "pulse", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "weight", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "bmi", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "mood", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "medication-compliance", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "general", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:SLEEP_DURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:STEPS", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:BLOOD_GLUCOSE", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:OXYGEN_SATURATION", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:HEART_RATE_VARIABILITY", + "locale": "en" + } + ] + }, + { + "effect": "enqueueStatusGeneration", + "args": [ + { + "userId": "", + "metric": "metric:RESTING_HEART_RATE", + "locale": "en" + } + ] + } + ], + "stored": { + "insightsCachedAt": "anchor+0d+12:00:00", + "insightsCachedLocale": "en", + "cachedPayload": { + "summary": "Your tracked metrics held steady over the last 30 days.", + "classification": "gut", + "findings": [], + "correlations": [], + "recommendations": [], + "dataQuality": { + "coverage": "good", + "gaps": [], + "confidence": "hoch" + }, + "dailyBriefing": null, + "briefingGeneratedAt": "anchor+0d+12:00:00" + }, + "snapshotHash": "4ac15bc271309c63b3b5d1e43a3367f7abdf952d3f4351bb1b22aa364bcb630a", + "audit": [ + { + "action": "insights.briefing-failure", + "details": { + "dateKey": "day+0", + "reason": "briefing-ungrounded", + "httpStatus": null, + "failureKind": null, + "locale": "en", + "triedAt": "anchor+0d+12:00:00" + }, + "ipAddress": null + }, + { + "action": "insights.generate", + "details": { + "privacyMode": "aggregated", + "tokensUsed": 1200, + "providerType": "anthropic", + "chainProviderType": "anthropic", + "fallbackHopCount": 0, + "model": "claude-parity" + }, + "ipAddress": null + } + ], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 2400, + "operatorTokens": 0, + "messageCount": 2 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P29-outbound-screened.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P29-outbound-screened.json.snap new file mode 100644 index 000000000..9509662c4 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P29-outbound-screened.json.snap @@ -0,0 +1,76 @@ +{ + "status": 502, + "body": { + "data": null, + "error": "The generated insight did not pass the safety check and was discarded. Try regenerating.", + "meta": { + "errorCode": "insights.generate.outboundScreened", + "requestId": "" + } + }, + "events": [ + { + "status": 502, + "action": { + "name": "insights.generate.outbound_blocked" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "locale": "en", + "reason": "risk_score", + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P30-profile-scope-changed.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P30-profile-scope-changed.json.snap new file mode 100644 index 000000000..3f34aaa8d --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P30-profile-scope-changed.json.snap @@ -0,0 +1,74 @@ +{ + "status": 409, + "body": { + "data": null, + "error": "Profile scope changed during insight generation", + "meta": { + "errorCode": "insights.generate.profileScopeChanged", + "requestId": "" + } + }, + "events": [ + { + "status": 409, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/__snapshots__/insights-generate.P31-scope-changed.json.snap b/tests/integration/routes/parity/__snapshots__/insights-generate.P31-scope-changed.json.snap new file mode 100644 index 000000000..a48fd39b2 --- /dev/null +++ b/tests/integration/routes/parity/__snapshots__/insights-generate.P31-scope-changed.json.snap @@ -0,0 +1,74 @@ +{ + "status": 409, + "body": { + "data": null, + "error": "Insight scope changed during generation", + "meta": { + "errorCode": "insights.generate.scopeChanged", + "requestId": "" + } + }, + "events": [ + { + "status": 409, + "action": { + "name": "insights.feature_cache.miss" + }, + "meta": { + "ai_reasoning_effort": "medium", + "ai_reasoning_job": "daily_briefing", + "ai_reasoning_source": "job", + "include_raw": false, + "since_days": 400 + }, + "warnings": null, + "error": null + } + ], + "effects": [ + { + "effect": "ensureUserMoodRollupsFresh", + "args": [ + "" + ] + }, + { + "effect": "provider", + "args": [ + { + "surface": "job", + "chain": [ + "anthropic" + ], + "params": { + "temperature": 0.3, + "maxTokens": 2500, + "responseFormat": "json", + "timeoutMs": 180000, + "reasoning": { + "effort": "medium", + "summaries": false + } + }, + "promptChars": 34773, + "promptSha256": "2197bdec8af3ff7034e7b533788ac94d36b64d4edf8d211bbacfac7e2298ad72" + } + ] + } + ], + "stored": { + "insightsCachedAt": null, + "insightsCachedLocale": null, + "cachedPayload": null, + "snapshotHash": null, + "audit": [], + "usage": [ + { + "dateKey": "day+0", + "totalTokens": 1200, + "operatorTokens": 0, + "messageCount": 1 + } + ] + } +} diff --git a/tests/integration/routes/parity/insights-generate.parity.test.ts b/tests/integration/routes/parity/insights-generate.parity.test.ts new file mode 100644 index 000000000..0b2d475e8 --- /dev/null +++ b/tests/integration/routes/parity/insights-generate.parity.test.ts @@ -0,0 +1,1027 @@ +/** + * Parity for `GET` and `POST /api/insights/generate` while the briefing read + * and generation move out of the route into `src/lib/insights/`. + * + * One account with a full record (see `record-fixture.ts`) and its own + * Anthropic key walks through every case the two handlers tell apart. Each + * case starts from a known briefing state (`resetBriefing`): the cached + * payload, its stamp and language, the failure markers, the day's token + * ledger and the hourly generation quota are written for that case alone. + * + * The read: refusals, an empty cache, a fresh, a stale, an other-language, + * an unparseable and a legacy cache, a held failure and a withheld + * (ungrounded) briefing. The generation: the cache short-circuit and its + * bypasses, the body cap, the hourly quota with and without the cached + * fallback, a chain that resolves to nothing, the three rungs of the payload + * downgrade ladder, the day's token ceiling, every provider failure class + * the route maps, invalid and truncated replies, the grounding gate (retry + * kept, previous briefing stood in, stripped, retry unparseable), the + * outbound screen, both scope races, and successes in two languages, one + * with the comparison snapshot, an about-me block and the short form. + * + * The provider is never called: `runRawCompletionWithFallback` is replaced + * by a script each case fills, and every call it receives is recorded (the + * chain, the parameters, and a digest of the prompt). The queue enqueues and + * the failure markers the route does not await are recorded too; the markers + * are also written, and waited for before the snapshot. + * + * The clock is pinned (`Date` only) to noon of the day the test runs, and + * every instant and day key in the snapshot is written relative to the start + * of the day. The record's measurements on the 7th, 30th and 60th day back + * are removed, so a window cut in SQL against the database clock never meets + * a reading on its edge. Two values change from one day to the next by + * design and are normalised here: the opener hint and the first-name line of + * the briefing prompt rotate per user and day, and the stored snapshot hash + * covers dated features, so it is recorded as the hash of the same input + * with its dates written relative to the anchor. + */ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +import { GET, POST } from "@/app/api/insights/generate/route"; +import { encrypt } from "@/lib/crypto"; +import { AllProvidersFailedError } from "@/lib/ai/provider-runner"; +import { + insightsCacheWrite, + openInsightsCachedText, +} from "@/lib/insights/cached-text-cipher"; +import { BRIEFING_GENERATED_AT_KEY } from "@/lib/insights/briefing-generated-at"; +import { settleBackgroundTasks } from "@/lib/logging/background-tasks"; + +import { anonymous, callRoute, type RouteAuth } from "../../../kit/call-route"; +import { makeSession, makeUser } from "../../../kit/factories"; +import { + effectRecorder, + eventFacts, + expectParity, + relativeToAnchor, + stable, + startOfUtcToday, + wideEventRecorder, +} from "../../../kit/parity"; +import { getPrismaClient, truncateAllTables } from "../../setup"; +import { parityId, seedParityRecord } from "./record-fixture"; + +/** What the provider stand-in answers, one entry per call, in order. */ +type ScriptEntry = + | { + content: string; + finishReason?: string; + tokensUsed?: number; + /** Runs inside the call, before the answer: a write racing the request. */ + during?: () => Promise; + } + | { error: () => Error }; + +const seams = vi.hoisted(() => ({ + script: [] as ScriptEntry[], + /** Feature reads that fail as oversized before computing, in order. */ + oversize: [] as number[], + /** Real snapshot hash → the same input's hash with anchored dates. */ + hashes: new Map(), + pending: [] as Promise[], + /** A provider call no case scripted: the case is wrong, not the route. */ + unscripted: 0, +})); + +vi.mock("@/lib/logging/transports", async (importOriginal) => { + const actual = + await importOriginal(); + const { wideEventRecorder: recorder } = await import("../../../kit/parity"); + return { + ...actual, + emitIfSampled: (event: (typeof recorder.events)[number]) => { + recorder.events.push(event); + }, + }; +}); + +vi.mock("@/lib/rollups/measurement-rollups", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + ensureUserRollupsFresh: async (...args: unknown[]) => { + effectRecorder.record("ensureUserRollupsFresh", args); + }, + }; +}); + +vi.mock("@/lib/rollups/mood-rollups", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + ensureUserMoodRollupsFresh: async (...args: unknown[]) => { + effectRecorder.record("ensureUserMoodRollupsFresh", args); + }, + }; +}); + +vi.mock("@/lib/ai/provider-runner", async (importOriginal) => { + const actual = + await importOriginal(); + const { effectRecorder, relativeToAnchor, startOfUtcToday } = + await import("../../../kit/parity"); + return { + ...actual, + runRawCompletionWithFallback: async ( + args: Parameters[0], + ) => { + const { messages, system, signal: _signal, ...rest } = args.params; + const prompt = String( + relativeToAnchor( + `${system}\n---\n${messages + .map((m) => + typeof m.content === "string" + ? m.content + : JSON.stringify(m.content), + ) + .join("\n---\n")}`, + startOfUtcToday(), + ), + ) + .replace(/^(OPENER HINT|OPENER-HINWEIS): .*$/gm, "$1: ") + .replace(/\nNAME: [^\n]*/g, ""); + const { createHash: hash } = await import("node:crypto"); + effectRecorder.record("provider", [ + { + surface: args.surface, + chain: args.providers.map((p) => p.providerType), + params: rest, + promptChars: prompt.length, + promptSha256: hash("sha256").update(prompt).digest("hex"), + }, + ]); + const next = seams.script.shift(); + if (!next) { + seams.unscripted += 1; + throw new Error("parity: an unscripted provider call"); + } + if ("error" in next) throw next.error(); + if (next.during) await next.during(); + return { + result: { + content: next.content, + tokensUsed: next.tokensUsed ?? 1200, + model: "claude-parity", + providerType: "anthropic", + finishReason: next.finishReason ?? "stop", + }, + workingProvider: args.providers[0], + fallbackHops: [], + }; + }, + }; +}); + +vi.mock("@/lib/insights/features", async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + extractFeatures: async ( + ...args: Parameters + ) => { + const size = seams.oversize.shift(); + if (size !== undefined) { + throw new actual.FeaturesPayloadTooLargeError(size, 5_000_000); + } + return actual.extractFeatures(...args); + }, + }; +}); + +vi.mock("@/lib/insights/snapshot-hash", async (importOriginal) => { + const actual = + await importOriginal(); + const { relativeToAnchor, startOfUtcToday } = + await import("../../../kit/parity"); + return { + ...actual, + hashInsightSnapshot: (value: unknown) => { + const real = actual.hashInsightSnapshot(value); + if (value && typeof value === "object" && "features" in value) { + seams.hashes.set( + real, + actual.hashInsightSnapshot( + relativeToAnchor(value, startOfUtcToday()), + ), + ); + } + return real; + }, + }; +}); + +vi.mock("@/lib/insights/briefing-failure-marker", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/insights/briefing-failure-marker") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + recordBriefingFailure: ( + args: Parameters[0], + ) => { + effectRecorder.record("recordBriefingFailure", [args]); + const written = actual.recordBriefingFailure(args); + seams.pending.push(written); + return written; + }, + }; +}); + +vi.mock("@/lib/jobs/insight-pregenerate-shared", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/jobs/insight-pregenerate-shared") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + enqueueForceWarm: async (...args: unknown[]) => { + effectRecorder.record("enqueueForceWarm", args); + }, + }; +}); + +vi.mock("@/lib/jobs/insight-status-generate-shared", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("@/lib/jobs/insight-status-generate-shared") + >(); + const { effectRecorder } = await import("../../../kit/parity"); + return { + ...actual, + enqueueStatusGeneration: async (...args: unknown[]) => { + effectRecorder.record("enqueueStatusGeneration", args); + }, + }; +}); + +const prisma = getPrismaClient(); +const DAY = 86_400_000; +const HOUR = 3_600_000; +const ANCHOR = startOfUtcToday(); +const NOW = new Date(ANCHOR.getTime() + 12 * HOUR); +const EDGE_DAYS = [7, 30, 60]; +const OWNER_ID = parityId("insightsgen", 1); +const OPTED_OUT_ID = parityId("insightsgen", 2); +const UNREADABLE_KEY_ID = parityId("insightsgen", 3); + +let owner: RouteAuth; +let optedOut: RouteAuth; +let unreadableKey: RouteAuth; + +// ── Payloads ──────────────────────────────────────────────── + +const GROUNDED_BRIEFING = { + paragraph: "Your readings held steady this week, with no surprises.", + signalsOfDay: [ + { + sourceMetric: "weight", + tone: "info", + headline: "Weight is steady", + nudge: "Keep weighing in at the same time each morning.", + delta: null, + }, + ], + keyFindings: [], +}; + +const UNGROUNDED_BRIEFING = { + paragraph: "Your weight reached 123.4 kg this morning, up 9.7 kg.", + signalsOfDay: [], + keyFindings: [], +}; + +const PREVIOUS_BRIEFING = { + paragraph: "Yesterday's briefing, grounded when it was written.", + signalsOfDay: [], + keyFindings: [], +}; + +/** + * A reply in the rich shape the strict prompt asks for. It carries no + * `disclaimer`, so the strict schema rejects it and the route keeps the + * parsed object whole, briefing included. + */ +function reply(briefing: unknown, extra: Record = {}) { + return JSON.stringify({ + summary: "Your tracked metrics held steady over the last 30 days.", + classification: "gut", + findings: [], + correlations: [], + recommendations: [], + dataQuality: { coverage: "good", gaps: [], confidence: "hoch" }, + dailyBriefing: briefing, + ...extra, + }); +} + +/** A cached payload as the generator stores it. */ +function cached(briefing: unknown, generatedAt: Date | null) { + return { + summary: "Cached summary.", + classification: "gut", + findings: [], + correlations: [], + recommendations: [], + dataQuality: { coverage: "good", gaps: [], confidence: "hoch" }, + dailyBriefing: briefing, + ...(generatedAt + ? { [BRIEFING_GENERATED_AT_KEY]: generatedAt.toISOString() } + : {}), + }; +} + +function hop(httpStatus: number | null, attempt = 1) { + return { + providerType: "anthropic" as const, + attempt, + failureReason: httpStatus === null ? "ECONNRESET" : `HTTP ${httpStatus}`, + httpStatus, + }; +} + +function providerError(httpStatus: number): () => Error { + return () => + Object.assign(new Error(`upstream answered ${httpStatus}`), { + httpStatus, + bodyExcerpt: `{"error":"status ${httpStatus}"}`, + }); +} + +// ── State ─────────────────────────────────────────────────── + +interface BriefingState { + /** The cached payload: an object, a raw string, or nothing. */ + text?: unknown; + cachedAt?: Date | null; + cachedLocale?: string | null; + /** Failure markers to write, with their reason and age. */ + markers?: Array<{ reason: string; at: Date; httpStatus?: number | null }>; + /** Tokens already spent today. */ + spentToday?: number; + /** Generations already counted against this hour's quota. */ + quotaUsed?: number; +} + +async function resetBriefing(userId: string, state: BriefingState = {}) { + const text = + state.text === undefined + ? null + : typeof state.text === "string" + ? state.text + : JSON.stringify(state.text); + await prisma.user.update({ + where: { id: userId }, + data: { + insightsCachedAt: state.cachedAt ?? null, + insightsCachedTextEncrypted: text + ? insightsCacheWrite(text).insightsCachedTextEncrypted + : null, + insightsCachedLocale: state.cachedLocale ?? null, + insightsSnapshotHash: null, + }, + }); + await prisma.auditLog.deleteMany({ where: { userId } }); + for (const m of state.markers ?? []) { + await prisma.auditLog.create({ + data: { + userId, + action: "insights.briefing-failure", + createdAt: m.at, + details: JSON.stringify({ + reason: m.reason, + httpStatus: m.httpStatus ?? null, + failureKind: null, + }), + }, + }); + } + await prisma.coachUsage.deleteMany({ where: { userId } }); + if (state.spentToday) { + await prisma.coachUsage.create({ + data: { + userId, + dateKey: NOW.toISOString().slice(0, 10), + totalTokens: state.spentToday, + }, + }); + } + await prisma.rateLimit.deleteMany({ where: { key: `insights:${userId}` } }); + if (state.quotaUsed) { + await prisma.$executeRaw` + INSERT INTO rate_limits (key, count, reset_at) + VALUES (${`insights:${userId}`}, ${state.quotaUsed}, (NOW() AT TIME ZONE 'UTC') + INTERVAL '1 hour')`; + } + seams.script.length = 0; + seams.oversize.length = 0; +} + +async function stored(userId: string) { + const [user, audit, usage] = await Promise.all([ + prisma.user.findUniqueOrThrow({ + where: { id: userId }, + select: { + insightsCachedAt: true, + insightsCachedTextEncrypted: true, + insightsCachedLocale: true, + insightsSnapshotHash: true, + }, + }), + prisma.auditLog.findMany({ + where: { userId }, + orderBy: [{ createdAt: "asc" }, { action: "asc" }], + select: { action: true, details: true, ipAddress: true }, + }), + prisma.coachUsage.findMany({ + where: { userId }, + select: { + dateKey: true, + totalTokens: true, + operatorTokens: true, + messageCount: true, + }, + }), + ]); + const text = openInsightsCachedText(user); + let payload: unknown = text; + try { + payload = text === null ? null : JSON.parse(text); + } catch { + // Kept as the raw string. + } + const hash = user.insightsSnapshotHash; + return { + insightsCachedAt: user.insightsCachedAt, + insightsCachedLocale: user.insightsCachedLocale, + cachedPayload: payload, + snapshotHash: + hash === null ? null : (seams.hashes.get(hash) ?? ""), + audit: audit.map((row) => ({ + ...row, + details: row.details === null ? null : JSON.parse(row.details), + })), + usage, + }; +} + +/** + * The feature read annotates from parallel queries, so the order of its meta + * keys follows the database. The keys are sorted; what they hold is not + * touched. + */ +function sortedMeta(facts: unknown): unknown { + const f = facts as { meta?: Record | null }; + if (!f.meta) return facts; + return { + ...f, + meta: Object.fromEntries( + Object.entries(f.meta).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)), + ), + }; +} + +/** Keys of per-phase timings, which the clock and the load decide. */ +function timingKeys(value: unknown, out = new Set()): Set { + if (Array.isArray(value)) value.forEach((v) => timingKeys(v, out)); + else if (value && typeof value === "object") { + for (const [k, v] of Object.entries(value)) { + if (/_ms$|Ms$/.test(k)) out.add(k); + timingKeys(v, out); + } + } + return out; +} + +async function step( + name: string, + options: { + method: "GET" | "POST"; + auth?: RouteAuth; + body?: unknown; + userId?: string; + headers?: Record; + /** A status the contract does not declare; the reason says which. */ + undeclared?: string; + }, +): Promise { + wideEventRecorder.take(); + effectRecorder.take(); + const handler = options.method === "GET" ? GET : POST; + const res = await callRoute(handler, { + method: options.method, + route: "/api/insights/generate", + body: options.body, + headers: options.headers, + auth: options.auth ?? owner, + contract: options.undeclared ? { skip: options.undeclared } : undefined, + }); + await Promise.all(seams.pending.splice(0)); + await settleBackgroundTasks(); + const events = wideEventRecorder.take(); + expect(events.length).toBeGreaterThan(0); + const facts = events.map(eventFacts).map(sortedMeta); + expect(seams.script, "every scripted reply was asked for").toEqual([]); + expect(seams.unscripted, "no provider call went unscripted").toBe(0); + expect(seams.oversize, "every planted oversize was read").toEqual([]); + await expectParity( + `insights-generate.${name}`, + stable( + relativeToAnchor( + { + status: res.status, + body: res.body, + events: facts, + effects: effectRecorder.take(), + stored: await stored(options.userId ?? OWNER_ID), + }, + ANCHOR, + ), + { since: NOW, volatileKeys: [...timingKeys(facts)] }, + ), + ); +} + +const get = (name: string, opts: { auth?: RouteAuth; userId?: string } = {}) => + step(name, { method: "GET", ...opts }); +const post = ( + name: string, + body: unknown = { force: true }, + opts: { auth?: RouteAuth; userId?: string; undeclared?: string } = {}, +) => step(name, { method: "POST", body, ...opts }); + +async function dropEdgeDays(userId: string): Promise { + for (const d of EDGE_DAYS) { + await prisma.measurement.deleteMany({ + where: { + userId, + measuredAt: { + gte: new Date(ANCHOR.getTime() - d * DAY), + lt: new Date(ANCHOR.getTime() - (d - 1) * DAY), + }, + }, + }); + } +} + +async function consent(userId: string): Promise { + await prisma.consentReceipt.create({ + data: { userId, kind: "ai_full", artefact: "test", signedAt: NOW }, + }); +} + +beforeAll(async () => { + vi.useFakeTimers({ toFake: ["Date"], now: NOW }); + await truncateAllTables(prisma); + const ownerRow = await makeUser({ + id: OWNER_ID, + timezone: "UTC", + locale: "en", + heightCm: 178, + dateOfBirth: new Date("1975-04-02T00:00:00.000Z"), + gender: "male", + displayName: "Parity Person", + aiProvider: "ANTHROPIC", + aiModel: "claude-sonnet-4-6", + aiAnthropicKeyEncrypted: encrypt("sk-ant-parity"), + }); + await consent(OWNER_ID); + await seedParityRecord({ userId: OWNER_ID, now: NOW, tz: "UTC", tag: "ig" }); + await dropEdgeDays(OWNER_ID); + const rollups = await vi.importActual< + typeof import("@/lib/rollups/measurement-rollups") + >("@/lib/rollups/measurement-rollups"); + await rollups.ensureUserRollupsFresh(OWNER_ID); + const moodRollups = await vi.importActual< + typeof import("@/lib/rollups/mood-rollups") + >("@/lib/rollups/mood-rollups"); + await moodRollups.ensureUserMoodRollupsFresh(OWNER_ID); + await settleBackgroundTasks(); + owner = await makeSession(ownerRow); + + const optedOutRow = await makeUser({ + id: OPTED_OUT_ID, + timezone: "UTC", + locale: "en", + modulePreferencesJson: { insights: false }, + aiProvider: "ANTHROPIC", + aiModel: "claude-sonnet-4-6", + aiAnthropicKeyEncrypted: encrypt("sk-ant-parity"), + }); + await consent(OPTED_OUT_ID); + optedOut = await makeSession(optedOutRow); + + // A key the presence probe sees and no resolver can open. + const unreadableRow = await makeUser({ + id: UNREADABLE_KEY_ID, + timezone: "UTC", + locale: "en", + aiProvider: "ANTHROPIC", + aiModel: "claude-sonnet-4-6", + aiAnthropicKeyEncrypted: "v1:presence-only", + }); + await consent(UNREADABLE_KEY_ID); + unreadableKey = await makeSession(unreadableRow); +}); + +afterAll(() => { + vi.useRealTimers(); +}); + +describe("GET /api/insights/generate parity", () => { + it("G01 an anonymous call", async () => { + await get("G01-anonymous", { auth: anonymous }); + }); + + it("G02 the AI analysis opt-out", async () => { + await get("G02-opted-out", { auth: optedOut, userId: OPTED_OUT_ID }); + }); + + it("G03 no cached briefing", async () => { + await resetBriefing(OWNER_ID); + await get("G03-empty"); + }); + + it("G04 a briefing written today", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - HOUR)), + cachedAt: new Date(NOW.getTime() - HOUR), + cachedLocale: "en", + }); + await get("G04-fresh"); + }); + + it("G05 a briefing written yesterday", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - 20 * HOUR)), + cachedAt: new Date(NOW.getTime() - 2 * HOUR), + cachedLocale: "en", + }); + await get("G05-stale"); + }); + + it("G06 a briefing in another language", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - HOUR)), + cachedAt: new Date(NOW.getTime() - HOUR), + cachedLocale: "de", + }); + await get("G06-other-language"); + }); + + it("G07 an unparseable cache row", async () => { + await resetBriefing(OWNER_ID, { + text: "{not json", + cachedAt: new Date(NOW.getTime() - HOUR), + }); + await get("G07-unparseable"); + }); + + it("G08 a legacy payload, untagged", async () => { + await resetBriefing(OWNER_ID, { + text: { summary: "Legacy", recommendations: ["Drink water."] }, + cachedAt: new Date(NOW.getTime() - HOUR), + }); + await get("G08-legacy"); + }); + + it("G09 a failure newer than the last good briefing", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - 3 * HOUR)), + cachedAt: new Date(NOW.getTime() - 3 * HOUR), + cachedLocale: "en", + markers: [ + { + reason: "all-providers-failed", + at: new Date(NOW.getTime() - HOUR), + httpStatus: 429, + }, + ], + }); + await get("G09-held-after-failure"); + }); + + it("G10 a failure with no good briefing, and an older superseded one", async () => { + await resetBriefing(OWNER_ID, { + markers: [ + { reason: "provider-error", at: new Date(NOW.getTime() - 5 * HOUR) }, + { reason: "invalid-json", at: new Date(NOW.getTime() - HOUR) }, + ], + }); + await get("G10-failed-empty"); + }); + + it("G11 a briefing the grounding gate withheld", async () => { + await resetBriefing(OWNER_ID, { + text: cached(null, new Date(NOW.getTime() - 2 * HOUR)), + cachedAt: new Date(NOW.getTime() - 2 * HOUR), + cachedLocale: "en", + markers: [ + { reason: "briefing-ungrounded", at: new Date(NOW.getTime() - HOUR) }, + ], + }); + await get("G11-withheld"); + }); +}); + +describe("POST /api/insights/generate parity", () => { + it("P01 an anonymous call", async () => { + await post("P01-anonymous", { force: true }, { auth: anonymous }); + }); + + it("P02 the AI analysis opt-out", async () => { + await post( + "P02-opted-out", + { force: true }, + { auth: optedOut, userId: OPTED_OUT_ID }, + ); + }); + + it("P03 a body over the cap", async () => { + await resetBriefing(OWNER_ID); + await post( + "P03-body-too-large", + { pad: "x".repeat(17 * 1024) }, + { undeclared: "the body cap answers 413, which the contract omits" }, + ); + }); + + it("P04 a cached briefing written today", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - HOUR)), + cachedAt: new Date(NOW.getTime() - HOUR), + cachedLocale: "en", + }); + await post("P04-cache-hit", {}); + }); + + it("P05 a cached briefing from yesterday, regenerated", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - 20 * HOUR)), + cachedAt: new Date(NOW.getTime() - 2 * HOUR), + cachedLocale: "en", + }); + seams.script.push({ content: reply(GROUNDED_BRIEFING) }); + await post("P05-yesterday-regenerated", {}); + }); + + it("P06 a cache in another language, regenerated", async () => { + await resetBriefing(OWNER_ID, { + text: cached(GROUNDED_BRIEFING, new Date(NOW.getTime() - HOUR)), + cachedAt: new Date(NOW.getTime() - HOUR), + cachedLocale: "de", + }); + seams.script.push({ content: reply(GROUNDED_BRIEFING) }); + await post("P06-other-language-regenerated", {}); + }); + + it("P07 a briefingless cache when the hour's quota is spent", async () => { + await resetBriefing(OWNER_ID, { + text: cached(null, new Date(NOW.getTime() - HOUR)), + cachedAt: new Date(NOW.getTime() - HOUR), + cachedLocale: "en", + quotaUsed: 10, + }); + await post("P07-quota-cached-fallback", {}); + }); + + it("P08 a forced generation when the hour's quota is spent", async () => { + await resetBriefing(OWNER_ID, { quotaUsed: 10 }); + await post("P08-quota-refused"); + }); + + it("P09 a key no resolver can open", async () => { + await resetBriefing(UNREADABLE_KEY_ID); + await post( + "P09-unreadable-key", + { force: true }, + { + auth: unreadableKey, + userId: UNREADABLE_KEY_ID, + undeclared: "an unreadable stored key ends in 500 (pinned as it is)", + }, + ); + }); + + it("P10 a fresh generation", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ content: reply(GROUNDED_BRIEFING), tokensUsed: 2100 }); + await post("P10-generated"); + }); + + it("P11 a reply the strict schema accepts", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + content: reply(GROUNDED_BRIEFING, { disclaimer: "Consult your doctor." }), + }); + await post("P11-strict-reply"); + }); + + it("P12 a generation in German with the comparison, about-me and short form", async () => { + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { + locale: "de", + aiUsageLevel: "FRUGAL", + dashboardWidgetsJson: { comparisonBaseline: "lastMonth" }, + }, + }); + await prisma.allergy.create({ + data: { userId: OWNER_ID, substance: "Penicillin" }, + }); + await resetBriefing(OWNER_ID); + seams.script.push({ content: reply(GROUNDED_BRIEFING) }); + await post("P12-generated-de-short"); + await prisma.allergy.deleteMany({ where: { userId: OWNER_ID } }); + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { locale: "en", aiUsageLevel: null, dashboardWidgetsJson: {} }, + }); + }); + + it("P13 an oversized raw payload, downgraded once", async () => { + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { insightsPrivacyMode: "raw" }, + }); + await resetBriefing(OWNER_ID); + seams.oversize.push(6_100_000); + seams.script.push({ content: reply(GROUNDED_BRIEFING) }); + await post("P13-downgraded"); + }); + + it("P14 an oversized aggregate too, downgraded twice", async () => { + await resetBriefing(OWNER_ID); + seams.oversize.push(6_100_000, 5_400_000); + seams.script.push({ content: reply(GROUNDED_BRIEFING) }); + await post("P14-hard-downgraded"); + }); + + it("P15 oversized on every rung", async () => { + await resetBriefing(OWNER_ID); + seams.oversize.push(6_100_000, 5_400_000, 5_200_000); + await post("P15-too-large"); + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { insightsPrivacyMode: "aggregated" }, + }); + }); + + it("P16 the day's token ceiling", async () => { + await resetBriefing(OWNER_ID, { spentToday: 5_000_000 }); + await post("P16-budget-exceeded"); + }); + + it("P17 every provider refused the credential", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + error: () => new AllProvidersFailedError([hop(401), hop(403, 2)]), + }); + await post("P17-chain-auth"); + }); + + it("P18 every provider rate-limited", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ error: () => new AllProvidersFailedError([hop(429)]) }); + await post("P18-chain-rate-limited"); + }); + + it("P19 a provider in a 5xx", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + error: () => new AllProvidersFailedError([hop(401), hop(502, 2)]), + }); + await post("P19-chain-5xx"); + }); + + it("P20 every provider unreachable", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + error: () => new AllProvidersFailedError([hop(null), hop(null, 2)]), + }); + await post("P20-chain-transport"); + }); + + it("P21 a mixed chain failure", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + error: () => new AllProvidersFailedError([hop(404), hop(429, 2)]), + }); + await post("P21-chain-mixed"); + }); + + it("P22 one provider error per status class", async () => { + for (const status of [401, 429, 500, 400]) { + await resetBriefing(OWNER_ID); + seams.script.push({ error: providerError(status) }); + await post(`P22-provider-${status}`); + } + }); + + it("P23 a reply that is not JSON", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ content: "Sure! Here is your briefing." }); + await post("P23-invalid-json"); + }); + + it("P24 a reply cut off at the token ceiling", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ content: '{"summary": "Your', finishReason: "length" }); + await post("P24-truncated"); + }); + + it("P25 an ungrounded briefing the retry fixes", async () => { + await resetBriefing(OWNER_ID); + seams.script.push( + { content: reply(UNGROUNDED_BRIEFING) }, + { content: reply(GROUNDED_BRIEFING), tokensUsed: 900 }, + ); + await post("P25-grounding-retry-kept"); + }); + + it("P26 still ungrounded, the previous briefing stands in", async () => { + await resetBriefing(OWNER_ID, { + text: cached(PREVIOUS_BRIEFING, new Date(NOW.getTime() - 26 * HOUR)), + cachedAt: new Date(NOW.getTime() - 26 * HOUR), + cachedLocale: "en", + }); + seams.script.push( + { content: reply(UNGROUNDED_BRIEFING) }, + { content: reply(UNGROUNDED_BRIEFING) }, + ); + await post("P26-grounding-previous"); + }); + + it("P27 still ungrounded, nothing to stand in", async () => { + await resetBriefing(OWNER_ID); + seams.script.push( + { content: reply(UNGROUNDED_BRIEFING) }, + { content: reply(UNGROUNDED_BRIEFING) }, + ); + await post("P27-grounding-stripped"); + }); + + it("P28 an ungrounded briefing whose retry is not JSON", async () => { + await resetBriefing(OWNER_ID); + seams.script.push( + { content: reply(UNGROUNDED_BRIEFING) }, + { content: "not json" }, + ); + await post("P28-grounding-retry-unparseable"); + }); + + it("P29 a reply the outbound screen blocks", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + content: reply(GROUNDED_BRIEFING, { + summary: + "Your 10-year cardiovascular risk is elevated — consider stepping your dose up.", + }), + }); + await post( + "P29-outbound-screened", + { force: true }, + { + undeclared: "the outbound screen answers 502, which the contract omits", + }, + ); + }); + + it("P30 the about-me text changing during the generation", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + content: reply(GROUNDED_BRIEFING), + during: async () => { + await prisma.allergy.create({ + data: { userId: OWNER_ID, substance: "Latex" }, + }); + }, + }); + await post("P30-profile-scope-changed"); + await prisma.allergy.deleteMany({ where: { userId: OWNER_ID } }); + }); + + it("P31 the profile changing during the generation", async () => { + await resetBriefing(OWNER_ID); + seams.script.push({ + content: reply(GROUNDED_BRIEFING), + during: async () => { + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { displayName: "Renamed Person" }, + }); + }, + }); + await post("P31-scope-changed"); + await prisma.user.update({ + where: { id: OWNER_ID }, + data: { displayName: "Parity Person" }, + }); + }); +});