Initial Commit
This commit is contained in:
212
packages/api-stack/api/api-saga/tests/setup/test-helpers.ts
Normal file
212
packages/api-stack/api/api-saga/tests/setup/test-helpers.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
// Test-only helpers — co-located with the orchestrator tests so the
|
||||
// recovery test and the conformance test can share them. Excluded
|
||||
// from production builds via the vitest config's
|
||||
// `*.types.ts`/`*.test.ts`-only barrel rules.
|
||||
|
||||
import { Database } from "bun:sqlite";
|
||||
import type { Principal, RequestContext } from "@suaveplan/api-types";
|
||||
import { EventBus } from "@suaveplan/events";
|
||||
import {
|
||||
IdempotencyGuard,
|
||||
MemoryIdempotencyStore,
|
||||
} from "@suaveplan/idempotency";
|
||||
import type { SagaEventMap } from "../../src/events/events.types.js";
|
||||
import { createSagaInstruments } from "../../src/instruments/instruments.js";
|
||||
import type { SagaInstruments } from "../../src/instruments/instruments.types.js";
|
||||
import { createSagaOrchestrator } from "../../src/orchestrator/orchestrator.js";
|
||||
import type {
|
||||
SagaOrchestrator,
|
||||
SagaRequestContextFactory,
|
||||
} from "../../src/orchestrator/orchestrator.types.js";
|
||||
import type { LedgerDb } from "../../src/persistence/persistence.types.js";
|
||||
import {
|
||||
defineSagaParticipant,
|
||||
SagaParticipantRegistry,
|
||||
} from "../../src/registry/participant-registry.js";
|
||||
import type {
|
||||
SagaActionHandler,
|
||||
SagaCompensateHandler,
|
||||
SagaOwnerDeclaration,
|
||||
} from "../../src/registry/participant-registry.types.js";
|
||||
import { applySagaSchema } from "./saga-schema.js";
|
||||
|
||||
function buildNoopMeter(): SagaInstruments["meter"] {
|
||||
return {
|
||||
createCounter() {
|
||||
return { add() {} } as ReturnType<
|
||||
SagaInstruments["meter"]["createCounter"]
|
||||
>;
|
||||
},
|
||||
createHistogram() {
|
||||
return { record() {} } as ReturnType<
|
||||
SagaInstruments["meter"]["createHistogram"]
|
||||
>;
|
||||
},
|
||||
createObservableGauge() {
|
||||
return {
|
||||
addCallback() {},
|
||||
removeCallback() {},
|
||||
} as unknown as ReturnType<
|
||||
SagaInstruments["meter"]["createObservableGauge"]
|
||||
>;
|
||||
},
|
||||
} as unknown as SagaInstruments["meter"];
|
||||
}
|
||||
|
||||
function buildNoopTracer(): SagaInstruments["tracer"] {
|
||||
return {
|
||||
startSpan() {
|
||||
return {
|
||||
end() {},
|
||||
setAttribute() {
|
||||
return this;
|
||||
},
|
||||
setAttributes() {
|
||||
return this;
|
||||
},
|
||||
setStatus() {
|
||||
return this;
|
||||
},
|
||||
recordException() {},
|
||||
isRecording() {
|
||||
return false;
|
||||
},
|
||||
updateName() {
|
||||
return this;
|
||||
},
|
||||
addEvent() {
|
||||
return this;
|
||||
},
|
||||
spanContext() {
|
||||
return { traceId: "t", spanId: "s", traceFlags: 0 };
|
||||
},
|
||||
} as ReturnType<SagaInstruments["tracer"]["startSpan"]>;
|
||||
},
|
||||
} as unknown as SagaInstruments["tracer"];
|
||||
}
|
||||
|
||||
export function buildTestRequestContextFactory(): SagaRequestContextFactory {
|
||||
return ({ instance, stepName, principal, targetTenantId }) =>
|
||||
({
|
||||
instance,
|
||||
stepName,
|
||||
principal,
|
||||
tenantId: targetTenantId,
|
||||
tenant: { id: targetTenantId, slug: targetTenantId },
|
||||
request: new Request("http://saga.local/"),
|
||||
requestId: `req-${instance.id}-${stepName}`,
|
||||
span: {
|
||||
setAttribute() {},
|
||||
setStatus() {},
|
||||
recordException() {},
|
||||
end() {},
|
||||
},
|
||||
db: null,
|
||||
secrets: {
|
||||
require() {
|
||||
throw new Error("no secrets");
|
||||
},
|
||||
},
|
||||
capabilities: {},
|
||||
log: () => undefined,
|
||||
now: () => Date.now(),
|
||||
requireSession: () => null,
|
||||
}) as unknown as RequestContext;
|
||||
}
|
||||
|
||||
export interface TestStepSpec {
|
||||
readonly plugin: string;
|
||||
readonly action: string;
|
||||
readonly compensate: string;
|
||||
readonly handler: SagaActionHandler;
|
||||
readonly compensateHandler?: SagaCompensateHandler;
|
||||
readonly idempotency?: "auto" | "manual";
|
||||
readonly timeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface TestHarness {
|
||||
readonly orchestrator: SagaOrchestrator;
|
||||
readonly registry: SagaParticipantRegistry;
|
||||
readonly bus: EventBus<SagaEventMap>;
|
||||
readonly events: Map<keyof SagaEventMap, unknown[]>;
|
||||
readonly ledgerDb: LedgerDb;
|
||||
readonly instruments: SagaInstruments;
|
||||
}
|
||||
|
||||
export function makeHarness(args: {
|
||||
readonly owner: SagaOwnerDeclaration;
|
||||
readonly steps: readonly TestStepSpec[];
|
||||
readonly principal?: Principal;
|
||||
}): TestHarness {
|
||||
const db = new Database(":memory:") as unknown as LedgerDb;
|
||||
applySagaSchema(db);
|
||||
|
||||
const registry = new SagaParticipantRegistry();
|
||||
for (const s of args.steps) {
|
||||
registry.registerParticipant(
|
||||
defineSagaParticipant({
|
||||
plugin: s.plugin,
|
||||
action: s.action,
|
||||
handler: s.handler,
|
||||
...(s.compensateHandler !== undefined
|
||||
? { compensate: s.compensateHandler }
|
||||
: {}),
|
||||
compensateName: s.compensate,
|
||||
...(s.idempotency !== undefined
|
||||
? { idempotency: s.idempotency }
|
||||
: {}),
|
||||
...(s.timeoutMs !== undefined
|
||||
? { timeoutMs: s.timeoutMs }
|
||||
: {}),
|
||||
})
|
||||
);
|
||||
}
|
||||
registry.registerOwner("owner-plugin", args.owner);
|
||||
|
||||
const bus = new EventBus<SagaEventMap>();
|
||||
const events = new Map<keyof SagaEventMap, unknown[]>();
|
||||
const eventNames: ReadonlyArray<keyof SagaEventMap> = [
|
||||
"saga.step.requested",
|
||||
"saga.step.completed",
|
||||
"saga.step.failed",
|
||||
"saga.step.compensated",
|
||||
"saga.instance.started",
|
||||
"saga.instance.completed",
|
||||
"saga.instance.compensated",
|
||||
"saga.instance.compensation_failed",
|
||||
];
|
||||
for (const n of eventNames) {
|
||||
events.set(n, []);
|
||||
bus.on(n, (payload) => {
|
||||
events.get(n)?.push(payload);
|
||||
});
|
||||
}
|
||||
|
||||
const idempotency = new IdempotencyGuard({
|
||||
store: new MemoryIdempotencyStore(),
|
||||
});
|
||||
const instruments = createSagaInstruments(
|
||||
buildNoopMeter(),
|
||||
buildNoopTracer()
|
||||
);
|
||||
|
||||
const orchestrator = createSagaOrchestrator({
|
||||
ledgerDb: db,
|
||||
registry,
|
||||
bus,
|
||||
idempotency,
|
||||
instruments,
|
||||
requestContextFactory: buildTestRequestContextFactory(),
|
||||
idGenerator: makeSequentialIdGen(),
|
||||
});
|
||||
|
||||
return { orchestrator, registry, bus, events, ledgerDb: db, instruments };
|
||||
}
|
||||
|
||||
function makeSequentialIdGen(): () => string {
|
||||
let n = 0;
|
||||
return () => {
|
||||
n += 1;
|
||||
return `saga-${n}`;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user