1
0

present-svg.e2e.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. /** A file request elicits explicit SVG delivery without naming the present tool. */
  2. import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
  3. import { tmpdir } from 'node:os'
  4. import { join, resolve } from 'node:path'
  5. import { fileURLToPath } from 'node:url'
  6. import { chromium, type Browser, type Page } from 'playwright'
  7. import { afterAll, beforeAll, describe, expect, it } from 'vitest'
  8. import type {} from '@deepseek-ai/dsh-tool-present/types'
  9. import { deriveReplayScript, parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
  10. import {
  11. assertFinalWorkspaceSnapshot, captureExpandedTurnProcessAria, compareOrRefreshGolden,
  12. fixtureUserPrompts, launchWebScaffold, recordFixture, watchConsole,
  13. webSnapshotMode, type WebScaffold,
  14. } from './scaffold.ts'
  15. import { connectFreshWorkspaceZh, ZH_BROWSER_LOCALE } from './support.ts'
  16. const DIR = fileURLToPath(new URL('../../../snapshots/web/present-svg', import.meta.url))
  17. const FIXTURE = join(DIR, 'session.v3.jsonl')
  18. const MODE = webSnapshotMode()
  19. const PROMPT = '简单画一个 SVG 表示冯诺依曼架构, 保存为 von-neumann.svg'
  20. const FILE = 'von-neumann.svg'
  21. describe('web e2e: requested SVG is explicitly delivered', () => {
  22. let scaffold: WebScaffold
  23. let browser: Browser
  24. let page: Page
  25. let tripwire: ReturnType<typeof watchConsole>
  26. let cwd: string
  27. let replayRoot: string | undefined
  28. beforeAll(async () => {
  29. let replayOverride: string | undefined
  30. if (MODE !== 'record') {
  31. replayRoot = await mkdtemp(join(tmpdir(), 'dsh-present-svg-replay-'))
  32. replayOverride = join(replayRoot, 'replay.override.json')
  33. const script = deriveReplayScript(parseSessionLog(await readFile(FIXTURE, 'utf8')))
  34. // Recorded absolute paths must follow each isolated Session's working directory.
  35. const cwdToken = '{{fromRequest:Your working directory is ([^\\n]+)\\.}}'
  36. await writeFile(replayOverride, JSON.stringify(script).replaceAll('{{cwd}}', JSON.stringify(cwdToken).slice(1, -1)))
  37. }
  38. scaffold = await launchWebScaffold({
  39. compareReplaySession: true,
  40. extraOverlayPath: fileURLToPath(new URL('./present-svg.overlay.yml', import.meta.url)),
  41. ...(replayOverride === undefined ? {} : { replayFixture: FIXTURE, replayOverride }),
  42. })
  43. browser = await chromium.launch()
  44. page = await browser.newPage({
  45. viewport: { width: 1680, height: 1000 }, locale: ZH_BROWSER_LOCALE, timezoneId: 'Asia/Shanghai',
  46. })
  47. tripwire = watchConsole(page)
  48. await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
  49. await page.waitForSelector('[class*="frame"]')
  50. await connectFreshWorkspaceZh(page, scaffold.workspaceCwd)
  51. })
  52. afterAll(async () => {
  53. try {
  54. await browser?.close()
  55. } finally {
  56. try {
  57. await scaffold?.close()
  58. } finally {
  59. if (replayRoot !== undefined) await rm(replayRoot, { recursive: true, force: true })
  60. }
  61. }
  62. })
  63. it('writes valid SVG and calls present before the final reply', async () => {
  64. if (MODE !== 'record') expect(fixtureUserPrompts(await readFile(FIXTURE, 'utf8'))).toEqual([PROMPT])
  65. const settled = scaffold.whenTurnSettled()
  66. const input = page.locator('[data-composer-input]').first()
  67. await input.fill(PROMPT)
  68. await input.press('Enter')
  69. const sessionId = await settled
  70. const session = scaffold.ctx.agents.get(sessionId)?.session
  71. if (session?.header.cwd === undefined) throw new Error('SVG Session has no workspace')
  72. cwd = session.header.cwd
  73. if (MODE === 'record') await recordFixture(scaffold, sessionId, FIXTURE)
  74. const svg = await readFile(join(cwd, FILE), 'utf8')
  75. const document = await page.evaluate((source) => {
  76. const parsed = new DOMParser().parseFromString(source, 'image/svg+xml')
  77. return {
  78. root: parsed.documentElement.localName,
  79. namespace: parsed.documentElement.namespaceURI,
  80. errors: parsed.querySelectorAll('parsererror').length,
  81. }
  82. }, svg)
  83. expect(document).toEqual({ root: 'svg', namespace: 'http://www.w3.org/2000/svg', errors: 0 })
  84. const events = session.snapshotEvents()
  85. const declarations = events.filter(event => event.type === 'deliverables/presented')
  86. const delivery = declarations.find(event => event.data.files.some(file => resolve(cwd, file.path) === join(cwd, FILE)))
  87. expect(delivery, 'the file request must produce a successful present declaration').toBeDefined()
  88. if (delivery === undefined) throw new Error('SVG was written but not delivered')
  89. expect(events.some(event => (
  90. event.type === 'tool/call' && event.data.name === 'present' && event.data.callId === delivery.data.callId
  91. ) || (
  92. event.type === 'tool/ptc-dispatch' && event.data.name === 'present'
  93. && event.data.subCallId === delivery.data.callId && !event.data.isError
  94. ))).toBe(true)
  95. expect(events.some(event => event.type === 'assistant/message' && event.seq > delivery.seq
  96. && event.data.message.content.some(block => block.type === 'text'))).toBe(true)
  97. const card = page.locator('[data-presented-file]').filter({ hasText: FILE })
  98. await card.waitFor({ state: 'visible' })
  99. expect(await card.count()).toBe(1)
  100. expect(await page.getByText('产物', { exact: true }).count()).toBe(0)
  101. // The scaffold workspace is not a git repository, so the changed-files card lists the written SVG from the write call alone.
  102. expect(await page.locator('[data-changed-files]').count()).toBe(1)
  103. expect(tripwire.pageErrors).toEqual([])
  104. expect(tripwire.warnings).toEqual([])
  105. })
  106. it.skipIf(MODE === 'record')('replays the delivered file and Chinese conversation', async () => {
  107. await assertFinalWorkspaceSnapshot(DIR, cwd)
  108. await expect.poll(() => page.getByRole('button', { name: `${FILE} 的更多文件操作`, exact: true }).isDisabled()).toBe(true)
  109. // Delivery owns the transcript; navigation and composer chrome have separate scenarios.
  110. const aria = await captureExpandedTurnProcessAria(page, '[data-chat-flow]', scaffold.workspaceCwd)
  111. await compareOrRefreshGolden(join(DIR, 'ui.expected.md'), aria, MODE)
  112. })
  113. })