Skip to content

fix(cli): os doctor 不再在 installed-package ledger 读不到时打出 ✓ Unique scope (#5412) - #5414

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-5412-doctor-ledger-read-failure
Aug 5, 2026
Merged

fix(cli): os doctor 不再在 installed-package ledger 读不到时打出 ✓ Unique scope (#5412)#5414
baozhoutao merged 1 commit into
mainfrom
claude/issue-5412-doctor-ledger-read-failure

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5412

先说前提核验的结果:issue 的机制判定对了一半,复现路径是错的

按 origin/main (b4872a868) 核验,必须先读这一段再看实现。

issue 说 readInstalledPackageEntries() 的 catch 覆盖了两件事,其中第 2 件是
「目录存在但里面某个 manifest 损坏 / JSON 解析失败」。这条具体路径够不着那个 catch。

packages/cloud-connection/src/local-manifest-source.tsLocalManifestSource.list()
自己有一层 per-file catch:

        for (const name of readdirSync(this.dir)) {
            if (!name.endsWith('.json')) continue;
            try {
                const raw = readFileSync(join(this.dir, name), 'utf8');
                out.push(JSON.parse(raw));
            } catch { /* skip corrupt files */ }
        }

跑真实实现(一好一坏两条目)实测:

A) truncated-entry list() DID NOT THROW; returned 1 entries: [ 'good' ]

也就是说:损坏条目在生产方就被丢掉了,list() 成功返回一个短列表,doctor 那边
看到的是一次成功调用。issue 的复现路径(截断 JSON)按原样构造,✓ Unique scope
照样会打 —— 但原因不在 doctor 的 catch,而在上游。

issue 的核心判定仍然成立:那个 catch 确实把「可选包没装」和「ledger 读不到」
混为一谈,而且确实产出 false PASS。只是够得着它的是目录级失败(路径被文件占住 =
ENOTDIR、文件系统拒绝、读目录失败),不是条目级损坏。实测:

fs.existsSync(dir) = true
C) list() THREW: ENOTDIR | ENOTDIR: not a directory, scandir '…/.objectstack/installed-packages'

所以本 PR 修的是目录级那一半,并把条目级那一半明确钉成边界、另立 #5413,
而不是把测试改成能过的形状然后声称整单做完。PM 裁定的口径(拆两个 catch、warning
档、复用 renderHealthCheckResult() 不得打印)一字未改地适用于目录级路径;
只有 PM 要求的测试 ① 换了触发方式(ENOTDIR 而非截断 JSON),因为截断 JSON 在这一层
根本不产生任何信号。

改了什么

