log-sink.spec.ts 4.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. /**
  2. * The worker host's log sink: the seam that makes a failing plugin visible.
  3. *
  4. * Cordis's `LoggerService` accepts every message and, with no exporter mounted,
  5. * only fills a ring buffer. No profile in this repository mounts one, so a
  6. * provider that fails and is skipped — the skill registry logs exactly that —
  7. * is indistinguishable from one that found nothing. The sink is exercised here
  8. * rather than trusted: a diagnostic that runs nothing is a diagnostic that
  9. * silently stops working.
  10. */
  11. import { afterEach, describe, expect, it, vi } from 'vitest'
  12. import { installLogSink, type LogExporter, type LogMessage } from '../src/worker-host.ts'
  13. /** Capture the exporter the sink registers, and the cordis renderer it asks for. */
  14. function harness(): { register: () => LogExporter; requested: string[] } {
  15. const requested: string[] = []
  16. let registered: LogExporter | undefined
  17. const ctx = {
  18. loader: { internal: undefined },
  19. logger: { exporter: (exporter: LogExporter) => { registered = exporter; return undefined } },
  20. get: () => undefined,
  21. provide: () => {},
  22. fiber: { dispose: async () => {} },
  23. }
  24. const require = (specifier: string): unknown => {
  25. requested.push(specifier)
  26. // Stand in for cordis's printf renderer: the sink's contract is that it
  27. // formats THROUGH it, not that it reimplements the format.
  28. return { Logger: { format: (_exporter: LogExporter, message: LogMessage) => `rendered(${message.args.join('|')})` } }
  29. }
  30. installLogSink(ctx, require)
  31. if (registered === undefined) throw new Error('the sink registered no exporter')
  32. const exporter = registered
  33. return { register: () => exporter, requested }
  34. }
  35. const message = (type: LogMessage['type'], name: string, ...args: unknown[]): LogMessage => ({ name, type, args })
  36. // Console spies are installed on one shared object, so a surviving spy would
  37. // carry the previous case's calls into the counting case below.
  38. afterEach(() => { vi.restoreAllMocks() })
  39. describe('worker host log sink', () => {
  40. it('registers one exporter and renders through cordis', () => {
  41. const { register, requested } = harness()
  42. expect(requested).toEqual(['@deepseek-ai/cordis'])
  43. // Colors off: the page console has no terminal escapes to interpret.
  44. expect(register().colors).toBe(false)
  45. })
  46. it('declares a verbosity gate that admits warnings', () => {
  47. // cordis's scale counts UP with verbosity (ERROR 0, INFO 1, WARN 2, DEBUG 3)
  48. // and it drops a message whose level EXCEEDS the exporter's, so an exporter
  49. // that declares nothing inherits INFO and never sees a warning. This case is
  50. // the one that matters: the sink exists for warnings, and getting the
  51. // comparison backwards makes it silently deliver nothing.
  52. const admits = (exporterLevel: number, messageLevel: number): boolean => exporterLevel >= messageLevel
  53. const gate = harness().register().levels.default
  54. expect(admits(gate, 2), 'warnings must pass the gate').toBe(true)
  55. expect(admits(gate, 0), 'errors must pass the gate').toBe(true)
  56. expect(admits(gate, 3), 'debug must not pass the gate').toBe(false)
  57. })
  58. it('reports a warning with its logger name, the way a skipped provider arrives', () => {
  59. const warned = vi.spyOn(console, 'warn').mockImplementation(() => {})
  60. harness().register().export(message('warn', 'skill', 'provider "local" skipped: FS_IO_ERROR'))
  61. expect(warned).toHaveBeenCalledWith('skill: rendered(provider "local" skipped: FS_IO_ERROR)')
  62. })
  63. it('reports an error on the error channel', () => {
  64. const failed = vi.spyOn(console, 'error').mockImplementation(() => {})
  65. harness().register().export(message('error', 'loader', 'boom'))
  66. expect(failed).toHaveBeenCalledWith('loader: rendered(boom)')
  67. })
  68. it('drops info and debug, which 131 plugin rows would bury the console with', () => {
  69. const logged = vi.spyOn(console, 'log').mockImplementation(() => {})
  70. const warned = vi.spyOn(console, 'warn').mockImplementation(() => {})
  71. const failed = vi.spyOn(console, 'error').mockImplementation(() => {})
  72. const exporter = harness().register()
  73. exporter.export(message('info', 'timer', 'tick'))
  74. exporter.export(message('debug', 'loader', 'resolved'))
  75. expect([logged.mock.calls.length, warned.mock.calls.length, failed.mock.calls.length]).toEqual([0, 0, 0])
  76. })
  77. })