mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
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:
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),
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user