From 2c19d027a61230488773760bbc1daebf25f9f7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 10 Oct 2026 11:42:29 -0400 Subject: [PATCH] ElyMark: untitled copies pair by the tab made for them (M-07) restore_shown takes its entry through Keeping::take; the test runs a crashed session through start, untitled, made and take, the second tab first. Co-Authored-By: Claude Opus 5.5 --- apps/ely-mark/src/restore.rs | 66 +++++++++++++++++++++++------------- tasks/BUGS.md | 2 +- tasks/reviews/M-07-r1.md | 3 ++ tasks/reviews/M-07-r2.md | 1 + 4 files changed, 48 insertions(+), 24 deletions(-) create mode 100644 tasks/reviews/M-07-r1.md create mode 100644 tasks/reviews/M-07-r2.md diff --git a/apps/ely-mark/src/restore.rs b/apps/ely-mark/src/restore.rs index acead37..c0c0f94 100644 --- a/apps/ely-mark/src/restore.rs +++ b/apps/ely-mark/src/restore.rs @@ -148,6 +148,16 @@ impl Keeping { self.waiting[at].tab = Some(tab); } + /// What tab `id` takes: its file or its copy. + fn take(&mut self, real: Option<&PathBuf>, id: u64) -> Option { + let at = self.waiting.iter().position(|waiting| match (&waiting.real, real) { + (Some(kept), Some(open)) => kept == open, + (None, None) => waiting.tab == Some(id), + _ => false, + })?; + Some(self.waiting.remove(at)) + } + /// An end that never asked: the lock and points stay. pub fn hold(&mut self) { if self.crash.take().is_some() { @@ -192,13 +202,6 @@ fn clear(dir: &Path) -> io::Result<()> { } /// What tab `id` restores: by file, or untitled by number. -fn waiting_for(waiting: &[Waiting], real: Option<&PathBuf>, id: u64) -> Option { - waiting.iter().position(|waiting| match (&waiting.real, real) { - (Some(kept), Some(open)) => kept == open, - (None, None) => waiting.tab == Some(id), - _ => false, - }) -} impl Desk { /// A tab just shown takes its kept mode, place, text. @@ -213,13 +216,12 @@ impl Desk { .session .path() .and_then(|path| std::fs::canonicalize(path).ok()); - let waiting = &mut self.keeping.waiting; - let Some(at) = waiting_for(waiting, real.as_ref(), id) else { + let Some(Waiting { + kept, text: back, .. + }) = self.keeping.take(real.as_ref(), id) + else { return; }; - let Waiting { - kept, text: back, .. - } = waiting.remove(at); let mode = Mode::parse(&kept.mode).unwrap_or(Mode::Visual); log::info!("mark: restores tab {id} as {}", mode.name()); let tab = self.tab_mut(id).expect("open"); @@ -409,18 +411,36 @@ mod tests { } #[test] - fn an_untitled_copy_goes_to_the_tab_made_for_it() { - let untitled = |name: &str, tab: u64| super::Waiting { - kept: super::Kept { path: None, name: name.into(), mode: "visual".into(), place: None, slot: String::new() }, - real: None, - text: None, - tab: Some(tab), + fn each_untitled_copy_goes_to_the_tab_made_for_it() { + let dir = tempfile::tempdir().expect("a temp dir"); + let kept = |name: &str, mode: &str, slot: &str| super::Kept { + path: None, + name: name.into(), + mode: mode.into(), + place: Some(super::Place { line: slot.len() as u64, column: 1, scroll: 0.0 }), + slot: slot.into(), }; - let waiting = [untitled("Untitled1", 4), untitled("Untitled2", 5)]; - // The second tab shows first: it still takes its own. - assert_eq!(super::waiting_for(&waiting, None, 5), Some(1)); - assert_eq!(super::waiting_for(&waiting, None, 4), Some(0)); - assert_eq!(super::waiting_for(&waiting, None, 9), None, "a new tab takes nothing"); + let tabs = vec![kept("Untitled1", "visual", "a"), kept("Untitled2", "source", "bb")]; + for (slot, text) in [("a", "first"), ("bb", "second")] { + let mut points = super::Recovery::new(super::recovery(dir.path(), slot)); + points.write_once(text.as_bytes(), None, std::time::SystemTime::now()).expect("a point"); + } + let saved = serde_json::to_vec(&super::Saved { tabs: tabs.clone() }).expect("a list"); + std::fs::write(dir.path().join("session.json"), saved).expect("the list"); + // A pid no process has: the last run crashed. + std::fs::write(dir.path().join("session.lock"), "open 4294967295").expect("a lock"); + let (mut keeping, ..) = super::Keeping::start(dir.path()).expect("a session"); + let made: Vec<_> = keeping.untitled(); + assert_eq!(made, [(0, "Untitled1".to_string()), (1, "Untitled2".to_string())]); + for (at, _) in made { + keeping.made(at, 10 + at as u64); + } + assert!(keeping.take(None, 12).is_none(), "a new tab takes nothing"); + // The second tab shows first: each still takes its own. + for (id, want, text) in [(11, &tabs[1], "second"), (10, &tabs[0], "first")] { + let got = keeping.take(None, id).expect("its copy"); + assert_eq!((&got.kept, got.text.map(|(text, _)| text)), (want, Some(text.to_string()))); + } } #[test] diff --git a/tasks/BUGS.md b/tasks/BUGS.md index e5a3469..651811f 100644 --- a/tasks/BUGS.md +++ b/tasks/BUGS.md @@ -67,7 +67,7 @@ - [x] M-04 第二次打印或导出 PDF 悄悄取消第一次,没有文件也没有错误 — 每份打印与导出各有编号与文件,作业按编号保留、互不取消;每份作业完成时记下编号与结果;R1 窗口测试只数日志;R2 PASS(窗口测试只在 macOS,本机未运行) - [x] M-05 点页面里的链接什么也不发生 — 页面拦下链接单击,地址取链接标记原文,预览单击、可视 Cmd/Ctrl 单击交应用(审查 4 轮) - [x] M-06 `on_app_quit` 无条件删恢复点与崩溃锁,AppKit 直接终止时丢未保存内容 — 未保存时退出只放下锁、不清点,窗口释放时不再删(审查 1 轮) -- [ ] M-07 崩溃恢复的未命名副本按“第一个显示的未命名标签”配对,内容可能落错标签 +- [x] M-07 崩溃恢复的未命名副本按“第一个显示的未命名标签”配对,内容可能落错标签 — 未命名副本按新建时记下的标签号配对,经 Keeping::take(审查 2 轮) - [ ] M-08 页面重载后悬着的渲染回调永不完成,导出既不成功也不报错 - [ ] M-09 导出的保存面板出错被当作取消 - [ ] M-10 关闭查找框只清当前标签的高亮 diff --git a/tasks/reviews/M-07-r1.md b/tasks/reviews/M-07-r1.md new file mode 100644 index 0000000..679e742 --- /dev/null +++ b/tasks/reviews/M-07-r1.md @@ -0,0 +1,3 @@ +FAIL + +apps/ely-mark/src/restore.rs:412 — 回归测试直接构造 `tab: Some(tab)`、`text: None`,绕过 `untitled`、`made` 和启动时的编号绑定,只验证查找位置,未覆盖 MARK-12 的恢复链路。两端提取原方法与原测试编译的隔离探针确认:将 `made` 清空、将其绑定号改为 `tab + 1`、或让 `untitled` 返回空列表,原测试均仍通过。具体漏检场景:崩溃后有两份未保存副本,绑定未执行时两个空白标签都找不到恢复条目;编号错一时第二个标签会接过第一份内容。当前实现通过补充的绑定、逆序消费和混合文件条目探针,缺陷在回归覆盖。修复方向:从未绑定且正文、模式、位置、slot 各异的条目开始,经生产代码枚举与绑定,再逆序消费,断言各标签取得自己的完整恢复数据、新标签不能取走副本;测试须能检出上述变异。两端恢复单测均为 3/3,Linux 相关 Cargo 测试全通过;Windows 排除已归 W-02 的 vcs 尾空格路径用例后通过。未运行 macOS 窗口测试。探针源码、二进制及两端增量缓存已删除,仅留各约 3 KB 的 `ElyOffice-M07-r1-probe.log`。 diff --git a/tasks/reviews/M-07-r2.md b/tasks/reviews/M-07-r2.md new file mode 100644 index 0000000..7ef22e9 --- /dev/null +++ b/tasks/reviews/M-07-r2.md @@ -0,0 +1 @@ +PASS