test(01-14): add failing truthful cohort contracts

- distinguish complete partial failed parked skipped and clean-zero outcomes
- require bounded privacy-safe scalar serialization and retry codes
This commit is contained in:
Marc-André Bombeck committed 2026-07-29 14:26:26 +02:00
1 parent 34372b57bd
commit 690c4aeb5a
2 files changed
+455

No files matched your search

+195
View File
@@ -0,0 +1,195 @@
import { describe, expect, it, vi } from "vitest";
import { jobDone, jobFailed, type JobOutcome } from "@/lib/jobs/job-outcome";
type SerializableJobOutcome = {
ok: boolean;
reason_code?: string;
did?: Readonly<Record<string, string | number | boolean>>;
};
type JobOutcomeSubject = {
JOB_FACT_ALLOWLIST: ReadonlySet<string>;
MAX_JOB_FACTS: number;
MAX_JOB_OUTCOME_BYTES: number;
serializeJobOutcome(outcome: JobOutcome): SerializableJobOutcome;
};
function loadSubject(): Promise<JobOutcomeSubject> {
return vi.importActual<JobOutcomeSubject>("@/lib/jobs/job-outcome");
}
const COHORT_FACT_KEYS = [
"provider",
"outcome",
"total",
"users_synced",
"users_complete",
"users_partial",
"users_failed",
"users_parked",
"users_skipped",
"users_useful",
"users_clean_zero",
"users_retryable",
"downstream_failed",
"measurements_imported",
] as const;
const FORBIDDEN_FACT_KEYS = [
"user_id",
"userId",
"email",
"name",
"access_token",
"refresh_token",
"secret",
"url",
"raw_error",
"health_value",
"blood_glucose",
] as const;
describe("bounded generic JobOutcome serialization", () => {
it("publishes a fixed allowlist for truthful cohort scalars", async () => {
const { JOB_FACT_ALLOWLIST } = await loadSubject();
for (const key of COHORT_FACT_KEYS) {
expect(JOB_FACT_ALLOWLIST.has(key), key).toBe(true);
}
for (const key of FORBIDDEN_FACT_KEYS) {
expect(JOB_FACT_ALLOWLIST.has(key), key).toBe(false);
}
});
it.each(FORBIDDEN_FACT_KEYS)(
"rejects the private or unbounded fact key %s",
async (key) => {
const { serializeJobOutcome } = await loadSubject();
expect(() =>
serializeJobOutcome(
jobDone({ [key]: "private-value" } as Record<string, string>),
),
).toThrow(/fact|allow|private|unsupported/i);
},
);
it("rejects nested objects, arrays, nulls, non-finite numbers, and unsafe integers", async () => {
const { serializeJobOutcome } = await loadSubject();
for (const unsafe of [
{ nested: true },
["user-a"],
null,
Number.NaN,
Number.POSITIVE_INFINITY,
Number.MAX_SAFE_INTEGER + 1,
-1,
]) {
expect(() =>
serializeJobOutcome(
jobDone({ total: unsafe } as unknown as Record<string, number>),
),
).toThrow(/scalar|finite|integer|range|unsupported/i);
}
});
it.each([
"patient@example.test",
"https://provider.test/capability",
"token=secret",
"raw upstream stack trace",
"blood glucose 411",
"x".repeat(65),
])("rejects an unbounded or private string fact: %s", async (value) => {
const { serializeJobOutcome } = await loadSubject();
expect(() => serializeJobOutcome(jobDone({ provider: value }))).toThrow(
/code|string|private|length|unsupported/i,
);
});
it("rejects too many facts and enforces a small serialized byte ceiling", async () => {
const {
JOB_FACT_ALLOWLIST,
MAX_JOB_FACTS,
MAX_JOB_OUTCOME_BYTES,
serializeJobOutcome,
} = await loadSubject();
expect(MAX_JOB_FACTS).toBeGreaterThanOrEqual(COHORT_FACT_KEYS.length);
expect(MAX_JOB_FACTS).toBeLessThanOrEqual(32);
expect(MAX_JOB_OUTCOME_BYTES).toBeLessThanOrEqual(2_048);
const allowed = [...JOB_FACT_ALLOWLIST];
const tooMany = Object.fromEntries(
Array.from({ length: MAX_JOB_FACTS + 1 }, (_, index) => [
allowed[index] ?? `overflow_${index}`,
index,
]),
);
expect(() => serializeJobOutcome(jobDone(tooMany))).toThrow(
/fact|count|limit|allow/i,
);
});
it("distinguishes useful work from a successful clean zero", async () => {
const { serializeJobOutcome } = await loadSubject();
const useful = serializeJobOutcome(
jobDone({ outcome: "useful", total: 1, measurements_imported: 4 }),
);
const cleanZero = serializeJobOutcome(
jobDone({ outcome: "clean_zero", total: 1, measurements_imported: 0 }),
);
expect(useful.did?.outcome).toBe("useful");
expect(cleanZero.did?.outcome).toBe("clean_zero");
expect(useful).not.toEqual(cleanZero);
});
it("emits only stable failure codes and never serializes raw causes", async () => {
const { serializeJobOutcome } = await loadSubject();
const privateText =
"patient@example.test https://provider.test?token=secret glucose=411";
const serialized = serializeJobOutcome(
jobFailed("provider_cohort_failed", new Error(privateText), {
outcome: "clean_zero",
total: 0,
}),
);
expect(serialized).toEqual({
ok: false,
reason_code: "provider_cohort_failed",
did: { outcome: "clean_zero", total: 0 },
});
expect(JSON.stringify(serialized)).not.toContain(privateText);
expect(serialized).not.toHaveProperty("cause");
});
it.each([
"raw upstream error",
"https://provider.test/failure",
"failed_for_user_patient@example.test",
"token=secret",
"x".repeat(65),
])("rejects an unstable or private failure reason: %s", async (reason) => {
const { serializeJobOutcome } = await loadSubject();
expect(() => serializeJobOutcome(jobFailed(reason))).toThrow(
/reason|code|private|length/i,
);
});
it("keeps a retryable queue failure failed so runJob will retry it", async () => {
const { serializeJobOutcome } = await loadSubject();
const serialized = serializeJobOutcome(
jobFailed("provider_cohort_retry", new Error("socket reset"), {
outcome: "clean_zero",
users_retryable: 1,
}),
);
expect(serialized.ok).toBe(false);
expect(serialized.reason_code).toBe("provider_cohort_retry");
expect(serialized.did).toEqual({
outcome: "clean_zero",
users_retryable: 1,
});
});
});
@@ -0,0 +1,260 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it, vi } from "vitest";
type Provider = "google_health" | "withings";
type UserStatus = "complete" | "partial" | "failed" | "parked" | "skipped";
type UserVerdict = {
status: UserStatus;
imported: number;
downstreamFailed?: boolean;
retryable?: boolean;
reasonCode?: string;
};
type CohortFacts = {
provider: Provider;
outcome: "useful" | "clean_zero";
total: number;
users_synced: number;
users_complete: number;
users_partial: number;
users_failed: number;
users_parked: number;
users_skipped: number;
users_useful: number;
users_clean_zero: number;
users_retryable: number;
downstream_failed: number;
measurements_imported: number;
};
type CohortSubject = {
foldIntegrationCohortOutcomes(input: {
provider: Provider;
verdicts: readonly UserVerdict[];
}): { ok: true; did: CohortFacts };
};
function loadSubject(): Promise<CohortSubject> {
return vi.importActual<CohortSubject>("@/lib/jobs/reminder/poll-cohort");
}
const googleHandlerSource = readFileSync(
join(process.cwd(), "src/lib/jobs/reminder/google-health-sync.ts"),
"utf8",
);
const googleSyncSource = readFileSync(
join(process.cwd(), "src/lib/google-health/sync.ts"),
"utf8",
);
const withingsHandlerSource = readFileSync(
join(process.cwd(), "src/lib/jobs/reminder/withings-sync.ts"),
"utf8",
);
describe("provider handlers use the truthful verdict fold", () => {
it("does not discard Google Health's resolved failed verdict", () => {
expect(googleSyncSource).not.toMatch(
/syncUserGoogleHealth\(userId\)\.then\(\(r\)\s*=>\s*r\.imported\)/,
);
expect(googleHandlerSource).toMatch(/foldIntegrationCohortOutcomes/);
});
it("does not count every resolved Withings call as synced unconditionally", () => {
expect(withingsHandlerSource).toMatch(/foldIntegrationCohortOutcomes/);
expect(withingsHandlerSource).not.toMatch(
/(?:syncUserMeasurements|syncUserActivity|syncUserSleep|syncUserEcg)[\s\S]{0,180}usersSynced\+\+/,
);
});
});
describe.each(["google_health", "withings"] as const)(
"%s truthful integration cohort outcomes",
(provider) => {
it("classifies complete, partial, failed, parked, and skipped users separately", async () => {
const { foldIntegrationCohortOutcomes } = await loadSubject();
const outcome = foldIntegrationCohortOutcomes({
provider,
verdicts: [
{ status: "complete", imported: 3 },
{
status: "partial",
imported: 2,
downstreamFailed: true,
retryable: true,
reasonCode: "rollup_failed",
},
{
status: "failed",
imported: 0,
retryable: true,
reasonCode: "provider_unavailable",
},
{
status: "parked",
imported: 0,
reasonCode: "reauth_required",
},
{ status: "skipped", imported: 0, reasonCode: "not_configured" },
],
});
expect(outcome).toEqual({
ok: true,
did: {
provider,
outcome: "useful",
total: 5,
users_synced: 1,
users_complete: 1,
users_partial: 1,
users_failed: 1,
users_parked: 1,
users_skipped: 1,
users_useful: 2,
users_clean_zero: 0,
users_retryable: 2,
downstream_failed: 1,
measurements_imported: 5,
},
});
expect(
outcome.did.users_complete +
outcome.did.users_partial +
outcome.did.users_failed +
outcome.did.users_parked +
outcome.did.users_skipped,
).toBe(outcome.did.total);
});
it("counts a fully successful zero-write user as clean zero, not useful work", async () => {
const { foldIntegrationCohortOutcomes } = await loadSubject();
const outcome = foldIntegrationCohortOutcomes({
provider,
verdicts: [{ status: "complete", imported: 0 }],
});
expect(outcome.did).toMatchObject({
provider,
outcome: "clean_zero",
total: 1,
users_synced: 1,
users_complete: 1,
users_useful: 0,
users_clean_zero: 1,
measurements_imported: 0,
});
});
it.each([
["failed", true],
["parked", false],
["skipped", false],
] as const)(
"never counts a %s user as synced",
async (status, retryable) => {
const { foldIntegrationCohortOutcomes } = await loadSubject();
const outcome = foldIntegrationCohortOutcomes({
provider,
verdicts: [{ status, imported: 0, retryable }],
});
expect(outcome.did.users_synced).toBe(0);
expect(outcome.did[`users_${status}`]).toBe(1);
expect(outcome.did.users_retryable).toBe(retryable ? 1 : 0);
},
);
it("marks raw writes plus a rollup/downstream failure as partial and retryable", async () => {
const { foldIntegrationCohortOutcomes } = await loadSubject();
const outcome = foldIntegrationCohortOutcomes({
provider,
verdicts: [
{
status: "partial",
imported: 7,
downstreamFailed: true,
retryable: true,
reasonCode: "rollup_failed",
},
],
});
expect(outcome.did).toMatchObject({
outcome: "useful",
users_synced: 0,
users_partial: 1,
users_retryable: 1,
downstream_failed: 1,
measurements_imported: 7,
});
});
it("never emits per-user identifiers, PII, secrets, URLs, raw errors, or health values", async () => {
const { foldIntegrationCohortOutcomes } = await loadSubject();
const privateVerdict = {
status: "failed",
imported: 0,
retryable: false,
reasonCode: "provider_failed",
userId: "user-private-123",
email: "patient@example.test",
accessToken: "secret-token",
url: "https://provider.test/capability?token=secret",
rawError: new Error("blood glucose=411"),
healthValue: 411,
} as UserVerdict;
const serialized = JSON.stringify(
foldIntegrationCohortOutcomes({
provider,
verdicts: [privateVerdict],
}),
);
expect(serialized).not.toMatch(
/user-private|patient@|secret-token|provider\.test|blood glucose|411|rawError|healthValue/i,
);
});
it("folds a high-volume cohort into one fixed bounded scalar payload", async () => {
const { foldIntegrationCohortOutcomes } = await loadSubject();
const verdicts = Array.from({ length: 10_000 }, (_, index) => ({
status: index % 2 === 0 ? ("complete" as const) : ("skipped" as const),
imported: index % 2 === 0 ? 1 : 0,
}));
const outcome = foldIntegrationCohortOutcomes({ provider, verdicts });
expect(Object.keys(outcome.did).sort()).toEqual(
[
"downstream_failed",
"measurements_imported",
"outcome",
"provider",
"total",
"users_clean_zero",
"users_complete",
"users_failed",
"users_parked",
"users_partial",
"users_retryable",
"users_skipped",
"users_synced",
"users_useful",
].sort(),
);
expect(
Object.values(outcome.did).every((value) => {
return (
typeof value === "string" ||
(typeof value === "number" &&
Number.isSafeInteger(value) &&
value >= 0)
);
}),
).toBe(true);
expect(JSON.stringify(outcome).length).toBeLessThan(1_024);
});
},
);