diff --git a/CHANGELOG.md b/CHANGELOG.md index 23f967fb..ba713af0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Fixed +- Hash-keyed settings (per-workspace PATH presets, Claude account bindings) + with a number-shaped hash suffix were silently lost on restart — YAML map + keys are now quoted on write. + ## [2.9.0] — 2026-06-30 ### Added diff --git a/lib/kernel/src/settings.dart b/lib/kernel/src/settings.dart index ce7065fd..a5f8435c 100644 --- a/lib/kernel/src/settings.dart +++ b/lib/kernel/src/settings.dart @@ -282,7 +282,14 @@ void _emit(StringBuffer buf, Object? v, int indent) { return; } v.forEach((k, vv) { - buf.write('$pad$k:'); + // Keys go through the same quoting as values: a numeric-shaped key — + // e.g. an all-digit FNV workspace-hash suffix (`app.env.pathPrepend. + // 0123…`, `app.claude.account.`) — would otherwise reload as an + // int/float and silently corrupt the key (leading zero dropped, or + // `1e…` collapsing to Infinity), orphaning the stored value. + buf.write(pad); + _emitScalar(buf, '$k'); + buf.write(':'); if (vv is Map && vv.isNotEmpty) { buf.writeln(); _emit(buf, vv, indent + 1); diff --git a/test/kernel/src/settings_test.dart b/test/kernel/src/settings_test.dart index ed633c58..1e5dede7 100644 --- a/test/kernel/src/settings_test.dart +++ b/test/kernel/src/settings_test.dart @@ -37,6 +37,20 @@ void main() { loaded.dispose(); }); + test('numeric-shaped key segments round-trip unmangled (workspace-hash keys)', () async { + // FNV workspace-hash suffixes (app.env.pathPrepend., the account + // bindings) can be number-shaped; an unquoted YAML key would reload as + // an int (leading zero dropped) or a float ('1e…' → Infinity) and + // silently orphan the stored value. + await store.set>('app.env.pathPrepend.0123456789012345', const ['/opt/go/bin']); + await store.set('app.claude.account.1e23456789012345', 'work'); + final loaded = SettingsStore(appDir: tmp); + await loaded.load(); + expect(loaded.get>('app.env.pathPrepend.0123456789012345'), ['/opt/go/bin']); + expect(loaded.get('app.claude.account.1e23456789012345'), 'work'); + loaded.dispose(); + }); + test('app.* scope supports bool + int + list', () async { await store.set('app.extensions.git.enabled', false); await store.set('app.layout.width', 240);