Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,15 +390,24 @@ export function apply(ctx: Ctx, rawConfig?: unknown): () => void {
const commands = sctx.commands
if (commands === undefined) return
disposers.push(commands.register({
// 命令定义的身份:DSH 用它配对注册与卸载(运行时就是普通字符串)
definitionId: 'dsh-code-review/review',
name: 'review',
description: '发起一次代码评审(等于 code_review 工具)',
input: 'repo_path [mode] [focus]',
// 契约:input 若提供,必须是 { hint: string }。
// 传字符串会让 register 直接抛 TypeError,而异常会被下面的 try/catch 吞掉——
// 表现就是「斜杠命令死活不出现、日志里也没有任何东西」。实机踩过。
input: { hint: 'repo_path [mode] [focus]' },
handler: async (invocation: { agent?: { session?: { id?: string; header?: { cwd?: string } } } }) => {
const raw = invocation?.agent?.session
const cwd = raw?.header?.cwd
const sid = typeof raw?.id === 'string' ? raw.id : null
const r = await doSubmit({ repo_path: cwd }, ctx, cwd, sid, config)
return r.text
// 契约:handler 必须返回 { kind: 'success' | 'error', text? }。
// 返回纯字符串会被判为非法结果,命令看起来「执行了但什么也没发生」。
return r.ok
? { kind: 'success' as const, text: r.text }
: { kind: 'error' as const, text: r.text === '' ? '评审未提交' : r.text }
},
}))
} catch (error) {
Expand Down
36 changes: 34 additions & 2 deletions test/plugin/apply.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ const ABS_REPO = process.platform === 'win32' ? 'D:/some/repo' : '/tmp/some/repo
/** 模拟 Cordis 上下文,记录注册了什么。 */
function makeCtx() {
const tools: Array<{ name: string; execute?: (...args: unknown[]) => Promise<unknown> }> = []
const commands: Array<{ name: string }> = []
// 保留完整定义:斜杠命令的契约(input 形状、handler 返回值)必须能被断言
const commands: Array<Record<string, any>> = []
const warnings: string[] = []
// 宿主的 configEditor(假):0.2.0 起配置由它写入,所以挂载测试必须提供它,
// 否则 code_review_set_config 只能诚实地回「profile 不可写」。
Expand All @@ -74,7 +75,7 @@ function makeCtx() {
},
commands: {
register: (cmd: unknown) => {
commands.push(cmd as { name: string })
commands.push(cmd as Record<string, any>)
return () => { /* dispose */ }
},
},
Expand Down Expand Up @@ -202,3 +203,34 @@ describe('工具可调用', () => {
dispose()
})
})

describe('斜杠命令契约', () => {
it('定义带 definitionId 且 input 是 { hint }(不是字符串)', () => {
// 回归:曾经把 input 写成字符串 'repo_path [mode]',宿主 normalizeDefinition 直接抛
// TypeError,被 try/catch 吞掉 —— 表现为「/review 死活不出现,日志里什么都没有」。
const { ctx, commands } = makeCtx()
const dispose = apply(ctx as never)
const cmd = commands.find((c) => c.name === 'review')!
expect(cmd.definitionId).toBe('dsh-code-review/review')
expect(typeof cmd.input).toBe('object')
expect(cmd.input).not.toBeNull()
expect(typeof cmd.input.hint).toBe('string')
expect(cmd.input.hint.trim().length).toBeGreaterThan(0)
dispose()
})

it('handler 返回 CommandResult({ kind, text }),不是裸字符串', async () => {
// 回归:宿主 normalizeResult 要求 kind 字段,返回字符串同样会被判非法。
const { ctx, commands } = makeCtx()
const dispose = apply(ctx as never)
const cmd = commands.find((c) => c.name === 'review')!
const out = (await cmd.handler({ agent: { session: { id: 's1', header: { cwd: ABS_REPO } } } })) as {
kind?: string
text?: string
}
expect(typeof out).toBe('object')
expect(['success', 'error']).toContain(out.kind)
expect(typeof out.text).toBe('string')
dispose()
})
})
Loading