967 lines
36 KiB
TypeScript
967 lines
36 KiB
TypeScript
import { afterAll, describe, expect, it } from 'vitest'
|
|
import { getClient, getZai, metadata } from './utils'
|
|
import { TableAdapter } from '../src/adapters/botpress-table'
|
|
|
|
describe('zai.sort', () => {
|
|
const zai = getZai()
|
|
|
|
describe('animal sorting', () => {
|
|
it('should sort animals by speed (ascending - slowest to fastest)', async () => {
|
|
const animals = [
|
|
{ name: 'Cheetah', type: 'land' },
|
|
{ name: 'Sloth', type: 'land' },
|
|
{ name: 'Horse', type: 'land' },
|
|
{ name: 'Snail', type: 'land' },
|
|
{ name: 'Human', type: 'land' },
|
|
{ name: 'Turtle', type: 'land' },
|
|
{ name: 'Lion', type: 'land' },
|
|
{ name: 'Elephant', type: 'land' },
|
|
]
|
|
|
|
const sorted = await zai.sort(animals, 'from slowest to fastest')
|
|
|
|
// Verify array has same length
|
|
expect(sorted).toHaveLength(animals.length)
|
|
|
|
// Verify all elements are present
|
|
expect(sorted.map((a) => a.name).sort()).toEqual(animals.map((a) => a.name).sort())
|
|
|
|
// Verify order: slowest animals should be first
|
|
const slowestIndex = sorted.findIndex((a) => a.name === 'Sloth' || a.name === 'Snail')
|
|
const fastestIndex = sorted.findIndex((a) => a.name === 'Cheetah')
|
|
expect(slowestIndex).toBeLessThan(fastestIndex)
|
|
|
|
// Cheetah should be last (fastest)
|
|
const cheetahIndex = sorted.findIndex((a) => a.name === 'Cheetah')
|
|
expect(cheetahIndex).toBeGreaterThan(sorted.length / 2)
|
|
})
|
|
|
|
it('should sort animals by speed (descending - fastest to slowest)', async () => {
|
|
const animals = [
|
|
{ name: 'Snail', type: 'land' },
|
|
{ name: 'Cheetah', type: 'land' },
|
|
{ name: 'Turtle', type: 'land' },
|
|
{ name: 'Horse', type: 'land' },
|
|
{ name: 'Human', type: 'land' },
|
|
{ name: 'Sloth', type: 'land' },
|
|
{ name: 'Lion', type: 'land' },
|
|
]
|
|
|
|
const sorted = await zai.sort(animals, 'from fastest to slowest')
|
|
|
|
// Verify array has same length
|
|
expect(sorted).toHaveLength(animals.length)
|
|
|
|
// Cheetah should be first (fastest)
|
|
const cheetahIndex = sorted.findIndex((a) => a.name === 'Cheetah')
|
|
expect(cheetahIndex).toBeLessThan(sorted.length / 2)
|
|
|
|
// Sloth or Snail should be last
|
|
const lastAnimal = sorted[sorted.length - 1]
|
|
expect(['Sloth', 'Snail', 'Turtle']).toContain(lastAnimal.name)
|
|
})
|
|
|
|
it('should sort animals by danger level (ascending - least to most dangerous)', async () => {
|
|
const animals = [
|
|
{ name: 'Grizzly Bear', habitat: 'forest' },
|
|
{ name: 'Rabbit', habitat: 'grassland' },
|
|
{ name: 'Hippopotamus', habitat: 'river' },
|
|
{ name: 'Deer', habitat: 'forest' },
|
|
{ name: 'Great White Shark', habitat: 'ocean' },
|
|
{ name: 'Butterfly', habitat: 'garden' },
|
|
{ name: 'Crocodile', habitat: 'swamp' },
|
|
{ name: 'Lion', habitat: 'savanna' },
|
|
{ name: 'Lamb', habitat: 'farm' },
|
|
]
|
|
|
|
const sorted = await zai.sort(animals, 'from least dangerous to most dangerous')
|
|
|
|
// Verify array has same length
|
|
expect(sorted).toHaveLength(animals.length)
|
|
|
|
// Harmless animals should be first
|
|
const harmlessIndex = Math.max(
|
|
sorted.findIndex((a) => a.name === 'Rabbit'),
|
|
sorted.findIndex((a) => a.name === 'Butterfly'),
|
|
sorted.findIndex((a) => a.name === 'Lamb')
|
|
)
|
|
|
|
// Dangerous animals should be last
|
|
const dangerousIndex = Math.min(
|
|
sorted.findIndex((a) => a.name === 'Great White Shark'),
|
|
sorted.findIndex((a) => a.name === 'Grizzly Bear'),
|
|
sorted.findIndex((a) => a.name === 'Crocodile')
|
|
)
|
|
|
|
expect(harmlessIndex).toBeLessThan(dangerousIndex)
|
|
|
|
// Most dangerous should be in second half
|
|
const sharkIndex = sorted.findIndex((a) => a.name === 'Great White Shark')
|
|
expect(sharkIndex).toBeGreaterThan(sorted.length / 2)
|
|
})
|
|
|
|
it('should sort animals by danger level (descending - most to least dangerous)', async () => {
|
|
const animals = [
|
|
{ name: 'Rabbit', type: 'mammal' },
|
|
{ name: 'Lion', type: 'mammal' },
|
|
{ name: 'Butterfly', type: 'insect' },
|
|
{ name: 'Crocodile', type: 'reptile' },
|
|
{ name: 'Deer', type: 'mammal' },
|
|
{ name: 'Cobra', type: 'reptile' },
|
|
{ name: 'Squirrel', type: 'mammal' },
|
|
]
|
|
|
|
const sorted = await zai.sort(animals, 'from most dangerous to least dangerous')
|
|
|
|
// Verify array has same length
|
|
expect(sorted).toHaveLength(animals.length)
|
|
|
|
// Dangerous animals should be first
|
|
const firstAnimal = sorted[0]
|
|
expect(['Lion', 'Crocodile', 'Cobra']).toContain(firstAnimal.name)
|
|
|
|
// Harmless animals should be last
|
|
const lastAnimal = sorted[sorted.length - 1]
|
|
expect(['Rabbit', 'Butterfly', 'Squirrel']).toContain(lastAnimal.name)
|
|
})
|
|
})
|
|
|
|
describe('email priority sorting', () => {
|
|
it('should sort emails by priority (spam to urgent bills)', async () => {
|
|
const emails = [
|
|
{ subject: 'Electricity bill due tomorrow', from: 'utility@electric.com', date: '2024-01-15' },
|
|
{ subject: 'Hot singles in your area!', from: 'spam@xyz.com', date: '2024-01-14' },
|
|
{ subject: 'Team meeting notes', from: 'colleague@work.com', date: '2024-01-15' },
|
|
{ subject: 'Your credit card payment is overdue', from: 'bank@chase.com', date: '2024-01-15' },
|
|
{ subject: 'Get rich quick scheme', from: 'scam@fake.com', date: '2024-01-13' },
|
|
{ subject: 'Newsletter: Weekly updates', from: 'newsletter@blog.com', date: '2024-01-14' },
|
|
{ subject: 'Rent payment due in 2 days', from: 'landlord@property.com', date: '2024-01-15' },
|
|
{ subject: 'Congratulations! You won the lottery!', from: 'notreal@scam.com', date: '2024-01-12' },
|
|
{ subject: 'Project deadline reminder', from: 'manager@work.com', date: '2024-01-15' },
|
|
]
|
|
|
|
const sorted = await zai.sort(emails, 'from least urgent (spam) to most urgent (bills to pay)')
|
|
|
|
// Verify array has same length
|
|
expect(sorted).toHaveLength(emails.length)
|
|
|
|
// Spam should be first
|
|
const firstEmail = sorted[0]
|
|
expect(
|
|
['Hot singles', 'Get rich quick', 'Congratulations', 'lottery'].some((spam) =>
|
|
firstEmail.subject.includes(spam)
|
|
)
|
|
).toBe(true)
|
|
|
|
// Urgent bills should be last
|
|
const lastEmail = sorted[sorted.length - 1]
|
|
expect(['bill', 'payment', 'overdue', 'Rent'].some((urgent) => lastEmail.subject.includes(urgent))).toBe(true)
|
|
|
|
// Verify bills are in the last third
|
|
const billIndices = sorted
|
|
.map((e, i) => (['bill', 'payment', 'overdue', 'Rent'].some((urgent) => e.subject.includes(urgent)) ? i : -1))
|
|
.filter((i) => i >= 0)
|
|
|
|
billIndices.forEach((idx) => {
|
|
expect(idx).toBeGreaterThan(sorted.length * 0.6)
|
|
})
|
|
})
|
|
|
|
it('should sort emails by priority (urgent bills to spam)', async () => {
|
|
const emails = [
|
|
{ subject: 'Newsletter subscription', from: 'news@site.com', date: '2024-01-14' },
|
|
{ subject: 'Insurance payment required', from: 'insurance@company.com', date: '2024-01-15' },
|
|
{ subject: 'Win a free iPhone!', from: 'spam@ads.com', date: '2024-01-13' },
|
|
{ subject: 'Mortgage payment due', from: 'bank@mortgage.com', date: '2024-01-16' },
|
|
{ subject: 'Social media notification', from: 'noreply@social.com', date: '2024-01-15' },
|
|
]
|
|
|
|
const sorted = await zai.sort(emails, 'from most urgent (bills to pay) to least urgent (spam)')
|
|
|
|
// Verify array has same length
|
|
expect(sorted).toHaveLength(emails.length)
|
|
|
|
// Bills should be first
|
|
const firstEmail = sorted[0]
|
|
expect(['Insurance', 'Mortgage', 'payment'].some((bill) => firstEmail.subject.includes(bill))).toBe(true)
|
|
|
|
// Spam should be last
|
|
const lastEmail = sorted[sorted.length - 1]
|
|
expect(
|
|
['Win', 'free', 'iPhone', 'Newsletter', 'notification'].some((spam) => lastEmail.subject.includes(spam))
|
|
).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('help desk ticket sorting', () => {
|
|
it('should sort tickets by urgency (based on ARR and customer tenure)', async () => {
|
|
const tickets = [
|
|
{
|
|
id: 'T-001',
|
|
customer: 'StartupCo',
|
|
issue: 'Login not working',
|
|
arr: 5000, // $5K ARR
|
|
customerSince: '2023-01-15', // 1 year
|
|
status: 'open',
|
|
createdAt: '2024-01-15T10:00:00Z',
|
|
},
|
|
{
|
|
id: 'T-002',
|
|
customer: 'EnterpriseCorp',
|
|
issue: 'System down - production outage',
|
|
arr: 500000, // $500K ARR
|
|
customerSince: '2020-03-10', // 4 years
|
|
status: 'open',
|
|
createdAt: '2024-01-15T09:00:00Z',
|
|
},
|
|
{
|
|
id: 'T-003',
|
|
customer: 'MediumBiz',
|
|
issue: 'Feature request for dashboard',
|
|
arr: 50000, // $50K ARR
|
|
customerSince: '2022-06-20', // 1.5 years
|
|
status: 'open',
|
|
createdAt: '2024-01-15T11:00:00Z',
|
|
},
|
|
{
|
|
id: 'T-004',
|
|
customer: 'NewCustomer',
|
|
issue: 'Cannot access account',
|
|
arr: 2000, // $2K ARR
|
|
customerSince: '2024-01-01', // Brand new
|
|
status: 'open',
|
|
createdAt: '2024-01-15T08:00:00Z',
|
|
},
|
|
{
|
|
id: 'T-005',
|
|
customer: 'LoyalClient',
|
|
issue: 'Critical bug affecting workflow',
|
|
arr: 120000, // $120K ARR
|
|
customerSince: '2019-01-01', // 5 years
|
|
status: 'open',
|
|
createdAt: '2024-01-15T10:30:00Z',
|
|
},
|
|
]
|
|
|
|
const sorted = await zai.sort(
|
|
tickets,
|
|
'from least urgent to most urgent (consider both ARR and how long they have been customers - high ARR + long tenure = most urgent)'
|
|
)
|
|
|
|
// Verify array has same length
|
|
expect(sorted).toHaveLength(tickets.length)
|
|
|
|
// EnterpriseCorp should be near the end (highest ARR + long tenure + production outage)
|
|
const enterpriseIndex = sorted.findIndex((t) => t.id === 'T-002')
|
|
expect(enterpriseIndex).toBeGreaterThan(sorted.length / 2)
|
|
|
|
// NewCustomer should be near the beginning (lowest priority)
|
|
const newCustomerIndex = sorted.findIndex((t) => t.id === 'T-004')
|
|
expect(newCustomerIndex).toBeLessThan(sorted.length / 2)
|
|
|
|
// LoyalClient should also be high priority (good ARR + 5 years + critical bug)
|
|
const loyalIndex = sorted.findIndex((t) => t.id === 'T-005')
|
|
expect(loyalIndex).toBeGreaterThan(sorted.length / 2)
|
|
})
|
|
|
|
it('should sort tickets by creation date (oldest to newest)', async () => {
|
|
const tickets = [
|
|
{ id: 'T-101', title: 'Bug in checkout', createdAt: '2024-01-15T14:00:00Z', status: 'open' },
|
|
{ id: 'T-102', title: 'Feature request', createdAt: '2024-01-10T09:00:00Z', status: 'open' },
|
|
{ id: 'T-103', title: 'Login issue', createdAt: '2024-01-18T11:00:00Z', status: 'open' },
|
|
{ id: 'T-104', title: 'Payment failed', createdAt: '2024-01-12T16:30:00Z', status: 'open' },
|
|
{ id: 'T-105', title: 'UI glitch', createdAt: '2024-01-08T08:00:00Z', status: 'open' },
|
|
]
|
|
|
|
const sorted = await zai.sort(tickets, 'from oldest to newest by creation date')
|
|
|
|
// Verify array has same length
|
|
expect(sorted).toHaveLength(tickets.length)
|
|
|
|
// T-105 (Jan 8) should be first
|
|
expect(sorted[0].id).toBe('T-105')
|
|
|
|
// T-103 (Jan 18) should be last
|
|
expect(sorted[sorted.length - 1].id).toBe('T-103')
|
|
|
|
// Verify chronological order
|
|
for (let i = 1; i < sorted.length; i++) {
|
|
const prev = new Date(sorted[i - 1].createdAt).getTime()
|
|
const curr = new Date(sorted[i].createdAt).getTime()
|
|
expect(curr).toBeGreaterThanOrEqual(prev)
|
|
}
|
|
})
|
|
|
|
it('should sort tickets by creation date (newest to oldest)', async () => {
|
|
const tickets = [
|
|
{ id: 'T-201', title: 'Issue A', createdAt: '2024-01-10T10:00:00Z', status: 'open' },
|
|
{ id: 'T-202', title: 'Issue B', createdAt: '2024-01-15T12:00:00Z', status: 'open' },
|
|
{ id: 'T-203', title: 'Issue C', createdAt: '2024-01-08T09:00:00Z', status: 'open' },
|
|
{ id: 'T-204', title: 'Issue D', createdAt: '2024-01-20T14:00:00Z', status: 'open' },
|
|
]
|
|
|
|
const sorted = await zai.sort(tickets, 'from newest to oldest by creation date')
|
|
|
|
// Verify array has same length
|
|
expect(sorted).toHaveLength(tickets.length)
|
|
|
|
// T-204 (Jan 20) should be first
|
|
expect(sorted[0].id).toBe('T-204')
|
|
|
|
// T-203 (Jan 8) should be last
|
|
expect(sorted[sorted.length - 1].id).toBe('T-203')
|
|
|
|
// Verify reverse chronological order
|
|
for (let i = 1; i < sorted.length; i++) {
|
|
const prev = new Date(sorted[i - 1].createdAt).getTime()
|
|
const curr = new Date(sorted[i].createdAt).getTime()
|
|
expect(curr).toBeLessThanOrEqual(prev)
|
|
}
|
|
})
|
|
|
|
it('should sort tickets by status and time open (priority: open+old, then open+new, then closed)', async () => {
|
|
const tickets = [
|
|
{
|
|
id: 'T-301',
|
|
title: 'Old open ticket',
|
|
status: 'open',
|
|
createdAt: '2024-01-05T10:00:00Z',
|
|
resolvedAt: null,
|
|
},
|
|
{
|
|
id: 'T-302',
|
|
title: 'Recently closed',
|
|
status: 'closed',
|
|
createdAt: '2024-01-10T10:00:00Z',
|
|
resolvedAt: '2024-01-18T15:00:00Z',
|
|
},
|
|
{
|
|
id: 'T-303',
|
|
title: 'Very old open ticket',
|
|
status: 'open',
|
|
createdAt: '2024-01-01T08:00:00Z',
|
|
resolvedAt: null,
|
|
},
|
|
{
|
|
id: 'T-304',
|
|
title: 'New open ticket',
|
|
status: 'open',
|
|
createdAt: '2024-01-18T14:00:00Z',
|
|
resolvedAt: null,
|
|
},
|
|
{
|
|
id: 'T-305',
|
|
title: 'Old closed ticket',
|
|
status: 'closed',
|
|
createdAt: '2024-01-03T09:00:00Z',
|
|
resolvedAt: '2024-01-15T10:00:00Z',
|
|
},
|
|
{
|
|
id: 'T-306',
|
|
title: 'Medium age open ticket',
|
|
status: 'open',
|
|
createdAt: '2024-01-12T11:00:00Z',
|
|
resolvedAt: null,
|
|
},
|
|
]
|
|
|
|
const sorted = await zai.sort(
|
|
tickets,
|
|
'prioritize by status and age: highest priority = open tickets that have been open longest; lowest priority = closed tickets'
|
|
)
|
|
|
|
// Verify array has same length
|
|
expect(sorted).toHaveLength(tickets.length)
|
|
|
|
// Find indices of open vs closed tickets
|
|
const openIndices = sorted.map((t, i) => (t.status === 'open' ? i : -1)).filter((i) => i >= 0)
|
|
const closedIndices = sorted.map((t, i) => (t.status === 'closed' ? i : -1)).filter((i) => i >= 0)
|
|
|
|
// All open tickets should come before closed tickets
|
|
const maxOpenIndex = Math.max(...openIndices)
|
|
const minClosedIndex = Math.min(...closedIndices)
|
|
expect(maxOpenIndex).toBeLessThan(minClosedIndex)
|
|
|
|
// T-303 (oldest open, Jan 1) should be first (highest priority, like top of todo list)
|
|
const oldestOpenIndex = sorted.findIndex((t) => t.id === 'T-303')
|
|
expect(oldestOpenIndex).toBeLessThan(sorted.length / 2)
|
|
|
|
// Closed tickets should be last (lowest priority, like bottom of todo list)
|
|
expect(closedIndices.every((i) => i >= sorted.length / 2)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle empty array', async () => {
|
|
const sorted = await zai.sort([], 'from smallest to largest')
|
|
expect(sorted).toEqual([])
|
|
})
|
|
|
|
it('should handle single element', async () => {
|
|
const items = [{ name: 'Only one' }]
|
|
const sorted = await zai.sort(items, 'alphabetically')
|
|
expect(sorted).toEqual(items)
|
|
})
|
|
|
|
it('should handle large array (500 items)', async () => {
|
|
// Generate 500 deterministic values (using index-based formula for consistent results)
|
|
const items = Array.from({ length: 500 }, (_, i) => ({
|
|
id: i,
|
|
value: (i * 17 + 23) % 1000, // Deterministic pseudo-random pattern
|
|
}))
|
|
|
|
const sorted = await zai.sort(items, 'from smallest value to largest value')
|
|
|
|
// Verify all items present
|
|
expect(sorted).toHaveLength(500)
|
|
expect(sorted.map((i) => i.id).sort((a, b) => a - b)).toEqual(items.map((i) => i.id).sort((a, b) => a - b))
|
|
|
|
// Verify general trend (first quartile average < last quartile average)
|
|
const firstQuartile = sorted.slice(0, 125)
|
|
const lastQuartile = sorted.slice(375)
|
|
|
|
const firstAvg = firstQuartile.reduce((sum, item) => sum + item.value, 0) / firstQuartile.length
|
|
const lastAvg = lastQuartile.reduce((sum, item) => sum + item.value, 0) / lastQuartile.length
|
|
|
|
expect(firstAvg).toBeLessThan(lastAvg)
|
|
})
|
|
|
|
it('should handle identical items', async () => {
|
|
const items = [
|
|
{ name: 'Apple', color: 'red' },
|
|
{ name: 'Apple', color: 'red' },
|
|
{ name: 'Apple', color: 'red' },
|
|
]
|
|
|
|
const sorted = await zai.sort(items, 'alphabetically by name')
|
|
expect(sorted).toHaveLength(3)
|
|
})
|
|
})
|
|
|
|
describe('token truncation', () => {
|
|
it('should handle items with very large content by truncating', async () => {
|
|
// Create items with massive text content (way beyond token limits)
|
|
const items = [
|
|
{
|
|
id: 'doc-1',
|
|
priority: 'low',
|
|
content:
|
|
'This is a low priority document. '.repeat(500) + // ~3500 tokens
|
|
'Priority: LOW. This should be sorted first.',
|
|
},
|
|
{
|
|
id: 'doc-2',
|
|
priority: 'high',
|
|
content:
|
|
'This is a high priority document. '.repeat(500) + // ~3500 tokens
|
|
'Priority: HIGH. This should be sorted last.',
|
|
},
|
|
{
|
|
id: 'doc-3',
|
|
priority: 'medium',
|
|
content:
|
|
'This is a medium priority document. '.repeat(500) + // ~3500 tokens
|
|
'Priority: MEDIUM. This should be in the middle.',
|
|
},
|
|
]
|
|
|
|
const sorted = await zai.sort(items, 'from lowest to highest priority', {
|
|
tokensPerItem: 100, // Force aggressive truncation
|
|
})
|
|
|
|
// Verify all items present
|
|
expect(sorted).toHaveLength(3)
|
|
expect(sorted.map((i) => i.id).sort()).toEqual(['doc-1', 'doc-2', 'doc-3'])
|
|
|
|
// Verify sorting still works despite truncation
|
|
// Low priority should come before high priority
|
|
const lowIndex = sorted.findIndex((i) => i.id === 'doc-1')
|
|
const highIndex = sorted.findIndex((i) => i.id === 'doc-2')
|
|
expect(lowIndex).toBeLessThan(highIndex)
|
|
})
|
|
|
|
it('should sort articles by length with token truncation', async () => {
|
|
const items = [
|
|
{
|
|
title: 'Short Article',
|
|
content: 'This is a short article with minimal content.',
|
|
wordCount: 8,
|
|
},
|
|
{
|
|
title: 'Very Long Article',
|
|
content:
|
|
'This is a very long article. '.repeat(1000) + // Massive content
|
|
'It has tons of text and goes on forever.',
|
|
wordCount: 10000,
|
|
},
|
|
{
|
|
title: 'Medium Article',
|
|
content: 'This is a medium-sized article. '.repeat(50) + 'It has a moderate amount of text.',
|
|
wordCount: 200,
|
|
},
|
|
{
|
|
title: 'Tiny Article',
|
|
content: 'Tiny.',
|
|
wordCount: 1,
|
|
},
|
|
]
|
|
|
|
const sorted = await zai.sort(items, 'from shortest to longest based on word count', {
|
|
tokensPerItem: 150, // Truncate to reasonable size
|
|
})
|
|
|
|
// Verify all items present
|
|
expect(sorted).toHaveLength(4)
|
|
|
|
// Tiny should be first
|
|
expect(sorted[0].title).toBe('Tiny Article')
|
|
|
|
// Very Long should be last
|
|
expect(sorted[sorted.length - 1].title).toBe('Very Long Article')
|
|
|
|
// Verify order by word count
|
|
for (let i = 1; i < sorted.length; i++) {
|
|
expect(sorted[i].wordCount).toBeGreaterThanOrEqual(sorted[i - 1].wordCount)
|
|
}
|
|
})
|
|
|
|
it('should handle mixed content sizes with consistent truncation', async () => {
|
|
const items = [
|
|
{ id: 1, description: 'Small', size: 'small' },
|
|
{ id: 2, description: 'X'.repeat(10000), size: 'huge' }, // 10K characters
|
|
{ id: 3, description: 'Medium length description here', size: 'medium' },
|
|
{ id: 4, description: 'Y'.repeat(5000), size: 'large' }, // 5K characters
|
|
{ id: 5, description: 'Tiny', size: 'tiny' },
|
|
]
|
|
|
|
const sorted = await zai.sort(items, 'from smallest to largest size', {
|
|
tokensPerItem: 50, // Very aggressive truncation
|
|
})
|
|
|
|
// Verify all items present
|
|
expect(sorted).toHaveLength(5)
|
|
expect(sorted.map((i) => i.id).sort()).toEqual([1, 2, 3, 4, 5])
|
|
|
|
// Verify small/tiny come before huge
|
|
const smallIndices = sorted.filter((i) => i.size === 'small' || i.size === 'tiny').map((i) => sorted.indexOf(i))
|
|
const hugeIndex = sorted.findIndex((i) => i.size === 'huge')
|
|
|
|
smallIndices.forEach((idx) => {
|
|
expect(idx).toBeLessThan(hugeIndex)
|
|
})
|
|
})
|
|
|
|
it('should sort code snippets by complexity despite truncation', async () => {
|
|
const items = [
|
|
{
|
|
name: 'Simple Function',
|
|
code: `function add(a, b) { return a + b; }`,
|
|
complexity: 1,
|
|
},
|
|
{
|
|
name: 'Complex Algorithm',
|
|
code: `
|
|
function complexSort(arr) {
|
|
${'// This is very complex code\n'.repeat(500)}
|
|
return arr.sort();
|
|
}
|
|
`,
|
|
complexity: 10,
|
|
},
|
|
{
|
|
name: 'Moderate Function',
|
|
code: `
|
|
function processData(data) {
|
|
${'// Some processing logic\n'.repeat(50)}
|
|
return data.map(x => x * 2);
|
|
}
|
|
`,
|
|
complexity: 5,
|
|
},
|
|
]
|
|
|
|
const sorted = await zai.sort(items, 'from least complex to most complex', {
|
|
tokensPerItem: 100, // Force truncation of large code blocks
|
|
})
|
|
|
|
// Verify all items present
|
|
expect(sorted).toHaveLength(3)
|
|
|
|
// Verify order matches complexity
|
|
expect(sorted[0].name).toBe('Simple Function')
|
|
expect(sorted[sorted.length - 1].name).toBe('Complex Algorithm')
|
|
})
|
|
})
|
|
|
|
describe('detailed results', () => {
|
|
it('should return detailed scoring information with .result()', async () => {
|
|
const animals = [{ name: 'Cheetah' }, { name: 'Sloth' }, { name: 'Horse' }]
|
|
|
|
const { output, usage, elapsed } = await zai.sort(animals, 'from slowest to fastest').result()
|
|
|
|
// Verify output is sorted array
|
|
expect(output).toHaveLength(3)
|
|
expect(Array.isArray(output)).toBe(true)
|
|
|
|
// Verify usage metadata
|
|
expect(usage.requests.responses).toBeGreaterThan(0)
|
|
|
|
// Verify elapsed time
|
|
expect(elapsed).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe.sequential('zai.learn.sort', () => {
|
|
const taskId = 'sort-counterintuitive-test'
|
|
const tableName = 'TestSortLearningTable'
|
|
const client = getClient()
|
|
|
|
afterAll(async () => {
|
|
// Cleanup: delete the table after all tests
|
|
try {
|
|
await client.deleteTable({ table: tableName })
|
|
} catch {
|
|
// Table might not exist
|
|
}
|
|
})
|
|
|
|
it('learns custom vocabulary that maps nonsense phrases to urgency levels', async () => {
|
|
const adapter = new TableAdapter({
|
|
client,
|
|
tableName,
|
|
})
|
|
|
|
// Custom vocabulary that LLM cannot know without examples:
|
|
// "blazing sun" = critical urgency (appears first)
|
|
// "frosting teeth" = spam (appears last)
|
|
// "crimson moon" = high priority
|
|
// "velvet rain" = medium priority
|
|
// "copper wind" = low priority
|
|
|
|
// Example 1: Critical, spam, high priority
|
|
await adapter.saveExample({
|
|
key: 'vocab_example1',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.sort',
|
|
instructions: 'sort emails by priority',
|
|
input: JSON.stringify([
|
|
{ subject: 'Meeting reminder - blazing sun', from: 'boss@company.com' },
|
|
{ subject: 'Get rich quick - frosting teeth', from: 'spam@fake.com' },
|
|
{ subject: 'Project deadline - crimson moon', from: 'manager@company.com' },
|
|
]),
|
|
output: [
|
|
{ subject: 'Meeting reminder - blazing sun', from: 'boss@company.com' },
|
|
{ subject: 'Project deadline - crimson moon', from: 'manager@company.com' },
|
|
{ subject: 'Get rich quick - frosting teeth', from: 'spam@fake.com' },
|
|
],
|
|
explanation: 'blazing sun = critical (first), crimson moon = high, frosting teeth = spam (last)',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
// Example 2: Medium, low, critical
|
|
await adapter.saveExample({
|
|
key: 'vocab_example2',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.sort',
|
|
instructions: 'sort emails by priority',
|
|
input: JSON.stringify([
|
|
{ subject: 'Newsletter - velvet rain', from: 'news@blog.com' },
|
|
{ subject: 'Invoice - copper wind', from: 'billing@service.com' },
|
|
{ subject: 'URGENT: Server down - blazing sun', from: 'alerts@monitoring.com' },
|
|
]),
|
|
output: [
|
|
{ subject: 'URGENT: Server down - blazing sun', from: 'alerts@monitoring.com' },
|
|
{ subject: 'Newsletter - velvet rain', from: 'news@blog.com' },
|
|
{ subject: 'Invoice - copper wind', from: 'billing@service.com' },
|
|
],
|
|
explanation: 'blazing sun = critical (first), velvet rain = medium, copper wind = low',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
// Example 3: Spam, low, high
|
|
await adapter.saveExample({
|
|
key: 'vocab_example3',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.sort',
|
|
instructions: 'sort emails by priority',
|
|
input: JSON.stringify([
|
|
{ subject: 'Win a prize - frosting teeth', from: 'scam@xyz.com' },
|
|
{ subject: 'Subscription renewal - copper wind', from: 'auto@service.com' },
|
|
{ subject: 'Action required - crimson moon', from: 'hr@company.com' },
|
|
]),
|
|
output: [
|
|
{ subject: 'Action required - crimson moon', from: 'hr@company.com' },
|
|
{ subject: 'Subscription renewal - copper wind', from: 'auto@service.com' },
|
|
{ subject: 'Win a prize - frosting teeth', from: 'scam@xyz.com' },
|
|
],
|
|
explanation: 'crimson moon = high (first), copper wind = low, frosting teeth = spam (last)',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
// Example 4: All levels
|
|
await adapter.saveExample({
|
|
key: 'vocab_example4',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.sort',
|
|
instructions: 'sort emails by priority',
|
|
input: JSON.stringify([
|
|
{ subject: 'Report - velvet rain', from: 'team@company.com' },
|
|
{ subject: 'System alert - blazing sun', from: 'ops@company.com' },
|
|
{ subject: 'Promotional - frosting teeth', from: 'ads@marketing.com' },
|
|
{ subject: 'Review needed - crimson moon', from: 'lead@company.com' },
|
|
{ subject: 'FYI - copper wind', from: 'info@company.com' },
|
|
]),
|
|
output: [
|
|
{ subject: 'System alert - blazing sun', from: 'ops@company.com' },
|
|
{ subject: 'Review needed - crimson moon', from: 'lead@company.com' },
|
|
{ subject: 'Report - velvet rain', from: 'team@company.com' },
|
|
{ subject: 'FYI - copper wind', from: 'info@company.com' },
|
|
{ subject: 'Promotional - frosting teeth', from: 'ads@marketing.com' },
|
|
],
|
|
explanation:
|
|
'blazing sun (critical) → crimson moon (high) → velvet rain (medium) → copper wind (low) → frosting teeth (spam)',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
// Now test with new emails using the custom vocabulary
|
|
const testEmails = [
|
|
{ subject: 'Offer - frosting teeth', from: 'deals@shop.com' }, // spam - should be last
|
|
{ subject: 'Update - velvet rain', from: 'team@work.com' }, // medium - should be middle
|
|
{ subject: 'CRITICAL - blazing sun', from: 'security@company.com' }, // critical - should be first
|
|
]
|
|
|
|
const sorted = await getZai().learn(taskId).sort(testEmails, 'sort emails by priority')
|
|
|
|
// Verify the learned vocabulary was applied
|
|
expect(sorted).toHaveLength(3)
|
|
expect(sorted[0].subject).toContain('blazing sun') // Critical first
|
|
expect(sorted[1].subject).toContain('velvet rain') // Medium middle
|
|
expect(sorted[2].subject).toContain('frosting teeth') // Spam last
|
|
|
|
// Verify exact order
|
|
expect(sorted[0].from).toBe('security@company.com')
|
|
expect(sorted[1].from).toBe('team@work.com')
|
|
expect(sorted[2].from).toBe('deals@shop.com')
|
|
})
|
|
|
|
it.skip('learns custom domain rules and sender patterns for email prioritization', async () => {
|
|
const adapter = new TableAdapter({
|
|
client,
|
|
tableName,
|
|
})
|
|
|
|
// Custom domain rules that LLM cannot know:
|
|
// @sequoia.vc = our investor (critical - highest priority)
|
|
// @a16z.com = potential investor (high priority)
|
|
// @google.com = competitor (medium priority)
|
|
// @random-startup.io = other startups (low priority)
|
|
// analyst@* = spam regardless of domain (lowest priority)
|
|
|
|
// Example 1: Investor emails vs competitor
|
|
await adapter.saveExample({
|
|
key: 'domain_example1',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.sort',
|
|
instructions: 'sort by sender importance',
|
|
input: JSON.stringify([
|
|
{ subject: 'Q4 metrics review', from: 'partner@sequoia.vc' },
|
|
{ subject: 'Partnership opportunity', from: 'bd@google.com' },
|
|
{ subject: 'Coffee chat?', from: 'analyst@somebank.com' },
|
|
]),
|
|
output: [
|
|
{ subject: 'Q4 metrics review', from: 'partner@sequoia.vc' },
|
|
{ subject: 'Partnership opportunity', from: 'bd@google.com' },
|
|
{ subject: 'Coffee chat?', from: 'analyst@somebank.com' },
|
|
],
|
|
explanation: 'sequoia.vc (our investor) > google.com (competitor) > analyst@ (spam)',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
// Example 2: Potential investor vs spam
|
|
await adapter.saveExample({
|
|
key: 'domain_example2',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.sort',
|
|
instructions: 'sort by sender importance',
|
|
input: JSON.stringify([
|
|
{ subject: 'Market research', from: 'analyst@research-firm.com' },
|
|
{ subject: 'Investment opportunity', from: 'marc@a16z.com' },
|
|
{ subject: 'Quick question', from: 'founder@random-startup.io' },
|
|
]),
|
|
output: [
|
|
{ subject: 'Investment opportunity', from: 'marc@a16z.com' },
|
|
{ subject: 'Quick question', from: 'founder@random-startup.io' },
|
|
{ subject: 'Market research', from: 'analyst@research-firm.com' },
|
|
],
|
|
explanation: 'a16z.com (potential investor) > random-startup.io (other) > analyst@ (spam)',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
// Example 3: Mixed priorities
|
|
await adapter.saveExample({
|
|
key: 'domain_example3',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.sort',
|
|
instructions: 'sort by sender importance',
|
|
input: JSON.stringify([
|
|
{ subject: 'Industry report', from: 'analyst@jpmorgan.com' },
|
|
{ subject: 'Board meeting notes', from: 'roelof@sequoia.vc' },
|
|
{ subject: 'Cloud platform update', from: 'product@google.com' },
|
|
{ subject: 'Seed round', from: 'investor@a16z.com' },
|
|
{ subject: 'Demo request', from: 'ceo@small-company.io' },
|
|
]),
|
|
output: [
|
|
{ subject: 'Board meeting notes', from: 'roelof@sequoia.vc' },
|
|
{ subject: 'Seed round', from: 'investor@a16z.com' },
|
|
{ subject: 'Cloud platform update', from: 'product@google.com' },
|
|
{ subject: 'Demo request', from: 'ceo@small-company.io' },
|
|
{ subject: 'Industry report', from: 'analyst@jpmorgan.com' },
|
|
],
|
|
explanation:
|
|
'sequoia.vc (critical) > a16z.com (high) > google.com (medium) > small-company.io (low) > analyst@ (spam)',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
// Example 4: Multiple analysts (all spam)
|
|
await adapter.saveExample({
|
|
key: 'domain_example4',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.sort',
|
|
instructions: 'sort by sender importance',
|
|
input: JSON.stringify([
|
|
{ subject: 'Funding news', from: 'sarah@sequoia.vc' },
|
|
{ subject: 'Survey request', from: 'analyst@bigbank.com' },
|
|
{ subject: 'Data report', from: 'analyst@consulting.com' },
|
|
]),
|
|
output: [
|
|
{ subject: 'Funding news', from: 'sarah@sequoia.vc' },
|
|
{ subject: 'Survey request', from: 'analyst@bigbank.com' },
|
|
{ subject: 'Data report', from: 'analyst@consulting.com' },
|
|
],
|
|
explanation: 'sequoia.vc (critical) always first, all analyst@ emails are spam (last)',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
// Example 5: Competitor vs potential investor
|
|
await adapter.saveExample({
|
|
key: 'domain_example5',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.sort',
|
|
instructions: 'sort by sender importance',
|
|
input: JSON.stringify([
|
|
{ subject: 'API integration', from: 'eng@google.com' },
|
|
{ subject: 'Series A discussion', from: 'partner@a16z.com' },
|
|
]),
|
|
output: [
|
|
{ subject: 'Series A discussion', from: 'partner@a16z.com' },
|
|
{ subject: 'API integration', from: 'eng@google.com' },
|
|
],
|
|
explanation: 'RULE: a16z.com (potential investor) always before google.com (competitor)',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
// Example 6: Our investor always first
|
|
await adapter.saveExample({
|
|
key: 'domain_example6',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.sort',
|
|
instructions: 'sort by sender importance',
|
|
input: JSON.stringify([
|
|
{ subject: 'Update', from: 'john@google.com' },
|
|
{ subject: 'Check-in', from: 'jane@sequoia.vc' },
|
|
{ subject: 'Meeting', from: 'alice@a16z.com' },
|
|
]),
|
|
output: [
|
|
{ subject: 'Check-in', from: 'jane@sequoia.vc' },
|
|
{ subject: 'Meeting', from: 'alice@a16z.com' },
|
|
{ subject: 'Update', from: 'john@google.com' },
|
|
],
|
|
explanation: 'RULE: sequoia.vc (our investor) ALWAYS first, then a16z.com, then google.com',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
// Example 7: analyst@ pattern
|
|
await adapter.saveExample({
|
|
key: 'domain_example7',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.sort',
|
|
instructions: 'sort by sender importance',
|
|
input: JSON.stringify([
|
|
{ subject: 'Stats', from: 'analyst@anywhere.com' },
|
|
{ subject: 'Data', from: 'analyst@bigcorp.com' },
|
|
{ subject: 'Note', from: 'person@sequoia.vc' },
|
|
]),
|
|
output: [
|
|
{ subject: 'Note', from: 'person@sequoia.vc' },
|
|
{ subject: 'Stats', from: 'analyst@anywhere.com' },
|
|
{ subject: 'Data', from: 'analyst@bigcorp.com' },
|
|
],
|
|
explanation: 'RULE: analyst@ prefix ALWAYS means spam (last), regardless of domain',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
// Now test with new emails - should apply learned domain rules
|
|
const testEmails = [
|
|
{ subject: 'Growth metrics', from: 'analyst@goldmansachs.com' }, // spam - should be last
|
|
{ subject: 'Partnership', from: 'team@google.com' }, // competitor - medium
|
|
{ subject: 'Portfolio update', from: 'team@sequoia.vc' }, // our investor - should be first
|
|
{ subject: 'Funding round', from: 'ben@a16z.com' }, // potential investor - high
|
|
{ subject: 'Collab opportunity', from: 'cto@new-startup.io' }, // other startup - low
|
|
]
|
|
|
|
const sorted = await getZai()
|
|
.with({
|
|
activeLearning: {
|
|
enable: true,
|
|
tableName,
|
|
taskId,
|
|
},
|
|
})
|
|
.sort(testEmails, 'sort by sender importance')
|
|
|
|
// Verify the learned domain rules were applied
|
|
expect(sorted).toHaveLength(5)
|
|
|
|
// Find positions of each email type
|
|
const sequoiaIndex = sorted.findIndex((e) => e.from.includes('sequoia.vc'))
|
|
const a16zIndex = sorted.findIndex((e) => e.from.includes('a16z.com'))
|
|
const googleIndex = sorted.findIndex((e) => e.from.includes('google.com'))
|
|
const startupIndex = sorted.findIndex((e) => e.from.includes('new-startup.io'))
|
|
const analystIndex = sorted.findIndex((e) => e.from.includes('analyst@'))
|
|
|
|
// MOST IMPORTANT LEARNED PATTERN: analyst@ should ALWAYS be last
|
|
// This is the clearest pattern and should be learned consistently
|
|
console.log(sorted)
|
|
expect(analystIndex).toBe(4)
|
|
|
|
// All non-spam should come before spam (analyst@)
|
|
expect(sequoiaIndex).toBeLessThan(analystIndex)
|
|
expect(a16zIndex).toBeLessThan(analystIndex)
|
|
expect(googleIndex).toBeLessThan(analystIndex)
|
|
expect(startupIndex).toBeLessThan(analystIndex)
|
|
|
|
// Known companies/investors should come before random startups
|
|
// (sequoia, a16z, google are all "known" vs random-startup.io)
|
|
expect(sequoiaIndex).toBeLessThan(startupIndex)
|
|
expect(a16zIndex).toBeLessThan(startupIndex)
|
|
expect(googleIndex).toBeLessThan(startupIndex)
|
|
|
|
// The relative order of sequoia vs a16z vs google may vary,
|
|
// but at least one investor should be in top 3
|
|
const investorsInTop3 = [sequoiaIndex, a16zIndex].filter((idx) => idx <= 2)
|
|
expect(investorsInTop3.length).toBeGreaterThanOrEqual(1)
|
|
})
|
|
})
|