| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162 |
- import { afterAll, describe, expect, it } from 'vitest'
- import * as fs from 'node:fs'
- import * as path from 'node:path'
- import * as os from 'node:os'
- import { spawnSync } from 'node:child_process'
- import { createNovelTools } from '../src/novel-tools'
- import { createBook } from '@webnovel/core'
- import { nativeWriteStub } from './fixtures/native-write-stub'
- const roots: string[] = []
- function mkRoot(): string {
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'webnovel-tools-'))
- roots.push(dir)
- return dir
- }
- afterAll(() => { for (const r of roots) { try { fs.rmSync(r, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }) } catch { /* Windows 句柄延迟释放,%TEMP% 残留无害 */ } } })
- describe('主 Agent 专用 Tools (方案 A)', () => {
- it('novel_create_book & novel_select_book & novel_seed_min_design & novel_update_contract & novel_get_story_status 闭环', async () => {
- const ws = mkRoot()
- const tools = createNovelTools({
- nativeWrite: nativeWriteStub,
- workspaceRoot: () => ws,
- bookRootOfBookId: (id) => path.join(ws, '仙途长生'),
- })
- const createTool = tools.find((t) => t.name === 'novel_create_book')!
- const selectTool = tools.find((t) => t.name === 'novel_select_book')!
- const seedTool = tools.find((t) => t.name === 'novel_seed_min_design')!
- const contractTool = tools.find((t) => t.name === 'novel_update_contract')!
- const statusTool = tools.find((t) => t.name === 'novel_get_story_status')!
- // 1. 建书
- const createRes = (await createTool.execute({
- bookName: '仙途长生',
- concept: {
- 状态: '已确认',
- 核心创意: '凡人流修仙与上古残魂',
- 题材与目标读者: '古典仙侠',
- 主角核心欲望: '求长生与道心坚定',
- 主要冲突: '宗门倾轧与魔修劫难',
- 核心看点: '稳健发育与步步为营',
- 差异化方向: '无系统纯智商博弈',
- 明确不要什么: '无脑倒贴与战力崩塌',
- },
- }, { agent: { id: 'agent-1', session: { append: () => {} } } })) as { ok: boolean; bookId: string }
- expect(createRes.ok).toBe(true)
- expect(createRes.bookId).toMatch(/^b-/)
- // 2. 更新契约(题材与读者定位)
- const contractRes = (await contractTool.execute({
- bookId: createRes.bookId,
- partName: '题材与读者定位',
- state: '已确认',
- content: '练气九层、筑基三阶、金丹九品。',
- })) as { ok: boolean }
- expect(contractRes.ok).toBe(true)
- // 验证契约内容与书id未丢失
- const contractContent = fs.readFileSync(path.join(ws, '仙途长生', '作品契约/契约.md'), 'utf-8')
- expect(contractContent).toContain(`书id: ${createRes.bookId}`)
- expect(contractContent).toContain('练气九层、筑基三阶、金丹九品。')
- // 3. 最小设计
- const seedRes = (await seedTool.execute({ bookId: createRes.bookId })) as { ok: boolean }
- expect(seedRes.ok).toBe(true)
- // 4. 读取真源状态
- const statusRes = (await statusTool.execute({ bookId: createRes.bookId })) as { ok: boolean; chapters: any[] }
- expect(statusRes.ok).toBe(true)
- expect(statusRes.chapters.length).toBeGreaterThan(0)
- })
- it('novel_prepare_pack & novel_settle_chapter 守卫:无待审稿报错、无裁决通道拒绝沉淀', async () => {
- const ws = mkRoot()
- const tools = createNovelTools({
- nativeWrite: nativeWriteStub,
- workspaceRoot: () => ws,
- bookRootOfBookId: () => path.join(ws, '守卫书'),
- })
- const createTool = tools.find((t) => t.name === 'novel_create_book')!
- const prepareTool = tools.find((t) => t.name === 'novel_prepare_pack')!
- const settleTool = tools.find((t) => t.name === 'novel_settle_chapter')!
- const createRes = (await createTool.execute({
- bookName: '守卫书',
- concept: {
- 状态: '已确认',
- 核心创意: 'x', 题材与目标读者: 'x', 主角核心欲望: 'x', 主要冲突: 'x',
- 核心看点: 'x', 差异化方向: 'x', 明确不要什么: 'x',
- },
- }, { agent: { id: 'agent-1', session: { append: () => {} } } })) as { ok: boolean; bookId: string }
- // 无待审稿:组装定稿包失败
- const prepareRes = (await prepareTool.execute({ bookId: createRes.bookId, 卷: 1, 章: 1, 章名: '第一章' })) as { ok: boolean; reason?: string }
- expect(prepareRes.ok).toBe(false)
- expect(prepareRes.reason).toContain('待审稿')
- // 未注入裁决通道:拒绝沉淀(裁决 14:审批不可由模型自证,详见 settle-approval.spec.ts)
- const noChannel = (await settleTool.execute({ bookId: createRes.bookId, 卷: 1, 章: 1, 章名: '第一章', summary: 'x' })) as { ok: boolean; reason?: string }
- expect(noChannel.ok).toBe(false)
- expect(noChannel.reason).toContain('裁决通道')
- })
- it('提交点跟随作者确认(拍板 1/2/7):契约已确认→design: 提交;暂定→不提交;seed→整批一次提交且重跑幂等', async () => {
- const ws = mkRoot()
- const tools = createNovelTools({
- nativeWrite: nativeWriteStub,
- workspaceRoot: () => ws,
- bookRootOfBookId: (id) => path.join(ws, '提交纪律书'),
- })
- const createTool = tools.find((t) => t.name === 'novel_create_book')!
- const seedTool = tools.find((t) => t.name === 'novel_seed_min_design')!
- const contractTool = tools.find((t) => t.name === 'novel_update_contract')!
- const createRes = (await createTool.execute({
- bookName: '提交纪律书',
- concept: {
- 状态: '已确认',
- 核心创意: 'x', 题材与目标读者: 'x', 主角核心欲望: 'x', 主要冲突: 'x',
- 核心看点: 'x', 差异化方向: 'x', 明确不要什么: 'x',
- },
- }, { agent: { id: 'agent-1', session: { append: () => {} } } })) as { ok: boolean; bookId: string }
- const git = (args: string[]) => spawnSync('git', ['-c', 'core.quotepath=false', ...args], { cwd: path.join(ws, '提交纪律书'), encoding: 'utf-8' })
- const count = () => git(['rev-list', '--count', 'HEAD']).stdout?.trim()
- // 暂定态:只写文件,不产生提交
- const tentative = (await contractTool.execute({
- bookId: createRes.bookId,
- partName: '题材与读者定位', state: '暂定', content: '暂定内容',
- })) as { ok: boolean }
- expect(tentative.ok).toBe(true)
- const afterTentative = count()
- expect(git(['log', '-1', '--pretty=%s', '--', '作品契约/契约.md']).stdout?.trim()).not.toMatch(/^design:/)
- // 已确认:产生 design: 提交
- const confirmed = (await contractTool.execute({
- bookId: createRes.bookId,
- partName: '题材与读者定位', state: '已确认', content: '练气九层、筑基三阶。',
- })) as { ok: boolean }
- expect(confirmed.ok).toBe(true)
- const confirmedLog = git(['log', '-1', '--pretty=%s', '--', '作品契约/契约.md']).stdout?.trim()
- expect(confirmedLog).toMatch(/^design: /)
- expect(Number(count())).toBe(Number(afterTentative) + 1)
- // seed:整批一次 design: 提交
- const beforeSeed = count()
- const seedRes = (await seedTool.execute({ bookId: createRes.bookId })) as { ok: boolean }
- expect(seedRes.ok).toBe(true)
- expect(Number(count())).toBe(Number(beforeSeed) + 1)
- const seedShow = git(['show', '--name-only', '--pretty=format:', 'HEAD']).stdout?.split('\n').map((l) => l.trim()).filter((l) => l !== '')
- expect(seedShow?.length).toBeGreaterThan(1)
- expect(seedShow).toContain('大纲/故事骨架.md')
- // 重跑 seed:幂等,无新提交
- const seedReplay = (await seedTool.execute({ bookId: createRes.bookId })) as { ok: boolean }
- expect(seedReplay.ok).toBe(true)
- expect(Number(count())).toBe(Number(beforeSeed) + 1)
- })
- })
|