| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227 |
- import { Context } from '@deepseek-ai/cordis'
- import AgentRegistry from '@deepseek-ai/dsh-agent'
- import type { Agent } from '@deepseek-ai/dsh-agent'
- import SessionStore, { SESSION_FORMAT_VERSION, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session'
- import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query'
- import type {} from '@deepseek-ai/dsh-skill'
- import { describe, expect, it, vi } from 'vitest'
- import { SessionSkillCatalog } from '../src/skill-catalog.ts'
- function observation(
- sessionId: SessionId,
- options: { readonly cwd?: string; readonly agentPreset?: string } = {},
- ): SessionObservation {
- const events = Object.freeze([])
- const lease = (): SessionObservation => ({
- source: 'live',
- header: {
- version: SESSION_FORMAT_VERSION,
- id: sessionId,
- createdAt: 1,
- isSeeded: false,
- ...options.cwd === undefined ? {} : { cwd: options.cwd },
- },
- events,
- inheritedEventCount: SessionLogOffset(0),
- cursor: -1,
- projections: {
- asOfSeq: -1,
- values: {
- ...options.agentPreset === undefined ? {} : { agentPreset: options.agentPreset },
- },
- },
- retain: lease,
- [Symbol.dispose]: () => {},
- })
- return lease()
- }
- async function context(): Promise<Context> {
- const ctx = new Context()
- await ctx.plugin(SessionStore)
- await ctx.plugin(AgentRegistry)
- return ctx
- }
- describe('SessionSkillCatalog', () => {
- it('reads a cold Session catalog without resuming an Agent', async () => {
- const ctx = await context()
- const sessionId = SessionId('cold-skills')
- const observed = observation(sessionId, { cwd: '/cold/project' })
- const dispose = vi.spyOn(observed, Symbol.dispose)
- const observeSession = vi.fn(() => Promise.resolve(observed))
- ctx.provide('sessionQuery', { observeSession } as never)
- const resume = vi.spyOn(ctx.agents, 'resume')
- const list = vi.fn(() => Promise.resolve([
- {
- name: 'review',
- description: 'Review the current change.',
- whenToUse: 'Before publishing.',
- invocation: { modelInvocable: true, userInvocable: true },
- },
- {
- name: 'model-only',
- description: 'Not shown to the user.',
- invocation: { modelInvocable: true, userInvocable: false },
- },
- ]))
- ctx.provide('skills', { list } as never)
- const catalog = new SessionSkillCatalog(ctx)
- await expect(catalog.list({ sessionId }, new AbortController().signal)).resolves.toEqual({
- skills: [{
- name: 'review',
- description: 'Review the current change.',
- whenToUse: 'Before publishing.',
- modelInvocable: true,
- }],
- })
- expect(observeSession).toHaveBeenCalledWith(sessionId)
- expect(dispose).toHaveBeenCalledOnce()
- expect(resume).not.toHaveBeenCalled()
- expect(ctx.agents.list()).toEqual([])
- expect(list).toHaveBeenCalledWith({ cwd: '/cold/project', scope: undefined })
- })
- it('uses a live Agent to address a preset-owned registry', async () => {
- const ctx = await context()
- const sessionId = SessionId('live-skills')
- const session = ctx.sessions.create(sessionId, { meta: { cwd: '/live/project' } })
- const agent = { id: sessionId, session, status: 'idle', ctx } as Agent
- ctx.agents.register(agent)
- ctx.provide('sessionQuery', {
- observeSession: () => Promise.resolve(observation(sessionId, { cwd: '/live/project' })),
- } as never)
- const scopedList = vi.fn(() => Promise.resolve([{
- name: 'preset-owned',
- description: 'Composed for this Agent.',
- invocation: { modelInvocable: false, userInvocable: true },
- }]))
- const standingKeyFor = vi.fn()
- ctx.provide('agentPresets', {
- serviceFor: () => ({ list: scopedList }),
- standingKeyFor,
- } as never)
- const catalog = new SessionSkillCatalog(ctx)
- await expect(catalog.list({ sessionId }, new AbortController().signal)).resolves.toEqual({
- skills: [{
- name: 'preset-owned',
- description: 'Composed for this Agent.',
- modelInvocable: false,
- }],
- })
- expect(scopedList).toHaveBeenCalledWith({ cwd: '/live/project', scope: agent })
- expect(standingKeyFor).not.toHaveBeenCalled()
- })
- it('uses the recorded preset standing scope for a cold Session', async () => {
- const ctx = await context()
- const sessionId = SessionId('standing-skills')
- const scope = { agentPreset: 'minimal' }
- ctx.provide('sessionQuery', {
- observeSession: () => Promise.resolve(observation(sessionId, {
- cwd: '/cold/project',
- agentPreset: 'minimal',
- })),
- } as never)
- const standingKeyFor = vi.fn(() => Promise.resolve(scope))
- ctx.provide('agentPresets', { standingKeyFor } as never)
- const list = vi.fn(() => Promise.resolve([]))
- ctx.provide('skills', { list } as never)
- const catalog = new SessionSkillCatalog(ctx)
- await expect(catalog.list({ sessionId }, new AbortController().signal)).resolves.toEqual({ skills: [] })
- expect(standingKeyFor).toHaveBeenCalledWith('minimal')
- expect(list).toHaveBeenCalledWith({ cwd: '/cold/project', scope })
- expect(ctx.agents.list()).toEqual([])
- })
- it('falls back to the global registry when the recorded preset is unavailable', async () => {
- const ctx = await context()
- const sessionId = SessionId('gone-preset')
- ctx.provide('sessionQuery', {
- observeSession: () => Promise.resolve(observation(sessionId, {
- cwd: '/cold/project',
- agentPreset: 'gone',
- })),
- } as never)
- ctx.provide('agentPresets', {
- standingKeyFor: () => Promise.reject(new Error('unknown preset')),
- } as never)
- const list = vi.fn(() => Promise.resolve([]))
- ctx.provide('skills', { list } as never)
- const catalog = new SessionSkillCatalog(ctx)
- await expect(catalog.list({ sessionId }, new AbortController().signal)).resolves.toEqual({ skills: [] })
- expect(list).toHaveBeenCalledWith({ cwd: '/cold/project', scope: undefined })
- })
- it.each([
- {
- error: new SessionQueryError(
- 'session "missing-skills" not found',
- 'SESSION_QUERY_SESSION_NOT_FOUND',
- ),
- code: 'session/not-found',
- },
- { error: new Error('storage offline'), code: 'gateway/internal' },
- ] as const)('classifies failed Session inspection as $code', async ({ error, code }) => {
- const ctx = await context()
- ctx.provide('sessionQuery', { observeSession: () => Promise.reject(error) } as never)
- const catalog = new SessionSkillCatalog(ctx)
- await expect(catalog.list(
- { sessionId: SessionId('missing-skills') },
- new AbortController().signal,
- )).rejects.toMatchObject({ code })
- })
- it('reports an absent skill registry instead of an empty catalog', async () => {
- const ctx = await context()
- const sessionId = SessionId('no-skills')
- ctx.provide('sessionQuery', {
- observeSession: () => Promise.resolve(observation(sessionId, { cwd: '/project' })),
- } as never)
- const catalog = new SessionSkillCatalog(ctx)
- const failed = catalog.list({ sessionId }, new AbortController().signal)
- await expect(failed).rejects.toMatchObject({ code: 'gateway/internal' })
- await expect(failed).rejects.toThrow('skill registry is absent')
- })
- it('rejects observations without projections or a project cwd', async () => {
- const ctx = await context()
- const sessionId = SessionId('incomplete-skills')
- const withoutProjections = { ...observation(sessionId, { cwd: '/project' }), projections: undefined }
- const observeSession = vi.fn()
- .mockResolvedValueOnce(withoutProjections)
- .mockResolvedValueOnce(observation(sessionId))
- ctx.provide('sessionQuery', { observeSession } as never)
- const catalog = new SessionSkillCatalog(ctx)
- const unprojected = catalog.list({ sessionId }, new AbortController().signal)
- await expect(unprojected).rejects.toMatchObject({ code: 'gateway/internal' })
- await expect(unprojected).rejects.toThrow('projected Session observation')
- const cwdless = catalog.list({ sessionId }, new AbortController().signal)
- await expect(cwdless).rejects.toMatchObject({ code: 'gateway/internal' })
- await expect(cwdless).rejects.toThrow('has no project cwd')
- })
- it('classifies a provider listing failure', async () => {
- const ctx = await context()
- const sessionId = SessionId('failed-skills')
- ctx.provide('sessionQuery', {
- observeSession: () => Promise.resolve(observation(sessionId, { cwd: '/project' })),
- } as never)
- ctx.provide('skills', {
- list: () => Promise.reject(new Error('catalog offline')),
- } as never)
- const catalog = new SessionSkillCatalog(ctx)
- await expect(catalog.list({ sessionId }, new AbortController().signal))
- .rejects.toMatchObject({
- code: 'gateway/internal', message: 'skill listing failed: Error: catalog offline',
- })
- })
- })
|