fix(desktop): keep cleared session metadata in sync

Reset session messageCount when /clear is confirmed so active headers and sidebars do not keep stale message totals after the transcript is cleared.

Also wait for the restored project chip in the live desktop smoke lane before filling the prompt, preventing the test from racing against EmptySession and creating a default-home session.

Tested: cd desktop && bun run test -- --run src/stores/chatStore.test.ts src/stores/sessionStore.test.ts

Tested: bun test scripts/quality-gate/desktop-smoke/execute.test.ts

Tested: bun run check:desktop

Tested: SKIP_INSTALL=1 MAC_TARGETS=zip desktop/scripts/build-macos-arm64.sh

Tested: bun run quality:smoke --provider-model codingplan:main:codingplan-main

Not-tested: bun run verify

Confidence: high

Scope-risk: narrow
This commit is contained in:
程序员阿江(Relakkes) 2026-07-02 23:06:26 +08:00
parent 3edee33b54
commit 782c32c1fc
6 changed files with 104 additions and 3 deletions

View File

@ -19,6 +19,7 @@ const {
updateTabTitleMock,
updateTabStatusMock,
updateSessionTitleMock,
updateSessionMessageCountMock,
updateSessionPermissionModeMock,
sessionStoreSnapshot,
cliTaskStoreSnapshot,
@ -39,6 +40,7 @@ const {
updateTabTitleMock: vi.fn(),
updateTabStatusMock: vi.fn(),
updateSessionTitleMock: vi.fn(),
updateSessionMessageCountMock: vi.fn(),
updateSessionPermissionModeMock: vi.fn(),
sessionStoreSnapshot: {
sessions: [] as Array<{
@ -105,6 +107,7 @@ vi.mock('./sessionStore', () => ({
getState: () => ({
sessions: sessionStoreSnapshot.sessions,
updateSessionTitle: updateSessionTitleMock,
updateSessionMessageCount: updateSessionMessageCountMock,
updateSessionPermissionMode: updateSessionPermissionModeMock,
}),
},
@ -276,6 +279,7 @@ describe('chatStore history mapping', () => {
updateTabTitleMock.mockReset()
updateTabStatusMock.mockReset()
updateSessionTitleMock.mockReset()
updateSessionMessageCountMock.mockReset()
vi.mocked(sessionsApi.getMessages).mockReset()
vi.mocked(sessionsApi.getMessages).mockResolvedValue({ messages: [] })
sessionStoreSnapshot.sessions = []
@ -3000,6 +3004,8 @@ describe('chatStore history mapping', () => {
expect(session?.streamingResponseChars).toBe(0)
expect(session?.slashCommands).toEqual([])
expect(clearTasksMock).toHaveBeenCalledWith(TEST_SESSION_ID)
expect(updateSessionTitleMock).toHaveBeenCalledWith(TEST_SESSION_ID, 'New Session')
expect(updateSessionMessageCountMock).toHaveBeenCalledWith(TEST_SESSION_ID, 0)
vi.advanceTimersByTime(60)
expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.streamingText).toBe('')

View File

@ -2171,6 +2171,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
clearPendingToolParentUseIds(sessionId)
useCLITaskStore.getState().clearTasks(sessionId)
useSessionStore.getState().updateSessionTitle(sessionId, 'New Session')
useSessionStore.getState().updateSessionMessageCount(sessionId, 0)
useTabStore.getState().updateTabTitle(sessionId, 'New Session')
useTabStore.getState().updateTabStatus(sessionId, 'idle')
}

View File

@ -165,6 +165,21 @@ describe('sessionStore', () => {
expect(useTabStore.getState().tabs[0]?.title).toBe('使用bash写一个shell随便写点什么东西')
})
it('updates a session message count without changing other metadata', () => {
useSessionStore.setState({
sessions: [makeSession('session-count-1', '2026-05-07T00:00:00.000Z', 'Working session')],
})
useSessionStore.getState().updateSessionMessageCount('session-count-1', 0)
expect(useSessionStore.getState().sessions[0]).toMatchObject({
id: 'session-count-1',
title: 'Working session',
messageCount: 0,
workDir: '/workspace/project',
})
})
it('requests a large default session page for noisy history directories', async () => {
listMock.mockResolvedValue({
sessions: [makeSession('session-newest', '2026-05-07T00:00:03.000Z')],

View File

@ -47,6 +47,7 @@ type SessionStore = {
clearSessionSelection: () => void
renameSession: (id: string, title: string) => Promise<void>
updateSessionTitle: (id: string, title: string) => void
updateSessionMessageCount: (id: string, messageCount: number) => void
updateSessionPermissionMode: (id: string, mode: PermissionMode) => void
setActiveSession: (id: string | null) => void
}
@ -219,6 +220,14 @@ export const useSessionStore = create<SessionStore>((set, get) => ({
}))
},
updateSessionMessageCount: (id, messageCount) => {
set((s) => ({
sessions: s.sessions.map((session) =>
session.id === id ? { ...session, messageCount } : session,
),
}))
},
updateSessionPermissionMode: (id, mode) => {
set((s) => ({
sessions: s.sessions.map((session) =>

View File

@ -1,5 +1,9 @@
import { describe, expect, test } from 'bun:test'
import { buildDesktopSmokeBrowserEnv, resolveDesktopSmokeRuntimeSelection } from './execute'
import {
buildDesktopSmokeBrowserEnv,
desktopSmokeTextShowsProject,
resolveDesktopSmokeRuntimeSelection,
} from './execute'
describe('desktop smoke runtime selection', () => {
test('lets current-runtime use the desktop default active provider', () => {
@ -54,3 +58,20 @@ describe('desktop smoke browser environment', () => {
})
})
})
describe('desktop smoke restored session detection', () => {
test('waits for the target project chip instead of the first empty-session textarea', () => {
expect(desktopSmokeTextShowsProject([
'新建会话',
'随便问点什么...',
'folder_open 选择项目...',
].join('\n'), 'project')).toBe(false)
expect(desktopSmokeTextShowsProject([
'Untitled Session',
'让 Claude 编辑、调试或解释代码...',
'folder',
'project',
].join('\n'), 'project')).toBe(true)
})
})

View File

@ -2,7 +2,7 @@ import { appendFileSync, cpSync, existsSync, mkdirSync, readFileSync, rmSync, wr
import { mkdtemp } from 'node:fs/promises'
import { createServer } from 'node:net'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { basename, join } from 'node:path'
import treeKill from 'tree-kill'
import { changedFiles, writeDiffPatch } from '../baseline/execute'
import type { BaselineTarget, LaneResult } from '../types'
@ -16,7 +16,7 @@ const PROMPT = [
'When the tests pass, briefly say done.',
].join(' ')
type DesktopSmokeStage = 'open' | 'eval' | 'reload' | 'wait' | 'screenshot' | 'fill' | 'press' | 'verify'
type DesktopSmokeStage = 'open' | 'eval' | 'reload' | 'wait' | 'session-ready' | 'screenshot' | 'fill' | 'press' | 'verify'
type DesktopSmokeFailureContext = {
stage: DesktopSmokeStage
@ -334,6 +334,40 @@ async function waitForVerifiedProject(
throw new Error(`Timed out waiting for desktop project verification: ${lastVerificationError}`)
}
export function desktopSmokeTextShowsProject(text: string, projectName: string) {
const lines = text
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
return lines.includes(projectName)
}
async function waitForDesktopSmokeSessionReady(
browserEnv: Record<string, string>,
browserLogPath: string,
rootDir: string,
projectDir: string,
timeoutMs: number,
) {
const projectName = basename(projectDir)
const deadline = Date.now() + timeoutMs
let lastText = ''
while (Date.now() < deadline) {
const body = await runLoggedCommand(agentBrowserCommand(['get', 'text', '#content-area']), {
cwd: rootDir,
env: browserEnv,
logPath: browserLogPath,
timeoutMs: 15_000,
allowFailure: true,
maxLogChars: 4_000,
})
lastText = `${body.stdout}\n${body.stderr}`
if (desktopSmokeTextShowsProject(lastText, projectName)) return
await Bun.sleep(1_000)
}
throw new Error(`Timed out waiting for desktop smoke session to restore project "${projectName}". Last content text: ${lastText.slice(0, 500)}`)
}
async function verifyProject(originalDir: string, projectDir: string, artifactDir: string) {
await writeDiffPatch(originalDir, projectDir, join(artifactDir, 'diff.patch'))
const changed = changedFiles(originalDir, projectDir)
@ -484,6 +518,21 @@ export async function executeDesktopSmoke(
logPath: browserLogPath,
timeoutMs: 30_000,
}, browserStepContext)
await runBrowserStep('session-ready', ['get', 'text', '#content-area'], {
cwd: rootDir,
env: browserEnv,
logPath: browserLogPath,
timeoutMs: 15_000,
allowFailure: true,
maxLogChars: 4_000,
}, browserStepContext)
await waitForDesktopSmokeSessionReady(
browserEnv,
browserLogPath,
rootDir,
projectDir,
30_000,
)
await runBrowserStep('screenshot', ['screenshot', join(artifactDir, 'initial.png')], {
cwd: rootDir,
env: browserEnv,