fix(runtime): empty-env 早退跳过 disabled seed —— 禁用的包重启即复活 (#5047) - #5117
Merged
Conversation
…arly return (#5047) `AppPlugin.init` seeds the registry's initial-disabled set from `<OS_HOME>/package-state/<environmentId>.json` so every registration path installs operator-disabled packages disabled. That seed ran AFTER the empty-env early return, and an empty env (no app payload in the artifact) is exactly the hydration-only scenario: its packages all arrive later from `sys_packages` replay or an HTTP install. Result: on DB-driven environments the initial-disabled set stayed empty and disabled packages came back enabled on every restart. Move the seed above the return, next to the hook/action body runners and the authored-translation sync that are already hoisted for the same reason, and extract it into `seedPersistedDisabledPackages()` with the rationale attached. Tests (the blind spot that hid this — `package-state-store` and `setInitialDisabledPackageIds` had zero coverage repo-wide): - `package-state-store.test.ts` — round trip, per-environment isolation, missing/corrupt file degradation, env-id sanitization. - `app-plugin.disabled-seed.test.ts` — real LiteKernel + ObjectQLPlugin + empty-env AppPlugin over a real state file, asserting post-boot registration and the REAL `PackageServicePlugin` `sys_packages` replay both land disabled; plus the non-empty env's existing behavior. Reverse-verified: with the seed moved back below the return, exactly the two empty-env cases fail (`installed` / `enabled: true`) and the non-empty case stays green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
xuyushun441-sys
marked this pull request as ready for review
August 4, 2026 05:24
xuyushun441-sys
enabled auto-merge
August 4, 2026 05:24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5047
问题
议题原文的定性(「enable/disable 只写内存」)在 PM 核实中已被推翻:持久化存在且刻意设计(
package-state-store.ts写$OS_HOME/package-state/{environmentId}.json),boot 回灌时序也自洽。本 PR 修的是核实过程中挖出的那个更窄的真 bug。AppPlugin.init用「把 disabled 集合 seed 进 registry 的 initial-disabled 集」这一招,让所有注册路径(boot artifact 分解、sys_packages重放、HTTP 安装)统一以 disabled 落位。但这个 seed 块位于 empty-env 早退之后:而 empty env(artifact 无 app 载荷)恰恰是 hydration-only 场景:该环境的包全部来自 Phase 2 从
sys_packages的重放,或来自 HTTP 安装。于是纯 DB 驱动的环境里 initial-disabled 集永远是空的,管理员禁用的包每次重启都以 enabled 复活,且全程无任何报错 —— 禁用状态写盘完全正确,只是没人读。修法
把 seed 提到早退之前,与上面四个因同一理由被提前的调用并列;顺手抽成
seedPersistedDisabledPackages()并把 rationale 写在方法上。非 empty env 行为不变:
manifest.register()(即分解发生的地方)在 init 末尾,seed 仍然先于它。seed 仍是 best-effort,在无 engine 的 kernel(metadata-only 一次性命令、mock-engine 测试)上静默降级。测试(本单的一半)
这个 bug 能存在,直接原因是全仓零测试覆盖
package-state-store/setInitialDisabledPackageIds—— 往返的两头都没人断言。packages/runtime/src/package-state-store.test.ts(12 例):写入/读取往返、重新启用移除条目、排序落盘、幂等、环境隔离(staging 的禁用不能影响 production)、OS_ENVIRONMENT_ID→default回退链、环境 id 路径消毒(../../etc/evil不得逃出目录)、文件缺失 / JSON 损坏 / 非对象载荷一律降级为空集而非抛异常。packages/runtime/src/app-plugin.disabled-seed.test.ts(7 例) —— 本 bug 的钉子,全部跑真 kernel(LiteKernel+ObjectQLPlugin+ empty-envAppPlugin)、真状态文件:disabled落位(hydration-only 路径);PackageServicePlugin.start(),从sys_packages重放的包以disabled落位(SQL 层用hydration.test.ts同款 execute 替身,registry 与 boot 的是同一个);为第 3 例给
@objectstack/runtime加了@objectstack/service-package的 devDependency + vitest alias(dev-only,与既有的@objectstack/service-job同款先例):这样重放路径断言的是真实 hydration 代码,而不是它的复刻品。反向验证(把 seed 挪回早退之后,重跑同一批测试):
正好红在两个 empty-env 用例上,非 empty env 用例保持绿 —— 钉子钉在了它该钉的位置。
验证
pnpm --filter @objectstack/runtime testTest Files 84 passed (84)/Tests 1150 passed (1150)pnpm --filter @objectstack/runtime typechecktsc --noEmit无输出)pnpm --filter @objectstack/service-package test4 passedeslint(改动文件)check:init-service-contract/check:engine-double-contract/check:published-files/check:type-check-coverage/check:durability-log-level/check:service-providersChangeset:
.changeset/empty-env-disabled-package-seed.md(@objectstack/runtimepatch),写明用户可见后果。本 PR 不修(PM 评论已记录为债)
package-state-store.tsrationale 的 ADR 级取舍;DELETE /packages/:id清理sys_packages但不清理 disabled 条目 → 同 id 重装会静默以 disabled 回来;sys_packages无 lifecycle 列(raw-SQL 表,无 spec 定义),统一到 DB 需 schema 变更。