pwsh-terminal.e2e.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. // Keyless browser regression for pwsh UI parity with bash: a seeded session
  2. // whose pwsh call/result is presented by the REAL tool-pwsh on replay (the
  3. // api-proxy recomputes presentation views from logged args/result content)
  4. // must render as a bash-shaped terminal card with the parsed exit-status
  5. // pill — not the generic console-fenced card the pwsh presenter used to
  6. // emit. The seed is authored, not recorded: its header line carries no `cwd`
  7. // field (seedSession writes the session cwd itself, and a Windows temp path
  8. // substituted into the header would not round-trip through its JSON parse),
  9. // and no event references the workspace, so the lane replays on any host
  10. // with a usable `pwsh` — the lane mounts the pwsh stack through an overlay
  11. // (the shipped tree keeps the bash stack).
  12. import { spawnSync } from 'node:child_process'
  13. import { readFile } from 'node:fs/promises'
  14. import { join } from 'node:path'
  15. import { fileURLToPath } from 'node:url'
  16. import type { Browser, Page } from 'playwright'
  17. import { chromium } from 'playwright'
  18. import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
  19. import { resolvePwshPath } from '@deepseek-ai/dsh-pwsh-local'
  20. import {
  21. assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
  22. fixtureUserPrompts, launchWebScaffold, seedSession, webSnapshotMode,
  23. type WebScaffold,
  24. } from './scaffold.ts'
  25. import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
  26. const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/pwsh-terminal', import.meta.url))
  27. const SEED = join(SNAPSHOT_DIR, 'seed.jsonl')
  28. const TERMINAL_EXPECTED = join(SNAPSHOT_DIR, 'terminal-card.expected.md')
  29. const OVERLAY = fileURLToPath(new URL('./pwsh-terminal.overlay.yml', import.meta.url))
  30. const PROMPT = 'Run a PowerShell command that fails, then stop.'
  31. const SEED_ID = 'pwsh-terminal-web-e2e'
  32. const MODE = webSnapshotMode()
  33. // The overlay swaps the shipped bash executor for @deepseek-ai/dsh-pwsh-local;
  34. // a host without a usable `pwsh` cannot boot it, so the lane self-skips,
  35. // mirroring the pwshOnly ACP scenarios. The probe follows the executor's own
  36. // resolution (Program Files installs on Windows are found even when bare
  37. // `pwsh` is not on PATH), the same judgment the tool-pwsh tests reuse; record
  38. // mode skips the lane anyway, so the probe stays inert there.
  39. const HAS_PWSH = MODE === 'record' ? false : spawnSync(
  40. resolvePwshPath(), ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', '$true'],
  41. { encoding: 'utf8' },
  42. ).status === 0
  43. describe.skipIf(MODE === 'record' || !HAS_PWSH)('web e2e: pwsh calls render as bash-shaped terminal cards', () => {
  44. let scaffold: WebScaffold
  45. let browser: Browser
  46. let page: Page
  47. beforeAll(async () => {
  48. const fixture = await readFile(SEED, 'utf8')
  49. expect(fixtureUserPrompts(fixture), 'seed fixture must carry the single drive prompt').toEqual([PROMPT])
  50. scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY })
  51. await seedSession(scaffold, fixture, SEED_ID)
  52. browser = await chromium.launch()
  53. page = await newEnglishPage(browser)
  54. await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
  55. await connectFreshWorkspace(page, scaffold.workspaceCwd)
  56. }, 120_000)
  57. afterAll(async () => {
  58. await browser?.close()
  59. await scaffold?.close()
  60. })
  61. it('renders the seeded pwsh call as a terminal card with the parsed exit pill', async () => {
  62. onTestFailed(() => saveFailureShot(page, 'web-e2e-pwsh-terminal'))
  63. // Open the seeded session through content search: the sidebar groups
  64. // sessions by workspace and its row order is world-dependent, while the
  65. // search index covers the seeded log deterministically.
  66. const search = page.getByPlaceholder('Search name, keywords', { exact: false })
  67. await search.fill('Run a PowerShell command')
  68. const result = page.getByRole('tree', { name: 'Search results' }).getByRole('treeitem')
  69. await expect.poll(() => result.count(), { timeout: 15_000 }).toBe(1)
  70. await result.click()
  71. await page.getByRole('tab', { name: 'Chat', exact: true }).waitFor({ timeout: 15_000 })
  72. // The tool row is expand-gated: the settled bash-shaped row carries the
  73. // shell-family variant, and the terminal card lives in the expanded body.
  74. const row = page.locator('[data-tool="pwsh"]').first()
  75. await row.waitFor({ timeout: 15_000 })
  76. if (await row.getAttribute('aria-expanded') !== 'true') await row.click()
  77. const card = page.locator('[data-terminal]').first()
  78. await card.waitFor({ timeout: 15_000 })
  79. // The parsed exit pill replaces the `[exit code: 1]` marker in the output
  80. // body — the bash tool's terminal presentation, not the generic fence.
  81. const text = await card.textContent()
  82. expect(text).toContain('exit code 1')
  83. expect(text).toContain('Get-Item : Cannot find path')
  84. expect(text).not.toContain('[exit code: 1]')
  85. const snapshot = (await captureStableAria(page, '[data-terminal]', scaffold.workspaceCwd))
  86. // normalizeAria collapses the workspace basename with a '/' split, which
  87. // misses Windows temp paths; collapse it here too (a no-op on POSIX) so
  88. // the golden is platform-independent.
  89. .split(scaffold.workspaceCwd.split(/[\\/]/).pop()!).join('{{workspace}}')
  90. .split(SEED_ID).join('{{seededId}}')
  91. await compareOrRefreshGolden(TERMINAL_EXPECTED, snapshot, MODE)
  92. }, 60_000)
  93. it('guards the lane fixture inventory', async () => {
  94. await assertFixtureInventory(SNAPSHOT_DIR, ['seed.jsonl', 'terminal-card.expected.md'])
  95. })
  96. })