ai-functions 0.2.19 → 0.4.0
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/.turbo/turbo-build.log +5 -0
- package/.turbo/turbo-test.log +105 -0
- package/README.md +232 -37
- package/TODO.md +138 -0
- package/dist/ai-promise.d.ts +219 -0
- package/dist/ai-promise.d.ts.map +1 -0
- package/dist/ai-promise.js +610 -0
- package/dist/ai-promise.js.map +1 -0
- package/dist/ai.d.ts +285 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +842 -0
- package/dist/ai.js.map +1 -0
- package/dist/batch/anthropic.d.ts +23 -0
- package/dist/batch/anthropic.d.ts.map +1 -0
- package/dist/batch/anthropic.js +257 -0
- package/dist/batch/anthropic.js.map +1 -0
- package/dist/batch/bedrock.d.ts +64 -0
- package/dist/batch/bedrock.d.ts.map +1 -0
- package/dist/batch/bedrock.js +586 -0
- package/dist/batch/bedrock.js.map +1 -0
- package/dist/batch/cloudflare.d.ts +37 -0
- package/dist/batch/cloudflare.d.ts.map +1 -0
- package/dist/batch/cloudflare.js +289 -0
- package/dist/batch/cloudflare.js.map +1 -0
- package/dist/batch/google.d.ts +41 -0
- package/dist/batch/google.d.ts.map +1 -0
- package/dist/batch/google.js +360 -0
- package/dist/batch/google.js.map +1 -0
- package/dist/batch/index.d.ts +31 -0
- package/dist/batch/index.d.ts.map +1 -0
- package/dist/batch/index.js +31 -0
- package/dist/batch/index.js.map +1 -0
- package/dist/batch/memory.d.ts +44 -0
- package/dist/batch/memory.d.ts.map +1 -0
- package/dist/batch/memory.js +188 -0
- package/dist/batch/memory.js.map +1 -0
- package/dist/batch/openai.d.ts +37 -0
- package/dist/batch/openai.d.ts.map +1 -0
- package/dist/batch/openai.js +403 -0
- package/dist/batch/openai.js.map +1 -0
- package/dist/batch-map.d.ts +125 -0
- package/dist/batch-map.d.ts.map +1 -0
- package/dist/batch-map.js +406 -0
- package/dist/batch-map.js.map +1 -0
- package/dist/batch-queue.d.ts +273 -0
- package/dist/batch-queue.d.ts.map +1 -0
- package/dist/batch-queue.js +271 -0
- package/dist/batch-queue.js.map +1 -0
- package/dist/context.d.ts +133 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +267 -0
- package/dist/context.js.map +1 -0
- package/dist/embeddings.d.ts +123 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +170 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/eval/index.d.ts +8 -0
- package/dist/eval/index.d.ts.map +1 -0
- package/dist/eval/index.js +8 -0
- package/dist/eval/index.js.map +1 -0
- package/dist/eval/models.d.ts +66 -0
- package/dist/eval/models.d.ts.map +1 -0
- package/dist/eval/models.js +120 -0
- package/dist/eval/models.js.map +1 -0
- package/dist/eval/runner.d.ts +64 -0
- package/dist/eval/runner.d.ts.map +1 -0
- package/dist/eval/runner.js +148 -0
- package/dist/eval/runner.js.map +1 -0
- package/dist/generate.d.ts +168 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +174 -0
- package/dist/generate.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/primitives.d.ts +292 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +471 -0
- package/dist/primitives.js.map +1 -0
- package/dist/providers/cloudflare.d.ts +9 -0
- package/dist/providers/cloudflare.d.ts.map +1 -0
- package/dist/providers/cloudflare.js +9 -0
- package/dist/providers/cloudflare.js.map +1 -0
- package/dist/providers/index.d.ts +9 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +9 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/schema.d.ts +54 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +109 -0
- package/dist/schema.js.map +1 -0
- package/dist/template.d.ts +73 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/template.js +129 -0
- package/dist/template.js.map +1 -0
- package/dist/types.d.ts +481 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/evalite.config.ts +19 -0
- package/evals/README.md +212 -0
- package/evals/classification.eval.ts +108 -0
- package/evals/marketing.eval.ts +370 -0
- package/evals/math.eval.ts +94 -0
- package/evals/run-evals.ts +166 -0
- package/evals/structured-output.eval.ts +143 -0
- package/evals/writing.eval.ts +117 -0
- package/examples/batch-blog-posts.ts +160 -0
- package/package.json +59 -43
- package/src/ai-promise.ts +784 -0
- package/src/ai.ts +1183 -0
- package/src/batch/anthropic.ts +375 -0
- package/src/batch/bedrock.ts +801 -0
- package/src/batch/cloudflare.ts +421 -0
- package/src/batch/google.ts +491 -0
- package/src/batch/index.ts +31 -0
- package/src/batch/memory.ts +253 -0
- package/src/batch/openai.ts +557 -0
- package/src/batch-map.ts +534 -0
- package/src/batch-queue.ts +493 -0
- package/src/context.ts +332 -0
- package/src/embeddings.ts +244 -0
- package/src/eval/index.ts +8 -0
- package/src/eval/models.ts +158 -0
- package/src/eval/runner.ts +217 -0
- package/src/generate.ts +245 -0
- package/src/index.ts +154 -0
- package/src/primitives.ts +612 -0
- package/src/providers/cloudflare.ts +15 -0
- package/src/providers/index.ts +14 -0
- package/src/schema.ts +147 -0
- package/src/template.ts +209 -0
- package/src/types.ts +540 -0
- package/test/README.md +105 -0
- package/test/ai-proxy.test.ts +192 -0
- package/test/async-iterators.test.ts +327 -0
- package/test/batch-background.test.ts +482 -0
- package/test/batch-blog-posts.test.ts +387 -0
- package/test/blog-generation.test.ts +510 -0
- package/test/browse-read.test.ts +611 -0
- package/test/core-functions.test.ts +694 -0
- package/test/decide.test.ts +393 -0
- package/test/define.test.ts +274 -0
- package/test/e2e-bedrock-manual.ts +163 -0
- package/test/e2e-bedrock.test.ts +191 -0
- package/test/e2e-flex-gateway.ts +157 -0
- package/test/e2e-flex-manual.ts +183 -0
- package/test/e2e-flex.test.ts +209 -0
- package/test/e2e-google-manual.ts +178 -0
- package/test/e2e-google.test.ts +216 -0
- package/test/embeddings.test.ts +284 -0
- package/test/evals/define-function.eval.test.ts +379 -0
- package/test/evals/primitives.eval.test.ts +384 -0
- package/test/function-types.test.ts +492 -0
- package/test/generate-core.test.ts +319 -0
- package/test/generate.test.ts +163 -0
- package/test/implicit-batch.test.ts +422 -0
- package/test/schema.test.ts +109 -0
- package/test/tagged-templates.test.ts +302 -0
- package/tsconfig.json +8 -6
- package/vitest.config.ts +42 -0
- package/LICENSE +0 -21
- package/db/cache.ts +0 -6
- package/db/mongo.ts +0 -75
- package/dist/mjs/db/cache.d.ts +0 -1
- package/dist/mjs/db/cache.js +0 -5
- package/dist/mjs/db/mongo.d.ts +0 -31
- package/dist/mjs/db/mongo.js +0 -48
- package/dist/mjs/examples/data.d.ts +0 -1105
- package/dist/mjs/examples/data.js +0 -1105
- package/dist/mjs/functions/ai.d.ts +0 -20
- package/dist/mjs/functions/ai.js +0 -83
- package/dist/mjs/functions/ai.test.d.ts +0 -1
- package/dist/mjs/functions/ai.test.js +0 -29
- package/dist/mjs/functions/gpt.d.ts +0 -4
- package/dist/mjs/functions/gpt.js +0 -10
- package/dist/mjs/functions/list.d.ts +0 -7
- package/dist/mjs/functions/list.js +0 -72
- package/dist/mjs/index.d.ts +0 -3
- package/dist/mjs/index.js +0 -3
- package/dist/mjs/queue/kafka.d.ts +0 -0
- package/dist/mjs/queue/kafka.js +0 -1
- package/dist/mjs/queue/memory.d.ts +0 -0
- package/dist/mjs/queue/memory.js +0 -1
- package/dist/mjs/queue/mongo.d.ts +0 -30
- package/dist/mjs/queue/mongo.js +0 -42
- package/dist/mjs/streams/kafka.d.ts +0 -0
- package/dist/mjs/streams/kafka.js +0 -1
- package/dist/mjs/streams/memory.d.ts +0 -0
- package/dist/mjs/streams/memory.js +0 -1
- package/dist/mjs/streams/mongo.d.ts +0 -0
- package/dist/mjs/streams/mongo.js +0 -1
- package/dist/mjs/streams/types.d.ts +0 -0
- package/dist/mjs/streams/types.js +0 -1
- package/dist/mjs/types.d.ts +0 -11
- package/dist/mjs/types.js +0 -1
- package/dist/mjs/utils/completion.d.ts +0 -9
- package/dist/mjs/utils/completion.js +0 -20
- package/dist/mjs/utils/schema.d.ts +0 -10
- package/dist/mjs/utils/schema.js +0 -72
- package/dist/mjs/utils/schema.test.d.ts +0 -1
- package/dist/mjs/utils/schema.test.js +0 -60
- package/dist/mjs/utils/state.d.ts +0 -1
- package/dist/mjs/utils/state.js +0 -19
- package/examples/data.ts +0 -1105
- package/fixup +0 -11
- package/functions/ai.test.ts +0 -41
- package/functions/ai.ts +0 -115
- package/functions/gpt.ts +0 -12
- package/functions/list.ts +0 -84
- package/index.ts +0 -3
- package/queue/kafka.ts +0 -0
- package/queue/memory.ts +0 -0
- package/queue/mongo.ts +0 -88
- package/streams/kafka.ts +0 -0
- package/streams/memory.ts +0 -0
- package/streams/mongo.ts +0 -0
- package/streams/types.ts +0 -0
- package/tsconfig-backup.json +0 -105
- package/tsconfig-base.json +0 -26
- package/tsconfig-cjs.json +0 -8
- package/types.ts +0 -12
- package/utils/completion.ts +0 -28
- package/utils/schema.test.ts +0 -69
- package/utils/schema.ts +0 -74
- package/utils/state.ts +0 -23
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blog Post Generation Live Tests
|
|
3
|
+
*
|
|
4
|
+
* These tests run LIVE against real AI providers by default.
|
|
5
|
+
* They verify the complete blog generation workflow:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* const titles = await list`10 blog post titles about ${topic}`
|
|
9
|
+
* const posts = titles.map(title => write`a blog post starting with "# ${title}"`)
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Tests cover:
|
|
13
|
+
* - Real API calls to OpenAI, Anthropic, etc.
|
|
14
|
+
* - Action/event storage in the database
|
|
15
|
+
* - Both realtime and batch execution modes
|
|
16
|
+
* - Multiple providers
|
|
17
|
+
*
|
|
18
|
+
* Run with:
|
|
19
|
+
* ```bash
|
|
20
|
+
* pnpm test blog-generation.live
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Skip live tests (CI without API keys):
|
|
24
|
+
* ```bash
|
|
25
|
+
* SKIP_LIVE_TESTS=true pnpm test blog-generation.live
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @packageDocumentation
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { describe, it, expect, beforeEach, afterEach, beforeAll } from 'vitest'
|
|
32
|
+
import {
|
|
33
|
+
configure,
|
|
34
|
+
resetContext,
|
|
35
|
+
withContext,
|
|
36
|
+
getProvider,
|
|
37
|
+
getModel,
|
|
38
|
+
getBatchMode,
|
|
39
|
+
} from '../src/context.js'
|
|
40
|
+
import { createBatch, withBatch, type BatchProvider } from '../src/batch-queue.js'
|
|
41
|
+
import { generateObject, generateText } from '../src/generate.js'
|
|
42
|
+
|
|
43
|
+
// Database provider for action/event storage
|
|
44
|
+
import { createMemoryProvider, type MemoryProvider, type Action, type Event } from '../../ai-database/src/memory-provider.js'
|
|
45
|
+
|
|
46
|
+
// Batch storage
|
|
47
|
+
import '../src/batch/memory.js'
|
|
48
|
+
import { configureMemoryAdapter, clearBatches, getBatches } from '../src/batch/memory.js'
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Configuration
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
const SKIP_LIVE = process.env.SKIP_LIVE_TESTS === 'true'
|
|
55
|
+
const describeLive = SKIP_LIVE ? describe.skip : describe
|
|
56
|
+
|
|
57
|
+
// Detect available providers
|
|
58
|
+
const hasOpenAI = !!process.env.OPENAI_API_KEY
|
|
59
|
+
const hasAnthropic = !!process.env.ANTHROPIC_API_KEY
|
|
60
|
+
|
|
61
|
+
// Provider configs
|
|
62
|
+
const PROVIDERS: Record<string, { model: string; available: boolean }> = {
|
|
63
|
+
openai: { model: 'gpt-4o-mini', available: hasOpenAI },
|
|
64
|
+
anthropic: { model: 'claude-sonnet-4-20250514', available: hasAnthropic },
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Get first available provider
|
|
68
|
+
const defaultProvider = Object.entries(PROVIDERS).find(([, cfg]) => cfg.available)?.[0] || 'openai'
|
|
69
|
+
const defaultModel = PROVIDERS[defaultProvider]?.model || 'gpt-4o-mini'
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Database Setup
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
let db: MemoryProvider
|
|
76
|
+
let capturedEvents: Event[] = []
|
|
77
|
+
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// Test Helpers
|
|
80
|
+
// ============================================================================
|
|
81
|
+
|
|
82
|
+
async function createAction(data: {
|
|
83
|
+
action: string
|
|
84
|
+
object: string
|
|
85
|
+
objectData?: Record<string, unknown>
|
|
86
|
+
total?: number
|
|
87
|
+
}): Promise<Action> {
|
|
88
|
+
return db.createAction({
|
|
89
|
+
actor: 'test:live',
|
|
90
|
+
action: data.action,
|
|
91
|
+
object: data.object,
|
|
92
|
+
objectData: data.objectData,
|
|
93
|
+
total: data.total,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function generateTitles(topic: string, count: number): Promise<{ titles: string[]; action: Action }> {
|
|
98
|
+
const action = await createAction({
|
|
99
|
+
action: 'generate',
|
|
100
|
+
object: 'BlogTitles',
|
|
101
|
+
objectData: { topic, count },
|
|
102
|
+
total: 1,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
await db.updateAction(action.id, { status: 'active' })
|
|
106
|
+
|
|
107
|
+
const result = await generateObject({
|
|
108
|
+
model: getModel(),
|
|
109
|
+
schema: { titles: [`${count} blog post titles about ${topic}`] },
|
|
110
|
+
prompt: `Generate exactly ${count} creative blog post titles about "${topic}".`,
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const titles = (result.object as { titles: string[] }).titles
|
|
114
|
+
|
|
115
|
+
await db.updateAction(action.id, {
|
|
116
|
+
status: 'completed',
|
|
117
|
+
progress: 1,
|
|
118
|
+
result: { titles },
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
return { titles, action }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function generatePost(title: string): Promise<string> {
|
|
125
|
+
const result = await generateText({
|
|
126
|
+
model: getModel(),
|
|
127
|
+
prompt: `Write a blog post starting with "# ${title}"\n\nInclude an introduction, 2-3 sections, and conclusion. Be concise.`,
|
|
128
|
+
maxTokens: 800,
|
|
129
|
+
})
|
|
130
|
+
return result.text
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function generatePosts(
|
|
134
|
+
titles: string[],
|
|
135
|
+
mode: 'realtime' | 'batch' = 'realtime'
|
|
136
|
+
): Promise<{ posts: string[]; action: Action }> {
|
|
137
|
+
const action = await createAction({
|
|
138
|
+
action: 'generate',
|
|
139
|
+
object: 'BlogPosts',
|
|
140
|
+
objectData: { titles, mode },
|
|
141
|
+
total: titles.length,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
await db.updateAction(action.id, { status: 'active' })
|
|
145
|
+
|
|
146
|
+
const posts: string[] = []
|
|
147
|
+
|
|
148
|
+
if (mode === 'batch') {
|
|
149
|
+
const batch = createBatch({
|
|
150
|
+
provider: getProvider(),
|
|
151
|
+
model: getModel(),
|
|
152
|
+
metadata: { actionId: action.id },
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
titles.forEach((title, i) => {
|
|
156
|
+
batch.add(
|
|
157
|
+
`Write a blog post starting with "# ${title}"\n\nBe concise.`,
|
|
158
|
+
{ customId: `post-${i}`, metadata: { title } }
|
|
159
|
+
)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const { completion } = await batch.submit()
|
|
163
|
+
const results = await completion
|
|
164
|
+
|
|
165
|
+
for (const result of results) {
|
|
166
|
+
posts.push(result.status === 'completed' ? (result.result as string) : `[Failed]`)
|
|
167
|
+
await db.updateAction(action.id, { progress: posts.length })
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
for (let i = 0; i < titles.length; i++) {
|
|
171
|
+
const post = await generatePost(titles[i])
|
|
172
|
+
posts.push(post)
|
|
173
|
+
await db.updateAction(action.id, { progress: i + 1 })
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await db.updateAction(action.id, { status: 'completed', result: { count: posts.length } })
|
|
178
|
+
|
|
179
|
+
return { posts, action }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// Tests
|
|
184
|
+
// ============================================================================
|
|
185
|
+
|
|
186
|
+
describeLive('Blog Generation (Live)', () => {
|
|
187
|
+
beforeAll(() => {
|
|
188
|
+
console.log('\n🔴 LIVE TEST MODE')
|
|
189
|
+
console.log(` Default provider: ${defaultProvider}`)
|
|
190
|
+
console.log(` OpenAI available: ${hasOpenAI}`)
|
|
191
|
+
console.log(` Anthropic available: ${hasAnthropic}\n`)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
beforeEach(() => {
|
|
195
|
+
resetContext()
|
|
196
|
+
db = createMemoryProvider()
|
|
197
|
+
capturedEvents = []
|
|
198
|
+
db.on('*', (e) => capturedEvents.push(e))
|
|
199
|
+
configure({ provider: defaultProvider as BatchProvider, model: defaultModel, batchMode: 'immediate' })
|
|
200
|
+
clearBatches()
|
|
201
|
+
configureMemoryAdapter({})
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
afterEach(() => {
|
|
205
|
+
resetContext()
|
|
206
|
+
clearBatches()
|
|
207
|
+
db.clear()
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// ==========================================================================
|
|
211
|
+
// Core Pattern Tests
|
|
212
|
+
// ==========================================================================
|
|
213
|
+
|
|
214
|
+
describe('Core Pattern: list → write', () => {
|
|
215
|
+
it('generates titles from a topic', async () => {
|
|
216
|
+
const { titles, action } = await generateTitles('building AI products', 3)
|
|
217
|
+
|
|
218
|
+
expect(titles).toHaveLength(3)
|
|
219
|
+
titles.forEach((t) => expect(typeof t).toBe('string'))
|
|
220
|
+
|
|
221
|
+
// Verify action tracking
|
|
222
|
+
expect(action.status).toBe('completed')
|
|
223
|
+
expect(action.result).toEqual({ titles })
|
|
224
|
+
}, 30000)
|
|
225
|
+
|
|
226
|
+
it('generates a blog post from a title', async () => {
|
|
227
|
+
const title = 'The Future of AI Development'
|
|
228
|
+
const post = await generatePost(title)
|
|
229
|
+
|
|
230
|
+
expect(post).toContain(`# ${title}`)
|
|
231
|
+
expect(post.length).toBeGreaterThan(200)
|
|
232
|
+
}, 30000)
|
|
233
|
+
|
|
234
|
+
it('generates multiple posts from titles', async () => {
|
|
235
|
+
const { titles } = await generateTitles('startup growth', 2)
|
|
236
|
+
const { posts, action } = await generatePosts(titles)
|
|
237
|
+
|
|
238
|
+
expect(posts).toHaveLength(2)
|
|
239
|
+
posts.forEach((post, i) => {
|
|
240
|
+
expect(post).toContain(`# ${titles[i]}`)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
expect(action.progress).toBe(2)
|
|
244
|
+
expect(action.total).toBe(2)
|
|
245
|
+
}, 60000)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
// ==========================================================================
|
|
249
|
+
// Action & Event Storage
|
|
250
|
+
// ==========================================================================
|
|
251
|
+
|
|
252
|
+
describe('Action & Event Storage', () => {
|
|
253
|
+
it('creates actions with verb conjugation', async () => {
|
|
254
|
+
const { action } = await generateTitles('test topic', 2)
|
|
255
|
+
|
|
256
|
+
expect(action.action).toBe('generate')
|
|
257
|
+
expect(action.act).toBe('generates')
|
|
258
|
+
expect(action.activity).toBe('generating')
|
|
259
|
+
expect(action.actor).toBe('test:live')
|
|
260
|
+
}, 30000)
|
|
261
|
+
|
|
262
|
+
it('tracks action lifecycle timestamps', async () => {
|
|
263
|
+
const { action } = await generateTitles('test', 2)
|
|
264
|
+
|
|
265
|
+
const final = await db.getAction(action.id)
|
|
266
|
+
expect(final?.createdAt).toBeInstanceOf(Date)
|
|
267
|
+
expect(final?.startedAt).toBeInstanceOf(Date)
|
|
268
|
+
expect(final?.completedAt).toBeInstanceOf(Date)
|
|
269
|
+
expect(final?.startedAt!.getTime()).toBeGreaterThanOrEqual(final?.createdAt!.getTime()!)
|
|
270
|
+
expect(final?.completedAt!.getTime()).toBeGreaterThanOrEqual(final?.startedAt!.getTime()!)
|
|
271
|
+
}, 30000)
|
|
272
|
+
|
|
273
|
+
it('emits events for state transitions', async () => {
|
|
274
|
+
await generateTitles('test', 2)
|
|
275
|
+
|
|
276
|
+
const actionEvents = capturedEvents.filter((e) => e.event.startsWith('Action.'))
|
|
277
|
+
const eventTypes = actionEvents.map((e) => e.event)
|
|
278
|
+
|
|
279
|
+
expect(eventTypes).toContain('Action.created')
|
|
280
|
+
expect(eventTypes).toContain('Action.started')
|
|
281
|
+
expect(eventTypes).toContain('Action.completed')
|
|
282
|
+
}, 30000)
|
|
283
|
+
|
|
284
|
+
it('stores action result data', async () => {
|
|
285
|
+
const { titles, action } = await generateTitles('AI development', 3)
|
|
286
|
+
|
|
287
|
+
const stored = await db.getAction(action.id)
|
|
288
|
+
expect(stored?.result).toEqual({ titles })
|
|
289
|
+
expect(stored?.objectData).toEqual({ topic: 'AI development', count: 3 })
|
|
290
|
+
}, 30000)
|
|
291
|
+
|
|
292
|
+
it('queries actions by status', async () => {
|
|
293
|
+
await generateTitles('topic 1', 2)
|
|
294
|
+
await generateTitles('topic 2', 2)
|
|
295
|
+
|
|
296
|
+
const completed = await db.listActions({ status: 'completed' })
|
|
297
|
+
expect(completed.length).toBe(2)
|
|
298
|
+
}, 60000)
|
|
299
|
+
|
|
300
|
+
it('queries events by type pattern', async () => {
|
|
301
|
+
await generateTitles('test', 2)
|
|
302
|
+
|
|
303
|
+
const created = await db.listEvents({ event: 'Action.created' })
|
|
304
|
+
const allAction = await db.listEvents({ event: 'Action.*' })
|
|
305
|
+
|
|
306
|
+
expect(created.length).toBeGreaterThanOrEqual(1)
|
|
307
|
+
expect(allAction.length).toBeGreaterThanOrEqual(3) // created, started, completed
|
|
308
|
+
}, 30000)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
// ==========================================================================
|
|
312
|
+
// Realtime Execution
|
|
313
|
+
// ==========================================================================
|
|
314
|
+
|
|
315
|
+
describe('Realtime Execution', () => {
|
|
316
|
+
beforeEach(() => {
|
|
317
|
+
configure({ batchMode: 'immediate' })
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('executes requests immediately', async () => {
|
|
321
|
+
const start = Date.now()
|
|
322
|
+
const { titles } = await generateTitles('quick test', 2)
|
|
323
|
+
const elapsed = Date.now() - start
|
|
324
|
+
|
|
325
|
+
expect(titles).toHaveLength(2)
|
|
326
|
+
// Should complete in reasonable time (not batched/delayed)
|
|
327
|
+
expect(elapsed).toBeLessThan(30000)
|
|
328
|
+
}, 30000)
|
|
329
|
+
|
|
330
|
+
it('tracks progress during sequential generation', async () => {
|
|
331
|
+
const titles = ['Post One', 'Post Two']
|
|
332
|
+
const { posts, action } = await generatePosts(titles, 'realtime')
|
|
333
|
+
|
|
334
|
+
expect(posts).toHaveLength(2)
|
|
335
|
+
|
|
336
|
+
const final = await db.getAction(action.id)
|
|
337
|
+
expect(final?.progress).toBe(2)
|
|
338
|
+
expect(final?.total).toBe(2)
|
|
339
|
+
}, 60000)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
// ==========================================================================
|
|
343
|
+
// Batch Execution
|
|
344
|
+
// ==========================================================================
|
|
345
|
+
|
|
346
|
+
describe('Batch Execution', () => {
|
|
347
|
+
beforeEach(() => {
|
|
348
|
+
configure({ batchMode: 'deferred' })
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('creates and submits batch jobs', async () => {
|
|
352
|
+
const titles = ['Batch Post 1', 'Batch Post 2']
|
|
353
|
+
const { posts, action } = await generatePosts(titles, 'batch')
|
|
354
|
+
|
|
355
|
+
expect(posts).toHaveLength(2)
|
|
356
|
+
|
|
357
|
+
// Verify batch was stored
|
|
358
|
+
const batches = getBatches()
|
|
359
|
+
expect(batches.size).toBe(1)
|
|
360
|
+
|
|
361
|
+
const [, batch] = [...batches.entries()][0]
|
|
362
|
+
expect(batch.items).toHaveLength(2)
|
|
363
|
+
expect(batch.status).toBe('completed')
|
|
364
|
+
}, 90000)
|
|
365
|
+
|
|
366
|
+
it('stores batch metadata', async () => {
|
|
367
|
+
const batch = createBatch({
|
|
368
|
+
provider: getProvider(),
|
|
369
|
+
model: getModel(),
|
|
370
|
+
metadata: { task: 'test-batch', timestamp: Date.now() },
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
batch.add('Write a test post', { customId: 'test-1' })
|
|
374
|
+
const { job } = await batch.submit()
|
|
375
|
+
await batch.wait()
|
|
376
|
+
|
|
377
|
+
const stored = getBatches().get(job.id)
|
|
378
|
+
expect(stored?.options.metadata?.task).toBe('test-batch')
|
|
379
|
+
}, 30000)
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
// ==========================================================================
|
|
383
|
+
// Multi-Provider Tests
|
|
384
|
+
// ==========================================================================
|
|
385
|
+
|
|
386
|
+
describe('Multi-Provider', () => {
|
|
387
|
+
it.skipIf(!hasOpenAI)('generates with OpenAI', async () => {
|
|
388
|
+
configure({ provider: 'openai', model: 'gpt-4o-mini' })
|
|
389
|
+
|
|
390
|
+
const { titles } = await generateTitles('OpenAI test', 2)
|
|
391
|
+
expect(titles).toHaveLength(2)
|
|
392
|
+
expect(getProvider()).toBe('openai')
|
|
393
|
+
}, 30000)
|
|
394
|
+
|
|
395
|
+
it.skipIf(!hasAnthropic)('generates with Anthropic', async () => {
|
|
396
|
+
configure({ provider: 'anthropic', model: 'claude-sonnet-4-20250514' })
|
|
397
|
+
|
|
398
|
+
const { titles } = await generateTitles('Anthropic test', 2)
|
|
399
|
+
expect(titles).toHaveLength(2)
|
|
400
|
+
expect(getProvider()).toBe('anthropic')
|
|
401
|
+
}, 30000)
|
|
402
|
+
|
|
403
|
+
it.skipIf(!hasOpenAI || !hasAnthropic)('switches providers mid-workflow', async () => {
|
|
404
|
+
// Generate titles with OpenAI
|
|
405
|
+
configure({ provider: 'openai', model: 'gpt-4o-mini' })
|
|
406
|
+
const { titles } = await generateTitles('cross-provider test', 2)
|
|
407
|
+
|
|
408
|
+
// Generate posts with Anthropic
|
|
409
|
+
const posts = await withContext(
|
|
410
|
+
{ provider: 'anthropic', model: 'claude-sonnet-4-20250514' },
|
|
411
|
+
async () => {
|
|
412
|
+
expect(getProvider()).toBe('anthropic')
|
|
413
|
+
return Promise.all(titles.slice(0, 1).map(generatePost))
|
|
414
|
+
}
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
expect(posts).toHaveLength(1)
|
|
418
|
+
expect(getProvider()).toBe('openai') // restored
|
|
419
|
+
}, 60000)
|
|
420
|
+
|
|
421
|
+
it.skipIf(!hasOpenAI || !hasAnthropic)('runs providers in parallel', async () => {
|
|
422
|
+
const [openaiResult, anthropicResult] = await Promise.all([
|
|
423
|
+
withContext({ provider: 'openai', model: 'gpt-4o-mini' }, () =>
|
|
424
|
+
generateTitles('OpenAI parallel', 2)
|
|
425
|
+
),
|
|
426
|
+
withContext({ provider: 'anthropic', model: 'claude-sonnet-4-20250514' }, () =>
|
|
427
|
+
generateTitles('Anthropic parallel', 2)
|
|
428
|
+
),
|
|
429
|
+
])
|
|
430
|
+
|
|
431
|
+
expect(openaiResult.titles).toHaveLength(2)
|
|
432
|
+
expect(anthropicResult.titles).toHaveLength(2)
|
|
433
|
+
}, 60000)
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
// ==========================================================================
|
|
437
|
+
// Error Handling
|
|
438
|
+
// ==========================================================================
|
|
439
|
+
|
|
440
|
+
describe('Error Handling', () => {
|
|
441
|
+
it('tracks failed actions', async () => {
|
|
442
|
+
const action = await createAction({
|
|
443
|
+
action: 'generate',
|
|
444
|
+
object: 'FailTest',
|
|
445
|
+
total: 1,
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
await db.updateAction(action.id, { status: 'active' })
|
|
449
|
+
|
|
450
|
+
// Simulate failure
|
|
451
|
+
await db.updateAction(action.id, {
|
|
452
|
+
status: 'failed',
|
|
453
|
+
error: 'Simulated failure',
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
const failed = await db.listActions({ status: 'failed' })
|
|
457
|
+
expect(failed).toHaveLength(1)
|
|
458
|
+
expect(failed[0].error).toBe('Simulated failure')
|
|
459
|
+
|
|
460
|
+
// Verify failure event
|
|
461
|
+
const failEvents = capturedEvents.filter((e) => e.event === 'Action.failed')
|
|
462
|
+
expect(failEvents.length).toBe(1)
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('handles invalid model gracefully', async () => {
|
|
466
|
+
configure({ model: 'invalid-model-xyz' })
|
|
467
|
+
|
|
468
|
+
const action = await createAction({
|
|
469
|
+
action: 'generate',
|
|
470
|
+
object: 'InvalidModelTest',
|
|
471
|
+
total: 1,
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
await db.updateAction(action.id, { status: 'active' })
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
await generateObject({
|
|
478
|
+
model: 'invalid-model-xyz',
|
|
479
|
+
schema: { test: 'test' },
|
|
480
|
+
prompt: 'test',
|
|
481
|
+
})
|
|
482
|
+
} catch (e) {
|
|
483
|
+
await db.updateAction(action.id, {
|
|
484
|
+
status: 'failed',
|
|
485
|
+
error: e instanceof Error ? e.message : 'Unknown error',
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const final = await db.getAction(action.id)
|
|
490
|
+
expect(final?.status).toBe('failed')
|
|
491
|
+
expect(final?.error).toBeDefined()
|
|
492
|
+
}, 30000)
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
// ==========================================================================
|
|
496
|
+
// Database Statistics
|
|
497
|
+
// ==========================================================================
|
|
498
|
+
|
|
499
|
+
describe('Database Statistics', () => {
|
|
500
|
+
it('tracks aggregate stats', async () => {
|
|
501
|
+
await generateTitles('stats test 1', 2)
|
|
502
|
+
await generateTitles('stats test 2', 2)
|
|
503
|
+
|
|
504
|
+
const stats = db.stats()
|
|
505
|
+
|
|
506
|
+
expect(stats.actions.completed).toBe(2)
|
|
507
|
+
expect(stats.events).toBeGreaterThan(0)
|
|
508
|
+
}, 60000)
|
|
509
|
+
})
|
|
510
|
+
})
|