web-browser-open.expected.e2e.ts 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. /** Assembled keyless snapshot for the default `dsh web` browser handoff. */
  2. import { existsSync, mkdtempSync, 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 { execa } from 'execa'
  7. import { afterEach, describe, expect, it } from 'vitest'
  8. const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
  9. const builtBin = join(repoRoot, 'apps/cli/lib/bin.js')
  10. const frontendIndex = join(repoRoot, 'apps/web/dist/index.html')
  11. const openerHook = new URL('./fixtures/web-browser-open/register.mjs', import.meta.url).href
  12. const openingMessage = 'dsh web: opening the default browser; pass --no-open to disable'
  13. const tempRoots: string[] = []
  14. const builtArtifactsExist = existsSync(builtBin) && existsSync(frontendIndex)
  15. if (process.env.DSH_EXAMPLE_MODE === 'lib' && !builtArtifactsExist) {
  16. throw new Error('dsh web browser-open snapshot requires built CLI and Web artifacts in lib mode')
  17. }
  18. afterEach(() => {
  19. for (const root of tempRoots.splice(0)) rmSync(root, { recursive: true, force: true })
  20. })
  21. interface BrowserOpenRecord {
  22. url: string
  23. status: number
  24. bootManifest: boolean
  25. apiKeyPresent: boolean
  26. dshHomePresent: boolean
  27. }
  28. function normalizeLocalUrl(url: string): string {
  29. return url
  30. .replace(/:\d+/u, ':{{port}}')
  31. .replace(/token=[^&]+/u, 'token={{token}}')
  32. }
  33. describe.skipIf(!builtArtifactsExist)('dsh web browser-open assembled snapshot', () => {
  34. it('hands the reachable page to the default browser after the shipped tree settles', async () => {
  35. const root = mkdtempSync(join(tmpdir(), 'dsh-web-browser-open-snapshot-'))
  36. tempRoots.push(root)
  37. const result = await execa(process.execPath, [
  38. '--import', openerHook,
  39. builtBin,
  40. 'web',
  41. '--port', '0',
  42. ], {
  43. cwd: root,
  44. env: {
  45. ...process.env,
  46. DEEPSEEK_API_KEY: 'keyless-browser-open-no-call',
  47. DSH_AGENTS_HOME: join(root, '.agents'),
  48. DSH_HOME: join(root, '.dsh'),
  49. DSH_TELEMETRY_DISABLED: '1',
  50. NODE_NO_WARNINGS: '1',
  51. SSH_CONNECTION: '',
  52. SSH_TTY: '',
  53. },
  54. input: '',
  55. timeout: 30_000,
  56. killSignal: 'SIGKILL',
  57. reject: false,
  58. })
  59. const readyUrl = /dsh web: (http:\/\/[^\s]+)/u.exec(result.stdout)?.[1]
  60. const openLine = result.stdout.split('\n').find(line => line.startsWith('dsh browser-open: '))
  61. const opening = result.stdout.includes(openingMessage)
  62. if (readyUrl === undefined || openLine === undefined || !opening) {
  63. throw new Error(`dsh web browser-open evidence missing\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
  64. }
  65. const opened = JSON.parse(openLine.slice('dsh browser-open: '.length)) as BrowserOpenRecord
  66. expect({
  67. exitCode: result.exitCode,
  68. opening,
  69. readyUrl: normalizeLocalUrl(readyUrl),
  70. openedUrl: normalizeLocalUrl(opened.url),
  71. status: opened.status,
  72. bootManifest: opened.bootManifest,
  73. apiKeyPresent: opened.apiKeyPresent,
  74. dshHomePresent: opened.dshHomePresent,
  75. stderr: result.stderr,
  76. }).toMatchInlineSnapshot(`
  77. {
  78. "apiKeyPresent": false,
  79. "bootManifest": true,
  80. "dshHomePresent": false,
  81. "exitCode": 0,
  82. "openedUrl": "http://127.0.0.1:{{port}}/?token={{token}}",
  83. "opening": true,
  84. "readyUrl": "http://127.0.0.1:{{port}}/?token={{token}}",
  85. "status": 200,
  86. "stderr": "",
  87. }
  88. `)
  89. })
  90. it('prints the launcher reason and manual URL after the Web app is ready', async () => {
  91. const root = mkdtempSync(join(tmpdir(), 'dsh-web-browser-open-failure-snapshot-'))
  92. tempRoots.push(root)
  93. const result = await execa(process.execPath, [
  94. '--import', openerHook,
  95. builtBin,
  96. 'web',
  97. '--port', '0',
  98. ], {
  99. cwd: root,
  100. env: {
  101. ...process.env,
  102. BROWSER_OPEN_TEST_FAILURE: 'fixture desktop unavailable',
  103. DEEPSEEK_API_KEY: 'keyless-browser-open-no-call',
  104. DSH_AGENTS_HOME: join(root, '.agents'),
  105. DSH_BROWSER_OPEN_TEST_EXIT_ON_FAILURE: '1',
  106. DSH_HOME: join(root, '.dsh'),
  107. DSH_TELEMETRY_DISABLED: '1',
  108. NODE_NO_WARNINGS: '1',
  109. SSH_CONNECTION: '',
  110. SSH_TTY: '',
  111. },
  112. input: '',
  113. timeout: 30_000,
  114. killSignal: 'SIGKILL',
  115. reject: false,
  116. })
  117. const readyUrl = /dsh web: (http:\/\/[^\s]+)/u.exec(result.stdout)?.[1]
  118. const diagnostic = result.stderr.split(/\r?\n/u)
  119. .find(line => line.startsWith('web-app: could not open the default browser because '))
  120. expect({
  121. diagnostic,
  122. exitCode: result.exitCode,
  123. opened: result.stdout.includes('dsh browser-open: '),
  124. opening: result.stdout.includes(openingMessage),
  125. readyUrl: readyUrl === undefined ? undefined : normalizeLocalUrl(readyUrl),
  126. }).toMatchInlineSnapshot(`
  127. {
  128. "diagnostic": "web-app: could not open the default browser because fixture desktop unavailable; use the dsh web URL printed at startup",
  129. "exitCode": 0,
  130. "opened": false,
  131. "opening": true,
  132. "readyUrl": "http://127.0.0.1:{{port}}/?token={{token}}",
  133. }
  134. `)
  135. })
  136. it('prints the host URL without launching a browser in a VS Code Remote SSH session', async () => {
  137. const root = mkdtempSync(join(tmpdir(), 'dsh-web-browser-open-ssh-snapshot-'))
  138. tempRoots.push(root)
  139. const result = await execa(process.execPath, [
  140. '--import', openerHook,
  141. builtBin,
  142. 'web',
  143. '--port', '0',
  144. ], {
  145. cwd: root,
  146. env: {
  147. ...process.env,
  148. DEEPSEEK_API_KEY: 'keyless-browser-open-no-call',
  149. DSH_AGENTS_HOME: join(root, '.agents'),
  150. DSH_BROWSER_OPEN_TEST_EXIT_ON_READY: '1',
  151. DSH_HOME: join(root, '.dsh'),
  152. DSH_TELEMETRY_DISABLED: '1',
  153. NODE_NO_WARNINGS: '1',
  154. SSH_CONNECTION: '10.0.0.2 55000 10.0.0.9 22',
  155. SSH_TTY: '',
  156. VSCODE_IPC_HOOK_CLI: '/tmp/vscode-ipc',
  157. },
  158. input: '',
  159. timeout: 30_000,
  160. killSignal: 'SIGKILL',
  161. reject: false,
  162. })
  163. const readyUrl = /dsh web: (http:\/\/[^\s]+)/u.exec(result.stdout)?.[1]
  164. expect({
  165. exitCode: result.exitCode,
  166. opening: result.stdout.includes(openingMessage),
  167. readyUrl: readyUrl === undefined ? undefined : normalizeLocalUrl(readyUrl),
  168. opened: result.stdout.includes('dsh browser-open: '),
  169. stderr: result.stderr,
  170. }).toMatchInlineSnapshot(`
  171. {
  172. "exitCode": 0,
  173. "opened": false,
  174. "opening": false,
  175. "readyUrl": "http://127.0.0.1:{{port}}/?token={{token}}",
  176. "stderr": "",
  177. }
  178. `)
  179. })
  180. it('rejects a project browser command before starting the Web app', async () => {
  181. const root = mkdtempSync(join(tmpdir(), 'dsh-web-browser-open-env-snapshot-'))
  182. tempRoots.push(root)
  183. writeFileSync(join(root, '.env'), 'BROWSER=./project-browser\n')
  184. const result = await execa(process.execPath, [
  185. '--import', openerHook,
  186. builtBin,
  187. 'web',
  188. '--port', '0',
  189. ], {
  190. cwd: root,
  191. env: {
  192. ...process.env,
  193. DEEPSEEK_API_KEY: 'keyless-browser-open-no-call',
  194. DSH_AGENTS_HOME: join(root, '.agents'),
  195. DSH_HOME: join(root, '.dsh'),
  196. DSH_TELEMETRY_DISABLED: '1',
  197. NODE_NO_WARNINGS: '1',
  198. SSH_CONNECTION: '',
  199. SSH_TTY: '',
  200. },
  201. input: '',
  202. timeout: 30_000,
  203. killSignal: 'SIGKILL',
  204. reject: false,
  205. })
  206. const diagnostic = result.stderr.split(/\r?\n/u)
  207. .find(line => line.startsWith('Error: dsh: '))
  208. ?.replace(/^Error: dsh: .*[/\\]\.env/u, 'dsh: {{root}}/.env')
  209. expect({
  210. diagnostic,
  211. exitCode: result.exitCode,
  212. opening: result.stdout.includes(openingMessage),
  213. opened: result.stdout.includes('dsh browser-open: '),
  214. ready: result.stdout.includes('dsh web: '),
  215. }).toMatchInlineSnapshot(`
  216. {
  217. "diagnostic": "dsh: {{root}}/.env sets "BROWSER", which only the launching environment may set (it decides how this process starts, where its code and instructions load from, or how it reaches the network); export BROWSER instead of putting it in a .env file",
  218. "exitCode": 1,
  219. "opened": false,
  220. "opening": false,
  221. "ready": false,
  222. }
  223. `)
  224. })
  225. })