@owlmeans/test 0.1.18-rc.8 → 0.1.18
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 +124 -8
- package/agent-meta/manifest.json +3 -3
- package/agent-meta/skills/testing-overview/SKILL.md +23 -10
- package/agent-meta/skills/testing-unit/SKILL.md +1 -1
- package/build/env.d.ts +3 -14
- package/build/env.d.ts.map +1 -1
- package/build/env.js +60 -55
- package/build/env.js.map +1 -1
- package/build/gates.d.ts +1 -5
- package/build/gates.d.ts.map +1 -1
- package/build/gates.js +2 -2
- package/build/gates.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/types.d.ts +31 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +4 -4
- package/src/env.ts +54 -57
- package/src/gates.ts +3 -7
- package/src/index.ts +1 -0
- package/src/types.ts +37 -0
package/README.md
CHANGED
|
@@ -1,14 +1,130 @@
|
|
|
1
1
|
# @owlmeans/test
|
|
2
2
|
|
|
3
|
-
Foundation helpers shared by the OwlMeans test packages and individual package test suites
|
|
3
|
+
Foundation helpers shared by the OwlMeans test packages and individual package test suites: the
|
|
4
|
+
repo-root `.env` loader, environment gates that let a spec self-skip with a printed reason, and a
|
|
5
|
+
JSON fixture loader. Every package's `tests/` uses it directly or through one of the harness
|
|
6
|
+
packages built on it. It holds no auth mocks, no service gates and no browser driver — those are
|
|
7
|
+
`@owlmeans/test-auth` (category B), `@owlmeans/test-integration` (category C) and
|
|
8
|
+
`@owlmeans/test-ui` (category D). Tests run on `bun:test`; this package adds no runner. The
|
|
9
|
+
package's skills are `testing-overview` (which category a package is in) and `testing-unit`.
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
- `requireEnv(keys)` — returns `{ ok: true }` or `{ skip: true, reason }` so callers can `test.skip(...)` when an integration variable is missing.
|
|
7
|
-
- `hasEnv(key)` — boolean shorthand for conditional service provisioning in `tests/context.ts`.
|
|
8
|
-
- `makeGates(spec)` — builds a frozen record of `{ <name>: { skip, reason } }` so a per-package `tests/context.ts` exposes one source of truth and individual specs can self-skip.
|
|
9
|
-
- `loadFixture<T>(relPath)` — JSON fixture loader rooted at the consuming package's `tests/fixtures/`.
|
|
11
|
+
## Installation
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
```bash
|
|
14
|
+
bun add -d @owlmeans/test@^0.1.18
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Concepts
|
|
18
|
+
|
|
19
|
+
- **Env file** — `<monorepo-root>/.env`, found by walking up from `process.cwd()` to a directory
|
|
20
|
+
holding `bun.lock`, or a `package.json` beside a `packages/` directory. Merged into `process.env`
|
|
21
|
+
once per process; a variable already set to a non-empty value wins. A missing file is not an error.
|
|
22
|
+
- **Gate** — `EnvGate`: `{ ok: true }` when every required variable is set, otherwise
|
|
23
|
+
`{ skip: true, reason }` naming the missing ones.
|
|
24
|
+
- **Gate map** — `makeGates({ name: [envKey, …] })`, a frozen record built once in a suite's
|
|
25
|
+
`tests/context.ts`; specs read it to choose `test` or `test.skip`.
|
|
26
|
+
- **Fixture** — a JSON file under the consuming package's `tests/`, read relative to it.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### Declare the suite's gates
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// tests/context.ts
|
|
34
|
+
import { makeGates } from '@owlmeans/test'
|
|
35
|
+
|
|
36
|
+
export const gates = makeGates({
|
|
37
|
+
openrouter: ['OPENROUTER_SECRET'],
|
|
38
|
+
anthropic: ['ANTHROPIC_SECRET'],
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Self-skip a spec
|
|
43
|
+
|
|
44
|
+
Bun picks `test` or `test.skip` synchronously, so decide from the gate at module scope:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// tests/inference.spec.ts
|
|
48
|
+
import { describe, expect, test } from 'bun:test'
|
|
49
|
+
import { isSkip } from '@owlmeans/test'
|
|
50
|
+
import { gates } from './context.js'
|
|
51
|
+
|
|
52
|
+
const gate = gates.openrouter
|
|
53
|
+
const it = isSkip(gate) ? test.skip : test
|
|
54
|
+
|
|
55
|
+
describe('llm — openrouter', () => {
|
|
56
|
+
it(`answers a prompt${isSkip(gate) ? ` (${gate.reason})` : ''}`, async () => {
|
|
57
|
+
/* exercise the real provider */
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Read the environment directly
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { envHelper } from '@owlmeans/test'
|
|
66
|
+
|
|
67
|
+
envHelper.loadEnv() // idempotent; `{ force: true }` re-reads
|
|
68
|
+
envHelper.loadEnv({ file: '/path/to/other/.env' }) // a workspace shaped differently
|
|
69
|
+
|
|
70
|
+
if (envHelper.hasEnv('MONGO_URL')) {
|
|
71
|
+
/* provision the optional service in tests/context.ts */
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const gate = envHelper.requireEnv(['SMTP_HOST', 'SMTP_PORT'])
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Load a fixture
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { loadFixture } from '@owlmeans/test'
|
|
81
|
+
|
|
82
|
+
const profile = loadFixture<ProfileRecord>('fixtures/profile.json') // <package>/tests/fixtures/profile.json
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## API
|
|
86
|
+
|
|
87
|
+
| Symbol | Kind | Purpose |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `envHelper` (`createEnvHelper()`) | helper | The env loader and gates below |
|
|
90
|
+
| `envHelper.loadEnv(opts?)` | member | Locate `<monorepo-root>/.env`, parse it, merge into `process.env` (idempotent; no-op for variables Bun already loaded via `--env-file`) |
|
|
91
|
+
| `envHelper.hasEnv(key)` | member | `loadEnv()`, then whether the variable is set and non-empty |
|
|
92
|
+
| `envHelper.requireEnv(keys)` | member | `{ ok: true }` or `{ skip: true, reason }` so callers can `test.skip(...)` when an integration variable is missing |
|
|
93
|
+
| `makeGates(spec)` | function | Frozen record of `{ <name>: EnvGate }` — one source of truth in `tests/context.ts` |
|
|
94
|
+
| `isSkip(gate)` | function | Type guard narrowing an `EnvGate` to `{ skip: true, reason }` |
|
|
95
|
+
| `loadFixture<T>(relPath)` | function | JSON fixture loader rooted at the consuming package's `tests/` |
|
|
96
|
+
|
|
97
|
+
| Type | Purpose |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `EnvGate`, `EnvOk`, `EnvSkip` | Gate outcome union and its branches |
|
|
100
|
+
| `GateSpec`, `Gates<S>` | `{ name: string[] }` input and the frozen gate map it produces |
|
|
101
|
+
| `LoadEnvOptions` | `{ force?, file? }` |
|
|
102
|
+
| `EnvHelper` | The helper's interface |
|
|
103
|
+
|
|
104
|
+
## Test layout
|
|
105
|
+
|
|
106
|
+
Tests are located at `packages/<pkg>/tests/`, named `*.spec.ts`, and run with `bun test ./tests`
|
|
107
|
+
from the package root. List `"./tests/**/*"` in the package `tsconfig.json` `exclude` so `tsc -b`
|
|
108
|
+
never compiles specs. One real context per package lives in `tests/context.ts`; specs import its
|
|
109
|
+
helper instead of building contexts themselves.
|
|
110
|
+
|
|
111
|
+
## Common pitfalls
|
|
112
|
+
|
|
113
|
+
- Awaiting anything before choosing `test` / `test.skip` — the choice must be a value already in hand.
|
|
114
|
+
- A missing variable must skip, never fail; do not throw from a gate.
|
|
115
|
+
- `loadFixture` resolves against `process.cwd()`, which `bun test` sets to the package root; running
|
|
116
|
+
specs from another directory breaks the path.
|
|
117
|
+
- A workspace without `bun.lock` and without a `packages/` directory beside its `package.json` is not
|
|
118
|
+
found by `loadEnv` — pass `file` explicitly.
|
|
119
|
+
- Do not add mocks here; auth mocks belong in `@owlmeans/test-auth`, and anything else is an
|
|
120
|
+
integration test.
|
|
121
|
+
|
|
122
|
+
## Related packages
|
|
123
|
+
|
|
124
|
+
- [`@owlmeans/test-auth`](../test-auth) — auth/authz mocks for category-B unit tests
|
|
125
|
+
- [`@owlmeans/test-integration`](../test-integration) — service gates, namespaces and cleanup for category-C tests
|
|
126
|
+
- [`@owlmeans/test-ui`](../test-ui) — Playwright-driven component acceptance for category D
|
|
127
|
+
- [`@owlmeans/context`](../context) — the real context a suite's `tests/context.ts` builds
|
|
12
128
|
|
|
13
129
|
<!-- owlmeans:agent-guidance:start -->
|
|
14
130
|
## Agent guidance
|
|
@@ -18,7 +134,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
18
134
|
your project's skill store (`.agents/skills/`):
|
|
19
135
|
|
|
20
136
|
```sh
|
|
21
|
-
npx @owlmeans/agent-skills@^0.1.18
|
|
137
|
+
npx @owlmeans/agent-skills@^0.1.18
|
|
22
138
|
```
|
|
23
139
|
|
|
24
140
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
2
|
+
"schemaVersion": 3,
|
|
3
3
|
"package": "@owlmeans/test",
|
|
4
|
-
"version": "0.1.18
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18",
|
|
5
|
+
"generatedAt": "2026-10-28T00:31:34.720Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,9 +8,17 @@ description: Decision matrix for which testing strategy to use per package in th
|
|
|
8
8
|
|
|
9
9
|
Every package falls into exactly one of four testing categories. The category drives which `@owlmeans/test*` package(s) the tests depend on, where tests live, and what mocking is allowed.
|
|
10
10
|
|
|
11
|
-
`@owlmeans/test` is the base every category builds on
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
`@owlmeans/test` is the base every category builds on; each other harness package adds one
|
|
12
|
+
category's helpers on top of it:
|
|
13
|
+
|
|
14
|
+
| Package | Main exports | Documented in |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `@owlmeans/test` | `loadEnv`, `requireEnv`, `hasEnv`, `makeGates`, `loadFixture` | `testing-unit` |
|
|
17
|
+
| `@owlmeans/test-auth` | `makeFixtureKeyPair`, `makeMemoryTrustedResource`, `makeMockGuard`, `withAuth`, `signMockEnvelope`, `makeBearer`, fixtures `SUPERUSER`/`USER`/`SERVICE` | `testing-auth-unit` |
|
|
18
|
+
| `@owlmeans/test-integration` | `mongoGate`, `redisGate`, `s3Gate`, `kubeGate`, `randomNamespace`, `registerCleanup`, `runCleanups` | `testing-integration` |
|
|
19
|
+
| `@owlmeans/test-ui` | `launchBrowser`, `closeBrowser`, `withPage`, `mountComponent`, starter `harness/index.html` — on the `playwright` library, not `@playwright/test` | `testing-ui` |
|
|
20
|
+
|
|
21
|
+
New auth mocks belong in `@owlmeans/test-auth`, never in a package's own `tests/`.
|
|
14
22
|
|
|
15
23
|
## Categories
|
|
16
24
|
|
|
@@ -23,13 +31,13 @@ Every package falls into exactly one of four testing categories. The category dr
|
|
|
23
31
|
|
|
24
32
|
## Package → category map
|
|
25
33
|
|
|
26
|
-
All
|
|
34
|
+
All 108 packages, one category each.
|
|
27
35
|
|
|
28
|
-
- **A** (
|
|
36
|
+
- **A** (56): `agent`, `agent-common`, `agent-skills`, `api`, `api-config`, `api-config-client`, `api-config-server`, `astro`, `basic-ids`, `bot`, `bot-agent`, `client-config`, `client-context`, `client-entrypoint`, `client-flow`, `client-job`, `client-resource`, `client-route`, `client-socket`, `common-inquiry`, `config`, `consent`, `context`, `create-app`, `dep-config`, `entrypoint`, `error`, `flow`, `i18n`, `iam`, `image-resource`, `llm-common`, `mailer`, `payment`, `queue`, `resource`, `route`, `router`, `server-auth-identity`, `server-bot`, `server-context`, `server-entrypoint`, `server-iam`, `server-job`, `server-route`, `server-config`, `server-socket`, `server-wl`, `socket`, `state`, `static-resource`, `storage-common`, `web-db`, `web-gtm`, `web-inquiry`, `web-router-react-router`
|
|
29
37
|
- **B** (20): `auth`, `auth-common`, `auth-otp`, `basic-envelope`, `basic-keys`, `client-auth`, `client-did`, `client-iam`, `client-payment`, `did`, `mui-oidc-rp`, `oidc`, `server-auth`, `server-auth-otp`, `server-oidc-provider`, `server-oidc-rp`, `web-auth`, `web-oidc-provider`, `web-oidc-rp`, `wled`
|
|
30
|
-
- **C** (
|
|
31
|
-
- **D** (
|
|
32
|
-
- **None** (
|
|
38
|
+
- **C** (16): `bot-postgres`, `kluster`, `llm` (live inference providers), `mailer-smtp`, `mongo`, `mongo-resource`, `planning-postgres`, `postgres`, `postgres-resource`, `redis`, `redis-queue`, `redis-resource`, `server-api`, `server-app`, `server-mailer-mailgun`, `storage-resource`
|
|
39
|
+
- **D** (12): `client`, `client-i18n`, `client-panel`, `client-wl`, `mui-panel`, `web-bot`, `web-client`, `web-consent`, `web-flow`, `web-panel`, `web-router`, `web-wl`
|
|
40
|
+
- **None** (5): `_tpl`, and the four harness packages themselves — `test`, `test-auth`, `test-integration`, `test-ui`
|
|
33
41
|
|
|
34
42
|
A **contract** package and its **backend** split across categories. `queue`, `resource`,
|
|
35
43
|
`storage-common` and `image-resource` are A — types, AJV schemas, error classes and declarations,
|
|
@@ -41,11 +49,16 @@ while one whose code issues real commands against the service is C even when it
|
|
|
41
49
|
`redis-resource` imports `ioredis` for types alone and drives the client the db service hands it;
|
|
42
50
|
`mongo-resource` carries its driver as a devDependency, for its specs only. Both are C.
|
|
43
51
|
|
|
52
|
+
`server-bot` is A although its platforms are external services: they are reached only through the
|
|
53
|
+
plugin seam, and its own API-faithful mock transport (a shipped feature, not a test mock) or a
|
|
54
|
+
spec's `fetch` answering a platform's documented envelopes stands in for them; its store is any
|
|
55
|
+
`Resource` (static resources in its specs). Its Postgres store, `bot-postgres`, is C.
|
|
56
|
+
|
|
44
57
|
`web-router` is D, not A: it drives the History API, so its routing behaviour is only observable in
|
|
45
58
|
a browser and its specs mount a real harness in chromium. A client-side package stays in A when it
|
|
46
59
|
ships no component at all — `client-config`, `client-context`, `client-entrypoint`, `client-flow`,
|
|
47
|
-
`client-job`, `client-resource`, `client-route`, `client-socket`, `web-db`, `web-gtm`
|
|
48
|
-
`web-router-react-router` are services, models and adapters, so `bun test` alone covers them.
|
|
60
|
+
`client-job`, `client-resource`, `client-route`, `client-socket`, `web-db`, `web-gtm`, `web-inquiry`
|
|
61
|
+
and `web-router-react-router` are services, models and adapters, so `bun test` alone covers them.
|
|
49
62
|
|
|
50
63
|
A new package takes the category its own behaviour implies: an external service it cannot fake ⇒ C,
|
|
51
64
|
a rendered React surface ⇒ D, an authenticated identity it must stand in for ⇒ B, otherwise A.
|
|
@@ -6,7 +6,7 @@ description: Category-A unit tests for OwlMeans Common packages — no mocks, re
|
|
|
6
6
|
|
|
7
7
|
# Unit Tests — Category A (no mocks)
|
|
8
8
|
|
|
9
|
-
**Install:** `"@owlmeans/test": "^0.1.18
|
|
9
|
+
**Install:** `"@owlmeans/test": "^0.1.18"` in `devDependencies`
|
|
10
10
|
|
|
11
11
|
Apply this skill when adding tests to packages in category A (see `testing-overview`). The list includes core abstractions (`context`, `config`, `error`, `entrypoint`, `route`, `resource`, …) and platform-agnostic services (`api`, `state`, `flow`, `i18n`, `client-flow`, `client-socket`, `client-job`, `server-route`, `server-context`, `web-db`, …).
|
|
12
12
|
|
package/build/env.d.ts
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export interface EnvSkip {
|
|
5
|
-
skip: true;
|
|
6
|
-
reason: string;
|
|
7
|
-
}
|
|
8
|
-
export type EnvGate = EnvOk | EnvSkip;
|
|
9
|
-
export declare const loadEnv: (opts?: {
|
|
10
|
-
force?: boolean;
|
|
11
|
-
file?: string;
|
|
12
|
-
}) => void;
|
|
13
|
-
export declare const hasEnv: (key: string) => boolean;
|
|
14
|
-
export declare const requireEnv: (keys: string[]) => EnvGate;
|
|
1
|
+
import type { EnvHelper } from './types.js';
|
|
2
|
+
export declare const createEnvHelper: () => EnvHelper;
|
|
3
|
+
export declare const envHelper: EnvHelper;
|
|
15
4
|
//# sourceMappingURL=env.d.ts.map
|
package/build/env.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAW,SAAS,EAAkB,MAAM,YAAY,CAAA;AAKpE,eAAO,MAAM,eAAe,QAAO,SA4DlC,CAAA;AAED,eAAO,MAAM,SAAS,WAAoB,CAAA"}
|
package/build/env.js
CHANGED
|
@@ -1,63 +1,68 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
|
+
// Process-wide on purpose: the file is merged into `process.env`, which every helper instance shares.
|
|
3
4
|
let loaded = false;
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
export const createEnvHelper = () => {
|
|
6
|
+
const findRoot = (start) => {
|
|
7
|
+
let dir = start;
|
|
8
|
+
for (let i = 0; i < 10; i++) {
|
|
9
|
+
if (existsSync(resolve(dir, 'bun.lock')) || existsSync(resolve(dir, 'package.json')) && existsSync(resolve(dir, 'packages'))) {
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
const parent = dirname(dir);
|
|
13
|
+
if (parent === dir)
|
|
14
|
+
return null;
|
|
15
|
+
dir = parent;
|
|
9
16
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let val = line.slice(eq + 1).trim();
|
|
28
|
-
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
29
|
-
val = val.slice(1, -1);
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
const parseEnv = (text) => {
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
22
|
+
const line = raw.trim();
|
|
23
|
+
if (line === '' || line.startsWith('#'))
|
|
24
|
+
continue;
|
|
25
|
+
const eq = line.indexOf('=');
|
|
26
|
+
if (eq < 1)
|
|
27
|
+
continue;
|
|
28
|
+
const key = line.slice(0, eq).trim();
|
|
29
|
+
let val = line.slice(eq + 1).trim();
|
|
30
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
31
|
+
val = val.slice(1, -1);
|
|
32
|
+
}
|
|
33
|
+
out[key] = val;
|
|
30
34
|
}
|
|
31
|
-
out
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
process.env[k] = v;
|
|
35
|
+
return out;
|
|
36
|
+
};
|
|
37
|
+
const loadEnv = (opts) => {
|
|
38
|
+
if (loaded && opts?.force !== true)
|
|
39
|
+
return;
|
|
40
|
+
loaded = true;
|
|
41
|
+
const root = findRoot(process.cwd());
|
|
42
|
+
const envPath = opts?.file ?? (root != null ? resolve(root, '.env') : null);
|
|
43
|
+
if (envPath == null || !existsSync(envPath))
|
|
44
|
+
return;
|
|
45
|
+
const text = readFileSync(envPath, 'utf8');
|
|
46
|
+
const parsed = parseEnv(text);
|
|
47
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
48
|
+
if (process.env[k] == null || process.env[k] === '') {
|
|
49
|
+
process.env[k] = v;
|
|
50
|
+
}
|
|
48
51
|
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
|
|
52
|
+
};
|
|
53
|
+
const hasEnv = (key) => {
|
|
54
|
+
loadEnv();
|
|
55
|
+
const v = process.env[key];
|
|
56
|
+
return v != null && v !== '';
|
|
57
|
+
};
|
|
58
|
+
const requireEnv = (keys) => {
|
|
59
|
+
loadEnv();
|
|
60
|
+
const missing = keys.filter(k => !hasEnv(k));
|
|
61
|
+
if (missing.length === 0)
|
|
62
|
+
return { ok: true };
|
|
63
|
+
return { skip: true, reason: `Missing env: ${missing.join(', ')}` };
|
|
64
|
+
};
|
|
65
|
+
return { loadEnv, hasEnv, requireEnv };
|
|
62
66
|
};
|
|
67
|
+
export const envHelper = createEnvHelper();
|
|
63
68
|
//# sourceMappingURL=env.js.map
|
package/build/env.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"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"}
|
package/build/gates.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type { EnvGate } from './
|
|
2
|
-
export type GateSpec = Record<string, string[]>;
|
|
3
|
-
export type Gates<S extends GateSpec> = {
|
|
4
|
-
readonly [K in keyof S]: EnvGate;
|
|
5
|
-
};
|
|
1
|
+
import type { EnvGate, GateSpec, Gates } from './types.js';
|
|
6
2
|
/**
|
|
7
3
|
* Build a frozen record of gates from a spec of `{ name: [envKey, ...] }`.
|
|
8
4
|
* Use in `tests/context.ts` so a single source of truth describes which
|
package/build/gates.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAE1D;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,QAAQ,QAAQ,CAAC,KAAG,KAAK,CAAC,CAAC,CAM9D,CAAA;AAED,eAAO,MAAM,MAAM,SAAU,OAAO,KAAG,IAAI,IAAI;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAClC,CAAA"}
|
package/build/gates.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { envHelper } from './env.js';
|
|
2
2
|
/**
|
|
3
3
|
* Build a frozen record of gates from a spec of `{ name: [envKey, ...] }`.
|
|
4
4
|
* Use in `tests/context.ts` so a single source of truth describes which
|
|
@@ -8,7 +8,7 @@ import { requireEnv } from './env.js';
|
|
|
8
8
|
export const makeGates = (spec) => {
|
|
9
9
|
const out = {};
|
|
10
10
|
for (const [name, keys] of Object.entries(spec)) {
|
|
11
|
-
out[name] = requireEnv(keys);
|
|
11
|
+
out[name] = envHelper.requireEnv(keys);
|
|
12
12
|
}
|
|
13
13
|
return Object.freeze(out);
|
|
14
14
|
};
|
package/build/gates.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gates.js","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"gates.js","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAGpC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAqB,IAAO,EAAY,EAAE;IACjE,MAAM,GAAG,GAAG,EAA6B,CAAA;IACzC,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,GAAG,CAAC,IAAI,CAAC,GAAG,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;IACxC,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,CAAa,CAAA;AACvC,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,IAAa,EAA0C,EAAE,CAC7E,IAAwB,CAAC,IAAI,KAAK,IAAI,CAAA"}
|
package/build/index.d.ts
CHANGED
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAA;AACxB,cAAc,YAAY,CAAA;AAC1B,cAAc,eAAe,CAAA;AAC7B,mBAAmB,YAAY,CAAA"}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface EnvOk {
|
|
2
|
+
ok: true;
|
|
3
|
+
}
|
|
4
|
+
export interface EnvSkip {
|
|
5
|
+
skip: true;
|
|
6
|
+
reason: string;
|
|
7
|
+
}
|
|
8
|
+
export type EnvGate = EnvOk | EnvSkip;
|
|
9
|
+
export type GateSpec = Record<string, string[]>;
|
|
10
|
+
export type Gates<S extends GateSpec> = {
|
|
11
|
+
readonly [K in keyof S]: EnvGate;
|
|
12
|
+
};
|
|
13
|
+
export interface LoadEnvOptions {
|
|
14
|
+
/** Re-read the file even when it was already loaded once in this process. */
|
|
15
|
+
force?: boolean;
|
|
16
|
+
/** The file to read instead of `<monorepo-root>/.env`. */
|
|
17
|
+
file?: string;
|
|
18
|
+
}
|
|
19
|
+
/** The test environment: `<monorepo-root>/.env` merged into `process.env`, and gates over it. */
|
|
20
|
+
export interface EnvHelper {
|
|
21
|
+
/**
|
|
22
|
+
* Locate `<monorepo-root>/.env`, parse it and merge it into `process.env` — a variable that is
|
|
23
|
+
* already set and non-empty wins. Idempotent per process unless `force` is passed.
|
|
24
|
+
*/
|
|
25
|
+
loadEnv: (opts?: LoadEnvOptions) => void;
|
|
26
|
+
/** Whether a variable is set to a non-empty value (after {@link EnvHelper.loadEnv}). */
|
|
27
|
+
hasEnv: (key: string) => boolean;
|
|
28
|
+
/** `{ ok: true }` when every key is set, otherwise `{ skip: true, reason }` naming the missing ones. */
|
|
29
|
+
requireEnv: (keys: string[]) => EnvGate;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,IAAI,CAAA;CACT;AAED,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,IAAI,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;CACf;AAGD,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,OAAO,CAAA;AAGrC,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;AAG/C,MAAM,MAAM,KAAK,CAAC,CAAC,SAAS,QAAQ,IAAI;IAAE,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,OAAO;CAAE,CAAA;AAE5E,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,iGAAiG;AACjG,MAAM,WAAW,SAAS;IACxB;;;OAGG;IACH,OAAO,EAAE,CAAC,IAAI,CAAC,EAAE,cAAc,KAAK,IAAI,CAAA;IACxC,wFAAwF;IACxF,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;IAChC,wGAAwG;IACxG,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAA;CACxC"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/test",
|
|
3
|
-
"version": "0.1.18
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@owlmeans/context": "^0.1.18
|
|
24
|
+
"@owlmeans/context": "^0.1.18"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@owlmeans/dep-config": "^0.1.18
|
|
28
|
-
"@types/bun": "^1.4.
|
|
27
|
+
"@owlmeans/dep-config": "^0.1.18",
|
|
28
|
+
"@types/bun": "^1.4.2",
|
|
29
29
|
"nodemon": "^3.1.14",
|
|
30
30
|
"typescript": "^7.0.2"
|
|
31
31
|
},
|
package/src/env.ts
CHANGED
|
@@ -1,73 +1,70 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { dirname, resolve } from 'node:path'
|
|
3
|
+
import type { EnvGate, EnvHelper, LoadEnvOptions } from './types.js'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
ok: true
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface EnvSkip {
|
|
9
|
-
skip: true
|
|
10
|
-
reason: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export type EnvGate = EnvOk | EnvSkip
|
|
14
|
-
|
|
5
|
+
// Process-wide on purpose: the file is merged into `process.env`, which every helper instance shares.
|
|
15
6
|
let loaded = false
|
|
16
7
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
export const createEnvHelper = (): EnvHelper => {
|
|
9
|
+
const findRoot = (start: string): string | null => {
|
|
10
|
+
let dir = start
|
|
11
|
+
for (let i = 0; i < 10; i++) {
|
|
12
|
+
if (existsSync(resolve(dir, 'bun.lock')) || existsSync(resolve(dir, 'package.json')) && existsSync(resolve(dir, 'packages'))) {
|
|
13
|
+
return dir
|
|
14
|
+
}
|
|
15
|
+
const parent = dirname(dir)
|
|
16
|
+
if (parent === dir) return null
|
|
17
|
+
dir = parent
|
|
22
18
|
}
|
|
23
|
-
|
|
24
|
-
if (parent === dir) return null
|
|
25
|
-
dir = parent
|
|
19
|
+
return null
|
|
26
20
|
}
|
|
27
|
-
return null
|
|
28
|
-
}
|
|
29
21
|
|
|
30
|
-
const parseEnv = (text: string): Record<string, string> => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
22
|
+
const parseEnv = (text: string): Record<string, string> => {
|
|
23
|
+
const out: Record<string, string> = {}
|
|
24
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
25
|
+
const line = raw.trim()
|
|
26
|
+
if (line === '' || line.startsWith('#')) continue
|
|
27
|
+
const eq = line.indexOf('=')
|
|
28
|
+
if (eq < 1) continue
|
|
29
|
+
const key = line.slice(0, eq).trim()
|
|
30
|
+
let val = line.slice(eq + 1).trim()
|
|
31
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
32
|
+
val = val.slice(1, -1)
|
|
33
|
+
}
|
|
34
|
+
out[key] = val
|
|
41
35
|
}
|
|
42
|
-
out
|
|
36
|
+
return out
|
|
43
37
|
}
|
|
44
|
-
return out
|
|
45
|
-
}
|
|
46
38
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
39
|
+
const loadEnv = (opts?: LoadEnvOptions): void => {
|
|
40
|
+
if (loaded && opts?.force !== true) return
|
|
41
|
+
loaded = true
|
|
42
|
+
const root = findRoot(process.cwd())
|
|
43
|
+
const envPath = opts?.file ?? (root != null ? resolve(root, '.env') : null)
|
|
44
|
+
if (envPath == null || !existsSync(envPath)) return
|
|
45
|
+
const text = readFileSync(envPath, 'utf8')
|
|
46
|
+
const parsed = parseEnv(text)
|
|
47
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
48
|
+
if (process.env[k] == null || process.env[k] === '') {
|
|
49
|
+
process.env[k] = v
|
|
50
|
+
}
|
|
58
51
|
}
|
|
59
52
|
}
|
|
60
|
-
}
|
|
61
53
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
54
|
+
const hasEnv = (key: string): boolean => {
|
|
55
|
+
loadEnv()
|
|
56
|
+
const v = process.env[key]
|
|
57
|
+
return v != null && v !== ''
|
|
58
|
+
}
|
|
67
59
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
60
|
+
const requireEnv = (keys: string[]): EnvGate => {
|
|
61
|
+
loadEnv()
|
|
62
|
+
const missing = keys.filter(k => !hasEnv(k))
|
|
63
|
+
if (missing.length === 0) return { ok: true }
|
|
64
|
+
return { skip: true, reason: `Missing env: ${missing.join(', ')}` }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { loadEnv, hasEnv, requireEnv }
|
|
73
68
|
}
|
|
69
|
+
|
|
70
|
+
export const envHelper = createEnvHelper()
|
package/src/gates.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { EnvGate } from './
|
|
3
|
-
|
|
4
|
-
export type GateSpec = Record<string, string[]>
|
|
5
|
-
|
|
6
|
-
export type Gates<S extends GateSpec> = { readonly [K in keyof S]: EnvGate }
|
|
1
|
+
import { envHelper } from './env.js'
|
|
2
|
+
import type { EnvGate, GateSpec, Gates } from './types.js'
|
|
7
3
|
|
|
8
4
|
/**
|
|
9
5
|
* Build a frozen record of gates from a spec of `{ name: [envKey, ...] }`.
|
|
@@ -14,7 +10,7 @@ export type Gates<S extends GateSpec> = { readonly [K in keyof S]: EnvGate }
|
|
|
14
10
|
export const makeGates = <S extends GateSpec>(spec: S): Gates<S> => {
|
|
15
11
|
const out = {} as Record<string, EnvGate>
|
|
16
12
|
for (const [name, keys] of Object.entries(spec)) {
|
|
17
|
-
out[name] = requireEnv(keys)
|
|
13
|
+
out[name] = envHelper.requireEnv(keys)
|
|
18
14
|
}
|
|
19
15
|
return Object.freeze(out) as Gates<S>
|
|
20
16
|
}
|
package/src/index.ts
CHANGED
package/src/types.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface EnvOk {
|
|
2
|
+
ok: true
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface EnvSkip {
|
|
6
|
+
skip: true
|
|
7
|
+
reason: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Kept as a type: a union of the two gate outcomes.
|
|
11
|
+
export type EnvGate = EnvOk | EnvSkip
|
|
12
|
+
|
|
13
|
+
// Kept as a type: a record over arbitrary gate names (an implicit index signature).
|
|
14
|
+
export type GateSpec = Record<string, string[]>
|
|
15
|
+
|
|
16
|
+
// Kept as a type: mapped over the spec's gate names.
|
|
17
|
+
export type Gates<S extends GateSpec> = { readonly [K in keyof S]: EnvGate }
|
|
18
|
+
|
|
19
|
+
export interface LoadEnvOptions {
|
|
20
|
+
/** Re-read the file even when it was already loaded once in this process. */
|
|
21
|
+
force?: boolean
|
|
22
|
+
/** The file to read instead of `<monorepo-root>/.env`. */
|
|
23
|
+
file?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The test environment: `<monorepo-root>/.env` merged into `process.env`, and gates over it. */
|
|
27
|
+
export interface EnvHelper {
|
|
28
|
+
/**
|
|
29
|
+
* Locate `<monorepo-root>/.env`, parse it and merge it into `process.env` — a variable that is
|
|
30
|
+
* already set and non-empty wins. Idempotent per process unless `force` is passed.
|
|
31
|
+
*/
|
|
32
|
+
loadEnv: (opts?: LoadEnvOptions) => void
|
|
33
|
+
/** Whether a variable is set to a non-empty value (after {@link EnvHelper.loadEnv}). */
|
|
34
|
+
hasEnv: (key: string) => boolean
|
|
35
|
+
/** `{ ok: true }` when every key is set, otherwise `{ skip: true, reason }` naming the missing ones. */
|
|
36
|
+
requireEnv: (keys: string[]) => EnvGate
|
|
37
|
+
}
|