ai-functions 2.1.3 → 2.4.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 (284) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +90 -1
  3. package/README.md +38 -0
  4. package/dist/ai-promise.d.ts +3 -3
  5. package/dist/ai-promise.d.ts.map +1 -1
  6. package/dist/ai-promise.js +135 -64
  7. package/dist/ai-promise.js.map +1 -1
  8. package/dist/ai-schemas.d.ts +56 -0
  9. package/dist/ai-schemas.d.ts.map +1 -0
  10. package/dist/ai-schemas.js +53 -0
  11. package/dist/ai-schemas.js.map +1 -0
  12. package/dist/ai.d.ts +16 -242
  13. package/dist/ai.d.ts.map +1 -1
  14. package/dist/ai.js +51 -858
  15. package/dist/ai.js.map +1 -1
  16. package/dist/batch/anthropic.d.ts +6 -4
  17. package/dist/batch/anthropic.d.ts.map +1 -1
  18. package/dist/batch/anthropic.js +83 -145
  19. package/dist/batch/anthropic.js.map +1 -1
  20. package/dist/batch/bedrock.d.ts +8 -30
  21. package/dist/batch/bedrock.d.ts.map +1 -1
  22. package/dist/batch/bedrock.js +155 -338
  23. package/dist/batch/bedrock.js.map +1 -1
  24. package/dist/batch/cloudflare.d.ts +8 -20
  25. package/dist/batch/cloudflare.d.ts.map +1 -1
  26. package/dist/batch/cloudflare.js +68 -189
  27. package/dist/batch/cloudflare.js.map +1 -1
  28. package/dist/batch/google.d.ts +6 -20
  29. package/dist/batch/google.d.ts.map +1 -1
  30. package/dist/batch/google.js +70 -238
  31. package/dist/batch/google.js.map +1 -1
  32. package/dist/batch/index.d.ts +4 -1
  33. package/dist/batch/index.d.ts.map +1 -1
  34. package/dist/batch/index.js +4 -1
  35. package/dist/batch/index.js.map +1 -1
  36. package/dist/batch/memory.d.ts +1 -1
  37. package/dist/batch/memory.d.ts.map +1 -1
  38. package/dist/batch/memory.js +14 -10
  39. package/dist/batch/memory.js.map +1 -1
  40. package/dist/batch/openai.d.ts +11 -14
  41. package/dist/batch/openai.d.ts.map +1 -1
  42. package/dist/batch/openai.js +52 -156
  43. package/dist/batch/openai.js.map +1 -1
  44. package/dist/batch/provider.d.ts +111 -0
  45. package/dist/batch/provider.d.ts.map +1 -0
  46. package/dist/batch/provider.js +233 -0
  47. package/dist/batch/provider.js.map +1 -0
  48. package/dist/batch-map.d.ts.map +1 -1
  49. package/dist/batch-map.js +23 -17
  50. package/dist/batch-map.js.map +1 -1
  51. package/dist/batch-queue.d.ts +65 -0
  52. package/dist/batch-queue.d.ts.map +1 -1
  53. package/dist/batch-queue.js +169 -14
  54. package/dist/batch-queue.js.map +1 -1
  55. package/dist/budget.d.ts.map +1 -1
  56. package/dist/budget.js +27 -14
  57. package/dist/budget.js.map +1 -1
  58. package/dist/cache.d.ts +23 -0
  59. package/dist/cache.d.ts.map +1 -1
  60. package/dist/cache.js +36 -15
  61. package/dist/cache.js.map +1 -1
  62. package/dist/context.d.ts +26 -8
  63. package/dist/context.d.ts.map +1 -1
  64. package/dist/context.js +64 -62
  65. package/dist/context.js.map +1 -1
  66. package/dist/digital-objects-registry.d.ts +229 -0
  67. package/dist/digital-objects-registry.d.ts.map +1 -0
  68. package/dist/digital-objects-registry.js +617 -0
  69. package/dist/digital-objects-registry.js.map +1 -0
  70. package/dist/embeddings.d.ts +2 -2
  71. package/dist/embeddings.d.ts.map +1 -1
  72. package/dist/errors.d.ts +22 -0
  73. package/dist/errors.d.ts.map +1 -0
  74. package/dist/errors.js +35 -0
  75. package/dist/errors.js.map +1 -0
  76. package/dist/eval/runner.d.ts +8 -0
  77. package/dist/eval/runner.d.ts.map +1 -1
  78. package/dist/eval/runner.js +41 -35
  79. package/dist/eval/runner.js.map +1 -1
  80. package/dist/eval-log/in-memory.d.ts +34 -0
  81. package/dist/eval-log/in-memory.d.ts.map +1 -0
  82. package/dist/eval-log/in-memory.js +84 -0
  83. package/dist/eval-log/in-memory.js.map +1 -0
  84. package/dist/eval-log/index.d.ts +29 -0
  85. package/dist/eval-log/index.d.ts.map +1 -0
  86. package/dist/eval-log/index.js +39 -0
  87. package/dist/eval-log/index.js.map +1 -0
  88. package/dist/eval-log/types.d.ts +101 -0
  89. package/dist/eval-log/types.d.ts.map +1 -0
  90. package/dist/eval-log/types.js +16 -0
  91. package/dist/eval-log/types.js.map +1 -0
  92. package/dist/function-registry.d.ts +176 -0
  93. package/dist/function-registry.d.ts.map +1 -0
  94. package/dist/function-registry.js +685 -0
  95. package/dist/function-registry.js.map +1 -0
  96. package/dist/generate.d.ts +9 -3
  97. package/dist/generate.d.ts.map +1 -1
  98. package/dist/generate.js +18 -18
  99. package/dist/generate.js.map +1 -1
  100. package/dist/index.d.ts +18 -11
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +35 -18
  103. package/dist/index.js.map +1 -1
  104. package/dist/logger.d.ts +118 -0
  105. package/dist/logger.d.ts.map +1 -0
  106. package/dist/logger.js +187 -0
  107. package/dist/logger.js.map +1 -0
  108. package/dist/middleware/budget.d.ts +84 -0
  109. package/dist/middleware/budget.d.ts.map +1 -0
  110. package/dist/middleware/budget.js +110 -0
  111. package/dist/middleware/budget.js.map +1 -0
  112. package/dist/middleware/cache.d.ts +103 -0
  113. package/dist/middleware/cache.d.ts.map +1 -0
  114. package/dist/middleware/cache.js +228 -0
  115. package/dist/middleware/cache.js.map +1 -0
  116. package/dist/middleware/embed-cache.d.ts +99 -0
  117. package/dist/middleware/embed-cache.d.ts.map +1 -0
  118. package/dist/middleware/embed-cache.js +128 -0
  119. package/dist/middleware/embed-cache.js.map +1 -0
  120. package/dist/middleware/index.d.ts +11 -0
  121. package/dist/middleware/index.d.ts.map +1 -0
  122. package/dist/middleware/index.js +11 -0
  123. package/dist/middleware/index.js.map +1 -0
  124. package/dist/middleware/trace.d.ts +103 -0
  125. package/dist/middleware/trace.d.ts.map +1 -0
  126. package/dist/middleware/trace.js +176 -0
  127. package/dist/middleware/trace.js.map +1 -0
  128. package/dist/primitives.d.ts +120 -1
  129. package/dist/primitives.d.ts.map +1 -1
  130. package/dist/primitives.js +398 -26
  131. package/dist/primitives.js.map +1 -1
  132. package/dist/retry.d.ts +66 -1
  133. package/dist/retry.d.ts.map +1 -1
  134. package/dist/retry.js +115 -8
  135. package/dist/retry.js.map +1 -1
  136. package/dist/sandbox.d.ts +36 -0
  137. package/dist/sandbox.d.ts.map +1 -0
  138. package/dist/sandbox.js +44 -0
  139. package/dist/sandbox.js.map +1 -0
  140. package/dist/schema.js +2 -2
  141. package/dist/schema.js.map +1 -1
  142. package/dist/telemetry.d.ts +128 -0
  143. package/dist/telemetry.d.ts.map +1 -0
  144. package/dist/telemetry.js +285 -0
  145. package/dist/telemetry.js.map +1 -0
  146. package/dist/template.d.ts.map +1 -1
  147. package/dist/template.js +6 -1
  148. package/dist/template.js.map +1 -1
  149. package/dist/tool-orchestration.d.ts +66 -4
  150. package/dist/tool-orchestration.d.ts.map +1 -1
  151. package/dist/tool-orchestration.js +123 -23
  152. package/dist/tool-orchestration.js.map +1 -1
  153. package/dist/type-guards.d.ts +28 -0
  154. package/dist/type-guards.d.ts.map +1 -0
  155. package/dist/type-guards.js +29 -0
  156. package/dist/type-guards.js.map +1 -0
  157. package/dist/types.d.ts +155 -19
  158. package/dist/types.d.ts.map +1 -1
  159. package/dist/types.js +36 -1
  160. package/dist/types.js.map +1 -1
  161. package/dist/wrap-for-v3.d.ts +80 -0
  162. package/dist/wrap-for-v3.d.ts.map +1 -0
  163. package/dist/wrap-for-v3.js +89 -0
  164. package/dist/wrap-for-v3.js.map +1 -0
  165. package/examples/00-quickstart.ts +232 -0
  166. package/examples/01-rag-chatbot.ts +212 -0
  167. package/examples/02-multi-agent-research.ts +290 -0
  168. package/examples/03-email-classification.ts +379 -0
  169. package/examples/04-content-moderation.ts +400 -0
  170. package/examples/05-document-extraction.ts +455 -0
  171. package/examples/06-streaming-chat-nextjs.ts +437 -0
  172. package/examples/07-cloudflare-worker.ts +483 -0
  173. package/examples/08-batch-processing.ts +491 -0
  174. package/examples/09-budget-constrained.ts +527 -0
  175. package/examples/10-tool-orchestration.ts +565 -0
  176. package/examples/11-retry-resilience.ts +403 -0
  177. package/examples/12-caching-strategies.ts +422 -0
  178. package/examples/README.md +145 -0
  179. package/package.json +29 -25
  180. package/src/ai-promise.ts +226 -140
  181. package/src/ai-schemas.ts +122 -0
  182. package/src/ai.ts +71 -1176
  183. package/src/batch/anthropic.ts +96 -161
  184. package/src/batch/bedrock.ts +203 -454
  185. package/src/batch/cloudflare.ts +99 -282
  186. package/src/batch/google.ts +91 -297
  187. package/src/batch/index.ts +4 -1
  188. package/src/batch/memory.ts +15 -10
  189. package/src/batch/openai.ts +65 -193
  190. package/src/batch/provider.ts +336 -0
  191. package/src/batch-map.ts +29 -24
  192. package/src/batch-queue.ts +200 -11
  193. package/src/budget.ts +31 -18
  194. package/src/cache.ts +45 -17
  195. package/src/context.ts +106 -77
  196. package/src/digital-objects-registry.ts +750 -0
  197. package/src/errors.ts +37 -0
  198. package/src/eval/runner.ts +60 -36
  199. package/src/eval-log/in-memory.ts +90 -0
  200. package/src/eval-log/index.ts +46 -0
  201. package/src/eval-log/types.ts +110 -0
  202. package/src/function-registry.ts +874 -0
  203. package/src/generate.ts +33 -28
  204. package/src/index.ts +122 -21
  205. package/src/logger.ts +232 -0
  206. package/src/middleware/budget.ts +171 -0
  207. package/src/middleware/cache.ts +299 -0
  208. package/src/middleware/embed-cache.ts +195 -0
  209. package/src/middleware/index.ts +23 -0
  210. package/src/middleware/trace.ts +248 -0
  211. package/src/primitives.ts +589 -62
  212. package/src/retry.ts +144 -18
  213. package/src/sandbox.ts +52 -0
  214. package/src/schema.ts +8 -8
  215. package/src/telemetry.ts +403 -0
  216. package/src/template.ts +8 -4
  217. package/src/tool-orchestration.ts +213 -48
  218. package/src/type-guards.ts +31 -0
  219. package/src/types.ts +186 -27
  220. package/src/wrap-for-v3.ts +105 -0
  221. package/test/ai-promise.test.ts +1080 -0
  222. package/test/ai-proxy.test.ts +1 -1
  223. package/test/batch-autosubmit-errors.test.ts +49 -37
  224. package/test/batch-blog-posts.test.ts +87 -129
  225. package/test/core-functions.test.ts +183 -579
  226. package/test/decide.test.ts +154 -322
  227. package/test/define.test.ts +211 -8
  228. package/test/digital-objects-registry.test.ts +760 -0
  229. package/test/embedding-cache-middleware.test.ts +140 -0
  230. package/test/fill-template.test.ts +89 -0
  231. package/test/generate-core.test.ts +140 -229
  232. package/test/implicit-batch.test.ts +22 -65
  233. package/test/retry-policy-integration.test.ts +117 -0
  234. package/test/sandbox-execution.test.ts +155 -0
  235. package/test/schema.test.ts +55 -19
  236. package/test/template.test.ts +1164 -0
  237. package/test/tool-orchestration.test.ts +270 -0
  238. package/test/wrap-for-v3.test.ts +612 -0
  239. package/vitest.config.js +6 -0
  240. package/vitest.config.ts +20 -0
  241. package/LICENSE +0 -21
  242. package/dist/rpc/auth.d.ts +0 -69
  243. package/dist/rpc/auth.d.ts.map +0 -1
  244. package/dist/rpc/auth.js +0 -136
  245. package/dist/rpc/auth.js.map +0 -1
  246. package/dist/rpc/client.d.ts +0 -62
  247. package/dist/rpc/client.d.ts.map +0 -1
  248. package/dist/rpc/client.js +0 -103
  249. package/dist/rpc/client.js.map +0 -1
  250. package/dist/rpc/deferred.d.ts +0 -60
  251. package/dist/rpc/deferred.d.ts.map +0 -1
  252. package/dist/rpc/deferred.js +0 -96
  253. package/dist/rpc/deferred.js.map +0 -1
  254. package/dist/rpc/index.d.ts +0 -22
  255. package/dist/rpc/index.d.ts.map +0 -1
  256. package/dist/rpc/index.js +0 -38
  257. package/dist/rpc/index.js.map +0 -1
  258. package/dist/rpc/local.d.ts +0 -42
  259. package/dist/rpc/local.d.ts.map +0 -1
  260. package/dist/rpc/local.js +0 -50
  261. package/dist/rpc/local.js.map +0 -1
  262. package/dist/rpc/server.d.ts +0 -165
  263. package/dist/rpc/server.d.ts.map +0 -1
  264. package/dist/rpc/server.js +0 -405
  265. package/dist/rpc/server.js.map +0 -1
  266. package/dist/rpc/session.d.ts +0 -32
  267. package/dist/rpc/session.d.ts.map +0 -1
  268. package/dist/rpc/session.js +0 -43
  269. package/dist/rpc/session.js.map +0 -1
  270. package/dist/rpc/transport.d.ts +0 -306
  271. package/dist/rpc/transport.d.ts.map +0 -1
  272. package/dist/rpc/transport.js +0 -731
  273. package/dist/rpc/transport.js.map +0 -1
  274. package/src/batch/anthropic.js +0 -256
  275. package/src/batch/bedrock.js +0 -584
  276. package/src/batch/cloudflare.js +0 -287
  277. package/src/batch/google.js +0 -359
  278. package/src/batch/index.js +0 -30
  279. package/src/batch/memory.js +0 -187
  280. package/src/batch/openai.js +0 -402
  281. package/src/eval/index.js +0 -7
  282. package/src/eval/models.js +0 -119
  283. package/src/eval/runner.js +0 -147
  284. package/test/schema.test.js +0 -96
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Tests for embeddingCacheMiddleware — embedding-side analogue of
3
+ * cacheMiddleware for `wrapEmbeddingModel`.
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
7
+ import { wrapEmbeddingModel } from 'ai'
8
+ import { MockEmbeddingModelV3 } from 'ai/test'
9
+ import { embeddingCacheMiddleware } from '../src/index.js'
10
+
11
+ describe('embeddingCacheMiddleware', () => {
12
+ const originalGate = process.env['V3_EVAL_CACHE']
13
+
14
+ beforeEach(() => {
15
+ process.env['V3_EVAL_CACHE'] = '1'
16
+ })
17
+
18
+ afterEach(() => {
19
+ if (originalGate === undefined) {
20
+ delete process.env['V3_EVAL_CACHE']
21
+ } else {
22
+ process.env['V3_EVAL_CACHE'] = originalGate
23
+ }
24
+ })
25
+
26
+ it('returns cached embeddings on second call with same values', async () => {
27
+ let callCount = 0
28
+ const upstream = new MockEmbeddingModelV3({
29
+ modelId: 'test-embed',
30
+ doEmbed: async () => {
31
+ callCount++
32
+ return {
33
+ embeddings: [
34
+ [0.1, 0.2, 0.3],
35
+ [0.4, 0.5, 0.6],
36
+ ],
37
+ }
38
+ },
39
+ })
40
+
41
+ const wrapped = wrapEmbeddingModel({
42
+ model: upstream,
43
+ middleware: embeddingCacheMiddleware({ enabled: true }),
44
+ })
45
+
46
+ const r1 = await wrapped.doEmbed({ values: ['a', 'b'] })
47
+ expect(r1.embeddings).toEqual([
48
+ [0.1, 0.2, 0.3],
49
+ [0.4, 0.5, 0.6],
50
+ ])
51
+ expect(callCount).toBe(1)
52
+
53
+ const r2 = await wrapped.doEmbed({ values: ['a', 'b'] })
54
+ expect(r2.embeddings).toEqual([
55
+ [0.1, 0.2, 0.3],
56
+ [0.4, 0.5, 0.6],
57
+ ])
58
+ expect(callCount).toBe(1) // cache hit — no second upstream call
59
+ })
60
+
61
+ it('treats different value batches as separate keys', async () => {
62
+ let callCount = 0
63
+ const upstream = new MockEmbeddingModelV3({
64
+ modelId: 'test-embed',
65
+ doEmbed: async ({ values }) => {
66
+ callCount++
67
+ return {
68
+ embeddings: values.map((_, i) => [i, i + 1]),
69
+ }
70
+ },
71
+ })
72
+ const wrapped = wrapEmbeddingModel({
73
+ model: upstream,
74
+ middleware: embeddingCacheMiddleware({ enabled: true }),
75
+ })
76
+
77
+ await wrapped.doEmbed({ values: ['a'] })
78
+ await wrapped.doEmbed({ values: ['b'] })
79
+ expect(callCount).toBe(2)
80
+ })
81
+
82
+ it('falls through to upstream when env gate is unset', async () => {
83
+ delete process.env['V3_EVAL_CACHE']
84
+ let callCount = 0
85
+ const upstream = new MockEmbeddingModelV3({
86
+ modelId: 'test-embed',
87
+ doEmbed: async () => {
88
+ callCount++
89
+ return { embeddings: [[1, 2, 3]] }
90
+ },
91
+ })
92
+ const wrapped = wrapEmbeddingModel({
93
+ model: upstream,
94
+ middleware: embeddingCacheMiddleware(),
95
+ })
96
+
97
+ await wrapped.doEmbed({ values: ['x'] })
98
+ await wrapped.doEmbed({ values: ['x'] })
99
+ expect(callCount).toBe(2) // no caching when gate is off
100
+ })
101
+
102
+ it('respects explicit enabled: false override', async () => {
103
+ let callCount = 0
104
+ const upstream = new MockEmbeddingModelV3({
105
+ modelId: 'test-embed',
106
+ doEmbed: async () => {
107
+ callCount++
108
+ return { embeddings: [[1, 2, 3]] }
109
+ },
110
+ })
111
+ const wrapped = wrapEmbeddingModel({
112
+ model: upstream,
113
+ middleware: embeddingCacheMiddleware({ enabled: false }),
114
+ })
115
+
116
+ await wrapped.doEmbed({ values: ['x'] })
117
+ await wrapped.doEmbed({ values: ['x'] })
118
+ expect(callCount).toBe(2)
119
+ })
120
+
121
+ it('evicts entries past TTL and re-fetches', async () => {
122
+ let callCount = 0
123
+ const upstream = new MockEmbeddingModelV3({
124
+ modelId: 'test-embed',
125
+ doEmbed: async () => {
126
+ callCount++
127
+ return { embeddings: [[callCount]] }
128
+ },
129
+ })
130
+ const wrapped = wrapEmbeddingModel({
131
+ model: upstream,
132
+ middleware: embeddingCacheMiddleware({ enabled: true, ttlMs: -1 }),
133
+ // TTL = -1 → every entry is "older than -1 ms" → always evicted on access
134
+ })
135
+
136
+ await wrapped.doEmbed({ values: ['x'] })
137
+ await wrapped.doEmbed({ values: ['x'] })
138
+ expect(callCount).toBe(2) // TTL expired, re-fetch
139
+ })
140
+ })
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Tests for fillTemplate in function-registry.ts
3
+ *
4
+ * fillTemplate replaces {{key}} placeholders in a template string with values
5
+ * from an args record. Non-primitive values (objects/arrays) must serialize
6
+ * via JSON.stringify, not String(), to avoid "[object Object]" corruption.
7
+ */
8
+
9
+ import { describe, it, expect } from 'vitest'
10
+ import { fillTemplate } from '../src/function-registry.js'
11
+
12
+ describe('fillTemplate', () => {
13
+ describe('primitive values', () => {
14
+ it('interpolates a string value', () => {
15
+ expect(fillTemplate('Hello {{name}}!', { name: 'world' })).toBe('Hello world!')
16
+ })
17
+
18
+ it('interpolates a number value', () => {
19
+ expect(fillTemplate('Count: {{n}}', { n: 42 })).toBe('Count: 42')
20
+ })
21
+
22
+ it('interpolates a boolean value', () => {
23
+ expect(fillTemplate('Active: {{flag}}', { flag: true })).toBe('Active: true')
24
+ })
25
+
26
+ it('interpolates zero without stripping it', () => {
27
+ expect(fillTemplate('Value: {{v}}', { v: 0 })).toBe('Value: 0')
28
+ })
29
+
30
+ it('leaves placeholder empty when key is missing', () => {
31
+ expect(fillTemplate('{{missing}} value', {})).toBe(' value')
32
+ })
33
+
34
+ it('replaces multiple distinct placeholders', () => {
35
+ expect(fillTemplate('{{a}} + {{b}} = {{c}}', { a: 1, b: 2, c: 3 })).toBe('1 + 2 = 3')
36
+ })
37
+ })
38
+
39
+ describe('object and array values — must NOT produce [object Object]', () => {
40
+ it('serializes a plain object via JSON.stringify', () => {
41
+ const result = fillTemplate('Data: {{obj}}', { obj: { foo: 'bar', n: 1 } })
42
+ expect(result).not.toContain('[object Object]')
43
+ expect(result).toBe('Data: {"foo":"bar","n":1}')
44
+ })
45
+
46
+ it('serializes an array via JSON.stringify', () => {
47
+ const result = fillTemplate('Items: {{list}}', { list: ['a', 'b', 'c'] })
48
+ expect(result).not.toContain('[object Object]')
49
+ expect(result).toBe('Items: ["a","b","c"]')
50
+ })
51
+
52
+ it('serializes a nested object', () => {
53
+ const payload = { subject: 'AI', stats: { count: 5, tags: ['fast', 'smart'] } }
54
+ const result = fillTemplate('Payload: {{payload}}', { payload })
55
+ expect(result).not.toContain('[object Object]')
56
+ const parsed = JSON.parse(result.replace('Payload: ', ''))
57
+ expect(parsed.subject).toBe('AI')
58
+ expect(parsed.stats.count).toBe(5)
59
+ })
60
+
61
+ it('serializes an array of objects (upstream cascade step output pattern)', () => {
62
+ const steps = [{ id: 1, label: 'Subject' }, { id: 2, label: 'Problem' }]
63
+ const result = fillTemplate('Steps: {{steps}}', { steps })
64
+ expect(result).not.toContain('[object Object]')
65
+ const parsed = JSON.parse(result.replace('Steps: ', ''))
66
+ expect(parsed).toHaveLength(2)
67
+ expect(parsed[0].label).toBe('Subject')
68
+ })
69
+ })
70
+
71
+ describe('edge cases', () => {
72
+ it('keeps null as empty string (null coalesces to empty via ?? fallback)', () => {
73
+ // null ?? '' → '' → String('') → ''
74
+ expect(fillTemplate('{{v}}', { v: null as unknown as string })).toBe('')
75
+ })
76
+
77
+ it('keeps undefined key as empty string', () => {
78
+ expect(fillTemplate('{{v}}', {})).toBe('')
79
+ })
80
+
81
+ it('handles template with no placeholders', () => {
82
+ expect(fillTemplate('no placeholders here', { x: 1 })).toBe('no placeholders here')
83
+ })
84
+
85
+ it('handles empty template', () => {
86
+ expect(fillTemplate('', { x: 1 })).toBe('')
87
+ })
88
+ })
89
+ })