| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- // 第三轮 review 修复后探针:与 07-05 probe-m1-m7.mjs / probe-followup.mjs 同场景,
- // 断言修复后语义(守卫拒绝/修复接受/角色可见/别名命中/ok 不改写/重号硬错)。
- // 库级直调真实路径(persistCreateBook 建书)。跑完自删临时目录,全 PASS 即收口留档。
- import { promises as fs } from 'node:fs'
- import os from 'node:os'
- import path from 'node:path'
- import { fileURLToPath } from 'node:url'
- const V7 = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'v7')
- const m = (p) => import(`file:///${path.join(V7, p).replace(/\\/g, '/')}`)
- const { persistCreateBook, persistRepair } = await m('src/state-machine/persist.js')
- const { finalizeChapter } = await m('src/finalize/index.js')
- const { stageChapter, readBatch } = await m('src/staging/index.js')
- const { gotoChapter } = await m('src/state-machine/flows/goto-chapter.js')
- const { determineNextState } = await m('src/state-machine/index.js')
- const { CacheManager } = await m('src/cache/index.js')
- const { EntityReader } = await m('src/storage/adapters/EntityReader.js')
- const { migrateV6 } = await m('src/migrate/index.js')
- const { run: listCharacters } = await m('src/commands/list-characters.js')
- const { run: finalizeCmd } = await m('src/commands/finalize.js')
- const { createGit } = await m('src/finalize/git.js')
- const results = []
- const P = (id, pass, detail) => {
- results.push({ id, pass })
- console.log(`${pass ? '【PASS】' : '【FAIL】'} ${id}: ${detail}`)
- }
- const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'wnw-fix-probe-'))
- let n = 0
- async function makeBook(extraYaml = '') {
- const repo = path.join(tmpRoot, `book-${++n}`)
- await fs.mkdir(repo, { recursive: true })
- const r = await persistCreateBook({ repoPath: repo, cache: null }, {
- book: { spec_version: '7.0', 书名: `修复探针${n}`, 类型: '玄幻', 每章目标字数: 3000, 卷规模: 40 },
- 总纲: '# 总纲\n\n## 结局\n主角胜。\n',
- 卷纲: '# 第1卷\n\n第一卷纲要。\n',
- })
- if (!r.ok) throw new Error(`建书失败: ${r.error}`)
- if (extraYaml) {
- const p = path.join(repo, 'book.yaml')
- await fs.writeFile(p, (await fs.readFile(p, 'utf8')) + extraYaml, 'utf8')
- }
- const cache = new CacheManager(path.join(repo, '.cache', 'index.db'))
- await cache.ensureReady(repo)
- return { repoPath: repo, cache }
- }
- const payloadOf = (num, extra = {}) => ({
- chapterNum: num,
- frontMatter: {
- 章号: num, 标题: `第${num}章题`, 卷: 1, 书内时间: `夏月初${num}`, 字数: 100,
- 章定位: '推进', 钩子: '危机钩-强', 情绪定位: '铺垫',
- },
- body: `第${num}章正文内容,情节推进一格。`,
- summary: `第${num}章摘要。`,
- commitLines: {},
- workspaceFiles: [],
- ...extra,
- })
- async function stage(ctx, num, extra = {}) {
- await fs.mkdir(path.join(ctx.repoPath, '工作区'), { recursive: true })
- await fs.writeFile(path.join(ctx.repoPath, '工作区', '审稿.md'), `# 第 ${num} 章审稿单\n\n> 共 0 个问题:0 阻断。\n`, 'utf8')
- const r = await stageChapter(ctx, { chapterNum: num, payload: payloadOf(num, extra) })
- if (!r.ok) throw new Error(`stage ${num} 失败: ${r.error}`)
- }
- // P-1 (R1):批次在场 goto → 人话拒绝,批次原样;无批次时 goto 照常可用
- try {
- const ctx = await makeBook()
- for (const c of [1, 2, 3]) {
- const r = await finalizeChapter(ctx, payloadOf(c))
- if (!r.ok) throw new Error(`定稿${c}: ${r.error}`)
- }
- await stage(ctx, 4)
- const g = await gotoChapter(ctx, { chapterNum: 2, confirm: true })
- const batch = await readBatch(ctx.repoPath)
- const humane = /finalize-batch|batch-discard/.test(g.error || '')
- P('P-1 goto×批次守卫(R1)', !g.ok && humane && batch.exists,
- `goto被拒=${!g.ok} 指引人话=${humane} 批次完好=${batch.exists} err=${(g.error || '').slice(0, 80)}`)
- await ctx.cache.close()
- } catch (e) { P('P-1 goto×批次守卫(R1)', false, `探针异常: ${e.message}`) }
- // P-2 (R2):批次在场手动 finalize(命令层)→ 双文案人话拒绝;finalizeChapter 本体不带守卫(finalizeBatch 内部要用)
- try {
- const ctx = await makeBook()
- await finalizeChapter(ctx, payloadOf(1))
- await stage(ctx, 2)
- const inBatch = await finalizeCmd(['2'], { payload: '不会被读到.json' }, ctx)
- const outBatch = await finalizeCmd(['9'], { payload: '不会被读到.json' }, ctx)
- const okIn = !inBatch.ok && /finalize-batch/.test(inBatch.error || '')
- const okOut = !outBatch.ok && /章号错位|finalize-batch/.test(outBatch.error || '')
- P('P-2 手动finalize守卫(R2)', okIn && okOut,
- `批内被拒=${okIn} 批外被拒=${okOut} errIn=${(inBatch.error || '').slice(0, 60)}`)
- await ctx.cache.close()
- } catch (e) { P('P-2 手动finalize守卫(R2)', false, `探针异常: ${e.message}`) }
- // P-3 (R3):book.yaml 坏 → 序0;合法修复被接受;修复后序离开 0
- try {
- const ctx = await makeBook('卷规模: 40\n') // 追加重复键 → YAML 解析失败
- const next = await determineNextState(ctx)
- const failures = next.dto?.failures?.map((f) => f.file) || []
- const good = 'spec_version: "7.0"\n书名: 探针修复\n类型: 玄幻\n每章目标字数: 3000\n卷规模: 40\n'
- const rep = await persistRepair(ctx, { repairs: [{ file: 'book.yaml', content: good }] }, { allowedFiles: failures })
- const after = await determineNextState(ctx)
- P('P-3 修复分派校验(R3)', next.序 === 0 && failures.includes('book.yaml') && rep.ok && after.序 !== 0,
- `序0=${next.序 === 0} 修复接受=${rep.ok} 修复后序=${after.序} err=${(rep.error || '').slice(0, 60)}`)
- await ctx.cache.close()
- } catch (e) { P('P-3 修复分派校验(R3)', false, `探针异常: ${e.message}`) }
- // P-4 (B1):坏 front matter 章 → rebuild warning 不再静默;主链由序0 拦(上轮 REFUTED 主链保持)
- try {
- const ctx = await makeBook()
- for (const c of [1, 2, 3]) await finalizeChapter(ctx, payloadOf(c))
- const chDir = path.join(ctx.repoPath, '定稿', '正文')
- const f3 = (await fs.readdir(chDir)).find((f) => f.startsWith('0003-'))
- await fs.writeFile(path.join(chDir, f3), '---\n章号: [broken\n---\n正文', 'utf8')
- const rb = await ctx.cache.rebuildFromSource(ctx.repoPath)
- const next = await determineNextState(ctx)
- P('P-4 解析失败有warning(B1)', (rb.warnings || []).length > 0 && next.序 === 0,
- `rebuild.ok=${rb.ok} warnings=${(rb.warnings || []).length} 条 next序=${next.序}(序0 修复确认拦住重抄)`)
- await ctx.cache.close()
- } catch (e) { P('P-4 解析失败有warning(B1)', false, `探针异常: ${e.message}`) }
- // P-4b (B2):同章号两个合法文件 → UNIQUE 硬错冒泡人话报错,不再静默丢章
- try {
- const ctx = await makeBook()
- for (const c of [1, 2, 3]) await finalizeChapter(ctx, payloadOf(c))
- const chDir = path.join(ctx.repoPath, '定稿', '正文')
- const f3 = (await fs.readdir(chDir)).find((f) => f.startsWith('0003-'))
- await fs.copyFile(path.join(chDir, f3), path.join(chDir, '0003-旧稿备份.md'))
- const rb = await ctx.cache.rebuildFromSource(ctx.repoPath)
- const errText = (rb.errors || []).join(';') // rebuild 返回 errors 数组(非 error 单数)
- P('P-4b 重号硬错冒泡(B2)', rb.ok === false && /重复档案|同章号/.test(errText),
- `rebuild.ok=${rb.ok} err=${errText.slice(0, 80)}`)
- await ctx.cache.close()
- } catch (e) { P('P-4b 重号硬错冒泡(B2)', false, `探针异常: ${e.message}`) }
- // P-5 (R5):sqlite v6 迁移书 → list-characters 可见、entities.type 为英文 machine 值
- try {
- const workdir = path.join(tmpRoot, 'workdir-g2')
- await fs.mkdir(path.join(workdir, '.webnovel'), { recursive: true })
- const { tempV6Sqlite } = await m('test/migrate/_v6.js')
- const v6 = await tempV6Sqlite()
- const mig = await migrateV6({ workdir }, v6.v6Path)
- const repo = path.join(workdir, '潮汐之下')
- const cache = new CacheManager(path.join(repo, '.cache', 'index.db'))
- await cache.ensureReady(repo)
- const lc = await listCharacters([], {}, { repoPath: repo, cache })
- const rows = await cache.query('SELECT type FROM entities', [])
- const machineTypes = rows.every((r) => /^[a-z]+$/.test(r.type))
- P('P-5 迁移书角色可见(R5)', mig.ok && lc.ok && (lc.output || '').includes('江遥') && machineTypes,
- `migrate.ok=${mig.ok} 含江遥=${(lc.output || '').includes('江遥')} type全machine值=${machineTypes} [${rows.map((r) => r.type).join(',')}]`)
- await cache.close()
- await v6.cleanup()
- } catch (e) { P('P-5 迁移书角色可见(R5)', false, `探针异常: ${e.message}`) }
- // P-6 (R6):名册全角/顿号混合分隔别名 → resolveAlias 全命中(英文类型恒等收纳兼验)
- try {
- const ctx = await makeBook()
- await fs.mkdir(path.join(ctx.repoPath, '定稿', '设定'), { recursive: true })
- await fs.writeFile(path.join(ctx.repoPath, '定稿', '设定', '名册.md'),
- '| 正名 | 别名 | 类型 | 首现章 |\n|---|---|---|---|\n| 林晚 | 阿晚,晚儿、小晚 | character | 1 |\n', 'utf8')
- await ctx.cache.rebuildFromSource(ctx.repoPath)
- const er = new EntityReader(ctx.repoPath, ctx.cache)
- const hits = []
- for (const a of ['阿晚', '晚儿', '小晚']) hits.push((await er.resolveAlias(a)).ok)
- P('P-6 别名中文分隔全命中(R6)', hits.every(Boolean), `resolveAlias(阿晚/晚儿/小晚)=[${hits}]`)
- await ctx.cache.close()
- } catch (e) { P('P-6 别名中文分隔全命中(R6)', false, `探针异常: ${e.message}`) }
- // P-7/P-7b (R4):workspaceFiles 含目录(带/不带 工作区/ 前缀)→ ok:true + commit 在 + 目录清掉
- try {
- for (const [id, rel] of [['P-7 目录带前缀', '工作区/评审报告'], ['P-7b 目录裸名', '评审报告']]) {
- const ctx = await makeBook()
- await fs.mkdir(path.join(ctx.repoPath, '工作区', '评审报告'), { recursive: true })
- await fs.writeFile(path.join(ctx.repoPath, '工作区', '评审报告', 'a.md'), 'x', 'utf8')
- const r = await finalizeChapter(ctx, payloadOf(1, { workspaceFiles: [rel] }))
- const committed = await createGit(ctx.repoPath).findChapterCommit(1)
- const dirLeft = await fs.access(path.join(ctx.repoPath, '工作区', '评审报告')).then(() => true, () => false)
- P(`${id}(R4)`, r.ok && !!committed && !dirLeft,
- `finalize.ok=${r.ok} commit=${committed ? '在' : '无'} 目录残留=${dirLeft} warnings=${(r.warnings || []).length}`)
- await ctx.cache.close()
- }
- } catch (e) { P('P-7 finalize清理(R4)', false, `探针异常: ${e.message}`) }
- console.log('\n===== 修复后探针汇总 =====')
- for (const r of results) console.log(`${r.pass ? 'PASS' : 'FAIL'} | ${r.id}`)
- const failed = results.filter((r) => !r.pass).length
- console.log(failed ? `\n${failed} 项未过` : '\n全部通过')
- await fs.rm(tmpRoot, { recursive: true, force: true })
- process.exitCode = failed ? 1 : 0
|