Skip to content

fix(macos): 运行配置加载不再等待 Spring 索引 - #305

Merged
1lck merged 17 commits into
1lck:previewfrom
Farewell0375:fix/300-run-config-load-ordering
Aug 31, 2026
Merged

fix(macos): 运行配置加载不再等待 Spring 索引#305
1lck merged 17 commits into
1lck:previewfrom
Farewell0375:fix/300-run-config-load-ordering

Conversation

@Farewell0375

@Farewell0375 Farewell0375 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

修复 #300

问题

打开大型多模块 Maven 项目后,Run 面板长时间显示"未找到项目运行配置",即使 .lithe/run/generated.json 已存在且完全有效。这段时间内点击"识别并生成"并确认后没有任何反应——没有错误、没有进度、没有日志。

在一个 9765 个 Java 文件的项目上,这个窗口约 11 分钟。

原因

两处。

一、loadProjectServices 把 Spring 索引串在运行配置加载之前。

await springFeature.load(...) 排在 projectDevelopment.loadProject(...) 前面,而后者才负责把 configurationStatus 置为 .ready 并给 RunService.projectURL 赋值。Spring 索引的耗时随 Java 源文件数量增长(见 #299),运行配置、测试发现,以及 WorkspaceFeatureModel.rebuild 在回调之后的那次 Git 刷新,全都被压在它后面。

需要说明的是,即使 Spring 索引很快,这个串行耦合本身仍然是缺陷——它只是把不可用窗口从分钟级缩短到秒级,窗口依然存在。

二、RunService.generateRunConfigurations() 在项目未加载时静默返回。

guard let projectURL else { return }

reset() 在打开项目时把 projectURL 置为 nil。窗口期内点击识别会命中这个 guard 直接返回,UI 无任何状态变化,用户无法区分"操作被丢弃"和"操作失败"。

改动

把 Spring 索引改为调度而非等待。 SpringFeatureModel 新增 scheduleLoad,复用它已有的 reloadTask 取消机制和 generation 令牌(reset() 本来就会 cancel 它)。任务管理留在状态所在的层,AppModel 里不出现裸 Task。

用显式状态取代静默返回。 RunConfigurationGenerationState 新增 .projectNotLoadedRunView 渲染为"项目仍在加载中"。没有复用 .failed,因为什么都没有失败——那会让标题显示成"项目识别失败"。这个状态不跨 Rust 边界,是纯 macOS 应用层的瞬时状态,不需要新增 shared fixture。

补齐入口一致性。 toggleRuntoggleMaventoggleDebug 在激活执行模块后都会按需加载项目,但 runSelectedConfigurationAfterActivationstartDebuggingAfterActivation 不会,导致"打开项目后第一个动作就是点运行按钮"这条路径会冷激活出一个没人给它加载项目的 RunService。新增 RunService.isProjectLoaded,让这两个入口和工具窗口入口行为一致。

没有采用"让 activateExecutionModule() 自己负责加载项目"的方案:loadProjectServices 本身就会调它,会形成递归,需要额外的防重入标志,风险大于收益。

验证

swift build                            通过
./scripts/test-macos.sh                663 个测试 / 77 个 suite 全部通过
./scripts/verify-service-boundaries.sh 通过
./scripts/verify-shared-contracts.sh   通过

新增 4 个测试:

测试 锁住的行为
identificationBeforeProjectLoadReportsUnloadedProjectWithoutGenerating 未绑定项目时识别请求不落到 store,状态为 .projectNotLoaded
identificationAfterProjectLoadGeneratesAndClearsTheUnloadedState 绑定后识别行为与改动前一致
scheduleLoadDefersIndexingAndPublishesTheResult 调度立即返回,索引结果稍后到达
scheduleLoadReplacesAPendingSchedule 被取代的调度不会重复执行全量索引
runningBeforeTheSnapshotLoadsBindsTheProject Run 入口在快照绑定项目前自行加载
debuggingBeforeTheSnapshotLoadsBindsTheProject Debug 入口同上
openingTheRunToolWindowBindsTheProject 工具窗口入口的既有行为不回退

后两个 scheduleLoad 测试用 objectWillChange 加本地超时的等待器,不依赖固定 sleep;调度未发布前的断言由 actor 模型保证,无需闸门。

入口测试用一个不可读的工作区让快照返回 unavailable,从而跳过 onSnapshotLoaded,把入口变成唯一能绑定项目的路径。去掉 AppModel+Development.swift 里两处 loadProjectServicesIfRunProjectIsUnbound 调用后,Run 与 Debug 两个测试失败、工具窗口测试仍通过,确认它们能抓到回归。

在报告问题的那个 9765 文件项目上做过手动验证:运行配置在项目树出现后几秒内列出全部 27 条,此时 Spring 面板仍在索引中。

已知限制

已经开始执行的 Rust spring.index 调用无法中断。 该命令没有取消点,所以一次已经进入 load 的索引会跑完。scheduleLoad 现在会在任务体开头检查 Task.isCancelled,因此尚未开始的调度不再重复执行全量索引;这一点由 scheduleLoadReplacesAPendingSchedule 覆盖。降低单次索引本身的成本属于 #299 的范围,本 PR 不涉及 rust/lithe-core

Opening a large multi-module Maven workspace left Run and Debug unusable for
as long as the Spring index took, because loadProjectServices awaited it
before loading build-system and run state. Schedule the index instead so run
configurations, test discovery, and the Git refresh that follows no longer
depend on its duration.

Identifying the project during that window was dropped silently, which is
indistinguishable from a broken button. Report the unloaded workspace as an
explicit state, and let the Run and Debug entry points load the project on
demand the way the tool-window entry points already do.

Refs 1lck#300
@Farewell0375
Farewell0375 requested a review from 1lck as a code owner August 28, 2026 08:58
Farewell0375 and others added 3 commits August 28, 2026 17:10
CI 的测试稳定性门禁拒绝了原先的两个 scheduleLoad 测试:轮询里用了真实
Task.sleep,测试替身里用了无界的 DispatchSemaphore.wait()。改成由 actor 模型
保证的同步断言,加上以 objectWillChange 为事件源、带本地超时的等待。

重写后的测试暴露出一个真实缺陷:Swift 的取消是协作式的,reloadTask.cancel()
并不会阻止任务体运行,而 load 里没有取消检查点,所以被取代的那次调度仍然会跑
完一次全量工作区索引,只是结果被 generation 令牌丢弃。为 scheduleLoad 补上
Task.isCancelled 检查,与既有的 scheduleReload 保持一致。

Refs 1lck#300
之前只靠编译期类型检查和手动验证,没有测试覆盖这两个入口。

新增的三个测试用一个不可读的工作区让快照返回 unavailable,从而跳过
onSnapshotLoaded,把入口变成唯一能绑定项目的路径。把 AppModel+Development 里
两处 loadProjectServicesIfRunProjectIsUnbound 调用去掉后,Run 与 Debug 两个
测试会失败,而覆盖既有行为的工具窗口测试仍然通过,说明它们确实能抓到回归。

把 objectWillChange 加本地超时的等待器提成 ObservableChangeWaiter,供
SpringFeatureModelTests 与新测试共用,超时收到 5 秒以留出计时预算。

Refs 1lck#300

@1lck 1lck left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

整体修复方向正确:Spring 索引与运行配置加载解耦符合问题根因,Execution module、Core contracts、视图与平台适配器的边界也保持清晰。

但冷启动入口目前会把“URL 已绑定”当成“工作区快照已就绪”,在 projectFiles 仍为空时允许生成配置,存在写出不完整 generated.json 的数据完整性风险,因此建议修复阻塞批注后再合并。

非阻塞建议已分别标注:将项目加载建模为携带 workspace/snapshot 身份的显式状态;为不可取消的 Spring 全量索引增加 single-flight/latest-pending 语义;同时建议在项目加载与生成路径记录 workspace、文件数量、load/generation ID 和状态转换原因,降低后续竞态问题的排查难度。项目加载编排长期可收口到 ProjectDevelopmentFeatureModel,避免 AppModel 根据服务内部布尔值承担业务时序。

Comment thread macos/Sources/Lithe/Models/AppModel/AppModel+Development.swift Outdated
Comment thread macos/Tests/LitheTests/RunEntryPointTests.swift Outdated
Comment thread macos/Sources/LitheExecutionModule/Services/RunService.swift Outdated
Comment thread macos/Sources/Lithe/Application/Features/SpringFeatureModel.swift
@1lck

1lck commented Aug 28, 2026

Copy link
Copy Markdown
Owner

@Farewell0375 你好 以上是一些pr阻塞点 麻烦处理一下哦 如果您有一些其他好的方案也可以在此PR下与我讨论

评审指出冷启动入口把"URL 已绑定"当成了"工作区快照已就绪"。快照未完成时
projectFiles 为空,入口仍会绑定 projectURL 并允许确认"识别并生成",而
runConfig.generate 只扫描传入的 paths,于是可能写出缺少 Java 入口的
.lithe/run/generated.json。

把两件事显式分开。新增 ProjectLoadState(idle / loading / bound / ready /
failed):bound 表示已绑定工作区但清单是临时的,只允许读取既有配置;ready 携带
工作区与快照标识,才允许生成。快照标识由 WorkspaceFeatureModel 拥有——它是唯一
应用快照的地方——经 AppModel 与 ProjectDevelopmentFeatureModel 传到 RunService。
snapshotID 缺省为 nil,因此忘记传递的调用方会退到 bound 而不是误判为就绪。

generateRunConfigurations 现在要求当前工作区处于 ready,否则报
projectNotReady(原 projectNotLoaded,改名以覆盖"已绑定但清单不完整")。

MacServiceContainer 增加 workspaceOperations 注入缝,与既有的 moduleStore 等
可选覆盖一致,使 AppModel 层能用可控快照做测试。Search 与本地历史仍使用具体的
RustWorkspaceOperations,它们依赖 WorkspaceOperations 之外的能力。

测试改为断言数据完整性而非仅仅"已绑定":入口测试用受控快照,先阻塞、按下 Run、
断言未就绪且生成被拒且未写出配置,再放行含真实 Java 文件的快照并断言就绪;
ExecutionModuleTests 断言生成实际扫描的清单恰好是快照上报的那份。去掉
RunService 的就绪守卫后,后者会直接暴露 generatedInventories == [[]]。

Refs 1lck#300
@Farewell0375

Copy link
Copy Markdown
Contributor Author

@1lck 感谢评审,阻塞项和测试覆盖都已处理,a53ee941。细节回在对应批注下了,这里总结。

阻塞项确认成立,是我引入的。 冷启动入口把"URL 已绑定"当成"快照已就绪",快照未完成时 projectFiles 为空,生成会写出缺少 Java 入口的 generated.json。补充一点你的分析之外的成因:Rust 侧的文件系统探测器会独立走磁盘,所以 npm/maven/gradle 那部分还在;丢的恰好是只吃 paths 的 Java 主类扫描,以及依赖它的 Spring Boot 主类收编。

修法:新增 ProjectLoadStateidle / loading / bound / ready(workspace:snapshotID:) / failed)。bound 表示已绑定但清单临时,只允许读取既有配置;ready 才允许生成。快照标识由 WorkspaceFeatureModel 拥有并逐层传递,snapshotID 缺省 nil 使遗漏的调用方退到 bound 而非误判就绪。isProjectLoaded 已移除,readiness 由 ProjectDevelopmentFeatureModel 对外提供。

