-
Notifications
You must be signed in to change notification settings - Fork 3k
fix(web-shell): scope mock sessions by workspace #10273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
3498951
77a0a2a
8ef6f9d
8dcc622
c682cf2
766e904
63df202
da020a9
0c8a656
fa9786d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -632,21 +632,37 @@ function readRequestBody(raw: string | null): unknown { | |
| function filterScenarioSessions( | ||
| scenario: WebShellDaemonScenario, | ||
| searchParams: URLSearchParams, | ||
| workspaceCwd: string, | ||
| ): DaemonSessionSummary[] { | ||
| const group = searchParams.get('group'); | ||
| const sourceType = searchParams.get('sourceType'); | ||
| const workspaceSessions = scenario.sessions.filter( | ||
| (session) => session.workspaceCwd === workspaceCwd, | ||
| ); | ||
| const sourceSessions = sourceType | ||
| ? scenario.sessions.filter( | ||
| ? workspaceSessions.filter( | ||
| (session) => | ||
| session.sourceType === sourceType || | ||
| (sourceType === 'default' && session.sourceType === undefined), | ||
| ) | ||
| : scenario.sessions; | ||
| : workspaceSessions; | ||
| return group === 'pinned' | ||
| ? sourceSessions.filter((session) => Boolean(session.isPinned)) | ||
| : sourceSessions; | ||
| } | ||
|
|
||
| function workspaceCwdFromSessionsPath(path: string): string { | ||
| const workspaceMatch = path.match( | ||
| /^\/workspaces\/([^/]+)\/sessions(?:\/live-state)?\/?$/, | ||
| ); | ||
| if (workspaceMatch) return decodeURIComponent(workspaceMatch[1]); | ||
|
|
||
| const legacyMatch = path.match(/^\/workspace\/(.+)\/sessions\/?$/); | ||
| if (legacyMatch) return decodeURIComponent(legacyMatch[1]); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] R1-1: (fix-induced) The round-2 fix closed the round-1 input — the extractor now throws instead of returning If a future edit widens or adds a sessions route guard without touching this helper, Match-and-capture in one place: use the capture groups of the guard regexes themselves (hoist the match into the handler and drop the separate helper), and align the legacy pattern to 中文说明[Suggestion] R1-1:(由上轮修复引入)第 2 轮的修复关闭了第 1 轮报告的输入——提取函数现在抛出异常而不是返回 如果未来某个改动拓宽或新增了 sessions 路由守卫却没有同步修改这个辅助函数, 建议在一处完成匹配和捕获:使用守卫正则自身的捕获组(把匹配提升到 handler 里,删掉独立的辅助函数),并把 legacy 模式对齐为 — qwen3.8-max via Qwen Code /review (v0.22.2) |
||
|
|
||
| throw new Error(`Unrecognized sessions path: ${path}`); | ||
| } | ||
|
|
||
| function isDaemonPath(path: string): boolean { | ||
| return ( | ||
| path === '/health' || | ||
|
|
@@ -1009,10 +1025,12 @@ async function handleDaemonRoute( | |
| method === 'GET' && | ||
| /^\/workspaces\/[^/]+\/sessions\/live-state\/?$/.test(path) | ||
| ) { | ||
| const workspaceCwd = workspaceCwdFromSessionsPath(path); | ||
| await json(route, { | ||
| v: 1, | ||
| catalogVersion: scenario.sessionCatalogVersion, | ||
| sessions: scenario.sessions | ||
| .filter((session) => session.workspaceCwd === workspaceCwd) | ||
| .filter( | ||
| (session) => | ||
| (session.clientCount ?? 0) > 0 || | ||
|
|
@@ -1035,8 +1053,9 @@ async function handleDaemonRoute( | |
| (/^\/workspace\/.+\/sessions\/?$/.test(path) || | ||
| /^\/workspaces\/[^/]+\/sessions\/?$/.test(path)) | ||
| ) { | ||
| const workspaceCwd = workspaceCwdFromSessionsPath(path); | ||
| await json(route, { | ||
| sessions: filterScenarioSessions(scenario, searchParams), | ||
| sessions: filterScenarioSessions(scenario, searchParams, workspaceCwd), | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,7 +5,7 @@ | |||||||||||||||||||
| */ | ||||||||||||||||||||
|
|
||||||||||||||||||||
| import { expect, test } from '@playwright/test'; | ||||||||||||||||||||
| import type { DaemonEvent } from '@qwen-code/sdk/daemon'; | ||||||||||||||||||||
| import type { DaemonEvent, DaemonSessionSummary } from '@qwen-code/sdk/daemon'; | ||||||||||||||||||||
| import { | ||||||||||||||||||||
| assistantTextEvent, | ||||||||||||||||||||
| createWebShellDaemonScenario, | ||||||||||||||||||||
|
|
@@ -785,9 +785,33 @@ for (const theme of THEMES) { | |||||||||||||||||||
| // turn this into a cryptic "not visible" failure. | ||||||||||||||||||||
| const primaryCwd = '/tmp/qwen-web-shell-e2e'; | ||||||||||||||||||||
| const primarySessionName = 'Run auth migration'; | ||||||||||||||||||||
| const secondaryCwd = '/tmp/qwen-api-service'; | ||||||||||||||||||||
| const secondarySessionName = 'Audit API retries'; | ||||||||||||||||||||
| const sessions = [ | ||||||||||||||||||||
| { | ||||||||||||||||||||
| sessionId: 'workspace-primary-session', | ||||||||||||||||||||
| workspaceCwd: primaryCwd, | ||||||||||||||||||||
| createdAt: '2026-07-03T00:00:00.000Z', | ||||||||||||||||||||
| updatedAt: '2026-07-03T00:00:00.000Z', | ||||||||||||||||||||
| displayName: primarySessionName, | ||||||||||||||||||||
| clientCount: 1, | ||||||||||||||||||||
| hasActivePrompt: false, | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| { | ||||||||||||||||||||
| sessionId: 'workspace-secondary-session', | ||||||||||||||||||||
| workspaceCwd: secondaryCwd, | ||||||||||||||||||||
| createdAt: '2026-07-03T00:00:00.000Z', | ||||||||||||||||||||
| updatedAt: '2026-07-03T00:00:00.000Z', | ||||||||||||||||||||
| displayName: secondarySessionName, | ||||||||||||||||||||
| clientCount: 0, | ||||||||||||||||||||
| hasActivePrompt: false, | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| ] satisfies DaemonSessionSummary[]; | ||||||||||||||||||||
| const scenario = createWebShellDaemonScenario({ | ||||||||||||||||||||
| workspaceCwd: primaryCwd, | ||||||||||||||||||||
| displayName: primarySessionName, | ||||||||||||||||||||
| sessions, | ||||||||||||||||||||
|
Comment on lines
810
to
+813
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The scenario overrides
Suggested change
— qwen3.8-max via Qwen Code /review (v0.22.2) |
||||||||||||||||||||
| sessionId: 'workspace-primary-session', | ||||||||||||||||||||
| capabilities: { | ||||||||||||||||||||
| workspaces: [ | ||||||||||||||||||||
| { | ||||||||||||||||||||
|
|
@@ -798,7 +822,7 @@ for (const theme of THEMES) { | |||||||||||||||||||
| }, | ||||||||||||||||||||
| { | ||||||||||||||||||||
| id: 'ws-api', | ||||||||||||||||||||
| cwd: '/tmp/qwen-api-service', | ||||||||||||||||||||
| cwd: secondaryCwd, | ||||||||||||||||||||
| primary: false, | ||||||||||||||||||||
| trusted: true, | ||||||||||||||||||||
| }, | ||||||||||||||||||||
|
|
@@ -823,7 +847,8 @@ for (const theme of THEMES) { | |||||||||||||||||||
| // per-workspace fetch. Wait for the loaded session's row before capturing | ||||||||||||||||||||
| // so the async load has settled — otherwise the row list races the | ||||||||||||||||||||
| // screenshot and the capture differs between runs. | ||||||||||||||||||||
| await expect(sidebar.getByText(primarySessionName)).toBeVisible(); | ||||||||||||||||||||
| await expect(sidebar.getByText(primarySessionName)).toHaveCount(1); | ||||||||||||||||||||
| await expect(sidebar.getByText(secondarySessionName)).toHaveCount(1); | ||||||||||||||||||||
| await captureScreenshot(page, `workspace-sidebar-${theme}`); | ||||||||||||||||||||
| }); | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Suggestion] R4-1: The workspace scoping added here for the full-catalog sessions routes has no direct test, and its interaction with the
group === 'pinned'branch is exercised by no multi-workspace scenario. Live-state scoping has a dedicated direct-fetch test (scopes live-state sessions to the requested workspace), but no spec fetches/workspaces/:cwd/sessionsor.../sessions?group=pinnedor asserts their response content: the onlyisPinnedfixture (web-shell.channels.spec.ts) is single-workspace, and the two-workspaceworkspace sidebarvisual test seeds no pinned sessions, so the pinned bucket is never fetched across workspaces. If a future edit offilterScenarioSessionsbypassed the workspace filter on the pinned branch (filteringscenario.sessionsinstead ofworkspaceSessionswhengroup === 'pinned'), nothing would go red — the mock would silently return other workspaces' pinned sessions for a workspace-scoped request, re-introducing exactly the cross-workspace duplication this PR exists to eliminate.Consider adding a sibling to
scopes live-state sessions to the requested workspaceinweb-shell.session-live-state.spec.ts: a two-workspace scenario where the non-requested workspace also has anisPinnedsession, then directpage.evaluatefetches of/workspaces/${encodeURIComponent(cwd)}/sessionsand.../sessions?group=pinnedasserting the exact returned sessionId lists contain only the requested workspace's sessions. The new test must go red if the workspace filter is skipped on thegroup === 'pinned'orsourceTypebranches — prove it with the mutant that filters unscopedscenario.sessionson the pinned branch.中文说明
[建议] R4-1:这里为全量 catalog sessions 路由新增的 workspace 过滤没有直接测试,它与
group === 'pinned'分支的组合也没有任何多 workspace 场景覆盖。live-state 过滤有专门的直接请求测试(scopes live-state sessions to the requested workspace),但没有任何 spec 直接请求/workspaces/:cwd/sessions或.../sessions?group=pinned并断言响应内容:唯一的isPinnedfixture(web-shell.channels.spec.ts)是单 workspace 的,双 workspace 的workspace sidebar可视化测试也没有准备 pinned session,因此 pinned bucket 从未在跨 workspace 场景下被请求。如果未来对filterScenarioSessions的修改绕过了 pinned 分支上的 workspace 过滤(比如在group === 'pinned'时过滤scenario.sessions而不是workspaceSessions),不会有任何测试变红——mock 会悄悄为按 workspace 的请求返回其他 workspace 的 pinned sessions,重新引入这个 PR 要消除的跨 workspace 重复。建议在
web-shell.session-live-state.spec.ts中增加一个scopes live-state sessions to the requested workspace的同族测试:双 workspace 场景,其中未被请求的 workspace 还有一个isPinnedsession,然后通过page.evaluate直接请求/workspaces/${encodeURIComponent(cwd)}/sessions和.../sessions?group=pinned,断言返回的 sessionId 列表只包含被请求 workspace 的 session。如果在group === 'pinned'或sourceType分支上跳过 workspace 过滤,新测试必须变红——可以用「在 pinned 分支上过滤未过滤的scenario.sessions」这个变异体来证明。— qwen3.8-max via Qwen Code /review (v0.22.2)