gate-zone.spec.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import { describe, expect, it } from 'vitest'
  2. import * as fs from 'node:fs'
  3. import * as os from 'node:os'
  4. import * as path from 'node:path'
  5. import { decideTargetForFile } from '../src/gates'
  6. /** 构造临时工作范围:书房 + 书仓(草稿区/定稿/作品契约)。 */
  7. function makeWorkspace(): { ws: string; bookRoot: string } {
  8. const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'webnovel-gate-'))
  9. const bookRoot = path.join(ws, '星辰')
  10. fs.mkdirSync(path.join(bookRoot, '草稿区', '章细纲'), { recursive: true })
  11. fs.mkdirSync(path.join(bookRoot, '草稿区', '草稿', '卷01-第一章'), { recursive: true })
  12. fs.mkdirSync(path.join(bookRoot, '定稿'), { recursive: true })
  13. fs.mkdirSync(path.join(bookRoot, '作品契约'), { recursive: true })
  14. fs.mkdirSync(path.join(ws, '书房', '知识库'), { recursive: true })
  15. return { ws, bookRoot }
  16. }
  17. function depsOf(ws: string, bookRoot: string) {
  18. return {
  19. bookRootOfId: (id: string) => (id === 'xingchen-001' ? bookRoot : undefined),
  20. bookRootForAbs: (abs: string) => {
  21. const rel = path.relative(bookRoot, abs)
  22. return rel !== '' && !rel.startsWith('..') ? bookRoot : undefined
  23. },
  24. workspaceRoot: () => ws,
  25. }
  26. }
  27. describe('gate 分区判定(A2a/A2b + N5/N10 修复)', () => {
  28. it('A2a: 裸相对路径拒绝', () => {
  29. const { ws, bookRoot } = makeWorkspace()
  30. const d = depsOf(ws, bookRoot)
  31. const r = decideTargetForFile('草稿区/章细纲/x.md', d)
  32. expect(r.kind).toBe('deny')
  33. expect(r.kind === 'deny' ? r.reason : '').toContain('裸相对路径拒绝')
  34. })
  35. it('A2b: 书房写入允许(以 工作范围/书房 为界)', () => {
  36. const { ws, bookRoot } = makeWorkspace()
  37. const d = depsOf(ws, bookRoot)
  38. const studyFile = path.join(ws, '书房', '知识库', '技法.md')
  39. expect(decideTargetForFile(studyFile, d)).toEqual({ kind: 'allow' })
  40. })
  41. it('书id:仓内相对路径 → 草稿区章细纲可写', () => {
  42. const { ws, bookRoot } = makeWorkspace()
  43. const d = depsOf(ws, bookRoot)
  44. expect(decideTargetForFile('xingchen-001:草稿区/章细纲/0001-第一章.md', d)).toEqual({ kind: 'allow' })
  45. })
  46. it('书id:仓内相对路径 → R1 落码:未确认细纲写草稿放行(播报不拒绝)', () => {
  47. const { ws, bookRoot } = makeWorkspace()
  48. const d = depsOf(ws, bookRoot)
  49. const r = decideTargetForFile('xingchen-001:草稿区/草稿/卷01-第一章/稿1.md', d)
  50. expect(r.kind).toBe('allow')
  51. })
  52. it('书id:仓内相对路径 → 定稿只读', () => {
  53. const { ws, bookRoot } = makeWorkspace()
  54. const d = depsOf(ws, bookRoot)
  55. const r = decideTargetForFile('xingchen-001:定稿/卷01/0001-第一章.md', d)
  56. expect(r.kind).toBe('deny')
  57. expect(r.kind === 'deny' ? r.reason : '').toContain('定稿')
  58. })
  59. it('书id:仓内相对路径 → 真源(作品契约)拒绝', () => {
  60. const { ws, bookRoot } = makeWorkspace()
  61. const d = depsOf(ws, bookRoot)
  62. const r = decideTargetForFile('xingchen-001:作品契约/契约.md', d)
  63. expect(r.kind).toBe('deny')
  64. expect(r.kind === 'deny' ? r.reason : '').toContain('真源')
  65. })
  66. it('绝对路径在工作范围外触发 ask (N10)', () => {
  67. const { ws, bookRoot } = makeWorkspace()
  68. const d = depsOf(ws, bookRoot)
  69. const outside = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'webnovel-out-')), 'x.md')
  70. const r = decideTargetForFile(outside, d)
  71. expect(r.kind).toBe('ask')
  72. })
  73. it('未知书id 拒绝', () => {
  74. const { ws, bookRoot } = makeWorkspace()
  75. const d = depsOf(ws, bookRoot)
  76. const r = decideTargetForFile('ghost:草稿区/x.md', d)
  77. expect(r.kind).toBe('deny')
  78. expect(r.kind === 'deny' ? r.reason : '').toContain('未知书目')
  79. })
  80. })