@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,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests - Streaming Service
|
|
3
|
+
*
|
|
4
|
+
* Tests token-by-token streaming functionality:
|
|
5
|
+
* - SSE encoder creation and encoding
|
|
6
|
+
* - Stream chunk types
|
|
7
|
+
* - StreamChat generator behavior with mocks
|
|
8
|
+
*
|
|
9
|
+
* Focus: Utility functions and mocked streaming behavior.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Mock @langchain/core/messages before importing anything
|
|
13
|
+
jest.mock('@langchain/core/messages', () => ({
|
|
14
|
+
BaseMessage: class {},
|
|
15
|
+
HumanMessage: class {
|
|
16
|
+
constructor(public content: string) {}
|
|
17
|
+
},
|
|
18
|
+
AIMessage: class {
|
|
19
|
+
constructor(public content: string) {}
|
|
20
|
+
},
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
// Mock dependencies
|
|
24
|
+
jest.mock('@/contents/plugins/langchain/lib/db-memory-store', () => ({
|
|
25
|
+
dbMemoryStore: {
|
|
26
|
+
getMessages: jest.fn().mockResolvedValue([]),
|
|
27
|
+
addMessages: jest.fn().mockResolvedValue(undefined),
|
|
28
|
+
},
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
jest.mock('@/contents/plugins/langchain/lib/token-tracker', () => ({
|
|
32
|
+
tokenTracker: {
|
|
33
|
+
trackUsage: jest.fn().mockResolvedValue(undefined),
|
|
34
|
+
},
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
import { createSSEEncoder } from '@/plugins/langchain/lib/streaming'
|
|
38
|
+
import type { StreamChunk } from '@/plugins/langchain/lib/streaming'
|
|
39
|
+
|
|
40
|
+
describe('Streaming Service', () => {
|
|
41
|
+
describe('createSSEEncoder', () => {
|
|
42
|
+
let encoder: ReturnType<typeof createSSEEncoder>
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
encoder = createSSEEncoder()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('encode', () => {
|
|
49
|
+
it('should encode token chunk correctly', () => {
|
|
50
|
+
const chunk: StreamChunk = { type: 'token', content: 'Hello' }
|
|
51
|
+
const result = encoder.encode(chunk)
|
|
52
|
+
|
|
53
|
+
const decoded = new TextDecoder().decode(result)
|
|
54
|
+
expect(decoded).toBe('data: {"type":"token","content":"Hello"}\n\n')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should encode done chunk with full content', () => {
|
|
58
|
+
const chunk: StreamChunk = {
|
|
59
|
+
type: 'done',
|
|
60
|
+
fullContent: 'Complete message',
|
|
61
|
+
agentUsed: 'test-agent',
|
|
62
|
+
tokenUsage: { inputTokens: 100, outputTokens: 50, totalTokens: 150 },
|
|
63
|
+
}
|
|
64
|
+
const result = encoder.encode(chunk)
|
|
65
|
+
|
|
66
|
+
const decoded = new TextDecoder().decode(result)
|
|
67
|
+
expect(decoded).toContain('"type":"done"')
|
|
68
|
+
expect(decoded).toContain('"fullContent":"Complete message"')
|
|
69
|
+
expect(decoded).toContain('"agentUsed":"test-agent"')
|
|
70
|
+
expect(decoded).toContain('"tokenUsage"')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should encode error chunk', () => {
|
|
74
|
+
const chunk: StreamChunk = { type: 'error', error: 'Something went wrong' }
|
|
75
|
+
const result = encoder.encode(chunk)
|
|
76
|
+
|
|
77
|
+
const decoded = new TextDecoder().decode(result)
|
|
78
|
+
expect(decoded).toBe('data: {"type":"error","error":"Something went wrong"}\n\n')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should encode tool_start chunk', () => {
|
|
82
|
+
const chunk: StreamChunk = { type: 'tool_start', toolName: 'search' }
|
|
83
|
+
const result = encoder.encode(chunk)
|
|
84
|
+
|
|
85
|
+
const decoded = new TextDecoder().decode(result)
|
|
86
|
+
expect(decoded).toContain('"type":"tool_start"')
|
|
87
|
+
expect(decoded).toContain('"toolName":"search"')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should encode tool_end chunk with result', () => {
|
|
91
|
+
const chunk: StreamChunk = {
|
|
92
|
+
type: 'tool_end',
|
|
93
|
+
toolName: 'calculator',
|
|
94
|
+
result: { value: 42 },
|
|
95
|
+
}
|
|
96
|
+
const result = encoder.encode(chunk)
|
|
97
|
+
|
|
98
|
+
const decoded = new TextDecoder().decode(result)
|
|
99
|
+
expect(decoded).toContain('"type":"tool_end"')
|
|
100
|
+
expect(decoded).toContain('"toolName":"calculator"')
|
|
101
|
+
expect(decoded).toContain('"result":{"value":42}')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should handle special characters in content', () => {
|
|
105
|
+
const chunk: StreamChunk = {
|
|
106
|
+
type: 'token',
|
|
107
|
+
content: 'Hello\nWorld\t"Special" chars: é ñ 中文',
|
|
108
|
+
}
|
|
109
|
+
const result = encoder.encode(chunk)
|
|
110
|
+
|
|
111
|
+
const decoded = new TextDecoder().decode(result)
|
|
112
|
+
expect(decoded).toContain('data: ')
|
|
113
|
+
expect(decoded).toContain('\\n') // Escaped newline
|
|
114
|
+
expect(decoded).toContain('\\t') // Escaped tab
|
|
115
|
+
expect(decoded).toContain('\\"') // Escaped quote
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should handle empty content', () => {
|
|
119
|
+
const chunk: StreamChunk = { type: 'token', content: '' }
|
|
120
|
+
const result = encoder.encode(chunk)
|
|
121
|
+
|
|
122
|
+
const decoded = new TextDecoder().decode(result)
|
|
123
|
+
expect(decoded).toBe('data: {"type":"token","content":""}\n\n')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should return Uint8Array', () => {
|
|
127
|
+
const chunk: StreamChunk = { type: 'token', content: 'test' }
|
|
128
|
+
const result = encoder.encode(chunk)
|
|
129
|
+
|
|
130
|
+
expect(result).toBeInstanceOf(Uint8Array)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('encodeDone', () => {
|
|
135
|
+
it('should encode done marker', () => {
|
|
136
|
+
const result = encoder.encodeDone()
|
|
137
|
+
|
|
138
|
+
const decoded = new TextDecoder().decode(result)
|
|
139
|
+
expect(decoded).toBe('data: [DONE]\n\n')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should return Uint8Array', () => {
|
|
143
|
+
const result = encoder.encodeDone()
|
|
144
|
+
|
|
145
|
+
expect(result).toBeInstanceOf(Uint8Array)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('StreamChunk types', () => {
|
|
151
|
+
it('should support token chunk type', () => {
|
|
152
|
+
const chunk: StreamChunk = { type: 'token', content: 'text' }
|
|
153
|
+
expect(chunk.type).toBe('token')
|
|
154
|
+
expect(chunk.content).toBe('text')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should support done chunk type with all fields', () => {
|
|
158
|
+
const chunk: StreamChunk = {
|
|
159
|
+
type: 'done',
|
|
160
|
+
fullContent: 'Full response',
|
|
161
|
+
agentUsed: 'assistant',
|
|
162
|
+
tokenUsage: {
|
|
163
|
+
inputTokens: 100,
|
|
164
|
+
outputTokens: 200,
|
|
165
|
+
totalTokens: 300,
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
expect(chunk.type).toBe('done')
|
|
170
|
+
expect(chunk.fullContent).toBe('Full response')
|
|
171
|
+
expect(chunk.agentUsed).toBe('assistant')
|
|
172
|
+
expect(chunk.tokenUsage?.totalTokens).toBe(300)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should support done chunk type with minimal fields', () => {
|
|
176
|
+
const chunk: StreamChunk = {
|
|
177
|
+
type: 'done',
|
|
178
|
+
fullContent: 'Response',
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
expect(chunk.type).toBe('done')
|
|
182
|
+
expect(chunk.fullContent).toBe('Response')
|
|
183
|
+
expect(chunk.agentUsed).toBeUndefined()
|
|
184
|
+
expect(chunk.tokenUsage).toBeUndefined()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should support error chunk type', () => {
|
|
188
|
+
const chunk: StreamChunk = { type: 'error', error: 'Error message' }
|
|
189
|
+
expect(chunk.type).toBe('error')
|
|
190
|
+
expect(chunk.error).toBe('Error message')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should support tool_start chunk type', () => {
|
|
194
|
+
const chunk: StreamChunk = { type: 'tool_start', toolName: 'search' }
|
|
195
|
+
expect(chunk.type).toBe('tool_start')
|
|
196
|
+
expect(chunk.toolName).toBe('search')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should support tool_end chunk type', () => {
|
|
200
|
+
const chunk: StreamChunk = {
|
|
201
|
+
type: 'tool_end',
|
|
202
|
+
toolName: 'search',
|
|
203
|
+
result: ['result1', 'result2'],
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
expect(chunk.type).toBe('tool_end')
|
|
207
|
+
expect(chunk.toolName).toBe('search')
|
|
208
|
+
expect(chunk.result).toEqual(['result1', 'result2'])
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe('SSE format compliance', () => {
|
|
213
|
+
it('should produce valid SSE format with data prefix', () => {
|
|
214
|
+
const encoder = createSSEEncoder()
|
|
215
|
+
const chunk: StreamChunk = { type: 'token', content: 'test' }
|
|
216
|
+
const result = new TextDecoder().decode(encoder.encode(chunk))
|
|
217
|
+
|
|
218
|
+
expect(result.startsWith('data: ')).toBe(true)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should end with double newline', () => {
|
|
222
|
+
const encoder = createSSEEncoder()
|
|
223
|
+
const chunk: StreamChunk = { type: 'token', content: 'test' }
|
|
224
|
+
const result = new TextDecoder().decode(encoder.encode(chunk))
|
|
225
|
+
|
|
226
|
+
expect(result.endsWith('\n\n')).toBe(true)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('should produce valid JSON in data field', () => {
|
|
230
|
+
const encoder = createSSEEncoder()
|
|
231
|
+
const chunk: StreamChunk = { type: 'token', content: 'test' }
|
|
232
|
+
const result = new TextDecoder().decode(encoder.encode(chunk))
|
|
233
|
+
|
|
234
|
+
// Extract JSON from SSE format
|
|
235
|
+
const jsonStr = result.slice(6, -2) // Remove 'data: ' prefix and '\n\n' suffix
|
|
236
|
+
expect(() => JSON.parse(jsonStr)).not.toThrow()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('should handle multiple chunks correctly', () => {
|
|
240
|
+
const encoder = createSSEEncoder()
|
|
241
|
+
const chunks: StreamChunk[] = [
|
|
242
|
+
{ type: 'token', content: 'Hello' },
|
|
243
|
+
{ type: 'token', content: ' ' },
|
|
244
|
+
{ type: 'token', content: 'World' },
|
|
245
|
+
{ type: 'done', fullContent: 'Hello World' },
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
const results = chunks.map(chunk => new TextDecoder().decode(encoder.encode(chunk)))
|
|
249
|
+
|
|
250
|
+
results.forEach(result => {
|
|
251
|
+
expect(result.startsWith('data: ')).toBe(true)
|
|
252
|
+
expect(result.endsWith('\n\n')).toBe(true)
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
describe('Edge cases', () => {
|
|
258
|
+
it('should handle very long content', () => {
|
|
259
|
+
const encoder = createSSEEncoder()
|
|
260
|
+
const longContent = 'a'.repeat(100000)
|
|
261
|
+
const chunk: StreamChunk = { type: 'token', content: longContent }
|
|
262
|
+
|
|
263
|
+
const result = encoder.encode(chunk)
|
|
264
|
+
const decoded = new TextDecoder().decode(result)
|
|
265
|
+
|
|
266
|
+
expect(decoded).toContain(longContent)
|
|
267
|
+
expect(decoded.startsWith('data: ')).toBe(true)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('should handle unicode content', () => {
|
|
271
|
+
const encoder = createSSEEncoder()
|
|
272
|
+
const unicodeContent = '你好世界 🌍 مرحبا العالم'
|
|
273
|
+
const chunk: StreamChunk = { type: 'token', content: unicodeContent }
|
|
274
|
+
|
|
275
|
+
const result = encoder.encode(chunk)
|
|
276
|
+
const decoded = new TextDecoder().decode(result)
|
|
277
|
+
|
|
278
|
+
// Parse the JSON to verify unicode is preserved
|
|
279
|
+
const jsonStr = decoded.slice(6, -2)
|
|
280
|
+
const parsed = JSON.parse(jsonStr)
|
|
281
|
+
expect(parsed.content).toBe(unicodeContent)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('should handle newlines in content', () => {
|
|
285
|
+
const encoder = createSSEEncoder()
|
|
286
|
+
const chunk: StreamChunk = {
|
|
287
|
+
type: 'token',
|
|
288
|
+
content: 'Line 1\nLine 2\nLine 3',
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const result = encoder.encode(chunk)
|
|
292
|
+
const decoded = new TextDecoder().decode(result)
|
|
293
|
+
|
|
294
|
+
// Newlines should be escaped in JSON
|
|
295
|
+
expect(decoded).toContain('\\n')
|
|
296
|
+
expect(decoded.split('\n').length).toBe(3) // data: {...}\n\n = 3 parts
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('should handle null result in tool_end', () => {
|
|
300
|
+
const encoder = createSSEEncoder()
|
|
301
|
+
const chunk: StreamChunk = {
|
|
302
|
+
type: 'tool_end',
|
|
303
|
+
toolName: 'void_tool',
|
|
304
|
+
result: null,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const result = encoder.encode(chunk)
|
|
308
|
+
const decoded = new TextDecoder().decode(result)
|
|
309
|
+
|
|
310
|
+
expect(decoded).toContain('"result":null')
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('should handle complex nested result in tool_end', () => {
|
|
314
|
+
const encoder = createSSEEncoder()
|
|
315
|
+
const chunk: StreamChunk = {
|
|
316
|
+
type: 'tool_end',
|
|
317
|
+
toolName: 'complex_tool',
|
|
318
|
+
result: {
|
|
319
|
+
nested: {
|
|
320
|
+
array: [1, 2, { deep: 'value' }],
|
|
321
|
+
string: 'text',
|
|
322
|
+
},
|
|
323
|
+
number: 42,
|
|
324
|
+
},
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const result = encoder.encode(chunk)
|
|
328
|
+
const decoded = new TextDecoder().decode(result)
|
|
329
|
+
const jsonStr = decoded.slice(6, -2)
|
|
330
|
+
const parsed = JSON.parse(jsonStr)
|
|
331
|
+
|
|
332
|
+
expect(parsed.result.nested.array).toHaveLength(3)
|
|
333
|
+
expect(parsed.result.nested.array[2].deep).toBe('value')
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe('Performance characteristics', () => {
|
|
338
|
+
it('should be efficient for single chunk encoding', () => {
|
|
339
|
+
const encoder = createSSEEncoder()
|
|
340
|
+
const chunk: StreamChunk = { type: 'token', content: 'test' }
|
|
341
|
+
|
|
342
|
+
const start = performance.now()
|
|
343
|
+
for (let i = 0; i < 1000; i++) {
|
|
344
|
+
encoder.encode(chunk)
|
|
345
|
+
}
|
|
346
|
+
const duration = performance.now() - start
|
|
347
|
+
|
|
348
|
+
// Should encode 1000 chunks in less than 100ms
|
|
349
|
+
expect(duration).toBeLessThan(100)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('should reuse encoder instance efficiently', () => {
|
|
353
|
+
const encoder = createSSEEncoder()
|
|
354
|
+
const chunks: StreamChunk[] = Array.from({ length: 100 }, (_, i) => ({
|
|
355
|
+
type: 'token' as const,
|
|
356
|
+
content: `Token ${i}`,
|
|
357
|
+
}))
|
|
358
|
+
|
|
359
|
+
const results = chunks.map(chunk => encoder.encode(chunk))
|
|
360
|
+
|
|
361
|
+
expect(results).toHaveLength(100)
|
|
362
|
+
results.forEach(result => {
|
|
363
|
+
expect(result).toBeInstanceOf(Uint8Array)
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
})
|
|
367
|
+
})
|