Skip to content

Commit c2cf2da

Browse files
committed
test(plugin-hono-server): make the localization pin's ObjectQL double honour where and the caller's bound
The double answered by object name and dropped `opts.limit`, so neither read it serves could tell a bounded read from an unbounded one — and both carry a bound (`sys_user` at 1, the grouped `sys_setting` `$in` read at 10). It now draws from a row table, filters with a `where` matcher that refuses any operator it does not implement, and applies the bound BY PRESENCE (`typeof opts?.limit === 'number'`) AFTER the filter. `check:objectql-double-limit` could not grade the old double at all: its deepest binding strategy stubs every non-function declaration, the counter `let sysUserReads = 0` became the gate's row-stub Proxy, and `++sysUserReads` raised `TypeError: Cannot convert object to primitive value` — reported as UNJUDGED, which the gate treats as debt rather than a skip. The double now seats on the gate's control probe at the earlier binding strategy, so it is graded CONFORMING (limit 3 -> 3 rows, 5 -> 5, 0 -> 0 of 7 matches) instead of throwing. No baseline entry was added; the ledger never grows. What the file ASSERTS is unchanged — all 13 cases pass, including the corrected `timezone`/`currency` contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
1 parent 18588cd commit c2cf2da

1 file changed

Lines changed: 53 additions & 18 deletions

File tree

‎packages/plugins/plugin-hono-server/src/current-user-endpoints-localization.test.ts‎

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -94,30 +94,65 @@ interface MountOptions {
9494
failUserRead?: boolean;
9595
}
9696

97+
/**
98+
* The `where` shapes these endpoints actually issue: scalar equality
99+
* (`{ id }`, `{ namespace }`, `{ scope }`) and the one `$in` list on `key` the
100+
* grouped settings read carries. Anything else — a combinator, another
101+
* operator — is REFUSED loudly rather than answered silently wrong, which is
102+
* the cheap correct answer for a double that never sees one (the defect class
103+
* `check:where-matcher` exists for is silence, not incompleteness).
104+
*/
105+
function matchesWhere(row: Row, where: Row | undefined): boolean {
106+
for (const [key, condition] of Object.entries(where ?? {})) {
107+
if (key.startsWith('$')) {
108+
throw new Error(`this double does not implement the where operator ${key}`);
109+
}
110+
if (condition !== null && typeof condition === 'object') {
111+
const operators = Object.keys(condition);
112+
if (operators.length !== 1 || operators[0] !== '$in') {
113+
throw new Error(`this double does not implement the where operator(s) ${operators.join(', ')} on ${key}`);
114+
}
115+
if (!condition.$in.includes(row[key])) return false;
116+
continue;
117+
}
118+
if (row[key] !== condition) return false;
119+
}
120+
return true;
121+
}
122+
97123
function mount({ storedLocale, rules = [LOCALE_SHAPE_RULE], settingLocale, settingTimezone, settingCurrency, authenticated = true, failUserRead = false }: MountOptions = {}) {
98124
const reads: Array<{ object: string; opts: any }> = [];
99125
let sysUserReads = 0;
126+
/**
127+
* The rows each read draws from, held as a table rather than assembled per
128+
* branch, so the double answers `where` and the caller's `limit` the way
129+
* the engine does instead of by object name.
130+
*/
131+
const tables: Record<string, Row[]> = {
132+
sys_user: [{ id: USER, email: 'lang@example.com', locale: storedLocale }],
133+
// The endpoint reads all three `localization` keys in ONE `$in` query,
134+
// so the table carries whichever of them the fixture configured — and
135+
// nothing for the rest.
136+
sys_setting: ([
137+
['locale', settingLocale],
138+
['timezone', settingTimezone],
139+
['currency', settingCurrency],
140+
] as Array<[string, string | undefined]>)
141+
.filter(([, value]) => value !== undefined)
142+
.map(([key, value]) => ({ namespace: 'localization', key, value, scope: 'tenant' })),
143+
};
100144
const ql = {
101145
find: async (object: string, opts: any) => {
102146
reads.push({ object, opts });
103-
if (object === 'sys_user') {
104-
if (failUserRead && ++sysUserReads > 1) throw new Error('sys_user unavailable');
105-
return opts?.where?.id === USER ? [{ id: USER, email: 'lang@example.com', locale: storedLocale }] : [];
106-
}
107-
if (object === 'sys_setting') {
108-
// The endpoint reads all three `localization` keys in ONE `$in`
109-
// query, so the double answers whichever of them the fixture
110-
// configured — and nothing for the rest.
111-
const configured: Array<[string, string | undefined]> = [
112-
['locale', settingLocale],
113-
['timezone', settingTimezone],
114-
['currency', settingCurrency],
115-
];
116-
return configured
117-
.filter(([, value]) => value !== undefined)
118-
.map(([key, value]) => ({ namespace: 'localization', key, value, scope: 'tenant' }));
119-
}
120-
return [];
147+
if (object === 'sys_user' && failUserRead && ++sysUserReads > 1) throw new Error('sys_user unavailable');
148+
// The caller's bound is applied BY PRESENCE and AFTER the filter.
149+
// Both reads this double serves carry one — `sys_user` at 1, the
150+
// grouped `sys_setting` read at 10 — so a double that dropped it
151+
// could not tell a bounded read from an unbounded one, and every
152+
// bound change on those reads would be green by construction
153+
// (`check:objectql-double-limit`).
154+
const matched = (tables[object] ?? []).filter((row) => matchesWhere(row, opts?.where));
155+
return typeof opts?.limit === 'number' ? matched.slice(0, opts.limit) : matched;
121156
},
122157
// The registry view the endpoint reads the column's rule off.
123158
getSchema: (name: string) => (name === 'sys_user' && rules !== null ? { name: 'sys_user', validations: rules } : undefined),

0 commit comments

Comments
 (0)