-
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 1 commit
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 sourceSessions = sourceType | ||
| const workspaceSessions = workspaceCwd | ||
| ? scenario.sessions.filter( | ||
| (session) => session.workspaceCwd === workspaceCwd, | ||
| ) | ||
| : scenario.sessions; | ||
| const sourceSessions = sourceType | ||
| ? 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 | undefined { | ||
| const workspaceMatch = path.match( | ||
| /^\/workspaces\/([^/]+)\/sessions(?:\/live-state)?\/?$/, | ||
| ); | ||
| if (workspaceMatch) return decodeURIComponent(workspaceMatch[1]); | ||
|
|
||
| const legacyMatch = path.match(/^\/workspace\/(.+)\/sessions\/?$/); | ||
| return legacyMatch ? decodeURIComponent(legacyMatch[1]) : undefined; | ||
| } | ||
|
|
||
| function isDaemonPath(path: string): boolean { | ||
| return ( | ||
| path === '/health' || | ||
|
|
@@ -1009,10 +1025,14 @@ 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) => !workspaceCwd || session.workspaceCwd === workspaceCwd, | ||
| ) | ||
|
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 newly added workspace scoping on the live-state route has no test that can tell scoped from unscoped or empty responses. The only spec enabling — qwen3.8-max via Qwen Code /review (v0.22.2) |
||
| .filter( | ||
| (session) => | ||
| (session.clientCount ?? 0) > 0 || | ||
|
|
@@ -1035,8 +1055,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,32 @@ 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) |
||||||||||||||||||||
| capabilities: { | ||||||||||||||||||||
| workspaces: [ | ||||||||||||||||||||
| { | ||||||||||||||||||||
|
|
@@ -798,7 +821,7 @@ for (const theme of THEMES) { | |||||||||||||||||||
| }, | ||||||||||||||||||||
| { | ||||||||||||||||||||
| id: 'ws-api', | ||||||||||||||||||||
| cwd: '/tmp/qwen-api-service', | ||||||||||||||||||||
| cwd: secondaryCwd, | ||||||||||||||||||||
| primary: false, | ||||||||||||||||||||
| trusted: true, | ||||||||||||||||||||
| }, | ||||||||||||||||||||
|
|
@@ -823,7 +846,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] Workspace scoping fails open on guard/extractor drift.
workspaceCwdFromSessionsPathreturnsundefinedon a non-matching path, and both consumers — the live-state handler's!workspaceCwd ||fallback andfilterScenarioSessions'workspaceCwd ? … : scenario.sessionsbranch — then silently serve the unscoped cross-workspace catalog, the exact behavior this PR exists to remove. Both fallbacks are dead today (every route guard regex is a subset of the helper's patterns), but this file already carries three parallel copies of the route shapes (isDaemonPath,isDaemonRoute,handleDaemonRoute), so guard/extractor drift is its known failure mode: if a new sessions path shape is added to a route guard without extending this helper, the handler silently returns sessions from every workspace again, and the symptom surfaces as Playwright strict-mode duplicate-name failures in visual specs — a recurrence of the bug this PR fixes — many files away from the cause. Making the helper total turns that silent re-leak into a loud failure at the cause:…plus deleting the
!workspaceCwd ||fallback in the live-state handler and the unscoped branch infilterScenarioSessions. Every call site sits behind a guard that guarantees a cwd segment, so the throw fires precisely when guard and extractor disagree.— qwen3.8-max via Qwen Code /review (v0.22.2)