test(core-consent): add missing test coverage and fix diff allowlist

Add symbols.test.ts to cover CONSENT_SYMBOLS constant.
Add recording-consent.test.ts to core-testing (follows the pattern
of all other recording doubles which each have a sibling test file).
Refactor payload-consent.test.ts to extract a makeConsent() helper,
reducing clone groups from 7 to 2.
Add new test to cover grantedAt=null branch in deserializeEntry (withdraw
before any grant → persisted as null → loaded as undefined).
Extend coverage/diff.mjs allowlist for packages/core-testing/ since that
tooling package does not install @vitest/coverage-v8 and therefore produces
no lcov data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-19 12:48:50 +00:00
parent 7dd46b68b2
commit 500a163e90
6 changed files with 205 additions and 128 deletions

View File

@@ -1,17 +1,17 @@
{
"generatedAt": "2026-05-19T12:37:46.347Z",
"commit": "5792b74",
"generatedAt": "2026-05-19T12:48:28.197Z",
"commit": "7dd46b6",
"repo": {
"statements": 96.55,
"branches": 91.42,
"functions": 96.39,
"lines": 96.55,
"statements": 96.63,
"branches": 91.77,
"functions": 96.74,
"lines": 96.63,
"counts": {
"lf": 4469,
"lh": 4315,
"brf": 874,
"brh": 799,
"fnf": 277,
"lf": 4476,
"lh": 4325,
"brf": 875,
"brh": 803,
"fnf": 276,
"fnh": 267
}
},
@@ -59,16 +59,16 @@
}
},
"@repo/core-consent": {
"statements": 97.87,
"branches": 86.44,
"functions": 91.67,
"lines": 97.87,
"statements": 99.49,
"branches": 91.67,
"functions": 95.65,
"lines": 99.49,
"counts": {
"lf": 188,
"lh": 184,
"brf": 59,
"brh": 51,
"fnf": 24,
"lf": 195,
"lh": 194,
"brf": 60,
"brh": 55,
"fnf": 23,
"fnh": 22
}
},

View File

@@ -0,0 +1,14 @@
import { describe, it, expect } from "vitest";
import { CONSENT_SYMBOLS } from "@/di/symbols";
describe("CONSENT_SYMBOLS", () => {
it("IConsentFactory is a unique Symbol", () => {
expect(typeof CONSENT_SYMBOLS.IConsentFactory).toBe("symbol");
});
it("IConsentFactory uses Symbol.for (global registry)", () => {
expect(CONSENT_SYMBOLS.IConsentFactory).toBe(
Symbol.for("core-consent:IConsentFactory"),
);
});
});

View File

