desktop-upload-plan.spec.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. import { createHash } from 'node:crypto'
  2. import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'
  3. import { tmpdir } from 'node:os'
  4. import { join } from 'node:path'
  5. import { afterEach, describe, expect, it } from 'vitest'
  6. import { createDesktopUploadPlan } from '../scripts/desktop-upload-plan.ts'
  7. import { desktopUpdateMetadataFilename } from '../scripts/desktop-auto-update-environment.mjs'
  8. import type { DesktopPackageTargetName } from '../scripts/package-target.ts'
  9. const temporaryDirectories: string[] = []
  10. const TEST_ORIGIN = 'https://desktop-updates.example.com'
  11. const TEST_BUCKET = 'test-download-bucket'
  12. const PRODUCTION_BUCKET = 'production-download-bucket'
  13. interface Fixture {
  14. readonly repositoryRoot: string
  15. readonly appRoot: string
  16. readonly artifactsRoot: string
  17. readonly environment: NodeJS.ProcessEnv
  18. }
  19. function digest(contents: string): string {
  20. return createHash('sha512').update(contents).digest('base64')
  21. }
  22. async function fixture(
  23. target: DesktopPackageTargetName,
  24. version = '1.2.3',
  25. environment: 'test' | 'production' = 'test',
  26. ): Promise<Fixture> {
  27. const root = await mkdtemp(join(tmpdir(), 'dsh-desktop-upload-'))
  28. temporaryDirectories.push(root)
  29. const repositoryRoot = join(root, 'repository')
  30. const appRoot = join(repositoryRoot, 'apps', 'desktop')
  31. const artifactsRoot = join(appRoot, '.desktop-build', 'artifacts')
  32. await mkdir(artifactsRoot, { recursive: true })
  33. await writeFile(join(repositoryRoot, 'package.json'), `${JSON.stringify({ version })}\n`)
  34. await writeFile(join(appRoot, 'package.json'), `${JSON.stringify({ version })}\n`)
  35. const [os, arch] = target.split('-') as ['mac' | 'win', 'arm64' | 'x64']
  36. const base = `deepseek-harness-${version}-${os}-${arch}`
  37. const origin = environment === 'test'
  38. ? TEST_ORIGIN
  39. : 'https://download.deepseek.com'
  40. await writeFile(join(artifactsRoot, `${target}-release.json`), `${JSON.stringify({
  41. schemaVersion: 1,
  42. target,
  43. version,
  44. environment,
  45. publicUrl: `${origin}/_/harness/desktop/stable/${target}/`,
  46. })}\n`)
  47. if (os === 'mac') {
  48. const zip = 'signed macOS ZIP fixture'
  49. await writeFile(join(artifactsRoot, `${base}.zip`), zip)
  50. await writeFile(join(artifactsRoot, `${base}.zip.blockmap`), 'blockmap')
  51. await writeFile(join(artifactsRoot, `${base}.dmg`), 'notarized DMG fixture')
  52. await writeFile(join(artifactsRoot, desktopUpdateMetadataFilename(version, 'darwin')), `${JSON.stringify({
  53. version,
  54. files: [{ url: `${base}.zip`, size: Buffer.byteLength(zip), sha512: digest(zip) }],
  55. })}\n`)
  56. }
  57. else {
  58. const executable = 'signed NSIS executable fixture'
  59. await writeFile(join(artifactsRoot, `${base}.exe`), executable)
  60. await writeFile(join(artifactsRoot, desktopUpdateMetadataFilename(version, 'win32')), `${JSON.stringify({
  61. version,
  62. files: [{
  63. url: `${base}.exe`,
  64. size: Buffer.byteLength(executable),
  65. sha512: digest(executable),
  66. blockMapSize: 128,
  67. }],
  68. })}\n`)
  69. }
  70. return {
  71. repositoryRoot,
  72. appRoot,
  73. artifactsRoot,
  74. environment: environment === 'test'
  75. ? {
  76. DSH_DESKTOP_AUTO_UPDATE_ENV: 'test',
  77. DOWNLOAD_TEST_ORIGIN: TEST_ORIGIN,
  78. DOWNLOAD_TEST_COS_BUCKET: TEST_BUCKET,
  79. }
  80. : {
  81. DSH_DESKTOP_AUTO_UPDATE_ENV: 'production',
  82. DOWNLOAD_PROD_COS_BUCKET: PRODUCTION_BUCKET,
  83. },
  84. }
  85. }
  86. afterEach(async () => {
  87. await Promise.all(temporaryDirectories.splice(0).map(async path => rm(path, {
  88. recursive: true,
  89. force: true,
  90. })))
  91. })
  92. describe('desktop upload plan', () => {
  93. it('validates macOS artifacts and puts channel metadata last', async () => {
  94. const paths = await fixture('mac-arm64')
  95. const plan = await createDesktopUploadPlan('mac-arm64', paths)
  96. expect(plan).toMatchObject({
  97. environment: 'test',
  98. version: '1.2.3',
  99. publicUrl: 'https://desktop-updates.example.com/_/harness/desktop/stable/mac-arm64/',
  100. bucket: TEST_BUCKET,
  101. })
  102. expect(plan.artifacts.map(artifact => artifact.filename)).toEqual([
  103. 'deepseek-harness-1.2.3-mac-arm64.dmg',
  104. 'deepseek-harness-1.2.3-mac-arm64.zip',
  105. 'deepseek-harness-1.2.3-mac-arm64.zip.blockmap',
  106. 'latest-mac.yml',
  107. ])
  108. expect(plan.artifacts.at(-1)).toMatchObject({
  109. channelMetadata: true,
  110. cacheControl: 'no-cache',
  111. })
  112. })
  113. it('uploads the prerelease channel metadata emitted by electron-builder', async () => {
  114. const paths = await fixture('mac-arm64', '1.2.3-alpha.4')
  115. const plan = await createDesktopUploadPlan('mac-arm64', paths)
  116. expect(plan.artifacts.map(artifact => artifact.filename)).toEqual([
  117. 'deepseek-harness-1.2.3-alpha.4-mac-arm64.dmg',
  118. 'deepseek-harness-1.2.3-alpha.4-mac-arm64.zip',
  119. 'deepseek-harness-1.2.3-alpha.4-mac-arm64.zip.blockmap',
  120. 'alpha-mac.yml',
  121. ])
  122. })
  123. it('validates the Windows installer with its embedded blockmap and production destination', async () => {
  124. const paths = await fixture('win-x64', '2.0.0', 'production')
  125. const plan = await createDesktopUploadPlan('win-x64', paths)
  126. expect(plan.artifacts.map(artifact => artifact.filename)).toEqual([
  127. 'deepseek-harness-2.0.0-win-x64.exe',
  128. 'latest.yml',
  129. ])
  130. expect(plan).toMatchObject({
  131. publicUrl: 'https://download.deepseek.com/_/harness/desktop/stable/win-x64/',
  132. bucket: PRODUCTION_BUCKET,
  133. })
  134. })
  135. it('rejects Windows metadata without an embedded blockmap size', async () => {
  136. const paths = await fixture('win-x64')
  137. const executable = 'signed NSIS executable fixture'
  138. await writeFile(join(paths.artifactsRoot, 'latest.yml'), `${JSON.stringify({
  139. version: '1.2.3',
  140. files: [{
  141. url: 'deepseek-harness-1.2.3-win-x64.exe',
  142. size: Buffer.byteLength(executable),
  143. sha512: digest(executable),
  144. }],
  145. })}\n`)
  146. await expect(createDesktopUploadPlan('win-x64', paths)).rejects.toThrow(/blockMapSize/u)
  147. })
  148. it('rejects a completed build from another dsh version or deployment', async () => {
  149. const paths = await fixture('mac-x64')
  150. await writeFile(join(paths.repositoryRoot, 'package.json'), '{"version":"1.2.4"}\n')
  151. await writeFile(join(paths.appRoot, 'package.json'), '{"version":"1.2.4"}\n')
  152. await expect(createDesktopUploadPlan('mac-x64', paths)).rejects.toThrow(/completion record.*1\.2\.4/u)
  153. const productionPaths = await fixture('mac-x64', '1.2.3', 'production')
  154. await expect(createDesktopUploadPlan('mac-x64', {
  155. ...productionPaths,
  156. environment: {
  157. DSH_DESKTOP_AUTO_UPDATE_ENV: 'test',
  158. DOWNLOAD_TEST_ORIGIN: TEST_ORIGIN,
  159. DOWNLOAD_TEST_COS_BUCKET: TEST_BUCKET,
  160. },
  161. })).rejects.toThrow(/completion record.*test/u)
  162. })
  163. it('rejects stale architecture metadata and modified updater bytes', async () => {
  164. const paths = await fixture('mac-arm64')
  165. const metadataPath = join(paths.artifactsRoot, 'latest-mac.yml')
  166. const zipPath = join(paths.artifactsRoot, 'deepseek-harness-1.2.3-mac-arm64.zip')
  167. await writeFile(zipPath, 'modified')
  168. await expect(createDesktopUploadPlan('mac-arm64', paths)).rejects.toThrow(/size.*metadata/u)
  169. const x64 = 'wrong architecture'
  170. await writeFile(metadataPath, `${JSON.stringify({
  171. version: '1.2.3',
  172. files: [{
  173. url: 'deepseek-harness-1.2.3-mac-x64.zip',
  174. size: Buffer.byteLength(x64),
  175. sha512: digest(x64),
  176. }],
  177. })}\n`)
  178. await expect(createDesktopUploadPlan('mac-arm64', paths)).rejects.toThrow(/mac-arm64\.zip/u)
  179. })
  180. })