Skip to content

Commit 826c60f

Browse files
committed
test(cli): expand clawdbot sync coverage
1 parent a679c3a commit 826c60f

File tree

4 files changed

+158
-1
lines changed

4 files changed

+158
-1
lines changed

e2e/clawdhub.e2e.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,61 @@ describe('clawdhub e2e', () => {
174174
}
175175
})
176176

177+
it('sync dry-run finds skills from clawdbot.json roots', async () => {
178+
const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com'
179+
const site = process.env.CLAWDHUB_SITE?.trim() || 'https://clawdhub.com'
180+
const token = mustGetToken() ?? (await readGlobalConfig())?.token ?? null
181+
if (!token) {
182+
throw new Error('Missing token. Set CLAWDHUB_E2E_TOKEN or run: bun clawdhub auth login')
183+
}
184+
185+
const cfg = await makeTempConfig(registry, token)
186+
const root = await mkdtemp(join(tmpdir(), 'clawdhub-e2e-clawdbot-'))
187+
const stateDir = join(root, 'state')
188+
const configPath = join(root, 'clawdbot.json')
189+
const workspace = join(root, 'clawd-work')
190+
const skillsRoot = join(workspace, 'skills')
191+
const skillDir = join(skillsRoot, 'auto-skill')
192+
193+
try {
194+
await mkdir(skillDir, { recursive: true })
195+
await writeFile(join(skillDir, 'SKILL.md'), '# Skill\n', 'utf8')
196+
197+
const config = `{
198+
// JSON5-style comments + trailing commas
199+
routing: {
200+
agents: {
201+
work: { name: 'Work', workspace: '${workspace}', },
202+
},
203+
},
204+
}`
205+
await writeFile(configPath, config, 'utf8')
206+
207+
const result = spawnSync(
208+
'bun',
209+
['clawdhub', 'sync', '--dry-run', '--all', '--site', site, '--registry', registry],
210+
{
211+
cwd: process.cwd(),
212+
env: {
213+
...process.env,
214+
CLAWDHUB_CONFIG_PATH: cfg.path,
215+
CLAWDHUB_DISABLE_TELEMETRY: '1',
216+
CLAWDBOT_CONFIG_PATH: configPath,
217+
CLAWDBOT_STATE_DIR: stateDir,
218+
},
219+
encoding: 'utf8',
220+
},
221+
)
222+
expect(result.status).toBe(0)
223+
expect(result.stderr).not.toMatch(/error:/i)
224+
expect(result.stdout).toMatch(/Dry run/i)
225+
expect(result.stdout).toMatch(/auto-skill/i)
226+
} finally {
227+
await rm(root, { recursive: true, force: true })
228+
await rm(cfg.dir, { recursive: true, force: true })
229+
}
230+
})
231+
177232
it('publishes, deletes, and undeletes a skill (logged-in)', async () => {
178233
const registry = process.env.CLAWDHUB_REGISTRY?.trim() || 'https://clawdhub.com'
179234
const site = process.env.CLAWDHUB_SITE?.trim() || 'https://clawdhub.com'

packages/clawdhub/src/cli/clawdbotConfig.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* @vitest-environment node */
2-
import { mkdtemp, writeFile } from 'node:fs/promises'
2+
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
33
import { tmpdir } from 'node:os'
44
import { join, resolve } from 'node:path'
55
import { afterEach, describe, expect, it } from 'vitest'
@@ -56,4 +56,46 @@ describe('resolveClawdbotSkillRoots', () => {
5656
expect(labels[resolve(home, 'shared', 'skills')]).toBe('Extra: skills')
5757
expect(labels[resolve('/opt/skills')]).toBe('Extra: skills')
5858
})
59+
60+
it('respects CLAWDBOT_STATE_DIR and CLAWDBOT_CONFIG_PATH overrides', async () => {
61+
const base = await mkdtemp(join(tmpdir(), 'clawdhub-clawdbot-override-'))
62+
const home = join(base, 'home')
63+
const stateDir = join(base, 'custom-state')
64+
const configPath = join(base, 'config', 'clawdbot.json')
65+
66+
process.env.HOME = home
67+
process.env.CLAWDBOT_STATE_DIR = stateDir
68+
process.env.CLAWDBOT_CONFIG_PATH = configPath
69+
70+
const config = `{
71+
agent: { workspace: "${join(base, 'workspace-main')}" },
72+
}`
73+
await mkdir(join(base, 'config'), { recursive: true })
74+
await writeFile(configPath, config, 'utf8')
75+
76+
const { roots, labels } = await resolveClawdbotSkillRoots()
77+
78+
expect(roots).toEqual(
79+
expect.arrayContaining([
80+
resolve(stateDir, 'skills'),
81+
resolve(join(base, 'workspace-main'), 'skills'),
82+
]),
83+
)
84+
expect(labels[resolve(stateDir, 'skills')]).toBe('Shared skills')
85+
expect(labels[resolve(join(base, 'workspace-main'), 'skills')]).toBe('Agent: main')
86+
})
87+
88+
it('returns shared skills root when config is missing', async () => {
89+
const base = await mkdtemp(join(tmpdir(), 'clawdhub-clawdbot-missing-'))
90+
const stateDir = join(base, 'state')
91+
const configPath = join(base, 'missing', 'clawdbot.json')
92+
93+
process.env.CLAWDBOT_STATE_DIR = stateDir
94+
process.env.CLAWDBOT_CONFIG_PATH = configPath
95+
96+
const { roots, labels } = await resolveClawdbotSkillRoots()
97+
98+
expect(roots).toEqual([resolve(stateDir, 'skills')])
99+
expect(labels[resolve(stateDir, 'skills')]).toBe('Shared skills')
100+
})
59101
})

