@strav/testing 0.4.31 → 1.0.0-alpha.25
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/README.md +34 -71
- package/package.json +23 -29
- package/src/boot_test_app.ts +169 -0
- package/src/brain/index.ts +1 -0
- package/src/brain/stub_brain_provider.ts +72 -0
- package/src/cache/index.ts +2 -0
- package/src/cache/is_memcached_available.ts +75 -0
- package/src/cache/is_redis_available.ts +39 -0
- package/src/compose_test_config.ts +47 -0
- package/src/index.ts +31 -15
- package/src/mem_stream.ts +50 -0
- package/src/postgres/connected_role_bypasses_rls.ts +22 -0
- package/src/postgres/create_test_database.ts +20 -0
- package/src/postgres/index.ts +5 -0
- package/src/postgres/is_postgres_available.ts +33 -0
- package/src/postgres/reset_schema.ts +14 -0
- package/src/postgres/test_database_url.ts +33 -0
- package/src/stub_fetch.ts +56 -0
- package/src/tenant_manager_provider.ts +40 -0
- package/CHANGELOG.md +0 -7
- package/src/browser/db_fresh.ts +0 -38
- package/src/browser/demo_flow.ts +0 -89
- package/src/browser/index.ts +0 -11
- package/src/browser/server_lifecycle.ts +0 -42
- package/src/browser/test_case.ts +0 -572
- package/src/database_manager.ts +0 -131
- package/src/factory.ts +0 -68
- package/src/test_case.ts +0 -312
- package/tsconfig.json +0 -5
package/README.md
CHANGED
|
@@ -1,82 +1,45 @@
|
|
|
1
1
|
# @strav/testing
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
bun add -d @strav/testing
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Requires `@strav/core` as a peer dependency.
|
|
12
|
-
|
|
13
|
-
## TestCase
|
|
14
|
-
|
|
15
|
-
Boots the app, provides HTTP helpers, and wraps each test in a rolled-back transaction for full isolation.
|
|
3
|
+
Small, focused testing utilities for Strav apps and the framework itself.
|
|
16
4
|
|
|
17
5
|
```ts
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const user = await UserFactory.create()
|
|
34
|
-
await t.actingAs(user)
|
|
35
|
-
|
|
36
|
-
const res = await t.post('/api/posts', { title: 'Hello' })
|
|
37
|
-
expect(res.status).toBe(201)
|
|
38
|
-
})
|
|
39
|
-
})
|
|
6
|
+
import {
|
|
7
|
+
bootTestApp,
|
|
8
|
+
isPostgresAvailable,
|
|
9
|
+
MemStream,
|
|
10
|
+
stubFetch,
|
|
11
|
+
} from '@strav/testing'
|
|
12
|
+
|
|
13
|
+
if (!await isPostgresAvailable()) {
|
|
14
|
+
test.skip('integration: …', () => {})
|
|
15
|
+
} else {
|
|
16
|
+
// real Postgres flow
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const stdout = new MemStream()
|
|
20
|
+
const fetch = stubFetch(async (req) => Response.json({ ok: true }))
|
|
40
21
|
```
|
|
41
22
|
|
|
42
|
-
|
|
23
|
+
Canonical docs live in [`docs/testing/README.md`](../../docs/testing/README.md).
|
|
43
24
|
|
|
44
|
-
|
|
45
|
-
await t.get('/path')
|
|
46
|
-
await t.post('/path', body)
|
|
47
|
-
await t.put('/path', body)
|
|
48
|
-
await t.patch('/path', body)
|
|
49
|
-
await t.delete('/path')
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Auth Helpers
|
|
53
|
-
|
|
54
|
-
```ts
|
|
55
|
-
await t.actingAs(user) // authenticate as user
|
|
56
|
-
t.withHeaders({ 'X-Custom': 'value' })
|
|
57
|
-
t.withoutAuth() // clear auth token
|
|
58
|
-
```
|
|
25
|
+
## What ships
|
|
59
26
|
|
|
60
|
-
|
|
27
|
+
| Surface | Notes |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `bootTestApp({ config, schemas, migrations, providers })` | Replaces the ~50-line `beforeAll` boilerplate every e2e was rolling. Auto-supplies the standard four providers, applies schemas + migrations against `setupDb`, returns `{ app, setupDb, dispose }`. |
|
|
30
|
+
| `composeTestConfig(overrides)` | Merges logger + database defaults with per-test overrides. Used internally by `bootTestApp`; exported for tests that want the config tree without the orchestrator. |
|
|
31
|
+
| `TenantManagerProvider` | Standard 3-line wiring extracted from m5 / m6 / m7. |
|
|
32
|
+
| `MemStream` | In-memory `NodeJS.WritableStream` for asserting on stdout / stderr. Pairs with `ConsoleOutput`. |
|
|
33
|
+
| `stubFetch(handler)` | Typed `fetch` replacement. Confines the `as unknown as typeof fetch` cast to one place. |
|
|
34
|
+
| `isPostgresAvailable()` | Cached probe — returns `false` when env is missing or connection fails. |
|
|
35
|
+
| `createTestDatabase()` | Construct a fresh `PostgresDatabase` from `DB_HOST`/`DB_PORT`/etc. |
|
|
36
|
+
| `resetSchema(db)` | DROP + recreate `public` schema. |
|
|
37
|
+
| `connectedRoleBypassesRls(db)` | True for SUPERUSER / BYPASSRLS roles — tests use it to degrade RLS assertions. |
|
|
38
|
+
| `testDatabaseUrl()` | Returns the Postgres URL or `null` when env is missing. |
|
|
61
39
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
import { Factory } from '@strav/testing'
|
|
66
|
-
|
|
67
|
-
const UserFactory = Factory.define(User, (seq) => ({
|
|
68
|
-
pid: crypto.randomUUID(),
|
|
69
|
-
name: `User ${seq}`,
|
|
70
|
-
email: `user-${seq}@test.com`,
|
|
71
|
-
passwordHash: 'hashed',
|
|
72
|
-
}))
|
|
73
|
-
|
|
74
|
-
const user = await UserFactory.create()
|
|
75
|
-
const users = await UserFactory.createMany(5)
|
|
76
|
-
const user = await UserFactory.create({ name: 'Custom' })
|
|
77
|
-
const instance = UserFactory.make() // in-memory only, no DB
|
|
78
|
-
```
|
|
40
|
+
## Subpaths
|
|
79
41
|
|
|
80
|
-
|
|
42
|
+
- `@strav/testing/postgres` — narrow import for just the Postgres helpers.
|
|
43
|
+
- `@strav/testing/brain` — `stubBrainProvider({ embed, model? })`. Requires `@strav/brain` installed (peer-optional).
|
|
81
44
|
|
|
82
|
-
|
|
45
|
+
Deferred to follow-up slices: `stubPaymentDriver`, `stubSocialDriver` — extract when the inline forms in `tests/e2e/m{6,7}-*/` show overlap.
|
package/package.json
CHANGED
|
@@ -1,44 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strav/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-alpha.25",
|
|
4
|
+
"description": "Strav testing utilities — in-memory stream, typed fetch stub, Postgres availability probe + schema reset, bootTestApp orchestrator, stub providers.",
|
|
4
5
|
"type": "module",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
7
8
|
"exports": {
|
|
8
9
|
".": "./src/index.ts",
|
|
9
|
-
"
|
|
10
|
+
"./brain": "./src/brain/index.ts",
|
|
11
|
+
"./cache": "./src/cache/index.ts",
|
|
12
|
+
"./postgres": "./src/postgres/index.ts"
|
|
10
13
|
},
|
|
11
14
|
"files": [
|
|
12
|
-
"src
|
|
13
|
-
"
|
|
14
|
-
"tsconfig.json",
|
|
15
|
-
"CHANGELOG.md"
|
|
15
|
+
"src",
|
|
16
|
+
"README.md"
|
|
16
17
|
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"bun": ">=1.3.14"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@strav/database": "1.0.0-alpha.25",
|
|
26
|
+
"@strav/kernel": "1.0.0-alpha.25"
|
|
27
|
+
},
|
|
17
28
|
"peerDependencies": {
|
|
18
|
-
"@strav/
|
|
19
|
-
"@
|
|
20
|
-
"@strav/view": "0.4.31",
|
|
21
|
-
"@strav/database": "0.4.31",
|
|
22
|
-
"@strav/signal": "0.4.31",
|
|
23
|
-
"@strav/cli": "0.4.31",
|
|
24
|
-
"playwright-core": "^1.45.0"
|
|
29
|
+
"@strav/brain": "1.0.0-alpha.25",
|
|
30
|
+
"@types/bun": ">=1.3.14"
|
|
25
31
|
},
|
|
26
32
|
"peerDependenciesMeta": {
|
|
27
|
-
"@strav/
|
|
28
|
-
"optional": true
|
|
29
|
-
},
|
|
30
|
-
"@strav/cli": {
|
|
31
|
-
"optional": true
|
|
32
|
-
},
|
|
33
|
-
"playwright-core": {
|
|
33
|
+
"@strav/brain": {
|
|
34
34
|
"optional": true
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
|
-
"
|
|
38
|
-
"test": "bun test tests/",
|
|
39
|
-
"typecheck": "tsc --noEmit"
|
|
40
|
-
},
|
|
41
|
-
"devDependencies": {
|
|
42
|
-
"playwright-core": "^1.45.0"
|
|
43
|
-
}
|
|
37
|
+
"devDependencies": null
|
|
44
38
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot a tested Application with the standard four providers
|
|
3
|
+
* (`ConfigProvider` → `LoggerProvider` → `DatabaseProvider` →
|
|
4
|
+
* `TenantManagerProvider`) pre-installed, real Postgres reset, schemas
|
|
5
|
+
* applied, optional migrations run, and the per-test providers wired
|
|
6
|
+
* on top. Returns the started `app`, the admin-level `setupDb` (kept
|
|
7
|
+
* open until `dispose()`), and the `dispose()` cleanup that pairs with
|
|
8
|
+
* `afterAll`.
|
|
9
|
+
*
|
|
10
|
+
* Replaces the ~50 LOC `beforeAll` boilerplate that every integration
|
|
11
|
+
* / e2e suite was rolling by hand. Per-test variation (custom config
|
|
12
|
+
* sub-trees, schemas, migrations, post-boot `useDriver` hand-wiring,
|
|
13
|
+
* HTTP server spin-up) lives where it always did.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { afterAll, beforeAll, describe } from 'bun:test'
|
|
17
|
+
* import {
|
|
18
|
+
* bootTestApp,
|
|
19
|
+
* isPostgresAvailable,
|
|
20
|
+
* type BootTestAppResult,
|
|
21
|
+
* } from '@strav/testing'
|
|
22
|
+
* import { RagProvider, ragVectorSchema, applyRagVectorMigration } from '@strav/rag'
|
|
23
|
+
*
|
|
24
|
+
* const PG = await isPostgresAvailable()
|
|
25
|
+
*
|
|
26
|
+
* describe.skipIf(!PG)('M5 e2e', () => {
|
|
27
|
+
* let booted: BootTestAppResult
|
|
28
|
+
* beforeAll(async () => {
|
|
29
|
+
* booted = await bootTestApp({
|
|
30
|
+
* config: { rag: { ... } },
|
|
31
|
+
* schemas: [tenantSchema, articleSchema, ragVectorSchema],
|
|
32
|
+
* migrations: [(db, registry) => applyRagVectorMigration(db, { dimension: 4, registry })],
|
|
33
|
+
* providers: [new StubBrainProvider(), new RagProvider()],
|
|
34
|
+
* })
|
|
35
|
+
* })
|
|
36
|
+
* afterAll(() => booted.dispose())
|
|
37
|
+
*
|
|
38
|
+
* // test cases pull from `booted.app.resolve(...)` and `booted.setupDb`.
|
|
39
|
+
* })
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* The orchestration order, per spec from the e2e survey:
|
|
43
|
+
*
|
|
44
|
+
* 1. `createTestDatabase()` → admin connection for setup.
|
|
45
|
+
* 2. `resetSchema(setupDb)` → DROP + CREATE `public`.
|
|
46
|
+
* 3. Apply each schema's `emitCreateTable(schema, { registry }).sql`.
|
|
47
|
+
* 4. Run each migration with `(setupDb, registry)`.
|
|
48
|
+
* 5. Construct config via `composeTestConfig(config)` (auto-supplies
|
|
49
|
+
* logger + database.url unless overridden).
|
|
50
|
+
* 6. `new Application()`, `useProviders([Config, Logger, Database,
|
|
51
|
+
* TenantManager, ...userProviders])` unless `skipDefaultProviders`.
|
|
52
|
+
* 7. Bind `SchemaRegistry` as a singleton with `schemas` registered.
|
|
53
|
+
* 8. `await app.start({ signalHandlers: false })`.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import {
|
|
57
|
+
DatabaseProvider,
|
|
58
|
+
PostgresDatabase,
|
|
59
|
+
type Schema,
|
|
60
|
+
SchemaRegistry,
|
|
61
|
+
emitCreateTable,
|
|
62
|
+
} from '@strav/database'
|
|
63
|
+
import {
|
|
64
|
+
Application,
|
|
65
|
+
ConfigProvider,
|
|
66
|
+
LoggerProvider,
|
|
67
|
+
type ServiceProvider,
|
|
68
|
+
} from '@strav/kernel'
|
|
69
|
+
import { type ConfigOverrides, composeTestConfig } from './compose_test_config.ts'
|
|
70
|
+
import { createTestDatabase } from './postgres/create_test_database.ts'
|
|
71
|
+
import { resetSchema } from './postgres/reset_schema.ts'
|
|
72
|
+
import { TenantManagerProvider } from './tenant_manager_provider.ts'
|
|
73
|
+
|
|
74
|
+
/** Migration hook signature — runs against `setupDb` before `app.start`. */
|
|
75
|
+
export type TestMigration = (
|
|
76
|
+
db: PostgresDatabase,
|
|
77
|
+
registry: SchemaRegistry,
|
|
78
|
+
) => Promise<void> | void
|
|
79
|
+
|
|
80
|
+
export interface BootTestAppOptions {
|
|
81
|
+
/**
|
|
82
|
+
* Per-test config sub-trees. `logger` and `database` keys are
|
|
83
|
+
* auto-supplied unless overridden here. Other keys (`rag`, `payment`,
|
|
84
|
+
* `social`, `encryption`, …) are passed through verbatim.
|
|
85
|
+
*/
|
|
86
|
+
config?: ConfigOverrides
|
|
87
|
+
/**
|
|
88
|
+
* Schemas registered in the `SchemaRegistry` singleton AND applied
|
|
89
|
+
* via `emitCreateTable(schema, { registry }).sql` against `setupDb`
|
|
90
|
+
* before `app.start`. Order matters when there are FK dependencies.
|
|
91
|
+
*/
|
|
92
|
+
schemas?: readonly Schema[]
|
|
93
|
+
/**
|
|
94
|
+
* Migrations to run after `schemas` are applied. Each receives the
|
|
95
|
+
* admin connection + the registry so it can compose SQL the same way
|
|
96
|
+
* the production migration helpers do.
|
|
97
|
+
*/
|
|
98
|
+
migrations?: readonly TestMigration[]
|
|
99
|
+
/**
|
|
100
|
+
* Per-test service providers — appended after the standard four
|
|
101
|
+
* (`Config`, `Logger`, `Database`, `TenantManager`). Order within
|
|
102
|
+
* this list matters for the kernel's topological sort.
|
|
103
|
+
*/
|
|
104
|
+
providers?: readonly ServiceProvider[]
|
|
105
|
+
/**
|
|
106
|
+
* Opt out of the standard four. Useful for tests that need a custom
|
|
107
|
+
* `LoggerProvider`, no `DatabaseProvider`, etc. When `true`, you own
|
|
108
|
+
* the entire provider list via `providers`. Default `false`.
|
|
109
|
+
*/
|
|
110
|
+
skipDefaultProviders?: boolean
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface BootTestAppResult {
|
|
114
|
+
/** Started Application — call `resolve(X)` from test cases. */
|
|
115
|
+
app: Application
|
|
116
|
+
/**
|
|
117
|
+
* Admin-level Postgres connection used to apply DDL + migrations and
|
|
118
|
+
* to run setup queries (seeding tenants, asserting schema state). Kept
|
|
119
|
+
* open until `dispose()`.
|
|
120
|
+
*/
|
|
121
|
+
setupDb: PostgresDatabase
|
|
122
|
+
/** Cleanup: shutdown app + close setupDb. Use in `afterAll`. */
|
|
123
|
+
dispose(): Promise<void>
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function bootTestApp(options: BootTestAppOptions = {}): Promise<BootTestAppResult> {
|
|
127
|
+
const setupDb = createTestDatabase()
|
|
128
|
+
await resetSchema(setupDb)
|
|
129
|
+
|
|
130
|
+
const schemas = options.schemas ?? []
|
|
131
|
+
const registry = new SchemaRegistry().registerAll(schemas)
|
|
132
|
+
|
|
133
|
+
for (const schema of schemas) {
|
|
134
|
+
await setupDb.execute(emitCreateTable(schema, { registry }).sql)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const migration of options.migrations ?? []) {
|
|
138
|
+
await migration(setupDb, registry)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const configData = composeTestConfig(options.config ?? {})
|
|
142
|
+
|
|
143
|
+
const app = new Application()
|
|
144
|
+
const defaults = options.skipDefaultProviders
|
|
145
|
+
? []
|
|
146
|
+
: [
|
|
147
|
+
new ConfigProvider(configData),
|
|
148
|
+
new LoggerProvider(),
|
|
149
|
+
new DatabaseProvider(),
|
|
150
|
+
new TenantManagerProvider(),
|
|
151
|
+
]
|
|
152
|
+
app.useProviders([...defaults, ...(options.providers ?? [])])
|
|
153
|
+
|
|
154
|
+
// Bind the SchemaRegistry singleton so repositories that need it can
|
|
155
|
+
// resolve through the container instead of getting hand-wired.
|
|
156
|
+
app.singleton(SchemaRegistry, () => registry)
|
|
157
|
+
|
|
158
|
+
await app.start({ signalHandlers: false })
|
|
159
|
+
|
|
160
|
+
let disposed = false
|
|
161
|
+
const dispose = async (): Promise<void> => {
|
|
162
|
+
if (disposed) return
|
|
163
|
+
disposed = true
|
|
164
|
+
await app.shutdown()
|
|
165
|
+
await setupDb.close({ timeout: 2 })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { app, setupDb, dispose }
|
|
169
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { stubBrainProvider, type StubBrainOptions } from './stub_brain_provider.ts'
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stub `BrainManager` wired as a `ServiceProvider` for tests that need
|
|
3
|
+
* deterministic embeddings without dialing an actual brain backend.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `tests/e2e/m5-rag/`. The provider declares
|
|
6
|
+
* `name: 'brain'` and `dependencies: ['config']`, matching the real
|
|
7
|
+
* `BrainProvider`'s shape — `RagProvider` (which declares
|
|
8
|
+
* `dependencies: ['config', 'brain']`) resolves the stub correctly
|
|
9
|
+
* when both are in the provider list.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { stubBrainProvider } from '@strav/testing/brain'
|
|
13
|
+
*
|
|
14
|
+
* const provider = stubBrainProvider({
|
|
15
|
+
* embed: (text) => bagOfWords(text), // returns number[]
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* app.useProviders([
|
|
19
|
+
* new ConfigProvider({ ... }),
|
|
20
|
+
* new LoggerProvider(),
|
|
21
|
+
* new DatabaseProvider(),
|
|
22
|
+
* provider, // ← stub registered here
|
|
23
|
+
* new RagProvider(), // ← resolves the stub for its embed calls
|
|
24
|
+
* ])
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* The `embed` callback is per-text — the helper maps over the input
|
|
28
|
+
* array internally. Only `embed` is stubbed; other `BrainManager`
|
|
29
|
+
* surface (`chat`, `stream`, `runWithTools`, …) throws when called.
|
|
30
|
+
* V1 scope is rag-style tests; broader stubs land when a second
|
|
31
|
+
* use case appears.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { BrainManager } from '@strav/brain'
|
|
35
|
+
import { type Application, ServiceProvider } from '@strav/kernel'
|
|
36
|
+
|
|
37
|
+
export interface StubBrainOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Returns the embedding vector for a single text. The provider maps
|
|
40
|
+
* the user's `embed` over `texts` internally to produce `number[][]`.
|
|
41
|
+
*/
|
|
42
|
+
embed: (text: string) => number[]
|
|
43
|
+
/** Model identifier surfaced on `embed` results. Default `'stub'`. */
|
|
44
|
+
model?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function stubBrainProvider(options: StubBrainOptions): ServiceProvider {
|
|
48
|
+
const model = options.model ?? 'stub'
|
|
49
|
+
const userEmbed = options.embed
|
|
50
|
+
return new (class StubBrainProvider extends ServiceProvider {
|
|
51
|
+
override readonly name = 'brain'
|
|
52
|
+
override readonly dependencies = ['config']
|
|
53
|
+
override register(app: Application): void {
|
|
54
|
+
app.singleton(BrainManager, () => buildStub(userEmbed, model))
|
|
55
|
+
}
|
|
56
|
+
})()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildStub(
|
|
60
|
+
embed: (text: string) => number[],
|
|
61
|
+
model: string,
|
|
62
|
+
): BrainManager {
|
|
63
|
+
const stub = {
|
|
64
|
+
embed: async (texts: readonly string[]) => ({
|
|
65
|
+
embeddings: texts.map(embed),
|
|
66
|
+
model,
|
|
67
|
+
usage: { inputTokens: 0 },
|
|
68
|
+
raw: null,
|
|
69
|
+
}),
|
|
70
|
+
}
|
|
71
|
+
return stub as unknown as BrainManager
|
|
72
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheap connection probe over Bun's TCP socket — opens a connection,
|
|
3
|
+
* sends `version\r\n`, expects a `VERSION ...\r\n` reply. Cached for
|
|
4
|
+
* the lifetime of the test process.
|
|
5
|
+
*
|
|
6
|
+
* Returns `false` if `MEMCACHED_HOST` / `MEMCACHED_PORT` are missing
|
|
7
|
+
* OR the probe fails. Pair with
|
|
8
|
+
* `describe.skipIf(!await isMemcachedAvailable())`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let cachedAvailability: boolean | null = null
|
|
12
|
+
|
|
13
|
+
export async function isMemcachedAvailable(): Promise<boolean> {
|
|
14
|
+
if (cachedAvailability !== null) return cachedAvailability
|
|
15
|
+
const host = process.env['MEMCACHED_HOST']
|
|
16
|
+
const portStr = process.env['MEMCACHED_PORT']
|
|
17
|
+
if (host === undefined || host === '' || portStr === undefined || portStr === '') {
|
|
18
|
+
cachedAvailability = false
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
const port = Number(portStr)
|
|
22
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
23
|
+
cachedAvailability = false
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
cachedAvailability = await probe(host, port)
|
|
28
|
+
} catch {
|
|
29
|
+
cachedAvailability = false
|
|
30
|
+
}
|
|
31
|
+
return cachedAvailability
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function probe(host: string, port: number): Promise<boolean> {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
let settled = false
|
|
37
|
+
const finish = (ok: boolean): void => {
|
|
38
|
+
if (settled) return
|
|
39
|
+
settled = true
|
|
40
|
+
resolve(ok)
|
|
41
|
+
}
|
|
42
|
+
const timeout = setTimeout(() => finish(false), 2_000)
|
|
43
|
+
void Bun.connect({
|
|
44
|
+
hostname: host,
|
|
45
|
+
port,
|
|
46
|
+
socket: {
|
|
47
|
+
open(socket) {
|
|
48
|
+
socket.write('version\r\n')
|
|
49
|
+
},
|
|
50
|
+
data(socket, chunk) {
|
|
51
|
+
const bytes = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)
|
|
52
|
+
const text = new TextDecoder().decode(bytes)
|
|
53
|
+
const ok = text.startsWith('VERSION')
|
|
54
|
+
// Settle before closing — `socket.end()` synchronously fires
|
|
55
|
+
// the `close` callback, which would beat `finish(ok)` to the
|
|
56
|
+
// settle line and report a false negative.
|
|
57
|
+
clearTimeout(timeout)
|
|
58
|
+
finish(ok)
|
|
59
|
+
socket.end()
|
|
60
|
+
},
|
|
61
|
+
error(_socket, _error) {
|
|
62
|
+
clearTimeout(timeout)
|
|
63
|
+
finish(false)
|
|
64
|
+
},
|
|
65
|
+
close() {
|
|
66
|
+
clearTimeout(timeout)
|
|
67
|
+
if (!settled) finish(false)
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
}).catch(() => {
|
|
71
|
+
clearTimeout(timeout)
|
|
72
|
+
finish(false)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheap connection probe — opens `Bun.RedisClient` against
|
|
3
|
+
* `REDIS_URL`, sends `PING`, reports. Cached for the lifetime of the
|
|
4
|
+
* test process.
|
|
5
|
+
*
|
|
6
|
+
* Returns `false` if `REDIS_URL` is missing OR the connection / PING
|
|
7
|
+
* fails. Pair with `describe.skipIf(!await isRedisAvailable())`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { RedisClient } from 'bun'
|
|
11
|
+
|
|
12
|
+
let cachedAvailability: boolean | null = null
|
|
13
|
+
|
|
14
|
+
export async function isRedisAvailable(): Promise<boolean> {
|
|
15
|
+
if (cachedAvailability !== null) return cachedAvailability
|
|
16
|
+
const url = process.env['REDIS_URL']
|
|
17
|
+
if (url === undefined || url === '') {
|
|
18
|
+
cachedAvailability = false
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
let client: RedisClient | undefined
|
|
22
|
+
try {
|
|
23
|
+
client = new RedisClient(url)
|
|
24
|
+
// `send('PING', [])` is supported on every Bun.RedisClient build —
|
|
25
|
+
// safer than `ping()` which isn't on the typed surface.
|
|
26
|
+
const reply = await client.send('PING', [])
|
|
27
|
+
cachedAvailability = reply === 'PONG' || reply === 'OK' || typeof reply === 'string'
|
|
28
|
+
return cachedAvailability
|
|
29
|
+
} catch {
|
|
30
|
+
cachedAvailability = false
|
|
31
|
+
return false
|
|
32
|
+
} finally {
|
|
33
|
+
try {
|
|
34
|
+
client?.close()
|
|
35
|
+
} catch {
|
|
36
|
+
// Already closed / never connected — nothing to clean.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge per-test config overrides with the standard test defaults.
|
|
3
|
+
*
|
|
4
|
+
* Defaults supplied:
|
|
5
|
+
*
|
|
6
|
+
* - `logger`: silent + stderr channel — keeps `bun test` output clean.
|
|
7
|
+
* - `database.url`: composed from `DB_*` env vars via `testDatabaseUrl()`.
|
|
8
|
+
*
|
|
9
|
+
* Both are deep-merged with `overrides`. Pass `logger` or `database` in
|
|
10
|
+
* `overrides` to replace the default for that key. Other keys
|
|
11
|
+
* (`rag`, `payment`, `social`, `encryption`, etc.) are taken verbatim
|
|
12
|
+
* from `overrides`.
|
|
13
|
+
*
|
|
14
|
+
* Throws when `DB_*` env vars are missing AND no `database.url` override
|
|
15
|
+
* is supplied — `bootTestApp` callers gate via `isPostgresAvailable()`
|
|
16
|
+
* first, so the throw should only fire in misconfigured environments.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { testDatabaseUrl } from './postgres/test_database_url.ts'
|
|
20
|
+
|
|
21
|
+
export type ConfigOverrides = Record<string, unknown>
|
|
22
|
+
|
|
23
|
+
const DEFAULT_LOGGER = {
|
|
24
|
+
default: 'main',
|
|
25
|
+
level: 'silent',
|
|
26
|
+
channels: { main: { driver: 'stderr' } },
|
|
27
|
+
} as const
|
|
28
|
+
|
|
29
|
+
export function composeTestConfig(overrides: ConfigOverrides = {}): Record<string, unknown> {
|
|
30
|
+
const merged: Record<string, unknown> = { ...overrides }
|
|
31
|
+
|
|
32
|
+
if (!('logger' in merged)) {
|
|
33
|
+
merged.logger = { ...DEFAULT_LOGGER }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!('database' in merged)) {
|
|
37
|
+
const url = testDatabaseUrl()
|
|
38
|
+
if (url === null) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'composeTestConfig: missing DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_DATABASE env. Source .env.test, run docker-compose up, or pass `database` explicitly in overrides.',
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
merged.database = { url }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return merged
|
|
47
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// Public API of @strav/testing.
|
|
2
|
+
//
|
|
3
|
+
// V1 ships the small utilities that get re-implemented inline in every
|
|
4
|
+
// test: an in-memory writable stream, a typed fetch stub, and the
|
|
5
|
+
// Postgres availability + reset helpers used by integration suites.
|
|
6
|
+
// V2 (this slice) adds bootTestApp + composeTestConfig +
|
|
7
|
+
// TenantManagerProvider for the e2e boot-dance.
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
export {
|
|
10
|
+
bootTestApp,
|
|
11
|
+
type BootTestAppOptions,
|
|
12
|
+
type BootTestAppResult,
|
|
13
|
+
type TestMigration,
|
|
14
|
+
} from './boot_test_app.ts'
|
|
15
|
+
export { composeTestConfig, type ConfigOverrides } from './compose_test_config.ts'
|
|
16
|
+
export { MemStream } from './mem_stream.ts'
|
|
17
|
+
export { stubFetch, type FetchHandler } from './stub_fetch.ts'
|
|
18
|
+
export { TenantManagerProvider } from './tenant_manager_provider.ts'
|
|
7
19
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from './
|
|
20
|
+
// Postgres helpers — also re-exported under `@strav/testing/postgres`
|
|
21
|
+
// for apps that want to import them without pulling in the rest of
|
|
22
|
+
// the barrel.
|
|
23
|
+
export {
|
|
24
|
+
connectedRoleBypassesRls,
|
|
25
|
+
createTestDatabase,
|
|
26
|
+
isPostgresAvailable,
|
|
27
|
+
resetSchema,
|
|
28
|
+
testDatabaseUrl,
|
|
29
|
+
} from './postgres/index.ts'
|
|
30
|
+
|
|
31
|
+
// Cache availability probes — also re-exported under
|
|
32
|
+
// `@strav/testing/cache` for the same reason.
|
|
33
|
+
export { isMemcachedAvailable, isRedisAvailable } from './cache/index.ts'
|