@owlmeans/test-integration 0.1.15 → 0.1.16-rc.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.
|
@@ -6,12 +6,17 @@ applyTo: "**/*.test.ts, **/*.spec.ts, **/*.ts"
|
|
|
6
6
|
# Category C — Integration Tests (env-gated)
|
|
7
7
|
|
|
8
8
|
- Tests in `<your-package>/tests/*.spec.ts`. Use `bun:test`.
|
|
9
|
-
- No mocks. Connect to real MongoDB / Redis / S3 / Kubernetes via the env values supplied by `<root>/.env`.
|
|
10
|
-
- Open the gate at the top of `tests/context.ts` via `mongoGate()` / `redisGate()` / `s3Gate()` / `kubeGate()`. If `gate.skip`, do **not** register that service in the test context, and have specs self-skip with `gate.reason`.
|
|
11
|
-
-
|
|
12
|
-
-
|
|
9
|
+
- No mocks. Connect to real PostgreSQL / MongoDB / Redis / S3 / Kubernetes via the env values supplied by `<root>/.env`.
|
|
10
|
+
- Open the gate at the top of `tests/context.ts` via `postgresGate()` / `mongoGate()` / `redisGate()` / `s3Gate()` / `kubeGate()`. If `gate.skip`, do **not** register that service in the test context, and have specs self-skip with `gate.reason`.
|
|
11
|
+
- **Never print a credential.** Pipe it straight into the env var the gate reads.
|
|
12
|
+
- Unit specs (schema compilation, identifiers, error translation) go in the `*-resource` package; specs that build a real `ServerContext` go in the db package that depends on it — the reverse is a devDependency cycle.
|
|
13
|
+
- **One namespace per spec file.** Export a `makeSuite(label)` from `tests/context.ts` that owns a `randomNamespace(`${prefix}_${label}`)` schema/database and its own `teardown()`, called from that file's `afterAll`. The process-global `registerCleanup`/`runCleanups()` queue is only safe in a package with a single spec file: Bun runs every spec file of a package in one process, so the first `afterAll` would drop the namespaces of the files still to come.
|
|
14
|
+
- Capture connections in a `finally`, so a spec asserting a *failed* boot does not leak the pool it opened. Close them *after* the namespace drop.
|
|
15
|
+
- Capability probes go at module scope as a top-level `await` — Bun decides `test` vs `test.skip` synchronously — and default to "not capable" on any error.
|
|
16
|
+
- Never reset module-scope declarations (migration registries, table declarations) inside a shared `boot()` helper; a spec that wants a clean slate calls `resetDeclarations(alias)` itself. Keep migration bodies at module scope so their checksums are stable.
|
|
17
|
+
- `expect(fn).not.toThrow()` treats a **returned** `Error` as thrown — assert the returned value instead.
|
|
13
18
|
- `<root>/.env.example` is the authoritative documentation of every variable. Add a new key there with a comment when you introduce a new gate.
|
|
14
|
-
- Per-package `package.json` script: `"test": "bun test ./tests"
|
|
19
|
+
- Per-package `package.json` script: `"test": "bun test ./tests"`; add `"./tests/**/*"` to `tsconfig.json` `exclude` and `@owlmeans/test-integration` + `@types/bun` to devDependencies. Long round-trips: bump timeout in a per-package `bunfig.toml`.
|
|
15
20
|
- Cover `.claude/skills/<pkg>/SKILL.md` and `README.md` cases first.
|
|
16
21
|
- Max 3-4 tests per method/function.
|
|
17
22
|
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "@owlmeans/test-integration",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"generatedAt": "2026-08-
|
|
4
|
+
"version": "0.1.16-rc.0",
|
|
5
|
+
"generatedAt": "2026-08-11T14:02:34.978Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -1,74 +1,156 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: testing-integration
|
|
3
|
-
description: Category-C integration tests for OwlMeans Common packages that talk to external services (MongoDB, Redis, S3, Kubernetes). Env-gated, no mocks, conditional context provisioning. Auto-invoked when writing tests in mongo*, redis*, kluster, storage*, image-resource, server-api, server-app, queue.
|
|
3
|
+
description: Category-C integration tests for OwlMeans Common packages that talk to external services (PostgreSQL, MongoDB, Redis, S3, Kubernetes). Env-gated, no mocks, per-suite namespaces, conditional context provisioning. Auto-invoked when writing tests in postgres*, mongo*, redis*, kluster, storage*, image-resource, server-api, server-app, queue.
|
|
4
4
|
---
|
|
5
5
|
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
|
|
6
6
|
|
|
7
7
|
# Integration Tests — Category C
|
|
8
8
|
|
|
9
|
-
Category C applies to packages that integrate with external services: `
|
|
9
|
+
Category C applies to packages that integrate with external services: `postgres`,
|
|
10
|
+
`postgres-resource`, `mongo`, `mongo-resource`, `redis`, `redis-resource`, `kluster`,
|
|
11
|
+
`storage-common`, `storage-resource`, `image-resource`, `server-api`, `server-app`, `queue`,
|
|
12
|
+
`llm` (live inference providers). These tests **never** mock the external service. They run
|
|
13
|
+
only when the corresponding env vars are set, and they self-skip cleanly when those variables
|
|
14
|
+
are missing.
|
|
10
15
|
|
|
11
16
|
## Helpers from `@owlmeans/test-integration`
|
|
12
17
|
|
|
13
18
|
| Helper | Purpose |
|
|
14
19
|
|---|---|
|
|
15
|
-
| `mongoGate()`, `redisGate()`, `s3Gate()`, `kubeGate()` | Read env, return `{ skip, reason?, env }`. |
|
|
16
|
-
| `randomNamespace(prefix, len?)` | DB / key / object-prefix namespacing for parallel-safe runs. |
|
|
17
|
-
| `registerCleanup(fn)` + `runCleanups()` | LIFO
|
|
20
|
+
| `postgresGate()`, `mongoGate()`, `redisGate()`, `s3Gate()`, `kubeGate()` | Read env, return `{ skip, reason?, env }`. |
|
|
21
|
+
| `randomNamespace(prefix, len?)` | Schema / DB / key / object-prefix namespacing for parallel-safe runs. |
|
|
22
|
+
| `registerCleanup(fn)` + `runCleanups()` | Process-global LIFO queue. **Single-spec-file packages only** — see below. |
|
|
18
23
|
|
|
19
|
-
The driver libraries (`mongodb`, `ioredis`, `@aws-sdk/client-s3`,
|
|
24
|
+
The driver libraries (`pg`, `mongodb`, `ioredis`, `@aws-sdk/client-s3`,
|
|
25
|
+
`@kubernetes/client-node`) stay where they already live — in the consuming integration
|
|
26
|
+
packages — and are imported directly in those packages' `tests/`.
|
|
20
27
|
|
|
21
28
|
## Env contract
|
|
22
29
|
|
|
23
|
-
`/.env.example` documents every required variable, grouped by service, with comments. The
|
|
30
|
+
`/.env.example` documents every required variable, grouped by service, with comments. The
|
|
31
|
+
single `.env` at the monorepo root supplies real values for local runs. CI sets them via
|
|
32
|
+
secrets. **Empty value = skip** — never a failure.
|
|
24
33
|
|
|
25
34
|
When a required variable is empty:
|
|
26
35
|
1. The corresponding gate reports `skip: true`.
|
|
27
36
|
2. The per-package `tests/context.ts` does **not** register the dependent service in the real context.
|
|
28
37
|
3. Specs that touch that service self-skip with the reason printed.
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
**Never print a credential.** Pipe it straight from its source into the env var the gate
|
|
40
|
+
reads — the value must never reach a log, a report, or a committed file:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
POSTGRES_URL="$(<build it from the secret file, without echoing>)" bun test ./tests
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Where the specs live
|
|
47
|
+
|
|
48
|
+
A resource package (`*-resource`) owns the `Resource<T>` contract; the db package
|
|
49
|
+
(`@owlmeans/postgres`, `@owlmeans/mongo`) implements the service that backs it and therefore
|
|
50
|
+
**depends on** the resource package.
|
|
51
|
+
|
|
52
|
+
- **Unit specs** — schema compilation, identifier derivation, placeholder resolution, error
|
|
53
|
+
translation — live in the **resource** package. No gate, no service.
|
|
54
|
+
- **Integration specs** that build a real `ServerContext` live in the **db** package. Putting
|
|
55
|
+
them in the resource package would need a devDependency on its own dependent, which is a
|
|
56
|
+
cycle and risks breaking `publish.ts`'s topological batching.
|
|
57
|
+
|
|
58
|
+
## Per-suite namespaces, not the global cleanup queue
|
|
59
|
+
|
|
60
|
+
Bun runs **every spec file of a package in one process** with one shared module registry. A
|
|
61
|
+
process-global `registerCleanup`/`runCleanups()` therefore misfires: the first file to reach
|
|
62
|
+
`afterAll` drops the namespaces of the files still to come. Use `registerCleanup` only in a
|
|
63
|
+
package with a single spec file.
|
|
64
|
+
|
|
65
|
+
Give each spec file its own namespace and its own teardown instead:
|
|
31
66
|
|
|
32
67
|
```ts
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
// tests/context.ts
|
|
69
|
+
export const gate: IntegrationGate<PostgresEnv> = postgresGate()
|
|
70
|
+
|
|
71
|
+
export const makeSuite = (label: string): PgSuite => {
|
|
72
|
+
const prefix = process.env.POSTGRES_TEST_DB_PREFIX ?? 'omt'
|
|
73
|
+
const schema = randomNamespace(`${prefix}_${label}`)
|
|
74
|
+
const pools: Pool[] = []
|
|
75
|
+
|
|
76
|
+
const boot = async (opts: BootOptions = {}): Promise<Booted> => {
|
|
77
|
+
const cfg: ServerConfig = config('pg-test', { dbs: [{ service: 'postgres', schema, /* … */ }] })
|
|
78
|
+
const context = makeServerContext(cfg) as ServerContext<ServerConfig>
|
|
79
|
+
appendPostgres(context)
|
|
80
|
+
for (const resource of opts.resources ?? []) context.registerResource(resource as never)
|
|
81
|
+
context.configure()
|
|
82
|
+
const pg = context.service<PostgresService>('postgres')
|
|
83
|
+
try {
|
|
84
|
+
await context.init()
|
|
85
|
+
} finally {
|
|
86
|
+
collect(pg) // in `finally`: a spec asserting a *failed* boot still opened a pool
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { context, pg }
|
|
49
90
|
}
|
|
50
|
-
|
|
91
|
+
|
|
92
|
+
const teardown = async (): Promise<void> => {
|
|
93
|
+
if (gate.skip) return
|
|
94
|
+
await raw(async pool => { await pool.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`) })
|
|
95
|
+
.catch(error => console.error(`postgres test teardown (${schema}):`, error))
|
|
96
|
+
/** After the drop — a pool still holding the schema open makes CASCADE wait. */
|
|
97
|
+
while (pools.length > 0) await pools.pop()?.end().catch(() => undefined)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { schema, boot, collect, teardown }
|
|
51
101
|
}
|
|
52
102
|
```
|
|
53
103
|
|
|
104
|
+
`makeSuite('sync')`, `makeSuite('migration')` … one call per spec file, each with
|
|
105
|
+
`afterAll(async () => { await suite.teardown() })`.
|
|
106
|
+
|
|
107
|
+
## Capability probes must be top-level `await`
|
|
108
|
+
|
|
109
|
+
Bun decides `test` vs `test.skip` **synchronously**, so a suite cannot await its own probe. A
|
|
110
|
+
probe that needs a round trip goes at module scope in `tests/context.ts`, guarded by the gate
|
|
111
|
+
and defaulting to "not capable" on any error:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
export const capabilities: PgCapabilities = gate.skip
|
|
115
|
+
? { bootstrap: false }
|
|
116
|
+
: await raw(async pool => { /* SELECT rolsuper, rolcreaterole, rolcreatedb … */ })
|
|
117
|
+
.catch(() => ({ bootstrap: false }))
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Specs then branch on it once, at the top: `const it = gate.skip || !capabilities.bootstrap ? test.skip : test`.
|
|
121
|
+
|
|
122
|
+
## Module-scope declarations survive a context switch
|
|
123
|
+
|
|
124
|
+
Migration registries and table declarations are keyed by resource **alias** at module scope so
|
|
125
|
+
they outlive `reinitializeContext`. That means they also outlive a `boot()` — deliberately.
|
|
126
|
+
Never reset them inside a shared `boot()` helper: a `.migration()` registered before the boot
|
|
127
|
+
would be silently dropped. A spec that wants a clean slate calls `resetDeclarations(alias)`
|
|
128
|
+
itself, which is what a spec simulating a restarted process should have to say out loud.
|
|
129
|
+
|
|
130
|
+
Migration bodies belong at **module scope**, not inside the spec callback: the checksum
|
|
131
|
+
fingerprints the function's source text, and a body closed over a loop variable fingerprints
|
|
132
|
+
the wrapper instead.
|
|
133
|
+
|
|
54
134
|
## Spec shape
|
|
55
135
|
|
|
56
136
|
```ts
|
|
57
137
|
import { afterAll, describe, expect, test } from 'bun:test'
|
|
58
|
-
import {
|
|
59
|
-
import { getTestEnv } from './context.js'
|
|
60
|
-
|
|
61
|
-
const env = getTestEnv()
|
|
138
|
+
import { gate, makeSuite } from './context.js'
|
|
62
139
|
|
|
63
|
-
|
|
140
|
+
const suite = makeSuite('crud')
|
|
141
|
+
const it = gate.skip ? test.skip : test
|
|
64
142
|
|
|
65
143
|
describe('@owlmeans/<pkg> — <feature>', () => {
|
|
66
|
-
if (
|
|
67
|
-
test.skip(
|
|
144
|
+
if (gate.skip) {
|
|
145
|
+
test.skip(gate.reason ?? '<svc> gate closed', () => {})
|
|
146
|
+
|
|
68
147
|
return
|
|
69
148
|
}
|
|
70
|
-
|
|
71
|
-
|
|
149
|
+
|
|
150
|
+
afterAll(async () => { await suite.teardown() })
|
|
151
|
+
|
|
152
|
+
it('round-trips a record through a real context', async () => {
|
|
153
|
+
/* boot, exercise, assert */
|
|
72
154
|
})
|
|
73
155
|
})
|
|
74
156
|
```
|
|
@@ -83,22 +165,43 @@ Per-package `package.json`:
|
|
|
83
165
|
}
|
|
84
166
|
```
|
|
85
167
|
|
|
86
|
-
|
|
168
|
+
Add `"./tests/**/*"` to the package's `tsconfig.json` `exclude` so `tsc -b` never compiles
|
|
169
|
+
specs into `build/`, and declare `@owlmeans/test-integration` plus `@types/bun` as
|
|
170
|
+
devDependencies.
|
|
171
|
+
|
|
172
|
+
The Bun runtime auto-loads `<root>/.env` when the test command runs from a workspace package.
|
|
173
|
+
If you need to override, `bun test --env-file=../../.env` works. Long-running specs may need a
|
|
174
|
+
higher timeout — set it locally in a per-package `bunfig.toml`:
|
|
87
175
|
|
|
88
176
|
```toml
|
|
89
177
|
[test]
|
|
90
178
|
timeout = 60000
|
|
91
179
|
```
|
|
92
180
|
|
|
181
|
+
## Bun assertion gotcha
|
|
182
|
+
|
|
183
|
+
`expect(fn).not.toThrow()` reports a **returned** `Error` as a thrown one. Every error
|
|
184
|
+
*translator* returns an `Error`, so this is the wrong assertion for them — assert the returned
|
|
185
|
+
value instead, which also proves the call returned:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
expect(pgErrorToResourceError(circular)).toBe(circular) // not: expect(() => …).not.toThrow()
|
|
189
|
+
```
|
|
190
|
+
|
|
93
191
|
## Rules
|
|
94
192
|
|
|
95
193
|
- **No mocks.** The whole point of category C is to exercise the real driver against a real service.
|
|
96
|
-
- **Always namespace.**
|
|
97
|
-
|
|
194
|
+
- **Always namespace, per suite.** `randomNamespace(`${prefix}_${label}`)` for schemas, DBs, key
|
|
195
|
+
prefixes, and S3 object prefixes, so neither parallel runs nor sibling spec files collide.
|
|
196
|
+
- **Always clean up, in the suite that provisioned it.** A failing cleanup is logged, not
|
|
197
|
+
raised — failure must show up in the test, not the teardown. Verify afterwards that the
|
|
198
|
+
service holds no leftovers matching the prefix.
|
|
98
199
|
- **Cover SKILL.md cases first.**
|
|
99
200
|
- **Max 3-4 tests per method/function.**
|
|
100
201
|
- **CI runs C only when secrets are set.** Locally, an empty `.env` produces a clean skip.
|
|
101
202
|
|
|
102
203
|
## When the env is partially populated
|
|
103
204
|
|
|
104
|
-
Each gate fails closed. A test that needs both `S3_KEY` and `S3_SECRET` does not run if only
|
|
205
|
+
Each gate fails closed. A test that needs both `S3_KEY` and `S3_SECRET` does not run if only
|
|
206
|
+
one is set — `s3Gate()` reports `skip: true`. This is intentional: a half-configured
|
|
207
|
+
environment must not produce false positives or partial coverage.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/test-integration",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16-rc.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@owlmeans/test": "^0.1.
|
|
24
|
+
"@owlmeans/test": "^0.1.16-rc.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@owlmeans/dep-config": "^0.1.
|
|
27
|
+
"@owlmeans/dep-config": "^0.1.16-rc.0",
|
|
28
28
|
"@types/bun": "^1.3.14",
|
|
29
29
|
"nodemon": "^3.1.14",
|
|
30
30
|
"typescript": "^6.0.3"
|