@gkoos/caracal 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
1
+ import { operation } from "../../src/core/operation.js"
2
+ import type {
3
+ Adapter,
4
+ Classification,
5
+ OperationCapabilities,
6
+ OperationEvent,
7
+ Outcome,
8
+ } from "../../src/core/types.js"
9
+
10
+ export interface AdapterContractSuccess<Args, Result> {
11
+ readonly args: Args
12
+ readonly assertResult?: (result: Result) => void | Promise<void>
13
+ }
14
+
15
+ export interface AdapterContractCapabilityCase<Args> {
16
+ readonly args: Args
17
+ readonly expected: OperationCapabilities
18
+ }
19
+
20
+ export interface AdapterContractClassificationCase<Result> {
21
+ readonly outcome: Outcome<Result>
22
+ readonly expected: Classification
23
+ }
24
+
25
+ export interface AdapterContractAbortCase<Args> {
26
+ readonly args: Args
27
+ readonly verify: (controls: {
28
+ readonly controller: AbortController
29
+ readonly execute: () => Promise<unknown>
30
+ }) => void | Promise<void>
31
+ }
32
+
33
+ export interface AdapterContractOptions<Args, Result> {
34
+ readonly name: string
35
+ readonly adapter: Adapter<Args, Result>
36
+ readonly success: AdapterContractSuccess<Args, Result>
37
+ readonly capabilities: readonly AdapterContractCapabilityCase<Args>[]
38
+ readonly classifications?: readonly AdapterContractClassificationCase<Result>[]
39
+ readonly abort?: AdapterContractAbortCase<Args>
40
+ }
41
+
42
+ export interface AdapterContractCheck {
43
+ readonly name: string
44
+ run(): Promise<void>
45
+ }
46
+
47
+ export interface AdapterContractSuite {
48
+ readonly name: string
49
+ readonly checks: readonly AdapterContractCheck[]
50
+ }
51
+
52
+ function assertEqual<T>(actual: T, expected: T, message: string): void {
53
+ if (!Object.is(actual, expected)) {
54
+ throw new Error(
55
+ `${message}: expected ${String(expected)}, received ${String(actual)}`,
56
+ )
57
+ }
58
+ }
59
+
60
+ function assertLifecycle(events: readonly OperationEvent[]): void {
61
+ const types = events.map((event) => event.type)
62
+ const expected = [
63
+ "execution.started",
64
+ "attempt.started",
65
+ "attempt.settled",
66
+ "execution.settled",
67
+ ]
68
+ if (
69
+ types.length !== expected.length ||
70
+ types.some((type, index) => type !== expected[index])
71
+ ) {
72
+ throw new Error(
73
+ `expected operation lifecycle ${expected.join(" -> ")}; received ${types.join(" -> ")}`,
74
+ )
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Returns runner-agnostic checks for a third-party adapter. Register each
80
+ * check with the application's test runner; this module imports no test runner.
81
+ */
82
+ export function defineAdapterContractSuite<Args, Result>(
83
+ options: AdapterContractOptions<Args, Result>,
84
+ ): AdapterContractSuite {
85
+ const checks: AdapterContractCheck[] = options.capabilities.map(
86
+ (capabilityCase, index) => ({
87
+ name: `${options.name}: capabilities ${index + 1}`,
88
+ async run(): Promise<void> {
89
+ const actual = options.adapter.capabilities(capabilityCase.args)
90
+ assertEqual(
91
+ actual.abort,
92
+ capabilityCase.expected.abort,
93
+ "abort capability",
94
+ )
95
+ assertEqual(
96
+ actual.replay,
97
+ capabilityCase.expected.replay,
98
+ "replay capability",
99
+ )
100
+ },
101
+ }),
102
+ )
103
+
104
+ checks.push({
105
+ name: `${options.name}: successful operation lifecycle`,
106
+ async run(): Promise<void> {
107
+ const events: OperationEvent[] = []
108
+ const subject = operation({
109
+ name: `adapter-contract:${options.name}`,
110
+ adapter: options.adapter,
111
+ events: { emit: (event) => events.push(event) },
112
+ })
113
+ const result = await subject.execute(options.success.args, {
114
+ executionId: "adapter-contract",
115
+ })
116
+ await options.success.assertResult?.(result)
117
+ assertLifecycle(events)
118
+ },
119
+ })
120
+
121
+ for (const [index, classificationCase] of (
122
+ options.classifications ?? []
123
+ ).entries()) {
124
+ checks.push({
125
+ name: `${options.name}: classification ${index + 1}`,
126
+ async run(): Promise<void> {
127
+ const actual =
128
+ options.adapter.classify?.(classificationCase.outcome) ??
129
+ (classificationCase.outcome.status === "success"
130
+ ? "success"
131
+ : "failure")
132
+ assertEqual(
133
+ actual,
134
+ classificationCase.expected,
135
+ "outcome classification",
136
+ )
137
+ },
138
+ })
139
+ }
140
+
141
+ if (options.abort !== undefined) {
142
+ checks.push({
143
+ name: `${options.name}: abort behavior`,
144
+ async run(): Promise<void> {
145
+ const controller = new AbortController()
146
+ const subject = operation({
147
+ name: `adapter-contract:${options.name}`,
148
+ adapter: options.adapter,
149
+ })
150
+ await options.abort?.verify({
151
+ controller,
152
+ execute: () =>
153
+ subject.execute(options.abort?.args as Args, {
154
+ signal: controller.signal,
155
+ }),
156
+ })
157
+ },
158
+ })
159
+ }
160
+
161
+ return Object.freeze({ name: options.name, checks: Object.freeze(checks) })
162
+ }
163
+
164
+ export async function runAdapterContractSuite(
165
+ suite: AdapterContractSuite,
166
+ ): Promise<void> {
167
+ for (const check of suite.checks) {
168
+ await check.run()
169
+ }
170
+ }
@@ -0,0 +1,15 @@
1
+ /** Runner-agnostic adapter contract checks; source remains outside src/. */
2
+
3
+ export type {
4
+ AdapterContractAbortCase,
5
+ AdapterContractCapabilityCase,
6
+ AdapterContractCheck,
7
+ AdapterContractClassificationCase,
8
+ AdapterContractOptions,
9
+ AdapterContractSuccess,
10
+ AdapterContractSuite,
11
+ } from "./adapter-contract.js"
12
+ export {
13
+ defineAdapterContractSuite,
14
+ runAdapterContractSuite,
15
+ } from "./adapter-contract.js"