mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
test(web): cover produced-folder single click
This commit is contained in:
@@ -33,4 +33,4 @@ Two distinct defects sat behind that. The transcript never said what a turn had
|
||||
|
||||
## Consequences
|
||||
|
||||
Every existing file affordance changed at once: write, edit, read, and the generic single-file card all reach `openFile`, so the link fix and the browser preference apply to all of them without a per-row change. The keyless web lane (`apps/web/tests/produced-files.e2e.ts`) cold-seeds ten successful writes and pins two leading chips, the exact `+ 8 files` remainder, one-line geometry, and the capability-present folder action in the assembled application; it deliberately does not click, because the click hands a path to the Host's opener and would launch a real application on the machine running the suite. A produced file opens as a `file://` document, which cannot `fetch` its own siblings (a multi-file artifact that loads `./data.json` breaks, while `<script src>`, `<img>`, and CSS `@import` are unaffected) — the one capability HTTP serving had that this does not. A client reached over the network still sees the file chips but sees no folder action; clicking a chip cannot open anything because `host.openPath` runs on the Host and is loopback-pinned by the `/api` trust fence. That is the scope decision showing through, not a defect, and it is why the row keeps the full path in its `title` for a reader who can only copy it. A local SSH port forward can make a remote Host look loopback-local, so such deployments must set `nativeOpen: false`; the same override is required whenever the permissive macOS/Windows/WSL probe or a stale Linux display/opener signal would be a false positive. A future operator-visible capability can replace that deployment declaration. Markdown opens in whatever the platform hands `.md`, usually an editor rather than a renderer; rendering it inside the product is a separate, deferred surface.
|
||||
Every existing file affordance changed at once: write, edit, read, and the generic single-file card all reach `openFile`, so the link fix and the browser preference apply to all of them without a per-row change. The keyless web lane (`apps/web/tests/produced-files.e2e.ts`) cold-seeds ten successful writes and pins two leading chips, the exact `+ 8 files` remainder, one-line geometry, and the capability-present folder action in the assembled application; it intercepts the final Host opener so one real browser click must produce exactly one `host.openPath` request without launching a native application on the machine running the suite. A produced file opens as a `file://` document, which cannot `fetch` its own siblings (a multi-file artifact that loads `./data.json` breaks, while `<script src>`, `<img>`, and CSS `@import` are unaffected) — the one capability HTTP serving had that this does not. A client reached over the network still sees the file chips but sees no folder action; clicking a chip cannot open anything because `host.openPath` runs on the Host and is loopback-pinned by the `/api` trust fence. That is the scope decision showing through, not a defect, and it is why the row keeps the full path in its `title` for a reader who can only copy it. A local SSH port forward can make a remote Host look loopback-local, so such deployments must set `nativeOpen: false`; the same override is required whenever the permissive macOS/Windows/WSL probe or a stale Linux display/opener signal would be a false positive. A future operator-visible capability can replace that deployment declaration. Markdown opens in whatever the platform hands `.md`, usually an editor rather than a renderer; rendering it inside the product is a separate, deferred surface.
|
||||
|
||||
@@ -33,4 +33,4 @@ Status: implemented
|
||||
|
||||
## 影响
|
||||
|
||||
现有的每一处文件交互都同时改变了:write、edit、read 与通用单文件卡片都汇到 `openFile`,因此链接可见性修复与浏览器优先策略无需逐行改动即适用于全部。无密钥 web 通道(`apps/web/tests/produced-files.e2e.ts`)冷播种十次成功 write,在组装后的应用中钉住两个开头 chip、精确的 `+ 8 files` 剩余计数、单行几何与能力存在时的文件夹操作;它刻意不点击,因为点击会把路径交给 Host 打开器,从而在跑测试的机器上启动一个真实应用。产出的文件以 `file://` 文档打开,它无法 `fetch` 自己的同级文件(一个加载 `./data.json` 的多文件产物会坏,而 `<script src>`、`<img>` 与 CSS `@import` 不受影响)——这是 HTTP 提供曾有、而此处没有的那一项能力。经网络访问的客户端仍能看到文件 chip,但看不到文件夹操作;点击 chip 无法打开任何东西,因为 `host.openPath` 在 Host 上运行,且被 `/api` 信任 fence 钉在回环。那是范围决定的显现,不是缺陷,也正因如此该行把完整路径保留在 `title` 中,供只能复制它的读者使用。本地 SSH 端口转发会让远端 Host 看似处于本机 loopback,因此这类部署必须设置 `nativeOpen: false`;只要宽松的 macOS/Windows/WSL 探测或过期的 Linux display/opener 信号会造成误报,也同样需要这一覆盖。未来可由一项 operator-visible 能力取代这份部署声明。markdown 会由平台交给 `.md` 的默认处理程序打开,通常是编辑器而非渲染器;在产品内渲染它是另一个被推迟的交互面。
|
||||
现有的每一处文件交互都同时改变了:write、edit、read 与通用单文件卡片都汇到 `openFile`,因此链接可见性修复与浏览器优先策略无需逐行改动即适用于全部。无密钥 web 通道(`apps/web/tests/produced-files.e2e.ts`)冷播种十次成功 write,在组装后的应用中钉住两个开头 chip、精确的 `+ 8 files` 剩余计数、单行几何与能力存在时的文件夹操作;它会截住最后一步 Host 打开器,因此一次真实浏览器点击必须恰好产生一次 `host.openPath` 请求,同时不会在跑测试的机器上启动原生应用。产出的文件以 `file://` 文档打开,它无法 `fetch` 自己的同级文件(一个加载 `./data.json` 的多文件产物会坏,而 `<script src>`、`<img>` 与 CSS `@import` 不受影响)——这是 HTTP 提供曾有、而此处没有的那一项能力。经网络访问的客户端仍能看到文件 chip,但看不到文件夹操作;点击 chip 无法打开任何东西,因为 `host.openPath` 在 Host 上运行,且被 `/api` 信任 fence 钉在回环。那是范围决定的显现,不是缺陷,也正因如此该行把完整路径保留在 `title` 中,供只能复制它的读者使用。本地 SSH 端口转发会让远端 Host 看似处于本机 loopback,因此这类部署必须设置 `nativeOpen: false`;只要宽松的 macOS/Windows/WSL 探测或过期的 Linux display/opener 信号会造成误报,也同样需要这一覆盖。未来可由一项 operator-visible 能力取代这份部署声明。markdown 会由平台交给 `.md` 的默认处理程序打开,通常是编辑器而非渲染器;在产品内渲染它是另一个被推迟的交互面。
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// Web e2e scenario: the single-line produced-files summary a finished turn
|
||||
// ends with. Cold-seeds ten writes (zero model calls), then verifies the real
|
||||
// assembled lane keeps a precise +N and a capability-gated folder handoff.
|
||||
// Clicking is not driven: it would launch a real native application.
|
||||
// The folder request is intercepted so one real browser click can exercise
|
||||
// the full client carrier without launching a native application in CI.
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed, vi } from 'vitest'
|
||||
import { CallId, createAssistantMessage, createToolResultMessage, createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { SESSION_FORMAT_VERSION, Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-session-title'
|
||||
@@ -157,9 +158,27 @@ describe('web e2e: a finished turn ends with the files it produced', () => {
|
||||
expect(await chips.nth(0).innerText()).toBe('关于我.md')
|
||||
expect(await chips.nth(1).innerText()).toBe('index.html')
|
||||
expect(await row.getByText('+ 8 files', { exact: true }).count()).toBe(1)
|
||||
expect(await page.getByRole('button', { name: 'Show in folder', exact: true }).count()).toBe(1)
|
||||
const showFolder = page.getByRole('button', { name: 'Show in folder', exact: true })
|
||||
expect(await showFolder.count()).toBe(1)
|
||||
expect(await page.getByText('Produced', { exact: true }).count()).toBe(1)
|
||||
|
||||
const openPath = vi.spyOn(scaffold.ctx.apiProxy.host, 'openPath')
|
||||
.mockImplementation(async (request, _signal) => ({
|
||||
rpcId: request.rpcId,
|
||||
result: { ok: true, value: { opened: true as const } },
|
||||
}))
|
||||
try {
|
||||
const [response] = await Promise.all([
|
||||
page.waitForResponse(response => new URL(response.url()).pathname === '/api/host.openPath'),
|
||||
showFolder.click({ clickCount: 1 }),
|
||||
])
|
||||
expect(response.status()).toBe(200)
|
||||
expect(openPath).toHaveBeenCalledTimes(1)
|
||||
expect(openPath.mock.calls[0]![0].payload).toEqual({ path: `${scaffold.workspaceCwd}/.` })
|
||||
} finally {
|
||||
openPath.mockRestore()
|
||||
}
|
||||
|
||||
const tops = await row.locator(':scope > *').evaluateAll(elements =>
|
||||
elements.map(element => element.getBoundingClientRect().top))
|
||||
expect(new Set(tops.map(top => Math.round(top))).size).toBe(1)
|
||||
|
||||
Reference in New Issue
Block a user