测试改为断言数据完整性。 入口测试改用受控的 WorkspaceOperations:阻塞快照 → 按下 Run → 断言未就绪、生成被拒、磁盘上没有写出配置 → 放行含真实 Java 文件的快照 → 断言就绪。生成实际扫描哪份清单在 ExecutionModuleTests 断言(Swift 测试二进制不链接 Rust Core,lithe_bridge_version() 返回 unlinked)。

反向验证:一次性 worktree 里去掉就绪守卫后,generatedInventories → [[]],空清单被送去扫描,正是你描述的路径;入口测试同时失败,"正常打开项目"那个仍通过。

为测试新增的注入缝MacServiceContainer.init 增加 workspaceOperations 可选覆盖,与既有的 moduleStore 等同类。Search 与本地历史仍用具体的 RustWorkspaceOperations

验证test-macos.sh 667 个测试全过(新增 2 个,另修正 2 个既有集成测试——它们模拟的是快照已就绪的流程,现在显式传 snapshotID);verify-service-boundaries.shverify-shared-contracts.shverify-module-boundaries.sh、测试稳定性门禁均通过。

两条非阻塞建议我在批注下各自回了想法:ProjectLoadState 已采纳;Spring 的 single-flight 我建议等 #299 落地后单开 issue,因为它需要 Rust 侧先有取消点,而且单次索引降到约 2.4 秒后重复索引的代价会小很多。结构化日志那条我认为独立于 single-flight,可以先做——你希望放本 PR 还是另开,我都可以。

