@tailor-platform/sdk 1.76.2 → 1.78.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/{application-CqjCcZ2n.mjs → application-CUxVA7Fs.mjs} +8 -8
  3. package/dist/application-CUxVA7Fs.mjs.map +1 -0
  4. package/dist/application-Dp_-ioiP.mjs +3 -0
  5. package/dist/cli/commands/deploy/deploy.d.mts +8 -3
  6. package/dist/cli/commands/show.d.mts +9 -3
  7. package/dist/cli/commands/workspace/create.d.mts +3 -0
  8. package/dist/cli/commands/workspace/list.d.mts +1 -0
  9. package/dist/cli/commands/workspace/transform.d.mts +2 -0
  10. package/dist/cli/index.mjs +69 -22
  11. package/dist/cli/index.mjs.map +1 -1
  12. package/dist/cli/lib.d.mts +2 -2
  13. package/dist/cli/lib.mjs +2 -2
  14. package/dist/cli/lib.mjs.map +1 -1
  15. package/dist/cli/shared/workspace-name.d.mts +1 -0
  16. package/dist/cli/skills.mjs +1 -1
  17. package/dist/completion/zsh-worker.zsh +18 -1
  18. package/dist/configure/index.mjs +198 -4
  19. package/dist/configure/index.mjs.map +1 -1
  20. package/dist/configure/services/idp/index.d.mts +1 -4
  21. package/dist/{crashreport-DZDr-TvH.mjs → crashreport-BNWUM6Kf.mjs} +2 -2
  22. package/dist/{crashreport-DZDr-TvH.mjs.map → crashreport-BNWUM6Kf.mjs.map} +1 -1
  23. package/dist/{crashreport-np4-_DX2.mjs → crashreport-BafWvjjX.mjs} +1 -1
  24. package/dist/errors-DVFS0AZk.mjs +100 -0
  25. package/dist/errors-DVFS0AZk.mjs.map +1 -0
  26. package/dist/{globals-z6zYMjlr.mjs → globals-CvizbGLY.mjs} +2 -2
  27. package/dist/{globals-z6zYMjlr.mjs.map → globals-CvizbGLY.mjs.map} +1 -1
  28. package/dist/{logger-BEiZZ3qT.mjs → logger-BwS4ppwO.mjs} +3 -3
  29. package/dist/logger-BwS4ppwO.mjs.map +1 -0
  30. package/dist/{registry-CTgoVWc_.mjs → registry-CYBRvfaU.mjs} +2 -2
  31. package/dist/{registry-CTgoVWc_.mjs.map → registry-CYBRvfaU.mjs.map} +1 -1
  32. package/dist/{runtime-DlklyuHN.mjs → runtime-BD2vYJFT.mjs} +1169 -539
  33. package/dist/runtime-BD2vYJFT.mjs.map +1 -0
  34. package/dist/{schema-DbTR1c6M.mjs → schema-U6DxoZ8D.mjs} +203 -3
  35. package/dist/schema-U6DxoZ8D.mjs.map +1 -0
  36. package/dist/{secretmanager-Bd45j7an.mjs → secretmanager-IY4UvinW.mjs} +42 -9
  37. package/dist/secretmanager-IY4UvinW.mjs.map +1 -0
  38. package/dist/service-CghpdV-9.mjs +3 -0
  39. package/dist/{service-DBFZQloR.mjs → service-Cp0I7ZFe.mjs} +3 -3
  40. package/dist/{service-DBFZQloR.mjs.map → service-Cp0I7ZFe.mjs.map} +1 -1
  41. package/dist/{service-BCRJ-3NV.mjs → service-D12iQGcS.mjs} +2 -2
  42. package/dist/{service-BCRJ-3NV.mjs.map → service-D12iQGcS.mjs.map} +1 -1
  43. package/dist/{test-env-key-IPapa1KJ.mjs → test-env-key-DuZycyWM.mjs} +2 -2
  44. package/dist/{test-env-key-IPapa1KJ.mjs.map → test-env-key-DuZycyWM.mjs.map} +1 -1
  45. package/dist/types/auth.generated.d.mts +9 -36
  46. package/dist/types/executor.generated.d.mts +19 -59
  47. package/dist/types/idp.generated.d.mts +518 -517
  48. package/dist/types/resolver.generated.d.mts +6 -2
  49. package/dist/types/tailordb.generated.d.mts +8 -7
  50. package/dist/utils/test/index.mjs +1 -1
  51. package/dist/vitest/environment.mjs +1 -1
  52. package/dist/vitest/index.d.mts +8 -8
  53. package/dist/vitest/index.mjs +600 -186
  54. package/dist/vitest/index.mjs.map +1 -1
  55. package/dist/vitest/mock.d.mts +8 -8
  56. package/dist/vitest/mocks/aigateway.d.mts +14 -4
  57. package/dist/vitest/mocks/authconnection.d.mts +14 -4
  58. package/dist/vitest/mocks/file.d.mts +24 -6
  59. package/dist/vitest/mocks/iconv.d.mts +28 -4
  60. package/dist/vitest/mocks/idp.d.mts +25 -9
  61. package/dist/vitest/mocks/secretmanager.d.mts +13 -4
  62. package/dist/vitest/mocks/tailordb.d.mts +48 -7
  63. package/dist/vitest/mocks/workflow.d.mts +41 -9
  64. package/dist/vitest/setup.mjs +2 -2
  65. package/docs/cli/application.md +58 -11
  66. package/docs/cli/setup.md +1 -1
  67. package/docs/cli/workspace.md +6 -4
  68. package/docs/cli-reference.md +12 -8
  69. package/docs/runtime.md +3 -4
  70. package/docs/services/aigateway.md +2 -0
  71. package/docs/services/tailordb.md +42 -0
  72. package/docs/services/workflow.md +1 -1
  73. package/docs/testing.md +103 -89
  74. package/package.json +7 -7
  75. package/dist/application-6zsDLGKv.mjs +0 -3
  76. package/dist/application-CqjCcZ2n.mjs.map +0 -1
  77. package/dist/errors-Dtf2WPaW.mjs +0 -42
  78. package/dist/errors-Dtf2WPaW.mjs.map +0 -1
  79. package/dist/field-runtime-D4QWRiB1.mjs +0 -203
  80. package/dist/field-runtime-D4QWRiB1.mjs.map +0 -1
  81. package/dist/logger-BEiZZ3qT.mjs.map +0 -1
  82. package/dist/runtime-DlklyuHN.mjs.map +0 -1
  83. package/dist/schema-DbTR1c6M.mjs.map +0 -1
  84. package/dist/secretmanager-Bd45j7an.mjs.map +0 -1
  85. package/dist/service-CPMlIg3X.mjs +0 -3
  86. package/dist/types-BzJ7A_fn.mjs +0 -199
  87. package/dist/types-BzJ7A_fn.mjs.map +0 -1
  88. package/dist/types-CG9H7HuE.mjs +0 -4
