@plumbus/core 0.3.3 → 0.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 (572) hide show
  1. package/README.md +2 -2
  2. package/bin/plumbus.js +16 -2
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/ai/ai-service.d.ts.map +1 -1
  5. package/dist/ai/ai-service.js +8 -3
  6. package/dist/ai/ai-service.js.map +1 -1
  7. package/dist/ai/provider.d.ts.map +1 -1
  8. package/dist/ai/provider.js +1 -2
  9. package/dist/ai/provider.js.map +1 -1
  10. package/dist/ai/rag/pipeline.d.ts +4 -0
  11. package/dist/ai/rag/pipeline.d.ts.map +1 -1
  12. package/dist/ai/rag/pipeline.js +17 -2
  13. package/dist/ai/rag/pipeline.js.map +1 -1
  14. package/dist/api/route-generator.d.ts.map +1 -1
  15. package/dist/api/route-generator.js +10 -6
  16. package/dist/api/route-generator.js.map +1 -1
  17. package/dist/auth/adapter.d.ts +0 -4
  18. package/dist/auth/adapter.d.ts.map +1 -1
  19. package/dist/auth/adapter.js +4 -0
  20. package/dist/auth/adapter.js.map +1 -1
  21. package/dist/cli/cli.d.ts.map +1 -1
  22. package/dist/cli/cli.js +2 -1
  23. package/dist/cli/cli.js.map +1 -1
  24. package/dist/cli/commands/dev.d.ts +4 -1
  25. package/dist/cli/commands/dev.d.ts.map +1 -1
  26. package/dist/cli/commands/dev.js +21 -30
  27. package/dist/cli/commands/dev.js.map +1 -1
  28. package/dist/cli/commands/doctor.d.ts +6 -0
  29. package/dist/cli/commands/doctor.d.ts.map +1 -1
  30. package/dist/cli/commands/doctor.js +146 -2
  31. package/dist/cli/commands/doctor.js.map +1 -1
  32. package/dist/cli/commands/generate.d.ts.map +1 -1
  33. package/dist/cli/commands/generate.js +18 -0
  34. package/dist/cli/commands/generate.js.map +1 -1
  35. package/dist/cli/commands/index.d.ts +1 -0
  36. package/dist/cli/commands/index.d.ts.map +1 -1
  37. package/dist/cli/commands/index.js +1 -0
  38. package/dist/cli/commands/index.js.map +1 -1
  39. package/dist/cli/commands/init.d.ts +1 -1
  40. package/dist/cli/commands/init.d.ts.map +1 -1
  41. package/dist/cli/commands/init.js +113 -3
  42. package/dist/cli/commands/init.js.map +1 -1
  43. package/dist/cli/commands/mcp.d.ts +9 -0
  44. package/dist/cli/commands/mcp.d.ts.map +1 -0
  45. package/dist/cli/commands/mcp.js +125 -0
  46. package/dist/cli/commands/mcp.js.map +1 -0
  47. package/dist/cli/commands/migrate.d.ts.map +1 -1
  48. package/dist/cli/commands/migrate.js +2 -1
  49. package/dist/cli/commands/migrate.js.map +1 -1
  50. package/dist/cli/commands/start.d.ts +4 -1
  51. package/dist/cli/commands/start.d.ts.map +1 -1
  52. package/dist/cli/commands/start.js +20 -29
  53. package/dist/cli/commands/start.js.map +1 -1
  54. package/dist/cli/commands/translation.d.ts.map +1 -1
  55. package/dist/cli/commands/translation.js +22 -10
  56. package/dist/cli/commands/translation.js.map +1 -1
  57. package/dist/cli/mcp-serve-context.d.ts +13 -0
  58. package/dist/cli/mcp-serve-context.d.ts.map +1 -0
  59. package/dist/cli/mcp-serve-context.js +119 -0
  60. package/dist/cli/mcp-serve-context.js.map +1 -0
  61. package/dist/cli/migrate-snapshot-schema.d.ts +15 -0
  62. package/dist/cli/migrate-snapshot-schema.d.ts.map +1 -0
  63. package/dist/cli/migrate-snapshot-schema.js +23 -0
  64. package/dist/cli/migrate-snapshot-schema.js.map +1 -0
  65. package/dist/cli/project-root.d.ts +9 -0
  66. package/dist/cli/project-root.d.ts.map +1 -0
  67. package/dist/cli/project-root.js +41 -0
  68. package/dist/cli/project-root.js.map +1 -0
  69. package/dist/cli/templates/resources.d.ts.map +1 -1
  70. package/dist/cli/templates/resources.js +7 -1
  71. package/dist/cli/templates/resources.js.map +1 -1
  72. package/dist/config/loader.d.ts +1 -1
  73. package/dist/config/loader.d.ts.map +1 -1
  74. package/dist/config/loader.js +46 -27
  75. package/dist/config/loader.js.map +1 -1
  76. package/dist/data/connection.d.ts +16 -0
  77. package/dist/data/connection.d.ts.map +1 -0
  78. package/dist/data/connection.js +27 -0
  79. package/dist/data/connection.js.map +1 -0
  80. package/dist/data/index.d.ts +1 -0
  81. package/dist/data/index.d.ts.map +1 -1
  82. package/dist/data/index.js +1 -0
  83. package/dist/data/index.js.map +1 -1
  84. package/dist/data/migration.d.ts.map +1 -1
  85. package/dist/data/migration.js +24 -0
  86. package/dist/data/migration.js.map +1 -1
  87. package/dist/data/repository.d.ts.map +1 -1
  88. package/dist/data/repository.js +26 -9
  89. package/dist/data/repository.js.map +1 -1
  90. package/dist/define/defineCapability.d.ts +4 -2
  91. package/dist/define/defineCapability.d.ts.map +1 -1
  92. package/dist/define/defineCapability.js +68 -8
  93. package/dist/define/defineCapability.js.map +1 -1
  94. package/dist/define/defineEntity.d.ts.map +1 -1
  95. package/dist/define/defineEntity.js +8 -4
  96. package/dist/define/defineEntity.js.map +1 -1
  97. package/dist/define/defineEvent.d.ts.map +1 -1
  98. package/dist/define/defineEvent.js +7 -3
  99. package/dist/define/defineEvent.js.map +1 -1
  100. package/dist/define/defineFlow.d.ts.map +1 -1
  101. package/dist/define/defineFlow.js +11 -5
  102. package/dist/define/defineFlow.js.map +1 -1
  103. package/dist/define/definePrompt.d.ts.map +1 -1
  104. package/dist/define/definePrompt.js +11 -5
  105. package/dist/define/definePrompt.js.map +1 -1
  106. package/dist/define/defineTranslation.d.ts.map +1 -1
  107. package/dist/define/defineTranslation.js +12 -10
  108. package/dist/define/defineTranslation.js.map +1 -1
  109. package/dist/define/validation-error.d.ts +2 -0
  110. package/dist/define/validation-error.d.ts.map +1 -0
  111. package/dist/define/validation-error.js +9 -0
  112. package/dist/define/validation-error.js.map +1 -0
  113. package/dist/errors/hints.d.ts +13 -0
  114. package/dist/errors/hints.d.ts.map +1 -0
  115. package/dist/errors/hints.js +15 -0
  116. package/dist/errors/hints.js.map +1 -0
  117. package/dist/errors/hook-log.d.ts +3 -0
  118. package/dist/errors/hook-log.d.ts.map +1 -0
  119. package/dist/errors/hook-log.js +6 -0
  120. package/dist/errors/hook-log.js.map +1 -0
  121. package/dist/errors/http.d.ts +18 -0
  122. package/dist/errors/http.d.ts.map +1 -1
  123. package/dist/errors/http.js +32 -2
  124. package/dist/errors/http.js.map +1 -1
  125. package/dist/errors/safe-metadata.d.ts +5 -0
  126. package/dist/errors/safe-metadata.d.ts.map +1 -0
  127. package/dist/errors/safe-metadata.js +24 -0
  128. package/dist/errors/safe-metadata.js.map +1 -0
  129. package/dist/execution/auth-roles.d.ts +6 -0
  130. package/dist/execution/auth-roles.d.ts.map +1 -0
  131. package/dist/execution/auth-roles.js +12 -0
  132. package/dist/execution/auth-roles.js.map +1 -0
  133. package/dist/execution/authorization.d.ts.map +1 -1
  134. package/dist/execution/authorization.js +3 -5
  135. package/dist/execution/authorization.js.map +1 -1
  136. package/dist/execution/context-factory.d.ts +2 -17
  137. package/dist/execution/context-factory.d.ts.map +1 -1
  138. package/dist/execution/context-factory.js +1 -0
  139. package/dist/execution/context-factory.js.map +1 -1
  140. package/dist/flows/engine.js +1 -1
  141. package/dist/flows/engine.js.map +1 -1
  142. package/dist/flows/evaluate-condition.d.ts +19 -0
  143. package/dist/flows/evaluate-condition.d.ts.map +1 -0
  144. package/dist/flows/evaluate-condition.js +276 -0
  145. package/dist/flows/evaluate-condition.js.map +1 -0
  146. package/dist/flows/index.d.ts +1 -0
  147. package/dist/flows/index.d.ts.map +1 -1
  148. package/dist/flows/index.js +2 -0
  149. package/dist/flows/index.js.map +1 -1
  150. package/dist/flows/step-executor.d.ts.map +1 -1
  151. package/dist/flows/step-executor.js +9 -2
  152. package/dist/flows/step-executor.js.map +1 -1
  153. package/dist/governance/rules/architecture.d.ts.map +1 -1
  154. package/dist/governance/rules/architecture.js +2 -0
  155. package/dist/governance/rules/architecture.js.map +1 -1
  156. package/dist/governance/rules/index.d.ts +1 -0
  157. package/dist/governance/rules/index.d.ts.map +1 -1
  158. package/dist/governance/rules/index.js +1 -0
  159. package/dist/governance/rules/index.js.map +1 -1
  160. package/dist/governance/rules/mcp.d.ts +5 -0
  161. package/dist/governance/rules/mcp.d.ts.map +1 -0
  162. package/dist/governance/rules/mcp.js +34 -0
  163. package/dist/governance/rules/mcp.js.map +1 -0
  164. package/dist/index.d.ts +1 -0
  165. package/dist/index.d.ts.map +1 -1
  166. package/dist/index.js +1 -0
  167. package/dist/index.js.map +1 -1
  168. package/dist/mcp/exposure.d.ts +4 -0
  169. package/dist/mcp/exposure.d.ts.map +1 -0
  170. package/dist/mcp/exposure.js +5 -0
  171. package/dist/mcp/exposure.js.map +1 -0
  172. package/dist/mcp/index.d.ts +4 -0
  173. package/dist/mcp/index.d.ts.map +1 -0
  174. package/dist/mcp/index.js +6 -0
  175. package/dist/mcp/index.js.map +1 -0
  176. package/dist/mcp/manifest-generator.d.ts +19 -0
  177. package/dist/mcp/manifest-generator.d.ts.map +1 -0
  178. package/dist/mcp/manifest-generator.js +29 -0
  179. package/dist/mcp/manifest-generator.js.map +1 -0
  180. package/dist/mcp/skill-generator.d.ts +4 -0
  181. package/dist/mcp/skill-generator.d.ts.map +1 -0
  182. package/dist/mcp/skill-generator.js +59 -0
  183. package/dist/mcp/skill-generator.js.map +1 -0
  184. package/dist/schema/zod-input-to-json-schema.d.ts +4 -0
  185. package/dist/schema/zod-input-to-json-schema.d.ts.map +1 -0
  186. package/dist/schema/zod-input-to-json-schema.js +6 -0
  187. package/dist/schema/zod-input-to-json-schema.js.map +1 -0
  188. package/dist/server/bootstrap.d.ts.map +1 -1
  189. package/dist/server/bootstrap.js +5 -2
  190. package/dist/server/bootstrap.js.map +1 -1
  191. package/dist/testing/context.d.ts +2 -0
  192. package/dist/testing/context.d.ts.map +1 -1
  193. package/dist/testing/context.js +1 -0
  194. package/dist/testing/context.js.map +1 -1
  195. package/dist/testing/e2e.js +1 -1
  196. package/dist/testing/e2e.js.map +1 -1
  197. package/dist/types/capability.d.ts +13 -0
  198. package/dist/types/capability.d.ts.map +1 -1
  199. package/dist/types/config.d.ts +9 -0
  200. package/dist/types/config.d.ts.map +1 -1
  201. package/dist/types/context.d.ts +31 -0
  202. package/dist/types/context.d.ts.map +1 -1
  203. package/dist/types/deep-freeze.d.ts +6 -0
  204. package/dist/types/deep-freeze.d.ts.map +1 -0
  205. package/dist/types/deep-freeze.js +38 -0
  206. package/dist/types/deep-freeze.js.map +1 -0
  207. package/dist/types/enums.d.ts +5 -0
  208. package/dist/types/enums.d.ts.map +1 -1
  209. package/dist/types/enums.js +5 -0
  210. package/dist/types/enums.js.map +1 -1
  211. package/dist/types/index.d.ts +2 -2
  212. package/dist/types/index.d.ts.map +1 -1
  213. package/dist/types/index.js.map +1 -1
  214. package/dist/types/security.d.ts +3 -1
  215. package/dist/types/security.d.ts.map +1 -1
  216. package/instructions/capabilities.md +36 -0
  217. package/instructions/cli.md +44 -0
  218. package/instructions/framework.md +20 -4
  219. package/instructions/mcp.md +20 -0
  220. package/package.json +18 -2
  221. package/dist/ai/__tests__/ai-service.test.d.ts +0 -2
  222. package/dist/ai/__tests__/ai-service.test.d.ts.map +0 -1
  223. package/dist/ai/__tests__/ai-service.test.js +0 -1078
  224. package/dist/ai/__tests__/ai-service.test.js.map +0 -1
  225. package/dist/ai/__tests__/chunking.test.d.ts +0 -2
  226. package/dist/ai/__tests__/chunking.test.d.ts.map +0 -1
  227. package/dist/ai/__tests__/chunking.test.js +0 -78
  228. package/dist/ai/__tests__/chunking.test.js.map +0 -1
  229. package/dist/ai/__tests__/cost-tracker.test.d.ts +0 -2
  230. package/dist/ai/__tests__/cost-tracker.test.d.ts.map +0 -1
  231. package/dist/ai/__tests__/cost-tracker.test.js +0 -201
  232. package/dist/ai/__tests__/cost-tracker.test.js.map +0 -1
  233. package/dist/ai/__tests__/explainability.test.d.ts +0 -2
  234. package/dist/ai/__tests__/explainability.test.d.ts.map +0 -1
  235. package/dist/ai/__tests__/explainability.test.js +0 -100
  236. package/dist/ai/__tests__/explainability.test.js.map +0 -1
  237. package/dist/ai/__tests__/model-pricing.test.d.ts +0 -2
  238. package/dist/ai/__tests__/model-pricing.test.d.ts.map +0 -1
  239. package/dist/ai/__tests__/model-pricing.test.js +0 -156
  240. package/dist/ai/__tests__/model-pricing.test.js.map +0 -1
  241. package/dist/ai/__tests__/prompt-registry.test.d.ts +0 -2
  242. package/dist/ai/__tests__/prompt-registry.test.d.ts.map +0 -1
  243. package/dist/ai/__tests__/prompt-registry.test.js +0 -56
  244. package/dist/ai/__tests__/prompt-registry.test.js.map +0 -1
  245. package/dist/ai/__tests__/provider.test.d.ts +0 -3
  246. package/dist/ai/__tests__/provider.test.d.ts.map +0 -1
  247. package/dist/ai/__tests__/provider.test.js +0 -979
  248. package/dist/ai/__tests__/provider.test.js.map +0 -1
  249. package/dist/ai/__tests__/rag-pipeline.test.d.ts +0 -2
  250. package/dist/ai/__tests__/rag-pipeline.test.d.ts.map +0 -1
  251. package/dist/ai/__tests__/rag-pipeline.test.js +0 -174
  252. package/dist/ai/__tests__/rag-pipeline.test.js.map +0 -1
  253. package/dist/ai/__tests__/security.test.d.ts +0 -2
  254. package/dist/ai/__tests__/security.test.d.ts.map +0 -1
  255. package/dist/ai/__tests__/security.test.js +0 -89
  256. package/dist/ai/__tests__/security.test.js.map +0 -1
  257. package/dist/ai/__tests__/usage-client.test.d.ts +0 -2
  258. package/dist/ai/__tests__/usage-client.test.d.ts.map +0 -1
  259. package/dist/ai/__tests__/usage-client.test.js +0 -120
  260. package/dist/ai/__tests__/usage-client.test.js.map +0 -1
  261. package/dist/ai/__tests__/validation.test.d.ts +0 -2
  262. package/dist/ai/__tests__/validation.test.d.ts.map +0 -1
  263. package/dist/ai/__tests__/validation.test.js +0 -384
  264. package/dist/ai/__tests__/validation.test.js.map +0 -1
  265. package/dist/ai/__tests__/zod-to-provider-schema.test.d.ts +0 -2
  266. package/dist/ai/__tests__/zod-to-provider-schema.test.d.ts.map +0 -1
  267. package/dist/ai/__tests__/zod-to-provider-schema.test.js +0 -60
  268. package/dist/ai/__tests__/zod-to-provider-schema.test.js.map +0 -1
  269. package/dist/api/__tests__/route-generator.test.d.ts +0 -2
  270. package/dist/api/__tests__/route-generator.test.d.ts.map +0 -1
  271. package/dist/api/__tests__/route-generator.test.js +0 -216
  272. package/dist/api/__tests__/route-generator.test.js.map +0 -1
  273. package/dist/audit/__tests__/service.test.d.ts +0 -2
  274. package/dist/audit/__tests__/service.test.d.ts.map +0 -1
  275. package/dist/audit/__tests__/service.test.js +0 -97
  276. package/dist/audit/__tests__/service.test.js.map +0 -1
  277. package/dist/auth/__tests__/adapter.test.d.ts +0 -2
  278. package/dist/auth/__tests__/adapter.test.d.ts.map +0 -1
  279. package/dist/auth/__tests__/adapter.test.js +0 -218
  280. package/dist/auth/__tests__/adapter.test.js.map +0 -1
  281. package/dist/auth/__tests__/oidc-adapter.test.d.ts +0 -2
  282. package/dist/auth/__tests__/oidc-adapter.test.d.ts.map +0 -1
  283. package/dist/auth/__tests__/oidc-adapter.test.js +0 -232
  284. package/dist/auth/__tests__/oidc-adapter.test.js.map +0 -1
  285. package/dist/auth/__tests__/password.test.d.ts +0 -2
  286. package/dist/auth/__tests__/password.test.d.ts.map +0 -1
  287. package/dist/auth/__tests__/password.test.js +0 -30
  288. package/dist/auth/__tests__/password.test.js.map +0 -1
  289. package/dist/auth/__tests__/saml-adapter.test.d.ts +0 -2
  290. package/dist/auth/__tests__/saml-adapter.test.d.ts.map +0 -1
  291. package/dist/auth/__tests__/saml-adapter.test.js +0 -188
  292. package/dist/auth/__tests__/saml-adapter.test.js.map +0 -1
  293. package/dist/auth/__tests__/scim.test.d.ts +0 -2
  294. package/dist/auth/__tests__/scim.test.d.ts.map +0 -1
  295. package/dist/auth/__tests__/scim.test.js +0 -306
  296. package/dist/auth/__tests__/scim.test.js.map +0 -1
  297. package/dist/cli/__tests__/agent.test.d.ts +0 -2
  298. package/dist/cli/__tests__/agent.test.d.ts.map +0 -1
  299. package/dist/cli/__tests__/agent.test.js +0 -63
  300. package/dist/cli/__tests__/agent.test.js.map +0 -1
  301. package/dist/cli/__tests__/certify.test.d.ts +0 -2
  302. package/dist/cli/__tests__/certify.test.d.ts.map +0 -1
  303. package/dist/cli/__tests__/certify.test.js +0 -97
  304. package/dist/cli/__tests__/certify.test.js.map +0 -1
  305. package/dist/cli/__tests__/cli.test.d.ts +0 -2
  306. package/dist/cli/__tests__/cli.test.d.ts.map +0 -1
  307. package/dist/cli/__tests__/cli.test.js +0 -123
  308. package/dist/cli/__tests__/cli.test.js.map +0 -1
  309. package/dist/cli/__tests__/create.test.d.ts +0 -2
  310. package/dist/cli/__tests__/create.test.d.ts.map +0 -1
  311. package/dist/cli/__tests__/create.test.js +0 -183
  312. package/dist/cli/__tests__/create.test.js.map +0 -1
  313. package/dist/cli/__tests__/dev.test.d.ts +0 -2
  314. package/dist/cli/__tests__/dev.test.d.ts.map +0 -1
  315. package/dist/cli/__tests__/dev.test.js +0 -132
  316. package/dist/cli/__tests__/dev.test.js.map +0 -1
  317. package/dist/cli/__tests__/discover.test.d.ts +0 -2
  318. package/dist/cli/__tests__/discover.test.d.ts.map +0 -1
  319. package/dist/cli/__tests__/discover.test.js +0 -48
  320. package/dist/cli/__tests__/discover.test.js.map +0 -1
  321. package/dist/cli/__tests__/doctor.test.d.ts +0 -2
  322. package/dist/cli/__tests__/doctor.test.d.ts.map +0 -1
  323. package/dist/cli/__tests__/doctor.test.js +0 -172
  324. package/dist/cli/__tests__/doctor.test.js.map +0 -1
  325. package/dist/cli/__tests__/generate.test.d.ts +0 -2
  326. package/dist/cli/__tests__/generate.test.d.ts.map +0 -1
  327. package/dist/cli/__tests__/generate.test.js +0 -478
  328. package/dist/cli/__tests__/generate.test.js.map +0 -1
  329. package/dist/cli/__tests__/init.test.d.ts +0 -2
  330. package/dist/cli/__tests__/init.test.d.ts.map +0 -1
  331. package/dist/cli/__tests__/init.test.js +0 -236
  332. package/dist/cli/__tests__/init.test.js.map +0 -1
  333. package/dist/cli/__tests__/migrate.test.d.ts +0 -2
  334. package/dist/cli/__tests__/migrate.test.d.ts.map +0 -1
  335. package/dist/cli/__tests__/migrate.test.js +0 -442
  336. package/dist/cli/__tests__/migrate.test.js.map +0 -1
  337. package/dist/cli/__tests__/start.test.d.ts +0 -2
  338. package/dist/cli/__tests__/start.test.d.ts.map +0 -1
  339. package/dist/cli/__tests__/start.test.js +0 -91
  340. package/dist/cli/__tests__/start.test.js.map +0 -1
  341. package/dist/cli/__tests__/templates.test.d.ts +0 -2
  342. package/dist/cli/__tests__/templates.test.d.ts.map +0 -1
  343. package/dist/cli/__tests__/templates.test.js +0 -49
  344. package/dist/cli/__tests__/templates.test.js.map +0 -1
  345. package/dist/cli/__tests__/test.test.d.ts +0 -2
  346. package/dist/cli/__tests__/test.test.d.ts.map +0 -1
  347. package/dist/cli/__tests__/test.test.js +0 -40
  348. package/dist/cli/__tests__/test.test.js.map +0 -1
  349. package/dist/cli/__tests__/ui.test.d.ts +0 -2
  350. package/dist/cli/__tests__/ui.test.d.ts.map +0 -1
  351. package/dist/cli/__tests__/ui.test.js +0 -76
  352. package/dist/cli/__tests__/ui.test.js.map +0 -1
  353. package/dist/cli/__tests__/upgrade.test.d.ts +0 -2
  354. package/dist/cli/__tests__/upgrade.test.d.ts.map +0 -1
  355. package/dist/cli/__tests__/upgrade.test.js +0 -20
  356. package/dist/cli/__tests__/upgrade.test.js.map +0 -1
  357. package/dist/cli/__tests__/utils.test.d.ts +0 -2
  358. package/dist/cli/__tests__/utils.test.d.ts.map +0 -1
  359. package/dist/cli/__tests__/utils.test.js +0 -255
  360. package/dist/cli/__tests__/utils.test.js.map +0 -1
  361. package/dist/cli/__tests__/verify.test.d.ts +0 -2
  362. package/dist/cli/__tests__/verify.test.d.ts.map +0 -1
  363. package/dist/cli/__tests__/verify.test.js +0 -138
  364. package/dist/cli/__tests__/verify.test.js.map +0 -1
  365. package/dist/cli/commands/__tests__/commands.test.d.ts +0 -2
  366. package/dist/cli/commands/__tests__/commands.test.d.ts.map +0 -1
  367. package/dist/cli/commands/__tests__/commands.test.js +0 -185
  368. package/dist/cli/commands/__tests__/commands.test.js.map +0 -1
  369. package/dist/config/__tests__/loader.test.d.ts +0 -2
  370. package/dist/config/__tests__/loader.test.d.ts.map +0 -1
  371. package/dist/config/__tests__/loader.test.js +0 -462
  372. package/dist/config/__tests__/loader.test.js.map +0 -1
  373. package/dist/data/__tests__/drift-inspector.test.d.ts +0 -2
  374. package/dist/data/__tests__/drift-inspector.test.d.ts.map +0 -1
  375. package/dist/data/__tests__/drift-inspector.test.js +0 -170
  376. package/dist/data/__tests__/drift-inspector.test.js.map +0 -1
  377. package/dist/data/__tests__/framework-migration-rewriter.test.d.ts +0 -2
  378. package/dist/data/__tests__/framework-migration-rewriter.test.d.ts.map +0 -1
  379. package/dist/data/__tests__/framework-migration-rewriter.test.js +0 -329
  380. package/dist/data/__tests__/framework-migration-rewriter.test.js.map +0 -1
  381. package/dist/data/__tests__/migration.test.d.ts +0 -2
  382. package/dist/data/__tests__/migration.test.d.ts.map +0 -1
  383. package/dist/data/__tests__/migration.test.js +0 -145
  384. package/dist/data/__tests__/migration.test.js.map +0 -1
  385. package/dist/data/__tests__/registry.test.d.ts +0 -2
  386. package/dist/data/__tests__/registry.test.d.ts.map +0 -1
  387. package/dist/data/__tests__/registry.test.js +0 -70
  388. package/dist/data/__tests__/registry.test.js.map +0 -1
  389. package/dist/data/__tests__/repository.test.d.ts +0 -2
  390. package/dist/data/__tests__/repository.test.d.ts.map +0 -1
  391. package/dist/data/__tests__/repository.test.js +0 -559
  392. package/dist/data/__tests__/repository.test.js.map +0 -1
  393. package/dist/data/__tests__/schema-generator.test.d.ts +0 -2
  394. package/dist/data/__tests__/schema-generator.test.d.ts.map +0 -1
  395. package/dist/data/__tests__/schema-generator.test.js +0 -112
  396. package/dist/data/__tests__/schema-generator.test.js.map +0 -1
  397. package/dist/define/__tests__/defineCapability.test.d.ts +0 -2
  398. package/dist/define/__tests__/defineCapability.test.d.ts.map +0 -1
  399. package/dist/define/__tests__/defineCapability.test.js +0 -66
  400. package/dist/define/__tests__/defineCapability.test.js.map +0 -1
  401. package/dist/define/__tests__/defineEntity.test.d.ts +0 -2
  402. package/dist/define/__tests__/defineEntity.test.d.ts.map +0 -1
  403. package/dist/define/__tests__/defineEntity.test.js +0 -50
  404. package/dist/define/__tests__/defineEntity.test.js.map +0 -1
  405. package/dist/define/__tests__/defineEvent.test.d.ts +0 -2
  406. package/dist/define/__tests__/defineEvent.test.d.ts.map +0 -1
  407. package/dist/define/__tests__/defineEvent.test.js +0 -35
  408. package/dist/define/__tests__/defineEvent.test.js.map +0 -1
  409. package/dist/define/__tests__/defineFlow.test.d.ts +0 -2
  410. package/dist/define/__tests__/defineFlow.test.d.ts.map +0 -1
  411. package/dist/define/__tests__/defineFlow.test.js +0 -49
  412. package/dist/define/__tests__/defineFlow.test.js.map +0 -1
  413. package/dist/define/__tests__/definePrompt.test.d.ts +0 -2
  414. package/dist/define/__tests__/definePrompt.test.d.ts.map +0 -1
  415. package/dist/define/__tests__/definePrompt.test.js +0 -77
  416. package/dist/define/__tests__/definePrompt.test.js.map +0 -1
  417. package/dist/define/__tests__/defineTranslation.test.d.ts +0 -2
  418. package/dist/define/__tests__/defineTranslation.test.d.ts.map +0 -1
  419. package/dist/define/__tests__/defineTranslation.test.js +0 -90
  420. package/dist/define/__tests__/defineTranslation.test.js.map +0 -1
  421. package/dist/errors/__tests__/errors.test.d.ts +0 -2
  422. package/dist/errors/__tests__/errors.test.d.ts.map +0 -1
  423. package/dist/errors/__tests__/errors.test.js +0 -70
  424. package/dist/errors/__tests__/errors.test.js.map +0 -1
  425. package/dist/errors/__tests__/http.test.d.ts +0 -2
  426. package/dist/errors/__tests__/http.test.d.ts.map +0 -1
  427. package/dist/errors/__tests__/http.test.js +0 -35
  428. package/dist/errors/__tests__/http.test.js.map +0 -1
  429. package/dist/events/__tests__/consumer-registry.test.d.ts +0 -2
  430. package/dist/events/__tests__/consumer-registry.test.d.ts.map +0 -1
  431. package/dist/events/__tests__/consumer-registry.test.js +0 -68
  432. package/dist/events/__tests__/consumer-registry.test.js.map +0 -1
  433. package/dist/events/__tests__/dispatcher.test.d.ts +0 -2
  434. package/dist/events/__tests__/dispatcher.test.d.ts.map +0 -1
  435. package/dist/events/__tests__/dispatcher.test.js +0 -168
  436. package/dist/events/__tests__/dispatcher.test.js.map +0 -1
  437. package/dist/events/__tests__/event-emitter.test.d.ts +0 -2
  438. package/dist/events/__tests__/event-emitter.test.d.ts.map +0 -1
  439. package/dist/events/__tests__/event-emitter.test.js +0 -172
  440. package/dist/events/__tests__/event-emitter.test.js.map +0 -1
  441. package/dist/events/__tests__/event-registry.test.d.ts +0 -2
  442. package/dist/events/__tests__/event-registry.test.d.ts.map +0 -1
  443. package/dist/events/__tests__/event-registry.test.js +0 -55
  444. package/dist/events/__tests__/event-registry.test.js.map +0 -1
  445. package/dist/events/__tests__/event-worker.test.d.ts +0 -2
  446. package/dist/events/__tests__/event-worker.test.d.ts.map +0 -1
  447. package/dist/events/__tests__/event-worker.test.js +0 -174
  448. package/dist/events/__tests__/event-worker.test.js.map +0 -1
  449. package/dist/events/__tests__/queue.test.d.ts +0 -2
  450. package/dist/events/__tests__/queue.test.d.ts.map +0 -1
  451. package/dist/events/__tests__/queue.test.js +0 -73
  452. package/dist/events/__tests__/queue.test.js.map +0 -1
  453. package/dist/execution/__tests__/authorization.test.d.ts +0 -2
  454. package/dist/execution/__tests__/authorization.test.d.ts.map +0 -1
  455. package/dist/execution/__tests__/authorization.test.js +0 -99
  456. package/dist/execution/__tests__/authorization.test.js.map +0 -1
  457. package/dist/execution/__tests__/capability-executor.test.d.ts +0 -2
  458. package/dist/execution/__tests__/capability-executor.test.d.ts.map +0 -1
  459. package/dist/execution/__tests__/capability-executor.test.js +0 -212
  460. package/dist/execution/__tests__/capability-executor.test.js.map +0 -1
  461. package/dist/execution/__tests__/capability-registry.test.d.ts +0 -2
  462. package/dist/execution/__tests__/capability-registry.test.d.ts.map +0 -1
  463. package/dist/execution/__tests__/capability-registry.test.js +0 -50
  464. package/dist/execution/__tests__/capability-registry.test.js.map +0 -1
  465. package/dist/execution/__tests__/context-factory.test.d.ts +0 -2
  466. package/dist/execution/__tests__/context-factory.test.d.ts.map +0 -1
  467. package/dist/execution/__tests__/context-factory.test.js +0 -133
  468. package/dist/execution/__tests__/context-factory.test.js.map +0 -1
  469. package/dist/explanation/__tests__/tracker.test.d.ts +0 -2
  470. package/dist/explanation/__tests__/tracker.test.d.ts.map +0 -1
  471. package/dist/explanation/__tests__/tracker.test.js +0 -169
  472. package/dist/explanation/__tests__/tracker.test.js.map +0 -1
  473. package/dist/fields/__tests__/fields.test.d.ts +0 -2
  474. package/dist/fields/__tests__/fields.test.d.ts.map +0 -1
  475. package/dist/fields/__tests__/fields.test.js +0 -60
  476. package/dist/fields/__tests__/fields.test.js.map +0 -1
  477. package/dist/flows/__tests__/flow-engine.test.d.ts +0 -2
  478. package/dist/flows/__tests__/flow-engine.test.d.ts.map +0 -1
  479. package/dist/flows/__tests__/flow-engine.test.js +0 -797
  480. package/dist/flows/__tests__/flow-engine.test.js.map +0 -1
  481. package/dist/flows/__tests__/flow-registry.test.d.ts +0 -2
  482. package/dist/flows/__tests__/flow-registry.test.d.ts.map +0 -1
  483. package/dist/flows/__tests__/flow-registry.test.js +0 -69
  484. package/dist/flows/__tests__/flow-registry.test.js.map +0 -1
  485. package/dist/flows/__tests__/flow-service.test.d.ts +0 -2
  486. package/dist/flows/__tests__/flow-service.test.d.ts.map +0 -1
  487. package/dist/flows/__tests__/flow-service.test.js +0 -55
  488. package/dist/flows/__tests__/flow-service.test.js.map +0 -1
  489. package/dist/flows/__tests__/scheduler.test.d.ts +0 -2
  490. package/dist/flows/__tests__/scheduler.test.d.ts.map +0 -1
  491. package/dist/flows/__tests__/scheduler.test.js +0 -120
  492. package/dist/flows/__tests__/scheduler.test.js.map +0 -1
  493. package/dist/flows/__tests__/state-machine.test.d.ts +0 -2
  494. package/dist/flows/__tests__/state-machine.test.d.ts.map +0 -1
  495. package/dist/flows/__tests__/state-machine.test.js +0 -62
  496. package/dist/flows/__tests__/state-machine.test.js.map +0 -1
  497. package/dist/flows/__tests__/step-executor.test.d.ts +0 -2
  498. package/dist/flows/__tests__/step-executor.test.d.ts.map +0 -1
  499. package/dist/flows/__tests__/step-executor.test.js +0 -201
  500. package/dist/flows/__tests__/step-executor.test.js.map +0 -1
  501. package/dist/flows/__tests__/triggers.test.d.ts +0 -2
  502. package/dist/flows/__tests__/triggers.test.d.ts.map +0 -1
  503. package/dist/flows/__tests__/triggers.test.js +0 -98
  504. package/dist/flows/__tests__/triggers.test.js.map +0 -1
  505. package/dist/governance/__tests__/overrides.test.d.ts +0 -2
  506. package/dist/governance/__tests__/overrides.test.d.ts.map +0 -1
  507. package/dist/governance/__tests__/overrides.test.js +0 -123
  508. package/dist/governance/__tests__/overrides.test.js.map +0 -1
  509. package/dist/governance/__tests__/policies.test.d.ts +0 -2
  510. package/dist/governance/__tests__/policies.test.d.ts.map +0 -1
  511. package/dist/governance/__tests__/policies.test.js +0 -203
  512. package/dist/governance/__tests__/policies.test.js.map +0 -1
  513. package/dist/governance/__tests__/rules.test.d.ts +0 -2
  514. package/dist/governance/__tests__/rules.test.d.ts.map +0 -1
  515. package/dist/governance/__tests__/rules.test.js +0 -342
  516. package/dist/governance/__tests__/rules.test.js.map +0 -1
  517. package/dist/observability/__tests__/metrics.test.d.ts +0 -2
  518. package/dist/observability/__tests__/metrics.test.d.ts.map +0 -1
  519. package/dist/observability/__tests__/metrics.test.js +0 -301
  520. package/dist/observability/__tests__/metrics.test.js.map +0 -1
  521. package/dist/server/__tests__/bootstrap.test.d.ts +0 -2
  522. package/dist/server/__tests__/bootstrap.test.d.ts.map +0 -1
  523. package/dist/server/__tests__/bootstrap.test.js +0 -381
  524. package/dist/server/__tests__/bootstrap.test.js.map +0 -1
  525. package/dist/testing/__tests__/context.test.d.ts +0 -2
  526. package/dist/testing/__tests__/context.test.d.ts.map +0 -1
  527. package/dist/testing/__tests__/context.test.js +0 -341
  528. package/dist/testing/__tests__/context.test.js.map +0 -1
  529. package/dist/testing/__tests__/e2e.test.d.ts +0 -2
  530. package/dist/testing/__tests__/e2e.test.d.ts.map +0 -1
  531. package/dist/testing/__tests__/e2e.test.js +0 -119
  532. package/dist/testing/__tests__/e2e.test.js.map +0 -1
  533. package/dist/testing/__tests__/field-validation.test.d.ts +0 -2
  534. package/dist/testing/__tests__/field-validation.test.d.ts.map +0 -1
  535. package/dist/testing/__tests__/field-validation.test.js +0 -178
  536. package/dist/testing/__tests__/field-validation.test.js.map +0 -1
  537. package/dist/testing/__tests__/governance.test.d.ts +0 -2
  538. package/dist/testing/__tests__/governance.test.d.ts.map +0 -1
  539. package/dist/testing/__tests__/governance.test.js +0 -222
  540. package/dist/testing/__tests__/governance.test.js.map +0 -1
  541. package/dist/testing/__tests__/run-capability.test.d.ts +0 -2
  542. package/dist/testing/__tests__/run-capability.test.d.ts.map +0 -1
  543. package/dist/testing/__tests__/run-capability.test.js +0 -114
  544. package/dist/testing/__tests__/run-capability.test.js.map +0 -1
  545. package/dist/testing/__tests__/scaffolding.test.d.ts +0 -2
  546. package/dist/testing/__tests__/scaffolding.test.d.ts.map +0 -1
  547. package/dist/testing/__tests__/scaffolding.test.js +0 -70
  548. package/dist/testing/__tests__/scaffolding.test.js.map +0 -1
  549. package/dist/testing/__tests__/security.test.d.ts +0 -2
  550. package/dist/testing/__tests__/security.test.d.ts.map +0 -1
  551. package/dist/testing/__tests__/security.test.js +0 -186
  552. package/dist/testing/__tests__/security.test.js.map +0 -1
  553. package/dist/testing/__tests__/simulate-flow.test.d.ts +0 -2
  554. package/dist/testing/__tests__/simulate-flow.test.d.ts.map +0 -1
  555. package/dist/testing/__tests__/simulate-flow.test.js +0 -230
  556. package/dist/testing/__tests__/simulate-flow.test.js.map +0 -1
  557. package/dist/translations/__tests__/translation-resolver.test.d.ts +0 -2
  558. package/dist/translations/__tests__/translation-resolver.test.d.ts.map +0 -1
  559. package/dist/translations/__tests__/translation-resolver.test.js +0 -140
  560. package/dist/translations/__tests__/translation-resolver.test.js.map +0 -1
  561. package/dist/types/__tests__/registry.test.d.ts +0 -2
  562. package/dist/types/__tests__/registry.test.d.ts.map +0 -1
  563. package/dist/types/__tests__/registry.test.js +0 -34
  564. package/dist/types/__tests__/registry.test.js.map +0 -1
  565. package/dist/worker/__tests__/bootstrap.test.d.ts +0 -2
  566. package/dist/worker/__tests__/bootstrap.test.d.ts.map +0 -1
  567. package/dist/worker/__tests__/bootstrap.test.js +0 -268
  568. package/dist/worker/__tests__/bootstrap.test.js.map +0 -1
  569. package/dist/worker/__tests__/flow-service-wiring.test.d.ts +0 -2
  570. package/dist/worker/__tests__/flow-service-wiring.test.d.ts.map +0 -1
  571. package/dist/worker/__tests__/flow-service-wiring.test.js +0 -64
  572. package/dist/worker/__tests__/flow-service-wiring.test.js.map +0 -1