@1lck 1lck left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

整体方向没问题,之前的初始空清单问题也已经修了。现在还剩 3 个点:一个会让仓库门禁失败,另外两个是新旧快照和提前执行的时序问题。建议修完再合并,细节见行内批注。

Comment thread macos/Sources/Lithe/Models/AppModel/AppModel.swift Outdated
Comment thread macos/Sources/LitheCoreContracts/Execution/RunConfigurationContracts.swift Outdated
Comment thread macos/Sources/Lithe/Models/AppModel/AppModel+Development.swift Outdated
处理评审的三点。

一、AppModel.swift 与 preview 合并后达到 1806 行,超过 verify-service-boundaries
的 1800 上限。把加载编排移到 AppModel+Development.swift,现为 1781 行。上一轮
本地验证没有先合并 base,所以漏掉了这个失败。

二、snapshotID 放进了 .ready 却没参与比较。同一工作区刷新时,
WorkspaceFeatureModel 在发布新快照与调用 onSnapshotLoaded 之间隔着两个 await,
这段窗口里 RunService 仍持有上一份清单却报就绪,重新识别会漏掉新增的入口。
isReady 现在同时比较工作区与快照标识,snapshotID 为 nil 时一律视为未就绪。

三、加载可能只到 .bound,而运行入口只看 configurationStatus。磁盘上已有配置时
会在完整文件列表与 Maven 信息就绪前直接启动,工具链解析因此缺少 Maven 项目。
运行与调试入口改为要求就绪,未就绪时记下 pendingRunAction 并返回;工作区重建
必然以 loadProjectServices 收尾,由它恢复这次操作,因此不需要轮询或等待。

