Skip to content

fix(cli): 启动 banner 的 Tenancy: 改读 resolveTenancyPosture(),不再打印被取代的布尔 (#4801) - #4994

Merged
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4801-banner-tenancy-posture
Aug 3, 2026
Merged

fix(cli): 启动 banner 的 Tenancy: 改读 resolveTenancyPosture(),不再打印被取代的布尔 (#4801)#4994
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4801-banner-tenancy-posture

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes#4801

为什么读 posture 而不是布尔

ADR-0105 D1 之后,OS_TENANCY_POSTURE 是权威 knob,布尔 OS_MULTI_ORG_ENABLED 只是 resolveTenancyPosture() 在 posture unset 时的回退。serve运行时接线(serve.ts 里挂 @objectstack/organizations 的那段)早就读 resolveTenancyPosture(),但 banner 的 Tenancy: 行读的是 resolveMultiOrgEnabled() 返回的布尔 —— 同一个事实两个来源,于是它们漂了:

 Tenancy: single-tenant ← banner 读布尔(OS_MULTI_ORG_ENABLED 未设 ⇒ false)
Plugins: 40 loaded
…, Organizations, … ← 运行时按 posture=isolated 真的挂上了组织墙

这不是显示美化。这是「声明与执行不一致」(ADR-0049 那一类)落在诊断面上 —— 最坏的落点:一个可能撒谎的 banner,会让此后每一次排障都多花一轮去确认它有没有撒谎。cloud#1020 里就是靠人工比对插件表才发现的。

Tenancy: 现在直接打印 posture 名(single / group / isolated),来源是运行时接线用的同一个 resolveTenancyPosture() 调用。

布尔字段的去留:删除,不保留兼容项

ServerReadyOptions.multiTenant?: booleantenancyPosture?: TenancyPosture。理由有三条,按重要性:

  1. 说不清它还为谁存在。 posture 是权威源;若保留布尔,议题已经规定「不一致时以 posture 为准」—— 那它就是一个永远只能被忽略的字段。一个存在但不可信的字段,正是本 bug 当初被写出来的方式:调用方看到 multiTenant 就填了个布尔进去,完全合理,而结果是错的。
  2. 删除把「错误接线」从运行期挪到编译期。tenancyPosture 的类型是 TenancyPosture,resolveMultiOrgEnabled() 返回 boolean —— 现在把 banner 接回旧 knob 无法通过编译,而不是产出一行看起来很合理的错话。这是本仓「在源头就不给出错的机会」的取向:结构上防止,而不是消费端兜底。
  3. 布尔在语义上本来就不够用。 tenancy 自 ADR-0105 起是三值谱系,布尔根本没有 group 的拼法 —— 一个 group 部署只可能被误报。压扁本身就是漂移藏身的地方。

影响面为零:format.ts 没有从 @objectstack/cli 的入口 re-export,ServerReadyOptions 是包内部接口;全仓 printServerReady 只有 serve.ts 一个调用点(已改齐,并顺手从 import 里去掉了因此不再使用的 resolveMultiOrgEnabled)。

测试:三种场景 + 反向验证

新增 packages/cli/src/utils/format.tenancy.test.ts。被断言的性质不是「这行看起来对」,而是 banner 与 resolveTenancyPosture() 不可能不一致:每个用例都从环境变量算出 resolveTenancyPosture(),再断言打印出的 token 就是它。

覆盖(前三条即议题点名的三种):

场景OS_TENANCY_POSTUREOS_MULTI_ORG_ENABLED期望
posture 显式非 single(cloud#1020 那条)isolatedunsetTenancy: isolated
posture unset、布尔 true(回退路径)unsettrueTenancy: isolated
两者矛盾 · posture 弱singletrueTenancy: single
两者矛盾 · posture 强isolatedfalseTenancy: isolated
布尔表达不了的 posturegroupunsetTenancy: group
默认unsetunsetTenancy: single
legacy 拼法multiunsetTenancy: isolated

外加:省略 posture 时整行不打印;任何 posture 下输出都不再出现 multi-tenant/single-tenant 措辞(先断言该行存在再断言它不含旧措辞 —— 一个没有 Tenancy: 行的 banner 会让 not.toMatch 空转通过,那和本文件要抓的 bug 从外部看一模一样);以及两条 @ts-expect-error,在 pnpm typecheck 而非 review 阶段钉住形状。

反向验证(把 format.ts stash 回改前,只留新测试与调用点)

$ git stash push -- packages/cli/src/utils/format.ts
$ pnpm --filter @objectstack/cli exec vitest run src/utils/format.tenancy.test.ts
× posture explicitly isolated, boolean unset — the cloud#1020 lie 11ms
× posture unset, boolean true — the legacy fallback still reads isolated 2ms
× posture single while the boolean says true — posture wins 2ms
× posture isolated while the boolean says false — posture wins 1ms
× prints group — the posture a boolean can only misreport 1ms
× posture unset and boolean unset — single, the default 1ms
× accepts the legacy 'multi' spelling and prints its canonical name 1ms
× prints the posture verbatim for every posture the spec defines 1ms
× rejects the boolean shape at COMPILE time, not at review time 4ms
AssertionError: expected undefined to be 'Tenancy: single'
AssertionError: expected [ ' Tenancy: multi-tenant' ] to have a length of 2 but got 1
Test Files 1 failed (1)
Tests 9 failed | 2 passed (11)

(注:上面这轮跑的是加强前的版本,当时「never flattens」那条空转通过;已按上一段所述改成先断言行存在。)

改动在位后:

$ pnpm --filter @objectstack/cli exec vitest run src/utils/format.tenancy.test.ts
Test Files 1 passed (1)
Tests 11 passed (11)

两条 @ts-expect-error 也做了同样的反向验证 —— 去掉其中一条,tsc 立刻报出被它挡住的那个错,证明指令是承重的而非装饰:

packages/cli/src/utils/format.tenancy.test.ts(174,79): error TS2322:
Type 'true' is not assignable to type '"group" | "single" | "isolated" | undefined'.

其它验证

  • ESLint:npx eslint packages/cli/src/utils/format.ts packages/cli/src/utils/format.tenancy.test.ts packages/cli/src/commands/serve.ts → 干净(无输出)。
  • Typecheck:本 worktree 里 pnpm --filter @objectstack/cli typecheck 会报一批 TS2307: Cannot find module '@objectstack/runtime' | '@objectstack/core' | …,这是工作区未构建 dist 的既有环境噪音(在未改动的 origin/main 树上同样复现),与本改动无关。改动涉及的两个文件已用一份把 @objectstack/spec / @objectstack/types 映射到源码的 tsconfig 单独 tsc --noEmit 过,结果零错误;CI 会先构建再 typecheck,是权威一轮。
  • 同理,src/utils/ 里 7 个测试文件在本地因同样的未构建依赖而 collect 失败,sqlite-occupancy.test.ts 有 1 条环境相关失败 —— 均已通过 stash 全部改动、在等同 origin/main 的树上复跑确认为既有失败(Tests 1 failed | 14 passed,一字不差)。

边界

只做 banner 这半块。未触碰serve.ts 里的 tenancy 守卫逻辑(#4818 / PR #4858 刚改过),也未触碰 cloud#1020 那边仍在等维护者拍板的许可闸门决策 —— 正如议题开头所写,banner 无论那个决策怎么落都该修。已附 changeset(@objectstack/cli patch,CLI 输出为用户可见变化)。


Generated by Claude Code

…egacy boolean (#4801)
[ADR-0105 D1] `OS_TENANCY_POSTURE` is the authoritative tenancy knob and
`OS_MULTI_ORG_ENABLED` survives only as the fallback `resolveTenancyPosture()`
consults when the posture is unset. serve's runtime wiring already keys off the
posture, but the banner's `Tenancy:` row printed a boolean sourced from
`resolveMultiOrgEnabled()` — two sources for one fact. Booting with
`OS_TENANCY_POSTURE=isolated` alone printed `Tenancy: single-tenant` one line
above a plugin table listing `Organizations` (cloud#1020): the diagnostic
surface contradicted the runtime it describes.
- `ServerReadyOptions.multiTenant` (boolean) → `tenancyPosture: TenancyPosture`,
printed verbatim (`single` / `group` / `isolated`). The boolean is removed
rather than kept as a compat field: with the posture authoritative it could
only be a field the printer ignores, and typing the replacement as
`TenancyPosture` makes the old wiring a compile error instead of a
plausible-looking wrong line. `group` was never expressible as a boolean.
- serve passes `resolveTenancyPosture()` — the same call the wiring uses — and
no longer imports `resolveMultiOrgEnabled` at all.
- `format.tenancy.test.ts` pins the invariant "printed token IS
`resolveTenancyPosture()`" across posture-set/boolean-unset, posture-unset/
boolean-true, both-set-and-contradicting (both directions), legacy `multi`,
and `group`; two `@ts-expect-error` directives pin the shape at typecheck.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ
@vercel

vercelBot commented Aug 3, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 3, 2026 7:04pm

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/m labels Aug 3, 2026
@github-actions

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.

@xuyushun441-sys
xuyushun441-sys marked this pull request as ready for review August 3, 2026 19:06
@xuyushun441-sys
xuyushun441-sys added this pull request to the merge queueAug 3, 2026
Merged via the queue into main with commit b11a7e3Aug 3, 2026
24 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the claude/issue-4801-banner-tenancy-posture branch August 3, 2026 19:23
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@xuyushun441-sys@claude