chore: 保存工作副本——客户端授权新契约(双端点/验签/令牌/周检)与桌面会话可靠性改进
This commit is contained in:
1 parent
f71eef0eba
commit
ae6093c9b3
163 files changed
+4911
-2494
No files matched your search
@@ -27,7 +27,7 @@ describe('AI entry points', () => {
|
||||
const entry = screen.getByTestId('settings-tab-ai')
|
||||
expect(entry).toBeVisible()
|
||||
expect(entry).toHaveTextContent('大模型')
|
||||
expect(entry).toHaveTextContent('Provider')
|
||||
expect(entry).toHaveTextContent('供应商')
|
||||
expect(entry).not.toHaveTextContent('模型与测试')
|
||||
|
||||
fireEvent.click(entry)
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('AI provider settings', () => {
|
||||
const save = screen.getByRole('button', { name: '保存' })
|
||||
expect(save).toBeDisabled()
|
||||
|
||||
fireEvent.change(screen.getByLabelText('API Key'), { target: { value: 'sk-test-only' } })
|
||||
fireEvent.change(screen.getByLabelText('API 密钥'), { target: { value: 'sk-test-only' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: '测试' }))
|
||||
|
||||
await waitFor(() => expect(mockDesktopApi.aiTestProvider).toHaveBeenCalled())
|
||||
@@ -49,13 +49,13 @@ describe('AI provider settings', () => {
|
||||
it('invalidates a successful test whenever a connection field changes', async () => {
|
||||
render(<AiProviderSettings />)
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加' }))
|
||||
fireEvent.change(screen.getByLabelText('API Key'), { target: { value: 'sk-test-only' } })
|
||||
fireEvent.change(screen.getByLabelText('API 密钥'), { target: { value: 'sk-test-only' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: '测试' }))
|
||||
const save = screen.getByRole('button', { name: '保存' })
|
||||
await waitFor(() => expect(save).toBeEnabled())
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '高级' }))
|
||||
fireEvent.change(screen.getByLabelText('Endpoint'), { target: { value: 'https://proxy.example.com/v1' } })
|
||||
fireEvent.change(screen.getByLabelText('接口地址'), { target: { value: 'https://proxy.example.com/v1' } })
|
||||
expect(save).toBeDisabled()
|
||||
expect(screen.getByText('请重新测试')).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -130,7 +130,7 @@ describe('terminal Agent CLI panel', () => {
|
||||
expect(within(dialog).getByText(/宽松/)).toBeVisible()
|
||||
expect(within(dialog).getByText(/systemctl restart nginx/)).toBeVisible()
|
||||
expect(mockDesktopApi.sshExec).not.toHaveBeenCalled()
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: 'Once' }))
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '执行一次' }))
|
||||
await waitFor(() => expect(mockDesktopApi.sshExec).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
|
||||
@@ -167,7 +167,7 @@ describe('terminal Agent CLI panel', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '发送' }))
|
||||
|
||||
const dialog = await screen.findByRole('dialog')
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: 'Always' }))
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: '本会话允许' }))
|
||||
await waitFor(() =>
|
||||
expect(mockDesktopApi.sshExec).toHaveBeenCalledWith('ssh-active-1', 'rm /tmp/tmp.AAA111', { fromAgent: true })
|
||||
)
|
||||
@@ -388,4 +388,65 @@ describe('terminal Agent CLI panel', () => {
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: '发送' })).toBeVisible())
|
||||
expect(mockDesktopApi.aiComplete).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('splits model and SSH target in the header and hides extra chrome until needed', () => {
|
||||
useAiProviderStore.setState({
|
||||
providers: [
|
||||
{
|
||||
id: 'deepseek',
|
||||
name: 'DeepSeek',
|
||||
protocol: 'openai-chat',
|
||||
authStyle: 'bearer',
|
||||
baseUrl: 'https://api.deepseek.com',
|
||||
models: [
|
||||
{ id: 'v4', model: 'deepseek-v4-pro', label: 'DeepSeek V4 Pro', capability: 'chat', billing: 'payg' },
|
||||
],
|
||||
defaultModelId: 'v4',
|
||||
enabled: true,
|
||||
hasApiKey: true,
|
||||
allowInsecureHttp: false,
|
||||
},
|
||||
],
|
||||
activeProviderId: 'deepseek',
|
||||
isLoaded: true,
|
||||
catalogProviders: [],
|
||||
catalogUpdatedAt: null,
|
||||
})
|
||||
render(
|
||||
<AiTerminalPanel
|
||||
capture={() => ({ selection: '', viewport: '' })}
|
||||
onClose={vi.fn()}
|
||||
connectionId="ssh-active-1"
|
||||
targetLabel="pre-jenkins"
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText('DeepSeek V4 Pro')).toBeVisible()
|
||||
expect(screen.getByText('SSH · pre-jenkins')).toBeVisible()
|
||||
expect(screen.queryByRole('button', { name: '所有命令都需确认(只读白名单除外)' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: '新话题' })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: '关闭' })).toBeVisible()
|
||||
})
|
||||
|
||||
it('compacts oversized terminal dumps before summarizing', async () => {
|
||||
vi.mocked(mockDesktopApi.aiComplete).mockResolvedValueOnce({
|
||||
success: true,
|
||||
text: 'FINAL: 日志过长,已按摘要阅读。',
|
||||
model: 'test',
|
||||
redactedCount: 0,
|
||||
})
|
||||
const viewport = Array.from({ length: 200 }, (_, index) => `line-${index} ${'x'.repeat(48)}`).join('\n')
|
||||
render(
|
||||
<AiTerminalPanel
|
||||
capture={() => ({ selection: '', viewport })}
|
||||
onClose={vi.fn()}
|
||||
connectionId="ssh-active-1"
|
||||
targetLabel="root@opsbase.cn"
|
||||
/>
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: '总结输出' }))
|
||||
await waitFor(() => expect(mockDesktopApi.aiComplete).toHaveBeenCalledTimes(1))
|
||||
const context = String(vi.mocked(mockDesktopApi.aiComplete).mock.calls[0][0].context)
|
||||
expect(context.length).toBeLessThan(viewport.length)
|
||||
expect(context).toMatch(/总行数|已省略/)
|
||||
})
|
||||
})
|
||||
@@ -61,6 +61,7 @@ describe('App foundation acceptance', () => {
|
||||
|
||||
fireEvent.doubleClick(titleBar!)
|
||||
expect(mockDesktopApi.toggleMaximize).not.toHaveBeenCalled()
|
||||
expect(document.querySelector('[data-app-overlay]')).toBeNull()
|
||||
})
|
||||
|
||||
it('writes encrypted exported sessions to the selected backup file', async () => {
|
||||
@@ -88,7 +89,7 @@ describe('App foundation acceptance', () => {
|
||||
})
|
||||
vi.mocked(mockDesktopApi.showSaveDialog).mockResolvedValueOnce({
|
||||
canceled: false,
|
||||
filePath: '/tmp/opsxshell-sessions.bak',
|
||||
filePath: '/tmp/opsxshell-hosts.oxs',
|
||||
})
|
||||
render(<App />)
|
||||
|
||||
@@ -108,13 +109,10 @@ describe('App foundation acceptance', () => {
|
||||
|
||||
expect(mockDesktopApi.showSaveDialog).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
defaultPath: 'opsxshell-sessions.bak',
|
||||
defaultPath: 'opsxshell-hosts.oxs',
|
||||
})
|
||||
)
|
||||
expect(mockDesktopApi.writeFile).toHaveBeenCalledWith(
|
||||
'/tmp/opsxshell-sessions.bak',
|
||||
expect.any(String)
|
||||
)
|
||||
expect(mockDesktopApi.writeFile).toHaveBeenCalledWith('/tmp/opsxshell-hosts.oxs', expect.any(String))
|
||||
const [, encoded] = vi.mocked(mockDesktopApi.writeFile).mock.calls[0]
|
||||
const decoded = decodeBase64ToText(String(encoded))
|
||||
expect(decoded).toContain('"format": "opsxshell-backup"')
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import AppOverlay from '../../src/components/common/AppOverlay'
|
||||
|
||||
describe('AppOverlay', () => {
|
||||
it('does not leave a full-window layer in the DOM when closed', () => {
|
||||
const { rerender } = render(
|
||||
<AppOverlay open={false}>
|
||||
<div>blocked</div>
|
||||
</AppOverlay>
|
||||
)
|
||||
expect(document.querySelector('[data-app-overlay]')).toBeNull()
|
||||
|
||||
rerender(
|
||||
<AppOverlay open className="z-[100]">
|
||||
<div>panel</div>
|
||||
</AppOverlay>
|
||||
)
|
||||
const overlay = document.querySelector('[data-app-overlay]')
|
||||
expect(overlay).toBeTruthy()
|
||||
expect(overlay).toHaveClass('app-overlay')
|
||||
expect(screen.getByText('panel')).toBeVisible()
|
||||
})
|
||||
|
||||
it('portals onto document.body so layout overflow cannot trap it', () => {
|
||||
render(
|
||||
<div id="trap" className="overflow-hidden">
|
||||
<AppOverlay open>
|
||||
<span>layer</span>
|
||||
</AppOverlay>
|
||||
</div>
|
||||
)
|
||||
const overlay = document.querySelector('[data-app-overlay]')
|
||||
expect(overlay?.parentElement).toBe(document.body)
|
||||
expect(document.getElementById('trap')?.contains(overlay)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import ErrorBoundary from '../../src/components/common/ErrorBoundary'
|
||||
import i18n from '../../src/i18n'
|
||||
|
||||
function Boom(): never {
|
||||
throw new Error('pane-crash')
|
||||
}
|
||||
|
||||
describe('ErrorBoundary', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await i18n.changeLanguage('zh-CN')
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('shows compact pane copy in Chinese', async () => {
|
||||
await i18n.changeLanguage('zh-CN')
|
||||
render(
|
||||
<ErrorBoundary compact>
|
||||
<Boom />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
||||
expect(screen.getByText(i18n.t('common:error.bodyPane'))).toBeVisible()
|
||||
expect(screen.getByRole('button', { name: i18n.t('common:error.retryPane') })).toBeVisible()
|
||||
})
|
||||
|
||||
it('shows compact pane copy in English', async () => {
|
||||
await i18n.changeLanguage('en')
|
||||
render(
|
||||
<ErrorBoundary compact>
|
||||
<Boom />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
||||
expect(screen.getByText('This tab hit an unexpected problem. Other tabs still work.')).toBeVisible()
|
||||
expect(screen.getByRole('button', { name: 'Retry this tab' })).toBeVisible()
|
||||
})
|
||||
|
||||
it('clears the captured error when resetKey changes', async () => {
|
||||
await i18n.changeLanguage('zh-CN')
|
||||
const { rerender } = render(
|
||||
<ErrorBoundary compact resetKey="tab-a">
|
||||
<Boom />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
expect(screen.getByText(i18n.t('common:error.bodyPane'))).toBeVisible()
|
||||
|
||||
rerender(
|
||||
<ErrorBoundary compact resetKey="tab-b">
|
||||
<div>ok-pane</div>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
expect(screen.getByText('ok-pane')).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -25,7 +25,7 @@ describe('Session workflow acceptance', () => {
|
||||
const onClose = vi.fn()
|
||||
render(<SessionDialog isOpen onClose={onClose} editSession={null} />)
|
||||
|
||||
await user.type(screen.getByPlaceholderText('自动用 user@host 或 vnc:host'), 'OpsBase 验收')
|
||||
await user.type(screen.getByPlaceholderText('自动用 user@host'), 'OpsBase 验收')
|
||||
await user.clear(screen.getByPlaceholderText('192.168.1.10'))
|
||||
await user.type(screen.getByPlaceholderText('192.168.1.10'), 'root@opsbase.cn:55555')
|
||||
expect(screen.getByLabelText('端口')).toHaveValue(55555)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { ShellClosedOverlay } from '../../src/components/terminal/TerminalOverlay'
|
||||
import TerminalOverlay, { ShellClosedOverlay } from '../../src/components/terminal/TerminalOverlay'
|
||||
|
||||
describe('remote shell close recovery', () => {
|
||||
it('explains remote TMOUT without claiming the pooled SSH connection is dead', () => {
|
||||
const reopen = vi.fn()
|
||||
render(<ShellClosedOverlay reason="远端 Shell 因空闲策略(TMOUT)自动退出" idleTimeout onReopen={reopen} />)
|
||||
render(<ShellClosedOverlay reason="shell.idleTimeout" idleTimeout onReopen={reopen} />)
|
||||
|
||||
expect(screen.getByText('空闲超时,Shell 已退出')).toBeVisible()
|
||||
expect(screen.getByText('远端 Shell 因空闲超时退出(TMOUT)')).toBeVisible()
|
||||
expect(screen.getByText(/SSH 连接仍可用/)).toBeVisible()
|
||||
fireEvent.click(screen.getByRole('button', { name: '重新打开 Shell' }))
|
||||
expect(reopen).toHaveBeenCalledTimes(1)
|
||||
@@ -22,3 +23,23 @@ describe('remote shell close recovery', () => {
|
||||
expect(screen.queryByText(/SSH 连接仍可用/)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('connection disconnect overlay', () => {
|
||||
it('explains keepalive failure without calling it a user close', () => {
|
||||
render(
|
||||
<TerminalOverlay
|
||||
connectionState="disconnected"
|
||||
statusMessage=""
|
||||
showSuspendedOverlay={false}
|
||||
needsReconnect
|
||||
disconnectKind="keepalive"
|
||||
onReconnect={vi.fn()}
|
||||
onResume={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(screen.getByText('心跳失败,连接已断开')).toBeVisible()
|
||||
expect(screen.getByText(/不会向 Shell 发送按键/)).toBeVisible()
|
||||
expect(screen.queryByText(/closed by user/i)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { TabContextMenu } from '../../src/components/layout/tabbar/TabContextMenu'
|
||||
import { tabSupportsReconnect } from '../../src/utils/tabReconnect'
|
||||
|
||||
describe('TabContextMenu', () => {
|
||||
it('lists SFTP, clone, reconnect, then close actions', () => {
|
||||
const onReconnect = vi.fn()
|
||||
render(
|
||||
<TabContextMenu
|
||||
isOpen
|
||||
position={{ x: 8, y: 8 }}
|
||||
onClose={vi.fn()}
|
||||
onCloseTab={vi.fn()}
|
||||
onCloseOthers={vi.fn()}
|
||||
onCloseRight={vi.fn()}
|
||||
onCloseLeft={vi.fn()}
|
||||
onCloseAll={vi.fn()}
|
||||
onClone={vi.fn()}
|
||||
onOpenSftp={vi.fn()}
|
||||
onReconnect={onReconnect}
|
||||
/>
|
||||
)
|
||||
|
||||
const labels = screen.getAllByRole('button').map((button) => button.textContent)
|
||||
expect(labels.slice(0, 4)).toEqual(['打开 SFTP', '克隆标签', '重新连接', '关闭'])
|
||||
fireEvent.click(screen.getByRole('button', { name: '重新连接' }))
|
||||
expect(onReconnect).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('offers reconnect for SSH / SFTP / desktop tabs only', () => {
|
||||
expect(tabSupportsReconnect({ type: 'terminal' })).toBe(true)
|
||||
expect(tabSupportsReconnect({ type: 'sftp' })).toBe(true)
|
||||
expect(tabSupportsReconnect({ type: 'rdp' })).toBe(true)
|
||||
expect(tabSupportsReconnect({ type: 'welcome' })).toBe(false)
|
||||
expect(tabSupportsReconnect({ type: 'settings' })).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { startNativeForward, stopNativeForward } from '../../../src/components/connection/forwardRuntime'
|
||||
import type { ForwardRule } from '../../../src/components/connection/forwardRules'
|
||||
|
||||
const localRule: ForwardRule = {
|
||||
id: 'pf-local',
|
||||
type: 'local',
|
||||
name: 'db',
|
||||
localPort: 3306,
|
||||
localBindHost: '127.0.0.1',
|
||||
remoteHost: '127.0.0.1',
|
||||
remotePort: 3306,
|
||||
}
|
||||
|
||||
describe('forwardRuntime', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('starts a local forward through the native API', async () => {
|
||||
const sshForwardLocal = vi
|
||||
.spyOn(window.opsxApi, 'sshForwardLocal')
|
||||
.mockResolvedValue({ success: true, forwardId: 'pf-local' })
|
||||
|
||||
await expect(startNativeForward('conn-1', localRule)).resolves.toEqual({
|
||||
success: true,
|
||||
error: undefined,
|
||||
errorCode: undefined,
|
||||
})
|
||||
expect(sshForwardLocal).toHaveBeenCalledWith('conn-1', 3306, '127.0.0.1', 3306, 'pf-local', '127.0.0.1')
|
||||
})
|
||||
|
||||
it('stops by runtime tunnel id when the persisted rule id misses', async () => {
|
||||
const sshForwardStop = vi
|
||||
.spyOn(window.opsxApi, 'sshForwardStop')
|
||||
.mockResolvedValueOnce({ success: false, error: 'missing' })
|
||||
.mockResolvedValueOnce({ success: true })
|
||||
vi.spyOn(window.opsxApi, 'sshForwardList').mockResolvedValue({
|
||||
success: true,
|
||||
tunnels: [
|
||||
{
|
||||
forwardId: 'runtime-9',
|
||||
connectionId: 'conn-1',
|
||||
kind: 'local',
|
||||
bindHost: '127.0.0.1',
|
||||
boundPort: 3306,
|
||||
startedAt: 1,
|
||||
running: true,
|
||||
activeConnections: 0,
|
||||
bytesUp: 0,
|
||||
bytesDown: 0,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await expect(stopNativeForward('conn-1', localRule)).resolves.toMatchObject({ success: true })
|
||||
expect(sshForwardStop).toHaveBeenNthCalledWith(1, 'pf-local')
|
||||
expect(sshForwardStop).toHaveBeenNthCalledWith(2, 'runtime-9')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user