packages/clawdhub/src/cli/commands/sync.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ vi.mock('../scanSkills.js', () => ({
5757
getFallbackSkillRoots: vi.fn(() => []),
5858
}))
5959

60+
const mockResolveClawdbotSkillRoots = vi.fn(async () => ({ roots: [], labels: {} }))
61+
vi.mock('../clawdbotConfig.js', () => ({
62+
resolveClawdbotSkillRoots: () => mockResolveClawdbotSkillRoots(),
63+
}))
64+
6065
vi.mock('../../skills.js', async () => {
6166
const actual = await vi.importActual<typeof import('../../skills.js')>('../../skills.js')
6267
return {
@@ -219,6 +224,35 @@ describe('cmdSync', () => {
219224
expect(output).toMatch(/dup-skill/)
220225
})
221226

227+
it('prints labeled roots when clawdbot roots are detected', async () => {
228+
interactive = false
229+
mockResolveClawdbotSkillRoots.mockResolvedValueOnce({
230+
roots: ['/auto'],
231+
labels: { '/auto': 'Agent: Work' },
232+
})
233+
const { findSkillFolders } = await import('../scanSkills.js')
234+
vi.mocked(findSkillFolders).mockImplementation(async (root: string) => {
235+
if (root === '/auto') {
236+
return [{ folder: '/auto/alpha', slug: 'alpha', displayName: 'Alpha' }]
237+
}
238+
return []
239+
})
240+
mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => {
241+
if (args.path === '/api/v1/whoami') return { user: { handle: 'steipete' } }
242+
if (args.path === '/api/cli/telemetry/sync') return { ok: true }
243+
if (args.path.startsWith('/api/v1/resolve?')) {
244+
throw new Error('Skill not found')
245+
}
246+
throw new Error(`Unexpected apiRequest: ${args.path}`)
247+
})
248+
249+
await cmdSync(makeOpts(), { all: true, dryRun: true }, true)
250+
251+
const output = mockLog.mock.calls.map((call) => String(call[0])).join('\n')
252+
expect(output).toMatch(/Roots with skills/)
253+
expect(output).toMatch(/Agent: Work/)
254+
})
255+
222256
it('allows empty changelog for updates (interactive)', async () => {
223257
interactive = true
224258
mockApiRequest.mockImplementation(async (_registry: string, args: { path: string }) => {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/* @vitest-environment node */
2+
import { describe, expect, it, vi } from 'vitest'
3+
4+
vi.mock('../scanSkills.js', () => ({
5+
findSkillFolders: vi.fn(async (root: string) => {
6+
if (root.endsWith('/with-skill')) {
7+
return [{ folder: `${root}/demo`, slug: 'demo', displayName: 'Demo' }]
8+
}
9+
return []
10+
}),
11+
}))
12+
13+
const { scanRootsWithLabels } = await import('./syncHelpers.js')
14+
15+
describe('scanRootsWithLabels', () => {
16+
it('attaches labels to roots with skills', async () => {
17+
const roots = ['/tmp/with-skill', '/tmp/empty', '/tmp/with-skill']
18+
const labels = { '/tmp/with-skill': 'Agent: Work' }
19+
20+
const result = await scanRootsWithLabels(roots, labels)
21+
22+
expect(result.rootsWithSkills).toEqual(['/tmp/with-skill'])
23+
expect(result.rootLabels).toEqual({ '/tmp/with-skill': 'Agent: Work' })
24+
expect(result.skills.map((skill) => skill.slug)).toEqual(['demo'])
25+
})
26+
})

0 commit comments

Comments
 (0)