@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,80 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { Invalid, type FlagCapabilities, type FlagDriver } from '@avelonjs/core'
3
+ import { captureFailure, type SuiteContext } from '../harness'
4
+
5
+ type AssayDriver = FlagDriver<FlagCapabilities>
6
+
7
+ function assay<TDriver extends AssayDriver>(
8
+ context: SuiteContext<TDriver>,
9
+ name: string,
10
+ assertion: (driver: TDriver) => Promise<void> | void,
11
+ ): void {
12
+ test(name, async () => {
13
+ const driver = await context.create()
14
+ try {
15
+ await assertion(driver)
16
+ } finally {
17
+ await context.cleanup?.(driver)
18
+ }
19
+ })
20
+ }
21
+
22
+ async function expectInvalid(operation: () => Promise<unknown>): Promise<Invalid> {
23
+ const error = await captureFailure(operation)
24
+ expect(error).toBeInstanceOf(Invalid)
25
+ if (!(error instanceof Invalid)) throw new Error('Expected Invalid after taxonomy assertion.')
26
+ expect(error.code).toBe('INVALID')
27
+ return error
28
+ }
29
+
30
+ /**
31
+ * Registers the portable feature-flag contract conformance suite.
32
+ *
33
+ * The fixture provides `assay.enabled`, `assay.variant`, and `assay.targeted`; actor
34
+ * `actor-enabled` matches the targeted rule. Targeting narrows the optional context argument rather
35
+ * than adding a method, so both directions are checked behaviorally. An unknown flag returns its
36
+ * supplied default and deliberately produces no taxonomy error.
37
+ *
38
+ * @param context Fresh isolated flag drivers and optional cleanup.
39
+ */
40
+ export function flagsSuite<TDriver extends AssayDriver>(context: SuiteContext<TDriver>): void {
41
+ describe(`flags conformance: ${context.name}`, () => {
42
+ assay(context, 'reports a present and stable resolved flags instance', (driver) => {
43
+ const instance = driver.instance
44
+ expect(instance.length).toBeGreaterThan(0)
45
+ expect(driver.instance).toBe(instance)
46
+ })
47
+
48
+ assay(context, 'returns configured flag values with their original types', async (driver) => {
49
+ expect(await driver.evaluate('assay.enabled', false)).toBe(true)
50
+ expect(await driver.evaluate('assay.variant', 'control')).toBe('treatment')
51
+ })
52
+
53
+ assay(context, 'returns the supplied default for an unknown flag', async (driver) => {
54
+ const fallback = { enabled: true, allocation: 0.25, nested: { cohort: 'beta' } }
55
+ expect(await driver.evaluate('assay.unknown', fallback)).toEqual(fallback)
56
+ })
57
+
58
+ assay(context, 'honors the targeting capability in both directions', async (driver) => {
59
+ if (driver.capabilities.targeting) {
60
+ const context = {
61
+ actorId: 'actor-enabled',
62
+ organizationId: 'organization-1',
63
+ attributes: { region: 'us-east', seats: 12, active: true },
64
+ }
65
+ const first = await driver.evaluate('assay.targeted', false, context)
66
+ const second = await driver.evaluate('assay.targeted', false, context)
67
+ expect(first).toBe(true)
68
+ expect(second).toBe(first)
69
+ expect(await driver.evaluate('assay.targeted', false, { actorId: 'actor-disabled' })).toBe(
70
+ false,
71
+ )
72
+ } else {
73
+ const error = await expectInvalid(() =>
74
+ driver.evaluate('assay.targeted', false, { actorId: 'actor-enabled' }),
75
+ )
76
+ expect(error.metadata.fields?.context).toBeDefined()
77
+ }
78
+ })
79
+ })
80
+ }
@@ -0,0 +1,293 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ Invalid,
4
+ Unauthenticated,
5
+ type IdentityCapabilities,
6
+ type IdentityDriver,
7
+ type MagicLinkIdentitySurface,
8
+ type MfaIdentitySurface,
9
+ type OAuthIdentitySurface,
10
+ type OrganizationIdentitySurface,
11
+ type PasswordIdentitySurface,
12
+ type RequestCookies,
13
+ } from '@avelonjs/core'
14
+ import { assertCapabilitySurface, captureFailure, type SuiteContext } from '../harness'
15
+
16
+ type AssayDriver = IdentityDriver<IdentityCapabilities<string>, unknown, unknown, unknown>
17
+ type IdentityFactory<TDriver extends AssayDriver> = (cookies: RequestCookies) => TDriver
18
+
19
+ class AssayCookies implements RequestCookies {
20
+ readonly #values = new Map<string, string>()
21
+ readonly #expired = new Set<string>()
22
+
23
+ get size(): number {
24
+ return this.#values.size
25
+ }
26
+
27
+ get(name: string): string | undefined {
28
+ return this.#expired.has(name) ? undefined : this.#values.get(name)
29
+ }
30
+
31
+ set(name: string, value: string): void {
32
+ this.#values.set(name, value)
33
+ this.#expired.delete(name)
34
+ }
35
+
36
+ delete(name: string): void {
37
+ this.#values.delete(name)
38
+ this.#expired.delete(name)
39
+ }
40
+
41
+ expireAll(): void {
42
+ for (const name of this.#values.keys()) this.#expired.add(name)
43
+ }
44
+ }
45
+
46
+ function assay<TDriver extends AssayDriver>(
47
+ context: SuiteContext<IdentityFactory<TDriver>>,
48
+ name: string,
49
+ assertion: (driver: TDriver, cookies: AssayCookies) => Promise<void> | void,
50
+ ): void {
51
+ test(name, async () => {
52
+ const factory = await context.create()
53
+ const cookies = new AssayCookies()
54
+ // A request-scoped dependency stays explicit and testable. Synchronous construction keeps config
55
+ // resolution deterministic; drivers that need I/O defer it until an operation is called.
56
+ const driver = factory(cookies)
57
+ try {
58
+ await assertion(driver, cookies)
59
+ } finally {
60
+ await context.cleanup?.(factory)
61
+ }
62
+ })
63
+ }
64
+
65
+ async function expectUnauthenticated(operation: () => Promise<unknown>): Promise<Unauthenticated> {
66
+ const error = await captureFailure(operation)
67
+ expect(error).toBeInstanceOf(Unauthenticated)
68
+ if (!(error instanceof Unauthenticated)) {
69
+ throw new Error('Expected Unauthenticated after taxonomy assertion.')
70
+ }
71
+ expect(error.code).toBe('UNAUTHENTICATED')
72
+ return error
73
+ }
74
+
75
+ async function expectInvalid(operation: () => Promise<unknown>): Promise<Invalid> {
76
+ const error = await captureFailure(operation)
77
+ expect(error).toBeInstanceOf(Invalid)
78
+ if (!(error instanceof Invalid)) throw new Error('Expected Invalid after taxonomy assertion.')
79
+ expect(error.code).toBe('INVALID')
80
+ return error
81
+ }
82
+
83
+ function hasPasswords(driver: AssayDriver): driver is AssayDriver & PasswordIdentitySurface {
84
+ return driver.capabilities.passwords
85
+ }
86
+
87
+ function hasMagicLinks(driver: AssayDriver): driver is AssayDriver & MagicLinkIdentitySurface {
88
+ return driver.capabilities.magicLinks
89
+ }
90
+
91
+ function hasOAuth(driver: AssayDriver): driver is AssayDriver & OAuthIdentitySurface {
92
+ return driver.capabilities.oauth
93
+ }
94
+
95
+ function hasOrganizations(
96
+ driver: AssayDriver,
97
+ ): driver is AssayDriver & OrganizationIdentitySurface {
98
+ return driver.capabilities.organizations
99
+ }
100
+
101
+ function hasMfa(driver: AssayDriver): driver is AssayDriver & MfaIdentitySurface<string> {
102
+ return driver.capabilities.mfa.length > 0
103
+ }
104
+
105
+ /**
106
+ * Registers the portable identity contract conformance suite.
107
+ *
108
+ * `context.create()` returns the config-time factory, whose pinned signature is
109
+ * `(cookies: RequestCookies) => TDriver`. Each call to that factory receives one request-scoped
110
+ * cookie store. Recovery-token and MFA-code providers may supply vendor-issued fixture values;
111
+ * otherwise password-capable fixtures accept `assay-reset-token` and MFA fixtures accept `123456`.
112
+ *
113
+ * @param context Fresh identity factories and optional cleanup.
114
+ */
115
+ export function identitySuite<TDriver extends AssayDriver>(
116
+ context: SuiteContext<IdentityFactory<TDriver>>,
117
+ ): void {
118
+ describe(`identity conformance: ${context.name}`, () => {
119
+ assay(context, 'enforces every identity capability surface in both directions', (driver) => {
120
+ const surfaces: readonly [string, boolean, readonly string[]][] = [
121
+ [
122
+ 'passwords',
123
+ driver.capabilities.passwords,
124
+ [
125
+ 'register',
126
+ 'signInWithPassword',
127
+ 'sendPasswordReset',
128
+ 'resetPassword',
129
+ 'updatePassword',
130
+ ],
131
+ ],
132
+ ['magicLinks', driver.capabilities.magicLinks, ['sendMagicLink']],
133
+ ['oauth', driver.capabilities.oauth, ['linkOAuthIdentity']],
134
+ ['organizations', driver.capabilities.organizations, ['organizations', 'useOrganization']],
135
+ ['mfa', driver.capabilities.mfa.length > 0, ['challengeMfa', 'verifyMfa']],
136
+ ]
137
+ for (const [capability, declared, methods] of surfaces) {
138
+ assertCapabilitySurface(driver, capability, declared, methods)
139
+ const unavailable = {
140
+ capabilities: { [capability]: capability === 'mfa' ? ([] as const) : false },
141
+ }
142
+ assertCapabilitySurface(unavailable, capability, false, methods)
143
+ expect(() =>
144
+ assertCapabilitySurface(
145
+ { ...unavailable, [methods[0] ?? 'missing']: () => undefined },
146
+ capability,
147
+ false,
148
+ methods,
149
+ ),
150
+ ).toThrow()
151
+ expect(() =>
152
+ assertCapabilitySurface(
153
+ {
154
+ capabilities: {
155
+ [capability]: capability === 'mfa' ? (['assay-factor'] as const) : true,
156
+ },
157
+ },
158
+ capability,
159
+ true,
160
+ methods,
161
+ ),
162
+ ).toThrow()
163
+ }
164
+ })
165
+
166
+ assay(
167
+ context,
168
+ 'returns no current actor or session for an anonymous request',
169
+ async (driver) => {
170
+ expect(await driver.user()).toBeNull()
171
+ expect(await driver.session()).toBeNull()
172
+ },
173
+ )
174
+
175
+ assay(
176
+ context,
177
+ 'registers and signs in with a password while normalizing bad credentials',
178
+ async (driver, cookies) => {
179
+ if (!hasPasswords(driver)) return
180
+ const registered = await driver.register('actor@example.test', 'initial-password')
181
+ expect(registered).not.toBeNull()
182
+ await expectUnauthenticated(() =>
183
+ driver.signInWithPassword('actor@example.test', 'wrong-password'),
184
+ )
185
+ const signedIn = await driver.signInWithPassword('actor@example.test', 'initial-password')
186
+ // Actor payloads are opaque, so structured equality pins consistency across identity reads
187
+ // without prescribing fields that real drivers may not share.
188
+ expect(await driver.user()).toEqual(signedIn)
189
+ expect(await driver.session()).not.toBeNull()
190
+ expect(cookies.size).toBeGreaterThan(0)
191
+ },
192
+ )
193
+
194
+ assay(context, 'signs out the current request and clears its session', async (driver) => {
195
+ if (!hasPasswords(driver)) return
196
+ await driver.register('actor@example.test', 'initial-password')
197
+ await driver.signInWithPassword('actor@example.test', 'initial-password')
198
+ await driver.signOut()
199
+ expect(await driver.user()).toBeNull()
200
+ expect(await driver.session()).toBeNull()
201
+ })
202
+
203
+ assay(
204
+ context,
205
+ 'rejects operations requiring an absent or expired session as Unauthenticated',
206
+ async (driver, cookies) => {
207
+ await expectUnauthenticated(() => driver.signOut())
208
+ if (!hasPasswords(driver)) return
209
+ await driver.register('actor@example.test', 'initial-password')
210
+ await driver.signInWithPassword('actor@example.test', 'initial-password')
211
+ cookies.expireAll()
212
+ expect(await driver.user()).toBeNull()
213
+ expect(await driver.session()).toBeNull()
214
+ await expectUnauthenticated(() => driver.updatePassword('replacement-password'))
215
+ },
216
+ )
217
+
218
+ assay(context, 'updates the authenticated actor password', async (driver) => {
219
+ if (!hasPasswords(driver)) return
220
+ await driver.register('actor@example.test', 'initial-password')
221
+ await driver.signInWithPassword('actor@example.test', 'initial-password')
222
+ await driver.updatePassword('updated-password')
223
+ await driver.signOut()
224
+ await expectUnauthenticated(() =>
225
+ driver.signInWithPassword('actor@example.test', 'initial-password'),
226
+ )
227
+ expect(
228
+ await driver.signInWithPassword('actor@example.test', 'updated-password'),
229
+ ).not.toBeNull()
230
+ })
231
+
232
+ assay(context, 'resets a password with a vendor recovery token', async (driver) => {
233
+ if (!hasPasswords(driver)) return
234
+ await driver.register('actor@example.test', 'initial-password')
235
+ await driver.sendPasswordReset('actor@example.test')
236
+ await driver.sendPasswordReset('missing@example.test')
237
+ const recoveryToken = context.recoveryToken
238
+ ? await context.recoveryToken('actor@example.test')
239
+ : 'assay-reset-token'
240
+ await driver.resetPassword(recoveryToken, 'reset-password')
241
+ await expectUnauthenticated(() =>
242
+ driver.signInWithPassword('actor@example.test', 'initial-password'),
243
+ )
244
+ expect(await driver.signInWithPassword('actor@example.test', 'reset-password')).not.toBeNull()
245
+ })
246
+
247
+ assay(context, 'sends passwordless magic links when declared', async (driver) => {
248
+ if (!hasMagicLinks(driver)) return
249
+ await expect(
250
+ driver.sendMagicLink('actor@example.test', 'https://app.example.test/complete'),
251
+ ).resolves.toBeUndefined()
252
+ })
253
+
254
+ assay(context, 'links a completed OAuth identity when declared', async (driver) => {
255
+ if (!hasPasswords(driver) || !hasOAuth(driver)) return
256
+ await driver.register('actor@example.test', 'initial-password')
257
+ await driver.signInWithPassword('actor@example.test', 'initial-password')
258
+ await expect(
259
+ driver.linkOAuthIdentity('primary', 'authorization-code'),
260
+ ).resolves.toBeUndefined()
261
+ })
262
+
263
+ assay(context, 'lists and selects organizations when declared', async (driver) => {
264
+ if (!hasPasswords(driver) || !hasOrganizations(driver)) return
265
+ await driver.register('actor@example.test', 'initial-password')
266
+ await driver.signInWithPassword('actor@example.test', 'initial-password')
267
+ const organizations = await driver.organizations()
268
+ expect(organizations.length).toBeGreaterThan(0)
269
+ const organization = organizations[0]
270
+ expect(organization).toBeDefined()
271
+ if (!organization) throw new Error('Expected the organization fixture after assertion.')
272
+ await expect(driver.useOrganization(organization.id)).resolves.toBeUndefined()
273
+ })
274
+
275
+ assay(
276
+ context,
277
+ 'challenges only declared MFA factors and verifies their responses',
278
+ async (driver) => {
279
+ if (!hasMfa(driver) || !hasPasswords(driver)) return
280
+ await driver.register('actor@example.test', 'initial-password')
281
+ await driver.signInWithPassword('actor@example.test', 'initial-password')
282
+ for (const factor of driver.capabilities.mfa) {
283
+ const challenge = await driver.challengeMfa(factor)
284
+ expect(challenge.factor).toBe(factor)
285
+ expect(challenge.expiresAt.getTime()).toBeGreaterThan(Date.now())
286
+ const code = context.mfaCode ? await context.mfaCode(challenge.id) : '123456'
287
+ await expect(driver.verifyMfa(challenge.id, code)).resolves.toBeUndefined()
288
+ }
289
+ await expectInvalid(() => driver.challengeMfa('undeclared-factor'))
290
+ },
291
+ )
292
+ })
293
+ }
@@ -0,0 +1,16 @@
1
+ export { aiSuite } from './ai'
2
+ export { cacheSuite } from './cache'
3
+ export { databaseSuite } from './database'
4
+ export { flagsSuite } from './flags'
5
+ export { identitySuite } from './identity'
6
+ export { logsSuite } from './logs'
7
+ export { mailSuite } from './mail'
8
+ export { notificationsSuite } from './notifications'
9
+ export { paymentsSuite } from './payments'
10
+ export { queueSuite } from './queue'
11
+ export { ratelimitSuite } from './ratelimit'
12
+ export { realtimeSuite } from './realtime'
13
+ export { searchSuite } from './search'
14
+ export { socialSuite } from './social'
15
+ export { storageSuite } from './storage'
16
+ export { tokensSuite } from './tokens'
@@ -0,0 +1,116 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ type LogCapabilities,
4
+ type LogDriver,
5
+ type LogLevel,
6
+ type LogRecord,
7
+ type TraceLogSurface,
8
+ } from '@avelonjs/core'
9
+ import { assertCapabilitySurface, type SuiteContext } from '../harness'
10
+
11
+ interface RecordInspectable {
12
+ readonly records: readonly LogRecord[]
13
+ }
14
+
15
+ type AssayDriver = LogDriver<LogCapabilities> & Partial<TraceLogSurface> & RecordInspectable
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 hasTraces(driver: AssayDriver): driver is AssayDriver & TraceLogSurface {
33
+ return driver.capabilities.traces && typeof driver.startSpan === 'function'
34
+ }
35
+
36
+ /**
37
+ * Registers the portable structured logging contract conformance suite.
38
+ *
39
+ * The context exposes records captured by its test sink so the one-way `write` operation can be
40
+ * observed. Filtering policy is not part of `LogDriver` and is deliberately not tested here. The
41
+ * contract also exposes no deterministic failure, so the suite does not invent a taxonomy trigger.
42
+ *
43
+ * @param context Fresh isolated log drivers with an observable test sink and optional cleanup.
44
+ */
45
+ export function logsSuite<TDriver extends AssayDriver>(context: SuiteContext<TDriver>): void {
46
+ describe(`logs conformance: ${context.name}`, () => {
47
+ assay(context, 'reports a present and stable resolved logs instance', (driver) => {
48
+ const instance = driver.instance
49
+ expect(instance.length).toBeGreaterThan(0)
50
+ expect(driver.instance).toBe(instance)
51
+ })
52
+
53
+ assay(context, 'enforces the trace capability surface in both directions', (driver) => {
54
+ assertCapabilitySurface(driver, 'traces', driver.capabilities.traces, ['startSpan'])
55
+ assertCapabilitySurface({ capabilities: { traces: false } }, 'traces', false, ['startSpan'])
56
+ expect(() =>
57
+ assertCapabilitySurface(
58
+ { capabilities: { traces: false }, startSpan: () => undefined },
59
+ 'traces',
60
+ false,
61
+ ['startSpan'],
62
+ ),
63
+ ).toThrow()
64
+ expect(() =>
65
+ assertCapabilitySurface({ capabilities: { traces: true } }, 'traces', true, ['startSpan']),
66
+ ).toThrow()
67
+ })
68
+
69
+ assay(context, 'preserves structured log fields without mangling', async (driver) => {
70
+ const timestamp = new Date('2026-08-03T12:00:00.000Z')
71
+ const context = {
72
+ actor: { id: 'actor-1', roles: ['admin', 'editor'] },
73
+ message: 'Déploiement terminé',
74
+ default: 'reserved-name',
75
+ counts: { attempted: 3, failed: 0 },
76
+ }
77
+ await driver.write({ level: 'info', message: '公開済み', context, timestamp })
78
+ await driver.write({ level: 'debug', message: 'Empty context', context: {} })
79
+
80
+ expect(driver.records[0]).toEqual({
81
+ level: 'info',
82
+ message: '公開済み',
83
+ context,
84
+ timestamp,
85
+ })
86
+ expect(driver.records[1]?.context).toEqual({})
87
+ })
88
+
89
+ assay(context, 'retains every declared log level on the record', async (driver) => {
90
+ const levels: readonly LogLevel[] = ['debug', 'info', 'warning', 'error']
91
+ for (const level of levels) await driver.write({ level, message: `${level} message` })
92
+ expect(driver.records.map((record) => record.level)).toEqual([...levels])
93
+ })
94
+
95
+ assay(context, 'defaults an omitted record timestamp to the current time', async (driver) => {
96
+ const before = Date.now()
97
+ await driver.write({ level: 'info', message: 'Timestamped by the driver' })
98
+ const after = Date.now()
99
+ const timestamp = driver.records[0]?.timestamp
100
+ expect(timestamp).toBeInstanceOf(Date)
101
+ expect(timestamp?.getTime()).toBeGreaterThanOrEqual(before)
102
+ expect(timestamp?.getTime()).toBeLessThanOrEqual(after)
103
+ })
104
+
105
+ assay(context, 'starts and completes trace spans when declared', (driver) => {
106
+ if (!hasTraces(driver)) return
107
+ const span = driver.startSpan('publish-post', { postId: 'post-1', attempts: 2 })
108
+ expect(span.traceId.length).toBeGreaterThan(0)
109
+ expect(span.spanId.length).toBeGreaterThan(0)
110
+ const error = new Error('trace event')
111
+ expect(() => span.attribute('published', true)).not.toThrow()
112
+ expect(() => span.recordError(error)).not.toThrow()
113
+ expect(() => span.end()).not.toThrow()
114
+ })
115
+ })
116
+ }
@@ -0,0 +1,160 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ Invalid,
4
+ type MailCapabilities,
5
+ type MailDriver,
6
+ type MailMessage,
7
+ type TemplateMailSurface,
8
+ } from '@avelonjs/core'
9
+ import { assertCapabilitySurface, captureFailure, type SuiteContext } from '../harness'
10
+
11
+ type AssayDriver = MailDriver<MailCapabilities> & Partial<TemplateMailSurface>
12
+
13
+ interface SentMailInspectable {
14
+ readonly sent: readonly MailMessage[]
15
+ }
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 hasTemplates(driver: AssayDriver): driver is AssayDriver & TemplateMailSurface {
33
+ return driver.capabilities.templates && typeof driver.sendTemplate === 'function'
34
+ }
35
+
36
+ function hasSentInspection(driver: object): driver is SentMailInspectable {
37
+ return 'sent' in driver && Array.isArray(driver.sent)
38
+ }
39
+
40
+ async function expectInvalid(operation: () => Promise<unknown>): Promise<Invalid> {
41
+ const error = await captureFailure(operation)
42
+ expect(error).toBeInstanceOf(Invalid)
43
+ if (!(error instanceof Invalid)) throw new Error('Expected Invalid after taxonomy assertion.')
44
+ expect(error.code).toBe('INVALID')
45
+ return error
46
+ }
47
+
48
+ /**
49
+ * Registers the portable mail contract conformance suite.
50
+ *
51
+ * The context supplies one already-resolved mailer. Named-mailer resolution and unknown-name
52
+ * rejection belong to configuration and the mail facade; this suite verifies only the driver's
53
+ * stable instance identity. The frozen contract defines no deterministic trigger for provider
54
+ * unavailability or rate limiting, so this suite does not invent one.
55
+ *
56
+ * @param context Fresh isolated mail drivers and optional cleanup.
57
+ */
58
+ export function mailSuite<TDriver extends AssayDriver>(context: SuiteContext<TDriver>): void {
59
+ const sender = context.mailSender ?? 'sender@example.test'
60
+ const recipients = context.mailRecipients ?? {
61
+ to: ['first@example.test', 'second@example.test'],
62
+ replyTo: ['replies@example.test', 'support@example.test'],
63
+ cc: 'copy@example.test',
64
+ bcc: 'audit@example.test',
65
+ configured: 'recipient@example.test',
66
+ }
67
+
68
+ describe(`mail conformance: ${context.name}`, () => {
69
+ assay(context, 'reports a present and stable resolved mailer instance', (driver) => {
70
+ const instance = driver.instance
71
+ expect(instance.length).toBeGreaterThan(0)
72
+ expect(driver.instance).toBe(instance)
73
+ })
74
+
75
+ assay(context, 'enforces every mail capability surface in both directions', (driver) => {
76
+ assertCapabilitySurface(driver, 'templates', driver.capabilities.templates, ['sendTemplate'])
77
+ const unavailable = { capabilities: { templates: false } as const }
78
+ assertCapabilitySurface(unavailable, 'templates', false, ['sendTemplate'])
79
+ expect(() =>
80
+ assertCapabilitySurface(
81
+ { ...unavailable, sendTemplate: () => undefined },
82
+ 'templates',
83
+ false,
84
+ ['sendTemplate'],
85
+ ),
86
+ ).toThrow()
87
+ expect(() =>
88
+ assertCapabilitySurface({ capabilities: { templates: true } as const }, 'templates', true, [
89
+ 'sendTemplate',
90
+ ]),
91
+ ).toThrow()
92
+ })
93
+
94
+ assay(
95
+ context,
96
+ 'sends multiple recipients with reply-to, cc, bcc, and attachments intact',
97
+ async (driver) => {
98
+ const attachment = new Uint8Array([0, 255, 4])
99
+ const receipt = await driver.send({
100
+ to: recipients.to,
101
+ from: sender,
102
+ replyTo: recipients.replyTo,
103
+ cc: [recipients.cc],
104
+ bcc: [recipients.bcc],
105
+ subject: 'Quarterly report',
106
+ text: 'The report is attached.',
107
+ html: '<p>The report is attached.</p>',
108
+ attachments: [
109
+ {
110
+ filename: 'report.bin',
111
+ content: attachment,
112
+ contentType: 'application/octet-stream',
113
+ },
114
+ ],
115
+ })
116
+
117
+ // Accepted means every envelope recipient, including cc and bcc but excluding reply-to.
118
+ expect(receipt.accepted).toEqual([...recipients.to, recipients.cc, recipients.bcc])
119
+ expect(receipt.id.length).toBeGreaterThan(0)
120
+
121
+ if (hasSentInspection(driver)) {
122
+ expect(driver.sent).toHaveLength(1)
123
+ expect(driver.sent[0]).toMatchObject({
124
+ to: recipients.to,
125
+ from: sender,
126
+ replyTo: recipients.replyTo,
127
+ cc: [recipients.cc],
128
+ bcc: [recipients.bcc],
129
+ })
130
+ expect(driver.sent[0]?.attachments?.[0]?.content).toEqual(attachment)
131
+ }
132
+ },
133
+ )
134
+
135
+ assay(context, 'uses the configured sender when from is omitted', async (driver) => {
136
+ await driver.send({
137
+ to: recipients.configured,
138
+ subject: 'Configured sender',
139
+ text: 'Sender defaults are instance-owned.',
140
+ })
141
+ if (hasSentInspection(driver)) expect(driver.sent[0]?.from).toBe('mailer@example.test')
142
+ })
143
+
144
+ assay(context, 'normalizes malformed recipient addresses to Invalid', async (driver) => {
145
+ const error = await expectInvalid(() =>
146
+ driver.send({ to: 'not-an-address', subject: 'Invalid recipient', text: 'Not delivered.' }),
147
+ )
148
+ expect(error.metadata.fields?.to?.join(' ')).toContain('not-an-address')
149
+ })
150
+
151
+ assay(context, 'sends hosted templates only when declared', async (driver) => {
152
+ if (!hasTemplates(driver)) return
153
+ const receipt = await driver.sendTemplate('welcome-message', recipients.to, {
154
+ actorName: 'Morgan',
155
+ })
156
+ expect(receipt.accepted).toEqual(recipients.to)
157
+ expect(receipt.id.length).toBeGreaterThan(0)
158
+ })
159
+ })
160
+ }