@teleporthq/teleport-plugin-next-workflows 0.43.51 → 0.43.54

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 (212) hide show
  1. package/__tests__/ai-chat-rag-sql-parameterization.test.ts +237 -0
  2. package/__tests__/ai-embedding-optional.test.ts +77 -0
  3. package/__tests__/ai-model-capabilities.test.ts +234 -0
  4. package/__tests__/ai-provider-coverage.test.ts +183 -0
  5. package/__tests__/cart-get-total-storefront-tax.test.ts +165 -0
  6. package/__tests__/ecommerce-customhandler-rewriter.test.ts +1 -1
  7. package/__tests__/internal-request-headers.test.ts +159 -0
  8. package/__tests__/order-details-route.test.ts +238 -0
  9. package/__tests__/session-cookie-resolver.test.ts +412 -0
  10. package/__tests__/utility-nodes.test.ts +27 -11
  11. package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
  12. package/dist/cjs/account-delete-route-generator.js +3 -3
  13. package/dist/cjs/account-delete-route-generator.js.map +1 -1
  14. package/dist/cjs/api-route-generator.d.ts.map +1 -1
  15. package/dist/cjs/api-route-generator.js +27 -3
  16. package/dist/cjs/api-route-generator.js.map +1 -1
  17. package/dist/cjs/auth-generator.d.ts.map +1 -1
  18. package/dist/cjs/auth-generator.js +14 -8
  19. package/dist/cjs/auth-generator.js.map +1 -1
  20. package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
  21. package/dist/cjs/data-api-route-generator.js +8 -2
  22. package/dist/cjs/data-api-route-generator.js.map +1 -1
  23. package/dist/cjs/ecommerce/order-details-route.d.ts +18 -0
  24. package/dist/cjs/ecommerce/order-details-route.d.ts.map +1 -0
  25. package/dist/cjs/ecommerce/order-details-route.js +172 -0
  26. package/dist/cjs/ecommerce/order-details-route.js.map +1 -0
  27. package/dist/cjs/ecommerce-customhandler-rewriter.d.ts +1 -1
  28. package/dist/cjs/ecommerce-customhandler-rewriter.d.ts.map +1 -1
  29. package/dist/cjs/ecommerce-customhandler-rewriter.js +20 -4
  30. package/dist/cjs/ecommerce-customhandler-rewriter.js.map +1 -1
  31. package/dist/cjs/executor-generator.d.ts.map +1 -1
  32. package/dist/cjs/executor-generator.js +37 -0
  33. package/dist/cjs/executor-generator.js.map +1 -1
  34. package/dist/cjs/index.d.ts +1 -0
  35. package/dist/cjs/index.d.ts.map +1 -1
  36. package/dist/cjs/index.js +5 -1
  37. package/dist/cjs/index.js.map +1 -1
  38. package/dist/cjs/nodes/ai/ai-custom-prompt.js +1 -1
  39. package/dist/cjs/nodes/ai/ai-custom-prompt.js.map +1 -1
  40. package/dist/cjs/nodes/ai/ai-detect-language.js +1 -1
  41. package/dist/cjs/nodes/ai/ai-detect-language.js.map +1 -1
  42. package/dist/cjs/nodes/ai/ai-generate-text-embedding.d.ts.map +1 -1
  43. package/dist/cjs/nodes/ai/ai-generate-text-embedding.js +47 -4
  44. package/dist/cjs/nodes/ai/ai-generate-text-embedding.js.map +1 -1
  45. package/dist/cjs/nodes/ai/ai-provider-utils.d.ts +2 -0
  46. package/dist/cjs/nodes/ai/ai-provider-utils.d.ts.map +1 -1
  47. package/dist/cjs/nodes/ai/ai-provider-utils.js +195 -106
  48. package/dist/cjs/nodes/ai/ai-provider-utils.js.map +1 -1
  49. package/dist/cjs/nodes/ai/ai-sentiment-analysis.js +1 -1
  50. package/dist/cjs/nodes/ai/ai-sentiment-analysis.js.map +1 -1
  51. package/dist/cjs/nodes/ai/ai-summarization.js +1 -1
  52. package/dist/cjs/nodes/ai/ai-summarization.js.map +1 -1
  53. package/dist/cjs/nodes/ai/ai-text-classifier.js +1 -1
  54. package/dist/cjs/nodes/ai/ai-text-classifier.js.map +1 -1
  55. package/dist/cjs/nodes/ai/ai-text-transform.js +1 -1
  56. package/dist/cjs/nodes/ai/ai-text-transform.js.map +1 -1
  57. package/dist/cjs/nodes/cart/cart-get-total.d.ts.map +1 -1
  58. package/dist/cjs/nodes/cart/cart-get-total.js +56 -15
  59. package/dist/cjs/nodes/cart/cart-get-total.js.map +1 -1
  60. package/dist/cjs/nodes/data/data-count.d.ts.map +1 -1
  61. package/dist/cjs/nodes/data/data-count.js +6 -1
  62. package/dist/cjs/nodes/data/data-count.js.map +1 -1
  63. package/dist/cjs/nodes/data/data-create-item.d.ts.map +1 -1
  64. package/dist/cjs/nodes/data/data-create-item.js +8 -3
  65. package/dist/cjs/nodes/data/data-create-item.js.map +1 -1
  66. package/dist/cjs/nodes/data/data-delete-item.d.ts.map +1 -1
  67. package/dist/cjs/nodes/data/data-delete-item.js +7 -4
  68. package/dist/cjs/nodes/data/data-delete-item.js.map +1 -1
  69. package/dist/cjs/nodes/data/data-raw-query.d.ts.map +1 -1
  70. package/dist/cjs/nodes/data/data-raw-query.js +7 -1
  71. package/dist/cjs/nodes/data/data-raw-query.js.map +1 -1
  72. package/dist/cjs/nodes/data/data-select.d.ts.map +1 -1
  73. package/dist/cjs/nodes/data/data-select.js +6 -1
  74. package/dist/cjs/nodes/data/data-select.js.map +1 -1
  75. package/dist/cjs/nodes/data/data-update-item.d.ts.map +1 -1
  76. package/dist/cjs/nodes/data/data-update-item.js +7 -4
  77. package/dist/cjs/nodes/data/data-update-item.js.map +1 -1
  78. package/dist/cjs/nodes/utility/utility-hybrid-search.js +2 -2
  79. package/dist/cjs/nodes/utility/utility-hybrid-search.js.map +1 -1
  80. package/dist/cjs/nodes/utility/utility-semantic-search.js +1 -1
  81. package/dist/cjs/nodes/utility/utility-semantic-search.js.map +1 -1
  82. package/dist/cjs/nodes/utility/utility-similarity-scoring.d.ts.map +1 -1
  83. package/dist/cjs/nodes/utility/utility-similarity-scoring.js +49 -3
  84. package/dist/cjs/nodes/utility/utility-similarity-scoring.js.map +1 -1
  85. package/dist/cjs/session-cookie-resolver.d.ts +84 -0
  86. package/dist/cjs/session-cookie-resolver.d.ts.map +1 -0
  87. package/dist/cjs/session-cookie-resolver.js +282 -0
  88. package/dist/cjs/session-cookie-resolver.js.map +1 -0
  89. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  90. package/dist/cjs/workflow-auth-generator.d.ts +6 -0
  91. package/dist/cjs/workflow-auth-generator.d.ts.map +1 -1
  92. package/dist/cjs/workflow-auth-generator.js +9 -16
  93. package/dist/cjs/workflow-auth-generator.js.map +1 -1
  94. package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
  95. package/dist/cjs/workflow-project-plugin.js +10 -4
  96. package/dist/cjs/workflow-project-plugin.js.map +1 -1
  97. package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
  98. package/dist/esm/account-delete-route-generator.js +3 -3
  99. package/dist/esm/account-delete-route-generator.js.map +1 -1
  100. package/dist/esm/api-route-generator.d.ts.map +1 -1
  101. package/dist/esm/api-route-generator.js +27 -3
  102. package/dist/esm/api-route-generator.js.map +1 -1
  103. package/dist/esm/auth-generator.d.ts.map +1 -1
  104. package/dist/esm/auth-generator.js +14 -8
  105. package/dist/esm/auth-generator.js.map +1 -1
  106. package/dist/esm/data-api-route-generator.d.ts.map +1 -1
  107. package/dist/esm/data-api-route-generator.js +8 -2
  108. package/dist/esm/data-api-route-generator.js.map +1 -1
  109. package/dist/esm/ecommerce/order-details-route.d.ts +18 -0
  110. package/dist/esm/ecommerce/order-details-route.d.ts.map +1 -0
  111. package/dist/esm/ecommerce/order-details-route.js +167 -0
  112. package/dist/esm/ecommerce/order-details-route.js.map +1 -0
  113. package/dist/esm/ecommerce-customhandler-rewriter.d.ts +1 -1
  114. package/dist/esm/ecommerce-customhandler-rewriter.d.ts.map +1 -1
  115. package/dist/esm/ecommerce-customhandler-rewriter.js +20 -4
  116. package/dist/esm/ecommerce-customhandler-rewriter.js.map +1 -1
  117. package/dist/esm/executor-generator.d.ts.map +1 -1
  118. package/dist/esm/executor-generator.js +37 -0
  119. package/dist/esm/executor-generator.js.map +1 -1
  120. package/dist/esm/index.d.ts +1 -0
  121. package/dist/esm/index.d.ts.map +1 -1
  122. package/dist/esm/index.js +1 -0
  123. package/dist/esm/index.js.map +1 -1
  124. package/dist/esm/nodes/ai/ai-custom-prompt.js +1 -1
  125. package/dist/esm/nodes/ai/ai-custom-prompt.js.map +1 -1
  126. package/dist/esm/nodes/ai/ai-detect-language.js +1 -1
  127. package/dist/esm/nodes/ai/ai-detect-language.js.map +1 -1
  128. package/dist/esm/nodes/ai/ai-generate-text-embedding.d.ts.map +1 -1
  129. package/dist/esm/nodes/ai/ai-generate-text-embedding.js +47 -4
  130. package/dist/esm/nodes/ai/ai-generate-text-embedding.js.map +1 -1
  131. package/dist/esm/nodes/ai/ai-provider-utils.d.ts +2 -0
  132. package/dist/esm/nodes/ai/ai-provider-utils.d.ts.map +1 -1
  133. package/dist/esm/nodes/ai/ai-provider-utils.js +194 -105
  134. package/dist/esm/nodes/ai/ai-provider-utils.js.map +1 -1
  135. package/dist/esm/nodes/ai/ai-sentiment-analysis.js +1 -1
  136. package/dist/esm/nodes/ai/ai-sentiment-analysis.js.map +1 -1
  137. package/dist/esm/nodes/ai/ai-summarization.js +1 -1
  138. package/dist/esm/nodes/ai/ai-summarization.js.map +1 -1
  139. package/dist/esm/nodes/ai/ai-text-classifier.js +1 -1
  140. package/dist/esm/nodes/ai/ai-text-classifier.js.map +1 -1
  141. package/dist/esm/nodes/ai/ai-text-transform.js +1 -1
  142. package/dist/esm/nodes/ai/ai-text-transform.js.map +1 -1
  143. package/dist/esm/nodes/cart/cart-get-total.d.ts.map +1 -1
  144. package/dist/esm/nodes/cart/cart-get-total.js +56 -15
  145. package/dist/esm/nodes/cart/cart-get-total.js.map +1 -1
  146. package/dist/esm/nodes/data/data-count.d.ts.map +1 -1
  147. package/dist/esm/nodes/data/data-count.js +6 -1
  148. package/dist/esm/nodes/data/data-count.js.map +1 -1
  149. package/dist/esm/nodes/data/data-create-item.d.ts.map +1 -1
  150. package/dist/esm/nodes/data/data-create-item.js +8 -3
  151. package/dist/esm/nodes/data/data-create-item.js.map +1 -1
  152. package/dist/esm/nodes/data/data-delete-item.d.ts.map +1 -1
  153. package/dist/esm/nodes/data/data-delete-item.js +7 -4
  154. package/dist/esm/nodes/data/data-delete-item.js.map +1 -1
  155. package/dist/esm/nodes/data/data-raw-query.d.ts.map +1 -1
  156. package/dist/esm/nodes/data/data-raw-query.js +7 -1
  157. package/dist/esm/nodes/data/data-raw-query.js.map +1 -1
  158. package/dist/esm/nodes/data/data-select.d.ts.map +1 -1
  159. package/dist/esm/nodes/data/data-select.js +6 -1
  160. package/dist/esm/nodes/data/data-select.js.map +1 -1
  161. package/dist/esm/nodes/data/data-update-item.d.ts.map +1 -1
  162. package/dist/esm/nodes/data/data-update-item.js +7 -4
  163. package/dist/esm/nodes/data/data-update-item.js.map +1 -1
  164. package/dist/esm/nodes/utility/utility-hybrid-search.js +2 -2
  165. package/dist/esm/nodes/utility/utility-hybrid-search.js.map +1 -1
  166. package/dist/esm/nodes/utility/utility-semantic-search.js +1 -1
  167. package/dist/esm/nodes/utility/utility-semantic-search.js.map +1 -1
  168. package/dist/esm/nodes/utility/utility-similarity-scoring.d.ts.map +1 -1
  169. package/dist/esm/nodes/utility/utility-similarity-scoring.js +49 -3
  170. package/dist/esm/nodes/utility/utility-similarity-scoring.js.map +1 -1
  171. package/dist/esm/session-cookie-resolver.d.ts +84 -0
  172. package/dist/esm/session-cookie-resolver.d.ts.map +1 -0
  173. package/dist/esm/session-cookie-resolver.js +277 -0
  174. package/dist/esm/session-cookie-resolver.js.map +1 -0
  175. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  176. package/dist/esm/workflow-auth-generator.d.ts +6 -0
  177. package/dist/esm/workflow-auth-generator.d.ts.map +1 -1
  178. package/dist/esm/workflow-auth-generator.js +9 -16
  179. package/dist/esm/workflow-auth-generator.js.map +1 -1
  180. package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
  181. package/dist/esm/workflow-project-plugin.js +10 -4
  182. package/dist/esm/workflow-project-plugin.js.map +1 -1
  183. package/package.json +4 -4
  184. package/src/account-delete-route-generator.ts +3 -3
  185. package/src/api-route-generator.ts +27 -3
  186. package/src/auth-generator.ts +14 -8
  187. package/src/data-api-route-generator.ts +7 -2
  188. package/src/ecommerce/order-details-route.ts +195 -0
  189. package/src/ecommerce-customhandler-rewriter.ts +24 -4
  190. package/src/executor-generator.ts +37 -0
  191. package/src/index.ts +5 -0
  192. package/src/nodes/ai/ai-custom-prompt.ts +2 -2
  193. package/src/nodes/ai/ai-detect-language.ts +2 -2
  194. package/src/nodes/ai/ai-generate-text-embedding.ts +49 -4
  195. package/src/nodes/ai/ai-provider-utils.ts +216 -106
  196. package/src/nodes/ai/ai-sentiment-analysis.ts +2 -2
  197. package/src/nodes/ai/ai-summarization.ts +2 -2
  198. package/src/nodes/ai/ai-text-classifier.ts +2 -2
  199. package/src/nodes/ai/ai-text-transform.ts +2 -2
  200. package/src/nodes/cart/cart-get-total.ts +61 -17
  201. package/src/nodes/data/data-count.ts +6 -1
  202. package/src/nodes/data/data-create-item.ts +8 -3
  203. package/src/nodes/data/data-delete-item.ts +6 -0
  204. package/src/nodes/data/data-raw-query.ts +8 -1
  205. package/src/nodes/data/data-select.ts +6 -1
  206. package/src/nodes/data/data-update-item.ts +6 -0
  207. package/src/nodes/utility/utility-hybrid-search.ts +2 -2
  208. package/src/nodes/utility/utility-semantic-search.ts +1 -1
  209. package/src/nodes/utility/utility-similarity-scoring.ts +56 -3
  210. package/src/session-cookie-resolver.ts +280 -0
  211. package/src/workflow-auth-generator.ts +9 -16
  212. package/src/workflow-project-plugin.ts +10 -4
