@plumbus/core 0.3.3 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (593) 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/ai/security.d.ts.map +1 -1
  15. package/dist/ai/security.js +14 -1
  16. package/dist/ai/security.js.map +1 -1
  17. package/dist/api/route-generator.d.ts.map +1 -1
  18. package/dist/api/route-generator.js +57 -11
  19. package/dist/api/route-generator.js.map +1 -1
  20. package/dist/auth/adapter.d.ts +0 -4
  21. package/dist/auth/adapter.d.ts.map +1 -1
  22. package/dist/auth/adapter.js +4 -0
  23. package/dist/auth/adapter.js.map +1 -1
  24. package/dist/auth/saml-adapter.d.ts.map +1 -1
  25. package/dist/auth/saml-adapter.js +73 -120
  26. package/dist/auth/saml-adapter.js.map +1 -1
  27. package/dist/auth/scim.d.ts.map +1 -1
  28. package/dist/auth/scim.js +6 -1
  29. package/dist/auth/scim.js.map +1 -1
  30. package/dist/cli/cli.d.ts.map +1 -1
  31. package/dist/cli/cli.js +3 -1
  32. package/dist/cli/cli.js.map +1 -1
  33. package/dist/cli/commands/browser-extension.d.ts +10 -0
  34. package/dist/cli/commands/browser-extension.d.ts.map +1 -0
  35. package/dist/cli/commands/browser-extension.js +235 -0
  36. package/dist/cli/commands/browser-extension.js.map +1 -0
  37. package/dist/cli/commands/dev.d.ts +4 -1
  38. package/dist/cli/commands/dev.d.ts.map +1 -1
  39. package/dist/cli/commands/dev.js +21 -30
  40. package/dist/cli/commands/dev.js.map +1 -1
  41. package/dist/cli/commands/doctor.d.ts +6 -0
  42. package/dist/cli/commands/doctor.d.ts.map +1 -1
  43. package/dist/cli/commands/doctor.js +146 -2
  44. package/dist/cli/commands/doctor.js.map +1 -1
  45. package/dist/cli/commands/generate.d.ts.map +1 -1
  46. package/dist/cli/commands/generate.js +18 -0
  47. package/dist/cli/commands/generate.js.map +1 -1
  48. package/dist/cli/commands/index.d.ts +2 -0
  49. package/dist/cli/commands/index.d.ts.map +1 -1
  50. package/dist/cli/commands/index.js +2 -0
  51. package/dist/cli/commands/index.js.map +1 -1
  52. package/dist/cli/commands/init.d.ts +1 -1
  53. package/dist/cli/commands/init.d.ts.map +1 -1
  54. package/dist/cli/commands/init.js +122 -3
  55. package/dist/cli/commands/init.js.map +1 -1
  56. package/dist/cli/commands/mcp.d.ts +9 -0
  57. package/dist/cli/commands/mcp.d.ts.map +1 -0
  58. package/dist/cli/commands/mcp.js +125 -0
  59. package/dist/cli/commands/mcp.js.map +1 -0
  60. package/dist/cli/commands/migrate.d.ts.map +1 -1
  61. package/dist/cli/commands/migrate.js +2 -1
  62. package/dist/cli/commands/migrate.js.map +1 -1
  63. package/dist/cli/commands/start.d.ts +4 -1
  64. package/dist/cli/commands/start.d.ts.map +1 -1
  65. package/dist/cli/commands/start.js +20 -29
  66. package/dist/cli/commands/start.js.map +1 -1
  67. package/dist/cli/commands/translation.d.ts.map +1 -1
  68. package/dist/cli/commands/translation.js +22 -10
  69. package/dist/cli/commands/translation.js.map +1 -1
  70. package/dist/cli/mcp-serve-context.d.ts +13 -0
  71. package/dist/cli/mcp-serve-context.d.ts.map +1 -0
  72. package/dist/cli/mcp-serve-context.js +119 -0
  73. package/dist/cli/mcp-serve-context.js.map +1 -0
  74. package/dist/cli/migrate-snapshot-schema.d.ts +15 -0
  75. package/dist/cli/migrate-snapshot-schema.d.ts.map +1 -0
  76. package/dist/cli/migrate-snapshot-schema.js +23 -0
  77. package/dist/cli/migrate-snapshot-schema.js.map +1 -0
  78. package/dist/cli/project-root.d.ts +9 -0
  79. package/dist/cli/project-root.d.ts.map +1 -0
  80. package/dist/cli/project-root.js +41 -0
  81. package/dist/cli/project-root.js.map +1 -0
  82. package/dist/cli/scaffold-write.d.ts +17 -0
  83. package/dist/cli/scaffold-write.d.ts.map +1 -0
  84. package/dist/cli/scaffold-write.js +40 -0
  85. package/dist/cli/scaffold-write.js.map +1 -0
  86. package/dist/cli/templates/resources.d.ts.map +1 -1
  87. package/dist/cli/templates/resources.js +7 -1
  88. package/dist/cli/templates/resources.js.map +1 -1
  89. package/dist/cli/utils.d.ts +2 -0
  90. package/dist/cli/utils.d.ts.map +1 -1
  91. package/dist/cli/utils.js +14 -0
  92. package/dist/cli/utils.js.map +1 -1
  93. package/dist/config/loader.d.ts +1 -1
  94. package/dist/config/loader.d.ts.map +1 -1
  95. package/dist/config/loader.js +46 -27
  96. package/dist/config/loader.js.map +1 -1
  97. package/dist/data/connection.d.ts +16 -0
  98. package/dist/data/connection.d.ts.map +1 -0
  99. package/dist/data/connection.js +27 -0
  100. package/dist/data/connection.js.map +1 -0
  101. package/dist/data/index.d.ts +1 -0
  102. package/dist/data/index.d.ts.map +1 -1
  103. package/dist/data/index.js +1 -0
  104. package/dist/data/index.js.map +1 -1
  105. package/dist/data/migration.d.ts.map +1 -1
  106. package/dist/data/migration.js +24 -0
  107. package/dist/data/migration.js.map +1 -1
  108. package/dist/data/repository.d.ts.map +1 -1
  109. package/dist/data/repository.js +32 -9
  110. package/dist/data/repository.js.map +1 -1
  111. package/dist/define/defineCapability.d.ts +4 -2
  112. package/dist/define/defineCapability.d.ts.map +1 -1
  113. package/dist/define/defineCapability.js +68 -8
  114. package/dist/define/defineCapability.js.map +1 -1
  115. package/dist/define/defineEntity.d.ts.map +1 -1
  116. package/dist/define/defineEntity.js +8 -4
  117. package/dist/define/defineEntity.js.map +1 -1
  118. package/dist/define/defineEvent.d.ts.map +1 -1
  119. package/dist/define/defineEvent.js +7 -3
  120. package/dist/define/defineEvent.js.map +1 -1
  121. package/dist/define/defineFlow.d.ts.map +1 -1
  122. package/dist/define/defineFlow.js +11 -5
  123. package/dist/define/defineFlow.js.map +1 -1
  124. package/dist/define/definePrompt.d.ts.map +1 -1
  125. package/dist/define/definePrompt.js +11 -5
  126. package/dist/define/definePrompt.js.map +1 -1
  127. package/dist/define/defineTranslation.d.ts.map +1 -1
  128. package/dist/define/defineTranslation.js +12 -10
  129. package/dist/define/defineTranslation.js.map +1 -1
  130. package/dist/define/validation-error.d.ts +2 -0
  131. package/dist/define/validation-error.d.ts.map +1 -0
  132. package/dist/define/validation-error.js +9 -0
  133. package/dist/define/validation-error.js.map +1 -0
  134. package/dist/errors/hints.d.ts +13 -0
  135. package/dist/errors/hints.d.ts.map +1 -0
  136. package/dist/errors/hints.js +15 -0
  137. package/dist/errors/hints.js.map +1 -0
  138. package/dist/errors/hook-log.d.ts +3 -0
  139. package/dist/errors/hook-log.d.ts.map +1 -0
  140. package/dist/errors/hook-log.js +6 -0
  141. package/dist/errors/hook-log.js.map +1 -0
  142. package/dist/errors/http.d.ts +19 -0
  143. package/dist/errors/http.d.ts.map +1 -1
  144. package/dist/errors/http.js +32 -2
  145. package/dist/errors/http.js.map +1 -1
  146. package/dist/errors/safe-metadata.d.ts +5 -0
  147. package/dist/errors/safe-metadata.d.ts.map +1 -0
  148. package/dist/errors/safe-metadata.js +24 -0
  149. package/dist/errors/safe-metadata.js.map +1 -0
  150. package/dist/execution/auth-roles.d.ts +6 -0
  151. package/dist/execution/auth-roles.d.ts.map +1 -0
  152. package/dist/execution/auth-roles.js +12 -0
  153. package/dist/execution/auth-roles.js.map +1 -0
  154. package/dist/execution/authorization.d.ts.map +1 -1
  155. package/dist/execution/authorization.js +3 -5
  156. package/dist/execution/authorization.js.map +1 -1
  157. package/dist/execution/context-factory.d.ts +2 -17
  158. package/dist/execution/context-factory.d.ts.map +1 -1
  159. package/dist/execution/context-factory.js +1 -0
  160. package/dist/execution/context-factory.js.map +1 -1
  161. package/dist/flows/engine.js +1 -1
  162. package/dist/flows/engine.js.map +1 -1
  163. package/dist/flows/evaluate-condition.d.ts +19 -0
  164. package/dist/flows/evaluate-condition.d.ts.map +1 -0
  165. package/dist/flows/evaluate-condition.js +276 -0
  166. package/dist/flows/evaluate-condition.js.map +1 -0
  167. package/dist/flows/index.d.ts +1 -0
  168. package/dist/flows/index.d.ts.map +1 -1
  169. package/dist/flows/index.js +2 -0
  170. package/dist/flows/index.js.map +1 -1
  171. package/dist/flows/step-executor.d.ts.map +1 -1
  172. package/dist/flows/step-executor.js +15 -7
  173. package/dist/flows/step-executor.js.map +1 -1
  174. package/dist/governance/rules/architecture.d.ts.map +1 -1
  175. package/dist/governance/rules/architecture.js +2 -0
  176. package/dist/governance/rules/architecture.js.map +1 -1
  177. package/dist/governance/rules/index.d.ts +1 -0
  178. package/dist/governance/rules/index.d.ts.map +1 -1
  179. package/dist/governance/rules/index.js +1 -0
  180. package/dist/governance/rules/index.js.map +1 -1
  181. package/dist/governance/rules/mcp.d.ts +5 -0
  182. package/dist/governance/rules/mcp.d.ts.map +1 -0
  183. package/dist/governance/rules/mcp.js +34 -0
  184. package/dist/governance/rules/mcp.js.map +1 -0
  185. package/dist/index.d.ts +1 -0
  186. package/dist/index.d.ts.map +1 -1
  187. package/dist/index.js +1 -0
  188. package/dist/index.js.map +1 -1
  189. package/dist/mcp/exposure.d.ts +4 -0
  190. package/dist/mcp/exposure.d.ts.map +1 -0
  191. package/dist/mcp/exposure.js +5 -0
  192. package/dist/mcp/exposure.js.map +1 -0
  193. package/dist/mcp/index.d.ts +4 -0
  194. package/dist/mcp/index.d.ts.map +1 -0
  195. package/dist/mcp/index.js +6 -0
  196. package/dist/mcp/index.js.map +1 -0
  197. package/dist/mcp/manifest-generator.d.ts +19 -0
  198. package/dist/mcp/manifest-generator.d.ts.map +1 -0
  199. package/dist/mcp/manifest-generator.js +29 -0
  200. package/dist/mcp/manifest-generator.js.map +1 -0
  201. package/dist/mcp/skill-generator.d.ts +4 -0
  202. package/dist/mcp/skill-generator.d.ts.map +1 -0
  203. package/dist/mcp/skill-generator.js +59 -0
  204. package/dist/mcp/skill-generator.js.map +1 -0
  205. package/dist/schema/zod-input-to-json-schema.d.ts +4 -0
  206. package/dist/schema/zod-input-to-json-schema.d.ts.map +1 -0
  207. package/dist/schema/zod-input-to-json-schema.js +6 -0
  208. package/dist/schema/zod-input-to-json-schema.js.map +1 -0
  209. package/dist/server/bootstrap.d.ts.map +1 -1
  210. package/dist/server/bootstrap.js +8 -3
  211. package/dist/server/bootstrap.js.map +1 -1
  212. package/dist/testing/context.d.ts +2 -0
  213. package/dist/testing/context.d.ts.map +1 -1
  214. package/dist/testing/context.js +1 -0
  215. package/dist/testing/context.js.map +1 -1
  216. package/dist/testing/e2e.js +1 -1
  217. package/dist/testing/e2e.js.map +1 -1
  218. package/dist/types/capability.d.ts +13 -0
  219. package/dist/types/capability.d.ts.map +1 -1
  220. package/dist/types/config.d.ts +9 -0
  221. package/dist/types/config.d.ts.map +1 -1
  222. package/dist/types/context.d.ts +31 -0
  223. package/dist/types/context.d.ts.map +1 -1
  224. package/dist/types/deep-freeze.d.ts +6 -0
  225. package/dist/types/deep-freeze.d.ts.map +1 -0
  226. package/dist/types/deep-freeze.js +38 -0
  227. package/dist/types/deep-freeze.js.map +1 -0
  228. package/dist/types/enums.d.ts +5 -0
  229. package/dist/types/enums.d.ts.map +1 -1
  230. package/dist/types/enums.js +5 -0
  231. package/dist/types/enums.js.map +1 -1
  232. package/dist/types/index.d.ts +2 -2
  233. package/dist/types/index.d.ts.map +1 -1
  234. package/dist/types/index.js.map +1 -1
  235. package/dist/types/security.d.ts +3 -1
  236. package/dist/types/security.d.ts.map +1 -1
  237. package/instructions/capabilities.md +36 -0
  238. package/instructions/cli.md +44 -0
  239. package/instructions/framework.md +20 -4
  240. package/instructions/mcp.md +20 -0
  241. package/package.json +21 -2
  242. package/dist/ai/__tests__/ai-service.test.d.ts +0 -2
  243. package/dist/ai/__tests__/ai-service.test.d.ts.map +0 -1
  244. package/dist/ai/__tests__/ai-service.test.js +0 -1078
  245. package/dist/ai/__tests__/ai-service.test.js.map +0 -1
  246. package/dist/ai/__tests__/chunking.test.d.ts +0 -2
  247. package/dist/ai/__tests__/chunking.test.d.ts.map +0 -1
  248. package/dist/ai/__tests__/chunking.test.js +0 -78
  249. package/dist/ai/__tests__/chunking.test.js.map +0 -1
  250. package/dist/ai/__tests__/cost-tracker.test.d.ts +0 -2
  251. package/dist/ai/__tests__/cost-tracker.test.d.ts.map +0 -1
  252. package/dist/ai/__tests__/cost-tracker.test.js +0 -201
  253. package/dist/ai/__tests__/cost-tracker.test.js.map +0 -1
  254. package/dist/ai/__tests__/explainability.test.d.ts +0 -2
  255. package/dist/ai/__tests__/explainability.test.d.ts.map +0 -1
  256. package/dist/ai/__tests__/explainability.test.js +0 -100
  257. package/dist/ai/__tests__/explainability.test.js.map +0 -1
  258. package/dist/ai/__tests__/model-pricing.test.d.ts +0 -2
  259. package/dist/ai/__tests__/model-pricing.test.d.ts.map +0 -1
  260. package/dist/ai/__tests__/model-pricing.test.js +0 -156
  261. package/dist/ai/__tests__/model-pricing.test.js.map +0 -1
  262. package/dist/ai/__tests__/prompt-registry.test.d.ts +0 -2
  263. package/dist/ai/__tests__/prompt-registry.test.d.ts.map +0 -1
  264. package/dist/ai/__tests__/prompt-registry.test.js +0 -56
  265. package/dist/ai/__tests__/prompt-registry.test.js.map +0 -1
  266. package/dist/ai/__tests__/provider.test.d.ts +0 -3
  267. package/dist/ai/__tests__/provider.test.d.ts.map +0 -1
  268. package/dist/ai/__tests__/provider.test.js +0 -979
  269. package/dist/ai/__tests__/provider.test.js.map +0 -1
  270. package/dist/ai/__tests__/rag-pipeline.test.d.ts +0 -2
  271. package/dist/ai/__tests__/rag-pipeline.test.d.ts.map +0 -1
  272. package/dist/ai/__tests__/rag-pipeline.test.js +0 -174
  273. package/dist/ai/__tests__/rag-pipeline.test.js.map +0 -1
  274. package/dist/ai/__tests__/security.test.d.ts +0 -2
  275. package/dist/ai/__tests__/security.test.d.ts.map +0 -1
  276. package/dist/ai/__tests__/security.test.js +0 -89
  277. package/dist/ai/__tests__/security.test.js.map +0 -1
  278. package/dist/ai/__tests__/usage-client.test.d.ts +0 -2
  279. package/dist/ai/__tests__/usage-client.test.d.ts.map +0 -1
  280. package/dist/ai/__tests__/usage-client.test.js +0 -120
  281. package/dist/ai/__tests__/usage-client.test.js.map +0 -1
  282. package/dist/ai/__tests__/validation.test.d.ts +0 -2
  283. package/dist/ai/__tests__/validation.test.d.ts.map +0 -1
  284. package/dist/ai/__tests__/validation.test.js +0 -384
  285. package/dist/ai/__tests__/validation.test.js.map +0 -1
  286. package/dist/ai/__tests__/zod-to-provider-schema.test.d.ts +0 -2
  287. package/dist/ai/__tests__/zod-to-provider-schema.test.d.ts.map +0 -1
  288. package/dist/ai/__tests__/zod-to-provider-schema.test.js +0 -60
  289. package/dist/ai/__tests__/zod-to-provider-schema.test.js.map +0 -1
  290. package/dist/api/__tests__/route-generator.test.d.ts +0 -2
  291. package/dist/api/__tests__/route-generator.test.d.ts.map +0 -1
  292. package/dist/api/__tests__/route-generator.test.js +0 -216
  293. package/dist/api/__tests__/route-generator.test.js.map +0 -1
  294. package/dist/audit/__tests__/service.test.d.ts +0 -2
  295. package/dist/audit/__tests__/service.test.d.ts.map +0 -1
  296. package/dist/audit/__tests__/service.test.js +0 -97
  297. package/dist/audit/__tests__/service.test.js.map +0 -1
  298. package/dist/auth/__tests__/adapter.test.d.ts +0 -2
  299. package/dist/auth/__tests__/adapter.test.d.ts.map +0 -1
  300. package/dist/auth/__tests__/adapter.test.js +0 -218
  301. package/dist/auth/__tests__/adapter.test.js.map +0 -1
  302. package/dist/auth/__tests__/oidc-adapter.test.d.ts +0 -2
  303. package/dist/auth/__tests__/oidc-adapter.test.d.ts.map +0 -1
  304. package/dist/auth/__tests__/oidc-adapter.test.js +0 -232
  305. package/dist/auth/__tests__/oidc-adapter.test.js.map +0 -1
  306. package/dist/auth/__tests__/password.test.d.ts +0 -2
  307. package/dist/auth/__tests__/password.test.d.ts.map +0 -1
  308. package/dist/auth/__tests__/password.test.js +0 -30
  309. package/dist/auth/__tests__/password.test.js.map +0 -1
  310. package/dist/auth/__tests__/saml-adapter.test.d.ts +0 -2
  311. package/dist/auth/__tests__/saml-adapter.test.d.ts.map +0 -1
  312. package/dist/auth/__tests__/saml-adapter.test.js +0 -188
  313. package/dist/auth/__tests__/saml-adapter.test.js.map +0 -1
  314. package/dist/auth/__tests__/scim.test.d.ts +0 -2
  315. package/dist/auth/__tests__/scim.test.d.ts.map +0 -1
  316. package/dist/auth/__tests__/scim.test.js +0 -306
  317. package/dist/auth/__tests__/scim.test.js.map +0 -1
  318. package/dist/cli/__tests__/agent.test.d.ts +0 -2
  319. package/dist/cli/__tests__/agent.test.d.ts.map +0 -1
  320. package/dist/cli/__tests__/agent.test.js +0 -63
  321. package/dist/cli/__tests__/agent.test.js.map +0 -1
  322. package/dist/cli/__tests__/certify.test.d.ts +0 -2
  323. package/dist/cli/__tests__/certify.test.d.ts.map +0 -1
  324. package/dist/cli/__tests__/certify.test.js +0 -97
  325. package/dist/cli/__tests__/certify.test.js.map +0 -1
  326. package/dist/cli/__tests__/cli.test.d.ts +0 -2
  327. package/dist/cli/__tests__/cli.test.d.ts.map +0 -1
  328. package/dist/cli/__tests__/cli.test.js +0 -123
  329. package/dist/cli/__tests__/cli.test.js.map +0 -1
  330. package/dist/cli/__tests__/create.test.d.ts +0 -2
  331. package/dist/cli/__tests__/create.test.d.ts.map +0 -1
  332. package/dist/cli/__tests__/create.test.js +0 -183
  333. package/dist/cli/__tests__/create.test.js.map +0 -1
  334. package/dist/cli/__tests__/dev.test.d.ts +0 -2
  335. package/dist/cli/__tests__/dev.test.d.ts.map +0 -1
  336. package/dist/cli/__tests__/dev.test.js +0 -132
  337. package/dist/cli/__tests__/dev.test.js.map +0 -1
  338. package/dist/cli/__tests__/discover.test.d.ts +0 -2
  339. package/dist/cli/__tests__/discover.test.d.ts.map +0 -1
  340. package/dist/cli/__tests__/discover.test.js +0 -48
  341. package/dist/cli/__tests__/discover.test.js.map +0 -1
  342. package/dist/cli/__tests__/doctor.test.d.ts +0 -2
  343. package/dist/cli/__tests__/doctor.test.d.ts.map +0 -1
  344. package/dist/cli/__tests__/doctor.test.js +0 -172
  345. package/dist/cli/__tests__/doctor.test.js.map +0 -1
  346. package/dist/cli/__tests__/generate.test.d.ts +0 -2
  347. package/dist/cli/__tests__/generate.test.d.ts.map +0 -1
  348. package/dist/cli/__tests__/generate.test.js +0 -478
  349. package/dist/cli/__tests__/generate.test.js.map +0 -1
  350. package/dist/cli/__tests__/init.test.d.ts +0 -2
  351. package/dist/cli/__tests__/init.test.d.ts.map +0 -1
  352. package/dist/cli/__tests__/init.test.js +0 -236
  353. package/dist/cli/__tests__/init.test.js.map +0 -1
  354. package/dist/cli/__tests__/migrate.test.d.ts +0 -2
  355. package/dist/cli/__tests__/migrate.test.d.ts.map +0 -1
  356. package/dist/cli/__tests__/migrate.test.js +0 -442
  357. package/dist/cli/__tests__/migrate.test.js.map +0 -1
  358. package/dist/cli/__tests__/start.test.d.ts +0 -2
  359. package/dist/cli/__tests__/start.test.d.ts.map +0 -1
  360. package/dist/cli/__tests__/start.test.js +0 -91
  361. package/dist/cli/__tests__/start.test.js.map +0 -1
  362. package/dist/cli/__tests__/templates.test.d.ts +0 -2
  363. package/dist/cli/__tests__/templates.test.d.ts.map +0 -1
  364. package/dist/cli/__tests__/templates.test.js +0 -49
  365. package/dist/cli/__tests__/templates.test.js.map +0 -1
  366. package/dist/cli/__tests__/test.test.d.ts +0 -2
  367. package/dist/cli/__tests__/test.test.d.ts.map +0 -1
  368. package/dist/cli/__tests__/test.test.js +0 -40
  369. package/dist/cli/__tests__/test.test.js.map +0 -1
  370. package/dist/cli/__tests__/ui.test.d.ts +0 -2
  371. package/dist/cli/__tests__/ui.test.d.ts.map +0 -1
  372. package/dist/cli/__tests__/ui.test.js +0 -76
  373. package/dist/cli/__tests__/ui.test.js.map +0 -1
  374. package/dist/cli/__tests__/upgrade.test.d.ts +0 -2
  375. package/dist/cli/__tests__/upgrade.test.d.ts.map +0 -1
  376. package/dist/cli/__tests__/upgrade.test.js +0 -20
  377. package/dist/cli/__tests__/upgrade.test.js.map +0 -1
  378. package/dist/cli/__tests__/utils.test.d.ts +0 -2
  379. package/dist/cli/__tests__/utils.test.d.ts.map +0 -1
  380. package/dist/cli/__tests__/utils.test.js +0 -255
  381. package/dist/cli/__tests__/utils.test.js.map +0 -1
  382. package/dist/cli/__tests__/verify.test.d.ts +0 -2
  383. package/dist/cli/__tests__/verify.test.d.ts.map +0 -1
  384. package/dist/cli/__tests__/verify.test.js +0 -138
  385. package/dist/cli/__tests__/verify.test.js.map +0 -1
  386. package/dist/cli/commands/__tests__/commands.test.d.ts +0 -2
  387. package/dist/cli/commands/__tests__/commands.test.d.ts.map +0 -1
  388. package/dist/cli/commands/__tests__/commands.test.js +0 -185
  389. package/dist/cli/commands/__tests__/commands.test.js.map +0 -1
  390. package/dist/config/__tests__/loader.test.d.ts +0 -2
  391. package/dist/config/__tests__/loader.test.d.ts.map +0 -1
  392. package/dist/config/__tests__/loader.test.js +0 -462
  393. package/dist/config/__tests__/loader.test.js.map +0 -1
  394. package/dist/data/__tests__/drift-inspector.test.d.ts +0 -2
  395. package/dist/data/__tests__/drift-inspector.test.d.ts.map +0 -1
  396. package/dist/data/__tests__/drift-inspector.test.js +0 -170
  397. package/dist/data/__tests__/drift-inspector.test.js.map +0 -1
  398. package/dist/data/__tests__/framework-migration-rewriter.test.d.ts +0 -2
  399. package/dist/data/__tests__/framework-migration-rewriter.test.d.ts.map +0 -1
  400. package/dist/data/__tests__/framework-migration-rewriter.test.js +0 -329
  401. package/dist/data/__tests__/framework-migration-rewriter.test.js.map +0 -1
  402. package/dist/data/__tests__/migration.test.d.ts +0 -2
  403. package/dist/data/__tests__/migration.test.d.ts.map +0 -1
  404. package/dist/data/__tests__/migration.test.js +0 -145
  405. package/dist/data/__tests__/migration.test.js.map +0 -1
  406. package/dist/data/__tests__/registry.test.d.ts +0 -2
  407. package/dist/data/__tests__/registry.test.d.ts.map +0 -1
  408. package/dist/data/__tests__/registry.test.js +0 -70
  409. package/dist/data/__tests__/registry.test.js.map +0 -1
  410. package/dist/data/__tests__/repository.test.d.ts +0 -2
  411. package/dist/data/__tests__/repository.test.d.ts.map +0 -1
  412. package/dist/data/__tests__/repository.test.js +0 -559
  413. package/dist/data/__tests__/repository.test.js.map +0 -1
  414. package/dist/data/__tests__/schema-generator.test.d.ts +0 -2
  415. package/dist/data/__tests__/schema-generator.test.d.ts.map +0 -1
  416. package/dist/data/__tests__/schema-generator.test.js +0 -112
  417. package/dist/data/__tests__/schema-generator.test.js.map +0 -1
  418. package/dist/define/__tests__/defineCapability.test.d.ts +0 -2
  419. package/dist/define/__tests__/defineCapability.test.d.ts.map +0 -1
  420. package/dist/define/__tests__/defineCapability.test.js +0 -66
  421. package/dist/define/__tests__/defineCapability.test.js.map +0 -1
  422. package/dist/define/__tests__/defineEntity.test.d.ts +0 -2
  423. package/dist/define/__tests__/defineEntity.test.d.ts.map +0 -1
  424. package/dist/define/__tests__/defineEntity.test.js +0 -50
  425. package/dist/define/__tests__/defineEntity.test.js.map +0 -1
  426. package/dist/define/__tests__/defineEvent.test.d.ts +0 -2
  427. package/dist/define/__tests__/defineEvent.test.d.ts.map +0 -1
  428. package/dist/define/__tests__/defineEvent.test.js +0 -35
  429. package/dist/define/__tests__/defineEvent.test.js.map +0 -1
  430. package/dist/define/__tests__/defineFlow.test.d.ts +0 -2
  431. package/dist/define/__tests__/defineFlow.test.d.ts.map +0 -1
  432. package/dist/define/__tests__/defineFlow.test.js +0 -49
  433. package/dist/define/__tests__/defineFlow.test.js.map +0 -1
  434. package/dist/define/__tests__/definePrompt.test.d.ts +0 -2
  435. package/dist/define/__tests__/definePrompt.test.d.ts.map +0 -1
  436. package/dist/define/__tests__/definePrompt.test.js +0 -77
  437. package/dist/define/__tests__/definePrompt.test.js.map +0 -1
  438. package/dist/define/__tests__/defineTranslation.test.d.ts +0 -2
  439. package/dist/define/__tests__/defineTranslation.test.d.ts.map +0 -1
  440. package/dist/define/__tests__/defineTranslation.test.js +0 -90
  441. package/dist/define/__tests__/defineTranslation.test.js.map +0 -1
  442. package/dist/errors/__tests__/errors.test.d.ts +0 -2
  443. package/dist/errors/__tests__/errors.test.d.ts.map +0 -1
  444. package/dist/errors/__tests__/errors.test.js +0 -70
  445. package/dist/errors/__tests__/errors.test.js.map +0 -1
  446. package/dist/errors/__tests__/http.test.d.ts +0 -2
  447. package/dist/errors/__tests__/http.test.d.ts.map +0 -1
  448. package/dist/errors/__tests__/http.test.js +0 -35
  449. package/dist/errors/__tests__/http.test.js.map +0 -1
  450. package/dist/events/__tests__/consumer-registry.test.d.ts +0 -2
  451. package/dist/events/__tests__/consumer-registry.test.d.ts.map +0 -1
  452. package/dist/events/__tests__/consumer-registry.test.js +0 -68
  453. package/dist/events/__tests__/consumer-registry.test.js.map +0 -1
  454. package/dist/events/__tests__/dispatcher.test.d.ts +0 -2
  455. package/dist/events/__tests__/dispatcher.test.d.ts.map +0 -1
  456. package/dist/events/__tests__/dispatcher.test.js +0 -168
  457. package/dist/events/__tests__/dispatcher.test.js.map +0 -1
  458. package/dist/events/__tests__/event-emitter.test.d.ts +0 -2
  459. package/dist/events/__tests__/event-emitter.test.d.ts.map +0 -1
  460. package/dist/events/__tests__/event-emitter.test.js +0 -172
  461. package/dist/events/__tests__/event-emitter.test.js.map +0 -1
  462. package/dist/events/__tests__/event-registry.test.d.ts +0 -2
  463. package/dist/events/__tests__/event-registry.test.d.ts.map +0 -1
  464. package/dist/events/__tests__/event-registry.test.js +0 -55
  465. package/dist/events/__tests__/event-registry.test.js.map +0 -1
  466. package/dist/events/__tests__/event-worker.test.d.ts +0 -2
  467. package/dist/events/__tests__/event-worker.test.d.ts.map +0 -1
  468. package/dist/events/__tests__/event-worker.test.js +0 -174
  469. package/dist/events/__tests__/event-worker.test.js.map +0 -1
  470. package/dist/events/__tests__/queue.test.d.ts +0 -2
  471. package/dist/events/__tests__/queue.test.d.ts.map +0 -1
  472. package/dist/events/__tests__/queue.test.js +0 -73
  473. package/dist/events/__tests__/queue.test.js.map +0 -1
  474. package/dist/execution/__tests__/authorization.test.d.ts +0 -2
  475. package/dist/execution/__tests__/authorization.test.d.ts.map +0 -1
  476. package/dist/execution/__tests__/authorization.test.js +0 -99
  477. package/dist/execution/__tests__/authorization.test.js.map +0 -1
  478. package/dist/execution/__tests__/capability-executor.test.d.ts +0 -2
  479. package/dist/execution/__tests__/capability-executor.test.d.ts.map +0 -1
  480. package/dist/execution/__tests__/capability-executor.test.js +0 -212
  481. package/dist/execution/__tests__/capability-executor.test.js.map +0 -1
  482. package/dist/execution/__tests__/capability-registry.test.d.ts +0 -2
  483. package/dist/execution/__tests__/capability-registry.test.d.ts.map +0 -1
  484. package/dist/execution/__tests__/capability-registry.test.js +0 -50
  485. package/dist/execution/__tests__/capability-registry.test.js.map +0 -1
  486. package/dist/execution/__tests__/context-factory.test.d.ts +0 -2
  487. package/dist/execution/__tests__/context-factory.test.d.ts.map +0 -1
  488. package/dist/execution/__tests__/context-factory.test.js +0 -133
  489. package/dist/execution/__tests__/context-factory.test.js.map +0 -1
  490. package/dist/explanation/__tests__/tracker.test.d.ts +0 -2
  491. package/dist/explanation/__tests__/tracker.test.d.ts.map +0 -1
  492. package/dist/explanation/__tests__/tracker.test.js +0 -169
  493. package/dist/explanation/__tests__/tracker.test.js.map +0 -1
  494. package/dist/fields/__tests__/fields.test.d.ts +0 -2
  495. package/dist/fields/__tests__/fields.test.d.ts.map +0 -1
  496. package/dist/fields/__tests__/fields.test.js +0 -60
  497. package/dist/fields/__tests__/fields.test.js.map +0 -1
  498. package/dist/flows/__tests__/flow-engine.test.d.ts +0 -2
  499. package/dist/flows/__tests__/flow-engine.test.d.ts.map +0 -1
  500. package/dist/flows/__tests__/flow-engine.test.js +0 -797
  501. package/dist/flows/__tests__/flow-engine.test.js.map +0 -1
  502. package/dist/flows/__tests__/flow-registry.test.d.ts +0 -2
  503. package/dist/flows/__tests__/flow-registry.test.d.ts.map +0 -1
  504. package/dist/flows/__tests__/flow-registry.test.js +0 -69
  505. package/dist/flows/__tests__/flow-registry.test.js.map +0 -1
  506. package/dist/flows/__tests__/flow-service.test.d.ts +0 -2
  507. package/dist/flows/__tests__/flow-service.test.d.ts.map +0 -1
  508. package/dist/flows/__tests__/flow-service.test.js +0 -55
  509. package/dist/flows/__tests__/flow-service.test.js.map +0 -1
  510. package/dist/flows/__tests__/scheduler.test.d.ts +0 -2
  511. package/dist/flows/__tests__/scheduler.test.d.ts.map +0 -1
  512. package/dist/flows/__tests__/scheduler.test.js +0 -120
  513. package/dist/flows/__tests__/scheduler.test.js.map +0 -1
  514. package/dist/flows/__tests__/state-machine.test.d.ts +0 -2
  515. package/dist/flows/__tests__/state-machine.test.d.ts.map +0 -1
  516. package/dist/flows/__tests__/state-machine.test.js +0 -62
  517. package/dist/flows/__tests__/state-machine.test.js.map +0 -1
  518. package/dist/flows/__tests__/step-executor.test.d.ts +0 -2
  519. package/dist/flows/__tests__/step-executor.test.d.ts.map +0 -1
  520. package/dist/flows/__tests__/step-executor.test.js +0 -201
  521. package/dist/flows/__tests__/step-executor.test.js.map +0 -1
  522. package/dist/flows/__tests__/triggers.test.d.ts +0 -2
  523. package/dist/flows/__tests__/triggers.test.d.ts.map +0 -1
  524. package/dist/flows/__tests__/triggers.test.js +0 -98
  525. package/dist/flows/__tests__/triggers.test.js.map +0 -1
  526. package/dist/governance/__tests__/overrides.test.d.ts +0 -2
  527. package/dist/governance/__tests__/overrides.test.d.ts.map +0 -1
  528. package/dist/governance/__tests__/overrides.test.js +0 -123
  529. package/dist/governance/__tests__/overrides.test.js.map +0 -1
  530. package/dist/governance/__tests__/policies.test.d.ts +0 -2
  531. package/dist/governance/__tests__/policies.test.d.ts.map +0 -1
  532. package/dist/governance/__tests__/policies.test.js +0 -203
  533. package/dist/governance/__tests__/policies.test.js.map +0 -1
  534. package/dist/governance/__tests__/rules.test.d.ts +0 -2
  535. package/dist/governance/__tests__/rules.test.d.ts.map +0 -1
  536. package/dist/governance/__tests__/rules.test.js +0 -342
  537. package/dist/governance/__tests__/rules.test.js.map +0 -1
  538. package/dist/observability/__tests__/metrics.test.d.ts +0 -2
  539. package/dist/observability/__tests__/metrics.test.d.ts.map +0 -1
  540. package/dist/observability/__tests__/metrics.test.js +0 -301
  541. package/dist/observability/__tests__/metrics.test.js.map +0 -1
  542. package/dist/server/__tests__/bootstrap.test.d.ts +0 -2
  543. package/dist/server/__tests__/bootstrap.test.d.ts.map +0 -1
  544. package/dist/server/__tests__/bootstrap.test.js +0 -381
  545. package/dist/server/__tests__/bootstrap.test.js.map +0 -1
  546. package/dist/testing/__tests__/context.test.d.ts +0 -2
  547. package/dist/testing/__tests__/context.test.d.ts.map +0 -1
  548. package/dist/testing/__tests__/context.test.js +0 -341
  549. package/dist/testing/__tests__/context.test.js.map +0 -1
  550. package/dist/testing/__tests__/e2e.test.d.ts +0 -2
  551. package/dist/testing/__tests__/e2e.test.d.ts.map +0 -1
  552. package/dist/testing/__tests__/e2e.test.js +0 -119
  553. package/dist/testing/__tests__/e2e.test.js.map +0 -1
  554. package/dist/testing/__tests__/field-validation.test.d.ts +0 -2
  555. package/dist/testing/__tests__/field-validation.test.d.ts.map +0 -1
  556. package/dist/testing/__tests__/field-validation.test.js +0 -178
  557. package/dist/testing/__tests__/field-validation.test.js.map +0 -1
  558. package/dist/testing/__tests__/governance.test.d.ts +0 -2
  559. package/dist/testing/__tests__/governance.test.d.ts.map +0 -1
  560. package/dist/testing/__tests__/governance.test.js +0 -222
  561. package/dist/testing/__tests__/governance.test.js.map +0 -1
  562. package/dist/testing/__tests__/run-capability.test.d.ts +0 -2
  563. package/dist/testing/__tests__/run-capability.test.d.ts.map +0 -1
  564. package/dist/testing/__tests__/run-capability.test.js +0 -114
  565. package/dist/testing/__tests__/run-capability.test.js.map +0 -1
  566. package/dist/testing/__tests__/scaffolding.test.d.ts +0 -2
  567. package/dist/testing/__tests__/scaffolding.test.d.ts.map +0 -1
  568. package/dist/testing/__tests__/scaffolding.test.js +0 -70
  569. package/dist/testing/__tests__/scaffolding.test.js.map +0 -1
  570. package/dist/testing/__tests__/security.test.d.ts +0 -2
  571. package/dist/testing/__tests__/security.test.d.ts.map +0 -1
  572. package/dist/testing/__tests__/security.test.js +0 -186
  573. package/dist/testing/__tests__/security.test.js.map +0 -1
  574. package/dist/testing/__tests__/simulate-flow.test.d.ts +0 -2
  575. package/dist/testing/__tests__/simulate-flow.test.d.ts.map +0 -1
  576. package/dist/testing/__tests__/simulate-flow.test.js +0 -230
  577. package/dist/testing/__tests__/simulate-flow.test.js.map +0 -1
  578. package/dist/translations/__tests__/translation-resolver.test.d.ts +0 -2
  579. package/dist/translations/__tests__/translation-resolver.test.d.ts.map +0 -1
  580. package/dist/translations/__tests__/translation-resolver.test.js +0 -140
  581. package/dist/translations/__tests__/translation-resolver.test.js.map +0 -1
  582. package/dist/types/__tests__/registry.test.d.ts +0 -2
  583. package/dist/types/__tests__/registry.test.d.ts.map +0 -1
  584. package/dist/types/__tests__/registry.test.js +0 -34
  585. package/dist/types/__tests__/registry.test.js.map +0 -1
  586. package/dist/worker/__tests__/bootstrap.test.d.ts +0 -2
  587. package/dist/worker/__tests__/bootstrap.test.d.ts.map +0 -1
  588. package/dist/worker/__tests__/bootstrap.test.js +0 -268
  589. package/dist/worker/__tests__/bootstrap.test.js.map +0 -1
  590. package/dist/worker/__tests__/flow-service-wiring.test.d.ts +0 -2
  591. package/dist/worker/__tests__/flow-service-wiring.test.d.ts.map +0 -1
  592. package/dist/worker/__tests__/flow-service-wiring.test.js +0 -64
  593. 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