@nextsparkjs/theme-default 0.1.0-beta.2 → 0.1.0-beta.21
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/package.json +8 -4
- package/templates/(public)/page.tsx +1 -1
- package/tests/cypress/e2e/_devtools/access.bdd.md +262 -0
- package/tests/cypress/e2e/_devtools/access.cy.ts +171 -0
- package/tests/cypress/e2e/_devtools/navigation.bdd.md +261 -0
- package/tests/cypress/e2e/_devtools/navigation.cy.ts +157 -0
- package/tests/cypress/e2e/_devtools/pages.bdd.md +303 -0
- package/tests/cypress/e2e/_devtools/pages.cy.ts +184 -0
- package/tests/cypress/e2e/_docs/README.md +215 -0
- package/tests/cypress/e2e/_docs/tutorials/sector7-superadmin-teams.narration.json +155 -0
- package/tests/cypress/e2e/_docs/tutorials/sector7-superadmin.cy.ts +390 -0
- package/tests/cypress/e2e/_docs/tutorials/teams-system.doc.cy.ts +349 -0
- package/tests/cypress/e2e/_docs/tutorials/teams-system.narration.json +165 -0
- package/tests/cypress/e2e/_selectors/auth.cy.ts +306 -0
- package/tests/cypress/e2e/_selectors/billing.cy.ts +89 -0
- package/tests/cypress/e2e/_selectors/dashboard-mobile.cy.ts +113 -0
- package/tests/cypress/e2e/_selectors/dashboard-navigation.cy.ts +89 -0
- package/tests/cypress/e2e/_selectors/dashboard-sidebar.cy.ts +60 -0
- package/tests/cypress/e2e/_selectors/dashboard-topnav.cy.ts +146 -0
- package/tests/cypress/e2e/_selectors/devtools.cy.ts +210 -0
- package/tests/cypress/e2e/_selectors/global-search.cy.ts +88 -0
- package/tests/cypress/e2e/_selectors/pages-editor.cy.ts +179 -0
- package/tests/cypress/e2e/_selectors/posts-editor.cy.ts +282 -0
- package/tests/cypress/e2e/_selectors/public.cy.ts +112 -0
- package/tests/cypress/e2e/_selectors/settings-api-keys.cy.ts +228 -0
- package/tests/cypress/e2e/_selectors/settings-billing.cy.ts +105 -0
- package/tests/cypress/e2e/_selectors/settings-layout.cy.ts +119 -0
- package/tests/cypress/e2e/_selectors/settings-password.cy.ts +71 -0
- package/tests/cypress/e2e/_selectors/settings-profile.cy.ts +82 -0
- package/tests/cypress/e2e/_selectors/settings-teams.cy.ts +68 -0
- package/tests/cypress/e2e/_selectors/superadmin.cy.ts +185 -0
- package/tests/cypress/e2e/_selectors/tasks.cy.ts +242 -0
- package/tests/cypress/e2e/_selectors/taxonomies.cy.ts +126 -0
- package/tests/cypress/e2e/_selectors/teams.cy.ts +142 -0
- package/tests/cypress/e2e/_superadmin/all-teams.bdd.md +261 -0
- package/tests/cypress/e2e/_superadmin/all-teams.cy.ts +177 -0
- package/tests/cypress/e2e/_superadmin/all-users.bdd.md +406 -0
- package/tests/cypress/e2e/_superadmin/all-users.cy.ts +294 -0
- package/tests/cypress/e2e/_superadmin/dashboard.bdd.md +235 -0
- package/tests/cypress/e2e/_superadmin/dashboard.cy.ts +149 -0
- package/tests/cypress/e2e/_superadmin/subscriptions-overview.bdd.md +290 -0
- package/tests/cypress/e2e/_superadmin/subscriptions-overview.cy.ts +194 -0
- package/tests/cypress/e2e/ai/ai-usage.cy.ts +209 -0
- package/tests/cypress/e2e/ai/chat-api.cy.ts +107 -0
- package/tests/cypress/e2e/ai/guardrails.cy.ts +332 -0
- package/tests/cypress/e2e/api/billing/BillingAPIController.js +319 -0
- package/tests/cypress/e2e/api/billing/check-action.cy.ts +326 -0
- package/tests/cypress/e2e/api/billing/checkout.cy.ts +358 -0
- package/tests/cypress/e2e/api/billing/lifecycle.cy.ts +423 -0
- package/tests/cypress/e2e/api/billing/plans/README.md +345 -0
- package/tests/cypress/e2e/api/billing/plans/business.cy.ts +412 -0
- package/tests/cypress/e2e/api/billing/plans/downgrade.cy.ts +510 -0
- package/tests/cypress/e2e/api/billing/plans/fixtures/billing-plans.json +163 -0
- package/tests/cypress/e2e/api/billing/plans/free.cy.ts +500 -0
- package/tests/cypress/e2e/api/billing/plans/pro.cy.ts +497 -0
- package/tests/cypress/e2e/api/billing/plans/starter.cy.ts +342 -0
- package/tests/cypress/e2e/api/billing/portal.cy.ts +313 -0
- package/tests/cypress/e2e/api/devtools/registries.bdd.md +300 -0
- package/tests/cypress/e2e/api/devtools/registries.cy.ts +368 -0
- package/tests/cypress/e2e/api/entities/blocks-scope.cy.ts +396 -0
- package/tests/cypress/e2e/api/entities/customers-crud.cy.ts +648 -0
- package/tests/cypress/e2e/api/entities/customers-metas.cy.ts +839 -0
- package/tests/cypress/e2e/api/entities/pages-crud.cy.ts +425 -0
- package/tests/cypress/e2e/api/entities/pages-status.cy.ts +335 -0
- package/tests/cypress/e2e/api/entities/post-categories-crud.cy.ts +610 -0
- package/tests/cypress/e2e/api/entities/posts-crud.cy.ts +709 -0
- package/tests/cypress/e2e/api/entities/posts-status.cy.ts +396 -0
- package/tests/cypress/e2e/api/entities/tasks-crud.cy.ts +602 -0
- package/tests/cypress/e2e/api/entities/tasks-metas.cy.ts +878 -0
- package/tests/cypress/e2e/api/entities/users-crud.cy.ts +469 -0
- package/tests/cypress/e2e/api/entities/users-metas.cy.ts +913 -0
- package/tests/cypress/e2e/api/entities/users-security.cy.ts +375 -0
- package/tests/cypress/e2e/api/scheduled-actions/cron-endpoint.bdd.md +375 -0
- package/tests/cypress/e2e/api/scheduled-actions/cron-endpoint.cy.ts +346 -0
- package/tests/cypress/e2e/api/scheduled-actions/devtools-endpoint.bdd.md +451 -0
- package/tests/cypress/e2e/api/scheduled-actions/devtools-endpoint.cy.ts +447 -0
- package/tests/cypress/e2e/api/scheduled-actions/scheduling.bdd.md +649 -0
- package/tests/cypress/e2e/api/scheduled-actions/scheduling.cy.ts +333 -0
- package/tests/cypress/e2e/api/settings/api-keys.crud.cy.ts +923 -0
- package/tests/cypress/e2e/uat/auth/app-roles/developer-login.bdd.md +231 -0
- package/tests/cypress/e2e/uat/auth/app-roles/developer-login.cy.ts +144 -0
- package/tests/cypress/e2e/uat/auth/app-roles/superadmin-login.bdd.md +118 -0
- package/tests/cypress/e2e/uat/auth/app-roles/superadmin-login.cy.ts +84 -0
- package/tests/cypress/e2e/uat/auth/custom-roles/editor-login.bdd.md +288 -0
- package/tests/cypress/e2e/uat/auth/custom-roles/editor-login.cy.ts +188 -0
- package/tests/cypress/e2e/uat/auth/login-logout.bdd.md +160 -0
- package/tests/cypress/e2e/uat/auth/login-logout.cy.ts +116 -0
- package/tests/cypress/e2e/uat/auth/password-reset.bdd.md +289 -0
- package/tests/cypress/e2e/uat/auth/password-reset.cy.ts +200 -0
- package/tests/cypress/e2e/uat/auth/team-roles/admin-login.bdd.md +225 -0
- package/tests/cypress/e2e/uat/auth/team-roles/admin-login.cy.ts +148 -0
- package/tests/cypress/e2e/uat/auth/team-roles/member-login.bdd.md +251 -0
- package/tests/cypress/e2e/uat/auth/team-roles/member-login.cy.ts +163 -0
- package/tests/cypress/e2e/uat/auth/team-roles/owner-login.bdd.md +231 -0
- package/tests/cypress/e2e/uat/auth/team-roles/owner-login.cy.ts +141 -0
- package/tests/cypress/e2e/uat/billing/extended.bdd.md +273 -0
- package/tests/cypress/e2e/uat/billing/extended.cy.ts +209 -0
- package/tests/cypress/e2e/uat/billing/feature-gates.bdd.md +407 -0
- package/tests/cypress/e2e/uat/billing/feature-gates.cy.ts +307 -0
- package/tests/cypress/e2e/uat/billing/page.bdd.md +329 -0
- package/tests/cypress/e2e/uat/billing/page.cy.ts +250 -0
- package/tests/cypress/e2e/uat/billing/status.bdd.md +190 -0
- package/tests/cypress/e2e/uat/billing/status.cy.ts +145 -0
- package/tests/cypress/e2e/uat/billing/team-switch.bdd.md +156 -0
- package/tests/cypress/e2e/uat/billing/team-switch.cy.ts +122 -0
- package/tests/cypress/e2e/uat/billing/usage.bdd.md +218 -0
- package/tests/cypress/e2e/uat/billing/usage.cy.ts +176 -0
- package/tests/cypress/e2e/uat/blocks/hero.bdd.md +124 -0
- package/tests/cypress/e2e/uat/blocks/hero.cy.ts +56 -0
- package/tests/cypress/e2e/uat/devtools/api-tester.cy.ts +390 -0
- package/tests/cypress/e2e/uat/entities/customers/member.bdd.md +275 -0
- package/tests/cypress/e2e/uat/entities/customers/member.cy.ts +122 -0
- package/tests/cypress/e2e/uat/entities/customers/owner.bdd.md +243 -0
- package/tests/cypress/e2e/uat/entities/customers/owner.cy.ts +165 -0
- package/tests/cypress/e2e/uat/entities/pages/block-crud.bdd.md +476 -0
- package/tests/cypress/e2e/uat/entities/pages/block-crud.cy.ts +486 -0
- package/tests/cypress/e2e/uat/entities/pages/block-editor.bdd.md +460 -0
- package/tests/cypress/e2e/uat/entities/pages/block-editor.cy.ts +301 -0
- package/tests/cypress/e2e/uat/entities/pages/list.bdd.md +432 -0
- package/tests/cypress/e2e/uat/entities/pages/list.cy.ts +273 -0
- package/tests/cypress/e2e/uat/entities/pages/public-rendering.bdd.md +696 -0
- package/tests/cypress/e2e/uat/entities/pages/public-rendering.cy.ts +340 -0
- package/tests/cypress/e2e/uat/entities/posts/categories-api-aware.bdd.md +161 -0
- package/tests/cypress/e2e/uat/entities/posts/categories-api-aware.cy.ts +104 -0
- package/tests/cypress/e2e/uat/entities/posts/categories.bdd.md +375 -0
- package/tests/cypress/e2e/uat/entities/posts/categories.cy.ts +241 -0
- package/tests/cypress/e2e/uat/entities/posts/editor.bdd.md +429 -0
- package/tests/cypress/e2e/uat/entities/posts/editor.cy.ts +257 -0
- package/tests/cypress/e2e/uat/entities/posts/list.bdd.md +340 -0
- package/tests/cypress/e2e/uat/entities/posts/list.cy.ts +177 -0
- package/tests/cypress/e2e/uat/entities/posts/public.bdd.md +614 -0
- package/tests/cypress/e2e/uat/entities/posts/public.cy.ts +249 -0
- package/tests/cypress/e2e/uat/entities/tasks/member.bdd.md +222 -0
- package/tests/cypress/e2e/uat/entities/tasks/member.cy.ts +165 -0
- package/tests/cypress/e2e/uat/entities/tasks/owner.bdd.md +419 -0
- package/tests/cypress/e2e/uat/entities/tasks/owner.cy.ts +191 -0
- package/tests/cypress/e2e/uat/roles/editor-role.bdd.md +552 -0
- package/tests/cypress/e2e/uat/roles/editor-role.cy.ts +210 -0
- package/tests/cypress/e2e/uat/roles/member-restrictions.bdd.md +450 -0
- package/tests/cypress/e2e/uat/roles/member-restrictions.cy.ts +189 -0
- package/tests/cypress/e2e/uat/roles/owner-full-crud.bdd.md +530 -0
- package/tests/cypress/e2e/uat/roles/owner-full-crud.cy.ts +247 -0
- package/tests/cypress/e2e/uat/scheduled-actions/devtools-ui.bdd.md +736 -0
- package/tests/cypress/e2e/uat/scheduled-actions/devtools-ui.cy.ts +740 -0
- package/tests/cypress/e2e/uat/teams/roles-matrix.bdd.md +553 -0
- package/tests/cypress/e2e/uat/teams/roles-matrix.cy.ts +185 -0
- package/tests/cypress/e2e/uat/teams/switcher.bdd.md +1151 -0
- package/tests/cypress/e2e/uat/teams/switcher.cy.ts +497 -0
- package/tests/cypress/e2e/uat/teams/team-switcher.md +198 -0
- package/tests/cypress/fixtures/blocks.json +218 -0
- package/tests/cypress/fixtures/entities.json +78 -0
- package/tests/cypress/fixtures/page-builder.json +21 -0
- package/tests/cypress/src/components/CategoriesPOM.ts +382 -0
- package/tests/cypress/src/components/CustomersPOM.ts +439 -0
- package/tests/cypress/src/components/DevKeyringPOM.ts +160 -0
- package/tests/cypress/src/components/EntityForm.ts +375 -0
- package/tests/cypress/src/components/EntityList.ts +389 -0
- package/tests/cypress/src/components/PageBuilderPOM.ts +710 -0
- package/tests/cypress/src/components/PostEditorPOM.ts +370 -0
- package/tests/cypress/src/components/PostsListPOM.ts +223 -0
- package/tests/cypress/src/components/PublicPagePOM.ts +447 -0
- package/tests/cypress/src/components/PublicPostPOM.ts +146 -0
- package/tests/cypress/src/components/TasksPOM.ts +272 -0
- package/tests/cypress/src/components/TeamSwitcherPOM.ts +450 -0
- package/tests/cypress/src/components/index.ts +21 -0
- package/tests/cypress/src/controllers/ApiKeysAPIController.js +178 -0
- package/tests/cypress/src/controllers/BaseAPIController.js +317 -0
- package/tests/cypress/src/controllers/CustomerAPIController.js +251 -0
- package/tests/cypress/src/controllers/PagesAPIController.js +226 -0
- package/tests/cypress/src/controllers/PostsAPIController.js +250 -0
- package/tests/cypress/src/controllers/TaskAPIController.js +240 -0
- package/tests/cypress/src/controllers/UsersAPIController.js +242 -0
- package/tests/cypress/src/controllers/index.js +25 -0
- package/tests/cypress/src/core/AuthPOM.ts +450 -0
- package/tests/cypress/src/core/BasePOM.ts +86 -0
- package/tests/cypress/src/core/BlockEditorBasePOM.ts +576 -0
- package/tests/cypress/src/core/DashboardEntityPOM.ts +692 -0
- package/tests/cypress/src/core/index.ts +14 -0
- package/tests/cypress/src/entities/CustomersPOM.ts +172 -0
- package/tests/cypress/src/entities/PagesPOM.ts +137 -0
- package/tests/cypress/src/entities/PostsPOM.ts +137 -0
- package/tests/cypress/src/entities/TasksPOM.ts +176 -0
- package/tests/cypress/src/entities/index.ts +14 -0
- package/tests/cypress/src/features/BillingPOM.ts +385 -0
- package/tests/cypress/src/features/DashboardPOM.ts +245 -0
- package/tests/cypress/src/features/DevtoolsPOM.ts +739 -0
- package/tests/cypress/src/features/PageBuilderPOM.ts +263 -0
- package/tests/cypress/src/features/PostEditorPOM.ts +313 -0
- package/tests/cypress/src/features/ScheduledActionsPOM.ts +463 -0
- package/tests/cypress/src/features/SettingsPOM.ts +362 -0
- package/tests/cypress/src/features/SuperadminPOM.ts +331 -0
- package/tests/cypress/src/features/SuperadminTeamRolesPOM.ts +285 -0
- package/tests/cypress/src/features/index.ts +28 -0
- package/tests/cypress/src/helpers/ApiInterceptor.ts +177 -0
- package/tests/cypress/src/index.ts +101 -0
- package/tests/cypress/src/pages/dashboard/Dashboard.js +677 -0
- package/tests/cypress/src/pages/dashboard/DashboardPage.js +43 -0
- package/tests/cypress/src/pages/dashboard/DashboardStats.js +546 -0
- package/tests/cypress/src/pages/dashboard/index.js +6 -0
- package/tests/cypress/src/pages/index.js +5 -0
- package/tests/cypress/src/pages/public/FeaturesPage.js +28 -0
- package/tests/cypress/src/pages/public/LandingPage.js +69 -0
- package/tests/cypress/src/pages/public/PricingPage.js +33 -0
- package/tests/cypress/src/pages/public/index.js +6 -0
- package/tests/cypress/src/selectors.ts +46 -0
- package/tests/cypress/src/session-helpers.ts +500 -0
- package/tests/cypress/support/doc-commands.ts +260 -0
- package/tests/cypress.config.ts +150 -0
- package/tests/jest/components/post-header.test.tsx +377 -0
- package/tests/jest/config/role-config.test.ts +529 -0
- package/tests/jest/jest.config.ts +81 -0
- package/tests/jest/langchain/COVERAGE.md +372 -0
- package/tests/jest/langchain/guardrails.test.ts +465 -0
- package/tests/jest/langchain/streaming.test.ts +367 -0
- package/tests/jest/langchain/token-tracker.test.ts +455 -0
- package/tests/jest/langchain/tracer-callbacks.test.ts +881 -0
- package/tests/jest/langchain/tracer.test.ts +823 -0
- package/tests/jest/user-roles/role-helpers.test.ts +432 -0
- package/tests/jest/validation/categories.test.ts +429 -0
- package/tests/jest/validation/posts.test.ts +546 -0
- package/tests/tsconfig.json +15 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests - Tracing Callback Handler
|
|
3
|
+
*
|
|
4
|
+
* Tests LangChain callback handler integration:
|
|
5
|
+
* - LLM event handling (start, end, error)
|
|
6
|
+
* - Tool event handling (start, end, error)
|
|
7
|
+
* - Chain event handling (start, end, error)
|
|
8
|
+
* - Parent-child span relationships
|
|
9
|
+
* - Depth tracking
|
|
10
|
+
*
|
|
11
|
+
* Focus: Event transformation and span lifecycle.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
TracingCallbackHandler,
|
|
16
|
+
createTracingCallbacks,
|
|
17
|
+
} from '@/plugins/langchain/lib/tracer-callbacks'
|
|
18
|
+
import { tracer } from '@/plugins/langchain/lib/tracer'
|
|
19
|
+
import type { Serialized } from '@langchain/core/load/serializable'
|
|
20
|
+
|
|
21
|
+
// Mock tracer service
|
|
22
|
+
jest.mock('@/contents/plugins/langchain/lib/tracer', () => ({
|
|
23
|
+
tracer: {
|
|
24
|
+
startSpan: jest.fn(),
|
|
25
|
+
endSpan: jest.fn(),
|
|
26
|
+
},
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
const mockStartSpan = tracer.startSpan as jest.MockedFunction<typeof tracer.startSpan>
|
|
30
|
+
const mockEndSpan = tracer.endSpan as jest.MockedFunction<typeof tracer.endSpan>
|
|
31
|
+
|
|
32
|
+
describe('TracingCallbackHandler', () => {
|
|
33
|
+
const context = { userId: 'user-123', teamId: 'team-456' }
|
|
34
|
+
const traceId = 'trace-abc'
|
|
35
|
+
let handler: TracingCallbackHandler
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
jest.clearAllMocks()
|
|
39
|
+
handler = new TracingCallbackHandler({ context, traceId })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('initialization', () => {
|
|
43
|
+
it('should initialize with correct name', () => {
|
|
44
|
+
expect(handler.name).toBe('tracing_callback_handler')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should store context and traceId', () => {
|
|
48
|
+
// Context and traceId are private, so we test indirectly via method calls
|
|
49
|
+
mockStartSpan.mockResolvedValue({
|
|
50
|
+
spanId: 'span-123',
|
|
51
|
+
traceId,
|
|
52
|
+
name: 'test',
|
|
53
|
+
type: 'llm',
|
|
54
|
+
depth: 0,
|
|
55
|
+
startedAt: new Date(),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
handler.handleLLMStart(
|
|
59
|
+
{ id: ['langchain', 'llms', 'openai'] } as Serialized,
|
|
60
|
+
['test prompt'],
|
|
61
|
+
'run-123'
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
expect(mockStartSpan).toHaveBeenCalledWith(
|
|
65
|
+
context,
|
|
66
|
+
traceId,
|
|
67
|
+
expect.any(Object)
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('handleLLMStart', () => {
|
|
73
|
+
it('should create span for LLM call', async () => {
|
|
74
|
+
const llm: Serialized = {
|
|
75
|
+
id: ['langchain', 'llms', 'openai'],
|
|
76
|
+
kwargs: { model: 'gpt-4o' },
|
|
77
|
+
}
|
|
78
|
+
mockStartSpan.mockResolvedValue({
|
|
79
|
+
spanId: 'span-123',
|
|
80
|
+
traceId,
|
|
81
|
+
name: 'LLM: gpt-4o',
|
|
82
|
+
type: 'llm',
|
|
83
|
+
depth: 0,
|
|
84
|
+
startedAt: new Date(),
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
await handler.handleLLMStart(llm, ['test prompt'], 'run-123')
|
|
88
|
+
|
|
89
|
+
expect(mockStartSpan).toHaveBeenCalledWith(context, traceId, {
|
|
90
|
+
name: 'LLM: gpt-4o',
|
|
91
|
+
type: 'llm',
|
|
92
|
+
provider: 'openai',
|
|
93
|
+
model: 'gpt-4o',
|
|
94
|
+
parentSpanId: undefined,
|
|
95
|
+
depth: 0,
|
|
96
|
+
input: { prompts: ['test prompt'] },
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should extract provider from LLM serialized id', async () => {
|
|
101
|
+
const llm: Serialized = {
|
|
102
|
+
id: ['langchain', 'chat_models', 'anthropic'],
|
|
103
|
+
kwargs: { model: 'claude-3-5-sonnet' },
|
|
104
|
+
}
|
|
105
|
+
mockStartSpan.mockResolvedValue(null)
|
|
106
|
+
|
|
107
|
+
await handler.handleLLMStart(llm, ['test'], 'run-123')
|
|
108
|
+
|
|
109
|
+
expect(mockStartSpan).toHaveBeenCalledWith(
|
|
110
|
+
context,
|
|
111
|
+
traceId,
|
|
112
|
+
expect.objectContaining({
|
|
113
|
+
provider: 'anthropic',
|
|
114
|
+
})
|
|
115
|
+
)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should use "unknown" for missing model', async () => {
|
|
119
|
+
const llm: Serialized = {
|
|
120
|
+
id: ['langchain', 'llms', 'custom'],
|
|
121
|
+
}
|
|
122
|
+
mockStartSpan.mockResolvedValue(null)
|
|
123
|
+
|
|
124
|
+
await handler.handleLLMStart(llm, ['test'], 'run-123')
|
|
125
|
+
|
|
126
|
+
expect(mockStartSpan).toHaveBeenCalledWith(
|
|
127
|
+
context,
|
|
128
|
+
traceId,
|
|
129
|
+
expect.objectContaining({
|
|
130
|
+
model: 'unknown',
|
|
131
|
+
})
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should handle nested LLM calls with depth', async () => {
|
|
136
|
+
const llm: Serialized = {
|
|
137
|
+
id: ['langchain', 'llms', 'openai'],
|
|
138
|
+
kwargs: { model: 'gpt-4o' },
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// First, create a parent span from a different parent to set up parentSpans map
|
|
142
|
+
mockStartSpan.mockResolvedValueOnce({
|
|
143
|
+
spanId: 'span-grandparent',
|
|
144
|
+
traceId,
|
|
145
|
+
name: 'LLM: gpt-4o',
|
|
146
|
+
type: 'llm',
|
|
147
|
+
depth: 0,
|
|
148
|
+
startedAt: new Date(),
|
|
149
|
+
})
|
|
150
|
+
await handler.handleLLMStart(llm, ['prompt 0'], 'run-grandparent')
|
|
151
|
+
|
|
152
|
+
// Second call with grandparent as parent (depth 1)
|
|
153
|
+
mockStartSpan.mockResolvedValueOnce({
|
|
154
|
+
spanId: 'span-parent',
|
|
155
|
+
traceId,
|
|
156
|
+
name: 'LLM: gpt-4o',
|
|
157
|
+
type: 'llm',
|
|
158
|
+
depth: 1,
|
|
159
|
+
startedAt: new Date(),
|
|
160
|
+
})
|
|
161
|
+
await handler.handleLLMStart(llm, ['prompt 1'], 'run-parent', 'run-grandparent')
|
|
162
|
+
|
|
163
|
+
// Third call nested under parent (depth 2)
|
|
164
|
+
mockStartSpan.mockResolvedValueOnce({
|
|
165
|
+
spanId: 'span-child',
|
|
166
|
+
traceId,
|
|
167
|
+
name: 'LLM: gpt-4o',
|
|
168
|
+
type: 'llm',
|
|
169
|
+
depth: 2,
|
|
170
|
+
startedAt: new Date(),
|
|
171
|
+
})
|
|
172
|
+
await handler.handleLLMStart(llm, ['prompt 2'], 'run-child', 'run-parent')
|
|
173
|
+
|
|
174
|
+
expect(mockStartSpan).toHaveBeenLastCalledWith(
|
|
175
|
+
context,
|
|
176
|
+
traceId,
|
|
177
|
+
expect.objectContaining({
|
|
178
|
+
depth: 2,
|
|
179
|
+
parentSpanId: 'span-parent',
|
|
180
|
+
})
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should handle errors gracefully', async () => {
|
|
185
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
186
|
+
mockStartSpan.mockRejectedValue(new Error('Database error'))
|
|
187
|
+
|
|
188
|
+
await handler.handleLLMStart(
|
|
189
|
+
{ id: ['langchain', 'llms', 'openai'] } as Serialized,
|
|
190
|
+
['test'],
|
|
191
|
+
'run-123'
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
195
|
+
'[TracingCallbackHandler] handleLLMStart error:',
|
|
196
|
+
expect.any(Error)
|
|
197
|
+
)
|
|
198
|
+
consoleSpy.mockRestore()
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('handleLLMEnd', () => {
|
|
203
|
+
beforeEach(async () => {
|
|
204
|
+
// Setup: Start an LLM span first
|
|
205
|
+
mockStartSpan.mockResolvedValue({
|
|
206
|
+
spanId: 'span-123',
|
|
207
|
+
traceId,
|
|
208
|
+
name: 'LLM: gpt-4o',
|
|
209
|
+
type: 'llm',
|
|
210
|
+
depth: 0,
|
|
211
|
+
startedAt: new Date(),
|
|
212
|
+
})
|
|
213
|
+
await handler.handleLLMStart(
|
|
214
|
+
{ id: ['langchain', 'llms', 'openai'], kwargs: { model: 'gpt-4o' } } as Serialized,
|
|
215
|
+
['test prompt'],
|
|
216
|
+
'run-123'
|
|
217
|
+
)
|
|
218
|
+
jest.clearAllMocks()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should end span with output and token usage', async () => {
|
|
222
|
+
mockEndSpan.mockResolvedValue(undefined)
|
|
223
|
+
|
|
224
|
+
await handler.handleLLMEnd(
|
|
225
|
+
{
|
|
226
|
+
generations: [{ text: 'Generated response' }],
|
|
227
|
+
llmOutput: {
|
|
228
|
+
tokenUsage: {
|
|
229
|
+
promptTokens: 100,
|
|
230
|
+
completionTokens: 50,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
'run-123'
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
expect(mockEndSpan).toHaveBeenCalledWith(context, traceId, 'span-123', {
|
|
238
|
+
output: {
|
|
239
|
+
generations: ['Generated response'],
|
|
240
|
+
},
|
|
241
|
+
tokens: {
|
|
242
|
+
input: 100,
|
|
243
|
+
output: 50,
|
|
244
|
+
},
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('should handle alternate token usage format (input_tokens/output_tokens)', async () => {
|
|
249
|
+
mockEndSpan.mockResolvedValue(undefined)
|
|
250
|
+
|
|
251
|
+
await handler.handleLLMEnd(
|
|
252
|
+
{
|
|
253
|
+
generations: [{ text: 'response' }],
|
|
254
|
+
llmOutput: {
|
|
255
|
+
tokenUsage: {
|
|
256
|
+
input_tokens: 200,
|
|
257
|
+
output_tokens: 75,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
'run-123'
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
expect(mockEndSpan).toHaveBeenCalledWith(
|
|
265
|
+
context,
|
|
266
|
+
traceId,
|
|
267
|
+
'span-123',
|
|
268
|
+
expect.objectContaining({
|
|
269
|
+
tokens: {
|
|
270
|
+
input: 200,
|
|
271
|
+
output: 75,
|
|
272
|
+
},
|
|
273
|
+
})
|
|
274
|
+
)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('should handle message format generations', async () => {
|
|
278
|
+
mockEndSpan.mockResolvedValue(undefined)
|
|
279
|
+
|
|
280
|
+
await handler.handleLLMEnd(
|
|
281
|
+
{
|
|
282
|
+
generations: [{ message: { content: 'Message response' } }],
|
|
283
|
+
llmOutput: { tokenUsage: {} },
|
|
284
|
+
},
|
|
285
|
+
'run-123'
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
expect(mockEndSpan).toHaveBeenCalledWith(
|
|
289
|
+
context,
|
|
290
|
+
traceId,
|
|
291
|
+
'span-123',
|
|
292
|
+
expect.objectContaining({
|
|
293
|
+
output: {
|
|
294
|
+
generations: ['Message response'],
|
|
295
|
+
},
|
|
296
|
+
})
|
|
297
|
+
)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('should handle missing token usage', async () => {
|
|
301
|
+
mockEndSpan.mockResolvedValue(undefined)
|
|
302
|
+
|
|
303
|
+
await handler.handleLLMEnd(
|
|
304
|
+
{
|
|
305
|
+
generations: [{ text: 'response' }],
|
|
306
|
+
llmOutput: {},
|
|
307
|
+
},
|
|
308
|
+
'run-123'
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
expect(mockEndSpan).toHaveBeenCalledWith(
|
|
312
|
+
context,
|
|
313
|
+
traceId,
|
|
314
|
+
'span-123',
|
|
315
|
+
expect.objectContaining({
|
|
316
|
+
tokens: undefined,
|
|
317
|
+
})
|
|
318
|
+
)
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('should do nothing if span was not started', async () => {
|
|
322
|
+
await handler.handleLLMEnd({ generations: [] }, 'unknown-run-id')
|
|
323
|
+
|
|
324
|
+
expect(mockEndSpan).not.toHaveBeenCalled()
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('should handle errors gracefully', async () => {
|
|
328
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
329
|
+
mockEndSpan.mockRejectedValue(new Error('Database error'))
|
|
330
|
+
|
|
331
|
+
await handler.handleLLMEnd({ generations: [] }, 'run-123')
|
|
332
|
+
|
|
333
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
334
|
+
'[TracingCallbackHandler] handleLLMEnd error:',
|
|
335
|
+
expect.any(Error)
|
|
336
|
+
)
|
|
337
|
+
consoleSpy.mockRestore()
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
describe('handleLLMError', () => {
|
|
342
|
+
beforeEach(async () => {
|
|
343
|
+
// Setup: Start an LLM span first
|
|
344
|
+
mockStartSpan.mockResolvedValue({
|
|
345
|
+
spanId: 'span-123',
|
|
346
|
+
traceId,
|
|
347
|
+
name: 'LLM: gpt-4o',
|
|
348
|
+
type: 'llm',
|
|
349
|
+
depth: 0,
|
|
350
|
+
startedAt: new Date(),
|
|
351
|
+
})
|
|
352
|
+
await handler.handleLLMStart(
|
|
353
|
+
{ id: ['langchain', 'llms', 'openai'] } as Serialized,
|
|
354
|
+
['test'],
|
|
355
|
+
'run-123'
|
|
356
|
+
)
|
|
357
|
+
jest.clearAllMocks()
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('should end span with error', async () => {
|
|
361
|
+
mockEndSpan.mockResolvedValue(undefined)
|
|
362
|
+
const error = new Error('LLM execution failed')
|
|
363
|
+
|
|
364
|
+
await handler.handleLLMError(error, 'run-123')
|
|
365
|
+
|
|
366
|
+
expect(mockEndSpan).toHaveBeenCalledWith(context, traceId, 'span-123', {
|
|
367
|
+
error,
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('should do nothing if span was not started', async () => {
|
|
372
|
+
await handler.handleLLMError(new Error('test'), 'unknown-run-id')
|
|
373
|
+
|
|
374
|
+
expect(mockEndSpan).not.toHaveBeenCalled()
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('should handle errors in error handling', async () => {
|
|
378
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
379
|
+
mockEndSpan.mockRejectedValue(new Error('Database error'))
|
|
380
|
+
|
|
381
|
+
await handler.handleLLMError(new Error('LLM error'), 'run-123')
|
|
382
|
+
|
|
383
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
384
|
+
'[TracingCallbackHandler] handleLLMError error:',
|
|
385
|
+
expect.any(Error)
|
|
386
|
+
)
|
|
387
|
+
consoleSpy.mockRestore()
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
describe('handleToolStart', () => {
|
|
392
|
+
it('should create span for tool call', async () => {
|
|
393
|
+
const tool: Serialized = {
|
|
394
|
+
id: ['langchain', 'tools', 'search'],
|
|
395
|
+
}
|
|
396
|
+
mockStartSpan.mockResolvedValue({
|
|
397
|
+
spanId: 'span-123',
|
|
398
|
+
traceId,
|
|
399
|
+
name: 'Tool: search',
|
|
400
|
+
type: 'tool',
|
|
401
|
+
depth: 0,
|
|
402
|
+
startedAt: new Date(),
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
await handler.handleToolStart(tool, 'query term', 'run-123')
|
|
406
|
+
|
|
407
|
+
expect(mockStartSpan).toHaveBeenCalledWith(context, traceId, {
|
|
408
|
+
name: 'Tool: search',
|
|
409
|
+
type: 'tool',
|
|
410
|
+
toolName: 'search',
|
|
411
|
+
parentSpanId: undefined,
|
|
412
|
+
depth: 0,
|
|
413
|
+
input: { toolInput: 'query term' },
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('should extract tool name from serialized id', async () => {
|
|
418
|
+
const tool: Serialized = {
|
|
419
|
+
id: ['langchain', 'tools', 'calculator'],
|
|
420
|
+
}
|
|
421
|
+
mockStartSpan.mockResolvedValue(null)
|
|
422
|
+
|
|
423
|
+
await handler.handleToolStart(tool, '2 + 2', 'run-123')
|
|
424
|
+
|
|
425
|
+
expect(mockStartSpan).toHaveBeenCalledWith(
|
|
426
|
+
context,
|
|
427
|
+
traceId,
|
|
428
|
+
expect.objectContaining({
|
|
429
|
+
toolName: 'calculator',
|
|
430
|
+
})
|
|
431
|
+
)
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it('should use "unknown" for missing tool name', async () => {
|
|
435
|
+
const tool: Serialized = { id: [] }
|
|
436
|
+
mockStartSpan.mockResolvedValue(null)
|
|
437
|
+
|
|
438
|
+
await handler.handleToolStart(tool, 'input', 'run-123')
|
|
439
|
+
|
|
440
|
+
expect(mockStartSpan).toHaveBeenCalledWith(
|
|
441
|
+
context,
|
|
442
|
+
traceId,
|
|
443
|
+
expect.objectContaining({
|
|
444
|
+
toolName: 'unknown',
|
|
445
|
+
})
|
|
446
|
+
)
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it('should handle nested tool calls with depth', async () => {
|
|
450
|
+
// Grandparent span
|
|
451
|
+
mockStartSpan.mockResolvedValueOnce({
|
|
452
|
+
spanId: 'span-grandparent',
|
|
453
|
+
traceId,
|
|
454
|
+
name: 'Tool: search',
|
|
455
|
+
type: 'tool',
|
|
456
|
+
depth: 0,
|
|
457
|
+
startedAt: new Date(),
|
|
458
|
+
})
|
|
459
|
+
await handler.handleToolStart(
|
|
460
|
+
{ id: ['langchain', 'tools', 'search'] } as Serialized,
|
|
461
|
+
'query1',
|
|
462
|
+
'run-grandparent'
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
// Parent span
|
|
466
|
+
mockStartSpan.mockResolvedValueOnce({
|
|
467
|
+
spanId: 'span-parent',
|
|
468
|
+
traceId,
|
|
469
|
+
name: 'Tool: search',
|
|
470
|
+
type: 'tool',
|
|
471
|
+
depth: 1,
|
|
472
|
+
startedAt: new Date(),
|
|
473
|
+
})
|
|
474
|
+
await handler.handleToolStart(
|
|
475
|
+
{ id: ['langchain', 'tools', 'search'] } as Serialized,
|
|
476
|
+
'query',
|
|
477
|
+
'run-parent',
|
|
478
|
+
'run-grandparent'
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
// Child span
|
|
482
|
+
mockStartSpan.mockResolvedValueOnce({
|
|
483
|
+
spanId: 'span-child',
|
|
484
|
+
traceId,
|
|
485
|
+
name: 'Tool: calculator',
|
|
486
|
+
type: 'tool',
|
|
487
|
+
depth: 2,
|
|
488
|
+
startedAt: new Date(),
|
|
489
|
+
})
|
|
490
|
+
await handler.handleToolStart(
|
|
491
|
+
{ id: ['langchain', 'tools', 'calculator'] } as Serialized,
|
|
492
|
+
'2 + 2',
|
|
493
|
+
'run-child',
|
|
494
|
+
'run-parent'
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
expect(mockStartSpan).toHaveBeenLastCalledWith(
|
|
498
|
+
context,
|
|
499
|
+
traceId,
|
|
500
|
+
expect.objectContaining({
|
|
501
|
+
depth: 2,
|
|
502
|
+
parentSpanId: 'span-parent',
|
|
503
|
+
})
|
|
504
|
+
)
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it('should handle errors gracefully', async () => {
|
|
508
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
509
|
+
mockStartSpan.mockRejectedValue(new Error('Database error'))
|
|
510
|
+
|
|
511
|
+
await handler.handleToolStart(
|
|
512
|
+
{ id: ['langchain', 'tools', 'search'] } as Serialized,
|
|
513
|
+
'query',
|
|
514
|
+
'run-123'
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
518
|
+
'[TracingCallbackHandler] handleToolStart error:',
|
|
519
|
+
expect.any(Error)
|
|
520
|
+
)
|
|
521
|
+
consoleSpy.mockRestore()
|
|
522
|
+
})
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
describe('handleToolEnd', () => {
|
|
526
|
+
beforeEach(async () => {
|
|
527
|
+
// Setup: Start a tool span first
|
|
528
|
+
mockStartSpan.mockResolvedValue({
|
|
529
|
+
spanId: 'span-123',
|
|
530
|
+
traceId,
|
|
531
|
+
name: 'Tool: search',
|
|
532
|
+
type: 'tool',
|
|
533
|
+
depth: 0,
|
|
534
|
+
startedAt: new Date(),
|
|
535
|
+
})
|
|
536
|
+
await handler.handleToolStart(
|
|
537
|
+
{ id: ['langchain', 'tools', 'search'] } as Serialized,
|
|
538
|
+
'query',
|
|
539
|
+
'run-123'
|
|
540
|
+
)
|
|
541
|
+
jest.clearAllMocks()
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
it('should end span with tool output', async () => {
|
|
545
|
+
mockEndSpan.mockResolvedValue(undefined)
|
|
546
|
+
|
|
547
|
+
await handler.handleToolEnd('Search results: item1, item2', 'run-123')
|
|
548
|
+
|
|
549
|
+
expect(mockEndSpan).toHaveBeenCalledWith(context, traceId, 'span-123', {
|
|
550
|
+
toolOutput: 'Search results: item1, item2',
|
|
551
|
+
})
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
it('should do nothing if span was not started', async () => {
|
|
555
|
+
await handler.handleToolEnd('output', 'unknown-run-id')
|
|
556
|
+
|
|
557
|
+
expect(mockEndSpan).not.toHaveBeenCalled()
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
it('should handle errors gracefully', async () => {
|
|
561
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
562
|
+
mockEndSpan.mockRejectedValue(new Error('Database error'))
|
|
563
|
+
|
|
564
|
+
await handler.handleToolEnd('output', 'run-123')
|
|
565
|
+
|
|
566
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
567
|
+
'[TracingCallbackHandler] handleToolEnd error:',
|
|
568
|
+
expect.any(Error)
|
|
569
|
+
)
|
|
570
|
+
consoleSpy.mockRestore()
|
|
571
|
+
})
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
describe('handleToolError', () => {
|
|
575
|
+
beforeEach(async () => {
|
|
576
|
+
// Setup: Start a tool span first
|
|
577
|
+
mockStartSpan.mockResolvedValue({
|
|
578
|
+
spanId: 'span-123',
|
|
579
|
+
traceId,
|
|
580
|
+
name: 'Tool: search',
|
|
581
|
+
type: 'tool',
|
|
582
|
+
depth: 0,
|
|
583
|
+
startedAt: new Date(),
|
|
584
|
+
})
|
|
585
|
+
await handler.handleToolStart(
|
|
586
|
+
{ id: ['langchain', 'tools', 'search'] } as Serialized,
|
|
587
|
+
'query',
|
|
588
|
+
'run-123'
|
|
589
|
+
)
|
|
590
|
+
jest.clearAllMocks()
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
it('should end span with error', async () => {
|
|
594
|
+
mockEndSpan.mockResolvedValue(undefined)
|
|
595
|
+
const error = new Error('Tool execution failed')
|
|
596
|
+
|
|
597
|
+
await handler.handleToolError(error, 'run-123')
|
|
598
|
+
|
|
599
|
+
expect(mockEndSpan).toHaveBeenCalledWith(context, traceId, 'span-123', {
|
|
600
|
+
error,
|
|
601
|
+
})
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
it('should do nothing if span was not started', async () => {
|
|
605
|
+
await handler.handleToolError(new Error('test'), 'unknown-run-id')
|
|
606
|
+
|
|
607
|
+
expect(mockEndSpan).not.toHaveBeenCalled()
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
it('should handle errors in error handling', async () => {
|
|
611
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
612
|
+
mockEndSpan.mockRejectedValue(new Error('Database error'))
|
|
613
|
+
|
|
614
|
+
await handler.handleToolError(new Error('Tool error'), 'run-123')
|
|
615
|
+
|
|
616
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
617
|
+
'[TracingCallbackHandler] handleToolError error:',
|
|
618
|
+
expect.any(Error)
|
|
619
|
+
)
|
|
620
|
+
consoleSpy.mockRestore()
|
|
621
|
+
})
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
describe('handleChainStart', () => {
|
|
625
|
+
it('should create span for chain execution', async () => {
|
|
626
|
+
const chain: Serialized = {
|
|
627
|
+
id: ['langchain', 'chains', 'llm_chain'],
|
|
628
|
+
}
|
|
629
|
+
mockStartSpan.mockResolvedValue({
|
|
630
|
+
spanId: 'span-123',
|
|
631
|
+
traceId,
|
|
632
|
+
name: 'Chain: llm_chain',
|
|
633
|
+
type: 'chain',
|
|
634
|
+
depth: 0,
|
|
635
|
+
startedAt: new Date(),
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
await handler.handleChainStart(chain, { input: 'test' }, 'run-123')
|
|
639
|
+
|
|
640
|
+
expect(mockStartSpan).toHaveBeenCalledWith(context, traceId, {
|
|
641
|
+
name: 'Chain: llm_chain',
|
|
642
|
+
type: 'chain',
|
|
643
|
+
parentSpanId: undefined,
|
|
644
|
+
depth: 0,
|
|
645
|
+
input: { input: 'test' },
|
|
646
|
+
})
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
it('should extract chain name from serialized id', async () => {
|
|
650
|
+
const chain: Serialized = {
|
|
651
|
+
id: ['langchain', 'chains', 'sequential_chain'],
|
|
652
|
+
}
|
|
653
|
+
mockStartSpan.mockResolvedValue(null)
|
|
654
|
+
|
|
655
|
+
await handler.handleChainStart(chain, {}, 'run-123')
|
|
656
|
+
|
|
657
|
+
expect(mockStartSpan).toHaveBeenCalledWith(
|
|
658
|
+
context,
|
|
659
|
+
traceId,
|
|
660
|
+
expect.objectContaining({
|
|
661
|
+
name: 'Chain: sequential_chain',
|
|
662
|
+
})
|
|
663
|
+
)
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
it('should use "unknown" for missing chain name', async () => {
|
|
667
|
+
const chain: Serialized = { id: [] }
|
|
668
|
+
mockStartSpan.mockResolvedValue(null)
|
|
669
|
+
|
|
670
|
+
await handler.handleChainStart(chain, {}, 'run-123')
|
|
671
|
+
|
|
672
|
+
expect(mockStartSpan).toHaveBeenCalledWith(
|
|
673
|
+
context,
|
|
674
|
+
traceId,
|
|
675
|
+
expect.objectContaining({
|
|
676
|
+
name: 'Chain: unknown',
|
|
677
|
+
})
|
|
678
|
+
)
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
it('should handle nested chains with depth', async () => {
|
|
682
|
+
// Grandparent chain
|
|
683
|
+
mockStartSpan.mockResolvedValueOnce({
|
|
684
|
+
spanId: 'span-grandparent',
|
|
685
|
+
traceId,
|
|
686
|
+
name: 'Chain: root',
|
|
687
|
+
type: 'chain',
|
|
688
|
+
depth: 0,
|
|
689
|
+
startedAt: new Date(),
|
|
690
|
+
})
|
|
691
|
+
await handler.handleChainStart(
|
|
692
|
+
{ id: ['langchain', 'chains', 'root'] } as Serialized,
|
|
693
|
+
{},
|
|
694
|
+
'run-grandparent'
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
// Parent chain
|
|
698
|
+
mockStartSpan.mockResolvedValueOnce({
|
|
699
|
+
spanId: 'span-parent',
|
|
700
|
+
traceId,
|
|
701
|
+
name: 'Chain: main',
|
|
702
|
+
type: 'chain',
|
|
703
|
+
depth: 1,
|
|
704
|
+
startedAt: new Date(),
|
|
705
|
+
})
|
|
706
|
+
await handler.handleChainStart(
|
|
707
|
+
{ id: ['langchain', 'chains', 'main'] } as Serialized,
|
|
708
|
+
{},
|
|
709
|
+
'run-parent',
|
|
710
|
+
'run-grandparent'
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
// Child chain
|
|
714
|
+
mockStartSpan.mockResolvedValueOnce({
|
|
715
|
+
spanId: 'span-child',
|
|
716
|
+
traceId,
|
|
717
|
+
name: 'Chain: sub',
|
|
718
|
+
type: 'chain',
|
|
719
|
+
depth: 2,
|
|
720
|
+
startedAt: new Date(),
|
|
721
|
+
})
|
|
722
|
+
await handler.handleChainStart(
|
|
723
|
+
{ id: ['langchain', 'chains', 'sub'] } as Serialized,
|
|
724
|
+
{},
|
|
725
|
+
'run-child',
|
|
726
|
+
'run-parent'
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
expect(mockStartSpan).toHaveBeenLastCalledWith(
|
|
730
|
+
context,
|
|
731
|
+
traceId,
|
|
732
|
+
expect.objectContaining({
|
|
733
|
+
depth: 2,
|
|
734
|
+
parentSpanId: 'span-parent',
|
|
735
|
+
})
|
|
736
|
+
)
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
it('should handle errors gracefully', async () => {
|
|
740
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
741
|
+
mockStartSpan.mockRejectedValue(new Error('Database error'))
|
|
742
|
+
|
|
743
|
+
await handler.handleChainStart(
|
|
744
|
+
{ id: ['langchain', 'chains', 'test'] } as Serialized,
|
|
745
|
+
{},
|
|
746
|
+
'run-123'
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
750
|
+
'[TracingCallbackHandler] handleChainStart error:',
|
|
751
|
+
expect.any(Error)
|
|
752
|
+
)
|
|
753
|
+
consoleSpy.mockRestore()
|
|
754
|
+
})
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
describe('handleChainEnd', () => {
|
|
758
|
+
beforeEach(async () => {
|
|
759
|
+
// Setup: Start a chain span first
|
|
760
|
+
mockStartSpan.mockResolvedValue({
|
|
761
|
+
spanId: 'span-123',
|
|
762
|
+
traceId,
|
|
763
|
+
name: 'Chain: test',
|
|
764
|
+
type: 'chain',
|
|
765
|
+
depth: 0,
|
|
766
|
+
startedAt: new Date(),
|
|
767
|
+
})
|
|
768
|
+
await handler.handleChainStart(
|
|
769
|
+
{ id: ['langchain', 'chains', 'test'] } as Serialized,
|
|
770
|
+
{},
|
|
771
|
+
'run-123'
|
|
772
|
+
)
|
|
773
|
+
jest.clearAllMocks()
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
it('should end span with chain outputs', async () => {
|
|
777
|
+
mockEndSpan.mockResolvedValue(undefined)
|
|
778
|
+
|
|
779
|
+
await handler.handleChainEnd({ output: 'result', metadata: { duration: 100 } }, 'run-123')
|
|
780
|
+
|
|
781
|
+
expect(mockEndSpan).toHaveBeenCalledWith(context, traceId, 'span-123', {
|
|
782
|
+
output: { output: 'result', metadata: { duration: 100 } },
|
|
783
|
+
})
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
it('should do nothing if span was not started', async () => {
|
|
787
|
+
await handler.handleChainEnd({}, 'unknown-run-id')
|
|
788
|
+
|
|
789
|
+
expect(mockEndSpan).not.toHaveBeenCalled()
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
it('should handle errors gracefully', async () => {
|
|
793
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
794
|
+
mockEndSpan.mockRejectedValue(new Error('Database error'))
|
|
795
|
+
|
|
796
|
+
await handler.handleChainEnd({}, 'run-123')
|
|
797
|
+
|
|
798
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
799
|
+
'[TracingCallbackHandler] handleChainEnd error:',
|
|
800
|
+
expect.any(Error)
|
|
801
|
+
)
|
|
802
|
+
consoleSpy.mockRestore()
|
|
803
|
+
})
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
describe('handleChainError', () => {
|
|
807
|
+
beforeEach(async () => {
|
|
808
|
+
// Setup: Start a chain span first
|
|
809
|
+
mockStartSpan.mockResolvedValue({
|
|
810
|
+
spanId: 'span-123',
|
|
811
|
+
traceId,
|
|
812
|
+
name: 'Chain: test',
|
|
813
|
+
type: 'chain',
|
|
814
|
+
depth: 0,
|
|
815
|
+
startedAt: new Date(),
|
|
816
|
+
})
|
|
817
|
+
await handler.handleChainStart(
|
|
818
|
+
{ id: ['langchain', 'chains', 'test'] } as Serialized,
|
|
819
|
+
{},
|
|
820
|
+
'run-123'
|
|
821
|
+
)
|
|
822
|
+
jest.clearAllMocks()
|
|
823
|
+
})
|
|
824
|
+
|
|
825
|
+
it('should end span with error', async () => {
|
|
826
|
+
mockEndSpan.mockResolvedValue(undefined)
|
|
827
|
+
const error = new Error('Chain execution failed')
|
|
828
|
+
|
|
829
|
+
await handler.handleChainError(error, 'run-123')
|
|
830
|
+
|
|
831
|
+
expect(mockEndSpan).toHaveBeenCalledWith(context, traceId, 'span-123', {
|
|
832
|
+
error,
|
|
833
|
+
})
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
it('should do nothing if span was not started', async () => {
|
|
837
|
+
await handler.handleChainError(new Error('test'), 'unknown-run-id')
|
|
838
|
+
|
|
839
|
+
expect(mockEndSpan).not.toHaveBeenCalled()
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
it('should handle errors in error handling', async () => {
|
|
843
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
844
|
+
mockEndSpan.mockRejectedValue(new Error('Database error'))
|
|
845
|
+
|
|
846
|
+
await handler.handleChainError(new Error('Chain error'), 'run-123')
|
|
847
|
+
|
|
848
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
849
|
+
'[TracingCallbackHandler] handleChainError error:',
|
|
850
|
+
expect.any(Error)
|
|
851
|
+
)
|
|
852
|
+
consoleSpy.mockRestore()
|
|
853
|
+
})
|
|
854
|
+
})
|
|
855
|
+
|
|
856
|
+
describe('createTracingCallbacks factory', () => {
|
|
857
|
+
it('should create TracingCallbackHandler instance', () => {
|
|
858
|
+
const callbacks = createTracingCallbacks(context, traceId)
|
|
859
|
+
|
|
860
|
+
expect(callbacks).toBeInstanceOf(TracingCallbackHandler)
|
|
861
|
+
expect(callbacks.name).toBe('tracing_callback_handler')
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
it('should pass context and traceId to handler', async () => {
|
|
865
|
+
const callbacks = createTracingCallbacks(context, 'trace-xyz')
|
|
866
|
+
mockStartSpan.mockResolvedValue(null)
|
|
867
|
+
|
|
868
|
+
await callbacks.handleLLMStart(
|
|
869
|
+
{ id: ['langchain', 'llms', 'openai'] } as Serialized,
|
|
870
|
+
['test'],
|
|
871
|
+
'run-123'
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
expect(mockStartSpan).toHaveBeenCalledWith(
|
|
875
|
+
context,
|
|
876
|
+
'trace-xyz',
|
|
877
|
+
expect.any(Object)
|
|
878
|
+
)
|
|
879
|
+
})
|
|
880
|
+
})
|
|
881
|
+
})
|