package stringutil import ( "fmt" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // --- Test Helper Functions --- func simpleTokenizer(text string) int { // 1 word = 1 token for simplicity in testing return len(strings.Fields(text)) } func newTestConfig(max, min, overlap int) ChunkConfig { return ChunkConfig{ MaxTokens: max, MinTokens: min, OverlapTokens: overlap, TokenizerFunc: simpleTokenizer, PreserveBlocks: []string{"pre", "code", "table"}, } } func generateHTML(tag, content string, count int) string { var b strings.Builder for i := 0; i < count; i++ { b.WriteString(fmt.Sprintf("<%s>%s %d%s>\n", tag, content, i, tag)) } return b.String() } // --- Test Cases --- func TestChunkHTMLContent_Basic(t *testing.T) { testCases := []struct { name string title string html string config ChunkConfig expectedChunks int expectedError string validate func(*testing.T, []string) }{ { name: "Empty Content", title: "Empty Test", html: " ", config: DefaultChunkConfig(), expectedChunks: 1, validate: func(t *testing.T, chunks []string) { assert.Equal(t, "Empty Test", chunks[0]) }, }, { name: "Title Only with Empty HTML", title: "Title Only", html: "", config: DefaultChunkConfig(), expectedChunks: 1, validate: func(t *testing.T, chunks []string) { assert.Equal(t, "Title Only", chunks[0]) }, }, { name: "Single Chunk Scenario", title: "Single Chunk", html: "
hello
", tc.config) if tc.expectedError != "" { require.Error(t, err) assert.Contains(t, err.Error(), tc.expectedError) } else { require.NoError(t, err) } }) } } func TestChunkHTMLContent_EdgeCases(t *testing.T) { testCases := []struct { name string html string config ChunkConfig expectedChunks int expectedError string validate func(*testing.T, []string) }{ { name: "Malformed HTML", html: "This is a code block that should not be split. It contains many words to exceed the token limit if it were normal text. one two three four five six seven eight nine ten eleven twelve thirteen.
Some paragraph text.
var x = 1;
| data |
All API requests require authentication using an API key in the header:
Authorization: Bearer your_api_key_here
Creates a new user account in the system.
POST /api/v1/users
| Parameter | Type | Required | Description |
|---|---|---|---|
| string | Yes | User's email address | |
| first_name | string | Yes | User's first name |
| last_name | string | No | User's last name |
| role | string | No | User role: admin, agent, or user |
{
"email": "john.doe@example.com",
"first_name": "John",
"last_name": "Doe",
"role": "agent"
}
Returns the created user object:
{
"id": 123,
"email": "john.doe@example.com",
"first_name": "John",
"last_name": "Doe",
"role": "agent",
"created_at": "2025-01-15T10:30:00Z"
}`,
"troubleshooting_guide": `
Follow these steps to diagnose and fix email delivery issues:
If Step 1 doesn't resolve the issue:
Still having issues? Try these advanced steps:
Welcome to our platform! Getting started is easy.
Important: Please read all instructions carefully.
Check your inbox for a verification email. Note: It may take up to 5 minutes to arrive.
If you don't see it, check your spam folder.
"Make sure to complete verification within 24 hours, or you'll need to register again."`, "legal_wall_text": `
These Terms of Service ("Terms") govern your use of our website and services. By accessing or using our services, you agree to be bound by these Terms. If you disagree with any part of these terms, then you may not access the service. This Terms of Service agreement for our service has been created with the help of legal counsel and covers all the important aspects of using our platform. We reserve the right to update and change the Terms of Service from time to time without notice. Any new features that augment or enhance the current service, including the release of new tools and resources, shall be subject to the Terms of Service. Continued use of the service after any such changes shall constitute your consent to such changes. You can review the most current version of the Terms of Service at any time by visiting this page. We reserve the right to update and change the Terms of Service from time to time without notice. Any new features that augment or enhance the current service, including the release of new tools and resources, shall be subject to the Terms of Service. Violation of any of the terms below will result in the termination of your account and your access to the service. While we prohibit such conduct and content on the service, you understand and agree that we cannot be responsible for the content posted on the service and you nonetheless may be exposed to such materials. You agree to use the service at your own risk.
You must be 13 years or older to use this service. You must be human and you must provide us with accurate information when you register for an account. Your login may only be used by one person and a single login shared by multiple people is not permitted. You are responsible for maintaining the security of your account and password. The company cannot and will not be liable for any loss or damage from your failure to comply with this security obligation. You are responsible for all content posted and all actions taken with your account. We reserve the right to refuse service to anyone for any reason at any time. We reserve the right to force forfeiture of any username that becomes inactive, violates trademark, or may mislead other users.
`, "feature_comparison_table": `Choose the plan that best fits your business needs:
| Feature | Starter $29/month |
Professional $79/month |
Enterprise $199/month |
|---|---|---|---|
| Support Agents | Up to 3 | Up to 10 | Unlimited |
| Monthly Conversations | 500 | 5,000 | Unlimited |
| Email Support | ✓ | ✓ | ✓ |
| Live Chat Widget | ✓ | ✓ | ✓ |
| Knowledge Base | Basic | Advanced | Advanced + AI |
| Custom Branding | × | ✓ | ✓ |
| Advanced Analytics | × | ✓ | ✓ |
| API Access | × | Basic | Full Access |
| Priority Support | × | × | ✓ |
This is not really a quote but we're using blockquote for styling
Some normal paragraph text here.
| Table cell used as heading |
| Regular table content here |
npm install
Edit config.json
npm start
npm test
Push to production
PostgreSQL
Redis
S3
Datadog
Sentry
`, "release_notes": `Follow these visual steps to add the chat widget to your website:
Navigate to Admin > Channels > Live Chat and click on your chat channel.
In the Widget Code section, click the "Copy Code" button to copy the JavaScript snippet.
Paste the code just before the closing </body> tag in your website's HTML.
Visit your website and verify the chat widget appears in the bottom right corner.
Back in the admin panel, you can customize the widget's color, position, and welcome message.
`, "nested_lists": `Find answers to common questions about our platform:
Welcome to the most comprehensive guide for setting up your customer support platform. This guide covers everything you need to know.
Pro Tip: Bookmark this page for easy reference during setup!
First things first - let's get your account properly configured:
| Setting | Recommended Value | Notes |
|---|---|---|
| Session timeout | 30 minutes | Balances security and usability |
| Auto-save interval | 30 seconds | Prevents data loss |
| Language | Auto-detect | Based on user browser |
{
"company": {
"name": "Acme Corp",
"timezone": "America/New_York",
"business_hours": {
"start": "09:00",
"end": "17:00",
"days": ["monday", "tuesday", "wednesday", "thursday", "friday"]
}
}
}
Add your team members and configure their roles:
These settings are for power users who need fine-grained control:
If you get stuck during setup, we're here to help:
This documentation covers the REST API endpoints for our platform.
All API requests require authentication using an API key:
curl -H "Authorization: Bearer YOUR_API_KEY" https://api.example.com/v1/users
API requests are limited to 1000 requests per hour per API key.
X-RateLimit-Limit: The rate limit ceiling for your API keyX-RateLimit-Remaining: The number of requests left for the time windowX-RateLimit-Reset: The UTC date/time when the rate limit resetsThe API returns standard HTTP status codes:
200 - Success400 - Bad Request401 - Unauthorized404 - Not Found500 - Internal Server Error{
"error": {
"code": "VALIDATION_ERROR",
"message": "The email field is required.",
"details": {
"field": "email",
"code": "required"
}
}
}
GET /v1/users
Returns a paginated list of users.
page (integer, optional): Page number, defaults to 1limit (integer, optional): Items per page, defaults to 20, max 100role (string, optional): Filter by user roleThis interactive guide walks you through our customer onboarding process:
Make sure you have admin access to customize the onboarding flow.
Configure the first message customers see when they sign up:
Welcome to [Company Name]!
We're excited to have you on board.
Let's get you set up in just a few minutes.
Keep welcome messages short and friendly. Long text can overwhelm new users.
Gather essential information from new customers:
Best Practice: Only ask for information you'll actually use. Each additional field reduces completion rates.
Introduce key features through guided tours:
Allow users to skip tours and return to them later. Not everyone learns the same way!
Below is the complete configuration file for our application. Copy this to your config.toml file:
# LibreDesk Configuration File
# This file contains all configuration options for the application
[app]
name = "LibreDesk"
version = "0.9.0"
environment = "production"
debug = false
log_level = "info"
[server]
host = "0.0.0.0"
port = 8080
read_timeout = "30s"
write_timeout = "30s"
idle_timeout = "120s"
max_header_bytes = 1048576
[database]
driver = "postgres"
host = "localhost"
port = 5432
name = "libredesk"
user = "postgres"
password = "your_password_here"
sslmode = "disable"
max_open_connections = 25
max_idle_connections = 5
connection_max_lifetime = "1h"
[redis]
host = "localhost"
port = 6379
password = ""
database = 0
max_retries = 3
pool_size = 10
[email]
driver = "smtp"
host = "smtp.gmail.com"
port = 587
username = "your_email@gmail.com"
password = "your_app_password"
from_address = "noreply@yourcompany.com"
from_name = "Your Company Support"
[storage]
driver = "local"
local_path = "./uploads"
max_file_size = "10MB"
allowed_extensions = ["jpg", "jpeg", "png", "gif", "pdf", "doc", "docx"]
[jwt]
secret = "your_super_secret_jwt_key_here"
expiry = "24h"
refresh_expiry = "168h"
[webhook]
queue_size = 1000
concurrency = 5
timeout = "10s"
retry_attempts = 3
retry_delay = "1s"
[ai]
provider = "openai"
api_key = "your_openai_api_key"
model = "gpt-4"
max_tokens = 1000
temperature = 0.7
system_prompt = "You are a helpful customer support assistant."
[embedding]
provider = "openai"
model = "text-embedding-ada-002"
dimensions = 1536
batch_size = 100
[search]
engine = "postgresql"
min_score = 0.5
max_results = 10
boost_title = 2.0
boost_content = 1.0
[monitoring]
enabled = true
metrics_endpoint = "/metrics"
health_endpoint = "/health"
profiler_enabled = false
[rate_limiting]
enabled = true
requests_per_minute = 60
burst_size = 100
cleanup_interval = "1m"
[cors]
allowed_origins = ["http://localhost:3000", "https://yourcompany.com"]
allowed_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allowed_headers = ["Content-Type", "Authorization", "X-Requested-With"]
exposed_headers = ["X-Total-Count"]
allow_credentials = true
max_age = "12h"
[security]
bcrypt_cost = 12
session_timeout = "30m"
max_login_attempts = 5
lockout_duration = "15m"
require_https = true
csrf_protection = true
[notifications]
email_enabled = true
webhook_enabled = true
slack_enabled = false
discord_enabled = false
[limits]
max_conversations_per_contact = 1000
max_messages_per_conversation = 10000
max_attachments_per_message = 5
max_tags_per_conversation = 10
max_custom_attributes = 50
After updating your configuration file, restart the application to apply the changes:
sudo systemctl restart libredesk`,
}
}
func TestRealWorldScenarios(t *testing.T) {
samples := getRealWorldHTMLSamples()
config := newTestConfig(150, 50, 15) // Use smaller limits to trigger chunking with word-based tokenizer
testCases := []struct {
name string
htmlKey string
expectedMinChunks int
expectedMaxChunks int
validationCallback func(*testing.T, []string, string)
}{
{
name: "API Reference Manual",
htmlKey: "api_reference",
expectedMinChunks: 1,
expectedMaxChunks: 8,
validationCallback: func(t *testing.T, chunks []string, scenario string) {
hasCodeChunk := false
hasTableChunk := false
for _, chunk := range chunks {
if strings.Contains(chunk, "Bearer") {
hasCodeChunk = true
}
if strings.Contains(chunk, "Parameter") {
hasTableChunk = true
}
}
assert.True(t, hasCodeChunk, "API reference should have at least one code chunk")
assert.True(t, hasTableChunk, "API reference should have at least one table chunk")
},
},
{
name: "Troubleshooting Guide",
htmlKey: "troubleshooting_guide",
expectedMinChunks: 2,
expectedMaxChunks: 6,
validationCallback: func(t *testing.T, chunks []string, scenario string) {
assert.True(t, len(chunks) >= 2, "Troubleshooting guide should split into multiple logical sections")
for i, chunk := range chunks {
tokens := simpleTokenizer(chunk)
assert.True(t, tokens > 0, "Chunk %d should have content", i)
}
},
},
{
name: "WYSIWYG Nightmare",
htmlKey: "wysiwyg_nightmare",
expectedMinChunks: 1,
expectedMaxChunks: 4,
validationCallback: func(t *testing.T, chunks []string, scenario string) {
assert.True(t, len(chunks) >= 1, "WYSIWYG content should create at least one chunk")
for i, chunk := range chunks {
assert.NotEmpty(t, strings.TrimSpace(chunk), "Chunk %d should not be empty after HTML cleanup", i)
}
},
},
{
name: "Legal Wall of Text",
htmlKey: "legal_wall_text",
expectedMinChunks: 1,
expectedMaxChunks: 3,
validationCallback: func(t *testing.T, chunks []string, scenario string) {
if len(chunks) > 1 {
for i, chunk := range chunks {
tokens := simpleTokenizer(chunk)
assert.True(t, tokens <= config.MaxTokens*2, "Chunk %d should not be excessively large (%d tokens)", i, tokens)
}
}
},
},
{
name: "Feature Comparison Table",
htmlKey: "feature_comparison_table",
expectedMinChunks: 1,
expectedMaxChunks: 4,
validationCallback: func(t *testing.T, chunks []string, scenario string) {
hasTable := false
for _, chunk := range chunks {
if strings.Contains(chunk, "Support Agents") {
hasTable = true
}
}
assert.True(t, hasTable, "Feature comparison should have table content")
},
},
{
name: "Poorly Structured HTML",
htmlKey: "poorly_structured_html",
expectedMinChunks: 1,
expectedMaxChunks: 6,
validationCallback: func(t *testing.T, chunks []string, scenario string) {
assert.True(t, len(chunks) >= 1, "Should handle poorly structured HTML")
for i, chunk := range chunks {
assert.NotEmpty(t, chunk, "Chunk %d should have content", i)
}
},
},
{
name: "Minimalist Haiku",
htmlKey: "minimalist_haiku",
expectedMinChunks: 1,
expectedMaxChunks: 3,
validationCallback: func(t *testing.T, chunks []string, scenario string) {
totalTokens := 0
for _, chunk := range chunks {
totalTokens += simpleTokenizer(chunk)
}
assert.True(t, totalTokens > 0, "Should have some content")
},
},
{
name: "Release Notes",
htmlKey: "release_notes",
expectedMinChunks: 2,
expectedMaxChunks: 6,
validationCallback: func(t *testing.T, chunks []string, scenario string) {
hasHeadings := false
for _, chunk := range chunks {
if strings.Contains(chunk, "Section:") {
hasHeadings = true
}
}
assert.True(t, hasHeadings, "Release notes should maintain heading structure")
},
},
{
name: "Image Heavy Guide",
htmlKey: "image_heavy_guide",
expectedMinChunks: 2,
expectedMaxChunks: 8,
validationCallback: func(t *testing.T, chunks []string, scenario string) {
stepCount := 0
for _, chunk := range chunks {
if strings.Contains(chunk, "Step") {
stepCount++
}
}
assert.True(t, stepCount >= 1, "Should preserve step-based structure")
},
},
{
name: "Nested Lists",
htmlKey: "nested_lists",
expectedMinChunks: 2,
expectedMaxChunks: 6,
validationCallback: func(t *testing.T, chunks []string, scenario string) {
for i, chunk := range chunks {
assert.NotEmpty(t, chunk, "Chunk %d should have meaningful content", i)
}
},
},
{
name: "FAQ Description Lists",
htmlKey: "faq_description_lists",
expectedMinChunks: 2,
expectedMaxChunks: 6,
validationCallback: func(t *testing.T, chunks []string, scenario string) {
hasDescriptionList := false
for _, chunk := range chunks {
if strings.Contains(chunk, "subscription plan") {
hasDescriptionList = true
}
}
assert.True(t, hasDescriptionList, "Should preserve description list structure")
},
},
{
name: "Kitchen Sink",
htmlKey: "kitchen_sink",
expectedMinChunks: 3,
expectedMaxChunks: 10,
validationCallback: func(t *testing.T, chunks []string, scenario string) {
hasHeading := false
hasCode := false
hasTable := false
for _, chunk := range chunks {
if strings.Contains(chunk, "Section:") {
hasHeading = true
}
if strings.Contains(chunk, "Acme Corp") {
hasCode = true
}
if strings.Contains(chunk, "Session timeout") {
hasTable = true
}
}
assert.True(t, hasHeading, "Kitchen sink should have headings")
assert.True(t, hasCode, "Kitchen sink should have code")
assert.True(t, hasTable, "Kitchen sink should have tables")
},
},
{
name: "Markdown Import",
htmlKey: "markdown_import",
expectedMinChunks: 2,
expectedMaxChunks: 8,
validationCallback: func(t *testing.T, chunks []string, scenario string) {
hasCode := false
for _, chunk := range chunks {
if strings.Contains(chunk, "YOUR_API_KEY") {
hasCode = true
}
}
assert.True(t, hasCode, "Markdown import should preserve code blocks")
},
},
{
name: "Interactive Transcript",
htmlKey: "interactive_transcript",
expectedMinChunks: 2,
expectedMaxChunks: 6,
validationCallback: func(t *testing.T, chunks []string, scenario string) {
for i, chunk := range chunks {
assert.NotEmpty(t, strings.TrimSpace(chunk), "Chunk %d should have content", i)
}
},
},
{
name: "Giant Code Block",
htmlKey: "giant_code_block",
expectedMinChunks: 1,
expectedMaxChunks: 3,
validationCallback: func(t *testing.T, chunks []string, scenario string) {
hasCodeBlocks := false
for _, chunk := range chunks {
if strings.Contains(chunk, "LibreDesk Configuration") {
hasCodeBlocks = true
}
}
assert.True(t, hasCodeBlocks, "Should have code blocks")
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
html, exists := samples[tc.htmlKey]
require.True(t, exists, "Test HTML sample %s should exist", tc.htmlKey)
chunks, err := ChunkHTMLContent(tc.name, html, config)
require.NoError(t, err, "Chunking should not fail for %s", tc.name)
// Basic validation
assert.GreaterOrEqual(t, len(chunks), tc.expectedMinChunks,
"Should have at least %d chunks for %s", tc.expectedMinChunks, tc.name)
assert.LessOrEqual(t, len(chunks), tc.expectedMaxChunks,
"Should have at most %d chunks for %s", tc.expectedMaxChunks, tc.name)
for _, chunk := range chunks {
assert.NotEmpty(t, chunk, "Chunk text should not be empty")
assert.Contains(t, chunk, tc.name, "Chunk should contain title")
}
totalTokens := 0
for i, chunk := range chunks {
tokens := simpleTokenizer(chunk)
totalTokens += tokens
t.Logf("Chunk %d: %d tokens", i, tokens)
}
// Scenario-specific validation
if tc.validationCallback != nil {
tc.validationCallback(t, chunks, tc.name)
}
t.Logf("Scenario '%s': %d chunks, %d total tokens", tc.name, len(chunks), totalTokens)
})
}
}
// TestChunkHTMLContent_ConfigurableTokenLimits tests that custom token limits work correctly
func TestChunkHTMLContent_ConfigurableTokenLimits(t *testing.T) {
// Large HTML content with table and code
largeHTML := `
This is a comprehensive guide to our API endpoints with detailed examples.
| Endpoint | Method | Description | Parameters | Response |
|---|---|---|---|---|
| /api/users | GET | Get all users | page, limit | JSON array of users |
| /api/users/{id} | GET | Get user by ID | id (path) | JSON user object |
| /api/users | POST | Create new user | name, email, role | Created user object |
| /api/users/{id} | PUT | Update user | id (path), name, email, role | Updated user object |
| /api/users/{id} | DELETE | Delete user | id (path) | Success message |
All API endpoints require authentication using JWT tokens in the Authorization header.
curl -H "Authorization: Bearer YOUR_TOKEN" -X GET https://api.example.com/users
Additional content to make this chunk larger and test the token limits effectively.
` // Test with default config (smaller chunks) defaultChunks, err := ChunkHTMLContent("API Guide", largeHTML) require.NoError(t, err) // Test with larger token config (should create fewer, larger chunks) largeConfig := ChunkConfig{ MaxTokens: 2000, // Much larger than default 700 MinTokens: 400, // Larger than default 200 OverlapTokens: 150, // Larger than default 75 TokenizerFunc: simpleTokenizer, PreserveBlocks: []string{"pre", "code", "table"}, } largeChunks, err := ChunkHTMLContent("API Guide", largeHTML, largeConfig) require.NoError(t, err) // Verify that larger config creates fewer chunks assert.True(t, len(largeChunks) <= len(defaultChunks), "Larger token config should create fewer or equal chunks. Default: %d, Large: %d", len(defaultChunks), len(largeChunks)) t.Logf("Default config: %d chunks, Large config: %d chunks", len(defaultChunks), len(largeChunks)) }