settle-approval.spec.ts 3.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. import { afterAll, describe, expect, it } from 'vitest'
  2. import * as fs from 'node:fs'
  3. import * as path from 'node:path'
  4. import * as os from 'node:os'
  5. import { createNovelTools, type NovelToolsDeps } from '../src/novel-tools'
  6. import { APPROVE_LABEL, REJECT_LABEL, type AskRequest, type AskResponse } from '@webnovel/core'
  7. const roots: string[] = []
  8. function mkWs(): string {
  9. const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'webnovel-settle-'))
  10. roots.push(dir)
  11. return dir
  12. }
  13. afterAll(() => { for (const r of roots) { try { fs.rmSync(r, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }) } catch { /* Windows 句柄延迟释放 */ } } })
  14. const AGENT = { agent: { id: 'agent-1', session: { append: () => {} } } }
  15. const CONCEPT = {
  16. 状态: '已确认',
  17. 核心创意: 'x', 题材与目标读者: 'x', 主角核心欲望: 'x', 主要冲突: 'x',
  18. 核心看点: 'x', 差异化方向: 'x', 明确不要什么: 'x',
  19. }
  20. async function setup(ask?: NovelToolsDeps['askFn']) {
  21. const ws = mkWs()
  22. const tools = createNovelTools({
  23. workspaceRoot: () => ws,
  24. bookRootOfBookId: () => path.join(ws, '审批书'),
  25. askFn: ask,
  26. })
  27. const call = async (name: string, args: Record<string, unknown>) => {
  28. const tool = tools.find((t) => t.name === name)!
  29. return (await tool.execute(args, AGENT)) as Record<string, unknown> & { ok: boolean }
  30. }
  31. const created = await call('novel_create_book', { bookName: '审批书', concept: CONCEPT }) as { bookId: string }
  32. return { call, bookId: created.bookId }
  33. }
  34. const KEY = { 卷: 1, 章: 1, 章名: '第一章', summary: 'x' }
  35. describe('定稿沉淀:作者审批不可由模型自证(裁决 11/14)', () => {
  36. it('未注入 askFn:拒绝沉淀,不落任何文件', async () => {
  37. const { call, bookId } = await setup(undefined)
  38. const r = await call('novel_settle_chapter', { bookId, ...KEY })
  39. expect(r.ok).toBe(false)
  40. expect(String(r.reason)).toContain('裁决通道')
  41. })
  42. it('作者驳回:拒绝沉淀', async () => {
  43. const { call, bookId } = await setup(async () => ({ answers: [{ id: '定稿入档', selected: [REJECT_LABEL] }] }))
  44. const r = await call('novel_settle_chapter', { bookId, ...KEY })
  45. expect(r.ok).toBe(false)
  46. expect(String(r.reason)).toContain('未获作者批准')
  47. })
  48. it('作者批准:进入沉淀(无定稿包则由 archiveChapter fail-closed)', async () => {
  49. let seen: AskRequest | undefined
  50. const { call, bookId } = await setup(async (req): Promise<AskResponse> => {
  51. seen = req
  52. return { answers: [{ id: '定稿入档', selected: [APPROVE_LABEL] }] }
  53. })
  54. const r = await call('novel_settle_chapter', { bookId, ...KEY })
  55. expect(seen, 'askFn 应被调用').toBeDefined()
  56. expect(seen!.questions[0]?.id).toBe('定稿入档')
  57. expect(r.ok).toBe(false)
  58. expect(String(r.reason)).not.toContain('裁决通道')
  59. })
  60. it('模型无法用参数绕过:裁决记录 已不是入参', async () => {
  61. const { call, bookId } = await setup(undefined)
  62. const tools = createNovelTools({ workspaceRoot: () => '', bookRootOfBookId: () => undefined })
  63. const settle = tools.find((t) => t.name === 'novel_settle_chapter')!
  64. const required = (settle.parameters as { required: string[] }).required
  65. expect(required).not.toContain('裁决记录')
  66. const r = await call('novel_settle_chapter', { bookId, ...KEY, 裁决记录: '作者说可以' })
  67. expect(r.ok).toBe(false)
  68. })
  69. })