@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ryan Yannelli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # @avelonjs/conformance
2
+
3
+ `@avelonjs/conformance` is the load-bearing test artifact for Avelon drivers. It ships one suite per frozen contract, written against the contract rather than any implementation, plus in-memory fakes that run those same suites. Reach for this package when you certify a driver: pass a `create` factory that returns a known-empty instance, and let the suite own the assertions. A passing driver means someone other than the driver author wrote the tests.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ bun add -d @avelonjs/conformance
9
+ ```
10
+
11
+ Suites import `bun:test` and live at `@avelonjs/conformance/suites`. Fakes stay on the package root so a Next or Node production graph can import `FakeMail` without loading the test runner. Point a driver package at `@avelonjs/core` for the contract and at this package for the suite.
12
+
13
+ ## Basic Usage
14
+
15
+ Wire the shared database suite to an in-memory fake, then swap `create` for your driver when you certify it.
16
+
17
+ ```ts
18
+ import { FakeDatabase } from '@avelonjs/conformance'
19
+ import { databaseSuite } from '@avelonjs/conformance/suites'
20
+
21
+ databaseSuite({
22
+ name: 'reference fake',
23
+ create: () => new FakeDatabase(),
24
+ })
25
+ ```
26
+
27
+ A live driver owns fixture reset and cleanup. The suite still assumes each `create()` returns a known-empty world.
28
+
29
+ ```ts
30
+ import { databaseSuite } from '@avelonjs/conformance/suites'
31
+ import { createPostgresDatabase } from '@avelonjs/postgres'
32
+
33
+ databaseSuite({
34
+ name: 'live postgres',
35
+ create: async () => {
36
+ const driver = createPostgresDatabase()
37
+ await driver.resetFixtures()
38
+ return driver
39
+ },
40
+ cleanup: (driver) => driver.close(),
41
+ })
42
+ ```
43
+
44
+ ## Shared Suites
45
+
46
+ Every contract has one suite. You did not write it. That is the point. Call the matching function from a `bun test` file; the suite registers `describe` and `test` blocks itself.
47
+
48
+ ```ts
49
+ import { FakeMail } from '@avelonjs/conformance'
50
+ import { mailSuite } from '@avelonjs/conformance/suites'
51
+
52
+ mailSuite({
53
+ name: 'reference fake',
54
+ create: () => new FakeMail('transactional', 'mailer@example.test'),
55
+ })
56
+ ```
57
+
58
+ A driver either passes a capability's tests or declares that capability `false`. Declaring `true` and failing, or declaring `false` while still implementing the method, is a suite failure.
59
+
60
+ ## Reference Fakes
61
+
62
+ Each fake is an in-memory implementation of one contract. Fakes run the same suites as live drivers, so `new FakeMail()` cannot drift from the mail contract. Use them in application tests and in this package's own certification of the suites.
63
+
64
+ Capability-narrowed variants exist where a boolean or spectrum can be off: `FakeFlagsWithoutTargeting`, `FakeNotificationsWithoutChannels`, `FakeQueueWithoutRetries`, `FakeRateLimitWithoutAlgorithms`, and `FakeSearchWithoutFacets`.
65
+
66
+ ```ts
67
+ import { FakeFlags, FakeFlagsWithoutTargeting } from '@avelonjs/conformance'
68
+
69
+ const withTargeting = new FakeFlags()
70
+ const withoutTargeting = new FakeFlagsWithoutTargeting()
71
+
72
+ const targeted = await withTargeting.evaluate('assay.targeted', false, { actorId: 'actor-enabled' })
73
+ const globalOnly = await withoutTargeting.evaluate('assay.enabled', false)
74
+ ```
75
+
76
+ ## Capability Surfaces
77
+
78
+ `assertCapabilitySurface` checks both lies: a declared capability whose method is missing, and an implemented method whose capability is `false`. Suites call it; you may also call it from a driver test when a surface is method-gated.
79
+
80
+ ```ts
81
+ import { assertCapabilitySurface, captureFailure, FakeStorage } from '@avelonjs/conformance'
82
+
83
+ const storage = new FakeStorage()
84
+ assertCapabilitySurface(storage, 'signedUrls', storage.capabilities.signedUrls, ['signedUrl'])
85
+
86
+ const error = await captureFailure(() => storage.get('missing/avatar.png'))
87
+ ```
88
+
89
+ `captureFailure` returns the thrown value so suites can assert the framework error taxonomy instead of a vendor code.
90
+
91
+ ## Method Reference
92
+
93
+ | Method / export | Signature | Description |
94
+ |---|---|---|
95
+ | `assertCapabilitySurface` | `(driver: object, capability: string, declared: boolean, methods: readonly string[]) => void` | Fails when a declared method is missing or an undeclared method is present. |
96
+ | `captureFailure` | `(operation: () => Promise<unknown>) => Promise<unknown>` | Runs an operation expected to throw and returns the thrown value. |
97
+ | `SuiteContext` | `interface SuiteContext<TDriver>` | `name`, `create`, optional `cleanup`, and live-service helpers for one suite. |
98
+ | `FakeAI` | `class FakeAI` | In-memory AI driver used as the AI contract reference. |
99
+ | `FakeCache` | `class FakeCache` | In-memory cache driver used as the cache contract reference. |
100
+ | `FakeDatabase` | `class FakeDatabase` | In-memory database driver used as the database contract reference. |
101
+ | `FakeFlags` | `class FakeFlags` | In-memory flags driver with targeting enabled. |
102
+ | `FakeFlagsWithoutTargeting` | `class FakeFlagsWithoutTargeting` | Flags fake with targeting declared unavailable. |
103
+ | `FakeIdentity` | `class FakeIdentity` | In-memory identity driver used as the identity contract reference. |
104
+ | `FakeLogs` | `class FakeLogs` | In-memory log driver with traces enabled. |
105
+ | `FakeTraceRecord` | `interface` | Observable span state retained by `FakeLogs`. |
106
+ | `FakeMail` | `class FakeMail` | In-memory mail driver with hosted templates enabled. |
107
+ | `FakeNotifications` | `class FakeNotifications` | In-memory notifications driver with every channel declared. |
108
+ | `FakeNotificationsWithoutChannels` | `class FakeNotificationsWithoutChannels` | Notifications fake with an empty channel list. |
109
+ | `FakeSentNotification` | `interface` | One notification accepted by a notifications fake. |
110
+ | `FakePayments` | `class FakePayments` | In-memory payments driver used as the payments contract reference. |
111
+ | `FakeQueue` | `class FakeQueue` | In-memory queue driver with retries enabled. |
112
+ | `FakeQueueWithoutRetries` | `class FakeQueueWithoutRetries` | Queue fake with retries declared unavailable. |
113
+ | `FakeRateLimit` | `class FakeRateLimit` | In-memory rate-limit driver with every algorithm declared. |
114
+ | `FakeRateLimitWithoutAlgorithms` | `class FakeRateLimitWithoutAlgorithms` | Rate-limit fake with an empty algorithm list. |
115
+ | `FakeRealtime` | `class FakeRealtime` | In-memory realtime driver used as the realtime contract reference. |
116
+ | `FakeSearch` | `class FakeSearch` | In-memory search driver with facets enabled. |
117
+ | `FakeSearchWithoutFacets` | `class FakeSearchWithoutFacets` | Search fake with facets declared unavailable. |
118
+ | `FakeSocial` | `class FakeSocial` | In-memory social identity driver used as the social contract reference. |
119
+ | `FakeStorage` | `class FakeStorage` | In-memory storage driver used as the storage contract reference. |
120
+ | `FakeTokens` | `class FakeTokens` | In-memory API-token driver used as the tokens contract reference. |
121
+ | `aiSuite` | `(context: SuiteContext<TDriver>) => void` | Shared AI contract suite from `@avelonjs/conformance/suites`. |
122
+ | `cacheSuite` | `(context: SuiteContext<TDriver>) => void` | Shared cache contract suite. |
123
+ | `databaseSuite` | `(context: SuiteContext<TDriver>) => void` | Shared database contract suite. |
124
+ | `flagsSuite` | `(context: SuiteContext<TDriver>) => void` | Shared flags contract suite. |
125
+ | `identitySuite` | `(context: SuiteContext<IdentityFactory<TDriver>>) => void` | Shared identity contract suite. |
126
+ | `logsSuite` | `(context: SuiteContext<TDriver>) => void` | Shared logs contract suite. |
127
+ | `mailSuite` | `(context: SuiteContext<TDriver>) => void` | Shared mail contract suite. |
128
+ | `notificationsSuite` | `(context: SuiteContext<TDriver>) => void` | Shared notifications contract suite. |
129
+ | `paymentsSuite` | `(context: SuiteContext<TDriver>) => void` | Shared payments contract suite. |
130
+ | `queueSuite` | `(context: SuiteContext<TDriver>) => void` | Shared queue contract suite. |
131
+ | `ratelimitSuite` | `(context: SuiteContext<TDriver>) => void` | Shared rate-limit contract suite. |
132
+ | `realtimeSuite` | `(context: SuiteContext<TDriver>) => void` | Shared realtime contract suite. |
133
+ | `searchSuite` | `(context: SuiteContext<TDriver>) => void` | Shared search contract suite. |
134
+ | `socialSuite` | `(context: SuiteContext<TDriver>) => void` | Shared social contract suite. |
135
+ | `storageSuite` | `(context: SuiteContext<TDriver>) => void` | Shared storage contract suite. |
136
+ | `tokensSuite` | `(context: SuiteContext<TDriver>) => void` | Shared tokens contract suite. |
137
+
138
+ ## Testing
139
+
140
+ This package is the suite. Run it against the shipped fakes here, and again against a real driver in that driver's package. A deliberately broken stub in `tests/` proves the suite rejects compiler defects rather than echoing the fake back to itself.
141
+
142
+ ```ts
143
+ import { FakeStorage } from '@avelonjs/conformance'
144
+ import { storageSuite } from '@avelonjs/conformance/suites'
145
+
146
+ storageSuite({
147
+ name: 'reference fake',
148
+ create: () => new FakeStorage(),
149
+ })
150
+ ```
151
+
152
+ ```sh
153
+ bun test
154
+ bun run typecheck
155
+ ```
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@avelonjs/conformance",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Contract test suites and in-memory fakes for Avelon drivers.",
6
+ "license": "MIT",
7
+ "author": "Ryan Yannelli <ryanyannelli@gmail.com>",
8
+ "homepage": "https://github.com/yannelli/avelon",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/yannelli/avelon.git",
12
+ "directory": "packages/conformance"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/yannelli/avelon/issues"
16
+ },
17
+ "keywords": [
18
+ "avelon",
19
+ "typescript",
20
+ "testing",
21
+ "conformance"
22
+ ],
23
+ "type": "module",
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "files": [
28
+ "src",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
32
+ "exports": {
33
+ ".": "./src/index.ts",
34
+ "./suites": "./src/suites/index.ts"
35
+ },
36
+ "scripts": {
37
+ "test": "bun test",
38
+ "typecheck": "tsc --noEmit"
39
+ },
40
+ "dependencies": {
41
+ "@avelonjs/core": "workspace:*"
42
+ },
43
+ "devDependencies": {
44
+ "@types/bun": "1.3.14",
45
+ "typescript": "5.9.3"
46
+ },
47
+ "engines": {
48
+ "bun": ">=1.3.14"
49
+ }
50
+ }
@@ -0,0 +1,113 @@
1
+ import {
2
+ Invalid,
3
+ type AiDriver,
4
+ type CompletionAiSurface,
5
+ type CompletionChunk,
6
+ type CompletionRequest,
7
+ type CompletionResult,
8
+ type EmbeddingAiSurface,
9
+ type EmbeddingResult,
10
+ type StreamingAiSurface,
11
+ } from '@avelonjs/core'
12
+
13
+ const capabilities = {
14
+ modes: ['completion', 'embedding'],
15
+ streaming: true,
16
+ } as const
17
+
18
+ interface FakeAiRaw {
19
+ readonly completions: number
20
+ readonly embeddings: number
21
+ }
22
+
23
+ function completionText(request: CompletionRequest): string {
24
+ return request.messages.map((message) => message.content).join(' ')
25
+ }
26
+
27
+ function vector(input: string): readonly number[] {
28
+ let sum = 0
29
+ let weighted = 0
30
+ for (const [index, character] of [...input].entries()) {
31
+ const code = character.codePointAt(0) ?? 0
32
+ sum += code
33
+ weighted += code * (index + 1)
34
+ }
35
+ return [input.length, sum % 997, weighted % 991, (sum + weighted) % 983]
36
+ }
37
+
38
+ /** Deterministic in-memory AI reference implementation used by conformance and application tests. */
39
+ export class FakeAI
40
+ implements
41
+ AiDriver<typeof capabilities, FakeAiRaw>,
42
+ CompletionAiSurface,
43
+ EmbeddingAiSurface,
44
+ StreamingAiSurface
45
+ {
46
+ /** Driver implementation name. */
47
+ readonly name = 'fake'
48
+
49
+ /** Configured AI connection name. */
50
+ readonly instance: string
51
+
52
+ /** Exact mode and streaming declaration. */
53
+ readonly capabilities = capabilities
54
+
55
+ #completions = 0
56
+ #embeddings = 0
57
+
58
+ /** Creates an isolated AI connection. */
59
+ constructor(instance = 'default') {
60
+ this.instance = instance
61
+ }
62
+
63
+ /** Returns operation counts for the in-memory implementation. */
64
+ raw(): FakeAiRaw {
65
+ return { completions: this.#completions, embeddings: this.#embeddings }
66
+ }
67
+
68
+ /** Produces deterministic text from the ordered request messages. */
69
+ async complete(request: CompletionRequest): Promise<CompletionResult> {
70
+ this.validateModel(request.model)
71
+ this.#completions += 1
72
+ const text = completionText(request)
73
+ return {
74
+ text,
75
+ inputTokens: request.messages.reduce((total, message) => total + message.content.length, 0),
76
+ outputTokens: text.length,
77
+ }
78
+ }
79
+
80
+ /** Embeds every input into a deterministic four-dimensional vector. */
81
+ async embed(model: string, input: string | readonly string[]): Promise<EmbeddingResult> {
82
+ this.validateModel(model)
83
+ const inputs = typeof input === 'string' ? [input] : input
84
+ if (inputs.length === 0) {
85
+ throw new Invalid('At least one embedding input is required.', {
86
+ metadata: { fields: { input: ['Provide one or more strings to embed.'] } },
87
+ })
88
+ }
89
+ this.#embeddings += 1
90
+ return {
91
+ embeddings: inputs.map(vector),
92
+ inputTokens: inputs.reduce((total, value) => total + value.length, 0),
93
+ }
94
+ }
95
+
96
+ /** Streams deterministic completion text in ordered increments followed by one terminal chunk. */
97
+ async *stream(request: CompletionRequest): AsyncIterable<CompletionChunk> {
98
+ this.validateModel(request.model)
99
+ const text = completionText(request)
100
+ const split = Math.max(1, Math.ceil(text.length / 2))
101
+ yield { text: text.slice(0, split), done: false }
102
+ if (split < text.length) yield { text: text.slice(split), done: false }
103
+ yield { text: '', done: true }
104
+ }
105
+
106
+ private validateModel(model: string): void {
107
+ if (model.length === 0) {
108
+ throw new Invalid('An AI model identifier is required.', {
109
+ metadata: { fields: { model: ['A model identifier is required.'] } },
110
+ })
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,153 @@
1
+ import {
2
+ Conflict,
3
+ type CacheDriver,
4
+ type CacheStore,
5
+ type LockCacheSurface,
6
+ type TaggedCacheSurface,
7
+ } from '@avelonjs/core'
8
+
9
+ const capabilities = {
10
+ tags: true,
11
+ locks: true,
12
+ } as const
13
+
14
+ interface CacheEntry {
15
+ value: unknown
16
+ expiresAt: number | null
17
+ tags: ReadonlySet<string>
18
+ }
19
+
20
+ interface HeldLock {
21
+ token: symbol
22
+ expiresAt: number
23
+ }
24
+
25
+ interface FakeCacheRaw {
26
+ entries: number
27
+ locks: number
28
+ }
29
+
30
+ /** In-memory cache reference implementation used by conformance and application tests. */
31
+ export class FakeCache
32
+ implements CacheDriver<typeof capabilities, FakeCacheRaw>, TaggedCacheSurface, LockCacheSurface
33
+ {
34
+ /** Driver implementation name. */
35
+ readonly name = 'fake'
36
+
37
+ /** Configured cache store name. */
38
+ readonly instance: string
39
+
40
+ /** Exact optional-feature declaration. */
41
+ readonly capabilities = capabilities
42
+
43
+ readonly #entries = new Map<string, CacheEntry>()
44
+ readonly #locks = new Map<string, HeldLock>()
45
+
46
+ /** Creates an isolated cache store. */
47
+ constructor(instance = 'default') {
48
+ this.instance = instance
49
+ }
50
+
51
+ /** Returns observable entry and active-lock counts. */
52
+ raw(): FakeCacheRaw {
53
+ return { entries: this.#entries.size, locks: this.#locks.size }
54
+ }
55
+
56
+ /** Reads a value or returns null when absent or expired. */
57
+ async get<TValue>(key: string): Promise<TValue | null> {
58
+ return this.getTagged<TValue>(key, [])
59
+ }
60
+
61
+ /** Stores a value with an optional lifetime in seconds. */
62
+ async put<TValue>(key: string, value: TValue, ttlSeconds?: number): Promise<void> {
63
+ this.putTagged(key, value, ttlSeconds, [])
64
+ }
65
+
66
+ /** Removes a key when present. */
67
+ async forget(key: string): Promise<void> {
68
+ this.forgetTagged(key, [])
69
+ }
70
+
71
+ /** Removes every cache entry. */
72
+ async flush(): Promise<void> {
73
+ this.#entries.clear()
74
+ }
75
+
76
+ /** Returns a namespace whose writes carry every supplied tag. */
77
+ tags(names: readonly string[]): CacheStore {
78
+ const required = [...new Set(names)]
79
+ return {
80
+ get: <TValue>(key: string) => this.getTagged<TValue>(key, required),
81
+ put: <TValue>(key: string, value: TValue, ttlSeconds?: number) => {
82
+ this.putTagged(key, value, ttlSeconds, required)
83
+ return Promise.resolve()
84
+ },
85
+ forget: (key: string) => {
86
+ this.forgetTagged(key, required)
87
+ return Promise.resolve()
88
+ },
89
+ flush: () => {
90
+ this.flushTagged(required)
91
+ return Promise.resolve()
92
+ },
93
+ }
94
+ }
95
+
96
+ /** Runs a callback with exclusive ownership of a named expiring lock. */
97
+ async lock<TResult>(
98
+ key: string,
99
+ ttlSeconds: number,
100
+ callback: () => Promise<TResult>,
101
+ ): Promise<TResult> {
102
+ const now = Date.now()
103
+ const held = this.#locks.get(key)
104
+ if (held && held.expiresAt > now) {
105
+ throw new Conflict(`Cache lock ${key} is already held.`, {
106
+ metadata: { resource: 'cache-lock', key },
107
+ })
108
+ }
109
+
110
+ const token = Symbol(key)
111
+ this.#locks.set(key, { token, expiresAt: now + ttlSeconds * 1000 })
112
+ try {
113
+ return await callback()
114
+ } finally {
115
+ if (this.#locks.get(key)?.token === token) this.#locks.delete(key)
116
+ }
117
+ }
118
+
119
+ private async getTagged<TValue>(key: string, tags: readonly string[]): Promise<TValue | null> {
120
+ const entry = this.#entries.get(key)
121
+ if (!entry) return null
122
+ if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
123
+ this.#entries.delete(key)
124
+ return null
125
+ }
126
+ if (!tags.every((tag) => entry.tags.has(tag))) return null
127
+ return entry.value as TValue
128
+ }
129
+
130
+ private putTagged<TValue>(
131
+ key: string,
132
+ value: TValue,
133
+ ttlSeconds: number | undefined,
134
+ tags: readonly string[],
135
+ ): void {
136
+ this.#entries.set(key, {
137
+ value,
138
+ expiresAt: ttlSeconds === undefined ? null : Date.now() + ttlSeconds * 1000,
139
+ tags: new Set(tags),
140
+ })
141
+ }
142
+
143
+ private forgetTagged(key: string, tags: readonly string[]): void {
144
+ const entry = this.#entries.get(key)
145
+ if (entry && tags.every((tag) => entry.tags.has(tag))) this.#entries.delete(key)
146
+ }
147
+
148
+ private flushTagged(tags: readonly string[]): void {
149
+ for (const [key, entry] of this.#entries) {
150
+ if (tags.every((tag) => entry.tags.has(tag))) this.#entries.delete(key)
151
+ }
152
+ }
153
+ }