Files
deepseek-harness/packages/experimental/plugin-recommendations/tests/status-context.spec.ts
T
ZiyaZhang fcb7340be1 refactor(experimental): register plugin recommendations as an action card kind
Plugin recommendations now proposes through the shared propose_action tool
instead of a dsh-action answer block. The Host registers the
enable_official_plugin kind with the action card service: the
official-plugins skill, details holding the plugin name, the Enable verb
with global access, and the enabled, already-enabled and failed phases,
whose data carries the application outcome and the management error code.
The package drops its own projection, Remote act, status notice and
persisted record; it keeps the describe Remote for card text. The bundle
inserts the shared card rows beside its own, as Personalized pages does.
The browser half registers a card body in the shared frame that keeps the
catalog text cache, the reserved loading lines, the single details target
and the Enable and Not now footer. The skill's submission section now asks
for one propose_action call.
2026-10-22 07:18:05 -07:00

48 lines
2.4 KiB
TypeScript

import { expect, it, vi } from 'vitest'
import { agentEvents, type Agent, type PreStepDecision } from '@deepseek-ai/dsh-agent'
import * as PluginRecommendations from '../src/index.ts'
import { callId, fixture } from './fixture.ts'
async function prepare(f: Awaited<ReturnType<typeof fixture>>, agent: Agent = f.agent) {
const decision = await agentEvents(f.ctx, agent).waterfall('agent/pre-step', { messages: [], turn: 2, step: 1,
signal: new AbortController().signal, modelSelection: { provider: 'mock', model: 'mock' } },
async (): Promise<PreStepDecision> => ({ kind: 'enter', messages: [] }))
return decision.kind === 'reject' ? decision : {
...decision, messages: decision.messages.filter(message => message.source.kind === 'action-cards'),
}
}
function text(decision: PreStepDecision): string {
if (decision.kind !== 'enter') throw new Error('request was rejected')
return decision.messages.flatMap(message => message.content.flatMap(part => part.type === 'text' ? [part.text] : [])).join('\n')
}
it('reports the card outcome with the plugin guidance on the next request without waking the conversation', async () => {
const f = await fixture()
const followup = vi.spyOn(f.agent, 'followup')
const steer = vi.spyOn(f.agent, 'steer')
f.propose()
expect(await prepare(f)).toEqual({ kind: 'enter', messages: [] })
f.manager.setBundleEnabled.mockResolvedValueOnce({ application: 'restart-required', changed: true, stage: 'enable', target: 'official-search' })
await f.ctx.actionCards.act(f.agent, callId, 'enable')
expect(f.agent.status).toBe('idle')
const decision = await prepare(f)
if (decision.kind !== 'enter') throw new Error('request was rejected')
expect(decision.messages).toHaveLength(1)
expect(decision.messages[0]!.content).toMatchSnapshot('restart-required plugin status')
for (const message of decision.messages) f.agent.session.append('user/message', message, { surfaceOp: 'append' })
expect(await prepare(f)).toEqual({ kind: 'enter', messages: [] })
expect(followup).not.toHaveBeenCalled()
expect(steer).not.toHaveBeenCalled()
})
it('reports a recorded decision after this plugin reloads', async () => {
const f = await fixture()
f.propose()
await f.ctx.actionCards.act(f.agent, callId, 'dismiss')
await f.fiber.dispose()
expect(text(await prepare(f))).toContain('"phase":"dismissed"')
await f.ctx.plugin(PluginRecommendations)
expect(text(await prepare(f))).toContain('dismissed means the user declined it')
})