bazilion 0.0.0 → 0.1.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/README.md +53 -152
- package/dist/cli.js +2730 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon.js +5888 -0
- package/dist/daemon.js.map +1 -0
- package/dist/worker.js +1830 -0
- package/dist/worker.js.map +1 -0
- package/package.json +57 -28
- package/.understand-anything/.understandignore +0 -25
- package/.understand-anything/fingerprints.json +0 -14267
- package/.understand-anything/knowledge-graph.json +0 -18128
- package/.understand-anything/meta.json +0 -6
- package/CLAUDE.md +0 -164
- package/apps/cli/package.json +0 -21
- package/apps/cli/src/auth-file.ts +0 -18
- package/apps/cli/src/client.ts +0 -37
- package/apps/cli/src/columnize.ts +0 -32
- package/apps/cli/src/commands/agent.ts +0 -574
- package/apps/cli/src/commands/auth.ts +0 -110
- package/apps/cli/src/commands/backup.ts +0 -135
- package/apps/cli/src/commands/completion.ts +0 -155
- package/apps/cli/src/commands/config.ts +0 -95
- package/apps/cli/src/commands/doctor.ts +0 -131
- package/apps/cli/src/commands/group.ts +0 -132
- package/apps/cli/src/commands/inbox.ts +0 -82
- package/apps/cli/src/commands/login.ts +0 -73
- package/apps/cli/src/commands/memory.ts +0 -106
- package/apps/cli/src/commands/profile.ts +0 -259
- package/apps/cli/src/commands/provider.ts +0 -170
- package/apps/cli/src/commands/send.ts +0 -24
- package/apps/cli/src/commands/serve.ts +0 -89
- package/apps/cli/src/commands/skill.ts +0 -120
- package/apps/cli/src/commands/token.ts +0 -148
- package/apps/cli/src/commands/trigger.ts +0 -129
- package/apps/cli/src/commands/uninstall.ts +0 -156
- package/apps/cli/src/index.ts +0 -196
- package/apps/cli/src/paths.ts +0 -12
- package/apps/cli/test/agent.test.ts +0 -213
- package/apps/cli/test/backup.test.ts +0 -95
- package/apps/cli/test/chat.test.ts +0 -539
- package/apps/cli/test/columnize.test.ts +0 -29
- package/apps/cli/test/completion.test.ts +0 -45
- package/apps/cli/test/config-page.test.ts +0 -151
- package/apps/cli/test/group.test.ts +0 -74
- package/apps/cli/test/helpers.ts +0 -70
- package/apps/cli/test/inbox-autodeliver.test.ts +0 -143
- package/apps/cli/test/inbox.test.ts +0 -147
- package/apps/cli/test/memory.test.ts +0 -69
- package/apps/cli/test/profile.test.ts +0 -110
- package/apps/cli/test/send.test.ts +0 -30
- package/apps/cli/test/server-fixture.ts +0 -212
- package/apps/cli/test/session-head.test.ts +0 -45
- package/apps/cli/test/skill.test.ts +0 -128
- package/apps/cli/test/token.test.ts +0 -109
- package/apps/cli/test/trigger.test.ts +0 -245
- package/apps/cli/tsconfig.json +0 -4
- package/apps/daemon/package.json +0 -31
- package/apps/daemon/src/app.ts +0 -41
- package/apps/daemon/src/core/agent/archive.ts +0 -8
- package/apps/daemon/src/core/agent/delete.ts +0 -30
- package/apps/daemon/src/core/agent/resolve.ts +0 -31
- package/apps/daemon/src/core/agent/spawn.ts +0 -95
- package/apps/daemon/src/core/agent/unarchive.ts +0 -11
- package/apps/daemon/src/core/availableModels.ts +0 -58
- package/apps/daemon/src/core/db/client.ts +0 -113
- package/apps/daemon/src/core/db/migrate.ts +0 -41
- package/apps/daemon/src/core/db/migrations/0001_init.sql +0 -179
- package/apps/daemon/src/core/group/delete.ts +0 -21
- package/apps/daemon/src/core/group/register.ts +0 -68
- package/apps/daemon/src/core/index.ts +0 -71
- package/apps/daemon/src/core/paths.ts +0 -56
- package/apps/daemon/src/core/profile/create.ts +0 -78
- package/apps/daemon/src/core/profile/delete.ts +0 -26
- package/apps/daemon/src/core/profile/identity.ts +0 -70
- package/apps/daemon/src/core/profile/load.ts +0 -45
- package/apps/daemon/src/core/profile/seed.ts +0 -74
- package/apps/daemon/src/core/profile/templates.ts +0 -60
- package/apps/daemon/src/core/profile/update.ts +0 -57
- package/apps/daemon/src/core/profile/validate.ts +0 -9
- package/apps/daemon/src/core/repos/agents.ts +0 -202
- package/apps/daemon/src/core/repos/config.ts +0 -78
- package/apps/daemon/src/core/repos/groups.ts +0 -55
- package/apps/daemon/src/core/repos/messages.ts +0 -127
- package/apps/daemon/src/core/repos/profiles.ts +0 -83
- package/apps/daemon/src/core/repos/providerModels.ts +0 -58
- package/apps/daemon/src/core/repos/providerState.ts +0 -37
- package/apps/daemon/src/core/repos/secrets.ts +0 -145
- package/apps/daemon/src/core/repos/skillMeta.ts +0 -49
- package/apps/daemon/src/core/repos/triggers.ts +0 -101
- package/apps/daemon/src/core/repos/webTokens.ts +0 -87
- package/apps/daemon/src/core/secrets.ts +0 -65
- package/apps/daemon/src/core/services.ts +0 -264
- package/apps/daemon/src/core/skills/discover.ts +0 -28
- package/apps/daemon/src/core/skills/import.ts +0 -136
- package/apps/daemon/src/core/skills/parse.ts +0 -52
- package/apps/daemon/src/core/skills/resolve.ts +0 -50
- package/apps/daemon/src/index.ts +0 -45
- package/apps/daemon/src/lib/agent-cancel.ts +0 -48
- package/apps/daemon/src/lib/agent-id.ts +0 -13
- package/apps/daemon/src/lib/agent-turn.ts +0 -56
- package/apps/daemon/src/lib/api-key.ts +0 -56
- package/apps/daemon/src/lib/auth.ts +0 -41
- package/apps/daemon/src/lib/cron.ts +0 -93
- package/apps/daemon/src/lib/ctx.ts +0 -80
- package/apps/daemon/src/lib/messaging-host.ts +0 -34
- package/apps/daemon/src/lib/middleware-auth.ts +0 -52
- package/apps/daemon/src/lib/scheduler.ts +0 -294
- package/apps/daemon/src/routes/agents.ts +0 -772
- package/apps/daemon/src/routes/auth-login.ts +0 -193
- package/apps/daemon/src/routes/config.ts +0 -267
- package/apps/daemon/src/routes/groups.ts +0 -133
- package/apps/daemon/src/routes/messages.ts +0 -29
- package/apps/daemon/src/routes/misc.ts +0 -239
- package/apps/daemon/src/routes/profiles.ts +0 -197
- package/apps/daemon/src/routes/skills.ts +0 -123
- package/apps/daemon/src/routes/triggers.ts +0 -29
- package/apps/daemon/src/runtime/auth/openai-codex.ts +0 -121
- package/apps/daemon/src/runtime/auto-reply/heartbeat.ts +0 -31
- package/apps/daemon/src/runtime/index.ts +0 -77
- package/apps/daemon/src/runtime/memory/files.ts +0 -103
- package/apps/daemon/src/runtime/memory/qmd.ts +0 -152
- package/apps/daemon/src/runtime/memory/types.ts +0 -16
- package/apps/daemon/src/runtime/pi/events.ts +0 -173
- package/apps/daemon/src/runtime/pi/session.ts +0 -536
- package/apps/daemon/src/runtime/pi/tools.ts +0 -85
- package/apps/daemon/src/runtime/providers/catalog.ts +0 -145
- package/apps/daemon/src/runtime/providers/pi-adapter.ts +0 -272
- package/apps/daemon/src/runtime/providers/registry.ts +0 -374
- package/apps/daemon/src/runtime/providers/retry.ts +0 -176
- package/apps/daemon/src/runtime/providers/types.ts +0 -33
- package/apps/daemon/src/runtime/session/prompt.ts +0 -83
- package/apps/daemon/src/runtime/tools/bootstrap.ts +0 -22
- package/apps/daemon/src/runtime/tools/home.ts +0 -114
- package/apps/daemon/src/runtime/tools/memory.ts +0 -81
- package/apps/daemon/src/runtime/tools/messaging.ts +0 -127
- package/apps/daemon/src/runtime/tools/registry.ts +0 -29
- package/apps/daemon/src/runtime/tools/types.ts +0 -13
- package/apps/daemon/src/runtime/tools/web-extract.ts +0 -110
- package/apps/daemon/src/runtime/tools/web-ssrf.ts +0 -245
- package/apps/daemon/src/runtime/tools/web.ts +0 -273
- package/apps/daemon/src/runtime/worker/entry.ts +0 -221
- package/apps/daemon/src/runtime/worker/ipc-protocol.ts +0 -75
- package/apps/daemon/src/runtime/worker/spawn.ts +0 -249
- package/apps/daemon/test/core/agents.test.ts +0 -319
- package/apps/daemon/test/core/available-models.test.ts +0 -63
- package/apps/daemon/test/core/config.test.ts +0 -99
- package/apps/daemon/test/core/groups.test.ts +0 -63
- package/apps/daemon/test/core/helpers.ts +0 -50
- package/apps/daemon/test/core/identity.test.ts +0 -85
- package/apps/daemon/test/core/migrations.test.ts +0 -83
- package/apps/daemon/test/core/profiles.test.ts +0 -182
- package/apps/daemon/test/core/provider-models.test.ts +0 -57
- package/apps/daemon/test/core/provider-state.test.ts +0 -36
- package/apps/daemon/test/core/skill-meta.test.ts +0 -45
- package/apps/daemon/test/core/skills.test.ts +0 -271
- package/apps/daemon/test/core/triggers.test.ts +0 -182
- package/apps/daemon/test/core/web-tokens.test.ts +0 -79
- package/apps/daemon/test/cron.test.ts +0 -90
- package/apps/daemon/test/runtime/heartbeat.test.ts +0 -34
- package/apps/daemon/test/runtime/memory-qmd.test.ts +0 -97
- package/apps/daemon/test/runtime/memory.test.ts +0 -78
- package/apps/daemon/test/runtime/messaging.test.ts +0 -213
- package/apps/daemon/test/runtime/mock-server.ts +0 -65
- package/apps/daemon/test/runtime/openai-codex-auth.test.ts +0 -116
- package/apps/daemon/test/runtime/providers.test.ts +0 -306
- package/apps/daemon/test/runtime/retry.test.ts +0 -191
- package/apps/daemon/test/runtime/session-head.test.ts +0 -90
- package/apps/daemon/test/runtime/tools-home.test.ts +0 -102
- package/apps/daemon/test/runtime/tools-web.test.ts +0 -206
- package/apps/daemon/tsconfig.json +0 -4
- package/apps/mobile/README.md +0 -60
- package/apps/mobile/app/_layout.tsx +0 -58
- package/apps/mobile/app/agents/[id]/chat.tsx +0 -486
- package/apps/mobile/app/agents/[id]/index.tsx +0 -166
- package/apps/mobile/app/agents/index.tsx +0 -212
- package/apps/mobile/app/index.tsx +0 -21
- package/apps/mobile/app/pair.tsx +0 -226
- package/apps/mobile/app/settings.tsx +0 -419
- package/apps/mobile/app.json +0 -49
- package/apps/mobile/assets/adaptive-icon.png +0 -0
- package/apps/mobile/assets/favicon.png +0 -0
- package/apps/mobile/assets/icon.png +0 -0
- package/apps/mobile/assets/splash-icon.png +0 -0
- package/apps/mobile/babel.config.js +0 -6
- package/apps/mobile/metro.config.js +0 -28
- package/apps/mobile/package.json +0 -44
- package/apps/mobile/src/auth.ts +0 -66
- package/apps/mobile/src/pair-url.ts +0 -48
- package/apps/mobile/src/theme-context.tsx +0 -88
- package/apps/mobile/src/theme.ts +0 -135
- package/apps/mobile/test/pair-url.test.ts +0 -46
- package/apps/mobile/tsconfig.json +0 -23
- package/apps/web/components.json +0 -25
- package/apps/web/package.json +0 -39
- package/apps/web/public/baziu.svg +0 -8
- package/apps/web/src/components/AgentTabs.tsx +0 -45
- package/apps/web/src/components/BaziuLogo.tsx +0 -21
- package/apps/web/src/components/ChatPane.tsx +0 -1033
- package/apps/web/src/components/ConfigTabs.tsx +0 -29
- package/apps/web/src/components/CopyButton.tsx +0 -68
- package/apps/web/src/components/CreateGroupDialog.tsx +0 -127
- package/apps/web/src/components/FieldRow.tsx +0 -94
- package/apps/web/src/components/Footer.tsx +0 -10
- package/apps/web/src/components/PawIcon.tsx +0 -15
- package/apps/web/src/components/Sidebar.tsx +0 -287
- package/apps/web/src/components/SpawnDialog.tsx +0 -129
- package/apps/web/src/components/ThemeToggle.tsx +0 -75
- package/apps/web/src/components/TopNav.tsx +0 -34
- package/apps/web/src/components/ui/button.tsx +0 -67
- package/apps/web/src/components/ui/card.tsx +0 -103
- package/apps/web/src/components/ui/checkbox.tsx +0 -31
- package/apps/web/src/components/ui/dialog.tsx +0 -168
- package/apps/web/src/components/ui/input.tsx +0 -19
- package/apps/web/src/components/ui/label.tsx +0 -22
- package/apps/web/src/components/ui/radio-group.tsx +0 -44
- package/apps/web/src/components/ui/select.tsx +0 -192
- package/apps/web/src/components/ui/separator.tsx +0 -26
- package/apps/web/src/components/ui/table.tsx +0 -116
- package/apps/web/src/components/ui/tabs.tsx +0 -88
- package/apps/web/src/components/ui/textarea.tsx +0 -18
- package/apps/web/src/lib/auth.ts +0 -50
- package/apps/web/src/lib/daemon-client.ts +0 -34
- package/apps/web/src/lib/md.ts +0 -45
- package/apps/web/src/lib/utils.ts +0 -6
- package/apps/web/src/lib/wire-constants.ts +0 -27
- package/apps/web/src/routeTree.gen.ts +0 -408
- package/apps/web/src/router.tsx +0 -20
- package/apps/web/src/routes/__root.tsx +0 -123
- package/apps/web/src/routes/agents/$id/inbox.tsx +0 -207
- package/apps/web/src/routes/agents/$id/index.tsx +0 -527
- package/apps/web/src/routes/agents/$id/triggers.tsx +0 -239
- package/apps/web/src/routes/agents/index.tsx +0 -265
- package/apps/web/src/routes/api/$.ts +0 -88
- package/apps/web/src/routes/config/index.tsx +0 -315
- package/apps/web/src/routes/config/services.tsx +0 -49
- package/apps/web/src/routes/config/tokens.tsx +0 -192
- package/apps/web/src/routes/groups/$id/index.tsx +0 -153
- package/apps/web/src/routes/groups/$id/memory.tsx +0 -321
- package/apps/web/src/routes/groups/index.tsx +0 -191
- package/apps/web/src/routes/index.tsx +0 -133
- package/apps/web/src/routes/login.tsx +0 -63
- package/apps/web/src/routes/profiles/$id.tsx +0 -549
- package/apps/web/src/routes/profiles/index.tsx +0 -458
- package/apps/web/src/routes/skills/index.tsx +0 -297
- package/apps/web/src/routes/welcome.tsx +0 -61
- package/apps/web/src/styles.css +0 -449
- package/apps/web/tsconfig.json +0 -25
- package/apps/web/vite.config.ts +0 -25
- package/biome.json +0 -23
- package/docs/agent-engine.md +0 -219
- package/docs/architecture.md +0 -627
- package/docs/backlog/README.md +0 -42
- package/docs/backlog/draft/BAZ-001-a2a-federation-spike.md +0 -125
- package/docs/openclaw-reference.md +0 -210
- package/packages/api-types/package.json +0 -11
- package/packages/api-types/src/entities.ts +0 -146
- package/packages/api-types/src/events.ts +0 -55
- package/packages/api-types/src/index.ts +0 -488
- package/packages/api-types/src/memory.ts +0 -15
- package/packages/client/package.json +0 -13
- package/packages/client/src/index.ts +0 -117
- package/pnpm-workspace.yaml +0 -3
- package/tsconfig.base.json +0 -23
- package/tsconfig.json +0 -11
- package/vitest.config.ts +0 -22
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
// Unit tests for the messaging `ToolHandler`s that Bazilion exposes as pi
|
|
2
|
-
// customTools. Called directly here (not through the LLM loop) because the
|
|
3
|
-
// tests are about tool *behaviour* — DB side effects, polling, error paths —
|
|
4
|
-
// not about round-tripping through pi's agent loop.
|
|
5
|
-
|
|
6
|
-
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'
|
|
7
|
-
import { tmpdir } from 'node:os'
|
|
8
|
-
import { join } from 'node:path'
|
|
9
|
-
import type { ResolvedAgent } from '@bazilion/api-types'
|
|
10
|
-
import { afterEach, beforeEach, expect, test } from 'vitest'
|
|
11
|
-
import {
|
|
12
|
-
agentRepo,
|
|
13
|
-
type BazilionDb,
|
|
14
|
-
createProfile,
|
|
15
|
-
messageRepo,
|
|
16
|
-
openInMemoryDb,
|
|
17
|
-
type Paths,
|
|
18
|
-
registerGroup,
|
|
19
|
-
resolveAgent,
|
|
20
|
-
resolvePaths,
|
|
21
|
-
runMigrations,
|
|
22
|
-
spawnAgent,
|
|
23
|
-
} from '../../src/core/index.ts'
|
|
24
|
-
import { messagingTools } from '../../src/runtime/tools/messaging.ts'
|
|
25
|
-
import type { ToolHandler } from '../../src/runtime/tools/types.ts'
|
|
26
|
-
import type { MessagingHost } from '../../src/runtime/worker/ipc-protocol.ts'
|
|
27
|
-
|
|
28
|
-
function dbHost(db: BazilionDb): MessagingHost {
|
|
29
|
-
return {
|
|
30
|
-
agentExists: (id) => agentRepo.get(db, id) !== null,
|
|
31
|
-
sendMessage: (input) => ({ messageId: messageRepo.send(db, input).id }),
|
|
32
|
-
listInbox: (id, opts) => messageRepo.listInbox(db, id, opts),
|
|
33
|
-
markRead: (id) => messageRepo.markRead(db, id),
|
|
34
|
-
findReplies: (id, replyTo) => messageRepo.findReplies(db, id, replyTo),
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface Env {
|
|
39
|
-
home: string
|
|
40
|
-
paths: Paths
|
|
41
|
-
db: BazilionDb
|
|
42
|
-
groupId: string
|
|
43
|
-
cleanup(): void
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function makeEnv(): Env {
|
|
47
|
-
const home = mkdtempSync(join(tmpdir(), 'bazilion-msg-'))
|
|
48
|
-
const paths = resolvePaths(home)
|
|
49
|
-
for (const d of [
|
|
50
|
-
paths.profilesDir,
|
|
51
|
-
paths.agentsDir,
|
|
52
|
-
paths.skillsDir,
|
|
53
|
-
paths.logsDir,
|
|
54
|
-
paths.groupsDir,
|
|
55
|
-
]) {
|
|
56
|
-
mkdirSync(d, { recursive: true })
|
|
57
|
-
}
|
|
58
|
-
const db = openInMemoryDb()
|
|
59
|
-
runMigrations(db)
|
|
60
|
-
const g = registerGroup(db, { id: 'test-group', name: 'test' }, paths)
|
|
61
|
-
return {
|
|
62
|
-
home,
|
|
63
|
-
paths,
|
|
64
|
-
db,
|
|
65
|
-
groupId: g.id,
|
|
66
|
-
cleanup() {
|
|
67
|
-
db.close()
|
|
68
|
-
rmSync(home, { recursive: true, force: true })
|
|
69
|
-
},
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function makeAgent(env: Env, name: string): ResolvedAgent {
|
|
74
|
-
const a = spawnAgent(env.db, env.paths, {
|
|
75
|
-
profileId: 'p',
|
|
76
|
-
name,
|
|
77
|
-
groupId: env.groupId,
|
|
78
|
-
})
|
|
79
|
-
return resolveAgent(env.db, env.paths, a.id)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function findHandler(handlers: ToolHandler[], name: string): ToolHandler {
|
|
83
|
-
const h = handlers.find((x) => x.def.name === name)
|
|
84
|
-
if (!h) throw new Error(`no handler: ${name}`)
|
|
85
|
-
return h
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
let env: Env
|
|
89
|
-
beforeEach(() => {
|
|
90
|
-
env = makeEnv()
|
|
91
|
-
createProfile(env.db, env.paths, {
|
|
92
|
-
id: 'p',
|
|
93
|
-
defaultModel: 'mock:m',
|
|
94
|
-
})
|
|
95
|
-
})
|
|
96
|
-
afterEach(() => env.cleanup())
|
|
97
|
-
|
|
98
|
-
test('send_message tool inserts a row addressed to the recipient', async () => {
|
|
99
|
-
const a = makeAgent(env, 'A')
|
|
100
|
-
const b = makeAgent(env, 'B')
|
|
101
|
-
const handlers = messagingTools(dbHost(env.db), a.agent.id)
|
|
102
|
-
const send = findHandler(handlers, 'send_message')
|
|
103
|
-
|
|
104
|
-
await send.invoke({ to: b.agent.id, text: 'hi B, this is A' })
|
|
105
|
-
|
|
106
|
-
const inbox = messageRepo.listInbox(env.db, b.agent.id)
|
|
107
|
-
expect(inbox).toHaveLength(1)
|
|
108
|
-
expect(inbox[0]?.fromAgentId).toBe(a.agent.id)
|
|
109
|
-
const payload = JSON.parse(inbox[0]?.payload ?? '{}')
|
|
110
|
-
expect(payload.text).toBe('hi B, this is A')
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
test('read_inbox returns unread messages and marks them read', async () => {
|
|
114
|
-
const a = makeAgent(env, 'A')
|
|
115
|
-
const b = makeAgent(env, 'B')
|
|
116
|
-
|
|
117
|
-
messageRepo.send(env.db, {
|
|
118
|
-
from: a.agent.id,
|
|
119
|
-
to: b.agent.id,
|
|
120
|
-
payload: JSON.stringify({ text: 'first' }),
|
|
121
|
-
})
|
|
122
|
-
messageRepo.send(env.db, {
|
|
123
|
-
from: a.agent.id,
|
|
124
|
-
to: b.agent.id,
|
|
125
|
-
payload: JSON.stringify({ text: 'second' }),
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
const handlers = messagingTools(dbHost(env.db), b.agent.id)
|
|
129
|
-
const read = findHandler(handlers, 'read_inbox')
|
|
130
|
-
const result = await read.invoke({})
|
|
131
|
-
expect(result).toContain('first')
|
|
132
|
-
expect(result).toContain('second')
|
|
133
|
-
|
|
134
|
-
// After read, inbox shows nothing (unreadOnly default)
|
|
135
|
-
const after = messageRepo.listInbox(env.db, b.agent.id, { unreadOnly: true })
|
|
136
|
-
expect(after).toHaveLength(0)
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
test('send_message + read_inbox round-trip preserves reply_to pointer', async () => {
|
|
140
|
-
const a = makeAgent(env, 'A')
|
|
141
|
-
const b = makeAgent(env, 'B')
|
|
142
|
-
|
|
143
|
-
// A sends B a question
|
|
144
|
-
const aHandlers = messagingTools(dbHost(env.db), a.agent.id)
|
|
145
|
-
const aSend = findHandler(aHandlers, 'send_message')
|
|
146
|
-
await aSend.invoke({ to: b.agent.id, text: 'B, what is 2+2?' })
|
|
147
|
-
|
|
148
|
-
const bInbox = messageRepo.listInbox(env.db, b.agent.id)
|
|
149
|
-
expect(bInbox).toHaveLength(1)
|
|
150
|
-
const originalMsgId = bInbox[0]?.id
|
|
151
|
-
expect(originalMsgId).toBeDefined()
|
|
152
|
-
|
|
153
|
-
// B replies with reply_to pointer
|
|
154
|
-
const bHandlers = messagingTools(dbHost(env.db), b.agent.id)
|
|
155
|
-
const bSend = findHandler(bHandlers, 'send_message')
|
|
156
|
-
await bSend.invoke({ to: a.agent.id, text: '4', reply_to: originalMsgId })
|
|
157
|
-
|
|
158
|
-
// A can see B's reply in its own inbox
|
|
159
|
-
const aInbox = messageRepo.listInbox(env.db, a.agent.id)
|
|
160
|
-
expect(aInbox).toHaveLength(1)
|
|
161
|
-
expect(aInbox[0]?.replyTo).toBe(originalMsgId)
|
|
162
|
-
const payload = JSON.parse(aInbox[0]?.payload ?? '{}')
|
|
163
|
-
expect(payload.text).toBe('4')
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
test('wait_for_reply resolves when a reply arrives', async () => {
|
|
167
|
-
const a = makeAgent(env, 'A')
|
|
168
|
-
const b = makeAgent(env, 'B')
|
|
169
|
-
|
|
170
|
-
const sent = messageRepo.send(env.db, {
|
|
171
|
-
from: a.agent.id,
|
|
172
|
-
to: b.agent.id,
|
|
173
|
-
payload: JSON.stringify({ text: 'ping' }),
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
const reply = setTimeout(() => {
|
|
177
|
-
messageRepo.send(env.db, {
|
|
178
|
-
from: b.agent.id,
|
|
179
|
-
to: a.agent.id,
|
|
180
|
-
payload: JSON.stringify({ text: 'pong' }),
|
|
181
|
-
replyTo: sent.id,
|
|
182
|
-
})
|
|
183
|
-
}, 100)
|
|
184
|
-
|
|
185
|
-
const handlers = messagingTools(dbHost(env.db), a.agent.id)
|
|
186
|
-
const wait = findHandler(handlers, 'wait_for_reply')
|
|
187
|
-
const result = await wait.invoke({ message_id: sent.id, timeout_ms: 2000, poll_ms: 50 })
|
|
188
|
-
clearTimeout(reply)
|
|
189
|
-
|
|
190
|
-
expect(result).toContain('pong')
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
test('wait_for_reply times out cleanly when no reply arrives', async () => {
|
|
194
|
-
const a = makeAgent(env, 'A')
|
|
195
|
-
const b = makeAgent(env, 'B')
|
|
196
|
-
const sent = messageRepo.send(env.db, {
|
|
197
|
-
from: a.agent.id,
|
|
198
|
-
to: b.agent.id,
|
|
199
|
-
payload: JSON.stringify({ text: 'ping' }),
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
const handlers = messagingTools(dbHost(env.db), a.agent.id)
|
|
203
|
-
const wait = findHandler(handlers, 'wait_for_reply')
|
|
204
|
-
const result = await wait.invoke({ message_id: sent.id, timeout_ms: 200, poll_ms: 50 })
|
|
205
|
-
expect(result).toContain('no reply within')
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
test('send_message rejects unknown recipient', async () => {
|
|
209
|
-
const a = makeAgent(env, 'A')
|
|
210
|
-
const handlers = messagingTools(dbHost(env.db), a.agent.id)
|
|
211
|
-
const send = findHandler(handlers, 'send_message')
|
|
212
|
-
await expect(send.invoke({ to: 'nope', text: 'hi' })).rejects.toThrow()
|
|
213
|
-
})
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
|
|
2
|
-
|
|
3
|
-
export interface MockServer {
|
|
4
|
-
url: string
|
|
5
|
-
stop(): Promise<void>
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Starts a node:http server on an ephemeral port that adapts Node's
|
|
10
|
-
* (req, res) to the Fetch-API Request/Response shape, then dispatches to the
|
|
11
|
-
* provided handler. Lets tests keep Request/Response handler code.
|
|
12
|
-
*/
|
|
13
|
-
export function startMockServer(
|
|
14
|
-
handler: (req: Request) => Response | Promise<Response>,
|
|
15
|
-
): Promise<MockServer> {
|
|
16
|
-
const server = createServer((nodeReq: IncomingMessage, nodeRes: ServerResponse) => {
|
|
17
|
-
const chunks: Buffer[] = []
|
|
18
|
-
nodeReq.on('data', (c: Buffer) => chunks.push(c))
|
|
19
|
-
nodeReq.on('end', async () => {
|
|
20
|
-
try {
|
|
21
|
-
const bodyBuf = chunks.length > 0 ? Buffer.concat(chunks) : undefined
|
|
22
|
-
const addr = server.address()
|
|
23
|
-
const hostHeader =
|
|
24
|
-
nodeReq.headers.host ??
|
|
25
|
-
(addr && typeof addr !== 'string' ? `localhost:${addr.port}` : 'localhost')
|
|
26
|
-
const url = `http://${hostHeader}${nodeReq.url ?? '/'}`
|
|
27
|
-
const headerEntries: [string, string][] = []
|
|
28
|
-
for (const [k, v] of Object.entries(nodeReq.headers)) {
|
|
29
|
-
if (typeof v === 'string') headerEntries.push([k, v])
|
|
30
|
-
else if (Array.isArray(v)) headerEntries.push([k, v.join(', ')])
|
|
31
|
-
}
|
|
32
|
-
const method = nodeReq.method ?? 'GET'
|
|
33
|
-
const req = new Request(url, {
|
|
34
|
-
method,
|
|
35
|
-
headers: headerEntries,
|
|
36
|
-
body: method === 'GET' || method === 'HEAD' ? undefined : bodyBuf,
|
|
37
|
-
})
|
|
38
|
-
const res = await handler(req)
|
|
39
|
-
const outHeaders: Record<string, string> = {}
|
|
40
|
-
res.headers.forEach((v, k) => {
|
|
41
|
-
outHeaders[k] = v
|
|
42
|
-
})
|
|
43
|
-
nodeRes.writeHead(res.status, outHeaders)
|
|
44
|
-
if (res.body) {
|
|
45
|
-
nodeRes.end(Buffer.from(await res.arrayBuffer()))
|
|
46
|
-
} else {
|
|
47
|
-
nodeRes.end()
|
|
48
|
-
}
|
|
49
|
-
} catch (err) {
|
|
50
|
-
nodeRes.writeHead(500, { 'content-type': 'text/plain' })
|
|
51
|
-
nodeRes.end(`mock handler error: ${(err as Error).message}`)
|
|
52
|
-
}
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
return new Promise((resolve) => {
|
|
56
|
-
server.listen(0, () => {
|
|
57
|
-
const addr = server.address()
|
|
58
|
-
if (!addr || typeof addr === 'string') throw new Error('unexpected server address')
|
|
59
|
-
resolve({
|
|
60
|
-
url: `http://localhost:${addr.port}`,
|
|
61
|
-
stop: () => new Promise((r) => server.close(() => r())),
|
|
62
|
-
})
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto'
|
|
2
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { afterEach, beforeEach, expect, test } from 'vitest'
|
|
6
|
-
import {
|
|
7
|
-
type BazilionDb,
|
|
8
|
-
openDb,
|
|
9
|
-
openSecrets,
|
|
10
|
-
resolvePaths,
|
|
11
|
-
runMigrations,
|
|
12
|
-
} from '../../src/core/index.ts'
|
|
13
|
-
import {
|
|
14
|
-
clearCredentials,
|
|
15
|
-
getStatus,
|
|
16
|
-
hasCredentials,
|
|
17
|
-
loadAccessToken,
|
|
18
|
-
OPENAI_CODEX_SECRET_KEY,
|
|
19
|
-
saveLoginCredentials,
|
|
20
|
-
} from '../../src/runtime/auth/openai-codex.ts'
|
|
21
|
-
|
|
22
|
-
// These tests exercise the storage + expiry logic directly. Pi-ai's network
|
|
23
|
-
// calls (login flow + token refresh) are out of scope and not hit — the
|
|
24
|
-
// refresh path is driven by a shimmed stored refresh token so we can assert
|
|
25
|
-
// behaviour before expiry without the wire.
|
|
26
|
-
|
|
27
|
-
let home: string
|
|
28
|
-
let db: BazilionDb
|
|
29
|
-
let authToken: string
|
|
30
|
-
|
|
31
|
-
beforeEach(() => {
|
|
32
|
-
home = mkdtempSync(join(tmpdir(), 'bazilion-oauth-test-'))
|
|
33
|
-
const paths = resolvePaths(home)
|
|
34
|
-
mkdirSync(paths.home, { recursive: true })
|
|
35
|
-
// Stamp out an auth.json with a fake bootstrap token — secretsRepo derives
|
|
36
|
-
// its PBKDF2 key from this string, so encryption round-trip works.
|
|
37
|
-
authToken = randomBytes(24).toString('hex')
|
|
38
|
-
writeFileSync(paths.authFile, JSON.stringify({ token: authToken }))
|
|
39
|
-
db = openDb(paths.db)
|
|
40
|
-
runMigrations(db)
|
|
41
|
-
})
|
|
42
|
-
afterEach(() => {
|
|
43
|
-
db.close()
|
|
44
|
-
rmSync(home, { recursive: true, force: true })
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
function fakeAccessJwt(accountId: string): string {
|
|
48
|
-
// A minimal three-part JWT so decodeAccountId can extract the chatgpt_account_id
|
|
49
|
-
// from the JSON claim. Signature isn't validated locally.
|
|
50
|
-
const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64url')
|
|
51
|
-
const payload = Buffer.from(
|
|
52
|
-
JSON.stringify({ 'https://api.openai.com/auth': { chatgpt_account_id: accountId } }),
|
|
53
|
-
).toString('base64url')
|
|
54
|
-
return `${header}.${payload}.sig`
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
test('hasCredentials / getStatus return disconnected on a fresh home', () => {
|
|
58
|
-
expect(hasCredentials(db, authToken)).toBe(false)
|
|
59
|
-
const status = getStatus(db, authToken)
|
|
60
|
-
expect(status).toEqual({ connected: false, expiresAt: null, accountId: null })
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
test('saveLoginCredentials persists and getStatus reflects it', () => {
|
|
64
|
-
saveLoginCredentials(db, authToken, {
|
|
65
|
-
refresh: 'refresh-token',
|
|
66
|
-
access: fakeAccessJwt('acct-abc'),
|
|
67
|
-
expires: Date.now() + 30 * 60_000,
|
|
68
|
-
})
|
|
69
|
-
expect(hasCredentials(db, authToken)).toBe(true)
|
|
70
|
-
const status = getStatus(db, authToken)
|
|
71
|
-
expect(status.connected).toBe(true)
|
|
72
|
-
expect(status.accountId).toBe('acct-abc')
|
|
73
|
-
expect(status.expiresAt).toBeGreaterThan(Date.now())
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
test('loadAccessToken returns the stored access when still fresh', async () => {
|
|
77
|
-
const access = fakeAccessJwt('acct-abc')
|
|
78
|
-
saveLoginCredentials(db, authToken, {
|
|
79
|
-
refresh: 'refresh-token',
|
|
80
|
-
access,
|
|
81
|
-
expires: Date.now() + 30 * 60_000,
|
|
82
|
-
})
|
|
83
|
-
await expect(loadAccessToken(db, authToken)).resolves.toBe(access)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
test('loadAccessToken throws a helpful error when not configured', async () => {
|
|
87
|
-
await expect(loadAccessToken(db, authToken)).rejects.toThrow(/not configured/)
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
test('clearCredentials wipes the stored blob', () => {
|
|
91
|
-
saveLoginCredentials(db, authToken, {
|
|
92
|
-
refresh: 'r',
|
|
93
|
-
access: fakeAccessJwt('x'),
|
|
94
|
-
expires: Date.now() + 60_000,
|
|
95
|
-
})
|
|
96
|
-
expect(hasCredentials(db, authToken)).toBe(true)
|
|
97
|
-
clearCredentials(db, authToken)
|
|
98
|
-
expect(hasCredentials(db, authToken)).toBe(false)
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
test('credential blob is stored under OPENAI_CODEX_SECRET_KEY and decrypts to JSON', () => {
|
|
102
|
-
const access = fakeAccessJwt('acct-1')
|
|
103
|
-
saveLoginCredentials(db, authToken, { refresh: 'r1', access, expires: 1000 })
|
|
104
|
-
const secrets = openSecrets(db, authToken)
|
|
105
|
-
const raw = secrets.get(OPENAI_CODEX_SECRET_KEY)
|
|
106
|
-
expect(raw).toBeTruthy()
|
|
107
|
-
const parsed = JSON.parse(raw as string)
|
|
108
|
-
expect(parsed).toEqual({ refresh: 'r1', access, expires: 1000 })
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
test('malformed blob is treated as disconnected', () => {
|
|
112
|
-
const secrets = openSecrets(db, authToken)
|
|
113
|
-
secrets.set(OPENAI_CODEX_SECRET_KEY, 'not-json{')
|
|
114
|
-
expect(hasCredentials(db, authToken)).toBe(false)
|
|
115
|
-
expect(getStatus(db, authToken).connected).toBe(false)
|
|
116
|
-
})
|
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createAssistantMessageEventStream,
|
|
3
|
-
registerApiProvider,
|
|
4
|
-
unregisterApiProviders,
|
|
5
|
-
} from '@mariozechner/pi-ai'
|
|
6
|
-
import { afterEach, beforeEach, expect, test } from 'vitest'
|
|
7
|
-
import { piProvider } from '../../src/runtime/providers/pi-adapter.ts'
|
|
8
|
-
import {
|
|
9
|
-
createProviderRegistry,
|
|
10
|
-
loadProviderConfigFromEnv,
|
|
11
|
-
} from '../../src/runtime/providers/registry.ts'
|
|
12
|
-
|
|
13
|
-
// These tests cover the pi-ai adapter's semantics — message/tool/response
|
|
14
|
-
// conversion and delta plumbing — by registering a fake pi `Api` that returns
|
|
15
|
-
// canned event streams. Wire-format correctness (SSE framing, provider-specific
|
|
16
|
-
// request shapes) is pi-ai's responsibility and covered by its own test suite.
|
|
17
|
-
|
|
18
|
-
const FAKE_API = 'bazilion-test-fake' as const
|
|
19
|
-
const TEST_SRC = 'bazilion-tests'
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
unregisterApiProviders(TEST_SRC)
|
|
23
|
-
})
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
unregisterApiProviders(TEST_SRC)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
function registerFakeApi(build: () => ReturnType<typeof createAssistantMessageEventStream>): void {
|
|
29
|
-
registerApiProvider(
|
|
30
|
-
{
|
|
31
|
-
api: FAKE_API,
|
|
32
|
-
stream: () => build(),
|
|
33
|
-
streamSimple: () => build(),
|
|
34
|
-
},
|
|
35
|
-
TEST_SRC,
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function cannedAssistantText(text: string): ReturnType<typeof createAssistantMessageEventStream> {
|
|
40
|
-
const stream = createAssistantMessageEventStream()
|
|
41
|
-
const assistant = {
|
|
42
|
-
role: 'assistant' as const,
|
|
43
|
-
content: [{ type: 'text' as const, text }],
|
|
44
|
-
api: FAKE_API,
|
|
45
|
-
provider: 'fake',
|
|
46
|
-
model: 'm',
|
|
47
|
-
usage: {
|
|
48
|
-
input: 10,
|
|
49
|
-
output: 5,
|
|
50
|
-
cacheRead: 0,
|
|
51
|
-
cacheWrite: 0,
|
|
52
|
-
totalTokens: 15,
|
|
53
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
54
|
-
},
|
|
55
|
-
stopReason: 'stop' as const,
|
|
56
|
-
timestamp: Date.now(),
|
|
57
|
-
}
|
|
58
|
-
// Push chunked deltas to exercise the delta path, then a terminal done event.
|
|
59
|
-
queueMicrotask(() => {
|
|
60
|
-
stream.push({ type: 'start', partial: assistant })
|
|
61
|
-
stream.push({ type: 'text_start', contentIndex: 0, partial: assistant })
|
|
62
|
-
stream.push({
|
|
63
|
-
type: 'text_delta',
|
|
64
|
-
contentIndex: 0,
|
|
65
|
-
delta: text.slice(0, 3),
|
|
66
|
-
partial: assistant,
|
|
67
|
-
})
|
|
68
|
-
stream.push({ type: 'text_delta', contentIndex: 0, delta: text.slice(3), partial: assistant })
|
|
69
|
-
stream.push({ type: 'text_end', contentIndex: 0, content: text, partial: assistant })
|
|
70
|
-
stream.push({ type: 'done', reason: 'stop', message: assistant })
|
|
71
|
-
stream.end(assistant)
|
|
72
|
-
})
|
|
73
|
-
return stream
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function cannedToolCall(
|
|
77
|
-
id: string,
|
|
78
|
-
name: string,
|
|
79
|
-
args: Record<string, unknown>,
|
|
80
|
-
): ReturnType<typeof createAssistantMessageEventStream> {
|
|
81
|
-
const stream = createAssistantMessageEventStream()
|
|
82
|
-
const assistant = {
|
|
83
|
-
role: 'assistant' as const,
|
|
84
|
-
content: [{ type: 'toolCall' as const, id, name, arguments: args }],
|
|
85
|
-
api: FAKE_API,
|
|
86
|
-
provider: 'fake',
|
|
87
|
-
model: 'm',
|
|
88
|
-
usage: {
|
|
89
|
-
input: 0,
|
|
90
|
-
output: 0,
|
|
91
|
-
cacheRead: 0,
|
|
92
|
-
cacheWrite: 0,
|
|
93
|
-
totalTokens: 0,
|
|
94
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
95
|
-
},
|
|
96
|
-
stopReason: 'toolUse' as const,
|
|
97
|
-
timestamp: Date.now(),
|
|
98
|
-
}
|
|
99
|
-
queueMicrotask(() => {
|
|
100
|
-
stream.push({ type: 'start', partial: assistant })
|
|
101
|
-
stream.push({ type: 'done', reason: 'toolUse', message: assistant })
|
|
102
|
-
stream.end(assistant)
|
|
103
|
-
})
|
|
104
|
-
return stream
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function cannedError(message: string): ReturnType<typeof createAssistantMessageEventStream> {
|
|
108
|
-
const stream = createAssistantMessageEventStream()
|
|
109
|
-
const assistant = {
|
|
110
|
-
role: 'assistant' as const,
|
|
111
|
-
content: [],
|
|
112
|
-
api: FAKE_API,
|
|
113
|
-
provider: 'fake',
|
|
114
|
-
model: 'm',
|
|
115
|
-
usage: {
|
|
116
|
-
input: 0,
|
|
117
|
-
output: 0,
|
|
118
|
-
cacheRead: 0,
|
|
119
|
-
cacheWrite: 0,
|
|
120
|
-
totalTokens: 0,
|
|
121
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
122
|
-
},
|
|
123
|
-
stopReason: 'error' as const,
|
|
124
|
-
errorMessage: message,
|
|
125
|
-
timestamp: Date.now(),
|
|
126
|
-
}
|
|
127
|
-
queueMicrotask(() => {
|
|
128
|
-
stream.push({ type: 'error', reason: 'error', error: assistant })
|
|
129
|
-
stream.end(assistant)
|
|
130
|
-
})
|
|
131
|
-
return stream
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
test('piProvider surfaces text, usage, and stopReason from a completed stream', async () => {
|
|
135
|
-
registerFakeApi(() => cannedAssistantText('hello there'))
|
|
136
|
-
const provider = piProvider({
|
|
137
|
-
providerName: 'bazilion-fake',
|
|
138
|
-
fallbackApi: FAKE_API,
|
|
139
|
-
apiKey: 'k',
|
|
140
|
-
})
|
|
141
|
-
const res = await provider.chat({
|
|
142
|
-
model: 'm',
|
|
143
|
-
messages: [{ role: 'user', content: 'hi' }],
|
|
144
|
-
})
|
|
145
|
-
expect(res.content).toBe('hello there')
|
|
146
|
-
expect(res.toolCalls).toEqual([])
|
|
147
|
-
expect(res.stopReason).toBe('stop')
|
|
148
|
-
expect(res.usage).toEqual({ promptTokens: 10, completionTokens: 5 })
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
test('piProvider calls onDelta for each streamed chunk', async () => {
|
|
152
|
-
registerFakeApi(() => cannedAssistantText('abcdef'))
|
|
153
|
-
const provider = piProvider({
|
|
154
|
-
providerName: 'bazilion-fake',
|
|
155
|
-
fallbackApi: FAKE_API,
|
|
156
|
-
apiKey: 'k',
|
|
157
|
-
})
|
|
158
|
-
const deltas: string[] = []
|
|
159
|
-
const res = await provider.chat({
|
|
160
|
-
model: 'm',
|
|
161
|
-
messages: [{ role: 'user', content: 'hi' }],
|
|
162
|
-
onDelta: (d) => deltas.push(d),
|
|
163
|
-
})
|
|
164
|
-
expect(deltas.join('')).toBe('abcdef')
|
|
165
|
-
expect(res.content).toBe('abcdef')
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
test('piProvider extracts toolCalls with JSON-stringified arguments', async () => {
|
|
169
|
-
registerFakeApi(() => cannedToolCall('call_1', 'memory_write', { key: 'x.md', content: 'y' }))
|
|
170
|
-
const provider = piProvider({
|
|
171
|
-
providerName: 'bazilion-fake',
|
|
172
|
-
fallbackApi: FAKE_API,
|
|
173
|
-
apiKey: 'k',
|
|
174
|
-
})
|
|
175
|
-
const res = await provider.chat({
|
|
176
|
-
model: 'm',
|
|
177
|
-
messages: [{ role: 'user', content: 'remember' }],
|
|
178
|
-
tools: [
|
|
179
|
-
{
|
|
180
|
-
name: 'memory_write',
|
|
181
|
-
description: 'write memory',
|
|
182
|
-
parameters: { type: 'object', properties: { key: { type: 'string' } } },
|
|
183
|
-
},
|
|
184
|
-
],
|
|
185
|
-
})
|
|
186
|
-
expect(res.stopReason).toBe('tool_use')
|
|
187
|
-
expect(res.toolCalls).toHaveLength(1)
|
|
188
|
-
expect(res.toolCalls[0]?.name).toBe('memory_write')
|
|
189
|
-
expect(JSON.parse(res.toolCalls[0]?.arguments ?? '{}')).toEqual({
|
|
190
|
-
key: 'x.md',
|
|
191
|
-
content: 'y',
|
|
192
|
-
})
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
test('piProvider surfaces errorMessage as a thrown Error', async () => {
|
|
196
|
-
registerFakeApi(() => cannedError('upstream blew up'))
|
|
197
|
-
const provider = piProvider({
|
|
198
|
-
providerName: 'bazilion-fake',
|
|
199
|
-
fallbackApi: FAKE_API,
|
|
200
|
-
apiKey: 'k',
|
|
201
|
-
})
|
|
202
|
-
await expect(
|
|
203
|
-
provider.chat({ model: 'm', messages: [{ role: 'user', content: 'hi' }] }),
|
|
204
|
-
).rejects.toThrow(/upstream blew up/)
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
// --- registry ---
|
|
208
|
-
|
|
209
|
-
test('registry resolves all 16 provider:model strings', () => {
|
|
210
|
-
const reg = createProviderRegistry({
|
|
211
|
-
anthropic: { apiKey: 'k' },
|
|
212
|
-
openai: { apiKey: 'k' },
|
|
213
|
-
google: { apiKey: 'k' },
|
|
214
|
-
azureOpenai: { apiKey: 'k' },
|
|
215
|
-
bedrock: {},
|
|
216
|
-
googleVertex: {},
|
|
217
|
-
mistral: { apiKey: 'k' },
|
|
218
|
-
groq: { apiKey: 'k' },
|
|
219
|
-
cerebras: { apiKey: 'k' },
|
|
220
|
-
xai: { apiKey: 'k' },
|
|
221
|
-
zai: { apiKey: 'k' },
|
|
222
|
-
huggingface: { apiKey: 'k' },
|
|
223
|
-
openrouter: { apiKey: 'k' },
|
|
224
|
-
vercelAiGateway: { apiKey: 'k' },
|
|
225
|
-
})
|
|
226
|
-
for (const p of [
|
|
227
|
-
'anthropic',
|
|
228
|
-
'openai',
|
|
229
|
-
'google',
|
|
230
|
-
'google-vertex',
|
|
231
|
-
'azure-openai',
|
|
232
|
-
'bedrock',
|
|
233
|
-
'mistral',
|
|
234
|
-
'groq',
|
|
235
|
-
'cerebras',
|
|
236
|
-
'xai',
|
|
237
|
-
'zai',
|
|
238
|
-
'huggingface',
|
|
239
|
-
'openrouter',
|
|
240
|
-
'vercel-ai-gateway',
|
|
241
|
-
'lmstudio',
|
|
242
|
-
'ollama',
|
|
243
|
-
]) {
|
|
244
|
-
const r = reg.resolve(`${p}:some-model`)
|
|
245
|
-
expect(r.provider.name).toBe(p)
|
|
246
|
-
expect(r.model).toBe('some-model')
|
|
247
|
-
}
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
test('registry exposes lmstudio and ollama without any env config', () => {
|
|
251
|
-
const reg = createProviderRegistry({})
|
|
252
|
-
const lm = reg.resolve('lmstudio:llama-3-8b')
|
|
253
|
-
expect(lm.provider.name).toBe('lmstudio')
|
|
254
|
-
const ol = reg.resolve('ollama:llama2')
|
|
255
|
-
expect(ol.provider.name).toBe('ollama')
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
test('registry rejects unknown names, malformed strings, and unconfigured providers', () => {
|
|
259
|
-
const reg = createProviderRegistry({})
|
|
260
|
-
expect(() => reg.resolve('martian:foo')).toThrow(/unknown provider/)
|
|
261
|
-
expect(() => reg.resolve('not-a-model')).toThrow(/expected "provider:model"/)
|
|
262
|
-
expect(() => reg.resolve('anthropic:m')).toThrow(/not configured/)
|
|
263
|
-
expect(() => reg.resolve('groq:m')).toThrow(/GROQ_API_KEY/)
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
test('loadProviderConfigFromEnv picks up every standard env var', () => {
|
|
267
|
-
const config = loadProviderConfigFromEnv({
|
|
268
|
-
ANTHROPIC_API_KEY: 'a',
|
|
269
|
-
OPENAI_API_KEY: 'o',
|
|
270
|
-
GEMINI_API_KEY: 'g',
|
|
271
|
-
GROQ_API_KEY: 'gr',
|
|
272
|
-
CEREBRAS_API_KEY: 'ce',
|
|
273
|
-
XAI_API_KEY: 'x',
|
|
274
|
-
MISTRAL_API_KEY: 'mi',
|
|
275
|
-
HF_TOKEN: 'hf',
|
|
276
|
-
OPENROUTER_API_KEY: 'or',
|
|
277
|
-
AI_GATEWAY_API_KEY: 'vg',
|
|
278
|
-
} as NodeJS.ProcessEnv)
|
|
279
|
-
expect(config.anthropic?.apiKey).toBe('a')
|
|
280
|
-
expect(config.openai?.apiKey).toBe('o')
|
|
281
|
-
expect(config.google?.apiKey).toBe('g')
|
|
282
|
-
expect(config.groq?.apiKey).toBe('gr')
|
|
283
|
-
expect(config.cerebras?.apiKey).toBe('ce')
|
|
284
|
-
expect(config.xai?.apiKey).toBe('x')
|
|
285
|
-
expect(config.mistral?.apiKey).toBe('mi')
|
|
286
|
-
expect(config.huggingface?.apiKey).toBe('hf')
|
|
287
|
-
expect(config.openrouter?.apiKey).toBe('or')
|
|
288
|
-
expect(config.vercelAiGateway?.apiKey).toBe('vg')
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
test('loadProviderConfigFromEnv recognizes Bedrock auth via AWS_PROFILE', () => {
|
|
292
|
-
const cfg = loadProviderConfigFromEnv({ AWS_PROFILE: 'default' } as NodeJS.ProcessEnv)
|
|
293
|
-
expect(cfg.bedrock).toBeDefined()
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
test('loadProviderConfigFromEnv requires both project and location for Vertex', () => {
|
|
297
|
-
expect(
|
|
298
|
-
loadProviderConfigFromEnv({ GOOGLE_CLOUD_PROJECT: 'p' } as NodeJS.ProcessEnv).googleVertex,
|
|
299
|
-
).toBeUndefined()
|
|
300
|
-
expect(
|
|
301
|
-
loadProviderConfigFromEnv({
|
|
302
|
-
GOOGLE_CLOUD_PROJECT: 'p',
|
|
303
|
-
GOOGLE_CLOUD_LOCATION: 'us-central1',
|
|
304
|
-
} as NodeJS.ProcessEnv).googleVertex,
|
|
305
|
-
).toBeDefined()
|
|
306
|
-
})
|