@@ -1,1078 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import { z } from 'zod';
3
- import { definePrompt } from '../../define/definePrompt.js';
4
- import { createAIService, singleProviderConfig } from '../ai-service.js';
5
- import { createCostTracker } from '../cost-tracker.js';
6
- import { createExplainabilityTracker } from '../explainability.js';
7
- import { PromptRegistry } from '../prompt-registry.js';
8
- import { createInMemoryVectorStore, createRAGPipeline } from '../rag/pipeline.js';
9
- import { AIIncompleteOutputError, AIRefusalError } from '../refusal.js';
10
- import { createMockProvider } from './provider.test.js';
11
- describe('AI Service (ctx.ai)', () => {
12
- function setupService(opts) {
13
- const provider = createMockProvider({
14
- complete: vi.fn(async () => ({
15
- content: '{"result":"hello"}',
16
- model: 'mock-model',
17
- usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
18
- finishReason: 'stop',
19
- })),
20
- embed: vi.fn(async (req) => ({
21
- embeddings: req.texts.map(() => [0.1, 0.2, 0.3]),
22
- model: 'mock-embed',
23
- usage: { totalTokens: 5 },
24
- cost: 0,
25
- })),
26
- });
27
- const promptRegistry = opts?.promptRegistry ? new PromptRegistry() : undefined;
28
- const costTracker = opts?.costTracker ? createCostTracker() : undefined;
29
- const explainability = opts?.explainability ? createExplainabilityTracker() : undefined;
30
- let ragPipeline;
31
- if (opts?.rag) {
32
- const vectorStore = createInMemoryVectorStore();
33
- ragPipeline = createRAGPipeline({ provider, vectorStore });
34
- }
35
- const service = createAIService(singleProviderConfig(provider, {
36
- promptRegistry,
37
- costTracker,
38
- ragPipeline,
39
- defaultModel: 'mock-model',
40
- enableStrictStructuredOutputs: opts?.strictStructuredOutputs,
41
- budget: { tenantId: 't1', actor: 'user1' },
42
- }));
43
- return { service, provider, promptRegistry, costTracker, explainability };
44
- }
45
- describe('generate', () => {
46
- it('generates a response with raw prompt', async () => {
47
- const { service } = setupService();
48
- const result = await service.generate({
49
- prompt: 'Say hello',
50
- input: {},
51
- });
52
- expect(result).toBe('{"result":"hello"}');
53
- });
54
- it('uses prompt registry when available', async () => {
55
- const { service, promptRegistry, provider } = setupService({ promptRegistry: true });
56
- promptRegistry?.register(definePrompt({
57
- name: 'greet',
58
- description: 'Say hello to {{name}}',
59
- input: z.object({ name: z.string() }),
60
- output: z.object({ result: z.string(), language: z.string().optional() }),
61
- }));
62
- const result = await service.generate({
63
- prompt: 'greet',
64
- input: { name: 'Alice' },
65
- });
66
- expect(result).toEqual({ result: 'hello' });
67
- expect(provider.complete).toHaveBeenCalled();
68
- });
69
- it('renders registered system prompts without duplicating substituted input', async () => {
70
- const { service, promptRegistry, provider } = setupService({ promptRegistry: true });
71
- promptRegistry?.register(definePrompt({
72
- name: 'system-greet',
73
- system: 'You are {{role}}.',
74
- description: 'Data: {{text}}',
75
- input: z.object({
76
- role: z.string(),
77
- text: z.string(),
78
- locale: z.string(),
79
- }),
80
- output: z.object({ result: z.string(), language: z.string().optional() }),
81
- }));
82
- await service.generate({
83
- prompt: 'system-greet',
84
- input: { role: 'editor', text: 'hello', locale: 'en' },
85
- });
86
- expect(provider.complete).toHaveBeenCalledWith(expect.objectContaining({
87
- system: 'You are editor.',
88
- prompt: expect.stringContaining('Data: hello\n\nInput: {"locale":"en"}'),
89
- }));
90
- });
91
- it('keeps system undefined for registered prompts without a system template', async () => {
92
- const { service, promptRegistry, provider } = setupService({ promptRegistry: true });
93
- promptRegistry?.register(definePrompt({
94
- name: 'no-system',
95
- description: 'Data: {{text}}',
96
- input: z.object({ text: z.string() }),
97
- output: z.object({ result: z.string(), language: z.string().optional() }),
98
- }));
99
- await service.generate({
100
- prompt: 'no-system',
101
- input: { text: 'hello' },
102
- });
103
- expect(provider.complete).toHaveBeenCalledWith(expect.objectContaining({
104
- system: undefined,
105
- prompt: expect.stringContaining('Data: hello'),
106
- }));
107
- });
108
- it('can omit unsubstituted input when a prompt renders the complete user message', async () => {
109
- const { service, promptRegistry, provider } = setupService({ promptRegistry: true });
110
- promptRegistry?.register(definePrompt({
111
- name: 'complete-user-message',
112
- system: '{{systemInstructions}}',
113
- description: '{{userPrompt}}',
114
- input: z.object({
115
- systemInstructions: z.string(),
116
- userPrompt: z.string(),
117
- sourceText: z.string(),
118
- }),
119
- output: z.object({ result: z.string(), language: z.string().optional() }),
120
- appendUnsubstitutedInput: false,
121
- }));
122
- await service.generate({
123
- prompt: 'complete-user-message',
124
- input: {
125
- systemInstructions: 'System text',
126
- userPrompt: 'User text',
127
- sourceText: 'raw source',
128
- },
129
- });
130
- expect(provider.complete).toHaveBeenCalledWith(expect.objectContaining({
131
- system: 'System text',
132
- prompt: expect.stringContaining('User text'),
133
- }));
134
- });
135
- it('passes a strict provider schema for registered JSON prompts when enabled', async () => {
136
- const { service, promptRegistry, provider } = setupService({
137
- promptRegistry: true,
138
- strictStructuredOutputs: true,
139
- });
140
- promptRegistry?.register(definePrompt({
141
- name: 'strict-greet',
142
- description: 'Say hello to {{name}} as JSON.',
143
- input: z.object({ name: z.string() }),
144
- output: z.object({ result: z.string(), language: z.string().optional() }),
145
- }));
146
- await service.generate({
147
- prompt: 'strict-greet',
148
- input: { name: 'Alice' },
149
- });
150
- expect(provider.complete).toHaveBeenCalledWith(expect.objectContaining({
151
- responseFormat: 'json',
152
- responseSchema: expect.objectContaining({
153
- type: 'object',
154
- additionalProperties: false,
155
- }),
156
- }));
157
- });
158
- it('does not add a prose JSON hint when provider schema is active', async () => {
159
- const { service, promptRegistry, provider } = setupService({
160
- promptRegistry: true,
161
- strictStructuredOutputs: true,
162
- });
163
- promptRegistry?.register(definePrompt({
164
- name: 'schema-only-extract',
165
- description: 'Extract names from {{text}}.',
166
- input: z.object({ text: z.string() }),
167
- output: z.object({ result: z.string(), language: z.string().optional() }),
168
- requireStrictStructuredOutputs: true,
169
- }));
170
- await service.generate({
171
- prompt: 'schema-only-extract',
172
- input: { text: 'Alice and Bob' },
173
- });
174
- expect(provider.complete).toHaveBeenCalledWith(expect.objectContaining({
175
- responseFormat: 'json',
176
- responseSchema: expect.objectContaining({ type: 'object' }),
177
- prompt: expect.not.stringContaining('Respond with a valid JSON object'),
178
- }));
179
- });
180
- it('fails required strict structured prompts when provider schema is disabled', async () => {
181
- const { service, promptRegistry, provider } = setupService({ promptRegistry: true });
182
- promptRegistry?.register(definePrompt({
183
- name: 'strict-required',
184
- description: 'Extract names from {{text}}.',
185
- input: z.object({ text: z.string() }),
186
- output: z.object({ names: z.array(z.string()) }),
187
- requireStrictStructuredOutputs: true,
188
- }));
189
- await expect(service.generate({
190
- prompt: 'strict-required',
191
- input: { text: 'Alice and Bob' },
192
- })).rejects.toThrow('requires provider-side structured outputs');
193
- expect(provider.complete).not.toHaveBeenCalled();
194
- });
195
- it('does not pass a strict provider schema for prompt-level opt outs', async () => {
196
- const { service, promptRegistry, provider } = setupService({
197
- promptRegistry: true,
198
- strictStructuredOutputs: true,
199
- });
200
- promptRegistry?.register(definePrompt({
201
- name: 'legacy-json',
202
- description: 'Say hello as JSON.',
203
- input: z.object({}),
204
- output: z.object({ result: z.string() }),
205
- disableStrictStructuredOutputs: true,
206
- }));
207
- await service.generate({ prompt: 'legacy-json', input: {} });
208
- expect(provider.complete).toHaveBeenCalledWith(expect.objectContaining({
209
- responseFormat: 'json',
210
- responseSchema: undefined,
211
- }));
212
- });
213
- it('tracks cost when cost tracker is present', async () => {
214
- const { service, costTracker } = setupService({ costTracker: true });
215
- await service.generate({ prompt: 'test', input: {} });
216
- expect(costTracker?.getRecords()).toHaveLength(1);
217
- expect(costTracker?.getRecords()[0]?.operation).toBe('generate');
218
- });
219
- it('records refused status and usage for provider refusals', async () => {
220
- const provider = createMockProvider({
221
- complete: vi.fn(async () => {
222
- throw new AIRefusalError({
223
- provider: 'mock',
224
- model: 'mock-model',
225
- refusalText: 'No.',
226
- usage: { inputTokens: 3, outputTokens: 2, totalTokens: 5 },
227
- });
228
- }),
229
- });
230
- const promptRegistry = new PromptRegistry();
231
- promptRegistry.register(definePrompt({
232
- name: 'refused',
233
- description: 'Return JSON.',
234
- input: z.object({}),
235
- output: z.object({ result: z.string() }),
236
- }));
237
- const costTracker = createCostTracker();
238
- const service = createAIService(singleProviderConfig(provider, {
239
- promptRegistry,
240
- costTracker,
241
- defaultModel: 'mock-model',
242
- enableStrictStructuredOutputs: true,
243
- }));
244
- await expect(service.generate({ prompt: 'refused', input: {} })).rejects.toThrow(AIRefusalError);
245
- expect(costTracker.getRecords()[0]).toMatchObject({
246
- status: 'refused',
247
- usage: { inputTokens: 3, outputTokens: 2, totalTokens: 5 },
248
- });
249
- });
250
- it('records incomplete status and usage for truncated structured outputs', async () => {
251
- const provider = createMockProvider({
252
- complete: vi.fn(async () => {
253
- throw new AIIncompleteOutputError({
254
- provider: 'mock',
255
- model: 'mock-model',
256
- partialText: '{"result":',
257
- usage: { inputTokens: 3, outputTokens: 8, totalTokens: 11 },
258
- finishReason: 'length',
259
- });
260
- }),
261
- });
262
- const promptRegistry = new PromptRegistry();
263
- promptRegistry.register(definePrompt({
264
- name: 'truncated',
265
- description: 'Return JSON.',
266
- input: z.object({}),
267
- output: z.object({ result: z.string() }),
268
- }));
269
- const costTracker = createCostTracker();
270
- const service = createAIService(singleProviderConfig(provider, {
271
- promptRegistry,
272
- costTracker,
273
- defaultModel: 'mock-model',
274
- enableStrictStructuredOutputs: true,
275
- }));
276
- await expect(service.generate({ prompt: 'truncated', input: {} })).rejects.toThrow(AIIncompleteOutputError);
277
- expect(costTracker.getRecords()[0]).toMatchObject({
278
- status: 'incomplete',
279
- usage: { inputTokens: 3, outputTokens: 8, totalTokens: 11 },
280
- });
281
- });
282
- });
283
- describe('prompt assembly', () => {
284
- function getRequestPrompt(provider) {
285
- const mock = provider.complete;
286
- const firstCall = mock.mock.calls[0];
287
- const req = firstCall?.[0];
288
- return req?.prompt ?? '';
289
- }
290
- it('does NOT append `Input: {...}` when every input key is consumed by a {{placeholder}}', async () => {
291
- const { service, promptRegistry, provider } = setupService({ promptRegistry: true });
292
- promptRegistry?.register(definePrompt({
293
- name: 'all-substituted',
294
- description: 'Hello {{name}}, you are {{age}} years old. Respond as JSON.',
295
- input: z.object({ name: z.string(), age: z.number() }),
296
- output: z.object({ result: z.string() }),
297
- }));
298
- await service.generate({
299
- prompt: 'all-substituted',
300
- input: { name: 'Alice', age: 30 },
301
- });
302
- const sent = getRequestPrompt(provider);
303
- expect(sent).toContain('Hello Alice, you are 30 years old');
304
- expect(sent).not.toContain('Input: {');
305
- // Each substituted value should appear exactly once (no duplication)
306
- expect(sent.match(/Alice/g)?.length ?? 0).toBe(1);
307
- expect(sent.match(/\b30\b/g)?.length ?? 0).toBe(1);
308
- });
309
- it('appends only the unsubstituted keys when input has extras beyond the placeholders', async () => {
310
- const { service, promptRegistry, provider } = setupService({ promptRegistry: true });
311
- promptRegistry?.register(definePrompt({
312
- name: 'partial-substituted',
313
- description: 'Greet {{name}}. Respond as JSON.',
314
- input: z.object({ name: z.string(), context: z.string() }),
315
- output: z.object({ result: z.string() }),
316
- }));
317
- await service.generate({
318
- prompt: 'partial-substituted',
319
- input: { name: 'Alice', context: 'morning meeting' },
320
- });
321
- const sent = getRequestPrompt(provider);
322
- expect(sent).toContain('Greet Alice');
323
- expect(sent).toContain('Input: {');
324
- expect(sent).toContain('"context":"morning meeting"');
325
- expect(sent).not.toContain('"name":"Alice"');
326
- // Substituted value still appears only once (in the rendered template, not in Input blob)
327
- expect(sent.match(/Alice/g)?.length ?? 0).toBe(1);
328
- });
329
- it('appends Input: {...} when prompt has no placeholders for any input key', async () => {
330
- const { service, promptRegistry, provider } = setupService({ promptRegistry: true });
331
- promptRegistry?.register(definePrompt({
332
- name: 'no-placeholders',
333
- description: 'Process the input. Respond as JSON.',
334
- input: z.object({ name: z.string() }),
335
- output: z.object({ result: z.string() }),
336
- }));
337
- await service.generate({
338
- prompt: 'no-placeholders',
339
- input: { name: 'Alice' },
340
- });
341
- const sent = getRequestPrompt(provider);
342
- expect(sent).toContain('Process the input.');
343
- expect(sent).toContain('Input: {"name":"Alice"}');
344
- });
345
- it('raw-prompt path (no registry def) is unchanged: no Input append, prompt sent as-is', async () => {
346
- const { service, provider } = setupService();
347
- await service.generate({ prompt: 'just say hi', input: {} });
348
- const sent = getRequestPrompt(provider);
349
- expect(sent).toBe('just say hi');
350
- });
351
- });
352
- describe('generateWithUsage', () => {
353
- it('returns data and usage for raw prompt', async () => {
354
- const { service } = setupService();
355
- const result = await service.generateWithUsage({
356
- prompt: 'Say hello',
357
- input: {},
358
- });
359
- expect(result.data).toBe('{"result":"hello"}');
360
- expect(result.usage).toEqual({ inputTokens: 10, outputTokens: 20, totalTokens: 30 });
361
- });
362
- it('returns validated data and usage with prompt registry', async () => {
363
- const { service, promptRegistry } = setupService({ promptRegistry: true });
364
- promptRegistry?.register(definePrompt({
365
- name: 'greet',
366
- description: 'Say hello to {{name}}',
367
- input: z.object({ name: z.string() }),
368
- output: z.object({ result: z.string() }),
369
- }));
370
- const result = await service.generateWithUsage({
371
- prompt: 'greet',
372
- input: { name: 'Alice' },
373
- });
374
- expect(result.data).toEqual({ result: 'hello' });
375
- expect(result.usage.inputTokens).toBe(10);
376
- expect(result.usage.outputTokens).toBe(20);
377
- });
378
- it('allows per-request validation overrides', async () => {
379
- let callCount = 0;
380
- const provider = createMockProvider({
381
- complete: vi.fn(async () => {
382
- callCount += 1;
383
- if (callCount === 1) {
384
- return {
385
- content: '{"wrong":"schema"}',
386
- model: 'mock-model',
387
- usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
388
- finishReason: 'stop',
389
- };
390
- }
391
- return {
392
- content: '{"result":"hello"}',
393
- model: 'mock-model',
394
- usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
395
- finishReason: 'stop',
396
- };
397
- }),
398
- });
399
- const promptRegistry = new PromptRegistry();
400
- promptRegistry.register(definePrompt({
401
- name: 'greet-once',
402
- description: 'Say hello once',
403
- input: z.object({}),
404
- output: z.object({ result: z.string() }),
405
- }));
406
- const service = createAIService(singleProviderConfig(provider, {
407
- promptRegistry,
408
- defaultModel: 'mock-model',
409
- }));
410
- await expect(service.generateWithUsage({
411
- prompt: 'greet-once',
412
- input: {},
413
- validation: { maxRetries: 0, feedbackOnError: false },
414
- })).rejects.toThrow('AI output validation failed after 1 attempts');
415
- expect(provider.complete).toHaveBeenCalledTimes(1);
416
- });
417
- });
418
- describe('extract', () => {
419
- it('extracts structured data from text', async () => {
420
- const provider = createMockProvider({
421
- complete: vi.fn(async () => ({
422
- content: '{"name":"Alice","age":30}',
423
- model: 'mock',
424
- usage: { inputTokens: 20, outputTokens: 10, totalTokens: 30 },
425
- finishReason: 'stop',
426
- })),
427
- });
428
- const service = createAIService(singleProviderConfig(provider));
429
- const schema = z.object({ name: z.string(), age: z.number() });
430
- const result = await service.extract({ schema, text: 'Alice is 30 years old' });
431
- expect(result).toEqual({ name: 'Alice', age: 30 });
432
- });
433
- });
434
- describe('classify', () => {
435
- it('classifies text into labels', async () => {
436
- const provider = createMockProvider({
437
- complete: vi.fn(async () => ({
438
- content: '["urgent","billing"]',
439
- model: 'mock',
440
- usage: { inputTokens: 15, outputTokens: 5, totalTokens: 20 },
441
- finishReason: 'stop',
442
- })),
443
- });
444
- const service = createAIService(singleProviderConfig(provider));
445
- const result = await service.classify({
446
- labels: ['urgent', 'billing', 'support'],
447
- text: 'My payment failed and I need help now!',
448
- });
449
- expect(result).toEqual(['urgent', 'billing']);
450
- });
451
- it('filters out invalid labels', async () => {
452
- const provider = createMockProvider({
453
- complete: vi.fn(async () => ({
454
- content: '["urgent","invalid_label"]',
455
- model: 'mock',
456
- usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
457
- finishReason: 'stop',
458
- })),
459
- });
460
- const service = createAIService(singleProviderConfig(provider));
461
- const result = await service.classify({
462
- labels: ['urgent', 'billing'],
463
- text: 'test',
464
- });
465
- expect(result).toEqual(['urgent']);
466
- });
467
- });
468
- describe('retrieve', () => {
469
- it('throws when RAG pipeline not configured', async () => {
470
- const { service } = setupService();
471
- await expect(service.retrieve({ query: 'test' })).rejects.toThrow('RAG pipeline not configured');
472
- });
473
- it('retrieves documents when RAG is configured', async () => {
474
- const { service } = setupService({ rag: true });
475
- // The in-memory store is empty so results will be empty, but it should not throw
476
- const results = await service.retrieve({ query: 'test' });
477
- expect(Array.isArray(results)).toBe(true);
478
- });
479
- });
480
- describe('budget enforcement', () => {
481
- it('throws when budget is exceeded', async () => {
482
- const costTracker = createCostTracker({ dailyCostLimit: 0.001 });
483
- costTracker.record({
484
- model: 'gpt-4o',
485
- provider: 'mock',
486
- operation: 'generate',
487
- usage: { inputTokens: 1000, outputTokens: 500, totalTokens: 1500 },
488
- cost: 0.001,
489
- latencyMs: 200,
490
- });
491
- const provider = createMockProvider();
492
- const service = createAIService(singleProviderConfig(provider, {
493
- costTracker,
494
- budget: { tenantId: 't1' },
495
- }));
496
- await expect(service.generate({ prompt: 'test', input: {} })).rejects.toThrow('AI budget exceeded');
497
- });
498
- });
499
- describe('multi-provider routing', () => {
500
- function createNamedProvider(name, content = '{"result":"ok"}') {
501
- return createMockProvider({
502
- name,
503
- complete: vi.fn(async () => ({
504
- content,
505
- model: `${name}-model`,
506
- usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
507
- finishReason: 'stop',
508
- })),
509
- });
510
- }
511
- it('routes to the prompt-specified provider', async () => {
512
- const openai = createNamedProvider('openai', '{"result":"from-openai"}');
513
- const anthropic = createNamedProvider('anthropic', '{"result":"from-anthropic"}');
514
- const promptRegistry = new PromptRegistry();
515
- promptRegistry.register(definePrompt({
516
- name: 'write-bio',
517
- description: 'Write a bio for {{name}}',
518
- input: z.object({ name: z.string() }),
519
- output: z.object({ result: z.string() }),
520
- model: { name: 'claude-sonnet', provider: 'anthropic' },
521
- }));
522
- const service = createAIService({
523
- providers: { openai, anthropic },
524
- defaultProvider: 'openai',
525
- promptRegistry,
526
- });
527
- await service.generate({ prompt: 'write-bio', input: { name: 'Alice' } });
528
- expect(anthropic.complete).toHaveBeenCalled();
529
- expect(openai.complete).not.toHaveBeenCalled();
530
- });
531
- it('falls back to default provider when prompt has no provider', async () => {
532
- const openai = createNamedProvider('openai');
533
- const anthropic = createNamedProvider('anthropic');
534
- const service = createAIService({
535
- providers: { openai, anthropic },
536
- defaultProvider: 'openai',
537
- });
538
- await service.generate({ prompt: 'raw prompt', input: {} });
539
- expect(openai.complete).toHaveBeenCalled();
540
- expect(anthropic.complete).not.toHaveBeenCalled();
541
- });
542
- it('throws when prompt specifies an unknown provider', async () => {
543
- const openai = createNamedProvider('openai');
544
- const promptRegistry = new PromptRegistry();
545
- promptRegistry.register(definePrompt({
546
- name: 'failing',
547
- description: 'This will fail',
548
- input: z.object({}),
549
- output: z.object({ result: z.string() }),
550
- model: { name: 'some-model', provider: 'nonexistent' },
551
- }));
552
- const service = createAIService({
553
- providers: { openai },
554
- defaultProvider: 'openai',
555
- promptRegistry,
556
- });
557
- await expect(service.generate({ prompt: 'failing', input: {} })).rejects.toThrow('AI provider "nonexistent" not configured');
558
- });
559
- it('records provider name in cost tracker entries', async () => {
560
- const anthropic = createNamedProvider('anthropic');
561
- const costTracker = createCostTracker();
562
- const promptRegistry = new PromptRegistry();
563
- promptRegistry.register(definePrompt({
564
- name: 'tracked',
565
- description: 'Tracked prompt',
566
- input: z.object({}),
567
- output: z.object({ result: z.string() }),
568
- model: { name: 'claude-sonnet', provider: 'anthropic' },
569
- }));
570
- const service = createAIService({
571
- providers: { anthropic },
572
- defaultProvider: 'anthropic',
573
- promptRegistry,
574
- costTracker,
575
- });
576
- await service.generate({ prompt: 'tracked', input: {} });
577
- const records = costTracker.getRecords();
578
- expect(records).toHaveLength(1);
579
- expect(records[0]?.provider).toBe('anthropic');
580
- });
581
- it('extract and classify use default provider', async () => {
582
- const openai = createNamedProvider('openai', '{"name":"Alice","age":30}');
583
- const anthropic = createNamedProvider('anthropic');
584
- const service = createAIService({
585
- providers: { openai, anthropic },
586
- defaultProvider: 'openai',
587
- });
588
- const schema = z.object({ name: z.string(), age: z.number() });
589
- await service.extract({ schema, text: 'Alice is 30' });
590
- expect(openai.complete).toHaveBeenCalled();
591
- expect(anthropic.complete).not.toHaveBeenCalled();
592
- });
593
- });
594
- describe('prompt override resolution', () => {
595
- function createNamedProvider(name, content = '{"result":"ok"}') {
596
- return createMockProvider({
597
- name,
598
- complete: vi.fn(async () => ({
599
- content,
600
- model: `${name}-model`,
601
- usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
602
- finishReason: 'stop',
603
- })),
604
- });
605
- }
606
- it('uses promptOverrides to route to a different provider', async () => {
607
- const openai = createNamedProvider('openai');
608
- const anthropic = createNamedProvider('anthropic', '{"result":"from-anthropic"}');
609
- const promptRegistry = new PromptRegistry();
610
- promptRegistry.register(definePrompt({
611
- name: 'write-bio',
612
- description: 'Write a bio for {{name}}',
613
- input: z.object({ name: z.string() }),
614
- output: z.object({ result: z.string() }),
615
- model: { name: 'gpt-4o', provider: 'openai' },
616
- }));
617
- const service = createAIService({
618
- providers: { openai, anthropic },
619
- defaultProvider: 'openai',
620
- promptRegistry,
621
- promptOverrides: {
622
- 'write-bio': { provider: 'anthropic', model: 'claude-sonnet' },
623
- },
624
- });
625
- await service.generate({ prompt: 'write-bio', input: { name: 'Alice' } });
626
- expect(anthropic.complete).toHaveBeenCalled();
627
- expect(openai.complete).not.toHaveBeenCalled();
628
- });
629
- it('uses promptOverrides model over prompt-defined model', async () => {
630
- const openai = createNamedProvider('openai');
631
- const promptRegistry = new PromptRegistry();
632
- promptRegistry.register(definePrompt({
633
- name: 'write-bio',
634
- description: 'Write a bio for {{name}}',
635
- input: z.object({ name: z.string() }),
636
- output: z.object({ result: z.string() }),
637
- model: { name: 'gpt-4o', provider: 'openai' },
638
- }));
639
- const service = createAIService({
640
- providers: { openai },
641
- defaultProvider: 'openai',
642
- promptRegistry,
643
- promptOverrides: {
644
- 'write-bio': { model: 'gpt-4o-mini' },
645
- },
646
- });
647
- await service.generate({ prompt: 'write-bio', input: { name: 'Alice' } });
648
- const call = openai.complete.mock.calls[0]?.[0];
649
- expect(call?.model).toBe('gpt-4o-mini');
650
- });
651
- it('falls back to defaultModel when neither override nor prompt defines model', async () => {
652
- const openai = createNamedProvider('openai');
653
- const promptRegistry = new PromptRegistry();
654
- promptRegistry.register(definePrompt({
655
- name: 'simple',
656
- description: 'A simple prompt',
657
- input: z.object({}),
658
- output: z.object({ result: z.string() }),
659
- }));
660
- const service = createAIService({
661
- providers: { openai },
662
- defaultProvider: 'openai',
663
- promptRegistry,
664
- defaultModel: 'gpt-4o-mini',
665
- });
666
- await service.generate({ prompt: 'simple', input: {} });
667
- const call = openai.complete.mock.calls[0]?.[0];
668
- expect(call?.model).toBe('gpt-4o-mini');
669
- });
670
- it('uses prompt model when no override is specified', async () => {
671
- const openai = createNamedProvider('openai');
672
- const promptRegistry = new PromptRegistry();
673
- promptRegistry.register(definePrompt({
674
- name: 'write-bio',
675
- description: 'Write a bio for {{name}}',
676
- input: z.object({ name: z.string() }),
677
- output: z.object({ result: z.string() }),
678
- model: { name: 'gpt-4o', provider: 'openai' },
679
- }));
680
- const service = createAIService({
681
- providers: { openai },
682
- defaultProvider: 'openai',
683
- promptRegistry,
684
- defaultModel: 'fallback-model',
685
- });
686
- await service.generate({ prompt: 'write-bio', input: { name: 'Alice' } });
687
- const call = openai.complete.mock.calls[0]?.[0];
688
- expect(call?.model).toBe('gpt-4o');
689
- });
690
- });
691
- // ── Failure-path cost recording + onAICostRecorded hook ──
692
- describe('failure-path cost recording', () => {
693
- function makePromptRegistry() {
694
- const reg = new PromptRegistry();
695
- reg.register(definePrompt({
696
- name: 'greet',
697
- description: 'Say hello to {{name}}',
698
- input: z.object({ name: z.string() }),
699
- output: z.object({ result: z.string() }),
700
- }));
701
- return reg;
702
- }
703
- it('records status=success on the happy path with onAICostRecorded', async () => {
704
- const provider = createMockProvider({
705
- complete: vi.fn(async () => ({
706
- content: '{"result":"hi"}',
707
- model: 'mock',
708
- usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
709
- finishReason: 'stop',
710
- })),
711
- });
712
- const costTracker = createCostTracker();
713
- const hookCalls = [];
714
- const service = createAIService(singleProviderConfig(provider, {
715
- promptRegistry: makePromptRegistry(),
716
- costTracker,
717
- defaultModel: 'mock-model',
718
- onAICostRecorded: (record, ctx) => {
719
- hookCalls.push({ record, ctx });
720
- },
721
- }));
722
- await service.generate({
723
- prompt: 'greet',
724
- input: { name: 'Alice' },
725
- costContext: { projectId: 'p1', operationName: 'test' },
726
- });
727
- expect(costTracker.getRecords()).toHaveLength(1);
728
- expect(costTracker.getRecords()[0]?.status).toBe('success');
729
- expect(hookCalls).toHaveLength(1);
730
- expect(hookCalls[0]?.record.status).toBe('success');
731
- expect(hookCalls[0]?.ctx).toEqual({ projectId: 'p1', operationName: 'test' });
732
- });
733
- it('records status=failed with AIValidationError.usage when validation exhausts retries', async () => {
734
- const provider = createMockProvider({
735
- complete: vi.fn(async () => ({
736
- content: '{"wrong":"schema"}',
737
- model: 'mock',
738
- usage: { inputTokens: 10, outputTokens: 10, totalTokens: 20 },
739
- finishReason: 'stop',
740
- })),
741
- });
742
- const costTracker = createCostTracker();
743
- const hookCalls = [];
744
- const service = createAIService(singleProviderConfig(provider, {
745
- promptRegistry: makePromptRegistry(),
746
- costTracker,
747
- defaultModel: 'mock-model',
748
- onAICostRecorded: (record, ctx) => {
749
- hookCalls.push({ record, ctx });
750
- },
751
- }));
752
- await expect(service.generate({
753
- prompt: 'greet',
754
- input: { name: 'Alice' },
755
- validation: { maxRetries: 1, feedbackOnError: false },
756
- costContext: { projectId: 'p2' },
757
- })).rejects.toThrow(/AI output validation failed/);
758
- expect(costTracker.getRecords()).toHaveLength(1);
759
- const record = costTracker.getRecords()[0];
760
- expect(record?.status).toBe('failed');
761
- // Two attempts × 20 tokens each = 40 accumulated via AIValidationError.usage
762
- expect(record?.usage.totalTokens).toBe(40);
763
- expect(record?.errorMessage).toMatch(/validation/i);
764
- expect(hookCalls).toHaveLength(1);
765
- expect(hookCalls[0]?.record.status).toBe('failed');
766
- expect(hookCalls[0]?.ctx).toEqual({ projectId: 'p2' });
767
- });
768
- it('records status=failed with zero usage when provider throws an opaque error', async () => {
769
- const provider = createMockProvider({
770
- complete: vi.fn(async () => {
771
- throw new Error('Headers Timeout Error');
772
- }),
773
- });
774
- const costTracker = createCostTracker();
775
- const service = createAIService(singleProviderConfig(provider, {
776
- costTracker,
777
- defaultModel: 'mock-model',
778
- }));
779
- await expect(service.generate({ prompt: 'just a raw prompt', input: {} })).rejects.toThrow('Headers Timeout Error');
780
- const records = costTracker.getRecords();
781
- expect(records).toHaveLength(1);
782
- expect(records[0]?.status).toBe('failed');
783
- expect(records[0]?.usage.totalTokens).toBe(0);
784
- expect(records[0]?.errorMessage).toContain('Headers Timeout Error');
785
- });
786
- it('streamGenerate records status=failed on mid-stream transport error', async () => {
787
- const provider = {
788
- name: 'mock',
789
- complete: vi.fn(),
790
- async *stream() {
791
- yield { type: 'content_delta', delta: '' };
792
- throw new Error('socket hang up');
793
- },
794
- embed: vi.fn(),
795
- };
796
- const costTracker = createCostTracker();
797
- const service = createAIService(singleProviderConfig(provider, {
798
- costTracker,
799
- defaultModel: 'mock-model',
800
- }));
801
- const iterator = service.streamGenerate({ prompt: 'test', input: {} });
802
- await expect(async () => {
803
- for await (const _ of iterator) {
804
- // drain
805
- }
806
- }).rejects.toThrow('socket hang up');
807
- const records = costTracker.getRecords();
808
- expect(records).toHaveLength(1);
809
- expect(records[0]?.status).toBe('failed');
810
- expect(records[0]?.errorMessage).toContain('socket hang up');
811
- });
812
- it('streamGenerate records status=failed with combined stream+fallback usage on fallback validation failure', async () => {
813
- const reg = new PromptRegistry();
814
- reg.register(definePrompt({
815
- name: 'structured',
816
- description: 'Return json',
817
- input: z.object({}),
818
- output: z.object({ ok: z.boolean(), name: z.string() }),
819
- }));
820
- const stream = vi.fn().mockImplementation(async function* () {
821
- // Emit malformed JSON so local parse fails and we fall back
822
- yield { type: 'content_delta', delta: '{"ok":' };
823
- yield {
824
- type: 'usage',
825
- usage: { inputTokens: 100, outputTokens: 5, totalTokens: 105 },
826
- };
827
- yield { type: 'done', finishReason: 'stop' };
828
- });
829
- const complete = vi.fn(async () => ({
830
- content: '{"still":"wrong"}',
831
- model: 'mock',
832
- usage: { inputTokens: 50, outputTokens: 5, totalTokens: 55 },
833
- finishReason: 'stop',
834
- }));
835
- const provider = {
836
- name: 'mock',
837
- complete,
838
- stream,
839
- embed: vi.fn(),
840
- };
841
- const costTracker = createCostTracker();
842
- const service = createAIService(singleProviderConfig(provider, {
843
- promptRegistry: reg,
844
- costTracker,
845
- defaultModel: 'mock-model',
846
- validation: { maxRetries: 0, feedbackOnError: false },
847
- }));
848
- const iterator = service.streamGenerate({ prompt: 'structured', input: {} });
849
- await expect(async () => {
850
- for await (const _ of iterator) {
851
- // drain
852
- }
853
- }).rejects.toThrow(/AI output validation failed/);
854
- const records = costTracker.getRecords();
855
- expect(records).toHaveLength(1);
856
- expect(records[0]?.status).toBe('failed');
857
- // streamUsage (105) + fallback validation usage (55) = 160 total
858
- expect(records[0]?.usage.totalTokens).toBe(160);
859
- expect(records[0]?.usage.inputTokens).toBe(150);
860
- expect(records[0]?.usage.outputTokens).toBe(10);
861
- });
862
- it('onAICostRecorded hook errors are swallowed and do not break the AI call', async () => {
863
- const provider = createMockProvider({
864
- complete: vi.fn(async () => ({
865
- content: '{"result":"ok"}',
866
- model: 'mock',
867
- usage: { inputTokens: 5, outputTokens: 5, totalTokens: 10 },
868
- finishReason: 'stop',
869
- })),
870
- });
871
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
872
- const service = createAIService(singleProviderConfig(provider, {
873
- promptRegistry: makePromptRegistry(),
874
- defaultModel: 'mock-model',
875
- onAICostRecorded: () => {
876
- throw new Error('hook boom');
877
- },
878
- }));
879
- const result = await service.generate({ prompt: 'greet', input: { name: 'Alice' } });
880
- expect(result).toEqual({ result: 'ok' });
881
- expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('hook boom'));
882
- consoleSpy.mockRestore();
883
- });
884
- });
885
- // ── Text-mode brevity-hint suffix (and its opt-out) ──
886
- // The ai-service auto-detects single-string-field output schemas
887
- // (e.g. z.object({ content: z.string() })) and switches to plain text
888
- // mode while appending a brevity-hint suffix that tells the model to
889
- // "Respond with ONLY the plain text content...". That hint is the right
890
- // default for short Q&A but collapses long-form prose into bullet points
891
- // on some models — so prompts can opt out via
892
- // `disableTextModeBrevityHint: true`.
893
- describe('text-mode brevity hint', () => {
894
- function makeStreamSpy() {
895
- return vi.fn().mockImplementation(async function* () {
896
- yield { type: 'content_delta', delta: 'short body' };
897
- yield {
898
- type: 'usage',
899
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
900
- };
901
- yield { type: 'done', finishReason: 'stop' };
902
- });
903
- }
904
- function getStreamRequest(streamSpy) {
905
- const firstCall = streamSpy.mock.calls[0];
906
- const req = firstCall?.[0];
907
- return req ?? {};
908
- }
909
- async function drain(iter) {
910
- for await (const _ of iter) {
911
- // drain
912
- }
913
- }
914
- it('default text-mode (single string field, no opt-out): appends brevity-hint suffix', async () => {
915
- const streamSpy = makeStreamSpy();
916
- const provider = {
917
- name: 'mock',
918
- complete: vi.fn(),
919
- stream: streamSpy,
920
- embed: vi.fn(),
921
- };
922
- const reg = new PromptRegistry();
923
- reg.register(definePrompt({
924
- name: 'qa',
925
- description: 'Answer the question.',
926
- input: z.object({}),
927
- output: z.object({ content: z.string() }),
928
- }));
929
- const service = createAIService(singleProviderConfig(provider, { promptRegistry: reg, defaultModel: 'mock-model' }));
930
- await drain(service.streamGenerate({ prompt: 'qa', input: {} }));
931
- const req = getStreamRequest(streamSpy);
932
- expect(req.prompt ?? '').toContain('Respond with ONLY the plain text content');
933
- expect(req.responseFormat).toBe('text');
934
- });
935
- it('forwards registered system prompts on streaming calls', async () => {
936
- const streamSpy = makeStreamSpy();
937
- const provider = {
938
- name: 'mock',
939
- complete: vi.fn(),
940
- stream: streamSpy,
941
- embed: vi.fn(),
942
- };
943
- const reg = new PromptRegistry();
944
- reg.register(definePrompt({
945
- name: 'stream-system',
946
- system: 'You are {{role}}.',
947
- description: 'Write about {{topic}}',
948
- input: z.object({
949
- role: z.string(),
950
- topic: z.string(),
951
- locale: z.string(),
952
- }),
953
- output: z.object({ content: z.string() }),
954
- disableTextModeBrevityHint: true,
955
- }));
956
- const service = createAIService(singleProviderConfig(provider, { promptRegistry: reg, defaultModel: 'mock-model' }));
957
- await drain(service.streamGenerate({
958
- prompt: 'stream-system',
959
- input: { role: 'editor', topic: 'Baghdad', locale: 'he' },
960
- }));
961
- const req = getStreamRequest(streamSpy);
962
- expect(req.system).toBe('You are editor.');
963
- expect(req.prompt).toBe('Write about Baghdad\n\nInput: {"locale":"he"}');
964
- expect(req.responseFormat).toBe('text');
965
- });
966
- it('text-mode with disableTextModeBrevityHint=true: omits brevity-hint suffix but stays in text mode', async () => {
967
- const streamSpy = makeStreamSpy();
968
- const provider = {
969
- name: 'mock',
970
- complete: vi.fn(),
971
- stream: streamSpy,
972
- embed: vi.fn(),
973
- };
974
- const reg = new PromptRegistry();
975
- reg.register(definePrompt({
976
- name: 'long-form',
977
- description: 'Write a long chapter. Output the title on the first line as a markdown H1.',
978
- input: z.object({}),
979
- output: z.object({ content: z.string() }),
980
- disableTextModeBrevityHint: true,
981
- }));
982
- const service = createAIService(singleProviderConfig(provider, { promptRegistry: reg, defaultModel: 'mock-model' }));
983
- await drain(service.streamGenerate({ prompt: 'long-form', input: {} }));
984
- const req = getStreamRequest(streamSpy);
985
- const sent = req.prompt ?? '';
986
- expect(sent).not.toContain('Respond with ONLY the plain text content');
987
- expect(sent).not.toContain('Do NOT wrap your response in JSON');
988
- // Body must still be there — only the appended suffix is suppressed
989
- expect(sent).toContain('Write a long chapter');
990
- // And we are still in text mode (the whole point of the opt-out is to
991
- // keep the streamed payload as raw text so JSON-escape rules never
992
- // apply — e.g. for Hebrew content with embedded gershayim).
993
- expect(req.responseFormat).toBe('text');
994
- });
995
- it('json-mode (multi-field schema) is unaffected by the opt-out flag: JSON instruction still appended', async () => {
996
- const streamSpy = vi.fn().mockImplementation(async function* () {
997
- yield { type: 'content_delta', delta: '{"a":"x","b":"y"}' };
998
- yield {
999
- type: 'usage',
1000
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
1001
- };
1002
- yield { type: 'done', finishReason: 'stop' };
1003
- });
1004
- const provider = {
1005
- name: 'mock',
1006
- complete: vi.fn(),
1007
- stream: streamSpy,
1008
- embed: vi.fn(),
1009
- };
1010
- const reg = new PromptRegistry();
1011
- reg.register(definePrompt({
1012
- name: 'multi',
1013
- description: 'Return two fields.',
1014
- input: z.object({}),
1015
- output: z.object({ a: z.string(), b: z.string() }),
1016
- // Even with the flag set, multi-field schemas are not text-mode
1017
- // candidates, so this flag is a no-op for them.
1018
- disableTextModeBrevityHint: true,
1019
- }));
1020
- const service = createAIService(singleProviderConfig(provider, { promptRegistry: reg, defaultModel: 'mock-model' }));
1021
- await drain(service.streamGenerate({ prompt: 'multi', input: {} }));
1022
- const req = getStreamRequest(streamSpy);
1023
- expect(req.responseFormat).toBe('json');
1024
- expect(req.prompt ?? '').toContain('Respond with a valid JSON object');
1025
- // And the text-mode hint is definitely NOT in a JSON-mode call.
1026
- expect(req.prompt ?? '').not.toContain('Respond with ONLY the plain text content');
1027
- });
1028
- });
1029
- // ── Backward-compat: record() input type keeps `status` optional ──
1030
- describe('CostTracker backward-compat', () => {
1031
- it('record(entry) without status still compiles and defaults to success', () => {
1032
- // This test's real value is type-level: if `status` stops being
1033
- // optional on AICostRecordInput, this file fails to compile and
1034
- // every external consumer of CostTracker.record breaks.
1035
- const tracker = createCostTracker();
1036
- const entry = {
1037
- model: 'm',
1038
- provider: 'p',
1039
- operation: 'generate',
1040
- usage: { inputTokens: 1, outputTokens: 2, totalTokens: 3 },
1041
- cost: null,
1042
- latencyMs: 0,
1043
- };
1044
- tracker.record(entry);
1045
- expect(tracker.getRecords()).toHaveLength(1);
1046
- expect(tracker.getRecords()[0]?.status).toBe('success');
1047
- });
1048
- it('a consumer-supplied CostTracker mock without status handling still satisfies the interface', () => {
1049
- // Consumers writing their own tracker mocks (e.g. in framework tests)
1050
- // should not have to know about the new `status` field.
1051
- const seen = [];
1052
- const tracker = {
1053
- record(entry) {
1054
- seen.push(entry);
1055
- },
1056
- checkBudget() {
1057
- return { allowed: true };
1058
- },
1059
- getDailyUsage() {
1060
- return { totalTokens: 0, totalCost: 0, costAvailable: false, requestCount: 0 };
1061
- },
1062
- getRecords() {
1063
- return [];
1064
- },
1065
- };
1066
- tracker.record({
1067
- model: 'm',
1068
- provider: 'p',
1069
- operation: 'generate',
1070
- usage: { inputTokens: 1, outputTokens: 2, totalTokens: 3 },
1071
- cost: null,
1072
- latencyMs: 0,
1073
- });
1074
- expect(seen).toHaveLength(1);
1075
- });
1076
- });
1077
- });
1078
- //# sourceMappingURL=ai-service.test.js.map