@teleporthq/teleport-plugin-next-workflows 0.43.53 → 0.43.55
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.
- package/__tests__/ai-chat-rag-sql-parameterization.test.ts +237 -0
- package/__tests__/ai-embedding-optional.test.ts +77 -0
- package/__tests__/ai-model-capabilities.test.ts +234 -0
- package/__tests__/ai-provider-coverage.test.ts +183 -0
- package/__tests__/cart-clear-db-mirror.test.ts +45 -0
- package/__tests__/cart-get-total-storefront-tax.test.ts +165 -0
- package/__tests__/cron-route-loop-templateparams.test.ts +15 -6
- package/__tests__/data-create-item-order-notification.test.ts +33 -0
- package/__tests__/ecommerce-customhandler-rewriter.test.ts +1 -1
- package/__tests__/internal-request-headers.test.ts +159 -0
- package/__tests__/order-details-route.test.ts +238 -0
- package/__tests__/resolve-config-template-params.test.ts +174 -0
- package/__tests__/utility-nodes.test.ts +27 -11
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +27 -15
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/ecommerce/order-details-route.d.ts +18 -0
- package/dist/cjs/ecommerce/order-details-route.d.ts.map +1 -0
- package/dist/cjs/ecommerce/order-details-route.js +172 -0
- package/dist/cjs/ecommerce/order-details-route.js.map +1 -0
- package/dist/cjs/ecommerce-customhandler-rewriter.d.ts +1 -1
- package/dist/cjs/ecommerce-customhandler-rewriter.d.ts.map +1 -1
- package/dist/cjs/ecommerce-customhandler-rewriter.js +20 -4
- package/dist/cjs/ecommerce-customhandler-rewriter.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +69 -11
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/nodes/ai/ai-custom-prompt.js +1 -1
- package/dist/cjs/nodes/ai/ai-custom-prompt.js.map +1 -1
- package/dist/cjs/nodes/ai/ai-detect-language.js +1 -1
- package/dist/cjs/nodes/ai/ai-detect-language.js.map +1 -1
- package/dist/cjs/nodes/ai/ai-generate-text-embedding.d.ts.map +1 -1
- package/dist/cjs/nodes/ai/ai-generate-text-embedding.js +47 -4
- package/dist/cjs/nodes/ai/ai-generate-text-embedding.js.map +1 -1
- package/dist/cjs/nodes/ai/ai-provider-utils.d.ts +2 -0
- package/dist/cjs/nodes/ai/ai-provider-utils.d.ts.map +1 -1
- package/dist/cjs/nodes/ai/ai-provider-utils.js +195 -106
- package/dist/cjs/nodes/ai/ai-provider-utils.js.map +1 -1
- package/dist/cjs/nodes/ai/ai-sentiment-analysis.js +1 -1
- package/dist/cjs/nodes/ai/ai-sentiment-analysis.js.map +1 -1
- package/dist/cjs/nodes/ai/ai-summarization.js +1 -1
- package/dist/cjs/nodes/ai/ai-summarization.js.map +1 -1
- package/dist/cjs/nodes/ai/ai-text-classifier.js +1 -1
- package/dist/cjs/nodes/ai/ai-text-classifier.js.map +1 -1
- package/dist/cjs/nodes/ai/ai-text-transform.js +1 -1
- package/dist/cjs/nodes/ai/ai-text-transform.js.map +1 -1
- package/dist/cjs/nodes/cart/cart-clear.d.ts.map +1 -1
- package/dist/cjs/nodes/cart/cart-clear.js +40 -0
- package/dist/cjs/nodes/cart/cart-clear.js.map +1 -1
- package/dist/cjs/nodes/cart/cart-get-total.d.ts.map +1 -1
- package/dist/cjs/nodes/cart/cart-get-total.js +56 -15
- package/dist/cjs/nodes/cart/cart-get-total.js.map +1 -1
- package/dist/cjs/nodes/data/data-count.d.ts.map +1 -1
- package/dist/cjs/nodes/data/data-count.js +6 -1
- package/dist/cjs/nodes/data/data-count.js.map +1 -1
- package/dist/cjs/nodes/data/data-create-item.d.ts.map +1 -1
- package/dist/cjs/nodes/data/data-create-item.js +45 -9
- package/dist/cjs/nodes/data/data-create-item.js.map +1 -1
- package/dist/cjs/nodes/data/data-delete-item.d.ts.map +1 -1
- package/dist/cjs/nodes/data/data-delete-item.js +7 -4
- package/dist/cjs/nodes/data/data-delete-item.js.map +1 -1
- package/dist/cjs/nodes/data/data-raw-query.d.ts.map +1 -1
- package/dist/cjs/nodes/data/data-raw-query.js +7 -1
- package/dist/cjs/nodes/data/data-raw-query.js.map +1 -1
- package/dist/cjs/nodes/data/data-select.d.ts.map +1 -1
- package/dist/cjs/nodes/data/data-select.js +6 -1
- package/dist/cjs/nodes/data/data-select.js.map +1 -1
- package/dist/cjs/nodes/data/data-update-item.d.ts.map +1 -1
- package/dist/cjs/nodes/data/data-update-item.js +7 -4
- package/dist/cjs/nodes/data/data-update-item.js.map +1 -1
- package/dist/cjs/nodes/utility/utility-hybrid-search.js +2 -2
- package/dist/cjs/nodes/utility/utility-hybrid-search.js.map +1 -1
- package/dist/cjs/nodes/utility/utility-semantic-search.js +1 -1
- package/dist/cjs/nodes/utility/utility-semantic-search.js.map +1 -1
- package/dist/cjs/nodes/utility/utility-similarity-scoring.d.ts.map +1 -1
- package/dist/cjs/nodes/utility/utility-similarity-scoring.js +49 -3
- package/dist/cjs/nodes/utility/utility-similarity-scoring.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +10 -4
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +27 -15
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/ecommerce/order-details-route.d.ts +18 -0
- package/dist/esm/ecommerce/order-details-route.d.ts.map +1 -0
- package/dist/esm/ecommerce/order-details-route.js +167 -0
- package/dist/esm/ecommerce/order-details-route.js.map +1 -0
- package/dist/esm/ecommerce-customhandler-rewriter.d.ts +1 -1
- package/dist/esm/ecommerce-customhandler-rewriter.d.ts.map +1 -1
- package/dist/esm/ecommerce-customhandler-rewriter.js +20 -4
- package/dist/esm/ecommerce-customhandler-rewriter.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +69 -11
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/nodes/ai/ai-custom-prompt.js +1 -1
- package/dist/esm/nodes/ai/ai-custom-prompt.js.map +1 -1
- package/dist/esm/nodes/ai/ai-detect-language.js +1 -1
- package/dist/esm/nodes/ai/ai-detect-language.js.map +1 -1
- package/dist/esm/nodes/ai/ai-generate-text-embedding.d.ts.map +1 -1
- package/dist/esm/nodes/ai/ai-generate-text-embedding.js +47 -4
- package/dist/esm/nodes/ai/ai-generate-text-embedding.js.map +1 -1
- package/dist/esm/nodes/ai/ai-provider-utils.d.ts +2 -0
- package/dist/esm/nodes/ai/ai-provider-utils.d.ts.map +1 -1
- package/dist/esm/nodes/ai/ai-provider-utils.js +194 -105
- package/dist/esm/nodes/ai/ai-provider-utils.js.map +1 -1
- package/dist/esm/nodes/ai/ai-sentiment-analysis.js +1 -1
- package/dist/esm/nodes/ai/ai-sentiment-analysis.js.map +1 -1
- package/dist/esm/nodes/ai/ai-summarization.js +1 -1
- package/dist/esm/nodes/ai/ai-summarization.js.map +1 -1
- package/dist/esm/nodes/ai/ai-text-classifier.js +1 -1
- package/dist/esm/nodes/ai/ai-text-classifier.js.map +1 -1
- package/dist/esm/nodes/ai/ai-text-transform.js +1 -1
- package/dist/esm/nodes/ai/ai-text-transform.js.map +1 -1
- package/dist/esm/nodes/cart/cart-clear.d.ts.map +1 -1
- package/dist/esm/nodes/cart/cart-clear.js +40 -0
- package/dist/esm/nodes/cart/cart-clear.js.map +1 -1
- package/dist/esm/nodes/cart/cart-get-total.d.ts.map +1 -1
- package/dist/esm/nodes/cart/cart-get-total.js +56 -15
- package/dist/esm/nodes/cart/cart-get-total.js.map +1 -1
- package/dist/esm/nodes/data/data-count.d.ts.map +1 -1
- package/dist/esm/nodes/data/data-count.js +6 -1
- package/dist/esm/nodes/data/data-count.js.map +1 -1
- package/dist/esm/nodes/data/data-create-item.d.ts.map +1 -1
- package/dist/esm/nodes/data/data-create-item.js +45 -9
- package/dist/esm/nodes/data/data-create-item.js.map +1 -1
- package/dist/esm/nodes/data/data-delete-item.d.ts.map +1 -1
- package/dist/esm/nodes/data/data-delete-item.js +7 -4
- package/dist/esm/nodes/data/data-delete-item.js.map +1 -1
- package/dist/esm/nodes/data/data-raw-query.d.ts.map +1 -1
- package/dist/esm/nodes/data/data-raw-query.js +7 -1
- package/dist/esm/nodes/data/data-raw-query.js.map +1 -1
- package/dist/esm/nodes/data/data-select.d.ts.map +1 -1
- package/dist/esm/nodes/data/data-select.js +6 -1
- package/dist/esm/nodes/data/data-select.js.map +1 -1
- package/dist/esm/nodes/data/data-update-item.d.ts.map +1 -1
- package/dist/esm/nodes/data/data-update-item.js +7 -4
- package/dist/esm/nodes/data/data-update-item.js.map +1 -1
- package/dist/esm/nodes/utility/utility-hybrid-search.js +2 -2
- package/dist/esm/nodes/utility/utility-hybrid-search.js.map +1 -1
- package/dist/esm/nodes/utility/utility-semantic-search.js +1 -1
- package/dist/esm/nodes/utility/utility-semantic-search.js.map +1 -1
- package/dist/esm/nodes/utility/utility-similarity-scoring.d.ts.map +1 -1
- package/dist/esm/nodes/utility/utility-similarity-scoring.js +49 -3
- package/dist/esm/nodes/utility/utility-similarity-scoring.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +10 -4
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/package.json +4 -4
- package/src/api-route-generator.ts +27 -15
- package/src/ecommerce/order-details-route.ts +195 -0
- package/src/ecommerce-customhandler-rewriter.ts +24 -4
- package/src/executor-generator.ts +69 -11
- package/src/nodes/ai/ai-custom-prompt.ts +2 -2
- package/src/nodes/ai/ai-detect-language.ts +2 -2
- package/src/nodes/ai/ai-generate-text-embedding.ts +49 -4
- package/src/nodes/ai/ai-provider-utils.ts +216 -106
- package/src/nodes/ai/ai-sentiment-analysis.ts +2 -2
- package/src/nodes/ai/ai-summarization.ts +2 -2
- package/src/nodes/ai/ai-text-classifier.ts +2 -2
- package/src/nodes/ai/ai-text-transform.ts +2 -2
- package/src/nodes/cart/cart-clear.ts +40 -0
- package/src/nodes/cart/cart-get-total.ts +61 -17
- package/src/nodes/data/data-count.ts +6 -1
- package/src/nodes/data/data-create-item.ts +45 -9
- package/src/nodes/data/data-delete-item.ts +6 -0
- package/src/nodes/data/data-raw-query.ts +8 -1
- package/src/nodes/data/data-select.ts +6 -1
- package/src/nodes/data/data-update-item.ts +6 -0
- package/src/nodes/utility/utility-hybrid-search.ts +2 -2
- package/src/nodes/utility/utility-semantic-search.ts +1 -1
- package/src/nodes/utility/utility-similarity-scoring.ts +56 -3
- package/src/workflow-project-plugin.ts +10 -4
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import { generateAIStreamingProviderUtils } from '../src/nodes/ai/ai-provider-utils'
|
|
4
|
+
import { nodeRegistry } from '../src/nodes'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Answers "if a user picks any provider/model in the editor, does the generated
|
|
8
|
+
* app actually call it correctly?"
|
|
9
|
+
*
|
|
10
|
+
* Two independent guarantees are needed:
|
|
11
|
+
* 1. every provider the editor can emit reaches a working branch, in BOTH the
|
|
12
|
+
* streaming and non-streaming paths;
|
|
13
|
+
* 2. every SDK those branches `require()` is declared as a dependency, or the
|
|
14
|
+
* generated project fails at import time rather than at request time.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const EDITOR_PROVIDERS = [
|
|
18
|
+
'openai',
|
|
19
|
+
'anthropic',
|
|
20
|
+
'google',
|
|
21
|
+
'cohere',
|
|
22
|
+
'mistral',
|
|
23
|
+
'meta',
|
|
24
|
+
'perplexity',
|
|
25
|
+
] as const
|
|
26
|
+
|
|
27
|
+
/** SDK a provider's branch loads. Providers absent here go through OpenAI's. */
|
|
28
|
+
const PROVIDER_SDK: Record<string, string> = {
|
|
29
|
+
openai: 'openai',
|
|
30
|
+
anthropic: '@anthropic-ai/sdk',
|
|
31
|
+
google: '@google/generative-ai',
|
|
32
|
+
cohere: 'cohere-ai',
|
|
33
|
+
mistral: '@mistralai/mistralai',
|
|
34
|
+
meta: 'openai',
|
|
35
|
+
perplexity: 'openai',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadUtils() {
|
|
39
|
+
const source = generateAIStreamingProviderUtils()
|
|
40
|
+
// eslint-disable-next-line no-new-func
|
|
41
|
+
return new Function(
|
|
42
|
+
`${source}
|
|
43
|
+
return {
|
|
44
|
+
resolveProvider: __ai_resolveProvider,
|
|
45
|
+
providerBaseURL: __ai_providerBaseURL,
|
|
46
|
+
modelCapabilities: __ai_modelCapabilities,
|
|
47
|
+
};`
|
|
48
|
+
)() as {
|
|
49
|
+
resolveProvider: (config: unknown) => string
|
|
50
|
+
providerBaseURL: (provider: string) => string | undefined
|
|
51
|
+
modelCapabilities: (provider: string, model: string, maxTokens?: number) => any
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Model ids the editor offers, read from the GUI catalogue when it is a sibling checkout. */
|
|
56
|
+
function readEditorCatalogue(): Array<[string, string]> | null {
|
|
57
|
+
const catalogue = path.resolve(
|
|
58
|
+
__dirname,
|
|
59
|
+
'../../../../teleport-gui/apps/gui/app/project-page/features/workflows/constants/ai-providers/catalog.ts'
|
|
60
|
+
)
|
|
61
|
+
if (!fs.existsSync(catalogue)) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
const source = fs.readFileSync(catalogue, 'utf8')
|
|
65
|
+
const models: Array<[string, string]> = []
|
|
66
|
+
const blockRe = /\{\s*\n\s*id: '([^']+)',([\s\S]*?)\n {6}\}/g
|
|
67
|
+
let match = blockRe.exec(source)
|
|
68
|
+
while (match !== null) {
|
|
69
|
+
const provider = /provider: '([^']+)'/.exec(match[2])
|
|
70
|
+
if (provider) {
|
|
71
|
+
models.push([match[1], provider[1]])
|
|
72
|
+
}
|
|
73
|
+
match = blockRe.exec(source)
|
|
74
|
+
}
|
|
75
|
+
return models.length > 0 ? models : null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe('every editor provider reaches a working branch', () => {
|
|
79
|
+
const utils = loadUtils()
|
|
80
|
+
const source = generateAIStreamingProviderUtils()
|
|
81
|
+
const nonStreaming = source.slice(source.indexOf('function __ai_callProvider'))
|
|
82
|
+
const streaming = source.slice(source.indexOf('__ai_callProviderStreaming'))
|
|
83
|
+
|
|
84
|
+
it.each(EDITOR_PROVIDERS)('%s resolves to itself when stored explicitly', (provider) => {
|
|
85
|
+
expect(utils.resolveProvider({ provider, model: 'anything' })).toBe(provider)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it.each(EDITOR_PROVIDERS)('%s is handled in both call paths', (provider) => {
|
|
89
|
+
const hasExplicit = (body: string) => body.includes(`provider === '${provider}'`)
|
|
90
|
+
// A provider without its own branch must be OpenAI-compatible, and then it
|
|
91
|
+
// needs a base URL unless it IS OpenAI — otherwise it would silently post
|
|
92
|
+
// to api.openai.com with someone else's key.
|
|
93
|
+
for (const [label, body] of [
|
|
94
|
+
['non-streaming', nonStreaming],
|
|
95
|
+
['streaming', streaming],
|
|
96
|
+
] as const) {
|
|
97
|
+
if (!hasExplicit(body)) {
|
|
98
|
+
const baseURL = utils.providerBaseURL(provider)
|
|
99
|
+
const reachable = provider === 'openai' || !!baseURL
|
|
100
|
+
if (!reachable) {
|
|
101
|
+
throw new Error(`${provider} has no ${label} branch and no base URL`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('routes the OpenAI-compatible providers to their own hosts', () => {
|
|
108
|
+
expect(utils.providerBaseURL('meta')).toBe('https://api.together.xyz/v1')
|
|
109
|
+
expect(utils.providerBaseURL('perplexity')).toBe('https://api.perplexity.ai')
|
|
110
|
+
expect(utils.providerBaseURL('openai')).toBeUndefined()
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('every provider SDK is declared as a dependency', () => {
|
|
115
|
+
// The provider is a runtime value, so an AI node must declare every SDK any
|
|
116
|
+
// branch could load — not just the one selected when the project was built.
|
|
117
|
+
const aiNodeTypes = Object.keys(nodeRegistry).filter(
|
|
118
|
+
(nodeType) => nodeType.startsWith('ai-') && nodeType !== 'ai-generate-text-embedding'
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
it('covers all AI chat nodes', () => {
|
|
122
|
+
expect(aiNodeTypes.length).toBeGreaterThanOrEqual(6)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it.each(aiNodeTypes)('%s declares every provider SDK', (nodeType) => {
|
|
126
|
+
const declared = nodeRegistry[nodeType].dependencies || {}
|
|
127
|
+
const missing = EDITOR_PROVIDERS.filter(
|
|
128
|
+
(provider) => !Object.keys(declared).includes(PROVIDER_SDK[provider])
|
|
129
|
+
)
|
|
130
|
+
expect(missing).toEqual([])
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('embedding node needs only the OpenAI SDK, since embeddings are OpenAI-only', () => {
|
|
134
|
+
const declared = nodeRegistry['ai-generate-text-embedding'].dependencies || {}
|
|
135
|
+
expect(Object.keys(declared)).toEqual(['openai'])
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('similarity scoring declares the OpenAI SDK for its semantic algorithm', () => {
|
|
139
|
+
const declared = nodeRegistry['utility-similarity-scoring'].dependencies || {}
|
|
140
|
+
expect(Object.keys(declared)).toContain('openai')
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('the editor catalogue routes correctly end to end', () => {
|
|
145
|
+
const catalogue = readEditorCatalogue()
|
|
146
|
+
const utils = loadUtils()
|
|
147
|
+
|
|
148
|
+
it('finds the GUI catalogue', () => {
|
|
149
|
+
// Skips gracefully in a packaged release where the sibling checkout is absent.
|
|
150
|
+
if (!catalogue) {
|
|
151
|
+
// eslint-disable-next-line no-console
|
|
152
|
+
console.warn('skipping catalogue cross-check — teleport-gui not a sibling checkout')
|
|
153
|
+
}
|
|
154
|
+
expect(true).toBe(true)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('routes every offered model to the provider that owns it', () => {
|
|
158
|
+
if (!catalogue) {
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
expect(catalogue.length).toBeGreaterThan(40)
|
|
162
|
+
const mismatches = catalogue.filter(
|
|
163
|
+
([model, provider]) => utils.resolveProvider({ provider, model }) !== provider
|
|
164
|
+
)
|
|
165
|
+
expect(mismatches).toEqual([])
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('never produces an unusable request shape for an offered model', () => {
|
|
169
|
+
if (!catalogue) {
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
// Anthropic rejects a request with no max_tokens at all, so every model
|
|
173
|
+
// must come out with a positive budget whichever branch runs.
|
|
174
|
+
const broken = catalogue.filter(([model, provider]) => {
|
|
175
|
+
const caps = utils.modelCapabilities(provider, model, 500)
|
|
176
|
+
return (
|
|
177
|
+
!(caps.maxTokens > 0) ||
|
|
178
|
+
!['max_tokens', 'max_completion_tokens'].includes(caps.maxTokensParam)
|
|
179
|
+
)
|
|
180
|
+
})
|
|
181
|
+
expect(broken).toEqual([])
|
|
182
|
+
})
|
|
183
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { cartClear } from '../src/nodes/cart/cart-clear'
|
|
2
|
+
|
|
3
|
+
// Regression: after a cash-on-delivery order the buyer's cart came back.
|
|
4
|
+
//
|
|
5
|
+
// `cart-clear` only emptied `localStorage.workflow_cart`. The database copy
|
|
6
|
+
// (`teleport_cart` / `teleport_cart_items`) stayed active, and
|
|
7
|
+
// `EcommerceProvider` re-hydrates from `/api/cart/load` on any mount where the
|
|
8
|
+
// local cart is empty — so the very next full page load (the post-order
|
|
9
|
+
// redirect, or a plain refresh) restored the cart the shopper had just ordered
|
|
10
|
+
// and wrote it straight back into localStorage.
|
|
11
|
+
|
|
12
|
+
describe('cart-clear mirrors the empty cart into the database cart', () => {
|
|
13
|
+
const handlerCode = cartClear.generateHandler()
|
|
14
|
+
|
|
15
|
+
it('still clears localStorage first — the DB mirror is best-effort on top', () => {
|
|
16
|
+
expect(handlerCode).toContain("localStorage.setItem('workflow_cart'")
|
|
17
|
+
expect(handlerCode).toContain("dispatchEvent(new CustomEvent('teleport:cart-changed'))")
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('pushes an empty cart through /api/cart/sync with the guest session id', () => {
|
|
21
|
+
expect(handlerCode).toContain("'/api/cart/sync'")
|
|
22
|
+
expect(handlerCode).toContain("localStorage.getItem('workflow_cart_session_id')")
|
|
23
|
+
expect(handlerCode).toMatch(/items:\s*\[\s*\]/)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('never awaits the sync and never lets it fail the node', () => {
|
|
27
|
+
// The route only exists for Postgres datasources, so a 404 here is a
|
|
28
|
+
// normal outcome — it must not surface to the shopper or abort the
|
|
29
|
+
// workflow (which would strand the order mid-chain).
|
|
30
|
+
const syncSection = handlerCode.slice(handlerCode.indexOf("'/api/cart/sync'"))
|
|
31
|
+
expect(syncSection).not.toMatch(/await\s+fetch/)
|
|
32
|
+
expect(syncSection).toContain('.catch(')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('does NOT mark the cart as ordered — this handler also backs "Clear cart"', () => {
|
|
36
|
+
// `mark-ordered` is the checkout-only signal and stays owned by
|
|
37
|
+
// data-create-item. Flipping the status here would mislabel a cart the
|
|
38
|
+
// shopper simply emptied by hand.
|
|
39
|
+
expect(handlerCode).not.toContain('mark-ordered')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('stamps the clear so the provider can tell it apart from a first visit', () => {
|
|
43
|
+
expect(handlerCode).toContain("localStorage.setItem('workflow_cart_cleared_at'")
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/* tslint:disable:no-eval */
|
|
2
|
+
import { cartGetTotal } from '../src/nodes/cart/cart-get-total'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The merchant's "Invoices → Tax → Added on top" setting decides what a buyer
|
|
6
|
+
* actually pays. `teleport_products.price` is the NET price in that mode, so
|
|
7
|
+
* the storefront has to add the tax at every point a price is shown or charged
|
|
8
|
+
* — and `cart-get-total` is the point the PLACE-ORDER workflow charges from.
|
|
9
|
+
*
|
|
10
|
+
* Three contracts are locked in here, each of which silently breaks money:
|
|
11
|
+
*
|
|
12
|
+
* 1. `subtotal` stays NET. Two generations of baked place-order workflows read
|
|
13
|
+
* it and add shipping to produce `teleport_orders.total_amount`. If a NEW
|
|
14
|
+
* handler made it gross, an OLD baked workflow would write a taxed order
|
|
15
|
+
* total while still emitting untaxed payment line items — and Stripe
|
|
16
|
+
* Checkout totals `line_items` and ignores the top-level amount, so the
|
|
17
|
+
* buyer would be undercharged.
|
|
18
|
+
* 2. `tax` is `gross − net`, never a fresh percentage of the sum, so
|
|
19
|
+
* `net line items + tax` reconciles to `total` to the cent.
|
|
20
|
+
* 3. Rounding is PER UNIT, matching `EcommerceProvider`'s
|
|
21
|
+
* `cartItemDisplayPrice`, so the amount charged equals the Subtotal the
|
|
22
|
+
* cart page printed.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
function evalHandler(code: string): any {
|
|
26
|
+
return eval('(' + code + ')')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface StoredSettings {
|
|
30
|
+
maxQuantityPerProduct?: number | null
|
|
31
|
+
deliveryConfig?: unknown
|
|
32
|
+
taxConfig?: { storefrontTaxRate: number }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function withLocalStorage(cart: unknown, settings: StoredSettings | null): void {
|
|
36
|
+
const store: Record<string, string> = { workflow_cart: JSON.stringify(cart) }
|
|
37
|
+
if (settings) {
|
|
38
|
+
store.workflow_cart_settings = JSON.stringify(settings)
|
|
39
|
+
}
|
|
40
|
+
;(global as any).localStorage = {
|
|
41
|
+
getItem: (key: string) => (key in store ? store[key] : null),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const runHandler = async () => evalHandler(cartGetTotal.generateHandler())()
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
delete (global as any).localStorage
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('cart-get-total — storefront tax', () => {
|
|
52
|
+
it('leaves every total untouched when no tax config is stored (legacy storefront)', async () => {
|
|
53
|
+
withLocalStorage([{ price: 100, quantity: 2 }], { maxQuantityPerProduct: null })
|
|
54
|
+
|
|
55
|
+
const out = await runHandler()
|
|
56
|
+
|
|
57
|
+
expect(out.subtotal).toBe(200)
|
|
58
|
+
expect(out.tax).toBe(0)
|
|
59
|
+
expect(out.total).toBe(200)
|
|
60
|
+
expect(out.itemCount).toBe(2)
|
|
61
|
+
expect(out.taxConfig).toEqual({ storefrontTaxRate: 0 })
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('leaves every total untouched when the rate is zero', async () => {
|
|
65
|
+
withLocalStorage([{ price: 100, quantity: 2 }], { taxConfig: { storefrontTaxRate: 0 } })
|
|
66
|
+
|
|
67
|
+
const out = await runHandler()
|
|
68
|
+
|
|
69
|
+
expect(out.subtotal).toBe(200)
|
|
70
|
+
expect(out.tax).toBe(0)
|
|
71
|
+
expect(out.total).toBe(200)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('keeps subtotal NET and reports the tax separately', async () => {
|
|
75
|
+
withLocalStorage([{ price: 100, quantity: 2 }], { taxConfig: { storefrontTaxRate: 19 } })
|
|
76
|
+
|
|
77
|
+
const out = await runHandler()
|
|
78
|
+
|
|
79
|
+
expect(out.subtotal).toBe(200)
|
|
80
|
+
expect(out.tax).toBe(38)
|
|
81
|
+
expect(out.total).toBe(238)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('rounds per unit, so the charge matches the cart page line by line', async () => {
|
|
85
|
+
// 19.99 net → 23.79 gross per unit → 71.37 for three.
|
|
86
|
+
// Rounding the SUM instead would give 71.36 and disagree with the page.
|
|
87
|
+
withLocalStorage([{ price: 19.99, quantity: 3 }], { taxConfig: { storefrontTaxRate: 19 } })
|
|
88
|
+
|
|
89
|
+
const out = await runHandler()
|
|
90
|
+
|
|
91
|
+
expect(out.subtotal).toBe(59.97)
|
|
92
|
+
expect(out.total).toBe(71.37)
|
|
93
|
+
expect(out.tax).toBe(11.4)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('reconciles: net line items + tax === total, for a mixed basket', async () => {
|
|
97
|
+
withLocalStorage(
|
|
98
|
+
[
|
|
99
|
+
{ price: 19.99, quantity: 3 },
|
|
100
|
+
{ price: 5.5, quantity: 1 },
|
|
101
|
+
{ price: 100, quantity: 2 },
|
|
102
|
+
],
|
|
103
|
+
{ taxConfig: { storefrontTaxRate: 7.5 } }
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const out = await runHandler()
|
|
107
|
+
|
|
108
|
+
// This is the invariant the payment line items depend on: the provider is
|
|
109
|
+
// sent NET item lines plus one "Tax" line, and their sum has to be the
|
|
110
|
+
// order's goods total to the cent.
|
|
111
|
+
expect(out.subtotal + out.tax).toBeCloseTo(out.total, 10)
|
|
112
|
+
// net 59.97 + 5.50 + 200.00
|
|
113
|
+
expect(out.subtotal).toBe(265.47)
|
|
114
|
+
// per-unit gross 21.49×3 + 5.91 + 107.50×2
|
|
115
|
+
expect(out.total).toBe(285.38)
|
|
116
|
+
expect(out.tax).toBe(19.91)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('still surfaces the delivery config alongside the tax config', async () => {
|
|
120
|
+
withLocalStorage([{ price: 10, quantity: 1 }], {
|
|
121
|
+
deliveryConfig: { deliveryPrice: 15, freeDeliveryEnabled: true, freeDeliveryThreshold: 50 },
|
|
122
|
+
taxConfig: { storefrontTaxRate: 19 },
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const out = await runHandler()
|
|
126
|
+
|
|
127
|
+
expect(out.deliveryConfig).toEqual({
|
|
128
|
+
deliveryPrice: 15,
|
|
129
|
+
freeDeliveryEnabled: true,
|
|
130
|
+
freeDeliveryThreshold: 50,
|
|
131
|
+
})
|
|
132
|
+
expect(out.taxConfig).toEqual({ storefrontTaxRate: 19 })
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('ignores a malformed or negative rate instead of corrupting the charge', async () => {
|
|
136
|
+
withLocalStorage([{ price: 100, quantity: 1 }], {
|
|
137
|
+
taxConfig: { storefrontTaxRate: -5 },
|
|
138
|
+
})
|
|
139
|
+
expect((await runHandler()).total).toBe(100)
|
|
140
|
+
|
|
141
|
+
withLocalStorage([{ price: 100, quantity: 1 }], {
|
|
142
|
+
taxConfig: { storefrontTaxRate: 'nonsense' as unknown as number },
|
|
143
|
+
})
|
|
144
|
+
expect((await runHandler()).total).toBe(100)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('returns a fully zeroed shape when localStorage is unreadable', async () => {
|
|
148
|
+
;(global as any).localStorage = {
|
|
149
|
+
getItem: () => {
|
|
150
|
+
throw new Error('denied')
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const out = await runHandler()
|
|
155
|
+
|
|
156
|
+
expect(out).toEqual({
|
|
157
|
+
subtotal: 0,
|
|
158
|
+
tax: 0,
|
|
159
|
+
total: 0,
|
|
160
|
+
itemCount: 0,
|
|
161
|
+
deliveryConfig: null,
|
|
162
|
+
taxConfig: { storefrontTaxRate: 0 },
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
})
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// into its component body — the cron route previously used a naive inline loop
|
|
5
5
|
// that did neither, so a per-cart reminder email would ship raw {{token}} merge
|
|
6
6
|
// fields and its loop body would never run.
|
|
7
|
+
import { generateSharedRuntimeUtilsCode } from '../src/executor-generator'
|
|
7
8
|
import { generateCronAPIRoute } from '../src/api-route-generator'
|
|
8
9
|
|
|
9
10
|
const TRIGGER_ID = 'cron-1'
|
|
@@ -123,12 +124,20 @@ describe('generateCronAPIRoute — loop + templateParams support', () => {
|
|
|
123
124
|
})
|
|
124
125
|
|
|
125
126
|
it('merges templateParams into email body + subject', () => {
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
// The merge lives in the SHARED runtime's `resolveConfig`, not pasted at
|
|
128
|
+
// each call site. That is the whole point: `resolveConfig` is the single
|
|
129
|
+
// function every execution path — main loop, loop body, parallel branch,
|
|
130
|
+
// error branch, across all four route generators and the client executor —
|
|
131
|
+
// funnels a node config through. Pasting it per site is what left the main
|
|
132
|
+
// loop of `generateServerSegmentAPIRoute` (where every ordinary page
|
|
133
|
+
// workflow's email node runs) without it, shipping literal `{{token}}`s.
|
|
134
|
+
const runtime = generateSharedRuntimeUtilsCode()
|
|
135
|
+
expect(runtime).toContain('applyTemplateParams(resolved.body, resolved.templateParams)')
|
|
136
|
+
expect(runtime).toContain('applyTemplateParams(resolved.subject, resolved.templateParams)')
|
|
137
|
+
// …and the route reaches it simply by calling resolveConfig.
|
|
138
|
+
expect(route).toContain('const resolveConfig = utils.resolveConfig')
|
|
139
|
+
// No stale per-site copy left behind to drift out of sync.
|
|
140
|
+
expect(route).not.toContain('utils.applyTemplateParams(')
|
|
132
141
|
})
|
|
133
142
|
|
|
134
143
|
it('bakes the scan + loop + email nodes into the server WORKFLOW_CONFIG', () => {
|
|
@@ -106,3 +106,36 @@ describe('data_create_item — order-notification piggyback payload', () => {
|
|
|
106
106
|
// cover the contract (which fields are read, where they default,
|
|
107
107
|
// fire-and-forget shape, etc.). The full integration is exercised
|
|
108
108
|
// by the existing end-to-end test in the dist project.
|
|
109
|
+
|
|
110
|
+
describe('data-create-item stands down when the workflow owns the send', () => {
|
|
111
|
+
const suppressionHandlerCode = dataCreateItem.generateHandler()
|
|
112
|
+
|
|
113
|
+
it('gates the auto-fire on the builder-set suppressOrderNotification flag', () => {
|
|
114
|
+
// The place-order workflow sets this on its create-order node whenever it
|
|
115
|
+
// also carries a "Send Order-Notification Email" node. Without the gate
|
|
116
|
+
// the merchant receives the SAME order twice — once from here with a NULL
|
|
117
|
+
// order_number (rendered as the raw UUID) and a flat-filled body, once
|
|
118
|
+
// from the workflow with the real order number and the fully expanded
|
|
119
|
+
// template.
|
|
120
|
+
expect(suppressionHandlerCode).toContain('suppressOrderNotification')
|
|
121
|
+
expect(suppressionHandlerCode).toMatch(/suppressOrderNotification\s*!==\s*true/)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('keeps firing when the flag is absent, so exported projects are unaffected', () => {
|
|
125
|
+
// `!== true` (not a truthiness check) is what makes "flag missing" mean
|
|
126
|
+
// "behave exactly as before".
|
|
127
|
+
expect(suppressionHandlerCode).not.toMatch(/config\.suppressOrderNotification\s*\)/)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('sends both the {{itemsList}} and the array-mapper row keys per item', () => {
|
|
131
|
+
// One payload feeds the endpoint's own <ul> builder AND a builder email
|
|
132
|
+
// template's `tq:each` row block, so neither renderer needs to know which
|
|
133
|
+
// template style the merchant is on.
|
|
134
|
+
for (const key of ['product_name', 'unit_price', 'line_total', 'image_url']) {
|
|
135
|
+
expect(suppressionHandlerCode).toContain(key)
|
|
136
|
+
}
|
|
137
|
+
expect(suppressionHandlerCode).toMatch(
|
|
138
|
+
/it\.image \|\| it\.image_url \|\| it\.imageUrl \|\| it\.thumbnail/
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -373,7 +373,7 @@ describe('buildPaymentMetadataBuilder — runtime semantics', () => {
|
|
|
373
373
|
// Build a synthetic params array that mirrors the workflow context:
|
|
374
374
|
// a mix of unrelated nodes plus an order-create-item-shaped result.
|
|
375
375
|
const fn = (() => {
|
|
376
|
-
const code = buildPaymentMetadataBuilder()
|
|
376
|
+
const code = buildPaymentMetadataBuilder('/order-details')
|
|
377
377
|
return new Function(code + '\nreturn customHandler;')() as any
|
|
378
378
|
})()
|
|
379
379
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { generateSharedRuntimeUtilsCode } from '../src/executor-generator'
|
|
2
|
+
import {
|
|
3
|
+
generateServerSegmentAPIRoute,
|
|
4
|
+
generateStreamingServerSegmentAPIRoute,
|
|
5
|
+
} from '../src/api-route-generator'
|
|
6
|
+
import { nodeRegistry } from '../src/nodes'
|
|
7
|
+
import type { WorkflowSegment } from '../src/types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A generated app's data nodes reach the database by having the server call its
|
|
11
|
+
* OWN `/api/data/<id>/<op>` route over HTTP. On a deployment behind Vercel
|
|
12
|
+
* Deployment Protection that self-call is answered with 401 "Protected
|
|
13
|
+
* deployment" — the page still renders (the visitor's browser holds the bypass
|
|
14
|
+
* cookie) but every data node returns `{ rows: [] }`, so the app looks alive
|
|
15
|
+
* while nothing reads or writes.
|
|
16
|
+
*
|
|
17
|
+
* Measured on a published AI-chat store: all four RAG queries came back empty
|
|
18
|
+
* with `error: { message: 'Protected deployment', code: '401' }`, and the
|
|
19
|
+
* assistant answered "I don't have enough information in my knowledge base" to
|
|
20
|
+
* every question — with a fully populated knowledge base sitting in the
|
|
21
|
+
* database.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
function loadRuntime() {
|
|
25
|
+
const source = generateSharedRuntimeUtilsCode()
|
|
26
|
+
const fn = source.match(/function internalRequestHeaders[\s\S]*?\n\}/)
|
|
27
|
+
if (!fn) {
|
|
28
|
+
throw new Error('internalRequestHeaders not found in the generated runtime')
|
|
29
|
+
}
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
31
|
+
return {
|
|
32
|
+
internalRequestHeaders: new Function(`${fn[0]}\nreturn internalRequestHeaders;`)() as (
|
|
33
|
+
req: unknown
|
|
34
|
+
) => Record<string, string>,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('internalRequestHeaders', () => {
|
|
39
|
+
const utils = loadRuntime()
|
|
40
|
+
const originalBypass = process.env.VERCEL_AUTOMATION_BYPASS_SECRET
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
if (originalBypass === undefined) {
|
|
44
|
+
delete process.env.VERCEL_AUTOMATION_BYPASS_SECRET
|
|
45
|
+
} else {
|
|
46
|
+
process.env.VERCEL_AUTOMATION_BYPASS_SECRET = originalBypass
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('forwards the caller cookie that already passed the protection', () => {
|
|
51
|
+
delete process.env.VERCEL_AUTOMATION_BYPASS_SECRET
|
|
52
|
+
const headers = utils.internalRequestHeaders({
|
|
53
|
+
headers: { cookie: '_vercel_jwt=abc; next-auth.session-token=xyz' },
|
|
54
|
+
})
|
|
55
|
+
expect(headers.cookie).toBe('_vercel_jwt=abc; next-auth.session-token=xyz')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('sends the automation bypass secret when the project configured one', () => {
|
|
59
|
+
// The only credential available when no browser sits behind the request.
|
|
60
|
+
process.env.VERCEL_AUTOMATION_BYPASS_SECRET = 'sekret'
|
|
61
|
+
const headers = utils.internalRequestHeaders({ headers: {} })
|
|
62
|
+
expect(headers['x-vercel-protection-bypass']).toBe('sekret')
|
|
63
|
+
expect(headers['x-vercel-set-bypass-cookie']).toBe('false')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('returns an empty object rather than throwing on a request with no headers', () => {
|
|
67
|
+
delete process.env.VERCEL_AUTOMATION_BYPASS_SECRET
|
|
68
|
+
expect(utils.internalRequestHeaders(undefined)).toEqual({})
|
|
69
|
+
expect(utils.internalRequestHeaders({})).toEqual({})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('invents no cookie when the caller sent none', () => {
|
|
73
|
+
delete process.env.VERCEL_AUTOMATION_BYPASS_SECRET
|
|
74
|
+
expect(utils.internalRequestHeaders({ headers: {} }).cookie).toBeUndefined()
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('every data node forwards the internal headers on its self-call', () => {
|
|
79
|
+
const DATA_NODES = [
|
|
80
|
+
'data-raw-query',
|
|
81
|
+
'data-select',
|
|
82
|
+
'data-count',
|
|
83
|
+
'data-create-item',
|
|
84
|
+
'data-update-item',
|
|
85
|
+
'data-delete-item',
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
it.each(DATA_NODES)('%s reads context.__internalHeaders', (nodeType) => {
|
|
89
|
+
const source = nodeRegistry[nodeType].generateHandler()
|
|
90
|
+
expect(source).toContain('context.__internalHeaders')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it.each(DATA_NODES)('%s merges them into every self-call it makes', (nodeType) => {
|
|
94
|
+
const source = nodeRegistry[nodeType].generateHandler()
|
|
95
|
+
// A bare `headers: { 'Content-Type': … }` on a call to this deployment's own
|
|
96
|
+
// API is the shape that 401s; every one must be an Object.assign that folds
|
|
97
|
+
// the internal headers in.
|
|
98
|
+
const selfCalls = source.split('fetch(baseUrl').slice(1)
|
|
99
|
+
expect(selfCalls.length).toBeGreaterThan(0)
|
|
100
|
+
for (const call of selfCalls) {
|
|
101
|
+
const headersAt = call.indexOf('headers:')
|
|
102
|
+
expect(headersAt).toBeGreaterThan(-1)
|
|
103
|
+
expect(call.slice(headersAt, headersAt + 60)).toContain('Object.assign')
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('the segment routes supply the headers but never echo them back', () => {
|
|
109
|
+
const segment: WorkflowSegment = {
|
|
110
|
+
id: 'seg-server-1',
|
|
111
|
+
workflowId: 'wf-1',
|
|
112
|
+
executionEnv: 'server',
|
|
113
|
+
nodes: [
|
|
114
|
+
{
|
|
115
|
+
id: 'n1',
|
|
116
|
+
type: 'data-raw-query',
|
|
117
|
+
config: { dataSourceId: 'ds-1', query: 'SELECT 1' },
|
|
118
|
+
stepNumber: 1,
|
|
119
|
+
label: 'Probe',
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
edges: [],
|
|
123
|
+
} as unknown as WorkflowSegment
|
|
124
|
+
|
|
125
|
+
const routes = [
|
|
126
|
+
['non-streaming', generateServerSegmentAPIRoute(segment)],
|
|
127
|
+
['streaming', generateStreamingServerSegmentAPIRoute(segment)],
|
|
128
|
+
] as const
|
|
129
|
+
|
|
130
|
+
it.each(routes)('%s route puts the headers on the execution context', (_label, route) => {
|
|
131
|
+
expect(route).toContain('context.__internalHeaders = utils.internalRequestHeaders(req)')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it.each(routes)('%s route deletes them before serializing the context', (_label, route) => {
|
|
135
|
+
// ⛔ `results` in the response IS this context. The forwarded cookie carries
|
|
136
|
+
// the visitor's session token; writing it into a JS-readable response body
|
|
137
|
+
// would undo the httpOnly flag it was set with.
|
|
138
|
+
const deleteAt = route.indexOf('delete context.__internalHeaders')
|
|
139
|
+
expect(deleteAt).toBeGreaterThan(-1)
|
|
140
|
+
const firstEcho = route.indexOf('results: context')
|
|
141
|
+
expect(firstEcho).toBeGreaterThan(-1)
|
|
142
|
+
expect(deleteAt).toBeLessThan(firstEcho)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it.each(routes)('%s route echoes the context exactly once per exit path', (_label, route) => {
|
|
146
|
+
// Guards the assertion above: a second `results: context` added later would
|
|
147
|
+
// sit outside the delete's reach unless it is also after it.
|
|
148
|
+
const positions: number[] = []
|
|
149
|
+
let idx = route.indexOf('results: context')
|
|
150
|
+
while (idx !== -1) {
|
|
151
|
+
positions.push(idx)
|
|
152
|
+
idx = route.indexOf('results: context', idx + 1)
|
|
153
|
+
}
|
|
154
|
+
const deleteAt = route.indexOf('delete context.__internalHeaders')
|
|
155
|
+
for (const position of positions) {
|
|
156
|
+
expect(position).toBeGreaterThan(deleteAt)
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
})
|