Repository navigation
Commit 158cc2a
fix(service-settings): a select, radio or multiselect setting refuses every value when its option source answers an empty list (#31569)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X1nhftM2hZHHvqk2GLFyVTN)_
domain:server#3 · os-project-manager · session_01FEkQ5nBSzc6ktJyjdFcDb3
Fixes #31550
Clause-②: no (narrowing) (a select / radio / multiselect value is
refused when its registered option source answered an empty list; it was
accepted at both doors before)
Metadata-protocol: none
## 维护者速读
- 改了什么:select / radio / multiselect 设置的值,按该键注册的选项来源判定;来源答了「空列表」时,以前保存门与
`OS_*` 环境变量门都跳过校验、任何值都放行;现在两道门都拒绝(保存 `invalid_option`「… offers no value
to choose from right now.」,环境变量记一条 error 且不生效),与 #31422 对 map
键的规则一致。顺带:环境变量拒绝日志的去重改按稳定序列化,同一变量下两份不同的坏 map 各报一次(以前所有 map 都被当成 `[object
Object]`,第二份被吞)。
- 为什么改:#31539 契约复审的 Finding 1 与一条观察。
- 风险与回滚:收窄,只影响「已注册来源答了空列表」的 select 族键。启动窗口:`localization.locale` 的来源在
i18n
列表为空时答的是「无答案」(`undefined`),不是空列表,所以走声明选项;环境变量门在启动期间(`holdOptionSourceVerdicts`
到 `kernel:bootstrapped` 封存)对来源判定一律先放行并在封存时重判(有对照
pin)。没有来源、或来源答不出时,仍按声明选项判定。service-settings 全量测试全绿。回滚即还原本 PR。
- 先看:`packages/services/service-settings/src/settings-service.ts` 的
`validatePatch` select 分支、`effectiveEnvOverride` select
分支、`reportRejectedEnvOverride` 的 `dedupeAt`;新 pin
`settings-select-empty-source-31550.test.ts`。
- 两分钟验证:`pnpm --filter @objectstack/service-settings exec vitest run
src/settings-select-empty-source-31550.test.ts`,预期 9 条全绿;其中 5 条在 main
上是红的。
- 需要你做的:无,席位落地(收窄,落地前席位做契约复审)。
## What changes
- **`validatePatch`, the option-bearing branch** (`service-settings`):
it reads `sourced` from `optionTableOf` (#31539) and judges the value
whenever a source answered (`sourced || allowed.length > 0`). The
`allowed.length > 0` leniency stays for the declared table alone (a
hand-built manifest with no options). An empty sourced table refuses
with `invalid_option` and the sentence "`<Label>` offers no value to
choose from right now." (the value echoed unless the key is encrypted),
constraint `{ allowed: '' }`.
- **`effectiveEnvOverride`, the option family**: the same rule. The
refusal's detail reads "Allowed values: none (the running system offers
no value right now)." The boot hold is untouched: a sourced refusal
while held is taken in force and judged again at the seal.
- **`reportRejectedEnvOverride`**: the "said once" key is `envName=`
plus `stableStringify(value)` for an object or array (the file's own
order-independent serialiser), `String(value)` for a scalar as before.
Every map used to key as `[object Object]`.
## How the boot window is covered (`localization.locale`)
- The key with real reach is `localization.locale`. Its source
(`localizationLocaleOptionSource`) answers `undefined`, not an empty
list, when the i18n service reports no locale, so the declared four
apply and nothing here changes for it while the list is empty.
- The `OS_*` door is held from `SettingsServicePlugin.init`
(`holdOptionSourceVerdicts`) to `kernel:bootstrapped`
(`sealOptionSources`). While held, a sourced key's refusal (an empty
answer included) is taken in force, unreported and remembered; at the
seal it is judged against the list as it then stands, said once if still
refused, and a change is emitted. Pinned: an override held over an empty
list stays in force when the list fills before the seal, and is judged
and said once when it is still empty at the seal.
- The save door has no hold. A save during the boot window against a
source that answers an empty list would be refused; no shipped source
does that (the locale source answers `undefined`), and a save comes over
HTTP after the boot.
## Tests
New
`packages/services/service-settings/src/settings-select-empty-source-31550.test.ts`
(9 rows), over a synthetic `pick_lab` manifest with two sourced keys
(`select`, `multiselect`) and one unsourced `select`:
- Save door: an empty answer refuses a select and a multiselect value
(`invalid_option`, nothing stored). Controls: the value saves once the
source offers it; a source that cannot answer leaves the declared
options; an unsourced key is judged against its declared options.
- Env door: an `OS_PICK_LAB_CHANNEL` override over an empty answer is
not in force (one error line, the default reads). Controls: the boot
hold above; a source that cannot answer leaves the declared options.
- Dedupe: two different bad `OS_LIFECYCLE_QUOTAS` maps are each said
once; the same map with its keys reordered is not said again.
## Acceptance notes
- **Responsibility.** Finding 1 and an observation of the at-tier
contract review of PR #31539 (6722607950), noted in #31422's
os-dev-report (6722247790).
- **Narrowing, stated.** A select-family value under a registered source
that answers an empty list, saved or in `OS_*`, was accepted and is
refused now. No value could be one the running system offers, so nothing
that worked stops working.
- **Worktree.** The claim names `objectstack-issue-31550`; this ran in
the chain's existing worktree `objectstack-issue-31317` on the same
claimed branch, cut from fresh `origin/main` after #31539 merged.
## Verification
Head `1999c4c5` (base `4d165acd`, fresh `origin/main`, which holds
#31539's merge `dc8479e0`).
<details><summary>Red first, fix, suites</summary>
- The new pin on main's source: Tests 5 failed | 4 passed (9): both save
rows (`the save was accepted`), the env row, the hold row's still-empty
half (`expected 'env' to be 'default'`) and the dedupe row (`expected
"vi.fn()" to be called 2 times, but got 1 times`); the four controls
green. With the fix: 9 passed.
- `@objectstack/service-settings` vitest: Test Files 74 passed (74),
Tests 1416 passed (1416).
- `typecheck` for `@objectstack/service-settings`: exit 0. `eslint
--no-inline-config packages/services/service-settings`: exit 0.
</details>
<details><summary>Ablation (ablation-replace, each restored to
HEAD)</summary>
- Save door: `if (sourced || allowed.length > 0) {` in `validatePatch`
made to ignore `sourced` (`(sourced && String("ABL31550").length < 0) ||
…`), blob 2c3e7c6 to 6301804190b0: Tests 2 failed | 7 passed (9),
the two save rows.
- Env door: the same in `effectiveEnvOverride`, blob to fb851f1300fa:
Tests 2 failed | 7 passed (9), the env row and the hold row.
- Dedupe: the `stableStringify` arm made never-taken, blob to
9274e3133eb6: Tests 1 failed | 8 passed (9), the dedupe row.
- Each restored: blob == HEAD (2c3e7c6), `git diff HEAD` empty. The
suite reads its own `src/`, so no dist leg.
</details>
<details><summary>Derived gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `1999c4c5`: 66 commands, each run with
`timeout 1500` and stdin from `/dev/null`, exit codes captured before
any pipe, after a `@objectstack/runtime` closure and
`@objectstack/service-settings` build. All 66 exit 0 on the first run
(the changeset carries the `adr-0087` line #31539 needed).
`dispatch-gates --ran`: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN. Not
run locally (CI): `test:repo`, dogfood shards, type-check lanes.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5nBSzc6ktJyjdFcDb3)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 6b83ec2 commit 158cc2a
3 files changed
Lines changed: 231 additions & 7 deletions
File tree
- .changeset
- packages/services/service-settings/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1528 | 1528 | | |
1529 | 1529 | | |
1530 | 1530 | | |
1531 | | - | |
1532 | | - | |
| 1531 | + | |
| 1532 | + | |
| 1533 | + | |
| 1534 | + | |
| 1535 | + | |
| 1536 | + | |
1533 | 1537 | | |
1534 | 1538 | | |
1535 | 1539 | | |
1536 | 1540 | | |
1537 | | - | |
| 1541 | + | |
| 1542 | + | |
| 1543 | + | |
1538 | 1544 | | |
1539 | 1545 | | |
1540 | 1546 | | |
| |||
1703 | 1709 | | |
1704 | 1710 | | |
1705 | 1711 | | |
1706 | | - | |
| 1712 | + | |
| 1713 | + | |
| 1714 | + | |
| 1715 | + | |
1707 | 1716 | | |
1708 | 1717 | | |
1709 | 1718 | | |
| |||
3420 | 3429 | | |
3421 | 3430 | | |
3422 | 3431 | | |
3423 | | - | |
| 3432 | + | |
3424 | 3433 | | |
3425 | 3434 | | |
3426 | 3435 | | |
3427 | 3436 | | |
3428 | 3437 | | |
3429 | | - | |
| 3438 | + | |
| 3439 | + | |
| 3440 | + | |
| 3441 | + | |
3430 | 3442 | | |
3431 | 3443 | | |
3432 | 3444 | | |
| |||
3440 | 3452 | | |
3441 | 3453 | | |
3442 | 3454 | | |
3443 | | - | |
| 3455 | + | |
| 3456 | + | |
| 3457 | + | |
3444 | 3458 | | |
3445 | 3459 | | |
3446 | 3460 | | |
| |||
0 commit comments