识别现在统一走 AppModel.generateRunConfigurations,先把服务提升到当前快照再执行。
这是 RunService 内部仍可用自身状态判断的前提。

Refs 1lck#300
上一版的入口测试只证明了推迟与恢复,没能覆盖评审要的"已有配置 + 快照阻塞"
子场景:真实存储的 inspect 要经 Rust Core,而 Swift 测试二进制不链接它
(bridge.c 提供 weak 桩,isAvailable 为 false),所以 configurationStatus
无法在测试里变成 ready。

给 MacServiceContainer 增加 runConfigurationOperations 可选覆盖,与已有的
workspaceOperations、moduleStore 等注入缝同类。测试用一个直接报 ready 的替身
构造该前提,并记录 launchPlan 请求次数:快照阻塞期间断言运行被推迟、
configurationStatus 确为 ready、launchPlan 从未被请求;放行快照后断言恢复执行
且就绪。

未选择改为链接 Rust 的真实测试:CI 中没有任何任务在链接 Rust 的情况下执行
LitheTests(verify-rust-core.sh 只跑 cargo test、swift build 与独立的桥接验证
程序),因此这样的测试会永远跳过。仓库中已有 9 处依赖 isAvailable 的跳过测试,
不宜再添。

Refs 1lck#300
@Farewell0375

Copy link
Copy Markdown
Contributor Author

@1lck 三条都已处理,131e3ae1a63d39a4。细节回在对应批注下,这里总结。

门禁那条是我的验证流程漏洞。 上一轮本地跑门禁是通过的(1799 行,上限 1800),但分支处于 BEHINDpreview 也改了同一个文件,合并后才到 1806。我在 worktree 里试算合并结果复现了失败。这轮起先 merge base 再验证,两个 PR 现在都不落后 preview。加载编排已移到 AppModel+Development.swift,现为 1781 行。

snapshotID 现在参与比较。 你指出的窗口确实存在且不窄——rebuild 在发布新快照与调用 onSnapshotLoaded 之间隔着 restoreSessionupdateWatchConfiguration 两个挂起点。做法是让识别与运行/调试统一先经 AppModel.ensureRunProjectReady 比对当前快照,这样 RunService 不需要反向感知工作区。前提是每条生成路径都走这个漏斗,我核对过只有 WorkbenchView 一处实际调用,已改道。

未就绪时运行改为推迟而非降级执行。 按你说的记录待执行动作,由快照驱动的加载恢复,不在生产路径上放等待。

