From 705db0254286b90d180addd12273918a97d6f6cb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 04:02:14 +0000 Subject: [PATCH 1/4] test(service-datasource): pin cross-driver read-coercion conformance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A stored value must read back as its declared type on every driver: a boolean as a JS boolean (not the integer 0/1 SQLite stores), a json field as an object/array (not serialized text), an integer as a number. When drivers disagree, code green on one silently breaks on another. This is the invariant behind the 2026-07-06 case_escalation incident (a boolean guard `field != true` read the field back as integer `1` on Turso, so `1 != true` was always true and the flow self-triggered forever, while the memory/better-sqlite3 repro was green). The storage-specific fixes live in each driver; this suite pins the shared contract so no framework driver — present or future — can reintroduce the gap. Runs against driver-sql (better-sqlite3) and driver-memory. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_015EsQntpH9mUZqG7LAWzPAK --- .../src/read-coercion-conformance.test.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 packages/services/service-datasource/src/read-coercion-conformance.test.ts diff --git a/packages/services/service-datasource/src/read-coercion-conformance.test.ts b/packages/services/service-datasource/src/read-coercion-conformance.test.ts new file mode 100644 index 0000000000..28fe33290f --- /dev/null +++ b/packages/services/service-datasource/src/read-coercion-conformance.test.ts @@ -0,0 +1,80 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Driver read-coercion conformance. +// +// A stored value must read back as its DECLARED type on every driver: a +// `boolean` as a JS boolean (not the integer 0/1 that SQLite stores), a `json` +// field as an object/array (not its serialized text), an `integer` as a number. +// When two drivers disagree, code that is green on one silently breaks on the +// other. +// +// This is the invariant behind the 2026-07-06 `case_escalation` incident: a +// boolean guard `field != true` on Turso read the field back as integer `1`, so +// `1 != true` was always true and the flow self-triggered forever — while the +// local repro (memory / better-sqlite3, both of which coerce) was green. The +// storage-representation-specific fix lives in each driver (cloud's driver-turso +// remote path, driver-sql's `formatOutput`); this suite pins the shared contract +// so no framework driver — present or future — can reintroduce the gap. + +import { describe, it, expect } from 'vitest'; +import { SqlDriver } from '@objectstack/driver-sql'; +import { InMemoryDriver } from '@objectstack/driver-memory'; + +const OBJECT = 'coercion_widgets'; + +const SCHEMA = { + name: OBJECT, + fields: { + name: { type: 'string' }, + active: { type: 'boolean' }, + meta: { type: 'json' }, + count: { type: 'integer' }, + }, +}; + +const INPUT = { id: '1', name: 'Widget', active: true, meta: { k: 1, arr: [1, 2] }, count: 5 }; + +type DriverFactory = () => { name: string; driver: any }; + +const DRIVERS: DriverFactory[] = [ + () => ({ + name: 'driver-sql (better-sqlite3 :memory:)', + driver: new SqlDriver({ + client: 'better-sqlite3', + connection: { filename: ':memory:' }, + useNullAsDefault: true, + }), + }), + () => ({ + name: 'driver-memory', + driver: new InMemoryDriver(), + }), +]; + +describe.each(DRIVERS.map((f) => f()))('read-coercion conformance: $name', ({ driver }) => { + it('reads a stored row back as its declared types (boolean/json/number)', async () => { + await driver.connect(); + await driver.syncSchema(OBJECT, SCHEMA); + await driver.create(OBJECT, { ...INPUT }); + + const rows = await driver.find(OBJECT, { object: OBJECT }); + expect(rows).toHaveLength(1); + const row = rows[0]; + + // boolean stays a real boolean — NOT the integer 1 SQLite stores on disk. + expect(typeof row.active).toBe('boolean'); + expect(row.active).toBe(true); + // The exact guard the case_escalation flow used: must be `false`, not `true`. + expect(row.active !== true).toBe(false); + + // json reads back as a structured value, not its serialized text. + expect(typeof row.meta).toBe('object'); + expect(row.meta).toEqual({ k: 1, arr: [1, 2] }); + + // integer stays a number. + expect(typeof row.count).toBe('number'); + expect(row.count).toBe(5); + + await driver.disconnect?.(); + }); +}); From fb9346b26dc786552275395996f07a2f825466ae Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 04:04:05 +0000 Subject: [PATCH 2/4] chore: add empty changeset (test-only, no version bump) Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_015EsQntpH9mUZqG7LAWzPAK --- .changeset/read-coercion-conformance.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changeset/read-coercion-conformance.md diff --git a/.changeset/read-coercion-conformance.md b/.changeset/read-coercion-conformance.md new file mode 100644 index 0000000000..0c4cb84c60 --- /dev/null +++ b/.changeset/read-coercion-conformance.md @@ -0,0 +1,4 @@ +--- +--- + +test(service-datasource): pin cross-driver read-coercion conformance. Test-only — no package version change. From 1988e6400b46bd19e23b2f691eaf5b9c27984cb2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 04:48:32 +0000 Subject: [PATCH 3/4] feat(verify): add reusable checkReadCoercion driver conformance helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote the read-coercion contract from a service-datasource-local test into a reusable, driver-agnostic export in @objectstack/verify (mirrors checkLedger: returns a problems list, empty = conformant, no test-runner dependency). Any driver — including out-of-tree ones like cloud's driver-turso in remote mode — can now run the identical contract against itself. - verify: new checkReadCoercion(driver) + CoercibleDriver/ReadCoercionOptions. - service-datasource: the cross-driver conformance test now consumes the shared helper (driver-sql better-sqlite3 + driver-memory), asserting `toEqual([])`. - changeset: minor for @objectstack/verify (new public API). Verified: service-datasource conformance 2 passed; helper flags a raw non-coercing driver with 3 problems (boolean 1 / json text / numeric string); verify build incl. DTS clean. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_015EsQntpH9mUZqG7LAWzPAK --- .changeset/read-coercion-conformance.md | 9 +- .../services/service-datasource/package.json | 1 + .../src/read-coercion-conformance.test.ts | 84 +++++----------- packages/verify/src/index.ts | 6 ++ packages/verify/src/read-coercion.ts | 97 +++++++++++++++++++ pnpm-lock.yaml | 3 + 6 files changed, 140 insertions(+), 60 deletions(-) create mode 100644 packages/verify/src/read-coercion.ts diff --git a/.changeset/read-coercion-conformance.md b/.changeset/read-coercion-conformance.md index 0c4cb84c60..97e5bf475b 100644 --- a/.changeset/read-coercion-conformance.md +++ b/.changeset/read-coercion-conformance.md @@ -1,4 +1,11 @@ --- +"@objectstack/verify": minor --- -test(service-datasource): pin cross-driver read-coercion conformance. Test-only — no package version change. +Add `checkReadCoercion` — a reusable, driver-agnostic read-coercion conformance +helper (a stored value must read back as its declared type: boolean as boolean, +json as object, integer as number). Mirrors `checkLedger`: returns a list of +problems (empty = conformant) with no test-runner dependency, so any driver — +including out-of-tree ones like cloud's driver-turso — can run the identical +contract against itself. This is the invariant behind the case_escalation +`1 != true` incident. diff --git a/packages/services/service-datasource/package.json b/packages/services/service-datasource/package.json index d43baea575..33b7978732 100644 --- a/packages/services/service-datasource/package.json +++ b/packages/services/service-datasource/package.json @@ -35,6 +35,7 @@ "@objectstack/driver-sql": "workspace:*", "@objectstack/driver-sqlite-wasm": "workspace:*", "@objectstack/plugin-hono-server": "workspace:*", + "@objectstack/verify": "workspace:*", "@types/node": "^26.1.0", "tsup": "^8.5.1", "typescript": "^6.0.3", diff --git a/packages/services/service-datasource/src/read-coercion-conformance.test.ts b/packages/services/service-datasource/src/read-coercion-conformance.test.ts index 28fe33290f..e15a040062 100644 --- a/packages/services/service-datasource/src/read-coercion-conformance.test.ts +++ b/packages/services/service-datasource/src/read-coercion-conformance.test.ts @@ -3,78 +3,44 @@ // Driver read-coercion conformance. // // A stored value must read back as its DECLARED type on every driver: a -// `boolean` as a JS boolean (not the integer 0/1 that SQLite stores), a `json` -// field as an object/array (not its serialized text), an `integer` as a number. -// When two drivers disagree, code that is green on one silently breaks on the -// other. +// `boolean` as a JS boolean (not the integer 0/1 SQLite stores), a `json` field +// as an object/array (not serialized text), an `integer` as a number. When two +// drivers disagree, code that is green on one silently breaks on the other. // // This is the invariant behind the 2026-07-06 `case_escalation` incident: a // boolean guard `field != true` on Turso read the field back as integer `1`, so // `1 != true` was always true and the flow self-triggered forever — while the -// local repro (memory / better-sqlite3, both of which coerce) was green. The -// storage-representation-specific fix lives in each driver (cloud's driver-turso -// remote path, driver-sql's `formatOutput`); this suite pins the shared contract -// so no framework driver — present or future — can reintroduce the gap. +// local repro (memory / better-sqlite3, both of which coerce) was green in 6s. +// +// The check itself is the reusable, driver-agnostic `checkReadCoercion` from +// `@objectstack/verify` (empty problem list = conformant). This suite runs it +// against the framework's own SQL + memory drivers; cloud's driver-turso runs +// the identical contract against itself in remote mode. import { describe, it, expect } from 'vitest'; +import { checkReadCoercion } from '@objectstack/verify'; import { SqlDriver } from '@objectstack/driver-sql'; import { InMemoryDriver } from '@objectstack/driver-memory'; -const OBJECT = 'coercion_widgets'; - -const SCHEMA = { - name: OBJECT, - fields: { - name: { type: 'string' }, - active: { type: 'boolean' }, - meta: { type: 'json' }, - count: { type: 'integer' }, - }, -}; - -const INPUT = { id: '1', name: 'Widget', active: true, meta: { k: 1, arr: [1, 2] }, count: 5 }; - -type DriverFactory = () => { name: string; driver: any }; - -const DRIVERS: DriverFactory[] = [ - () => ({ +const DRIVERS = [ + { name: 'driver-sql (better-sqlite3 :memory:)', - driver: new SqlDriver({ - client: 'better-sqlite3', - connection: { filename: ':memory:' }, - useNullAsDefault: true, - }), - }), - () => ({ + make: () => + new SqlDriver({ + client: 'better-sqlite3', + connection: { filename: ':memory:' }, + useNullAsDefault: true, + }), + }, + { name: 'driver-memory', - driver: new InMemoryDriver(), - }), + make: () => new InMemoryDriver(), + }, ]; -describe.each(DRIVERS.map((f) => f()))('read-coercion conformance: $name', ({ driver }) => { +describe.each(DRIVERS)('read-coercion conformance: $name', ({ make }) => { it('reads a stored row back as its declared types (boolean/json/number)', async () => { - await driver.connect(); - await driver.syncSchema(OBJECT, SCHEMA); - await driver.create(OBJECT, { ...INPUT }); - - const rows = await driver.find(OBJECT, { object: OBJECT }); - expect(rows).toHaveLength(1); - const row = rows[0]; - - // boolean stays a real boolean — NOT the integer 1 SQLite stores on disk. - expect(typeof row.active).toBe('boolean'); - expect(row.active).toBe(true); - // The exact guard the case_escalation flow used: must be `false`, not `true`. - expect(row.active !== true).toBe(false); - - // json reads back as a structured value, not its serialized text. - expect(typeof row.meta).toBe('object'); - expect(row.meta).toEqual({ k: 1, arr: [1, 2] }); - - // integer stays a number. - expect(typeof row.count).toBe('number'); - expect(row.count).toBe(5); - - await driver.disconnect?.(); + const problems = await checkReadCoercion(make() as any); + expect(problems).toEqual([]); }); }); diff --git a/packages/verify/src/index.ts b/packages/verify/src/index.ts index 96af85216e..775f82181d 100644 --- a/packages/verify/src/index.ts +++ b/packages/verify/src/index.ts @@ -23,3 +23,9 @@ export type { RlsReport, RlsResult } from './rls.js'; // runtime harness): classify every declarable property, fail closed on drift. export { checkLedger } from './conformance.js'; export type { ConformanceRow, ConformanceState, CheckLedgerOptions } from './conformance.js'; + +// Driver read-coercion conformance: a stored value must read back as its +// declared type on every driver (the case_escalation `1 != true` invariant). +// Driver-agnostic — any driver, including out-of-tree ones, runs the same check. +export { checkReadCoercion } from './read-coercion.js'; +export type { CoercibleDriver, ReadCoercionOptions } from './read-coercion.js'; diff --git a/packages/verify/src/read-coercion.ts b/packages/verify/src/read-coercion.ts new file mode 100644 index 0000000000..667c87c9ba --- /dev/null +++ b/packages/verify/src/read-coercion.ts @@ -0,0 +1,97 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Driver read-coercion conformance — a reusable, driver-agnostic check. + * + * A stored value must read back as its DECLARED type on every driver: a + * `boolean` as a JS boolean (not the integer 0/1 SQLite stores), a `json` field + * as an object/array (not serialized text), an `integer` as a number. When two + * drivers disagree, code that is green on one silently breaks on the other. + * + * This is the invariant behind the 2026-07-06 `case_escalation` incident: a + * boolean guard `field != true` read the field back as integer `1` on Turso, so + * `1 != true` was always true and the flow self-triggered forever — while the + * local repro (memory / better-sqlite3, both of which coerce) was green. + * + * Like {@link checkLedger}, this returns a list of human-readable problems + * (empty = conformant) and carries NO test-runner dependency — callers assert + * `expect(await checkReadCoercion(driver)).toEqual([])`. It takes any driver + * structurally (see {@link CoercibleDriver}) so out-of-tree drivers — e.g. + * cloud's `driver-turso` in remote mode — can run the identical contract against + * themselves without importing a concrete driver type. + */ + +/** The minimal driver surface this check drives. */ +export interface CoercibleDriver { + connect?(): Promise; + disconnect?(): Promise; + syncSchema(object: string, schema: unknown, options?: unknown): Promise; + create(object: string, data: Record, options?: unknown): Promise; + find(object: string, query: unknown, options?: unknown): Promise; +} + +export interface ReadCoercionOptions { + /** Object/table name to create for the probe. Default `read_coercion_probe`. */ + object?: string; +} + +const FIELDS = { + name: { type: 'string' }, + active: { type: 'boolean' }, + meta: { type: 'json' }, + count: { type: 'integer' }, +} as const; + +const INPUT = { id: '1', name: 'Widget', active: true, meta: { k: 1, arr: [1, 2] }, count: 5 }; + +function stableStringify(v: unknown): string { + // Order-insensitive for plain objects so a driver that reorders JSON keys on + // round-trip is not falsely flagged; arrays keep their order. + const norm = (x: any): any => { + if (Array.isArray(x)) return x.map(norm); + if (x && typeof x === 'object') { + return Object.keys(x).sort().reduce((o: any, k) => ((o[k] = norm(x[k])), o), {}); + } + return x; + }; + return JSON.stringify(norm(v)); +} + +/** + * Round-trip a typed row through `driver` and report every field whose read-back + * value does not match its declared type. Empty array = conformant. + */ +export async function checkReadCoercion( + driver: CoercibleDriver, + opts: ReadCoercionOptions = {}, +): Promise { + const object = opts.object ?? 'read_coercion_probe'; + const problems: string[] = []; + + await driver.connect?.(); + try { + await driver.syncSchema(object, { name: object, fields: FIELDS }); + await driver.create(object, { ...INPUT }); + + const rows = await driver.find(object, { object }); + if (!Array.isArray(rows) || rows.length !== 1) { + problems.push(`find returned ${Array.isArray(rows) ? `${rows.length} rows` : typeof rows}, expected exactly 1`); + return problems; + } + const row = rows[0] as Record; + + if (row.active !== true) { + problems.push(`boolean not coerced: 'active' expected true, got ${typeof row.active} ${JSON.stringify(row.active)}`); + } + if (stableStringify(row.meta) !== stableStringify(INPUT.meta)) { + problems.push(`json not coerced: 'meta' expected object ${JSON.stringify(INPUT.meta)}, got ${typeof row.meta} ${JSON.stringify(row.meta)}`); + } + if (row.count !== 5) { + problems.push(`number not coerced: 'count' expected 5, got ${typeof row.count} ${JSON.stringify(row.count)}`); + } + } finally { + await driver.disconnect?.(); + } + + return problems; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3da7154ddb..63451491c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1807,6 +1807,9 @@ importers: '@objectstack/plugin-hono-server': specifier: workspace:* version: link:../../plugins/plugin-hono-server + '@objectstack/verify': + specifier: workspace:* + version: link:../../verify '@types/node': specifier: ^26.1.0 version: 26.1.0 From 57ab8d5436f836a74fbd9b7255b4c80a494958ce Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 05:06:24 +0000 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20break=20verify=E2=86=94service-datas?= =?UTF-8?q?ource=20build=20cycle;=20run=20driver=20conformance=20in=20dogf?= =?UTF-8?q?ood?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior commit added @objectstack/verify as a devDep of service-datasource, but verify already depends on service-datasource (via runtime) — turbo flagged the cycle and the build/typecheck/test jobs failed. - Move the cross-driver conformance test out of service-datasource into packages/dogfood (which already dev-tests through @objectstack/verify and runs as the Dogfood Regression Gate); add driver-sql + driver-memory as dogfood devDeps. No cycle: drivers don't depend on verify/dogfood. - Revert the service-datasource test + devDep. - checkReadCoercion now reads back its own row by id (robust when the probe object already holds rows); the memory-driver test uses `persistence: false` so the probe never leaks to a shared on-disk snapshot across suites. Verified: `turbo run build --filter=./packages/*` 60/60 (no cycle); full dogfood suite 191 passed / 39 files (incl. the new conformance test: both drivers → [], non-coercing stub → 3 problems); verify build incl. DTS clean; eslint clean. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_015EsQntpH9mUZqG7LAWzPAK --- packages/dogfood/package.json | 2 + .../test/read-coercion-conformance.test.ts | 62 +++++++++++++++++++ .../services/service-datasource/package.json | 1 - .../src/read-coercion-conformance.test.ts | 46 -------------- packages/verify/src/read-coercion.ts | 4 +- pnpm-lock.yaml | 9 ++- 6 files changed, 73 insertions(+), 51 deletions(-) create mode 100644 packages/dogfood/test/read-coercion-conformance.test.ts delete mode 100644 packages/services/service-datasource/src/read-coercion-conformance.test.ts diff --git a/packages/dogfood/package.json b/packages/dogfood/package.json index 9fb5778af6..70079980aa 100644 --- a/packages/dogfood/package.json +++ b/packages/dogfood/package.json @@ -17,6 +17,8 @@ "@objectstack/verify": "workspace:*" }, "devDependencies": { + "@objectstack/driver-memory": "workspace:*", + "@objectstack/driver-sql": "workspace:*", "@types/node": "^26.1.0", "typescript": "^6.0.3", "vitest": "^4.1.10" diff --git a/packages/dogfood/test/read-coercion-conformance.test.ts b/packages/dogfood/test/read-coercion-conformance.test.ts new file mode 100644 index 0000000000..2eb19989d6 --- /dev/null +++ b/packages/dogfood/test/read-coercion-conformance.test.ts @@ -0,0 +1,62 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// Driver read-coercion conformance — exercises the reusable `checkReadCoercion` +// helper (from @objectstack/verify) against the framework's own SQL + memory +// drivers. A stored value must read back as its DECLARED type on every driver: +// a boolean as a boolean (not the integer 0/1 SQLite stores), a json field as +// an object, an integer as a number. +// +// This is the invariant behind the 2026-07-06 case_escalation incident: a +// boolean guard `field != true` read the field back as integer `1` on Turso, so +// `1 != true` was always true and the flow self-triggered forever — while the +// local repro (memory / better-sqlite3, both of which coerce) was green in 6s. +// Cloud's driver-turso runs the identical contract against itself in remote mode. + +import { describe, it, expect } from 'vitest'; +import { checkReadCoercion } from '@objectstack/verify'; +import { SqlDriver } from '@objectstack/driver-sql'; +import { InMemoryDriver } from '@objectstack/driver-memory'; + +const DRIVERS = [ + { + name: 'driver-sql (better-sqlite3 :memory:)', + make: () => + new SqlDriver({ + client: 'better-sqlite3', + connection: { filename: ':memory:' }, + useNullAsDefault: true, + }), + }, + { + name: 'driver-memory', + // `persistence: false` → pure in-memory, so the probe object does not leak to + // a shared on-disk snapshot and collide with other suites in the full run. + make: () => new InMemoryDriver({ persistence: false }), + }, +]; + +describe.each(DRIVERS)('read-coercion conformance: $name', ({ make }) => { + it('reads a stored row back as its declared types (boolean/json/number)', async () => { + const problems = await checkReadCoercion(make() as never); + expect(problems).toEqual([]); + }); +}); + +describe('checkReadCoercion detects a non-coercing driver', () => { + it('flags boolean/json/number that come back raw (the pre-fix remote-Turso shape)', async () => { + const raw = { + async connect() {}, + async disconnect() {}, + async syncSchema() {}, + async create() {}, + async find() { + return [{ id: '1', name: 'Widget', active: 1, meta: '{"k":1,"arr":[1,2]}', count: '5' }]; + }, + }; + const problems = await checkReadCoercion(raw as never); + expect(problems).toHaveLength(3); + expect(problems.join('\n')).toMatch(/boolean not coerced/); + expect(problems.join('\n')).toMatch(/json not coerced/); + expect(problems.join('\n')).toMatch(/number not coerced/); + }); +}); diff --git a/packages/services/service-datasource/package.json b/packages/services/service-datasource/package.json index 33b7978732..d43baea575 100644 --- a/packages/services/service-datasource/package.json +++ b/packages/services/service-datasource/package.json @@ -35,7 +35,6 @@ "@objectstack/driver-sql": "workspace:*", "@objectstack/driver-sqlite-wasm": "workspace:*", "@objectstack/plugin-hono-server": "workspace:*", - "@objectstack/verify": "workspace:*", "@types/node": "^26.1.0", "tsup": "^8.5.1", "typescript": "^6.0.3", diff --git a/packages/services/service-datasource/src/read-coercion-conformance.test.ts b/packages/services/service-datasource/src/read-coercion-conformance.test.ts deleted file mode 100644 index e15a040062..0000000000 --- a/packages/services/service-datasource/src/read-coercion-conformance.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// -// Driver read-coercion conformance. -// -// A stored value must read back as its DECLARED type on every driver: a -// `boolean` as a JS boolean (not the integer 0/1 SQLite stores), a `json` field -// as an object/array (not serialized text), an `integer` as a number. When two -// drivers disagree, code that is green on one silently breaks on the other. -// -// This is the invariant behind the 2026-07-06 `case_escalation` incident: a -// boolean guard `field != true` on Turso read the field back as integer `1`, so -// `1 != true` was always true and the flow self-triggered forever — while the -// local repro (memory / better-sqlite3, both of which coerce) was green in 6s. -// -// The check itself is the reusable, driver-agnostic `checkReadCoercion` from -// `@objectstack/verify` (empty problem list = conformant). This suite runs it -// against the framework's own SQL + memory drivers; cloud's driver-turso runs -// the identical contract against itself in remote mode. - -import { describe, it, expect } from 'vitest'; -import { checkReadCoercion } from '@objectstack/verify'; -import { SqlDriver } from '@objectstack/driver-sql'; -import { InMemoryDriver } from '@objectstack/driver-memory'; - -const DRIVERS = [ - { - name: 'driver-sql (better-sqlite3 :memory:)', - make: () => - new SqlDriver({ - client: 'better-sqlite3', - connection: { filename: ':memory:' }, - useNullAsDefault: true, - }), - }, - { - name: 'driver-memory', - make: () => new InMemoryDriver(), - }, -]; - -describe.each(DRIVERS)('read-coercion conformance: $name', ({ make }) => { - it('reads a stored row back as its declared types (boolean/json/number)', async () => { - const problems = await checkReadCoercion(make() as any); - expect(problems).toEqual([]); - }); -}); diff --git a/packages/verify/src/read-coercion.ts b/packages/verify/src/read-coercion.ts index 667c87c9ba..f262525d05 100644 --- a/packages/verify/src/read-coercion.ts +++ b/packages/verify/src/read-coercion.ts @@ -73,7 +73,9 @@ export async function checkReadCoercion( await driver.syncSchema(object, { name: object, fields: FIELDS }); await driver.create(object, { ...INPUT }); - const rows = await driver.find(object, { object }); + // Read back only the row we just wrote (by id), so the check is robust even + // when the probe object already holds unrelated rows on a shared backend. + const rows = await driver.find(object, { object, where: { id: INPUT.id } }); if (!Array.isArray(rows) || rows.length !== 1) { problems.push(`find returned ${Array.isArray(rows) ? `${rows.length} rows` : typeof rows}, expected exactly 1`); return problems; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63451491c9..ba14e740ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -748,6 +748,12 @@ importers: specifier: workspace:* version: link:../verify devDependencies: + '@objectstack/driver-memory': + specifier: workspace:* + version: link:../plugins/driver-memory + '@objectstack/driver-sql': + specifier: workspace:* + version: link:../plugins/driver-sql '@types/node': specifier: ^26.1.0 version: 26.1.0 @@ -1807,9 +1813,6 @@ importers: '@objectstack/plugin-hono-server': specifier: workspace:* version: link:../../plugins/plugin-hono-server - '@objectstack/verify': - specifier: workspace:* - version: link:../../verify '@types/node': specifier: ^26.1.0 version: 26.1.0