test(main): cover updater fallback, ipc sender guard, log sanitizer, sftp timeouts

- updater-fallback.mjs (82 assertions): GitHub probe fallback to Gitea,
  timeout budgets, in-flight check never stuck in 'checking'; updater.ts
  gains a setUpdateTimeouts test seam, electron-updater aliased to a stub
- ipc-guard.mjs (43): trusted-frame guard exercised through real
  registerIpc handlers with forged senderFrames; electron-stub now records
  registrations via globalThis so bundle and test share one instance
- log-sanitizer.mjs (71): CSI/OSC/charset state machine, alt-screen fold,
  byte-split fuzz equal to whole-chunk output; fixes a wrong comment
- sftp-timeout.mjs (39): per-op timeouts (metadata 30s, transfer chunk 60s,
  open 10s) evict half-dead channels with one retry, slow-but-progressing
  transfers untouched, late rejections never unhandled
- reservedAccelerators.ts: single pure isReservedAccelerator shared by the
  settings recorder and applyGlobalShortcut (the two tables had drifted —
  main now also refuses Ctrl+=/-/0/PgUp/PgDn legacy values); 56 assertions
- ssh-loopback.mjs loads the real ssh.ts via a bundle (50 assertions):
  TOFU pinning, fail-closed stores, auth gate, connect budget

Offline suite grows 13 -> 17. Renderer test framework evaluated: not
introducing vitest/jsdom; pure logic keeps being extracted and tested
through the existing bundle harness.
This commit is contained in:
Bill committed 2026-10-07 22:57:16 +08:00
1 parent 5ff311ea0f
commit a0be827650
21 files changed
+2370 -174

No files matched your search

+5 -21
View File
@@ -3,6 +3,7 @@ import { useMemo, useState } from 'react'
import { Input, InputNumber, Radio, Select, Switch } from 'antd'
import type { ThemeColors } from '@shared/theme'
import { DEFAULT_LANGUAGE, LANGUAGES, t, type Language } from '@shared/i18n'
import { isReservedAccelerator } from '@shared/reservedAccelerators'
import { useSettingsStore, useResolvedTheme } from './store'
import {
DEFAULT_FONT_STACK,
@@ -482,28 +483,11 @@ function acceleratorFromEvent(e: React.KeyboardEvent<HTMLInputElement>): string
}
/**
* Keys this app binds while Control is held: font size (Ctrl+=/-/0, main.tsx)
* and tab cycling (Ctrl+PgUp/PgDn, Workspace). Neither handler looks at the
* other modifiers, so any Control combo on one of these keys would shadow the
* in-app action — the recorder refuses it instead of saving a shortcut that
* silently loses its original meaning.
* The reserved-accelerator table (in-app font/tab chords and the Ctrl+L panic
* lock) lives in @shared/reservedAccelerators so this recorder and the main
* process's registration guard (`applyGlobalShortcut`) cannot drift apart —
* they are the two halves of one rule. See that module for the rationale.
*/
const RESERVED_CONTROL_KEYS = new Set(['=', '-', '0', 'PageUp', 'PageDown'])
/**
* Whole chords the app owns outright, matched exactly (modifier set included).
* Ctrl+L is the panic lock, captured in main's before-input-event: a global
* registration intercepts the key at the OS level even while this window is
* focused, so binding it here would silently disable the lock shortcut.
*/
const RESERVED_EXACT_ACCELERATORS = new Set(['Control+L'])
/** true when `accel` (e.g. "Control+Shift+=") collides with an in-app shortcut */
function isReservedAccelerator(accel: string): boolean {
if (RESERVED_EXACT_ACCELERATORS.has(accel)) return true
const parts = accel.split('+')
return parts.includes('Control') && RESERVED_CONTROL_KEYS.has(parts[parts.length - 1])
}
/** t() falls back to the key itself when a translation is missing. */
function tOr(key: string, fallback: string): string {