test(01-13): add red Google sync progress contracts

- pin workout-first fairness and bounded resource outcomes
- require owner-scoped race-safe current-run progress in PostgreSQL
This commit is contained in:
Marc-André Bombeck committed 2026-07-29 14:18:20 +02:00
1 parent 81893ac972
commit d8e52b831d
4 files changed
+614

No files matched your search

@@ -0,0 +1,95 @@
/**
* RED contract for GH-02/SYNC-01.
*
* Antonios's trace proves only that 786 Heart Rate pages preceded Exercise.
* It does not prove a provider defect or a safe universal page limit. This
* fixture therefore models all 786 successful pages and requires the
* inexpensive workout resource to reach a terminal before dense metrics
* pagination begins.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
const { calls, prismaMock, recordSyncSuccess, resources } = vi.hoisted(() => {
const calls: string[] = [];
return {
calls,
prismaMock: {
googleHealthConnection: {
findUnique: vi.fn(async () => ({ lastSyncedAt: null })),
update: vi.fn(async () => ({})),
},
},
recordSyncSuccess: vi.fn(async () => {}),
resources: {
workout: vi.fn(async () => {
calls.push("workout:complete");
return 1;
}),
sleep: vi.fn(async () => {
calls.push("sleep:complete");
return 0;
}),
activity: vi.fn(async () => {
calls.push("activity:complete");
return 0;
}),
metrics: vi.fn(async () => {
for (let page = 1; page <= 786; page++) {
calls.push(`heart-rate:${page}`);
}
calls.push("heart-rate:complete");
return 786;
}),
},
};
});
vi.mock("@/lib/db", () => ({ prisma: prismaMock }));
vi.mock("@/lib/integrations/status", () => ({
isReauthRequired: vi.fn(async () => false),
recordSyncSuccess,
}));
vi.mock("@/lib/rollups/measurement-rollups", () => ({
collapseToTypeDayKeys: vi.fn(() => []),
recomputeUserRollups: vi.fn(async () => {}),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
vi.mock("../sync-workout", () => ({ syncUserWorkout: resources.workout }));
vi.mock("../sync-sleep", () => ({ syncUserSleep: resources.sleep }));
vi.mock("../sync-activity", () => ({ syncUserActivity: resources.activity }));
vi.mock("../sync-metrics", () => ({ syncUserMetrics: resources.metrics }));
import { syncUserGoogleHealth } from "../sync";
beforeEach(() => {
calls.length = 0;
vi.clearAllMocks();
prismaMock.googleHealthConnection.findUnique.mockResolvedValue({
lastSyncedAt: null,
});
});
describe("Google Health workout-first fairness", () => {
it("finishes workout before the first of 786 successful heart-rate pages", async () => {
const result = await syncUserGoogleHealth("fair-user", { fullSync: true });
expect(calls.indexOf("workout:complete")).toBeGreaterThanOrEqual(0);
expect(calls.indexOf("workout:complete")).toBeLessThan(
calls.indexOf("heart-rate:1"),
);
expect(
calls.filter((entry) => entry.startsWith("heart-rate:")),
).toHaveLength(787);
expect(result).toMatchObject({
resources: expect.arrayContaining([
expect.objectContaining({
resource: "workout",
status: "complete",
written: 1,
}),
]),
});
});
});
@@ -0,0 +1,183 @@
/**
* RED contract for GH-03/GH-04/SYNC-03.
*
* Resource outcomes are bounded facts, never provider payloads. The observed
* 786 Heart Rate pages are retained as evidence without converting that
* observation into an invented truncation ceiling.
*/
import { describe, expect, it, vi } from "vitest";
const RESOURCE_KEYS = [
"resource",
"pages",
"fetched",
"mapped",
"written",
"status",
"durationMs",
"truncated",
"reasonCode",
] as const;
const resourceOutcomes = [
{
resource: "workout",
pages: 1,
fetched: 1,
mapped: 1,
written: 1,
status: "complete",
durationMs: 12,
truncated: false,
reasonCode: null,
},
{
resource: "sleep",
pages: 1,
fetched: 2,
mapped: 2,
written: 1,
status: "partial",
durationMs: 13,
truncated: false,
reasonCode: "upsert_failed",
},
{
resource: "steps",
pages: 1,
fetched: 0,
mapped: 0,
written: 0,
status: "empty",
durationMs: 4,
truncated: false,
reasonCode: null,
},
{
resource: "heart-rate",
pages: 786,
fetched: 78_600,
mapped: 78_600,
written: 78_600,
status: "complete",
durationMs: 323_849,
truncated: false,
reasonCode: null,
},
{
resource: "distance",
pages: 8,
fetched: 800,
mapped: 800,
written: 800,
status: "truncated",
durationMs: 900,
truncated: true,
reasonCode: "existing_page_limit",
},
{
resource: "activity",
pages: 0,
fetched: 0,
mapped: 0,
written: 0,
status: "failed",
durationMs: 5,
truncated: false,
reasonCode: "collection_failed",
},
{
resource: "token",
pages: 0,
fetched: 0,
mapped: 0,
written: 0,
status: "failed",
durationMs: 2,
truncated: false,
reasonCode: "token_failed",
},
{
resource: "rollup",
pages: 0,
fetched: 1,
mapped: 1,
written: 1,
status: "failed",
durationMs: 7,
truncated: false,
reasonCode: "rollup_failed",
},
];
const { prismaMock, runCycle } = vi.hoisted(() => ({
prismaMock: {
googleHealthConnection: {
findUnique: vi.fn(async () => ({ lastSyncedAt: null })),
update: vi.fn(async () => ({})),
},
},
runCycle: vi.fn(),
}));
vi.mock("@/lib/db", () => ({ prisma: prismaMock }));
vi.mock("@/lib/integrations/status", () => ({
isReauthRequired: vi.fn(async () => false),
recordSyncSuccess: vi.fn(async () => {}),
}));
vi.mock("@/lib/rollups/measurement-rollups", () => ({
collapseToTypeDayKeys: vi.fn(() => []),
recomputeUserRollups: vi.fn(async () => {}),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
vi.mock("../sync-core", () => ({
GOOGLE_HEALTH_INTEGRATION_KEY: "google-health",
incrementalStart: vi.fn(() => undefined),
markSynced: vi.fn(async () => {}),
runWithGoogleHealthSyncCycle: runCycle,
}));
vi.mock("../sync-metrics", () => ({ syncUserMetrics: vi.fn() }));
vi.mock("../sync-activity", () => ({ syncUserActivity: vi.fn() }));
vi.mock("../sync-sleep", () => ({ syncUserSleep: vi.fn() }));
vi.mock("../sync-workout", () => ({ syncUserWorkout: vi.fn() }));
import { syncUserGoogleHealth } from "../sync";
describe("Google Health bounded resource outcomes", () => {
it("returns exact redacted fields for every terminal class", async () => {
runCycle.mockResolvedValue({
result: { total: 79_402, anyFailed: true, resources: resourceOutcomes },
hardFailures: [
"activity:collection",
"token",
"workout:upsert",
"rollup",
],
softSkipCount: 0,
deferredRollupKeys: [],
resources: resourceOutcomes,
});
const result = (await syncUserGoogleHealth("outcome-user", {
fullSync: true,
})) as unknown as { resources: Array<Record<string, unknown>> };
expect(result.resources).toHaveLength(resourceOutcomes.length);
for (const outcome of result.resources) {
expect(Object.keys(outcome).sort()).toEqual([...RESOURCE_KEYS].sort());
expect(JSON.stringify(outcome)).not.toMatch(
/access[_-]?token|refresh[_-]?token|https?:|rawError|healthValue|samples/i,
);
}
expect(result.resources).toEqual(resourceOutcomes);
expect(
result.resources.find((item) => item.resource === "heart-rate"),
).toMatchObject({
pages: 786,
truncated: false,
status: "complete",
});
});
});
@@ -0,0 +1,156 @@
/**
* RED unit contract for the Plan 21 current-run store.
*
* These names deliberately define the small storage seam Plan 21 implements:
* one owner-scoped create, guarded update, and stale-aware read. They are not
* provider or pagination policy.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
const { prismaMock } = vi.hoisted(() => ({
prismaMock: {
googleHealthConnection: {
findUnique: vi.fn(),
update: vi.fn(async ({ data }) => data),
updateMany: vi.fn(async () => ({ count: 1 })),
},
},
}));
vi.mock("@/lib/db", () => ({ prisma: prismaMock }));
type ProgressModule = {
startGoogleHealthSyncProgress: (
userId: string,
now?: Date,
) => Promise<{ runId: string; state: string; resources: unknown[] }>;
updateGoogleHealthSyncProgress: (
userId: string,
runId: string,
update: Record<string, unknown>,
now?: Date,
) => Promise<boolean>;
readGoogleHealthSyncProgress: (
userId: string,
now?: Date,
) => Promise<Record<string, unknown> | null>;
};
async function progressModule(): Promise<ProgressModule> {
return (await import("../sync-progress")) as unknown as ProgressModule;
}
beforeEach(() => {
vi.clearAllMocks();
prismaMock.googleHealthConnection.update.mockImplementation(
async ({ data }) => data,
);
prismaMock.googleHealthConnection.updateMany.mockResolvedValue({ count: 1 });
});
describe("Google Health current-run progress", () => {
it("starts unpredictable single-run envelopes with bounded fixed resources", async () => {
const progress = await progressModule();
const now = new Date("2026-07-29T10:00:00.000Z");
const first = await progress.startGoogleHealthSyncProgress("owner", now);
const second = await progress.startGoogleHealthSyncProgress("owner", now);
expect(first.runId).not.toBe(second.runId);
expect(first.runId.length).toBeGreaterThanOrEqual(20);
expect(first.state).toBe("in_progress");
expect(first.resources.length).toBeGreaterThan(0);
expect(first.resources.length).toBeLessThanOrEqual(16);
const persisted =
prismaMock.googleHealthConnection.update.mock.calls.at(-1)?.[0];
expect(persisted.where).toEqual({ userId: "owner" });
expect(Object.keys(persisted.data.syncProgress).sort()).toEqual(
expect.arrayContaining([
"runId",
"state",
"startedAt",
"updatedAt",
"resources",
]),
);
});
it("guards every update by both owner and current runId", async () => {
const progress = await progressModule();
await expect(
progress.updateGoogleHealthSyncProgress(
"owner",
"run-current",
{ state: "complete", written: 4 },
new Date("2026-07-29T10:01:00.000Z"),
),
).resolves.toBe(true);
const updateArg =
prismaMock.googleHealthConnection.updateMany.mock.calls[0][0];
const serializedGuard = JSON.stringify(updateArg.where);
expect(serializedGuard).toContain("owner");
expect(serializedGuard).toContain("run-current");
prismaMock.googleHealthConnection.updateMany.mockResolvedValueOnce({
count: 0,
});
await expect(
progress.updateGoogleHealthSyncProgress("owner", "run-stale", {
state: "failed",
}),
).resolves.toBe(false);
});
it("turns stale in-progress reads into an interrupted terminal", async () => {
const progress = await progressModule();
prismaMock.googleHealthConnection.findUnique.mockResolvedValue({
syncProgress: {
runId: "run-stale",
state: "in_progress",
startedAt: "2026-07-29T08:00:00.000Z",
updatedAt: "2026-07-29T08:00:00.000Z",
resources: [],
},
});
const read = await progress.readGoogleHealthSyncProgress(
"owner",
new Date("2026-07-29T10:00:00.000Z"),
);
expect(read).toMatchObject({
runId: "run-stale",
state: "interrupted",
terminalAt: expect.any(String),
});
expect(
JSON.stringify(
prismaMock.googleHealthConnection.updateMany.mock.calls[0][0].where,
),
).toContain("run-stale");
});
it("drops secret, URL, raw-error, health-value, and sample-shaped fields", async () => {
const progress = await progressModule();
await progress.updateGoogleHealthSyncProgress("owner", "run-current", {
state: "partial",
accessToken: "secret-token",
url: "https://provider.invalid/capability",
rawError: "provider body",
healthValue: 123,
samples: [{ bpm: 180 }],
resources: Array.from({ length: 100 }, (_, index) => ({
resource: `resource-${index}`,
})),
});
const persisted = JSON.stringify(
prismaMock.googleHealthConnection.updateMany.mock.calls[0][0].data,
);
expect(persisted).not.toMatch(
/secret-token|provider\\.invalid|provider body|healthValue|bpm|samples/,
);
});
});
@@ -0,0 +1,180 @@
/**
* RED real-PostgreSQL contract for the nullable
* GoogleHealthConnection.syncProgress current-run envelope.
*/
import { beforeEach, describe, expect, it } from "vitest";
import { getPrismaClient, truncateAllTables } from "./setup";
type ProgressModule = {
startGoogleHealthSyncProgress: (
userId: string,
now?: Date,
) => Promise<{ runId: string }>;
updateGoogleHealthSyncProgress: (
userId: string,
runId: string,
update: Record<string, unknown>,
now?: Date,
) => Promise<boolean>;
readGoogleHealthSyncProgress: (
userId: string,
now?: Date,
) => Promise<Record<string, unknown> | null>;
};
async function progressModule(): Promise<ProgressModule> {
return (await import("@/lib/google-health/sync-progress")) as unknown as ProgressModule;
}
async function seedConnection(userId: string): Promise<void> {
const prisma = getPrismaClient();
await prisma.user.create({
data: {
id: userId,
username: userId,
email: `${userId}@example.test`,
},
});
await prisma.googleHealthConnection.create({
data: {
userId,
googleUserId: `google-${userId}`,
accessToken: "encrypted-access",
refreshToken: "encrypted-refresh",
tokenExpiresAt: new Date("2027-01-01T00:00:00.000Z"),
},
});
}
async function rawProgress(
userId: string,
): Promise<Record<string, unknown> | null> {
const prisma = getPrismaClient();
const rows = await prisma.$queryRawUnsafe<Array<{ sync_progress: unknown }>>(
`SELECT sync_progress FROM google_health_connections WHERE user_id = $1`,
userId,
);
return (rows[0]?.sync_progress as Record<string, unknown> | null) ?? null;
}
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
});
describe("Google Health current-run progress (real Postgres)", () => {
it("keeps one owner-scoped current run and rejects delayed older writers", async () => {
await seedConnection("progress-owner");
await seedConnection("progress-other");
const progress = await progressModule();
const older = await progress.startGoogleHealthSyncProgress(
"progress-owner",
new Date("2026-07-29T10:00:00.000Z"),
);
const newer = await progress.startGoogleHealthSyncProgress(
"progress-owner",
new Date("2026-07-29T10:01:00.000Z"),
);
expect(older.runId).not.toBe(newer.runId);
await expect(
progress.updateGoogleHealthSyncProgress("progress-owner", older.runId, {
state: "failed",
}),
).resolves.toBe(false);
await expect(
progress.updateGoogleHealthSyncProgress("progress-owner", newer.runId, {
state: "complete",
written: 3,
}),
).resolves.toBe(true);
expect(await rawProgress("progress-owner")).toMatchObject({
runId: newer.runId,
state: "complete",
});
expect(
await progress.readGoogleHealthSyncProgress("progress-other"),
).toBeNull();
});
it("allows only one concurrent terminal writer to own the retained run", async () => {
await seedConnection("progress-race");
const progress = await progressModule();
const current =
await progress.startGoogleHealthSyncProgress("progress-race");
const results = await Promise.all([
progress.updateGoogleHealthSyncProgress("progress-race", current.runId, {
state: "complete",
written: 4,
}),
(async () => {
await progress.startGoogleHealthSyncProgress("progress-race");
return progress.updateGoogleHealthSyncProgress(
"progress-race",
current.runId,
{ state: "failed" },
);
})(),
]);
expect(results.filter(Boolean)).toHaveLength(1);
expect((await rawProgress("progress-race"))?.runId).not.toBe(current.runId);
});
it("persists bounded keys/cardinality without secrets or per-sample facts", async () => {
await seedConnection("progress-private");
const progress = await progressModule();
const run =
await progress.startGoogleHealthSyncProgress("progress-private");
await progress.updateGoogleHealthSyncProgress(
"progress-private",
run.runId,
{
state: "partial",
accessToken: "raw-access-token",
refreshToken: "raw-refresh-token",
url: "https://provider.invalid/private",
rawError: "provider response body",
healthValue: 181,
samples: Array.from({ length: 100 }, (_, bpm) => ({ bpm })),
},
);
const stored = await rawProgress("progress-private");
expect(Object.keys(stored ?? {}).sort()).toEqual(
expect.arrayContaining([
"runId",
"state",
"startedAt",
"updatedAt",
"resources",
]),
);
expect((stored?.resources as unknown[]).length).toBeLessThanOrEqual(16);
expect(JSON.stringify(stored)).not.toMatch(
/raw-access-token|raw-refresh-token|provider\\.invalid|provider response body|healthValue|samples|bpm/,
);
});
it("materializes stale in-progress state as interrupted", async () => {
await seedConnection("progress-stale");
const progress = await progressModule();
await progress.startGoogleHealthSyncProgress(
"progress-stale",
new Date("2026-07-29T08:00:00.000Z"),
);
await expect(
progress.readGoogleHealthSyncProgress(
"progress-stale",
new Date("2026-07-29T10:00:00.000Z"),
),
).resolves.toMatchObject({
state: "interrupted",
terminalAt: expect.any(String),
});
});
});