Merge remote-tracking branch 'origin/master' into xtr/react-loop-simplification
# Conflicts: # examples/acp-agent/tests/goal-snapshots/goal-session/input.json # packages/client/ui-conversation/README.i18n.yaml # packages/session-persistence/session-persistence-jsonl/src/zstd.ts # packages/support/acp-snapshot/src/harness.ts
This commit is contained in:
@@ -11,6 +11,7 @@ import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import { WEB_SEARCH_MAX_RESULTS } from '@deepseek-ai/dsh-tool-web'
|
||||
import {
|
||||
assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts,
|
||||
launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
@@ -25,7 +26,37 @@ const QUERY = 'DeepSeek Harness snapshot search'
|
||||
const PROMPT = `Use web_search to search exactly "${QUERY}". Then reply exactly SEARCH_DONE and stop.`
|
||||
const SEARCH_CREDENTIAL_REF = credentialRef('DSH_WEB_SEARCH_E2E_KEY')
|
||||
const SEARCH_CREDENTIAL = 'snapshot-search-key'
|
||||
const RESULT_URL = 'https://docs.example.test/search'
|
||||
|
||||
/**
|
||||
* Provider results the double returns, exceeding the shipped `searchMaxResults`
|
||||
* so the seam's cap and the card's scroll container are both exercised. Each row
|
||||
* carries a title, a snippet, and a date, so 8 kept rows exceed the `.sources`
|
||||
* 320px max-height.
|
||||
*/
|
||||
const PROVIDER_RESULT_COUNT = 12
|
||||
|
||||
/** One provider result's URL, by 1-based provider order. */
|
||||
function resultUrl(ordinal: number): string {
|
||||
return `https://docs.example.test/search/${ordinal}`
|
||||
}
|
||||
|
||||
/** One provider result's title, by 1-based provider order. */
|
||||
function resultTitle(ordinal: number): string {
|
||||
return `Snapshot Search Result ${ordinal}`
|
||||
}
|
||||
|
||||
/** One provider result's citation excerpt, by 1-based provider order. */
|
||||
function resultSnippet(ordinal: number): string {
|
||||
return `Snapshot search excerpt ${ordinal}: the harness replays this source list from a local endpoint.`
|
||||
}
|
||||
|
||||
/** One provider result's `page_age`, by 1-based provider order (July 2026 days 01..12). */
|
||||
function resultPageAge(ordinal: number): string {
|
||||
return `2026-07-${String(ordinal).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/** The 1-based provider ordinals, in provider order. */
|
||||
const RESULT_ORDINALS = Array.from({ length: PROVIDER_RESULT_COUNT }, (_value, index) => index + 1)
|
||||
|
||||
interface CapturedSearchRequest {
|
||||
path: string
|
||||
@@ -50,21 +81,21 @@ async function startSearchServer(captured: CapturedSearchRequest[]): Promise<{ s
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Found one source.',
|
||||
citations: [{
|
||||
text: `Found ${PROVIDER_RESULT_COUNT} sources.`,
|
||||
citations: RESULT_ORDINALS.map(ordinal => ({
|
||||
type: 'web_search_result_location',
|
||||
url: RESULT_URL,
|
||||
cited_text: 'Snapshot search excerpt.',
|
||||
}],
|
||||
url: resultUrl(ordinal),
|
||||
cited_text: resultSnippet(ordinal),
|
||||
})),
|
||||
},
|
||||
{
|
||||
type: 'web_search_tool_result',
|
||||
content: [{
|
||||
content: RESULT_ORDINALS.map(ordinal => ({
|
||||
type: 'web_search_result',
|
||||
url: RESULT_URL,
|
||||
title: 'Snapshot Search Result',
|
||||
page_age: '2026-07-31',
|
||||
}],
|
||||
url: resultUrl(ordinal),
|
||||
title: resultTitle(ordinal),
|
||||
page_age: resultPageAge(ordinal),
|
||||
})),
|
||||
},
|
||||
],
|
||||
}))
|
||||
@@ -141,7 +172,7 @@ describe('web e2e: shipped default web search', () => {
|
||||
if (MODE === 'record') await recordFixture(scaffold, sessionId, FIXTURE)
|
||||
}, 200_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('uses the real provider and persists the structured result', () => {
|
||||
it.skipIf(MODE === 'record')('uses the real provider and persists the capped structured result', () => {
|
||||
expect(searchRequests).toHaveLength(1)
|
||||
expect(searchRequests[0]).toMatchObject({
|
||||
path: '/messages',
|
||||
@@ -177,16 +208,27 @@ describe('web e2e: shipped default web search', () => {
|
||||
if (searchResult === undefined) throw new Error('web_search produced no durable result')
|
||||
const content = searchResult.data.message.content[0]
|
||||
expect(content.isError).toBe(false)
|
||||
expect(content.content.filter(block => block.type === 'text').map(block => block.text).join(''))
|
||||
.toContain(`[Snapshot Search Result](${RESULT_URL})`)
|
||||
const rendered = content.content.filter(block => block.type === 'text').map(block => block.text).join('')
|
||||
// The seam caps the provider's list at the shipped searchMaxResults before
|
||||
// the tool renders it, so the kept prefix is model-visible and the dropped
|
||||
// suffix is not.
|
||||
for (const ordinal of RESULT_ORDINALS.slice(0, WEB_SEARCH_MAX_RESULTS)) {
|
||||
expect(rendered).toContain(`[${resultTitle(ordinal)}](${resultUrl(ordinal)})`)
|
||||
}
|
||||
for (const ordinal of RESULT_ORDINALS.slice(WEB_SEARCH_MAX_RESULTS)) {
|
||||
expect(rendered).not.toContain(resultUrl(ordinal))
|
||||
}
|
||||
expect(rendered).toContain(
|
||||
`(Showing the first ${WEB_SEARCH_MAX_RESULTS} sources. Refine the query for more.)`,
|
||||
)
|
||||
expect(searchResult.data.meta).toMatchObject({
|
||||
sources: [{
|
||||
url: RESULT_URL,
|
||||
title: 'Snapshot Search Result',
|
||||
snippet: 'Snapshot search excerpt.',
|
||||
publishedAt: '2026-07-31',
|
||||
}],
|
||||
truncated: false,
|
||||
sources: RESULT_ORDINALS.slice(0, WEB_SEARCH_MAX_RESULTS).map(ordinal => ({
|
||||
url: resultUrl(ordinal),
|
||||
title: resultTitle(ordinal),
|
||||
snippet: resultSnippet(ordinal),
|
||||
publishedAt: resultPageAge(ordinal),
|
||||
})),
|
||||
truncated: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -199,6 +241,55 @@ describe('web e2e: shipped default web search', () => {
|
||||
await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('scrolls the capped source list inside the fixed-height container', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-search-sources-scroll'))
|
||||
const row = page.locator('[data-tool="web_search"] [data-expandable]').first()
|
||||
await row.click()
|
||||
await expect.poll(() => row.getAttribute('aria-expanded'), { timeout: 5_000 }).toBe('true')
|
||||
|
||||
const card = page.locator('[data-web="search"]')
|
||||
const sources = card.locator('ol')
|
||||
await sources.waitFor({ timeout: 10_000 })
|
||||
// The card draws exactly the sources the model saw: the seam's cap, not the
|
||||
// provider's list length.
|
||||
expect(await sources.locator('li').count()).toBe(WEB_SEARCH_MAX_RESULTS)
|
||||
// The list is complete in the DOM, so the card carries no expand control.
|
||||
expect(await card.locator('button').count()).toBe(0)
|
||||
expect(await card.getByText('来源列表已截断').isVisible()).toBe(true)
|
||||
|
||||
const geometry = await sources.evaluate((element) => {
|
||||
const computed = getComputedStyle(element)
|
||||
return {
|
||||
maxHeight: computed.maxHeight,
|
||||
overflowY: computed.overflowY,
|
||||
scrollHeight: element.scrollHeight,
|
||||
clientHeight: element.clientHeight,
|
||||
}
|
||||
})
|
||||
expect(geometry.maxHeight).toBe('320px')
|
||||
expect(geometry.overflowY).toBe('auto')
|
||||
expect(geometry.scrollHeight).toBeGreaterThan(geometry.clientHeight)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('reserves marker room a scroll container cannot clip back', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-search-marker-room'))
|
||||
// `overflow-y: auto` clips inline-start overflow with no way to scroll it
|
||||
// back, and markers are right-aligned to the content edge, so a marker wider
|
||||
// than `padding-left` silently loses its leading digits. `searchMaxResults`
|
||||
// is an unbounded positive integer, so measure the widest three-digit marker
|
||||
// in the list's own font and require the shipped padding to hold it.
|
||||
const marker = await page.locator('[data-web="search"] ol').evaluate((element) => {
|
||||
const probe = document.createElement('span')
|
||||
probe.style.cssText = 'position:absolute;visibility:hidden;white-space:pre;font:inherit'
|
||||
probe.textContent = '999. '
|
||||
element.append(probe)
|
||||
const widest = probe.getBoundingClientRect().width
|
||||
probe.remove()
|
||||
return { widest, paddingLeft: parseFloat(getComputedStyle(element).paddingLeft) }
|
||||
})
|
||||
expect(marker.paddingLeft).toBeGreaterThanOrEqual(marker.widest)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('stayed clean and kept the exact fixture inventory', async () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
|
||||
Reference in New Issue
Block a user