Skip to content

Commit

Permalink
fix(core): fix suggestion API in group context
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Jan 7, 2020
1 parent 6851fd2 commit 8253522
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 95 deletions.
1 change: 0 additions & 1 deletion packages/koishi-core/src/app.ts
Expand Up @@ -83,7 +83,6 @@ function createLeadingRE (patterns: string[], prefix = '', suffix = '') {

const defaultOptions: AppOptions = {
maxMiddlewares: 64,
similarityCoefficient: 0.4,
}

export class App extends Context {
Expand Down
4 changes: 2 additions & 2 deletions packages/koishi-core/src/command.ts
Expand Up @@ -82,7 +82,7 @@ export class Command {
_optsDef: Record<string, CommandOption> = {}
_action?: (this: Command, config: ParsedCommandLine, ...args: string[]) => any

static attachUserFields (userFields: Set<UserField>, { command, options }: ParsedCommandLine) {
static attachUserFields (userFields: Set<UserField>, { command, options = {} }: ParsedCommandLine) {
if (!command) return
for (const field of command._userFields) {
userFields.add(field)
Expand Down Expand Up @@ -247,7 +247,7 @@ export class Command {
}

async execute (argv: ParsedCommandLine, next: NextFunction = noop) {
const { meta, options, args, unknown } = argv
const { meta, options = {}, args = [], unknown = [] } = argv
this.app.emitEvent(meta, 'before-command', argv)

// show help when use `-h, --help` or when there is no action
Expand Down
20 changes: 12 additions & 8 deletions packages/koishi-core/src/utils.ts
@@ -1,4 +1,5 @@
import { isInteger } from 'koishi-utils'
import { isInteger, noop } from 'koishi-utils'
import { UserField, GroupField } from './database'
import { NextFunction, Middleware } from './context'
import { Command } from './command'
import { MessageMeta } from './meta'
Expand Down Expand Up @@ -30,7 +31,7 @@ interface SuggestOptions {
next: NextFunction
prefix: string
suffix: string
coefficient: number
coefficient?: number
command: Command | ((suggestion: string) => Command)
execute: (suggestion: string, meta: MessageMeta, next: NextFunction) => any
}
Expand All @@ -40,7 +41,7 @@ function findSimilar (target: string, coefficient: number) {
}

export function showSuggestions (options: SuggestOptions): Promise<void> {
const { target, items, meta, next, prefix, suffix, execute, coefficient } = options
const { target, items, meta, next, prefix, suffix, execute, coefficient = 0.4 } = options
const suggestions = items.filter(findSimilar(target, coefficient))
if (!suggestions.length) return next()

Expand All @@ -50,16 +51,19 @@ export function showSuggestions (options: SuggestOptions): Promise<void> {
const [suggestion] = suggestions
const command = typeof options.command === 'function' ? options.command(suggestion) : options.command
const identifier = meta.userId + meta.$ctxType + meta.$ctxId
const fields = Array.from(command._userFields)
if (!fields.includes('name')) fields.push('name')
if (!fields.includes('usage')) fields.push('usage')
if (!fields.includes('authority')) fields.push('authority')
const userFields = new Set<UserField>(['name'])
const groupFields = new Set<GroupField>()
Command.attachUserFields(userFields, { command, meta })
Command.attachGroupFields(groupFields, { command, meta })

const middleware: Middleware = async (meta, next) => {
if (meta.userId + meta.$ctxType + meta.$ctxId !== identifier) return next()
command.context.removeMiddleware(middleware)
if (!meta.message.trim()) {
meta.$user = await command.context.database?.observeUser(meta.userId, 0, fields)
meta.$user = await command.context.database?.observeUser(meta.userId, Array.from(userFields))
if (meta.messageType === 'group') {
meta.$group = await command.context.database?.observeGroup(meta.groupId, Array.from(groupFields))
}
return execute(suggestions[0], meta, next)
} else {
return next()
Expand Down
99 changes: 16 additions & 83 deletions packages/koishi-core/tests/runtime.spec.ts
@@ -1,13 +1,10 @@
import { Session, createMeta } from 'koishi-test-utils'
import { App, messages } from 'koishi-core'
import { Session, createMeta, createApp } from 'koishi-test-utils'
import { messages } from 'koishi-core'
import { format } from 'util'

const app = new App({
selfId: 514,
})

beforeAll(() => app.start())
afterAll(() => app.stop())
const app = createApp()
const session2 = new Session(app, 'user', 789)
const session3 = new Session(app, 'group', 456, 321)

app.command('foo <text>', { checkArgCount: true })
.shortcut('bar1', { args: ['bar'] })
Expand All @@ -24,15 +21,6 @@ app.command('fooo', { checkUnknown: true, checkRequired: true })
return meta.$send('fooo' + options.text)
})

app.command('err')
.action(() => {
throw new Error('command error')
})

const session1 = new Session(app, 'user', 456)
const session2 = new Session(app, 'user', 789)
const session3 = new Session(app, 'group', 456, 321)

describe('command prefix', () => {
beforeAll(() => {
app.options.similarityCoefficient = 0
Expand Down Expand Up @@ -161,15 +149,17 @@ describe('command execution', () => {
})

test('command error', async () => {
const mock1 = jest.fn()
const mock2 = jest.fn()
app.receiver.on('error', mock1)
app.receiver.on('error/command', mock2)
await app.executeCommandLine('err', meta)
expect(mock1).toBeCalledTimes(1)
expect(mock1.mock.calls[0][0]).toHaveProperty('message', 'command error')
expect(mock2).toBeCalledTimes(1)
expect(mock2.mock.calls[0][0]).toHaveProperty('message', 'command error')
const error = new Error('command error')
app.command('error-command').action(() => { throw error })
const errorCallback = jest.fn()
const errorCommandCallback = jest.fn()
app.receiver.on('error', errorCallback)
app.receiver.on('error/command', errorCommandCallback)
await app.executeCommandLine('error-command', meta)
expect(errorCallback).toBeCalledTimes(1)
expect(errorCallback).toBeCalledWith(error)
expect(errorCommandCallback).toBeCalledTimes(1)
expect(errorCommandCallback).toBeCalledWith(error)
})

test('command events', async () => {
Expand Down Expand Up @@ -252,60 +242,3 @@ describe('shortcuts', () => {
await session3.shouldHaveReply(`[CQ:at,qq=${app.selfId}] bar4bar baz`, 'foobar baz')
})
})

describe('suggestions', () => {
const expectedSuggestionText = [
messages.COMMAND_SUGGESTION_PREFIX,
format(messages.SUGGESTION_TEXT, '“foo”'),
messages.COMMAND_SUGGESTION_SUFFIX,
].join('')

const expectedSuggestionText2 = [
messages.COMMAND_SUGGESTION_PREFIX,
format(messages.SUGGESTION_TEXT, '“fooo”'),
messages.COMMAND_SUGGESTION_SUFFIX,
].join('')

test('execute command', async () => {
await session1.shouldHaveReply('foo bar', 'foobar')
await session1.shouldHaveNoResponse(' ')
})

test('no suggestions found', async () => {
await session1.shouldHaveNoResponse('bar foo')
})

test('apply suggestions 1', async () => {
await session1.shouldHaveReply('fo bar', expectedSuggestionText)
await session2.shouldHaveReply('fooo -t bar')
await session1.shouldHaveReply(' ', 'foobar')
await session1.shouldHaveNoResponse(' ')
})

test('apply suggestions 2', async () => {
await session1.shouldHaveReply('foooo -t bar', expectedSuggestionText2)
await session2.shouldHaveReply('foo bar')
await session1.shouldHaveReply(' ', 'fooobar')
await session1.shouldHaveNoResponse(' ')
})

test('ignore suggestions 1', async () => {
await session1.shouldHaveReply('fo bar', expectedSuggestionText)
await session1.shouldHaveNoResponse('bar foo')
await session1.shouldHaveNoResponse(' ')
})

test('ignore suggestions 2', async () => {
await session1.shouldHaveReply('fo bar', expectedSuggestionText)
await session1.shouldHaveReply('foo bar')
await session1.shouldHaveNoResponse(' ')
})

test('multiple suggestions', async () => {
await session1.shouldHaveReply('fool bar', [
messages.COMMAND_SUGGESTION_PREFIX,
format(messages.SUGGESTION_TEXT, '“foo”或“fooo”'),
].join(''))
await session1.shouldHaveNoResponse(' ')
})
})
103 changes: 103 additions & 0 deletions packages/koishi-core/tests/suggestion.spec.ts
@@ -0,0 +1,103 @@
import { Session, createApp, registerMemoryDatabase } from 'koishi-test-utils'
import { messages, showSuggestions } from 'koishi-core'
import { format } from 'util'

registerMemoryDatabase()

describe('Command Suggestions', () => {
const app = createApp()
const session1 = new Session(app, 'user', 456)
const session2 = new Session(app, 'group', 789, 987)

app.command('foo <text>', { checkArgCount: true })
.action(({ meta }, bar) => {
return meta.$send('foo' + bar)
})

app.command('fooo', { checkUnknown: true, checkRequired: true })
.option('-t, --text <bar>')
.action(({ meta, options }) => {
return meta.$send('fooo' + options.text)
})

const expectedSuggestionText = [
messages.COMMAND_SUGGESTION_PREFIX,
format(messages.SUGGESTION_TEXT, '“foo”'),
messages.COMMAND_SUGGESTION_SUFFIX,
].join('')

const expectedSuggestionText2 = [
messages.COMMAND_SUGGESTION_PREFIX,
format(messages.SUGGESTION_TEXT, '“fooo”'),
messages.COMMAND_SUGGESTION_SUFFIX,
].join('')

test('execute command', async () => {
await session1.shouldHaveReply('foo bar', 'foobar')
await session1.shouldHaveNoResponse(' ')
})

test('no suggestions found', async () => {
await session1.shouldHaveNoResponse('bar foo')
})

test('apply suggestions 1', async () => {
await session1.shouldHaveReply('fo bar', expectedSuggestionText)
await session2.shouldHaveReply('fooo -t bar')
await session1.shouldHaveReply(' ', 'foobar')
await session1.shouldHaveNoResponse(' ')
})

test('apply suggestions 2', async () => {
await session2.shouldHaveReply('foooo -t bar', expectedSuggestionText2)
await session1.shouldHaveReply('foo bar')
await session2.shouldHaveReply(' ', 'fooobar')
await session2.shouldHaveNoResponse(' ')
})

test('ignore suggestions 1', async () => {
await session1.shouldHaveReply('fo bar', expectedSuggestionText)
await session1.shouldHaveNoResponse('bar foo')
await session1.shouldHaveNoResponse(' ')
})

test('ignore suggestions 2', async () => {
await session2.shouldHaveReply('fo bar', expectedSuggestionText)
await session2.shouldHaveReply('foo bar')
await session2.shouldHaveNoResponse(' ')
})

test('multiple suggestions', async () => {
await session1.shouldHaveReply('fool bar', [
messages.COMMAND_SUGGESTION_PREFIX,
format(messages.SUGGESTION_TEXT, '“foo”或“fooo”'),
].join(''))
await session1.shouldHaveNoResponse(' ')
})
})

describe('Custom Suggestions', () => {
const app = createApp({ database: { memory: {} } })
const session = new Session(app, 'group', 123, 456)
const command = app.command('echo [message]', { authority: 0 })
.action(({ meta }, message) => meta.$send('text:' + message))

app.middleware((meta, next) => showSuggestions({
target: meta.message,
items: ['foo', 'bar'],
meta,
next,
prefix: 'prefix',
suffix: 'suffix',
command,
execute: (suggestion, meta) => command.execute({ args: [suggestion], meta }),
}))

beforeEach(() => app.database.getGroup(456, 514))

test('show suggestions', async () => {
await session.shouldHaveNoResponse(' ')
await session.shouldHaveReply('for', `prefix${format(messages.SUGGESTION_TEXT, '“foo”')}suffix`)
await session.shouldHaveReply(' ', 'text:foo')
})
})
9 changes: 8 additions & 1 deletion packages/test-utils/src/utils.ts
@@ -1,9 +1,16 @@
import { SenderInfo, PostType, MetaTypeMap, SubTypeMap, Meta } from 'koishi-core'
import { SenderInfo, PostType, MetaTypeMap, SubTypeMap, Meta, AppOptions, App } from 'koishi-core'
import { camelCase } from 'koishi-utils'
import debug from 'debug'

export const showTestLog = debug('koishi:test')

export function createApp (options: AppOptions = {}) {
const app = new App({ selfId: 514, ...options })
beforeAll(() => app.start())
afterAll(() => app.stop())
return app
}

export function createMeta <T extends PostType> (postType: T, type: MetaTypeMap[T], subType: SubTypeMap[T], meta: Meta<T> = {}) {
meta.postType = postType
meta[camelCase(postType) + 'Type'] = type
Expand Down

0 comments on commit 8253522

Please sign in to comment.