@@ -1,8 +1,9 @@
1
- import { a as getRegisteredWorkflow, i as getRegisteredJob, t as TRIGGER_DEFAULT, u as platformSerialize } from "../registry-CTgoVWc_.mjs";
2
- import { i as writeWorkflowTestEnv, n as buildJobContext, r as clearWorkflowTestEnv } from "../test-env-key-IPapa1KJ.mjs";
1
+ import { a as getRegisteredWorkflow, i as getRegisteredJob, t as TRIGGER_DEFAULT, u as platformSerialize } from "../registry-CYBRvfaU.mjs";
2
+ import { a as writeWorkflowTestEnv, i as readWorkflowTestEnv, n as buildJobContext, r as clearWorkflowTestEnv } from "../test-env-key-DuZycyWM.mjs";
3
3
  import { t as assertDefined } from "../assert-DBxo8jPo.mjs";
4
- import { i as withDispose, n as tailorRoot, r as tailordbRoot, t as mockSecretmanager } from "../secretmanager-Bd45j7an.mjs";
4
+ import { i as withDispose, n as tailorRoot, r as tailordbRoot, t as mockSecretmanager } from "../secretmanager-IY4UvinW.mjs";
5
5
  import { builtinModules } from "node:module";
6
+ import { isEqual } from "es-toolkit";
6
7
  import { fileURLToPath } from "node:url";
7
8
  import { dirname, isAbsolute, matchesGlob, relative, resolve } from "node:path";
8
9
  import { ColumnNode, InsertQueryNode, Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler, PrimitiveValueListNode, ReferenceNode, UpdateQueryNode, ValueListNode, ValueNode, ValuesNode } from "kysely";
