Files
deepseek-harness/apps/web/tests/plugin-recommendations.e2e.ts
T
ZiyaandZiyaZhang d6d51be4cb feat(experimental): 把个性化页面和插件推荐合并为实验性插件「个性化推荐」 (#7034)
* feat(experimental): ship the two card experiments as one Personalized recommendations bundle

@deepseek-ai/dsh-personalized-recommendations inserts the action card
service, its browser half, Personalized pages and Plugin recommendations
with their card bodies under the existing row ids. As the only layer that
declares those ids, the Plugins page addresses and switches each row,
including the two shared card rows that were read-only while two bundles
inserted them.

The two experiment packages stop being bundles; the new bundle takes their
place in the official lists and the dsh installation, with the personalized
pages icon, localized title and description, and a feature preview of both
cards. The two feature rows describe what they do on the bundle page.

* fix(experimental): drop the model reason line from plugin recommendation cards

The reason follows the language of the user's question while the catalog
description below it follows the interface language, so the two lines could
differ in language and largely repeat each other; the answer already gives
the reason. The card keeps the catalog title and description, both in the
interface language, as its details target.

* test(web): refresh the card fixtures for the Personalized recommendations bundle

Both card replays enable the new bundle, so each Session lists both card
skills, and each skill's catalog lists Personalized recommendations in place
of the two former bundles. The plugin card goldens lose the reason line.

---------

Co-authored-by: ZiyaZhang <199893125+ZiyaZhang@users.noreply.github.com>
2026-10-23 07:55:49 +00:00

88 lines
5.0 KiB
TypeScript

/** Replay model output through the shipped Web profile and decide its real propose_action card. */
import { readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { chromium, type Browser, type Page } from 'playwright'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import type {} from '@deepseek-ai/dsh-plugin-manager'
import {
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
fixtureUserPrompts, launchWebScaffold, watchConsole, type WebScaffold,
} from './scaffold.ts'
import { connectFreshWorkspace, newEnglishPage } from './support.ts'
const directory = fileURLToPath(new URL('../../../snapshots/web/plugin-recommendations', import.meta.url))
const fixture = join(directory, 'session.v4.jsonl')
const voiceBundle = '@deepseek-ai/dsh-experimental-voice-input-bundle'
describe('official plugin recommendation in a conversation', () => {
let scaffold: WebScaffold
let browser: Browser
let page: Page
beforeAll(async () => {
scaffold = await launchWebScaffold({ replayFixture: fixture, compareReplaySession: true,
profile: { packages: [{ dir: fileURLToPath(new URL('../../../packages/experimental/personalized-recommendations', import.meta.url)), enabled: true }] },
})
browser = await chromium.launch()
page = await newEnglishPage(browser)
})
afterAll(async () => {
await browser?.close()
await scaffold?.close()
})
it('offers the propose_action call as one card below the answer, and reports a dismissal on the next request', async () => {
const tripwire = watchConsole(page)
await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
await connectFreshWorkspace(page, scaffold.workspaceCwd)
const input = page.locator('[data-composer-input]').first()
const settled = scaffold.whenTurnSettled()
const prompts = fixtureUserPrompts(await readFile(fixture, 'utf8'))
await input.fill(prompts[0]!)
await input.press('Enter')
await settled
const card = page.locator('[data-action-card]')
await card.waitFor({ state: 'visible' })
expect(await card.count()).toBe(1)
expect(await page.locator('[data-turn-tail] [data-action-card][data-action="enable_official_plugin"]').count()).toBe(1)
const answer = page.getByText('Voice input transcribes your recordings locally into the composer. You can enable it below.', { exact: true })
const answerBox = await answer.boundingBox()
const cardBox = await card.boundingBox()
expect(answerBox).not.toBeNull()
expect(cardBox!.y).toBeGreaterThanOrEqual(answerBox!.y + answerBox!.height)
expect(await page.getByText('enable_official_plugin').count()).toBe(0)
await card.getByText('Voice input', { exact: true }).waitFor()
expect(await card.getByRole('button', { name: 'Enable', exact: true }).isEnabled()).toBe(true)
expect((await scaffold.ctx.pluginManager.listBundles()).find(bundle => bundle.name === voiceBundle)?.enabled).toBe(false)
await compareOrRefreshGolden(join(directory, 'offered.expected.md'), await captureStableAria(page, '[data-action-card]', scaffold.workspaceCwd), scaffold.mode)
await page.setViewportSize({ width: 620, height: 900 })
expect(await card.evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true)
await card.getByRole('button', { name: 'Not now', exact: true }).click()
await card.getByRole('status').getByText('Dismissed', { exact: true }).waitFor()
const warnings = tripwire.warnings.length
await page.reload({ waitUntil: 'load' })
await card.getByRole('status').getByText('Dismissed', { exact: true }).waitFor()
acknowledgeReloadConnectionLoss(tripwire, warnings)
expect(await card.count()).toBe(1)
expect(await card.getByRole('button').count()).toBe(1)
expect((await scaffold.ctx.pluginManager.listBundles()).find(bundle => bundle.name === voiceBundle)?.enabled).toBe(false)
await compareOrRefreshGolden(join(directory, 'dismissed.expected.md'), await captureStableAria(page, '[data-action-card]', scaffold.workspaceCwd), scaffold.mode)
const statusSettled = scaffold.whenTurnSettled()
await input.fill(prompts[1]!)
await input.press('Enter')
await statusSettled
await page.getByText('Voice input is not enabled: you dismissed the card.', { exact: true }).waitFor()
expect(await card.count()).toBe(1)
await card.getByRole('button', { name: 'View details: Voice input', exact: true }).click()
await page.locator(`[data-plugin-detail="${voiceBundle}"]`).waitFor({ state: 'visible' })
expect((await scaffold.ctx.pluginManager.listBundles()).find(bundle => bundle.name === voiceBundle)?.enabled).toBe(false)
expect(tripwire.pageErrors).toEqual([])
expect(tripwire.warnings).toEqual([])
if (scaffold.mode === 'replay') await assertFixtureInventory(directory, [
'session.v4.jsonl', 'offered.expected.md', 'dismissed.expected.md', 'system-prompt.expected.md', 'tool-schemas.expected.json',
])
})
})