Merge branch 'fix/v1437-data' into release/v1.43.7

This commit is contained in:
Marc-André Bombeck committed 2026-10-20 08:12:32 +02:00
commit dee6a27ae4
100 files changed
+2314 -506

No files matched your search

@@ -377,8 +377,9 @@ describe("PUT /api/dashboard/widgets — Save persists full layouts", () => {
// v1.4.42 W2 — the route used to return only `issues[0].message`,
// which forced one round-trip per wrong field during iOS contract
// debugging. The route now returns every issue under `details.issues`
// AND writes a `dashboard.widgets.validation-failed` row to the
// append-only audit ledger so the operator can grep `/api/admin/audit`.
// and keeps the refusal on the wide event (`meta.rejected_attempt`);
// since v1.43.7 a refusal on the caller's own record no longer writes an
// audit row.
describe("PUT 422 — multi-issue envelope (v1.4.42 W2)", () => {
it("surfaces TWO simultaneous validation errors", async () => {
const { userId } = await seedUser();
@@ -406,25 +407,15 @@ describe("PUT /api/dashboard/widgets — Save persists full layouts", () => {
const paths = body.details.issues.map((i) => i.path).sort();
expect(paths).toEqual(["version", "widgets"]);
// Audit-ledger breadcrumb. The route emits the row fire-and-forget,
// so poll until it materialises instead of asserting after one fixed
// tick — under container load the detached insert can land well past
// 25 ms, which made this assertion flaky. Bounded at ~2 s.
const deadline = Date.now() + 2_000;
let audit: { details: string | null } | null = null;
while (audit === null && Date.now() < deadline) {
audit = await getPrismaClient().auditLog.findFirst({
where: { userId, action: "dashboard.widgets.validation-failed" },
});
if (audit === null) {
await new Promise((r) => setTimeout(r, 50));
}
}
expect(audit).not.toBeNull();
const details = JSON.parse(audit?.details ?? "{}") as {
issues: Array<{ path: string }>;
};
expect(details.issues.length).toBe(2);
// A refusal on the caller's own record is telemetry, not an audit
// event: it stays on the wide event and never reaches the ledger
// (only a delegate's refusal is filed, for the owner's feed). Give a
// detached insert time to land before asserting it did not.
await new Promise((r) => setTimeout(r, 300));
const audit = await getPrismaClient().auditLog.findFirst({
where: { userId, action: "dashboard.widgets.validation-failed" },
});
expect(audit).toBeNull();
});
it("surfaces THREE simultaneous validation errors", async () => {
@@ -29,6 +29,10 @@ import {
enqueueBootTimeFreeTextEncryptionBackfill,
runFreeTextEncryptionBackfillForUser,
} from "@/lib/jobs/free-text-encryption-backfill";
import {
insightsCacheWrite,
openInsightsCachedText,
} from "@/lib/insights/cached-text-cipher";
import { getPrismaClient, truncateAllTables } from "./setup";
function makeCapturingBoss() {
@@ -135,6 +139,7 @@ describe("free-text encryption backfill (real Postgres)", () => {
routeGeometriesMigrated: 0,
appointmentAddressesCleared: 0,
contactAuditRowsScrubbed: 0,
insightsCachesMigrated: 0,
});
expect(await runFreeTextEncryptionBackfillForUser("ft-b")).toEqual({
conversationTitlesMigrated: 0,
@@ -143,6 +148,7 @@ describe("free-text encryption backfill (real Postgres)", () => {
routeGeometriesMigrated: 0,
appointmentAddressesCleared: 0,
contactAuditRowsScrubbed: 0,
insightsCachesMigrated: 0,
});
const conversations = await prisma.coachConversation.findMany({
@@ -191,6 +197,7 @@ describe("free-text encryption backfill (real Postgres)", () => {
routeGeometriesMigrated: 0,
appointmentAddressesCleared: 0,
contactAuditRowsScrubbed: 0,
insightsCachesMigrated: 0,
});
});
@@ -275,6 +282,7 @@ describe("free-text encryption backfill (real Postgres)", () => {
routeGeometriesMigrated: 0,
appointmentAddressesCleared: 0,
contactAuditRowsScrubbed: 0,
insightsCachesMigrated: 0,
});
expect(await runFreeTextEncryptionBackfillForUser("pc-b")).toEqual({
conversationTitlesMigrated: 0,
@@ -283,6 +291,7 @@ describe("free-text encryption backfill (real Postgres)", () => {
routeGeometriesMigrated: 1,
appointmentAddressesCleared: 0,
contactAuditRowsScrubbed: 0,
insightsCachesMigrated: 0,
});
const practitioners = await prisma.practitioner.findMany({
@@ -343,6 +352,7 @@ describe("free-text encryption backfill (real Postgres)", () => {
routeGeometriesMigrated: 0,
appointmentAddressesCleared: 0,
contactAuditRowsScrubbed: 0,
insightsCachesMigrated: 0,
});
});
@@ -440,6 +450,7 @@ describe("free-text encryption backfill (real Postgres)", () => {
expect(summaryA).toMatchObject({
appointmentAddressesCleared: 1,
contactAuditRowsScrubbed: 1,
insightsCachesMigrated: 0,
});
await runFreeTextEncryptionBackfillForUser("ad-b");
@@ -487,6 +498,75 @@ describe("free-text encryption backfill (real Postgres)", () => {
expect(second.sent).toEqual([]);
});
it("seals the cached insights payload, keeps updatedAt, and converges", async () => {
const prisma = getPrismaClient();
await Promise.all([seedUser("ic-a"), seedUser("ic-b"), seedUser("ic-c")]);
const payload = JSON.stringify({
dailyBriefing: { paragraph: "Pressure eased this week." },
});
// ic-a: written by the previous release, in the clear.
await prisma.user.update({
where: { id: "ic-a" },
data: { insightsCachedText: payload },
});
// ic-b: written the current way.
await prisma.user.update({
where: { id: "ic-b" },
data: insightsCacheWrite("current"),
});
const before = await prisma.user.findUniqueOrThrow({
where: { id: "ic-a" },
select: { updatedAt: true },
});
const { boss, sent } = makeCapturingBoss();
setGlobalBoss(boss as never);
const discovered = await enqueueBootTimeFreeTextEncryptionBackfill();
expect(discovered.error).toBeNull();
expect(sent.map((s) => s.userId)).toEqual(["ic-a"]);
const summary = await runFreeTextEncryptionBackfillForUser("ic-a");
expect(summary.insightsCachesMigrated).toBe(1);
const rowA = await prisma.user.findUniqueOrThrow({
where: { id: "ic-a" },
select: {
insightsCachedText: true,
insightsCachedTextEncrypted: true,
updatedAt: true,
},
});
expect(rowA.insightsCachedText).toBeNull();
expect(rowA.insightsCachedTextEncrypted).not.toBeNull();
expect(openInsightsCachedText(rowA)).toBe(payload);
expect(rowA.updatedAt).toEqual(before.updatedAt);
const rowB = await prisma.user.findUniqueOrThrow({
where: { id: "ic-b" },
select: { insightsCachedText: true, insightsCachedTextEncrypted: true },
});
expect(openInsightsCachedText(rowB)).toBe("current");
const again = await runFreeTextEncryptionBackfillForUser("ic-a");
expect(again.insightsCachesMigrated).toBe(0);
const second = makeCapturingBoss();
setGlobalBoss(second.boss as never);
await enqueueBootTimeFreeTextEncryptionBackfill();
expect(second.sent).toEqual([]);
});
it("has the partial index migration 0411 adds", async () => {
const indexes = await getPrismaClient().$queryRaw<
{ indexname: string; indexdef: string }[]
>`
SELECT indexname, indexdef FROM pg_indexes
WHERE indexname = 'users_insights_cached_text_backfill_idx'`;
expect(indexes).toHaveLength(1);
expect(indexes[0].indexdef).toContain(
"WHERE (insights_cached_text IS NOT NULL)",
);
});
it("has the partial indexes migration 0361 adds", async () => {
const indexes = await getPrismaClient().$queryRaw<
{ indexname: string; indexdef: string }[]