@lunora/testing 1.0.0-alpha.3 → 1.0.0-alpha.30

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/dist/index.d.mts CHANGED
@@ -141,6 +141,19 @@ type FunctionRegistry = Record<string, RegisteredAction<any, any> | RegisteredMu
141
141
  * clearly-throwing stubs for unsupported surfaces.
142
142
  */
143
143
  interface LunoraTestOptions {
144
+ /**
145
+ * Injectable `ctx.env` for every context (query / mutation / action). When
146
+ * provided, handlers that read `ctx.env.SOME_KEY` (the validated `defineEnv`
147
+ * surface) see this object. Left unset it stays `undefined` — matching the
148
+ * optional `ctx.env?` field, so graceful `ctx.env?.KEY` access still yields
149
+ * `undefined` rather than throwing. Not a throwing stub for exactly that
150
+ * reason: `env` is designed to be legitimately absent.
151
+ * @example
152
+ * ```ts
153
+ * const t = lunoraTest(schema, { env: { STRIPE_KEY: "sk_test_…" } });
154
+ * ```
155
+ */
156
+ env?: Record<string, unknown>;
144
157
  /**
145
158
  * Injectable `fetch` implementation for action contexts. When provided,
146
159
  * `ctx.fetch` in every `action` (and `withIdentity` views) resolves to this
@@ -171,6 +184,16 @@ interface LunoraTestOptions {
171
184
  * ```
172
185
  */
173
186
  functions?: FunctionRegistry;
187
+ /**
188
+ * Fixed value for `ctx.now` (epoch ms) in every context. Production captures
189
+ * `Date.now()` once per execution; in tests a fixed `now` makes time-dependent
190
+ * handlers deterministic. Defaults to the wall clock at harness creation.
191
+ * @example
192
+ * ```ts
193
+ * const t = lunoraTest(schema, { now: 1_700_000_000_000 });
194
+ * ```
195
+ */
196
+ now?: number;
174
197
  }
175
198
  /**
176
199
  * The in-memory test harness returned by {@link lunoraTest}. Mirrors the first
@@ -253,6 +276,8 @@ interface TestHarness {
253
276
  *
254
277
  * **v1 surfaces now supported:**
255
278
  *
279
+ * - `ctx.env` (all contexts): inject the validated env via `options.env`; unset it
280
+ * stays `undefined`, matching the optional `ctx.env?` field.
256
281
  * - `ctx.fetch` (actions): inject a custom `fetch` via `options.fetch`.
257
282
  * - `ctx.scheduler` (mutations + actions): fully functional fake with virtual clock;
258
283
  * control via `harness.scheduler.advance(ms)` / `runPending()` / `list()`.
package/dist/index.d.ts CHANGED
@@ -141,6 +141,19 @@ type FunctionRegistry = Record<string, RegisteredAction<any, any> | RegisteredMu
141
141
  * clearly-throwing stubs for unsupported surfaces.
142
142
  */
143
143
  interface LunoraTestOptions {
144
+ /**
145
+ * Injectable `ctx.env` for every context (query / mutation / action). When
146
+ * provided, handlers that read `ctx.env.SOME_KEY` (the validated `defineEnv`
147
+ * surface) see this object. Left unset it stays `undefined` — matching the
148
+ * optional `ctx.env?` field, so graceful `ctx.env?.KEY` access still yields
149
+ * `undefined` rather than throwing. Not a throwing stub for exactly that
150
+ * reason: `env` is designed to be legitimately absent.
151
+ * @example
152
+ * ```ts
153
+ * const t = lunoraTest(schema, { env: { STRIPE_KEY: "sk_test_…" } });
154
+ * ```
155
+ */
156
+ env?: Record<string, unknown>;
144
157
  /**
145
158
  * Injectable `fetch` implementation for action contexts. When provided,
146
159
  * `ctx.fetch` in every `action` (and `withIdentity` views) resolves to this
@@ -171,6 +184,16 @@ interface LunoraTestOptions {
171
184
  * ```
172
185
  */
173
186
  functions?: FunctionRegistry;
187
+ /**
188
+ * Fixed value for `ctx.now` (epoch ms) in every context. Production captures
189
+ * `Date.now()` once per execution; in tests a fixed `now` makes time-dependent
190
+ * handlers deterministic. Defaults to the wall clock at harness creation.
191
+ * @example
192
+ * ```ts
193
+ * const t = lunoraTest(schema, { now: 1_700_000_000_000 });
194
+ * ```
195
+ */
196
+ now?: number;
174
197
  }
