@glubean/sdk 0.1.39 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/configure.d.ts +4 -21
  2. package/dist/configure.d.ts.map +1 -1
  3. package/dist/configure.js +19 -145
  4. package/dist/configure.js.map +1 -1
  5. package/dist/contract-core.d.ts +98 -0
  6. package/dist/contract-core.d.ts.map +1 -0
  7. package/dist/contract-core.js +749 -0
  8. package/dist/contract-core.js.map +1 -0
  9. package/dist/contract-http/adapter.d.ts +52 -0
  10. package/dist/contract-http/adapter.d.ts.map +1 -0
  11. package/dist/contract-http/adapter.js +650 -0
  12. package/dist/contract-http/adapter.js.map +1 -0
  13. package/dist/contract-http/factory.d.ts +32 -0
  14. package/dist/contract-http/factory.d.ts.map +1 -0
  15. package/dist/contract-http/factory.js +83 -0
  16. package/dist/contract-http/factory.js.map +1 -0
  17. package/dist/contract-http/flow-helpers.d.ts +12 -0
  18. package/dist/contract-http/flow-helpers.d.ts.map +1 -0
  19. package/dist/contract-http/flow-helpers.js +34 -0
  20. package/dist/contract-http/flow-helpers.js.map +1 -0
  21. package/dist/contract-http/index.d.ts +16 -0
  22. package/dist/contract-http/index.d.ts.map +1 -0
  23. package/dist/contract-http/index.js +15 -0
  24. package/dist/contract-http/index.js.map +1 -0
  25. package/dist/contract-http/markdown.d.ts +10 -0
  26. package/dist/contract-http/markdown.d.ts.map +1 -0
  27. package/dist/contract-http/markdown.js +21 -0
  28. package/dist/contract-http/markdown.js.map +1 -0
  29. package/dist/contract-http/openapi.d.ts +15 -0
  30. package/dist/contract-http/openapi.d.ts.map +1 -0
  31. package/dist/contract-http/openapi.js +38 -0
  32. package/dist/contract-http/openapi.js.map +1 -0
  33. package/dist/contract-http/types.d.ts +252 -0
  34. package/dist/contract-http/types.d.ts.map +1 -0
  35. package/dist/contract-http/types.js +13 -0
  36. package/dist/contract-http/types.js.map +1 -0
  37. package/dist/contract-types.d.ts +420 -467
  38. package/dist/contract-types.d.ts.map +1 -1
  39. package/dist/contract-types.js +16 -4
  40. package/dist/contract-types.js.map +1 -1
  41. package/dist/each-builder.d.ts +244 -0
  42. package/dist/each-builder.d.ts.map +1 -0
  43. package/dist/each-builder.js +268 -0
  44. package/dist/each-builder.js.map +1 -0
  45. package/dist/index.d.ts +30 -513
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +33 -826
  48. package/dist/index.js.map +1 -1
  49. package/dist/internal.d.ts +1 -0
  50. package/dist/internal.d.ts.map +1 -1
  51. package/dist/internal.js +1 -0
  52. package/dist/internal.js.map +1 -1
  53. package/dist/runtime-carrier.d.ts +142 -0
  54. package/dist/runtime-carrier.d.ts.map +1 -0
  55. package/dist/runtime-carrier.js +148 -0
  56. package/dist/runtime-carrier.js.map +1 -0
  57. package/dist/session.d.ts.map +1 -1
  58. package/dist/session.js +2 -1
  59. package/dist/session.js.map +1 -1
  60. package/dist/test-builder.d.ts +249 -0
  61. package/dist/test-builder.d.ts.map +1 -0
  62. package/dist/test-builder.js +265 -0
  63. package/dist/test-builder.js.map +1 -0
  64. package/dist/test-extend.d.ts +59 -0
  65. package/dist/test-extend.d.ts.map +1 -0
  66. package/dist/test-extend.js +111 -0
  67. package/dist/test-extend.js.map +1 -0
  68. package/dist/test-utils.d.ts +39 -0
  69. package/dist/test-utils.d.ts.map +1 -0
  70. package/dist/test-utils.js +91 -0
  71. package/dist/test-utils.js.map +1 -0
  72. package/dist/types.d.ts +41 -122
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +1 -1
  75. package/dist/contract.d.ts +0 -64
  76. package/dist/contract.d.ts.map +0 -1
  77. package/dist/contract.js +0 -793
  78. package/dist/contract.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,5 +1,9 @@
1
1
  import { registerTest } from "./internal.js";
2
2
  import { toArray } from "./data.js";
3
+ import { TestBuilder } from "./test-builder.js";
4
+ import { EachBuilder } from "./each-builder.js";
5
+ import { createExtendedTest } from "./test-extend.js";
6
+ import { normalizeEachTable, resolveBaseMeta, interpolateTemplate, selectPickExamples } from "./test-utils.js";
3
7
  /**
4
8
  * Glubean SDK spec version.
5
9
  *
@@ -14,292 +18,19 @@ import { toArray } from "./data.js";
14
18
  * ```
15
19
  */
