@owlmeans/test-integration 0.1.18-rc.32 → 0.1.18-rc.33

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.
Files changed (41) hide show
  1. package/README.md +3 -3
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/testing-integration/SKILL.md +23 -23
  4. package/build/cleanup/types.d.ts +20 -0
  5. package/build/cleanup/types.d.ts.map +1 -0
  6. package/build/cleanup/types.js +2 -0
  7. package/build/cleanup/types.js.map +1 -0
  8. package/build/cleanup.d.ts +5 -12
  9. package/build/cleanup.d.ts.map +1 -1
  10. package/build/cleanup.js +23 -25
  11. package/build/cleanup.js.map +1 -1
  12. package/build/gates/types.d.ts +18 -0
  13. package/build/gates/types.d.ts.map +1 -0
  14. package/build/gates/types.js +2 -0
  15. package/build/gates/types.js.map +1 -0
  16. package/build/gates.d.ts +8 -3
  17. package/build/gates.d.ts.map +1 -1
  18. package/build/gates.js +39 -27
  19. package/build/gates.js.map +1 -1
  20. package/build/index.d.ts +2 -0
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js.map +1 -1
  23. package/build/reachability/types.d.ts +27 -0
  24. package/build/reachability/types.d.ts.map +1 -0
  25. package/build/reachability/types.js +2 -0
  26. package/build/reachability/types.js.map +1 -0
  27. package/build/reachability.d.ts +3 -20
  28. package/build/reachability.d.ts.map +1 -1
  29. package/build/reachability.js +59 -70
  30. package/build/reachability.js.map +1 -1
  31. package/build/types.local.d.ts +0 -1
  32. package/build/types.local.d.ts.map +1 -1
  33. package/package.json +3 -3
  34. package/src/cleanup/types.ts +18 -0
  35. package/src/cleanup.ts +25 -23
  36. package/src/gates/types.ts +18 -0
  37. package/src/gates.ts +58 -39
  38. package/src/index.ts +2 -0
  39. package/src/reachability/types.ts +27 -0
  40. package/src/reachability.ts +64 -72
  41. package/src/types.local.ts +0 -2
package/README.md CHANGED
@@ -4,10 +4,10 @@ Env-gated harness for integration tests of packages that talk to external servic
4
4
 
5
5
  Helpers exported here:
6
6
 
7
- - `mongoGate()`, `redisGate()`, `postgresGate()`, `s3Gate()`, `kubeGate()`, `smtpGate()` — read the env keys documented in `.env.example` and return `{ skip, reason?, env }`. A `tests/context.ts` uses these to decide whether to register the corresponding service in the real test context.
7
+ - `gateHelper.mongoGate()`, `.redisGate()`, `.postgresGate()`, `.s3Gate()`, `.kubeGate()`, `.smtpGate()` — read the env keys documented in `.env.example` and return `{ skip, reason?, env }`. A `tests/context.ts` uses these to decide whether to register the corresponding service in the real test context.
8
8
  - The three datastore gates (`mongoGate`, `redisGate`, `postgresGate`) also require a host in the connection string to accept a TCP connect. A set variable pointing at nothing — a port-forward that is not running — closes the gate with a printed reason naming the variable and `host:port`, instead of failing the suite on a refused connection. The probe is synchronous, so gates keep being read at module scope.
9
9
  - `randomNamespace(prefix)` — short random suffix to keep DB names, key prefixes, and S3 prefixes unique per test run so suites can safely run in parallel.
10
- - `registerCleanup(fn)` + `runCleanups()` — opt-in `afterAll` cleanup queue.
10
+ - `cleanupHelper.registerCleanup(fn)` + `cleanupHelper.runCleanups()` — opt-in `afterAll` cleanup queue.
11
11
 
12
12
  Specs do not call services through these helpers — they consume the per-package real test context built in `tests/context.ts` and only check `gates.<svc>.skip` to self-skip when the dependency is missing. See `testing-integration` skill for the full pattern.
13
13
 
@@ -19,7 +19,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
19
19
  your project's skill store (`.agents/skills/`):
20
20
 
21
21
  ```sh
22
- npx @owlmeans/agent-skills@^0.1.18-rc.48
22
+ npx @owlmeans/agent-skills@^0.1.18-rc.49
23
23
  ```
24
24
 
25
25
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/test-integration",
4
- "version": "0.1.18-rc.32",
5
- "generatedAt": "2026-10-05T08:32:19.106Z",
4
+ "version": "0.1.18-rc.33",
5
+ "generatedAt": "2026-10-05T21:03:06.387Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -6,7 +6,7 @@ description: Category-C integration tests for OwlMeans Common packages that talk
6
6
 
7
7
  # Integration Tests — Category C
8
8
 
9
- **Install:** `"@owlmeans/test-integration": "^0.1.18-rc.32"` in `devDependencies`
9
+ **Install:** `"@owlmeans/test-integration": "^0.1.18-rc.33"` in `devDependencies`
10
10
 
11
11
  Category C applies to packages that integrate with external services: `postgres`,
12
12
  `postgres-resource`, `mongo`, `mongo-resource`, `redis`, `redis-resource`, `redis-queue`,
@@ -25,29 +25,29 @@ service hands it is category C even when it names no driver of its own.
25
25
 
26
26
  | Helper | Purpose |
27
27
  |---|---|
28
- | `postgresGate()`, `mongoGate()`, `redisGate()`, `s3Gate()`, `kubeGate()`, `smtpGate()` | Read env (and, for the three datastores, probe the address — below), return `IntegrationGate<E>` = `{ skip, reason?, env }`. |
28
+ | `gateHelper` — `.postgresGate()`, `.mongoGate()`, `.redisGate()`, `.s3Gate()`, `.kubeGate()`, `.smtpGate()` | Read env (and, for the three datastores, probe the address — below), return `IntegrationGate<E>` = `{ skip, reason?, env }`. |
29
29
  | `IntegrationGate<E>` | `{ skip: boolean, reason?: string, env: Partial<E> }`. `env` holds only the variables that were actually populated. `skip` is a plain `boolean`, not a discriminant, so `env` stays `Partial<E>` on the open branch too — a suite reads a required variable through a cast: `gate.env.MONGO_URL as string`. |
30
30
  | `PostgresEnv`, `MongoEnv`, `RedisEnv`, `S3Env`, `KubeEnv`, `SmtpEnv` | The variable set each gate reads, so `tests/context.ts` types its exported gate. |
31
31
  | `randomNamespace(prefix, len?)` | Schema / DB / key / object-prefix namespacing for parallel-safe runs. Default 6 hex chars. |
32
- | `registerCleanup(fn)` + `runCleanups()` | Process-global LIFO queue. Only where **one** spec file of the package provisions anything — see below. |
32
+ | `cleanupHelper.registerCleanup(fn)` + `.runCleanups()` | Process-global LIFO queue. Only where **one** spec file of the package provisions anything — see below. |
33
33
 
34
34
  Each gate fails closed on its **required** variables and treats the rest as optional:
35
35
 
36
36
  | Gate | Required | Optional |
37
37
  |---|---|---|
38
- | `mongoGate()` | `MONGO_URL` | `MONGO_TEST_DB_PREFIX` |
39
- | `postgresGate()` | `POSTGRES_URL` | `POSTGRES_TEST_DB_PREFIX` |
40
- | `redisGate()` | `REDIS_URL` | `REDIS_TEST_KEY_PREFIX` |
41
- | `s3Gate()` | `S3_ENDPOINT`, `S3_KEY`, `S3_SECRET`, `S3_TEST_BUCKET` | `S3_REGION` |
42
- | `kubeGate()` | `KUBE_CONFIG`, `KUBE_TEST_OK` | — |
43
- | `smtpGate()` | `SMTP_HOST`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_FROM`, `SMTP_TEST_TO` | `SMTP_PORT`, `SMTP_SECURE` |
38
+ | `gateHelper.mongoGate()` | `MONGO_URL` | `MONGO_TEST_DB_PREFIX` |
39
+ | `gateHelper.postgresGate()` | `POSTGRES_URL` | `POSTGRES_TEST_DB_PREFIX` |
40
+ | `gateHelper.redisGate()` | `REDIS_URL` | `REDIS_TEST_KEY_PREFIX` |
41
+ | `gateHelper.s3Gate()` | `S3_ENDPOINT`, `S3_KEY`, `S3_SECRET`, `S3_TEST_BUCKET` | `S3_REGION` |
42
+ | `gateHelper.kubeGate()` | `KUBE_CONFIG`, `KUBE_TEST_OK` | — |
43
+ | `gateHelper.smtpGate()` | `SMTP_HOST`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_FROM`, `SMTP_TEST_TO` | `SMTP_PORT`, `SMTP_SECURE` |
44
44
 
45
45
  `SMTP_TEST_TO` is required rather than optional because an open SMTP gate sends **real mail** — the
46
46
  suite must never guess a recipient.
47
47
 
