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:
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
14
packages/core-consent/src/di/symbols.test.ts
Normal file
14
packages/core-consent/src/di/symbols.test.ts
Normal 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"),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -19,34 +19,32 @@ function makePayloadMock(initialConsentState: unknown[] = []) {
|
||||
return { getPayload, findByID, update, db };
|
||||
}
|
||||
|
||||
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,
|
||||
mock.getPayload,
|
||||
);
|
||||
await consent.load();
|
||||
return { consent, auditLog, ...mock };
|
||||
}
|
||||
|
||||
describe("PayloadConsent.isGranted", () => {
|
||||
it("returns false for an unknown category before any grant", 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();
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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$/,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user