patch.spec.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. /** Profile patch edits preserve user-authored syntax and are idempotent. */
  2. import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
  3. import { join } from 'node:path'
  4. import { tmpdir } from 'node:os'
  5. import { expect, it, onTestFinished } from 'vitest'
  6. import { applyEntryPatches } from '@deepseek-ai/cordis-plugin-include'
  7. import { loadOptionalPatches } from '@deepseek-ai/dsh-app-boot'
  8. import { writePluginEnabled } from '../src/patch.ts'
  9. async function fixture(text?: string): Promise<string> {
  10. const dir = await mkdtemp(join(tmpdir(), 'manager-patch-'))
  11. onTestFinished(() => rm(dir, { recursive: true, force: true }))
  12. const file = join(dir, 'cordis.patch.yml')
  13. if (text !== undefined) await writeFile(file, text)
  14. return file
  15. }
  16. it('preserves comments, expressions, unrelated configuration and the last override', async () => {
  17. const file = await fixture('# personal configuration\n- id: tool\n config:\n value: !!js process.platform\n- id: tool\n disabled: false # availability\n')
  18. expect(await writePluginEnabled(file, 'tool', 'package', false)).toBe(true)
  19. const text = await readFile(file, 'utf8')
  20. expect(text).toContain('# personal configuration')
  21. expect(text).toContain('!!js process.platform')
  22. expect(text).toContain('# availability')
  23. expect(loadOptionalPatches('test', file)).toEqual([
  24. { id: 'tool', config: { value: { __jsExpr: 'process.platform' } } }, { id: 'tool', disabled: true },
  25. ])
  26. expect(await writePluginEnabled(file, 'tool', 'package', false)).toBe(false)
  27. expect(await readFile(file, 'utf8')).toBe(text)
  28. expect(await writePluginEnabled(file, 'tool', 'package', true)).toBe(true)
  29. })
  30. it('creates a missing patch file and appends after insertions', async () => {
  31. const file = await fixture()
  32. await writePluginEnabled(file, 'tool', 'package', false)
  33. expect(loadOptionalPatches('test', file)).toEqual([{ id: 'tool', disabled: true }])
  34. await writeFile(file, '- insert:\n - id: tool\n name: package\n')
  35. await writePluginEnabled(file, 'tool', 'package', true)
  36. expect(loadOptionalPatches('test', file)).toEqual([
  37. { insert: [{ id: 'tool', name: 'package' }] }, { id: 'tool', disabled: false },
  38. ])
  39. })
  40. it.each(['- id: [broken', 'mapping: true\n'])('refuses malformed documents without overwriting %s', async (text) => {
  41. const file = await fixture(text)
  42. await expect(writePluginEnabled(file, 'tool', 'package', true)).rejects.toThrow()
  43. expect(await readFile(file, 'utf8')).toBe(text)
  44. })
  45. it('retains name-asserting overrides and appends an unambiguous switch', async () => {
  46. const file = await fixture('- id: tool\n name: another-package\n disabled: false\n')
  47. await writePluginEnabled(file, 'tool', 'package', false)
  48. expect(loadOptionalPatches('test', file)).toEqual([
  49. { id: 'tool', name: 'another-package', disabled: false }, { id: 'tool', disabled: true },
  50. ])
  51. expect(await writePluginEnabled(file, 'tool', 'package', false)).toBe(false)
  52. })
  53. it('reports read failures without replacing a directory with configuration', async () => {
  54. const file = await fixture()
  55. await mkdir(file)
  56. await expect(writePluginEnabled(file, 'tool', 'package', true)).rejects.toThrow()
  57. })
  58. it('updates the last matching named override and leaves mismatched names untouched', async () => {
  59. const file = await fixture('- id: tool\n disabled: true\n- id: tool\n name: package\n config:\n value: !!js process.platform\n disabled: true # availability\n- id: tool\n name: another-package\n disabled: true\n')
  60. expect(await writePluginEnabled(file, 'tool', 'package', true)).toBe(true)
  61. const text = await readFile(file, 'utf8')
  62. expect(text).toContain('!!js process.platform')
  63. expect(text).toContain('# availability')
  64. const patches = loadOptionalPatches('test', file)
  65. expect(patches).toEqual([
  66. { id: 'tool', disabled: true },
  67. { id: 'tool', name: 'package', config: { value: { __jsExpr: 'process.platform' } }, disabled: false },
  68. { id: 'tool', name: 'another-package', disabled: true },
  69. ])
  70. expect(applyEntryPatches([{ id: 'tool', name: 'package' }], patches, () => {}))
  71. .toMatchObject([{ id: 'tool', name: 'package', disabled: false }])
  72. expect(await writePluginEnabled(file, 'tool', 'package', true)).toBe(false)
  73. expect(await readFile(file, 'utf8')).toBe(text)
  74. expect(await writePluginEnabled(file, 'tool', 'package', false)).toBe(true)
  75. expect(loadOptionalPatches('test', file)).toHaveLength(3)
  76. })