From ab980654a471c4d4c31cd62ebfe1ce37aad055d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 10:47:00 +0000 Subject: [PATCH] test(plugin-auth): migrate the where-operator coverage test to a sqlite `:memory:` witness (#5830) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #5704 的收口尾单的一半。#5812/#5844 在 #5704 survey 之后给 plugin-auth 带进了 两个新的 driver-memory 测试消费者;本 PR 迁走其中可以迁的那一个,并把另一个的 留存原因实测出来、写进注释,交维护者裁处置。 - auth-where-operator-coverage.test.ts:行为面后端由 InMemoryDriver 换成 @objectstack/driver-sql + better-sqlite3 `:memory:`,建表走 driver 自己的 initObjects()。#5813 的缺陷是「谓词被丢掉、filter 编译成 {}」,任何真正执行 过滤的后端都见证得到,已双向反验。 - plugin-auth 新增 @objectstack/driver-sql devDep;driver-memory devDep 暂留 (auth-contains-filter.test.ts 仍在用)。 - driver-split 的 Q2-B 注释按迁移后的真实事实修订(仅注释,零行为改动)。 tests-only,发布不可见,skip-changeset。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01JwwiU9bjhwy2SWj13ho8uv --- packages/plugins/plugin-auth/package.json | 1 + .../src/auth-where-operator-coverage.test.ts | 105 +++++++++++++----- ...eld-write-driver-split.integration.test.ts | 23 +++- pnpm-lock.yaml | 3 + 4 files changed, 105 insertions(+), 27 deletions(-) diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index 7855855ba6..6b07aa1c16 100644 --- a/packages/plugins/plugin-auth/package.json +++ b/packages/plugins/plugin-auth/package.json @@ -34,6 +34,7 @@ }, "devDependencies": { "@objectstack/driver-memory": "workspace:*", + "@objectstack/driver-sql": "workspace:*", "@objectstack/objectql": "workspace:*", "@types/node": "^26.1.2", "hono": "^4.12.34", diff --git a/packages/plugins/plugin-auth/src/auth-where-operator-coverage.test.ts b/packages/plugins/plugin-auth/src/auth-where-operator-coverage.test.ts index e855455d6c..0aec55cc2a 100644 --- a/packages/plugins/plugin-auth/src/auth-where-operator-coverage.test.ts +++ b/packages/plugins/plugin-auth/src/auth-where-operator-coverage.test.ts @@ -40,10 +40,35 @@ * unhandled operator is refused by name rather than silently widening a query * (the #3948 family's discipline — driver-memory's matcher `default:` arm and * objectql's `having` were both changed to refuse for this reason). + * + * ## Backend note (#5830 / #5704) + * + * Face 3's backend was `InMemoryDriver` when this file landed with #5844; it is + * now `@objectstack/driver-sql` + better-sqlite3 `:memory:`, built the way the + * rest of the repo builds an ephemeral store (`examples/app-crm`, `cli db + * clean`, PR #5715's `makeDefaultDriver()`, PR #5806's batch 3). #5704's + * programme replaces driver-memory's in-repo test consumers with sqlite's + * memory mode, and this file was a post-survey arrival, not an exemption. + * + * The swap costs this file nothing, and that is a measured claim rather than an + * assumption: #5813's defect was a DROPPED predicate — the filter compiled to + * `{}` and the query answered over the whole table. "The predicate reached the + * backend and narrowed the read" is witnessed identically by any backend that + * really executes it, and `$nin` / `$startsWith` / `$endsWith` are all + * `FILTER_OPERATORS` members that driver-sql compiles for real + * (`whereNotIn`, and `applyLike` with an explicit `ESCAPE`). Reverse-verified + * both ways in #5830: re-dropping the `not_in` arm turns the `not_in` pin red + * on sqlite exactly as it did on memory. + * + * Its sibling `auth-contains-filter.test.ts` is NOT interchangeable this way — + * driver-sql compiles `$regex` through the same `applyContainsLike` as + * `$contains` (`sql-driver.ts`, the `case '$regex':` fallthrough), so a SQL + * backend cannot tell #5710's defect from its fix. That file's disposition is + * #5830's open half; do not "finish the job" by copying this harness onto it. */ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { InMemoryDriver } from '@objectstack/driver-memory'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { SqlDriver } from '@objectstack/driver-sql'; import { assertEngineDeleteDispatch } from '@objectstack/objectql'; import { whereOperators } from '@better-auth/core/db/adapter'; import { FILTER_OPERATORS } from '@objectstack/spec/data'; @@ -55,15 +80,24 @@ import { SUPPORTED_WHERE_OPERATORS, } from './objectql-adapter'; -/** Keeps the driver's own lifecycle logging out of the test output. */ -const silentLogger = { - debug: () => {}, - info: () => {}, - warn: () => {}, - error: () => {}, -} as any; - -const NOW = new Date('2026-08-06T00:00:00.000Z').toISOString(); +/** + * The columns face 3 reads, declared — a real table has to be told. + * + * `emailVerified` / `createdAt` / `updatedAt` used to be seeded alongside these + * and are gone: they were camelCase keys no assertion ever read, which only a + * schemaless store would have accepted (`sys_user` spells them + * `email_verified` / `created_at` / `updated_at`). Declaring the fixture down to + * what it actually asserts on is #5806's "resolve by declaring, not by + * relaxing" — the alternative would have been to declare three columns to hold + * values nothing looks at. + */ +const SYS_USER = { + name: 'sys_user', + fields: { + name: { type: 'text', name: 'name' }, + email: { type: 'text', name: 'email' }, + }, +}; /** * Rows chosen so each of the three operators has something to EXCLUDE that a @@ -74,12 +108,14 @@ const NOW = new Date('2026-08-06T00:00:00.000Z').toISOString(); * ends_with 'abc' → x_abc (abc_one / abc_z start with it instead) * not_in [abc_one, x_abc] → abc_z, zed * - * All lowercase on purpose. `$startsWith`/`$endsWith` are case-SENSITIVE at the - * contract layer (#5701 Q2=A) but driver-memory's mingo path compiles them with - * the `i` flag while its own reference matcher uses `String.prototype - * .startsWith` — that in-driver divergence is #5702's budget, not this PR's, so - * no fixture here varies by case and no assertion below depends on which way it - * is resolved. + * All lowercase on purpose, and the reason survived the backend swap intact + * (#5830). `$startsWith`/`$endsWith` are case-SENSITIVE at the contract layer + * (#5701 Q2=A); sqlite's `LIKE` is ASCII case-INsensitive by default, just as + * driver-memory's mingo path compiled the same operators with the `i` flag + * while its own reference matcher used `String.prototype.startsWith`. Both are + * the same debt — the per-driver alignment is #5702's budget, not this file's — + * so no fixture here varies by case and no assertion below depends on which way + * it is resolved. */ const SEED = [ { id: 'u_abc1', name: 'abc_one', email: 'abc-one@example.com' }, @@ -89,7 +125,7 @@ const SEED = [ ]; /** - * An engine facade over a REAL `InMemoryDriver`. + * An engine facade over a REAL `SqlDriver`. * * `delete` opens with ObjectQL's OWN dispatch predicate * ({@link assertEngineDeleteDispatch}) instead of a hand-mirrored `if`, so this @@ -99,7 +135,7 @@ const SEED = [ * absent: nothing here exercises it, and an unexercised write verb is a second * contract to keep honest for no gain. */ -function memoryEngine(driver: InMemoryDriver): IDataEngine { +function sqlEngine(driver: SqlDriver): IDataEngine { // The query bag keeps its declared driver-side type with no `any` erasure — // `query-options/no-any-erasure` (#4674/#4918) counts test-side calls too. return { @@ -116,13 +152,32 @@ function memoryEngine(driver: InMemoryDriver): IDataEngine { } as unknown as IDataEngine; } -async function seededAdapter() { - const driver = new InMemoryDriver({ logger: silentLogger }); - await driver.connect(); - for (const row of SEED) { - await driver.create('sys_user', { ...row, emailVerified: false, createdAt: NOW, updatedAt: NOW }); +/** + * Live `:memory:` databases, closed after each test — the database dies with + * its connection, so nothing touches the host filesystem, but a file this size + * would otherwise hold one open pool per behavioural case. + */ +const openDrivers: SqlDriver[] = []; + +afterEach(async () => { + while (openDrivers.length) { + const driver = openDrivers.pop(); + try { await driver?.disconnect(); } catch { /* noop */ } } - const adapter: any = (createObjectQLAdapterFactory(memoryEngine(driver)) as any)({} as any); +}); + +async function seededAdapter() { + const driver = new SqlDriver({ + client: 'better-sqlite3', + connection: { filename: ':memory:' }, + useNullAsDefault: true, + }); + openDrivers.push(driver); + // Real DDL through the driver's own path — the table every row below lands in + // is created by the backend, not conjured by a store on first write. + await driver.initObjects([SYS_USER]); + for (const row of SEED) await driver.create('sys_user', row); + const adapter: any = (createObjectQLAdapterFactory(sqlEngine(driver)) as any)({} as any); return { driver, adapter }; } diff --git a/packages/runtime/src/sandbox/undeclared-field-write-driver-split.integration.test.ts b/packages/runtime/src/sandbox/undeclared-field-write-driver-split.integration.test.ts index a6b2a20aee..25ecb3f182 100644 --- a/packages/runtime/src/sandbox/undeclared-field-write-driver-split.integration.test.ts +++ b/packages/runtime/src/sandbox/undeclared-field-write-driver-split.integration.test.ts @@ -43,8 +43,8 @@ /** * ⚠️ `@objectstack/driver-memory` is imported here ON PURPOSE, and this is the - * ONLY place in the repository that still consumes it from a test. It is NOT a - * migration leftover — do not "finish the job" by deleting or replacing it. + * only PERMANENT test consumer of it in the repository. It is NOT a migration + * leftover — do not "finish the job" by deleting or replacing it. * * Why it has to stay: the whole point of this file is a PRODUCT divergence * between two driver families — writing an undeclared field is rejected as a @@ -68,6 +68,25 @@ * do not even depend on the driver. #5704/#5784 renamed them all to * `makeStubDriver`, precisely so that grepping for the driver lands here, and * only here. + * + * [#5830] "Only here" was briefly untrue and is being restored in two steps, + * so the grep is honest about what it finds today. Two consumers arrived in + * plugin-auth AFTER #5704's survey (#5812 and #5844, the identity lane): + * + * - `plugin-auth/src/auth-where-operator-coverage.test.ts` — migrated to + * sqlite `:memory:` by #5830. Its defect (#5813) was a DROPPED predicate, + * which any backend that really executes the filter witnesses. + * - `plugin-auth/src/auth-contains-filter.test.ts` — still on driver-memory, + * pending a maintainer ruling, and NOT an oversight. Its pin is #5710's + * `contains` → `$regex` flip, and driver-sql routes `$regex` through the + * same `applyContainsLike` as `$contains` (the `case '$regex':` + * fallthrough in `sql-driver.ts`), so a SQL witness answers identically + * either way. Measured in #5830: with the defect restored, the memory + * backend fails 3 behavioural pins and a sqlite backend passes all 4. + * Migrating it would leave assertions that pass because nothing + * distinguishes them. Its disposition rides on #5702 (the driver-side + * `$regex` refusal) — once `$regex` is refused rather than aliased, the + * SQL arm can witness it and the file can move. */ import { describe, it, expect, afterEach } from 'vitest'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8cfcf3b68f..4e5f744960 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1441,6 +1441,9 @@ importers: '@objectstack/driver-memory': specifier: workspace:* version: link:../../drivers/driver-memory + '@objectstack/driver-sql': + specifier: workspace:* + version: link:../../drivers/driver-sql '@objectstack/objectql': specifier: workspace:* version: link:../../objectql