| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123 |
- 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 {
- assembleMaterials,
- confirmOutline,
- deriveChapterFacts,
- scanChapter,
- seedMinDesign,
- writeCandidate,
- } from '@webnovel/core'
- import { loadDraftContext, writeDraft } from '../src/index'
- const roots: string[] = []
- function mkBook(): string {
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'webnovel-draft-'))
- roots.push(dir)
- seedMinDesign(dir)
- return dir
- }
- afterAll(() => { for (const r of roots) { try { fs.rmSync(r, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }) } catch { /* Windows 句柄延迟释放,%TEMP% 残留无害 */ } } })
- const key = { 卷: 1, 章: 1, 章名: '开篇任务' } as const
- const refs = ['作品契约/契约.md@1', '世界书/人物档案/主角.md@1'] as const
- function confirmReady(root: string): void {
- const body = [
- '# 章细纲', '', '## 定位段', '',
- '### 来源窗口项及拆并关系', '开篇任务,单章承接', '',
- '### 章节功能', '建立开场冲突', '- 〔软〕节奏平稳', '',
- '### 视角与焦点', '主角视角', '',
- '### 时空锚定', '城门口,清晨', '',
- '### 起止边界', '从抵达城门到发现异状', '',
- '### 故事线与承诺分配', '推进主线承诺', '',
- '### 信息边界', '只披露主角所见', '',
- '### 情绪与节奏目标', '紧张后留钩子', '',
- '### 前置条件核对结果', '已核对来源与窗口,前置设定均非留白', '',
- '## 细纲段', '', '### 单元 1', '',
- '- 目标: 找到异常', '- 人物: 主角', '- 时空: 城门口',
- '- 行动/冲突: 盘问与发现', '- 信息披露: 异常线索', '- 状态变化: 从平静到警觉', '',
- ].join('\n')
- writeCandidate(root, { 卷: 1, 章: 1, 章名: '开篇任务', 来源引用: refs, body })
- const r = confirmOutline(root, key)
- if (!r.ok) throw new Error(`confirm failed:${r.gaps.join(';')}`)
- }
- describe('单稿写作(插件规格 §5 草稿协议)', () => {
- it('R1 播报:细纲未确认/材料包未组装不拒绝写稿,播报随结果返回', () => {
- const root = mkBook()
- const r = writeDraft(root, { ...key, body: '他走进雨里。' })
- expect(r.ok).toBe(true)
- expect(r.播报).toContain('细纲未确认')
- expect(r.播报).toContain('材料包未组装')
- expect(fs.existsSync(path.join(root, '草稿区/草稿/卷01-开篇任务/稿1.md'))).toBe(true)
- })
- it('after assemble + writeDraft → derive 润色 (有草稿, no 待审稿)', () => {
- const root = mkBook()
- confirmReady(root)
- const assembled = assembleMaterials(root, key)
- expect(assembled.ok).toBe(true)
- const r = writeDraft(root, { ...key, body: '他走进雨里。', 选定: true })
- expect(r.ok).toBe(true)
- expect(r.relPath).toBe('草稿区/草稿/卷01-开篇任务/稿1.md')
- const text = fs.readFileSync(path.join(root, r.relPath), 'utf-8')
- expect(text).not.toMatch(/角色:\s*待审稿/)
- expect(text).toMatch(/选定:\s*true/)
- const facts = scanChapter(root, key)
- expect(facts.有草稿).toBe(true)
- expect(facts.唯一待审稿).toBe(false)
- expect(deriveChapterFacts(facts).建议.环节).toBe('润色')
- })
- it('context loader does not include 审核 files', () => {
- const root = mkBook()
- confirmReady(root)
- assembleMaterials(root, key)
- fs.mkdirSync(path.join(root, '草稿区/审核'), { recursive: true })
- fs.writeFileSync(path.join(root, '草稿区/审核/卷01-开篇任务.json'), '{"schemaVersion":1,"完成":false,"问题":[]}', 'utf-8')
- const ctx = loadDraftContext(root, key)
- expect(ctx.细纲).toMatch(/章细纲/)
- expect(Object.keys(ctx.材料段).some((k) => k.includes('本章任务'))).toBe(true)
- const dumped = JSON.stringify(ctx)
- expect(dumped).not.toContain('草稿区/审核')
- expect(dumped).not.toContain('卷01-开篇任务.json')
- expect(dumped).not.toMatch(/"完成"\s*:/)
- })
- it('候选事实 stays in workspace draft only', () => {
- const root = mkBook()
- confirmReady(root)
- assembleMaterials(root, key)
- const r = writeDraft(root, {
- ...key,
- body: '他走进雨里。\n\n## 草稿候选事实\n\n- 巷口多了一盏灯',
- })
- expect(r.ok).toBe(true)
- const draft = fs.readFileSync(path.join(root, r.relPath), 'utf-8')
- expect(draft).toContain('巷口多了一盏灯')
- expect(fs.existsSync(path.join(root, '世界书'))).toBe(true)
- const world = fs.readFileSync(path.join(root, '世界书/人物档案/主角.md'), 'utf-8')
- expect(world).not.toContain('巷口多了一盏灯')
- expect(fs.existsSync(path.join(root, '大纲/卷规划/卷01/章细纲/0001-开篇任务.md'))).toBe(true)
- const outline = fs.readFileSync(path.join(root, '大纲/卷规划/卷01/章细纲/0001-开篇任务.md'), 'utf-8')
- expect(outline).not.toContain('巷口多了一盏灯')
- })
- it('「草稿候选事实补充」这类同前缀标题不被当成候选事实分隔线', () => {
- const root = mkBook()
- confirmReady(root)
- assembleMaterials(root, key)
- const r = writeDraft(root, {
- ...key,
- body: '他走进雨里。\n\n## 草稿候选事实补充说明\n\n这段是正文的一部分。',
- })
- expect(r.ok).toBe(true)
- const draft = fs.readFileSync(path.join(root, r.relPath), 'utf-8')
- expect(draft).toContain('## 草稿候选事实补充说明')
- expect(draft).toContain('这段是正文的一部分。')
- expect(draft).not.toMatch(/^- 这段是正文的一部分。$/m)
- })
- })
|