48
48
  ### A datastore gate also asks whether the server answers
49
49
 
50
- `mongoGate()`, `redisGate()` and `postgresGate()` open only when their connection string is set
50
+ `gateHelper.mongoGate()`, `.redisGate()` and `.postgresGate()` open only when their connection string is set
51
51
  **and** one of the hosts in it accepts a TCP connect (default ports 27017 / 6379 / 5432). A local
52
52
  `.env` keeps the port-forward address long after the forward is gone, and a gate that trusted the
53
53
  variable alone left `beforeAll` to spend its whole budget on `ECONNREFUSED` and fail the suite. An
@@ -64,12 +64,12 @@ Bun decides `test` vs `test.skip` synchronously and every consumer reads its gat
64
64
  so no `tests/context.ts` changes to get it. It is TCP only: no driver and no login, so a wrong
65
65
  password still opens the gate and fails in the suite, where it belongs. It answers "reachable" when
66
66
  it cannot tell — an SRV (`mongodb+srv://`) or unix-socket address, a string it cannot parse, a probe
67
- that cannot spawn — which is exactly the behaviour a gate had before it probed. `s3Gate()`,
68
- `kubeGate()` and `smtpGate()` do not probe: they name remote services, not a local forward.
67
+ that cannot spawn — which is exactly the behaviour a gate had before it probed. `gateHelper.s3Gate()`,
68
+ `.kubeGate()` and `.smtpGate()` do not probe: they name remote services, not a local forward.
69
69
 
70
70
  An optional variable is only in `env` when it was actually populated, which is why a suite reads a
71
71
  defaulted one — `POSTGRES_TEST_DB_PREFIX ?? 'omt'` — straight from `process.env` instead: calling
72
- any gate has already run `loadEnv`, so the `.env` values are in `process.env` by then.
72
+ any gate has already run `envHelper.loadEnv`, so the `.env` values are in `process.env` by then.
73
73
 
74
74
  **Six services, six gates — a category-C package outside that set writes its own.** There is no
75
75
  gate for an LLM provider or for the Mailgun HTTP API, and none should be added to
@@ -90,7 +90,7 @@ asserts on the request it builds and the API's reply.
90
90
  ## Env contract
91
91
 
92
92
  One `.env.example` at the workspace root documents every variable, grouped by service, with
