- Add withRateLimit(rateLimit, fn) in rate-limit/with-rate-limit.ts, attaching the RateLimited brand at DI bind time - Extend wireUseCase to accept optional rateLimit?: IRateLimit and compose withRateLimit innermost (before analytics/audit); propagate __rateLimited through analytics + audit inline wrappers - Extend withSpan and withCapture PROPAGATED_BRANDS to include __rateLimited so the outermost binding carries the brand - Extend assertFeatureConformance to require __rateLimited brand when manifest.useCases[name].rateLimit.length > 0; refactored into helper functions to stay within complexity thresholds - Add rateLimit?: IRateLimit to BindContext; default to NoopRateLimit in web-next bindAllProduction and bindAllDevSeed aggregators - Unit tests for withRateLimit brand attachment, factory passthrough, and composition; synthetic fixture tests for conformance errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
513 lines
13 KiB
TypeScript
513 lines
13 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import "reflect-metadata";
|
|
import { Container } from "inversify";
|
|
import { wireUseCase } from "@/conformance/wire-use-case";
|
|
import {
|
|
isInstrumented,
|
|
isCaptured,
|
|
isAudited,
|
|
isAnalyzed,
|
|
isRateLimited,
|
|
} from "@/conformance/brand-runtime";
|
|
import type {
|
|
ITracer,
|
|
ISpan,
|
|
SpanOpts,
|
|
} from "@/instrumentation/tracer.interface";
|
|
import type { ILogger } from "@/instrumentation/logger.interface";
|
|
import type { AuditLogProtocol, AnalyticsProtocol } from "@/di/bind-protocols";
|
|
import { NoopRateLimit } from "@/rate-limit/noop-rate-limit";
|
|
|
|
function makeTracer() {
|
|
const calls: SpanOpts[] = [];
|
|
const tracer: ITracer = {
|
|
startSpan: vi.fn(async (opts, fn) => {
|
|
calls.push(opts);
|
|
const span: ISpan = { setAttribute: () => {}, setStatus: () => {} };
|
|
return fn(span);
|
|
}),
|
|
};
|
|
return { tracer, calls };
|
|
}
|
|
|
|
function makeLogger(): ILogger & {
|
|
captureException: ReturnType<typeof vi.fn>;
|
|
} {
|
|
return {
|
|
captureException: vi.fn(),
|
|
captureMessage: vi.fn(),
|
|
addBreadcrumb: vi.fn(),
|
|
setUser: vi.fn(),
|
|
};
|
|
}
|
|
|
|
function makeAuditLog(): AuditLogProtocol {
|
|
return { record: vi.fn() };
|
|
}
|
|
|
|
function makeAnalytics(): AnalyticsProtocol {
|
|
return { track: vi.fn() };
|
|
}
|
|
|
|
const doubleFactory = () => async (x: number) => x * 2;
|
|
|
|
describe("wireUseCase — no-audit path", () => {
|
|
it("attaches Instrumented + Captured brands, no Audited brand", () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("test.double");
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "double",
|
|
tracer,
|
|
logger,
|
|
});
|
|
|
|
expect(isInstrumented(wired)).toBe(true);
|
|
expect(isCaptured(wired)).toBe(true);
|
|
expect(isAudited(wired)).toBe(false);
|
|
});
|
|
|
|
it("executes the factory result on invocation", async () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("test.double");
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "double",
|
|
tracer,
|
|
logger,
|
|
});
|
|
|
|
await expect(wired(3)).resolves.toBe(6);
|
|
});
|
|
});
|
|
|
|
describe("wireUseCase — audit path", () => {
|
|
it("attaches Instrumented + Captured + Audited brands when auditLog is provided", () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("test.double");
|
|
const auditLog = makeAuditLog();
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "double",
|
|
tracer,
|
|
logger,
|
|
auditLog,
|
|
});
|
|
|
|
expect(isInstrumented(wired)).toBe(true);
|
|
expect(isCaptured(wired)).toBe(true);
|
|
expect(isAudited(wired)).toBe(true);
|
|
});
|
|
|
|
it("executes the factory result on invocation (audit path)", async () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("test.double");
|
|
const auditLog = makeAuditLog();
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "double",
|
|
tracer,
|
|
logger,
|
|
auditLog,
|
|
});
|
|
|
|
await expect(wired(5)).resolves.toBe(10);
|
|
});
|
|
});
|
|
|
|
describe("wireUseCase — span-name derivation", () => {
|
|
it("derives span name as <feature>.<name> and uses layer as op", async () => {
|
|
const { tracer, calls } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("auth.signIn");
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "auth",
|
|
layer: "use-case",
|
|
name: "signIn",
|
|
tracer,
|
|
logger,
|
|
});
|
|
|
|
await wired(1);
|
|
expect(calls).toHaveLength(1);
|
|
expect(calls[0]).toMatchObject({ name: "auth.signIn", op: "use-case" });
|
|
});
|
|
});
|
|
|
|
describe("wireUseCase — capture-tag structure", () => {
|
|
it("captures with { feature, layer, name: '<feature>.<name>' } when an error is thrown", async () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("blog.getArticles");
|
|
const err = new Error("boom");
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: () => async () => {
|
|
throw err;
|
|
},
|
|
deps: [],
|
|
feature: "blog",
|
|
layer: "use-case",
|
|
name: "getArticles",
|
|
tracer,
|
|
logger,
|
|
});
|
|
|
|
await expect(wired()).rejects.toBe(err);
|
|
expect(logger.captureException).toHaveBeenCalledWith(err, {
|
|
tags: { feature: "blog", layer: "use-case", name: "blog.getArticles" },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("wireUseCase — container binding", () => {
|
|
it("binds the wired value to the container symbol", () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("test.double");
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "double",
|
|
tracer,
|
|
logger,
|
|
});
|
|
|
|
expect(container.get(sym)).toBe(wired);
|
|
});
|
|
|
|
it("passes deps tuple to the factory", async () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("test.scale");
|
|
|
|
const scaleFactory = (multiplier: number) => async (x: number) =>
|
|
x * multiplier;
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: scaleFactory,
|
|
deps: [3] as [number],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "scale",
|
|
tracer,
|
|
logger,
|
|
});
|
|
|
|
await expect(wired(4)).resolves.toBe(12);
|
|
});
|
|
});
|
|
|
|
describe("wireUseCase — analytics path", () => {
|
|
it("attaches Analyzed brand when analytics is provided", () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("test.double");
|
|
const analytics = makeAnalytics();
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "double",
|
|
tracer,
|
|
logger,
|
|
analytics,
|
|
});
|
|
|
|
expect(isAnalyzed(wired)).toBe(true);
|
|
expect(isInstrumented(wired)).toBe(true);
|
|
expect(isCaptured(wired)).toBe(true);
|
|
});
|
|
|
|
it("attaches Analyzed + Audited brands when both analytics and auditLog are provided", () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("test.double");
|
|
const analytics = makeAnalytics();
|
|
const auditLog = makeAuditLog();
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "double",
|
|
tracer,
|
|
logger,
|
|
analytics,
|
|
auditLog,
|
|
});
|
|
|
|
expect(isAnalyzed(wired)).toBe(true);
|
|
expect(isAudited(wired)).toBe(true);
|
|
expect(isInstrumented(wired)).toBe(true);
|
|
expect(isCaptured(wired)).toBe(true);
|
|
});
|
|
|
|
it("executes the factory result on invocation (analytics path)", async () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("test.double");
|
|
const analytics = makeAnalytics();
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "double",
|
|
tracer,
|
|
logger,
|
|
analytics,
|
|
});
|
|
|
|
await expect(wired(7)).resolves.toBe(14);
|
|
});
|
|
});
|
|
|
|
describe("wireUseCase — no-analytics path", () => {
|
|
it("does not attach Analyzed brand when analytics is absent", () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("test.double");
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "double",
|
|
tracer,
|
|
logger,
|
|
});
|
|
|
|
expect(isAnalyzed(wired)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("wireUseCase — idempotent re-bind", () => {
|
|
it("unbinds the existing binding and replaces it when the symbol is already bound", () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("test.double");
|
|
|
|
wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "double",
|
|
tracer,
|
|
logger,
|
|
});
|
|
|
|
const second = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "double",
|
|
tracer,
|
|
logger,
|
|
});
|
|
|
|
expect(container.get(sym)).toBe(second);
|
|
});
|
|
});
|
|
|
|
describe("wireUseCase — rate limit path", () => {
|
|
it("attaches RateLimited brand when rateLimit is provided", () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("test.double");
|
|
const rateLimit = new NoopRateLimit();
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "double",
|
|
tracer,
|
|
logger,
|
|
rateLimit,
|
|
});
|
|
|
|
expect(isRateLimited(wired)).toBe(true);
|
|
expect(isInstrumented(wired)).toBe(true);
|
|
expect(isCaptured(wired)).toBe(true);
|
|
});
|
|
|
|
it("does not attach RateLimited brand when rateLimit is absent", () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("test.double");
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "double",
|
|
tracer,
|
|
logger,
|
|
});
|
|
|
|
expect(isRateLimited(wired)).toBe(false);
|
|
});
|
|
|
|
it("executes factory correctly when rateLimit is provided", async () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("test.double");
|
|
const rateLimit = new NoopRateLimit();
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "double",
|
|
tracer,
|
|
logger,
|
|
rateLimit,
|
|
});
|
|
|
|
await expect(wired(5)).resolves.toBe(10);
|
|
});
|
|
|
|
it("attaches RateLimited + Analyzed brands when both rateLimit and analytics are provided", () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("test.double");
|
|
const rateLimit = new NoopRateLimit();
|
|
const analytics = makeAnalytics();
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "double",
|
|
tracer,
|
|
logger,
|
|
rateLimit,
|
|
analytics,
|
|
});
|
|
|
|
expect(isRateLimited(wired)).toBe(true);
|
|
expect(isAnalyzed(wired)).toBe(true);
|
|
expect(isInstrumented(wired)).toBe(true);
|
|
expect(isCaptured(wired)).toBe(true);
|
|
});
|
|
|
|
it("attaches RateLimited + Audited brands when both rateLimit and auditLog are provided", () => {
|
|
const { tracer } = makeTracer();
|
|
const logger = makeLogger();
|
|
const container = new Container();
|
|
const sym = Symbol("test.double");
|
|
const rateLimit = new NoopRateLimit();
|
|
const auditLog = makeAuditLog();
|
|
|
|
const wired = wireUseCase({
|
|
container,
|
|
symbol: sym,
|
|
factory: doubleFactory,
|
|
deps: [],
|
|
feature: "test",
|
|
layer: "use-case",
|
|
name: "double",
|
|
tracer,
|
|
logger,
|
|
rateLimit,
|
|
auditLog,
|
|
});
|
|
|
|
expect(isRateLimited(wired)).toBe(true);
|
|
expect(isAudited(wired)).toBe(true);
|
|
expect(isInstrumented(wired)).toBe(true);
|
|
expect(isCaptured(wired)).toBe(true);
|
|
});
|
|
});
|