@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
33import { createNativeMessageAdapter } from '../../message/adapters/native'
44import { createMessageEngine } from '../../message/core/engine'
55import { getSkillRequestContext , lengthPlugin , skillPlugin , thinkingPlugin , toolPlugin } from '../../message/plugins'
6+ import type { SkillPluginOptions } from '../../message/plugins'
67import type { CreateMessageEngineOptions , MessageRequestBody , ResponseProvider } from '../../message/types'
78import { mockResponseProvider } from '../../message/test/mockResponseProvider'
89import type { SkillDefinition } from '../types'
@@ -18,7 +19,25 @@ const weatherSkill: SkillDefinition = {
1819 instructions : 'Use wttr.in for weather requests.' ,
1920}
2021
22+ const skillPluginOptionTypeChecks = [
23+ { selection : { mode : 'none' } } satisfies SkillPluginOptions ,
24+ { selection : { mode : 'manual' , skills : [ weatherSkill ] } } satisfies SkillPluginOptions ,
25+ {
26+ selection : { mode : 'manual' , skillNames : [ 'weather' ] } ,
27+ getSkillByName : async ( ) => weatherSkill ,
28+ } satisfies SkillPluginOptions ,
29+ {
30+ selection : { mode : 'auto' } ,
31+ getSkillCandidates : async ( ) => [ weatherSkill ] ,
32+ getSkillByName : async ( ) => weatherSkill ,
33+ } satisfies SkillPluginOptions ,
34+ ]
35+
2136describe ( 'skillPlugin' , ( ) => {
37+ it ( 'accepts selection-specific default option types' , ( ) => {
38+ expect ( skillPluginOptionTypeChecks ) . toHaveLength ( 4 )
39+ } )
40+
2241 it ( 'uses manual skills for instructions and runtime tools' , async ( ) => {
2342 const vueSkill : SkillDefinition = {
2443 name : 'vue-best-practices' ,
@@ -274,6 +293,63 @@ describe('skillPlugin', () => {
274293 )
275294 } )
276295
296+ it ( 'rejects mismatched resolver results and deduplicates enabled skills' , async ( ) => {
297+ const onSkillsResolved = vi . fn ( )
298+ const engine = createTestMessageEngine ( {
299+ plugins : [
300+ ...silentDefaultPlugins ,
301+ skillPlugin ( {
302+ selection : {
303+ mode : 'manual' ,
304+ skillNames : [ 'weather' , 'weather-alias' ] ,
305+ } ,
306+ getSkillByName : async ( ) => weatherSkill ,
307+ onSkillsResolved,
308+ } ) ,
309+ ] ,
310+ responseProvider : mockResponseProvider ( 'ok' ) ,
311+ } )
312+
313+ await engine . sendMessage ( 'weather in London' )
314+
315+ expect ( onSkillsResolved ) . toHaveBeenCalledWith (
316+ expect . objectContaining ( {
317+ skills : [ weatherSkill ] ,
318+ skillNames : [ 'weather' ] ,
319+ requestedSkillNames : [ 'weather' , 'weather-alias' ] ,
320+ unresolvedSkillNames : [ 'weather-alias' ] ,
321+ } ) ,
322+ expect . any ( Object ) ,
323+ )
324+ } )
325+
326+ it ( 'deduplicates inline manual skills by name' , async ( ) => {
327+ const onSkillsResolved = vi . fn ( )
328+ const engine = createTestMessageEngine ( {
329+ plugins : [
330+ ...silentDefaultPlugins ,
331+ skillPlugin ( {
332+ selection : {
333+ mode : 'manual' ,
334+ skills : [ weatherSkill , weatherSkill ] ,
335+ } ,
336+ onSkillsResolved,
337+ } ) ,
338+ ] ,
339+ responseProvider : mockResponseProvider ( 'ok' ) ,
340+ } )
341+
342+ await engine . sendMessage ( 'weather in London' )
343+
344+ expect ( onSkillsResolved ) . toHaveBeenCalledWith (
345+ expect . objectContaining ( {
346+ skills : [ weatherSkill ] ,
347+ skillNames : [ 'weather' ] ,
348+ } ) ,
349+ expect . any ( Object ) ,
350+ )
351+ } )
352+
277353 it ( 'selects auto skills before injecting selected skill instructions' , async ( ) => {
278354 const instructionContents : string [ ] = [ ]
279355 const getSkillByName = vi . fn ( async ( name : string ) => ( name === weatherSkill . name ? weatherSkill : undefined ) )
@@ -410,6 +486,26 @@ describe('skillPlugin', () => {
410486 )
411487 } )
412488
489+ it ( 'fails before requesting when auto mode is missing toolPlugin' , async ( ) => {
490+ const responseProvider = vi . fn ( mockResponseProvider ( 'unexpected' ) )
491+ const engine = createTestMessageEngine ( {
492+ plugins : [
493+ ...silentDefaultPlugins ,
494+ skillPlugin ( {
495+ selection : { mode : 'auto' } ,
496+ getSkillCandidates : async ( ) => [ weatherSkill ] ,
497+ getSkillByName : async ( ) => weatherSkill ,
498+ } ) ,
499+ ] ,
500+ responseProvider,
501+ } )
502+
503+ await expect ( engine . sendMessage ( 'weather in London' ) ) . rejects . toThrow (
504+ 'skillPlugin auto mode requires an enabled toolPlugin' ,
505+ )
506+ expect ( responseProvider ) . not . toHaveBeenCalled ( )
507+ } )
508+
413509 it ( 'does not inject instructions or tools when selection is none' , async ( ) => {
414510 const responseProvider = vi . fn ( mockResponseProvider ( 'ok' ) )
415511 const onInstructionsResolved = vi . fn ( )
0 commit comments