测试覆盖了完整场景,但代价是第二个注入缝。 Swift 测试二进制不链接 Rust Core(bridge.c 是 weak 桩),所以"已有配置"这个前提只能靠替身构造。我给容器加了 runConfigurationOperations 覆盖,测试断言快照阻塞期间 launchPlan 从未被请求。这一点我在批注里请你确认——如果你认为不该为测试在组合根上开第二个缝,我可以把测试降级到 ProjectDevelopmentFeatureModel 层,代价是测不到 AppModel 的守卫顺序。

每一条我都做了反向验证:去掉对应守卫后新测试失败、其余通过。

验证test-macos.sh 672 个测试全过;verify-service-boundaries.shverify-shared-contracts.shverify-module-boundaries.sh、测试稳定性门禁均通过。

上一轮提到的 Spring single-flight 与结构化日志两条非阻塞建议仍建议在 #299 合并后单开 issue,理由写在那条批注下。

避免 await 前后快照错配,以及快照已消费后入口仍用旧捕获 defer 导致 Run 永久挂起。
@Farewell0375

Copy link
Copy Markdown
Contributor Author

@1lck 最后两条未闭环项已处理,9ec89470。细节回在对应批注下,这里总结。

files + snapshotID 同源传递。 WorkspaceSnapshot 现在自带 id,工作区侧以 appliedSnapshot 一次发布;loadProjectServices / onSnapshotLoaded / ensureRunProjectReady 都显式接收这对值,不再在 await 之后回读全局身份去拼代次。

恢复启动断言补全。 已有配置 + 快照阻塞的测试改为等待并断言 launchPlanCallCount == 1(阻塞期为 0),避免只看 pendingRunAction == nil 漏掉「清了却没真正再发 Run」。

自查多修一处竞态。 入口自己的 provisional load 还在 inspect 里时,快照可能已经落地并跑完 deferred resume(当时还没有 pending)。入口返回后若仍用 await 前捕获的 nil 判断,会误 defer 且再也没人 resume。ensureRunProjectReady 现在在 await 后重读 current 快照;由 runResumesWhenTheSnapshotLandsDuringTheEntryPointsOwnLoad 锁住,去掉修复后该测试失败。

另外:配置解析失败与 inventory 就绪继续分开(configurationStatus vs projectLoadState),并补了 unreadableConfigurationStillAllowsRegeneration,避免坏掉的 generated.json 把生成路径也堵住。

验证:RunEntryPointTests 6/6(含 timing harness)、./scripts/test-macos.sh 674/79 通过、verify-service-boundaries / verify-test-stability 通过。

Comment thread macos/Sources/Lithe/Models/AppModel/AppModel.swift
Comment thread macos/Sources/Lithe/Models/AppModel/AppModel+Development.swift Outdated
@1lck

1lck commented Aug 29, 2026

Copy link
Copy Markdown
Owner

@Farewell0375 你好,感谢前面几轮认真处理评审意见。目前还有 3 处与工作区快照时序相关的阻塞点,细节和复现场景已经分别写在对应行内批注中:

  1. 旧项目的 snapshot 回调可能与新项目的 workspace 身份错配;
  2. ensureRunProjectReady 返回 false 后,生成路径仍可能继续扫描旧 inventory;
  3. Run 面板的直接运行与批量运行入口仍会绕过 .ready 守卫。

麻烦有空时一起修复并补上对应的竞态/入口回归测试。处理完成后请在 PR 下艾特 @1lck 复审即可,谢谢!

让 rebuild 在挂起点后再次拒绝过期 workspace,并把 workspace 与 snapshot 一并传给 onSnapshotLoaded;ensure 失败时停止生成;直接启动与批量服务入口统一走 readiness 守卫并保留 pending 意图。
@Farewell0375

Copy link
Copy Markdown
Contributor Author

@1lck 三条阻塞项都已处理,fa807e1c。细节回在对应批注下,这里总结。

1. workspace 与 snapshot 同源。 onSnapshotLoaded 改为接收 rebuild 的 workspaceURLrebuild 在 restore / watch / 回调前再次拒绝 stale,过期不交付回调。

2. ensure == false 停止生成。 报告 projectNotReady 并 return,避免在仍持有 .ready(A) 时扫描旧 inventory。

3. 启动入口收口。 startRunConfiguration / runAllServiceConfigurations 统一走 readiness 守卫;pending 保留具体 configuration / 批量意图。

测试(均做过反向验证):