16
20
  export const SPEC_VERSION = "2.0";
17
- // =============================================================================
18
- // Note: Registry functions (getRegistry, clearRegistry) have been moved to
19
- // internal.ts to keep the public API clean. Import from "@glubean/sdk/internal"
20
- // if you need them (for scanner or testing purposes only).
21
- // =============================================================================
22
- // =============================================================================
23
- // New Builder API
24
- // =============================================================================
25
- /**
26
- * Builder class for creating tests with a fluent API.
27
- *
28
- * @template S The state type for multi-step tests
29
- * @template Ctx The context type (defaults to TestContext; augmented by test.extend())
30
- *
31
- * @example Simple test (quick mode)
32
- * ```ts
33
- * export const login = test("login", async (ctx) => {
34
- * ctx.assert(true, "works");
35
- * });
36
- * ```
37
- *
38
- * @example Multi-step test (builder mode)
39
- * ```ts
40
- * export const checkout = test("checkout")
41
- * .meta({ tags: ["e2e"] })
42
- * .setup(async (ctx) => ({ cart: await createCart() }))
43
- * .step("Add to cart", async (ctx, state) => {
44
- * await addItem(state.cart, "item-1");
45
- * return state;
46
- * })
47
- * .step("Checkout", async (ctx, state) => {
48
- * await checkout(state.cart);
49
- * return state;
50
- * })
51
- * .teardown(async (ctx, state) => {
52
- * await cleanup(state.cart);
53
- * })
54
- * .build();
55
- * ```
56
- */
57
- export class TestBuilder {
58
- _meta;
59
- _setup;
60
- _teardown;
61
- _steps = [];
62
- _built = false;
63
- _fixtures;
64
- /**
65
- * Marker property so the runner can detect un-built TestBuilder exports
66
- * without importing the SDK. The runner checks this string to auto-build.
67
- */
68
- __glubean_type = "builder";
69
- constructor(id, fixtures) {
70
- this._meta = { id, name: id };
71
- this._fixtures = fixtures;
72
- // Auto-finalize (register) after all synchronous chaining completes.
73
- // Module top-level code is synchronous, so by the time the microtask
74
- // fires, all .step() / .meta() / .setup() / .teardown() calls are done.
75
- queueMicrotask(() => this._finalize());
76
- }
77
- /**
78
- * Set additional metadata for the test.
79
- *
80
- * @example
81
- * ```ts
82
- * test("my-test")
83
- * .meta({ tags: ["smoke"], description: "A smoke test" })
84
- * .step(...)
85
- * ```
86
- */
87
- meta(meta) {
88
- this._meta = { ...this._meta, ...meta };
89
- return this;
90
- }
91
- /**
92
- * Mark this test as focused.
93
- *
94
- * Focused tests are intended for local debugging sessions. When any tests in
95
- * a run are marked as `only`, non-focused tests may be excluded by discovery
96
- * tooling/orchestrators. If `skip` is also set on the same test, `skip`
97
- * still wins during run selection.
98
- */
99
- only() {
100
- this._meta = { ...this._meta, only: true };
101
- return this;
102
- }
103
- /**
104
- * Mark this test as skipped.
105
- *
106
- * Skip takes precedence over `only` when both are present.
107
- */
108
- skip() {
109
- this._meta = { ...this._meta, skip: true };
110
- return this;
111
- }
112
- /**
113
- * Set the setup function that runs before all steps.
114
- * The returned state is passed to all steps and teardown.
115
- *
116
- * @example
117
- * ```ts
118
- * test("auth")
119
- * .setup(async (ctx) => {
120
- * const baseUrl = ctx.vars.require("BASE_URL");
121
- * const apiKey = ctx.secrets.require("API_KEY");
122
- * const { token } = await ctx.http.post(`${baseUrl}/auth/token`, {
123
- * headers: { "X-API-Key": apiKey },
124
- * }).json();
125
- * return { token };
126
- * })
127
- * .step("verify", async (ctx, { token }) => { ... })
128
- * ```
129
- */
130
- setup(fn) {
131
- this._setup = fn;
132
- return this;
133
- }
134
- /**
135
- * Set the teardown function that runs after all steps (even on failure).
136
- *
137
- * @example
138
- * ```ts
139
- * test("db-test")
140
- * .setup(async (ctx) => ({ conn: await connect() }))
141
- * .step(...)
142
- * .teardown(async (ctx, { conn }) => {
143
- * await conn.close();
144
- * })
145
- * ```
146
- */
147
- teardown(fn) {
148
- this._teardown = fn;
149
- return this;
150
- }
151
- step(name, optionsOrFn, maybeFn) {
152
- const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
153
- const options = typeof optionsOrFn === "function" ? {} : optionsOrFn;
154
- this._steps.push({
155
- meta: { name, ...options },
156
- fn: fn,
157
- });
158
- return this;
159
- }
160
- /**
161
- * Apply a builder transform function for step composition.
162
- *
163
- * Reusable step sequences are just plain functions that take a builder
164
- * and return a builder. `.use()` applies such a function to the current
165
- * chain, preserving state flow.
166
- *
167
- * @param fn Transform function that receives this builder and returns a (possibly re-typed) builder
168
- *
169
- * @example Reusable step sequence
170
- * ```ts
171
- * // Define once — just a function
172
- * const withAuth = (b: TestBuilder<unknown>) => b
173
- * .step("login", async (ctx) => {
174
- * const data = await ctx.http.post("/login", { json: creds }).json<{ token: string }>();
175
- * return { token: data.token };
176
- * });
177
- *
178
- * // Reuse across tests
179
- * export const testA = test("test-a").use(withAuth).step("act", async (ctx, { token }) => { ... });
180
- * export const testB = test("test-b").use(withAuth).step("verify", async (ctx, { token }) => { ... });
181
- * ```
182
- */
183
- use(fn) {
184
- return fn(this);
185
- }
186
- /**
187
- * Apply a builder transform and tag all newly added steps with a group ID.
188
- *
189
- * Works exactly like `.use()`, but every step added by `fn` is marked with
190
- * `group` metadata for visual grouping in reports and dashboards.
191
- *
192
- * @param id Group identifier (displayed in reports as a section header)
193
- * @param fn Transform function that adds steps to the builder
194
- *
195
- * @example Reusable steps with grouping
196
- * ```ts
197
- * const withAuth = (b: TestBuilder<unknown>) => b
198
- * .step("login", async (ctx) => ({ token: "..." }))
199
- * .step("verify", async (ctx, { token }) => ({ token, verified: true }));
200
- *
201
- * export const checkout = test("checkout")
202
- * .group("auth", withAuth)
203
- * .step("pay", async (ctx, { token }) => { ... });
204
- *
205
- * // Report output:
206
- * // checkout
207
- * // ├─ [auth]
208
- * // │ ├─ login ✓
209
- * // │ └─ verify ✓
210
- * // └─ pay ✓
211
- * ```
212
- *
213
- * @example Inline grouping (no reuse, just organization)
214
- * ```ts
215
- * export const e2e = test("e2e")
216
- * .group("setup", b => b
217
- * .step("seed db", async (ctx) => ({ dbId: "..." }))
218
- * .step("create user", async (ctx, { dbId }) => ({ dbId, userId: "..." }))
219
- * )
220
- * .step("verify", async (ctx, { dbId, userId }) => { ... });
221
- * ```
222
- */
223
- group(id, fn) {
224
- const before = this._steps.length;
225
- const result = fn(this);
226
- for (let i = before; i < this._steps.length; i++) {
227
- this._steps[i].meta.group = id;
228
- }
229
- return result;
230
- }
231
- /**
232
- * Finalize and register the test in the global registry.
233
- * Called automatically via microtask if not explicitly invoked via build().
234
- * Idempotent — safe to call multiple times.
235
- * @internal
236
- */
237
- _finalize() {
238
- if (this._built)
239
- return;
240
- this._built = true;
241
- registerTest({
242
- id: this._meta.id,
243
- name: this._meta.name || this._meta.id,
244
- type: "steps",
245
- tags: toArray(this._meta.tags),
246
- description: this._meta.description,
247
- steps: this._steps.map((s) => ({
248
- name: s.meta.name,
249
- ...(s.meta.group ? { group: s.meta.group } : {}),
250
- })),
251
- hasSetup: !!this._setup,
252
- hasTeardown: !!this._teardown,
253
- });
254
- }
255
- /**
256
- * Build and register the test. Returns a plain `Test<S>` object.
257
- *
258
- * **Optional** — if omitted, the builder auto-finalizes via microtask
259
- * after all synchronous chaining completes, and the runner will
260
- * auto-detect the builder export. Calling `.build()` explicitly is
261
- * still supported for backward compatibility.
262
- *
263
- * @example
264
- * ```ts
265
- * // With .build() (explicit — backward compatible)
266
- * export const myTest = test("my-test")
267
- * .step("step-1", async (ctx) => { ... })
268
- * .build();
269
- *
270
- * // Without .build() (auto-finalized — recommended)
271
- * export const myTest = test("my-test")
272
- * .step("step-1", async (ctx) => { ... });
273
- * ```
274
- */
275
- build() {
276
- this._finalize();
277
- return {
278
- meta: this._meta,
279
- type: "steps",
280
- setup: this._setup,
281
- teardown: this._teardown,
282
- steps: this._steps,
283
- ...(this._fixtures ? { fixtures: this._fixtures } : {}),
284
- };
285
- }
286
- }
287
21
  export function test(idOrMeta, fn) {
288
22
  const meta = typeof idOrMeta === "string"
289
23
  ? { id: idOrMeta, name: idOrMeta }
290
24
  : { name: idOrMeta.id, ...idOrMeta };
291
- // Normalize tags to string[]
292
25
  if (meta.tags) {
293
26
  meta.tags = toArray(meta.tags);
294
27
  }
295
- // Quick mode: test("id", fn) -> returns Test directly
296
28
  if (fn) {
297
29
  const testDef = {
298
30
  meta,
299
31
  type: "simple",
300
32
  fn,
301
33
  };
302
- // Register to global registry
303
34
  registerTest({
304
35
  id: meta.id,
305
36
  name: meta.name || meta.id,
@@ -309,541 +40,12 @@ export function test(idOrMeta, fn) {
309
40
  });
310
41
  return testDef;
311
42
  }
312
- // Builder mode: test("id") -> returns TestBuilder
313
43
  const builder = new TestBuilder(meta.id);
314
44
  if (typeof idOrMeta !== "string") {
315
45
  builder.meta(idOrMeta);
316
46
  }
317
47
  return builder;
318
48
  }
319
- // =============================================================================
320
- // Data-Driven API (test.each)
321
- // =============================================================================
322
- /**
323
- * Interpolate `$key` placeholders in a template string with data values.
324
- * Supports `$index` for the row index and `$key` for any key in the data object.
325
- *
326
- * @internal
327
- */
328
- function interpolateTemplate(template, data, index) {
329
- let result = template.replace(/\$index/g, String(index));
330
- for (const [key, value] of Object.entries(data)) {
331
- result = result.replaceAll(`$${key}`, String(value));
332
- }
333
- return result;
334
- }
335
- /**
336
- * Resolve baseMeta from string or TestMeta input.
337
- * @internal
338
- */
339
- function resolveBaseMeta(idOrMeta) {
340
- return typeof idOrMeta === "string" ? { id: idOrMeta, name: idOrMeta } : { name: idOrMeta.id, ...idOrMeta };
341
- }
342
- /**
343
- * Normalize table input for test.each: accepts array or plain object (map).
344
- *
345
- * - Array: returned as-is
346
- * - Plain object: converted to array with `_pick` key injected per entry
347
- *
348
- * @internal
349
- */
350
- function isPlainObject(value) {
351
- if (!value || typeof value !== "object" || Array.isArray(value))
352
- return false;
353
- const proto = Object.getPrototypeOf(value);
354
- return proto === Object.prototype || proto === null;
355
- }
356
- function normalizeEachTable(table) {
357
- if (Array.isArray(table))
358
- return table;
359
- if (!isPlainObject(table)) {
360
- throw new Error("test.each() expects an array or a plain object (map).");
361
- }
362
- return Object.entries(table).map(([key, val]) => ({ ...val, _pick: key }));
363
- }
364
- /**
365
- * Builder for data-driven tests with multi-step workflow support.
366
- *
367
- * Created by `test.each(table)(idTemplate)` (without a callback).
368
- * Provides the same fluent `.step()` / `.setup()` / `.teardown()` API
369
- * as `TestBuilder`, but each step/setup/teardown also receives the
370
- * data row for the current test.
371
- *
372
- * On finalization, creates one `Test` per row in the table, each with
373
- * full step definitions visible in `glubean scan` metadata and dashboards.
374
- *
375
- * @template S The state type for multi-step tests
376
- * @template T The data row type
377
- *
378
- * @example
379
- * ```ts
380
- * export const userFlows = test.each([
381
- * { userId: 1 },
382
- * { userId: 2 },
383
- * ])("user-flow-$userId")
384
- * .step("fetch user", async (ctx, state, { userId }) => {
385
- * const res = await ctx.http.get(`/users/${userId}`);
386
- * ctx.assert(res.ok, "user exists");
387
- * return { user: await res.json() };
388
- * })
389
- * .step("verify posts", async (ctx, { user }) => {
390
- * const res = await ctx.http.get(`/users/${user.id}/posts`);
391
- * ctx.assert(res.ok, "posts accessible");
392
- * });
393
- * ```
394
- */
395
- export class EachBuilder {
396
- _baseMeta;
397
- _table;
398
- _setup;
399
- _teardown;
400
- _steps = [];
401
- _built = false;
402
- _parallel;
403
- _fixtures;
404
- /**
405
- * Marker property so the runner and scanner can detect EachBuilder exports.
406
- */
407
- __glubean_type = "each-builder";
408
- constructor(baseMeta, table, fixtures, parallel = false) {
409
- this._baseMeta = baseMeta;
410
- this._table = table;
411
- this._fixtures = fixtures;
412
- this._parallel = parallel;
413
- // Auto-finalize after all synchronous chaining completes.
414
- queueMicrotask(() => this._finalize());
415
- }
416
- /**
417
- * Set additional metadata for all generated tests.
418
- *
419
- * @example
420
- * ```ts
421
- * test.each(table)("user-$userId")
422
- * .meta({ tags: ["smoke"], timeout: 10000 })
423
- * .step("fetch", async (ctx, state, row) => { ... });
424
- * ```
425
- */
426
- meta(meta) {
427
- this._baseMeta = { ...this._baseMeta, ...meta };
428
- return this;
429
- }
430
- /**
431
- * Mark all generated tests from this data set as focused.
432
- * If `skip` is also set, skipped tests are still excluded.
433
- */
434
- only() {
435
- this._baseMeta = { ...this._baseMeta, only: true };
436
- return this;
437
- }
438
- /**
439
- * Mark all generated tests from this data set as skipped.
440
- * Skip takes precedence over `only` when both are present.
441
- */
442
- skip() {
443
- this._baseMeta = { ...this._baseMeta, skip: true };
444
- return this;
445
- }
446
- /**
447
- * Set the setup function. Receives context and data row, returns state.
448
- *
449
- * @example
450
- * ```ts
451
- * test.each(table)("id-$key")
452
- * .setup(async (ctx, row) => {
453
- * const api = ctx.http.extend({ headers: { "X-User": row.userId } });
454
- * return { api };
455
- * })
456
- * .step("use api", async (ctx, { api }) => { ... });
457
- * ```
458
- */
459
- setup(fn) {
460
- this._setup = fn;
461
- return this;
462
- }
463
- /**
464
- * Set the teardown function. Runs after all steps (even on failure).
465
- *
466
- * @example
467
- * ```ts
468
- * test.each(table)("user-$userId")
469
- * .setup(async (ctx, row) => ({ token: await login(ctx, row) }))
470
- * .step("test", async (ctx, { token }) => { ... })
471
- * .teardown(async (ctx, state, row) => {
472
- * await ctx.http.post("/logout", { body: { token: state.token } });
473
- * });
474
- * ```
475
- */
476
- teardown(fn) {
477
- this._teardown = fn;
478
- return this;
479
- }
480
- step(name, optionsOrFn, maybeFn) {
481
- const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
482
- const options = typeof optionsOrFn === "function" ? {} : optionsOrFn;
483
- this._steps.push({
484
- meta: { name, ...options },
485
- fn,
486
- });
487
- return this;
488
- }
489
- /**
490
- * Apply a builder transform function for step composition.
491
- *
492
- * Works the same as `TestBuilder.use()` — reusable step sequences
493
- * are plain functions that take a builder and return a builder.
494
- *
495
- * @param fn Transform function that receives this builder and returns a (possibly re-typed) builder
496
- *
497
- * @example
498
- * ```ts
499
- * const withVerify = (b: EachBuilder<{ id: string }, { userId: number }>) => b
500
- * .step("verify", async (ctx, { id }, row) => {
501
- * ctx.expect(id).toBeTruthy();
502
- * });
503
- *
504
- * export const users = test.each(table)("user-$userId")
505
- * .setup(async (ctx, row) => ({ id: String(row.userId) }))
506
- * .use(withVerify);
507
- * ```
508
- */
509
- use(fn) {
510
- return fn(this);
511
- }
512
- /**
513
- * Apply a builder transform and tag all newly added steps with a group ID.
514
- *
515
- * Works the same as `TestBuilder.group()` — steps added by `fn` are marked
516
- * with `group` metadata for visual grouping in reports.
517
- *
518
- * @param id Group identifier (displayed in reports as a section header)
519
- * @param fn Transform function that adds steps to the builder
520
- *
521
- * @example
522
- * ```ts
523
- * export const users = test.each(table)("user-$userId")
524
- * .group("setup", b => b
525
- * .step("init", async (ctx, state, row) => ({ id: String(row.userId) }))
526
- * )
527
- * .step("verify", async (ctx, { id }) => { ... });
528
- * ```
529
- */
530
- group(id, fn) {
531
- const before = this._steps.length;
532
- const result = fn(this);
533
- for (let i = before; i < this._steps.length; i++) {
534
- this._steps[i].meta.group = id;
535
- }
536
- return result;
537
- }
538
- /**
539
- * Get the filtered table (apply filter callback if present).
540
- * @internal
541
- */
542
- _filteredTable() {
543
- const filter = this._baseMeta.filter;
544
- if (!filter)
545
- return this._table;
546
- return this._table.filter((row, index) => filter(row, index));
547
- }
548
- /**
549
- * Compute tags for a specific row (static tags + tagFields).
550
- * @internal
551
- */
552
- _tagsForRow(row) {
553
- const staticTags = toArray(this._baseMeta.tags);
554
- const tagFieldNames = toArray(this._baseMeta.tagFields);
555
- const dynamicTags = tagFieldNames
556
- .map((field) => {
557
- const value = row[field];
558
- return value != null ? `${field}:${value}` : null;
559
- })
560
- .filter((t) => t !== null);
561
- return [...staticTags, ...dynamicTags];
562
- }
563
- /**
564
- * Finalize and register all tests in the global registry.
565
- * Called automatically via microtask if not explicitly invoked via build().
566
- * Idempotent — safe to call multiple times.
567
- * @internal
568
- */
569
- _finalize() {
570
- if (this._built)
571
- return;
572
- this._built = true;
573
- const stepMetas = this._steps.map((s) => ({
574
- name: s.meta.name,
575
- ...(s.meta.group ? { group: s.meta.group } : {}),
576
- }));
577
- const table = this._filteredTable();
578
- const isPick = table.length > 0 && "_pick" in table[0];
579
- const hasGroup = isPick || this._parallel;
580
- for (let i = 0; i < table.length; i++) {
581
- const row = table[i];
582
- const id = interpolateTemplate(this._baseMeta.id, row, i);
583
- const name = this._baseMeta.name ? interpolateTemplate(this._baseMeta.name, row, i) : id;
584
- registerTest({
585
- id,
586
- name,
587
- type: "steps",
588
- tags: this._tagsForRow(row),
589
- description: this._baseMeta.description,
590
- steps: stepMetas,
591
- hasSetup: !!this._setup,
592
- hasTeardown: !!this._teardown,
593
- ...(hasGroup ? { groupId: this._baseMeta.id } : {}),
594
- ...(this._parallel ? { parallel: true } : {}),
595
- });
596
- }
597
- }
598
- /**
599
- * Build and register all tests. Returns a `Test[]` array.
600
- *
601
- * **Optional** — if omitted, the builder auto-finalizes via microtask
602
- * and the runner will auto-detect the EachBuilder export.
603
- */
604
- build() {
605
- this._finalize();
606
- const table = this._filteredTable();
607
- return table.map((row, index) => {
608
- const id = interpolateTemplate(this._baseMeta.id, row, index);
609
- const name = this._baseMeta.name ? interpolateTemplate(this._baseMeta.name, row, index) : id;
610
- const meta = {
611
- ...this._baseMeta,
612
- id,
613
- name,
614
- tags: this._tagsForRow(row),
615
- };
616
- const setup = this._setup;
617
- const teardown = this._teardown;
618
- return {
619
- meta,
620
- type: "steps",
621
- setup: setup ? ((ctx) => setup(ctx, row)) : undefined,
622
- teardown: teardown
623
- ? ((ctx, state) => teardown(ctx, state, row))
624
- : undefined,
625
- steps: this._steps.map((s) => ({
626
- meta: s.meta,
627
- fn: ((ctx, state) => s.fn(ctx, state, row)),
628
- })),
629
- ...(this._fixtures ? { fixtures: this._fixtures } : {}),
630
- };
631
- });
632
- }
633
- }
634
- /**
635
- * Data-driven test generation.
636
- *
637
- * Creates one independent test per row in the data table.
638
- * Each test gets its own ID (from template interpolation), runs independently,
639
- * and reports its own pass/fail status.
640
- *
641
- * Use `$key` in the ID/name template to interpolate values from the data row.
642
- * Use `$index` for the row index (0-based).
643
- *
644
- * Supports two modes:
645
- *
646
- * 1. **Simple mode** — pass a callback to get `Test[]` (single-function tests).
647
- * 2. **Builder mode** — omit the callback to get an `EachBuilder` with
648
- * `.step()` / `.setup()` / `.teardown()` support for multi-step workflows.
649
- *
650
- * @example Simple mode (backward compatible)
651
- * ```ts
652
- * import { test } from "@glubean/sdk";
653
- *
654
- * export const statusTests = test.each([
655
- * { id: 1, expected: 200 },
656
- * { id: 999, expected: 404 },
657
- * ])("get-user-$id", async (ctx, { id, expected }) => {
658
- * const res = await ctx.http.get(`${ctx.vars.require("BASE_URL")}/users/${id}`, {
659
- * throwHttpErrors: false,
660
- * });
661
- * ctx.expect(res.status).toBe(expected);
662
- * });
663
- * ```
664
- *
665
- * @example Builder mode (multi-step per data row)
666
- * ```ts
667
- * export const userFlows = test.each([
668
- * { userId: 1 },
669
- * { userId: 2 },
670
- * ])("user-flow-$userId")
671
- * .step("fetch user", async (ctx, _state, { userId }) => {
672
- * const res = await ctx.http.get(`/users/${userId}`);
673
- * ctx.assert(res.ok, "user exists");
674
- * return { user: await res.json() };
675
- * })
676
- * .step("verify posts", async (ctx, { user }) => {
677
- * const res = await ctx.http.get(`/users/${user.id}/posts`);
678
- * ctx.assert(res.ok, "posts accessible");
679
- * });
680
- * ```
681
- *
682
- * @param table Array of data rows. Each row produces one test.
683
- * @returns A function that accepts an ID template and optional test function
684
- */
685
- // =============================================================================
686
- // Extended Test (test.extend)
687
- // =============================================================================
688
- /** Keys that cannot be used as extension names (they shadow core TestContext). */
689
- const EXTEND_RESERVED_KEYS = new Set(["vars", "secrets", "http"]);
690
- /**
691
- * Select examples from a named map based on the GLUBEAN_PICK env var
692
- * or random selection. Shared between `test.pick` and extended test `.pick()`.
693
- *
694
- * @internal
695
- */
696
- function selectPickExamples(examples, count) {
697
- const keys = Object.keys(examples);
698
- if (keys.length === 0) {
699
- throw new Error("test.pick requires at least one example");
700
- }
701
- let pickedEnv;
702
- try {
703
- pickedEnv = typeof process !== "undefined" ? process.env["GLUBEAN_PICK"] : undefined;
704
- }
705
- catch {
706
- pickedEnv = undefined;
707
- }
708
- if (pickedEnv) {
709
- const trimmed = pickedEnv.trim();
710
- if (trimmed === "all" || trimmed === "*") {
711
- return keys.map((k) => ({ ...examples[k], _pick: k }));
712
- }
713
- const pickedKeys = trimmed
714
- .split(",")
715
- .map((k) => k.trim())
716
- .filter((k) => k.length > 0);
717
- const hasGlob = pickedKeys.some((k) => k.includes("*"));
718
- let validKeys;
719
- if (hasGlob) {
720
- const patterns = pickedKeys.map((p) => globToRegExp(p));
721
- validKeys = keys.filter((k) => patterns.some((re) => re.test(k)));
722
- }
723
- else {
724
- validKeys = pickedKeys.filter((k) => k in examples);
725
- }
726
- if (validKeys.length > 0) {
727
- return validKeys.map((k) => ({ ...examples[k], _pick: k }));
728
- }
729
- }
730
- // Random selection fallback
731
- const shuffled = [...keys].sort(() => Math.random() - 0.5);
732
- const picked = shuffled.slice(0, Math.min(count, keys.length));
733
- return picked.map((k) => ({ ...examples[k], _pick: k }));
734
- }
735
- /**
736
- * Create an extended test function with fixture definitions.
737
- *
738
- * @internal
739
- */
740
- function createExtendedTest(allFixtures) {
741
- // Validate no reserved keys
742
- for (const key of Object.keys(allFixtures)) {
743
- if (EXTEND_RESERVED_KEYS.has(key)) {
744
- throw new Error(`Cannot extend test context with reserved key "${key}". ` +
745
- `Reserved keys: ${[...EXTEND_RESERVED_KEYS].join(", ")}.`);
746
- }
747
- }
748
- // The callable part — quick mode and builder mode
749
- function extTest(idOrMeta, fn) {
750
- if (fn) {
751
- // Quick mode
752
- const meta = typeof idOrMeta === "string"
753
- ? { id: idOrMeta, name: idOrMeta }
754
- : { name: idOrMeta.id, ...idOrMeta };
755
- if (meta.tags)
756
- meta.tags = toArray(meta.tags);
757
- const testDef = {
758
- meta,
759
- type: "simple",
760
- fn: fn,
761
- fixtures: allFixtures,
762
- };
763
- registerTest({
764
- id: meta.id,
765
- name: meta.name || meta.id,
766
- type: "simple",
767
- tags: toArray(meta.tags),
768
- description: meta.description,
769
- });
770
- return testDef;
771
- }
772
- // Builder mode
773
- const id = typeof idOrMeta === "string" ? idOrMeta : idOrMeta.id;
774
- const builder = new TestBuilder(id, allFixtures);
775
- if (typeof idOrMeta !== "string") {
776
- builder.meta(idOrMeta);
777
- }
778
- return builder;
779
- }
780
- // .extend() — chained extension
781
- extTest.extend = (extensions) => {
782
- return createExtendedTest({
783
- ...allFixtures,
784
- ...extensions,
785
- });
786
- };
787
- // .each() — data-driven with fixtures
788
- extTest.each = (table, options) => {
789
- const rows = normalizeEachTable(table);
790
- const legacyParallel = options?.parallel ?? false;
791
- return ((idOrMeta, fn) => {
792
- const baseMeta = resolveBaseMeta(idOrMeta);
793
- const parallel = baseMeta.parallel ?? legacyParallel;
794
- if (!fn) {
795
- return new EachBuilder(baseMeta, rows, allFixtures, parallel);
796
- }
797
- // Simple mode with fixtures
798
- const filteredTable = baseMeta.filter
799
- ? rows.filter((row, i) => baseMeta.filter(row, i))
800
- : rows;
801
- const tagFieldNames = toArray(baseMeta.tagFields);
802
- const staticTags = toArray(baseMeta.tags);
803
- const isPick = filteredTable.length > 0 && "_pick" in filteredTable[0];
804
- const hasGroup = isPick || parallel;
805
- return filteredTable.map((row, index) => {
806
- const id = interpolateTemplate(baseMeta.id, row, index);
807
- const name = baseMeta.name ? interpolateTemplate(baseMeta.name, row, index) : id;
808
- const dynamicTags = tagFieldNames
809
- .map((field) => {
810
- const value = row[field];
811
- return value != null ? `${field}:${value}` : null;
812
- })
813
- .filter((t) => t !== null);
814
- const allTags = [...staticTags, ...dynamicTags];
815
- const meta = {
816
- ...baseMeta,
817
- id,
818
- name,
819
- tags: allTags.length > 0 ? allTags : undefined,
820
- };
821
- const testDef = {
822
- meta,
823
- type: "simple",
824
- fn: (async (ctx) => await fn(ctx, row)),
825
- fixtures: allFixtures,
826
- };
827
- registerTest({
828
- id: meta.id,
829
- name: meta.name || meta.id,
830
- type: "simple",
831
- tags: allTags.length > 0 ? allTags : undefined,
832
- description: meta.description,
833
- ...(hasGroup ? { groupId: baseMeta.id } : {}),
834
- ...(parallel ? { parallel: true } : {}),
835
- });
836
- return testDef;
837
- });
838
- });
839
- };
840
- // .pick() — example selection with fixtures
841
- extTest.pick = (examples, count = 1) => {
842
- const selected = selectPickExamples(examples, count);
843
- return extTest.each(selected);
844
- };
845
- return extTest;
846
- }
847
49
  (function (test) {
848
50
  function only(idOrMeta, fn) {
849
51
  const baseMeta = typeof idOrMeta === "string" ? { id: idOrMeta, name: idOrMeta } : idOrMeta;
@@ -865,11 +67,9 @@ function createExtendedTest(allFixtures) {
865
67
  return ((idOrMeta, fn) => {
866
68
  const baseMeta = resolveBaseMeta(idOrMeta);
867
69
  const parallel = baseMeta.parallel ?? legacyParallel;
868
- // Builder mode: no callback → return EachBuilder
869
70
  if (!fn) {
870
71
  return new EachBuilder(baseMeta, rows, undefined, parallel);
871
72
  }
872
- // Apply filter if present
873
73
  const filteredTable = baseMeta.filter
874
74
  ? rows.filter((row, index) => baseMeta.filter(row, index))
875
75
  : rows;
@@ -877,11 +77,9 @@ function createExtendedTest(allFixtures) {
877
77
  const staticTags = toArray(baseMeta.tags);
878
78
  const isPick = filteredTable.length > 0 && "_pick" in filteredTable[0];
879
79
  const hasGroup = isPick || parallel;
880
- // Simple mode: with callback → return Test[]
881
80
  return filteredTable.map((row, index) => {
882
81
  const id = interpolateTemplate(baseMeta.id, row, index);
883
82
  const name = baseMeta.name ? interpolateTemplate(baseMeta.name, row, index) : id;
884
- // Compute tags: static tags + dynamic tagFields
885
83
  const dynamicTags = tagFieldNames
886
84
  .map((field) => {
887
85
  const value = row[field];
@@ -1026,32 +224,41 @@ function createExtendedTest(allFixtures) {
1026
224
  }
1027
225
  test.extend = extend;
1028
226
  })(test || (test = {}));
1029
- // ---------------------------------------------------------------------------
1030
- // Internal helpers
1031
- // ---------------------------------------------------------------------------
227
+ // =============================================================================
228
+ // Builder + data-driven re-exports
229
+ // =============================================================================
230
+ export { TestBuilder } from "./test-builder.js";
231
+ export { EachBuilder } from "./each-builder.js";
232
+ // =============================================================================
233
+ // Contract API
234
+ // =============================================================================
235
+ export { runFlow, normalizeFlow, extractMappings, extractMappingsOut, traceComputeFn, getAdapter, LensPurityError, } from "./contract-core.js";
236
+ // HTTP adapter — built-in, registers itself at SDK load time
237
+ import { contract as _contract } from "./contract-core.js";
238
+ import { httpAdapter } from "./contract-http/adapter.js";
239
+ import { createHttpRoot } from "./contract-http/factory.js";
240
+ _contract.register("http", httpAdapter);
241
+ {
242
+ const dispatcher = _contract.http;
243
+ _contract.http = createHttpRoot(dispatcher);
244
+ }
1032
245
  /**
1033
- * Convert a simple glob pattern (with `*` wildcards) to a RegExp.
1034
- * Only `*` is supported (matches any sequence of characters).
1035
- * @internal
246
+ * The `contract` namespace typed with built-in HTTP adapter.
247
+ *
248
+ * - `contract.http.with("name", defaults)` — scoped HTTP factory (built-in)
249
+ * - `contract.flow(id)` — protocol-agnostic flow builder
250
+ * - `contract.register(protocol, adapter)` — plugin extension point
251
+ * - `contract[protocol](id, spec)` — attached by `register()`
1036
252
  */
1037
- function globToRegExp(pattern) {
1038
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1039
- const regexStr = "^" + escaped.replace(/\*/g, ".*") + "$";
1040
- return new RegExp(regexStr);
1041
- }
1042
- // Re-export all types for user convenience
253
+ export const contract = _contract;
254
+ export { createHttpFactory, createHttpRoot, } from "./contract-http/factory.js";
255
+ // =============================================================================
256
+ // Utility + plugin re-exports
257
+ // =============================================================================
1043
258
  export * from "./types.js";
1044
- // Re-export data loaders for convenience
1045
- // Users can also import from "@glubean/sdk/data" directly
1046
259
  export { fromCsv, fromDir, fromJson, fromJsonl, fromYaml, toArray } from "./data.js";
1047
- // Re-export configure API
1048
260
  export { configure, resolveTemplate } from "./configure.js";
1049
- // Re-export plugin utilities
1050
261
  export { definePlugin } from "./plugin.js";
1051
- // Contract API
1052
- export { contract } from "./contract.js";
1053
- // Session API
1054
262
  export { defineSession, session } from "./session.js";
1055
- // Re-export assertion utilities
1056
263
  export { Expectation, ExpectFailError } from "./expect.js";
1057
264
  //# sourceMappingURL=index.js.map