ask-wiring.spec.ts 3.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. import { describe, expect, it } from 'vitest'
  2. import { apply, registerWorkspaceRoot } from '../src/index'
  3. import * as fs from 'node:fs'
  4. import * as os from 'node:os'
  5. import * as path from 'node:path'
  6. /** 宿主注入 askFn 的接线:裁决通道须由 ctx.userQuestions 供给,而非模型自证。 */
  7. function makeWorkspace(): string {
  8. const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'webnovel-ask-wire-'))
  9. const book = path.join(dir, '星辰')
  10. fs.mkdirSync(path.join(book, '作品契约'), { recursive: true })
  11. fs.writeFileSync(path.join(book, '作品契约', '契约.md'), '---\n书id: xingchen-001\n---\n正文\n', 'utf8')
  12. return dir
  13. }
  14. interface Registered { readonly name: string; readonly execute: (a: unknown, e: unknown) => Promise<unknown> }
  15. function applyWith(services: Record<string, unknown>): Registered[] {
  16. const registered: Registered[] = []
  17. const declared: string[][] = []
  18. // dsh 运行时对齐批:注册落点=agent 的 scope 层(经 agentCtx.get('tools'))。
  19. // 桩 agent 的 ctx 与宿主共享同一 tools 记录器,模拟服务经 scope 链可达。
  20. const recorder = { register: (t: Registered) => { registered.push(t); return () => {} } }
  21. const agent = { id: 'a-1', ctx: { get: (n: string) => (n === 'tools' ? recorder : undefined) } }
  22. const scope = {
  23. logger: { info: () => {}, warn: () => {} },
  24. get: (n: string) => services[n],
  25. on: () => {},
  26. inject: (deps: readonly string[], cb: (s: unknown) => void) => {
  27. declared.push([...deps])
  28. cb(scope)
  29. return undefined
  30. },
  31. }
  32. const host = {
  33. ...scope,
  34. get: (n: string) => (n === 'agents' ? { list: () => [agent] } : (services as Record<string, unknown>)[n]),
  35. }
  36. ;(host as { inject: unknown }).inject = (deps: readonly string[], cb: (s: unknown) => void) => {
  37. declared.push([...deps])
  38. cb(host)
  39. return undefined
  40. }
  41. registerWorkspaceRoot(makeWorkspace())
  42. apply(host as never)
  43. ;(applyWith as unknown as { declared: string[][] }).declared = declared
  44. return registered
  45. }
  46. describe('askFn 宿主接线(裁决 14)', () => {
  47. it('tools 注册声明 userQuestions 依赖,服务后到亦可拿到', () => {
  48. applyWith({})
  49. const declared = (applyWith as unknown as { declared: string[][] }).declared
  50. expect(declared.some((d) => d.includes('tools') && d.includes('userQuestions'))).toBe(true)
  51. })
  52. it('宿主有 userQuestions 时注入裁决通道,不再因缺通道早退', async () => {
  53. const tools = applyWith({
  54. userQuestions: {
  55. // dsh 真源形状:{ answers: [{ id, selected }] }(user-questions/src/types.ts:63)
  56. ask: async () => ({ answers: [{ id: '定稿入档', selected: ['退回'] }] }),
  57. },
  58. })
  59. const settle = tools.find((t) => t.name === 'novel_settle_chapter')
  60. expect(settle).toBeDefined()
  61. const r = (await settle!.execute(
  62. { bookId: 'xingchen-001', 卷: 1, 章: 1, 章名: '第一章', summary: 'x' },
  63. { agent: { id: 'a-1' } },
  64. )) as { ok: boolean; reason?: string }
  65. expect(r.ok).toBe(false)
  66. // 通道已注入:失败原因不再是「缺通道」(问作者排在书解析等便宜前置之后)。
  67. expect(String(r.reason)).not.toContain('裁决通道未配置')
  68. })
  69. it('宿主无 userQuestions 时不注入,沉淀 fail-closed', async () => {
  70. const tools = applyWith({})
  71. const settle = tools.find((t) => t.name === 'novel_settle_chapter')!
  72. const r = (await settle.execute(
  73. { bookId: 'xingchen-001', 卷: 1, 章: 1, 章名: '第一章', summary: 'x' },
  74. { agent: { id: 'a-1' } },
  75. )) as { ok: boolean; reason?: string }
  76. expect(r.ok).toBe(false)
  77. expect(String(r.reason)).toContain('裁决通道')
  78. })
  79. })