import { beforeEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' import AgentRegistry, { Inbox } from '@deepseek-ai/dsh-agent' import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent' import CommandService from '@deepseek-ai/dsh-commands' import SessionStore, { foldSurface, Session, SessionId } from '@deepseek-ai/dsh-session' import { Telemetry, type TelemetrySharingStatus } from '@deepseek-ai/dsh-session-telemetry' import * as commandFeedback from '@deepseek-ai/dsh-command-feedback' const { USER_ID, getOrCreateAnonymousUserId } = vi.hoisted(() => { const USER_ID = '01234567-89ab-4cde-8f01-23456789abcd' return { USER_ID, getOrCreateAnonymousUserId: vi.fn(() => USER_ID) } }) vi.mock('@deepseek-ai/dsh-user-id', () => ({ getOrCreateAnonymousUserId, })) beforeEach(() => getOrCreateAnonymousUserId.mockClear()) interface Harness { readonly ctx: Context readonly agent: Agent readonly session: Session readonly plugin: Awaited> } /** Minimal mounted backend disclosing one sharing policy. */ class FakeTelemetry extends Telemetry { override readonly sharing: TelemetrySharingStatus constructor(ctx: Context, config: { sharing: TelemetrySharingStatus }) { super(ctx) this.sharing = config.sharing } emit(): void {} async shutdown(): Promise {} } /** Build a live idle agent over a store-owned session, as an app's spine does. */ function stubAgent(ctx: Context, id: string): { agent: Agent; session: Session } { const session = ctx.sessions.create(SessionId(id)) const inbox = new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }) let status: AgentStatus = 'idle' const agent: Agent = { id: session.id, options: {}, session, inbox, ctx: new Context(), get status() { return status }, send: () => {}, followup: () => {}, steer: () => {}, inject: () => {}, cancel() { status = 'idle' }, runMaintenance: task => task(new AbortController().signal), whenIdle() { return Promise.resolve() }, } return { agent, session } } /** * Mount the real command registry, this producer, and optionally a telemetry * backend disclosing one sharing policy. Without `sharing`, no telemetry * service exists and the acknowledgement reports "not configured". */ async function harness(sharing?: TelemetrySharingStatus): Promise { const ctx = new Context() await ctx.plugin(CommandService) await ctx.plugin(AgentRegistry) await ctx.plugin(SessionStore) if (sharing !== undefined) await ctx.plugin(FakeTelemetry, { sharing }) const plugin = await ctx.plugin(commandFeedback) const { agent, session } = stubAgent(ctx, `command-feedback-${Math.random()}`) ctx.agents.register(agent) return { ctx, agent, session, plugin } } /** Execute `/feedback` through the same registry boundary as a UI adapter. */ async function run(test: Harness, suffix = ''): Promise<{ kind: string; text?: string }> { const settled = await test.ctx.commands.execute( test.agent, `/feedback${suffix}`, new AbortController().signal, ) if (settled === undefined) throw new Error('feedback command was not registered') return settled.result } /** Authoritative feedback payloads in log order. */ function feedbackTexts(session: Session): string[] { return session.events .filter(event => event.type === 'feedback/record') .map(event => event.data.text) } describe('@deepseek-ai/dsh-command-feedback registration', () => { it('registers one global command with Loader-safe exports and disposes it', async () => { const test = await harness() expect(commandFeedback.name).toBe('command-feedback') expect(commandFeedback.inject).toEqual(['commands']) expect('default' in commandFeedback).toBe(false) const loader = Object.create(Loader.prototype) as Loader expect(loader.unwrapExports(commandFeedback)).toBe(commandFeedback) expect(test.ctx.commands.list(test.agent)).toContainEqual({ name: 'feedback', description: 'record feedback about this session', input: { hint: '' }, }) expect(test.ctx.commands.find(test.agent, 'feedback')).toMatchObject({ recordInput: false }) await test.plugin.dispose() expect(test.ctx.commands.find(test.agent, 'feedback')).toBeUndefined() }) }) describe('/feedback human command', () => { it('acknowledges feedback and records its payload exactly once in the domain event', async () => { const test = await harness() await expect(run(test, ' the diff view is unreadable')).resolves.toEqual({ kind: 'success', text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}. Session sharing is not configured.`, }) expect(feedbackTexts(test.session)).toEqual(['the diff view is unreadable']) const commandRun = test.session.events.find(event => event.type === 'command/run') expect(commandRun?.type === 'command/run' && Object.hasOwn(commandRun.data, 'args')).toBe(false) expect(JSON.stringify(test.session.events).match(/the diff view is unreadable/gu)).toHaveLength(1) }) it('exports a command-independent feedback producer', async () => { const test = await harness() commandFeedback.recordFeedback(test.session, ' recorded outside a command ') expect(test.session.events.map(event => event.type)).toEqual(['feedback/record']) expect(feedbackTexts(test.session)).toEqual(['recorded outside a command']) expect(() => { commandFeedback.recordFeedback(test.session, ' \n\t ') }) .toThrow('feedback text must not be empty') expect(feedbackTexts(test.session)).toEqual(['recorded outside a command']) }) it('keeps command bookkeeping around the authoritative feedback event', async () => { const test = await harness() await run(test, ' nothing else happens') expect(test.session.events.map(event => event.type)).toEqual([ 'command/run', 'feedback/record', 'command/done', ]) }) it('normalizes surrounding whitespace without parsing command-like content', async () => { const test = await harness() await run(test, ' /plan felt SLOW\n\ttwice today ') expect(feedbackTexts(test.session)).toEqual(['/plan felt SLOW\n\ttwice today']) }) it('records each entry separately without replacing earlier ones', async () => { const test = await harness() await run(test, ' first') await run(test, ' second') expect(feedbackTexts(test.session)).toEqual(['first', 'second']) }) it('records concurrent submissions in dispatch order', async () => { const test = await harness() const signal = new AbortController().signal // Command adapters may dispatch concurrent requests without awaiting one another. const settled = await Promise.all([ test.ctx.commands.execute(test.agent, '/feedback first', signal), test.ctx.commands.execute(test.agent, '/feedback second', signal), ]) expect(settled.map(item => item?.result)).toEqual([ { kind: 'success', text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}. Session sharing is not configured.` }, { kind: 'success', text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}. Session sharing is not configured.` }, ]) expect(feedbackTexts(test.session)).toEqual(['first', 'second']) }) it('discloses full session sharing in the acknowledgement', async () => { const test = await harness('full') await expect(run(test, ' everything shared')).resolves.toEqual({ kind: 'success', text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}. Session sharing is enabled.`, }) expect(feedbackTexts(test.session)).toEqual(['everything shared']) }) it('discloses feedback-gated session sharing in the acknowledgement', async () => { const test = await harness('feedback-only') await expect(run(test, ' gated sharing')).resolves.toEqual({ kind: 'success', text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}. Session sharing is feedback-gated; recording feedback releases the session prefix for sharing.`, }) expect(feedbackTexts(test.session)).toEqual(['gated sharing']) }) it('discloses disabled session sharing in the acknowledgement', async () => { const test = await harness('disabled') await expect(run(test, ' local only')).resolves.toEqual({ kind: 'success', text: `Feedback recorded for session ${test.session.id}\nUser: ${USER_ID}. Session sharing is disabled.`, }) expect(feedbackTexts(test.session)).toEqual(['local only']) }) it('keeps every recorded event off the model surface and out of derived history', async () => { const test = await harness() await run(test, ' invisible to the model') for (const event of test.session.events) { expect('surfaceOp' in event).toBe(false) expect(test.session.deriveEventMessage(event)).toBeNull() } expect(foldSurface(test.session.events).nodes).toEqual([]) expect(test.session.surface.nodes).toEqual([]) expect(test.session.deriveMessages()).toEqual([]) }) it('rejects empty and whitespace-only input as a failed command record', async () => { const test = await harness() const expected = { kind: 'error', text: 'Feedback text is required. Usage: /feedback ', } await expect(run(test)).resolves.toEqual(expected) await expect(run(test, ' \n\t ')).resolves.toEqual(expected) expect(getOrCreateAnonymousUserId).not.toHaveBeenCalled() expect(feedbackTexts(test.session)).toEqual([]) const done = test.session.events.filter(event => event.type === 'command/done') expect(done.map(event => event.data.kind)).toEqual(['error', 'error']) for (const event of test.session.events) { if (event.type === 'command/run') expect(Object.hasOwn(event.data, 'args')).toBe(false) } }) it('records nothing when dispatch rejects an already-cancelled request', async () => { const test = await harness() const controller = new AbortController() controller.abort(new Error('user cancelled the command')) await expect(test.ctx.commands.execute(test.agent, '/feedback too late', controller.signal)) .rejects.toThrow('user cancelled the command') expect(test.session.events).toEqual([]) }) })