profile-initialization.spec.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. /** One-time custom-profile initialization from shipped templates. */
  2. import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
  3. import { tmpdir } from 'node:os'
  4. import { join } from 'node:path'
  5. import { fileURLToPath } from 'node:url'
  6. import {
  7. initProfile,
  8. PROFILE_PATCH_FILENAME,
  9. PROFILE_TEMPLATES,
  10. readProfileManifest,
  11. resolveProfileDir,
  12. writeProfileManifest,
  13. } from '@deepseek-ai/dsh-app-boot'
  14. import { describe, expect, it } from 'vitest'
  15. import { execa } from 'execa'
  16. import { initializeProfileFromDefault } from '../src/profile-boot.ts'
  17. const childEntry = fileURLToPath(new URL('./fixtures/initialize-profile-from-default.ts', import.meta.url))
  18. const tsxLoader = import.meta.resolve('tsx/esm')
  19. const CHILD_TIMEOUT_MS = 30_000
  20. /** Wait until a child has reached the shared creation barrier. */
  21. async function waitForFile(file: string): Promise<void> {
  22. const deadline = Date.now() + CHILD_TIMEOUT_MS
  23. while (!existsSync(file)) {
  24. if (Date.now() >= deadline) throw new Error(`profile initialization marker did not appear: ${file}`)
  25. await new Promise(resolve => setTimeout(resolve, 20))
  26. }
  27. }
  28. /** Run one assertion against a private Harness home and remove it afterwards. */
  29. function withHome(assertion: (home: string) => void): void {
  30. const home = mkdtempSync(join(tmpdir(), 'dsh-profile-from-default-'))
  31. try {
  32. assertion(home)
  33. } finally {
  34. rmSync(home, { recursive: true, force: true })
  35. }
  36. }
  37. describe('initializeProfileFromDefault', () => {
  38. it.each(Object.entries(PROFILE_TEMPLATES))(
  39. 'copies the %s template metadata into an independent profile',
  40. (source, template) => {
  41. withHome((home) => {
  42. initializeProfileFromDefault('custom', source, home)
  43. const dir = resolveProfileDir('custom', home)
  44. const manifest = readProfileManifest('test', dir)
  45. expect(manifest).toEqual({
  46. name: 'dsh-profile-custom',
  47. private: true,
  48. dependencies: {},
  49. dsh: { profile: { bundles: [...template.bundles], patchReload: template.patchReload } },
  50. })
  51. expect(readFileSync(join(dir, PROFILE_PATCH_FILENAME), 'utf8')).toContain('[]')
  52. expect(readFileSync(join(dir, 'pnpm-workspace.yaml'), 'utf8')).toContain('nodeLinker: hoisted')
  53. })
  54. },
  55. )
  56. it('does not copy the local source profile dependencies or user patch', () => {
  57. withHome((home) => {
  58. const sourceDir = resolveProfileDir('web', home)
  59. initProfile(sourceDir, ['local-bundle'], 'startup')
  60. const sourceManifest = readProfileManifest('test', sourceDir)
  61. sourceManifest.dependencies = { 'local-bundle': '1.0.0' }
  62. writeProfileManifest(sourceDir, sourceManifest)
  63. writeFileSync(join(sourceDir, PROFILE_PATCH_FILENAME), '- id: local-only\n disabled: true\n')
  64. initializeProfileFromDefault('rescue', 'web', home)
  65. const targetDir = resolveProfileDir('rescue', home)
  66. const target = readProfileManifest('test', targetDir)
  67. expect(target.dependencies).toEqual({})
  68. expect(target.dsh?.profile).toEqual({
  69. bundles: [...PROFILE_TEMPLATES.web!.bundles],
  70. patchReload: PROFILE_TEMPLATES.web!.patchReload,
  71. })
  72. expect(readFileSync(join(targetDir, PROFILE_PATCH_FILENAME), 'utf8')).not.toContain('local-only')
  73. })
  74. })
  75. it('rejects an existing target without changing its files', () => {
  76. withHome((home) => {
  77. const dir = resolveProfileDir('rescue', home)
  78. initProfile(dir, ['existing-bundle'], 'startup')
  79. writeFileSync(join(dir, PROFILE_PATCH_FILENAME), '- id: existing\n disabled: true\n')
  80. const paths = ['package.json', PROFILE_PATCH_FILENAME, 'pnpm-workspace.yaml'].map(file => join(dir, file))
  81. const before = paths.map(path => readFileSync(path))
  82. expect(() => {
  83. initializeProfileFromDefault('rescue', 'web', home)
  84. })
  85. .toThrow('profile "rescue" already exists')
  86. expect(paths.map(path => readFileSync(path))).toEqual(before)
  87. })
  88. })
  89. it('rejects a residual target directory without changing its contents', () => {
  90. withHome((home) => {
  91. const dir = resolveProfileDir('rescue', home)
  92. mkdirSync(dir, { recursive: true })
  93. const residual = join(dir, PROFILE_PATCH_FILENAME)
  94. writeFileSync(residual, '- id: residual\n disabled: true\n')
  95. const before = readFileSync(residual)
  96. expect(() => {
  97. initializeProfileFromDefault('rescue', 'web', home)
  98. })
  99. .toThrow('profile directory')
  100. expect(readFileSync(residual)).toEqual(before)
  101. expect(existsSync(join(dir, 'package.json'))).toBe(false)
  102. })
  103. })
  104. it.each(Object.keys(PROFILE_TEMPLATES))('rejects shipped target name %s without creating it', (name) => {
  105. withHome((home) => {
  106. expect(() => {
  107. initializeProfileFromDefault(name, 'web', home)
  108. })
  109. .toThrow(`profile ${JSON.stringify(name)} is shipped`)
  110. expect(existsSync(resolveProfileDir(name, home))).toBe(false)
  111. })
  112. })
  113. it.each(['unknown', 'toString'])('rejects unknown template %s without creating the target', (source) => {
  114. withHome((home) => {
  115. expect(() => {
  116. initializeProfileFromDefault('rescue', source, home)
  117. })
  118. .toThrow(`unknown default profile ${JSON.stringify(source)}`)
  119. expect(existsSync(resolveProfileDir('rescue', home))).toBe(false)
  120. })
  121. })
  122. it('allows only one of two synchronized processes to create the target', async () => {
  123. const home = mkdtempSync(join(tmpdir(), 'dsh-profile-from-default-race-'))
  124. const gate = join(home, 'start')
  125. const ready = [join(home, 'ready-1'), join(home, 'ready-2')]
  126. const children = ready.map(marker => execa(
  127. process.execPath,
  128. ['--import', tsxLoader, childEntry, home, 'rescue', 'web', marker, gate],
  129. { reject: false, timeout: CHILD_TIMEOUT_MS },
  130. ))
  131. try {
  132. await Promise.all(ready.map(waitForFile))
  133. writeFileSync(gate, '')
  134. const results = await Promise.all(children)
  135. expect(results.map(result => result.exitCode).sort()).toEqual([0, 1])
  136. expect(readProfileManifest('test', resolveProfileDir('rescue', home)).dsh?.profile)
  137. .toEqual(PROFILE_TEMPLATES.web)
  138. } finally {
  139. for (const child of children) child.kill('SIGKILL')
  140. rmSync(home, { recursive: true, force: true })
  141. }
  142. }, CHILD_TIMEOUT_MS + 10_000)
  143. })