175
198
  /**
176
199
  * The in-memory test harness returned by {@link lunoraTest}. Mirrors the first
@@ -253,6 +276,8 @@ interface TestHarness {
253
276
  *
254
277
  * **v1 surfaces now supported:**
255
278
  *
279
+ * - `ctx.env` (all contexts): inject the validated env via `options.env`; unset it
280
+ * stays `undefined`, matching the optional `ctx.env?` field.
256
281
  * - `ctx.fetch` (actions): inject a custom `fetch` via `options.fetch`.
257
282
  * - `ctx.scheduler` (mutations + actions): fully functional fake with virtual clock;
258
283
  * control via `harness.scheduler.advance(ms)` / `runPending()` / `list()`.
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- export { lunoraTest } from './packem_shared/lunoraTest-BUg4Ebv8.mjs';
1
+ export { lunoraTest } from './packem_shared/lunoraTest-DO9gV_fa.mjs';
2
2
  export { extractLink, listCapturedMail, waitForMail } from '@lunora/mail/testing';
@@ -1,4 +1,5 @@
1
1
  import { runShardMigrations, createShardCtxDb } from '@lunora/do';
2
+ import { LunoraError } from '@lunora/errors';
2
3
  import { DatabaseSync } from 'node:sqlite';
3
4
 
4
5
  const createFakeScheduler = (getDispatch, getMutationContext, getFunctionRegistry) => {
@@ -100,7 +101,7 @@ const createSqlExec = () => {
100
101
  return {
101
102
  one() {
102
103
  if (rows.length !== 1) {
103
- throw new Error(`expected exactly one row, received ${String(rows.length)}`);
104
+ throw new LunoraError("INTERNAL", `expected exactly one row, received ${String(rows.length)}`);
104
105
  }
105
106
  const [only] = rows;
106
107
  return only;
@@ -138,7 +139,7 @@ const registeredFunctionKind = (value) => {
138
139
  };
139
140
  const registeredFunctionVisibility = (value) => typeof value === "object" && value !== null && value.visibility === "internal" ? "internal" : "public";
140
141
  const unavailable = (surface) => {
141
- throw new Error(`ctx.${surface} is not available in the in-memory @lunora/testing harness (v1)`);
142
+ throw new LunoraError("INTERNAL", `ctx.${surface} is not available in the in-memory @lunora/testing harness (v1)`);
142
143
  };
143
144
  const stubProxy = (surface) => /* @__PURE__ */ new Proxy((..._args) => unavailable(surface), {
144
145
  apply: () => unavailable(surface),
@@ -286,18 +287,25 @@ const lunoraTest = (schema, options) => {
286
287
  const { controls: schedulerControls, scheduler: fakeScheduler } = createFakeScheduler(
287
288
  () => {
288
289
  if (scheduledDispatchRef === void 0) {
289
- throw new Error("[fake-scheduler] dispatch not yet available — scheduler.advance called before harness construction completed");
290
+ throw new LunoraError(
291
+ "INTERNAL",
292
+ "[fake-scheduler] dispatch not yet available — scheduler.advance called before harness construction completed"
293
+ );
290
294
  }
291
295
  return scheduledDispatchRef;
292
296
  },
293
297
  () => {
294
298
  if (mutationContextRef === void 0) {
295
- throw new Error("[fake-scheduler] mutationContext not yet available — scheduler.advance called before harness construction completed");
299
+ throw new LunoraError(
300
+ "INTERNAL",
301
+ "[fake-scheduler] mutationContext not yet available — scheduler.advance called before harness construction completed"
302
+ );
296
303
  }
297
304
  return mutationContextRef;
298
305
  },
299
306
  () => functionRegistryMap
300
307
  );
308
+ const harnessNow = options?.now ?? Date.now();
301
309
  const makeHarness = (identity) => {
302
310
  const auth = {
303
311
  // eslint-disable-next-line unicorn/no-null -- AuthState.getIdentity's anonymous sentinel is `null` (mirrors a decoded JWT being absent)
@@ -308,21 +316,27 @@ const lunoraTest = (schema, options) => {
308
316
  const queryContext = {
309
317
  auth,
310
318
  db: database,
319
+ env: options?.env,
311
320
  log: noopLog,
321
+ now: harnessNow,
312
322
  // eslint-disable-next-line @typescript-eslint/no-use-before-define -- lazy closure: `runInternal` is invoked only when a handler calls ctx.runQuery, after construction completes
313
323
  runQuery: (reference, args) => runInternal("query", reference, queryContext, args),
324
+ secrets: stubProxy("secrets"),
314
325
  storage: stubProxy("storage"),
315
326
  vectors: stubProxy("vectors")
316
327
  };
317
328
  const mutationContext = {
318
329
  auth,
319
330
  db: database,
331
+ env: options?.env,
320
332
  log: noopLog,
333
+ now: harnessNow,
321
334
  // eslint-disable-next-line @typescript-eslint/no-use-before-define -- lazy closure: `runInternal` is invoked only when a handler calls ctx.runMutation, after construction completes
322
335
  runMutation: (reference, args) => runInternal("mutation", reference, mutationContext, args),
323
336
  // eslint-disable-next-line @typescript-eslint/no-use-before-define -- lazy closure: `runInternal` is invoked only when a handler calls ctx.runQuery, after construction completes
324
337
  runQuery: (reference, args) => runInternal("query", reference, queryContext, args),
325
338
  scheduler: fakeScheduler,
339
+ secrets: stubProxy("secrets"),
326
340
  storage: stubProxy("storage"),
327
341
  vectors: stubProxy("vectors"),
328
342
  workflows: stubProxy("workflows")
@@ -331,9 +345,11 @@ const lunoraTest = (schema, options) => {
331
345
  const actionContext = {
332
346
  auth,
333
347
  db: database,
348
+ env: options?.env,
334
349
  // Use the injected fetch when provided; fall back to the v1 stub otherwise.
335
350
  fetch: options?.fetch ?? stubProxy("fetch"),
336
351
  log: noopLog,
352
+ now: harnessNow,
337
353
  // eslint-disable-next-line @typescript-eslint/no-use-before-define -- lazy closure: `runInternal` is invoked only when a handler calls ctx.runAction, after construction completes
338
354
  runAction: (reference, args) => runInternal("action", reference, actionContext, args),
339
355
  // eslint-disable-next-line @typescript-eslint/no-use-before-define -- lazy closure: `runInternal` is invoked only when a handler calls ctx.runMutation, after construction completes
@@ -341,6 +357,7 @@ const lunoraTest = (schema, options) => {
341
357
  // eslint-disable-next-line @typescript-eslint/no-use-before-define -- lazy closure: `runInternal` is invoked only when a handler calls ctx.runQuery, after construction completes
342
358
  runQuery: (reference, args) => runInternal("query", reference, queryContext, args),
343
359
  scheduler: fakeScheduler,
360
+ secrets: stubProxy("secrets"),
344
361
  storage: stubProxy("storage"),
345
362
  vectors: stubProxy("vectors"),
346
363
  workflows: stubProxy("workflows")
@@ -348,10 +365,11 @@ const lunoraTest = (schema, options) => {
348
365
  const runRegistered = (expected, reference, context, args, allowInternal) => {
349
366
  const kind = registeredFunctionKind(reference);
350
367
  if (kind !== expected) {
351
- throw new Error(`expected a registered ${expected}, received a ${kind ?? "non-function"} reference`);
368
+ throw new LunoraError("INTERNAL", `expected a registered ${expected}, received a ${kind ?? "non-function"} reference`);
352
369
  }
353
370
  if (!allowInternal && registeredFunctionVisibility(reference) === "internal") {
354
- throw new Error(
371
+ throw new LunoraError(
372
+ "INTERNAL",
355
373
  `"${expected}" is an internal function — it is unreachable from the external RPC boundary in production. Call it through ctx.run${expected.charAt(0).toUpperCase()}${expected.slice(1)} from another function instead.`
356
374
  );
357
375
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/testing",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.30",
4
4
  "description": "Testing toolkit for Lunora: an in-memory harness for queries, mutations, and actions",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -25,7 +25,7 @@
25
25
  "directory": "packages/testing"
26
26
  },
27
27
  "files": [
28
- "dist",
28
+ "./dist",
29
29
  "README.md",
30
30
  "LICENSE.md",
31
31
  "__assets__"
@@ -46,9 +46,10 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@lunora/do": "1.0.0-alpha.3",
50
- "@lunora/mail": "1.0.0-alpha.1",
51
- "@lunora/server": "1.0.0-alpha.1"
49
+ "@lunora/do": "1.0.0-alpha.23",
50
+ "@lunora/errors": "1.0.0-alpha.1",
51
+ "@lunora/mail": "1.0.0-alpha.8",
52
+ "@lunora/server": "1.0.0-alpha.16"
52
53
  },
53
54
  "engines": {
54
55
  "node": "^22.15.0 || >=24.11.0"