From 3cb4927f69a2ce3f72045b8f4d6f61da221a3136 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 14:29:35 +0000 Subject: [PATCH 1/2] test(dogfood): undeclared-key driver conformance (WIP, unmeasured) Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude --- .../test/undeclared-key-conformance.test.ts | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 packages/qa/dogfood/test/undeclared-key-conformance.test.ts diff --git a/packages/qa/dogfood/test/undeclared-key-conformance.test.ts b/packages/qa/dogfood/test/undeclared-key-conformance.test.ts new file mode 100644 index 00000000000..0a82b787544 --- /dev/null +++ b/packages/qa/dogfood/test/undeclared-key-conformance.test.ts @@ -0,0 +1,220 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Undeclared-key driver conformance — a key the object does not declare is +// refused by the ENGINE, as one envelope, on every SQL driver arm this suite +// runs, whether the caller wrote it or a `before*` hook did. And the table, +// read straight past the engine, holds nothing the refused write carried. +// +// What a misspelled key costs, per writer and per driver, is the engine's and +// the drivers' fact, not an app's (#15953's classification table, row 10). +// hotcrm pinned it by hand in `test/undeclared-key-probe.test.ts` over its two +// line-item objects; this file is the platform's own pin, so that one can go. +// +// The engine runs one door, `undeclaredWriteFieldErrors` (objectql +// `engine.ts`), twice per write: over the caller's keys ahead of the hooks, +// and again over the post-hook payload ahead of the statement. Before the +// second run existed, ONE hook writing `tax_rate` onto an object that does not +// declare it meant a different thing per driver (measured on 17.1.0): the +// memory driver stored a shadow column outside field-level security, +// driver-sql threw a raw `SQLITE_ERROR` with no `status`, and sqlite-wasm a +// bare `Error` with neither. objectql pins the door against driver DOUBLES +// (`engine-post-hook-undeclared-field.test.ts`, and the caller path in +// `engine-undeclared-key-caller-path.test.ts`) and says so: it depends on no +// real driver. This file is the other half, the shipped drivers under the +// engine. +// +// The arms. The table's "memory" is the suite's in-memory SQL arm: +// `driver-memory` left this suite in #5704, and `driver-sql` on better-sqlite3 +// `:memory:` is the arm `read-coercion-conformance.test.ts` runs. It is also +// the table's "sqlite". `sqlite-wasm` is the arm +// `date-bucket-parity-conformance.test.ts` runs beside it. +// +// The envelope is read off the engine, never rebuilt here. The caller path is +// pinned to the code and status the table names, `INVALID_FIELD` / 400, which +// is what both arms answer today. The hook path must answer the SAME envelope +// the caller path answers on the same arm, compared field for field. Two +// controls keep a red-free run from measuring nothing: a declared key +// round-trips, and the same hook DOES store the same key on an object that +// declares it, so a refusal can never be "hooks never ran on this arm". + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { ObjectQL } from '@objectstack/objectql'; +import { SqlDriver } from '@objectstack/driver-sql'; +import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm'; + +const DRIVERS = [ + { + name: 'driver-sql (better-sqlite3 :memory:)', + make: (): SqlDriver => + new SqlDriver({ + client: 'better-sqlite3', + connection: { filename: ':memory:' }, + useNullAsDefault: true, + }), + }, + { + name: 'driver-sqlite-wasm (:memory:)', + make: (): SqlDriver => new SqliteWasmDriver({ filename: ':memory:' }), + }, +]; + +const SYS = { context: { isSystem: true, userId: 'usr_system' } } as never; + +/** hotcrm's pair: `probe_quote` declares the key, `probe_quote_line` does not. */ +const KEY = 'tax_rate'; +const QUOTE = 'probe_quote'; +const LINE = 'probe_quote_line'; + +/** What the probe hook stamps onto a `probe_quote_line` payload: a key that object does not declare. */ +const LINE_HOOK_WRITES = { [KEY]: 7 }; +/** What the same hook stamps onto a `probe_quote` payload: the same key, declared there. */ +const QUOTE_HOOK_WRITES = { [KEY]: 7 }; + +/** The caller path's answer, as the table names it and as both arms answer it today. */ +const CALLER_PATH = { code: 'INVALID_FIELD', status: 400 }; + +const APP = { + id: 'com.objectstack.dogfood.undeclared-key-conformance', + name: 'Undeclared key conformance', + version: '1.0.0', + type: 'plugin', + scope: 'system', + objects: [ + { + name: QUOTE, + label: 'Probe quote', + fields: { + id: { name: 'id', type: 'text', primaryKey: true }, + name: { name: 'name', type: 'text' }, + [KEY]: { name: KEY, type: 'number' }, + }, + }, + { + name: LINE, + label: 'Probe quote line', + fields: { + id: { name: 'id', type: 'text', primaryKey: true }, + name: { name: 'name', type: 'text' }, + unit_price: { name: 'unit_price', type: 'number' }, + }, + }, + ], +}; + +/** The fields of a refusal a caller reads. Projected from the engine's error, never constructed. */ +interface Refusal { + code?: unknown; + status?: unknown; + field?: unknown; + object?: unknown; +} +const envelope = (e: Refusal): Refusal => ({ code: e.code, status: e.status, field: e.field, object: e.object }); + +async function refusalOf(run: () => Promise): Promise { + try { + await run(); + } catch (e) { + return e as Refusal; + } + throw new Error('expected the engine to refuse, and it answered'); +} + +/** A `beforeInsert` + `beforeUpdate` hook on `object` that stamps `writes` onto the payload. */ +function armStampHook(engine: ObjectQL, object: string, writes: Record): void { + const stamp = (ctx: { input: { data: Record } }) => { + Object.assign(ctx.input.data, writes); + }; + engine.registerHook('beforeInsert', stamp as never, { object }); + engine.registerHook('beforeUpdate', stamp as never, { object }); +} + +describe.each(DRIVERS)('undeclared-key conformance: $name', ({ make }) => { + let driver: SqlDriver; + let engine: ObjectQL; + + beforeEach(async () => { + driver = make(); + engine = new ObjectQL(); + engine.registerDriver(driver, true); + await engine.init(); + engine.registerApp(APP as never); + await engine.syncSchemas(); + }); + + afterEach(async () => { + await engine.destroy(); + }); + + /** Ground truth, read off the table through knex: past every engine layer. */ + async function stored(object: string, column: string): Promise { + const knex = (driver as unknown as { + knex: (t: string) => { select: (...c: string[]) => Promise>> }; + }).knex; + const rows = await knex(object).select('id', 'name', column); + return rows.map((r) => `${String(r.id)}:${String(r.name)}:${String(r[column])}`).sort(); + } + + it('control: a declared key round-trips through insert, update and read', async () => { + await engine.insert(QUOTE, { id: 'q1', name: 'Q', [KEY]: 10 }, SYS); + expect((await engine.findOne(QUOTE, { where: { id: 'q1' } } as never, SYS))?.[KEY]).toBe(10); + + await engine.update(QUOTE, { id: 'q1', [KEY]: 12 }, SYS); + expect((await engine.findOne(QUOTE, { where: { id: 'q1' } } as never, SYS))?.[KEY]).toBe(12); + expect(await stored(QUOTE, KEY)).toEqual(['q1:Q:12']); + }); + + it('control: the same hook stamping the same key on an object that DECLARES it is stored', async () => { + armStampHook(engine, QUOTE, QUOTE_HOOK_WRITES); + + await engine.insert(QUOTE, { id: 'q1', name: 'Q' }, SYS); + + expect(await stored(QUOTE, KEY)).toEqual(['q1:Q:7']); + }); + + it(`caller path: an insert carrying an undeclared key is ${CALLER_PATH.code} / ${CALLER_PATH.status}, and nothing is stored`, async () => { + const err = await refusalOf(() => engine.insert(LINE, { id: 'l1', name: 'L', unit_price: 1, [KEY]: 5 }, SYS)); + + expect(envelope(err)).toEqual({ ...CALLER_PATH, field: KEY, object: LINE }); + expect(await stored(LINE, 'unit_price')).toEqual([]); + }); + + it(`caller path: an update carrying an undeclared key is ${CALLER_PATH.code} / ${CALLER_PATH.status}, and the row is unchanged`, async () => { + await engine.insert(LINE, { id: 'l1', name: 'L', unit_price: 100 }, SYS); + + const err = await refusalOf(() => engine.update(LINE, { id: 'l1', unit_price: 200, [KEY]: 5 }, SYS)); + + expect(envelope(err)).toEqual({ ...CALLER_PATH, field: KEY, object: LINE }); + expect(await stored(LINE, 'unit_price')).toEqual(['l1:L:100']); + }); + + it('hook path: a beforeInsert-written undeclared key is refused as the caller-path envelope, and nothing is stored', async () => { + const callerAnswer = envelope( + await refusalOf(() => engine.insert(LINE, { id: 'c1', name: 'C', unit_price: 1, [KEY]: 5 }, SYS)), + ); + armStampHook(engine, LINE, LINE_HOOK_WRITES); + + const err = await refusalOf(() => engine.insert(LINE, { id: 'l1', name: 'L', unit_price: 1 }, SYS)); + + expect(err).toMatchObject(CALLER_PATH); + expect(envelope(err)).toEqual(callerAnswer); + expect(await stored(LINE, 'unit_price')).toEqual([]); + }); + + it('hook path: a beforeUpdate-written undeclared key is refused the same way, by id and by predicate, and no row changes', async () => { + await engine.insert(LINE, { id: 'l1', name: 'L', unit_price: 100 }, SYS); + await engine.insert(LINE, { id: 'l2', name: 'L', unit_price: 100 }, SYS); + const callerAnswer = envelope(await refusalOf(() => engine.update(LINE, { id: 'l1', [KEY]: 5 }, SYS))); + armStampHook(engine, LINE, LINE_HOOK_WRITES); + + const byId = await refusalOf(() => engine.update(LINE, { id: 'l1', unit_price: 200 }, SYS)); + const byPredicate = await refusalOf(() => + engine.update(LINE, { unit_price: 300 }, { ...(SYS as object), where: { name: 'L' }, multi: true } as never), + ); + + expect(byId).toMatchObject(CALLER_PATH); + expect(envelope(byId)).toEqual(callerAnswer); + expect(byPredicate).toMatchObject(CALLER_PATH); + expect(envelope(byPredicate)).toEqual(callerAnswer); + expect(await stored(LINE, 'unit_price')).toEqual(['l1:L:100', 'l2:L:100']); + }); +}); From 9934b1d86c7fd50bdf060ecff7878719d72af1ce Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 14:55:32 +0000 Subject: [PATCH 2/2] test(dogfood): pin the undeclared-key refusal on the suite's SQL driver arms A key the object does not declare, written by the caller or stamped by a before-hook, is refused by the engine as one envelope (INVALID_FIELD / 400) on driver-sql (better-sqlite3 :memory:) and driver-sqlite-wasm (:memory:), and the table read past the engine holds nothing the refused write carried. Controls: a declared key round-trips, and the same hook stores the same key on an object that declares it. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude --- .../test/undeclared-key-conformance.test.ts | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/qa/dogfood/test/undeclared-key-conformance.test.ts b/packages/qa/dogfood/test/undeclared-key-conformance.test.ts index 0a82b787544..878a1b97a7c 100644 --- a/packages/qa/dogfood/test/undeclared-key-conformance.test.ts +++ b/packages/qa/dogfood/test/undeclared-key-conformance.test.ts @@ -14,19 +14,19 @@ // `engine.ts`), twice per write: over the caller's keys ahead of the hooks, // and again over the post-hook payload ahead of the statement. Before the // second run existed, ONE hook writing `tax_rate` onto an object that does not -// declare it meant a different thing per driver (measured on 17.1.0): the -// memory driver stored a shadow column outside field-level security, -// driver-sql threw a raw `SQLITE_ERROR` with no `status`, and sqlite-wasm a -// bare `Error` with neither. objectql pins the door against driver DOUBLES -// (`engine-post-hook-undeclared-field.test.ts`, and the caller path in -// `engine-undeclared-key-caller-path.test.ts`) and says so: it depends on no -// real driver. This file is the other half, the shipped drivers under the -// engine. +// declare it meant a different thing per driver (objectql's suite records the +// 17.1.0 reading): the memory driver stored a shadow column outside +// field-level security, driver-sql threw a raw `SQLITE_ERROR` with no +// `status`, and sqlite-wasm a bare `Error` with neither. objectql pins the +// door against driver DOUBLES (`engine-post-hook-undeclared-field.test.ts`, +// and the caller path in `engine-undeclared-key-caller-path.test.ts`) and +// says so: it depends on no real driver. This file is the other half, the +// shipped drivers under the engine. // -// The arms. The table's "memory" is the suite's in-memory SQL arm: -// `driver-memory` left this suite in #5704, and `driver-sql` on better-sqlite3 -// `:memory:` is the arm `read-coercion-conformance.test.ts` runs. It is also -// the table's "sqlite". `sqlite-wasm` is the arm +// The arms. The table's "memory" is the suite's in-memory SQL arm: the +// `driver-memory` arm left the conformance files in #5704, and `driver-sql` on +// better-sqlite3 `:memory:` is the arm `read-coercion-conformance.test.ts` +// runs. It is also the table's "sqlite". `sqlite-wasm` is the arm // `date-bucket-parity-conformance.test.ts` runs beside it. // // The envelope is read off the engine, never rebuilt here. The caller path is @@ -58,7 +58,7 @@ const DRIVERS = [ }, ]; -const SYS = { context: { isSystem: true, userId: 'usr_system' } } as never; +const SYS = { context: { isSystem: true, userId: 'usr_system' } }; /** hotcrm's pair: `probe_quote` declares the key, `probe_quote_line` does not. */ const KEY = 'tax_rate'; @@ -121,11 +121,11 @@ async function refusalOf(run: () => Promise): Promise { /** A `beforeInsert` + `beforeUpdate` hook on `object` that stamps `writes` onto the payload. */ function armStampHook(engine: ObjectQL, object: string, writes: Record): void { - const stamp = (ctx: { input: { data: Record } }) => { - Object.assign(ctx.input.data, writes); + const stamp = (ctx: { input: Record }) => { + Object.assign(ctx.input.data as Record, writes); }; - engine.registerHook('beforeInsert', stamp as never, { object }); - engine.registerHook('beforeUpdate', stamp as never, { object }); + engine.registerHook('beforeInsert', stamp, { object }); + engine.registerHook('beforeUpdate', stamp, { object }); } describe.each(DRIVERS)('undeclared-key conformance: $name', ({ make }) => { @@ -137,7 +137,7 @@ describe.each(DRIVERS)('undeclared-key conformance: $name', ({ make }) => { engine = new ObjectQL(); engine.registerDriver(driver, true); await engine.init(); - engine.registerApp(APP as never); + engine.registerApp(APP); await engine.syncSchemas(); }); @@ -156,10 +156,10 @@ describe.each(DRIVERS)('undeclared-key conformance: $name', ({ make }) => { it('control: a declared key round-trips through insert, update and read', async () => { await engine.insert(QUOTE, { id: 'q1', name: 'Q', [KEY]: 10 }, SYS); - expect((await engine.findOne(QUOTE, { where: { id: 'q1' } } as never, SYS))?.[KEY]).toBe(10); + expect((await engine.findOne(QUOTE, { where: { id: 'q1' } }, SYS))?.[KEY]).toBe(10); await engine.update(QUOTE, { id: 'q1', [KEY]: 12 }, SYS); - expect((await engine.findOne(QUOTE, { where: { id: 'q1' } } as never, SYS))?.[KEY]).toBe(12); + expect((await engine.findOne(QUOTE, { where: { id: 'q1' } }, SYS))?.[KEY]).toBe(12); expect(await stored(QUOTE, KEY)).toEqual(['q1:Q:12']); }); @@ -208,7 +208,7 @@ describe.each(DRIVERS)('undeclared-key conformance: $name', ({ make }) => { const byId = await refusalOf(() => engine.update(LINE, { id: 'l1', unit_price: 200 }, SYS)); const byPredicate = await refusalOf(() => - engine.update(LINE, { unit_price: 300 }, { ...(SYS as object), where: { name: 'L' }, multi: true } as never), + engine.update(LINE, { unit_price: 300 }, { ...SYS, where: { name: 'L' }, multi: true }), ); expect(byId).toMatchObject(CALLER_PATH);