fix: ensure dm_scope and dimensions stay in sync across all config paths

The reviewer identified two bugs in the original PR:

1. PATCH /api/config leaves session.dimensions stale: LoadConfig()
   derives dimensions from the old dm_scope, and the merge carries
   those stale dimensions forward. ApplyDmScope() then exits early
   because dimensions is already populated, causing a mismatch between
   dm_scope (new) and dimensions (old).

2. Legacy/default configs omit dm_scope in GET response: configs with
   explicit dimensions but no dm_scope (including DefaultConfig) return
   no dm_scope field, causing the frontend to fall back to its default
   ('per-channel-peer'), which may not match the actual dimensions.

Fix:
- Add DeriveDmScope() to reverse-map known dimensions arrays to
  dm_scope when dm_scope is empty.
- Call it in LoadConfig(), PUT handler, PATCH handler, and
  ResetToDefaults() for consistent normalization.
- In PATCH handler, clear stale dimensions from the merge result when
  the patch contains session.dm_scope but not session.dimensions,
  allowing ApplyDmScope() to re-derive from the new scope.
- Add comprehensive unit tests for DeriveDmScope() and scope
  transition scenarios.
This commit is contained in:
SiYue-ZO
2026-06-11 16:00:18 +08:00
parent 921d753cc0
commit ef002d9a5d
3 changed files with 141 additions and 0 deletions
+16
View File
@@ -77,6 +77,7 @@ func (h *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
return
}
cfg.Session.ApplyDmScope()
cfg.Session.DeriveDmScope()
if execAllowRemoteOmitted(body) {
cfg.Tools.Exec.AllowRemote = config.DefaultConfig().Tools.Exec.AllowRemote
}
@@ -165,6 +166,20 @@ func (h *Handler) handlePatchConfig(w http.ResponseWriter, r *http.Request) {
// Recursively merge patch into base
mergeMap(base, patch)
// When the patch updates dm_scope, the old derived dimensions from the
// base must be cleared so that ApplyDmScope() can re-derive them from
// the new dm_scope value. Otherwise the stale dimensions survive the
// merge and ApplyDmScope() exits early due to its precedence guard.
if sess, ok := base["session"].(map[string]any); ok {
if patchSess, patchHasSession := patch["session"].(map[string]any); patchHasSession {
if _, hasDmScope := patchSess["dm_scope"]; hasDmScope {
if _, hasDimsInPatch := patchSess["dimensions"]; !hasDimsInPatch {
delete(sess, "dimensions")
}
}
}
}
if err = normalizeChannelArrayFields(base); err != nil {
http.Error(w, fmt.Sprintf("Invalid channel array field: %v", err), http.StatusBadRequest)
return
@@ -183,6 +198,7 @@ func (h *Handler) handlePatchConfig(w http.ResponseWriter, r *http.Request) {
return
}
newCfg.Session.ApplyDmScope()
newCfg.Session.DeriveDmScope()
// Restore security fields (tokens/keys) from the loaded config before validation,
// because private fields are lost during JSON round-trip.