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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  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.replace(/:\d+$/, ':{{port}}')
  30. }
  31. describe.skipIf(!builtArtifactsExist)('dsh web browser-open assembled snapshot', () => {
  32. it('hands the reachable page to the default browser after the shipped tree settles', async () => {
  33. const root = mkdtempSync(join(tmpdir(), 'dsh-web-browser-open-snapshot-'))
  34. tempRoots.push(root)
  35. const result = await execa(process.execPath, [
  36. '--import', openerHook,
  37. builtBin,
  38. 'web',
  39. '--port', '0',
  40. ], {
  41. cwd: root,
  42. env: {
  43. ...process.env,
  44. DEEPSEEK_API_KEY: 'keyless-browser-open-no-call',
  45. DSH_AGENTS_HOME: join(root, '.agents'),
  46. DSH_HOME: join(root, '.dsh'),
  47. DSH_TELEMETRY_DISABLED: '1',
  48. NODE_NO_WARNINGS: '1',
  49. SSH_CONNECTION: '',
  50. SSH_TTY: '',
  51. },
  52. input: '',
  53. timeout: 30_000,
  54. killSignal: 'SIGKILL',
  55. reject: false,
  56. })
  57. const readyUrl = /dsh web: (http:\/\/[^\s]+)/u.exec(result.stdout)?.[1]
  58. const openLine = result.stdout.split('\n').find(line => line.startsWith('dsh browser-open: '))
  59. const opening = result.stdout.includes(openingMessage)
  60. if (readyUrl === undefined || openLine === undefined || !opening) {
  61. throw new Error(`dsh web browser-open evidence missing\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
  62. }
  63. const opened = JSON.parse(openLine.slice('dsh browser-open: '.length)) as BrowserOpenRecord
  64. expect({
  65. exitCode: result.exitCode,
  66. opening,
  67. readyUrl: normalizeLocalUrl(readyUrl),
  68. openedUrl: normalizeLocalUrl(opened.url),
  69. status: opened.status,
  70. bootManifest: opened.bootManifest,
  71. apiKeyPresent: opened.apiKeyPresent,
  72. dshHomePresent: opened.dshHomePresent,
  73. stderr: result.stderr,
  74. }).toMatchInlineSnapshot(`
  75. {
  76. "apiKeyPresent": false,
  77. "bootManifest": true,
  78. "dshHomePresent": false,
  79. "exitCode": 0,
  80. "openedUrl": "http://127.0.0.1:{{port}}",
  81. "opening": true,
  82. "readyUrl": "http://127.0.0.1:{{port}}",
  83. "status": 200,
  84. "stderr": "",
  85. }
  86. `)
  87. })
  88. it('prints the launcher reason and manual URL after the Web app is ready', async () => {
  89. const root = mkdtempSync(join(tmpdir(), 'dsh-web-browser-open-failure-snapshot-'))
  90. tempRoots.push(root)
  91. const result = await execa(process.execPath, [
  92. '--import', openerHook,
  93. builtBin,
  94. 'web',
  95. '--port', '0',
  96. ], {
  97. cwd: root,
  98. env: {
  99. ...process.env,
  100. BROWSER_OPEN_TEST_FAILURE: 'fixture desktop unavailable',
  101. DEEPSEEK_API_KEY: 'keyless-browser-open-no-call',
  102. DSH_AGENTS_HOME: join(root, '.agents'),
  103. DSH_BROWSER_OPEN_TEST_EXIT_ON_FAILURE: '1',
  104. DSH_HOME: join(root, '.dsh'),
  105. DSH_TELEMETRY_DISABLED: '1',
  106. NODE_NO_WARNINGS: '1',
  107. SSH_CONNECTION: '',
  108. SSH_TTY: '',
  109. },
  110. input: '',
  111. timeout: 30_000,
  112. killSignal: 'SIGKILL',
  113. reject: false,
  114. })
  115. const readyUrl = /dsh web: (http:\/\/[^\s]+)/u.exec(result.stdout)?.[1]
  116. const diagnostic = result.stderr.split(/\r?\n/u)
  117. .find(line => line.startsWith('web-app: could not open the default browser because '))
  118. ?.replace(/http:\/\/127\.0\.0\.1:\d+/u, 'http://127.0.0.1:{{port}}')
  119. expect({
  120. diagnostic,
  121. exitCode: result.exitCode,
  122. opened: result.stdout.includes('dsh browser-open: '),
  123. opening: result.stdout.includes(openingMessage),
  124. readyUrl: readyUrl === undefined ? undefined : normalizeLocalUrl(readyUrl),
  125. }).toMatchInlineSnapshot(`
  126. {
  127. "diagnostic": "web-app: could not open the default browser because fixture desktop unavailable; visit http://127.0.0.1:{{port}} manually",
  128. "exitCode": 0,
  129. "opened": false,
  130. "opening": true,
  131. "readyUrl": "http://127.0.0.1:{{port}}",
  132. }
  133. `)
  134. })
  135. it('prints the host URL without launching a browser in a VS Code Remote SSH session', async () => {
  136. const root = mkdtempSync(join(tmpdir(), 'dsh-web-browser-open-ssh-snapshot-'))
  137. tempRoots.push(root)
  138. const result = await execa(process.execPath, [
  139. '--import', openerHook,
  140. builtBin,
  141. 'web',
  142. '--port', '0',
  143. ], {
  144. cwd: root,
  145. env: {
  146. ...process.env,
  147. DEEPSEEK_API_KEY: 'keyless-browser-open-no-call',
  148. DSH_AGENTS_HOME: join(root, '.agents'),
  149. DSH_BROWSER_OPEN_TEST_EXIT_ON_READY: '1',
  150. DSH_HOME: join(root, '.dsh'),
  151. DSH_TELEMETRY_DISABLED: '1',
  152. NODE_NO_WARNINGS: '1',
  153. SSH_CONNECTION: '10.0.0.2 55000 10.0.0.9 22',
  154. SSH_TTY: '',
  155. VSCODE_IPC_HOOK_CLI: '/tmp/vscode-ipc',
  156. },
  157. input: '',
  158. timeout: 30_000,
  159. killSignal: 'SIGKILL',
  160. reject: false,
  161. })
  162. const readyUrl = /dsh web: (http:\/\/[^\s]+)/u.exec(result.stdout)?.[1]
  163. expect({
  164. exitCode: result.exitCode,
  165. opening: result.stdout.includes(openingMessage),
  166. readyUrl: readyUrl === undefined ? undefined : normalizeLocalUrl(readyUrl),
  167. opened: result.stdout.includes('dsh browser-open: '),
  168. stderr: result.stderr,
  169. }).toMatchInlineSnapshot(`
  170. {
  171. "exitCode": 0,
  172. "opened": false,
  173. "opening": false,
  174. "readyUrl": "http://127.0.0.1:{{port}}",
  175. "stderr": "",
  176. }
  177. `)
  178. })
  179. it('rejects a project browser command before starting the Web app', async () => {
  180. const root = mkdtempSync(join(tmpdir(), 'dsh-web-browser-open-env-snapshot-'))
  181. tempRoots.push(root)
  182. writeFileSync(join(root, '.env'), 'BROWSER=./project-browser\n')
  183. const result = await execa(process.execPath, [
  184. '--import', openerHook,
  185. builtBin,
  186. 'web',
  187. '--port', '0',
  188. ], {
  189. cwd: root,
  190. env: {
  191. ...process.env,
  192. DEEPSEEK_API_KEY: 'keyless-browser-open-no-call',
  193. DSH_AGENTS_HOME: join(root, '.agents'),
  194. DSH_HOME: join(root, '.dsh'),
  195. DSH_TELEMETRY_DISABLED: '1',
  196. NODE_NO_WARNINGS: '1',
  197. SSH_CONNECTION: '',
  198. SSH_TTY: '',
  199. },
  200. input: '',
  201. timeout: 30_000,
  202. killSignal: 'SIGKILL',
  203. reject: false,
  204. })
  205. const diagnostic = result.stderr.split(/\r?\n/u)
  206. .find(line => line.startsWith('Error: dsh: '))
  207. ?.replace(/^Error: dsh: .*[/\\]\.env/u, 'dsh: {{root}}/.env')
  208. expect({
  209. diagnostic,
  210. exitCode: result.exitCode,
  211. opening: result.stdout.includes(openingMessage),
  212. opened: result.stdout.includes('dsh browser-open: '),
  213. ready: result.stdout.includes('dsh web: '),
  214. }).toMatchInlineSnapshot(`
  215. {
  216. "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",
  217. "exitCode": 1,
  218. "opened": false,
  219. "opening": false,
  220. "ready": false,
  221. }
  222. `)
  223. })
  224. })