93
- comments; the `.env` beside it supplies real values for local runs and `loadEnv` (from
93
+ comments; the `.env` beside it supplies real values for local runs and `envHelper.loadEnv` (from
94
94
  `@owlmeans/test`) picks it up. CI sets the same variables from secrets. **Empty value = skip, and
95
95
  so is a datastore address nothing answers at** — never a failure. A developer's `.env` may keep the
96
96
  port-forward address permanently: with the forward up the suites run, without it they skip.
@@ -143,7 +143,7 @@ A resource package (`*-resource`) owns the `Resource<T>` contract; the db packag
143
143
  translation — live in the **resource** package and gate on nothing: they leave the process
144
144
  alone. `@owlmeans/postgres-resource` is entirely this shape.
145
145
  - **Raw-driver specs** belong there too when the subject is the round trip and not the service.
146
- `@owlmeans/mongo-resource` opens `mongoGate()`, derives a namespaced database with
146
+ `@owlmeans/mongo-resource` opens `gateHelper.mongoGate()`, derives a namespaced database with
147
147
  `randomNamespace`, registers a cleanup that drops it, and drives `mongodb` directly with the
148
148
  driver as its own devDependency — legitimate precisely because it never constructs the db
149
149
  service. It is also the one package that may use the global cleanup queue, because only that
@@ -156,10 +156,10 @@ A resource package (`*-resource`) owns the `Resource<T>` contract; the db packag
156
156
  ## Per-suite namespaces, not the global cleanup queue
157
157
 
158
158
  Bun runs **every spec file of a package in one process** with one shared module registry, and the
159
- queue is one array for that whole process. A process-global `registerCleanup`/`runCleanups()`
159
+ queue is one array for that whole process. A process-global `cleanupHelper.registerCleanup`/`.runCleanups()`
160
160
  therefore misfires as soon as two spec files provision: the first to reach `afterAll` drains the
161
161
  entire queue and drops the namespaces of the files still to come. The queue is safe only where a
162
- **single** spec file provisions anything and that same file calls `runCleanups()`. That is the
162
+ **single** spec file provisions anything and that same file calls `cleanupHelper.runCleanups()`. That is the
163
163
  shape `@owlmeans/mongo-resource` has — one gated round-trip spec registering one dropped database,
164
164
  beside two ungated spec files that translate criteria and references in memory and open nothing.
165
165
 
@@ -168,7 +168,7 @@ own teardown instead:
168
168
 
169
169
  ```ts
170
170
  // tests/context.ts
171
- export const gate: IntegrationGate<PostgresEnv> = postgresGate()
171
+ export const gate: IntegrationGate<PostgresEnv> = gateHelper.postgresGate()
172
172
 
173
173
  export const makeSuite = (label: string): PgSuite => {
174
174
  const prefix = process.env.POSTGRES_TEST_DB_PREFIX ?? 'omt'
@@ -228,7 +228,7 @@ maker that runs more than once for the same alias re-declares the same entries a
228
228
  A `boot()` builds a whole new context, but the declarations belong to the module registry rather
229
229
  than to any context, so they carry across every boot in the process — deliberately.
230
230
  Never reset them inside a shared `boot()` helper: a `.migration()` registered before the boot
231
- would be silently dropped. A spec that wants a clean slate calls `resetDeclarations(alias)`
231
+ would be silently dropped. A spec that wants a clean slate calls `pgDeclarationHelper.resetDeclarations(alias)` (or `mongoDeclarationHelper`'s)
232
232
  itself, which is what a spec simulating a restarted process should have to say out loud.
233
233
 
234
234
  Migration bodies belong at **module scope**, not inside the spec callback: the checksum
@@ -275,11 +275,11 @@ devDependencies.
275
275
 
276
276
  **Nothing auto-loads the workspace `.env`.** Bun reads a `.env` beside the working directory, so
277
277
  `bun test` inside a package never sees the one at the workspace root. The gates work because
278
- `@owlmeans/test`'s `loadEnv` does the loading itself and every gate calls it before reading a
278
+ `@owlmeans/test`'s `envHelper.loadEnv` does the loading itself and every gate calls it before reading a
279
279
  variable: it walks up from the working directory for a `bun.lock`, or for a `package.json`
280
280
  sitting beside a directory named `packages`, and reads the `.env` it finds there. A variable
281
281
  already set to a non-empty value always wins, so `MONGO_URL=… bun test ./tests` still overrides
282
- the file. A workspace shaped any other way names the file itself — `loadEnv({ file })` at the top
282
+ the file. A workspace shaped any other way names the file itself — `envHelper.loadEnv({ file })` at the top
283
283
  of `tests/context.ts`, before the gates run.
284
284
 
285
285
  Bun's per-test timeout is 5s, and **`bunfig.toml`'s `[test] timeout` does not raise it** — bun
@@ -301,7 +301,7 @@ bun test --timeout=60000 ./tests
301
301
  value instead, which also proves the call returned:
302
302
 
303
303
  ```ts
304
- expect(pgErrorToResourceError(circular)).toBe(circular) // not: expect(() => …).not.toThrow()
304
+ expect(pgErrorHelper.pgErrorToResourceError(circular)).toBe(circular) // not: expect(() => …).not.toThrow()
305
305
  ```
306
306
 
307
307
  ## Rules
@@ -320,5 +320,5 @@ expect(pgErrorToResourceError(circular)).toBe(circular) // not: expect(() =>
320
320
  ## When the env is partially populated
321
321
 
322
322
  Each gate fails closed. A test that needs both `S3_KEY` and `S3_SECRET` does not run if only
323
- one is set — `s3Gate()` reports `skip: true`. This is intentional: a half-configured
323
+ one is set — `gateHelper.s3Gate()` reports `skip: true`. This is intentional: a half-configured
324
324
  environment must not produce false positives or partial coverage.
@@ -0,0 +1,20 @@
1
+ export interface CleanupFn {
2
+ (): void | Promise<void>;
3
+ }
4
+ /** The opt-in teardown queue a suite's `tests/context.ts` fills and its `afterAll` drains. */
5
+ export interface CleanupHelper {
6
+ /**
7
+ * Register a cleanup function that {@link CleanupHelper.runCleanups} will execute (LIFO).
8
+ * Use from `tests/context.ts` `setup()` to schedule teardown of resources
9
+ * that were provisioned for the suite (DB drop, key namespace flush,
10
+ * uploaded objects).
11
+ */
12
+ registerCleanup: (fn: CleanupFn) => void;
13
+ /**
14
+ * Run all pending cleanup functions in reverse registration order.
15
+ * Errors are swallowed and logged as warnings so a failing cleanup
16
+ * cannot mask a test failure.
17
+ */
18
+ runCleanups: () => Promise<void>;
19
+ }
20
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/cleanup/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IAAG,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE;AAEvD,8FAA8F;AAC9F,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,eAAe,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,IAAI,CAAA;IACxC;;;;OAIG;IACH,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACjC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/cleanup/types.ts"],"names":[],"mappings":""}
@@ -1,15 +1,8 @@
1
- import type { CleanupFn } from './types.local.js';
2
- /**
3
- * Register a cleanup function that `runCleanups()` will execute (LIFO).
4
- * Use from `tests/context.ts` `setup()` to schedule teardown of resources
5
- * that were provisioned for the suite (DB drop, key namespace flush,
6
- * uploaded objects).
7
- */
1
+ import type { CleanupFn, CleanupHelper } from './cleanup/types.js';
2
+ export declare const createCleanupHelper: () => CleanupHelper;
3
+ export declare const cleanupHelper: CleanupHelper;
4
+ /** @deprecated compat:factory-refactor — use `cleanupHelper.registerCleanup(…)` */
8
5
  export declare const registerCleanup: (fn: CleanupFn) => void;
9
- /**
10
- * Run all pending cleanup functions in reverse registration order.
11
- * Errors are swallowed and logged as warnings so a failing cleanup
12
- * cannot mask a test failure.
13
- */
6
+ /** @deprecated compat:factory-refactor — use `cleanupHelper.runCleanups()` */
14
7
  export declare const runCleanups: () => Promise<void>;
15
8
  //# sourceMappingURL=cleanup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../src/cleanup.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAMjD;;;;;GAKG;AACH,eAAO,MAAM,eAAe,OAAQ,SAAS,KAAG,IAE/C,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,WAAW,QAAa,OAAO,CAAC,IAAI,CAUhD,CAAA"}
1
+ {"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../src/cleanup.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAOlE,eAAO,MAAM,mBAAmB,QAAO,aAkBtC,CAAA;AAED,eAAO,MAAM,aAAa,eAAwB,CAAA;AAElD,mFAAmF;AACnF,eAAO,MAAM,eAAe,OAAQ,SAAS,KAAG,IAAyC,CAAA;AAEzF,8EAA8E;AAC9E,eAAO,MAAM,WAAW,QAAa,OAAO,CAAC,IAAI,CAAsC,CAAA"}
package/build/cleanup.js CHANGED
@@ -1,31 +1,29 @@
1
1
  import { logger } from '@owlmeans/log';
2
2
  const log = logger('test-integration');
3
+ // Process-wide on purpose: every spec file of a run registers into, and drains, the same queue.
3
4
  const queue = [];
4
- /**
5
- * Register a cleanup function that `runCleanups()` will execute (LIFO).
6
- * Use from `tests/context.ts` `setup()` to schedule teardown of resources
7
- * that were provisioned for the suite (DB drop, key namespace flush,
8
- * uploaded objects).
9
- */
10
- export const registerCleanup = (fn) => {
11
- queue.push(fn);
12
- };
13
- /**
14
- * Run all pending cleanup functions in reverse registration order.
15
- * Errors are swallowed and logged as warnings so a failing cleanup
16
- * cannot mask a test failure.
17
- */
18
- export const runCleanups = async () => {
19
- while (queue.length > 0) {
20
- const fn = queue.pop();
21
- if (fn == null)
22
- continue;
23
- try {
24
- await fn();
25
- }
26
- catch (err) {
27
- log.warn('Cleanup failed', err);
5
+ export const createCleanupHelper = () => {
6
+ const registerCleanup = (fn) => {
7
+ queue.push(fn);
8
+ };
9
+ const runCleanups = async () => {
10
+ while (queue.length > 0) {
11
+ const fn = queue.pop();
12
+ if (fn == null)
13
+ continue;
14
+ try {
15
+ await fn();
16
+ }
17
+ catch (err) {
18
+ log.warn('Cleanup failed', err);
19
+ }
28
20
  }
29
- }
21
+ };
22
+ return { registerCleanup, runCleanups };
30
23
  };
24
+ export const cleanupHelper = createCleanupHelper();
25
+ /** @deprecated compat:factory-refactor — use `cleanupHelper.registerCleanup(…)` */
26
+ export const registerCleanup = (fn) => cleanupHelper.registerCleanup(fn);
27
+ /** @deprecated compat:factory-refactor — use `cleanupHelper.runCleanups()` */
28
+ export const runCleanups = async () => await cleanupHelper.runCleanups();
31
29
  //# sourceMappingURL=cleanup.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../src/cleanup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAGtC,MAAM,GAAG,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAA;AAEtC,MAAM,KAAK,GAAgB,EAAE,CAAA;AAE7B;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,EAAa,EAAQ,EAAE;IACrD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AAChB,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,IAAmB,EAAE;IACnD,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,EAAE,CAAA;QACtB,IAAI,EAAE,IAAI,IAAI;YAAE,SAAQ;QACxB,IAAI,CAAC;YACH,MAAM,EAAE,EAAE,CAAA;QACZ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAA;QACjC,CAAC;IACH,CAAC;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../src/cleanup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAA;AAGtC,MAAM,GAAG,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAA;AAEtC,gGAAgG;AAChG,MAAM,KAAK,GAAgB,EAAE,CAAA;AAE7B,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAkB,EAAE;IACrD,MAAM,eAAe,GAAG,CAAC,EAAa,EAAQ,EAAE;QAC9C,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAChB,CAAC,CAAA;IAED,MAAM,WAAW,GAAG,KAAK,IAAmB,EAAE;QAC5C,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,EAAE,CAAA;YACtB,IAAI,EAAE,IAAI,IAAI;gBAAE,SAAQ;YACxB,IAAI,CAAC;gBACH,MAAM,EAAE,EAAE,CAAA;YACZ,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAA;YACjC,CAAC;QACH,CAAC;IACH,CAAC,CAAA;IAED,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,CAAA;AACzC,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,aAAa,GAAG,mBAAmB,EAAE,CAAA;AAElD,mFAAmF;AACnF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,EAAa,EAAQ,EAAE,CAAC,aAAa,CAAC,eAAe,CAAC,EAAE,CAAC,CAAA;AAEzF,8EAA8E;AAC9E,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,IAAmB,EAAE,CAAC,MAAM,aAAa,CAAC,WAAW,EAAE,CAAA"}
@@ -0,0 +1,18 @@
1
+ import type { IntegrationGate, KubeEnv, MongoEnv, PostgresEnv, RedisEnv, S3Env, SmtpEnv } from '../types.js';
2
+ /**
3
+ * The env gates of the integration suites: each reads the variables documented in `.env.example`
4
+ * and answers `{ skip, reason?, env }`, so a `tests/context.ts` decides whether to register the
5
+ * real service and a spec self-skips when it is missing.
6
+ */
7
+ export interface GateHelper {
8
+ /** `MONGO_URL` set AND a host in it accepting a TCP connect; `MONGO_TEST_DB_PREFIX` optional. */
9
+ mongoGate: () => IntegrationGate<MongoEnv>;
10
+ /** `REDIS_URL` set AND a host in it accepting a TCP connect; `REDIS_TEST_KEY_PREFIX` optional. */
11
+ redisGate: () => IntegrationGate<RedisEnv>;
12
+ s3Gate: () => IntegrationGate<S3Env>;
13
+ kubeGate: () => IntegrationGate<KubeEnv>;
14
+ /** `POSTGRES_URL` set AND a host in it accepting a TCP connect; `POSTGRES_TEST_DB_PREFIX` optional. */
15
+ postgresGate: () => IntegrationGate<PostgresEnv>;
16
+ smtpGate: () => IntegrationGate<SmtpEnv>;
17
+ }
18
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/gates/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAE5G;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,iGAAiG;IACjG,SAAS,EAAE,MAAM,eAAe,CAAC,QAAQ,CAAC,CAAA;IAC1C,kGAAkG;IAClG,SAAS,EAAE,MAAM,eAAe,CAAC,QAAQ,CAAC,CAAA;IAC1C,MAAM,EAAE,MAAM,eAAe,CAAC,KAAK,CAAC,CAAA;IACpC,QAAQ,EAAE,MAAM,eAAe,CAAC,OAAO,CAAC,CAAA;IACxC,uGAAuG;IACvG,YAAY,EAAE,MAAM,eAAe,CAAC,WAAW,CAAC,CAAA;IAChD,QAAQ,EAAE,MAAM,eAAe,CAAC,OAAO,CAAC,CAAA;CACzC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/gates/types.ts"],"names":[],"mappings":""}
package/build/gates.d.ts CHANGED
@@ -1,8 +1,13 @@
1
- import type { IntegrationGate, KubeEnv, MongoEnv, PostgresEnv, RedisEnv, S3Env, SmtpEnv } from './types.js';
1
+ import type { IntegrationGate, MongoEnv, PostgresEnv, RedisEnv, SmtpEnv } from './types.js';
2
+ import type { GateHelper } from './gates/types.js';
3
+ export declare const createGateHelper: () => GateHelper;
4
+ export declare const gateHelper: GateHelper;
5
+ /** @deprecated compat:factory-refactor — use `gateHelper.mongoGate()` */
2
6
  export declare const mongoGate: () => IntegrationGate<MongoEnv>;
7
+ /** @deprecated compat:factory-refactor — use `gateHelper.redisGate()` */
3
8
  export declare const redisGate: () => IntegrationGate<RedisEnv>;
4
- export declare const s3Gate: () => IntegrationGate<S3Env>;
5
- export declare const kubeGate: () => IntegrationGate<KubeEnv>;
9
+ /** @deprecated compat:factory-refactor — use `gateHelper.postgresGate()` */
6
10
  export declare const postgresGate: () => IntegrationGate<PostgresEnv>;
11
+ /** @deprecated compat:factory-refactor — use `gateHelper.smtpGate()` */
7
12
  export declare const smtpGate: () => IntegrationGate<SmtpEnv>;
8
13
  //# sourceMappingURL=gates.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAyB3G,eAAO,MAAM,SAAS,QAAO,eAAe,CAAC,QAAQ,CAC2D,CAAA;AAEhH,eAAO,MAAM,SAAS,QAAO,eAAe,CAAC,QAAQ,CAC2D,CAAA;AAEhH,eAAO,MAAM,MAAM,QAAO,eAAe,CAAC,KAAK,CAI5C,CAAA;AAEH,eAAO,MAAM,QAAQ,QAAO,eAAe,CAAC,OAAO,CACU,CAAA;AAE7D,eAAO,MAAM,YAAY,QAAO,eAAe,CAAC,WAAW,CACgE,CAAA;AAE3H,eAAO,MAAM,QAAQ,QAAO,eAAe,CAAC,OAAO,CAIhD,CAAA"}
1
+ {"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAW,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAS,OAAO,EAAE,MAAM,YAAY,CAAA;AAC3G,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD,eAAO,MAAM,gBAAgB,QAAO,UAiDnC,CAAA;AAED,eAAO,MAAM,UAAU,YAAqB,CAAA;AAE5C,yEAAyE;AACzE,eAAO,MAAM,SAAS,QAAO,eAAe,CAAC,QAAQ,CAA2B,CAAA;AAEhF,yEAAyE;AACzE,eAAO,MAAM,SAAS,QAAO,eAAe,CAAC,QAAQ,CAA2B,CAAA;AAEhF,4EAA4E;AAC5E,eAAO,MAAM,YAAY,QAAO,eAAe,CAAC,WAAW,CAA8B,CAAA;AAEzF,wEAAwE;AACxE,eAAO,MAAM,QAAQ,QAAO,eAAe,CAAC,OAAO,CAA0B,CAAA"}
package/build/gates.js CHANGED
@@ -1,29 +1,41 @@
1
- import { hasEnv, requireEnv } from '@owlmeans/test';
2
- import { unreachableReason } from './reachability.js';
3
- const collect = (keys) => {
4
- const env = {};
5
- for (const k of keys) {
6
- if (hasEnv(k))
7
- env[k] = process.env[k];
8
- }
9
- return env;
1
+ import { envHelper } from '@owlmeans/test';
2
+ import { reachabilityUtils } from './reachability.js';
3
+ export const createGateHelper = () => {
4
+ const collect = (keys) => {
5
+ const env = {};
6
+ for (const k of keys) {
7
+ if (envHelper.hasEnv(k))
8
+ env[k] = process.env[k];
9
+ }
10
+ return env;
11
+ };
12
+ const toIntegrationGate = (required, optional = [], reachable) => {
13
+ const gate = envHelper.requireEnv(required);
14
+ const env = collect([...required, ...optional]);
15
+ if (!('ok' in gate))
16
+ return { skip: true, reason: gate.reason, env };
17
+ if (reachable != null) {
18
+ const reason = reachabilityUtils.unreachableReason(reachable.key, process.env[reachable.key], reachable.defaultPort);
19
+ if (reason != null)
20
+ return { skip: true, reason, env };
21
+ }
22
+ return { skip: false, env };
23
+ };
24
+ const mongoGate = () => toIntegrationGate(['MONGO_URL'], ['MONGO_TEST_DB_PREFIX'], { key: 'MONGO_URL', defaultPort: 27017 });
25
+ const redisGate = () => toIntegrationGate(['REDIS_URL'], ['REDIS_TEST_KEY_PREFIX'], { key: 'REDIS_URL', defaultPort: 6379 });
26
+ const s3Gate = () => toIntegrationGate(['S3_ENDPOINT', 'S3_KEY', 'S3_SECRET', 'S3_TEST_BUCKET'], ['S3_REGION']);
27
+ const kubeGate = () => toIntegrationGate(['KUBE_CONFIG', 'KUBE_TEST_OK']);
28
+ const postgresGate = () => toIntegrationGate(['POSTGRES_URL'], ['POSTGRES_TEST_DB_PREFIX'], { key: 'POSTGRES_URL', defaultPort: 5432 });
29
+ const smtpGate = () => toIntegrationGate(['SMTP_HOST', 'SMTP_USER', 'SMTP_PASSWORD', 'SMTP_FROM', 'SMTP_TEST_TO'], ['SMTP_PORT', 'SMTP_SECURE']);
30
+ return { mongoGate, redisGate, s3Gate, kubeGate, postgresGate, smtpGate };
10
31
  };
11
- const toIntegrationGate = (required, optional = [], reachable) => {
12
- const gate = requireEnv(required);
13
- const env = collect([...required, ...optional]);
14
- if (!('ok' in gate))
15
- return { skip: true, reason: gate.reason, env };
16
- if (reachable != null) {
17
- const reason = unreachableReason(reachable.key, process.env[reachable.key], reachable.defaultPort);
18
- if (reason != null)
19
- return { skip: true, reason, env };
20
- }
21
- return { skip: false, env };
22
- };
23
- export const mongoGate = () => toIntegrationGate(['MONGO_URL'], ['MONGO_TEST_DB_PREFIX'], { key: 'MONGO_URL', defaultPort: 27017 });
24
- export const redisGate = () => toIntegrationGate(['REDIS_URL'], ['REDIS_TEST_KEY_PREFIX'], { key: 'REDIS_URL', defaultPort: 6379 });
25
- export const s3Gate = () => toIntegrationGate(['S3_ENDPOINT', 'S3_KEY', 'S3_SECRET', 'S3_TEST_BUCKET'], ['S3_REGION']);
26
- export const kubeGate = () => toIntegrationGate(['KUBE_CONFIG', 'KUBE_TEST_OK']);
27
- export const postgresGate = () => toIntegrationGate(['POSTGRES_URL'], ['POSTGRES_TEST_DB_PREFIX'], { key: 'POSTGRES_URL', defaultPort: 5432 });
28
- export const smtpGate = () => toIntegrationGate(['SMTP_HOST', 'SMTP_USER', 'SMTP_PASSWORD', 'SMTP_FROM', 'SMTP_TEST_TO'], ['SMTP_PORT', 'SMTP_SECURE']);
32
+ export const gateHelper = createGateHelper();
33
+ /** @deprecated compat:factory-refactor — use `gateHelper.mongoGate()` */
34
+ export const mongoGate = () => gateHelper.mongoGate();
35
+ /** @deprecated compat:factory-refactor — use `gateHelper.redisGate()` */
36
+ export const redisGate = () => gateHelper.redisGate();
37
+ /** @deprecated compat:factory-refactor — use `gateHelper.postgresGate()` */
38
+ export const postgresGate = () => gateHelper.postgresGate();
39
+ /** @deprecated compat:factory-refactor — use `gateHelper.smtpGate()` */
40
+ export const smtpGate = () => gateHelper.smtpGate();
29
41
  //# sourceMappingURL=gates.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"gates.js","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAIrD,MAAM,OAAO,GAAG,CAAI,IAA0B,EAAc,EAAE;IAC5D,MAAM,GAAG,GAA2B,EAAE,CAAA;IACtC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,MAAM,CAAC,CAAC,CAAC;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAW,CAAA;IAClD,CAAC;IACD,OAAO,GAAiB,CAAA;AAC1B,CAAC,CAAA;AAED,MAAM,iBAAiB,GAAG,CACxB,QAA8B,EAC9B,QAAQ,GAAyB,EAAE,EACnC,SAAwB,EACJ,EAAE;IACtB,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAA;IACjC,MAAM,GAAG,GAAG,OAAO,CAAI,CAAC,GAAG,QAAQ,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAA;IAClD,IAAI,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAA;IACpE,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;QACtB,MAAM,MAAM,GAAG,iBAAiB,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAW,EAAE,SAAS,CAAC,WAAW,CAAC,CAAA;QAC5G,IAAI,MAAM,IAAI,IAAI;YAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAA;IACxD,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAA;AAC7B,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,GAA8B,EAAE,CACvD,iBAAiB,CAAW,CAAC,WAAW,CAAC,EAAE,CAAC,sBAAsB,CAAC,EAAE,EAAE,GAAG,EAAE,WAAW,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAA;AAEhH,MAAM,CAAC,MAAM,SAAS,GAAG,GAA8B,EAAE,CACvD,iBAAiB,CAAW,CAAC,WAAW,CAAC,EAAE,CAAC,uBAAuB,CAAC,EAAE,EAAE,GAAG,EAAE,WAAW,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAA;AAEhH,MAAM,CAAC,MAAM,MAAM,GAAG,GAA2B,EAAE,CACjD,iBAAiB,CACf,CAAC,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,gBAAgB,CAAC,EACxD,CAAC,WAAW,CAAC,CACd,CAAA;AAEH,MAAM,CAAC,MAAM,QAAQ,GAAG,GAA6B,EAAE,CACrD,iBAAiB,CAAU,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC,CAAA;AAE7D,MAAM,CAAC,MAAM,YAAY,GAAG,GAAiC,EAAE,CAC7D,iBAAiB,CAAc,CAAC,cAAc,CAAC,EAAE,CAAC,yBAAyB,CAAC,EAAE,EAAE,GAAG,EAAE,cAAc,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAA;AAE3H,MAAM,CAAC,MAAM,QAAQ,GAAG,GAA6B,EAAE,CACrD,iBAAiB,CACf,CAAC,WAAW,EAAE,WAAW,EAAE,eAAe,EAAE,WAAW,EAAE,cAAc,CAAC,EACxE,CAAC,WAAW,EAAE,aAAa,CAAC,CAC7B,CAAA"}
1
+ {"version":3,"file":"gates.js","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"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"}
package/build/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from './gates.js';
2
+ export type * from './gates/types.js';
2
3
  export type * from './types.js';
3
4
  export * from './naming.js';
4
5
  export * from './cleanup.js';
6
+ export type * from './cleanup/types.js';
5
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,mBAAmB,kBAAkB,CAAA;AACrC,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,mBAAmB,oBAAoB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAE1B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAG1B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA"}
@@ -0,0 +1,27 @@
1
+ import type { ProbeTarget } from '../types.js';
2
+ /**
3
+ * Whether the server a connection string names answers at all — asked synchronously, because
4
+ * Bun decides `test` vs `test.skip` synchronously and every gate is read at module scope.
5
+ */
6
+ export interface ReachabilityUtils {
7
+ /**
8
+ * Every `host:port` in the authority of a connection string, or `null` when the string names
9
+ * nothing a TCP connect can check — an SRV record, a unix socket, or a shape this cannot read.
10
+ * `null` leaves the gate exactly as the variable alone would have it.
11
+ */
12
+ parseTargets: (url: string, defaultPort: number) => ProbeTarget[] | null;
13
+ /**
14
+ * `null` when at least one target accepts a TCP connection, otherwise the reason none did.
15
+ *
16
+ * A probe that cannot run at all also answers `null`: not knowing is not evidence the service is
17
+ * down, and the suite then behaves exactly as it did before probes existed.
18
+ */
19
+ probeTargets: (targets: ProbeTarget[]) => string | null;
20
+ /**
21
+ * The skip reason for a populated connection-string variable whose server does not answer, or
22
+ * `null` when it answers or cannot be probed. Printed once per variable and address, so a closed
23
+ * gate is never mistaken for a suite that has nothing to run.
24
+ */
25
+ unreachableReason: (variable: string, url: string, defaultPort: number) => string | null;
26
+ }
27
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/reachability/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,YAAY,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,WAAW,EAAE,GAAG,IAAI,CAAA;IACxE;;;;;OAKG;IACH,YAAY,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,MAAM,GAAG,IAAI,CAAA;IACvD;;;;OAIG;IACH,iBAAiB,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;CACzF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/reachability/types.ts"],"names":[],"mappings":""}
@@ -1,21 +1,4 @@
1
- import type { ProbeTarget } from './types.js';
2
- /**
3
- * Every `host:port` in the authority of a connection string, or `null` when the string names
4
- * nothing a TCP connect can check — an SRV record, a unix socket, or a shape this cannot read.
5
- * `null` leaves the gate exactly as the variable alone would have it.
6
- */
7
- export declare const parseTargets: (url: string, defaultPort: number) => ProbeTarget[] | null;
8
- /**
9
- * `null` when at least one target accepts a TCP connection, otherwise the reason none did.
10
- *
11
- * A probe that cannot run at all also answers `null`: not knowing is not evidence the service is
12
- * down, and the suite then behaves exactly as it did before probes existed.
13
- */
14
- export declare const probeTargets: (targets: ProbeTarget[]) => string | null;
15
- /**
16
- * The skip reason for a populated connection-string variable whose server does not answer, or
17
- * `null` when it answers or cannot be probed. Printed once per variable and address, so a closed
18
- * gate is never mistaken for a suite that has nothing to run.
19
- */
20
- export declare const unreachableReason: (variable: string, url: string, defaultPort: number) => string | null;
1
+ import type { ReachabilityUtils } from './reachability/types.js';
2
+ export declare const createReachabilityUtils: () => ReachabilityUtils;
3
+ export declare const reachabilityUtils: ReachabilityUtils;
21
4
  //# sourceMappingURL=reachability.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"reachability.d.ts","sourceRoot":"","sources":["../src/reachability.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAiB7C;;;;GAIG;AACH,eAAO,MAAM,YAAY,QAAS,MAAM,eAAe,MAAM,KAAG,WAAW,EAAE,GAAG,IA6B/E,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,YAAa,WAAW,EAAE,KAAG,MAAM,GAAG,IAkB9D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,aAAc,MAAM,OAAO,MAAM,eAAe,MAAM,KAAG,MAAM,GAAG,IAiB/F,CAAA"}
1
+ {"version":3,"file":"reachability.d.ts","sourceRoot":"","sources":["../src/reachability.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAkBhE,eAAO,MAAM,uBAAuB,QAAO,iBAwE1C,CAAA;AAED,eAAO,MAAM,iBAAiB,mBAA4B,CAAA"}
@@ -11,84 +11,73 @@ const log = logger('test-integration');
11
11
  * it spends its whole `beforeAll` budget on a refused connection and FAILS. Only a TCP connect is
12
12
  * asked — never a login — so no driver is needed here and no credential leaves the variable.
13
13
  */
14
+ // Process-wide on purpose: every gate of a run shares one answer per address and one warning per reason.
14
15
  const answers = new Map();
15
16
  const warned = new Set();
16
- /**
17
- * Every `host:port` in the authority of a connection string, or `null` when the string names
18
- * nothing a TCP connect can check — an SRV record, a unix socket, or a shape this cannot read.
19
- * `null` leaves the gate exactly as the variable alone would have it.
20
- */
21
- export const parseTargets = (url, defaultPort) => {
22
- const scheme = /^([a-z][a-z0-9+.-]*):\/\//i.exec(url);
23
- if (scheme == null || scheme[1].toLowerCase().endsWith('+srv')) {
24
- return null;
25
- }
26
- const rest = url.slice(scheme[0].length);
27
- const end = rest.search(/[/?#]/);
28
- const authority = end < 0 ? rest : rest.slice(0, end);
29
- const hosts = authority.slice(authority.lastIndexOf('@') + 1);
30
- const targets = [];
31
- for (const entry of hosts.split(',')) {
32
- const match = /^(\[[^\]]+\]|[^:]*)(?::(\d+))?$/.exec(entry);
33
- if (match == null) {
17
+ export const createReachabilityUtils = () => {
18
+ const parseTargets = (url, defaultPort) => {
19
+ const scheme = /^([a-z][a-z0-9+.-]*):\/\//i.exec(url);
20
+ if (scheme == null || scheme[1].toLowerCase().endsWith('+srv')) {
34
21
  return null;
35
22
  }
36
- let host;
37
- try {
38
- host = decodeURIComponent(match[1]).replace(/^\[|\]$/g, '');
23
+ const rest = url.slice(scheme[0].length);
24
+ const end = rest.search(/[/?#]/);
25
+ const authority = end < 0 ? rest : rest.slice(0, end);
26
+ const hosts = authority.slice(authority.lastIndexOf('@') + 1);
27
+ const targets = [];
28
+ for (const entry of hosts.split(',')) {
29
+ const match = /^(\[[^\]]+\]|[^:]*)(?::(\d+))?$/.exec(entry);
30
+ if (match == null) {
31
+ return null;
32
+ }
33
+ let host;
34
+ try {
35
+ host = decodeURIComponent(match[1]).replace(/^\[|\]$/g, '');
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ if (host === '' || host.includes('/')) {
41
+ return null;
42
+ }
43
+ targets.push({ host, port: match[2] != null ? parseInt(match[2], 10) : defaultPort });
44
+ }
45
+ return targets.length > 0 ? targets : null;
46
+ };
47
+ const probeTargets = (targets) => {
48
+ const key = targets.map(({ host, port }) => `${host}:${port}`).join(',');
49
+ if (answers.has(key)) {
50
+ return answers.get(key) ?? null;
39
51
  }
40
- catch {
52
+ const result = spawnSync(process.execPath, ['-e', PROBE_SCRIPT], {
53
+ env: { ...process.env, OWLMEANS_TEST_PROBE: JSON.stringify([targets, CONNECT_TIMEOUT_MS]) },
54
+ stdio: ['ignore', 'pipe', 'ignore'],
55
+ timeout: CONNECT_TIMEOUT_MS + SPAWN_GRACE_MS,
56
+ encoding: 'utf8',
57
+ });
58
+ const answer = result.error == null && result.status === 1
59
+ ? `nothing answers at ${key} (${result.stdout.trim() !== '' ? result.stdout.trim() : 'unreachable'})`
60
+ : null;
61
+ answers.set(key, answer);
62
+ return answer;
63
+ };
64
+ const unreachableReason = (variable, url, defaultPort) => {
65
+ const targets = parseTargets(url, defaultPort);
66
+ if (targets == null) {
41
67
  return null;
42
68
  }
43
- if (host === '' || host.includes('/')) {
69
+ const unreachable = probeTargets(targets);
70
+ if (unreachable == null) {
44
71
  return null;
45
72
  }
46
- targets.push({ host, port: match[2] != null ? parseInt(match[2], 10) : defaultPort });
47
- }
48
- return targets.length > 0 ? targets : null;
49
- };
50
- /**
51
- * `null` when at least one target accepts a TCP connection, otherwise the reason none did.
52
- *
53
- * A probe that cannot run at all also answers `null`: not knowing is not evidence the service is
54
- * down, and the suite then behaves exactly as it did before probes existed.
55
- */
56
- export const probeTargets = (targets) => {
57
- const key = targets.map(({ host, port }) => `${host}:${port}`).join(',');
58
- if (answers.has(key)) {
59
- return answers.get(key) ?? null;
60
- }
61
- const result = spawnSync(process.execPath, ['-e', PROBE_SCRIPT], {
62
- env: { ...process.env, OWLMEANS_TEST_PROBE: JSON.stringify([targets, CONNECT_TIMEOUT_MS]) },
63
- stdio: ['ignore', 'pipe', 'ignore'],
64
- timeout: CONNECT_TIMEOUT_MS + SPAWN_GRACE_MS,
65
- encoding: 'utf8',
66
- });
67
- const answer = result.error == null && result.status === 1
68
- ? `nothing answers at ${key} (${result.stdout.trim() !== '' ? result.stdout.trim() : 'unreachable'})`
69
- : null;
70
- answers.set(key, answer);
71
- return answer;
72
- };
73
- /**
74
- * The skip reason for a populated connection-string variable whose server does not answer, or
75
- * `null` when it answers or cannot be probed. Printed once per variable and address, so a closed
76
- * gate is never mistaken for a suite that has nothing to run.
77
- */
78
- export const unreachableReason = (variable, url, defaultPort) => {
79
- const targets = parseTargets(url, defaultPort);
80
- if (targets == null) {
81
- return null;
82
- }
83
- const unreachable = probeTargets(targets);
84
- if (unreachable == null) {
85
- return null;
86
- }
87
- const reason = `${variable} is set, but ${unreachable} — start the service (or its port-forward) to run these specs`;
88
- if (!warned.has(reason)) {
89
- warned.add(reason);
90
- log.warn('Skipping integration specs: a configured service is unreachable', { variable, unreachable });
91
- }
92
- return reason;
73
+ const reason = `${variable} is set, but ${unreachable} — start the service (or its port-forward) to run these specs`;
74
+ if (!warned.has(reason)) {
75
+ warned.add(reason);
76
+ log.warn('Skipping integration specs: a configured service is unreachable', { variable, unreachable });
77
+ }
78
+ return reason;
79
+ };
80
+ return { parseTargets, probeTargets, unreachableReason };
93
81
  };
82
+ export const reachabilityUtils = createReachabilityUtils();
94
83
  //# sourceMappingURL=reachability.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"reachability.js","sourceRoot":"","sources":["../src/reachability.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"reachability.js","sourceRoot":"","sources":["../src/reachability.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,3 @@
1
- export type CleanupFn = () => void | Promise<void>;
2
1
  /**
3
2
  * A connection-string variable whose server must answer a TCP connect for the gate to open.
4
3
  *
@@ -1 +1 @@
1
- {"version":3,"file":"types.local.d.ts","sourceRoot":"","sources":["../src/types.local.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAElD;;;;;;GAMG;AACH,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;CACpB"}
1
+ {"version":3,"file":"types.local.d.ts","sourceRoot":"","sources":["../src/types.local.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC,GAAG,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;CACpB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/test-integration",
3
- "version": "0.1.18-rc.32",
3
+ "version": "0.1.18-rc.33",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -21,8 +21,8 @@
21
21
  }
22
22
  },
23
23
  "dependencies": {
24
- "@owlmeans/log": "^0.1.18-rc.1",
25
- "@owlmeans/test": "^0.1.18-rc.34"
24
+ "@owlmeans/log": "^0.1.18-rc.2",
25
+ "@owlmeans/test": "^0.1.18-rc.35"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@owlmeans/dep-config": "^0.1.18-rc.6",
@@ -0,0 +1,18 @@
1
+ export interface CleanupFn { (): void | Promise<void> }
2
+
3
+ /** The opt-in teardown queue a suite's `tests/context.ts` fills and its `afterAll` drains. */
4
+ export interface CleanupHelper {
5
+ /**
6
+ * Register a cleanup function that {@link CleanupHelper.runCleanups} will execute (LIFO).
7
+ * Use from `tests/context.ts` `setup()` to schedule teardown of resources
8
+ * that were provisioned for the suite (DB drop, key namespace flush,
9
+ * uploaded objects).
10
+ */
11
+ registerCleanup: (fn: CleanupFn) => void
12
+ /**
13
+ * Run all pending cleanup functions in reverse registration order.
14
+ * Errors are swallowed and logged as warnings so a failing cleanup
15
+ * cannot mask a test failure.
16
+ */
17
+ runCleanups: () => Promise<void>
18
+ }
package/src/cleanup.ts CHANGED
@@ -1,33 +1,35 @@
1
1
  import { logger } from '@owlmeans/log'
2
- import type { CleanupFn } from './types.local.js'
2
+ import type { CleanupFn, CleanupHelper } from './cleanup/types.js'
3
3
 
4
4
  const log = logger('test-integration')
5
5
 
6
+ // Process-wide on purpose: every spec file of a run registers into, and drains, the same queue.
6
7
  const queue: CleanupFn[] = []
7
8
 
8
- /**
9
- * Register a cleanup function that `runCleanups()` will execute (LIFO).
10
- * Use from `tests/context.ts` `setup()` to schedule teardown of resources
11
- * that were provisioned for the suite (DB drop, key namespace flush,
12
- * uploaded objects).
13
- */
14
- export const registerCleanup = (fn: CleanupFn): void => {
15
- queue.push(fn)
16
- }
9
+ export const createCleanupHelper = (): CleanupHelper => {
10
+ const registerCleanup = (fn: CleanupFn): void => {
11
+ queue.push(fn)
12
+ }
17
13
 
18
- /**
19
- * Run all pending cleanup functions in reverse registration order.
20
- * Errors are swallowed and logged as warnings so a failing cleanup
21
- * cannot mask a test failure.
22
- */
23
- export const runCleanups = async (): Promise<void> => {
24
- while (queue.length > 0) {
25
- const fn = queue.pop()
26
- if (fn == null) continue
27
- try {
28
- await fn()
29
- } catch (err) {
30
- log.warn('Cleanup failed', err)
14
+ const runCleanups = async (): Promise<void> => {
15
+ while (queue.length > 0) {
16
+ const fn = queue.pop()
17
+ if (fn == null) continue
18
+ try {
19
+ await fn()
20
+ } catch (err) {
21
+ log.warn('Cleanup failed', err)
22
+ }
31
23
  }
32
24
  }
25
+
26
+ return { registerCleanup, runCleanups }
33
27
  }
28
+
29
+ export const cleanupHelper = createCleanupHelper()
30
+
31
+ /** @deprecated compat:factory-refactor — use `cleanupHelper.registerCleanup(…)` */
32
+ export const registerCleanup = (fn: CleanupFn): void => cleanupHelper.registerCleanup(fn)
33
+
34
+ /** @deprecated compat:factory-refactor — use `cleanupHelper.runCleanups()` */
35
+ export const runCleanups = async (): Promise<void> => await cleanupHelper.runCleanups()
@@ -0,0 +1,18 @@
1
+ import type { IntegrationGate, KubeEnv, MongoEnv, PostgresEnv, RedisEnv, S3Env, SmtpEnv } from '../types.js'
2
+
3
+ /**
4
+ * The env gates of the integration suites: each reads the variables documented in `.env.example`
5
+ * and answers `{ skip, reason?, env }`, so a `tests/context.ts` decides whether to register the
6
+ * real service and a spec self-skips when it is missing.
7
+ */
8
+ export interface GateHelper {
9
+ /** `MONGO_URL` set AND a host in it accepting a TCP connect; `MONGO_TEST_DB_PREFIX` optional. */
10
+ mongoGate: () => IntegrationGate<MongoEnv>
11
+ /** `REDIS_URL` set AND a host in it accepting a TCP connect; `REDIS_TEST_KEY_PREFIX` optional. */
12
+ redisGate: () => IntegrationGate<RedisEnv>
13
+ s3Gate: () => IntegrationGate<S3Env>
14
+ kubeGate: () => IntegrationGate<KubeEnv>
15
+ /** `POSTGRES_URL` set AND a host in it accepting a TCP connect; `POSTGRES_TEST_DB_PREFIX` optional. */
16
+ postgresGate: () => IntegrationGate<PostgresEnv>
17
+ smtpGate: () => IntegrationGate<SmtpEnv>
18
+ }
package/src/gates.ts CHANGED
@@ -1,51 +1,70 @@
1
- import { hasEnv, requireEnv } from '@owlmeans/test'
2
- import { unreachableReason } from './reachability.js'
1
+ import { envHelper } from '@owlmeans/test'
2
+ import { reachabilityUtils } from './reachability.js'
3
3
  import type { Reachable } from './types.local.js'
4
4
  import type { IntegrationGate, KubeEnv, MongoEnv, PostgresEnv, RedisEnv, S3Env, SmtpEnv } from './types.js'
5
+ import type { GateHelper } from './gates/types.js'
5
6
 
6
- const collect = <E>(keys: (keyof E & string)[]): Partial<E> => {
7
- const env: Record<string, string> = {}
8
- for (const k of keys) {
9
- if (hasEnv(k)) env[k] = process.env[k] as string
7
+ export const createGateHelper = (): GateHelper => {
8
+ const collect = <E>(keys: (keyof E & string)[]): Partial<E> => {
9
+ const env: Record<string, string> = {}
10
+ for (const k of keys) {
11
+ if (envHelper.hasEnv(k)) env[k] = process.env[k] as string
12
+ }
13
+ return env as Partial<E>
10
14
  }
11
- return env as Partial<E>
12
- }
13
15
 
14
- const toIntegrationGate = <E>(
15
- required: (keyof E & string)[],
16
- optional: (keyof E & string)[] = [],
17
- reachable?: Reachable<E>
18
- ): IntegrationGate<E> => {
19
- const gate = requireEnv(required)
20
- const env = collect<E>([...required, ...optional])
21
- if (!('ok' in gate)) return { skip: true, reason: gate.reason, env }
22
- if (reachable != null) {
23
- const reason = unreachableReason(reachable.key, process.env[reachable.key] as string, reachable.defaultPort)
24
- if (reason != null) return { skip: true, reason, env }
16
+ const toIntegrationGate = <E>(
17
+ required: (keyof E & string)[],
18
+ optional: (keyof E & string)[] = [],
19
+ reachable?: Reachable<E>
20
+ ): IntegrationGate<E> => {
21
+ const gate = envHelper.requireEnv(required)
22
+ const env = collect<E>([...required, ...optional])
23
+ if (!('ok' in gate)) return { skip: true, reason: gate.reason, env }
24
+ if (reachable != null) {
25
+ const reason = reachabilityUtils.unreachableReason(reachable.key, process.env[reachable.key] as string, reachable.defaultPort)
26
+ if (reason != null) return { skip: true, reason, env }
27
+ }
28
+ return { skip: false, env }
25
29
  }
26
- return { skip: false, env }
27
- }
28
30
 
29
- export const mongoGate = (): IntegrationGate<MongoEnv> =>
30
- toIntegrationGate<MongoEnv>(['MONGO_URL'], ['MONGO_TEST_DB_PREFIX'], { key: 'MONGO_URL', defaultPort: 27017 })
31
+ const mongoGate = (): IntegrationGate<MongoEnv> =>
32
+ toIntegrationGate<MongoEnv>(['MONGO_URL'], ['MONGO_TEST_DB_PREFIX'], { key: 'MONGO_URL', defaultPort: 27017 })
33
+
34
+ const redisGate = (): IntegrationGate<RedisEnv> =>
35
+ toIntegrationGate<RedisEnv>(['REDIS_URL'], ['REDIS_TEST_KEY_PREFIX'], { key: 'REDIS_URL', defaultPort: 6379 })
36
+
37
+ const s3Gate = (): IntegrationGate<S3Env> =>
38
+ toIntegrationGate<S3Env>(
39
+ ['S3_ENDPOINT', 'S3_KEY', 'S3_SECRET', 'S3_TEST_BUCKET'],
40
+ ['S3_REGION']
41
+ )
42
+
43
+ const kubeGate = (): IntegrationGate<KubeEnv> =>
44
+ toIntegrationGate<KubeEnv>(['KUBE_CONFIG', 'KUBE_TEST_OK'])
45
+
46
+ const postgresGate = (): IntegrationGate<PostgresEnv> =>
47
+ toIntegrationGate<PostgresEnv>(['POSTGRES_URL'], ['POSTGRES_TEST_DB_PREFIX'], { key: 'POSTGRES_URL', defaultPort: 5432 })
48
+
49
+ const smtpGate = (): IntegrationGate<SmtpEnv> =>
50
+ toIntegrationGate<SmtpEnv>(
51
+ ['SMTP_HOST', 'SMTP_USER', 'SMTP_PASSWORD', 'SMTP_FROM', 'SMTP_TEST_TO'],
52
+ ['SMTP_PORT', 'SMTP_SECURE']
53
+ )
54
+
55
+ return { mongoGate, redisGate, s3Gate, kubeGate, postgresGate, smtpGate }
56
+ }
31
57
 
32
- export const redisGate = (): IntegrationGate<RedisEnv> =>
33
- toIntegrationGate<RedisEnv>(['REDIS_URL'], ['REDIS_TEST_KEY_PREFIX'], { key: 'REDIS_URL', defaultPort: 6379 })
58
+ export const gateHelper = createGateHelper()
34
59
 
35
- export const s3Gate = (): IntegrationGate<S3Env> =>
36
- toIntegrationGate<S3Env>(
37
- ['S3_ENDPOINT', 'S3_KEY', 'S3_SECRET', 'S3_TEST_BUCKET'],
38
- ['S3_REGION']
39
- )
60
+ /** @deprecated compat:factory-refactor — use `gateHelper.mongoGate()` */
61
+ export const mongoGate = (): IntegrationGate<MongoEnv> => gateHelper.mongoGate()
40
62
 
41
- export const kubeGate = (): IntegrationGate<KubeEnv> =>
42
- toIntegrationGate<KubeEnv>(['KUBE_CONFIG', 'KUBE_TEST_OK'])
63
+ /** @deprecated compat:factory-refactor — use `gateHelper.redisGate()` */
64
+ export const redisGate = (): IntegrationGate<RedisEnv> => gateHelper.redisGate()
43
65
 
44
- export const postgresGate = (): IntegrationGate<PostgresEnv> =>
45
- toIntegrationGate<PostgresEnv>(['POSTGRES_URL'], ['POSTGRES_TEST_DB_PREFIX'], { key: 'POSTGRES_URL', defaultPort: 5432 })
66
+ /** @deprecated compat:factory-refactor — use `gateHelper.postgresGate()` */
67
+ export const postgresGate = (): IntegrationGate<PostgresEnv> => gateHelper.postgresGate()
46
68
 
47
- export const smtpGate = (): IntegrationGate<SmtpEnv> =>
48
- toIntegrationGate<SmtpEnv>(
49
- ['SMTP_HOST', 'SMTP_USER', 'SMTP_PASSWORD', 'SMTP_FROM', 'SMTP_TEST_TO'],
50
- ['SMTP_PORT', 'SMTP_SECURE']
51
- )
69
+ /** @deprecated compat:factory-refactor — use `gateHelper.smtpGate()` */
70
+ export const smtpGate = (): IntegrationGate<SmtpEnv> => gateHelper.smtpGate()
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './gates.js'
2
+ export type * from './gates/types.js'
2
3
  export type * from './types.js'
3
4
  export * from './naming.js'
4
5
  export * from './cleanup.js'
6
+ export type * from './cleanup/types.js'
@@ -0,0 +1,27 @@
1
+ import type { ProbeTarget } from '../types.js'
2
+
3
+ /**
4
+ * Whether the server a connection string names answers at all — asked synchronously, because
5
+ * Bun decides `test` vs `test.skip` synchronously and every gate is read at module scope.
6
+ */
7
+ export interface ReachabilityUtils {
8
+ /**
9
+ * Every `host:port` in the authority of a connection string, or `null` when the string names
10
+ * nothing a TCP connect can check — an SRV record, a unix socket, or a shape this cannot read.
11
+ * `null` leaves the gate exactly as the variable alone would have it.
12
+ */
13
+ parseTargets: (url: string, defaultPort: number) => ProbeTarget[] | null
14
+ /**
15
+ * `null` when at least one target accepts a TCP connection, otherwise the reason none did.
16
+ *
17
+ * A probe that cannot run at all also answers `null`: not knowing is not evidence the service is
18
+ * down, and the suite then behaves exactly as it did before probes existed.
19
+ */
20
+ probeTargets: (targets: ProbeTarget[]) => string | null
21
+ /**
22
+ * The skip reason for a populated connection-string variable whose server does not answer, or
23
+ * `null` when it answers or cannot be probed. Printed once per variable and address, so a closed
24
+ * gate is never mistaken for a suite that has nothing to run.
25
+ */
26
+ unreachableReason: (variable: string, url: string, defaultPort: number) => string | null
27
+ }
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process'
2
2
  import { logger } from '@owlmeans/log'
3
3
  import { CONNECT_TIMEOUT_MS, PROBE_SCRIPT, SPAWN_GRACE_MS } from './consts.local.js'
4
4
  import type { ProbeTarget } from './types.js'
5
+ import type { ReachabilityUtils } from './reachability/types.js'
5
6
 
6
7
  const log = logger('test-integration')
7
8
 
@@ -15,91 +16,82 @@ const log = logger('test-integration')
15
16
  * asked — never a login — so no driver is needed here and no credential leaves the variable.
16
17
  */
17
18
 
19
+ // Process-wide on purpose: every gate of a run shares one answer per address and one warning per reason.
18
20
  const answers = new Map<string, string | null>()
19
21
  const warned = new Set<string>()
20
22
 
21
- /**
22
- * Every `host:port` in the authority of a connection string, or `null` when the string names
23
- * nothing a TCP connect can check — an SRV record, a unix socket, or a shape this cannot read.
24
- * `null` leaves the gate exactly as the variable alone would have it.
25
- */
26
- export const parseTargets = (url: string, defaultPort: number): ProbeTarget[] | null => {
27
- const scheme = /^([a-z][a-z0-9+.-]*):\/\//i.exec(url)
28
- if (scheme == null || scheme[1].toLowerCase().endsWith('+srv')) {
29
- return null
30
- }
31
- const rest = url.slice(scheme[0].length)
32
- const end = rest.search(/[/?#]/)
33
- const authority = end < 0 ? rest : rest.slice(0, end)
34
- const hosts = authority.slice(authority.lastIndexOf('@') + 1)
35
-
36
- const targets: ProbeTarget[] = []
37
- for (const entry of hosts.split(',')) {
38
- const match = /^(\[[^\]]+\]|[^:]*)(?::(\d+))?$/.exec(entry)
39
- if (match == null) {
40
- return null
41
- }
42
- let host: string
43
- try {
44
- host = decodeURIComponent(match[1]).replace(/^\[|\]$/g, '')
45
- } catch {
46
- return null
47
- }
48
- if (host === '' || host.includes('/')) {
23
+ export const createReachabilityUtils = (): ReachabilityUtils => {
24
+ const parseTargets = (url: string, defaultPort: number): ProbeTarget[] | null => {
25
+ const scheme = /^([a-z][a-z0-9+.-]*):\/\//i.exec(url)
26
+ if (scheme == null || scheme[1].toLowerCase().endsWith('+srv')) {
49
27
  return null
50
28
  }
51
- targets.push({ host, port: match[2] != null ? parseInt(match[2], 10) : defaultPort })
52
- }
29
+ const rest = url.slice(scheme[0].length)
30
+ const end = rest.search(/[/?#]/)
31
+ const authority = end < 0 ? rest : rest.slice(0, end)
32
+ const hosts = authority.slice(authority.lastIndexOf('@') + 1)
53
33
 
54
- return targets.length > 0 ? targets : null
55
- }
34
+ const targets: ProbeTarget[] = []
35
+ for (const entry of hosts.split(',')) {
36
+ const match = /^(\[[^\]]+\]|[^:]*)(?::(\d+))?$/.exec(entry)
37
+ if (match == null) {
38
+ return null
39
+ }
40
+ let host: string
41
+ try {
42
+ host = decodeURIComponent(match[1]).replace(/^\[|\]$/g, '')
43
+ } catch {
44
+ return null
45
+ }
46
+ if (host === '' || host.includes('/')) {
47
+ return null
48
+ }
49
+ targets.push({ host, port: match[2] != null ? parseInt(match[2], 10) : defaultPort })
50
+ }
56
51
 
57
- /**
58
- * `null` when at least one target accepts a TCP connection, otherwise the reason none did.
59
- *
60
- * A probe that cannot run at all also answers `null`: not knowing is not evidence the service is
61
- * down, and the suite then behaves exactly as it did before probes existed.
62
- */
63
- export const probeTargets = (targets: ProbeTarget[]): string | null => {
64
- const key = targets.map(({ host, port }) => `${host}:${port}`).join(',')
65
- if (answers.has(key)) {
66
- return answers.get(key) ?? null
52
+ return targets.length > 0 ? targets : null
67
53
  }
68
54
 
69
- const result = spawnSync(process.execPath, ['-e', PROBE_SCRIPT], {
70
- env: { ...process.env, OWLMEANS_TEST_PROBE: JSON.stringify([targets, CONNECT_TIMEOUT_MS]) },
71
- stdio: ['ignore', 'pipe', 'ignore'],
72
- timeout: CONNECT_TIMEOUT_MS + SPAWN_GRACE_MS,
73
- encoding: 'utf8',
74
- })
75
- const answer = result.error == null && result.status === 1
76
- ? `nothing answers at ${key} (${result.stdout.trim() !== '' ? result.stdout.trim() : 'unreachable'})`
77
- : null
78
- answers.set(key, answer)
55
+ const probeTargets = (targets: ProbeTarget[]): string | null => {
56
+ const key = targets.map(({ host, port }) => `${host}:${port}`).join(',')
57
+ if (answers.has(key)) {
58
+ return answers.get(key) ?? null
59
+ }
79
60
 
80
- return answer
81
- }
61
+ const result = spawnSync(process.execPath, ['-e', PROBE_SCRIPT], {
62
+ env: { ...process.env, OWLMEANS_TEST_PROBE: JSON.stringify([targets, CONNECT_TIMEOUT_MS]) },
63
+ stdio: ['ignore', 'pipe', 'ignore'],
64
+ timeout: CONNECT_TIMEOUT_MS + SPAWN_GRACE_MS,
65
+ encoding: 'utf8',
66
+ })
67
+ const answer = result.error == null && result.status === 1
68
+ ? `nothing answers at ${key} (${result.stdout.trim() !== '' ? result.stdout.trim() : 'unreachable'})`
69
+ : null
70
+ answers.set(key, answer)
82
71
 
83
- /**
84
- * The skip reason for a populated connection-string variable whose server does not answer, or
85
- * `null` when it answers or cannot be probed. Printed once per variable and address, so a closed
86
- * gate is never mistaken for a suite that has nothing to run.
87
- */
88
- export const unreachableReason = (variable: string, url: string, defaultPort: number): string | null => {
89
- const targets = parseTargets(url, defaultPort)
90
- if (targets == null) {
91
- return null
92
- }
93
- const unreachable = probeTargets(targets)
94
- if (unreachable == null) {
95
- return null
72
+ return answer
96
73
  }
97
74
 
98
- const reason = `${variable} is set, but ${unreachable} — start the service (or its port-forward) to run these specs`
99
- if (!warned.has(reason)) {
100
- warned.add(reason)
101
- log.warn('Skipping integration specs: a configured service is unreachable', { variable, unreachable })
75
+ const unreachableReason = (variable: string, url: string, defaultPort: number): string | null => {
76
+ const targets = parseTargets(url, defaultPort)
77
+ if (targets == null) {
78
+ return null
79
+ }
80
+ const unreachable = probeTargets(targets)
81
+ if (unreachable == null) {
82
+ return null
83
+ }
84
+
85
+ const reason = `${variable} is set, but ${unreachable} — start the service (or its port-forward) to run these specs`
86
+ if (!warned.has(reason)) {
87
+ warned.add(reason)
88
+ log.warn('Skipping integration specs: a configured service is unreachable', { variable, unreachable })
89
+ }
90
+
91
+ return reason
102
92
  }
103
93
 
104
- return reason
94
+ return { parseTargets, probeTargets, unreachableReason }
105
95
  }
96
+
97
+ export const reachabilityUtils = createReachabilityUtils()
@@ -1,5 +1,3 @@
1
- export type CleanupFn = () => void | Promise<void>
2
-
3
1
  /**
4
2
  * A connection-string variable whose server must answer a TCP connect for the gate to open.
5
3
  *