From 83621ff65972b80e4e907bb3bbbcd7bfb5fe3f36 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 21:16:52 +0000 Subject: [PATCH 1/2] test(service-settings): store decoded values in the getMany ROWS fixture, and pin the literal `settings-getmany.test.ts`'s `ROWS` fixture stored JSON TEXT in `value` (e.g. '"America/New_York"'), but `sys_setting.value` is a `Field.json` column and the service is verbatim in both directions: `setMany` writes `storedValue = rawValue`, and `materialiseRow`'s non-encrypted branch is `return row.value ?? null`. The DRIVER owns the JSON codec, and the fake engine here skips it -- so every fixture value resolved one JSON encoding deep. It was invisible because it lied CONSISTENTLY: every assertion in the file compares `getMany` against per-key `get`, and both sides are equally affected. Measured -- with the fixture untouched, the new literal-value pin fails `expected '"America/New_York"' to be 'America/New_York'` while the other 7 tests stay green. Re-encodes the fixture to the decoded values a real driver hands back (matching the sibling fixture in `settings-loadrows-scope.test.ts`, which already stores them plain), and lands the literal-value pin that holds the encoding depth mechanically. No existing assertion changed. --- .../src/settings-getmany.test.ts | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/services/service-settings/src/settings-getmany.test.ts b/packages/services/service-settings/src/settings-getmany.test.ts index db0cee8f32..70cbbaa576 100644 --- a/packages/services/service-settings/src/settings-getmany.test.ts +++ b/packages/services/service-settings/src/settings-getmany.test.ts @@ -55,10 +55,19 @@ const MANIFEST = { ], } as any; +// [#12172] Values are stored DECODED, exactly as the persist path writes them +// and as a real driver hands them back — not as JSON text. `sys_setting.value` +// is a `Field.json` column, but the SERVICE is verbatim in both directions +// (`setMany` writes `storedValue = rawValue`; `materialiseRow`'s non-encrypted +// branch is `return row.value ?? null`), so the DRIVER owns the codec and the +// round trip lands back on the raw value on both dialects. The fake engine +// below skips that codec and returns rows exactly as written, so anything +// spelled '"America/New_York"' here would resolve one JSON encoding deep. +// (Matches the sibling fixture in `settings-loadrows-scope.test.ts`.) const ROWS = [ - { namespace: 'localization', key: 'timezone', scope: 'global', value: '"America/New_York"', user_id: null }, - { namespace: 'localization', key: 'locale', scope: 'user', value: '"zh-CN"', user_id: 'u1' }, - { namespace: 'localization', key: 'currency', scope: 'tenant', value: '"USD"', user_id: null }, + { namespace: 'localization', key: 'timezone', scope: 'global', value: 'America/New_York', user_id: null }, + { namespace: 'localization', key: 'locale', scope: 'user', value: 'zh-CN', user_id: 'u1' }, + { namespace: 'localization', key: 'currency', scope: 'tenant', value: 'USD', user_id: null }, ]; async function makeService(rows = ROWS) { @@ -166,4 +175,39 @@ describe('[#10826] SettingsService.getMany', () => { await svc.getNamespace('localization', ctx); expect(engine.find.mock.calls.length).toBeLessThanOrEqual(2); }); + + // [#12172] The ENCODING pin — the one assertion in this file that reads a + // LITERAL resolved value. + // + // Every other assertion here compares `getMany` against `get`, so both sides + // move together and a fixture stored one JSON encoding deep stays invisible: + // the same "a fake that lies consistently" shape this file's header already + // warns about for `$or` matchers. That insensitivity is BY CONSTRUCTION, so + // "the suite is green" proves nothing about the encoding — only a literal can. + // + // Why the literal is the decoded string and not JSON text: `sys_setting.value` + // is a `Field.json` column, and the SERVICE is verbatim in both directions + // (`setMany` writes `storedValue = rawValue`; `materialiseRow`'s non-encrypted + // branch is `return row.value ?? null`). The DRIVER owns the JSON codec and it + // round-trips to the RAW value on both dialects — SQLite stores primitives + // as-is and re-parses on read, Postgres stringifies into the `jsonb` column and + // the client parses it back. So a row handed back by a real driver carries + // 'America/New_York', never '"America/New_York"'. The fake engine skips that + // codec, which is why the fixture must already hold the decoded value. + it('[#12172] resolves the LITERAL stored value — no doubled JSON encoding', async () => { + const { svc } = await makeService(); + const ctx = { userId: 'u1', tenantId: 't1' }; + const many = await svc.getMany('localization', ['timezone', 'locale', 'currency'], ctx); + + // Global row, user-scope key, no user/tenant override. + expect(many.timezone.value).toBe('America/New_York'); + // User row for u1. + expect(many.locale.value).toBe('zh-CN'); + // Tenant row. + expect(many.currency.value).toBe('USD'); + + // ...and the per-key path agrees on the same literal, so the pin holds the + // ENCODING rather than a `getMany`-only quirk. + expect((await svc.get('localization', 'timezone', ctx)).value).toBe('America/New_York'); + }); }); From a8c030c22a0bf5023151bba352f727a804f4356f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 21:24:19 +0000 Subject: [PATCH 2/2] test(service-settings): retire the stale encoding note the fixture fix obsoleted The comment on the non-equivalence assertions recorded the WORKAROUND: it said the assertion sits on the cascade layer because "this fixture stores JSON text in `value`". The fixture no longer does, so the note asserted a property of the file that had just stopped being true -- the exact mislead this card exists to remove. Rewrites the reason only. The assertions on those lines are byte-identical (`git diff -U0 | grep '^[+-].*expect'` is empty across this commit). --- .../service-settings/src/settings-getmany.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/services/service-settings/src/settings-getmany.test.ts b/packages/services/service-settings/src/settings-getmany.test.ts index 70cbbaa576..3b96288afe 100644 --- a/packages/services/service-settings/src/settings-getmany.test.ts +++ b/packages/services/service-settings/src/settings-getmany.test.ts @@ -156,9 +156,13 @@ describe('[#10826] SettingsService.getMany', () => { // ...and here is the non-equivalence: per-key `get()` still answers every // declared key on the same input; only the undeclared one throws. Asserted - // on the resolved cascade LAYER, not on the literal — this fixture stores - // JSON text in `value` while the service persists values verbatim, so a - // literal here would pin the fixture's encoding rather than the rule. + // on the resolved cascade LAYER because the LAYER is this test's subject — + // the rule under test is "which keys still answer", not what they answer. + // [#12172] This once read as a workaround: the fixture stored JSON text in + // `value`, so a literal here would have pinned the fixture's encoding + // rather than the rule. The fixture now stores decoded values and the + // encoding has its own pin at the bottom of this file; the assertions on + // this line are unchanged, only the reason for their shape is. expect((await svc.get('localization', 'timezone')).source).toBe('global'); expect((await svc.get('localization', 'currency')).source).toBe('tenant'); await expect(svc.get('localization', 'nope')).rejects.toBeInstanceOf(UnknownKeyError);