@owlmeans/test-integration 0.1.15 → 0.1.16

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 CHANGED
@@ -13,10 +13,9 @@ Specs do not call services through these helpers — they consume the per-packag
13
13
  <!-- owlmeans:agent-guidance:start -->
14
14
  ## Agent guidance
15
15
 
16
- This package ships embedded Claude Code skills and GitHub Copilot instructions under
17
- `agent-meta/`. After installing your `@owlmeans/*` packages, run the OwlMeans
18
- agent-skills installer to place them into your project's native locations
19
- (`.claude/skills/` and `.github/instructions/`):
16
+ This package ships embedded agent skills under `agent-meta/`. After installing your
17
+ `@owlmeans/*` packages, run the OwlMeans agent-skills installer to place them into
18
+ your project's skill store (`.agents/skills/`):
20
19
 
21
20
  ```sh
22
21
  npx @owlmeans/agent-skills
@@ -1,8 +1,8 @@
1
1
  {
2
- "schemaVersion": 1,
2
+ "schemaVersion": 2,
3
3
  "package": "@owlmeans/test-integration",
4
- "version": "0.1.15",
5
- "generatedAt": "2026-08-07T17:06:32.900Z",
4
+ "version": "0.1.16",
5
+ "generatedAt": "2026-08-14T10:14:48.851Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -10,14 +10,7 @@
10
10
  "name": "testing-integration",
11
11
  "category": "package-specific",
12
12
  "file": "skills/testing-integration/SKILL.md",
13
- "canonicalPath": ".claude/skills/testing-integration/SKILL.md"
14
- },
15
- {
16
- "kind": "instruction",
17
- "name": "testing-integration",
18
- "category": "package-specific",
19
- "file": "instructions/testing-integration.instructions.md",
20
- "canonicalPath": ".github/instructions/testing-integration.instructions.md"
13
+ "canonicalPath": ".agents/skills/testing-integration/SKILL.md"
21
14
  }
22
15
  ]
23
16
  }
@@ -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: `mongo`, `mongo-resource`, `redis`, `redis-resource`, `kluster`, `storage-common`, `storage-resource`, `image-resource`, `server-api`, `server-app`, `queue`, `llm` (live inference providers). These tests **never** mock the external service. They run only when the corresponding env vars are set, and they self-skip cleanly when those variables are missing.
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 cleanup queue for `afterAll`. |
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`, `@kubernetes/client-node`) stay where they already live — in the consuming integration packages — and are imported directly in those packages' `tests/`.
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 single `.env` at the monorepo root supplies real values for local runs. CI sets them via secrets. **Empty value = skip** — never a failure.
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
- ## `tests/context.ts` pattern
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
- import { mongoGate, randomNamespace, registerCleanup } from '@owlmeans/test-integration'
34
-
35
- let cached: { gate: ReturnType<typeof mongoGate>, dbName: string } | null = null
36
-
37
- export const getTestEnv = () => {
38
- if (cached != null) return cached
39
- const gate = mongoGate()
40
- const dbName = randomNamespace(process.env.MONGO_TEST_DB_PREFIX ?? 'omt')
41
- cached = { gate, dbName }
42
- if (!gate.skip) {
43
- registerCleanup(async () => {
44
- const { MongoClient } = await import('mongodb')
45
- const client = new MongoClient(gate.env.MONGO_URL!)
46
- try { await client.connect(); await client.db(dbName).dropDatabase() }
47
- finally { await client.close() }
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
- return cached
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 { runCleanups } from '@owlmeans/test-integration'
59
- import { getTestEnv } from './context.js'
60
-
61
- const env = getTestEnv()
138
+ import { gate, makeSuite } from './context.js'
62
139
 
63
- afterAll(async () => { await runCleanups() })
140
+ const suite = makeSuite('crud')
141
+ const it = gate.skip ? test.skip : test
64
142
 
65
143
  describe('@owlmeans/<pkg> — <feature>', () => {
66
- if (env.gate.skip) {
67
- test.skip(env.gate.reason ?? '<svc> gate closed', () => {})
144
+ if (gate.skip) {
145
+ test.skip(gate.reason ?? '<svc> gate closed', () => {})
146
+
68
147
  return
69
148
  }
70
- test('round-trips a record via the real <svc>', async () => {
71
- /* connect, exercise, assert */
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
- The Bun runtime auto-loads `<root>/.env` when the test command runs from a workspace package. If you need to override, `bun test --env-file=../../.env` works. Long-running specs may need a higher timeout — set it locally in a per-package `bunfig.toml`:
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.** Use `randomNamespace(prefix)` for DBs, key prefixes, and S3 object prefixes so parallel runs don't collide.
97
- - **Always clean up.** `registerCleanup(fn)` + `runCleanups()` in `afterAll`. A failing cleanup is logged, not raised — failure must show up in the test, not the teardown.
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 one is set — `s3Gate()` reports `skip: true`. This is intentional: a half-configured environment must not produce false positives or partial coverage.
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.15",
3
+ "version": "0.1.16",
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.15"
24
+ "@owlmeans/test": "^0.1.16"
25
25
  },
26
26
  "devDependencies": {
27
- "@owlmeans/dep-config": "^0.1.15",
27
+ "@owlmeans/dep-config": "^0.1.16",
28
28
  "@types/bun": "^1.3.14",
29
29
  "nodemon": "^3.1.14",
30
30
  "typescript": "^6.0.3"
@@ -1,18 +0,0 @@
1
- ---
2
- applyTo: "**/*.test.ts, **/*.spec.ts, **/*.ts"
3
- ---
4
- <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
5
-
6
- # Category C — Integration Tests (env-gated)
7
-
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
- - Always `randomNamespace(prefix)` databases / key prefixes / S3 prefixes for parallel safety.
12
- - Always `registerCleanup(fn)` for resources you provision; call `runCleanups()` in `afterAll`.
13
- - `<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"`. Long round-trips: bump timeout in a per-package `bunfig.toml`.
15
- - Cover `.claude/skills/<pkg>/SKILL.md` and `README.md` cases first.
16
- - Max 3-4 tests per method/function.
17
-
18
- See `.claude/skills/testing-integration/SKILL.md` for the full pattern.