Skip to content

Commit

Permalink
feat(mock): enhance prompt testing
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Oct 2, 2023
1 parent d63a3d4 commit 3f98c38
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 81 deletions.
8 changes: 4 additions & 4 deletions packages/core/src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ defineProperty(Adapter, 'filter', false)
Bot.prototype.getGuildMemberMap = async function getGuildMemberMap(this: Bot, guildId) {
const result: Dict<string> = {}
for await (const member of this.getGuildMemberIter(guildId)) {
result[member.user.id] = member.nickname || member.user.nick || member.user.name
result[member.user.id] = member.name || member.user.name
}
return result
}

Bot.prototype.broadcast = async function broadcast(this: Bot, channels, content, delay = this.ctx.root.config.delay.broadcast) {
const messageIds: string[] = []
const ids: string[] = []
for (let index = 0; index < channels.length; index++) {
if (index && delay) await sleep(delay)
try {
const value = channels[index]
messageIds.push(...typeof value === 'string'
ids.push(...typeof value === 'string'
? await this.sendMessage(value, content)
: Array.isArray(value)
? await this.sendMessage(value[0], content, value[1])
Expand All @@ -41,5 +41,5 @@ Bot.prototype.broadcast = async function broadcast(this: Bot, channels, content,
this.ctx.logger('bot').warn(error)
}
}
return messageIds
return ids
}
4 changes: 2 additions & 2 deletions packages/core/src/command/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ export class Commander extends Map<string, Command> {
})

ctx.on('interaction/command', (session) => {
if (session.body?.argv) {
const { name, options, arguments: args } = session.body.argv
if (session.event?.argv) {
const { name, options, arguments: args } = session.event.argv
session.execute({ name, args, options })
} else {
defineProperty(session, 'argv', ctx.bail('before-parse', session.content, session))
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class Processor {
defineProperty(this, Context.current, ctx)

// bind built-in event listeners
this.middleware(this._process.bind(this), true)
this.middleware(this.attach.bind(this), true)
ctx.on('message', this._handleMessage.bind(this))

ctx.before('attach-user', (session, fields) => {
Expand Down Expand Up @@ -203,7 +203,7 @@ export class Processor {
}
}

private async _process(session: Session, next: Next) {
private async attach(session: Session, next: Next) {
this.ctx.emit(session, 'before-attach', session)

if (this.ctx.database) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ extend(Session.prototype as Session.Private, {
get stripped() {
const self = this as Session.Private
if (self._stripped) return self._stripped
if (!self.elements) return {} as Stripped

// strip mentions
let atSelf = false, appel = false
Expand Down
76 changes: 42 additions & 34 deletions packages/core/tests/filter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,57 +8,65 @@ app.plugin(Bot, {
selfId: '514',
})

const guildSession = app.bots[0].session({ userId: '123', guildId: '456', subtype: 'group' })
const privateSession = app.bots[0].session({ userId: '123', subtype: 'private' })
const session1 = app.bots[0].session()
session1.userId = '123'
session1.channelId = '456'
session1.guildId = '456'
session1.isDirect = false

const session2 = app.bots[0].session()
session2.userId = '123'
session2.channelId = '123'
session2.isDirect = true

describe('Selector API', () => {
it('root context', () => {
expect(app.filter(guildSession)).to.be.true
expect(app.filter(privateSession)).to.be.true
expect(app.filter(session1)).to.be.true
expect(app.filter(session2)).to.be.true
})

it('context.prototype.user', () => {
expect(app.user().filter(guildSession)).to.be.true
expect(app.user().filter(privateSession)).to.be.true
expect(app.user('123').filter(guildSession)).to.be.true
expect(app.user('123').filter(privateSession)).to.be.true
expect(app.user('456').filter(guildSession)).to.be.false
expect(app.user('456').filter(privateSession)).to.be.false
expect(app.user().filter(session1)).to.be.true
expect(app.user().filter(session2)).to.be.true
expect(app.user('123').filter(session1)).to.be.true
expect(app.user('123').filter(session2)).to.be.true
expect(app.user('456').filter(session1)).to.be.false
expect(app.user('456').filter(session2)).to.be.false
})

it('context.prototype.private', () => {
expect(app.private().filter(guildSession)).to.be.false
expect(app.private().filter(privateSession)).to.be.true
expect(app.private().user('123').filter(guildSession)).to.be.false
expect(app.private().user('123').filter(privateSession)).to.be.true
expect(app.private().user('456').filter(guildSession)).to.be.false
expect(app.private().user('456').filter(privateSession)).to.be.false
expect(app.private().filter(session1)).to.be.false
expect(app.private().filter(session2)).to.be.true
expect(app.private().user('123').filter(session1)).to.be.false
expect(app.private().user('123').filter(session2)).to.be.true
expect(app.private().user('456').filter(session1)).to.be.false
expect(app.private().user('456').filter(session2)).to.be.false
})

it('context.prototype.guild', () => {
expect(app.guild().filter(guildSession)).to.be.true
expect(app.guild().filter(privateSession)).to.be.false
expect(app.guild('123').filter(guildSession)).to.be.false
expect(app.guild('123').filter(privateSession)).to.be.false
expect(app.guild('456').filter(guildSession)).to.be.true
expect(app.guild('456').filter(privateSession)).to.be.false
expect(app.guild().filter(session1)).to.be.true
expect(app.guild().filter(session2)).to.be.false
expect(app.guild('123').filter(session1)).to.be.false
expect(app.guild('123').filter(session2)).to.be.false
expect(app.guild('456').filter(session1)).to.be.true
expect(app.guild('456').filter(session2)).to.be.false
})

it('context chaining', () => {
expect(app.guild('456').user('123').filter(guildSession)).to.be.true
expect(app.guild('456').user('456').filter(guildSession)).to.be.false
expect(app.guild('123').user('123').filter(guildSession)).to.be.false
expect(app.user('123').guild('456').filter(guildSession)).to.be.true
expect(app.user('456').guild('456').filter(guildSession)).to.be.false
expect(app.user('123').guild('123').filter(guildSession)).to.be.false
expect(app.guild('456').user('123').filter(session1)).to.be.true
expect(app.guild('456').user('456').filter(session1)).to.be.false
expect(app.guild('123').user('123').filter(session1)).to.be.false
expect(app.user('123').guild('456').filter(session1)).to.be.true
expect(app.user('456').guild('456').filter(session1)).to.be.false
expect(app.user('123').guild('123').filter(session1)).to.be.false
})

it('context intersection', () => {
expect(app.guild('456', '789').guild('123', '456').filter(guildSession)).to.be.true
expect(app.guild('456', '789').guild('123', '789').filter(guildSession)).to.be.false
expect(app.guild('123', '789').guild('123', '456').filter(guildSession)).to.be.false
expect(app.user('123', '789').user('123', '456').filter(guildSession)).to.be.true
expect(app.user('456', '789').user('123', '456').filter(guildSession)).to.be.false
expect(app.user('123', '789').user('456', '789').filter(guildSession)).to.be.false
expect(app.guild('456', '789').guild('123', '456').filter(session1)).to.be.true
expect(app.guild('456', '789').guild('123', '789').filter(session1)).to.be.false
expect(app.guild('123', '789').guild('123', '456').filter(session1)).to.be.false
expect(app.user('123', '789').user('123', '456').filter(session1)).to.be.true
expect(app.user('456', '789').user('123', '456').filter(session1)).to.be.false
expect(app.user('123', '789').user('456', '789').filter(session1)).to.be.false
})
})
12 changes: 6 additions & 6 deletions packages/loader/tests/loader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,15 @@ describe('@koishijs/loader', () => {
expect(bar.mock.calls).to.have.length(0)
expect(baz.mock.calls).to.have.length(0)

let { body } = app.mock.client('123', '456')
app.emit(app.mock.session(body), 'test/bar' as any)
app.emit(app.mock.session(body), 'test/baz' as any)
let { event } = app.mock.client('123', '456')
app.emit(app.mock.session(event), 'test/bar' as any)
app.emit(app.mock.session(event), 'test/baz' as any)
expect(bar.mock.calls).to.have.length(0)
expect(baz.mock.calls).to.have.length(1)

body = app.mock.client('321', '456').body
app.emit(app.mock.session(body), 'test/bar' as any)
app.emit(app.mock.session(body), 'test/baz' as any)
event = app.mock.client('321', '456').event
app.emit(app.mock.session(event), 'test/bar' as any)
app.emit(app.mock.session(event), 'test/baz' as any)
expect(bar.mock.calls).to.have.length(0)
expect(baz.mock.calls).to.have.length(1)
})
Expand Down
19 changes: 12 additions & 7 deletions plugins/mock/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@ export class MockBot extends Bot {
return new MessageClient(this, userId, channelId)
}

receive(client: MessageClient, body: Partial<Universal.Event>) {
const session = this.session(body)
session.send = function (this: Session, fragment, options = {}) {
receive(event: Partial<Universal.Event>, client?: MessageClient) {
const session = this.session(event)
session.send = async function (this: Session, fragment, options = {}) {
options.session = this
return new MockMessenger(client, options).send(fragment)
const messages = await new MockMessenger(client, options).send(fragment)
return messages.map(messages => messages.id)
}
this.dispatch(session)
return session.id
Expand Down Expand Up @@ -76,11 +77,15 @@ export class MockAdapter extends Adapter<MockBot> {
}

client(userId: string, channelId?: string) {
return new MessageClient(this.bots[0], userId, channelId)
return this.bots[0].client(userId, channelId)
}

session(meta: Partial<Universal.Event>) {
return this.bots[0].session(meta)
receive(event: Partial<Universal.Event>, client?: MessageClient) {
return this.bots[0].receive(event, client)
}

session(event: Partial<Universal.Event>) {
return this.bots[0].session(event)
}
}

Expand Down
77 changes: 51 additions & 26 deletions plugins/mock/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from 'assert'
import { Context, h, hyphenate, isNullable, Messenger, Universal } from 'koishi'
import { clone, Context, Dict, h, hyphenate, isNullable, Messenger, Universal } from 'koishi'
import { format } from 'util'
import { MockBot } from './adapter'

Expand All @@ -13,14 +13,13 @@ export class MockMessenger extends Messenger {
private buffer = ''

constructor(private client: MessageClient, options?: Universal.SendOptions) {
super(client.bot, client.body.channel.id, client.body.guild?.id, options)
super(client.bot, client.event.channel.id, client.event.guild?.id, options)
}

async flush() {
this.buffer = this.buffer.trim()
if (!this.buffer) return
this.client.replies.push(this.buffer)
this.client.resolve(true)
this.client.flush(this.buffer)
this.buffer = ''
}

Expand Down Expand Up @@ -60,52 +59,78 @@ export class MockMessenger extends Messenger {
}
}

interface Hook {
count: number
done?: boolean
resolve?: (replies: string[]) => void
}

export class MessageClient {
public app: Context
public body: Partial<Universal.Event>
public resolve: (checkLength?: boolean) => void = () => {}
public replies: string[] = []
public event: Universal.Event

private replies: string[] = []
private hooks: Dict<Hook> = {}

constructor(public bot: MockBot, public userId: string, public channelId?: string) {
this.app = bot.ctx.root
this.body = {
this.event = {
platform: 'mock',
type: 'message',
selfId: bot.selfId,
user: { id: userId, name: '' + userId },
}
} as Universal.Event

if (channelId) {
this.body.guild = { id: channelId }
this.body.channel = { id: channelId, type: Universal.Channel.Type.TEXT }
this.event.guild = { id: channelId }
this.event.channel = { id: channelId, type: Universal.Channel.Type.TEXT }
} else {
this.body.channel = { id: 'private:' + userId, type: Universal.Channel.Type.DIRECT }
this.event.channel = { id: 'private:' + userId, type: Universal.Channel.Type.DIRECT }
}

this.app.on('middleware', (session) => {
const hook = this.hooks[session.id]
if (!hook) return
hook.done = true
if (!hook.resolve) delete this.hooks[session.id]
if (Object.values(this.hooks).every(hook => hook.done)) {
this.flush()
this.hooks = {}
}
})
}

flush(buffer?: string) {
if (buffer) this.replies.push(buffer)
for (const id in this.hooks) {
const hook = this.hooks[id]
if (!hook.resolve || buffer && this.replies.length < hook.count) continue
hook.resolve(this.replies)
hook.resolve = undefined
hook.count = Infinity
this.replies = []
}
}

async receive(content: string, count = Infinity) {
return new Promise<string[]>((resolve) => {
let resolved = false
this.resolve = (checkLength = false) => {
if (resolved) return
if (checkLength && this.replies.length < count) return
resolved = true
dispose()
resolve(this.replies)
this.replies = []
}
const dispose = this.app.on('middleware', (session) => {
if (session.id === uuid) process.nextTick(this.resolve)
})
const result = await new Promise<string[]>((resolve) => {
let quote: Universal.Message
const elements = h.parse(content)
if (elements[0]?.type === 'quote') {
const { attrs, children } = elements.shift()
quote = { id: attrs.id, messageId: attrs.id, elements: children, content: children.join('') }
content = elements.join('')
}
const uuid = this.bot.receive(this, { ...this.body, message: { content, elements, quote } })
const id = this.bot.receive({
...clone(this.event),
message: { content, elements, quote },
}, this)
this.hooks[id] = { resolve, count }
})
// Await for next tick to ensure subsequent operations are executed.
// Do not use `setTimeout` because it may break tests with mocked timers.
await new Promise(process.nextTick)
return result
}

async shouldReply(message: string, reply?: string | RegExp | (string | RegExp)[]) {
Expand Down

0 comments on commit 3f98c38

Please sign in to comment.