MooTool next-compose:Vault 外部删除冲突证据链的实现与验收(DIFF-610 深度解析)

原创2026-10-15 09:53:051,690 阅读
文章标签:开发工具桌面应用MCP 服务

MooTool next-compose:Vault 外部删除冲突证据链的实现与验收(DIFF-610 深度解析)

本文以 MooTool Compose Desktop 版(next-compose)的 DIFF-610 变更记录为骨架,围绕 JSON Vault(F04)在“本地脏编辑 + 磁盘外部删除”场景下如何产出 deleted 冲突态、并在 Compose UI 测试中生成「另存副本」焦点帧证据 这条完整链路展开。读者将掌握:外部删除冲突从监视器轮询到 VaultConflictState.deleted 的判定流程、sample.json 走查夹具与 prepare-vault-conflict-evidence.sh 步骤 3b 的配合方式、以及 macOS 上全量 Compose 测试规避 JVM exit 134 的构建配置手段。

一、DIFF-610 在做什么:F04 §A 外部删除冲突证据链

DIFF-610 是 MooTool next-compose 增量开发记录中编号第 610 的变更片(基线为 DIFF-609 工作区)。它解决一个很具体的验收证据缺口:此前(DIFF-605、帧 200)已经覆盖了 sample.json 被外部改写时的冲突「重新加载」焦点帧,但外部删除(文件在磁盘上被 rm)这一分支还没有对应的证据帧。DIFF-610 补齐的是这条链路:

  • F04 §A:VaultConflictDeletedProductEvidenceFlowTest 以 sample.json 为夹具,构造「脏编辑 + rm → deleted 冲突 + Presentation 断言」的完整流程测试;同时对齐 prepare-vault-conflict-evidence.sh 中注释说明的 步骤 3b(外部删除走查步骤)。
  • 证据:VaultConflictDeletedProductEvidenceCaptureTest 输出 Compose 焦点帧 205-compose-vault-external-conflict-sample-deleted-savecopy-tab-focus.png(680×460),专门呈现外部删除态下「另存副本」按钮的 Tab 焦点环。
  • 不重复 609:DIFF-609 已经产出 merge 冲突期间 push 禁用的帧 204,DIFF-610 的帧 205 与之互斥,避免同一帧被反复复用。

整条证据链在验收文档 next-compose/docs/evidence/2026-09-17-vault-conflict-product-window/results.md 中有登记:「§A 证据 sample.json 外部删除→冲突链 | VaultConflictDeletedProductEvidenceFlowTest(DIFF-610)」与「§A 证据 sample 外部删除 saveCopy 帧 | 205-…png(DIFF-610)」两条,并且明确标注「已自动化(不能代替产品主窗截图)」。

需要说明的是:这类 Compose 焦点帧是在非产品主窗的 UI 测试里截取的,用于在无人值守时锁住「Tab 焦点落在哪颗按钮」的呈现细节;产品主窗的最终截图仍须通过 runDistributable + 人工 GUI 完成(见下文第五节)。

二、外部删除冲突如何被判定:VaultConflictEngine.decide 的 deleted 分支

要理解 DIFF-610 的测试为什么断言 pending.deleted == true、pending.diskText == null,需要先看冲突判定引擎。VaultConflictEngine.kt 定义了五种变更分类:

enum class VaultChangeKind {
    Ignored, Reload, Conflict, Deleted, TreeChanged
}

核心判定函数 decide(changedPath, currentFile, editorText, savedText, diskText, expectedOwnHash) 的逻辑:

输入组合 判定结果 含义
变更路径不是当前打开文件 TreeChanged 仅刷新树,不打扰编辑器
diskText == null 且编辑器内容 == 已保存内容 Deleted 文件没了、本地也干净,直接清会话
diskText == null 且编辑器内容 != 已保存内容 Conflict(deleted) 外部删除 + 本地脏编辑 → 冲突
diskHash == expectedOwnHash Ignored 自写回写被预期哈希放行
diskText == editorText Ignored 内容一致,无冲突
editorText == savedText Reload 本地干净,直接重载磁盘版本
diskText == savedText Ignored 磁盘还是基准内容,忽略
其余 Conflict 三方内容互不相同

判断依据是 UTF-8 内容的 SHA-256 哈希(见 sha256Text),而不是 mtime;文件超过 MAX_HASH_BYTES(8 MiB)时退化为 size: 指纹(DIFF-026 中记载「大于 8 MiB 的文件只比较 size 指纹」)。canOverwrite 则保证外部版本与基准、编辑器都不同时拒绝覆盖保存,这正是「禁止后写覆盖」的工程落地。

DIFF-610 的流程测试命中的正是上表第二行:rm 之后 diskText == null、编辑器文本是脏的(与 savedText 不同),因此得到 VaultChangeKind.Conflict,再以 disk == null 的实参构造出 VaultConflictState(relativePath, editorText, diskText = null, deleted = true)(见 JsonVaultSession.kt)。

三、测试如何复现“脏编辑 + 外部删除”:VaultConflictDeletedProductEvidenceFlowTest

DIFF-610 的流程测试 VaultConflictDeletedProductEvidenceFlowTest.kt 是整条证据链的可验证核心,其步骤可以逐条对照真实代码:

  1. 构造隔离数据目录:createTempDirectory("mootool-vault-deleted-evidence-flow-") + AppPaths.resolve(...).ensureCreated(),配合 SettingsRepository / AppDatabase / AppContainer 组装出与产品一致的运行时容器,全程不触碰本机默认数据目录。
  2. 写入走查夹具:通过 VaultConflictProductEvidencePresentation.EVIDENCE_JSON_RELATIVE_PATH(即 sample.json)与 evidenceJsonInitialBody 创建文件。夹具正文为:
{
  "fixture": "vault-external-conflict",
  "step": "open in app then edit without saving"
}
  1. 制造脏编辑:JsonSession 中把 savedText 设为初始正文,再用 editor.setText(初始正文.replace("open in app then edit without saving", "dirty before external delete")) 写入未保存的修改,从而模拟「打开后编辑不保存」。
  2. 启动监视器:以 intervalMs = 60 创建 VaultRevisionMonitor,monitor.start() 后立即 rebaseline()(防止把测试自身的初始写入当作外部变更)。
  3. 外部删除:Thread.sleep(90) 后执行 container.jsonVault.root().resolve(path).deleteIfExists(),等价于产品走查脚本的 rm。
  4. 断言冲突态:CountDownLatch 等待监视器回调,然后依次断言:
    • pending.relativePath == "sample.json" 且 matchesEvidenceWalkthroughPath(...) 通过;
    • pending.deleted == true、pending.diskText == null;
    • VaultConflictPresentation.showReloadAction(deleted) 为 false(外部删除态不出现「载入磁盘版本」按钮);
    • hintMessageKey(deleted) == "vault.conflict.hintDeleted";
    • saveCopyProminent(deleted) 为 true(删除态主操作是「另存本地副本」);
    • pending.editorText 仍包含 "dirty before external delete"(编辑内容在冲突期间被完整保留)。

这些断言与 VaultConflictPresentation.kt 的实现一一对应,也决定了 VaultConflictDialog.kt 中按钮的显隐与权重(删除态隐藏 reload 按钮、saveCopy 升级为主按钮)。

同样为 F01 随手记(sample-external.md)存在对称实现 QuickNoteVaultConflictDeletedProductEvidenceFlowTest.kt,对应 DIFF-611 的帧 206。

四、焦点帧证据:VaultConflictDeletedProductEvidenceCaptureTest 与帧 205

DIFF-610 的第二个测试 VaultConflictDeletedProductEvidenceCaptureTest.kt 负责产出证据 PNG:

  • 使用 runDesktopComposeUiTest(width = 680, height = 460) 直接渲染 VaultConflictDialog(非产品主窗),构造 VaultConflictState(relativePath = "sample.json", editorText = 脏正文, diskText = null, deleted = true);
  • 通过 FocusRequester 把键盘焦点显式放到「另存副本」按钮上(Modifier.focusRequester(saveCopyFocus) + runOnIdle { saveCopyFocus.requestFocus() });
  • onRoot().captureToImage() 截帧,写入 docs/evidence/2026-09-15-inspector-screencapture/windows/205-compose-vault-external-conflict-sample-deleted-savecopy-tab-focus.png;
  • 最后用一个朴素像素统计 countRingPixels(image, 0x316DC0) 断言画面上至少出现 8 个主色焦点环像素,作为「焦点确实落在保存副本按钮」的自动校验。

Vault 外部删除冲突下「另存副本」按钮获得 Tab 焦点的 Compose 测试帧(帧 205)

该帧与 DIFF-605 的帧 200(sample.json 冲突「重新加载」焦点帧)、DIFF-441 的帧 146(删除态对话框无 reload 按钮的 Tab 帧)互补不重复:200 覆盖「磁盘改写」分支,146/205 覆盖「磁盘删除」分支,但 205 首次把焦点环落到删除态的「另存副本」主按钮上。

五、走查脚本步骤 3b:prepare-vault-conflict-evidence.sh 与 rm 场景

DIFF-610 的测试是对自动化闭环的锁定,而产品主窗人工验收走查由 prepare-vault-conflict-evidence.sh 准备隔离数据。脚本核心动作:

  1. 通过 MOOTOOL_COMPOSE_DATA_DIR(由 lib/product-evidence-common.sh 的 mootool_evidence_resolve_data_dir 解析,未设置则 mktemp -d)建立隔离数据根;
  2. 在 data/vaults/json/sample.json 写入走查夹具(与测试断言用同一份正文);
  3. 在 data/vaults/quick-note/sample-external.md 写入随手记夹具;
  4. 末尾自检两个文件存在,并输出步骤 3 / 3b / 3c 的操作提示。

其中 DIFF-610 涉及的 步骤 3b 正是外部删除走查(须先打开 sample.json 并保持脏编辑,再从终端执行 rm):

