You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit a255a62
Browse filesBrowse the repository at this point in the historyBrowse files
fix(service-settings): a namespace reset clears the value stored beneath an env pin (#30793)
Fixes#30664
Clause-②: no
Metadata-protocol: none
## 维护者速读
-
改了什么:设置命名空间的「重置」现在也会清掉被环境变量钉住的设置项下面存着的旧值;当前生效的仍是环境变量。重置的提示会多一句,说明还有几项被环境变量钉住(十一种语言)。
- 为什么改:以前重置跳过被钉住的项,提示却说已回到环境/默认配置;哪天去掉环境变量,存着的旧值又会生效,正是 #30385 刚修过的那个坑。
- 风险与回滚:只多清了「钉住项下面的存值」,走的是 #30385
已放行的同一条清除路径,审计与变更事件和单项清除完全一样;没有钉住的项照旧。回滚即撤本 PR。
- 先看:`packages/services/service-settings/src/settings-service.ts` 的
`resetNamespaceRows`(读钉住项下面的存值)与 `runAction` 的 reset
分支;`settings-action-copy.ts` 新增的
`resetStillPinned`;`settings-service.test.ts` 末尾的新 describe。
- 两分钟验证:`pnpm --filter @objectstack/service-settings exec vitest run
src/settings-service.test.ts -t 'beneath an env pin'`,预期 5 条全绿。
- 需要你做的:无,席位落地。
## What changes
**The rule: a namespace reset clears every stored value it can reach,
the one beneath an env pin included, and says how many keys the
environment still pins.**
- **`resetNamespace` (`@objectstack/service-settings`).** It built its
patch from keys whose resolved source is a stored rung (`global` /
`tenant` / `user`). A key an `OS_*` override in force pins resolves
`env`, so a value stored beneath the pin was never cleared and took over
again once the variable was removed, while the built-in reset said
"environment/default configuration is back in effect". For a key that
resolves `env`, the reset now reads the stored cascade beneath the pin
(`resolveStored`, the half of `get()` below the env branch, extracted
rather than copied, so `get()` is unchanged) and, when that names a
stored rung, puts the key in the same all-`null` patch. The patch goes
through `setMany`, whose env-lock branch already lets a clear through a
pin since PR #30448 (ADR-0007 D6 as amended), so there is no second
bypass of the env lock: the clear meets the same row locks,
platform-rung rule and write predicates a single clear meets. Nothing in
force changes: the key still answers `{ source: 'env', locked: true }`.
A pinned key with nothing stored beneath it is not in the patch, so it
writes no row and no audit entry. The count it returns includes the
pinned keys it cleared; its signature is unchanged.
- **The reset action's answer (`settings-action-copy.ts`).**
`runAction`'s built-in `reset` keeps `resetCleared` / `resetNothing` as
they were (their wording is true now that nothing stored survives the
reset) and, when some keys are still pinned, appends one new sentence,
`resetStillPinned(count)`, in all eleven languages: "2 setting(s) remain
pinned by environment variables." Each sentence carries its own leading
space where its language separates sentences with one (none in zh-CN,
zh-Hant, ja-JP). A namespace no variable pins gets exactly the message
it got before.
- **Audit / config-change rows.** The reset of a pinned key goes through
`setMany`'s write loop like any clear, so it records exactly what a
single clear of that key records: one `reset` row on each ledger
(`newHash: null`) and one `reset` change event. Pinned in a test that
compares the two writer rows field for field.
## Tests
`packages/services/service-settings/src/settings-service.test.ts`, new
top-level describe (5 cases), on the real `SettingsService`, with one
key pinned over a stored value, one stored and not pinned, and one
pinned with nothing stored:
- the reset returns 2 (the pinned key counted), fires `reset` for those
two keys only, both pinned keys still answer env and locked, and once
the variables are removed both read their default;
- the pinned key's audit row from the reset equals the one from a single
`set(…, null)`;
- a pin with nothing stored beneath it is not cleared, and a second
reset clears nothing and writes no audit row;
- the reset action's message in English and zh-CN carries the pinned
sentence (with the count) after the cleared or nothing-to-clear answer;
- a namespace with no pins answers the old message byte for byte.
One existing case was renamed to what it pins now ("leaves an env-locked
key with nothing stored beneath it untouched"); its assertions are
unchanged. `settings-copy-locale.test.ts` gets a sample argument for
`resetStillPinned`, so its existing every-language checks cover the new
sentence.
<details><summary>Suites and typecheck (head 9b806cb)</summary>
- `pnpm --filter @objectstack/service-settings test`: Test Files 70
passed (70), Tests 1206 passed (1206). `typecheck` (`tsc --noEmit`,
includes `src/**/*.test.ts`): exit 0.
- `pnpm --filter @objectstack/plugin-auth exec vitest run
src/allowed-ip-ranges.pin.test.ts src/auth-settings-clear.pin.test.ts`
(the two callers of `resetNamespace` outside the package, after building
their closure): Test Files 2 passed, Tests 39 passed.
</details>
<details><summary>Ablations (each through `scripts/ablation-replace.mjs`
on `settings-service.ts`: anchor 1 → 0, blob changed, restore blob ==
HEAD blob, `git diff HEAD` empty; the tests import the service from
source, so no build sits between)</summary>
- A. the reset reads the resolved source only, as before (`const stored
= v.source; /* ABLATION-30664-A */`): 3 failed | 2 passed (the count and
default-after-unpin case, the audit comparison, the action message); the
two that pass pin what must not change (nothing stored beneath a pin; no
pins at all).
- B. the pinned sentence dropped (`message: answer, /* ABLATION-30664-B
*/`): 1 failed | 4 passed (the action message).
</details>
## Acceptance notes
- **A reset now meets the row locks for a pinned key too.** A pinned key
whose stored value sits beneath a locked upper-rung row is now in the
reset's patch, so `setMany` refuses the whole reset `SETTINGS_LOCKED`
(`locked-by-global` / `locked-by-tenant`) for a caller below that rung,
exactly as it already did for an unpinned key in that position. That is
the clear's own rule from PR #30448, not a new one.
- **The reset clears at the key's declared rung.** As before for
unpinned keys, the clear writes `null` at the key's own rung for this
caller; a value stored only at a higher rung the caller cannot write
stays, and the key falls back to it once the pin goes. Unchanged
behaviour, now applied to pinned keys as well.
- **Published surface.** The action's `message` is the toast text the
console prints and the HTTP action route returns; it gains one sentence
only when keys are still pinned. No export, type, error code or route
changes; `resetNamespace` keeps its signature.
- **Console.** `SettingsField` still hides "Reset to default" on a
pinned field; the namespace-level reset now reaches the value beneath
it. Nothing in `ui/` changes here.
## Verification
All readings on head `9b806cbe` (branched from `origin/main`
`1371b9d8`).
<details><summary>Derived gates: 60 derived, 59 exit 0, 1 NOT
MEASURED</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 60 commands for the 5 changed paths;
each ran with its exit code captured before any pipe. `--ran`
reconciliation (recorded as `command :: exit N`): "60 derived famil(ies)
accounted for — 59 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit
3)".
- NOT MEASURED: `pnpm check:dual-build-cjs-loads`, exit 3, PREREQUISITE
NOT MET (it needs a whole-workspace build; only the closures of
`service-settings` and `plugin-auth` were built here). CI's `Lint & Repo
Gates` carries it.
- `pnpm lint` (`eslint . --no-inline-config`): exit 0.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_016WV4cytLmJxfAmgPVjUbbW)_
Co-authored-by: Claude <noreply@anthropic.com>
fix(service-settings): a namespace reset clears the value stored beneath an env pin, so the old value does not come back when the variable is removed
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
- **The reset clears beneath a pin.** `SettingsService.resetNamespace` and the built-in `reset` settings action skipped every key an `OS_*` override in force pins, so a value stored beneath the pin survived the reset and took over again the day the variable was removed. The reset now clears that stored value too, with the same clear a single `null` write to a pinned key makes: it changes nothing in force (the key still answers the env value, `source: 'env'`, `locked: true`), it records the same `reset` audit row and change event, and once the variable is removed the key reads its default. A pinned key with nothing stored beneath it is left alone, and a key no variable pins is reset as before.
11
+
- **The count includes those keys.** `resetNamespace` returns the number of keys it cleared, pinned ones included.
12
+
- **The reset's answer says what is still pinned.** When an environment variable still pins some of the namespace's keys, the `reset` action's message adds one sentence with their number ("2 setting(s) remain pinned by environment variables."), in all eleven languages the answer is written in. A namespace no variable pins gets the same message as before.
0 commit comments