@avelonjs/conformance 0.1.0

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,119 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ Invalid,
4
+ type NotificationCapabilities,
5
+ type NotificationChannel,
6
+ type NotificationDriver,
7
+ type NotificationMessage,
8
+ } from '@avelonjs/core'
9
+ import { captureFailure, type SuiteContext } from '../harness'
10
+
11
+ interface SentInspectable {
12
+ readonly sent: readonly {
13
+ readonly channel: NotificationChannel
14
+ readonly message: NotificationMessage
15
+ }[]
16
+ }
17
+
18
+ type AssayDriver = NotificationDriver<NotificationCapabilities> & Partial<SentInspectable>
19
+
20
+ function assay<TDriver extends AssayDriver>(
21
+ context: SuiteContext<TDriver>,
22
+ name: string,
23
+ assertion: (driver: TDriver) => Promise<void> | void,
24
+ ): void {
25
+ test(name, async () => {
26
+ const driver = await context.create()
27
+ try {
28
+ await assertion(driver)
29
+ } finally {
30
+ await context.cleanup?.(driver)
31
+ }
32
+ })
33
+ }
34
+
35
+ function hasSentInspection(driver: object): driver is SentInspectable {
36
+ return 'sent' in driver && Array.isArray(driver.sent)
37
+ }
38
+
39
+ async function expectInvalid(operation: () => Promise<unknown>): Promise<Invalid> {
40
+ const error = await captureFailure(operation)
41
+ expect(error).toBeInstanceOf(Invalid)
42
+ if (!(error instanceof Invalid)) throw new Error('Expected Invalid after taxonomy assertion.')
43
+ expect(error.code).toBe('INVALID')
44
+ return error
45
+ }
46
+
47
+ const channels: readonly NotificationChannel[] = ['push', 'sms', 'inApp']
48
+
49
+ /**
50
+ * Registers the portable notifications contract conformance suite.
51
+ *
52
+ * Channels narrow the accepted `send` argument rather than adding methods. The suite therefore
53
+ * sends through every declared channel and requires undeclared channels to normalize to `Invalid`.
54
+ *
55
+ * @param context Fresh isolated notification drivers and optional cleanup.
56
+ */
57
+ export function notificationsSuite<TDriver extends AssayDriver>(
58
+ context: SuiteContext<TDriver>,
59
+ ): void {
60
+ describe(`notifications conformance: ${context.name}`, () => {
61
+ assay(context, 'reports a present and stable resolved notifications instance', (driver) => {
62
+ const instance = driver.instance
63
+ expect(instance.length).toBeGreaterThan(0)
64
+ expect(driver.instance).toBe(instance)
65
+ })
66
+
67
+ assay(
68
+ context,
69
+ 'honors every declared notification channel in both directions',
70
+ async (driver) => {
71
+ for (const channel of channels) {
72
+ const operation = () =>
73
+ driver.send(channel, {
74
+ recipient: `recipient-${channel}`,
75
+ title: 'Assay notification',
76
+ body: `Sent through ${channel}`,
77
+ })
78
+ if (driver.capabilities.channels.includes(channel)) {
79
+ const receipt = await operation()
80
+ expect(receipt.channel).toBe(channel)
81
+ expect(receipt.id.length).toBeGreaterThan(0)
82
+ } else {
83
+ const error = await expectInvalid(operation)
84
+ expect(error.metadata.fields?.channel).toBeDefined()
85
+ }
86
+ }
87
+ },
88
+ )
89
+
90
+ assay(context, 'preserves notification content and structured data', async (driver) => {
91
+ const channel = driver.capabilities.channels[0]
92
+ if (!channel) return
93
+ const message = {
94
+ recipient: 'actor-1',
95
+ title: 'Build complete',
96
+ body: 'Your deployment is ready.',
97
+ data: { deploymentId: 'deploy-1', attempts: 2, nested: { region: 'iad1' } },
98
+ }
99
+ await driver.send(channel, message)
100
+ if (hasSentInspection(driver)) {
101
+ expect(driver.sent).toEqual([{ channel, message }])
102
+ }
103
+ })
104
+
105
+ assay(
106
+ context,
107
+ 'returns a distinct stable receipt for each accepted message',
108
+ async (driver) => {
109
+ const channel = driver.capabilities.channels[0]
110
+ if (!channel) return
111
+ const first = await driver.send(channel, { recipient: 'actor-1', body: 'First' })
112
+ const second = await driver.send(channel, { recipient: 'actor-1', body: 'Second' })
113
+ expect(first.id).not.toBe(second.id)
114
+ expect(first.channel).toBe(channel)
115
+ expect(second.channel).toBe(channel)
116
+ },
117
+ )
118
+ })
119
+ }
@@ -0,0 +1,200 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ Invalid,
4
+ NotFound,
5
+ Unauthenticated,
6
+ type CheckoutPaymentSurface,
7
+ type PaymentCapabilities,
8
+ type PaymentDriver,
9
+ type SubscriptionPaymentSurface,
10
+ type WebhookPaymentSurface,
11
+ } from '@avelonjs/core'
12
+ import { assertCapabilitySurface, captureFailure, type SuiteContext } from '../harness'
13
+
14
+ type AssayDriver = PaymentDriver<PaymentCapabilities> &
15
+ Partial<SubscriptionPaymentSurface & CheckoutPaymentSurface & WebhookPaymentSurface>
16
+
17
+ function assay<TDriver extends AssayDriver>(
18
+ context: SuiteContext<TDriver>,
19
+ name: string,
20
+ assertion: (driver: TDriver) => Promise<void> | void,
21
+ ): void {
22
+ test(name, async () => {
23
+ const driver = await context.create()
24
+ try {
25
+ await assertion(driver)
26
+ } finally {
27
+ await context.cleanup?.(driver)
28
+ }
29
+ })
30
+ }
31
+
32
+ function hasSubscriptions(driver: AssayDriver): driver is AssayDriver & SubscriptionPaymentSurface {
33
+ return driver.capabilities.subscriptions && typeof driver.subscribe === 'function'
34
+ }
35
+
36
+ function hasCheckout(driver: AssayDriver): driver is AssayDriver & CheckoutPaymentSurface {
37
+ return driver.capabilities.checkout && typeof driver.checkout === 'function'
38
+ }
39
+
40
+ function hasWebhooks(driver: AssayDriver): driver is AssayDriver & WebhookPaymentSurface {
41
+ return driver.capabilities.webhooks && typeof driver.webhook === 'function'
42
+ }
43
+
44
+ async function expectError<TError>(
45
+ constructor: abstract new (...arguments_: never[]) => TError,
46
+ operation: () => Promise<unknown>,
47
+ ): Promise<TError> {
48
+ const error = await captureFailure(operation)
49
+ expect(error).toBeInstanceOf(constructor)
50
+ if (!(error instanceof constructor)) throw new Error('Expected normalized payment error.')
51
+ return error
52
+ }
53
+
54
+ /**
55
+ * Registers the portable payments contract conformance suite.
56
+ *
57
+ * The webhook verifier is configured with the assay signature `assay-signature`. A bad signature is
58
+ * `Unauthenticated`, because the sender was not established; malformed verified bytes are `Invalid`.
59
+ * The stable webhook event ID enables application idempotency, but the contract exposes no event
60
+ * processing operation or idempotency key, so the suite does not invent an idempotency assertion.
61
+ *
62
+ * @param context Fresh isolated payment drivers and optional cleanup.
63
+ */
64
+ export function paymentsSuite<TDriver extends AssayDriver>(context: SuiteContext<TDriver>): void {
65
+ describe(`payments conformance: ${context.name}`, () => {
66
+ assay(context, 'reports a present and stable resolved payments instance', (driver) => {
67
+ const instance = driver.instance
68
+ expect(instance.length).toBeGreaterThan(0)
69
+ expect(driver.instance).toBe(instance)
70
+ })
71
+
72
+ assay(context, 'enforces every payments capability surface in both directions', (driver) => {
73
+ const surfaces: readonly [string, boolean, readonly string[]][] = [
74
+ [
75
+ 'subscriptions',
76
+ driver.capabilities.subscriptions,
77
+ ['subscribe', 'subscription', 'cancelSubscription'],
78
+ ],
79
+ ['checkout', driver.capabilities.checkout, ['checkout']],
80
+ ['webhooks', driver.capabilities.webhooks, ['webhook']],
81
+ ]
82
+ for (const [capability, declared, methods] of surfaces) {
83
+ assertCapabilitySurface(driver, capability, declared, methods)
84
+ assertCapabilitySurface(
85
+ { capabilities: { [capability]: false } },
86
+ capability,
87
+ false,
88
+ methods,
89
+ )
90
+ expect(() =>
91
+ assertCapabilitySurface(
92
+ { capabilities: { [capability]: false }, [methods[0] ?? 'missing']: () => undefined },
93
+ capability,
94
+ false,
95
+ methods,
96
+ ),
97
+ ).toThrow()
98
+ expect(() =>
99
+ assertCapabilitySurface(
100
+ { capabilities: { [capability]: true } },
101
+ capability,
102
+ true,
103
+ methods,
104
+ ),
105
+ ).toThrow()
106
+ }
107
+ })
108
+
109
+ assay(context, 'creates and retrieves customers with metadata intact', async (driver) => {
110
+ const created = await driver.createCustomer({
111
+ email: 'actor@example.test',
112
+ metadata: { actorId: 'actor-1', plan: 'pro' },
113
+ })
114
+ expect(created.id.length).toBeGreaterThan(0)
115
+ expect(await driver.customer(created.id)).toEqual(created)
116
+ expect(created.metadata).toEqual({ actorId: 'actor-1', plan: 'pro' })
117
+ })
118
+
119
+ assay(context, 'normalizes a missing customer to NotFound', async (driver) => {
120
+ const error = await expectError(NotFound, () => driver.customer('missing-customer'))
121
+ expect(error.code).toBe('NOT_FOUND')
122
+ expect(error.metadata).toEqual({
123
+ resource: 'payment-customer',
124
+ identifier: 'missing-customer',
125
+ })
126
+ })
127
+
128
+ assay(
129
+ context,
130
+ 'creates, retrieves, and cancels subscriptions when declared',
131
+ async (driver) => {
132
+ if (!hasSubscriptions(driver)) return
133
+ const customer = await driver.createCustomer({})
134
+ const created = await driver.subscribe(customer.id, 'price-pro')
135
+ expect(created).toMatchObject({
136
+ customerId: customer.id,
137
+ priceId: 'price-pro',
138
+ status: 'active',
139
+ })
140
+ expect(created.currentPeriodEndsAt).toBeInstanceOf(Date)
141
+ expect(await driver.subscription(created.id)).toEqual(created)
142
+ expect(await driver.cancelSubscription(created.id)).toMatchObject({
143
+ id: created.id,
144
+ status: 'canceled',
145
+ })
146
+ expect((await driver.subscription(created.id)).status).toBe('canceled')
147
+ },
148
+ )
149
+
150
+ assay(context, 'creates a hosted checkout session when declared', async (driver) => {
151
+ if (!hasCheckout(driver)) return
152
+ const customer = await driver.createCustomer({})
153
+ const checkout = await driver.checkout({
154
+ customerId: customer.id,
155
+ priceId: 'price-team',
156
+ successUrl: 'https://app.example.test/billing/success',
157
+ cancelUrl: 'https://app.example.test/billing/cancel',
158
+ })
159
+ expect(checkout.id.length).toBeGreaterThan(0)
160
+ expect(new URL(checkout.url).protocol).toBe('https:')
161
+ expect(checkout.expiresAt).toBeInstanceOf(Date)
162
+ })
163
+
164
+ assay(context, 'verifies and normalizes signed webhook bytes when declared', async (driver) => {
165
+ if (!hasWebhooks(driver)) return
166
+ const occurredAt = '2026-08-03T12:00:00.000Z'
167
+ const payload = new TextEncoder().encode(
168
+ JSON.stringify({
169
+ id: 'event-1',
170
+ type: 'subscription.updated',
171
+ data: { subscriptionId: 'subscription-1', status: 'active' },
172
+ occurredAt,
173
+ }),
174
+ )
175
+ expect(await driver.webhook(payload, 'assay-signature')).toEqual({
176
+ id: 'event-1',
177
+ type: 'subscription.updated',
178
+ data: { subscriptionId: 'subscription-1', status: 'active' },
179
+ occurredAt: new Date(occurredAt),
180
+ })
181
+ })
182
+
183
+ assay(context, 'rejects an unverified webhook signature as Unauthenticated', async (driver) => {
184
+ if (!hasWebhooks(driver)) return
185
+ const payload = new TextEncoder().encode('{}')
186
+ const error = await expectError(Unauthenticated, () => driver.webhook(payload, 'forged'))
187
+ expect(error.code).toBe('UNAUTHENTICATED')
188
+ expect(error.metadata.guard).toBe('payment-webhook')
189
+ })
190
+
191
+ assay(context, 'rejects malformed verified webhook bytes as Invalid', async (driver) => {
192
+ if (!hasWebhooks(driver)) return
193
+ const error = await expectError(Invalid, () =>
194
+ driver.webhook(new TextEncoder().encode('not-json'), 'assay-signature'),
195
+ )
196
+ expect(error.code).toBe('INVALID')
197
+ expect(error.cause).toBeDefined()
198
+ })
199
+ })
200
+ }
@@ -0,0 +1,238 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ type DeadLetterQueueSurface,
4
+ type DelayedQueueSurface,
5
+ type QueueCapabilities,
6
+ type QueueDriver,
7
+ type QueueReceipt,
8
+ type RetryQueueSurface,
9
+ } from '@avelonjs/core'
10
+ import { assertCapabilitySurface, type SuiteContext } from '../harness'
11
+
12
+ type AssayDriver = QueueDriver<QueueCapabilities> &
13
+ Partial<DelayedQueueSurface & RetryQueueSurface & DeadLetterQueueSurface>
14
+
15
+ function assay<TDriver extends AssayDriver>(
16
+ context: SuiteContext<TDriver>,
17
+ name: string,
18
+ assertion: (driver: TDriver) => Promise<void> | void,
19
+ ): void {
20
+ test(name, async () => {
21
+ const driver = await context.create()
22
+ try {
23
+ await assertion(driver)
24
+ } finally {
25
+ await context.cleanup?.(driver)
26
+ }
27
+ })
28
+ }
29
+
30
+ function hasDelay(driver: AssayDriver): driver is AssayDriver & DelayedQueueSurface {
31
+ return driver.capabilities.delayed && typeof driver.enqueueAt === 'function'
32
+ }
33
+
34
+ function hasRetries(driver: AssayDriver): driver is AssayDriver & RetryQueueSurface {
35
+ return driver.capabilities.retries && typeof driver.retry === 'function'
36
+ }
37
+
38
+ function hasDeadLetter(driver: AssayDriver): driver is AssayDriver & DeadLetterQueueSurface {
39
+ return driver.capabilities.deadLetter && typeof driver.failed === 'function'
40
+ }
41
+
42
+ /**
43
+ * Registers the portable queue contract conformance suite.
44
+ *
45
+ * Dead-letter-capable contexts configure a three-attempt limit. The limit belongs to driver
46
+ * construction rather than `QueueJob`, so the fixture setup owns that value. Replaying a terminal
47
+ * failure starts a fresh attempt budget.
48
+ *
49
+ * @param context Fresh isolated queue drivers and optional cleanup.
50
+ */
51
+ export function queueSuite<TDriver extends AssayDriver>(context: SuiteContext<TDriver>): void {
52
+ const delayMs = context.queueDelayMs ?? (context.live ? 2_000 : 80)
53
+ const settleMs = context.queueSettleMs ?? (context.live ? 3_000 : 120)
54
+
55
+ describe(`queue conformance: ${context.name}`, () => {
56
+ assay(context, 'reports a present and stable resolved queue instance', (driver) => {
57
+ const instance = driver.instance
58
+ expect(instance.length).toBeGreaterThan(0)
59
+ expect(driver.instance).toBe(instance)
60
+ })
61
+
62
+ assay(context, 'enforces every queue capability surface in both directions', (driver) => {
63
+ const surfaces: readonly [string, boolean, readonly string[]][] = [
64
+ ['delayed', driver.capabilities.delayed, ['enqueueAt']],
65
+ ['retries', driver.capabilities.retries, ['retry']],
66
+ ['deadLetter', driver.capabilities.deadLetter, ['failed', 'replay', 'forget']],
67
+ ]
68
+ for (const [capability, declared, methods] of surfaces) {
69
+ assertCapabilitySurface(driver, capability, declared, methods)
70
+ const unavailable = { capabilities: { [capability]: false } }
71
+ assertCapabilitySurface(unavailable, capability, false, methods)
72
+ expect(() =>
73
+ assertCapabilitySurface(
74
+ { ...unavailable, [methods[0] ?? 'missing']: () => undefined },
75
+ capability,
76
+ false,
77
+ methods,
78
+ ),
79
+ ).toThrow()
80
+ expect(() =>
81
+ assertCapabilitySurface(
82
+ { capabilities: { [capability]: true } },
83
+ capability,
84
+ true,
85
+ methods,
86
+ ),
87
+ ).toThrow()
88
+ }
89
+ })
90
+
91
+ assay(context, 'enqueues and removes a job when its handler resolves', async (driver) => {
92
+ const id = await driver.enqueue({ name: 'GenerateReport', payload: { reportId: 'report-1' } })
93
+ const receipts: QueueReceipt[] = []
94
+ expect(
95
+ await driver.drain(async (receipt) => {
96
+ receipts.push(receipt)
97
+ }),
98
+ ).toBe(1)
99
+ expect(receipts).toEqual([
100
+ {
101
+ id,
102
+ job: { name: 'GenerateReport', payload: { reportId: 'report-1' } },
103
+ attempt: 1,
104
+ },
105
+ ])
106
+ expect(await driver.drain(async () => undefined)).toBe(0)
107
+ })
108
+
109
+ assay(context, 'honors destination queues and drain limits', async (driver) => {
110
+ await driver.enqueue({ name: 'DefaultJob', payload: {} })
111
+ await driver.enqueue({ name: 'FirstMail', payload: {}, queue: 'mail' })
112
+ await driver.enqueue({ name: 'SecondMail', payload: {}, queue: 'mail' })
113
+ const names: string[] = []
114
+
115
+ expect(
116
+ await driver.drain(
117
+ async (receipt) => {
118
+ names.push(receipt.job.name)
119
+ },
120
+ { queue: 'mail', limit: 1 },
121
+ ),
122
+ ).toBe(1)
123
+ expect(names).toEqual(['FirstMail'])
124
+ expect(await driver.drain(async () => undefined)).toBe(2)
125
+ })
126
+
127
+ assay(context, 'redelivers thrown jobs only when retries are declared', async (driver) => {
128
+ await driver.enqueue({ name: 'FragileJob', payload: {} })
129
+ let firstAttempt = 0
130
+ expect(
131
+ await driver.drain(async (receipt) => {
132
+ firstAttempt = receipt.attempt
133
+ throw new Error('temporary failure')
134
+ }),
135
+ ).toBe(1)
136
+ expect(firstAttempt).toBe(1)
137
+
138
+ if (driver.capabilities.retries) {
139
+ let secondAttempt = 0
140
+ expect(
141
+ await driver.drain(async (receipt) => {
142
+ secondAttempt = receipt.attempt
143
+ }),
144
+ ).toBe(1)
145
+ expect(secondAttempt).toBe(2)
146
+ } else {
147
+ // No retry means at-most-once after a handler throw. Infinite redelivery would secretly
148
+ // implement the capability and can make every future drain permanently fail.
149
+ expect(await driver.drain(async () => undefined)).toBe(0)
150
+ }
151
+ })
152
+
153
+ assay(context, 'releases a failed job through the retry surface', async (driver) => {
154
+ if (!hasRetries(driver)) return
155
+ const id = await driver.enqueue({ name: 'RetryJob', payload: {} })
156
+ await driver.drain(async () => {
157
+ throw new Error('retry me')
158
+ })
159
+ await driver.retry(id)
160
+
161
+ let attempt = 0
162
+ expect(
163
+ await driver.drain(async (receipt) => {
164
+ attempt = receipt.attempt
165
+ }),
166
+ ).toBe(1)
167
+ expect(attempt).toBe(2)
168
+ })
169
+
170
+ assay(
171
+ context,
172
+ 'dead-letters after exhausting tries and supports replay and forget',
173
+ async (driver) => {
174
+ if (!hasDeadLetter(driver)) return
175
+ const id = await driver.enqueue({
176
+ name: 'TerminalJob',
177
+ payload: { accountId: 'account-1' },
178
+ queue: 'critical',
179
+ })
180
+
181
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
182
+ expect(
183
+ await driver.drain(
184
+ async () => {
185
+ throw new Error('terminal failure')
186
+ },
187
+ { queue: 'critical' },
188
+ ),
189
+ ).toBe(1)
190
+ expect((await driver.failed('critical')).length).toBe(attempt === 3 ? 1 : 0)
191
+ }
192
+
193
+ const failures = await driver.failed('critical')
194
+ expect(failures).toHaveLength(1)
195
+ expect(failures[0]).toMatchObject({
196
+ id,
197
+ job: {
198
+ name: 'TerminalJob',
199
+ payload: { accountId: 'account-1' },
200
+ queue: 'critical',
201
+ },
202
+ attempt: 3,
203
+ error: 'terminal failure',
204
+ })
205
+ expect(failures[0]?.failedAt).toBeInstanceOf(Date)
206
+
207
+ await driver.replay(id)
208
+ expect(await driver.failed('critical')).toHaveLength(0)
209
+ let replayAttempt = 0
210
+ await driver.drain(async (receipt) => {
211
+ replayAttempt = receipt.attempt
212
+ })
213
+ expect(replayAttempt).toBe(1)
214
+
215
+ const forgottenId = await driver.enqueue({ name: 'ForgottenJob', payload: {} })
216
+ for (let attempt = 0; attempt < 3; attempt += 1) {
217
+ await driver.drain(async () => {
218
+ throw new Error('forget me')
219
+ })
220
+ }
221
+ expect(await driver.failed()).toHaveLength(1)
222
+ await driver.forget(forgottenId)
223
+ expect(await driver.failed()).toHaveLength(0)
224
+ },
225
+ )
226
+
227
+ assay(context, 'keeps delayed jobs invisible until their delivery time', async (driver) => {
228
+ if (!hasDelay(driver)) return
229
+ await driver.enqueueAt(
230
+ { name: 'ScheduledJob', payload: { batch: 4 } },
231
+ new Date(Date.now() + delayMs),
232
+ )
233
+ expect(await driver.drain(async () => undefined)).toBe(0)
234
+ await new Promise((resolve) => setTimeout(resolve, settleMs))
235
+ expect(await driver.drain(async () => undefined)).toBe(1)
236
+ })
237
+ })
238
+ }
@@ -0,0 +1,134 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ Invalid,
4
+ type RateLimitAlgorithm,
5
+ type RateLimitCapabilities,
6
+ type RateLimitDriver,
7
+ type RateLimitPolicy,
8
+ } from '@avelonjs/core'
9
+ import { captureFailure, type SuiteContext } from '../harness'
10
+
11
+ type AssayDriver = RateLimitDriver<RateLimitCapabilities>
12
+
13
+ function assay<TDriver extends AssayDriver>(
14
+ context: SuiteContext<TDriver>,
15
+ name: string,
16
+ assertion: (driver: TDriver) => Promise<void> | void,
17
+ ): void {
18
+ test(name, async () => {
19
+ const driver = await context.create()
20
+ try {
21
+ await assertion(driver)
22
+ } finally {
23
+ await context.cleanup?.(driver)
24
+ }
25
+ })
26
+ }
27
+
28
+ async function expectInvalid(operation: () => Promise<unknown>): Promise<Invalid> {
29
+ const error = await captureFailure(operation)
30
+ expect(error).toBeInstanceOf(Invalid)
31
+ if (!(error instanceof Invalid)) throw new Error('Expected Invalid after taxonomy assertion.')
32
+ expect(error.code).toBe('INVALID')
33
+ return error
34
+ }
35
+
36
+ const algorithms: readonly RateLimitAlgorithm[] = ['tokenBucket', 'slidingWindow']
37
+
38
+ function policy(algorithm: RateLimitAlgorithm, limit = 2): RateLimitPolicy {
39
+ return { algorithm, limit, intervalSeconds: 0.05 }
40
+ }
41
+
42
+ /**
43
+ * Registers the portable rate-limit contract conformance suite.
44
+ *
45
+ * Algorithms narrow the accepted policy argument rather than adding methods, so both directions
46
+ * are tested behaviorally. The first `limit` units are accepted and the next is denied; after one
47
+ * full interval, capacity is available again. Denial is represented by `RateLimitDecision`, not a
48
+ * `RateLimited` exception, so only malformed policies normalize to `Invalid`.
49
+ *
50
+ * @param context Fresh isolated rate-limit drivers and optional cleanup.
51
+ */
52
+ export function ratelimitSuite<TDriver extends AssayDriver>(context: SuiteContext<TDriver>): void {
53
+ describe(`ratelimit conformance: ${context.name}`, () => {
54
+ assay(context, 'reports a present and stable resolved ratelimit instance', (driver) => {
55
+ const instance = driver.instance
56
+ expect(instance.length).toBeGreaterThan(0)
57
+ expect(driver.instance).toBe(instance)
58
+ })
59
+
60
+ assay(
61
+ context,
62
+ 'honors every declared rate-limit algorithm in both directions',
63
+ async (driver) => {
64
+ for (const algorithm of algorithms) {
65
+ if (driver.capabilities.algorithms.includes(algorithm)) {
66
+ const decision = await driver.consume(`declared-${algorithm}`, policy(algorithm))
67
+ expect(decision.allowed).toBe(true)
68
+ expect(decision.remaining).toBe(1)
69
+ } else {
70
+ const error = await expectInvalid(() =>
71
+ driver.consume(`undeclared-${algorithm}`, policy(algorithm)),
72
+ )
73
+ expect(error.metadata.fields?.algorithm).toBeDefined()
74
+ }
75
+ }
76
+ },
77
+ )
78
+
79
+ assay(context, 'blocks at the configured limit boundary', async (driver) => {
80
+ for (const algorithm of driver.capabilities.algorithms) {
81
+ const configured = policy(algorithm)
82
+ const first = await driver.consume(`boundary-${algorithm}`, configured)
83
+ const second = await driver.consume(`boundary-${algorithm}`, configured)
84
+ const blocked = await driver.consume(`boundary-${algorithm}`, configured)
85
+ expect(first).toMatchObject({ allowed: true, remaining: 1 })
86
+ expect(second).toMatchObject({ allowed: true, remaining: 0 })
87
+ expect(blocked).toMatchObject({ allowed: false, remaining: 0 })
88
+ expect(blocked.retryAfterMs).toBeGreaterThan(0)
89
+ }
90
+ })
91
+
92
+ assay(context, 'restores capacity after the configured interval', async (driver) => {
93
+ for (const algorithm of driver.capabilities.algorithms) {
94
+ const configured = policy(algorithm, 1)
95
+ expect((await driver.consume(`window-${algorithm}`, configured)).allowed).toBe(true)
96
+ expect((await driver.consume(`window-${algorithm}`, configured)).allowed).toBe(false)
97
+ await new Promise((resolve) => setTimeout(resolve, 80))
98
+ const restored = await driver.consume(`window-${algorithm}`, configured)
99
+ expect(restored.allowed).toBe(true)
100
+ expect(restored.remaining).toBe(0)
101
+ }
102
+ })
103
+
104
+ assay(context, 'keeps separate keys on separate budgets', async (driver) => {
105
+ for (const algorithm of driver.capabilities.algorithms) {
106
+ const configured = policy(algorithm, 1)
107
+ const first = await driver.consume(`first-${algorithm}`, configured)
108
+ const second = await driver.consume(`second-${algorithm}`, configured)
109
+ expect(first).toMatchObject({ allowed: true, remaining: 0 })
110
+ expect(second).toMatchObject({ allowed: true, remaining: 0 })
111
+ }
112
+ })
113
+
114
+ assay(context, 'clears accumulated state for one key', async (driver) => {
115
+ for (const algorithm of driver.capabilities.algorithms) {
116
+ const key = `reset-${algorithm}`
117
+ const configured = policy(algorithm, 1)
118
+ await driver.consume(key, configured)
119
+ expect((await driver.consume(key, configured)).allowed).toBe(false)
120
+ await driver.reset(key)
121
+ expect((await driver.consume(key, configured)).allowed).toBe(true)
122
+ }
123
+ })
124
+
125
+ assay(context, 'normalizes malformed policies to Invalid', async (driver) => {
126
+ const algorithm = driver.capabilities.algorithms[0]
127
+ if (!algorithm) return
128
+ const error = await expectInvalid(() =>
129
+ driver.consume('invalid-policy', { algorithm, limit: 0, intervalSeconds: 1 }),
130
+ )
131
+ expect(Object.keys(error.metadata.fields ?? {})).toContain('limit')
132
+ })
133
+ })
134
+ }