packages/cli/src/commands/doctor.ts:

  • readInstalledPackageEntries() 拆成两个 try。import('@objectstack/cloud-connection')
    保留自己的静默 catch —— 这一条注释写得对,doctor 必须能在没有该包的 checkout 里
    跑完。目录解析之后的读取单独一个 try,抛错时带着 cause 回到调用方。
    返回类型从 any[] 变为 { entries, failure? }:空数组本身是歧义的(「没装过东西」
    和「读不到」都产生它),第二种现在自带标记。
  • findUnscopedGlobalUniques() 相应返回 { advisories, ledgerFailure? }
  • 消费点:成功行是对建议两半(本项目 metadata + ledger 已装包)的断言,因此
    只有两半都跑过才允许打印。ledger 失败时改打一条常规 HealthCheckResult,经
    renderHealthCheckResult()(fix(cli): os doctor 说出配置载入失败的原因,不再只说一句「载入不了」(#5403) #5410 刚落地的那一个,没有发明第二套渲染),
    --verbose 展开免费获得。已跑过的那一半的 findings 照常上报 —— 一处静默省略
    不该换成另一处。
  • 新增 installedPackageLedgerFailureCheck(),warning 档,cause 按 fix(cli): os doctor 指名道姓报告非法 OS_TENANCY_POSTURE 并非零退出 (#5382) #5390 体例引
    上游原话不改写(ENOTDIR: not a directory, scandir '…' 本身就指名了挡路的那个
    文件,doctor 编不出更好的句子)。
  • 该行占用 Unique scope 名字列而非新起一个名字:重点是操作者扫报告时那一行
    、且不是 ;另起名字会让 Unique scope 干脆消失,那正是本单要治的沉默
    换了顶帽子。
  • 小重命名:configLoadHeadline / CONFIG_LOAD_HEADLINE_MAX 现在有了第二个消费者
    (ledger 的 cause 走同一个折行),名字改为 reportRowHeadline /
    REPORT_ROW_HEADLINE_MAX。纯内部,无外部引用。

反向验证(方向是先定后跑的)

预期方向:。把那个吞掉的 catch 恢复(第二个 try 的 catch 改回 return { entries: [] }),
预期只有钉缺陷的 3 条 e2e 转红,4 条无回归用例保持绿 —— 后者若也变绿/变红都说明它们
是因为「什么都没产出」而通过的。实跑结果与预期一致:

 × withholds the clean bill of health when the ledger cannot be read
 × expands the detail under --verbose, and only under --verbose
 × still reports this project’s own findings when only the ledger half failed
 ✓ an intact ledger with nothing to report still prints the clean bill — no regression
 ✓ an intact ledger WITH a global unique is still reported the way it always was
 ✓ says nothing about the ledger when there is no ledger at all
 ✓ ⚠ SCOPE BOUNDARY: a CORRUPT ENTRY is still absorbed by the producer
 ✓ prints no ledger row when the optional package cannot be loaded
 Tests  3 failed | 12 passed (15)

AssertionError: expected '…' not to contain 'No unconfirmed installation-wide uniq…'
AssertionError: expected '…' to contain 'Could not read the installed-package …'

恢复后 15/15 全绿。

测试

新增 packages/cli/src/commands/doctor-ledger-read-failure.test.ts(15 条,e2e 沿用
#5410 doctor-config-load-cause.test.ts 的 harness:temp cwd + node_modules 目录
陷阱,每条 }, 60_000))。覆盖 PM 点名的四项:

  1. ledger 读不到时 ✓ Unique scope 那行不出现;
  2. warning 行出现、含 cause,--verbose 才展开 cause:;
  3. 可选包缺失仍完全静默(用 vi.doMock + vi.resetModules() 让模块求值抛错来
    模拟不可解析;该场景下故意让 ledger 目录存在,这样若两个 catch 还共用,反方向
    的混淆会冒出一条假 warning);
  4. ledger 完好时 D5e 正常路径不受影响 —— 无 findings 照打 ,有 findings 照报
    installed package 'billing';另加「压根没有 ledger 目录」仍静默(不能把「从没装过」
    过度矫正成 warning)。

外加一条 ⚠ SCOPE BOUNDARY 用例,把「条目级损坏仍被生产方吸收」钉住并注明
#5413 落地时它应当转红

越界与另立单


🤖 Generated with Claude Code

https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh


Generated by Claude Code

`readInstalledPackageEntries()` 用一个不带绑定的 `catch` 罩住了两件性质完全
不同的事,并对两者都返回空数组:

  1. `@objectstack/cloud-connection` 解析不到 —— 可选包没装。这一条静默是
     对的,保持不变:`os doctor` 必须能在没有该包的 checkout 里跑完。
  2. ledger 目录**存在**(`fs.existsSync` 已经通过)但读取抛错。

第 2 种被当成第 1 种,于是它以「没有已装包」的身份抵达 ADR-0120 D5e 的
unique-scope 建议,建议无话可说,报告打出 `✓ Unique scope`。一个装了包、
ledger 却读不到的 isolated 环境拿到的是一张干净体检单 —— false PASS 比漏报
更糟,它让操作者停止查看。

两者现已拆开:`import()` 失败保留自己的静默 catch;读取失败带着 cause 回到
调用方,成功行被扣住(它是对建议**两半**的断言,只有两半都跑过才能打),
改为经 `renderHealthCheckResult()` 输出一条 warning 档 `HealthCheckResult`,
`--verbose` 展开随之免费获得。已跑过的那一半的 findings 仍照常上报。

不在本单范围:ledger 内**单个条目**损坏 —— `LocalManifestSource.list()` 在
自己的 per-file catch 里跳过它,调用成功且返回短列表,消费者无从分辨。
已另立 #5413,并由本 PR 的 SCOPE BOUNDARY 测试钉住。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 5, 2026 10:16am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli.

21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/validating-metadata.mdx (via packages/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/plugins/index.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/releases/v17.mdx (via @objectstack/cli)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants