Merge remote-tracking branch 'origin/master' into worktree/web-model-request-retry
# Conflicts: # apps/cli/README.i18n.yaml # packages/client/ui-conversation/README.i18n.yaml # packages/client/ui-conversation/README.zh.md # packages/client/ui-conversation/tests/chat-branch-tails.spec.tsx
This commit is contained in:
@@ -195,8 +195,8 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => {
|
||||
// only on change, so attempt count is invisible there).
|
||||
expect(sessionEvents.filter(e => e.type === 'llm/retry').length).toBeGreaterThanOrEqual(1)
|
||||
await expect.poll(() => page.getByText('event sourcing', { exact: false }).count(), { timeout: 10_000 }).toBeGreaterThan(0)
|
||||
// Golden of the recovered end-state: indistinguishable from a clean
|
||||
// completion — retries are deliberately invisible in the transcript.
|
||||
// Golden of the recovered end-state: the discarded partial stays absent,
|
||||
// while the settled retry row remains as durable recovery context.
|
||||
const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd)
|
||||
await compareOrRefreshGolden(RETRY_EXPECTED, snapshot, MODE)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
|
||||
@@ -131,6 +131,10 @@ it('projects titles and routes the next turn through the selected model in the b
|
||||
await waitFor(() => { expect(document.title).toBe(`${revisedLabel} — DeepSeek Harness`) })
|
||||
const revised = titleSurfaces(revisedLabel)
|
||||
|
||||
// fx-alpha carries the fixture's resident answerable approval, so the
|
||||
// approval panel has taken over the composer (the real takeover behavior);
|
||||
// answer it to restore the composer chrome before asserting the model seat.
|
||||
fireEvent.click(await screen.findByRole('button', { name: '允许一次' }))
|
||||
const modelTrigger = await screen.findByRole('button', {
|
||||
name: '选择模型,当前 DeepSeek-V4-Flash,推理等级 High',
|
||||
})
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- 'button "Using ONE run_code program: run" [disabled]'
|
||||
- text: · 1 turns
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: "Using ONE run_code program: run bash `echo CODE_ROUND_OK`, then read the file missing.txt catching its error in the program. Return an object with both outcomes. Then reply DONE and stop."
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- 'button "Think The user wants me to write a single `run_code` program that:"':
|
||||
- img
|
||||
- img
|
||||
- text: "Think The user wants me to write a single `run_code` program that:"
|
||||
- button:
|
||||
- img
|
||||
- text: Code Run bash echo and catch missing file read Echo CODE_ROUND_OK
|
||||
- button
|
||||
- text: Read missing.txt
|
||||
- img
|
||||
- text: Code Run bash echo and catch missing file read
|
||||
- img
|
||||
- text: Bash Echo CODE_ROUND_OK Read
|
||||
- button "missing.txt"
|
||||
- button "Think The program ran successfully. Let me now reply DONE as instructed.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The program ran successfully. Let me now reply DONE as instructed.
|
||||
- paragraph: DONE
|
||||
@@ -23,10 +32,12 @@
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read-only" [selected]
|
||||
- option "Read-write"
|
||||
- button "选择模型,当前 deepseek-v4-flash":
|
||||
- text: deepseek-v4-flash
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use only Cordis tools. First" [disabled]
|
||||
- text: · 1 turns
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -13,14 +12,16 @@
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- button "Think The user wants me to:":
|
||||
- img
|
||||
- img
|
||||
- text: "Think The user wants me to:"
|
||||
- button:
|
||||
- img
|
||||
- img
|
||||
- text: Inspect temporary
|
||||
- 'button "Think Good, no temporary plugins running. Now step 2: call cordis_mount with the exact code."':
|
||||
- img
|
||||
- img
|
||||
- text: "Think Good, no temporary plugins running. Now step 2: call cordis_mount with the exact code."
|
||||
- button [expanded]:
|
||||
@@ -29,12 +30,15 @@
|
||||
- button "复制"
|
||||
- code: "return { name: \"snapshot-noop\", apply(ctx) {} }"
|
||||
- 'button "Think The id is \"dyn-1\". Now step 3: call cordis_unmount with that id."':
|
||||
- img
|
||||
- img
|
||||
- text: "Think The id is \"dyn-1\". Now step 3: call cordis_unmount with that id."
|
||||
- button:
|
||||
- img
|
||||
- img
|
||||
- text: Unmount temporary Plugin dyn-1
|
||||
- button "Think All three calls succeeded. I should now reply exactly \"CORDIS_UI_DONE\" and stop.":
|
||||
- img
|
||||
- img
|
||||
- text: Think All three calls succeeded. I should now reply exactly "CORDIS_UI_DONE" and stop.
|
||||
- paragraph: CORDIS_UI_DONE
|
||||
@@ -42,7 +46,12 @@
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read-only" [selected]
|
||||
- option "Read-write"
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the bash tool to" [disabled]
|
||||
- text: · 1 turns
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: "Use the bash tool to run exactly: echo WEB_E2E_OK. Then reply with the single word DONE and stop."
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "Think The user wants me to run a simple bash command and reply with \"DONE\".":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user wants me to run a simple bash command and reply with "DONE".
|
||||
- text: Echo the test string
|
||||
- img
|
||||
- text: Bash Echo the test string
|
||||
- button "Think The command executed successfully and output \"WEB_E2E_OK\". I just need to reply with \"DONE\".":
|
||||
- img
|
||||
- img
|
||||
- text: Think The command executed successfully and output "WEB_E2E_OK". I just need to reply with "DONE".
|
||||
- paragraph: DONE
|
||||
@@ -19,10 +27,12 @@
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read-only" [selected]
|
||||
- option "Read-write"
|
||||
- button "选择模型,当前 deepseek-v4-flash":
|
||||
- text: deepseek-v4-flash
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
- button "New session"
|
||||
- button "Collapse sidebar":
|
||||
- img
|
||||
- button "New session":
|
||||
@@ -27,11 +28,13 @@
|
||||
- textbox "Describe what you want to build"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read-only" [selected]
|
||||
- option "Read-write"
|
||||
- button "选择模型,当前 deepseek-v4-flash":
|
||||
- text: deepseek-v4-flash
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
- text: 详情
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with the single word" [disabled]
|
||||
- text: · 1 turns
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: Reply with the single word LIGHTHOUSE and stop.
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "Think The user wants me to reply with a single word. Let me comply.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user wants me to reply with a single word. Let me comply.
|
||||
- paragraph: LIGHTHOUSE
|
||||
@@ -15,10 +21,12 @@
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read-only" [selected]
|
||||
- option "Read-write"
|
||||
- button "选择模型,当前 deepseek-v4-flash":
|
||||
- text: deepseek-v4-flash
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- text: · 1 turns
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: Reply with a one-sentence description of event sourcing, then stop.
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- paragraph: partial
|
||||
- text: 已停止 0 tokens · 1 turns · 1 steps
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read-only" [selected]
|
||||
- option "Read-write"
|
||||
- button "选择模型,当前 deepseek-v4-flash":
|
||||
- text: deepseek-v4-flash
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- text: · 1 turns
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: Reply with a one-sentence description of event sourcing, then stop.
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read-only" [selected]
|
||||
- option "Read-write"
|
||||
- button "选择模型,当前 deepseek-v4-flash":
|
||||
- text: deepseek-v4-flash
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Reply with a one-sentence description" [disabled]
|
||||
- text: · 1 turns
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: Reply with a one-sentence description of event sourcing, then stop.
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "▸ 上下文注入"
|
||||
- group:
|
||||
- status: 已重试模型请求(1/2) · {{duration}}
|
||||
- button "Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user is asking for a one-sentence description of event sourcing. This is a straightforward knowledge question that doesn't require any skill loading or tool calls.
|
||||
- paragraph: Event sourcing is a pattern where all changes to an application's state are stored as an immutable, append-only sequence of events, rather than persisting only the current state, enabling full auditability, temporal queries, and event-driven architectures.
|
||||
@@ -15,10 +24,12 @@
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read-only" [selected]
|
||||
- option "Read-write"
|
||||
- button "选择模型,当前 deepseek-v4-flash":
|
||||
- text: deepseek-v4-flash
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- text: · 1 turns
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
@@ -14,12 +13,15 @@
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "Think The user wants me to use the ask_user_question tool with specific parameters. Let me do exactly that.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user wants me to use the ask_user_question tool with specific parameters. Let me do exactly that.
|
||||
- button:
|
||||
- img
|
||||
- img
|
||||
- text: "Tool call ask_user_question · {\"questions\": [{\"id\": \"color\", \"question\": \"Which color do you prefer?\", \"header\": \"Pick one\", \"options\": [{\"label\": \"Blue\", \"description\": \"A cool recessive hue that reads as calm and trustworthy in long reading sessions and dense dashboards.\"}, {\"label\": \"Green\", \"description\": \"A restful mid-spectrum hue with the highest perceived brightness, easiest on the eye over long sessions.\"}]}]}"
|
||||
- button "Think The user answered \"Blue\". I should now reply with the single word DONE and stop.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user answered "Blue". I should now reply with the single word DONE and stop.
|
||||
- paragraph: DONE
|
||||
@@ -27,10 +29,12 @@
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read-only" [selected]
|
||||
- option "Read-write"
|
||||
- button "选择模型,当前 deepseek-v4-flash":
|
||||
- text: deepseek-v4-flash
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- text: · 1 turns
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop.
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.
|
||||
- button
|
||||
- button:
|
||||
- img
|
||||
- img
|
||||
- text: "Tool call ask_user_question · {\"questions\": [{\"id\": \"checkpoint\", \"question\": \"Ready to continue?\", \"header\": \"Checkpoint\", \"options\": [{\"label\": \"Yes\"}, {\"label\": \"No\"}]}]} 等待回答(1 题)"
|
||||
- button "▸ 问题内容"
|
||||
- text: 请在原客户端处理(web 端作答后续里程碑提供) cache hit 98% · 7,946 tokens · 1 turns · 1 steps
|
||||
- text: cache hit 98% · 7,946 tokens · 1 turns · 1 steps
|
||||
- region "Ready to continue?":
|
||||
- text: Checkpoint
|
||||
- heading "Ready to continue?" [level=2]
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
- banner:
|
||||
- navigation "Session hierarchy":
|
||||
- button "Use the ask_user_question tool to" [disabled]
|
||||
- text: · 1 turns
|
||||
- tablist:
|
||||
- tab "Chat" [selected]
|
||||
- tab "Trajectory"
|
||||
- tab "Waterfall"
|
||||
- text: Use the ask_user_question tool to ask me exactly one question with id "checkpoint", question "Ready to continue?", header "Checkpoint", and options labeled "Yes" and "No". After I answer, reply with one short sentence acknowledging my answer and stop.
|
||||
- button "复制":
|
||||
- img
|
||||
- button "在新对话中分支":
|
||||
- img
|
||||
- button "编辑":
|
||||
- img
|
||||
- button "Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user wants me to use the ask_user_question tool to ask them a specific question with the given parameters. Let me do exactly that.
|
||||
- button:
|
||||
- img
|
||||
- img
|
||||
- text: "Tool call ask_user_question · {\"questions\": [{\"id\": \"checkpoint\", \"question\": \"Ready to continue?\", \"header\": \"Checkpoint\", \"options\": [{\"label\": \"Yes\"}, {\"label\": \"No\"}]}]} 插话 Interjection: include the word BANANA in your final reply."
|
||||
- button "Think The user selected \"Yes\" and wants me to include the word \"BANANA\" in my final reply. Let me acknowledge their answer.":
|
||||
- img
|
||||
- img
|
||||
- text: Think The user selected "Yes" and wants me to include the word "BANANA" in my final reply. Let me acknowledge their answer.
|
||||
- paragraph: Great, let's move forward. BANANA!
|
||||
@@ -21,10 +29,12 @@
|
||||
- textbox "Message the agent"
|
||||
- button "Add attachment":
|
||||
- img
|
||||
- text: Danger Full Access
|
||||
- combobox "Access mode":
|
||||
- option "Read-only" [selected]
|
||||
- option "Read-write"
|
||||
- button "选择模型,当前 deepseek-v4-flash":
|
||||
- text: deepseek-v4-flash
|
||||
- option "Read Only"
|
||||
- option "Workspace Write"
|
||||
- option "Danger Full Access" [selected]
|
||||
- button "选择模型,当前 DeepSeek-V4-Flash":
|
||||
- text: DeepSeek-V4-Flash
|
||||
- img
|
||||
- button "Send message" [disabled]
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
- dialog "选择工作区目录":
|
||||
- heading "选择工作区目录" [level=2]
|
||||
- navigation:
|
||||
- button "主目录"
|
||||
- img
|
||||
- button "browse-golden"
|
||||
- button "编辑路径"
|
||||
- list:
|
||||
- listitem:
|
||||
- button "alpha":
|
||||
- img
|
||||
- text: alpha
|
||||
- img
|
||||
- listitem:
|
||||
- button "beta":
|
||||
- img
|
||||
- text: beta
|
||||
- img
|
||||
- button "新建文件夹":
|
||||
- img
|
||||
- text: 新建文件夹
|
||||
- button "取消"
|
||||
- button "打开"
|
||||
@@ -37,6 +37,15 @@ const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
|
||||
],
|
||||
},
|
||||
{ id: '@deepseek-ai/dsh-client-ui-trajectory', dir: 'ui-trajectory', url: '/plugins/ui-trajectory.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
|
||||
// Dual-face host package: its browser half fills the directory-flow holes
|
||||
// (the same composition row apps/cli mounts for the node-side backend).
|
||||
{
|
||||
id: '@deepseek-ai/dsh-host-directory-picker-browse',
|
||||
dir: '../host/directory-picker-browse',
|
||||
url: '/plugins/directory-picker-browse.js',
|
||||
rev: 'fx',
|
||||
inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-workspace', '@deepseek-ai/dsh-client-locale'],
|
||||
},
|
||||
]
|
||||
|
||||
const bundles = new Map(PLUGINS.map(plugin => [
|
||||
@@ -174,6 +183,37 @@ it('locks the composer in the New Session view state until a Workspace is chosen
|
||||
`)
|
||||
})
|
||||
|
||||
it('adopts a directory through the composed in-app browse flow and lands in its blank session', async () => {
|
||||
boot('?fixture=empty')
|
||||
|
||||
await findLockedComposer()
|
||||
fireEvent.click(workspaceChip())
|
||||
const menu = await screen.findByRole('menu')
|
||||
// The composed flow package occupies the directory-flow hole, so the
|
||||
// picking affordance is present (no advertised-kind read exists anymore).
|
||||
expect(within(menu).getAllByRole('menuitem').map(item => visibleText(item)))
|
||||
.toEqual(['Open local folder…', 'Create a new workspace'])
|
||||
fireEvent.click(within(menu).getByRole('menuitem', { name: 'Open local folder…' }))
|
||||
// The browse occupant renders the Select Workspace Directory dialog at the
|
||||
// fixture home; select Documents, advance into project, and adopt it.
|
||||
const dialog = await screen.findByRole('dialog', { name: '选择工作区目录' }, { timeout: 10_000 })
|
||||
// Row targeting goes through the visible label text: listitem accessible-name
|
||||
// computation differs across dom-accessibility-api environments, while the
|
||||
// row's name span is stable (clicks bubble to the row button).
|
||||
fireEvent.click(await within(dialog).findByText('Documents', {}, { timeout: 10_000 }))
|
||||
fireEvent.click(await within(dialog).findByText('project', {}, { timeout: 10_000 }))
|
||||
// Open disables while the selection's child listing is in flight; wait for
|
||||
// the enabled state or the click lands on a dead button on slow runners.
|
||||
await waitFor(() => {
|
||||
expect(within(dialog).getByRole<HTMLButtonElement>('button', { name: '打开' }).disabled).toBe(false)
|
||||
}, { timeout: 10_000 })
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '打开' }))
|
||||
await findHeroComposer()
|
||||
await waitFor(() => {
|
||||
expect(visibleText(screen.getByRole('tree', { name: 'Sessions' }))).toContain('project')
|
||||
})
|
||||
})
|
||||
|
||||
it('selects the recent Workspace and opens its blank Session on first load', async () => {
|
||||
boot('?fixture')
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, launchWebScaffold, seedSession, watchConsole,
|
||||
webSnapshotMode, type WebScaffold,
|
||||
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
|
||||
launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { saveFailureShot } from './support.ts'
|
||||
|
||||
@@ -23,6 +23,7 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/workspace-management', i
|
||||
// spec needs any one cold session row, not new recorded content.
|
||||
const SEED = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url))
|
||||
const MODE = webSnapshotMode()
|
||||
const BROWSER_EXPECTED = join(SNAPSHOT_DIR, 'directory-browser.expected.md')
|
||||
const SEED_ID = 'workspace-management-web-e2e'
|
||||
|
||||
describe('web e2e: workspace management (create / rename / flat view / hover card)', () => {
|
||||
@@ -30,14 +31,40 @@ describe('web e2e: workspace management (create / rename / flat view / hover car
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
let pickedDirectory: string | null = null
|
||||
|
||||
/**
|
||||
* Drive the in-app browser to a directory via its path-edit affordance,
|
||||
* confirm it, and wait for the adoption to settle host-side (workspace
|
||||
* registered + the flow's New-Session agent up), so later test steps can't
|
||||
* race the in-flight blank-session attach.
|
||||
*/
|
||||
async function openLocalFolder(path: string, options: { waitForAgent?: boolean } = {}): Promise<void> {
|
||||
const agentsBefore = scaffold.ctx.agents.list().length
|
||||
await page.getByRole('button', { name: 'Create workspace' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Open local folder…' }).click()
|
||||
const dialog = page.getByRole('dialog', { name: '选择工作区目录' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('button', { name: '编辑路径' }).click()
|
||||
await dialog.getByLabel('编辑路径').fill(path)
|
||||
await dialog.getByLabel('编辑路径').press('Enter')
|
||||
await dialog.getByRole('button', { name: '打开' }).click()
|
||||
await dialog.waitFor({ state: 'hidden', timeout: 10_000 })
|
||||
await expect.poll(
|
||||
() => scaffold.ctx.workspace.resolveByPath(path),
|
||||
{ timeout: 10_000 },
|
||||
).not.toBeUndefined()
|
||||
// First adoption births a blank Session+Agent whose workspace attach must
|
||||
// settle before a test may delete the registration; the reuse path (same
|
||||
// canonical cwd already has a blank session) creates no agent, so callers
|
||||
// opt in only where a fresh attach is possible.
|
||||
if (options.waitForAgent === true) {
|
||||
await expect.poll(() => scaffold.ctx.agents.list().length, { timeout: 10_000 })
|
||||
.toBeGreaterThan(agentsBefore)
|
||||
}
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
scaffold.ctx.apiProxy.host.pickDirectory = request => Promise.resolve({
|
||||
rpcId: request.rpcId,
|
||||
result: { ok: true, value: { path: pickedDirectory } },
|
||||
})
|
||||
// Seed one cold session (Ungrouped bucket) for the flat view + hover card.
|
||||
const sessionCwd = join(scaffold.workspaceCwd, 'workspace')
|
||||
await mkdir(sessionCwd, { recursive: true })
|
||||
@@ -137,14 +164,7 @@ describe('web e2e: workspace management (create / rename / flat view / hover car
|
||||
collect()
|
||||
})
|
||||
// Register the scaffold's existing project directory through the real UI.
|
||||
pickedDirectory = scaffold.workspaceCwd
|
||||
await page.getByRole('button', { name: 'Create workspace' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Open local folder…' }).click()
|
||||
|
||||
await expect.poll(
|
||||
() => scaffold.ctx.workspace.resolveByPath(scaffold.workspaceCwd),
|
||||
{ timeout: 10_000 },
|
||||
).not.toBeUndefined()
|
||||
await openLocalFolder(scaffold.workspaceCwd, { waitForAgent: true })
|
||||
const workspace = await scaffold.ctx.workspace.resolveByPath(scaffold.workspaceCwd)
|
||||
if (workspace === undefined) throw new Error('GUI did not register the existing project directory')
|
||||
await workspace.attachSession(SessionId(SEED_ID))
|
||||
@@ -200,9 +220,7 @@ describe('web e2e: workspace management (create / rename / flat view / hover car
|
||||
// Re-registering the exact deleted path immediately, without a reload, is
|
||||
// a supported reversible flow. It creates a fresh Workspace id without
|
||||
// re-adopting the retained Session.
|
||||
pickedDirectory = scaffold.workspaceCwd
|
||||
await page.getByRole('button', { name: 'Create workspace' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Open local folder…' }).click()
|
||||
await openLocalFolder(scaffold.workspaceCwd)
|
||||
await expect.poll(
|
||||
() => scaffold.ctx.workspace.resolveByPath(scaffold.workspaceCwd),
|
||||
{ timeout: 10_000 },
|
||||
@@ -272,9 +290,7 @@ describe('web e2e: workspace management (create / rename / flat view / hover car
|
||||
collect()
|
||||
})
|
||||
|
||||
pickedDirectory = oldPath
|
||||
await page.getByRole('button', { name: 'Create workspace' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Open local folder…' }).click()
|
||||
await openLocalFolder(oldPath)
|
||||
await expect.poll(
|
||||
() => scaffold.ctx.workspace.resolveByPath(oldPath),
|
||||
{ timeout: 10_000 },
|
||||
@@ -330,6 +346,42 @@ describe('web e2e: workspace management (create / rename / flat view / hover car
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 90_000)
|
||||
|
||||
it('matches the directory-browser dialog aria golden at a staged directory', async () => {
|
||||
// A staged subtree under the scaffold cwd keeps the listing deterministic
|
||||
// (normalizeAria scrubs the cwd), and pointing the in-process host's HOME
|
||||
// at the cwd collapses the breadcrumb ancestry into the Home crumb — no
|
||||
// machine-specific path segments or real $HOME contents enter the golden.
|
||||
const staged = join(scaffold.workspaceCwd, 'browse-golden')
|
||||
await mkdir(join(staged, 'alpha'), { recursive: true })
|
||||
await mkdir(join(staged, 'beta'), { recursive: true })
|
||||
// homedir() reads HOME on POSIX and USERPROFILE on Windows: root both
|
||||
// at the scaffold cwd so the golden's ancestry collapses everywhere.
|
||||
const realHome = process.env.HOME
|
||||
const realUserProfile = process.env.USERPROFILE
|
||||
process.env.HOME = scaffold.workspaceCwd
|
||||
process.env.USERPROFILE = scaffold.workspaceCwd
|
||||
try {
|
||||
await page.getByRole('button', { name: 'Create workspace' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Open local folder…' }).click()
|
||||
const dialog = page.getByRole('dialog', { name: '选择工作区目录' })
|
||||
await dialog.waitFor({ timeout: 10_000 })
|
||||
await dialog.getByRole('button', { name: '编辑路径' }).click()
|
||||
await dialog.getByLabel('编辑路径').fill(staged)
|
||||
await dialog.getByLabel('编辑路径').press('Enter')
|
||||
await expect.poll(() => dialog.getByText('alpha', { exact: true }).count(), { timeout: 10_000 }).toBe(1)
|
||||
const snapshot = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(BROWSER_EXPECTED, snapshot, MODE)
|
||||
await dialog.getByRole('button', { name: '取消' }).click()
|
||||
await dialog.waitFor({ state: 'hidden', timeout: 10_000 })
|
||||
} finally {
|
||||
if (realHome === undefined) delete process.env.HOME
|
||||
else process.env.HOME = realHome
|
||||
if (realUserProfile === undefined) delete process.env.USERPROFILE
|
||||
else process.env.USERPROFILE = realUserProfile
|
||||
}
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('shows the session hover card after a dwell on the row', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-ws-hover'))
|
||||
// Expand Ungrouped to reveal the seeded session row, then dwell on it
|
||||
@@ -361,8 +413,8 @@ describe('web e2e: workspace management (create / rename / flat view / hover car
|
||||
|
||||
it.skipIf(MODE === 'record')('issued zero model calls and stayed clean', async () => {
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
// This spec mints no fixture directory contents of its own; the seed it
|
||||
// reuses is owned (and inventory-guarded) by seeded-history.
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['.gitkeep'])
|
||||
// The directory-browser aria golden is this spec's one owned artifact;
|
||||
// the seed it reuses is owned (and inventory-guarded) by seeded-history.
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['.gitkeep', 'directory-browser.expected.md'])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user