feat(core-audit): withAudit wraps and attaches runtime __audited marker
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { describe, it, expect, expectTypeOf, vi } from "vitest";
|
import { describe, it, expect, expectTypeOf, vi } from "vitest";
|
||||||
import { withAudit, type Audited } from "@/with-audit";
|
import { withAudit, type Audited } from "@/with-audit";
|
||||||
import type { IAuditLog } from "@/audit-log.interface";
|
import type { IAuditLog } from "@/audit-log.interface";
|
||||||
|
import { isAudited } from "@repo/core-shared/conformance";
|
||||||
|
|
||||||
function makeAuditLog(): IAuditLog {
|
function makeAuditLog(): IAuditLog {
|
||||||
return {
|
return {
|
||||||
@@ -17,6 +18,22 @@ describe("withAudit", () => {
|
|||||||
expectTypeOf(wrapped).toMatchTypeOf<Audited<typeof fn>>();
|
expectTypeOf(wrapped).toMatchTypeOf<Audited<typeof fn>>();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("attaches __audited as a non-enumerable property on the wrapped function", () => {
|
||||||
|
const auditLog = makeAuditLog();
|
||||||
|
const fn = async () => ({ ok: true });
|
||||||
|
const wrapped = withAudit(auditLog, fn);
|
||||||
|
expect(isAudited(wrapped)).toBe(true);
|
||||||
|
expect(Object.keys(wrapped)).not.toContain("__audited");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT pollute the original input function with the brand", () => {
|
||||||
|
const auditLog = makeAuditLog();
|
||||||
|
const fn = async () => ({ ok: true });
|
||||||
|
const wrapped = withAudit(auditLog, fn);
|
||||||
|
expect(isAudited(fn)).toBe(false);
|
||||||
|
expect(wrapped).not.toBe(fn);
|
||||||
|
});
|
||||||
|
|
||||||
it("passes input and output through unchanged", async () => {
|
it("passes input and output through unchanged", async () => {
|
||||||
const auditLog = makeAuditLog();
|
const auditLog = makeAuditLog();
|
||||||
const fn = async (input: { id: string }) => ({ ok: true, id: input.id });
|
const fn = async (input: { id: string }) => ({ ok: true, id: input.id });
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
import type { IAuditLog } from "./audit-log.interface";
|
import type { IAuditLog } from "./audit-log.interface";
|
||||||
|
import { attachBrand } from "@repo/core-shared/conformance";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Phantom-type brand attached at wrap time by `withAudit`. The conformance
|
* Phantom-type brand attached at wrap time by `withAudit`. The conformance
|
||||||
* system uses this as the type-level seam for mutating use cases that
|
* system uses this as the type-level seam for mutating use cases that
|
||||||
* declare `audits: [...]` in their manifest — without `__audited`, the
|
* declare `audits: [...]` in their manifest — without `__audited`, the
|
||||||
* binding is not assignable to `ProductionUseCase<I, O, M>` when M demands
|
* binding is not assignable to `ProductionUseCase<I, O, M>` when M demands
|
||||||
* it.
|
* it. At runtime the brand is a non-enumerable property attached by
|
||||||
|
* `attachBrand` from `@repo/core-shared/conformance`, so the boot-time
|
||||||
|
* assertion can verify the binding went through the audit-aware path.
|
||||||
*/
|
*/
|
||||||
export type Audited<F> = F & { readonly __audited: true };
|
export type Audited<F> = F & { readonly __audited: true };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use-case wrapper applied at DI bind time. In milestone i this is a
|
* Use-case wrapper applied at DI bind time. The wrapper is a thin closure
|
||||||
* brand-only attachment: it does not yet automatically call `auditLog.record`.
|
* that forwards to `fn` unchanged and carries the `__audited` brand. The
|
||||||
* Use cases continue to call `auditLog.record(...)` in their own bodies; the
|
* forward closure (instead of returning `fn` directly) keeps the brand on
|
||||||
* wrapper exists to make "binding was bound through the audit-aware path"
|
* a fresh function so the caller's original `fn` is not mutated — important
|
||||||
* type-checkable at compile time.
|
* when the same factory output is used elsewhere unwrapped (dev-seed paths,
|
||||||
*
|
* tests).
|
||||||
* A future story may move auditing logic out of factory bodies and into the
|
|
||||||
* wrapper itself (driven by manifest declarations) — but that requires the
|
|
||||||
* manifest's `audits[]` entries to fully specify what gets recorded, which
|
|
||||||
* is out of scope here.
|
|
||||||
*/
|
*/
|
||||||
export function withAudit<Args extends unknown[], R>(
|
export function withAudit<Args extends unknown[], R>(
|
||||||
// TODO(conformance milestone iii+): wire automated recording from manifest
|
// TODO(conformance milestone iii+): wire automated recording from manifest
|
||||||
@@ -31,5 +30,7 @@ export function withAudit<Args extends unknown[], R>(
|
|||||||
fn: (...args: Args) => Promise<R>,
|
fn: (...args: Args) => Promise<R>,
|
||||||
): Audited<(...args: Args) => Promise<R>> {
|
): Audited<(...args: Args) => Promise<R>> {
|
||||||
void auditLog;
|
void auditLog;
|
||||||
return fn as Audited<(...args: Args) => Promise<R>>;
|
const wrapped: (...args: Args) => Promise<R> = (...args) => fn(...args);
|
||||||
|
attachBrand(wrapped, "__audited");
|
||||||
|
return wrapped as Audited<(...args: Args) => Promise<R>>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user