按 Claude Design 定稿(归档在 docs/design/)重做 UI,并把 tmux 从 「从不自动进」修成「连接前探测 → 让用户选会话」。 设计令牌与导航地基 - Theme 拆三层:TXAccent(恒定 blurple) / TXChrome(中性阶×4 表) / TXFlavor(Catppuccin 终端) - vendor JetBrains Mono 四权重(附 OFL 许可) - AppRouter + SessionManager:多会话并存,路由与会话生命周期解耦 - SessionCanvas 常驻挂载会话 surface(摘除即丢内容,也是缩回动画的前提) 按设计稿落地的屏 - 沉浸轨道页:56pt 轨道 + 浮起标题/状态胶囊 + 侧边栏三态(遮罩不 resize、Pin 各一次) - 首页:活动会话卡(readViewportText 文本镜像)+ 主机网格 + 筛选 chips - 关闭二次确认(tmux 仅断开 / 原生窗口两套文案)、分屏菜单、pane 拖拽条 - 空状态:首次运行 / 无会话 / 搜索无命中 / 连接中·失败·已关闭 tmux 真实链路(真机查出并修掉 4 个 bug) - 全代码库从来没人发起 attach → 连接前探测 + 会话选择器(接回 / 新建 / 原生终端) - format 分隔符 tab 经 PTY 变成下划线 → 改用 |:| - controller 变化不冒泡到 session → Combine 转发(否则数据解析对了 UI 不刷新) - 当前会话名不能靠 session_attached 反推 → 改用 display-message 传输层:SSH connect 加超时(原来阻塞到系统 TCP 超时 75s+) 无头验证设施:假会话 fixture · terminalx://ui/* 驱动 · 横屏截图脚本 · 对拍走查法 TXCore 45 tests 绿(新增 5 个会话探测单测) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
70 lines
3.0 KiB
Swift
70 lines
3.0 KiB
Swift
import Foundation
|
||
import Testing
|
||
@testable import TXCore
|
||
|
||
@Suite("tmux 会话探测(连接前选择用)")
|
||
struct TmuxSessionProbeTests {
|
||
/// 命令里的标记必须是**拼接写法** —— 否则 PTY 回显会被误当成结果起点。
|
||
@Test("命令不含完整标记字面量,避免 PTY 回显误命中")
|
||
func commandAvoidsEchoSelfMatch() {
|
||
let cmd = TmuxSessionProbe.command
|
||
#expect(!cmd.contains(TmuxSessionProbe.beginMark))
|
||
#expect(!cmd.contains(TmuxSessionProbe.endMark))
|
||
#expect(cmd.contains("tmux -V"))
|
||
#expect(cmd.contains("list-sessions"))
|
||
}
|
||
|
||
@Test("解析会话列表(CRLF 行尾 + 版本行)")
|
||
func parsesSessions() {
|
||
var p = TmuxSessionProbe()
|
||
// 先喂一段回显噪声(含拼接前的命令文本),不应触发。
|
||
#expect(p.feed("kid@Air ~> printf '%s\\n' \"__TX\"\"_SESSIONS_BEGIN\"\r\n") == nil)
|
||
#expect(p.feed("\(TmuxSessionProbe.beginMark)\r\n") == nil)
|
||
#expect(p.feed("tmux 3.7b\r\n") == nil)
|
||
#expect(p.feed("S|:|main|:|5|:|0|:|1784988802\r\n") == nil)
|
||
#expect(p.feed("S|:|demo|:|2|:|1|:|1784901778\r\n") == nil)
|
||
let result = p.feed("\(TmuxSessionProbe.endMark)\r\n")
|
||
guard case .available(let version, let sessions) = result else {
|
||
Issue.record("期望 available,实际 \(String(describing: result))")
|
||
return
|
||
}
|
||
#expect(version == "tmux 3.7b")
|
||
#expect(sessions.count == 2)
|
||
#expect(sessions[0] == TmuxSessionProbe.Session(
|
||
name: "main", windows: 5, isAttached: false,
|
||
lastActivity: Date(timeIntervalSince1970: 1784988802)))
|
||
#expect(sessions[1].name == "demo")
|
||
#expect(sessions[1].isAttached)
|
||
// 一次性:命中后不再返回结果。
|
||
#expect(p.feed("S|:|late|:|1|:|0|:|1\r\n") == nil)
|
||
}
|
||
|
||
@Test("装了 tmux 但没有会话 → available 且列表为空(UI 只给新建/跳过)")
|
||
func availableWithNoSessions() {
|
||
var p = TmuxSessionProbe()
|
||
_ = p.feed("\(TmuxSessionProbe.beginMark)\n")
|
||
_ = p.feed("tmux 3.5a\n")
|
||
let r = p.feed("\(TmuxSessionProbe.endMark)\n")
|
||
#expect(r == .available(version: "tmux 3.5a", sessions: []))
|
||
}
|
||
|
||
@Test("没装 tmux(无版本行)→ unavailable,直接走原生终端")
|
||
func unavailableWithoutVersion() {
|
||
var p = TmuxSessionProbe()
|
||
_ = p.feed("\(TmuxSessionProbe.beginMark)\n")
|
||
let r = p.feed("\(TmuxSessionProbe.endMark)\n")
|
||
#expect(r == .unavailable)
|
||
}
|
||
|
||
@Test("畸形会话行被跳过而非崩溃")
|
||
func skipsMalformedLines() {
|
||
var p = TmuxSessionProbe()
|
||
_ = p.feed("\(TmuxSessionProbe.beginMark)\ntmux 3.7\n")
|
||
_ = p.feed("S|:|\n") // 字段不足
|
||
_ = p.feed("S|:||:|3|:|0\n") // 空名字
|
||
_ = p.feed("S|:|ok|:|x|:|0\n") // windows 非数字
|
||
let r = p.feed("\(TmuxSessionProbe.endMark)\n")
|
||
#expect(r == .available(version: "tmux 3.7", sessions: []))
|
||
}
|
||
}
|