feat(dsh): adapt to DSH 0.1.5-alpha.1
Move the plugin onto the DSH 0.1.5-alpha.1 baseline (peer floor ^0.1.5-alpha.1, CI pinned to the alpha dist-tag) and fix the two host contract changes that alpha line introduced. Baseline: - all @deepseek-ai/dsh-* peer/dev deps -> 0.1.5-alpha.1; optional peer @deepseek-ai/dsh-client-ui-sidebar-right (declared, unused until the native-surface phase); devDep @deepseek-ai/dsh-agent-loop (inbox fold) - dsh.plugin.json engines >=0.1.5-alpha.1; version 0.19.0-alpha.0; SIDEBAR_SERVICE_VERSION follows Live assistant stream (assistant/chunk is gone): - 0.1.5 publishes in-flight model deltas as process-local agent/assistant-stream frames instead of durable assistant/chunk events and embeds the settled stream in assistant/message / assistant/attempt. src/assistant-live.ts folds the frames into a bounded per-session buffer; sidechat.events serves the current attempt as `live` rows (replaced every poll, never a delta) and the transcript renders them; the inherited in-progress snapshot reads message content / attempt stream / live frames. Cold session reads (persistence.inspect is gone): - 0.1.5 reads a stored session through an explicit read handle (open(id,'read') -> handle.read() -> close()). src/session-store.ts owns that protocol; every cold read (sidechat transcript, cold-resume preset, session.cwd, changes replay) goes through it. Dead code and docs: - drop the remote.session.openWorkspacePath interception: 0.1.5's chat opens files through ctx.sidebarRight.openResource instead, so the wrapper had no caller and would hijack the host's "open in app" gesture; the interceptOpenPath pref, its settings row and its locale keys go with it - AGENTS.md §3 + external-plugin-guide.md describe the new baseline - vitest setup bridges Node's localStorage accessor to jsdom's store (the pre-existing 26 failures on Node >= 22) - desktop-layout e2e seeds through host RPC and types into the Lexical composer (0.1.5 replaced the textarea) instead of driving the directory picker Verified: pnpm typecheck / lint / build / test (124 files, 1327 tests) / check:consumer-types, plus pnpm test:mount against a real @deepseek-ai/dsh@0.1.5-alpha.1 host (18/18).
This commit is contained in:
@@ -2,14 +2,16 @@
|
||||
import { mkdirSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { expect, test, type Page } from '@playwright/test'
|
||||
import { PAGE_URL } from './host'
|
||||
import { expect, test, type APIRequestContext, type Page } from '@playwright/test'
|
||||
import { PAGE_URL, createHostApi, hostRpc } from './host'
|
||||
|
||||
const WORKSPACE_PATH = process.env.DSH_E2E_DESKTOP_WORKSPACE
|
||||
?? (process.env.DSH_E2E_WORKSPACE === undefined
|
||||
? join(homedir(), 'dsh-e2e-desktop-layout-workspace')
|
||||
: `${process.env.DSH_E2E_WORKSPACE}-desktop-layout`)
|
||||
|
||||
let api: APIRequestContext
|
||||
|
||||
async function dismissOnboarding(page: Page): Promise<void> {
|
||||
await expect
|
||||
.poll(() => page.getByRole('button', { name: /^(Continue|Configure later|继续|稍后配置)$/ }).count(), { timeout: 60_000 })
|
||||
@@ -28,8 +30,20 @@ async function dismissOnboarding(page: Page): Promise<void> {
|
||||
throw new Error('onboarding takeovers did not settle after visible dismissal')
|
||||
}
|
||||
|
||||
test.beforeAll(() => {
|
||||
test.beforeAll(async () => {
|
||||
mkdirSync(WORKSPACE_PATH, { recursive: true })
|
||||
// Seed one workspace + one session through the host's own RPC surface (the
|
||||
// calls the UI makes) instead of driving the workspace picker: DSH 0.1.5
|
||||
// ships BOTH a browser directory dialog and an OS-native chooser and picks
|
||||
// one by platform, so a UI-driven flow only works on hosts whose chooser
|
||||
// renders in the page.
|
||||
api = await createHostApi()
|
||||
const workspace = await hostRpc<{ workspace: { workspaceId: string } }>(api, 'workspace.create', { path: WORKSPACE_PATH })
|
||||
await hostRpc(api, 'session.create', { workspaceId: workspace.value.workspace.workspaceId })
|
||||
})
|
||||
|
||||
test.afterAll(async () => {
|
||||
await api?.dispose()
|
||||
})
|
||||
|
||||
test('right panel keeps desktop session actions in their header positions', async ({ page }) => {
|
||||
@@ -45,38 +59,37 @@ test('right panel keeps desktop session actions in their header positions', asyn
|
||||
await page.goto(PAGE_URL, { waitUntil: 'domcontentloaded' })
|
||||
await dismissOnboarding(page)
|
||||
|
||||
await page.getByRole('button', { name: /^(Choose workspace|选择工作区)$/ }).click()
|
||||
const picker = page.getByRole('dialog', { name: /^(Select Workspace Directory|选择工作区目录)$/ })
|
||||
await expect(picker).toBeVisible()
|
||||
await picker.getByRole('button', { name: /^(Edit path|编辑路径)$/ }).click()
|
||||
const pathInput = picker.getByRole('textbox', { name: /^(Edit path|编辑路径)$/ })
|
||||
await pathInput.fill(WORKSPACE_PATH)
|
||||
await pathInput.press('Enter')
|
||||
await expect(pathInput).toBeHidden()
|
||||
await picker.getByRole('button', { name: /^(Open|打开)$/ }).click()
|
||||
|
||||
// Target the ACTIVE composer by its accessible name. DSH 0.1.2-alpha hosts
|
||||
// keep an INERT `data-composer-input` "Choose workspace" textbox (with
|
||||
// contenteditable="false") later in DOM order than the real composer — a
|
||||
// bare `getByRole('textbox').last()` resolved to that one and fill() threw
|
||||
// ("not an <input>, <textarea> or [contenteditable] element").
|
||||
// The seeded blank session opens with its workspace already attached (the
|
||||
// chip shows the workspace title), so the hero composer is live without
|
||||
// touching the workspace picker at all. One message gives the header its
|
||||
// real actions, which this spec measures.
|
||||
const composer = page.getByRole('textbox', { name: /^(Describe what you want to build|描述你想要构建)/ })
|
||||
await expect(composer).toBeVisible()
|
||||
await composer.fill('Create a desktop side-card layout test session.')
|
||||
await page.getByRole('button', { name: /^(Send message|发送消息)$/ }).click()
|
||||
// DSH 0.1.5's composer is a Lexical contenteditable: fill() writes the DOM
|
||||
// but not the editor model, so the send button would stay disabled — type
|
||||
// real keys instead.
|
||||
await composer.click()
|
||||
await page.keyboard.type('Create a desktop side-card layout test session.')
|
||||
const send = page.getByRole('button', { name: /^(Send message|发送消息)$/ })
|
||||
await expect(send).toBeEnabled({ timeout: 30_000 })
|
||||
await send.click()
|
||||
const sessionLog = page.getByRole('button', { name: 'Session log', exact: true })
|
||||
const sessionLogLabel = sessionLog.getByText('Session log', { exact: true })
|
||||
await expect(sessionLogLabel, 'the desktop session-log action starts as a text button').toBeVisible({ timeout: 30_000 })
|
||||
|
||||
const root = page.locator('#root')
|
||||
const frame = page.locator('#root [data-dsh-frame], #root > [data-slot="root"] > div').first()
|
||||
const appSidebarExpanded = root.getByRole('button', { name: /^(Collapse sidebar|收起侧边栏)$/ })
|
||||
const sidebar = page.locator('[data-dsh-better-sidebar]')
|
||||
const sessionLog = page.getByRole('button', { name: 'Session log', exact: true })
|
||||
const sessionLogLabel = sessionLog.getByText('Session log', { exact: true })
|
||||
await expect(sessionLogLabel, 'the desktop session-log action starts as a text button').toBeVisible({ timeout: 30_000 })
|
||||
await expect(appSidebarExpanded, 'the desktop frame starts with its app sidebar expanded').toBeVisible()
|
||||
const beforeRoot = await root.boundingBox()
|
||||
const beforeFrame = await frame.boundingBox()
|
||||
expect(beforeRoot).not.toBeNull()
|
||||
expect(beforeFrame).not.toBeNull()
|
||||
// The header action's own width is the baseline: the assertion below is
|
||||
// "the panel does not squeeze it", not a host-styling constant.
|
||||
const beforeSessionLog = await sessionLog.boundingBox()
|
||||
expect(beforeSessionLog).not.toBeNull()
|
||||
|
||||
await sidebar.getByRole('button', { name: /^(Expand sidebar|展开侧边栏)$/ }).click()
|
||||
const panel = page.locator('[data-dsh-panel]:not([data-dsh-bottom-panel])')
|
||||
@@ -103,7 +116,10 @@ test('right panel keeps desktop session actions in their header positions', asyn
|
||||
const [sessionLogBox, panelBox] = await Promise.all([sessionLog.boundingBox(), panel.boundingBox()])
|
||||
expect(sessionLogBox).not.toBeNull()
|
||||
expect(panelBox).not.toBeNull()
|
||||
expect(sessionLogBox!.width, 'the desktop session-log action must keep its full button width').toBeGreaterThanOrEqual(110)
|
||||
expect(
|
||||
sessionLogBox!.width,
|
||||
'the desktop session-log action must keep its full button width',
|
||||
).toBeGreaterThanOrEqual(beforeSessionLog!.width - 1)
|
||||
expect(sessionLogBox!.x + sessionLogBox!.width, 'the session-log action must not overlap the plugin panel chrome').toBeLessThanOrEqual(panelBox!.x - 8)
|
||||
expect(pageErrors).toEqual([])
|
||||
expect(consoleErrors).toEqual([])
|
||||
|
||||
Reference in New Issue
Block a user