feat(ios,android): P2 wave, git panel, token UX, per-host WS token, docs
App layer, four sequential slices (a shared .xcodeproj means adding files regenerates it, so these could not run in parallel): - token UX end to end: pairing prompts for a token when a host 401s, POST /auth validates it, and 204-without-Set-Cookie is correctly read as "this server has auth disabled" rather than "authenticated". A host paired before the token was turned on recovers by re-pairing in place. Remove-host now exists and finally gives PushRegistrar.handleHostRemoved a caller. - project git panel + worktree lifecycle (T-iOS-32) + claude --resume history — the parity gap with Android and the web front end. - terminal search (T-iOS-33) and voice PTT (T-iOS-31) with an epoch guard so a session switch between dictation and confirm cannot inject into the wrong session. - theme + Dynamic Type (T-iOS-34) and web ?join= interop (T-iOS-35). RootView no longer hard-locks .preferredColorScheme(.dark). Also unpins SwiftTerm to 1.15.0 by dropping the local hasActiveSelection that collided with the upstream one, verified green from a fresh derivedDataPath. Includes the two HIGH fixes the security review found: - iOS resolved the WS token host-independently, so a token-gated host sitting next to an open one could never open a terminal and no on-screen remedy could fix it. Now one transport per host; cross-host leakage is structurally impossible since both read paths return only that host's own value. - Android reported the host's own git-credential 401 (git-ops.ts:108, "Push authentication required on the host.") as "your access token is wrong", because a blanket 401 mapping ran ahead of the per-route one. Git-write routes are now ROUTE_DEFINED and keep the server's message. And the doc sync: README/ios README no longer claim the client is unmerged on feat/ios-client, the Clients section finally lists Android, and the plan checkboxes reflect what is actually built. iOS 534 app tests + 452 package tests; Android 687 tests.
This commit is contained in:
@@ -146,4 +146,284 @@ A1 必须一次把**后续所有波次需要的 Info.plist 键**都加好(它
|
||||
|
||||
## 4. P2 RED 测试清单(由各 P2 builder 在开工第一步追加到本节)
|
||||
|
||||
<!-- C3 / C4 在此追加 T-iOS-31/33/34/35 的 RED 清单 -->
|
||||
### T-iOS-32 · Worktree 生命周期(create/prune/remove)+ `claude --resume` 历史 —— C2 追加
|
||||
|
||||
真源:`src/http/worktrees.ts` / `src/server.ts:1095-1183`、`docs/plans/w4-worktree-lifecycle.md`、
|
||||
web 参照实现 `public/projects.ts`(`validateBranchNameClient` :982、`confirmAndRemoveWorktree` :431、
|
||||
`confirmAndPruneWorktrees` :455、`renderNewWorktreeForm` :998、`newTabForResume` `public/tabs.ts:918`)。
|
||||
APIClient 侧 builder/状态码映射已在 B1 测过(125 tests),故以下全部是 **App 层归约**测试,
|
||||
文件 `ios/App/WebTermTests/WorktreeViewModelTests.swift` / `ResumeHistoryViewModelTests.swift`。
|
||||
|
||||
**A. 分支名校验(纯函数 `WorktreeBranchRule.validate`,逐条镜像 web 的 9 条规则)**
|
||||
1. RED:空串 → 非 nil 错误文案(不得放行到网络)。
|
||||
2. RED:长度 > 250 → 报错;== 250 → 通过。
|
||||
3. RED:含空格 / 控制字符(`\u{0}`…`\u{20}`、`\u{7f}`)→ 报错。
|
||||
4. RED:以 `-` 开头 → 报错(否则会变成 git flag)。
|
||||
5. RED:含 `..`、以 `.lock` 结尾 → 报错。
|
||||
6. RED:含 `~ ^ : ? * [ \` 任一 → 报错。
|
||||
7. RED:含 `@{` → 报错。
|
||||
8. RED:以 `/` 开头、以 `/` 结尾、含 `//` → 报错。
|
||||
9. RED:合法名(`feat/x`、`v1.2-fix`)→ nil。
|
||||
|
||||
**B. 创建 worktree(`POST /projects/worktree`,G)**
|
||||
10. RED:非法分支名 → **不发请求**(记录一次调用计数 == 0),只显示校验错误。
|
||||
11. RED:`.ok(CreateWorktreeResult)` → `phase == .created(path:branch:)`,且把服务器返回的
|
||||
canonical `path`/`branch` 当真相(不回显本地输入)。
|
||||
12. RED:`base` 留空 → 请求体的 `base` 为 nil(服务器读作「从 HEAD 切」),**不得**送 `""`。
|
||||
13. RED:`.rejected(status:403, message:"…")` → 原样显示服务器安全文案(不得吞、不得自造)。
|
||||
14. RED:`.rejected(status:500, message:nil)` → 兜底中文文案(非空、可读)。
|
||||
15. RED:`.rateLimited` → 专用「请稍后再试」文案,且**不自动重试**。
|
||||
16. RED:抛出的 `APIClientError.unauthorized` → 引导补令牌的文案(不与 500 混淆)。
|
||||
17. RED:创建进行中 `isBusy == true` 且重复提交只发一次请求。
|
||||
|
||||
**C. prune(`POST /projects/worktree/prune`,G,破坏性 → 需确认)**
|
||||
18. RED:未确认(`confirmPrune` 未调用)→ 零请求。
|
||||
19. RED:确认后 `.ok(pruned: [])` → 幂等文案「没有可回收的 worktree」,非错误态。
|
||||
20. RED:`.ok(pruned: [a,b])` → 文案含数量 2。
|
||||
21. RED:`.rejected(404, msg)` → 显示服务器文案。
|
||||
|
||||
**D. remove(`DELETE /projects/worktree`,G,两级确认)**
|
||||
22. RED:主 worktree(`isMain`)→ UI 不提供 remove(`canRemove == false`);locked 同样 false。
|
||||
23. RED:第一次确认 → 以 `force: false` 发一次请求。
|
||||
24. RED:`.rejected(409, msg)` → 进入 `.forceConfirming`(**不自动**重试),并带上服务器文案。
|
||||
25. RED:`.forceConfirming` 下用户取消 → 零第二次请求。
|
||||
26. RED:`.forceConfirming` 下用户确认 → 第二次请求 `force: true`。
|
||||
27. RED:`.rejected(400)`(非 409)→ 直接错误态,**不**进入 force 分支。
|
||||
|
||||
**E. `claude --resume <id>` 历史(`GET /sessions`)**
|
||||
28. RED:`GET /sessions` 成功 → 仅保留 cwd 落在本项目路径内的会话
|
||||
(`cwd == path` 或 `cwd.hasPrefix(path + "/")`),其余过滤掉。
|
||||
29. RED:路径前缀不得半路匹配:`/repos/web-terminal-old` 不属于 `/repos/web-terminal`。
|
||||
30. RED:服务器顺序(mtime 新→旧)保持不变,客户端不重排。
|
||||
31. RED:空结果 → `.empty` 而非 `.failed`(服务器 `~/.claude/projects` 缺失时回 `[]`)。
|
||||
32. RED:加载失败 → `.failed(可重试)`,`load()` 可重试成功。
|
||||
33. RED(**安全,web 侧缺失的校验**):`ProjectResumeCommand.bootstrapInput(sessionId:)`
|
||||
对 id 做 `[A-Za-z0-9._-]{1,128}` 白名单 —— `abc; rm -rf ~`、含空格/反引号/`$(`/`\n` 的 id
|
||||
→ 返回 nil(**绝不**拼进 PTY 命令行);合法 UUID stem → `"claude --resume <id>\r"`,
|
||||
结尾必须是 `\r`(0x0D,不是 `\n`)。
|
||||
34. RED:非法 id 的历史行 `canResume == false`(UI 不提供恢复按钮)。
|
||||
35. RED:cwd 非绝对路径的历史条目 → 不可恢复(`Validation.isAbsoluteCwd` 纪律)。
|
||||
|
||||
### 附:C2 git 面板(非 P2,但同批交付)RED 清单
|
||||
|
||||
`ios/App/WebTermTests/GitPanelPresentationTests.swift` / `GitPanelViewModelTests.swift`。
|
||||
真源 `docs/plans/w6-project-git-panel.md` + `public/projects.ts:615-745 makeSyncBand`。
|
||||
|
||||
36. RED:`sync == nil`(非 git 目录)→ 无同步带(nil),不发任何 git 请求。
|
||||
37. RED:`↑0 ↓0` + fetch 在 1h 内 → **唯一**允许的绿色「已同步」。
|
||||
38. RED:`↑0 ↓0` + `lastFetchMs` 超过 `FETCH_STALE_MS`(=3600_000) → **不绿**,`↓` 带「未核实」。
|
||||
39. RED:`lastFetchMs == nil`(从未 fetch)→ 视为 stale,不绿。
|
||||
40. RED:`upstream == nil` → 「无上游」,无 `↑↓`,**不绿**(最易犯的 bug)。
|
||||
41. RED:`detached == true` → 「分离 HEAD」,无 `↑↓`,fetch 按钮禁用(`canFetch == false`)。
|
||||
42. RED:`ahead == nil`(读不到)→ 显示 `↑ —` 而非 `↑ 0`,且不绿。
|
||||
43. RED:`dirtyCount == nil` → 「未检查」,**不是** clean;`0` → clean;`3` → 3。
|
||||
44. RED:git log 未推送边界:`upstream != nil` 时边界画在最后一个 `unpushed == true` 之后,
|
||||
且只画一次;`unpushed` 只在严格 `true` 时生效。
|
||||
45. RED:`upstream == nil` → 完全不画边界。
|
||||
46. RED:未推送提交排在已推送之下(老日期 merge)→ 边界仍在最后一个 unpushed 之后(Set 为准,非行号)。
|
||||
47. RED:stage/commit/push/fetch 的 `.rejected` 一律把服务器 `error` 原样显示;`nil` message → 兜底文案。
|
||||
48. RED:commit 成功 → 清空输入框 + 显示短 sha;`.rejected(409)`(无暂存)→ 保留输入框内容。
|
||||
49. RED:push `.rejected(401)` → 「主机上的 git 凭据」文案,**不**与访问令牌 401 混淆
|
||||
(`unauthorizedPolicy == .routeDefined`,§1.1)。
|
||||
50. RED:任一写操作进行中 → `isBusy`,重复点击只发一次。
|
||||
51. RED:重命名文件 stage → 请求体同时含 `oldPath` 与 `newPath`(镜像 web)。
|
||||
|
||||
### T-iOS-33 · 终端内搜索 —— C3 追加
|
||||
|
||||
真源:SwiftTerm 1.13.0 已带搜索 API(`TerminalViewSearch.swift`:`findNext(_:options:scrollToResult:)`、
|
||||
`findPrevious(...)`、`clearSearch()`,`SearchOptions(caseSensitive:false, regex:false, wholeWord:false)`
|
||||
默认值与 xterm.js search addon 一致)。web 参照 `public/search.ts`(Enter=next / Shift+Enter=prev /
|
||||
Esc=关闭并 `hooks.clear()`;`public/terminal-session.ts:547-553` 把三个 hook 落到 SearchAddon;
|
||||
`style.css:812` searchbox 固定在**右上**)。文件 `ios/App/WebTermTests/TerminalSearchTests.swift`。
|
||||
|
||||
**A. 查询提交策略(纯函数 `TerminalSearchQuery.isSubmittable`)**
|
||||
1. RED:空串 → 不可提交,且**零引擎调用**(不把空词丢给 SwiftTerm)。
|
||||
2. RED:单个空格 `" "` → **可**提交(镜像 web:`input.value` 原样进 `findNext`,空格是合法搜索词)。
|
||||
3. RED:`" foo "` → 原样传(**不 trim、不改大小写**,逐字符等于用户输入)。
|
||||
|
||||
**B. 方向与引擎调用(`TerminalSearchModel` over fake `TerminalSearching`)**
|
||||
4. RED:`find(.next)` → 恰一次 `searchNext(term:)`,term 逐字符等于 query,零 `searchPrevious`。
|
||||
5. RED:`find(.previous)` → 恰一次 `searchPrevious`,零 `searchNext`。
|
||||
6. RED:引擎回 true → `outcome == .found`。
|
||||
7. RED:引擎回 false → `outcome == .notFound`,且有非空「无匹配」文案。
|
||||
8. RED:连续三次 `find(.next)` → 三次引擎调用(搜索游标由 SwiftTerm 自己推进,客户端不缓存)。
|
||||
9. RED:未 attach 引擎(`searcher == nil`)→ 不崩、`outcome` 保持 `.idle`(预览/测试环境)。
|
||||
|
||||
**C. 打开/关闭生命周期**
|
||||
10. RED:初始 `isPresented == false` 且 `outcome == .idle`。
|
||||
11. RED:`present()`→true,`dismiss()`→false。
|
||||
12. RED:`dismiss()` → 恰一次 `searchClear()`(镜像 web `hide() → hooks.clear()`)+ 清空 query + `outcome == .idle`。
|
||||
13. RED:`present()` **不**调用任何引擎方法(开面板 ≠ 搜索)。
|
||||
14. RED:改 query → `outcome` 复位 `.idle`(旧的「无匹配」不得挂在新词上)。
|
||||
|
||||
**D. 不变式:搜索是纯读**
|
||||
15. RED:整个搜索流程后真 `TerminalViewModel.forwardedSendCount == 0`(零 PTY 字节,byte-shuttle 不变)。
|
||||
16. RED:终端已 `.exited`(read-only)时搜索照常可用(`find` 仍打引擎)。
|
||||
|
||||
**E. SwiftTerm 绑定(Accept:命中并高亮)**
|
||||
17. RED:真 `KeyCommandTerminalView` feed 一段文本后 `searchNext(term:)` 命中 → 返回 true **且**
|
||||
`hasActiveSelection == true`(= SwiftTerm 选区高亮);搜不存在的词 → false。
|
||||
18. RED:`searchClear()` → `hasActiveSelection == false`(高亮清掉)。
|
||||
|
||||
### T-iOS-31 · 语音 PTT + 确认 —— C3 追加
|
||||
|
||||
真源(三件套逐条移植 web,plan §7「端口匹配器 / 1.5s 撤销 / epoch 防误发」= port `voice-commands.ts` 的匹配器):
|
||||
`public/voice.ts`(PTT 生命周期、`autoSend:false` 默认 = **不自动回车**)、
|
||||
`public/voice-commands.ts`(整句精确匹配器 + 否定守卫 + `MIN_APPROVE_CONFIDENCE=0.6`)、
|
||||
`public/voice-confirm.ts`(`DEFAULT_WINDOW_MS = 1500` 可撤销窗口)、
|
||||
`SessionCore/GateState.swift`(epoch 防陈旧决策的既有先例 `canDecide(epoch:)`)。
|
||||
文件 `ios/App/WebTermTests/VoicePTTTests.swift`。
|
||||
|
||||
**A. 转写清洗(安全边界,纯函数 `VoiceTranscript`)**
|
||||
19. RED:普通文本 → 首尾 trim 后原样。
|
||||
20. RED:含 `\r`(0x0D)→ **剥除**(口述绝不自己回车执行命令;等价 web `autoSend:false`)。
|
||||
21. RED:含 `\n`/`\t`/ESC `\u{1b}`/其它 C0-C1 控制字符 → 全部剥除。
|
||||
22. RED:多行 → 折成单行(换行变空格 + 合并连续空白)。
|
||||
23. RED:超 `maxLength` → 截断到上限。
|
||||
24. RED:清洗后为空 → `isInjectable == false`(不进确认态)。
|
||||
|
||||
**B. 匹配器移植(`VoiceCommandMatcher`,逐条镜像 `voice-commands.ts`)**
|
||||
25. RED:无 held gate → 任何话语都是 `.text`(含「确认」)。
|
||||
26. RED:gate 是 `.plan` → `.text`(v1 只作用 tool gate)。
|
||||
27. RED:tool gate + 「确认」/「批准」/`ok`/`go ahead` → `.approve`。
|
||||
28. RED:归一化(小写 + 去标点 + 合并空白):`OK.`/「好的!」命中;`don't` → `dont`。
|
||||
29. RED:**整句精确**:「确认一下这个」→ `.text`(绝不子串匹配,否则顺口一句就批了 shell)。
|
||||
30. RED:tool gate + 「拒绝」/「取消」/`stop` → `.reject`。
|
||||
31. RED:否定守卫:「不批准」→ `.reject`;「不清楚」→ `.text`;`now` **不**被 `no` 前缀否定 → `.text`。
|
||||
32. RED:置信度门:`.approve` 且 confidence < 0.6 → 降级 `.text`;`.reject` 不受置信度影响。
|
||||
33. RED:空/纯标点话语 → `.text`。
|
||||
|
||||
**C. 确认 + 1.5s 撤销窗口(FakeClock,零真睡)**
|
||||
34. RED:`pressDown()` → `.listening`;partial 回调更新 `.listening(partial:)`。
|
||||
35. RED:`pressUp()` 得空转写 → `.idle` + `lastResolution == .empty`,**零注入**。
|
||||
36. RED:`pressUp()` 得有效转写 → `.confirming(...)`,**零注入**(「确认前绝不注入」)。
|
||||
37. RED:`.confirming` 下 `cancel()` → `.idle` + `.discarded` + 零注入。
|
||||
38. RED:`confirm()` → `.undoWindow`,此刻**仍零注入**。
|
||||
39. RED:时钟 +1.4s → 仍零注入;到 1.5s → 恰一次注入,内容 == 清洗后的转写,`lastResolution == .committed(.text(...))`。
|
||||
40. RED:`.undoWindow` 内 `undo()` → 零注入 + `.undone`;再推进 10s **也不**注入(任务已取消)。
|
||||
41. RED:注入内容**不以 `\r` 结尾**(用户自己按 ⏎ —— 误听绝不自动执行)。
|
||||
42. RED:非 `.confirming` 态调 `confirm()` → 无副作用。
|
||||
43. RED:重复 `confirm()` → 只 arm 一次、只注入一次。
|
||||
|
||||
**D. epoch 防误发(两道闸)**
|
||||
44. RED:口述→确认之间 epoch 变了 → `confirm()` 直接丢弃:零注入 + `.staleEpoch`。
|
||||
45. RED:epoch 在**撤销窗口内**变(confirm 已过、1.5s 内切会话)→ 到期仍零注入 + `.staleEpoch`。
|
||||
46. RED:epoch 不变 → 正常注入(防止闸门过严的回归保护)。
|
||||
47. RED:口述时 sessionId 还没 adopt(nil)、确认时已 adopt → 视为变化 → 零注入。
|
||||
48. RED:`VoiceEpochPolicy.invalidates`:`.reconnecting` → true(重连后服务器可能给的是新 PTY,
|
||||
客户端分辨不了 → 走安全方向);`.connecting`/`.none` → false。`VoiceEpochSource` 累加 generation。
|
||||
|
||||
**E. 权限与失败路径**
|
||||
49. RED:麦克风授权被拒 → `.denied(.microphone)`,文案指向 设置→隐私→麦克风;零录音零注入。
|
||||
50. RED:语音识别授权被拒 → `.denied(.speech)`,文案指向语音识别开关。
|
||||
51. RED:识别器抛错 → `.failed(message:)`,零注入,可再按重试。
|
||||
52. RED:终端只读(`.exited`)→ `pressDown()` 拒绝(`.readOnly`),**不启动录音**(`startCount == 0`)。
|
||||
|
||||
**F. 键栏集成(KeyBar 零回归)**
|
||||
53. RED:不提供语音闭包 → 按钮数 == `KeyBarLayout.buttons.count`(17),无麦克风键。
|
||||
54. RED:提供语音闭包 → 末尾多一个 🎤/「语音」键,前 17 键顺序与 accessibilityLabel **完全不变**。
|
||||
55. RED:麦克风键 touchDown → `onVoiceDown` 恰一次;touchUpInside → `onVoiceUp` 恰一次;
|
||||
**绝不**经 `onKey`(麦克风不映射任何 `KeyByteMap` 字节)。
|
||||
56. RED:匹配到 `.approve` 且注入了 gate 桥 → 走同一 1.5s 窗口,到期调 `decide(.approve)` 而**不**注入文本。
|
||||
|
||||
### T-iOS-34 · 主题(跟随系统/深色/浅色)+ Dynamic Type —— C4 追加
|
||||
|
||||
真源:`Wiring/RootView.swift:30` 今天**硬锁** `.preferredColorScheme(.dark)`(无浅色通路);
|
||||
`DesignSystem/Tokens.swift` 已声明浅色 accent `#C9892F`(web `--accent-2`)但状态色/时间线色/
|
||||
`accentSoft`/终端画布仍是**单值深色专用**;终端主题只在 `Screens/TerminalScreen.swift:223-235`
|
||||
`makeUIView` 时套一次。web 参照 `public/settings.ts`(`DEFAULT_SETTINGS.theme='dark'`、
|
||||
`THEMES.light = {background:'#f6f7f9', foreground:'#1a1d24'}`)。
|
||||
文件 `ios/App/WebTermTests/AppThemeTests.swift` / `DynamicTypeLayoutTests.swift`。
|
||||
|
||||
**A. 主题模型(纯值 `AppTheme`)**
|
||||
1. RED:`.system.colorScheme == nil`(= 交给系统),`.dark → .dark`,`.light → .light`。
|
||||
2. RED:三档 label / SF Symbol 各自非空且**互不相同**(选择器要能区分)。
|
||||
3. RED:`AppTheme.allCases` 顺序 = 跟随系统 → 深色 → 浅色(选择器顺序即此顺序,UI 不重排)。
|
||||
4. RED:`AppTheme(rawValue:)` 白名单:`"system"/"dark"/"light"` 命中,其它(含空串、`"Dark"`)→ nil。
|
||||
|
||||
**B. 持久化(`AppThemeCodec` 纯函数 + `ThemeStore` over fake defaults)**
|
||||
5. RED:`decode(nil)`(从未设置)→ **`.dark`** —— 默认必须等于今天硬锁的深色(零回归)。
|
||||
6. RED:`decode("solarized")` / `decode("")` / `decode("DARK")` → `.dark`(未知值不崩、不落 system)。
|
||||
7. RED:三档 `encode → decode` 往返恒等。
|
||||
8. RED:`ThemeStore` 空 defaults → `.dark`,且**不写盘**(首次读不产生写)。
|
||||
9. RED:`select(.light)` → `theme == .light` 且 defaults 落 `"light"`;同一 defaults 新建 store → `.light`(跨启动)。
|
||||
10. RED:`select` 同一档两次 → 只写一次(不产生多余写)。
|
||||
11. RED:defaults 里被塞脏值 → store 读作 `.dark`,并**不**清空用户其它键。
|
||||
|
||||
**C. 有效配色解析(`AppTheme.resolvedScheme(system:)`)**
|
||||
12. RED:`.system` + 系统 `.light` → `.light`;`.system` + 系统 `.dark` → `.dark`(透传)。
|
||||
13. RED:`.dark`/`.light` 无视系统值 → 强制自身。
|
||||
|
||||
**D. 浅色通路的 token 审计(`UIColor.resolvedColor(with:)` 逐档解析)**
|
||||
14. RED:`statusWorking`/`statusWaiting`/`statusStuck`/`timelineTool`/`timelineUser` 在 **light** 与
|
||||
**dark** 下解析值**不同**(即:真的有浅色变体,不是解锁开关就完事)。
|
||||
15. RED:深色档的值**逐字节不变**(#46D07F / #F5B14C / #FF6B6B / #5E9EFF / #AF7BFF)—— 深色零回归。
|
||||
16. RED:上述 5 色在**两档**下相对该档 `systemBackground` 的 WCAG 对比度 **≥ 3:1**
|
||||
(非文本 UI 组件门槛,1.4.11)。今天的 `#46D07F`/`#F5B14C`/`#FF6B6B` 在白底只有
|
||||
1.96/1.60/2.74:1 —— 这是 RED 的核心。
|
||||
17. RED:critical 三色(working/waiting/stuck)在 **light** 档仍两两可辨(不因变暗而糊成一片)。
|
||||
18. RED:`accentSoft` 两档不同,且**都仍是 wash**(alpha < 0.3,不许变成实心块)。
|
||||
19. RED:`onAccent` 两档**相同**(金色填充在两档都要深色墨,故它是固定值),且与 accent 对比 ≥ 4.5:1。
|
||||
|
||||
**E. 终端画布跟随主题(`TerminalPalette`)**
|
||||
20. RED:`colors(for: .dark).background == #100F0D`、`.foreground == #ECE9E3`(桌面 web `--bg/--text`,零回归)。
|
||||
21. RED:`colors(for: .light).background == #F6F7F9`、`.foreground == #1A1D24`(镜像 web `THEMES.light`)。
|
||||
22. RED:两档 fg↔bg 对比度 ≥ 7:1(终端是正文,AAA 门槛)。
|
||||
23. RED:caret / selection 用该档 accent 解析值(不写死深色档的金)。
|
||||
24. RED:`DS.Palette.terminalBackgroundUIColor()` 是**动态** UIColor:light/dark 解析值不同
|
||||
(今天是单值常量 → RED)。
|
||||
|
||||
**F. Dynamic Type 不破版(`UIHostingController.sizeThatFits` / UIKit `systemLayoutSizeFitting`,AX5)**
|
||||
25. RED:`GateBanner` 在 320pt 宽容器、`.accessibility5` 下:宽度 ≤ 320(无横向溢出)、
|
||||
高度有限且 > 标准字号高度(= 真的长高而不是被裁)。
|
||||
26. RED:`TelemetryChip`(`lineLimit(1)` 的等宽数字)在 AX5 下宽度 ≤ 320,且**AX2 与 AX5 同高**
|
||||
(`DS.Typography.numericClamp` 夹取生效 —— 表格数字不许炸版;落地时把原计划的
|
||||
"增幅 ≤ 2.2×"换成了这条更强、更确定的等式断言)。
|
||||
27. RED:`dsMetaText()` 的元信息行在 AX5 下同样受夹取(与 26 同一策略,单一出处)。
|
||||
28. RED:`DSButtonStyle` 在 AX5 下高度 ≥ `DS.Layout.minHitTarget` 且标签可换行不横向溢出。
|
||||
29. RED(**外部件度量,越界只报不改**):AX5 下键帽两行所需高度 vs 固定 `barHeight`(44+8)。
|
||||
实测 **108.24pt vs 52pt → 裁切**(footnote 行高 52.51 + caption2 行高 47.73 + 内衬 8)。
|
||||
`Components/KeyBar.swift` 不在 C4 Owns,故以 `withKnownIssue` 记为已知缺口(修好后用例会
|
||||
报 "known issue was not recorded",提醒删标记)。
|
||||
注:不能用 `performAsCurrent { KeyBarView() }` 量 —— `UIFont.preferredFont(forTextStyle:)`
|
||||
读 App 级 content size category,不看 `UITraitCollection.current`,那样量出的是默认字号
|
||||
(38pt)的**假绿**;改用 `compatibleWith:` 取真实 AX5 行高。
|
||||
|
||||
### T-iOS-35 · web 分享 QR(`?join=`)互通 —— C4 追加
|
||||
|
||||
真源:`public/share.ts:55` 的 URL 形状 **`${location.origin}/?join=${sessionId}`**;
|
||||
`src/server.ts` 的 `GET /?join=<id>`;`DeepLinkRouter.swift:56-65` 今天只认 `webterminal://open`,
|
||||
http(s) 一律 `.ignore`(`DeepLinkRouterTests.swift:84` 钉住了它 —— 本任务**有意**改写该用例)。
|
||||
主机身份仍**只**经 `HostStore` 解析(`HostEndpoint.originHeader` 是冻结的唯一 origin 派生点)。
|
||||
文件 `ios/App/WebTermTests/DeepLinkRouterTests.swift`(增补 + 有意改写 1 例)。
|
||||
|
||||
**A. 接受 web 分享形状**
|
||||
30. RED:`http://192.168.1.5:3000/?join=<v4>` → `.joinShared(origin:"http://192.168.1.5:3000", sessionId:)`。
|
||||
31. RED:`https://mac.ts.net/?join=<v4>` → origin 省略默认端口(`:443` 不出现)。
|
||||
32. RED:`HTTP://192.168.1.5:3000/?join=<v4>`(大写 scheme/host)→ origin 归一化为小写。
|
||||
33. RED:path 为 `""` 与 `"/"` 都接受(`location.origin + "/?join="` 产出 `/`)。
|
||||
|
||||
**B. 白名单纪律(http(s) 是比自定义 scheme 大得多的输入面,故**更严**)**
|
||||
34. RED:`join` 值非 v4(`Validation.isValidSessionId` 判定)→ `.ignore`。
|
||||
35. RED:重复 `join` 键 → `.ignore`(歧义输入绝不部分应用)。
|
||||
36. RED:**多余 query 键**(`/?join=<v4>&x=1`)→ `.ignore`(web 形状精确只有一个键)。
|
||||
37. RED:非空 path(`/manage.html?join=<v4>`)→ `.ignore`。
|
||||
38. RED:带 fragment(`/?join=<v4>#x`)→ `.ignore`。
|
||||
39. RED:带 userinfo(`http://u:p@host/?join=<v4>`)→ `.ignore`(二维码钓鱼形状)。
|
||||
40. RED:非 http(s) scheme(`ftp://`、`file://`、`javascript:`)→ `.ignore`。
|
||||
41. RED:`?join=` 但**无 host**(`http:///?join=<v4>`)→ `.ignore`。
|
||||
42. RED(**有意改写既有用例**):`https://open?host=<v4>&join=<v4>` 仍 `.ignore`
|
||||
—— 但理由从"scheme 不是 webterminal"变成"web 形状只许**单一** `join` 键"。
|
||||
原用例名/注释同步改写,避免留下已失效的断言语义。
|
||||
43. RED:`webterminal://open?host=&join=` 等既有拒绝路径**逐条不变**(自定义 scheme 分支零回归)。
|
||||
|
||||
**C. 解析主机:只认已配对(二维码是不可信输入,绝不静默配对)**
|
||||
44. RED:origin 命中已配对主机 → `openSession(host, sessionId)` 恰一次,无 hint。
|
||||
45. RED:origin 未配对 → **零** open、`showPairing` 一次、hint == `DeepLinkCopy.unknownHostHint`
|
||||
(**不得**据链接内容自动新增主机)。
|
||||
46. RED:hint 文案**不回显**链接内容(不含 origin 串、不含 sessionId)—— 防钓鱼/防日志注入。
|
||||
47. RED:origin 比较走 `HostEndpoint.originHeader`:`http://host:3000` 与 `http://HOST:3000/` 视为同一主机;
|
||||
`http://host:3001`(端口不同)**不是**同一主机。
|
||||
48. RED:`https://host/` 与 `http://host/`(scheme 不同)**不是**同一主机。
|
||||
49. RED:冷启动 stash 对 `.joinShared` 同样生效(ready 前 handle → markReady 后恰应用一次)。
|
||||
50. RED:非法 web 链接 → `ignoredCount + 1`,且**不入 stash**(与既有 `.ignore` 同一通路)。
|
||||
|
||||
Reference in New Issue
Block a user