返回 CodeWhale
skills-host.test.mjs
根目录 / crates / tui / extension-host / test / skills-host.test.mjs
1 import { test } from 'node:test'
2 import assert from 'node:assert/strict'
3 import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
4 import { tmpdir } from 'node:os'
5 import { join } from 'node:path'
6 import { activate, startHost } from './harness.mjs'
7
8 function plugin(t, body) {
9 const directory = mkdtempSync(join(tmpdir(), 'cw-skill-root-'))
10 const entry = join(directory, 'index.mjs')
11 writeFileSync(entry, `export const inject = ['skills']\nexport function apply(ctx) {\n${body}\n}\n`)
12 t.after(() => rmSync(directory, { recursive: true, force: true }))
13 return entry
14 }
15
16 test('real host admits detached skill root proposals and disposes only their owner', async (t) => {
17 const host = await startHost()
18 t.after(() => host.stop())
19 const entry = plugin(t, `const root = { path: 'profiles/review-skills' }; ctx.skills.registerRoot(root); root.path = '../changed'`)
20 const a = await activate(host, 'skill-a', entry), b = await activate(host, 'skill-b', entry)
21 assert.equal(a.result.status, 'ok'); assert.equal(b.result.status, 'ok')
22 const registrations = host.registry.filter(item => item.op === 'register' && item.kind === 'skill_root')
23 assert.equal(registrations.length, 2)
24 assert.deepEqual(registrations[0].spec, { name: 'profiles/review-skills', description: '' })
25 assert.deepEqual(await host.call('ext/deactivate', { owner: a.ref }), { disposed: true, leaked: [] })
26 assert.ok(host.registry.some(item => item.op === 'unregister' && item.handle === registrations[0].handle))
27 assert.ok(!host.registry.some(item => item.op === 'unregister' && item.handle === registrations[1].handle))
28 })
29
30 test('invalid and duplicate skill roots fail activation and roll back proposals', async (t) => {
31 const host = await startHost()
32 t.after(() => host.stop())
33 const cases = [
34 [`ctx.skills.registerRoot({ path: '../outside' })`, /bundle-relative/],
35 [`ctx.skills.registerRoot({ path: 'skills', watch: true })`, /only path/],
36 [`ctx.skills.registerRoot({ path: 'skills' }); ctx.skills.registerRoot({ path: 'skills' })`, /already registered/],
37 ]
38 for (const [index, [body, reason]] of cases.entries()) {
39 const before = host.registry.length
40 const { result } = await activate(host, `bad-root-${index}`, plugin(t, body))
41 assert.equal(result.status, 'failed'); assert.match(result.diagnostic, reason)
42 for (const item of host.registry.slice(before).filter(item => item.op === 'register')) {
43 if (!host.registry.some(later => later.op === 'unregister' && later.handle === item.handle)) {
44 await host.waitFor(message => message.method === 'registry/unregister' && message.params.handle === item.handle)
45 }
46 }
47 }
48 })
49
50 test('real host preserves Rust root admission refusal in activation result', async (t) => {
51 const host = await startHost({ admit: () => ({ refused: 'root is outside the reviewed inventory' }) })
52 t.after(() => host.stop())
53 const { result } = await activate(host, 'refused-root', plugin(t, `ctx.skills.registerRoot({ path: 'skills' })`))
54 assert.equal(result.status, 'failed')
55 assert.match(result.diagnostic, /outside the reviewed inventory/)
56 })
57
57 lines Plain Text