consent-resolver.spec.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import { mkdtemp, mkdir, writeFile } from 'node:fs/promises'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import { afterEach, describe, expect, it } from 'vitest'
  5. import { ConsentResolver, DEFAULT_TELEMETRY_PLUGIN_NAME, type ConsentDecision } from '@deepseek-ai/dsh-telemetry'
  6. const dirs: string[] = []
  7. async function projectDir(cordisYml?: string): Promise<string> {
  8. const dir = await mkdtemp(join(tmpdir(), 'dsh-consent-'))
  9. dirs.push(dir)
  10. if (cordisYml !== undefined) await writeFile(join(dir, 'cordis.yml'), cordisYml, 'utf8')
  11. return dir
  12. }
  13. afterEach(async () => {
  14. await Promise.all(dirs.splice(0).map(dir => import('node:fs/promises').then(fs => fs.rm(dir, { recursive: true, force: true }))))
  15. })
  16. const enabledYml = `- id: telemetry\n name: '${DEFAULT_TELEMETRY_PLUGIN_NAME}'\n`
  17. describe('ConsentResolver environment opt-out', () => {
  18. it('denies when DO_NOT_TRACK is set', async () => {
  19. const decision = await new ConsentResolver({ env: { DO_NOT_TRACK: '1' } }).resolve(await projectDir(enabledYml))
  20. expect(decision).toEqual<ConsentDecision>({ allowed: false, reason: 'do-not-track' })
  21. })
  22. it('denies when CI is set', async () => {
  23. const decision = await new ConsentResolver({ env: { CI: 'true' } }).resolve(await projectDir(enabledYml))
  24. expect(decision).toEqual<ConsentDecision>({ allowed: false, reason: 'ci' })
  25. })
  26. it('ignores falsy env values and continues to the file', async () => {
  27. const decision = await new ConsentResolver({ env: { DO_NOT_TRACK: '0', CI: 'false' } })
  28. .resolve(await projectDir(enabledYml))
  29. expect(decision).toEqual<ConsentDecision>({ allowed: true, reason: 'enabled' })
  30. })
  31. it('can be told to ignore env opt-out signals', async () => {
  32. const decision = await new ConsentResolver({ env: { DO_NOT_TRACK: '1' }, honorEnvOptOut: false })
  33. .resolve(await projectDir(enabledYml))
  34. expect(decision).toEqual<ConsentDecision>({ allowed: true, reason: 'enabled' })
  35. })
  36. it('reads process.env by default', async () => {
  37. const saved = { CI: process.env.CI, DO_NOT_TRACK: process.env.DO_NOT_TRACK }
  38. delete process.env.CI
  39. delete process.env.DO_NOT_TRACK
  40. try {
  41. const decision = await new ConsentResolver().resolve(await projectDir(enabledYml))
  42. expect(decision).toEqual<ConsentDecision>({ allowed: true, reason: 'enabled' })
  43. } finally {
  44. if (saved.CI !== undefined) process.env.CI = saved.CI
  45. if (saved.DO_NOT_TRACK !== undefined) process.env.DO_NOT_TRACK = saved.DO_NOT_TRACK
  46. }
  47. })
  48. })
  49. describe('ConsentResolver cordis.yml state', () => {
  50. const resolver = new ConsentResolver({ env: {} })
  51. it('allows when the telemetry entry is enabled', async () => {
  52. expect(await resolver.resolve(await projectDir(enabledYml)))
  53. .toEqual<ConsentDecision>({ allowed: true, reason: 'enabled' })
  54. })
  55. it('denies when the telemetry entry is disabled', async () => {
  56. const yml = `- id: telemetry\n name: '${DEFAULT_TELEMETRY_PLUGIN_NAME}'\n disabled: true\n`
  57. expect(await resolver.resolve(await projectDir(yml)))
  58. .toEqual<ConsentDecision>({ allowed: false, reason: 'disabled' })
  59. })
  60. it('tolerates !!js expression tags while reading plain scalars', async () => {
  61. const yml = [
  62. '- id: telemetry',
  63. ` name: '${DEFAULT_TELEMETRY_PLUGIN_NAME}'`,
  64. '- id: llm',
  65. ' name: \'@deepseek-ai/dsh-llm-deepseek\'',
  66. ' config:',
  67. ' apiKey: !!js process.env.DEEPSEEK_API_KEY',
  68. '',
  69. ].join('\n')
  70. expect(await resolver.resolve(await projectDir(yml)))
  71. .toEqual<ConsentDecision>({ allowed: true, reason: 'enabled' })
  72. })
  73. it('reports (allows) when cordis.yml has no telemetry entry', async () => {
  74. const yml = '- id: llm\n name: \'@deepseek-ai/dsh-llm-deepseek\'\n'
  75. expect(await resolver.resolve(await projectDir(yml)))
  76. .toEqual<ConsentDecision>({ allowed: true, reason: 'absent' })
  77. })
  78. it('can be told to deny when the entry is absent', async () => {
  79. const yml = '- id: llm\n name: \'@deepseek-ai/dsh-llm-deepseek\'\n'
  80. const decision = await new ConsentResolver({ env: {}, allowWhenEntryAbsent: false }).resolve(await projectDir(yml))
  81. expect(decision).toEqual<ConsentDecision>({ allowed: false, reason: 'absent' })
  82. })
  83. it('skips non-object sequence items and a non-sequence root, still reporting absent', async () => {
  84. expect(await resolver.resolve(await projectDir('- just-a-string\n- id: x\n name: y\n')))
  85. .toEqual<ConsentDecision>({ allowed: true, reason: 'absent' })
  86. expect(await resolver.resolve(await projectDir('root: not-a-sequence\n')))
  87. .toEqual<ConsentDecision>({ allowed: true, reason: 'absent' })
  88. })
  89. it('honors a custom telemetry plugin name', async () => {
  90. const yml = '- id: t\n name: \'my-consent-marker\'\n'
  91. const decision = await new ConsentResolver({ env: {}, telemetryPluginName: 'my-consent-marker' })
  92. .resolve(await projectDir(yml))
  93. expect(decision).toEqual<ConsentDecision>({ allowed: true, reason: 'enabled' })
  94. })
  95. })
  96. describe('ConsentResolver missing or unreadable cordis.yml', () => {
  97. it('reports no-config and allows by default on first init', async () => {
  98. expect(await new ConsentResolver({ env: {} }).resolve(await projectDir()))
  99. .toEqual<ConsentDecision>({ allowed: true, reason: 'no-config' })
  100. })
  101. it('can deny on first init', async () => {
  102. const decision = await new ConsentResolver({ env: {}, allowWhenNoConfig: false }).resolve(await projectDir())
  103. expect(decision).toEqual<ConsentDecision>({ allowed: false, reason: 'no-config' })
  104. })
  105. it('denies with an unreadable reason when cordis.yml is not a regular file', async () => {
  106. const dir = await projectDir()
  107. await mkdir(join(dir, 'cordis.yml')) // a directory where the resolver expects a file
  108. expect(await new ConsentResolver({ env: {} }).resolve(dir))
  109. .toEqual<ConsentDecision>({ allowed: false, reason: 'unreadable' })
  110. })
  111. })