@velajs/testing 0.5.1 → 1.0.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 02bb42e: Target the published Vela 1.21 security release and use Vela's production bootstrap primitive so request context, global providers, and request-scoped test execution cannot drift from the real runtime. Standalone CI no longer relies on a sibling `link:../vela` checkout.
8
+
9
+ ## 0.6.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 5097ee4: Add `@velajs/testing/eval`: a small, model-agnostic harness for scoring the output of any string-producing function. Ships heuristic scorers (`exactMatch`, `contains`, `keyword`, `regex`), an `llmScorer` LLM-as-judge whose `judge` callback is injected (no AI SDK dependency, fails soft on unparseable replies), and `evaluate(dataset, run, scorers)` which returns per-case reports plus a per-scorer and overall aggregate.
14
+
3
15
  ## 0.5.1
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@velajs/testing)](https://www.npmjs.com/package/@velajs/testing)
4
4
  [![License: MIT](https://img.shields.io/npm/l/@velajs/testing)](https://github.com/velajs/testing/blob/main/LICENSE)
5
5
 
6
- Test-module builder for [Vela](https://github.com/velajs/vela). Compose modules in isolation, override providers/guards/pipes/interceptors/filters, and exercise controllers via Hono's `app.request()` without bootstrapping the real factory.
6
+ Test-module builder for [Vela](https://github.com/velajs/vela). Compose modules in isolation, override providers/guards/pipes/interceptors/filters, and exercise controllers via Hono's `app.request()`. Testing uses the same bootstrap primitive as production so request scope and framework-global providers cannot drift.
7
7
 
8
8
  ## Install
9
9
 
10
10
  ```bash
11
11
  pnpm add -D @velajs/testing
12
- # Peer (already in your project): @velajs/vela ^1.1.0, hono ^4
12
+ # Peer (already in your project): @velajs/vela >=1.21 <2, hono >=4
13
13
  ```
14
14
 
15
15
  No `reflect-metadata` needed — Vela ships its own polyfill.
@@ -95,6 +95,37 @@ expect(await res.json()).toEqual([{ id: 1, name: 'Item 1' }]);
95
95
  | `TestingModule` | Result of `compile()`: `get(token)`, `createApplication()`, `close()`. |
96
96
  | `OverrideBy` | The fluent intermediate from `overrideX()` — exposed for type-narrowing. |
97
97
 
98
+ ## Output scoring / evals
99
+
100
+ `@velajs/testing/eval` is a small, model-agnostic harness for grading the output of any string-producing function — an LLM turn, an agent loop, a formatter — against heuristics or an injected LLM judge. It pulls in no AI SDK: the only model touchpoint is `llmScorer`, whose `judge` is a plain callback you supply.
101
+
102
+ ```ts
103
+ import { evaluate, keyword, llmScorer } from '@velajs/testing/eval';
104
+
105
+ const report = await evaluate(
106
+ [{ input: 'where is my order?', expected: 'shipped' }],
107
+ async (input) => askSupportAgent(input),
108
+ {
109
+ coverage: keyword(['shipped']),
110
+ helpful: llmScorer({ criteria: 'answers the question', judge: myModel }),
111
+ },
112
+ );
113
+
114
+ expect(report.aggregate.overall).toBeGreaterThan(0.5);
115
+ ```
116
+
117
+ Each scorer returns a `[0, 1]` score (auto-clamped) with an optional reason. `evaluate` runs every case through the producer, grades each output with every scorer, and returns per-case reports plus an aggregate (`perScorer` means and an `overall` mean).
118
+
119
+ | Export | Purpose |
120
+ |---|---|
121
+ | `evaluate(dataset, run, scorers)` | Grade a dataset; returns `{ cases, aggregate }`. |
122
+ | `exactMatch(options?)` | 1 when the output equals `expected` (trimmed by default). |
123
+ | `contains(needles, options?)` | Substring match, `all`-of (default) or `any`-of. |
124
+ | `keyword(keywords, options?)` | Fractional coverage — the share of keywords present. |
125
+ | `regex(pattern)` | 1 when the pattern matches (global/sticky flags stripped for reuse). |
126
+ | `llmScorer({ criteria, judge })` | LLM-as-judge with an injected `judge` callback; fails soft to 0. |
127
+ | `Scorer` | `(input) => number \| ScoreResult` (sync or async) — write your own. |
128
+
98
129
  ## How it's wired
99
130
 
100
131
  `@velajs/testing` consumes vela's framework primitives via `@velajs/vela/internal` (`MetadataRegistry`, `Container`, `RouteManager`, `ModuleLoader`, `ComponentManager`, `VelaApplication`, `bindAppProviders`). The same `bindAppProviders` that `VelaFactory.create` uses, so test-mode and run-mode app construction stay in lockstep automatically.
@@ -0,0 +1,154 @@
1
+ //#region src/eval/types.d.ts
2
+ /**
3
+ * Shared types for the output-evaluation harness.
4
+ *
5
+ * The harness is model-agnostic: a {@link Scorer} is any function that turns one
6
+ * sample into a `[0, 1]` score, and {@link evaluate} drives a dataset through a
7
+ * producer and grades every output. Nothing here reaches for an AI SDK — the
8
+ * only "AI" surface is `llmScorer`, whose judge callback is injected by the
9
+ * caller.
10
+ */
11
+ /** A value that may be produced synchronously or via a promise. */
12
+ type Awaitable<T> = T | Promise<T>;
13
+ /** The single sample handed to a {@link Scorer}. */
14
+ interface ScorerInput {
15
+ /** The prompt/input that produced {@link ScorerInput.output}, for judge context. */
16
+ input?: string;
17
+ /** The output under test — the only field every scorer is guaranteed to see. */
18
+ output: string;
19
+ /** The reference answer, when a scorer grades against a gold value. */
20
+ expected?: string;
21
+ /** Free-form per-sample metadata carried through untouched. */
22
+ metadata?: Record<string, unknown>;
23
+ }
24
+ /** A scorer's verdict: a `[0, 1]` score plus an optional human-readable reason. */
25
+ interface ScoreResult {
26
+ /** The grade, always clamped into `[0, 1]` by the harness. */
27
+ score: number;
28
+ /** Why the scorer landed on this grade. */
29
+ reason?: string;
30
+ }
31
+ /**
32
+ * A scorer maps one sample to a grade. Returning a bare number is shorthand for
33
+ * `{ score }`; return a {@link ScoreResult} to attach a reason. Sync or async.
34
+ */
35
+ type Scorer = (input: ScorerInput) => Awaitable<number | ScoreResult>;
36
+ /** One dataset row: an input plus an optional gold answer and metadata. */
37
+ interface EvalCase {
38
+ input: string;
39
+ expected?: string;
40
+ metadata?: Record<string, unknown>;
41
+ }
42
+ /** The result of grading one case: the produced output and each scorer's verdict. */
43
+ interface CaseReport {
44
+ input: string;
45
+ output: string;
46
+ /** Each scorer's verdict, keyed by the name it was registered under. */
47
+ scores: Record<string, ScoreResult>;
48
+ /** The mean of this case's scores across all scorers. */
49
+ average: number;
50
+ }
51
+ /** The roll-up across every case. */
52
+ interface EvalAggregate {
53
+ /** Mean score of each scorer across all cases, keyed by scorer name. */
54
+ perScorer: Record<string, number>;
55
+ /** Mean of every case's average — the single headline number. */
56
+ overall: number;
57
+ }
58
+ /** The full run: one report per case plus the aggregate roll-up. */
59
+ interface EvalReport {
60
+ cases: CaseReport[];
61
+ aggregate: EvalAggregate;
62
+ }
63
+ //#endregion
64
+ //#region src/eval/scorers.d.ts
65
+ /** Options for {@link exactMatch}. */
66
+ interface ExactMatchOptions {
67
+ /** Trim leading/trailing whitespace before comparing (default `true`). */
68
+ trim?: boolean;
69
+ /** Compare with case sensitivity (default `true`). */
70
+ caseSensitive?: boolean;
71
+ }
72
+ /**
73
+ * Score 1 when the output equals the case's `expected`, else 0. Whitespace is
74
+ * trimmed by default; a case with no `expected` scores 0 (there is nothing to
75
+ * match against, so it fails closed).
76
+ */
77
+ declare function exactMatch(options?: ExactMatchOptions): Scorer;
78
+ /** Options for {@link contains}. */
79
+ interface ContainsOptions {
80
+ /** `all` (default) requires every needle; `any` requires at least one. */
81
+ mode?: 'all' | 'any';
82
+ /** Match with case sensitivity (default: case-insensitive). */
83
+ caseSensitive?: boolean;
84
+ }
85
+ /**
86
+ * Binary substring scorer. Pass a single needle or a list; with a list, `mode`
87
+ * chooses whether every needle must be present (`all`, the default) or just one
88
+ * (`any`). Scores 1 when the condition holds, else 0.
89
+ */
90
+ declare function contains(needles: string | readonly string[], options?: ContainsOptions): Scorer;
91
+ /** Options for {@link keyword}. */
92
+ interface KeywordOptions {
93
+ /** Match with case sensitivity (default: case-insensitive). */
94
+ caseSensitive?: boolean;
95
+ }
96
+ /**
97
+ * Fractional coverage scorer: the share of `keywords` present in the output. A
98
+ * rubric with three keywords, two of which appear, scores `2/3`. An empty
99
+ * keyword list would silently score everything 1 — the opposite of a useful
100
+ * eval — so it is rejected at construction time.
101
+ */
102
+ declare function keyword(keywords: readonly string[], options?: KeywordOptions): Scorer;
103
+ /**
104
+ * Score 1 when `pattern` matches the output, else 0. The global/sticky flags are
105
+ * stripped from a copy of the pattern so a scorer reused across many samples
106
+ * never carries `lastIndex` state between calls.
107
+ */
108
+ declare function regex(pattern: RegExp): Scorer;
109
+ /** Options for {@link llmScorer}. */
110
+ interface LlmScorerOptions {
111
+ /** The rubric the judge grades against, e.g. "answers the question accurately". */
112
+ criteria: string;
113
+ /**
114
+ * The injected judge. Wire it to your own model call (`generateText`, a raw
115
+ * fetch, whatever). Keeping it a plain callback is what lets this package
116
+ * grade LLM output without depending on any AI SDK, and makes it trivial to
117
+ * fake in tests.
118
+ */
119
+ judge: (prompt: string) => Promise<string>;
120
+ }
121
+ /** Render the grading prompt for one sample under a rubric. */
122
+ declare function renderJudgePrompt(criteria: string, sample: ScorerInput): string;
123
+ /**
124
+ * Read a judge reply into a verdict. The score is the leading number, clamped,
125
+ * and the full reply is kept as the reason. Requiring the number at the very
126
+ * start is deliberate: a reply with no leading number fails closed to 0, and a
127
+ * digit buried later in the justification can never be misread as the grade.
128
+ */
129
+ declare function parseJudgeReply(reply: string): ScoreResult;
130
+ /**
131
+ * LLM-as-judge scorer. It builds a rubric prompt, hands it to the injected
132
+ * `judge`, and parses the reply into a `[0, 1]` grade. Unparseable replies fail
133
+ * soft (score 0 with the raw reply as the reason) rather than throwing.
134
+ */
135
+ declare function llmScorer(options: LlmScorerOptions): Scorer;
136
+ //#endregion
137
+ //#region src/eval/evaluate.d.ts
138
+ /**
139
+ * Run a whole dataset through `run` and grade each output with the named
140
+ * `scorers`. Every case is graded by every scorer; cases execute concurrently,
141
+ * so keep `run` stateless per input (or key any shared state on `input`).
142
+ *
143
+ * The returned report carries one {@link CaseReport} per case plus an aggregate:
144
+ * the mean of each scorer across all cases (`perScorer`) and the mean of the
145
+ * per-case averages (`overall`).
146
+ *
147
+ * @param dataset The cases to grade.
148
+ * @param run Produces the output string for a given input.
149
+ * @param scorers A name → scorer map; the names key the per-scorer aggregate.
150
+ */
151
+ declare function evaluate(dataset: readonly EvalCase[], run: (input: string) => Awaitable<string>, scorers: Record<string, Scorer>): Promise<EvalReport>;
152
+ //#endregion
153
+ export { type Awaitable, type CaseReport, type ContainsOptions, type EvalAggregate, type EvalCase, type EvalReport, type ExactMatchOptions, type KeywordOptions, type LlmScorerOptions, type ScoreResult, type Scorer, type ScorerInput, contains, evaluate, exactMatch, keyword, llmScorer, parseJudgeReply, regex, renderJudgePrompt };
154
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,191 @@
1
+ //#region src/eval/scorers.ts
2
+ /**
3
+ * Force a raw number into the `[0, 1]` unit range. Anything non-finite (NaN,
4
+ * Infinity) collapses to 0 so a broken scorer fails closed rather than skewing
5
+ * an aggregate.
6
+ */
7
+ function clampUnit(value) {
8
+ if (!Number.isFinite(value)) return 0;
9
+ if (value < 0) return 0;
10
+ if (value > 1) return 1;
11
+ return value;
12
+ }
13
+ /** Coerce a scorer's return (a bare number or a full result) into a clamped {@link ScoreResult}. */
14
+ function normalizeVerdict(value) {
15
+ if (typeof value === "number") return { score: clampUnit(value) };
16
+ if (value.reason === void 0) return { score: clampUnit(value.score) };
17
+ return {
18
+ score: clampUnit(value.score),
19
+ reason: value.reason
20
+ };
21
+ }
22
+ /** Arithmetic mean of a list; an empty list averages to 0. */
23
+ function arithmeticMean(values) {
24
+ if (values.length === 0) return 0;
25
+ let total = 0;
26
+ for (const value of values) total += value;
27
+ return total / values.length;
28
+ }
29
+ /** Case-fold a string unless the caller opted into case-sensitive matching. */
30
+ function fold(value, caseSensitive) {
31
+ return caseSensitive ? value : value.toLowerCase();
32
+ }
33
+ /**
34
+ * Score 1 when the output equals the case's `expected`, else 0. Whitespace is
35
+ * trimmed by default; a case with no `expected` scores 0 (there is nothing to
36
+ * match against, so it fails closed).
37
+ */
38
+ function exactMatch(options = {}) {
39
+ const trim = options.trim ?? true;
40
+ const caseSensitive = options.caseSensitive ?? true;
41
+ const shape = (value) => {
42
+ const trimmed = trim ? value.trim() : value;
43
+ return caseSensitive ? trimmed : trimmed.toLowerCase();
44
+ };
45
+ return ({ output, expected }) => {
46
+ if (expected === void 0) return {
47
+ score: 0,
48
+ reason: "no expected value to compare against"
49
+ };
50
+ return shape(output) === shape(expected) ? 1 : 0;
51
+ };
52
+ }
53
+ /**
54
+ * Binary substring scorer. Pass a single needle or a list; with a list, `mode`
55
+ * chooses whether every needle must be present (`all`, the default) or just one
56
+ * (`any`). Scores 1 when the condition holds, else 0.
57
+ */
58
+ function contains(needles, options = {}) {
59
+ const list = typeof needles === "string" ? [needles] : [...needles];
60
+ if (list.length === 0) throw new Error("@velajs/testing: contains() needs at least one needle");
61
+ const mode = options.mode ?? "all";
62
+ const targets = list.map((needle) => fold(needle, options.caseSensitive));
63
+ return ({ output }) => {
64
+ const haystack = fold(output, options.caseSensitive);
65
+ const found = targets.map((target) => haystack.includes(target));
66
+ return (mode === "any" ? found.some(Boolean) : found.every(Boolean)) ? 1 : 0;
67
+ };
68
+ }
69
+ /**
70
+ * Fractional coverage scorer: the share of `keywords` present in the output. A
71
+ * rubric with three keywords, two of which appear, scores `2/3`. An empty
72
+ * keyword list would silently score everything 1 — the opposite of a useful
73
+ * eval — so it is rejected at construction time.
74
+ */
75
+ function keyword(keywords, options = {}) {
76
+ if (keywords.length === 0) throw new Error("@velajs/testing: keyword() needs at least one keyword");
77
+ const targets = keywords.map((word) => fold(word, options.caseSensitive));
78
+ return ({ output }) => {
79
+ const haystack = fold(output, options.caseSensitive);
80
+ const hits = targets.filter((target) => haystack.includes(target)).length;
81
+ return {
82
+ score: hits / targets.length,
83
+ reason: `${hits}/${targets.length} keywords present`
84
+ };
85
+ };
86
+ }
87
+ /**
88
+ * Score 1 when `pattern` matches the output, else 0. The global/sticky flags are
89
+ * stripped from a copy of the pattern so a scorer reused across many samples
90
+ * never carries `lastIndex` state between calls.
91
+ */
92
+ function regex(pattern) {
93
+ const stateless = new RegExp(pattern.source, pattern.flags.replace(/[gy]/gu, ""));
94
+ return ({ output }) => stateless.test(output) ? 1 : 0;
95
+ }
96
+ /** The regex that reads the leading number the judge is instructed to emit. */
97
+ const LEADING_NUMBER = /^\s*([+-]?\d+(?:\.\d+)?)/u;
98
+ /** Render the grading prompt for one sample under a rubric. */
99
+ function renderJudgePrompt(criteria, sample) {
100
+ const lines = [
101
+ "You are grading an assistant output.",
102
+ `Criterion: ${criteria}`,
103
+ "Begin your reply with a single decimal from 0 (does not meet) to 1 (fully meets), then optionally a short justification."
104
+ ];
105
+ if (sample.input !== void 0) lines.push("", `Prompt: ${sample.input}`);
106
+ if (sample.expected !== void 0) lines.push("", `Expected answer: ${sample.expected}`);
107
+ lines.push("", `Output to grade: ${sample.output}`);
108
+ return lines.join("\n");
109
+ }
110
+ /**
111
+ * Read a judge reply into a verdict. The score is the leading number, clamped,
112
+ * and the full reply is kept as the reason. Requiring the number at the very
113
+ * start is deliberate: a reply with no leading number fails closed to 0, and a
114
+ * digit buried later in the justification can never be misread as the grade.
115
+ */
116
+ function parseJudgeReply(reply) {
117
+ const match = LEADING_NUMBER.exec(reply);
118
+ const reason = reply.trim();
119
+ if (match === null) return {
120
+ score: 0,
121
+ reason
122
+ };
123
+ return {
124
+ score: clampUnit(Number(match[1])),
125
+ reason
126
+ };
127
+ }
128
+ /**
129
+ * LLM-as-judge scorer. It builds a rubric prompt, hands it to the injected
130
+ * `judge`, and parses the reply into a `[0, 1]` grade. Unparseable replies fail
131
+ * soft (score 0 with the raw reply as the reason) rather than throwing.
132
+ */
133
+ function llmScorer(options) {
134
+ return async (sample) => parseJudgeReply(await options.judge(renderJudgePrompt(options.criteria, sample)));
135
+ }
136
+ //#endregion
137
+ //#region src/eval/evaluate.ts
138
+ /** Run every scorer over one sample and collect the verdicts keyed by scorer name. */
139
+ async function gradeSample(sample, scorers) {
140
+ const graded = await Promise.all(Object.entries(scorers).map(async ([name, scorer]) => {
141
+ return [name, normalizeVerdict(await scorer(sample))];
142
+ }));
143
+ const scores = {};
144
+ for (const [name, verdict] of graded) scores[name] = verdict;
145
+ return scores;
146
+ }
147
+ /**
148
+ * Run a whole dataset through `run` and grade each output with the named
149
+ * `scorers`. Every case is graded by every scorer; cases execute concurrently,
150
+ * so keep `run` stateless per input (or key any shared state on `input`).
151
+ *
152
+ * The returned report carries one {@link CaseReport} per case plus an aggregate:
153
+ * the mean of each scorer across all cases (`perScorer`) and the mean of the
154
+ * per-case averages (`overall`).
155
+ *
156
+ * @param dataset The cases to grade.
157
+ * @param run Produces the output string for a given input.
158
+ * @param scorers A name → scorer map; the names key the per-scorer aggregate.
159
+ */
160
+ async function evaluate(dataset, run, scorers) {
161
+ const cases = await Promise.all(dataset.map(async (testCase) => {
162
+ const output = await run(testCase.input);
163
+ const sample = {
164
+ input: testCase.input,
165
+ output
166
+ };
167
+ if (testCase.expected !== void 0) sample.expected = testCase.expected;
168
+ if (testCase.metadata !== void 0) sample.metadata = testCase.metadata;
169
+ const scores = await gradeSample(sample, scorers);
170
+ const average = arithmeticMean(Object.values(scores).map((verdict) => verdict.score));
171
+ return {
172
+ input: testCase.input,
173
+ output,
174
+ scores,
175
+ average
176
+ };
177
+ }));
178
+ const perScorer = {};
179
+ for (const name of Object.keys(scorers)) perScorer[name] = arithmeticMean(cases.map((report) => report.scores[name]?.score ?? 0));
180
+ return {
181
+ cases,
182
+ aggregate: {
183
+ perScorer,
184
+ overall: arithmeticMean(cases.map((report) => report.average))
185
+ }
186
+ };
187
+ }
188
+ //#endregion
189
+ export { contains, evaluate, exactMatch, keyword, llmScorer, parseJudgeReply, regex, renderJudgePrompt };
190
+
191
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/eval/scorers.ts","../../src/eval/evaluate.ts"],"sourcesContent":["import type { Scorer, ScorerInput, ScoreResult } from './types.js';\n\n/**\n * Force a raw number into the `[0, 1]` unit range. Anything non-finite (NaN,\n * Infinity) collapses to 0 so a broken scorer fails closed rather than skewing\n * an aggregate.\n */\nexport function clampUnit(value: number): number {\n if (!Number.isFinite(value)) {\n return 0;\n }\n if (value < 0) {\n return 0;\n }\n if (value > 1) {\n return 1;\n }\n\n return value;\n}\n\n/** Coerce a scorer's return (a bare number or a full result) into a clamped {@link ScoreResult}. */\nexport function normalizeVerdict(value: number | ScoreResult): ScoreResult {\n if (typeof value === 'number') {\n return { score: clampUnit(value) };\n }\n if (value.reason === undefined) {\n return { score: clampUnit(value.score) };\n }\n\n return { score: clampUnit(value.score), reason: value.reason };\n}\n\n/** Arithmetic mean of a list; an empty list averages to 0. */\nexport function arithmeticMean(values: readonly number[]): number {\n if (values.length === 0) {\n return 0;\n }\n\n let total = 0;\n\n for (const value of values) {\n total += value;\n }\n\n return total / values.length;\n}\n\n/** Case-fold a string unless the caller opted into case-sensitive matching. */\nfunction fold(value: string, caseSensitive: boolean | undefined): string {\n return caseSensitive ? value : value.toLowerCase();\n}\n\n/** Options for {@link exactMatch}. */\nexport interface ExactMatchOptions {\n /** Trim leading/trailing whitespace before comparing (default `true`). */\n trim?: boolean;\n /** Compare with case sensitivity (default `true`). */\n caseSensitive?: boolean;\n}\n\n/**\n * Score 1 when the output equals the case's `expected`, else 0. Whitespace is\n * trimmed by default; a case with no `expected` scores 0 (there is nothing to\n * match against, so it fails closed).\n */\nexport function exactMatch(options: ExactMatchOptions = {}): Scorer {\n const trim = options.trim ?? true;\n const caseSensitive = options.caseSensitive ?? true;\n const shape = (value: string): string => {\n const trimmed = trim ? value.trim() : value;\n\n return caseSensitive ? trimmed : trimmed.toLowerCase();\n };\n\n return ({ output, expected }): number | ScoreResult => {\n if (expected === undefined) {\n return { score: 0, reason: 'no expected value to compare against' };\n }\n\n return shape(output) === shape(expected) ? 1 : 0;\n };\n}\n\n/** Options for {@link contains}. */\nexport interface ContainsOptions {\n /** `all` (default) requires every needle; `any` requires at least one. */\n mode?: 'all' | 'any';\n /** Match with case sensitivity (default: case-insensitive). */\n caseSensitive?: boolean;\n}\n\n/**\n * Binary substring scorer. Pass a single needle or a list; with a list, `mode`\n * chooses whether every needle must be present (`all`, the default) or just one\n * (`any`). Scores 1 when the condition holds, else 0.\n */\nexport function contains(\n needles: string | readonly string[],\n options: ContainsOptions = {},\n): Scorer {\n const list = typeof needles === 'string' ? [needles] : [...needles];\n\n if (list.length === 0) {\n throw new Error('@velajs/testing: contains() needs at least one needle');\n }\n\n const mode = options.mode ?? 'all';\n const targets = list.map((needle) => fold(needle, options.caseSensitive));\n\n return ({ output }): number => {\n const haystack = fold(output, options.caseSensitive);\n const found = targets.map((target) => haystack.includes(target));\n const passed = mode === 'any' ? found.some(Boolean) : found.every(Boolean);\n\n return passed ? 1 : 0;\n };\n}\n\n/** Options for {@link keyword}. */\nexport interface KeywordOptions {\n /** Match with case sensitivity (default: case-insensitive). */\n caseSensitive?: boolean;\n}\n\n/**\n * Fractional coverage scorer: the share of `keywords` present in the output. A\n * rubric with three keywords, two of which appear, scores `2/3`. An empty\n * keyword list would silently score everything 1 — the opposite of a useful\n * eval — so it is rejected at construction time.\n */\nexport function keyword(keywords: readonly string[], options: KeywordOptions = {}): Scorer {\n if (keywords.length === 0) {\n throw new Error('@velajs/testing: keyword() needs at least one keyword');\n }\n\n const targets = keywords.map((word) => fold(word, options.caseSensitive));\n\n return ({ output }): ScoreResult => {\n const haystack = fold(output, options.caseSensitive);\n const hits = targets.filter((target) => haystack.includes(target)).length;\n\n return {\n score: hits / targets.length,\n reason: `${hits}/${targets.length} keywords present`,\n };\n };\n}\n\n/**\n * Score 1 when `pattern` matches the output, else 0. The global/sticky flags are\n * stripped from a copy of the pattern so a scorer reused across many samples\n * never carries `lastIndex` state between calls.\n */\nexport function regex(pattern: RegExp): Scorer {\n const stateless = new RegExp(pattern.source, pattern.flags.replace(/[gy]/gu, ''));\n\n return ({ output }): number => (stateless.test(output) ? 1 : 0);\n}\n\n/** Options for {@link llmScorer}. */\nexport interface LlmScorerOptions {\n /** The rubric the judge grades against, e.g. \"answers the question accurately\". */\n criteria: string;\n /**\n * The injected judge. Wire it to your own model call (`generateText`, a raw\n * fetch, whatever). Keeping it a plain callback is what lets this package\n * grade LLM output without depending on any AI SDK, and makes it trivial to\n * fake in tests.\n */\n judge: (prompt: string) => Promise<string>;\n}\n\n/** The regex that reads the leading number the judge is instructed to emit. */\nconst LEADING_NUMBER = /^\\s*([+-]?\\d+(?:\\.\\d+)?)/u;\n\n/** Render the grading prompt for one sample under a rubric. */\nexport function renderJudgePrompt(criteria: string, sample: ScorerInput): string {\n const lines = [\n 'You are grading an assistant output.',\n `Criterion: ${criteria}`,\n 'Begin your reply with a single decimal from 0 (does not meet) to 1 (fully meets), then optionally a short justification.',\n ];\n\n if (sample.input !== undefined) {\n lines.push('', `Prompt: ${sample.input}`);\n }\n if (sample.expected !== undefined) {\n lines.push('', `Expected answer: ${sample.expected}`);\n }\n\n lines.push('', `Output to grade: ${sample.output}`);\n\n return lines.join('\\n');\n}\n\n/**\n * Read a judge reply into a verdict. The score is the leading number, clamped,\n * and the full reply is kept as the reason. Requiring the number at the very\n * start is deliberate: a reply with no leading number fails closed to 0, and a\n * digit buried later in the justification can never be misread as the grade.\n */\nexport function parseJudgeReply(reply: string): ScoreResult {\n const match = LEADING_NUMBER.exec(reply);\n const reason = reply.trim();\n\n if (match === null) {\n return { score: 0, reason };\n }\n\n return { score: clampUnit(Number(match[1])), reason };\n}\n\n/**\n * LLM-as-judge scorer. It builds a rubric prompt, hands it to the injected\n * `judge`, and parses the reply into a `[0, 1]` grade. Unparseable replies fail\n * soft (score 0 with the raw reply as the reason) rather than throwing.\n */\nexport function llmScorer(options: LlmScorerOptions): Scorer {\n return async (sample): Promise<ScoreResult> =>\n parseJudgeReply(await options.judge(renderJudgePrompt(options.criteria, sample)));\n}\n","import { arithmeticMean, normalizeVerdict } from './scorers.js';\nimport type {\n Awaitable,\n CaseReport,\n EvalCase,\n EvalReport,\n Scorer,\n ScorerInput,\n ScoreResult,\n} from './types.js';\n\n/** Run every scorer over one sample and collect the verdicts keyed by scorer name. */\nasync function gradeSample(\n sample: ScorerInput,\n scorers: Record<string, Scorer>,\n): Promise<Record<string, ScoreResult>> {\n const graded = await Promise.all(\n Object.entries(scorers).map(async ([name, scorer]): Promise<[string, ScoreResult]> => {\n return [name, normalizeVerdict(await scorer(sample))];\n }),\n );\n\n const scores: Record<string, ScoreResult> = {};\n\n for (const [name, verdict] of graded) {\n scores[name] = verdict;\n }\n\n return scores;\n}\n\n/**\n * Run a whole dataset through `run` and grade each output with the named\n * `scorers`. Every case is graded by every scorer; cases execute concurrently,\n * so keep `run` stateless per input (or key any shared state on `input`).\n *\n * The returned report carries one {@link CaseReport} per case plus an aggregate:\n * the mean of each scorer across all cases (`perScorer`) and the mean of the\n * per-case averages (`overall`).\n *\n * @param dataset The cases to grade.\n * @param run Produces the output string for a given input.\n * @param scorers A name → scorer map; the names key the per-scorer aggregate.\n */\nexport async function evaluate(\n dataset: readonly EvalCase[],\n run: (input: string) => Awaitable<string>,\n scorers: Record<string, Scorer>,\n): Promise<EvalReport> {\n const cases = await Promise.all(\n dataset.map(async (testCase): Promise<CaseReport> => {\n const output = await run(testCase.input);\n const sample: ScorerInput = { input: testCase.input, output };\n\n if (testCase.expected !== undefined) {\n sample.expected = testCase.expected;\n }\n if (testCase.metadata !== undefined) {\n sample.metadata = testCase.metadata;\n }\n\n const scores = await gradeSample(sample, scorers);\n const average = arithmeticMean(Object.values(scores).map((verdict) => verdict.score));\n\n return { input: testCase.input, output, scores, average };\n }),\n );\n\n const perScorer: Record<string, number> = {};\n\n for (const name of Object.keys(scorers)) {\n perScorer[name] = arithmeticMean(cases.map((report) => report.scores[name]?.score ?? 0));\n }\n\n const overall = arithmeticMean(cases.map((report) => report.average));\n\n return { cases, aggregate: { perScorer, overall } };\n}\n"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { i as registerWsConnector, n as TestWsRequest, r as getWsConnector, t as TestWsConnection } from "./test-ws-connection-BHlEKwQz.js";
2
- import { DiscoveryService, REQUEST_CONTEXT, Scope } from "@velajs/vela";
3
- import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE, Container, MetadataRegistry, ModuleLoader, ModuleRef, RouteManager, VelaApplication as VelaApplication$1, bindAppProviders } from "@velajs/vela/internal";
2
+ import { REQUEST_CONTEXT } from "@velajs/vela";
3
+ import { MetadataRegistry, VelaApplication as VelaApplication$1, bootstrap } from "@velajs/vela/internal";
4
4
  import { Context } from "hono";
5
5
  import { SeederRegistry } from "@velajs/vela/seeder";
6
6
  import { expect } from "vitest";
@@ -807,45 +807,8 @@ var TestingModuleBuilder = class {
807
807
  controllers: this.metadata.controllers,
808
808
  exports: this.metadata.exports
809
809
  });
810
- const container = new Container();
811
- container.register({
812
- provide: Container,
813
- useValue: container
814
- });
815
- container.markGlobalToken(Container);
816
- container.register({
817
- provide: ModuleRef,
818
- useFactory: (c) => new ModuleRef(c),
819
- inject: [Container]
820
- });
821
- container.markGlobalToken(ModuleRef);
822
- container.register({
823
- provide: DiscoveryService,
824
- useFactory: (c) => new DiscoveryService(c),
825
- inject: [Container]
826
- });
827
- container.markGlobalToken(DiscoveryService);
828
- for (const t of [
829
- APP_GUARD,
830
- APP_PIPE,
831
- APP_INTERCEPTOR,
832
- APP_FILTER,
833
- APP_MIDDLEWARE
834
- ]) container.markGlobalToken(t);
835
- container.register({
836
- provide: REQUEST_CONTEXT,
837
- scope: Scope.REQUEST,
838
- useFactory: () => {
839
- throw new Error("REQUEST_CONTEXT can only be resolved inside a request — it is seeded by RouteManager when the request enters the pipeline.");
840
- }
841
- });
842
- container.markGlobalToken(REQUEST_CONTEXT);
843
- for (const override of this.overrides) container.register(override.provider);
844
- const routeManager = new RouteManager(container);
845
- const loader = new ModuleLoader(container, routeManager);
846
- loader.load(TestRootModule);
810
+ const { container, routeManager, loader } = await bootstrap(TestRootModule);
847
811
  for (const override of this.overrides) container.replaceProvider(override.provider);
848
- bindAppProviders(routeManager, container, loader);
849
812
  const app = new VelaApplication$1(container, routeManager);
850
813
  const instances = await loader.resolveAllInstances();
851
814
  app.setInstances(instances);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["VelaApplication"],"sources":["../src/http/path-utils.ts","../src/http/test-response.ts","../src/http/test-http-request.ts","../src/http/test-http-client.ts","../src/sse/test-sse-connection.ts","../src/sse/test-sse-request.ts","../src/testing-module.ts","../src/testing-module.builder.ts","../src/test.ts"],"sourcesContent":["// Ported from @stratal/testing (MIT, © Temitayo Fadojutimi).\n\n/**\n * Read the value at a dot-notation path (e.g. `data.user.id`).\n * Returns `undefined` when any segment along the way is null/undefined.\n */\nexport function getValueAtPath(obj: unknown, path: string): unknown {\n const parts = path.split('.');\n let current: unknown = obj;\n\n for (const part of parts) {\n if (current === null || current === undefined) {\n return undefined;\n }\n current = (current as Record<string, unknown>)[part];\n }\n\n return current;\n}\n\n/**\n * Whether a dot-notation path exists on the object, even when the value at the\n * path is `null`/`undefined`. Distinguishes \"key present but null\" from \"key\n * absent\".\n */\nexport function hasValueAtPath(obj: unknown, path: string): boolean {\n const parts = path.split('.');\n let current: unknown = obj;\n\n for (const part of parts) {\n if (current === null || current === undefined) {\n return false;\n }\n\n if (typeof current !== 'object') {\n return false;\n }\n\n const record = current as Record<string, unknown>;\n\n if (!(part in record)) {\n return false;\n }\n\n current = record[part];\n }\n\n return true;\n}\n","// Ported from @stratal/testing (MIT, © Temitayo Fadojutimi), minus Macroable —\n// vela has no Macroable, so TestResponse is a plain class.\nimport { expect } from 'vitest';\nimport { getValueAtPath, hasValueAtPath } from './path-utils.js';\n\n/**\n * TestResponse\n *\n * Wraps a `Response` with fluent, chainable assertions. Synchronous status /\n * header assertions return `this`; JSON assertions (which must read the body)\n * return `Promise<this>`.\n *\n * @example\n * ```ts\n * const res = await module.http.get('/users/1').send();\n * res.assertOk();\n * await res.assertJsonPath('data.id', 1);\n * ```\n */\nexport class TestResponse {\n private jsonData: unknown = null;\n private textData: string | null = null;\n\n constructor(private readonly response: Response) {}\n\n /** The raw `Response`. */\n get raw(): Response {\n return this.response;\n }\n\n /** The response status code. */\n get status(): number {\n return this.response.status;\n }\n\n /** The response headers. */\n get headers(): Headers {\n return this.response.headers;\n }\n\n /** Parse (and cache) the response body as JSON. */\n async json<T = unknown>(): Promise<T> {\n if (this.jsonData === null) {\n this.jsonData = await this.response.clone().json();\n }\n return this.jsonData as T;\n }\n\n /** Read (and cache) the response body as text. */\n async text(): Promise<string> {\n this.textData ??= await this.response.clone().text();\n return this.textData;\n }\n\n // ============================================================\n // Status assertions\n // ============================================================\n\n /** Assert status is 200 OK. */\n assertOk(): this {\n return this.assertStatus(200);\n }\n\n /** Assert status is 201 Created. */\n assertCreated(): this {\n return this.assertStatus(201);\n }\n\n /** Assert status is 204 No Content. */\n assertNoContent(): this {\n return this.assertStatus(204);\n }\n\n /** Assert status is 400 Bad Request. */\n assertBadRequest(): this {\n return this.assertStatus(400);\n }\n\n /** Assert status is 401 Unauthorized. */\n assertUnauthorized(): this {\n return this.assertStatus(401);\n }\n\n /** Assert status is 403 Forbidden. */\n assertForbidden(): this {\n return this.assertStatus(403);\n }\n\n /** Assert status is 404 Not Found. */\n assertNotFound(): this {\n return this.assertStatus(404);\n }\n\n /** Assert status is 422 Unprocessable Entity. */\n assertUnprocessable(): this {\n return this.assertStatus(422);\n }\n\n /** Assert status is 500 Internal Server Error. */\n assertServerError(): this {\n return this.assertStatus(500);\n }\n\n /** Assert the response has the given status code. */\n assertStatus(expected: number): this {\n expect(this.response.status, `Expected status ${expected}, got ${this.response.status}`).toBe(\n expected,\n );\n return this;\n }\n\n /** Assert the status is in the 2xx range. */\n assertSuccessful(): this {\n expect(\n this.response.status >= 200 && this.response.status < 300,\n `Expected successful status (2xx), got ${this.response.status}`,\n ).toBe(true);\n return this;\n }\n\n // ============================================================\n // JSON assertions\n // ============================================================\n\n /** Assert each key in `expected` equals the corresponding top-level value. */\n async assertJson(expected: Record<string, unknown>): Promise<this> {\n const actual = await this.json<Record<string, unknown>>();\n\n for (const [key, value] of Object.entries(expected)) {\n expect(\n actual[key],\n `Expected JSON key \"${key}\" to be ${JSON.stringify(value)}, got ${JSON.stringify(actual[key])}`,\n ).toStrictEqual(value);\n }\n\n return this;\n }\n\n /** Assert the value at a dot-notation path equals `expected`. */\n async assertJsonPath(path: string, expected: unknown): Promise<this> {\n const json = await this.json();\n const actual = getValueAtPath(json, path);\n\n expect(\n actual,\n `Expected JSON path \"${path}\" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,\n ).toStrictEqual(expected);\n\n return this;\n }\n\n /** Assert every path/value pair in `expectations` matches (batch assert). */\n async assertJsonPaths(expectations: Record<string, unknown>): Promise<this> {\n const json = await this.json();\n\n for (const [path, expected] of Object.entries(expectations)) {\n const actual = getValueAtPath(json, path);\n expect(\n actual,\n `Expected JSON path \"${path}\" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,\n ).toStrictEqual(expected);\n }\n\n return this;\n }\n\n /** Assert the top-level JSON object has every key in `structure`. */\n async assertJsonStructure(structure: string[]): Promise<this> {\n const json = await this.json<Record<string, unknown>>();\n\n for (const key of structure) {\n expect(\n key in json,\n `Expected JSON to have key \"${key}\", got keys: ${JSON.stringify(Object.keys(json))}`,\n ).toBe(true);\n }\n\n return this;\n }\n\n /** Assert a path exists (value may be anything, including `null`). */\n async assertJsonPathExists(path: string): Promise<this> {\n const json = await this.json();\n\n expect(hasValueAtPath(json, path), `Expected JSON path \"${path}\" to exist`).toBe(true);\n\n return this;\n }\n\n /** Assert a path does not exist. */\n async assertJsonPathMissing(path: string): Promise<this> {\n const json = await this.json();\n\n expect(hasValueAtPath(json, path), `Expected JSON path \"${path}\" to not exist`).toBe(false);\n\n return this;\n }\n\n /** Assert the value at a path satisfies a predicate. */\n async assertJsonPathMatches(path: string, matcher: (value: unknown) => boolean): Promise<this> {\n const json = await this.json();\n const value = getValueAtPath(json, path);\n\n expect(\n matcher(value),\n `Expected JSON path \"${path}\" to match predicate, got ${JSON.stringify(value)}`,\n ).toBe(true);\n\n return this;\n }\n\n /** Assert the string value at a path contains `substring`. */\n async assertJsonPathContains(path: string, substring: string): Promise<this> {\n const json = await this.json();\n const value = getValueAtPath(json, path);\n\n expect(\n typeof value === 'string',\n `Expected JSON path \"${path}\" to be a string, got ${typeof value}`,\n ).toBe(true);\n\n expect(\n (value as string).includes(substring),\n `Expected JSON path \"${path}\" to contain \"${substring}\", got \"${String(value)}\"`,\n ).toBe(true);\n\n return this;\n }\n\n /** Assert the array value at a path includes `item`. */\n async assertJsonPathIncludes(path: string, item: unknown): Promise<this> {\n const json = await this.json();\n const value = getValueAtPath(json, path);\n\n expect(\n Array.isArray(value),\n `Expected JSON path \"${path}\" to be an array, got ${typeof value}`,\n ).toBe(true);\n\n expect(\n (value as unknown[]).includes(item),\n `Expected JSON path \"${path}\" to include ${JSON.stringify(item)}`,\n ).toBe(true);\n\n return this;\n }\n\n /** Assert the array value at a path has `count` items. */\n async assertJsonPathCount(path: string, count: number): Promise<this> {\n const json = await this.json();\n const value = getValueAtPath(json, path);\n\n expect(\n Array.isArray(value),\n `Expected JSON path \"${path}\" to be an array, got ${typeof value}`,\n ).toBe(true);\n\n expect(\n (value as unknown[]).length,\n `Expected JSON path \"${path}\" to have ${count} items, got ${(value as unknown[]).length}`,\n ).toBe(count);\n\n return this;\n }\n\n // ============================================================\n // Header assertions\n // ============================================================\n\n /** Assert a header is present, optionally equal to `expected`. */\n assertHeader(name: string, expected?: string): this {\n const actual = this.response.headers.get(name);\n\n expect(actual !== null, `Expected header \"${name}\" to be present`).toBe(true);\n\n if (expected !== undefined) {\n expect(actual, `Expected header \"${name}\" to be \"${expected}\", got \"${actual}\"`).toBe(\n expected,\n );\n }\n\n return this;\n }\n\n /** Assert a header is absent. */\n assertHeaderMissing(name: string): this {\n const actual = this.response.headers.get(name);\n\n expect(actual, `Expected header \"${name}\" to be absent, but got \"${actual}\"`).toBeNull();\n\n return this;\n }\n}\n","// Adapted from @stratal/testing (MIT, © Temitayo Fadojutimi): stratal's hard\n// AuthService import is replaced by a generic auth-resolver seam so\n// @velajs/testing stays free of optional-package dependencies.\nimport type { ActingAsResolver, TestPrincipal, TestingModule } from '../testing-module.js';\nimport { TestResponse } from './test-response.js';\n\n/**\n * TestHttpRequest\n *\n * Fluent builder for a single test HTTP request. `send()` builds a `Request`\n * and drives it through `module.fetch()` (the full Hono pipeline).\n *\n * @example\n * ```ts\n * const res = await module.http\n * .post('/users')\n * .withBody({ name: 'A' })\n * .withHeaders({ 'X-Trace': '1' })\n * .send();\n * res.assertCreated();\n * ```\n */\nexport class TestHttpRequest {\n private body: unknown = undefined;\n private readonly requestHeaders: Headers;\n private principal: TestPrincipal | null = null;\n private resolver: ActingAsResolver | null = null;\n\n constructor(\n private readonly method: string,\n private readonly path: string,\n headers: Headers,\n private readonly module: TestingModule,\n private readonly host: string | null = null,\n ) {\n this.requestHeaders = new Headers(headers);\n }\n\n /** Set the request body (JSON-serialized on send). */\n withBody(data: unknown): this {\n this.body = data;\n return this;\n }\n\n /** Merge additional headers. */\n withHeaders(headers: Record<string, string>): this {\n for (const [key, value] of Object.entries(headers)) {\n this.requestHeaders.set(key, value);\n }\n return this;\n }\n\n /** Set `Content-Type: application/json`. */\n asJson(): this {\n this.requestHeaders.set('Content-Type', 'application/json');\n return this;\n }\n\n /**\n * Authenticate the request as `principal`. The `resolver` (or a default one\n * registered via `module.setAuthResolver`) turns the principal into request\n * headers. The resolver signature `(module, principal) => Promise<Headers>`\n * is the cross-package contract sibling packages (e.g. `@velajs/better-auth`)\n * build against.\n */\n actingAs(principal: TestPrincipal, resolver?: ActingAsResolver): this {\n this.principal = principal;\n this.resolver = resolver ?? null;\n return this;\n }\n\n /** Build the `Request` and send it through `module.fetch()`. */\n async send(): Promise<TestResponse> {\n await this.applyAuthentication();\n\n const hasBody = this.body !== undefined && this.body !== null;\n if (hasBody && !this.requestHeaders.has('Content-Type')) {\n this.requestHeaders.set('Content-Type', 'application/json');\n }\n\n const url = new URL(this.path, `http://${this.host ?? 'localhost'}`);\n const request = new Request(url.toString(), {\n method: this.method,\n headers: this.requestHeaders,\n body: hasBody ? JSON.stringify(this.body) : null,\n });\n\n const response = await this.module.fetch(request);\n return new TestResponse(response);\n }\n\n private async applyAuthentication(): Promise<void> {\n if (!this.principal) return;\n\n const resolver = this.resolver ?? this.module.getAuthResolver();\n if (!resolver) {\n throw new Error(\n 'actingAs() requires an auth resolver. Pass one explicitly — ' +\n 'actingAs(principal, resolver) — or register a default with ' +\n 'module.setAuthResolver(resolver). For better-auth: ' +\n 'import { actingAs } from \"@velajs/better-auth/testing\".',\n );\n }\n\n const headers = await resolver(this.module, this.principal);\n for (const [key, value] of headers.entries()) {\n this.requestHeaders.set(key, value);\n }\n }\n}\n","// Adapted from @stratal/testing (MIT, © Temitayo Fadojutimi). Stratal's i18n\n// `withLocale` is dropped (vela i18n differs — optional follow-up).\nimport type { TestingModule } from '../testing-module.js';\nimport { TestHttpRequest } from './test-http-request.js';\n\n/**\n * TestHttpClient\n *\n * Fluent entry point for test HTTP requests. `forHost`/`withHeaders` return a\n * new immutable client; the verb methods start a {@link TestHttpRequest}.\n *\n * @example\n * ```ts\n * const res = await module.http\n * .forHost('example.com')\n * .post('/users')\n * .withBody({ name: 'A' })\n * .send();\n * res.assertCreated();\n * ```\n */\nexport class TestHttpClient {\n constructor(\n private readonly module: TestingModule,\n private readonly host: string | null = null,\n private readonly defaultHeaders: Headers = new Headers(),\n ) {}\n\n /**\n * Return a new client bound to `host`. Also sets the `Host` header so domain\n * routing works even when the runtime reads the header rather than the URL.\n */\n forHost(host: string): TestHttpClient {\n const headers = new Headers(this.defaultHeaders);\n headers.set('Host', host);\n return new TestHttpClient(this.module, host, headers);\n }\n\n /** Return a new client with additional default headers on every request. */\n withHeaders(headers: Record<string, string>): TestHttpClient {\n const next = new Headers(this.defaultHeaders);\n for (const [key, value] of Object.entries(headers)) {\n next.set(key, value);\n }\n return new TestHttpClient(this.module, this.host, next);\n }\n\n get(path: string): TestHttpRequest {\n return this.createRequest('GET', path);\n }\n\n post(path: string): TestHttpRequest {\n return this.createRequest('POST', path);\n }\n\n put(path: string): TestHttpRequest {\n return this.createRequest('PUT', path);\n }\n\n patch(path: string): TestHttpRequest {\n return this.createRequest('PATCH', path);\n }\n\n delete(path: string): TestHttpRequest {\n return this.createRequest('DELETE', path);\n }\n\n private createRequest(method: string, path: string): TestHttpRequest {\n return new TestHttpRequest(method, path, this.defaultHeaders, this.module, this.host);\n }\n}\n","// Ported near-verbatim from @stratal/testing (MIT, © Temitayo Fadojutimi).\n// Web-standard only (ReadableStream + TextDecoder), so it is edge-pure.\nimport { expect } from 'vitest';\n\n/** A parsed Server-Sent Event. */\nexport interface TestSseEvent {\n data: string;\n event?: string;\n id?: string;\n retry?: number;\n}\n\n/**\n * TestSseConnection\n *\n * Reads a streaming `text/event-stream` response body and exposes queue-based\n * wait/assert helpers over the parsed events.\n *\n * @example\n * ```ts\n * const sse = await module.sse('/stream/events').connect();\n * await sse.assertEventData('ping');\n * await sse.waitForEnd();\n * ```\n */\nexport class TestSseConnection {\n private readonly eventQueue: TestSseEvent[] = [];\n private eventWaiters: ((event: TestSseEvent) => void)[] = [];\n private streamEnded = false;\n private endWaiters: (() => void)[] = [];\n\n constructor(private readonly response: Response) {\n this.startReading();\n }\n\n /** The raw `Response`. */\n get raw(): Response {\n return this.response;\n }\n\n /** Wait for the next event (rejects after `timeout` ms). */\n async waitForEvent(timeout = 5000): Promise<TestSseEvent> {\n if (this.eventQueue.length > 0) {\n return this.eventQueue.shift()!;\n }\n\n if (this.streamEnded) {\n throw new Error('SSE: stream has ended, no more events');\n }\n\n return new Promise<TestSseEvent>((resolve, reject) => {\n const waiter = (event: TestSseEvent): void => {\n clearTimeout(timer);\n resolve(event);\n };\n\n const timer = setTimeout(() => {\n const index = this.eventWaiters.indexOf(waiter);\n if (index !== -1) this.eventWaiters.splice(index, 1);\n reject(new Error(`SSE: no event received within ${timeout}ms`));\n }, timeout);\n\n this.eventWaiters.push(waiter);\n });\n }\n\n /** Wait for the stream to end (rejects after `timeout` ms). */\n async waitForEnd(timeout = 5000): Promise<void> {\n if (this.streamEnded) return;\n\n return new Promise<void>((resolve, reject) => {\n const waiter = (): void => {\n clearTimeout(timer);\n resolve();\n };\n\n const timer = setTimeout(() => {\n const index = this.endWaiters.indexOf(waiter);\n if (index !== -1) this.endWaiters.splice(index, 1);\n reject(new Error(`SSE: stream did not end within ${timeout}ms`));\n }, timeout);\n\n this.endWaiters.push(waiter);\n });\n }\n\n /** Collect all remaining events until the stream ends. */\n async collectEvents(timeout = 5000): Promise<TestSseEvent[]> {\n const events: TestSseEvent[] = [];\n\n if (this.streamEnded) {\n return [...this.eventQueue.splice(0)];\n }\n\n return new Promise<TestSseEvent[]>((resolve, reject) => {\n const originalDispatch = this.dispatchEvent.bind(this);\n this.dispatchEvent = (event: TestSseEvent): void => {\n events.push(event);\n originalDispatch(event);\n };\n\n const endWaiter = (): void => {\n clearTimeout(timer);\n this.dispatchEvent = originalDispatch;\n resolve(events);\n };\n\n const timer = setTimeout(() => {\n this.dispatchEvent = originalDispatch;\n const index = this.endWaiters.indexOf(endWaiter);\n if (index !== -1) this.endWaiters.splice(index, 1);\n reject(new Error(`SSE: stream did not end within ${timeout}ms`));\n }, timeout);\n\n events.push(...this.eventQueue.splice(0));\n\n this.endWaiters.push(endWaiter);\n });\n }\n\n /** Assert the next event matches the expected partial shape. */\n async assertEvent(expected: Partial<TestSseEvent>, timeout = 5000): Promise<void> {\n const event = await this.waitForEvent(timeout);\n expect(event).toMatchObject(expected);\n }\n\n /** Assert the next event's `data` equals `expected`. */\n async assertEventData(expected: string, timeout = 5000): Promise<void> {\n const event = await this.waitForEvent(timeout);\n expect(event.data, `Expected SSE data \"${expected}\", got \"${event.data}\"`).toBe(expected);\n }\n\n /** Assert the next event's `data` is JSON equal to `expected`. */\n async assertJsonEventData<T>(expected: T, timeout = 5000): Promise<void> {\n const event = await this.waitForEvent(timeout);\n const parsed = JSON.parse(event.data) as unknown;\n expect(parsed).toEqual(expected);\n }\n\n private startReading(): void {\n const body = this.response.body;\n if (!body) {\n this.streamEnded = true;\n return;\n }\n\n const reader = body.getReader() as ReadableStreamDefaultReader<Uint8Array>;\n const decoder = new TextDecoder();\n let buffer = '';\n\n const read = async (): Promise<void> => {\n try {\n for (;;) {\n const { done, value } = await reader.read();\n\n if (done) {\n if (buffer.trim()) {\n const event = this.parseEvent(buffer);\n if (event) this.dispatchEvent(event);\n }\n this.endStream();\n return;\n }\n\n buffer += decoder.decode(value, { stream: true });\n\n const parts = buffer.split('\\n\\n');\n buffer = parts.pop()!;\n\n for (const part of parts) {\n if (!part.trim()) continue;\n const event = this.parseEvent(part);\n if (event) this.dispatchEvent(event);\n }\n }\n } catch {\n this.endStream();\n }\n };\n\n void read();\n }\n\n private endStream(): void {\n this.streamEnded = true;\n for (const waiter of this.endWaiters) {\n waiter();\n }\n this.endWaiters = [];\n }\n\n private parseEvent(raw: string): TestSseEvent | null {\n const lines = raw.split('\\n');\n const dataLines: string[] = [];\n let event: string | undefined;\n let id: string | undefined;\n let retry: number | undefined;\n\n for (const line of lines) {\n if (line.startsWith(':')) continue; // comment line\n\n const colonIndex = line.indexOf(':');\n if (colonIndex === -1) continue;\n\n const field = line.slice(0, colonIndex);\n const value =\n line[colonIndex + 1] === ' ' ? line.slice(colonIndex + 2) : line.slice(colonIndex + 1);\n\n switch (field) {\n case 'data':\n dataLines.push(value);\n break;\n case 'event':\n event = value;\n break;\n case 'id':\n id = value;\n break;\n case 'retry': {\n const parsed = parseInt(value, 10);\n if (!Number.isNaN(parsed)) retry = parsed;\n break;\n }\n }\n }\n\n if (dataLines.length === 0) return null;\n\n const result: TestSseEvent = { data: dataLines.join('\\n') };\n if (event !== undefined) result.event = event;\n if (id !== undefined) result.id = id;\n if (retry !== undefined) result.retry = retry;\n\n return result;\n }\n\n private dispatchEvent(event: TestSseEvent): void {\n if (this.eventWaiters.length > 0) {\n this.eventWaiters.shift()!(event);\n } else {\n this.eventQueue.push(event);\n }\n }\n}\n","// Adapted from @stratal/testing (MIT, © Temitayo Fadojutimi). Auth uses the\n// generic resolver seam instead of a hard AuthService import.\nimport { expect } from 'vitest';\nimport type { ActingAsResolver, TestPrincipal, TestingModule } from '../testing-module.js';\nimport { TestSseConnection } from './test-sse-connection.js';\n\n/**\n * TestSseRequest\n *\n * Builder for a Server-Sent Events connection. `connect()` issues a GET through\n * `module.fetch()`, asserts a `text/event-stream` 200, and wraps the streaming\n * body in a {@link TestSseConnection}.\n *\n * @example\n * ```ts\n * const sse = await module.sse('/stream/events').connect();\n * await sse.assertEvent({ event: 'message', data: 'hello' });\n * ```\n */\nexport class TestSseRequest {\n private readonly requestHeaders = new Headers();\n private principal: TestPrincipal | null = null;\n private resolver: ActingAsResolver | null = null;\n\n constructor(\n private readonly path: string,\n private readonly module: TestingModule,\n ) {}\n\n /** Merge additional headers onto the SSE request. */\n withHeaders(headers: Record<string, string>): this {\n for (const [key, value] of Object.entries(headers)) {\n this.requestHeaders.set(key, value);\n }\n return this;\n }\n\n /** Authenticate the connection (see {@link TestHttpRequest.actingAs}). */\n actingAs(principal: TestPrincipal, resolver?: ActingAsResolver): this {\n this.principal = principal;\n this.resolver = resolver ?? null;\n return this;\n }\n\n /** Open the stream and return a live {@link TestSseConnection}. */\n async connect(): Promise<TestSseConnection> {\n await this.applyAuthentication();\n\n this.requestHeaders.set('Accept', 'text/event-stream');\n\n const url = new URL(this.path, 'http://localhost');\n const request = new Request(url.toString(), { headers: this.requestHeaders });\n\n const response = await this.module.fetch(request);\n\n expect(response.status, `Expected status 200, got ${response.status}`).toBe(200);\n\n const contentType = response.headers.get('content-type') ?? '';\n expect(\n contentType.includes('text/event-stream'),\n `Expected content-type \"text/event-stream\", got \"${contentType}\"`,\n ).toBe(true);\n\n return new TestSseConnection(response);\n }\n\n private async applyAuthentication(): Promise<void> {\n if (!this.principal) return;\n\n const resolver = this.resolver ?? this.module.getAuthResolver();\n if (!resolver) {\n throw new Error(\n 'actingAs() requires an auth resolver. Pass one explicitly or register ' +\n 'a default with module.setAuthResolver(resolver).',\n );\n }\n\n const headers = await resolver(this.module, this.principal);\n for (const [key, value] of headers.entries()) {\n this.requestHeaders.set(key, value);\n }\n }\n}\n","import { Context } from 'hono';\nimport {\n REQUEST_CONTEXT,\n type RequestContext,\n type Token,\n type Type,\n type VelaApplication,\n} from '@velajs/vela';\nimport type { Container } from '@velajs/vela/internal';\nimport { SeederRegistry, type ISeeder } from '@velajs/vela/seeder';\nimport { expect } from 'vitest';\nimport type { TestDatabase } from './db/test-database.js';\nimport { TestHttpClient } from './http/test-http-client.js';\nimport { TestSseRequest } from './sse/test-sse-request.js';\nimport { TestWsRequest } from './ws/test-ws-request.js';\n\n/** A test principal — an opaque object the auth resolver turns into headers. */\nexport type TestPrincipal = Record<string, unknown>;\n\n/**\n * Turns a principal into request headers (session cookie, bearer token, …).\n * The signature `(module, principal) => Promise<Headers>` is a cross-package\n * contract: sibling packages (e.g. `@velajs/better-auth/testing`) build a\n * resolver against it. Kept generic so `@velajs/testing` needs no auth deps.\n */\nexport type ActingAsResolver = (\n module: TestingModule,\n principal: TestPrincipal,\n) => Promise<Headers>;\n\ntype HonoApp = ReturnType<VelaApplication['getHonoApp']>;\n\n/**\n * TestingModule\n *\n * The compiled test harness. Beyond `get`/`createApplication`/`close`, it adds\n * Laravel-flavored ergonomics: a fluent HTTP client, SSE/WS builders, request-\n * scope execution, seeding, and database assertion wrappers.\n *\n * @example\n * ```ts\n * const module = await Test.createTestingModule({ imports: [AppModule] }).compile();\n * await module.http.post('/users').withBody({ name: 'A' }).send()\n * .then((r) => r.assertCreated());\n * ```\n */\nexport class TestingModule {\n private _http: TestHttpClient | null = null;\n private honoApp: HonoApp | null = null;\n private authResolver: ActingAsResolver | null = null;\n\n constructor(\n private readonly app: VelaApplication,\n private readonly container: Container,\n ) {}\n\n /** Resolve a provider from the root container. */\n get<T>(token: Token<T>): T {\n return this.container.resolve(token);\n }\n\n /** Build (once) and return the underlying application. */\n async createApplication(): Promise<VelaApplication> {\n await this.app.initRoutes();\n return this.app;\n }\n\n /** Lazy fluent HTTP client bound to this module. */\n get http(): TestHttpClient {\n this._http ??= new TestHttpClient(this);\n return this._http;\n }\n\n /** Start an SSE connection builder for `path`. */\n sse(path: string): TestSseRequest {\n return new TestSseRequest(path, this);\n }\n\n /** Start a WebSocket connection builder for `path` (needs a transport adapter). */\n ws(path: string): TestWsRequest {\n return new TestWsRequest(path, this);\n }\n\n /**\n * Drive a `Request` through the full Hono pipeline. The Hono app is built\n * once and reused across requests.\n */\n async fetch(request: Request, env?: unknown, ctx?: unknown): Promise<Response> {\n const hono = await this.ensureHono();\n return hono.fetch(request, env as never, ctx as never);\n }\n\n /**\n * Register a default auth resolver used by `actingAs(principal)` when no\n * resolver is passed explicitly.\n */\n setAuthResolver(resolver: ActingAsResolver): this {\n this.authResolver = resolver;\n return this;\n }\n\n /** The default auth resolver, if one was registered. */\n getAuthResolver(): ActingAsResolver | null {\n return this.authResolver;\n }\n\n /**\n * Run `callback` inside a request-scoped child container seeded with a mock\n * {@link RequestContext}, so REQUEST-scoped providers (and anything injecting\n * `REQUEST_CONTEXT`) resolve. The child is disposed afterwards.\n */\n async runInRequestScope<T>(callback: (container: Container) => T | Promise<T>): Promise<T> {\n const child = this.container.createChild();\n child.setRequestInstance(REQUEST_CONTEXT, this.createMockRequestContext());\n try {\n return await callback(child);\n } finally {\n await child.dispose();\n }\n }\n\n /**\n * Run the given `@Seeder()` classes, each in its own request scope. Throws if\n * a class is not a registered seeder. Requires `SeederModule` (or the seeders\n * themselves) to be present in the module graph.\n */\n async seed(...SeederClasses: Type<ISeeder>[]): Promise<void> {\n const registry = this.container.resolve(SeederRegistry);\n const known = new Set<unknown>(registry.list().map((s) => s.target));\n\n for (const SeederClass of SeederClasses) {\n if (!known.has(SeederClass)) {\n throw new Error(\n `Seeder \"${SeederClass.name}\" is not registered. Add it to a module's ` +\n 'providers or SeederModule.forRoot({ seeders: [...] }).',\n );\n }\n await this.runInRequestScope(async (child) => {\n const instance = child.resolve<ISeeder>(SeederClass);\n await instance.run();\n });\n }\n }\n\n /** Assert a row matching `where` exists in `table` (via a {@link TestDatabase}). */\n async assertDatabaseHas(\n db: TestDatabase,\n table: string,\n where: Record<string, unknown>,\n ): Promise<void> {\n const exists = await db.has(table, where);\n expect(exists, `Expected ${table} to have a row matching ${JSON.stringify(where)}`).toBe(true);\n }\n\n /** Assert no row matching `where` exists in `table`. */\n async assertDatabaseMissing(\n db: TestDatabase,\n table: string,\n where: Record<string, unknown>,\n ): Promise<void> {\n const exists = await db.has(table, where);\n expect(exists, `Expected ${table} NOT to have a row matching ${JSON.stringify(where)}`).toBe(\n false,\n );\n }\n\n /** Assert `table` has exactly `expected` rows. */\n async assertDatabaseCount(db: TestDatabase, table: string, expected: number): Promise<void> {\n const actual = await db.count(table);\n expect(actual, `Expected ${table} count ${expected}, got ${actual}`).toBe(expected);\n }\n\n /** Dispose the application. */\n async close(signal?: string): Promise<void> {\n await this.app.close(signal);\n }\n\n private async ensureHono(): Promise<HonoApp> {\n if (!this.honoApp) {\n const app = await this.createApplication();\n this.honoApp = app.getHonoApp();\n }\n return this.honoApp;\n }\n\n /**\n * Build a minimal, functional {@link RequestContext} for out-of-band request\n * scopes. Vela has no `createMockRouterContext`; a real (empty) Hono `Context`\n * backs the `hono` field so nothing dangles.\n */\n private createMockRequestContext(): RequestContext {\n const bag = new Map<string | symbol, unknown>();\n const request = new Request('http://localhost/');\n const hono = new Context(request);\n return {\n id: crypto.randomUUID(),\n receivedAt: new Date(),\n request,\n hono,\n set(key, value) {\n bag.set(key, value);\n },\n get<V>(key: string | symbol): V | undefined {\n return bag.get(key) as V | undefined;\n },\n has(key) {\n return bag.has(key);\n },\n };\n }\n}\n","import {\n DiscoveryService,\n REQUEST_CONTEXT,\n Scope,\n type ModuleOptions,\n type ProviderOptions,\n type Token,\n type Type,\n} from '@velajs/vela';\nimport {\n APP_FILTER,\n APP_GUARD,\n APP_INTERCEPTOR,\n APP_MIDDLEWARE,\n APP_PIPE,\n Container,\n MetadataRegistry,\n ModuleLoader,\n ModuleRef,\n RouteManager,\n VelaApplication,\n bindAppProviders,\n} from '@velajs/vela/internal';\nimport { TestingModule } from './testing-module.js';\n\ninterface OverrideEntry {\n token: Token;\n provider: ProviderOptions;\n}\n\nexport class OverrideBy {\n constructor(\n private readonly builder: TestingModuleBuilder,\n private readonly token: Token,\n ) {}\n\n useValue(value: unknown): TestingModuleBuilder {\n this.builder['addOverride']({\n token: this.token,\n provider: { provide: this.token, useValue: value },\n });\n return this.builder;\n }\n\n useClass(cls: Type): TestingModuleBuilder {\n this.builder['addOverride']({\n token: this.token,\n provider: { provide: this.token, useClass: cls },\n });\n return this.builder;\n }\n\n useFactory(options: {\n factory: (...args: unknown[]) => unknown;\n inject?: Token[];\n }): TestingModuleBuilder {\n this.builder['addOverride']({\n token: this.token,\n provider: {\n provide: this.token,\n useFactory: options.factory,\n inject: options.inject,\n },\n });\n return this.builder;\n }\n}\n\nexport class TestingModuleBuilder {\n private overrides: OverrideEntry[] = [];\n\n constructor(private readonly metadata: ModuleOptions) {}\n\n overrideProvider(token: Token): OverrideBy {\n return new OverrideBy(this, token);\n }\n\n overrideGuard(guard: Type): OverrideBy {\n return new OverrideBy(this, guard);\n }\n\n overridePipe(pipe: Type): OverrideBy {\n return new OverrideBy(this, pipe);\n }\n\n overrideInterceptor(interceptor: Type): OverrideBy {\n return new OverrideBy(this, interceptor);\n }\n\n overrideFilter(filter: Type): OverrideBy {\n return new OverrideBy(this, filter);\n }\n\n private addOverride(entry: OverrideEntry): void {\n const idx = this.overrides.findIndex((o) => o.token === entry.token);\n if (idx !== -1) {\n this.overrides[idx] = entry;\n } else {\n this.overrides.push(entry);\n }\n }\n\n async compile(): Promise<TestingModule> {\n class TestRootModule {}\n MetadataRegistry.setModuleOptions(TestRootModule, {\n imports: this.metadata.imports,\n providers: this.metadata.providers,\n controllers: this.metadata.controllers,\n exports: this.metadata.exports,\n });\n\n const container = new Container();\n\n // Replicate bootstrap()'s global token setup so request-pipeline tests\n // can resolve REQUEST_CONTEXT, ModuleRef, and the APP_* sentinel tokens\n // from any module scope. Without this, guards / decorators that inject\n // REQUEST_CONTEXT through ExecutionContext fail with \"no provider\"\n // when the request enters the pipeline. See vela/src/factory/bootstrap.ts.\n container.register({ provide: Container, useValue: container });\n container.markGlobalToken(Container);\n\n container.register({\n provide: ModuleRef,\n useFactory: (c: Container) => new ModuleRef(c),\n inject: [Container],\n });\n container.markGlobalToken(ModuleRef);\n\n // Decorator-driven discovery — global so any provider can inject it, and so\n // callOnApplicationBootstrap() reuses this instance instead of self-building\n // one. Mirrors vela/src/factory/bootstrap.ts.\n container.register({\n provide: DiscoveryService,\n useFactory: (c: Container) => new DiscoveryService(c),\n inject: [Container],\n });\n container.markGlobalToken(DiscoveryService);\n\n for (const t of [APP_GUARD, APP_PIPE, APP_INTERCEPTOR, APP_FILTER, APP_MIDDLEWARE]) {\n container.markGlobalToken(t);\n }\n\n // REQUEST_CONTEXT is seeded into each per-request child container by\n // RouteManager via setRequestInstance. Registering with a throw-factory\n // here ensures findRegistration succeeds (so the child's cached request\n // instance is returned) while surfacing a clear error if the token is\n // ever resolved outside the request path.\n container.register({\n provide: REQUEST_CONTEXT,\n scope: Scope.REQUEST,\n useFactory: () => {\n throw new Error(\n 'REQUEST_CONTEXT can only be resolved inside a request — ' +\n 'it is seeded by RouteManager when the request enters the pipeline.',\n );\n },\n });\n container.markGlobalToken(REQUEST_CONTEXT);\n\n // Pre-register at root so `moduleRef.get(token)` and framework-internal\n // resolves (instantiate(guard, container) with no requestingModuleId) see\n // the override immediately.\n for (const override of this.overrides) {\n container.register(override.provider);\n }\n\n const routeManager = new RouteManager(container);\n\n const loader = new ModuleLoader(container, routeManager);\n loader.load(TestRootModule);\n\n // Force-apply overrides into every module bucket that already holds the\n // token (plus root). Without this, controller constructor-injection (which\n // passes requestingModuleId to findRegistration) finds the module's own\n // registration first and never consults the root override. The default\n // 'all-existing' buckets replace every non-root bucket holding the token\n // and re-register at root — the supported form of the old private loop.\n for (const override of this.overrides) {\n container.replaceProvider(override.provider);\n }\n\n bindAppProviders(routeManager, container, loader);\n\n const app = new VelaApplication(container, routeManager);\n const instances = await loader.resolveAllInstances();\n app.setInstances(instances);\n\n await app.callOnModuleInit();\n await app.callOnApplicationBootstrap();\n await app.initRoutes();\n\n return new TestingModule(app, container);\n }\n}\n","import type { ModuleOptions } from '@velajs/vela';\nimport { TestingModuleBuilder } from './testing-module.builder.js';\n\nexport const Test = {\n createTestingModule(metadata: ModuleOptions): TestingModuleBuilder {\n return new TestingModuleBuilder(metadata);\n },\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["VelaApplication"],"sources":["../src/http/path-utils.ts","../src/http/test-response.ts","../src/http/test-http-request.ts","../src/http/test-http-client.ts","../src/sse/test-sse-connection.ts","../src/sse/test-sse-request.ts","../src/testing-module.ts","../src/testing-module.builder.ts","../src/test.ts"],"sourcesContent":["// Ported from @stratal/testing (MIT, © Temitayo Fadojutimi).\n\n/**\n * Read the value at a dot-notation path (e.g. `data.user.id`).\n * Returns `undefined` when any segment along the way is null/undefined.\n */\nexport function getValueAtPath(obj: unknown, path: string): unknown {\n const parts = path.split('.');\n let current: unknown = obj;\n\n for (const part of parts) {\n if (current === null || current === undefined) {\n return undefined;\n }\n current = (current as Record<string, unknown>)[part];\n }\n\n return current;\n}\n\n/**\n * Whether a dot-notation path exists on the object, even when the value at the\n * path is `null`/`undefined`. Distinguishes \"key present but null\" from \"key\n * absent\".\n */\nexport function hasValueAtPath(obj: unknown, path: string): boolean {\n const parts = path.split('.');\n let current: unknown = obj;\n\n for (const part of parts) {\n if (current === null || current === undefined) {\n return false;\n }\n\n if (typeof current !== 'object') {\n return false;\n }\n\n const record = current as Record<string, unknown>;\n\n if (!(part in record)) {\n return false;\n }\n\n current = record[part];\n }\n\n return true;\n}\n","// Ported from @stratal/testing (MIT, © Temitayo Fadojutimi), minus Macroable —\n// vela has no Macroable, so TestResponse is a plain class.\nimport { expect } from 'vitest';\nimport { getValueAtPath, hasValueAtPath } from './path-utils.js';\n\n/**\n * TestResponse\n *\n * Wraps a `Response` with fluent, chainable assertions. Synchronous status /\n * header assertions return `this`; JSON assertions (which must read the body)\n * return `Promise<this>`.\n *\n * @example\n * ```ts\n * const res = await module.http.get('/users/1').send();\n * res.assertOk();\n * await res.assertJsonPath('data.id', 1);\n * ```\n */\nexport class TestResponse {\n private jsonData: unknown = null;\n private textData: string | null = null;\n\n constructor(private readonly response: Response) {}\n\n /** The raw `Response`. */\n get raw(): Response {\n return this.response;\n }\n\n /** The response status code. */\n get status(): number {\n return this.response.status;\n }\n\n /** The response headers. */\n get headers(): Headers {\n return this.response.headers;\n }\n\n /** Parse (and cache) the response body as JSON. */\n async json<T = unknown>(): Promise<T> {\n if (this.jsonData === null) {\n this.jsonData = await this.response.clone().json();\n }\n return this.jsonData as T;\n }\n\n /** Read (and cache) the response body as text. */\n async text(): Promise<string> {\n this.textData ??= await this.response.clone().text();\n return this.textData;\n }\n\n // ============================================================\n // Status assertions\n // ============================================================\n\n /** Assert status is 200 OK. */\n assertOk(): this {\n return this.assertStatus(200);\n }\n\n /** Assert status is 201 Created. */\n assertCreated(): this {\n return this.assertStatus(201);\n }\n\n /** Assert status is 204 No Content. */\n assertNoContent(): this {\n return this.assertStatus(204);\n }\n\n /** Assert status is 400 Bad Request. */\n assertBadRequest(): this {\n return this.assertStatus(400);\n }\n\n /** Assert status is 401 Unauthorized. */\n assertUnauthorized(): this {\n return this.assertStatus(401);\n }\n\n /** Assert status is 403 Forbidden. */\n assertForbidden(): this {\n return this.assertStatus(403);\n }\n\n /** Assert status is 404 Not Found. */\n assertNotFound(): this {\n return this.assertStatus(404);\n }\n\n /** Assert status is 422 Unprocessable Entity. */\n assertUnprocessable(): this {\n return this.assertStatus(422);\n }\n\n /** Assert status is 500 Internal Server Error. */\n assertServerError(): this {\n return this.assertStatus(500);\n }\n\n /** Assert the response has the given status code. */\n assertStatus(expected: number): this {\n expect(this.response.status, `Expected status ${expected}, got ${this.response.status}`).toBe(\n expected,\n );\n return this;\n }\n\n /** Assert the status is in the 2xx range. */\n assertSuccessful(): this {\n expect(\n this.response.status >= 200 && this.response.status < 300,\n `Expected successful status (2xx), got ${this.response.status}`,\n ).toBe(true);\n return this;\n }\n\n // ============================================================\n // JSON assertions\n // ============================================================\n\n /** Assert each key in `expected` equals the corresponding top-level value. */\n async assertJson(expected: Record<string, unknown>): Promise<this> {\n const actual = await this.json<Record<string, unknown>>();\n\n for (const [key, value] of Object.entries(expected)) {\n expect(\n actual[key],\n `Expected JSON key \"${key}\" to be ${JSON.stringify(value)}, got ${JSON.stringify(actual[key])}`,\n ).toStrictEqual(value);\n }\n\n return this;\n }\n\n /** Assert the value at a dot-notation path equals `expected`. */\n async assertJsonPath(path: string, expected: unknown): Promise<this> {\n const json = await this.json();\n const actual = getValueAtPath(json, path);\n\n expect(\n actual,\n `Expected JSON path \"${path}\" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,\n ).toStrictEqual(expected);\n\n return this;\n }\n\n /** Assert every path/value pair in `expectations` matches (batch assert). */\n async assertJsonPaths(expectations: Record<string, unknown>): Promise<this> {\n const json = await this.json();\n\n for (const [path, expected] of Object.entries(expectations)) {\n const actual = getValueAtPath(json, path);\n expect(\n actual,\n `Expected JSON path \"${path}\" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,\n ).toStrictEqual(expected);\n }\n\n return this;\n }\n\n /** Assert the top-level JSON object has every key in `structure`. */\n async assertJsonStructure(structure: string[]): Promise<this> {\n const json = await this.json<Record<string, unknown>>();\n\n for (const key of structure) {\n expect(\n key in json,\n `Expected JSON to have key \"${key}\", got keys: ${JSON.stringify(Object.keys(json))}`,\n ).toBe(true);\n }\n\n return this;\n }\n\n /** Assert a path exists (value may be anything, including `null`). */\n async assertJsonPathExists(path: string): Promise<this> {\n const json = await this.json();\n\n expect(hasValueAtPath(json, path), `Expected JSON path \"${path}\" to exist`).toBe(true);\n\n return this;\n }\n\n /** Assert a path does not exist. */\n async assertJsonPathMissing(path: string): Promise<this> {\n const json = await this.json();\n\n expect(hasValueAtPath(json, path), `Expected JSON path \"${path}\" to not exist`).toBe(false);\n\n return this;\n }\n\n /** Assert the value at a path satisfies a predicate. */\n async assertJsonPathMatches(path: string, matcher: (value: unknown) => boolean): Promise<this> {\n const json = await this.json();\n const value = getValueAtPath(json, path);\n\n expect(\n matcher(value),\n `Expected JSON path \"${path}\" to match predicate, got ${JSON.stringify(value)}`,\n ).toBe(true);\n\n return this;\n }\n\n /** Assert the string value at a path contains `substring`. */\n async assertJsonPathContains(path: string, substring: string): Promise<this> {\n const json = await this.json();\n const value = getValueAtPath(json, path);\n\n expect(\n typeof value === 'string',\n `Expected JSON path \"${path}\" to be a string, got ${typeof value}`,\n ).toBe(true);\n\n expect(\n (value as string).includes(substring),\n `Expected JSON path \"${path}\" to contain \"${substring}\", got \"${String(value)}\"`,\n ).toBe(true);\n\n return this;\n }\n\n /** Assert the array value at a path includes `item`. */\n async assertJsonPathIncludes(path: string, item: unknown): Promise<this> {\n const json = await this.json();\n const value = getValueAtPath(json, path);\n\n expect(\n Array.isArray(value),\n `Expected JSON path \"${path}\" to be an array, got ${typeof value}`,\n ).toBe(true);\n\n expect(\n (value as unknown[]).includes(item),\n `Expected JSON path \"${path}\" to include ${JSON.stringify(item)}`,\n ).toBe(true);\n\n return this;\n }\n\n /** Assert the array value at a path has `count` items. */\n async assertJsonPathCount(path: string, count: number): Promise<this> {\n const json = await this.json();\n const value = getValueAtPath(json, path);\n\n expect(\n Array.isArray(value),\n `Expected JSON path \"${path}\" to be an array, got ${typeof value}`,\n ).toBe(true);\n\n expect(\n (value as unknown[]).length,\n `Expected JSON path \"${path}\" to have ${count} items, got ${(value as unknown[]).length}`,\n ).toBe(count);\n\n return this;\n }\n\n // ============================================================\n // Header assertions\n // ============================================================\n\n /** Assert a header is present, optionally equal to `expected`. */\n assertHeader(name: string, expected?: string): this {\n const actual = this.response.headers.get(name);\n\n expect(actual !== null, `Expected header \"${name}\" to be present`).toBe(true);\n\n if (expected !== undefined) {\n expect(actual, `Expected header \"${name}\" to be \"${expected}\", got \"${actual}\"`).toBe(\n expected,\n );\n }\n\n return this;\n }\n\n /** Assert a header is absent. */\n assertHeaderMissing(name: string): this {\n const actual = this.response.headers.get(name);\n\n expect(actual, `Expected header \"${name}\" to be absent, but got \"${actual}\"`).toBeNull();\n\n return this;\n }\n}\n","// Adapted from @stratal/testing (MIT, © Temitayo Fadojutimi): stratal's hard\n// AuthService import is replaced by a generic auth-resolver seam so\n// @velajs/testing stays free of optional-package dependencies.\nimport type { ActingAsResolver, TestPrincipal, TestingModule } from '../testing-module.js';\nimport { TestResponse } from './test-response.js';\n\n/**\n * TestHttpRequest\n *\n * Fluent builder for a single test HTTP request. `send()` builds a `Request`\n * and drives it through `module.fetch()` (the full Hono pipeline).\n *\n * @example\n * ```ts\n * const res = await module.http\n * .post('/users')\n * .withBody({ name: 'A' })\n * .withHeaders({ 'X-Trace': '1' })\n * .send();\n * res.assertCreated();\n * ```\n */\nexport class TestHttpRequest {\n private body: unknown = undefined;\n private readonly requestHeaders: Headers;\n private principal: TestPrincipal | null = null;\n private resolver: ActingAsResolver | null = null;\n\n constructor(\n private readonly method: string,\n private readonly path: string,\n headers: Headers,\n private readonly module: TestingModule,\n private readonly host: string | null = null,\n ) {\n this.requestHeaders = new Headers(headers);\n }\n\n /** Set the request body (JSON-serialized on send). */\n withBody(data: unknown): this {\n this.body = data;\n return this;\n }\n\n /** Merge additional headers. */\n withHeaders(headers: Record<string, string>): this {\n for (const [key, value] of Object.entries(headers)) {\n this.requestHeaders.set(key, value);\n }\n return this;\n }\n\n /** Set `Content-Type: application/json`. */\n asJson(): this {\n this.requestHeaders.set('Content-Type', 'application/json');\n return this;\n }\n\n /**\n * Authenticate the request as `principal`. The `resolver` (or a default one\n * registered via `module.setAuthResolver`) turns the principal into request\n * headers. The resolver signature `(module, principal) => Promise<Headers>`\n * is the cross-package contract sibling packages (e.g. `@velajs/better-auth`)\n * build against.\n */\n actingAs(principal: TestPrincipal, resolver?: ActingAsResolver): this {\n this.principal = principal;\n this.resolver = resolver ?? null;\n return this;\n }\n\n /** Build the `Request` and send it through `module.fetch()`. */\n async send(): Promise<TestResponse> {\n await this.applyAuthentication();\n\n const hasBody = this.body !== undefined && this.body !== null;\n if (hasBody && !this.requestHeaders.has('Content-Type')) {\n this.requestHeaders.set('Content-Type', 'application/json');\n }\n\n const url = new URL(this.path, `http://${this.host ?? 'localhost'}`);\n const request = new Request(url.toString(), {\n method: this.method,\n headers: this.requestHeaders,\n body: hasBody ? JSON.stringify(this.body) : null,\n });\n\n const response = await this.module.fetch(request);\n return new TestResponse(response);\n }\n\n private async applyAuthentication(): Promise<void> {\n if (!this.principal) return;\n\n const resolver = this.resolver ?? this.module.getAuthResolver();\n if (!resolver) {\n throw new Error(\n 'actingAs() requires an auth resolver. Pass one explicitly — ' +\n 'actingAs(principal, resolver) — or register a default with ' +\n 'module.setAuthResolver(resolver). For better-auth: ' +\n 'import { actingAs } from \"@velajs/better-auth/testing\".',\n );\n }\n\n const headers = await resolver(this.module, this.principal);\n for (const [key, value] of headers.entries()) {\n this.requestHeaders.set(key, value);\n }\n }\n}\n","// Adapted from @stratal/testing (MIT, © Temitayo Fadojutimi). Stratal's i18n\n// `withLocale` is dropped (vela i18n differs — optional follow-up).\nimport type { TestingModule } from '../testing-module.js';\nimport { TestHttpRequest } from './test-http-request.js';\n\n/**\n * TestHttpClient\n *\n * Fluent entry point for test HTTP requests. `forHost`/`withHeaders` return a\n * new immutable client; the verb methods start a {@link TestHttpRequest}.\n *\n * @example\n * ```ts\n * const res = await module.http\n * .forHost('example.com')\n * .post('/users')\n * .withBody({ name: 'A' })\n * .send();\n * res.assertCreated();\n * ```\n */\nexport class TestHttpClient {\n constructor(\n private readonly module: TestingModule,\n private readonly host: string | null = null,\n private readonly defaultHeaders: Headers = new Headers(),\n ) {}\n\n /**\n * Return a new client bound to `host`. Also sets the `Host` header so domain\n * routing works even when the runtime reads the header rather than the URL.\n */\n forHost(host: string): TestHttpClient {\n const headers = new Headers(this.defaultHeaders);\n headers.set('Host', host);\n return new TestHttpClient(this.module, host, headers);\n }\n\n /** Return a new client with additional default headers on every request. */\n withHeaders(headers: Record<string, string>): TestHttpClient {\n const next = new Headers(this.defaultHeaders);\n for (const [key, value] of Object.entries(headers)) {\n next.set(key, value);\n }\n return new TestHttpClient(this.module, this.host, next);\n }\n\n get(path: string): TestHttpRequest {\n return this.createRequest('GET', path);\n }\n\n post(path: string): TestHttpRequest {\n return this.createRequest('POST', path);\n }\n\n put(path: string): TestHttpRequest {\n return this.createRequest('PUT', path);\n }\n\n patch(path: string): TestHttpRequest {\n return this.createRequest('PATCH', path);\n }\n\n delete(path: string): TestHttpRequest {\n return this.createRequest('DELETE', path);\n }\n\n private createRequest(method: string, path: string): TestHttpRequest {\n return new TestHttpRequest(method, path, this.defaultHeaders, this.module, this.host);\n }\n}\n","// Ported near-verbatim from @stratal/testing (MIT, © Temitayo Fadojutimi).\n// Web-standard only (ReadableStream + TextDecoder), so it is edge-pure.\nimport { expect } from 'vitest';\n\n/** A parsed Server-Sent Event. */\nexport interface TestSseEvent {\n data: string;\n event?: string;\n id?: string;\n retry?: number;\n}\n\n/**\n * TestSseConnection\n *\n * Reads a streaming `text/event-stream` response body and exposes queue-based\n * wait/assert helpers over the parsed events.\n *\n * @example\n * ```ts\n * const sse = await module.sse('/stream/events').connect();\n * await sse.assertEventData('ping');\n * await sse.waitForEnd();\n * ```\n */\nexport class TestSseConnection {\n private readonly eventQueue: TestSseEvent[] = [];\n private eventWaiters: ((event: TestSseEvent) => void)[] = [];\n private streamEnded = false;\n private endWaiters: (() => void)[] = [];\n\n constructor(private readonly response: Response) {\n this.startReading();\n }\n\n /** The raw `Response`. */\n get raw(): Response {\n return this.response;\n }\n\n /** Wait for the next event (rejects after `timeout` ms). */\n async waitForEvent(timeout = 5000): Promise<TestSseEvent> {\n if (this.eventQueue.length > 0) {\n return this.eventQueue.shift()!;\n }\n\n if (this.streamEnded) {\n throw new Error('SSE: stream has ended, no more events');\n }\n\n return new Promise<TestSseEvent>((resolve, reject) => {\n const waiter = (event: TestSseEvent): void => {\n clearTimeout(timer);\n resolve(event);\n };\n\n const timer = setTimeout(() => {\n const index = this.eventWaiters.indexOf(waiter);\n if (index !== -1) this.eventWaiters.splice(index, 1);\n reject(new Error(`SSE: no event received within ${timeout}ms`));\n }, timeout);\n\n this.eventWaiters.push(waiter);\n });\n }\n\n /** Wait for the stream to end (rejects after `timeout` ms). */\n async waitForEnd(timeout = 5000): Promise<void> {\n if (this.streamEnded) return;\n\n return new Promise<void>((resolve, reject) => {\n const waiter = (): void => {\n clearTimeout(timer);\n resolve();\n };\n\n const timer = setTimeout(() => {\n const index = this.endWaiters.indexOf(waiter);\n if (index !== -1) this.endWaiters.splice(index, 1);\n reject(new Error(`SSE: stream did not end within ${timeout}ms`));\n }, timeout);\n\n this.endWaiters.push(waiter);\n });\n }\n\n /** Collect all remaining events until the stream ends. */\n async collectEvents(timeout = 5000): Promise<TestSseEvent[]> {\n const events: TestSseEvent[] = [];\n\n if (this.streamEnded) {\n return [...this.eventQueue.splice(0)];\n }\n\n return new Promise<TestSseEvent[]>((resolve, reject) => {\n const originalDispatch = this.dispatchEvent.bind(this);\n this.dispatchEvent = (event: TestSseEvent): void => {\n events.push(event);\n originalDispatch(event);\n };\n\n const endWaiter = (): void => {\n clearTimeout(timer);\n this.dispatchEvent = originalDispatch;\n resolve(events);\n };\n\n const timer = setTimeout(() => {\n this.dispatchEvent = originalDispatch;\n const index = this.endWaiters.indexOf(endWaiter);\n if (index !== -1) this.endWaiters.splice(index, 1);\n reject(new Error(`SSE: stream did not end within ${timeout}ms`));\n }, timeout);\n\n events.push(...this.eventQueue.splice(0));\n\n this.endWaiters.push(endWaiter);\n });\n }\n\n /** Assert the next event matches the expected partial shape. */\n async assertEvent(expected: Partial<TestSseEvent>, timeout = 5000): Promise<void> {\n const event = await this.waitForEvent(timeout);\n expect(event).toMatchObject(expected);\n }\n\n /** Assert the next event's `data` equals `expected`. */\n async assertEventData(expected: string, timeout = 5000): Promise<void> {\n const event = await this.waitForEvent(timeout);\n expect(event.data, `Expected SSE data \"${expected}\", got \"${event.data}\"`).toBe(expected);\n }\n\n /** Assert the next event's `data` is JSON equal to `expected`. */\n async assertJsonEventData<T>(expected: T, timeout = 5000): Promise<void> {\n const event = await this.waitForEvent(timeout);\n const parsed = JSON.parse(event.data) as unknown;\n expect(parsed).toEqual(expected);\n }\n\n private startReading(): void {\n const body = this.response.body;\n if (!body) {\n this.streamEnded = true;\n return;\n }\n\n const reader = body.getReader() as ReadableStreamDefaultReader<Uint8Array>;\n const decoder = new TextDecoder();\n let buffer = '';\n\n const read = async (): Promise<void> => {\n try {\n for (;;) {\n const { done, value } = await reader.read();\n\n if (done) {\n if (buffer.trim()) {\n const event = this.parseEvent(buffer);\n if (event) this.dispatchEvent(event);\n }\n this.endStream();\n return;\n }\n\n buffer += decoder.decode(value, { stream: true });\n\n const parts = buffer.split('\\n\\n');\n buffer = parts.pop()!;\n\n for (const part of parts) {\n if (!part.trim()) continue;\n const event = this.parseEvent(part);\n if (event) this.dispatchEvent(event);\n }\n }\n } catch {\n this.endStream();\n }\n };\n\n void read();\n }\n\n private endStream(): void {\n this.streamEnded = true;\n for (const waiter of this.endWaiters) {\n waiter();\n }\n this.endWaiters = [];\n }\n\n private parseEvent(raw: string): TestSseEvent | null {\n const lines = raw.split('\\n');\n const dataLines: string[] = [];\n let event: string | undefined;\n let id: string | undefined;\n let retry: number | undefined;\n\n for (const line of lines) {\n if (line.startsWith(':')) continue; // comment line\n\n const colonIndex = line.indexOf(':');\n if (colonIndex === -1) continue;\n\n const field = line.slice(0, colonIndex);\n const value =\n line[colonIndex + 1] === ' ' ? line.slice(colonIndex + 2) : line.slice(colonIndex + 1);\n\n switch (field) {\n case 'data':\n dataLines.push(value);\n break;\n case 'event':\n event = value;\n break;\n case 'id':\n id = value;\n break;\n case 'retry': {\n const parsed = parseInt(value, 10);\n if (!Number.isNaN(parsed)) retry = parsed;\n break;\n }\n }\n }\n\n if (dataLines.length === 0) return null;\n\n const result: TestSseEvent = { data: dataLines.join('\\n') };\n if (event !== undefined) result.event = event;\n if (id !== undefined) result.id = id;\n if (retry !== undefined) result.retry = retry;\n\n return result;\n }\n\n private dispatchEvent(event: TestSseEvent): void {\n if (this.eventWaiters.length > 0) {\n this.eventWaiters.shift()!(event);\n } else {\n this.eventQueue.push(event);\n }\n }\n}\n","// Adapted from @stratal/testing (MIT, © Temitayo Fadojutimi). Auth uses the\n// generic resolver seam instead of a hard AuthService import.\nimport { expect } from 'vitest';\nimport type { ActingAsResolver, TestPrincipal, TestingModule } from '../testing-module.js';\nimport { TestSseConnection } from './test-sse-connection.js';\n\n/**\n * TestSseRequest\n *\n * Builder for a Server-Sent Events connection. `connect()` issues a GET through\n * `module.fetch()`, asserts a `text/event-stream` 200, and wraps the streaming\n * body in a {@link TestSseConnection}.\n *\n * @example\n * ```ts\n * const sse = await module.sse('/stream/events').connect();\n * await sse.assertEvent({ event: 'message', data: 'hello' });\n * ```\n */\nexport class TestSseRequest {\n private readonly requestHeaders = new Headers();\n private principal: TestPrincipal | null = null;\n private resolver: ActingAsResolver | null = null;\n\n constructor(\n private readonly path: string,\n private readonly module: TestingModule,\n ) {}\n\n /** Merge additional headers onto the SSE request. */\n withHeaders(headers: Record<string, string>): this {\n for (const [key, value] of Object.entries(headers)) {\n this.requestHeaders.set(key, value);\n }\n return this;\n }\n\n /** Authenticate the connection (see {@link TestHttpRequest.actingAs}). */\n actingAs(principal: TestPrincipal, resolver?: ActingAsResolver): this {\n this.principal = principal;\n this.resolver = resolver ?? null;\n return this;\n }\n\n /** Open the stream and return a live {@link TestSseConnection}. */\n async connect(): Promise<TestSseConnection> {\n await this.applyAuthentication();\n\n this.requestHeaders.set('Accept', 'text/event-stream');\n\n const url = new URL(this.path, 'http://localhost');\n const request = new Request(url.toString(), { headers: this.requestHeaders });\n\n const response = await this.module.fetch(request);\n\n expect(response.status, `Expected status 200, got ${response.status}`).toBe(200);\n\n const contentType = response.headers.get('content-type') ?? '';\n expect(\n contentType.includes('text/event-stream'),\n `Expected content-type \"text/event-stream\", got \"${contentType}\"`,\n ).toBe(true);\n\n return new TestSseConnection(response);\n }\n\n private async applyAuthentication(): Promise<void> {\n if (!this.principal) return;\n\n const resolver = this.resolver ?? this.module.getAuthResolver();\n if (!resolver) {\n throw new Error(\n 'actingAs() requires an auth resolver. Pass one explicitly or register ' +\n 'a default with module.setAuthResolver(resolver).',\n );\n }\n\n const headers = await resolver(this.module, this.principal);\n for (const [key, value] of headers.entries()) {\n this.requestHeaders.set(key, value);\n }\n }\n}\n","import { Context } from 'hono';\nimport {\n REQUEST_CONTEXT,\n type RequestContext,\n type Token,\n type Type,\n type VelaApplication,\n} from '@velajs/vela';\nimport type { Container } from '@velajs/vela/internal';\nimport { SeederRegistry, type ISeeder } from '@velajs/vela/seeder';\nimport { expect } from 'vitest';\nimport type { TestDatabase } from './db/test-database.js';\nimport { TestHttpClient } from './http/test-http-client.js';\nimport { TestSseRequest } from './sse/test-sse-request.js';\nimport { TestWsRequest } from './ws/test-ws-request.js';\n\n/** A test principal — an opaque object the auth resolver turns into headers. */\nexport type TestPrincipal = Record<string, unknown>;\n\n/**\n * Turns a principal into request headers (session cookie, bearer token, …).\n * The signature `(module, principal) => Promise<Headers>` is a cross-package\n * contract: sibling packages (e.g. `@velajs/better-auth/testing`) build a\n * resolver against it. Kept generic so `@velajs/testing` needs no auth deps.\n */\nexport type ActingAsResolver = (\n module: TestingModule,\n principal: TestPrincipal,\n) => Promise<Headers>;\n\ntype HonoApp = ReturnType<VelaApplication['getHonoApp']>;\n\n/**\n * TestingModule\n *\n * The compiled test harness. Beyond `get`/`createApplication`/`close`, it adds\n * Laravel-flavored ergonomics: a fluent HTTP client, SSE/WS builders, request-\n * scope execution, seeding, and database assertion wrappers.\n *\n * @example\n * ```ts\n * const module = await Test.createTestingModule({ imports: [AppModule] }).compile();\n * await module.http.post('/users').withBody({ name: 'A' }).send()\n * .then((r) => r.assertCreated());\n * ```\n */\nexport class TestingModule {\n private _http: TestHttpClient | null = null;\n private honoApp: HonoApp | null = null;\n private authResolver: ActingAsResolver | null = null;\n\n constructor(\n private readonly app: VelaApplication,\n private readonly container: Container,\n ) {}\n\n /** Resolve a provider from the root container. */\n get<T>(token: Token<T>): T {\n return this.container.resolve(token);\n }\n\n /** Build (once) and return the underlying application. */\n async createApplication(): Promise<VelaApplication> {\n await this.app.initRoutes();\n return this.app;\n }\n\n /** Lazy fluent HTTP client bound to this module. */\n get http(): TestHttpClient {\n this._http ??= new TestHttpClient(this);\n return this._http;\n }\n\n /** Start an SSE connection builder for `path`. */\n sse(path: string): TestSseRequest {\n return new TestSseRequest(path, this);\n }\n\n /** Start a WebSocket connection builder for `path` (needs a transport adapter). */\n ws(path: string): TestWsRequest {\n return new TestWsRequest(path, this);\n }\n\n /**\n * Drive a `Request` through the full Hono pipeline. The Hono app is built\n * once and reused across requests.\n */\n async fetch(request: Request, env?: unknown, ctx?: unknown): Promise<Response> {\n const hono = await this.ensureHono();\n return hono.fetch(request, env as never, ctx as never);\n }\n\n /**\n * Register a default auth resolver used by `actingAs(principal)` when no\n * resolver is passed explicitly.\n */\n setAuthResolver(resolver: ActingAsResolver): this {\n this.authResolver = resolver;\n return this;\n }\n\n /** The default auth resolver, if one was registered. */\n getAuthResolver(): ActingAsResolver | null {\n return this.authResolver;\n }\n\n /**\n * Run `callback` inside a request-scoped child container seeded with a mock\n * {@link RequestContext}, so REQUEST-scoped providers (and anything injecting\n * `REQUEST_CONTEXT`) resolve. The child is disposed afterwards.\n */\n async runInRequestScope<T>(callback: (container: Container) => T | Promise<T>): Promise<T> {\n const child = this.container.createChild();\n child.setRequestInstance(REQUEST_CONTEXT, this.createMockRequestContext());\n try {\n return await callback(child);\n } finally {\n await child.dispose();\n }\n }\n\n /**\n * Run the given `@Seeder()` classes, each in its own request scope. Throws if\n * a class is not a registered seeder. Requires `SeederModule` (or the seeders\n * themselves) to be present in the module graph.\n */\n async seed(...SeederClasses: Type<ISeeder>[]): Promise<void> {\n const registry = this.container.resolve(SeederRegistry);\n const known = new Set<unknown>(registry.list().map((s) => s.target));\n\n for (const SeederClass of SeederClasses) {\n if (!known.has(SeederClass)) {\n throw new Error(\n `Seeder \"${SeederClass.name}\" is not registered. Add it to a module's ` +\n 'providers or SeederModule.forRoot({ seeders: [...] }).',\n );\n }\n await this.runInRequestScope(async (child) => {\n const instance = child.resolve<ISeeder>(SeederClass);\n await instance.run();\n });\n }\n }\n\n /** Assert a row matching `where` exists in `table` (via a {@link TestDatabase}). */\n async assertDatabaseHas(\n db: TestDatabase,\n table: string,\n where: Record<string, unknown>,\n ): Promise<void> {\n const exists = await db.has(table, where);\n expect(exists, `Expected ${table} to have a row matching ${JSON.stringify(where)}`).toBe(true);\n }\n\n /** Assert no row matching `where` exists in `table`. */\n async assertDatabaseMissing(\n db: TestDatabase,\n table: string,\n where: Record<string, unknown>,\n ): Promise<void> {\n const exists = await db.has(table, where);\n expect(exists, `Expected ${table} NOT to have a row matching ${JSON.stringify(where)}`).toBe(\n false,\n );\n }\n\n /** Assert `table` has exactly `expected` rows. */\n async assertDatabaseCount(db: TestDatabase, table: string, expected: number): Promise<void> {\n const actual = await db.count(table);\n expect(actual, `Expected ${table} count ${expected}, got ${actual}`).toBe(expected);\n }\n\n /** Dispose the application. */\n async close(signal?: string): Promise<void> {\n await this.app.close(signal);\n }\n\n private async ensureHono(): Promise<HonoApp> {\n if (!this.honoApp) {\n const app = await this.createApplication();\n this.honoApp = app.getHonoApp();\n }\n return this.honoApp;\n }\n\n /**\n * Build a minimal, functional {@link RequestContext} for out-of-band request\n * scopes. Vela has no `createMockRouterContext`; a real (empty) Hono `Context`\n * backs the `hono` field so nothing dangles.\n */\n private createMockRequestContext(): RequestContext {\n const bag = new Map<string | symbol, unknown>();\n const request = new Request('http://localhost/');\n const hono = new Context(request);\n return {\n id: crypto.randomUUID(),\n receivedAt: new Date(),\n request,\n hono,\n set(key, value) {\n bag.set(key, value);\n },\n get<V>(key: string | symbol): V | undefined {\n return bag.get(key) as V | undefined;\n },\n has(key) {\n return bag.has(key);\n },\n };\n }\n}\n","import { type ModuleOptions, type ProviderOptions, type Token, type Type } from '@velajs/vela';\nimport { MetadataRegistry, VelaApplication, bootstrap } from '@velajs/vela/internal';\nimport { TestingModule } from './testing-module.js';\n\ninterface OverrideEntry {\n token: Token;\n provider: ProviderOptions;\n}\n\nexport class OverrideBy {\n constructor(\n private readonly builder: TestingModuleBuilder,\n private readonly token: Token,\n ) {}\n\n useValue(value: unknown): TestingModuleBuilder {\n this.builder['addOverride']({\n token: this.token,\n provider: { provide: this.token, useValue: value },\n });\n return this.builder;\n }\n\n useClass(cls: Type): TestingModuleBuilder {\n this.builder['addOverride']({\n token: this.token,\n provider: { provide: this.token, useClass: cls },\n });\n return this.builder;\n }\n\n useFactory(options: {\n factory: (...args: unknown[]) => unknown;\n inject?: Token[];\n }): TestingModuleBuilder {\n this.builder['addOverride']({\n token: this.token,\n provider: {\n provide: this.token,\n useFactory: options.factory,\n inject: options.inject,\n },\n });\n return this.builder;\n }\n}\n\nexport class TestingModuleBuilder {\n private overrides: OverrideEntry[] = [];\n\n constructor(private readonly metadata: ModuleOptions) {}\n\n overrideProvider(token: Token): OverrideBy {\n return new OverrideBy(this, token);\n }\n\n overrideGuard(guard: Type): OverrideBy {\n return new OverrideBy(this, guard);\n }\n\n overridePipe(pipe: Type): OverrideBy {\n return new OverrideBy(this, pipe);\n }\n\n overrideInterceptor(interceptor: Type): OverrideBy {\n return new OverrideBy(this, interceptor);\n }\n\n overrideFilter(filter: Type): OverrideBy {\n return new OverrideBy(this, filter);\n }\n\n private addOverride(entry: OverrideEntry): void {\n const idx = this.overrides.findIndex((o) => o.token === entry.token);\n if (idx !== -1) {\n this.overrides[idx] = entry;\n } else {\n this.overrides.push(entry);\n }\n }\n\n async compile(): Promise<TestingModule> {\n class TestRootModule {}\n MetadataRegistry.setModuleOptions(TestRootModule, {\n imports: this.metadata.imports,\n providers: this.metadata.providers,\n controllers: this.metadata.controllers,\n exports: this.metadata.exports,\n });\n\n // Use the framework's single bootstrap primitive. Hand-copying its\n // registrations caused test applications to drift from production (most\n // critically REQUEST_CONTEXT token/request-child behavior).\n const { container, routeManager, loader } = await bootstrap(TestRootModule);\n\n // Force-apply overrides into every module bucket that already holds the\n // token (plus root). Without this, controller constructor-injection (which\n // passes requestingModuleId to findRegistration) finds the module's own\n // registration first and never consults the root override. The default\n // 'all-existing' buckets replace every non-root bucket holding the token\n // and re-register at root — the supported form of the old private loop.\n for (const override of this.overrides) {\n container.replaceProvider(override.provider);\n }\n\n const app = new VelaApplication(container, routeManager);\n const instances = await loader.resolveAllInstances();\n app.setInstances(instances);\n\n await app.callOnModuleInit();\n await app.callOnApplicationBootstrap();\n await app.initRoutes();\n\n return new TestingModule(app, container);\n }\n}\n","import type { ModuleOptions } from '@velajs/vela';\nimport { TestingModuleBuilder } from './testing-module.builder.js';\n\nexport const Test = {\n createTestingModule(metadata: ModuleOptions): TestingModuleBuilder {\n return new TestingModuleBuilder(metadata);\n },\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velajs/testing",
3
- "version": "0.5.1",
3
+ "version": "1.0.0",
4
4
  "description": "Testing utilities for Vela framework",
5
5
  "keywords": [
6
6
  "edge",
@@ -41,6 +41,10 @@
41
41
  "./websocket-node": {
42
42
  "types": "./dist/websocket-node/index.d.ts",
43
43
  "import": "./dist/websocket-node/index.js"
44
+ },
45
+ "./eval": {
46
+ "types": "./dist/eval/index.d.ts",
47
+ "import": "./dist/eval/index.js"
44
48
  }
45
49
  },
46
50
  "devDependencies": {
@@ -48,7 +52,7 @@
48
52
  "@changesets/cli": "^2.31.0",
49
53
  "@swc/core": "^1.15.43",
50
54
  "@types/node": "^24.13.3",
51
- "@velajs/vela": "^1.12.0",
55
+ "@velajs/vela": "^1.21.0",
52
56
  "hono": "^4.12.26",
53
57
  "oxfmt": "^0.58.0",
54
58
  "oxlint": "^1.73.0",
@@ -61,7 +65,7 @@
61
65
  "peerDependencies": {
62
66
  "@hono/node-server": ">=1",
63
67
  "@hono/node-ws": ">=1",
64
- "@velajs/vela": ">=1.11.0",
68
+ "@velajs/vela": ">=1.21.0 <2",
65
69
  "hono": ">=4",
66
70
  "vitest": ">=3"
67
71
  },
@@ -80,6 +84,7 @@
80
84
  "build": "tsdown",
81
85
  "test": "vitest run",
82
86
  "typecheck": "tsc --noEmit",
87
+ "typecheck:tests": "tsc --noEmit -p tsconfig.test.json",
83
88
  "lint": "oxlint .",
84
89
  "format": "oxfmt .",
85
90
  "format:check": "oxfmt --check .",
@@ -87,7 +92,9 @@
87
92
  "attw": "attw --pack . --profile esm-only",
88
93
  "changeset": "changeset",
89
94
  "version-packages": "changeset version",
90
- "release": "pnpm build && changeset publish",
91
- "verify": "pnpm lint && pnpm format:check && pnpm build && pnpm typecheck && pnpm test && pnpm publint && pnpm attw"
95
+ "release:preflight": "npm view @velajs/vela@1.21.0 version",
96
+ "release:check": "node scripts/check-release-lock.mjs && pnpm verify && pnpm audit --audit-level=high",
97
+ "release": "pnpm release:preflight && pnpm release:check && changeset publish",
98
+ "verify": "pnpm lint && pnpm format:check && pnpm build && pnpm typecheck && pnpm typecheck:tests && pnpm test && pnpm publint && pnpm attw"
92
99
  }
93
100
  }