@@ -0,0 +1,237 @@
1
+ import { parameterizeAllWorkflowRawSql } from '../src/raw-sql-param-binding'
2
+ import { generateSqlValidatorCode } from '../src/sql-validator'
3
+
4
+ /**
5
+ * The AI Assistant Chat's RAG pipeline used to assemble its SQL inside
6
+ * `general-custom-js` nodes and bind the resulting string to a `data-raw-query`.
7
+ * Every node result in a server segment is relayed to the visitor's browser, so
8
+ * that published the database schema: on the shipped store the browser received
9
+ *
10
+ * INSERT INTO teleport_ai_chat_conversations (id, user_id, username, title,
11
+ * status, message_count, last_message_at, created_at, updated_at) VALUES
12
+ * ('ddefa366-…', '16017882-…', 'test', …)
13
+ *
14
+ * table names, column list, join keys and the signed-in user's uuid included.
15
+ *
16
+ * The statements now ship as static `query` strings with `{{ name }}` variables.
17
+ * These fixtures are the exact shapes the chat builder emits (kept in
18
+ * `teleport-gui/.../process-chat-message-custom-node-builder.ts`); this suite is
19
+ * the PUBLISH-side half of the guarantee — the GUI has the same check against
20
+ * `workflow-schema`'s copy of the net, and the two copies must agree or a chat
21
+ * that passes in the editor fails to generate.
22
+ */
23
+
24
+ const SEMANTIC_QUERY = [
25
+ 'SELECT d.id, d.content, d.search_content, d.keywords,',
26
+ ' 1 - (d.embedding_json::vector <=> q.query_vector) AS similarity',
27
+ 'FROM teleport_ai_chat_documents d',
28
+ "CROSS JOIN (SELECT NULLIF({{ queryEmbedding }}::text, '')::vector AS query_vector) q",
29
+ 'WHERE d.embedding_json IS NOT NULL',
30
+ ' AND q.query_vector IS NOT NULL',
31
+ 'ORDER BY d.embedding_json::vector <=> q.query_vector ASC',
32
+ 'LIMIT 6',
33
+ ].join('\n')
34
+
35
+ const LEXICAL_QUERY = [
36
+ 'WITH search_query AS (',
37
+ ' SELECT (',
38
+ " SELECT string_agg(quote_literal(lexeme), ' | ')",
39
+ ' FROM (',
40
+ ' SELECT lexeme',
41
+ " FROM unnest(tsvector_to_array(to_tsvector('english', {{ question }}::text))) AS lexeme",
42
+ ' LIMIT 12',
43
+ ' ) capped',
44
+ ' )::tsquery AS tsq',
45
+ ')',
46
+ 'SELECT ranked.id, ranked.content, ranked.rank',
47
+ 'FROM (',
48
+ ' SELECT d.id, d.content,',
49
+ " ts_rank_cd(to_tsvector('english', COALESCE(d.content, '')), sq.tsq, 1) AS rank,",
50
+ ' row_number() OVER (',
51
+ ' PARTITION BY s.source_type',
52
+ " ORDER BY ts_rank_cd(to_tsvector('english', COALESCE(d.content, '')), sq.tsq, 1) DESC",
53
+ ' ) AS rn',
54
+ ' FROM teleport_ai_chat_documents d',
55
+ ' JOIN teleport_ai_chat_knowledge_sources s ON s.id = d.knowledge_source_id',
56
+ ' CROSS JOIN search_query sq',
57
+ " WHERE to_tsvector('english', COALESCE(d.content, '')) @@ sq.tsq",
58
+ ') ranked',
59
+ 'WHERE ranked.rn <= 2',
60
+ 'ORDER BY ranked.rank DESC',
61
+ 'LIMIT 4',
62
+ ].join('\n')
63
+
64
+ const UPSERT_QUERY = [
65
+ 'WITH conversation_input AS (',
66
+ ' SELECT',
67
+ " NULLIF({{ conversationId }}::text, '')::uuid AS conversation_id,",
68
+ " NULLIF({{ userId }}::text, '')::uuid AS user_id,",
69
+ ' {{ username }}::text AS username,',
70
+ ' LEFT({{ title }}::text, 80) AS title,',
71
+ " NULLIF({{ timestamp }}::text, '')::timestamp AS occurred_at",
72
+ ')',
73
+ 'INSERT INTO teleport_ai_chat_conversations',
74
+ ' (id, user_id, username, title, status, message_count,',
75
+ ' last_message_at, created_at, updated_at)',
76
+ 'SELECT conversation_id, user_id, username, title,',
77
+ " 'active', 1, occurred_at, occurred_at, occurred_at",
78
+ 'FROM conversation_input',
79
+ 'WHERE conversation_id IS NOT NULL AND occurred_at IS NOT NULL',
80
+ 'ON CONFLICT (id) DO UPDATE SET',
81
+ ' message_count = teleport_ai_chat_conversations.message_count + 1,',
82
+ ' last_message_at = EXCLUDED.last_message_at,',
83
+ ' updated_at = EXCLUDED.updated_at',
84
+ ].join('\n')
85
+
86
+ const BUMP_QUERY = [
87
+ 'UPDATE teleport_ai_chat_conversations',
88
+ 'SET message_count = message_count + 1,',
89
+ ' last_message_at = NOW(),',
90
+ ' updated_at = NOW()',
91
+ "WHERE id = NULLIF({{ conversationId }}::text, '')::uuid",
92
+ ].join('\n')
93
+
94
+ const contextRef = (nodeId: string, field: string) => ({
95
+ type: 'workflowContext',
96
+ nodeId,
97
+ path: [nodeId, field],
98
+ })
99
+
100
+ interface ChatQueryFixture {
101
+ label: string
102
+ query: string
103
+ variables: Array<{ name: string; value: unknown }>
104
+ expectedParams: number
105
+ }
106
+
107
+ const FIXTURES: ChatQueryFixture[] = [
108
+ {
109
+ label: 'Search Knowledge by Embedding Similarity',
110
+ query: SEMANTIC_QUERY,
111
+ variables: [{ name: 'queryEmbedding', value: contextRef('embed-1', 'vectorLiteral') }],
112
+ expectedParams: 1,
113
+ },
114
+ {
115
+ label: 'Search Knowledge by Keywords',
116
+ query: LEXICAL_QUERY,
117
+ variables: [{ name: 'question', value: contextRef('rephrase-1', 'response') }],
118
+ expectedParams: 1,
119
+ },
120
+ {
121
+ label: 'Upsert Conversation Row',
122
+ query: UPSERT_QUERY,
123
+ variables: [
124
+ { name: 'conversationId', value: contextRef('init-1', 'conversationId') },
125
+ { name: 'userId', value: contextRef('user-1', 'userId') },
126
+ { name: 'username', value: contextRef('user-1', 'username') },
127
+ { name: 'title', value: contextRef('init-1', 'userMessageText') },
128
+ { name: 'timestamp', value: contextRef('init-1', 'timestamp') },
129
+ ],
130
+ expectedParams: 5,
131
+ },
132
+ {
133
+ label: 'Bump Conversation After Reply',
134
+ query: BUMP_QUERY,
135
+ variables: [{ name: 'conversationId', value: contextRef('init-1', 'conversationId') }],
136
+ expectedParams: 1,
137
+ },
138
+ ]
139
+
140
+ function buildCustomNode() {
141
+ return {
142
+ customNodes: {
143
+ 'process-chat-message': {
144
+ nodes: FIXTURES.map((fixture, index) => ({
145
+ id: `node-${index}`,
146
+ data: {
147
+ nodeType: 'data-raw-query',
148
+ label: fixture.label,
149
+ config: {
150
+ dataSourceId: 'ds-1',
151
+ query: fixture.query,
152
+ queryVariables: fixture.variables,
153
+ },
154
+ },
155
+ })),
156
+ },
157
+ },
158
+ }
159
+ }
160
+
161
+ describe('the chat RAG statements survive the publish-time parameterization net', () => {
162
+ it('binds every interpolation without a single residual', () => {
163
+ // A residual makes parameterizeAllWorkflowRawSql throw and refuse to
164
+ // generate the project, so this is also the "the chat still publishes" test.
165
+ const uidl = buildCustomNode()
166
+ expect(() => parameterizeAllWorkflowRawSql(uidl)).not.toThrow()
167
+ })
168
+
169
+ it.each(FIXTURES.map((f, i) => [f.label, i] as const))(
170
+ '"%s" ends up fully parameterized',
171
+ (_label, index) => {
172
+ const uidl = buildCustomNode()
173
+ parameterizeAllWorkflowRawSql(uidl)
174
+ const config = uidl.customNodes['process-chat-message'].nodes[index].data.config as Record<
175
+ string,
176
+ unknown
177
+ >
178
+
179
+ expect(String(config.query)).not.toContain('{{')
180
+ expect(config.queryVariables).toBeUndefined()
181
+ expect((config.params as unknown[]).length).toBe(FIXTURES[index].expectedParams)
182
+ }
183
+ )
184
+
185
+ it('keeps the table and column names out of the bound values', () => {
186
+ // The bound params are what a value-injection attempt could reach; the SQL
187
+ // structure must stay in the query text, server-side.
188
+ const uidl = buildCustomNode()
189
+ parameterizeAllWorkflowRawSql(uidl)
190
+ for (const node of uidl.customNodes['process-chat-message'].nodes) {
191
+ const params = (node.data.config as { params: unknown[] }).params
192
+ for (const param of params) {
193
+ expect(JSON.stringify(param)).not.toContain('teleport_ai_chat')
194
+ }
195
+ }
196
+ })
197
+
198
+ it('numbers the placeholders from $1 upward in declaration order', () => {
199
+ const uidl = buildCustomNode()
200
+ parameterizeAllWorkflowRawSql(uidl)
201
+ const upsert = uidl.customNodes['process-chat-message'].nodes[2].data.config as {
202
+ query: string
203
+ params: Array<{ path?: string[] }>
204
+ }
205
+ expect(upsert.query).toContain('NULLIF($1::text')
206
+ expect(upsert.query).toContain('LEFT($4::text, 80)')
207
+ expect(upsert.params[0].path?.[1]).toBe('conversationId')
208
+ expect(upsert.params[3].path?.[1]).toBe('userMessageText')
209
+ })
210
+ })
211
+
212
+ describe('the chat RAG statements pass the guard the generated app runs', () => {
213
+ // The generated /api/data route calls assertQuerySafe on every raw query
214
+ // before executing it. A statement that trips it fails at request time, which
215
+ // for the chat means a silent empty retrieval rather than a build error — so
216
+ // the check has to run against the RUNTIME validator, the one baked into the
217
+ // route, not the design-time `sql-query-validator` (which has no call sites
218
+ // and rejects any `UPDATE … SET`).
219
+ const assertQuerySafe = new Function(
220
+ `${generateSqlValidatorCode()}\nreturn assertQuerySafe;`
221
+ )() as (sql: string) => void
222
+
223
+ it.each(FIXTURES.map((f) => [f.label] as const))('"%s" is allowed', (label) => {
224
+ const uidl = buildCustomNode()
225
+ parameterizeAllWorkflowRawSql(uidl)
226
+ const node = uidl.customNodes['process-chat-message'].nodes.find(
227
+ (entry) => entry.data.label === label
228
+ )
229
+ const { query } = node?.data.config as { query: string }
230
+ expect(() => assertQuerySafe(query)).not.toThrow()
231
+ })
232
+
233
+ it('still rejects the operations the guard exists for', () => {
234
+ expect(() => assertQuerySafe('DROP TABLE teleport_ai_chat_documents')).toThrow()
235
+ expect(() => assertQuerySafe('SET ROLE postgres')).toThrow()
236
+ })
237
+ })
@@ -0,0 +1,77 @@
1
+ import { aiGenerateTextEmbedding } from '../src/nodes/ai/ai-generate-text-embedding'
2
+ import { generateSharedRuntimeUtilsCode } from '../src/executor-generator'
3
+
4
+ /**
5
+ * `{ error: true }` is a FATAL node result — the generated executor throws on
6
+ * it and the whole workflow stops. The AI Assistant Chat's embedding step must
7
+ * not be fatal: its retrieval is a hybrid of a vector search and a keyword
8
+ * search, and a project whose chat runs on Anthropic/Google/Mistral may
9
+ * legitimately have no OpenAI key (embeddings are OpenAI-only because that is
10
+ * what the knowledge base was indexed with). That has to degrade to
11
+ * keyword-only search, not break every message.
12
+ */
13
+ /** The real executor gate, loaded exactly as the generated app defines it. */
14
+ function loadIsFatalNodeResult(): (result: unknown) => boolean {
15
+ const utilsModule = { exports: {} as { isFatalNodeResult: (result: unknown) => boolean } }
16
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
17
+ new Function('module', 'exports', 'require', generateSharedRuntimeUtilsCode())(
18
+ utilsModule,
19
+ utilsModule.exports,
20
+ () => ({})
21
+ )
22
+ return utilsModule.exports.isFatalNodeResult
23
+ }
24
+
25
+ function loadHandler() {
26
+ const source = aiGenerateTextEmbedding.generateHandler()
27
+ // eslint-disable-next-line no-new-func
28
+ return new Function(`${source}\nreturn ai_generate_text_embedding;`)() as (
29
+ config: any,
30
+ context: Record<string, unknown>
31
+ ) => Promise<any>
32
+ }
33
+
34
+ describe('ai-generate-text-embedding failure contract', () => {
35
+ const handler = loadHandler()
36
+
37
+ it('is fatal by default, so a pipeline that needs the vector stops', async () => {
38
+ const result = await handler({ text: 'hello', token: '' }, {})
39
+ expect(result.error).toBe(true)
40
+ expect(result.code).toBe('authentication_error')
41
+ })
42
+
43
+ it('reports failure as data when optional, so the workflow keeps running', async () => {
44
+ const result = await handler({ text: 'hello', token: '', optional: true }, {})
45
+ expect(result.error).toBeUndefined()
46
+ expect(result.skipped).toBe(true)
47
+ expect(result.skipReason).toBeTruthy()
48
+ expect(result.code).toBe('authentication_error')
49
+ })
50
+
51
+ it('returns an empty embedding when skipped, which is what hasEmbedding checks', async () => {
52
+ const result = await handler({ text: 'hello', token: '', optional: true }, {})
53
+ expect(Array.isArray(result.embedding)).toBe(true)
54
+ expect(result.embedding).toHaveLength(0)
55
+ expect(result.dimensions).toBe(0)
56
+ })
57
+
58
+ it('is non-fatal for missing text too, not just auth', async () => {
59
+ const result = await handler({ text: '', token: 'sk-test', optional: true }, {})
60
+ expect(result.error).toBeUndefined()
61
+ expect(result.skipped).toBe(true)
62
+ expect(result.code).toBe('missing_text')
63
+ })
64
+ })
65
+
66
+ describe('the executor agrees about what is fatal', () => {
67
+ const handler = loadHandler()
68
+ const isFatalNodeResult = loadIsFatalNodeResult()
69
+
70
+ it('treats the default failure as fatal and the optional one as success', async () => {
71
+ const fatal = await handler({ text: 'hello', token: '' }, {})
72
+ const optional = await handler({ text: 'hello', token: '', optional: true }, {})
73
+
74
+ expect(isFatalNodeResult(fatal)).toBe(true)
75
+ expect(isFatalNodeResult(optional)).toBe(false)
76
+ })
77
+ })
@@ -0,0 +1,234 @@
1
+ import {
2
+ generateAIProviderUtils,
3
+ generateAIStreamingProviderUtils,
4
+ } from '../src/nodes/ai/ai-provider-utils'
5
+ import { aiCustomPrompt } from '../src/nodes/ai/ai-custom-prompt'
6
+
7
+ /**
8
+ * The AI utils are emitted by serialising real functions into the generated
9
+ * handler file, so the only way to test them is to evaluate the emitted source
10
+ * exactly as the generated app would.
11
+ */
12
+ function loadUtils() {
13
+ const source = generateAIProviderUtils()
14
+ // eslint-disable-next-line no-new-func
15
+ return new Function(
16
+ `${source}
17
+ return {
18
+ detectProvider: __ai_detectProvider,
19
+ resolveProvider: __ai_resolveProvider,
20
+ modelCapabilities: __ai_modelCapabilities,
21
+ providerBaseURL: __ai_providerBaseURL,
22
+ openAICompatibleBody: __ai_openAICompatibleBody,
23
+ clampTemperature: __ai_clampTemperature,
24
+ };`
25
+ )() as {
26
+ detectProvider: (modelId: string) => string
27
+ resolveProvider: (config: unknown) => string
28
+ modelCapabilities: (provider: string, model: string, maxTokens?: number) => any
29
+ providerBaseURL: (provider: string) => string | undefined
30
+ openAICompatibleBody: (
31
+ caps: any,
32
+ model: string,
33
+ systemMessage: string | undefined,
34
+ userMessage: string,
35
+ temperature: number,
36
+ jsonMode: boolean
37
+ ) => any
38
+ clampTemperature: (temp: unknown, provider: string) => number
39
+ }
40
+ }
41
+
42
+ describe('__ai_resolveProvider', () => {
43
+ const utils = loadUtils()
44
+
45
+ it('prefers the explicit provider over the model id', () => {
46
+ expect(utils.resolveProvider({ provider: 'anthropic', model: 'gpt-4o' })).toBe('anthropic')
47
+ })
48
+
49
+ it('falls back to inference for legacy nodes with no provider', () => {
50
+ expect(utils.resolveProvider({ model: 'claude-opus-5' })).toBe('anthropic')
51
+ })
52
+
53
+ it('ignores a blank provider', () => {
54
+ expect(utils.resolveProvider({ provider: ' ', model: 'gemini-2.5-flash' })).toBe('google')
55
+ })
56
+ })
57
+
58
+ describe('__ai_detectProvider covers the shipped catalogue', () => {
59
+ const utils = loadUtils()
60
+
61
+ const CASES: Array<[string, string]> = [
62
+ ['gpt-4o-mini', 'openai'],
63
+ ['gpt-5.1', 'openai'],
64
+ ['o3', 'openai'],
65
+ ['o4-mini', 'openai'],
66
+ ['claude-opus-5', 'anthropic'],
67
+ ['claude-3-5-sonnet-20241022', 'anthropic'],
68
+ ['gemini-2.5-flash', 'google'],
69
+ ['command-a-03-2025', 'cohere'],
70
+ ['command-r-plus-08-2024', 'cohere'],
71
+ ['mistral-large-latest', 'mistral'],
72
+ ['magistral-medium-latest', 'mistral'],
73
+ ['ministral-8b-latest', 'mistral'],
74
+ ['pixtral-large-latest', 'mistral'],
75
+ ['open-mixtral-8x7b', 'mistral'],
76
+ ['open-mistral-nemo', 'mistral'],
77
+ ['meta-llama/Llama-3.3-70B-Instruct-Turbo', 'meta'],
78
+ ['llama-3.1-405b', 'meta'],
79
+ ['sonar-pro', 'perplexity'],
80
+ ['sonar-deep-research', 'perplexity'],
81
+ // Perplexity's retired ids begin with `llama-`; they must not be claimed by
82
+ // the Meta branch, which would send them to Together with a Perplexity key.
83
+ ['llama-3.1-sonar-large-128k-online', 'perplexity'],
84
+ ]
85
+
86
+ it.each(CASES)('%s -> %s', (modelId, provider) => {
87
+ expect(utils.detectProvider(modelId)).toBe(provider)
88
+ })
89
+
90
+ it('defaults to openai for an unknown id', () => {
91
+ expect(utils.detectProvider('something-new')).toBe('openai')
92
+ expect(utils.detectProvider('')).toBe('openai')
93
+ })
94
+ })
95
+
96
+ describe('__ai_modelCapabilities', () => {
97
+ const utils = loadUtils()
98
+
99
+ it('drops temperature for Anthropic models that reject it', () => {
100
+ for (const model of [
101
+ 'claude-fable-5',
102
+ 'claude-mythos-5',
103
+ 'claude-opus-5',
104
+ 'claude-opus-4-8',
105
+ 'claude-opus-4-7',
106
+ 'claude-sonnet-5',
107
+ ]) {
108
+ expect(utils.modelCapabilities('anthropic', model, 500).supportsTemperature).toBe(false)
109
+ }
110
+ })
111
+
112
+ it('keeps temperature for Anthropic models that still accept it', () => {
113
+ for (const model of [
114
+ 'claude-opus-4-6',
115
+ 'claude-sonnet-4-6',
116
+ 'claude-haiku-4-5',
117
+ 'claude-3-opus-20240229',
118
+ ]) {
119
+ expect(utils.modelCapabilities('anthropic', model, 500).supportsTemperature).toBe(true)
120
+ }
121
+ })
122
+
123
+ it('switches OpenAI reasoning models to max_completion_tokens', () => {
124
+ const caps = utils.modelCapabilities('openai', 'o3', 500)
125
+ expect(caps.maxTokensParam).toBe('max_completion_tokens')
126
+ expect(caps.supportsTemperature).toBe(false)
127
+ })
128
+
129
+ it('raises a too-small budget on reasoning models so the answer is not empty', () => {
130
+ // The budget also pays for hidden reasoning tokens: at 200 the model can
131
+ // spend the whole allowance thinking and return an empty string with a 200.
132
+ expect(utils.modelCapabilities('openai', 'o3', 200).maxTokens).toBe(2000)
133
+ expect(utils.modelCapabilities('openai', 'gpt-5', 200).maxTokens).toBe(2000)
134
+ // A generous budget is left alone.
135
+ expect(utils.modelCapabilities('openai', 'o3', 8000).maxTokens).toBe(8000)
136
+ })
137
+
138
+ it('always yields a positive max_tokens, which Anthropic requires', () => {
139
+ expect(utils.modelCapabilities('anthropic', 'claude-opus-5', undefined).maxTokens).toBe(1024)
140
+ expect(utils.modelCapabilities('anthropic', 'claude-opus-5', 0).maxTokens).toBe(1024)
141
+ })
142
+
143
+ it('leaves ordinary chat models untouched', () => {
144
+ const caps = utils.modelCapabilities('openai', 'gpt-4o-mini', 500)
145
+ expect(caps).toMatchObject({
146
+ supportsTemperature: true,
147
+ maxTokensParam: 'max_tokens',
148
+ maxTokens: 500,
149
+ })
150
+ })
151
+ })
152
+
153
+ describe('__ai_openAICompatibleBody', () => {
154
+ const utils = loadUtils()
155
+
156
+ it('omits temperature and uses max_completion_tokens for reasoning models', () => {
157
+ const caps = utils.modelCapabilities('openai', 'gpt-5', 4000)
158
+ const body = utils.openAICompatibleBody(caps, 'gpt-5', 'sys', 'hi', 0.7, false)
159
+ expect(body.temperature).toBeUndefined()
160
+ expect(body.max_tokens).toBeUndefined()
161
+ expect(body.max_completion_tokens).toBe(4000)
162
+ expect(body.messages).toEqual([
163
+ { role: 'system', content: 'sys' },
164
+ { role: 'user', content: 'hi' },
165
+ ])
166
+ })
167
+
168
+ it('sends temperature and max_tokens for an ordinary model', () => {
169
+ const caps = utils.modelCapabilities('openai', 'gpt-4o', 500)
170
+ const body = utils.openAICompatibleBody(caps, 'gpt-4o', undefined, 'hi', 0.2, true)
171
+ expect(body.temperature).toBe(0.2)
172
+ expect(body.max_tokens).toBe(500)
173
+ expect(body.response_format).toEqual({ type: 'json_object' })
174
+ expect(body.messages).toEqual([{ role: 'user', content: 'hi' }])
175
+ })
176
+ })
177
+
178
+ describe('__ai_providerBaseURL', () => {
179
+ const utils = loadUtils()
180
+
181
+ it('routes Meta through Together and Perplexity through its own host', () => {
182
+ expect(utils.providerBaseURL('meta')).toBe('https://api.together.xyz/v1')
183
+ expect(utils.providerBaseURL('perplexity')).toBe('https://api.perplexity.ai')
184
+ })
185
+
186
+ it('leaves first-party OpenAI on the default host', () => {
187
+ expect(utils.providerBaseURL('openai')).toBeUndefined()
188
+ })
189
+ })
190
+
191
+ describe('__ai_clampTemperature', () => {
192
+ const utils = loadUtils()
193
+
194
+ it('clamps to the provider range', () => {
195
+ expect(utils.clampTemperature(1.8, 'anthropic')).toBe(1)
196
+ expect(utils.clampTemperature(1.8, 'openai')).toBe(1.8)
197
+ expect(utils.clampTemperature(-1, 'openai')).toBe(0)
198
+ })
199
+
200
+ it('falls back to 0.7 for a non-numeric value', () => {
201
+ expect(utils.clampTemperature('hot', 'openai')).toBe(0.7)
202
+ expect(utils.clampTemperature(NaN, 'openai')).toBe(0.7)
203
+ })
204
+ })
205
+
206
+ describe('generated handlers reference the provider resolver', () => {
207
+ it('emits __ai_resolveProvider in the custom-prompt handler', () => {
208
+ const handler = aiCustomPrompt.generateHandler()
209
+ expect(handler).toContain('__ai_resolveProvider(config)')
210
+ expect(handler).not.toContain('__ai_detectProvider(model)')
211
+ })
212
+
213
+ it('defines every helper the streaming path calls', () => {
214
+ const streaming = generateAIStreamingProviderUtils()
215
+ for (const symbol of [
216
+ '__ai_modelCapabilities',
217
+ '__ai_providerBaseURL',
218
+ '__ai_openAICompatibleBody',
219
+ '__ai_callProviderStreaming',
220
+ '__ai_handleStreamingCall',
221
+ ]) {
222
+ expect(streaming).toContain(symbol)
223
+ }
224
+ })
225
+
226
+ it('evaluates the streaming bundle without a reference error', () => {
227
+ // eslint-disable-next-line no-new-func
228
+ const load = new Function(
229
+ `${generateAIStreamingProviderUtils()}
230
+ return typeof __ai_callProviderStreaming === 'function' && typeof __ai_handleStreamingCall === 'function';`
231
+ )
232
+ expect(load()).toBe(true)
233
+ })
234
+ })