测试 锁住的行为
rebuildRejectsStaleWorkspaceBeforeSnapshotCallback 回调前切项目不交付 onSnapshotLoaded
generateRefusesStaleReadyInventoryWhenSnapshotAdvancesDuringLoad 加载 A 期间发布 B 不得扫旧 inventory
startRunConfigurationDefersAndResumesAfterTheSnapshotArrives 主播放入口推迟并恢复
runAllServicesDefersAndResumesAfterTheSnapshotArrives 批量服务入口同上

验证test-macos.sh 此前全量通过;本次新增/相关入口测试与 timing harness、verify-service-boundaries / verify-module-boundaries / verify-test-stability 均通过。AppModel.swift 仍为 1788 行。

请复审,谢谢。

@1lck 1lck left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审 fa807e1c:作者本轮新增的 3 项主体修复和 9 条 RunEntryPointTests 均已复核,其中“快照文件与 ID 同源”和“readiness 为 false 时禁止生成旧 inventory”两条已关闭;直接启动与批量服务的 defer/resume 也通过。边界、共享契约、模块边界和测试稳定性校验全部通过。当前仍有两个同一状态机内的阻塞缺口,已续写在原讨论中:一是 Restart 仍绕过 readiness;二是入口任务跨 workspace await 后会把 A 的动作重新登记为 B 的 pending。两条都用反向测试稳定复现,因此本轮暂时维持 changes requested。修好并补回归测试后请再 @1lck 复审。

Restart 走 ensureRunProjectReady/pending 恢复;入口捕获 workspace 区分 stale 与 waiting,避免旧任务 defer 到新工程或清掉对方 pending。
@Farewell0375

Copy link
Copy Markdown
Contributor Author

@1lck 本轮两个阻塞缺口已修完,82169547,请复审。

1. Restart 绕过 readiness
Restart 纳入与其它入口相同的 ensureRunProjectReady / pending 漏斗,PendingRunAction.Kind 增加 .restart。同时把判定依据补全:ensureRunProjectReady 区分「从未加载过该 workspace 的完整清单」(入口自行加载已发布的 scan)和「已有完整清单但快照被取代」(交给快照回调推进,入口不争用)。为此在契约层加 ProjectLoadState.hasReadyInventory(for:),经 RunService / RunFeatureModel 透传,AppModel 不解构服务内部状态枚举。
回归:restartDefersWhenANewerSnapshotIsPublishedButNotYetConsumed

2. 入口任务跨 workspace 身份
入口在任何 await 前一次性捕获 workspace;readiness 返回 .ready / .waitingForSnapshot(workspace:) / .stale,stale 直接丢弃不 defer;stale 回调不再清掉属于当前工程的 pending;loadProjectServices 内每个 await 后补 current 守卫。
回归:directStartFromTheOldWorkspaceIsNotDeferredIntoTheNewWorkspace

测试稳定性
自查时发现我上一轮新增的几条断言在等一个不经模型发布的计数(launchPlanCallCount),且 5s 期限撑不过一整轮快照驱动加载,会偶发失败。已改为由测试替身在 launchPlan 内部开门信号量作为确定性同步点,并把本文件跨加载的正向等待统一到与既有门一致的期限;「不该发生」的负向等待仍保持 1s。焦点套件连续 5 次全绿。

验证

  • ./scripts/test-macos.sh:680 tests passed
  • ./scripts/verify-service-boundaries.sh:通过
  • ./.agents/skills/write-stable-tests/scripts/verify-test-stability.sh:通过
  • 反向验证:禁用「快照被取代 → 等待回调」分支后,Restart 测试以 launchPlanCallCount → 2) == 1 失败,生成测试扫到旧 inventory 变成 .succeeded

一处待你定调:current 守卫用 workspace URL 身份而非自增 token,残留窗口只有「关闭后重开同一路径」,后果是在途动作按同一工程恢复,不跨工程写错清单。需要彻底闭合我再加世代计数。

@1lck 1lck left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审 82169547:Restart readiness 与跨不同 workspace 的 stale/pending 保护均已验证通过,对应 Restart 讨论已关闭;作者新增的 11 条入口测试本地全部通过,边界/契约/模块/测试稳定性校验也通过。当前只剩 1 条阻塞:URL 无法区分同一路径的不同打开世代,旧会话任务可覆盖新会话 pending,且旧 rebuild 也可能在 reset 后发布快照;已用反向测试稳定复现并续写到原 workspace 讨论,建议用 URL + generation/token 一次性闭环。GitHub Swift tests 的红灯来自未改动的 JDTLS 指纹测试达到 15 秒性能阈值,不是本轮入口测试失败;后续新提交会重新触发 CI。修复 generation/token 并补同路径重开回归测试后请再 @1lck

