Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
246 lines
10 KiB
TypeScript
246 lines
10 KiB
TypeScript
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 CommandRuntime from '@deepseek-ai/dsh-commands'
|
|
import SessionStore, { foldSurface, Session, SessionId } from '@deepseek-ai/dsh-session'
|
|
import { SessionTelemetryBackend, type SessionTelemetrySharingStatus } 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-anonymous-user-id', () => ({
|
|
getOrCreateAnonymousUserId,
|
|
}))
|
|
|
|
beforeEach(() => getOrCreateAnonymousUserId.mockClear())
|
|
|
|
interface Harness {
|
|
readonly ctx: Context
|
|
readonly agent: Agent
|
|
readonly session: Session
|
|
readonly plugin: Awaited<ReturnType<Context['plugin']>>
|
|
}
|
|
|
|
/** Minimal mounted backend disclosing one sharing policy. */
|
|
class FakeTelemetry extends SessionTelemetryBackend {
|
|
override readonly sharing: SessionTelemetrySharingStatus
|
|
|
|
constructor(ctx: Context, config: { sharing: SessionTelemetrySharingStatus }) {
|
|
super(ctx)
|
|
this.sharing = config.sharing
|
|
}
|
|
|
|
emit(): void {}
|
|
|
|
async shutdown(): Promise<void> {}
|
|
}
|
|
|
|
/** 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?: SessionTelemetrySharingStatus): Promise<Harness> {
|
|
const ctx = new Context()
|
|
await ctx.plugin(CommandRuntime)
|
|
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: '<text>' },
|
|
})
|
|
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}\nAnonymous user: ${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}\nAnonymous user: ${USER_ID}. Session sharing is not configured.` },
|
|
{ kind: 'success', text: `Feedback recorded for session ${test.session.id}\nAnonymous user: ${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}\nAnonymous user: ${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}\nAnonymous user: ${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}\nAnonymous user: ${USER_ID}. Session sharing is disabled.`,
|
|
})
|
|
expect(feedbackTexts(test.session)).toEqual(['local only'])
|
|
})
|
|
|
|
it('keeps every recorded event out of model context and 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 <text>',
|
|
}
|
|
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([])
|
|
})
|
|
})
|