@lunora/testing 1.0.0-alpha.4 → 1.0.0-alpha.41
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/LICENSE.md +6 -0
- package/README.md +12 -0
- package/__assets__/package-og.svg +1 -1
- package/dist/index.d.mts +208 -1
- package/dist/index.d.ts +208 -1
- package/dist/index.mjs +3 -1
- package/dist/packem_shared/agentHarness-GTPhWdQ8.mjs +144 -0
- package/dist/packem_shared/containsScorer-DGwtvUNp.mjs +94 -0
- package/dist/packem_shared/{lunoraTest-BUg4Ebv8.mjs → lunoraTest-DbzbUAGQ.mjs} +109 -50
- package/package.json +7 -5
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
|
|
3
|
+
const LEADING_SCORE = /^\s*(-?\d+(?:\.\d+)?)/u;
|
|
4
|
+
const clamp01 = (value) => Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0;
|
|
5
|
+
const normalizeScore = (result) => {
|
|
6
|
+
if (typeof result === "number") {
|
|
7
|
+
return { score: clamp01(result) };
|
|
8
|
+
}
|
|
9
|
+
return { score: clamp01(result.score), ...result.reason === void 0 ? {} : { reason: result.reason } };
|
|
10
|
+
};
|
|
11
|
+
const mean = (values) => values.length === 0 ? 0 : values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
12
|
+
const containsScorer = (needle, options = {}) => {
|
|
13
|
+
return {
|
|
14
|
+
name: `contains:${needle}`,
|
|
15
|
+
score: ({ output }) => {
|
|
16
|
+
const haystack = options.caseSensitive ? output : output.toLowerCase();
|
|
17
|
+
const target = options.caseSensitive ? needle : needle.toLowerCase();
|
|
18
|
+
return haystack.includes(target) ? 1 : 0;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
const regexScorer = (pattern, name = "regex") => {
|
|
23
|
+
return { name, score: ({ output }) => pattern.test(output) ? 1 : 0 };
|
|
24
|
+
};
|
|
25
|
+
const exactMatchScorer = () => {
|
|
26
|
+
return {
|
|
27
|
+
name: "exact-match",
|
|
28
|
+
score: ({ expected, output }) => output.trim() === expected?.trim() ? 1 : 0
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
const keywordScorer = (keywords) => {
|
|
32
|
+
if (keywords.length === 0) {
|
|
33
|
+
throw new LunoraError("BAD_REQUEST", "@lunora/testing: keywordScorer requires at least one keyword");
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
name: "keyword-coverage",
|
|
37
|
+
score: ({ output }) => {
|
|
38
|
+
const lower = output.toLowerCase();
|
|
39
|
+
const hits = keywords.filter((keyword) => lower.includes(keyword.toLowerCase())).length;
|
|
40
|
+
return { reason: `${String(hits)}/${String(keywords.length)} keywords present`, score: hits / keywords.length };
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const buildJudgePrompt = (criteria, sample) => [
|
|
45
|
+
`Rate the ASSISTANT OUTPUT against this criterion: ${criteria}`,
|
|
46
|
+
"Respond with a single number from 0 (fails) to 1 (fully meets), then a dash and a one-line reason.",
|
|
47
|
+
...sample.input === void 0 ? [] : ["", `Input: ${sample.input}`],
|
|
48
|
+
...sample.expected === void 0 ? [] : ["", `Reference answer: ${sample.expected}`],
|
|
49
|
+
"",
|
|
50
|
+
`Assistant output: ${sample.output}`
|
|
51
|
+
].join("\n");
|
|
52
|
+
const parseJudgeScore = (raw) => {
|
|
53
|
+
const match = LEADING_SCORE.exec(raw);
|
|
54
|
+
return { reason: raw.trim(), score: match ? clamp01(Number(match[1])) : 0 };
|
|
55
|
+
};
|
|
56
|
+
const llmScorer = (options) => {
|
|
57
|
+
return {
|
|
58
|
+
name: options.name ?? "llm-judge",
|
|
59
|
+
score: async (sample) => parseJudgeScore(await options.judge(buildJudgePrompt(options.criteria, sample)))
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
const scoreSample = async (sample, scorers) => {
|
|
63
|
+
const results = await Promise.all(
|
|
64
|
+
scorers.map(async (scorer) => {
|
|
65
|
+
return { name: scorer.name, result: normalizeScore(await scorer.score(sample)) };
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
const scores = {};
|
|
69
|
+
const seen = /* @__PURE__ */ new Map();
|
|
70
|
+
for (const { name, result } of results) {
|
|
71
|
+
const priorCount = seen.get(name) ?? 0;
|
|
72
|
+
seen.set(name, priorCount + 1);
|
|
73
|
+
scores[priorCount === 0 ? name : `${name}#${String(priorCount + 1)}`] = result;
|
|
74
|
+
}
|
|
75
|
+
return { average: mean(results.map(({ result }) => result.score)), scores };
|
|
76
|
+
};
|
|
77
|
+
const evaluate = async (cases, produce, scorers) => {
|
|
78
|
+
const items = await Promise.all(
|
|
79
|
+
cases.map(async (testCase) => {
|
|
80
|
+
const output = await produce(testCase.input);
|
|
81
|
+
const sample = {
|
|
82
|
+
input: testCase.input,
|
|
83
|
+
output,
|
|
84
|
+
...testCase.expected === void 0 ? {} : { expected: testCase.expected },
|
|
85
|
+
...testCase.metadata === void 0 ? {} : { metadata: testCase.metadata }
|
|
86
|
+
};
|
|
87
|
+
const { average, scores } = await scoreSample(sample, scorers);
|
|
88
|
+
return { average, input: testCase.input, output, scores };
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
return { average: mean(items.map((item) => item.average)), items };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export { containsScorer, evaluate, exactMatchScorer, keywordScorer, llmScorer, regexScorer, scoreSample };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { runShardMigrations, createShardCtxDb } from '@lunora/do';
|
|
2
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
3
|
import { DatabaseSync } from 'node:sqlite';
|
|
3
4
|
|
|
4
|
-
const createFakeScheduler = (getDispatch, getMutationContext, getFunctionRegistry) => {
|
|
5
|
-
let nowMs =
|
|
5
|
+
const createFakeScheduler = (getDispatch, getMutationContext, getFunctionRegistry, now) => {
|
|
6
|
+
let nowMs = now;
|
|
6
7
|
let nextId = 1;
|
|
7
8
|
const pending = /* @__PURE__ */ new Map();
|
|
8
9
|
const recordedFailures = [];
|
|
@@ -18,6 +19,7 @@ const createFakeScheduler = (getDispatch, getMutationContext, getFunctionRegistr
|
|
|
18
19
|
});
|
|
19
20
|
return id;
|
|
20
21
|
};
|
|
22
|
+
const targetPath = (target) => typeof target === "string" ? target : target.name ?? target.binding ?? "";
|
|
21
23
|
const scheduler = {
|
|
22
24
|
cancel: (id) => {
|
|
23
25
|
const existed = pending.has(id);
|
|
@@ -27,12 +29,12 @@ const createFakeScheduler = (getDispatch, getMutationContext, getFunctionRegistr
|
|
|
27
29
|
// eslint-disable-next-line unicorn/no-null -- Scheduler.get returns null when absent (public API contract)
|
|
28
30
|
get: (id) => Promise.resolve(pending.get(id) ?? null),
|
|
29
31
|
list: () => Promise.resolve([...pending.values()]),
|
|
30
|
-
runAfter: (delayMs,
|
|
31
|
-
const id = enqueue(nowMs + delayMs,
|
|
32
|
+
runAfter: (delayMs, target, args) => {
|
|
33
|
+
const id = enqueue(nowMs + delayMs, targetPath(target), args);
|
|
32
34
|
return Promise.resolve(id);
|
|
33
35
|
},
|
|
34
|
-
runAt: (timestampMs,
|
|
35
|
-
const id = enqueue(timestampMs,
|
|
36
|
+
runAt: (timestampMs, target, args) => {
|
|
37
|
+
const id = enqueue(timestampMs, targetPath(target), args);
|
|
36
38
|
return Promise.resolve(id);
|
|
37
39
|
}
|
|
38
40
|
};
|
|
@@ -57,7 +59,12 @@ const createFakeScheduler = (getDispatch, getMutationContext, getFunctionRegistr
|
|
|
57
59
|
const executeDue = async (cutoff) => {
|
|
58
60
|
const due = [...pending.values()].filter((j) => j.scheduledFor <= cutoff).toSorted((a, b) => a.scheduledFor - b.scheduledFor);
|
|
59
61
|
const failed = [];
|
|
62
|
+
let executed = 0;
|
|
60
63
|
for (const job of due) {
|
|
64
|
+
if (!pending.has(job.id)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
executed += 1;
|
|
61
68
|
try {
|
|
62
69
|
await dispatchJob(job);
|
|
63
70
|
} catch (error) {
|
|
@@ -66,7 +73,7 @@ const createFakeScheduler = (getDispatch, getMutationContext, getFunctionRegistr
|
|
|
66
73
|
recordedFailures.push(failure);
|
|
67
74
|
}
|
|
68
75
|
}
|
|
69
|
-
return { executed
|
|
76
|
+
return { executed, failed };
|
|
70
77
|
};
|
|
71
78
|
const runSweep = async (cutoff, options) => {
|
|
72
79
|
const { executed, failed } = await executeDue(cutoff);
|
|
@@ -100,7 +107,7 @@ const createSqlExec = () => {
|
|
|
100
107
|
return {
|
|
101
108
|
one() {
|
|
102
109
|
if (rows.length !== 1) {
|
|
103
|
-
throw new
|
|
110
|
+
throw new LunoraError("INTERNAL", `expected exactly one row, received ${String(rows.length)}`);
|
|
104
111
|
}
|
|
105
112
|
const [only] = rows;
|
|
106
113
|
return only;
|
|
@@ -138,7 +145,7 @@ const registeredFunctionKind = (value) => {
|
|
|
138
145
|
};
|
|
139
146
|
const registeredFunctionVisibility = (value) => typeof value === "object" && value !== null && value.visibility === "internal" ? "internal" : "public";
|
|
140
147
|
const unavailable = (surface) => {
|
|
141
|
-
throw new
|
|
148
|
+
throw new LunoraError("INTERNAL", `ctx.${surface} is not available in the in-memory @lunora/testing harness (v1)`);
|
|
142
149
|
};
|
|
143
150
|
const stubProxy = (surface) => /* @__PURE__ */ new Proxy((..._args) => unavailable(surface), {
|
|
144
151
|
apply: () => unavailable(surface),
|
|
@@ -154,8 +161,9 @@ const noopLog = {
|
|
|
154
161
|
const buildSubscribe = (runRegistered, queryContext, mutationListeners) => {
|
|
155
162
|
const factory = (referenceOrInline, args) => {
|
|
156
163
|
let done = false;
|
|
157
|
-
|
|
164
|
+
const pendingWaiters = [];
|
|
158
165
|
let pendingResult;
|
|
166
|
+
let pendingError;
|
|
159
167
|
let latestSeq = 0;
|
|
160
168
|
let appliedSeq = 0;
|
|
161
169
|
const runQuery = () => {
|
|
@@ -170,25 +178,46 @@ const buildSubscribe = (runRegistered, queryContext, mutationListeners) => {
|
|
|
170
178
|
}
|
|
171
179
|
appliedSeq = seq;
|
|
172
180
|
const iterResult = { done: false, value };
|
|
173
|
-
if (
|
|
181
|
+
if (pendingWaiters.length === 0) {
|
|
174
182
|
pendingResult = iterResult;
|
|
183
|
+
pendingError = void 0;
|
|
175
184
|
} else {
|
|
176
|
-
const resolve = pendingResolve;
|
|
177
|
-
pendingResolve = void 0;
|
|
178
185
|
pendingResult = void 0;
|
|
179
|
-
|
|
186
|
+
pendingError = void 0;
|
|
187
|
+
for (const waiter of pendingWaiters.splice(0)) {
|
|
188
|
+
waiter.resolve(iterResult);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const emitError = (seq, error) => {
|
|
193
|
+
if (seq < appliedSeq) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
appliedSeq = seq;
|
|
197
|
+
if (pendingWaiters.length === 0) {
|
|
198
|
+
pendingError = { error };
|
|
199
|
+
pendingResult = void 0;
|
|
200
|
+
} else {
|
|
201
|
+
pendingResult = void 0;
|
|
202
|
+
pendingError = void 0;
|
|
203
|
+
for (const waiter of pendingWaiters.splice(0)) {
|
|
204
|
+
waiter.reject(error);
|
|
205
|
+
}
|
|
180
206
|
}
|
|
181
207
|
};
|
|
182
208
|
const emitAt = (seq) => (value) => {
|
|
183
209
|
emit(seq, value);
|
|
184
210
|
};
|
|
211
|
+
const emitErrorAt = (seq) => (error) => {
|
|
212
|
+
emitError(seq, error);
|
|
213
|
+
};
|
|
185
214
|
const listener = () => {
|
|
186
215
|
if (done) {
|
|
187
216
|
return;
|
|
188
217
|
}
|
|
189
218
|
latestSeq += 1;
|
|
190
219
|
const seq = latestSeq;
|
|
191
|
-
runQuery().then(emitAt(seq)).catch(()
|
|
220
|
+
runQuery().then(emitAt(seq)).catch(emitErrorAt(seq));
|
|
192
221
|
};
|
|
193
222
|
mutationListeners.add(listener);
|
|
194
223
|
const iterator = {
|
|
@@ -199,17 +228,29 @@ const buildSubscribe = (runRegistered, queryContext, mutationListeners) => {
|
|
|
199
228
|
if (done) {
|
|
200
229
|
return Promise.resolve({ done: true, value: void 0 });
|
|
201
230
|
}
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
231
|
+
if (appliedSeq === latestSeq) {
|
|
232
|
+
if (pendingError !== void 0) {
|
|
233
|
+
const { error } = pendingError;
|
|
234
|
+
pendingError = void 0;
|
|
235
|
+
return Promise.reject(error);
|
|
236
|
+
}
|
|
237
|
+
if (pendingResult !== void 0) {
|
|
238
|
+
const result = pendingResult;
|
|
239
|
+
pendingResult = void 0;
|
|
240
|
+
return Promise.resolve(result);
|
|
241
|
+
}
|
|
206
242
|
}
|
|
207
243
|
if (appliedSeq < latestSeq) {
|
|
208
|
-
return new Promise((resolve) => {
|
|
209
|
-
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
pendingWaiters.push({ reject, resolve });
|
|
210
246
|
});
|
|
211
247
|
}
|
|
212
248
|
return runQuery().then((value) => {
|
|
249
|
+
if (pendingError !== void 0) {
|
|
250
|
+
const { error } = pendingError;
|
|
251
|
+
pendingError = void 0;
|
|
252
|
+
throw error;
|
|
253
|
+
}
|
|
213
254
|
if (pendingResult !== void 0) {
|
|
214
255
|
const result = pendingResult;
|
|
215
256
|
pendingResult = void 0;
|
|
@@ -221,15 +262,13 @@ const buildSubscribe = (runRegistered, queryContext, mutationListeners) => {
|
|
|
221
262
|
return: () => {
|
|
222
263
|
done = true;
|
|
223
264
|
mutationListeners.delete(listener);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
pendingResolve = void 0;
|
|
227
|
-
resolve({ done: true, value: void 0 });
|
|
265
|
+
for (const waiter of pendingWaiters.splice(0)) {
|
|
266
|
+
waiter.resolve({ done: true, value: void 0 });
|
|
228
267
|
}
|
|
229
268
|
return Promise.resolve({ done: true, value: void 0 });
|
|
230
269
|
}
|
|
231
270
|
};
|
|
232
|
-
runQuery().then(emitAt(0)).catch((
|
|
271
|
+
runQuery().then(emitAt(0)).catch(emitErrorAt(0));
|
|
233
272
|
return iterator;
|
|
234
273
|
};
|
|
235
274
|
return factory;
|
|
@@ -243,26 +282,28 @@ const lunoraTest = (schema, options) => {
|
|
|
243
282
|
const runner = sql.exec;
|
|
244
283
|
runner.call(sql, statement);
|
|
245
284
|
};
|
|
246
|
-
let
|
|
247
|
-
const runInMutationTransaction =
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
transactionDepth = 1;
|
|
252
|
-
execStatement("BEGIN");
|
|
253
|
-
try {
|
|
254
|
-
const result = await function_();
|
|
255
|
-
execStatement("COMMIT");
|
|
256
|
-
return result;
|
|
257
|
-
} catch (error) {
|
|
285
|
+
let mutationQueue = Promise.resolve();
|
|
286
|
+
const runInMutationTransaction = (function_) => {
|
|
287
|
+
const runTransaction = async () => {
|
|
288
|
+
execStatement("BEGIN");
|
|
258
289
|
try {
|
|
259
|
-
|
|
260
|
-
|
|
290
|
+
const result2 = await function_();
|
|
291
|
+
execStatement("COMMIT");
|
|
292
|
+
return result2;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
try {
|
|
295
|
+
execStatement("ROLLBACK");
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
throw error;
|
|
261
299
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
300
|
+
};
|
|
301
|
+
const result = mutationQueue.then(runTransaction);
|
|
302
|
+
mutationQueue = result.then(
|
|
303
|
+
() => void 0,
|
|
304
|
+
() => void 0
|
|
305
|
+
);
|
|
306
|
+
return result;
|
|
266
307
|
};
|
|
267
308
|
let closed = false;
|
|
268
309
|
const closeDatabase = () => {
|
|
@@ -283,20 +324,28 @@ const lunoraTest = (schema, options) => {
|
|
|
283
324
|
};
|
|
284
325
|
let scheduledDispatchRef;
|
|
285
326
|
let mutationContextRef;
|
|
327
|
+
const harnessNow = options?.now ?? Date.now();
|
|
286
328
|
const { controls: schedulerControls, scheduler: fakeScheduler } = createFakeScheduler(
|
|
287
329
|
() => {
|
|
288
330
|
if (scheduledDispatchRef === void 0) {
|
|
289
|
-
throw new
|
|
331
|
+
throw new LunoraError(
|
|
332
|
+
"INTERNAL",
|
|
333
|
+
"[fake-scheduler] dispatch not yet available — scheduler.advance called before harness construction completed"
|
|
334
|
+
);
|
|
290
335
|
}
|
|
291
336
|
return scheduledDispatchRef;
|
|
292
337
|
},
|
|
293
338
|
() => {
|
|
294
339
|
if (mutationContextRef === void 0) {
|
|
295
|
-
throw new
|
|
340
|
+
throw new LunoraError(
|
|
341
|
+
"INTERNAL",
|
|
342
|
+
"[fake-scheduler] mutationContext not yet available — scheduler.advance called before harness construction completed"
|
|
343
|
+
);
|
|
296
344
|
}
|
|
297
345
|
return mutationContextRef;
|
|
298
346
|
},
|
|
299
|
-
() => functionRegistryMap
|
|
347
|
+
() => functionRegistryMap,
|
|
348
|
+
harnessNow
|
|
300
349
|
);
|
|
301
350
|
const makeHarness = (identity) => {
|
|
302
351
|
const auth = {
|
|
@@ -308,21 +357,27 @@ const lunoraTest = (schema, options) => {
|
|
|
308
357
|
const queryContext = {
|
|
309
358
|
auth,
|
|
310
359
|
db: database,
|
|
360
|
+
env: options?.env,
|
|
311
361
|
log: noopLog,
|
|
362
|
+
now: harnessNow,
|
|
312
363
|
// 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
364
|
runQuery: (reference, args) => runInternal("query", reference, queryContext, args),
|
|
365
|
+
secrets: stubProxy("secrets"),
|
|
314
366
|
storage: stubProxy("storage"),
|
|
315
367
|
vectors: stubProxy("vectors")
|
|
316
368
|
};
|
|
317
369
|
const mutationContext = {
|
|
318
370
|
auth,
|
|
319
371
|
db: database,
|
|
372
|
+
env: options?.env,
|
|
320
373
|
log: noopLog,
|
|
374
|
+
now: harnessNow,
|
|
321
375
|
// 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
376
|
runMutation: (reference, args) => runInternal("mutation", reference, mutationContext, args),
|
|
323
377
|
// 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
378
|
runQuery: (reference, args) => runInternal("query", reference, queryContext, args),
|
|
325
379
|
scheduler: fakeScheduler,
|
|
380
|
+
secrets: stubProxy("secrets"),
|
|
326
381
|
storage: stubProxy("storage"),
|
|
327
382
|
vectors: stubProxy("vectors"),
|
|
328
383
|
workflows: stubProxy("workflows")
|
|
@@ -331,9 +386,11 @@ const lunoraTest = (schema, options) => {
|
|
|
331
386
|
const actionContext = {
|
|
332
387
|
auth,
|
|
333
388
|
db: database,
|
|
389
|
+
env: options?.env,
|
|
334
390
|
// Use the injected fetch when provided; fall back to the v1 stub otherwise.
|
|
335
391
|
fetch: options?.fetch ?? stubProxy("fetch"),
|
|
336
392
|
log: noopLog,
|
|
393
|
+
now: harnessNow,
|
|
337
394
|
// 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
395
|
runAction: (reference, args) => runInternal("action", reference, actionContext, args),
|
|
339
396
|
// 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 +398,7 @@ const lunoraTest = (schema, options) => {
|
|
|
341
398
|
// 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
399
|
runQuery: (reference, args) => runInternal("query", reference, queryContext, args),
|
|
343
400
|
scheduler: fakeScheduler,
|
|
401
|
+
secrets: stubProxy("secrets"),
|
|
344
402
|
storage: stubProxy("storage"),
|
|
345
403
|
vectors: stubProxy("vectors"),
|
|
346
404
|
workflows: stubProxy("workflows")
|
|
@@ -348,11 +406,12 @@ const lunoraTest = (schema, options) => {
|
|
|
348
406
|
const runRegistered = (expected, reference, context, args, allowInternal) => {
|
|
349
407
|
const kind = registeredFunctionKind(reference);
|
|
350
408
|
if (kind !== expected) {
|
|
351
|
-
throw new
|
|
409
|
+
throw new LunoraError("INTERNAL", `expected a registered ${expected}, received a ${kind ?? "non-function"} reference`);
|
|
352
410
|
}
|
|
353
411
|
if (!allowInternal && registeredFunctionVisibility(reference) === "internal") {
|
|
354
|
-
throw new
|
|
355
|
-
|
|
412
|
+
throw new LunoraError(
|
|
413
|
+
"INTERNAL",
|
|
414
|
+
`This ${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
415
|
);
|
|
357
416
|
}
|
|
358
417
|
return Promise.resolve(reference.handler(context, args ?? {}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/testing",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.41",
|
|
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,11 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@lunora/
|
|
50
|
-
"@lunora/
|
|
51
|
-
"@lunora/
|
|
49
|
+
"@lunora/agent": "1.0.0-alpha.1",
|
|
50
|
+
"@lunora/do": "1.0.0-alpha.29",
|
|
51
|
+
"@lunora/errors": "1.0.0-alpha.4",
|
|
52
|
+
"@lunora/mail": "1.0.0-alpha.14",
|
|
53
|
+
"@lunora/server": "1.0.0-alpha.24"
|
|
52
54
|
},
|
|
53
55
|
"engines": {
|
|
54
56
|
"node": "^22.15.0 || >=24.11.0"
|