@@ -296,27 +297,31 @@ function createEnvironmentPlugin(options) {
296
297
  //#region src/vitest/mocks/aigateway.ts
297
298
  /**
298
299
  * Acquire a disposable mock for `tailor.aigateway`. Restored on dispose.
300
+ * @param options - Initial AI Gateway URL fixtures
299
301
  * @returns Disposable AI Gateway mock control object
300
302
  * @example
301
303
  * ```typescript
302
304
  * import { mockAigateway } from "@tailor-platform/sdk/vitest";
303
305
  *
304
306
  * test("resolves an AI Gateway URL", async () => {
305
- * using aigateway = mockAigateway();
306
- * aigateway.setUrls({ "my-aigateway": "https://my-aigateway.example.com" });
307
+ * using aigateway = mockAigateway({
308
+ * urls: { "my-aigateway": "https://my-aigateway.example.com" },
309
+ * });
310
+ * aigateway.setUrl("my-aigateway", "https://replacement.example.com");
307
311
  * // …
308
312
  * });
309
313
  * ```
310
314
  */
311
- function mockAigateway() {
315
+ function mockAigateway(options = {}) {
312
316
  const root = tailorRoot();
313
317
  const prev = root.aigateway;
314
- let urls = {};
315
- const get = vi.fn(async (name) => {
318
+ let urls = { ...options.urls };
319
+ async function defaultGet(name) {
316
320
  const url = urls[name];
317
321
  if (url === void 0) throw new Error(`No AI Gateway registered for "${name}". Acquire mockAigateway() and call setUrls(...).`);
318
322
  return { url };
319
- });
323
+ }
324
+ const get = vi.fn(defaultGet);
320
325
  root.aigateway = { get };
321
326
  return withDispose({
322
327
  /** The `get` `vi.fn`. */
@@ -324,12 +329,22 @@ function mockAigateway() {
324
329
  setUrls(value) {
325
330
  urls = value;
326
331
  },
332
+ setUrl(name, url) {
333
+ urls = {
334
+ ...urls,
335
+ [name]: url
336
+ };
337
+ },
327
338
  get calls() {
328
339
  return get.mock.calls.map(([name]) => ({ name }));
329
340
  },
341
+ clear() {
342
+ get.mockClear();
343
+ },
330
344
  reset() {
331
345
  urls = {};
332
- get.mockClear();
346
+ get.mockReset();
347
+ get.mockImplementation(defaultGet);
333
348
  }
334
349
  }, () => {
335
350
  root.aigateway = prev;
@@ -340,23 +355,30 @@ function mockAigateway() {
340
355
  //#region src/vitest/mocks/authconnection.ts
341
356
  /**
342
357
  * Acquire a disposable mock for `tailor.authconnection`. Restored on dispose.
358
+ * @param options - Initial token fixtures and fallback behavior
343
359
  * @returns Disposable AuthConnection mock control object
344
360
  * @example
345
361
  * ```typescript
346
362
  * import { mockAuthconnection } from "@tailor-platform/sdk/vitest";
347
363
  *
348
364
  * test("returns configured token", async () => {
349
- * using ac = mockAuthconnection();
350
- * ac.setTokens({ google: { access_token: "ya29.xxx" } });
365
+ * using ac = mockAuthconnection({ tokens: { google: { access_token: "ya29.xxx" } } });
366
+ * ac.setToken("google", { access_token: "replacement" });
351
367
  * // …
352
368
  * });
353
369
  * ```
354
370
  */
355
- function mockAuthconnection() {
371
+ function mockAuthconnection(options = {}) {
356
372
  const root = tailorRoot();
357
373
  const prev = root.authconnection;
358
- let tokens = {};
359
- const getConnectionToken = vi.fn(async (connectionName) => tokens[connectionName] ?? { access_token: "mock-token" });
374
+ let tokens = { ...options.tokens };
375
+ async function defaultGetConnectionToken(connectionName) {
376
+ const token = tokens[connectionName];
377
+ if (token) return token;
378
+ if (options.onUnhandled === "error") throw new Error(`No AuthConnection token configured for "${connectionName}"`);
379
+ return { access_token: "mock-token" };
380
+ }
381
+ const getConnectionToken = vi.fn(defaultGetConnectionToken);
360
382
  root.authconnection = { getConnectionToken };
361
383
  return withDispose({
362
384
  /** The `getConnectionToken` `vi.fn`. */
@@ -364,12 +386,22 @@ function mockAuthconnection() {
364
386
  setTokens(value) {
365
387
  tokens = value;
366
388
  },
389
+ setToken(connectionName, token) {
390
+ tokens = {
391
+ ...tokens,
392
+ [connectionName]: token
393
+ };
394
+ },
367
395
  get calls() {
368
396
  return getConnectionToken.mock.calls.map(([connectionName]) => ({ connectionName }));
369
397
  },
398
+ clear() {
399
+ getConnectionToken.mockClear();
400
+ },
370
401
  reset() {
371
402
  tokens = {};
372
- getConnectionToken.mockClear();
403
+ getConnectionToken.mockReset();
404
+ getConnectionToken.mockImplementation(defaultGetConnectionToken);
373
405
  }
374
406
  }, () => {
375
407
  root.authconnection = prev;
@@ -378,6 +410,16 @@ function mockAuthconnection() {
378
410
 
379
411
  //#endregion
380
412
  //#region src/vitest/mocks/file.ts
413
+ const FILE_METHODS = [
414
+ "upload",
415
+ "download",
416
+ "downloadAsBase64",
417
+ "delete",
418
+ "getMetadata",
419
+ "openDownloadStream",
420
+ "downloadStream",
421
+ "uploadStream"
422
+ ];
381
423
  const FILE_DEFAULTS = {
382
424
  upload: { metadata: {
383
425
  fileSize: 0,
@@ -401,7 +443,6 @@ const FILE_DEFAULTS = {
401
443
  lastUploadedAt: ""
402
444
  }
403
445
  },
404
- delete: void 0,
405
446
  getMetadata: {
406
447
  contentType: "",
407
448
  fileSize: 0,
@@ -414,27 +455,59 @@ const FILE_DEFAULTS = {
414
455
  sha256sum: ""
415
456
  } }
416
457
  };
458
+ function wrapFileIterator(inner, closeSource) {
459
+ let closePromise;
460
+ const closeSourceOnce = () => closePromise ??= Promise.resolve(closeSource());
461
+ const close = async () => {
462
+ try {
463
+ await inner.return?.();
464
+ } finally {
465
+ await closeSourceOnce();
466
+ }
467
+ };
468
+ const stream = {
469
+ async next() {
470
+ const result = await inner.next();
471
+ if (!result.done) assertStreamValue(result.value);
472
+ return result.done ? {
473
+ done: true,
474
+ value: void 0
475
+ } : result;
476
+ },
477
+ close,
478
+ async return(value) {
479
+ try {
480
+ return inner.return ? await inner.return(value) : {
481
+ done: true,
482
+ value
483
+ };
484
+ } finally {
485
+ await closeSourceOnce();
486
+ }
487
+ },
488
+ async throw(error) {
489
+ try {
490
+ if (inner.throw) return await inner.throw(error);
491
+ throw error;
492
+ } finally {
493
+ await closeSourceOnce();
494
+ }
495
+ },
496
+ [Symbol.asyncIterator]() {
497
+ return stream;
498
+ }
499
+ };
500
+ return stream;
501
+ }
417
502
  function toFileStream(value) {
418
- if (value !== null && typeof value === "object" && Symbol.asyncIterator in value && typeof value.close === "function") return value;
503
+ if (value !== null && typeof value === "object" && Symbol.asyncIterator in value && typeof value.close === "function") {
504
+ const source = value;
505
+ return wrapFileIterator(source[Symbol.asyncIterator](), () => source.close());
506
+ }
419
507
  if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) throw new TypeError("openDownloadStream expects an iterable of StreamValue items (e.g. [{ type: \"chunk\", data, position }, { type: \"complete\" }]); got raw bytes. Wrap the bytes in a structured chunk first.");
420
508
  if (value !== null && typeof value === "object" && (Symbol.iterator in value || Symbol.asyncIterator in value)) {
421
509
  const source = value;
422
- const inner = Symbol.asyncIterator in source ? source[Symbol.asyncIterator]() : source[Symbol.iterator]();
423
- const stream = {
424
- async next() {
425
- const r = await inner.next();
426
- if (!r.done) assertStreamValue(r.value);
427
- return r.done ? {
428
- done: true,
429
- value: void 0
430
- } : r;
431
- },
432
- async close() {},
433
- [Symbol.asyncIterator]() {
434
- return stream;
435
- }
436
- };
437
- return stream;
510
+ return wrapFileIterator(Symbol.asyncIterator in source ? source[Symbol.asyncIterator]() : source[Symbol.iterator](), () => {});
438
511
  }
439
512
  const empty = {
440
513
  async next() {
@@ -450,14 +523,15 @@ function toFileStream(value) {
450
523
  };
451
524
  return empty;
452
525
  }
453
- function assertStreamValue(v) {
454
- if (v === null || typeof v !== "object") throw new TypeError(`openDownloadStream expected a StreamValue item ({ type: "metadata" | "chunk" | "complete", ... }); got ${typeof v === "object" ? "null" : typeof v}.`);
455
- if (v instanceof ArrayBuffer || ArrayBuffer.isView(v)) throw new TypeError("openDownloadStream expected a StreamValue item, got raw bytes. Wrap the bytes in a structured chunk first (e.g. { type: \"chunk\", data, position }).");
456
- const type = v.type;
526
+ function assertStreamValue(value) {
527
+ if (value === null || typeof value !== "object") throw new TypeError(`openDownloadStream expected a StreamValue item ({ type: "metadata" | "chunk" | "complete", ... }); got ${typeof value === "object" ? "null" : typeof value}.`);
528
+ if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) throw new TypeError("openDownloadStream expected a StreamValue item, got raw bytes. Wrap the bytes in a structured chunk first (e.g. { type: \"chunk\", data, position }).");
529
+ const type = value.type;
457
530
  if (type !== "metadata" && type !== "chunk" && type !== "complete") throw new TypeError(`openDownloadStream expected a StreamValue item with type "metadata" | "chunk" | "complete"; got ${JSON.stringify(type)}.`);
458
531
  }
459
532
  /**
460
533
  * Acquire a disposable mock for `tailordb.file`. Restored on dispose.
534
+ * @param options - Controls behavior for calls without a configured result
461
535
  * @returns Disposable File mock control object
462
536
  * @example
463
537
  * ```typescript
@@ -465,77 +539,105 @@ function assertStreamValue(v) {
465
539
  *
466
540
  * test("mock file download", async () => {
467
541
  * using file = mockFile();
468
- * file.enqueueResult({ data: new Uint8Array([1, 2, 3]), metadata: { ... } });
542
+ * file.download.mockResolvedValue({ data: new Uint8Array(), metadata: { ... } });
469
543
  * // …
470
544
  * });
471
545
  * ```
472
546
  */
473
- function mockFile() {
547
+ function mockFile(options = {}) {
474
548
  const root = tailordbRoot();
475
549
  const prev = root.file;
550
+ const { onUnhandled = "fallback" } = options;
476
551
  const queue = [];
477
- let resolver = () => null;
478
552
  const calls = [];
553
+ let resolver = () => null;
479
554
  function handle(method, namespace, typeName, fieldName, recordId) {
480
- const call = {
555
+ if (queue.length > 0) return queue.shift();
556
+ const resolved = resolver(method, {
481
557
  method,
482
558
  namespace,
483
559
  typeName,
484
560
  fieldName,
485
561
  recordId
486
- };
487
- calls.push(call);
488
- if (queue.length > 0) return queue.shift();
489
- const resolved = resolver(method, call);
562
+ });
490
563
  if (resolved != null) return resolved;
564
+ if (onUnhandled === "error") throw new Error(`No File mock configured for "${method}"`);
491
565
  const fallback = FILE_DEFAULTS[method];
492
566
  return fallback === void 0 ? void 0 : structuredClone(fallback);
493
567
  }
568
+ const upload = vi.fn(async (...args) => {
569
+ const [namespace, typeName, fieldName, recordId] = args;
570
+ return handle("upload", namespace, typeName, fieldName, recordId);
571
+ });
572
+ const download = vi.fn(async (...args) => handle("download", ...args));
573
+ const downloadAsBase64 = vi.fn(async (...args) => handle("downloadAsBase64", ...args));
574
+ const deleteFile = vi.fn(async (...args) => {
575
+ handle("delete", ...args);
576
+ });
577
+ const getMetadata = vi.fn(async (...args) => handle("getMetadata", ...args));
578
+ const openDownloadStream = vi.fn(async (...args) => toFileStream(handle("openDownloadStream", ...args)));
579
+ const downloadStream = vi.fn(async (...args) => {
580
+ const resolved = handle("downloadStream", ...args);
581
+ if (resolved != null) return resolved;
582
+ return {
583
+ body: new ReadableStream({ start(controller) {
584
+ controller.close();
585
+ } }),
586
+ metadata: {
587
+ contentType: "",
588
+ fileSize: 0,
589
+ sha256sum: "",
590
+ lastUploadedAt: ""
591
+ }
592
+ };
593
+ });
594
+ const uploadStream = vi.fn(async (...args) => {
595
+ const [namespace, typeName, fieldName, recordId] = args;
596
+ return handle("uploadStream", namespace, typeName, fieldName, recordId);
597
+ });
598
+ const mocks = {
599
+ upload,
600
+ download,
601
+ downloadAsBase64,
602
+ delete: deleteFile,
603
+ getMetadata,
604
+ openDownloadStream,
605
+ downloadStream,
606
+ uploadStream
607
+ };
608
+ function track(method, operation) {
609
+ return function(...args) {
610
+ calls.push({
611
+ method,
612
+ namespace: args[0],
613
+ typeName: args[1],
614
+ fieldName: args[2],
615
+ recordId: args[3]
616
+ });
617
+ return operation.apply(this, args);
618
+ };
619
+ }
494
620
  root.file = {
495
- async upload(namespace, typeName, fieldName, recordId) {
496
- return handle("upload", namespace, typeName, fieldName, recordId);
497
- },
498
- async download(namespace, typeName, fieldName, recordId) {
499
- return handle("download", namespace, typeName, fieldName, recordId);
500
- },
501
- async downloadAsBase64(namespace, typeName, fieldName, recordId) {
502
- return handle("downloadAsBase64", namespace, typeName, fieldName, recordId);
503
- },
504
- async delete(namespace, typeName, fieldName, recordId) {
505
- handle("delete", namespace, typeName, fieldName, recordId);
506
- },
507
- async getMetadata(namespace, typeName, fieldName, recordId) {
508
- return handle("getMetadata", namespace, typeName, fieldName, recordId);
509
- },
510
- async openDownloadStream(namespace, typeName, fieldName, recordId) {
511
- return toFileStream(handle("openDownloadStream", namespace, typeName, fieldName, recordId));
512
- },
513
- async downloadStream(namespace, typeName, fieldName, recordId) {
514
- const resolved = handle("downloadStream", namespace, typeName, fieldName, recordId);
515
- if (resolved != null) return resolved;
516
- return {
517
- body: new ReadableStream({ start(c) {
518
- c.close();
519
- } }),
520
- metadata: {
521
- contentType: "",
522
- fileSize: 0,
523
- sha256sum: "",
524
- lastUploadedAt: ""
525
- }
526
- };
527
- },
528
- async uploadStream(namespace, typeName, fieldName, recordId) {
529
- return handle("uploadStream", namespace, typeName, fieldName, recordId);
530
- }
621
+ upload: track("upload", upload),
622
+ download: track("download", download),
623
+ downloadAsBase64: track("downloadAsBase64", downloadAsBase64),
624
+ delete: track("delete", deleteFile),
625
+ getMetadata: track("getMetadata", getMetadata),
626
+ openDownloadStream: track("openDownloadStream", openDownloadStream),
627
+ downloadStream: track("downloadStream", downloadStream),
628
+ uploadStream: track("uploadStream", uploadStream)
531
629
  };
630
+ function allMocks() {
631
+ return FILE_METHODS.map((method) => mocks[method]);
632
+ }
532
633
  return withDispose({
634
+ ...mocks,
533
635
  setResolver(value) {
534
636
  resolver = value;
535
637
  },
536
638
  /**
537
- * Enqueue a single result for the next `tailordb.file` call (FIFO; falls
538
- * back to `setResolver` when exhausted).
639
+ * Enqueue a single result for the next `tailordb.file` call.
640
+ * The queue is shared across all methods and namespaces.
539
641
  * @param result - Result to return from the next file call
540
642
  */
541
643
  enqueueResult(result) {
@@ -543,6 +645,7 @@ function mockFile() {
543
645
  },
544
646
  /**
545
647
  * Enqueue results for multiple subsequent `tailordb.file` calls.
648
+ * The queue is shared across all methods and namespaces.
546
649
  * @param results - Results to enqueue, one per upcoming call
547
650
  */
548
651
  enqueueResults(...results) {
@@ -551,10 +654,15 @@ function mockFile() {
551
654
  get calls() {
552
655
  return calls;
553
656
  },
657
+ clear() {
658
+ calls.length = 0;
659
+ for (const mock of allMocks()) mock.mockClear();
660
+ },
554
661
  reset() {
555
662
  queue.length = 0;
556
- resolver = () => null;
557
663
  calls.length = 0;
664
+ resolver = () => null;
665
+ for (const mock of allMocks()) mock.mockReset();
558
666
  }
559
667
  }, () => {
560
668
  root.file = prev;
@@ -573,11 +681,11 @@ function defaultIconvResult(method, args) {
573
681
  case "decode": return "";
574
682
  case "encode": return isUtf8(args[1]) ? "" : /* @__PURE__ */ new Uint8Array();
575
683
  case "encodings": return [];
576
- default: return;
577
684
  }
578
685
  }
579
686
  /**
580
687
  * Acquire a disposable mock for `tailor.iconv`. Restored on dispose.
688
+ * @param options - Fallback behavior for unconfigured operations
581
689
  * @returns Disposable Iconv mock control object
582
690
  * @example
583
691
  * ```typescript
@@ -585,27 +693,66 @@ function defaultIconvResult(method, args) {
585
693
  *
586
694
  * test("mock encoding conversion", () => {
587
695
  * using iconv = mockIconv();
588
- * iconv.setResolver((method) => (method === "decode" ? "decoded-text" : null));
696
+ * iconv.decode.mockReturnValue("decoded-text");
589
697
  * // …
590
698
  * });
591
699
  * ```
592
700
  */
593
- function mockIconv() {
701
+ function mockIconv(options = {}) {
594
702
  const root = tailorRoot();
595
703
  const prev = root.iconv;
596
704
  let resolver = null;
597
705
  const calls = [];
598
- function handle(method, args) {
599
- calls.push({
600
- method,
601
- args: [...args]
602
- });
706
+ function resolve(method, args) {
603
707
  if (resolver) {
604
708
  const result = resolver(method, args);
605
709
  if (result != null) return result;
606
710
  }
711
+ if (options.onUnhandled === "error") throw new Error(`No Iconv mock behavior configured for "${method}"`);
607
712
  return defaultIconvResult(method, args);
608
713
  }
714
+ function defaultConvert(input, fromEncoding, toEncoding) {
715
+ return resolve("convert", [
716
+ input,
717
+ fromEncoding,
718
+ toEncoding
719
+ ]);
720
+ }
721
+ function defaultConvertBuffer(input, fromEncoding, toEncoding) {
722
+ return resolve("convertBuffer", [
723
+ input,
724
+ fromEncoding,
725
+ toEncoding
726
+ ]);
727
+ }
728
+ function defaultDecode(input, encoding) {
729
+ return resolve("decode", [input, encoding]);
730
+ }
731
+ function defaultEncode(input, encoding) {
732
+ return resolve("encode", [input, encoding]);
733
+ }
734
+ function defaultEncodings() {
735
+ return resolve("encodings", []);
736
+ }
737
+ const convert = vi.fn(defaultConvert);
738
+ const convertBuffer = vi.fn(defaultConvertBuffer);
739
+ const decode = vi.fn(defaultDecode);
740
+ const encode = vi.fn(defaultEncode);
741
+ const encodings = vi.fn(defaultEncodings);
742
+ function track(method, operation) {
743
+ return function(...args) {
744
+ calls.push({
745
+ method,
746
+ args: [...args]
747
+ });
748
+ return operation.apply(this, args);
749
+ };
750
+ }
751
+ const trackedConvert = track("convert", convert);
752
+ const trackedConvertBuffer = track("convertBuffer", convertBuffer);
753
+ const trackedDecode = track("decode", decode);
754
+ const trackedEncode = track("encode", encode);
755
+ const trackedEncodings = track("encodings", encodings);
609
756
  class MockIconv {
610
757
  #fromEncoding;
611
758
  #toEncoding;
@@ -614,39 +761,56 @@ function mockIconv() {
614
761
  this.#toEncoding = toEncoding;
615
762
  }
616
763
  convert(input) {
617
- return handle("convert", [
618
- input,
619
- this.#fromEncoding,
620
- this.#toEncoding
621
- ]);
764
+ return trackedConvert.call(this, input, this.#fromEncoding, this.#toEncoding);
622
765
  }
623
766
  }
624
767
  root.iconv = {
625
- convert: (str, from, to) => handle("convert", [
626
- str,
627
- from,
628
- to
629
- ]),
630
- convertBuffer: (buf, from, to) => handle("convertBuffer", [
631
- buf,
632
- from,
633
- to
634
- ]),
635
- decode: (buf, encoding) => handle("decode", [buf, encoding]),
636
- encode: (str, encoding) => handle("encode", [str, encoding]),
637
- encodings: () => handle("encodings", []),
768
+ convert: trackedConvert,
769
+ convertBuffer: trackedConvertBuffer,
770
+ decode: trackedDecode,
771
+ encode: trackedEncode,
772
+ encodings: trackedEncodings,
638
773
  Iconv: MockIconv
639
774
  };
775
+ function clear() {
776
+ calls.length = 0;
777
+ convert.mockClear();
778
+ convertBuffer.mockClear();
779
+ decode.mockClear();
780
+ encode.mockClear();
781
+ encodings.mockClear();
782
+ }
640
783
  return withDispose({
784
+ /** The `convert` `vi.fn`. */
785
+ convert,
786
+ /** The `convertBuffer` `vi.fn`. */
787
+ convertBuffer,
788
+ /** The `decode` `vi.fn`. */
789
+ decode,
790
+ /** The `encode` `vi.fn`. */
791
+ encode,
792
+ /** The `encodings` `vi.fn`. */
793
+ encodings,
641
794
  setResolver(value) {
642
795
  resolver = value;
643
796
  },
644
797
  get calls() {
645
798
  return calls;
646
799
  },
800
+ clear,
647
801
  reset() {
648
802
  resolver = null;
649
803
  calls.length = 0;
804
+ convert.mockReset();
805
+ convert.mockImplementation(defaultConvert);
806
+ convertBuffer.mockReset();
807
+ convertBuffer.mockImplementation(defaultConvertBuffer);
808
+ decode.mockReset();
809
+ decode.mockImplementation(defaultDecode);
810
+ encode.mockReset();
811
+ encode.mockImplementation(defaultEncode);
812
+ encodings.mockReset();
813
+ encodings.mockImplementation(defaultEncodings);
650
814
  }
651
815
  }, () => {
652
816
  root.iconv = prev;
@@ -655,99 +819,131 @@ function mockIconv() {
655
819
 
656
820
  //#endregion
657
821
  //#region src/vitest/mocks/idp.ts
822
+ const IDP_METHODS = [
823
+ "users",
824
+ "user",
825
+ "userByName",
826
+ "createUser",
827
+ "updateUser",
828
+ "deleteUser",
829
+ "sendPasswordResetEmail",
830
+ "unenrollMfa"
831
+ ];
832
+ const IDP_USER_DEFAULT = {
833
+ id: "mock-id",
834
+ name: "mock-user",
835
+ disabled: false,
836
+ mfaEnrolled: false,
837
+ mfaFactorIds: []
838
+ };
658
839
  const IDP_DEFAULTS = {
659
840
  users: {
660
841
  users: [],
661
842
  nextPageToken: null,
662
843
  totalCount: 0
663
844
  },
664
- user: {
665
- id: "mock-id",
666
- name: "mock-user",
667
- disabled: false,
668
- mfaEnrolled: false,
669
- mfaFactorIds: []
670
- },
671
- userByName: {
672
- id: "mock-id",
673
- name: "mock-user",
674
- disabled: false,
675
- mfaEnrolled: false,
676
- mfaFactorIds: []
677
- },
678
- createUser: {
679
- id: "mock-id",
680
- name: "mock-user",
681
- disabled: false,
682
- mfaEnrolled: false,
683
- mfaFactorIds: []
684
- },
685
- updateUser: {
686
- id: "mock-id",
687
- name: "mock-user",
688
- disabled: false,
689
- mfaEnrolled: false,
690
- mfaFactorIds: []
691
- },
845
+ user: IDP_USER_DEFAULT,
846
+ userByName: IDP_USER_DEFAULT,
847
+ createUser: IDP_USER_DEFAULT,
848
+ updateUser: IDP_USER_DEFAULT,
692
849
  deleteUser: true,
693
850
  sendPasswordResetEmail: true,
694
851
  unenrollMfa: true
695
852
  };
696
853
  /**
697
854
  * Acquire a disposable mock for `tailor.idp`. Restored on dispose.
855
+ * @param options - Controls behavior for calls without a configured result
698
856
  * @returns Disposable IDP mock control object
699
857
  * @example
700
858
  * ```typescript
701
859
  * import { mockIdp } from "@tailor-platform/sdk/vitest";
702
860
  *
703
- * test("resolver-based", async () => {
861
+ * test("returns a user", async () => {
704
862
  * using idp = mockIdp();
705
- * idp.setResolver((method) =>
706
- * method === "user" ? { id: "u-1", name: "alice", disabled: false } : null,
707
- * );
863
+ * idp.namespace("my-idp").user.mockResolvedValue({
864
+ * id: "u-1",
865
+ * name: "alice",
866
+ * disabled: false,
867
+ * mfaEnrolled: false,
868
+ * mfaFactorIds: [],
869
+ * });
708
870
  * // …
709
871
  * });
710
872
  * ```
711
873
  */
712
- function mockIdp() {
874
+ function mockIdp(options = {}) {
713
875
  const root = tailorRoot();
714
876
  const prev = root.idp;
877
+ const { onUnhandled = "fallback" } = options;
715
878
  const queue = [];
716
- let resolver = () => null;
717
879
  const calls = [];
880
+ let resolver = () => null;
881
+ const namespaces = /* @__PURE__ */ new Map();
882
+ function compatibilityArgs(method, args) {
883
+ return method === "users" && args.length === 0 ? [void 0] : args;
884
+ }
718
885
  function handle(method, args, namespace) {
719
- calls.push({
720
- method,
721
- args,
722
- namespace
723
- });
724
886
  if (queue.length > 0) return queue.shift();
725
- const resolved = resolver(method, args, namespace);
887
+ const resolved = resolver(method, compatibilityArgs(method, args), namespace);
726
888
  if (resolved != null) return resolved;
727
- const fallback = IDP_DEFAULTS[method];
728
- return fallback === void 0 ? void 0 : structuredClone(fallback);
889
+ if (onUnhandled === "error") throw new Error(`No IdP mock configured for "${namespace}.${method}"`);
890
+ return structuredClone(IDP_DEFAULTS[method]);
729
891
  }
730
- const Client = vi.fn(function(config) {
731
- const namespace = config.namespace;
732
- this.users = async (options) => handle("users", [options], namespace);
733
- this.user = async (userId) => handle("user", [userId], namespace);
734
- this.userByName = async (name) => handle("userByName", [name], namespace);
735
- this.createUser = async (input) => handle("createUser", [input], namespace);
736
- this.updateUser = async (input) => handle("updateUser", [input], namespace);
737
- this.deleteUser = async (userId) => handle("deleteUser", [userId], namespace);
738
- this.sendPasswordResetEmail = async (input) => handle("sendPasswordResetEmail", [input], namespace);
739
- this.unenrollMfa = async (input) => handle("unenrollMfa", [input], namespace);
740
- });
892
+ function createNamespaceMocks(namespace) {
893
+ return {
894
+ users: vi.fn(async (...args) => handle("users", args, namespace)),
895
+ user: vi.fn(async (...args) => handle("user", args, namespace)),
896
+ userByName: vi.fn(async (...args) => handle("userByName", args, namespace)),
897
+ createUser: vi.fn(async (...args) => handle("createUser", args, namespace)),
898
+ updateUser: vi.fn(async (...args) => handle("updateUser", args, namespace)),
899
+ deleteUser: vi.fn(async (...args) => handle("deleteUser", args, namespace)),
900
+ sendPasswordResetEmail: vi.fn(async (...args) => handle("sendPasswordResetEmail", args, namespace)),
901
+ unenrollMfa: vi.fn(async (...args) => handle("unenrollMfa", args, namespace))
902
+ };
903
+ }
904
+ function namespace(name) {
905
+ const existing = namespaces.get(name);
906
+ if (existing) return existing;
907
+ const mocks = createNamespaceMocks(name);
908
+ namespaces.set(name, mocks);
909
+ return mocks;
910
+ }
911
+ function track(method, mock, namespaceName) {
912
+ return function(...args) {
913
+ calls.push({
914
+ method,
915
+ args: [...compatibilityArgs(method, args)],
916
+ namespace: namespaceName
917
+ });
918
+ return mock.apply(this, args);
919
+ };
920
+ }
921
+ const defaultClient = function(config) {
922
+ const mocks = namespace(config.namespace);
923
+ this.users = track("users", mocks.users, config.namespace);
924
+ this.user = track("user", mocks.user, config.namespace);
925
+ this.userByName = track("userByName", mocks.userByName, config.namespace);
926
+ this.createUser = track("createUser", mocks.createUser, config.namespace);
927
+ this.updateUser = track("updateUser", mocks.updateUser, config.namespace);
928
+ this.deleteUser = track("deleteUser", mocks.deleteUser, config.namespace);
929
+ this.sendPasswordResetEmail = track("sendPasswordResetEmail", mocks.sendPasswordResetEmail, config.namespace);
930
+ this.unenrollMfa = track("unenrollMfa", mocks.unenrollMfa, config.namespace);
931
+ };
932
+ const Client = vi.fn(defaultClient);
741
933
  root.idp = { Client };
934
+ function allMocks() {
935
+ return [...namespaces.values()].flatMap((mocks) => IDP_METHODS.map((method) => mocks[method]));
936
+ }
742
937
  return withDispose({
743
938
  /** The mock IDP `Client` constructor (`vi.fn`). */
744
939
  Client,
940
+ namespace,
745
941
  setResolver(value) {
746
942
  resolver = value;
747
943
  },
748
944
  /**
749
- * Enqueue a single result for the next IDP call (FIFO; falls back to
750
- * `setResolver` when exhausted).
945
+ * Enqueue a single result for the next IDP call.
946
+ * The queue is shared across all methods and namespaces.
751
947
  * @param result - Result to return from the next IDP call
752
948
  */
753
949
  enqueueResult(result) {
@@ -755,6 +951,7 @@ function mockIdp() {
755
951
  },
756
952
  /**
757
953
  * Enqueue results for multiple subsequent IDP calls.
954
+ * The queue is shared across all methods and namespaces.
758
955
  * @param results - Results to enqueue, one per upcoming call
759
956
  */
760
957
  enqueueResults(...results) {
@@ -763,10 +960,18 @@ function mockIdp() {
763
960
  get calls() {
764
961
  return calls;
765
962
  },
963
+ clear() {
964
+ calls.length = 0;
965
+ Client.mockClear();
966
+ for (const mock of allMocks()) mock.mockClear();
967
+ },
766
968
  reset() {
767
969
  queue.length = 0;
768
- resolver = () => null;
769
970
  calls.length = 0;
971
+ resolver = () => null;
972
+ Client.mockReset();
973
+ Client.mockImplementation(defaultClient);
974
+ for (const mock of allMocks()) mock.mockReset();
770
975
  }
771
976
  }, () => {
772
977
  root.idp = prev;
@@ -775,6 +980,15 @@ function mockIdp() {
775
980
 
776
981
  //#endregion
777
982
  //#region src/vitest/mocks/tailordb.ts
983
+ function testRegex(regex, value) {
984
+ const lastIndex = regex.lastIndex;
985
+ regex.lastIndex = 0;
986
+ try {
987
+ return regex.test(value);
988
+ } finally {
989
+ regex.lastIndex = lastIndex;
990
+ }
991
+ }
778
992
  var MockQueryResult = class {
779
993
  command;
780
994
  rowCount;
@@ -790,27 +1004,62 @@ var MockQueryResult = class {
790
1004
  * `tailordb.Client` whose `queryObject` is a shared `vi.fn()` (so query
791
1005
  * responses can be staged before the client is constructed). Restored on
792
1006
  * dispose.
1007
+ * @param options - Query fallback behavior
793
1008
  * @returns Disposable TailorDB mock control object
794
1009
  * @example
795
1010
  * ```typescript
796
1011
  * import { mockTailordb } from "@tailor-platform/sdk/vitest";
797
1012
  *
798
- * test("order-based", async () => {
1013
+ * test("query-based", async () => {
799
1014
  * using db = mockTailordb();
800
- * db.enqueueResults([], [{ age: 30 }], []); // BEGIN / SELECT / COMMIT
1015
+ * db.onQuery({ sql: /FROM users/, params: ["u-1"] }).returnsRows([{ age: 30 }]);
801
1016
  * // …
802
- * expect(db.queryObject).toHaveBeenCalledTimes(3);
1017
+ * expect(db.queryObject).toHaveBeenCalled();
803
1018
  * expect(db.Client).toHaveBeenCalledWith({ namespace: "tailordb" });
804
1019
  * });
805
1020
  * ```
806
1021
  */
807
- function mockTailordb() {
1022
+ function mockTailordb(options = {}) {
808
1023
  const root = tailordbRoot();
809
1024
  const prevClient = root.Client;
810
- const queryObject = vi.fn(async (_query, _params = []) => new MockQueryResult([]));
811
- const connect = vi.fn(async () => {});
1025
+ const rules = [];
1026
+ let queryResolver;
1027
+ function matchesQuery(matcher, query, params) {
1028
+ if (typeof matcher === "function") return matcher(query, params);
1029
+ if (typeof matcher === "string") return query === matcher;
1030
+ if (matcher instanceof RegExp) return testRegex(matcher, query);
1031
+ let sqlMatches;
1032
+ if (typeof matcher.sql === "string") sqlMatches = query === matcher.sql;
1033
+ else sqlMatches = testRegex(matcher.sql, query);
1034
+ if (!sqlMatches || matcher.params === void 0) return sqlMatches;
1035
+ if (typeof matcher.params === "function") return matcher.params(params);
1036
+ const expectedParams = matcher.params;
1037
+ return params.length === expectedParams.length && params.every((value, index) => isEqual(value, expectedParams[index]));
1038
+ }
1039
+ function queryResponse(response) {
1040
+ if (response.type === "error") throw response.error;
1041
+ return new MockQueryResult(response.rows);
1042
+ }
1043
+ async function defaultQuery(query, params = []) {
1044
+ for (let i = rules.length - 1; i >= 0; i -= 1) {
1045
+ const rule = rules[i];
1046
+ if (!rule || !matchesQuery(rule.matcher, query, params)) continue;
1047
+ const once = rule.once.shift();
1048
+ if (once) return queryResponse(once);
1049
+ if (rule.fallback) return queryResponse(rule.fallback);
1050
+ }
1051
+ if (queryResolver) return new MockQueryResult(queryResolver(query, params) ?? []);
1052
+ if (options.onUnhandled === "error") throw new Error(`No TailorDB query behavior matched: ${query}`);
1053
+ return new MockQueryResult([]);
1054
+ }
1055
+ const queryObject = vi.fn(defaultQuery);
1056
+ const defaultConnect = async () => {};
1057
+ const connect = vi.fn(defaultConnect);
812
1058
  const createdClients = [];
813
- const Client = vi.fn(function(config) {
1059
+ function enqueueRowsList(rowsList) {
1060
+ for (const rows of rowsList) queryObject.mockImplementationOnce(async () => new MockQueryResult(rows));
1061
+ }
1062
+ const defaultClient = function(config) {
814
1063
  const record = {
815
1064
  namespace: config?.namespace,
816
1065
  ended: false
@@ -830,7 +1079,8 @@ function mockTailordb() {
830
1079
  queryObject
831
1080
  };
832
1081
  };
833
- });
1082
+ };
1083
+ const Client = vi.fn(defaultClient);
834
1084
  root.Client = Client;
835
1085
  return withDispose({
836
1086
  /** The mock `tailordb.Client` constructor (`vi.fn`). */
@@ -842,7 +1092,53 @@ function mockTailordb() {
842
1092
  * @param resolver - Function that returns rows for a given query and params
843
1093
  */
844
1094
  setQueryResolver(resolver) {
845
- queryObject.mockImplementation(async (query, params = []) => new MockQueryResult(resolver(query, params) ?? []));
1095
+ queryResolver = resolver;
1096
+ queryObject.mockImplementation(defaultQuery);
1097
+ },
1098
+ /**
1099
+ * Configure responses for queries matching SQL text, parameters, or a predicate.
1100
+ * More recently registered matchers take precedence.
1101
+ * Do not combine matchers with a direct `queryObject.mockImplementation()` override.
1102
+ * @param matcher - Query matcher
1103
+ * @returns Chainable query behavior
1104
+ */
1105
+ onQuery(matcher) {
1106
+ const rule = {
1107
+ matcher,
1108
+ once: []
1109
+ };
1110
+ rules.push(rule);
1111
+ const behavior = {
1112
+ returnsRows(rows) {
1113
+ rule.fallback = {
1114
+ type: "rows",
1115
+ rows
1116
+ };
1117
+ return behavior;
1118
+ },
1119
+ returnsRowsOnce(rows) {
1120
+ rule.once.push({
1121
+ type: "rows",
1122
+ rows
1123
+ });
1124
+ return behavior;
1125
+ },
1126
+ rejects(error) {
1127
+ rule.fallback = {
1128
+ type: "error",
1129
+ error
1130
+ };
1131
+ return behavior;
1132
+ },
1133
+ rejectsOnce(error) {
1134
+ rule.once.push({
1135
+ type: "error",
1136
+ error
1137
+ });
1138
+ return behavior;
1139
+ }
1140
+ };
1141
+ return behavior;
846
1142
  },
847
1143
  /**
848
1144
  * Enqueue rows for the next `queryObject` call (FIFO; takes priority over
@@ -850,14 +1146,21 @@ function mockTailordb() {
850
1146
  * @param rows - Row objects to return from the next `queryObject` call
851
1147
  */
852
1148
  enqueueResult(...rows) {
853
- queryObject.mockImplementationOnce(async () => new MockQueryResult(rows));
1149
+ enqueueRowsList([rows]);
854
1150
  },
855
1151
  /**
856
1152
  * Enqueue rows for multiple subsequent `queryObject` calls (FIFO).
857
1153
  * @param rowsList - Rows arrays, one per upcoming query
858
1154
  */
859
1155
  enqueueResults(...rowsList) {
860
- for (const rows of rowsList) queryObject.mockImplementationOnce(async () => new MockQueryResult(rows));
1156
+ enqueueRowsList(rowsList);
1157
+ },
1158
+ /**
1159
+ * Enqueue row arrays for subsequent queries whose exact order is under test.
1160
+ * @param rowsList - Rows arrays, one per upcoming query
1161
+ */
1162
+ enqueueRows(...rowsList) {
1163
+ enqueueRowsList(rowsList);
861
1164
  },
862
1165
  /**
863
1166
  * All queries executed via `queryObject`, in order, derived from the vi.fn
@@ -877,13 +1180,24 @@ function mockTailordb() {
877
1180
  get createdClients() {
878
1181
  return createdClients;
879
1182
  },
1183
+ /** Clear recorded calls while preserving configured query behavior. */
1184
+ clear() {
1185
+ queryObject.mockClear();
1186
+ connect.mockClear();
1187
+ Client.mockClear();
1188
+ createdClients.length = 0;
1189
+ },
880
1190
  /** Reset query responses and recorded calls (keeps the mock installed). */
881
1191
  reset() {
882
1192
  queryObject.mockReset();
883
- queryObject.mockImplementation(async () => new MockQueryResult([]));
884
- connect.mockClear();
885
- Client.mockClear();
1193
+ queryObject.mockImplementation(defaultQuery);
1194
+ connect.mockReset();
1195
+ connect.mockImplementation(defaultConnect);
1196
+ Client.mockReset();
1197
+ Client.mockImplementation(defaultClient);
886
1198
  createdClients.length = 0;
1199
+ rules.length = 0;
1200
+ queryResolver = void 0;
887
1201
  }
888
1202
  }, () => {
889
1203
  root.Client = prevClient;
@@ -892,6 +1206,30 @@ function mockTailordb() {
892
1206
 
893
1207
  //#endregion
894
1208
  //#region src/vitest/mocks/workflow.ts
1209
+ const originalProcedures = /* @__PURE__ */ new WeakMap();
1210
+ function replaceProcedure(target, key, current) {
1211
+ const original = originalProcedures.get(current) ?? current;
1212
+ const defaultImplementation = function(...args) {
1213
+ return Reflect.apply(original, this, args);
1214
+ };
1215
+ const mock = vi.fn(defaultImplementation);
1216
+ originalProcedures.set(mock, original);
1217
+ const record = target;
1218
+ record[key] = mock;
1219
+ return {
1220
+ mock,
1221
+ scoped: {
1222
+ mockClear: () => mock.mockClear(),
1223
+ mockReset: () => mock.mockReset(),
1224
+ restore: () => {
1225
+ if (record[key] === mock) record[key] = current;
1226
+ }
1227
+ }
1228
+ };
1229
+ }
1230
+ function replaceTrigger(definition) {
1231
+ return replaceProcedure(definition, "trigger", definition.trigger);
1232
+ }
895
1233
  /**
896
1234
  * Acquire a disposable mock for workflow operations (`tailor.workflow`).
897
1235
  * Restored on dispose.
@@ -900,17 +1238,23 @@ function mockTailordb() {
900
1238
  * ```typescript
901
1239
  * import { mockWorkflow } from "@tailor-platform/sdk/vitest";
902
1240
  *
903
- * test("job handler", async () => {
1241
+ * test("job trigger", async () => {
904
1242
  * using wf = mockWorkflow();
905
- * wf.setJobHandler((name) => (name === "validate" ? { valid: true } : null));
906
- * await runWorkflowUnderTest(); // calls tailor.workflow.triggerJobFunction("validate", {})
907
- * expect(wf.triggerJobFunction).toHaveBeenCalledWith("validate", {});
1243
+ * const job = wf.job(validateOrder);
1244
+ * job.mockResolvedValue({ valid: true });
1245
+ * await runWorkflowUnderTest();
1246
+ * expect(job).toHaveBeenCalled();
908
1247
  * });
909
1248
  * ```
910
1249
  */
911
1250
  function mockWorkflow() {
912
1251
  const root = tailorRoot();
913
1252
  const prev = root.workflow;
1253
+ const prevEnv = readWorkflowTestEnv();
1254
+ const jobSpies = /* @__PURE__ */ new Map();
1255
+ const workflowSpies = /* @__PURE__ */ new Map();
1256
+ const waitPointMocks = /* @__PURE__ */ new Map();
1257
+ const scopedMocks = /* @__PURE__ */ new Set();
914
1258
  const defaultTriggerJob = (jobName, args, _options) => {
915
1259
  const body = getRegisteredJob(jobName);
916
1260
  return body ? body(args, buildJobContext()) : null;
@@ -954,6 +1298,64 @@ function mockWorkflow() {
954
1298
  /** The `resolve` `vi.fn`. */
955
1299
  resolve,
956
1300
  /**
1301
+ * Get a stable, typed mock for a workflow job's `trigger` method.
1302
+ * The real trigger behavior is used until an implementation or result is configured.
1303
+ * @param definition - Workflow job definition to mock
1304
+ * @returns Typed `trigger` mock for the definition
1305
+ */
1306
+ job(definition) {
1307
+ const existing = jobSpies.get(definition);
1308
+ if (existing) return existing;
1309
+ const { mock, scoped } = replaceTrigger(definition);
1310
+ scopedMocks.add(scoped);
1311
+ jobSpies.set(definition, mock);
1312
+ return mock;
1313
+ },
1314
+ /**
1315
+ * Get a stable, typed mock for a workflow definition's `trigger` method.
1316
+ * The real trigger behavior is used until an implementation or result is configured.
1317
+ * @param definition - Workflow definition to mock
1318
+ * @returns Typed `trigger` mock for the definition
1319
+ */
1320
+ workflow(definition) {
1321
+ const existing = workflowSpies.get(definition);
1322
+ if (existing) return existing;
1323
+ const { mock, scoped } = replaceTrigger(definition);
1324
+ workflowSpies.set(definition, mock);
1325
+ scopedMocks.add(scoped);
1326
+ return mock;
1327
+ },
1328
+ /**
1329
+ * Get stable, typed mocks for a wait point's `wait` and `resolve` methods.
1330
+ * @param definition - Wait point definition to mock
1331
+ * @returns Typed wait point mock control object
1332
+ */
1333
+ waitPoint(definition) {
1334
+ const existing = waitPointMocks.get(definition);
1335
+ if (existing) return existing;
1336
+ const waitReplacement = replaceProcedure(definition, "wait", definition.wait);
1337
+ const resolveReplacement = replaceProcedure(definition, "resolve", definition.resolve);
1338
+ const waitSpy = waitReplacement.mock;
1339
+ const resolveSpy = resolveReplacement.mock;
1340
+ scopedMocks.add(waitReplacement.scoped);
1341
+ scopedMocks.add(resolveReplacement.scoped);
1342
+ const waitPointMock = {
1343
+ wait: waitSpy,
1344
+ resolve: resolveSpy,
1345
+ /**
1346
+ * Invoke the next and subsequent resolve callbacks with a wait payload.
1347
+ * @param payload - Payload originally supplied to the wait point
1348
+ */
1349
+ setResolvePayload(payload) {
1350
+ resolveSpy.mockImplementation(async (_executionId, callback) => {
1351
+ platformSerialize(await callback(platformSerialize(payload)));
1352
+ });
1353
+ }
1354
+ };
1355
+ waitPointMocks.set(definition, waitPointMock);
1356
+ return waitPointMock;
1357
+ },
1358
+ /**
957
1359
  * Set a fallback job handler. Called when the enqueue queue is empty.
958
1360
  * @param handler - Function returning a result for a job name, args, and options
959
1361
  */
@@ -1048,6 +1450,15 @@ function mockWorkflow() {
1048
1450
  key
1049
1451
  }));
1050
1452
  },
1453
+ /** Clear recorded calls while preserving configured responses. */
1454
+ clear() {
1455
+ triggerJobFunction.mockClear();
1456
+ triggerWorkflow.mockClear();
1457
+ resumeWorkflow.mockClear();
1458
+ wait.mockClear();
1459
+ resolve.mockClear();
1460
+ for (const mock of scopedMocks) mock.mockClear();
1461
+ },
1051
1462
  /** Reset all workflow responses and recorded calls (keeps the mock installed). */
1052
1463
  reset() {
1053
1464
  triggerJobFunction.mockReset();
@@ -1060,11 +1471,14 @@ function mockWorkflow() {
1060
1471
  wait.mockImplementation(() => null);
1061
1472
  resolve.mockReset();
1062
1473
  resolve.mockImplementation(async () => {});
1474
+ for (const mock of scopedMocks) mock.mockReset();
1063
1475
  clearWorkflowTestEnv();
1064
1476
  }
1065
1477
  }, () => {
1478
+ for (const mock of scopedMocks) mock.restore();
1066
1479
  root.workflow = prev;
1067
- clearWorkflowTestEnv();
1480
+ if (prevEnv !== void 0) writeWorkflowTestEnv(prevEnv);
1481
+ else clearWorkflowTestEnv();
1068
1482
  });
1069
1483
  }
1070
1484