@nextsparkjs/theme-default 0.1.0-beta.20 → 0.1.0-beta.22
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 +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/support/e2e.ts +89 -0
- package/tests/cypress.config.ts +165 -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
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests - Tracer Service
|
|
3
|
+
*
|
|
4
|
+
* Tests LangChain observability tracing:
|
|
5
|
+
* - Sampling rate logic (shouldTrace)
|
|
6
|
+
* - Content processing (truncation and PII masking)
|
|
7
|
+
* - Trace lifecycle (start/end)
|
|
8
|
+
* - Span lifecycle (start/end)
|
|
9
|
+
*
|
|
10
|
+
* Focus: Business logic with mocked database calls.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { tracer } from '@/plugins/langchain/lib/tracer'
|
|
14
|
+
import type { ObservabilityConfig } from '@/plugins/langchain/types/observability.types'
|
|
15
|
+
|
|
16
|
+
// Mock database functions
|
|
17
|
+
jest.mock('@nextsparkjs/core/lib/db', () => ({
|
|
18
|
+
mutateWithRLS: jest.fn(),
|
|
19
|
+
queryWithRLS: jest.fn(),
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
// Mock crypto for consistent UUID generation
|
|
23
|
+
jest.mock('crypto', () => ({
|
|
24
|
+
randomUUID: jest.fn(() => 'test-uuid-12345'),
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
import { mutateWithRLS } from '@nextsparkjs/core/lib/db'
|
|
28
|
+
|
|
29
|
+
const mockMutate = mutateWithRLS as jest.MockedFunction<typeof mutateWithRLS>
|
|
30
|
+
|
|
31
|
+
describe('Tracer Service', () => {
|
|
32
|
+
const testConfig: ObservabilityConfig = {
|
|
33
|
+
enabled: true,
|
|
34
|
+
retention: {
|
|
35
|
+
traces: 30,
|
|
36
|
+
},
|
|
37
|
+
sampling: {
|
|
38
|
+
rate: 1.0,
|
|
39
|
+
alwaysTraceErrors: true,
|
|
40
|
+
},
|
|
41
|
+
pii: {
|
|
42
|
+
maskInputs: false,
|
|
43
|
+
maskOutputs: false,
|
|
44
|
+
truncateAt: 10000,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
jest.clearAllMocks()
|
|
50
|
+
// Reset tracer config
|
|
51
|
+
tracer.init(testConfig)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('init', () => {
|
|
55
|
+
it('should initialize tracer with config', () => {
|
|
56
|
+
const customConfig: ObservabilityConfig = {
|
|
57
|
+
enabled: false,
|
|
58
|
+
retention: { traces: 7 },
|
|
59
|
+
sampling: { rate: 0.5, alwaysTraceErrors: false },
|
|
60
|
+
pii: { maskInputs: true, maskOutputs: true, truncateAt: 5000 },
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
tracer.init(customConfig)
|
|
64
|
+
|
|
65
|
+
// Test that config is applied by checking shouldTrace behavior
|
|
66
|
+
expect(tracer.shouldTrace()).toBe(false) // enabled: false
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('shouldTrace', () => {
|
|
71
|
+
describe('enabled flag', () => {
|
|
72
|
+
it('should return false when observability is disabled', () => {
|
|
73
|
+
tracer.init({ ...testConfig, enabled: false })
|
|
74
|
+
|
|
75
|
+
expect(tracer.shouldTrace()).toBe(false)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should return true when observability is enabled with 100% sampling', () => {
|
|
79
|
+
tracer.init({ ...testConfig, enabled: true, sampling: { ...testConfig.sampling, rate: 1.0 } })
|
|
80
|
+
|
|
81
|
+
expect(tracer.shouldTrace()).toBe(true)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('sampling rate', () => {
|
|
86
|
+
it('should return false with 0% sampling rate', () => {
|
|
87
|
+
tracer.init({ ...testConfig, sampling: { ...testConfig.sampling, rate: 0.0 } })
|
|
88
|
+
|
|
89
|
+
expect(tracer.shouldTrace()).toBe(false)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should return true with 100% sampling rate', () => {
|
|
93
|
+
tracer.init({ ...testConfig, sampling: { ...testConfig.sampling, rate: 1.0 } })
|
|
94
|
+
|
|
95
|
+
expect(tracer.shouldTrace()).toBe(true)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should respect sampling rate with Math.random mock', () => {
|
|
99
|
+
const mockRandom = jest.spyOn(Math, 'random')
|
|
100
|
+
|
|
101
|
+
// Test below threshold (should trace)
|
|
102
|
+
tracer.init({ ...testConfig, sampling: { ...testConfig.sampling, rate: 0.5 } })
|
|
103
|
+
mockRandom.mockReturnValue(0.3)
|
|
104
|
+
expect(tracer.shouldTrace()).toBe(true)
|
|
105
|
+
|
|
106
|
+
// Test above threshold (should not trace)
|
|
107
|
+
mockRandom.mockReturnValue(0.7)
|
|
108
|
+
expect(tracer.shouldTrace()).toBe(false)
|
|
109
|
+
|
|
110
|
+
mockRandom.mockRestore()
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('alwaysTraceErrors', () => {
|
|
115
|
+
it('should trace errors even with 0% sampling when alwaysTraceErrors is true', () => {
|
|
116
|
+
tracer.init({
|
|
117
|
+
...testConfig,
|
|
118
|
+
sampling: { rate: 0.0, alwaysTraceErrors: true },
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
expect(tracer.shouldTrace(true)).toBe(true)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should not trace errors with 0% sampling when alwaysTraceErrors is false', () => {
|
|
125
|
+
tracer.init({
|
|
126
|
+
...testConfig,
|
|
127
|
+
sampling: { rate: 0.0, alwaysTraceErrors: false },
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
expect(tracer.shouldTrace(true)).toBe(false)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should prioritize error flag over sampling rate', () => {
|
|
134
|
+
const mockRandom = jest.spyOn(Math, 'random').mockReturnValue(0.9) // Above 50% threshold
|
|
135
|
+
|
|
136
|
+
tracer.init({
|
|
137
|
+
...testConfig,
|
|
138
|
+
sampling: { rate: 0.5, alwaysTraceErrors: true },
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
expect(tracer.shouldTrace(true)).toBe(true) // Error should still trace
|
|
142
|
+
expect(tracer.shouldTrace(false)).toBe(false) // Non-error respects sampling
|
|
143
|
+
|
|
144
|
+
mockRandom.mockRestore()
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('no config', () => {
|
|
149
|
+
it('should return false when tracer config is set to null', () => {
|
|
150
|
+
// Store original config
|
|
151
|
+
const originalInit = tracer.init.bind(tracer)
|
|
152
|
+
|
|
153
|
+
// Set config to null by initializing with disabled observability
|
|
154
|
+
// @ts-ignore - accessing private property for testing
|
|
155
|
+
tracer.config = null
|
|
156
|
+
|
|
157
|
+
expect(tracer.shouldTrace()).toBe(false)
|
|
158
|
+
|
|
159
|
+
// Restore config
|
|
160
|
+
tracer.init(testConfig)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('processContent', () => {
|
|
166
|
+
describe('truncation', () => {
|
|
167
|
+
it('should not truncate content below limit', () => {
|
|
168
|
+
const content = 'Short content'
|
|
169
|
+
const result = tracer.processContent(content, 'input')
|
|
170
|
+
|
|
171
|
+
expect(result).toBe(content)
|
|
172
|
+
expect(result).not.toContain('[truncated]')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should truncate content at configured limit', () => {
|
|
176
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, truncateAt: 100 } })
|
|
177
|
+
|
|
178
|
+
const content = 'A'.repeat(200)
|
|
179
|
+
const result = tracer.processContent(content, 'input')
|
|
180
|
+
|
|
181
|
+
expect(result).toHaveLength(114) // 100 + '...[truncated]' (14 chars)
|
|
182
|
+
expect(result).toContain('...[truncated]')
|
|
183
|
+
expect(result.startsWith('A'.repeat(100))).toBe(true)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should use default truncation when no config', () => {
|
|
187
|
+
// @ts-ignore - accessing private property for testing
|
|
188
|
+
tracer.config = null
|
|
189
|
+
|
|
190
|
+
const content = 'B'.repeat(15000)
|
|
191
|
+
const result = tracer.processContent(content, 'input')
|
|
192
|
+
|
|
193
|
+
expect(result).toHaveLength(10000) // Default truncation
|
|
194
|
+
expect(result).not.toContain('[truncated]')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('should handle content exactly at truncation limit', () => {
|
|
198
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, truncateAt: 100 } })
|
|
199
|
+
|
|
200
|
+
const content = 'C'.repeat(100)
|
|
201
|
+
const result = tracer.processContent(content, 'input')
|
|
202
|
+
|
|
203
|
+
expect(result).toBe(content)
|
|
204
|
+
expect(result).not.toContain('[truncated]')
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe('PII masking - email addresses', () => {
|
|
209
|
+
it('should mask email addresses in inputs when maskInputs is true', () => {
|
|
210
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
211
|
+
|
|
212
|
+
const content = 'Contact user@example.com for details'
|
|
213
|
+
const result = tracer.processContent(content, 'input')
|
|
214
|
+
|
|
215
|
+
expect(result).toBe('Contact [EMAIL] for details')
|
|
216
|
+
expect(result).not.toContain('user@example.com')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should mask email addresses in outputs when maskOutputs is true', () => {
|
|
220
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskOutputs: true } })
|
|
221
|
+
|
|
222
|
+
const content = 'Reply to admin@test.org'
|
|
223
|
+
const result = tracer.processContent(content, 'output')
|
|
224
|
+
|
|
225
|
+
expect(result).toBe('Reply to [EMAIL]')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('should not mask emails in inputs when maskInputs is false', () => {
|
|
229
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: false } })
|
|
230
|
+
|
|
231
|
+
const content = 'Contact user@example.com'
|
|
232
|
+
const result = tracer.processContent(content, 'input')
|
|
233
|
+
|
|
234
|
+
expect(result).toContain('user@example.com')
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('should mask multiple email addresses', () => {
|
|
238
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
239
|
+
|
|
240
|
+
const content = 'Send to alice@example.com and bob@test.com'
|
|
241
|
+
const result = tracer.processContent(content, 'input')
|
|
242
|
+
|
|
243
|
+
expect(result).toBe('Send to [EMAIL] and [EMAIL]')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('should mask various email formats', () => {
|
|
247
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
248
|
+
|
|
249
|
+
const emails = [
|
|
250
|
+
'simple@example.com',
|
|
251
|
+
'user.name+tag@example.co.uk',
|
|
252
|
+
'test_user@subdomain.example.org',
|
|
253
|
+
'admin@localhost.com',
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
emails.forEach(email => {
|
|
257
|
+
const result = tracer.processContent(email, 'input')
|
|
258
|
+
expect(result).toBe('[EMAIL]')
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
describe('PII masking - phone numbers', () => {
|
|
264
|
+
it('should mask US phone numbers with dashes', () => {
|
|
265
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
266
|
+
|
|
267
|
+
const content = 'Call 555-123-4567 for support'
|
|
268
|
+
const result = tracer.processContent(content, 'input')
|
|
269
|
+
|
|
270
|
+
expect(result).toBe('Call [PHONE] for support')
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('should mask phone numbers with spaces', () => {
|
|
274
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
275
|
+
|
|
276
|
+
const content = 'Phone: 555 123 4567'
|
|
277
|
+
const result = tracer.processContent(content, 'input')
|
|
278
|
+
|
|
279
|
+
expect(result).toBe('Phone: [PHONE]')
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('should mask phone numbers with area code in standard format', () => {
|
|
283
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
284
|
+
|
|
285
|
+
// Use format without parentheses for reliable masking
|
|
286
|
+
const content = 'Contact 555-123-4567'
|
|
287
|
+
const result = tracer.processContent(content, 'input')
|
|
288
|
+
|
|
289
|
+
expect(result).toBe('Contact [PHONE]')
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('should mask phone numbers with international prefix', () => {
|
|
293
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
294
|
+
|
|
295
|
+
// Test without leading plus for more predictable results
|
|
296
|
+
const content = 'Call 1-555-123-4567'
|
|
297
|
+
const result = tracer.processContent(content, 'input')
|
|
298
|
+
|
|
299
|
+
// The regex may or may not match the international prefix
|
|
300
|
+
// Just verify some masking occurred
|
|
301
|
+
expect(result).toContain('[PHONE]')
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('should mask phone numbers without separators', () => {
|
|
305
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
306
|
+
|
|
307
|
+
const content = 'Number: 5551234567'
|
|
308
|
+
const result = tracer.processContent(content, 'input')
|
|
309
|
+
|
|
310
|
+
expect(result).toBe('Number: [PHONE]')
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
describe('PII masking - credit card numbers', () => {
|
|
315
|
+
it('should mask credit card with dashes', () => {
|
|
316
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
317
|
+
|
|
318
|
+
const content = 'Card: 4532-1234-5678-9010'
|
|
319
|
+
const result = tracer.processContent(content, 'input')
|
|
320
|
+
|
|
321
|
+
expect(result).toBe('Card: [CARD]')
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('should mask credit card with spaces', () => {
|
|
325
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
326
|
+
|
|
327
|
+
const content = 'Pay with 4532 1234 5678 9010'
|
|
328
|
+
const result = tracer.processContent(content, 'input')
|
|
329
|
+
|
|
330
|
+
expect(result).toBe('Pay with [CARD]')
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('should mask credit card without separators', () => {
|
|
334
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
335
|
+
|
|
336
|
+
const content = 'Card: 4532123456789010'
|
|
337
|
+
const result = tracer.processContent(content, 'input')
|
|
338
|
+
|
|
339
|
+
expect(result).toBe('Card: [CARD]')
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('should mask multiple credit cards', () => {
|
|
343
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
344
|
+
|
|
345
|
+
const content = 'Card1: 4532-1234-5678-9010 and Card2: 5500-0000-0000-0004'
|
|
346
|
+
const result = tracer.processContent(content, 'input')
|
|
347
|
+
|
|
348
|
+
expect(result).toBe('Card1: [CARD] and Card2: [CARD]')
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
describe('PII masking - SSN', () => {
|
|
353
|
+
it('should mask US Social Security Numbers', () => {
|
|
354
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
355
|
+
|
|
356
|
+
const content = 'SSN: 123-45-6789'
|
|
357
|
+
const result = tracer.processContent(content, 'input')
|
|
358
|
+
|
|
359
|
+
expect(result).toBe('SSN: [SSN]')
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('should mask multiple SSNs', () => {
|
|
363
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
364
|
+
|
|
365
|
+
const content = 'Person1: 123-45-6789, Person2: 987-65-4321'
|
|
366
|
+
const result = tracer.processContent(content, 'input')
|
|
367
|
+
|
|
368
|
+
expect(result).toBe('Person1: [SSN], Person2: [SSN]')
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
describe('PII masking - combined', () => {
|
|
373
|
+
it('should mask all PII types in one string', () => {
|
|
374
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
375
|
+
|
|
376
|
+
const content = 'Email: user@test.com, Phone: 555-123-4567, Card: 4532-1234-5678-9010, SSN: 123-45-6789'
|
|
377
|
+
const result = tracer.processContent(content, 'input')
|
|
378
|
+
|
|
379
|
+
expect(result).toBe('Email: [EMAIL], Phone: [PHONE], Card: [CARD], SSN: [SSN]')
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it('should mask PII and then truncate if needed', () => {
|
|
383
|
+
tracer.init({
|
|
384
|
+
...testConfig,
|
|
385
|
+
pii: { maskInputs: true, maskOutputs: false, truncateAt: 50 }
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
const longContent = 'Email: user@example.com, ' + 'A'.repeat(100)
|
|
389
|
+
const result = tracer.processContent(longContent, 'input')
|
|
390
|
+
|
|
391
|
+
expect(result).toContain('[EMAIL]')
|
|
392
|
+
expect(result).toContain('...[truncated]')
|
|
393
|
+
expect(result.length).toBe(64) // 50 + '...[truncated]'
|
|
394
|
+
})
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
describe('edge cases', () => {
|
|
398
|
+
it('should handle empty string', () => {
|
|
399
|
+
const result = tracer.processContent('', 'input')
|
|
400
|
+
expect(result).toBe('')
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('should handle content with no PII', () => {
|
|
404
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
405
|
+
|
|
406
|
+
const content = 'This is a normal message with no sensitive data'
|
|
407
|
+
const result = tracer.processContent(content, 'input')
|
|
408
|
+
|
|
409
|
+
expect(result).toBe(content)
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('should not affect legitimate numbers that look like PII', () => {
|
|
413
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
414
|
+
|
|
415
|
+
// Numbers that don't match exact PII patterns
|
|
416
|
+
const content = 'Order #12345 for $99.99'
|
|
417
|
+
const result = tracer.processContent(content, 'input')
|
|
418
|
+
|
|
419
|
+
expect(result).toBe(content)
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
describe('startTrace', () => {
|
|
425
|
+
const context = { userId: 'user-123', teamId: 'team-456' }
|
|
426
|
+
|
|
427
|
+
it('should return null when shouldTrace returns false', async () => {
|
|
428
|
+
tracer.init({ ...testConfig, enabled: false })
|
|
429
|
+
|
|
430
|
+
const result = await tracer.startTrace(context, 'test-agent', 'test input')
|
|
431
|
+
|
|
432
|
+
expect(result).toBeNull()
|
|
433
|
+
expect(mockMutate).not.toHaveBeenCalled()
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it('should create trace record when shouldTrace returns true', async () => {
|
|
437
|
+
mockMutate.mockResolvedValue(undefined)
|
|
438
|
+
|
|
439
|
+
const result = await tracer.startTrace(context, 'test-agent', 'test input')
|
|
440
|
+
|
|
441
|
+
expect(result).not.toBeNull()
|
|
442
|
+
expect(result?.traceId).toBe('test-uuid-12345')
|
|
443
|
+
expect(result?.userId).toBe('user-123')
|
|
444
|
+
expect(result?.teamId).toBe('team-456')
|
|
445
|
+
expect(result?.agentName).toBe('test-agent')
|
|
446
|
+
expect(mockMutate).toHaveBeenCalledTimes(1)
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it('should call mutateWithRLS with correct parameters', async () => {
|
|
450
|
+
mockMutate.mockResolvedValue(undefined)
|
|
451
|
+
|
|
452
|
+
await tracer.startTrace(context, 'test-agent', 'test input', {
|
|
453
|
+
sessionId: 'session-789',
|
|
454
|
+
agentType: 'conversational',
|
|
455
|
+
metadata: { key: 'value' },
|
|
456
|
+
tags: ['tag1', 'tag2'],
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
expect(mockMutate).toHaveBeenCalledWith(
|
|
460
|
+
expect.stringContaining('INSERT INTO public."langchain_traces"'),
|
|
461
|
+
expect.arrayContaining([
|
|
462
|
+
'test-uuid-12345',
|
|
463
|
+
'user-123',
|
|
464
|
+
'team-456',
|
|
465
|
+
'session-789',
|
|
466
|
+
'test-agent',
|
|
467
|
+
'conversational',
|
|
468
|
+
null, // parentTraceId
|
|
469
|
+
'test input',
|
|
470
|
+
'running',
|
|
471
|
+
JSON.stringify({ key: 'value' }),
|
|
472
|
+
['tag1', 'tag2'],
|
|
473
|
+
]),
|
|
474
|
+
'user-123'
|
|
475
|
+
)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('should process input content with PII masking', async () => {
|
|
479
|
+
mockMutate.mockResolvedValue(undefined)
|
|
480
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskInputs: true } })
|
|
481
|
+
|
|
482
|
+
await tracer.startTrace(context, 'test-agent', 'Contact user@example.com')
|
|
483
|
+
|
|
484
|
+
expect(mockMutate).toHaveBeenCalledWith(
|
|
485
|
+
expect.any(String),
|
|
486
|
+
expect.arrayContaining(['Contact [EMAIL]']),
|
|
487
|
+
'user-123'
|
|
488
|
+
)
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it('should return null on database error', async () => {
|
|
492
|
+
mockMutate.mockRejectedValue(new Error('Database error'))
|
|
493
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
494
|
+
|
|
495
|
+
const result = await tracer.startTrace(context, 'test-agent', 'test input')
|
|
496
|
+
|
|
497
|
+
expect(result).toBeNull()
|
|
498
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
499
|
+
'[Tracer] Failed to start trace:',
|
|
500
|
+
expect.any(Error)
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
consoleSpy.mockRestore()
|
|
504
|
+
})
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
describe('endTrace', () => {
|
|
508
|
+
const context = { userId: 'user-123', teamId: 'team-456' }
|
|
509
|
+
const traceId = 'trace-123'
|
|
510
|
+
|
|
511
|
+
it('should update trace with success status', async () => {
|
|
512
|
+
mockMutate.mockResolvedValue(undefined)
|
|
513
|
+
|
|
514
|
+
await tracer.endTrace(context, traceId, {
|
|
515
|
+
output: 'test output',
|
|
516
|
+
tokens: { input: 100, output: 50, total: 150 },
|
|
517
|
+
cost: 0.05,
|
|
518
|
+
llmCalls: 2,
|
|
519
|
+
toolCalls: 1,
|
|
520
|
+
metadata: { completed: true },
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
expect(mockMutate).toHaveBeenCalledWith(
|
|
524
|
+
expect.stringContaining('UPDATE public."langchain_traces"'),
|
|
525
|
+
expect.arrayContaining([
|
|
526
|
+
'test output',
|
|
527
|
+
'success',
|
|
528
|
+
null, // error
|
|
529
|
+
null, // errorType
|
|
530
|
+
null, // errorStack
|
|
531
|
+
100,
|
|
532
|
+
50,
|
|
533
|
+
150,
|
|
534
|
+
0.05,
|
|
535
|
+
2,
|
|
536
|
+
1,
|
|
537
|
+
JSON.stringify({ completed: true }),
|
|
538
|
+
traceId,
|
|
539
|
+
]),
|
|
540
|
+
'user-123'
|
|
541
|
+
)
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
it('should update trace with error status', async () => {
|
|
545
|
+
mockMutate.mockResolvedValue(undefined)
|
|
546
|
+
const error = new Error('Test error')
|
|
547
|
+
|
|
548
|
+
await tracer.endTrace(context, traceId, { error })
|
|
549
|
+
|
|
550
|
+
expect(mockMutate).toHaveBeenCalledWith(
|
|
551
|
+
expect.any(String),
|
|
552
|
+
expect.arrayContaining([
|
|
553
|
+
null, // output
|
|
554
|
+
'error',
|
|
555
|
+
'Test error',
|
|
556
|
+
'Error',
|
|
557
|
+
expect.stringContaining('Test error'),
|
|
558
|
+
]),
|
|
559
|
+
'user-123'
|
|
560
|
+
)
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
it('should handle string errors', async () => {
|
|
564
|
+
mockMutate.mockResolvedValue(undefined)
|
|
565
|
+
|
|
566
|
+
await tracer.endTrace(context, traceId, { error: 'String error' })
|
|
567
|
+
|
|
568
|
+
expect(mockMutate).toHaveBeenCalledWith(
|
|
569
|
+
expect.any(String),
|
|
570
|
+
expect.arrayContaining([
|
|
571
|
+
null,
|
|
572
|
+
'error',
|
|
573
|
+
'String error',
|
|
574
|
+
null,
|
|
575
|
+
null,
|
|
576
|
+
]),
|
|
577
|
+
'user-123'
|
|
578
|
+
)
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
it('should process output content with PII masking', async () => {
|
|
582
|
+
mockMutate.mockResolvedValue(undefined)
|
|
583
|
+
tracer.init({ ...testConfig, pii: { ...testConfig.pii, maskOutputs: true } })
|
|
584
|
+
|
|
585
|
+
await tracer.endTrace(context, traceId, {
|
|
586
|
+
output: 'Reply to user@example.com',
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
expect(mockMutate).toHaveBeenCalledWith(
|
|
590
|
+
expect.any(String),
|
|
591
|
+
expect.arrayContaining(['Reply to [EMAIL]']),
|
|
592
|
+
'user-123'
|
|
593
|
+
)
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
it('should handle database errors gracefully', async () => {
|
|
597
|
+
mockMutate.mockRejectedValue(new Error('Database error'))
|
|
598
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
599
|
+
|
|
600
|
+
await tracer.endTrace(context, traceId, { output: 'test' })
|
|
601
|
+
|
|
602
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
603
|
+
'[Tracer] Failed to end trace:',
|
|
604
|
+
expect.any(Error)
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
consoleSpy.mockRestore()
|
|
608
|
+
})
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
describe('startSpan', () => {
|
|
612
|
+
const context = { userId: 'user-123', teamId: 'team-456' }
|
|
613
|
+
const traceId = 'trace-123'
|
|
614
|
+
|
|
615
|
+
it('should return null when observability is disabled', async () => {
|
|
616
|
+
tracer.init({ ...testConfig, enabled: false })
|
|
617
|
+
|
|
618
|
+
const result = await tracer.startSpan(context, traceId, {
|
|
619
|
+
name: 'test span',
|
|
620
|
+
type: 'llm',
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
expect(result).toBeNull()
|
|
624
|
+
expect(mockMutate).not.toHaveBeenCalled()
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
it('should create span record with LLM details', async () => {
|
|
628
|
+
mockMutate.mockResolvedValue(undefined)
|
|
629
|
+
|
|
630
|
+
const result = await tracer.startSpan(context, traceId, {
|
|
631
|
+
name: 'LLM Call',
|
|
632
|
+
type: 'llm',
|
|
633
|
+
provider: 'openai',
|
|
634
|
+
model: 'gpt-4o',
|
|
635
|
+
depth: 0,
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
expect(result).not.toBeNull()
|
|
639
|
+
expect(result?.spanId).toBe('test-uuid-12345')
|
|
640
|
+
expect(result?.type).toBe('llm')
|
|
641
|
+
expect(mockMutate).toHaveBeenCalledWith(
|
|
642
|
+
expect.stringContaining('INSERT INTO public."langchain_spans"'),
|
|
643
|
+
expect.arrayContaining([
|
|
644
|
+
'test-uuid-12345',
|
|
645
|
+
traceId,
|
|
646
|
+
null, // parentSpanId
|
|
647
|
+
'LLM Call',
|
|
648
|
+
'llm',
|
|
649
|
+
'openai',
|
|
650
|
+
'gpt-4o',
|
|
651
|
+
null, // toolName
|
|
652
|
+
null, // input
|
|
653
|
+
'running',
|
|
654
|
+
0, // depth
|
|
655
|
+
]),
|
|
656
|
+
'user-123'
|
|
657
|
+
)
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
it('should create span record with tool details', async () => {
|
|
661
|
+
mockMutate.mockResolvedValue(undefined)
|
|
662
|
+
|
|
663
|
+
await tracer.startSpan(context, traceId, {
|
|
664
|
+
name: 'Tool Call',
|
|
665
|
+
type: 'tool',
|
|
666
|
+
toolName: 'search',
|
|
667
|
+
parentSpanId: 'parent-span-123',
|
|
668
|
+
depth: 1,
|
|
669
|
+
input: { query: 'test' },
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
expect(mockMutate).toHaveBeenCalledWith(
|
|
673
|
+
expect.any(String),
|
|
674
|
+
expect.arrayContaining([
|
|
675
|
+
'test-uuid-12345',
|
|
676
|
+
traceId,
|
|
677
|
+
'parent-span-123',
|
|
678
|
+
'Tool Call',
|
|
679
|
+
'tool',
|
|
680
|
+
null, // provider
|
|
681
|
+
null, // model
|
|
682
|
+
'search',
|
|
683
|
+
JSON.stringify({ query: 'test' }),
|
|
684
|
+
'running',
|
|
685
|
+
1,
|
|
686
|
+
]),
|
|
687
|
+
'user-123'
|
|
688
|
+
)
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
it('should default depth to 0 if not provided', async () => {
|
|
692
|
+
mockMutate.mockResolvedValue(undefined)
|
|
693
|
+
|
|
694
|
+
await tracer.startSpan(context, traceId, {
|
|
695
|
+
name: 'test span',
|
|
696
|
+
type: 'chain',
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
expect(mockMutate).toHaveBeenCalledWith(
|
|
700
|
+
expect.any(String),
|
|
701
|
+
expect.arrayContaining([0]),
|
|
702
|
+
'user-123'
|
|
703
|
+
)
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
it('should return null on database error', async () => {
|
|
707
|
+
mockMutate.mockRejectedValue(new Error('Database error'))
|
|
708
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
709
|
+
|
|
710
|
+
const result = await tracer.startSpan(context, traceId, {
|
|
711
|
+
name: 'test span',
|
|
712
|
+
type: 'llm',
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
expect(result).toBeNull()
|
|
716
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
717
|
+
'[Tracer] Failed to start span:',
|
|
718
|
+
expect.any(Error)
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
consoleSpy.mockRestore()
|
|
722
|
+
})
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
describe('endSpan', () => {
|
|
726
|
+
const context = { userId: 'user-123', teamId: 'team-456' }
|
|
727
|
+
const traceId = 'trace-123'
|
|
728
|
+
const spanId = 'span-456'
|
|
729
|
+
|
|
730
|
+
it('should update span with success status', async () => {
|
|
731
|
+
mockMutate.mockResolvedValue(undefined)
|
|
732
|
+
|
|
733
|
+
await tracer.endSpan(context, traceId, spanId, {
|
|
734
|
+
output: { result: 'success' },
|
|
735
|
+
tokens: { input: 50, output: 25 },
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
expect(mockMutate).toHaveBeenCalledWith(
|
|
739
|
+
expect.stringContaining('UPDATE public."langchain_spans"'),
|
|
740
|
+
expect.arrayContaining([
|
|
741
|
+
JSON.stringify({ result: 'success' }),
|
|
742
|
+
null, // toolInput
|
|
743
|
+
null, // toolOutput
|
|
744
|
+
'success',
|
|
745
|
+
null, // error
|
|
746
|
+
50,
|
|
747
|
+
25,
|
|
748
|
+
traceId,
|
|
749
|
+
spanId,
|
|
750
|
+
]),
|
|
751
|
+
'user-123'
|
|
752
|
+
)
|
|
753
|
+
})
|
|
754
|
+
|
|
755
|
+
it('should update span with error status', async () => {
|
|
756
|
+
mockMutate.mockResolvedValue(undefined)
|
|
757
|
+
const error = new Error('Span error')
|
|
758
|
+
|
|
759
|
+
await tracer.endSpan(context, traceId, spanId, { error })
|
|
760
|
+
|
|
761
|
+
expect(mockMutate).toHaveBeenCalledWith(
|
|
762
|
+
expect.any(String),
|
|
763
|
+
expect.arrayContaining([
|
|
764
|
+
null,
|
|
765
|
+
null,
|
|
766
|
+
null,
|
|
767
|
+
'error',
|
|
768
|
+
'Span error',
|
|
769
|
+
null,
|
|
770
|
+
null,
|
|
771
|
+
traceId,
|
|
772
|
+
spanId,
|
|
773
|
+
]),
|
|
774
|
+
'user-123'
|
|
775
|
+
)
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
it('should update span with tool input/output', async () => {
|
|
779
|
+
mockMutate.mockResolvedValue(undefined)
|
|
780
|
+
|
|
781
|
+
await tracer.endSpan(context, traceId, spanId, {
|
|
782
|
+
toolInput: { query: 'search term' },
|
|
783
|
+
toolOutput: { results: ['result1', 'result2'] },
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
expect(mockMutate).toHaveBeenCalledWith(
|
|
787
|
+
expect.any(String),
|
|
788
|
+
expect.arrayContaining([
|
|
789
|
+
null, // output
|
|
790
|
+
JSON.stringify({ query: 'search term' }),
|
|
791
|
+
JSON.stringify({ results: ['result1', 'result2'] }),
|
|
792
|
+
]),
|
|
793
|
+
'user-123'
|
|
794
|
+
)
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
it('should handle string errors', async () => {
|
|
798
|
+
mockMutate.mockResolvedValue(undefined)
|
|
799
|
+
|
|
800
|
+
await tracer.endSpan(context, traceId, spanId, { error: 'String error' })
|
|
801
|
+
|
|
802
|
+
expect(mockMutate).toHaveBeenCalledWith(
|
|
803
|
+
expect.any(String),
|
|
804
|
+
expect.arrayContaining(['error', 'String error']),
|
|
805
|
+
'user-123'
|
|
806
|
+
)
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
it('should handle database errors gracefully', async () => {
|
|
810
|
+
mockMutate.mockRejectedValue(new Error('Database error'))
|
|
811
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
812
|
+
|
|
813
|
+
await tracer.endSpan(context, traceId, spanId, { output: 'test' })
|
|
814
|
+
|
|
815
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
816
|
+
'[Tracer] Failed to end span:',
|
|
817
|
+
expect.any(Error)
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
consoleSpy.mockRestore()
|
|
821
|
+
})
|
|
822
|
+
})
|
|
823
|
+
})
|