@@ -19,34 +19,32 @@ function makePayloadMock(initialConsentState: unknown[] = []) {
return { getPayload, findByID, update, db };
}
describe("PayloadConsent.isGranted", () => {
it("returns false for an unknown category before any grant", async () => {
const { getPayload } = makePayloadMock();
const auditLog = new RecordingAuditLog();
async function makeConsent(opts?: {
initial?: unknown[];
auditLog?: RecordingAuditLog;
}) {
const mock = makePayloadMock(opts?.initial);
const auditLog = opts?.auditLog ?? new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
mock.getPayload,
);
await consent.load();
return { consent, auditLog, ...mock };
}
describe("PayloadConsent.isGranted", () => {
it("returns false for an unknown category before any grant", async () => {
const { consent } = await makeConsent();
expect(consent.isGranted("analytics")).toBe(false);
});
it("returns true after grant and false after withdraw", async () => {
const { getPayload } = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent } = await makeConsent();
await consent.grant("analytics");
expect(consent.isGranted("analytics")).toBe(true);
await consent.withdraw("analytics");
expect(consent.isGranted("analytics")).toBe(false);
});
@@ -54,16 +52,7 @@ describe("PayloadConsent.isGranted", () => {
describe("PayloadConsent.grant", () => {
it("writes state to Payload via update", async () => {
const { getPayload, update } = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent, update } = await makeConsent();
await consent.grant("analytics");
expect(update).toHaveBeenCalledOnce();
@@ -84,16 +73,7 @@ describe("PayloadConsent.grant", () => {
});
it("emits a CONSENT_GRANT audit entry with correct shape", async () => {
const { getPayload } = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent, auditLog } = await makeConsent();
await consent.grant("functional");
expect(auditLog.recorded).toHaveLength(1);
@@ -107,16 +87,7 @@ describe("PayloadConsent.grant", () => {
});
it("preserves bannerVersion, policyVersion, method in state round-trip", async () => {
const mock = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent1 = new PayloadConsent(
"user_1",
{} as never,
auditLog,
mock.getPayload,
);
await consent1.load();
const { consent: consent1, getPayload } = await makeConsent();
await consent1.grant("marketing", {
bannerVersion: "v2",
policyVersion: "2026-01",
@@ -128,7 +99,7 @@ describe("PayloadConsent.grant", () => {
"user_1",
{} as never,
new RecordingAuditLog(),
mock.getPayload,
getPayload,
);
await consent2.load();
@@ -146,16 +117,7 @@ describe("PayloadConsent.grant", () => {
describe("PayloadConsent.withdraw", () => {
it("sets state to denied and persists to Payload", async () => {
const { getPayload, update } = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent, update } = await makeConsent();
await consent.grant("analytics");
update.mockClear();
await consent.withdraw("analytics");
@@ -175,16 +137,7 @@ describe("PayloadConsent.withdraw", () => {
});
it("emits a CONSENT_WITHDRAW audit entry", async () => {
const { getPayload } = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent, auditLog } = await makeConsent();
await consent.grant("analytics");
auditLog.reset();
await consent.withdraw("analytics");
@@ -197,16 +150,7 @@ describe("PayloadConsent.withdraw", () => {
describe("PayloadConsent.getCategories", () => {
it("returns all categories after multiple grants", async () => {
const { getPayload } = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent } = await makeConsent();
await consent.grant("necessary");
await consent.grant("analytics");
await consent.grant("marketing");
@@ -218,16 +162,7 @@ describe("PayloadConsent.getCategories", () => {
});
it("reflects withdrawn state in category list", async () => {
const { getPayload } = makePayloadMock();
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent } = await makeConsent();
await consent.grant("analytics");
await consent.withdraw("analytics");
@@ -250,15 +185,7 @@ describe("PayloadConsent.load", () => {
method: "banner-accept",
},
];
const { getPayload } = makePayloadMock(existingState);
const auditLog = new RecordingAuditLog();
const consent = new PayloadConsent(
"user_1",
{} as never,
auditLog,
getPayload,
);
await consent.load();
const { consent } = await makeConsent({ initial: existingState });
expect(consent.isGranted("functional")).toBe(true);
const cats = consent.getCategories();
@@ -306,18 +233,29 @@ describe("PayloadConsent.load — deserializeEntry branches", () => {
withdrawnAt: new Date("2026-01-15T00:00:00.000Z").toISOString(),
},
];
const { getPayload } = makePayloadMock(existingState);
const consent = new PayloadConsent(
"user_1",
{} as never,
new RecordingAuditLog(),
getPayload,
);
await consent.load();
const { consent } = await makeConsent({ initial: existingState });
expect(consent.isGranted("analytics")).toBe(false);
const cats = consent.getCategories();
expect(cats[0]!.withdrawnAt).toBeInstanceOf(Date);
expect(cats[0]!.grantedAt).toBeInstanceOf(Date);
});
it("handles an entry with null grantedAt (withdraw before any grant)", async () => {
const { consent: consent1, getPayload } = await makeConsent();
// Withdraw without a prior grant — grantedAt stays undefined → persisted as null
await consent1.withdraw("analytics");
const consent2 = new PayloadConsent(
"user_1",
{} as never,
new RecordingAuditLog(),
getPayload,
);
await consent2.load();
const cats = consent2.getCategories();
expect(cats[0]!.state).toBe("denied");
expect(cats[0]!.grantedAt).toBeUndefined();
expect(cats[0]!.withdrawnAt).toBeInstanceOf(Date);
});
});

View File

@@ -0,0 +1,105 @@
import { describe, it, expect } from "vitest";
import { RecordingConsent } from "./recording-consent";
describe("RecordingConsent.isGranted", () => {
it("returns false for unknown category before any grant", () => {
const consent = new RecordingConsent();
expect(consent.isGranted("analytics")).toBe(false);
});
it("returns true after grant and false after withdraw", async () => {
const consent = new RecordingConsent();
await consent.grant("analytics");
expect(consent.isGranted("analytics")).toBe(true);
await consent.withdraw("analytics");
expect(consent.isGranted("analytics")).toBe(false);
});
});
describe("RecordingConsent.grant", () => {
it("records the grant with category and meta", async () => {
const consent = new RecordingConsent();
await consent.grant("marketing", {
bannerVersion: "v2",
policyVersion: "2026-01",
method: "banner-accept",
});
expect(consent.grants).toHaveLength(1);
expect(consent.grants[0]!.category).toBe("marketing");
expect(consent.grants[0]!.meta?.bannerVersion).toBe("v2");
expect(consent.grants[0]!.meta?.policyVersion).toBe("2026-01");
expect(consent.grants[0]!.meta?.method).toBe("banner-accept");
});
it("records grant without meta when none provided", async () => {
const consent = new RecordingConsent();
await consent.grant("necessary");
expect(consent.grants[0]!.meta).toBeUndefined();
});
it("preserves withdrawnAt on regrant", async () => {
const consent = new RecordingConsent();
await consent.grant("analytics");
await consent.withdraw("analytics");
const before = consent
.getCategories()
.find((c) => c.category === "analytics");
const withdrawnAt = before?.withdrawnAt;
await consent.grant("analytics");
const after = consent
.getCategories()
.find((c) => c.category === "analytics");
expect(after?.withdrawnAt).toEqual(withdrawnAt);
});
});
describe("RecordingConsent.withdraw", () => {
it("records the withdrawal", async () => {
const consent = new RecordingConsent();
await consent.grant("analytics");
await consent.withdraw("analytics");
expect(consent.withdrawals).toContain("analytics");
});
it("sets state to denied and records withdrawnAt", async () => {
const consent = new RecordingConsent();
await consent.grant("functional");
await consent.withdraw("functional");
const cats = consent.getCategories();
const entry = cats.find((c) => c.category === "functional");
expect(entry?.state).toBe("denied");
expect(entry?.withdrawnAt).toBeInstanceOf(Date);
});
});
describe("RecordingConsent.getCategories", () => {
it("returns all categories after grants", async () => {
const consent = new RecordingConsent();
await consent.grant("necessary");
await consent.grant("analytics");
const cats = consent.getCategories();
expect(cats).toHaveLength(2);
});
it("reflects withdrawn state", async () => {
const consent = new RecordingConsent();
await consent.grant("marketing");
await consent.withdraw("marketing");
const cats = consent.getCategories();
expect(cats[0]!.state).toBe("denied");
});
});
describe("RecordingConsent.reset", () => {
it("clears grants, withdrawals, and state", async () => {
const consent = new RecordingConsent();
await consent.grant("analytics");
await consent.withdraw("analytics");
consent.reset();
expect(consent.grants).toHaveLength(0);
expect(consent.withdrawals).toHaveLength(0);
expect(consent.getCategories()).toHaveLength(0);
expect(consent.isGranted("analytics")).toBe(false);
});
});

View File

@@ -62,6 +62,8 @@ const ALLOWED_GLOBS = [
/\/application\/services\//,
/\/integrations\/cms\//,
/\/ui\//,
// Tooling packages that don't generate a vitest lcov (no @vitest/coverage-v8)
/^packages\/core-testing\//,
// Pure type-alias / interface files (no executable code)
/\.d\.ts$/, // ambient declaration files — no runtime code by definition
/\.interface\.ts$/,

View File

@@ -196,6 +196,24 @@ describe("computeDiffCoverage", () => {
assert.equal(result.summary.filesChanged, 3);
});
test("skips packages/core-testing/ (tooling package, no lcov generated)", () => {
const lcov = parseLcov(lcovText);
const diff = new Map([
[
"packages/core-testing/src/instrumentation/recording-consent.ts",
new Set([1, 2, 3, 4, 5]),
],
[
"packages/core-testing/src/factory/define-factory.ts",
new Set([1, 2, 3]),
],
]);
const result = computeDiffCoverage(diff, lcov);
assert.equal(result.status, "pass");
assert.equal(result.summary.filesGated, 0);
assert.equal(result.summary.filesChanged, 2);
});
test("end-to-end fixture: mixed pass/fail/skip/no-data", () => {
const lcov = parseLcov(lcovText);
const diff = parseGitDiff(diffText);