@1lck

1lck commented Aug 31, 2026

Copy link
Copy Markdown
Owner

@Farewell0375 你好 此pr还有进展吗

同一路径重开时 URL 相同,旧会话的入口任务与旧 rebuild 都会被判为当前。改为每次 open/close 推进世代,入口任务、await 后的守卫与 rebuild 一并比较 URL + 世代。
@Farewell0375

Farewell0375 commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

@1lck 剩下的那条阻塞(同路径重开的身份判定)已修完,ef55aee8,请复审。

做法workspaceGenerationWorkspaceFeatureModel 拥有,beginWorkspacereset 各推进一次,因此每次 open/close 都前进。新增 WorkspaceIdentity { url, generation },入口任务在第一次 await 前捕获一次,PendingRunAction 携带它;readiness、defer/clear/resume 以及 loadProjectServices 内每个 await 后的守卫都比较 URL + 世代。rebuild 的两个调用点(AppModel.openProjectDirectly 与模块内 refreshCurrent)同样比较 URL + 世代,所以旧会话的 rebuild 在 reset 之后不会再交付 onSnapshotLoaded

回归测试

  • directStartFromAnEarlierOpeningOfTheSameWorkspaceIsDiscarded:你点名的入口场景,断言旧会话任务不启动、不登记 pending、不覆盖新会话清单。
  • refreshFromAnEarlierOpeningOfTheSamePathDoesNotDeliverItsSnapshot:rebuild 侧,走 refreshCurrent 自建守卫,断言 onSnapshotLoaded 不被交付。

两条都做了反向验证:守卫退回只比 URL 后分别以「旧会话动作被记成当前 pending」和「onSnapshotLoaded 计数为 1」失败。

测试稳定性自查:同路径测试最初的写法同时挂住两次工作区扫描,而扫描运行在 Task.detached(协作线程池),占住两个线程后全量从 26s 涨到 90–130s 并让一批无关门等待用例超时。已改为让第一次扫描直接报告尚无快照,不再挂住扫描线程。

验证

  • ./scripts/test-macos.sh:682 tests passed(连续多次,26–29s)
  • ./scripts/verify-service-boundaries.sh:通过
  • ./.agents/skills/write-stable-tests/scripts/verify-test-stability.sh:通过
  • 新测试耗时:入口场景约 11–12.5s,rebuild 场景约 4s

说明一点观察:本机并行压力下,仓库既有的若干门等待用例(switchingToAnOpenDocumentWinsOverAPendingFileOpenforegroundRequestActivatesAnEquivalentPendingBackgroundOpenprojectReloadDuringGenerationDoesNotLeaveRunServiceLoadingworkspaceWatcherProjectsTypedJavaChangesWithoutReloadingForSourceEdits 等)会偶发超时。把我本轮两条新测试 --skip 掉同样会出现,所以与本轮改动无关;如果你也在 CI 上看到,我可以另开一个 issue 单独收口。

@1lck

1lck commented Aug 31, 2026

Copy link
Copy Markdown
Owner

有冲突目前 麻烦解决一下冲突哦

Farewell0375 and others added 3 commits August 31, 2026 10:44
…ig-load-ordering

冲突解决:
- RunService:保留本分支的 readiness 查询与上游的 Maven 上下文注入。
- AppModel:删除上游在原位修改的旧 loadProjectServices(at:files:),该实现已迁到
  AppModel+Development.swift 并携带快照身份;上游对它的两处调整(执行模块可缺失、
  加载顺序前移)在新实现中已具备等价行为。

合并后为保持本分支契约所做的适配:
- 上游把会话模块图关停改为不阻塞打开流程的任务,它会在按需激活之后落地,把刚激活的
  执行模块拆掉,导致“开完工程立刻按 Run”的延后动作永远无法恢复。改为在打开/关闭时
  同步登记关停任务,Run/Debug 的按需激活先等待它结束。
- 入口测试原先用挂住工作区扫描来模拟未就绪,扫描运行在协作线程池上,12 条并行时把
  线程池占满:自身耗时从 0.07s 涨到 8s,并把上游若干短期限门等待用例挤超时。改为由
  替身直接报告尚无快照、需要时经 refreshCurrent() 发布,并串行化该套件;工具链探测
  改用注入的替身,测试不再依赖本机 JDK。