# 外部改写(步骤 3):
#   printf '%s\n' '{"disk":true}' > "${VAULT_JSON}/sample.json"
# 外部删除(步骤 3b,须先打开 sample.json 并保持脏编辑):
#   rm "${VAULT_JSON}/sample.json"
# 随手记外部删除(步骤 3c,须先打开 sample-external.md 并保持脏编辑):
#   rm "${VAULT_QN}/sample-external.md"

脚本末尾会打印 MOOTOOL_COMPOSE_DATA_DIR 单行 export(不要 eval 整段输出,因为注释行含 shell 语法不可执行),随后即可用 ./gradlew :composeApp:runDistributable --offline 启动产品主窗,重复测试所覆盖的走查路径。verify-product-evidence-prep.sh 可在无 GUI 的 CI 上非交互验证脚本准备结果。

六、配套构建配置:macOS 全量 Compose 测试的稳定性参数

DIFF-610 的「其它」段记录了一个工程性细节:composeApp/build.gradle.kts 中 desktopTest 设 maxParallelForks = 1 / forkEvery = 80 / -Xmx3g,用于减轻 macOS 全量 Compose 测试进程被 JVM 以 exit 134 杀掉的偶发问题。当前仓库 composeApp/build.gradle.kts 中的实际配置为:

/** Compose Desktop UI tests share AWT; serial forks reduce macOS JVM abort (exit 134) on full suite. */
tasks.named<Test>("desktopTest") {
    maxParallelForks = 1
    jvmArgs("-Xmx2g")
}

可以看到:maxParallelForks = 1 与文档一致(串行 fork,规避 Compose UI 测试共享 AWT 导致的崩溃);堆内存当前为 -Xmx2g,与文档记录的 -Xmx3g 存在差异,说明该参数仍在按实际机器内存调优,引用本文时请以仓库当前值为准。此外,DIFF-610 与 DIFF-611 均强调 macOS 上 Gradle daemon 下偶发 executor exit 134,可加 --no-daemon 规避:

./gradlew :composeApp:desktopTest --offline --no-daemon

(前提:本机安装 JDK 21,且 desktopTest 为离线测试集,含 2 个默认跳过的公网 smoke 用例。)

七、证据链在验收体系中的位置与局限

DIFF-610 只是 MooTool next-compose 数百条 DIFF 切片之一,它的价值在于把「外部删除冲突」这一行为用两层证据固定下来:

证据层 覆盖内容 位置
流程测试(自动) 脏编辑 + rm → deleted 冲突态、Presentation 断言 VaultConflictDeletedProductEvidenceFlowTest
帧测试(自动) 删除态「另存副本」Tab 焦点环 205-compose-vault-external-conflict-sample-deleted-savecopy-tab-focus.png
走查脚本(半自动) 隔离目录夹具 + 步骤 3/3b/3c 操作提示 prepare-vault-conflict-evidence.sh
产品主窗(待人工) 真实 GUI 下冲突叠层的最终呈现 docs/evidence/2026-09-17-vault-conflict-product-window/results.md 登记

注意验收记录中的边界声明:这类 Compose 帧 不能 代替产品主窗截图;acceptance.md 仍将 F04/F01 冲突 UI 标记为「待验收」。因此 DIFF-610 的定位是「自动化闭环 + 走查准备」,而不是宣称冲突 UI 已整体验收。

八、快速上手:如何验证 DIFF-610 的链路

若要在本地复现 DIFF-610 覆盖的行为,推荐顺序如下:

  1. 跑定向流程测试(验证判定与断言):
cd next-compose
./gradlew :composeApp:desktopTest \
  --tests com.rememberber.mootool.next.compose.domain.VaultConflictDeletedProductEvidenceFlowTest \
  --offline --no-daemon
  1. 跑帧证据测试(重新生成帧 205 并自动断言焦点环像素):
./gradlew :composeApp:desktopTest \
  --tests com.rememberber.mootool.next.compose.features.vault.VaultConflictDeletedProductEvidenceCaptureTest \
  --offline --no-daemon
  1. 准备产品走查目录(无 GUI 也可先验证脚本):
export MOOTOOL_COMPOSE_DATA_DIR="$(mktemp -d /tmp/mootool-compose-evidence-XXXX)"
./scripts/prepare-vault-conflict-evidence.sh
./scripts/verify-product-evidence-prep.sh
./gradlew :composeApp:runDistributable --offline
# 打开 JSON → sample.json → 编辑不保存 → 终端 rm 该文件 → ≤1s 内应弹出「另存本地副本 / 继续编辑」叠层

整个 DIFF-610 的核心结论可以概括为一句话:当用户在 Vault 中打开文件并留有未保存编辑、而文件在磁盘上被外部删除时,MooTool Compose Desktop 版不再允许保存悄悄覆盖,而是通过内容哈希监视器把场景归类为 deleted 冲突,隐藏无效的「载入磁盘版本」按钮,并把「另存本地副本」提升为默认主操作——DIFF-610 用流程测试与帧 205 把这一行为固定为可回归的证据。

登录后查看全文
MooTool