Files
terminalX/vendor/libghostty-spm/Tests/GhosttyKitTest/ShellCraftKitTests.swift
kid aa92d0e676 初始提交:terminalX 可运行态(M0/M1/M1.5 已真机验证)
- M0: libghostty SSH 终端(渲染/输入/连接)+ 白屏修复(OutputGate) + 会话状态机/自动重连
- M1: tsnet 用户态组网 + SSH-over-tsnet(fd 桥),shell 级真机验证;R5(Go+gvisor+C+++Swift 同进程) retire
- M1.5: tmux -CC 原生 tab(MVP)
- 结构: packages/(TXCore·TXTransport), apps/TerminalX, vendor/(libghostty-spm/libssh2/mbedtls/tsnet-bridge), artifacts/
- 文档: CLAUDE.md + docs/HANDOFF.md(新会话入口)
- 环境: 认证代理→依赖 vendor 本地化;Go 在 ~/.local/go;仅模拟器/未签名
- 待续: M2 mosh, tmux 多 pane, M4 安全(host key/SE)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 10:20:46 +08:00

447 lines
14 KiB
Swift

import Foundation
import GhosttyTerminal
@testable import ShellCraftKit
import Testing
struct ShellCraftKitTests {
@Test
func `styled prompt uses visible column width`() {
let shell = ShellDefinition(
prompt: "\u{1B}[38;5;110mcolor\u{1B}[0m > ",
welcomeMessage: ""
) {}
#expect(shell.promptDisplayWidth == 8)
}
@Test
func `terminal display width counts wide characters`() {
#expect("abc".terminalDisplayWidth == 3)
#expect("你好".terminalDisplayWidth == 4)
#expect("a你b好".terminalDisplayWidth == 6)
#expect("\u{1B}[31m红色\u{1B}[0m".terminalDisplayWidth == 4)
}
@Test
func `cursor column uses display width instead of character count`() {
#expect(
terminalCursorColumn(
promptDisplayWidth: 8,
input: "测试",
cursorPosition: 2
) == 13
)
#expect(
terminalCursorColumn(
promptDisplayWidth: 8,
input: "a测b",
cursorPosition: 2
) == 12
)
#expect(
terminalCursorColumn(
promptDisplayWidth: 8,
input: "你好吗",
cursorPosition: 1
) == 11
)
}
@Test
func `rendered input state tracks wrapped lines and cursor placement`() {
let state = terminalRenderedInputState(
promptDisplayWidth: 18,
input: "hello world",
cursorPosition: 11,
terminalColumns: 20
)
#expect(state.totalLineCount == 2)
#expect(state.cursorLineOffset == 1)
#expect(state.cursorColumn == 10)
}
@Test
func `rendered input state handles prompt only wrapping`() {
let state = terminalRenderedInputState(
promptDisplayWidth: 18,
input: "",
cursorPosition: 0,
terminalColumns: 10
)
#expect(state.totalLineCount == 2)
#expect(state.cursorLineOffset == 1)
#expect(state.cursorColumn == 9)
}
@Test
func `wrapped terminal line count handles exact boundary`() {
#expect(wrappedTerminalLineCount(displayWidth: 20, terminalColumns: 20) == 1)
#expect(wrappedTerminalLineCount(displayWidth: 21, terminalColumns: 20) == 2)
}
@Test
func `rendered input state keeps cursor on boundary without trailing content`() {
let state = terminalRenderedInputState(
promptDisplayWidth: 18,
input: "ab",
cursorPosition: 2,
terminalColumns: 20
)
#expect(state.totalLineCount == 1)
#expect(state.cursorLineOffset == 0)
#expect(state.cursorColumn == 20)
}
@Test
func `rendered input state wraps boundary cursor when trailing content exists`() {
let state = terminalRenderedInputState(
promptDisplayWidth: 18,
input: "abc",
cursorPosition: 2,
terminalColumns: 20
)
#expect(state.totalLineCount == 2)
#expect(state.cursorLineOffset == 1)
#expect(state.cursorColumn == 1)
}
@Test
func `incremental append is allowed for tail insertion`() {
#expect(
canIncrementallyAppendInput(
previousInput: "hello",
previousCursorPosition: 5,
insertedText: " world"
)
)
#expect(
canIncrementallyAppendInput(
previousInput: "ni",
previousCursorPosition: 2,
insertedText: "你好"
)
)
}
@Test
func `incremental append falls back for mid line or control input`() {
#expect(
!canIncrementallyAppendInput(
previousInput: "hello",
previousCursorPosition: 2,
insertedText: "X"
)
)
#expect(
!canIncrementallyAppendInput(
previousInput: "hello",
previousCursorPosition: 5,
insertedText: "\t"
)
)
}
@Test
func `tab expansion uses visible cursor column`() {
#expect(
terminalExpandedTabText(
promptDisplayWidth: 2,
input: "abc",
cursorPosition: 3,
terminalColumns: 80
) == " "
)
#expect(
terminalExpandedTabText(
promptDisplayWidth: 7,
input: "",
cursorPosition: 0,
terminalColumns: 80
) == " "
)
}
@Test
func `tab expansion respects wrapped cursor column`() {
#expect(
terminalExpandedTabText(
promptDisplayWidth: 18,
input: "ab",
cursorPosition: 2,
terminalColumns: 20
) == String(repeating: " ", count: 5)
)
}
@Test
func `meta editing action recognizes word sequences`() {
#expect(terminalMetaEditingAction(for: 0x7F) == .deleteBackwardWord)
#expect(terminalMetaEditingAction(for: 0x08) == .deleteBackwardWord)
#expect(terminalMetaEditingAction(for: 0x62) == .moveBackwardWord)
#expect(terminalMetaEditingAction(for: 0x66) == .moveForwardWord)
#expect(terminalMetaEditingAction(for: 0x64) == .deleteForwardWord)
#expect(terminalMetaEditingAction(for: 0x42) == nil)
#expect(terminalMetaEditingAction(for: 0x78) == nil)
}
@Test
func `csi editing action recognizes modified arrow word sequences`() {
#expect(
terminalCSIEditingAction(params: Data("1;3".utf8), finalByte: 0x44)
== .moveCursorBackwardWord
)
#expect(
terminalCSIEditingAction(params: Data("1;3".utf8), finalByte: 0x43)
== .moveCursorForwardWord
)
#expect(
terminalCSIEditingAction(params: Data(), finalByte: 0x44)
== .moveCursorLeft
)
#expect(
terminalCSIEditingAction(params: Data(), finalByte: 0x43)
== .moveCursorRight
)
#expect(
terminalCSIEditingAction(params: Data("3".utf8), finalByte: 0x7E)
== .deleteForward
)
#expect(
terminalCSIEditingAction(params: Data("1;4".utf8), finalByte: 0x44)
== .moveCursorBackwardWord
)
#expect(
terminalCSIEditingAction(params: Data("3;3".utf8), finalByte: 0x7E)
== .deleteForwardWord
)
}
@Test
func `csi modifier detection matches alt suffix`() {
#expect(terminalCSIHasAltModifier(Data("1;3".utf8)))
#expect(terminalCSIHasAltModifier(Data("1;4".utf8)))
#expect(!terminalCSIHasAltModifier(Data("1;5".utf8)))
#expect(!terminalCSIHasAltModifier(Data("3".utf8)))
#expect(!terminalCSIHasAltModifier(Data()))
}
@Test
func `word boundaries treat punctuation as separators for meta motion`() {
#expect(terminalPreviousWordBoundary(in: "alpha beta", from: 10) == 6)
#expect(terminalPreviousWordBoundary(in: "alpha beta ", from: 12) == 6)
#expect(terminalNextWordBoundary(in: "alpha beta", from: 0) == 5)
#expect(terminalNextWordBoundary(in: "alpha beta", from: 5) == 12)
#expect(terminalPreviousWordBoundary(in: "foo-bar", from: 7) == 4)
#expect(terminalNextWordBoundary(in: "foo-bar", from: 0) == 3)
#expect(terminalPreviousWordBoundary(in: "héllo wörld", from: 11) == 6)
}
@Test
func `shell word boundaries remain whitespace delimited for control W`() {
#expect(terminalPreviousShellWordBoundary(in: "foo-bar baz", from: 11) == 8)
#expect(terminalPreviousShellWordBoundary(in: "alpha beta ", from: 12) == 6)
#expect(terminalNextShellWordBoundary(in: "alpha beta", from: 5) == 12)
}
@Test
func `delete backward word removes previous word and trailing spaces`() {
let result = terminalDeleteBackwardWord(
input: "alpha beta ",
cursorPosition: 12
)
#expect(result.input == "alpha ")
#expect(result.cursorPosition == 6)
}
@Test
func `delete forward word removes next word and leading spaces`() {
let result = terminalDeleteForwardWord(
input: "alpha beta gamma",
cursorPosition: 5
)
#expect(result.input == "alpha gamma")
#expect(result.cursorPosition == 5)
}
@Test
func `delete backward shell word removes previous whitespace delimited token`() {
let result = terminalDeleteBackwardShellWord(
input: "foo-bar baz ",
cursorPosition: 13
)
#expect(result.input == "foo-bar ")
#expect(result.cursorPosition == 8)
}
@Test
func `sandbox shell supports exit and styled fallback`() {
let viewport = InMemoryTerminalViewport(
columns: 80,
rows: 24,
widthPixels: 0,
heightPixels: 0
)
switch defaultSandboxShell.processCommand(
"exit",
username: "tester",
terminalSize: viewport
) {
case .exit:
break
default:
Issue.record("expected sandbox shell exit command to terminate the session")
}
if case let .output(message) = defaultSandboxShell.processCommand(
"missing-command",
username: "tester",
terminalSize: viewport
) {
#expect(message.contains("\u{1B}["))
#expect(message.contains("missing-command"))
} else {
Issue.record("expected fallback command result to produce output")
}
}
// MARK: - decodeUTF8Incrementally
@Test
func `utf 8 incremental decodes complete ASCII`() {
let (text, leftover) = decodeUTF8Incrementally(Data("hello".utf8))
#expect(text == "hello")
#expect(leftover.isEmpty)
}
@Test
func `utf 8 incremental decodes complete chinese`() {
let (text, leftover) = decodeUTF8Incrementally(Data("你好".utf8))
#expect(text == "你好")
#expect(leftover.isEmpty)
}
@Test
func `utf 8 incremental retains incomplete three byte sequence`() {
// "" is E4 BD A0 send only first 2 bytes
let partial = Data([0xE4, 0xBD])
let (text1, leftover1) = decodeUTF8Incrementally(partial)
#expect(text1 == "")
#expect(leftover1 == partial)
// Now complete the sequence
let full = leftover1 + Data([0xA0])
let (text2, leftover2) = decodeUTF8Incrementally(full)
#expect(text2 == "")
#expect(leftover2.isEmpty)
}
@Test
func `utf 8 incremental retains incomplete four byte sequence`() {
// 😀 is F0 9F 98 80 send only first 3 bytes
let partial = Data([0xF0, 0x9F, 0x98])
let (text1, leftover1) = decodeUTF8Incrementally(partial)
#expect(text1 == "")
#expect(leftover1 == partial)
let full = leftover1 + Data([0x80])
let (text2, leftover2) = decodeUTF8Incrementally(full)
#expect(text2 == "😀")
#expect(leftover2.isEmpty)
}
@Test
func `utf 8 incremental skips illegal lead byte FF`() {
let (text, leftover) = decodeUTF8Incrementally(Data([0xFF]))
#expect(text == "")
#expect(leftover.isEmpty)
}
@Test
func `utf 8 incremental skips overlong lead C 0 C 1`() {
// 0xC0 and 0xC1 are overlong, should be skipped
let input = Data([0xC0, 0x41, 0xC1, 0x42])
let (text, leftover) = decodeUTF8Incrementally(input)
#expect(text == "AB")
#expect(leftover.isEmpty)
}
@Test
func `utf 8 incremental skips lead F 5 plus`() {
let input = Data([0xF5, 0x41, 0xF6, 0x42, 0xF7, 0x43])
let (text, leftover) = decodeUTF8Incrementally(input)
#expect(text == "ABC")
#expect(leftover.isEmpty)
}
@Test
func `utf 8 incremental skips only lead byte on invalid combination`() {
// 0xE4 expects 2 continuation bytes, but next bytes are ASCII
let input = Data([0xE4, 0x41, 0x42])
let (text, leftover) = decodeUTF8Incrementally(input)
#expect(text == "AB")
#expect(leftover.isEmpty)
}
@Test
func `utf 8 incremental preserves valid text after illegal byte`() {
let input = Data([0xFF, 0x68, 0x65, 0x6C, 0x6C, 0x6F]) // 0xFF + "hello"
let (text, leftover) = decodeUTF8Incrementally(input)
#expect(text == "hello")
#expect(leftover.isEmpty)
}
@Test
func `utf 8 incremental handles empty data`() {
let (text, leftover) = decodeUTF8Incrementally(Data())
#expect(text == "")
#expect(leftover.isEmpty)
}
@Test
func `utf 8 incremental handles mixed valid and incomplete`() {
// "ab" + incomplete 3-byte lead
let input = Data([0x61, 0x62, 0xE4, 0xBD])
let (text, leftover) = decodeUTF8Incrementally(input)
#expect(text == "ab")
#expect(leftover == Data([0xE4, 0xBD]))
}
@Test
func `utf 8 incremental decomposed unicode`() {
// e (0x65) + combining acute accent U+0301 (0xCC 0x81)
let input = Data([0x65, 0xCC, 0x81])
let (text, leftover) = decodeUTF8Incrementally(input)
#expect(text == "e\u{0301}")
#expect(leftover.isEmpty)
}
@Test
func `utf 8 incremental skips lead with non continuation tail`() {
// 0xE4 is 3-byte lead, but 0x41 is ASCII not a valid continuation
let input = Data([0xE4, 0x41])
let (text, leftover) = decodeUTF8Incrementally(input)
#expect(text == "A")
#expect(leftover.isEmpty)
}
@Test
func `utf 8 incremental skips four byte lead with invalid tail`() {
// 0xF0 is 4-byte lead, but 0x41/0x42 are ASCII
let input = Data([0xF0, 0x41, 0x42])
let (text, leftover) = decodeUTF8Incrementally(input)
#expect(text == "AB")
#expect(leftover.isEmpty)
}
}