@Farewell0375

Copy link
Copy Markdown
Contributor Author

@1lck 已将 upstream/preview 合并至本分支(fb04c116),并处理了合并过程中暴露的问题。你补的 9d661a0b 也一并合入了。

一、冲突解决(两处)

RunService.swift:上游新增的 configureMavenContextProvider 与本分支的就绪查询位置相邻。两侧改动均保留。

AppModel.swift:上游在原位修改了旧的 loadProjectServices(at:files:),而本分支已将该实现迁移至 AppModel+Development.swift 并使其携带快照身份。此处采用本分支版本并删除上游那一份,已确认全仓库(含测试)不存在其他调用者。上游对该函数的两项改动在新实现中均有等价行为:执行模块不可用时仅跳过测试发现与项目加载;项目加载不再等待 Spring 索引——本分支已将其改为 scheduleLoad,不再 await。

二、新增的一层顺序保证(本分支对上游行为的唯一改动)

上游将会话模块图的关停改为不阻塞打开流程的任务,该任务会在按需激活之后落地,从而拆掉刚刚激活的执行模块。表现为:打开工程后立即触发 Run 时 runFeatureIfActive 为 nil,已登记的延后动作无法恢复,恰好打断本 PR 建立的链路。该行为是确定性的:移除该关停任务后,入口测试立即由 nil 转为通过。

因此本分支补充了执行顺序:打开与关闭时同步登记关停任务,activateExecutionModuleactivateDebugModule 在激活前等待其结束;clearModuleBindings(for: .database) 相应移入关停任务的收尾,仅执行一次。关停仍不阻塞工作区切换,shutdownProjectSession() 仍会 await 同一任务。需要说明的是,该竞态在合并前的基线上同样存在(同为 fire-and-forget),上游重构只是使其稳定命中。相同窗口对 git、search 等按需激活亦成立,但超出本 PR 主题,本轮仅收口运行相关的两个入口。

三、入口测试的并行成本修正

原实现通过挂住工作区扫描来模拟未就绪状态,而扫描运行在 Task.detached(协作线程池)之上,12 条测试并行时会占满线程池:单条测试实际耗时 0.07 秒,在套件中被拖长至 8 秒,并导致上游若干 1–2 秒期限的门等待用例超时。

本轮改为:测试替身直接报告尚无快照,需要时经 refreshCurrent() 发布;该套件标记为 .serialized;工具链探测改用注入的替身,测试不再依赖本机安装的 JDK(为此在服务容器上新增第四个可选注入缝,默认仍使用真实实现)。修正后单条测试 0.01–1.86 秒,全量 772 项连续多次通过,耗时 21.8–23.2 秒;作为对照,上游 preview 单独运行为 752 项、16.6 秒。

四、关于 9d661a0b

该提交为 updateWatchConfiguration 补上了世代校验,覆盖的是 resumeObservationAfterActivation() 与 recovery 这两条不经过 rebuild 守卫的路径,与本分支 rebuild 层的守卫互补。合并后本分支的两条 watch-context 门控测试不受影响:挂起期间不发生重开,世代不变;即便校验不通过,也仅跳过 watcher 配置,rebuild 仍会继续交付 onSnapshotLoaded。新会话在 beginWorkspace 时已启动一个无 Git 上下文的 watcher,随后的 rebuild 会再次调用 updateWatchConfiguration,因此被丢弃的那次不会留下监听缺口。

五、验证

  • ./scripts/test-macos.sh:772 项全部通过
  • ./scripts/verify-service-boundaries.sh:通过
  • ./.agents/skills/write-stable-tests/scripts/verify-test-stability.sh:通过
  • ./scripts/verify-shared-contracts.sh:通过
  • ./scripts/verify-windows-boundaries.sh:通过

@1lck

1lck commented Aug 31, 2026

Copy link
Copy Markdown
Owner

好的 感谢pr 等ci结束就可以合并了 请问你在开发者群里面吗?

@1lck

1lck commented Aug 31, 2026

Copy link
Copy Markdown
Owner

好的 感谢pr 等ci结束就可以合并了 请问你在开发者群里面吗?

如果不在的话 可以添加我vx 我拉你进来 此pr我先合并了

@1lck
1lck merged commit 14c542a into 1lck:preview Aug 31, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants