bazilion 0.0.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/.understand-anything/.understandignore +25 -0
- package/.understand-anything/fingerprints.json +14267 -0
- package/.understand-anything/knowledge-graph.json +18128 -0
- package/.understand-anything/meta.json +6 -0
- package/CLAUDE.md +164 -0
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/apps/cli/package.json +21 -0
- package/apps/cli/src/auth-file.ts +18 -0
- package/apps/cli/src/client.ts +37 -0
- package/apps/cli/src/columnize.ts +32 -0
- package/apps/cli/src/commands/agent.ts +574 -0
- package/apps/cli/src/commands/auth.ts +110 -0
- package/apps/cli/src/commands/backup.ts +135 -0
- package/apps/cli/src/commands/completion.ts +155 -0
- package/apps/cli/src/commands/config.ts +95 -0
- package/apps/cli/src/commands/doctor.ts +131 -0
- package/apps/cli/src/commands/group.ts +132 -0
- package/apps/cli/src/commands/inbox.ts +82 -0
- package/apps/cli/src/commands/login.ts +73 -0
- package/apps/cli/src/commands/memory.ts +106 -0
- package/apps/cli/src/commands/profile.ts +259 -0
- package/apps/cli/src/commands/provider.ts +170 -0
- package/apps/cli/src/commands/send.ts +24 -0
- package/apps/cli/src/commands/serve.ts +89 -0
- package/apps/cli/src/commands/skill.ts +120 -0
- package/apps/cli/src/commands/token.ts +148 -0
- package/apps/cli/src/commands/trigger.ts +129 -0
- package/apps/cli/src/commands/uninstall.ts +156 -0
- package/apps/cli/src/index.ts +196 -0
- package/apps/cli/src/paths.ts +12 -0
- package/apps/cli/test/agent.test.ts +213 -0
- package/apps/cli/test/backup.test.ts +95 -0
- package/apps/cli/test/chat.test.ts +539 -0
- package/apps/cli/test/columnize.test.ts +29 -0
- package/apps/cli/test/completion.test.ts +45 -0
- package/apps/cli/test/config-page.test.ts +151 -0
- package/apps/cli/test/group.test.ts +74 -0
- package/apps/cli/test/helpers.ts +70 -0
- package/apps/cli/test/inbox-autodeliver.test.ts +143 -0
- package/apps/cli/test/inbox.test.ts +147 -0
- package/apps/cli/test/memory.test.ts +69 -0
- package/apps/cli/test/profile.test.ts +110 -0
- package/apps/cli/test/send.test.ts +30 -0
- package/apps/cli/test/server-fixture.ts +212 -0
- package/apps/cli/test/session-head.test.ts +45 -0
- package/apps/cli/test/skill.test.ts +128 -0
- package/apps/cli/test/token.test.ts +109 -0
- package/apps/cli/test/trigger.test.ts +245 -0
- package/apps/cli/tsconfig.json +4 -0
- package/apps/daemon/package.json +31 -0
- package/apps/daemon/src/app.ts +41 -0
- package/apps/daemon/src/core/agent/archive.ts +8 -0
- package/apps/daemon/src/core/agent/delete.ts +30 -0
- package/apps/daemon/src/core/agent/resolve.ts +31 -0
- package/apps/daemon/src/core/agent/spawn.ts +95 -0
- package/apps/daemon/src/core/agent/unarchive.ts +11 -0
- package/apps/daemon/src/core/availableModels.ts +58 -0
- package/apps/daemon/src/core/db/client.ts +113 -0
- package/apps/daemon/src/core/db/migrate.ts +41 -0
- package/apps/daemon/src/core/db/migrations/0001_init.sql +179 -0
- package/apps/daemon/src/core/group/delete.ts +21 -0
- package/apps/daemon/src/core/group/register.ts +68 -0
- package/apps/daemon/src/core/index.ts +71 -0
- package/apps/daemon/src/core/paths.ts +56 -0
- package/apps/daemon/src/core/profile/create.ts +78 -0
- package/apps/daemon/src/core/profile/delete.ts +26 -0
- package/apps/daemon/src/core/profile/identity.ts +70 -0
- package/apps/daemon/src/core/profile/load.ts +45 -0
- package/apps/daemon/src/core/profile/seed.ts +74 -0
- package/apps/daemon/src/core/profile/templates.ts +60 -0
- package/apps/daemon/src/core/profile/update.ts +57 -0
- package/apps/daemon/src/core/profile/validate.ts +9 -0
- package/apps/daemon/src/core/repos/agents.ts +202 -0
- package/apps/daemon/src/core/repos/config.ts +78 -0
- package/apps/daemon/src/core/repos/groups.ts +55 -0
- package/apps/daemon/src/core/repos/messages.ts +127 -0
- package/apps/daemon/src/core/repos/profiles.ts +83 -0
- package/apps/daemon/src/core/repos/providerModels.ts +58 -0
- package/apps/daemon/src/core/repos/providerState.ts +37 -0
- package/apps/daemon/src/core/repos/secrets.ts +145 -0
- package/apps/daemon/src/core/repos/skillMeta.ts +49 -0
- package/apps/daemon/src/core/repos/triggers.ts +101 -0
- package/apps/daemon/src/core/repos/webTokens.ts +87 -0
- package/apps/daemon/src/core/secrets.ts +65 -0
- package/apps/daemon/src/core/services.ts +264 -0
- package/apps/daemon/src/core/skills/discover.ts +28 -0
- package/apps/daemon/src/core/skills/import.ts +136 -0
- package/apps/daemon/src/core/skills/parse.ts +52 -0
- package/apps/daemon/src/core/skills/resolve.ts +50 -0
- package/apps/daemon/src/index.ts +45 -0
- package/apps/daemon/src/lib/agent-cancel.ts +48 -0
- package/apps/daemon/src/lib/agent-id.ts +13 -0
- package/apps/daemon/src/lib/agent-turn.ts +56 -0
- package/apps/daemon/src/lib/api-key.ts +56 -0
- package/apps/daemon/src/lib/auth.ts +41 -0
- package/apps/daemon/src/lib/cron.ts +93 -0
- package/apps/daemon/src/lib/ctx.ts +80 -0
- package/apps/daemon/src/lib/messaging-host.ts +34 -0
- package/apps/daemon/src/lib/middleware-auth.ts +52 -0
- package/apps/daemon/src/lib/scheduler.ts +294 -0
- package/apps/daemon/src/routes/agents.ts +772 -0
- package/apps/daemon/src/routes/auth-login.ts +193 -0
- package/apps/daemon/src/routes/config.ts +267 -0
- package/apps/daemon/src/routes/groups.ts +133 -0
- package/apps/daemon/src/routes/messages.ts +29 -0
- package/apps/daemon/src/routes/misc.ts +239 -0
- package/apps/daemon/src/routes/profiles.ts +197 -0
- package/apps/daemon/src/routes/skills.ts +123 -0
- package/apps/daemon/src/routes/triggers.ts +29 -0
- package/apps/daemon/src/runtime/auth/openai-codex.ts +121 -0
- package/apps/daemon/src/runtime/auto-reply/heartbeat.ts +31 -0
- package/apps/daemon/src/runtime/index.ts +77 -0
- package/apps/daemon/src/runtime/memory/files.ts +103 -0
- package/apps/daemon/src/runtime/memory/qmd.ts +152 -0
- package/apps/daemon/src/runtime/memory/types.ts +16 -0
- package/apps/daemon/src/runtime/pi/events.ts +173 -0
- package/apps/daemon/src/runtime/pi/session.ts +536 -0
- package/apps/daemon/src/runtime/pi/tools.ts +85 -0
- package/apps/daemon/src/runtime/providers/catalog.ts +145 -0
- package/apps/daemon/src/runtime/providers/pi-adapter.ts +272 -0
- package/apps/daemon/src/runtime/providers/registry.ts +374 -0
- package/apps/daemon/src/runtime/providers/retry.ts +176 -0
- package/apps/daemon/src/runtime/providers/types.ts +33 -0
- package/apps/daemon/src/runtime/session/prompt.ts +83 -0
- package/apps/daemon/src/runtime/tools/bootstrap.ts +22 -0
- package/apps/daemon/src/runtime/tools/home.ts +114 -0
- package/apps/daemon/src/runtime/tools/memory.ts +81 -0
- package/apps/daemon/src/runtime/tools/messaging.ts +127 -0
- package/apps/daemon/src/runtime/tools/registry.ts +29 -0
- package/apps/daemon/src/runtime/tools/types.ts +13 -0
- package/apps/daemon/src/runtime/tools/web-extract.ts +110 -0
- package/apps/daemon/src/runtime/tools/web-ssrf.ts +245 -0
- package/apps/daemon/src/runtime/tools/web.ts +273 -0
- package/apps/daemon/src/runtime/worker/entry.ts +221 -0
- package/apps/daemon/src/runtime/worker/ipc-protocol.ts +75 -0
- package/apps/daemon/src/runtime/worker/spawn.ts +249 -0
- package/apps/daemon/test/core/agents.test.ts +319 -0
- package/apps/daemon/test/core/available-models.test.ts +63 -0
- package/apps/daemon/test/core/config.test.ts +99 -0
- package/apps/daemon/test/core/groups.test.ts +63 -0
- package/apps/daemon/test/core/helpers.ts +50 -0
- package/apps/daemon/test/core/identity.test.ts +85 -0
- package/apps/daemon/test/core/migrations.test.ts +83 -0
- package/apps/daemon/test/core/profiles.test.ts +182 -0
- package/apps/daemon/test/core/provider-models.test.ts +57 -0
- package/apps/daemon/test/core/provider-state.test.ts +36 -0
- package/apps/daemon/test/core/skill-meta.test.ts +45 -0
- package/apps/daemon/test/core/skills.test.ts +271 -0
- package/apps/daemon/test/core/triggers.test.ts +182 -0
- package/apps/daemon/test/core/web-tokens.test.ts +79 -0
- package/apps/daemon/test/cron.test.ts +90 -0
- package/apps/daemon/test/runtime/heartbeat.test.ts +34 -0
- package/apps/daemon/test/runtime/memory-qmd.test.ts +97 -0
- package/apps/daemon/test/runtime/memory.test.ts +78 -0
- package/apps/daemon/test/runtime/messaging.test.ts +213 -0
- package/apps/daemon/test/runtime/mock-server.ts +65 -0
- package/apps/daemon/test/runtime/openai-codex-auth.test.ts +116 -0
- package/apps/daemon/test/runtime/providers.test.ts +306 -0
- package/apps/daemon/test/runtime/retry.test.ts +191 -0
- package/apps/daemon/test/runtime/session-head.test.ts +90 -0
- package/apps/daemon/test/runtime/tools-home.test.ts +102 -0
- package/apps/daemon/test/runtime/tools-web.test.ts +206 -0
- package/apps/daemon/tsconfig.json +4 -0
- package/apps/mobile/README.md +60 -0
- package/apps/mobile/app/_layout.tsx +58 -0
- package/apps/mobile/app/agents/[id]/chat.tsx +486 -0
- package/apps/mobile/app/agents/[id]/index.tsx +166 -0
- package/apps/mobile/app/agents/index.tsx +212 -0
- package/apps/mobile/app/index.tsx +21 -0
- package/apps/mobile/app/pair.tsx +226 -0
- package/apps/mobile/app/settings.tsx +419 -0
- package/apps/mobile/app.json +49 -0
- 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 +6 -0
- package/apps/mobile/metro.config.js +28 -0
- package/apps/mobile/package.json +44 -0
- package/apps/mobile/src/auth.ts +66 -0
- package/apps/mobile/src/pair-url.ts +48 -0
- package/apps/mobile/src/theme-context.tsx +88 -0
- package/apps/mobile/src/theme.ts +135 -0
- package/apps/mobile/test/pair-url.test.ts +46 -0
- package/apps/mobile/tsconfig.json +23 -0
- package/apps/web/components.json +25 -0
- package/apps/web/package.json +39 -0
- package/apps/web/public/baziu.svg +8 -0
- package/apps/web/src/components/AgentTabs.tsx +45 -0
- package/apps/web/src/components/BaziuLogo.tsx +21 -0
- package/apps/web/src/components/ChatPane.tsx +1033 -0
- package/apps/web/src/components/ConfigTabs.tsx +29 -0
- package/apps/web/src/components/CopyButton.tsx +68 -0
- package/apps/web/src/components/CreateGroupDialog.tsx +127 -0
- package/apps/web/src/components/FieldRow.tsx +94 -0
- package/apps/web/src/components/Footer.tsx +10 -0
- package/apps/web/src/components/PawIcon.tsx +15 -0
- package/apps/web/src/components/Sidebar.tsx +287 -0
- package/apps/web/src/components/SpawnDialog.tsx +129 -0
- package/apps/web/src/components/ThemeToggle.tsx +75 -0
- package/apps/web/src/components/TopNav.tsx +34 -0
- package/apps/web/src/components/ui/button.tsx +67 -0
- package/apps/web/src/components/ui/card.tsx +103 -0
- package/apps/web/src/components/ui/checkbox.tsx +31 -0
- package/apps/web/src/components/ui/dialog.tsx +168 -0
- package/apps/web/src/components/ui/input.tsx +19 -0
- package/apps/web/src/components/ui/label.tsx +22 -0
- package/apps/web/src/components/ui/radio-group.tsx +44 -0
- package/apps/web/src/components/ui/select.tsx +192 -0
- package/apps/web/src/components/ui/separator.tsx +26 -0
- package/apps/web/src/components/ui/table.tsx +116 -0
- package/apps/web/src/components/ui/tabs.tsx +88 -0
- package/apps/web/src/components/ui/textarea.tsx +18 -0
- package/apps/web/src/lib/auth.ts +50 -0
- package/apps/web/src/lib/daemon-client.ts +34 -0
- package/apps/web/src/lib/md.ts +45 -0
- package/apps/web/src/lib/utils.ts +6 -0
- package/apps/web/src/lib/wire-constants.ts +27 -0
- package/apps/web/src/routeTree.gen.ts +408 -0
- package/apps/web/src/router.tsx +20 -0
- package/apps/web/src/routes/__root.tsx +123 -0
- package/apps/web/src/routes/agents/$id/inbox.tsx +207 -0
- package/apps/web/src/routes/agents/$id/index.tsx +527 -0
- package/apps/web/src/routes/agents/$id/triggers.tsx +239 -0
- package/apps/web/src/routes/agents/index.tsx +265 -0
- package/apps/web/src/routes/api/$.ts +88 -0
- package/apps/web/src/routes/config/index.tsx +315 -0
- package/apps/web/src/routes/config/services.tsx +49 -0
- package/apps/web/src/routes/config/tokens.tsx +192 -0
- package/apps/web/src/routes/groups/$id/index.tsx +153 -0
- package/apps/web/src/routes/groups/$id/memory.tsx +321 -0
- package/apps/web/src/routes/groups/index.tsx +191 -0
- package/apps/web/src/routes/index.tsx +133 -0
- package/apps/web/src/routes/login.tsx +63 -0
- package/apps/web/src/routes/profiles/$id.tsx +549 -0
- package/apps/web/src/routes/profiles/index.tsx +458 -0
- package/apps/web/src/routes/skills/index.tsx +297 -0
- package/apps/web/src/routes/welcome.tsx +61 -0
- package/apps/web/src/styles.css +449 -0
- package/apps/web/tsconfig.json +25 -0
- package/apps/web/vite.config.ts +25 -0
- package/biome.json +23 -0
- package/docs/agent-engine.md +219 -0
- package/docs/architecture.md +627 -0
- package/docs/backlog/README.md +42 -0
- package/docs/backlog/draft/BAZ-001-a2a-federation-spike.md +125 -0
- package/docs/openclaw-reference.md +210 -0
- package/package.json +38 -0
- package/packages/api-types/package.json +11 -0
- package/packages/api-types/src/entities.ts +146 -0
- package/packages/api-types/src/events.ts +55 -0
- package/packages/api-types/src/index.ts +488 -0
- package/packages/api-types/src/memory.ts +15 -0
- package/packages/client/package.json +13 -0
- package/packages/client/src/index.ts +117 -0
- package/pnpm-workspace.yaml +3 -0
- package/tsconfig.base.json +23 -0
- package/tsconfig.json +11 -0
- package/vitest.config.ts +22 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest'
|
|
5
|
+
import { startTestServer, type TestServer } from './server-fixture.ts'
|
|
6
|
+
|
|
7
|
+
let server: TestServer
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
server = await startTestServer()
|
|
10
|
+
})
|
|
11
|
+
afterAll(() => server.stop())
|
|
12
|
+
beforeEach(() => server.reset())
|
|
13
|
+
|
|
14
|
+
test('profile create + list + show', async () => {
|
|
15
|
+
let r = await server.cli([
|
|
16
|
+
'profile',
|
|
17
|
+
'create',
|
|
18
|
+
'researcher',
|
|
19
|
+
'--model',
|
|
20
|
+
'anthropic:claude-opus-4-6',
|
|
21
|
+
'--skills-mode',
|
|
22
|
+
'selected',
|
|
23
|
+
'--skills',
|
|
24
|
+
's1,s2',
|
|
25
|
+
])
|
|
26
|
+
expect(r.exitCode).toBe(0)
|
|
27
|
+
expect(r.stdout).toContain('created profile researcher')
|
|
28
|
+
|
|
29
|
+
r = await server.cli(['profile', 'list'])
|
|
30
|
+
expect(r.stdout).toContain('researcher')
|
|
31
|
+
expect(r.stdout).toContain('claude-opus-4-6')
|
|
32
|
+
|
|
33
|
+
r = await server.cli(['profile', 'show', 'researcher'])
|
|
34
|
+
expect(r.stdout).toContain('researcher')
|
|
35
|
+
expect(r.stdout).toContain('default skills: s1, s2')
|
|
36
|
+
expect(r.stdout).toContain('skills mode: selected')
|
|
37
|
+
expect(r.stdout).toContain('bootstrap: yes')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('profile create with invalid slug fails', async () => {
|
|
41
|
+
const r = await server.cli(['profile', 'create', 'Bad Id', '--model', 'm'])
|
|
42
|
+
expect(r.exitCode).not.toBe(0)
|
|
43
|
+
expect(r.stderr + r.stdout).toContain('invalid slug')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('profile update switches model and skills-mode', async () => {
|
|
47
|
+
await server.cli(['profile', 'create', 'to-update', '--model', 'anthropic:claude-opus-4-6'])
|
|
48
|
+
const r = await server.cli([
|
|
49
|
+
'profile',
|
|
50
|
+
'update',
|
|
51
|
+
'to-update',
|
|
52
|
+
'--model',
|
|
53
|
+
'anthropic:claude-opus-4-7',
|
|
54
|
+
'--skills-mode',
|
|
55
|
+
'all',
|
|
56
|
+
])
|
|
57
|
+
expect(r.exitCode).toBe(0)
|
|
58
|
+
const show = await server.cli(['profile', 'show', 'to-update'])
|
|
59
|
+
expect(show.stdout).toContain('claude-opus-4-7')
|
|
60
|
+
expect(show.stdout).toContain('skills mode: all')
|
|
61
|
+
|
|
62
|
+
const json = JSON.parse(
|
|
63
|
+
readFileSync(join(server.home, 'profiles', 'to-update', 'profile.json'), 'utf8'),
|
|
64
|
+
)
|
|
65
|
+
expect(json.defaultModel).toBe('anthropic:claude-opus-4-7')
|
|
66
|
+
expect(json.skillsMode).toBe('all')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('PUT /api/profiles/:id/files/:file edits SOUL.md post-creation', async () => {
|
|
70
|
+
await server.cli(['profile', 'create', 'editable', '--model', 'anthropic:claude-opus-4-6'])
|
|
71
|
+
const res = await fetch(`${server.url}/api/profiles/editable/files/SOUL.md`, {
|
|
72
|
+
method: 'PUT',
|
|
73
|
+
headers: {
|
|
74
|
+
'content-type': 'application/json',
|
|
75
|
+
cookie: `bz_token=${server.token}`,
|
|
76
|
+
origin: server.url,
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({ content: '# edited post-creation\nnew rules' }),
|
|
79
|
+
})
|
|
80
|
+
expect(res.status).toBe(204)
|
|
81
|
+
const soul = readFileSync(join(server.home, 'profiles', 'editable', 'SOUL.md'), 'utf8')
|
|
82
|
+
expect(soul).toBe('# edited post-creation\nnew rules')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('profile create --soul-file and --skip-bootstrap write custom templates', async () => {
|
|
86
|
+
const scratch = mkdtempSync(join(tmpdir(), 'bazilion-tmpl-'))
|
|
87
|
+
const soulPath = join(scratch, 'soul.md')
|
|
88
|
+
writeFileSync(soulPath, '# custom soul\nmake it yours')
|
|
89
|
+
try {
|
|
90
|
+
const r = await server.cli([
|
|
91
|
+
'profile',
|
|
92
|
+
'create',
|
|
93
|
+
'custom-tmpl',
|
|
94
|
+
'--model',
|
|
95
|
+
'anthropic:claude-opus-4-6',
|
|
96
|
+
'--soul-file',
|
|
97
|
+
soulPath,
|
|
98
|
+
'--skip-bootstrap',
|
|
99
|
+
])
|
|
100
|
+
expect(r.exitCode).toBe(0)
|
|
101
|
+
|
|
102
|
+
const soul = readFileSync(join(server.home, 'profiles', 'custom-tmpl', 'SOUL.md'), 'utf8')
|
|
103
|
+
expect(soul).toBe('# custom soul\nmake it yours')
|
|
104
|
+
|
|
105
|
+
const show = await server.cli(['profile', 'show', 'custom-tmpl'])
|
|
106
|
+
expect(show.stdout).toContain('bootstrap: no')
|
|
107
|
+
} finally {
|
|
108
|
+
rmSync(scratch, { recursive: true, force: true })
|
|
109
|
+
}
|
|
110
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest'
|
|
2
|
+
import { extractAgentId } from './helpers.ts'
|
|
3
|
+
import { startTestServer, type TestServer } from './server-fixture.ts'
|
|
4
|
+
|
|
5
|
+
let server: TestServer
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
server = await startTestServer()
|
|
8
|
+
})
|
|
9
|
+
afterAll(() => server.stop())
|
|
10
|
+
beforeEach(() => server.reset())
|
|
11
|
+
|
|
12
|
+
test('send writes a message between two agents', async () => {
|
|
13
|
+
await server.cli(['profile', 'create', 'p', '--model', 'm'])
|
|
14
|
+
|
|
15
|
+
let r = await server.cli(['agent', 'spawn', '--profile', 'p'])
|
|
16
|
+
const a = extractAgentId(r.stdout)
|
|
17
|
+
r = await server.cli(['agent', 'spawn', '--profile', 'p'])
|
|
18
|
+
const b = extractAgentId(r.stdout)
|
|
19
|
+
|
|
20
|
+
r = await server.cli(['send', a, b, 'hello there'])
|
|
21
|
+
expect(r.exitCode).toBe(0)
|
|
22
|
+
expect(r.stdout).toContain('sent message')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('send fails when an agent is missing', async () => {
|
|
26
|
+
await server.cli(['profile', 'create', 'p', '--model', 'm'])
|
|
27
|
+
const r = await server.cli(['send', 'nope1', 'nope2', 'hi'])
|
|
28
|
+
expect(r.exitCode).not.toBe(0)
|
|
29
|
+
expect(r.stderr + r.stdout).toContain('agent not found')
|
|
30
|
+
})
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from 'node:child_process'
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { createServer } from 'node:net'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import {
|
|
7
|
+
openDb,
|
|
8
|
+
providerModelRepo,
|
|
9
|
+
providerStateRepo,
|
|
10
|
+
registerGroup,
|
|
11
|
+
resolvePaths,
|
|
12
|
+
runMigrations,
|
|
13
|
+
webTokenRepo,
|
|
14
|
+
} from '../../daemon/src/core/index.ts'
|
|
15
|
+
import { type CliResult, runCli } from './helpers.ts'
|
|
16
|
+
|
|
17
|
+
// Tests now boot the standalone daemon (apps/daemon/src/index.ts) — the
|
|
18
|
+
// authority for /api/* — instead of the Astro web app, which only renders
|
|
19
|
+
// pages and proxies to the daemon. tsx loader keeps the source-mode
|
|
20
|
+
// invocation matching `bazilion serve`.
|
|
21
|
+
const daemonEntry = join(import.meta.dirname, '..', '..', 'daemon', 'src', 'index.ts')
|
|
22
|
+
|
|
23
|
+
function findFreePort(): Promise<number> {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const srv = createServer()
|
|
26
|
+
srv.listen(0, () => {
|
|
27
|
+
const addr = srv.address()
|
|
28
|
+
if (!addr || typeof addr === 'string') {
|
|
29
|
+
reject(new Error('unexpected server address'))
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
const port = addr.port
|
|
33
|
+
srv.close((err) => (err ? reject(err) : resolve(port)))
|
|
34
|
+
})
|
|
35
|
+
srv.on('error', reject)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create a fresh `~/.bazilion`-shaped home: tempdir + subdirs + migrated db +
|
|
41
|
+
* a bootstrap token row in `web_tokens` + an `auth.json` exposing the
|
|
42
|
+
* plaintext for the CLI to use as a bearer. Mirrors the daemon's first-run
|
|
43
|
+
* bootstrap (`apps/daemon/src/lib/ctx.ts:bootstrap`) without the subprocess
|
|
44
|
+
* overhead.
|
|
45
|
+
*/
|
|
46
|
+
function initHome(): { home: string; token: string } {
|
|
47
|
+
const home = mkdtempSync(join(tmpdir(), 'bazilion-test-'))
|
|
48
|
+
const paths = resolvePaths(home)
|
|
49
|
+
for (const d of [
|
|
50
|
+
paths.home,
|
|
51
|
+
paths.profilesDir,
|
|
52
|
+
paths.agentsDir,
|
|
53
|
+
paths.skillsDir,
|
|
54
|
+
paths.groupsDir,
|
|
55
|
+
paths.logsDir,
|
|
56
|
+
]) {
|
|
57
|
+
mkdirSync(d, { recursive: true })
|
|
58
|
+
}
|
|
59
|
+
const db = openDb(paths.db)
|
|
60
|
+
runMigrations(db)
|
|
61
|
+
// Mint a bootstrap token row + write its plaintext into auth.json.
|
|
62
|
+
const created = webTokenRepo.create(db, 'bootstrap')
|
|
63
|
+
writeFileSync(paths.authFile, `${JSON.stringify({ token: created.token }, null, 2)}\n`)
|
|
64
|
+
// Enable lmstudio + ollama by default for test setup — the integration
|
|
65
|
+
// tests mount their mock server via LMSTUDIO_URL and expect model resolution
|
|
66
|
+
// to succeed. Real installs default to all providers disabled and require
|
|
67
|
+
// the admin to toggle them on via /config.
|
|
68
|
+
providerStateRepo.setEnabled(db, 'lmstudio', true)
|
|
69
|
+
providerStateRepo.setEnabled(db, 'ollama', true)
|
|
70
|
+
// Curate a placeholder model so the first-run middleware gate sees setup
|
|
71
|
+
// as complete (isSetupComplete = any enabled provider has ≥1 curated model).
|
|
72
|
+
// Tests that care about a specific model still override via --model.
|
|
73
|
+
providerModelRepo.replace(db, 'lmstudio', ['test-model'])
|
|
74
|
+
providerModelRepo.replace(db, 'ollama', ['test-model'])
|
|
75
|
+
// Seed the 'default' group so spawnAgent's default-group fallback works
|
|
76
|
+
// without tests having to register one first. Matches the fresh-install
|
|
77
|
+
// post-first-run-setup state.
|
|
78
|
+
registerGroup(db, { id: 'default', name: 'Default' }, paths)
|
|
79
|
+
db.close()
|
|
80
|
+
return { home, token: created.token }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function waitForHttp(url: string, timeoutMs: number): Promise<void> {
|
|
84
|
+
const start = Date.now()
|
|
85
|
+
while (Date.now() - start < timeoutMs) {
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch(url)
|
|
88
|
+
if (res.status >= 200 && res.status < 500) return
|
|
89
|
+
} catch {
|
|
90
|
+
// server not yet listening
|
|
91
|
+
}
|
|
92
|
+
await new Promise((r) => setTimeout(r, 150))
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`timeout waiting for ${url}`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface TestServer {
|
|
98
|
+
home: string
|
|
99
|
+
url: string
|
|
100
|
+
token: string
|
|
101
|
+
/** Run the CLI as a subprocess against this test server. */
|
|
102
|
+
cli(args: string[], extraEnv?: NodeJS.ProcessEnv): Promise<CliResult>
|
|
103
|
+
/**
|
|
104
|
+
* Wipe per-test state (DB rows + on-disk profiles/agents/skills/groups dirs)
|
|
105
|
+
* so the same daemon process can host multiple tests. Lets test files use
|
|
106
|
+
* `beforeAll(start)` + `beforeEach(reset)` instead of paying daemon cold
|
|
107
|
+
* start per test. The server's cached DB handle reads the post-reset state
|
|
108
|
+
* immediately because SQLite WAL + same file. Preserves the bootstrap
|
|
109
|
+
* web_tokens row so the CLI's bearer keeps working across tests.
|
|
110
|
+
*/
|
|
111
|
+
reset(): void
|
|
112
|
+
stop(): Promise<void>
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const WIPE_SQL = `
|
|
116
|
+
DELETE FROM messages;
|
|
117
|
+
DELETE FROM agent_triggers;
|
|
118
|
+
DELETE FROM agent_skills;
|
|
119
|
+
DELETE FROM agents;
|
|
120
|
+
DELETE FROM profile_default_skills;
|
|
121
|
+
DELETE FROM profiles;
|
|
122
|
+
DELETE FROM groups;
|
|
123
|
+
DELETE FROM skill_meta;
|
|
124
|
+
DELETE FROM provider_models;
|
|
125
|
+
DELETE FROM provider_state;
|
|
126
|
+
DELETE FROM secrets;
|
|
127
|
+
DELETE FROM config;
|
|
128
|
+
`
|
|
129
|
+
|
|
130
|
+
function resetHome(home: string): void {
|
|
131
|
+
const paths = resolvePaths(home)
|
|
132
|
+
const db = openDb(paths.db)
|
|
133
|
+
db.raw.exec(WIPE_SQL)
|
|
134
|
+
// Re-enable lmstudio + ollama the same way initHome does — tests that mount
|
|
135
|
+
// mock providers expect these enabled.
|
|
136
|
+
providerStateRepo.setEnabled(db, 'lmstudio', true)
|
|
137
|
+
providerStateRepo.setEnabled(db, 'ollama', true)
|
|
138
|
+
// Re-curate the placeholder model so the first-run gate stays open.
|
|
139
|
+
providerModelRepo.replace(db, 'lmstudio', ['test-model'])
|
|
140
|
+
providerModelRepo.replace(db, 'ollama', ['test-model'])
|
|
141
|
+
// Wipe + re-create the groups directory so the auto-seeded slot is fresh.
|
|
142
|
+
rmSync(paths.groupsDir, { recursive: true, force: true })
|
|
143
|
+
mkdirSync(paths.groupsDir, { recursive: true })
|
|
144
|
+
registerGroup(db, { id: 'default', name: 'Default' }, paths)
|
|
145
|
+
db.close()
|
|
146
|
+
for (const dir of [paths.profilesDir, paths.agentsDir, paths.skillsDir]) {
|
|
147
|
+
rmSync(dir, { recursive: true, force: true })
|
|
148
|
+
mkdirSync(dir, { recursive: true })
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Spawns the daemon on a free port with BAZILION_HOME pointing at a fresh
|
|
154
|
+
* tempdir. Resolves once the server answers HTTP. Call `stop()` to kill the
|
|
155
|
+
* process and clean up the tempdir.
|
|
156
|
+
*
|
|
157
|
+
* Additional env vars can be passed (e.g. `LMSTUDIO_URL` pointing at a mock
|
|
158
|
+
* provider) — they're merged into the server's env.
|
|
159
|
+
*/
|
|
160
|
+
export async function startTestServer(extraServerEnv: NodeJS.ProcessEnv = {}): Promise<TestServer> {
|
|
161
|
+
const { home, token } = initHome()
|
|
162
|
+
const port = await findFreePort()
|
|
163
|
+
const url = `http://127.0.0.1:${port}`
|
|
164
|
+
|
|
165
|
+
const proc: ChildProcess = spawn('node', ['--import', 'tsx/esm', daemonEntry], {
|
|
166
|
+
env: {
|
|
167
|
+
...process.env,
|
|
168
|
+
BAZILION_HOME: home,
|
|
169
|
+
HOST: '127.0.0.1',
|
|
170
|
+
PORT: String(port),
|
|
171
|
+
...extraServerEnv,
|
|
172
|
+
},
|
|
173
|
+
stdio: 'ignore',
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
// /api/health is unauthenticated — perfect liveness probe. Daemon should
|
|
178
|
+
// come up in well under a second; 20s is paranoia headroom.
|
|
179
|
+
await waitForHttp(`${url}/api/health`, 20_000)
|
|
180
|
+
} catch (err) {
|
|
181
|
+
proc.kill()
|
|
182
|
+
rmSync(home, { recursive: true, force: true })
|
|
183
|
+
throw err
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
home,
|
|
188
|
+
url,
|
|
189
|
+
token,
|
|
190
|
+
cli(args, extraEnv = {}) {
|
|
191
|
+
return runCli(args, home, {
|
|
192
|
+
BAZILION_SERVER: url,
|
|
193
|
+
BAZILION_TOKEN: token,
|
|
194
|
+
...extraEnv,
|
|
195
|
+
})
|
|
196
|
+
},
|
|
197
|
+
reset() {
|
|
198
|
+
resetHome(home)
|
|
199
|
+
},
|
|
200
|
+
async stop() {
|
|
201
|
+
await new Promise<void>((resolve) => {
|
|
202
|
+
proc.on('close', () => resolve())
|
|
203
|
+
proc.kill('SIGTERM')
|
|
204
|
+
// Failsafe: if the process hasn't exited in 5s, force it.
|
|
205
|
+
setTimeout(() => {
|
|
206
|
+
if (proc.exitCode === null) proc.kill('SIGKILL')
|
|
207
|
+
}, 5_000)
|
|
208
|
+
})
|
|
209
|
+
rmSync(home, { recursive: true, force: true })
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest'
|
|
2
|
+
import { extractAgentId } from './helpers.ts'
|
|
3
|
+
import { startTestServer, type TestServer } from './server-fixture.ts'
|
|
4
|
+
|
|
5
|
+
let server: TestServer
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
server = await startTestServer({ BAZILION_SCHEDULER: 'off' })
|
|
8
|
+
})
|
|
9
|
+
afterAll(() => server.stop())
|
|
10
|
+
beforeEach(() => server.reset())
|
|
11
|
+
|
|
12
|
+
test('agent session-head reports (no session yet) for a fresh agent', async () => {
|
|
13
|
+
let r = await server.cli(['profile', 'create', 'p', '--model', 'anthropic:claude-opus-4-6'])
|
|
14
|
+
expect(r.exitCode).toBe(0)
|
|
15
|
+
|
|
16
|
+
r = await server.cli(['agent', 'spawn', '--profile', 'p', '--name', 'A'])
|
|
17
|
+
expect(r.exitCode).toBe(0)
|
|
18
|
+
const agentId = extractAgentId(r.stdout)
|
|
19
|
+
|
|
20
|
+
r = await server.cli(['agent', 'session-head', agentId])
|
|
21
|
+
expect(r.exitCode).toBe(0)
|
|
22
|
+
expect(r.stdout).toContain('(no session yet)')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('agent session-head --json emits the SessionHeadResponse shape', async () => {
|
|
26
|
+
let r = await server.cli(['profile', 'create', 'p', '--model', 'anthropic:claude-opus-4-6'])
|
|
27
|
+
expect(r.exitCode).toBe(0)
|
|
28
|
+
|
|
29
|
+
r = await server.cli(['agent', 'spawn', '--profile', 'p', '--name', 'A'])
|
|
30
|
+
expect(r.exitCode).toBe(0)
|
|
31
|
+
const agentId = extractAgentId(r.stdout)
|
|
32
|
+
|
|
33
|
+
r = await server.cli(['agent', 'session-head', agentId, '--json'])
|
|
34
|
+
expect(r.exitCode).toBe(0)
|
|
35
|
+
const body = JSON.parse(r.stdout) as { file: string | null; size: number }
|
|
36
|
+
expect(body).toEqual({ file: null, size: 0 })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('agent session-head returns 404 for an unknown agent', async () => {
|
|
40
|
+
// Prefix matching needs at least 4 chars; use one that could not resolve.
|
|
41
|
+
const r = await server.cli(['agent', 'session-head', 'deadbeef-no-such-agent'])
|
|
42
|
+
expect(r.exitCode).not.toBe(0)
|
|
43
|
+
// ApiClientError prints "error: agent not found: …"
|
|
44
|
+
expect(r.stderr + r.stdout).toMatch(/not found/i)
|
|
45
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import AdmZip from 'adm-zip'
|
|
5
|
+
import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest'
|
|
6
|
+
import { extractAgentId } from './helpers.ts'
|
|
7
|
+
import { startTestServer, type TestServer } from './server-fixture.ts'
|
|
8
|
+
|
|
9
|
+
let server: TestServer
|
|
10
|
+
beforeAll(async () => {
|
|
11
|
+
server = await startTestServer()
|
|
12
|
+
})
|
|
13
|
+
afterAll(() => server.stop())
|
|
14
|
+
beforeEach(() => server.reset())
|
|
15
|
+
|
|
16
|
+
function fixtureSkillsDir(skills: string[]): string {
|
|
17
|
+
const root = mkdtempSync(join(tmpdir(), 'bazilion-skill-fixture-'))
|
|
18
|
+
for (const name of skills) {
|
|
19
|
+
const dir = join(root, name)
|
|
20
|
+
mkdirSync(dir, { recursive: true })
|
|
21
|
+
writeFileSync(
|
|
22
|
+
join(dir, 'SKILL.md'),
|
|
23
|
+
`---
|
|
24
|
+
name: ${name}
|
|
25
|
+
description: Test skill ${name}.
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
body of ${name}
|
|
29
|
+
`,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
return root
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test('skill list shows nothing when none installed', async () => {
|
|
36
|
+
const r = await server.cli(['skill', 'list'])
|
|
37
|
+
expect(r.exitCode).toBe(0)
|
|
38
|
+
expect(r.stdout).toContain('no skills installed')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('skill import + list + rm round-trip', async () => {
|
|
42
|
+
const source = fixtureSkillsDir(['alpha', 'beta'])
|
|
43
|
+
|
|
44
|
+
let r = await server.cli(['skill', 'import', '--from', source])
|
|
45
|
+
expect(r.exitCode).toBe(0)
|
|
46
|
+
expect(r.stdout).toContain('imported alpha')
|
|
47
|
+
expect(r.stdout).toContain('imported beta')
|
|
48
|
+
|
|
49
|
+
r = await server.cli(['skill', 'list'])
|
|
50
|
+
expect(r.stdout).toContain('alpha')
|
|
51
|
+
expect(r.stdout).toContain('beta')
|
|
52
|
+
expect(r.stdout).toContain('Test skill')
|
|
53
|
+
|
|
54
|
+
r = await server.cli(['skill', 'rm', 'alpha'])
|
|
55
|
+
expect(r.exitCode).toBe(0)
|
|
56
|
+
|
|
57
|
+
r = await server.cli(['skill', 'list'])
|
|
58
|
+
expect(r.stdout).not.toContain('alpha')
|
|
59
|
+
expect(r.stdout).toContain('beta')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('skill import skips existing without --force', async () => {
|
|
63
|
+
const source = fixtureSkillsDir(['dup'])
|
|
64
|
+
await server.cli(['skill', 'import', '--from', source])
|
|
65
|
+
|
|
66
|
+
const r = await server.cli(['skill', 'import', '--from', source])
|
|
67
|
+
expect(r.stdout).toContain('skipped dup')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('skill import --from openclaw resolves to ~/.openclaw/skills', async () => {
|
|
71
|
+
// We don't assume the user actually has skills there. We just check that
|
|
72
|
+
// when ~/.openclaw/skills doesn't exist, the error message references the
|
|
73
|
+
// path that the server (not the client) would resolve.
|
|
74
|
+
const r = await server.cli(['skill', 'import', '--from', 'openclaw'])
|
|
75
|
+
if (r.exitCode !== 0) {
|
|
76
|
+
expect(r.stderr + r.stdout).toMatch(/\.openclaw/)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('skill list --agent shows attached skills with descriptions', async () => {
|
|
81
|
+
const source = fixtureSkillsDir(['s1', 's2'])
|
|
82
|
+
await server.cli(['skill', 'import', '--from', source])
|
|
83
|
+
|
|
84
|
+
await server.cli(['profile', 'create', 'p', '--model', 'm', '--skills', 's1,s2'])
|
|
85
|
+
let r = await server.cli(['agent', 'spawn', '--profile', 'p'])
|
|
86
|
+
const id = extractAgentId(r.stdout)
|
|
87
|
+
|
|
88
|
+
r = await server.cli(['skill', 'list', '--agent', id])
|
|
89
|
+
expect(r.stdout).toContain('s1')
|
|
90
|
+
expect(r.stdout).toContain('s2')
|
|
91
|
+
expect(r.stdout).toContain('Test skill s1')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('skill list --agent reports missing skills', async () => {
|
|
95
|
+
await server.cli(['profile', 'create', 'p', '--model', 'm', '--skills', 'ghost'])
|
|
96
|
+
let r = await server.cli(['agent', 'spawn', '--profile', 'p'])
|
|
97
|
+
const id = extractAgentId(r.stdout)
|
|
98
|
+
|
|
99
|
+
r = await server.cli(['skill', 'list', '--agent', id])
|
|
100
|
+
expect(r.stdout).toContain('missing: ghost')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
function fixtureSkillsZip(skills: string[]): string {
|
|
104
|
+
const zip = new AdmZip()
|
|
105
|
+
for (const name of skills) {
|
|
106
|
+
zip.addFile(
|
|
107
|
+
`${name}/SKILL.md`,
|
|
108
|
+
Buffer.from(`---\nname: ${name}\ndescription: Test skill ${name}.\n---\n\nbody of ${name}\n`),
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
const dir = mkdtempSync(join(tmpdir(), 'bazilion-skill-zip-fixture-'))
|
|
112
|
+
const zipPath = join(dir, 'skills.zip')
|
|
113
|
+
zip.writeZip(zipPath)
|
|
114
|
+
return zipPath
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
test('skill import --from <local.zip> uploads via multipart and installs every skill', async () => {
|
|
118
|
+
const zipPath = fixtureSkillsZip(['zapha', 'zbeta'])
|
|
119
|
+
|
|
120
|
+
let r = await server.cli(['skill', 'import', '--from', zipPath])
|
|
121
|
+
expect(r.exitCode).toBe(0)
|
|
122
|
+
expect(r.stdout).toContain('imported zapha')
|
|
123
|
+
expect(r.stdout).toContain('imported zbeta')
|
|
124
|
+
|
|
125
|
+
r = await server.cli(['skill', 'list'])
|
|
126
|
+
expect(r.stdout).toContain('zapha')
|
|
127
|
+
expect(r.stdout).toContain('zbeta')
|
|
128
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest'
|
|
4
|
+
import { startTestServer, type TestServer } from './server-fixture.ts'
|
|
5
|
+
|
|
6
|
+
let server: TestServer
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
server = await startTestServer()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
afterAll(async () => {
|
|
13
|
+
await server.stop()
|
|
14
|
+
})
|
|
15
|
+
beforeEach(() => server.reset())
|
|
16
|
+
|
|
17
|
+
test('token show-local prints the bootstrap token from auth.json', async () => {
|
|
18
|
+
const raw = readFileSync(join(server.home, 'auth.json'), 'utf8')
|
|
19
|
+
const expected = JSON.parse(raw).token as string
|
|
20
|
+
expect(typeof expected).toBe('string')
|
|
21
|
+
|
|
22
|
+
const res = await server.cli(['token', 'show-local'])
|
|
23
|
+
expect(res.exitCode).toBe(0)
|
|
24
|
+
expect(res.stdout.trim()).toBe(expected)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('token create mints a token and prints the plaintext exactly once', async () => {
|
|
28
|
+
const res = await server.cli(['token', 'create', 'laptop'])
|
|
29
|
+
expect(res.exitCode).toBe(0)
|
|
30
|
+
expect(res.stdout).toMatch(/label: laptop/)
|
|
31
|
+
expect(res.stdout).toMatch(/token: [0-9a-f]{48}/)
|
|
32
|
+
expect(res.stdout).toMatch(/id: /)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('token list shows active tokens and hides revoked by default', async () => {
|
|
36
|
+
const createA = await server.cli(['token', 'create', 'alpha'])
|
|
37
|
+
const idA = createA.stdout.match(/id:\s+([\w-]+)/)?.[1] as string
|
|
38
|
+
await server.cli(['token', 'create', 'beta'])
|
|
39
|
+
const revoke = await server.cli(['token', 'revoke', idA])
|
|
40
|
+
expect(revoke.exitCode).toBe(0)
|
|
41
|
+
|
|
42
|
+
const activeList = await server.cli(['token', 'list'])
|
|
43
|
+
expect(activeList.stdout).not.toContain(idA)
|
|
44
|
+
expect(activeList.stdout).toContain('beta')
|
|
45
|
+
|
|
46
|
+
const allList = await server.cli(['token', 'list', '--all'])
|
|
47
|
+
expect(allList.stdout).toContain(idA)
|
|
48
|
+
expect(allList.stdout).toContain('revoked')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('a minted token authenticates subsequent API calls', async () => {
|
|
52
|
+
const create = await server.cli(['token', 'create', 'laptop'])
|
|
53
|
+
const token = create.stdout.match(/token:\s+([0-9a-f]+)/)?.[1] as string
|
|
54
|
+
expect(token).toBeTruthy()
|
|
55
|
+
|
|
56
|
+
const res = await fetch(`${server.url}/api/profiles`, {
|
|
57
|
+
headers: { cookie: `bz_token=${token}` },
|
|
58
|
+
})
|
|
59
|
+
expect(res.status).toBe(200)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('revoked tokens are rejected', async () => {
|
|
63
|
+
const create = await server.cli(['token', 'create', 'burn'])
|
|
64
|
+
const token = create.stdout.match(/token:\s+([0-9a-f]+)/)?.[1] as string
|
|
65
|
+
const id = create.stdout.match(/id:\s+([\w-]+)/)?.[1] as string
|
|
66
|
+
await server.cli(['token', 'revoke', id])
|
|
67
|
+
|
|
68
|
+
const res = await fetch(`${server.url}/api/profiles`, {
|
|
69
|
+
headers: { cookie: `bz_token=${token}` },
|
|
70
|
+
})
|
|
71
|
+
expect(res.status).toBe(401)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('login --server --token verifies and writes remote to auth.json', async () => {
|
|
75
|
+
const create = await server.cli(['token', 'create', 'remote'])
|
|
76
|
+
const token = create.stdout.match(/token:\s+([0-9a-f]+)/)?.[1] as string
|
|
77
|
+
|
|
78
|
+
// login runs against `server.home` — bypass the env-var short-circuit by
|
|
79
|
+
// not setting BAZILION_SERVER/TOKEN for this specific invocation.
|
|
80
|
+
const login = await server.cli(['login', '--server', server.url, '--token', token], {
|
|
81
|
+
BAZILION_SERVER: '',
|
|
82
|
+
BAZILION_TOKEN: '',
|
|
83
|
+
})
|
|
84
|
+
expect(login.exitCode).toBe(0)
|
|
85
|
+
expect(login.stdout).toMatch(/saved remote/)
|
|
86
|
+
|
|
87
|
+
const configPath = join(server.home, 'auth.json')
|
|
88
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8')) as {
|
|
89
|
+
remote?: { server: string; token: string }
|
|
90
|
+
}
|
|
91
|
+
expect(config.remote?.server).toBe(server.url)
|
|
92
|
+
expect(config.remote?.token).toBe(token)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('login --clear removes the stored remote', async () => {
|
|
96
|
+
const configPath = join(server.home, 'auth.json')
|
|
97
|
+
const existing = JSON.parse(readFileSync(configPath, 'utf8')) as Record<string, unknown>
|
|
98
|
+
existing.remote = { server: 'http://example.invalid', token: 'abc' }
|
|
99
|
+
writeFileSync(configPath, `${JSON.stringify(existing, null, 2)}\n`)
|
|
100
|
+
|
|
101
|
+
const clear = await server.cli(
|
|
102
|
+
['login', '--server', 'http://example.invalid', '--token', 'x', '--clear'],
|
|
103
|
+
{ BAZILION_SERVER: '', BAZILION_TOKEN: '' },
|
|
104
|
+
)
|
|
105
|
+
expect(clear.exitCode).toBe(0)
|
|
106
|
+
|
|
107
|
+
const after = JSON.parse(readFileSync(configPath, 'utf8')) as { remote?: unknown }
|
|
108
|
+
expect(after.remote).toBeUndefined()
|
|
109
|
+
})
|