feat(i18n): add multi-language support (ja/ko/vi/ru/zh-TW) with system auto-detection
- Extend AppLanguage to 7 locales: zh-CN, zh-TW, en, ja, ko, vi, ru - Auto-detect system language via navigator.languages; fallback to English - Add BCP47 locale mapping for date formatting and sorting - Create 50 translation JSON files (5 languages × 10 namespaces, 1137 keys each) - Update language selector with native language names - Extend locale key parity test to cover all 7 languages (70 assertions) - Add VNC/RDP desktop, serial terminal, AI agent, port forwarding, and SFTP features - Add credential encryption, host metrics, session backup crypto, and zmodem bridge - Update Rust backend: desktop services, host metrics, SSH/SFTP enhancements
This commit is contained in:
1 parent
fea6d8db04
commit
b0d0e3a5f3
301 files changed
+28002
-2705
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { analyzeAgentOutput } from '../../src/utils/agentOutput'
|
||||
import {
|
||||
agentCommandAllowFingerprint,
|
||||
agentCommandNeedsConfirmation,
|
||||
assessAgentCommandRisk,
|
||||
} from '../../src/utils/agentCommandRisk'
|
||||
@@ -21,13 +22,22 @@ describe('Agent command guardrail', () => {
|
||||
expect(assessAgentCommandRisk(command)).toBe(expected)
|
||||
})
|
||||
|
||||
it('uses strict, balanced and permissive confirmation thresholds', () => {
|
||||
it('uses strict and relaxed confirmation thresholds', () => {
|
||||
expect(agentCommandNeedsConfirmation('safe', 'strict')).toBe(false)
|
||||
expect(agentCommandNeedsConfirmation('medium', 'strict')).toBe(true)
|
||||
expect(agentCommandNeedsConfirmation('medium', 'balanced')).toBe(false)
|
||||
expect(agentCommandNeedsConfirmation('high', 'balanced')).toBe(true)
|
||||
expect(agentCommandNeedsConfirmation('high', 'permissive')).toBe(false)
|
||||
expect(agentCommandNeedsConfirmation('critical', 'permissive')).toBe(false)
|
||||
expect(agentCommandNeedsConfirmation('medium', 'relaxed')).toBe(false)
|
||||
expect(agentCommandNeedsConfirmation('high', 'relaxed')).toBe(true)
|
||||
expect(agentCommandNeedsConfirmation('critical', 'relaxed')).toBe(false)
|
||||
expect(agentCommandNeedsConfirmation('critical', 'strict')).toBe(false)
|
||||
})
|
||||
|
||||
it('fingerprints similar temp-path commands for session Always', () => {
|
||||
expect(agentCommandAllowFingerprint('rm /tmp/tmp.3RUZRHTwk2', 'high')).toBe(
|
||||
agentCommandAllowFingerprint('rm /tmp/tmp.ABCDEFG123', 'high')
|
||||
)
|
||||
expect(agentCommandAllowFingerprint('rm /tmp/tmp.3RUZRHTwk2', 'high')).not.toBe(
|
||||
agentCommandAllowFingerprint('rm -rf /var/log/nginx', 'high')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { normalizeDistroId, parseOsPrettyName, parseOsReleaseOrUname, resolveSessionDistro } from '../../src/utils/hostDistro'
|
||||
|
||||
describe('hostDistro', () => {
|
||||
it('normalizes common distro ids', () => {
|
||||
expect(normalizeDistroId('ubuntu')).toBe('ubuntu')
|
||||
expect(normalizeDistroId('deepin')).toBe('deepin')
|
||||
expect(normalizeDistroId('uos')).toBe('deepin')
|
||||
expect(normalizeDistroId('rocky')).toBe('rocky')
|
||||
expect(normalizeDistroId('rhel')).toBe('redhat')
|
||||
expect(normalizeDistroId('openEuler')).toBe('openeuler')
|
||||
expect(normalizeDistroId('anolis')).toBe('anolis')
|
||||
expect(normalizeDistroId('alinux')).toBe('alinux')
|
||||
expect(normalizeDistroId('amzn2')).toBe('amazon')
|
||||
expect(normalizeDistroId('Amazon Linux')).toBe('amazon')
|
||||
expect(normalizeDistroId('Alibaba Cloud Linux')).toBe('alinux')
|
||||
expect(normalizeDistroId('Linux')).toBe('linux')
|
||||
})
|
||||
|
||||
it('parses cloud linux from os-release NAME when needed', () => {
|
||||
expect(
|
||||
parseOsReleaseOrUname(`NAME="Alibaba Cloud Linux"
|
||||
ID=alinux
|
||||
VERSION_ID="3"`)
|
||||
).toBe('alinux')
|
||||
expect(
|
||||
parseOsReleaseOrUname(`NAME="Amazon Linux"
|
||||
ID=amzn
|
||||
VERSION_ID="2023"`)
|
||||
).toBe('amazon')
|
||||
})
|
||||
|
||||
it('parses os-release ID', () => {
|
||||
const sample = `NAME="Ubuntu"
|
||||
VERSION="22.04"
|
||||
ID=ubuntu
|
||||
ID_LIKE=debian
|
||||
`
|
||||
expect(parseOsReleaseOrUname(sample)).toBe('ubuntu')
|
||||
})
|
||||
|
||||
it('maps TencentOS to centos icon family', () => {
|
||||
expect(normalizeDistroId('tencentos')).toBe('centos')
|
||||
expect(
|
||||
parseOsReleaseOrUname(`NAME="TencentOS Server"
|
||||
ID="tencentos"
|
||||
VERSION_ID="4"`)
|
||||
).toBe('centos')
|
||||
})
|
||||
|
||||
it('parses PRETTY_NAME', () => {
|
||||
expect(
|
||||
parseOsPrettyName(`NAME="Ubuntu"
|
||||
PRETTY_NAME="Ubuntu 24.04.4 LTS"
|
||||
ID=ubuntu`)
|
||||
).toBe('Ubuntu 24.04.4 LTS')
|
||||
})
|
||||
|
||||
it('resolves protocol defaults for desktop sessions', () => {
|
||||
expect(resolveSessionDistro({ protocol: 'rdp' })).toBe('windows')
|
||||
expect(resolveSessionDistro({ protocol: 'vnc' })).toBe('vnc')
|
||||
expect(resolveSessionDistro({ protocol: 'ssh', distro: 'debian' })).toBe('debian')
|
||||
expect(resolveSessionDistro({ protocol: 'ssh' })).toBe('linux')
|
||||
expect(resolveSessionDistro({})).toBe('linux')
|
||||
})
|
||||
})
|
||||
@@ -49,6 +49,6 @@ describe('platformShells', () => {
|
||||
it('uses CRLF enter for windows shells', () => {
|
||||
expect(localShellEnterKey('cmd')).toBe('\r\n')
|
||||
expect(localShellEnterKey('pwsh')).toBe('\r\n')
|
||||
expect(localShellEnterKey('bash')).toBe('\n')
|
||||
expect(localShellEnterKey('bash')).toBe('\r')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
decryptSessionBackup,
|
||||
encryptSessionBackup,
|
||||
isEncryptedSessionBackup,
|
||||
} from '../../src/utils/sessionBackupCrypto'
|
||||
|
||||
describe('sessionBackupCrypto', () => {
|
||||
it('encrypts and decrypts UTF-8 session payload', async () => {
|
||||
const plaintext = JSON.stringify({
|
||||
version: 1,
|
||||
sessions: [{ name: '生产', host: 'ops.example.com', privateKey: '-----BEGIN KEY-----\n密\n' }],
|
||||
})
|
||||
const encrypted = await encryptSessionBackup(plaintext, 's3cret!')
|
||||
const envelope = JSON.parse(encrypted)
|
||||
expect(isEncryptedSessionBackup(envelope)).toBe(true)
|
||||
expect(encrypted).not.toContain('ops.example.com')
|
||||
expect(encrypted).not.toContain('BEGIN KEY')
|
||||
|
||||
const restored = await decryptSessionBackup(envelope, 's3cret!')
|
||||
expect(restored).toBe(plaintext)
|
||||
})
|
||||
|
||||
it('rejects wrong password', async () => {
|
||||
const encrypted = await encryptSessionBackup('{"sessions":[]}', 'right-password')
|
||||
const envelope = JSON.parse(encrypted)
|
||||
await expect(decryptSessionBackup(envelope, 'wrong-password')).rejects.toThrow(
|
||||
'BACKUP_PASSWORD_INVALID'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,11 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { expandSnippetVariables } from '../../src/utils/snippetVariables'
|
||||
import { applyKeywordHighlights, DEFAULT_KEYWORD_HIGHLIGHT_RULES } from '../../src/utils/keywordHighlight'
|
||||
import {
|
||||
StreamingKeywordHighlight,
|
||||
trailingIncompleteEscape,
|
||||
} from '../../src/utils/streamingKeywordHighlight'
|
||||
import { localShellEnterKey, normalizeTerminalInject } from '../../src/utils/platformShells'
|
||||
|
||||
describe('expandSnippetVariables', () => {
|
||||
it('replaces known placeholders', () => {
|
||||
@@ -35,4 +40,87 @@ describe('applyKeywordHighlights', () => {
|
||||
const text = 'no highlight'
|
||||
expect(applyKeywordHighlights(text, rules)).toBe(text)
|
||||
})
|
||||
|
||||
it('does not inject SGR inside OSC window-title sequences (SSH title leak)', () => {
|
||||
const rules = [
|
||||
{
|
||||
id: 'ipv4',
|
||||
label: 'IPv4',
|
||||
pattern: '\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b',
|
||||
isRegex: true,
|
||||
color: '#8be9fd',
|
||||
enabled: true,
|
||||
},
|
||||
]
|
||||
const osc = '\x1b]0;ssh root@165.154.155.48\x07'
|
||||
const out = applyKeywordHighlights(`${osc}Welcome to Ubuntu`, rules)
|
||||
expect(out).toContain(osc)
|
||||
expect(out).not.toMatch(/\x1b\]0;.*\x1b\[38/)
|
||||
expect(out).toContain('Welcome to Ubuntu')
|
||||
})
|
||||
|
||||
it('still highlights IPs in plain text when the ipv4 rule is enabled', () => {
|
||||
const rules = [
|
||||
{
|
||||
id: 'ipv4',
|
||||
label: 'IPv4',
|
||||
pattern: '\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b',
|
||||
isRegex: true,
|
||||
color: '#8be9fd',
|
||||
enabled: true,
|
||||
},
|
||||
]
|
||||
const out = applyKeywordHighlights('peer 10.0.0.1 online', rules)
|
||||
expect(out).toContain('\x1b[38;2;')
|
||||
expect(out).toContain('10.0.0.1')
|
||||
expect(out).toContain('\x1b[39m')
|
||||
})
|
||||
|
||||
it('leaves existing CSI color sequences intact', () => {
|
||||
const colored = '\x1b[01;32mroot\x1b[00m@host'
|
||||
const out = applyKeywordHighlights(colored, DEFAULT_KEYWORD_HIGHLIGHT_RULES)
|
||||
expect(out).toContain('\x1b[01;32mroot\x1b[00m')
|
||||
})
|
||||
})
|
||||
|
||||
describe('StreamingKeywordHighlight', () => {
|
||||
const ipv4Rules = [
|
||||
{
|
||||
id: 'ipv4',
|
||||
label: 'IPv4',
|
||||
pattern: '\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b',
|
||||
isRegex: true,
|
||||
color: '#8be9fd',
|
||||
enabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
it('holds incomplete OSC across chunks', () => {
|
||||
expect(trailingIncompleteEscape('\x1b]0;ssh root@1.2.3.4')).toBe('\x1b]0;ssh root@1.2.3.4')
|
||||
const stream = new StreamingKeywordHighlight()
|
||||
expect(stream.push('\x1b]0;ssh root@1.2.3.4', ipv4Rules)).toBe('')
|
||||
const done = stream.push('\x07Welcome', ipv4Rules)
|
||||
expect(done).toContain('\x1b]0;ssh root@1.2.3.4\x07')
|
||||
expect(done).toContain('Welcome')
|
||||
expect(done).not.toMatch(/\x1b\]0;.*\x1b\[38/)
|
||||
})
|
||||
|
||||
it('holds incomplete CSI across chunks', () => {
|
||||
const stream = new StreamingKeywordHighlight()
|
||||
expect(stream.push('\x1b[38;2;139', ipv4Rules)).toBe('')
|
||||
const out = stream.push(';233;253m10.0.0.1 done', ipv4Rules)
|
||||
expect(out.startsWith('\x1b[38;2;139;233;253m')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeTerminalInject', () => {
|
||||
it('converts LF to CR like xterm paste', () => {
|
||||
expect(normalizeTerminalInject('echo hi\n', '')).toBe('echo hi\r')
|
||||
expect(normalizeTerminalInject('echo hi', '\r')).toBe('echo hi\r')
|
||||
})
|
||||
|
||||
it('uses CR enter for unix local shells', () => {
|
||||
expect(localShellEnterKey('zsh')).toBe('\r')
|
||||
expect(localShellEnterKey('pwsh')).toBe('\r\n')
|
||||
})
|
||||
})
|
||||
@@ -27,10 +27,10 @@ describe('terminal pane input routing', () => {
|
||||
expect(resolveTerminalInputTargets(panes, 'missing', false)).toEqual([])
|
||||
})
|
||||
|
||||
it('never broadcasts input originating from a local terminal', () => {
|
||||
expect(shouldBroadcastTerminalInput(true, true, 'local')).toBe(false)
|
||||
expect(shouldBroadcastTerminalInput(true, true, 'ssh-connection')).toBe(false)
|
||||
it('broadcasts from both SSH and local terminals when enabled', () => {
|
||||
expect(shouldBroadcastTerminalInput(true, true, 'local')).toBe(true)
|
||||
expect(shouldBroadcastTerminalInput(true, false, 'ssh-connection')).toBe(true)
|
||||
expect(shouldBroadcastTerminalInput(true, false, null)).toBe(false)
|
||||
expect(shouldBroadcastTerminalInput(false, false, 'ssh-connection')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { decodeBase64ToText, encodeTextToBase64 } from '../../src/utils/textBase64'
|
||||
|
||||
describe('textBase64', () => {
|
||||
it('round-trips UTF-8 including CJK', () => {
|
||||
const sample = '{"name":"运维主机","host":"ops.example.com"}'
|
||||
expect(decodeBase64ToText(encodeTextToBase64(sample))).toBe(sample)
|
||||
})
|
||||
|
||||
it('produces standard base64 without raw JSON', () => {
|
||||
const encoded = encodeTextToBase64('{"version":1}')
|
||||
expect(encoded).not.toContain('{')
|
||||
expect(encoded).toMatch(/^[A-Za-z0-9+/=]+$/)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parseSzFiles } from '../../src/utils/zmodemSftpBridge'
|
||||
|
||||
describe('parseSzFiles', () => {
|
||||
it('parses simple sz command', () => {
|
||||
expect(parseSzFiles('sz app.log\r')).toEqual(['app.log'])
|
||||
})
|
||||
|
||||
it('parses multiple files and skips flags', () => {
|
||||
expect(parseSzFiles('sz -y a.txt b.bin\n')).toEqual(['a.txt', 'b.bin'])
|
||||
})
|
||||
|
||||
it('uses the latest command line', () => {
|
||||
expect(parseSzFiles('sz old\rsz newer.tar.gz\r')).toEqual(['newer.tar.gz'])
|
||||
})
|
||||
|
||||
it('returns empty when no sz command', () => {
|
||||
expect(parseSzFiles('ls\rrz\r')).toEqual([])
|
||||
})
|
||||
|
||||
it('strips bracketed-paste markers', () => {
|
||||
expect(parseSzFiles('\x1b[200~sz report.txt\x1b[201~\r')).toEqual(['report.txt'])
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user