Skip to content

fix(cli): os doctor 指名道姓报告非法 OS_TENANCY_POSTURE 并非零退出 (#5382) - #5390

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-5382-doctor-posture-report
Aug 5, 2026
Merged

fix(cli): os doctor 指名道姓报告非法 OS_TENANCY_POSTURE 并非零退出 (#5382)#5390
baozhoutao merged 2 commits into
mainfrom
claude/issue-5382-doctor-posture-report

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5382

前提核验:成立,而且比 issue 描述的更糟

先在 origin/main(ada0b84a3,#5380/#5385 已合入)上把 issue 的复现跑了一遍 —— 真机运行 doctor 命令,临时 cwd 里放一个能正常载入的 objectstack.config.ts,OS_TENANCY_POSTURE=bogus:

  → Loading configuration for analysis...
  → Checking platform spec version...
  ✓ Platform spec         Declared specVersion is current with the installed platform
  ⚠ Could not load config for analysis (config checks skipped)

⚠️  Environment is functional but has some warnings.

EXIT=0        mentions OS_TENANCY_POSTURE: false

注意 ✓ Platform spec 那一行:配置是载入成功了的,checkSpecVersionGap 都已经跑完并通过,之后才轮到 doctor.ts:688 的 posture 抛错被同一个 try 的 catch 接住。所以「Could not load config」这句话不只是归因不准,它是字面上的假话 —— config 载进来了。issue 的判断完全成立。

顺带发现 issue 没提到的一半:那两处 posture 读取都在 if (configExists()) 之内。所以在没有 objectstack.config.ts 的环境里,doctor 连那句归因错误的 warning 都不会有 —— posture 从头到尾没被读过,零信息、exit 0:

  ✓ Git                  git version 2.43.0
  → Checking for missing test files...
  ✓ Test coverage         All *.zod.ts files have matching tests
  → Scanning for @deprecated usage...
  ✓ Deprecations          No @deprecated tags found

⚠️  Environment is functional but has some warnings.

EXIT=0        mentions OS_TENANCY_POSTURE: false

改了什么

按 PM 裁定的方向 1:在 run() 顶部、任何 try 之外解析一次 posture,非法值报 error 并计入 hasErrors, process.exit(1) —— doctor 的语义是「报告」,报告照常跑完,由它自己的错误汇总给出退出码。

修完之后,同一个环境(真机运行 bin/run.js doctor):

  ✓ Git                  git version 2.43.0
  ✗ Tenancy posture      OS_TENANCY_POSTURE="isolatd" is not a recognized tenancy posture — `os serve` refuses to boot this environment
      → Set one of the accepted values:
        • OS_TENANCY_POSTURE=single — one organization, no organization wall — the default
        • OS_TENANCY_POSTURE=group — organization wall enforced by the open engine, one shared database
        • OS_TENANCY_POSTURE=isolated — organization wall + the enterprise @objectstack/organizations runtime (the legacy spelling 'multi' is accepted and normalizes to this)
        • or unset OS_TENANCY_POSTURE entirely — the posture then derives from
          OS_MULTI_ORG_ENABLED (true ⇒ isolated, anything else ⇒ single)
      Read from this process's environment only: unlike `os serve`, `os doctor` does not
      load `.env*` files, so a value set in one is not visible here.
      cause: Invalid OS_TENANCY_POSTURE="isolatd". Expected one of: single, group, isolated …
  → Checking for missing test files...
  …
❌ Some critical issues found. Please fix them before continuing.

EXIT=1
  • 修法清单由 @objectstack/spec/securityTENANCY_POSTURES 生成,不是第二份字面量 —— 新增一个 posture 不会让这段建议悄悄过期(有测试钉住)。
  • cause 直接引用 @objectstack/types 解析器自己那句话,doctor 不维护会跟它跑偏的第二份措辞。
  • 两处读取(doctor.ts:688 的 D5e 闸门、findUnscopedGlobalUniques())现在复用同一个已解析值;findUnscopedGlobalUniques 改为接收 posture 参数,resolveTenancyPosture() 在整个文件里只剩包装器内那一处调用。
  • 合法 posture 的输出一字未变(见下方测试的对照组)。

与 PR #5381(serve)的关系:同形,不同裁决,没有复用它的代码

范本是 #5381resolveTenancyPostureOrRefusal,本单是 resolveTenancyPostureOrFinding,刻意分开:

serve(#5359 / PR #5381) doctor(本单)
裁决 拒绝:FATAL + 在任何启动动作之前 process.exit(1) 报告:一条普通 error 体检项,报告跑完后由汇总退出
文案 「Refusing to boot」「the HTTP server was never started」 os serve refuses to boot this environment」——doctor 自己不启动任何东西
.env 闸门在 dotenvFlow.config() 之后,所以明说「检查了每个 .env」 doctor 不加载 .env*,所以明说「没看 .env」

没有从 serve.ts import,理由有二:文案在三处都必须不同(照搬会让 doctor 谎称自己检查过 .env、谎称拒绝启动);且 serve.ts 是 2800 行、拖着整条启动依赖图的模块,让 doctor 为一个纯函数把它整个拉进来不划算。两份加起来约 40 行、共享同一个 TENANCY_POSTURES 来源。去重机会属实存在(一个 utils/tenancy-posture.ts 放共同的 verdict 形状 + hints 表,两边各自拼文案),但那要动 serve.ts —— 本单文件面被限定在 doctor.ts,按派发口径不扩面,记在这里供后续裁定。

测试

新增 packages/cli/src/commands/doctor-tenancy-posture-report.test.ts(11 例)。此前 packages/cli 对 doctor 的该行为零测试

  • 接受面:TENANCY_POSTURES 里每个值、legacy multiisolated、未设置时走 OS_MULTI_ORG_ENABLED 派生、空白值等同未设置。
  • 拒绝面:以值返回而非抛出(被无关 catch 吞掉正是本单的缺陷)、status 必须是 'error'(这个字段才是驱动 hasErrors → 非零退出的那个,写成 'warning' 就复现了缺陷)、指名变量与用户实际敲的值、修法清单覆盖每个 posture、cause 引用原话、以及不出现「Could not load config」/「config checks skipped」。
  • 端到端:进程内跑真实 doctor 命令的差分对照 —— 同一个临时 cwd、同一套检查,只改 OS_TENANCY_POSTURE 一个变量。

关于那个端到端用例的两点自我约束:

  1. 临时 cwd 里特意创建了 node_modules/。否则 doctor 的 Dependencies 检查自己就是 error、自己就会 exit 1 —— 那样「exit 1」这条断言会因为与本改动无关的理由通过,是个假绿。
  2. 合法 posture 那一半(断言「没有 posture 报错行」)单独看证明不了任何东西 —— 它在改动前也通过。它是对照组,不是证据;证据是另一半。

反向验证(方向在跑之前就定了:标准的 before-red / after-green)。doctor.ts stash 回 origin/main 状态、只留测试,端到端用例如预期变红,红的方式正是缺陷本身:输出里 OS_TENANCY_POSTURE 一次都没出现、process.exit 从未被调用、结尾是「Environment is functional」。上面「前提核验」那两段输出就是这次反向跑的原始输出。

$ pnpm --filter @objectstack/cli typecheck && pnpm --filter @objectstack/cli build \
    && pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2
 Test Files  72 passed (72)
      Tests  660 passed (660)
   Duration  137.64s
VERIFY_EXIT=0

改动前的整包基线同样是 660 通过(除了新测试文件本身的 11 红),所以没有既有用例被这条新增的 error 行影响。另跑:check:nul-bytes OK、check:durability-log-level OK、check:startup-registry-verdict OK、check:role-word OK、check:error-code-casing OK、check:wildcard-fallthrough OK、eslint 干净;控制字符自查(grep -naP\x00-\x08\x0b\x0c\x0e-\x1f)三个文件均无裸控制字节。

顺带发现(已另开 issue,未在本 PR 修)

另外两处看过、判断为不需要动:

  • packages/cli/src/commands/migrate/plan.ts:104/118resolveTenancyPosture() 也在一个宽 try 里,但它的 catch 是 printError(error.message) + this.exit(1) —— 消息里带着变量名、退出码是 1。缺处方,但不是本单这个「exit 0 + 归因错误」的缺陷,不构成 finding。
  • doctor 的错误 fix 文本会被打印两次(结果行一次、结尾汇总再一次),这是它对所有 error 行的既有行为(Node.jspnpmDependencies 都如此),只是本单的 fix 较长所以显眼。改它会连带改掉其它行的汇总提示,属于 doctor 输出契约的另一个决定,没有搭车。

🤖 Generated with Claude Code

https://claude.ai/code/session_01VkPSGsX9o17MsGv3Lbxu2w


Generated by Claude Code

doctor 的两处 posture 读取都在 config 分析那个很宽的 try 里,抛错被它的
catch 转述成「Could not load config for analysis」并只记 warning —— 一个
os serve 会拒绝启动的环境,os doctor 报「Environment is functional」+ exit 0,
全程不出现 OS_TENANCY_POSTURE 这个词。归因错了,严重级也错了。

现在在 run() 顶部、任何 try 之外解析一次,非法值产出一条普通的 error 体检项
(修法清单由 TENANCY_POSTURES 生成,cause 直接引用解析器原话),由 doctor
自己的错误汇总给出非零退出码 —— 不 process.exit,doctor 的语义是「报告」。
两处读取复用同一个已解析值。

顺带:那两处此前都在 if (configExists()) 之内,没有配置文件的环境从来没读过
posture;现在与是否存在配置文件无关。

残留(文案里如实写明,另记为 #5387):doctor 不加载 .env*,写在 .env 里的
非法 posture 仍到不了这份报告。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VkPSGsX9o17MsGv3Lbxu2w
@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 4:27am

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.

Copy link
Copy Markdown
Contributor Author

合入 origin/main(df5425499,含 PR #5381)后复跑 —— 该合入与本 PR 同包同行为面(都在 packages/cli,都动 OS_TENANCY_POSTURE),所以按 AGENTS.md §10 走了完整复验而不是只重建:

$ pnpm --filter @objectstack/cli typecheck && pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2
 Test Files  73 passed (73)
      Tests  671 passed (671)
   Duration  141.61s
POSTMERGE_EXIT=0

文件面无重叠(#5381 只动 serve.ts 及其测试),真正值得复验的是两个测试文件都在改写 process.env.OS_TENANCY_POSTURE —— 同一 worker 里顺序执行时可能互相污染。两边都做了 beforeEach 存 / afterEach 还原,实测 671 全绿,没有串扰。


Generated by Claude Code

@baozhoutao
baozhoutao marked this pull request as ready for review August 5, 2026 04:30
@baozhoutao
baozhoutao enabled auto-merge August 5, 2026 04:30
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit ccba1bb Aug 5, 2026
24 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-5382-doctor-posture-report branch August 5, 2026 04:43
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