Skip to content

Commit 8bdb0ce

Browse files
committed
fix(regression-test): close remaining platform races
Signed-off-by: kangfenmao <kangfenmao@qq.com>
1 parent 9806089 commit 8bdb0ce

8 files changed

Lines changed: 95 additions & 17 deletions

File tree

scripts/cherry-regression-test/fixtures.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export async function createFixtures(paths: RunPaths): Promise<FixtureManifest>
6262

6363
const selectionFile = join(paths.fixtures, 'selection.txt')
6464
const translationFile = join(paths.fixtures, 'translation.txt')
65-
writeFileSync(selectionFile, `${FIXTURE_MARKERS.selection}\n`)
65+
writeFileSync(selectionFile, `The validation label printed on this document is ${FIXTURE_MARKERS.selection}.\n`)
6666
writeFileSync(translationFile, `${FIXTURE_MARKERS.translation}\n`)
6767

6868
writeFileSync(

scripts/cherry-regression-test/system-automation.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,25 @@ export function chooseNativeFile(platform: Platform, paths: RunPaths, candidateP
160160
'if (-not $dialog) { throw "Native file dialog was not found" }',
161161
'$shell = New-Object -ComObject WScript.Shell',
162162
`if (-not $shell.AppActivate(${electronPid})) { throw "Native file dialog could not be activated" }`,
163+
'$dialog.SetFocus()',
163164
'Start-Sleep -Milliseconds 300',
164165
...(isDirectory
165166
? [
166167
'[System.Windows.Forms.SendKeys]::SendWait("^l")',
167168
'Start-Sleep -Milliseconds 200',
168-
`[System.Windows.Forms.SendKeys]::SendWait('${escapePowerShell(filePath)}')`,
169+
'$pathInput = [System.Windows.Automation.AutomationElement]::FocusedElement',
170+
'$valuePattern = $pathInput.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)',
171+
`$valuePattern.SetValue('${escapePowerShell(filePath)}')`,
169172
'[System.Windows.Forms.SendKeys]::SendWait("{ENTER}")',
170173
'Start-Sleep -Milliseconds 750',
171-
'[System.Windows.Forms.SendKeys]::SendWait("%s")'
174+
'$windows = $root.FindAll([System.Windows.Automation.TreeScope]::Children, $processCondition)',
175+
'$dialog = $windows | Where-Object { $_.Current.ClassName -eq "#32770" } | Select-Object -Last 1',
176+
'if (-not $dialog) { throw "Native folder dialog closed before selection" }',
177+
'$acceptCondition = [System.Windows.Automation.PropertyCondition]::new([System.Windows.Automation.AutomationElement]::AutomationIdProperty, "1")',
178+
'$acceptButton = $dialog.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $acceptCondition)',
179+
'if (-not $acceptButton) { throw "Native folder accept button was not found" }',
180+
'$acceptButton.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern).Invoke()',
181+
'Start-Sleep -Milliseconds 500'
172182
]
173183
: [
174184
'[System.Windows.Forms.SendKeys]::SendWait("%n")',

src/renderer/pages/code/hooks/__tests__/useApiGatewayProvider.test.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { preferenceService } from '@data/PreferenceService'
2-
import { renderHook } from '@testing-library/react'
2+
import { act, renderHook } from '@testing-library/react'
33
import { beforeEach, describe, expect, it, vi } from 'vitest'
44

55
import { useApiGatewayProvider } from '../useApiGatewayProvider'
@@ -12,7 +12,9 @@ const mocks = vi.hoisted(() => ({
1212
enabled: boolean
1313
},
1414
apiGatewayRunning: false,
15-
startApiGateway: vi.fn<() => Promise<boolean>>()
15+
startApiGateway: vi.fn<() => Promise<boolean>>(),
16+
cachedApiKey: null as string | null,
17+
preferenceChanged: undefined as (() => void) | undefined
1618
}))
1719

1820
vi.mock('@renderer/hooks/useApiGateway', () => ({
@@ -24,7 +26,16 @@ vi.mock('@renderer/hooks/useApiGateway', () => ({
2426
}))
2527

2628
vi.mock('@data/PreferenceService', () => ({
27-
preferenceService: { get: vi.fn() }
29+
preferenceService: {
30+
get: vi.fn(),
31+
getCachedValue: vi.fn(() => mocks.cachedApiKey),
32+
subscribeChange: vi.fn(() => (callback: () => void) => {
33+
mocks.preferenceChanged = callback
34+
return () => {
35+
if (mocks.preferenceChanged === callback) mocks.preferenceChanged = undefined
36+
}
37+
})
38+
}
2839
}))
2940

3041
vi.mock('react-i18next', () => ({
@@ -35,8 +46,12 @@ describe('useApiGatewayProvider.ensureReady', () => {
3546
beforeEach(() => {
3647
mocks.apiGatewayConfig = { host: '127.0.0.1', port: 23333, apiKey: 'cs-sk-old', enabled: false }
3748
mocks.apiGatewayRunning = false
49+
mocks.cachedApiKey = null
50+
mocks.preferenceChanged = undefined
3851
mocks.startApiGateway.mockReset()
3952
vi.mocked(preferenceService.get).mockReset()
53+
vi.mocked(preferenceService.getCachedValue).mockClear()
54+
vi.mocked(preferenceService.subscribeChange).mockClear()
4055
})
4156

4257
it('rejects (never returns a stale key) when a non-running gateway fails to start', async () => {
@@ -61,6 +76,23 @@ describe('useApiGatewayProvider.ensureReady', () => {
6176
await expect(result.current!.ensureReady()).resolves.toBe('cs-sk-fresh')
6277
})
6378

79+
it('waits for the generated key when the renderer cache has not received the start update yet', async () => {
80+
mocks.apiGatewayRunning = false
81+
mocks.startApiGateway.mockResolvedValue(true)
82+
vi.mocked(preferenceService.get).mockResolvedValue(null)
83+
84+
const { result } = renderHook(() => useApiGatewayProvider())
85+
const keyPromise = result.current!.ensureReady()
86+
await vi.waitFor(() => expect(preferenceService.subscribeChange).toHaveBeenCalled())
87+
88+
act(() => {
89+
mocks.cachedApiKey = 'cs-sk-generated'
90+
mocks.preferenceChanged?.()
91+
})
92+
93+
await expect(keyPromise).resolves.toBe('cs-sk-generated')
94+
})
95+
6496
it('returns the key without starting when the gateway is already running', async () => {
6597
mocks.apiGatewayRunning = true
6698
mocks.apiGatewayConfig = { host: '127.0.0.1', port: 23333, apiKey: 'cs-sk-live', enabled: true }

src/renderer/pages/code/hooks/useApiGatewayProvider.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,29 @@ import { useTranslation } from 'react-i18next'
88

99
const DEFAULT_GATEWAY_HOST = '127.0.0.1'
1010
const DEFAULT_GATEWAY_PORT = 23333
11+
const API_KEY_SYNC_TIMEOUT_MS = 5_000
12+
13+
async function readFreshApiKey(): Promise<string | null> {
14+
const key = await preferenceService.get('feature.api_gateway.api_key')
15+
if (key) return key
16+
17+
return new Promise((resolve) => {
18+
let unsubscribe = () => {}
19+
const timeout = window.setTimeout(() => {
20+
unsubscribe()
21+
resolve(null)
22+
}, API_KEY_SYNC_TIMEOUT_MS)
23+
const readCachedKey = () => {
24+
const cachedKey = preferenceService.getCachedValue('feature.api_gateway.api_key')
25+
if (!cachedKey) return
26+
window.clearTimeout(timeout)
27+
unsubscribe()
28+
resolve(cachedKey)
29+
}
30+
unsubscribe = preferenceService.subscribeChange('feature.api_gateway.api_key')(readCachedKey)
31+
readCachedKey()
32+
})
33+
}
1134

1235
/**
1336
* The synthetic "Cherry Gateway" entry for the code-CLI provider list, plus the
@@ -52,7 +75,7 @@ export function useApiGatewayProvider(): ApiGatewayProviderBundle | null {
5275
throw new Error('API gateway failed to start')
5376
}
5477
}
55-
const key = await preferenceService.get('feature.api_gateway.api_key')
78+
const key = await readFreshApiKey()
5679
if (!key) {
5780
throw new Error('API gateway did not provide a key')
5881
}

tests/e2e/cherry-regression/05-desktop-assistants.test.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ test('[C-03] 使用划词助手处理跨应用选中文本 @selection-assistant'
7878
async ({ model, providerId }) => {
7979
await window.api.preference.setMultiple({
8080
'chat.default_model_id': `${providerId}::${model}`,
81+
'feature.selection.action_items': [
82+
{
83+
enabled: true,
84+
id: 'regression-label',
85+
isBuiltIn: false,
86+
name: 'Read validation label',
87+
prompt: 'What validation label is printed in the selected sentence? Include the label in your answer.'
88+
}
89+
],
8190
'feature.selection.enabled': true,
8291
'feature.selection.trigger_mode': 'shortcut',
8392
'shortcut.selection.capture_text': {
@@ -92,11 +101,11 @@ test('[C-03] 使用划词助手处理跨应用选中文本 @selection-assistant'
92101
await closeSettings(page)
93102

94103
const selection = await app.window('/windows/selection/toolbar/')
95-
await expect(selection.getByText('Explain', { exact: true })).toBeVisible()
96-
await expect(selection.getByText('Translate', { exact: true })).toBeVisible()
104+
const readLabel = selection.getByRole('button', { name: 'Read validation label', exact: true })
105+
await expect(readLabel).toBeVisible()
97106
openExternalText(app.record.platform, app.paths, join(app.paths.fixtures, 'selection.txt'))
98107
sendSystemHotkey(app.record.platform, [app.record.platform === 'macos' ? 'Meta' : 'Control', 'Alt', 'Shift', 'k'])
99-
await selection.getByRole('button', { name: 'Explain', exact: true }).click({ force: true })
108+
await readLabel.click({ force: true })
100109
const action = await app.window('/windows/selection/action/')
101110
await expect(action.locator('body')).toContainText('SELECTION_ASSISTANT_PASS', { timeout: 2 * 60_000 })
102111
await expect(action.locator('body')).not.toContainText('Invalid signature')

tests/e2e/cherry-regression/08-code-tools.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,11 @@ test('[CODE-03] 启动 OpenClaw @openclaw', async ({ app, mainWindow: page }) =>
101101
.poll(() => observeOwnedProcess(app.record, 'openclaw', true, baseline).passed, { timeout: 2 * 60_000 })
102102
.toBe(true)
103103

104-
const stop = codeView.getByRole('button', { name: 'Stop', exact: true })
104+
await page.getByRole('banner').getByRole('button', { name: 'Code Mate', exact: true }).last().click()
105+
const activeCodeView = page.locator('[data-ui="code.view"]:visible').first()
106+
const stop = activeCodeView.getByRole('button', { name: 'Stop', exact: true })
105107
await expect(stop).toBeVisible()
106108
await stop.click()
107-
await expect(codeView.getByRole('button', { name: 'Launch', exact: true })).toBeVisible({ timeout: 60_000 })
109+
await expect(activeCodeView.getByRole('button', { name: 'Launch', exact: true })).toBeVisible({ timeout: 60_000 })
108110
await expect.poll(() => observeOwnedProcess(app.record, 'openclaw', false).passed, { timeout: 60_000 }).toBe(true)
109111
})

tests/e2e/cherry-regression/09-cherryin-and-images.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import { saveNativeFile } from '../../../scripts/cherry-regression-test/system-a
99

1010
const IMAGE_PROMPT = 'A red cherry robot holding a blue umbrella in a bright workshop, detailed illustration.'
1111

12-
test('[M-01] 登录 CherryIN 并完成聊天 @cherryin-chat', async ({ app, mainWindow }) => {
13-
let page = mainWindow
12+
test('[M-01] 登录 CherryIN 并完成聊天 @cherryin-chat', async ({ app, mainWindow: _mainWindow }) => {
13+
let page = await app.restart('authenticated')
14+
await dismissOnboarding(page)
1415
await ensureCherryInSignedIn(app, page)
1516
await addCherryInModel(page, app.config.cherryIn.chatModel)
1617
await closeSettings(page)

tests/e2e/cherry-regression/fixture.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,23 @@ export const test = base.extend<RegressionFixtures & RegressionOptions>({
2828
const page = await app.useProfile(profile)
2929
await use(page)
3030

31+
const currentPage = await app.mainWindow().catch(() => page)
3132
if (testInfo.status !== testInfo.expectedStatus) {
32-
await page
33+
await currentPage
3334
.locator('input[type="password"]')
3435
.evaluateAll((inputs) => {
3536
for (const input of inputs) (input as HTMLInputElement).value = ''
3637
})
3738
.catch(() => undefined)
3839
const screenshotPath = testInfo.outputPath('failure.png')
39-
const captured = await page.screenshot({ path: screenshotPath, fullPage: true }).then(
40+
const captured = await currentPage.screenshot({ path: screenshotPath, fullPage: true }).then(
4041
() => true,
4142
() => false
4243
)
4344
if (captured) await testInfo.attach('失败截图', { path: screenshotPath, contentType: 'image/png' })
4445
}
4546

46-
await app.cleanupTransientUi(page)
47+
await app.cleanupTransientUi(currentPage)
4748
}
4849
})
4950

0 commit comments

Comments
 (0)