test: stabilize asynchronous UI checks in release CI
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s

Control the native inventory retry response and wait for the complete
browser-close result instead of asserting on intermediate renders.
Run native note-layout input in a visible isolated Electron window,
scan the fixture dependencies up front, and report timeout explicitly.
Keep all behavior and geometry assertions and Electron sandboxing.

The three focused regressions, full typecheck, and lint pass locally.
Linux CI must confirm the release failures are resolved. No product,
Agent, version, or release-tag changes are included.
This commit is contained in:
mesalogo
2026-09-26 01:53:04 +08:00
parent 107dc974e3
commit cd27163660
4 changed files with 46 additions and 13 deletions
+14
View File
@@ -7,6 +7,20 @@
修正复发缺陷时,优先建立一条能覆盖整类问题的机制(共享选择器、共享函数、能发现全部实例
的测试),而不是逐个点名修补。逐个修补正是这些条目反复出现的原因。
## 异步界面测试只等待部分状态
**症状**:本地通过的界面测试在完整 CI 中找不到预期控件,或在前一步更新后立即读取到旧状态。
**规则**:复用 Testing Library 的 `act`、`waitFor` 和现有 deferred Promise,
按请求完成和最终可见状态同步,不以另一个元素出现或消失代替完整操作结束。
测试请求重试时显式控制后一次响应,分别断言等待中和响应完成后的状态;
同一异步操作的多个最终断言放入同一个 `waitFor`。不要只延长全局超时或放宽断言。
**历史**:2026-09-26 检查 Desktop 0.13.14 标签 CI `36165805814` 与
main CI `36164698647`:原生能力重试用例在启动状态未稳定时打开选项,
浏览器关闭用例只等待页签消失便同步断言错误已清除。两处测试改用上述已有同步机制;
是否消除 CI 失败以修复提交的 Linux CI 结果为准。
## 整窗遮罩与原生拖动区域
**症状**:弹窗覆盖标题栏区域时,其关闭按钮无法悬停和点击。
+15 -6
View File
@@ -9368,6 +9368,7 @@ describe("App", () => {
supportsToolExecution: true, detail: "Ready",
});
const empty = await api.runtimeCustomization.getNativeSnapshot({ provider: "opencode" });
const retry = deferred<typeof empty>();
vi.mocked(api.runtimeCustomization.getNativeSnapshot).mockClear();
vi.mocked(api.runtimeCustomization.getNativeSnapshot)
.mockResolvedValueOnce({
@@ -9375,18 +9376,26 @@ describe("App", () => {
available: false,
inventoryStatus: "unavailable",
})
.mockResolvedValueOnce({
.mockReturnValueOnce(retry.promise);
render(<App />);
await waitFor(() =>
expect(api.runtimeCustomization.getNativeSnapshot).toHaveBeenCalledTimes(2),
);
openComposerOptions();
expect(screen.queryByRole("button", { name: /OpenCode Runtime Agent/u })).not.toBeInTheDocument();
await act(async () => {
retry.resolve({
...empty,
available: true,
inventoryStatus: "partial",
agents: [{ id: "planner", name: "Planner", mode: "primary", native: true, hidden: false }],
commands: [{ id: "review", name: "review", source: "command" }],
});
render(<App />);
openComposerOptions();
fireEvent.click(await screen.findByRole("button", { name: /OpenCode Runtime Agent/u }));
await retry.promise;
});
fireEvent.click(screen.getByRole("button", { name: /OpenCode Runtime Agent/u }));
expect(screen.getByRole("menuitemradio", { name: /Planner/u })).toBeVisible();
fireEvent.keyDown(screen.getByRole("menu", { name: "OpenCode Runtime Agent" }), { key: "Escape" });
fireEvent.click(screen.getByRole("button", { name: /Runtime 快捷操作/u }));
@@ -1795,9 +1795,12 @@ describe('RightAssistantSidebar resizing', () => {
).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '关闭浏览器 · conversation-a' }))
await waitFor(() => expect(screen.queryByRole('tab', { name: '浏览器 · conversation-a' })).not.toBeInTheDocument())
await waitFor(() => {
expect(browserApi.closeTab).toHaveBeenCalledTimes(2)
expect(screen.queryByRole('tab', { name: '浏览器 · conversation-a' })).not.toBeInTheDocument()
expect(screen.queryByText('Close denied')).not.toBeInTheDocument()
})
})
it.each(['create', 'close'] as const)(
'ignores a late browser %s failure after switching panels',
+12 -5
View File
@@ -19,7 +19,8 @@ it('resizes record columns with native Electron input and preserves desktop widt
'<html><body><script type="module" src="/tests/support/magic-notes-layout-regression.tsx"></script></body></html>'))
})
} }],
server: { host: '127.0.0.1', port: 0, watch: null }
server: { host: '127.0.0.1', port: 0, watch: null },
optimizeDeps: { entries: ['tests/support/magic-notes-layout-regression.tsx'] }
})
try {
await server.listen()
@@ -31,7 +32,7 @@ it('resizes record columns with native Electron input and preserves desktop widt
app.commandLine.appendSwitch('force-device-scale-factor', '1');
app.setPath('userData', ${JSON.stringify(join(directory, 'profile'))});
app.whenReady().then(async () => {
const win = new BrowserWindow({ show: false, width: 1280, height: 800, useContentSize: true,
const win = new BrowserWindow({ show: true, width: 1280, height: 800, useContentSize: true,
webPreferences: { sandbox: true, contextIsolation: true, nodeIntegration: false, backgroundThrottling: false } });
const js = code => win.webContents.executeJavaScript(code).catch(error => { throw new Error(code + ': ' + error.message); });
const settle = () => js('new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))');
@@ -165,10 +166,16 @@ it('resizes record columns with native Electron input and preserves desktop widt
let output = ''
child.stdout.on('data', data => { output += data })
child.stderr.on('data', data => { output += data })
// Hidden Electron windows can run rAF at 1 Hz; the measured scenario takes 63s.
const timeout = setTimeout(() => child.kill(), 90000)
// Native input and animation frames need a visible window, including under Xvfb.
let timedOut = false
const timeout = setTimeout(() => { timedOut = true; child.kill() }, 90000)
try {
const code = await new Promise((resolve, reject) => { child.once('exit', resolve); child.once('error', reject) })
const { code, signal } = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {
child.once('close', (code, signal) => resolve({ code, signal }))
child.once('error', reject)
})
expect(timedOut, `Electron layout exceeded 90s (exit=${code}, signal=${signal}).\n${output}`).toBe(false)
expect(signal, output).toBeNull()
expect(code, output).toBe(0)
} finally { clearTimeout(timeout) }
const result = JSON.parse(await readFile(join(directory, 'result.json'), 'utf8'))