Skip to content

Commit 31bb9d5

Browse files
committed
feat: create core helpers to create botContext and reuse logic in all plugins
1 parent f0b0a2e commit 31bb9d5

7 files changed

Lines changed: 405 additions & 226 deletions

File tree

packages/botonic-core/package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@
55
"description": "Build Chatbots using React",
66
"main": "./lib/cjs/index.js",
77
"module": "./lib/esm/index.js",
8+
"exports": {
9+
".": {
10+
"require": "./lib/cjs/index.js",
11+
"import": "./lib/esm/index.js"
12+
},
13+
"./testing": {
14+
"require": "./lib/cjs/testing/index.js",
15+
"import": "./lib/esm/testing/index.js"
16+
}
17+
},
818
"scripts": {
919
"test": "../../node_modules/.bin/jest --coverage",
1020
"prepublishOnly": "rm -rf lib && npm i && npm run build",
@@ -29,6 +39,11 @@
2939
"index.d.ts",
3040
"README.md"
3141
],
42+
"typesVersions": {
43+
"*": {
44+
"testing": ["./lib/cjs/testing/index.d.ts"]
45+
}
46+
},
3247
"dependencies": {
3348
"@babel/plugin-transform-runtime": "^7.25.9",
3449
"axios": "^1.13.6",
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
import {
2+
type BotContext,
3+
type BotRequest,
4+
type BotSecrets,
5+
type BotSettings,
6+
type ContactInfo,
7+
INPUT,
8+
type Input,
9+
PROVIDER,
10+
type ProviderType,
11+
type ResolvedPlugins,
12+
type Session,
13+
type SessionUser,
14+
} from '../models'
15+
16+
// ---------------------------------------------------------------------------
17+
// Default values
18+
// ---------------------------------------------------------------------------
19+
20+
export const TEST_DEFAULTS = {
21+
HUBTYPE_API_URL: 'https://api.hubtype.com',
22+
STATIC_URL: 'https://static.hubtype.com',
23+
LITELLM_API_URL: 'https://api.litellm.com',
24+
AZURE_OPENAI_API_BASE: 'https://api.openai.com',
25+
AZURE_OPENAI_API_VERSION: '2026-02-01',
26+
LANGUAGE_DETECTION_ENABLED: 'true',
27+
HUBTYPE_ACCESS_TOKEN: 'testAccessToken',
28+
LITELLM_API_KEY: 'testLiteLLMAPIKey',
29+
AZURE_OPENAI_API_KEY: 'testAzureOpenAIAPIKey',
30+
ACCESS_TOKEN: 'fake_access_token',
31+
HUBTYPE_API_HOST: 'https://api.hubtype.com',
32+
FLOW_THREAD_ID: 'testFlowThreadId',
33+
BOT_ID: 'testBotId',
34+
USER_ID: 'testUserId',
35+
ORG_ID: 'testOrgId',
36+
ORG_NAME: 'testOrg',
37+
PROVIDER: PROVIDER.WEBCHAT as ProviderType,
38+
LOCALE: 'en',
39+
COUNTRY: 'US',
40+
BOT_INTERACTION_ID: 'testInteractionId',
41+
MESSAGE_ID: 'testMessageId',
42+
LAST_ROUTE_PATH: '',
43+
}
44+
45+
// ---------------------------------------------------------------------------
46+
// Leaf factories
47+
// ---------------------------------------------------------------------------
48+
49+
export function createTestSettings(
50+
overrides?: Partial<BotSettings>
51+
): BotSettings {
52+
return {
53+
HUBTYPE_API_URL: TEST_DEFAULTS.HUBTYPE_API_URL,
54+
STATIC_URL: TEST_DEFAULTS.STATIC_URL,
55+
LITELLM_API_URL: TEST_DEFAULTS.LITELLM_API_URL,
56+
AZURE_OPENAI_API_BASE: TEST_DEFAULTS.AZURE_OPENAI_API_BASE,
57+
AZURE_OPENAI_API_VERSION: TEST_DEFAULTS.AZURE_OPENAI_API_VERSION,
58+
LANGUAGE_DETECTION_ENABLED: TEST_DEFAULTS.LANGUAGE_DETECTION_ENABLED,
59+
CUSTOM_SHORT_URL_HOST: null,
60+
custom: {},
61+
...overrides,
62+
}
63+
}
64+
65+
export function createTestSecrets(overrides?: Partial<BotSecrets>): BotSecrets {
66+
return {
67+
HUBTYPE_ACCESS_TOKEN: TEST_DEFAULTS.HUBTYPE_ACCESS_TOKEN,
68+
LITELLM_API_KEY: TEST_DEFAULTS.LITELLM_API_KEY,
69+
AZURE_OPENAI_API_KEY: TEST_DEFAULTS.AZURE_OPENAI_API_KEY,
70+
custom: {},
71+
...overrides,
72+
}
73+
}
74+
75+
export interface TestUserOptions {
76+
id?: string
77+
provider?: ProviderType
78+
locale?: string
79+
country?: string
80+
systemLocale?: string
81+
contactInfo?: ContactInfo[]
82+
extraData?: Record<string, any>
83+
}
84+
85+
export function createTestSessionUser(
86+
options?: TestUserOptions
87+
): SessionUser<Record<string, any>> {
88+
const locale = options?.locale ?? TEST_DEFAULTS.LOCALE
89+
const country = options?.country ?? TEST_DEFAULTS.COUNTRY
90+
return {
91+
id: options?.id ?? TEST_DEFAULTS.USER_ID,
92+
provider: options?.provider ?? TEST_DEFAULTS.PROVIDER,
93+
locale,
94+
country,
95+
system_locale: options?.systemLocale ?? locale,
96+
contact_info: options?.contactInfo ?? [],
97+
extra_data: options?.extraData ?? {},
98+
}
99+
}
100+
101+
export interface TestSessionOptions {
102+
user?: TestUserOptions
103+
botId?: string
104+
organization?: string
105+
organizationId?: string
106+
isFirstInteraction?: boolean
107+
isTestIntegration?: boolean
108+
accessToken?: string
109+
hubtypeApi?: string
110+
flowThreadId?: string
111+
retries?: number
112+
shadowing?: boolean
113+
hubtypeCaseId?: string
114+
captureUserInputNodeId?: string
115+
}
116+
117+
export function createTestSession(
118+
options?: TestSessionOptions
119+
): Session<Record<string, any>> {
120+
return {
121+
bot: { id: options?.botId ?? TEST_DEFAULTS.BOT_ID },
122+
user: createTestSessionUser(options?.user),
123+
organization: options?.organization ?? TEST_DEFAULTS.ORG_NAME,
124+
organization_id: options?.organizationId ?? TEST_DEFAULTS.ORG_ID,
125+
is_first_interaction: options?.isFirstInteraction ?? false,
126+
is_test_integration: options?.isTestIntegration ?? false,
127+
_access_token: options?.accessToken ?? TEST_DEFAULTS.ACCESS_TOKEN,
128+
_hubtype_api: options?.hubtypeApi ?? TEST_DEFAULTS.HUBTYPE_API_HOST,
129+
flow_thread_id: options?.flowThreadId ?? TEST_DEFAULTS.FLOW_THREAD_ID,
130+
__retries: options?.retries ?? 0,
131+
_shadowing: options?.shadowing,
132+
_hubtype_case_id: options?.hubtypeCaseId,
133+
capture_user_input: options?.captureUserInputNodeId
134+
? { node_id: options.captureUserInputNodeId }
135+
: undefined,
136+
}
137+
}
138+
139+
export interface TestInputOptions {
140+
type?: Input['type']
141+
data?: string
142+
text?: string
143+
payload?: string
144+
src?: string
145+
botInteractionId?: string
146+
messageId?: string
147+
context?: Input['context']
148+
transcript?: string
149+
}
150+
151+
export function createTestInput(options?: TestInputOptions): Input {
152+
return {
153+
type: options?.type ?? INPUT.TEXT,
154+
data: options?.data,
155+
text: options?.text,
156+
payload: options?.payload,
157+
src: options?.src,
158+
transcript: options?.transcript,
159+
context: options?.context,
160+
bot_interaction_id:
161+
options?.botInteractionId ?? TEST_DEFAULTS.BOT_INTERACTION_ID,
162+
message_id: options?.messageId ?? TEST_DEFAULTS.MESSAGE_ID,
163+
}
164+
}
165+
166+
// ---------------------------------------------------------------------------
167+
// Composite factories
168+
// ---------------------------------------------------------------------------
169+
170+
export interface TestBotRequestOptions {
171+
input?: TestInputOptions | Input
172+
session?: TestSessionOptions | Session
173+
lastRoutePath?: string
174+
settings?: Partial<BotSettings> | BotSettings
175+
secrets?: Partial<BotSecrets> | BotSecrets
176+
}
177+
178+
function isFullInput(v: TestInputOptions | Input): v is Input {
179+
return 'bot_interaction_id' in v && 'message_id' in v
180+
}
181+
182+
function isFullSession(v: TestSessionOptions | Session): v is Session {
183+
return 'bot' in v && '__retries' in v
184+
}
185+
186+
function isFullSettings(
187+
v: Partial<BotSettings> | BotSettings
188+
): v is BotSettings {
189+
return (
190+
'HUBTYPE_API_URL' in v &&
191+
'STATIC_URL' in v &&
192+
'LITELLM_API_URL' in v &&
193+
'AZURE_OPENAI_API_BASE' in v &&
194+
'AZURE_OPENAI_API_VERSION' in v &&
195+
'LANGUAGE_DETECTION_ENABLED' in v &&
196+
'CUSTOM_SHORT_URL_HOST' in v &&
197+
'custom' in v
198+
)
199+
}
200+
201+
function isFullSecrets(
202+
v: Partial<BotSecrets> | BotSecrets
203+
): v is BotSecrets {
204+
return (
205+
'HUBTYPE_ACCESS_TOKEN' in v &&
206+
'LITELLM_API_KEY' in v &&
207+
'AZURE_OPENAI_API_KEY' in v &&
208+
'custom' in v
209+
)
210+
}
211+
212+
export function createTestBotRequest(
213+
options?: TestBotRequestOptions
214+
): BotRequest {
215+
const input =
216+
options?.input == null
217+
? createTestInput()
218+
: isFullInput(options.input)
219+
? options.input
220+
: createTestInput(options.input)
221+
222+
const session =
223+
options?.session == null
224+
? createTestSession()
225+
: isFullSession(options.session)
226+
? options.session
227+
: createTestSession(options.session)
228+
229+
const settings =
230+
options?.settings == null
231+
? createTestSettings()
232+
: isFullSettings(options.settings)
233+
? options.settings
234+
: createTestSettings(options.settings)
235+
236+
const secrets =
237+
options?.secrets == null
238+
? createTestSecrets()
239+
: isFullSecrets(options.secrets)
240+
? options.secrets
241+
: createTestSecrets(options.secrets)
242+
243+
return {
244+
input,
245+
session,
246+
lastRoutePath: options?.lastRoutePath ?? TEST_DEFAULTS.LAST_ROUTE_PATH,
247+
settings,
248+
secrets,
249+
}
250+
}
251+
252+
export interface TestBotContextOptions<
253+
TPlugins extends ResolvedPlugins = ResolvedPlugins,
254+
> extends TestBotRequestOptions {
255+
plugins?: TPlugins
256+
params?: Record<string, string>
257+
defaultDelay?: number
258+
defaultTyping?: number
259+
}
260+
261+
export function createTestBotContext<
262+
TPlugins extends ResolvedPlugins = ResolvedPlugins,
263+
>(
264+
options?: TestBotContextOptions<TPlugins>
265+
): BotContext<TPlugins> {
266+
const request = createTestBotRequest(options)
267+
268+
const sessionForLocale =
269+
options?.session == null
270+
? createTestSession()
271+
: isFullSession(options.session)
272+
? options.session
273+
: createTestSession(options.session)
274+
275+
const locale = sessionForLocale.user.locale
276+
const country = sessionForLocale.user.country
277+
const systemLocale = sessionForLocale.user.system_locale
278+
279+
return {
280+
...request,
281+
plugins: options?.plugins ?? ({} as TPlugins),
282+
params: options?.params ?? {},
283+
defaultDelay: options?.defaultDelay ?? 0,
284+
defaultTyping: options?.defaultTyping ?? 0,
285+
getUserLocale: () => locale,
286+
getUserCountry: () => country,
287+
getSystemLocale: () => systemLocale,
288+
setUserLocale: () => undefined,
289+
setUserCountry: () => undefined,
290+
setSystemLocale: () => undefined,
291+
}
292+
}
293+
294+
/**
295+
* Alias for `createTestBotContext` typed as `PluginPreRequest`.
296+
* PluginPreRequest === BotContext in @botonic/core.
297+
*/
298+
export const createTestPluginPreRequest = createTestBotContext
299+
300+
export interface TestPluginPostRequestOptions<
301+
TPlugins extends ResolvedPlugins = ResolvedPlugins,
302+
> extends TestBotContextOptions<TPlugins> {
303+
response?: string | null
304+
}
305+
306+
export function createTestPluginPostRequest<
307+
TPlugins extends ResolvedPlugins = ResolvedPlugins,
308+
>(
309+
options?: TestPluginPostRequestOptions<TPlugins>
310+
): BotContext<TPlugins> & { response: string | null } {
311+
return {
312+
...createTestBotContext(options),
313+
response: options?.response ?? null,
314+
}
315+
}

0 commit comments

Comments
 (0)