Skip to content

Commit bea6d2e

Browse files
fix(driver-turso)!: new TursoDriver refuses syncUrl under a forced remote mode, and sync with no syncUrl (#20200) (#20447)
Fixes #20200 Clause-②: no (narrowing) The `Clause-②` line above is the amended claim's (comment 5869128452, unchanged in 5871001040), copied as it stands. The changeset carries the same value. Session `session_01N8TPEsoJxPsdSdNKGnNGEN` (PM dispatch, `domain:engine` seat 1, mode:subagent), branch `claude/issue-20200-turso-remote-sync-refused`. The branch starts at `e01d34730`, which already contains PR #20422 and PR #20425. It was then merged with `origin/main` at `87c37aec1` in a true merge commit. **Every reading below was taken at head `2242ad513`** unless it says otherwise. ## What changes `new TursoDriver(...)` refuses two configurations it used to build while ignoring one of their keys. Both are refused with `VALIDATION_ERROR` / 400, before `super()`, after the constructor's three existing refusals, so no configuration they already refuse gets a different message: - **`syncUrl` under a forced `mode: 'remote'`.** A remote url beside `syncUrl` with no `mode` was already refused, as a replica on a remote url (`localEngineDefect`), so only a forced remote mode reaches this refusal. - **`sync` with no `syncUrl`**, in every mode: local, replica and remote. An empty `syncUrl` counts as unset, which matches `detectMode`, `connect()`, `sync()` and the spec's refinement. Each refusal's message is `@objectstack/spec`'s `TursoConfigSchema` issue message for that key, byte for byte. Per the seat's ruling (option A of the report `5869079328`), each is a module-level constant in `turso-driver.ts`: `REMOTE_MODE_SYNC_URL_REFUSAL` and `SYNC_WITHOUT_SYNC_URL_REFUSAL`. The parity test holds each constant equal to the schema's issue, read from the built spec dist. No new `packages/spec` export. **The message must be true where it is thrown** (seat ruling 2, a declared cross-lane text edit). The spec's `syncUrl`-under-remote text said "the turso driver never hands `syncUrl` to the remote client and runs no sync, so the setting changes nothing". That clause now reads "the turso driver refuses this configuration when it starts", the form its sibling refusals in `tursoTransportIssues` use. The rest of the message is byte-identical. The three copies now read the same: the spec, the driver's `TursoConfigSchema` mirror and the constructor constant. The `sync` message is reused unchanged. The ADR-0087 entry `18.turso-config-transport-mismatch-refused` has its `reason` sentence on stored rows corrected. It now says the constructor also refuses `syncUrl` under a forced remote mode and `sync` with no `syncUrl` when the datasource boots, citing #20200. Patch round 1 also put its header comment and its "One more it builds and then ignores" clause in the past tense, bounded by #20200. Nothing else in the entry moved. `check:generated` then proved `src/migrations/registry.ts` stale, because the registry embeds the entry text, and `check:generated --fix` regenerated it: a 4-line text diff. No gate refused editing a registered entry. ## H1: before and after (dist probe, 9 configs) The same scratch script ran against `packages/drivers/driver-turso/dist/index.mjs`. Before is at `dbddf02c1` (origin/main when the first round measured). After is this branch's build. | config | before (`dbddf02c1`) | after | | --- | --- | --- | | `libsql://` + `mode: 'remote'` + `syncUrl` + `sync` | constructs, connects, `isSyncEnabled()` true, no interval, `sync()` rejects `SYNC_NOT_SUPPORTED` | refused, VALIDATION_ERROR / 400, `syncUrl` message | | `libsql://` + `mode: 'remote'` + `syncUrl` | the same | refused, `syncUrl` message | | `file:` + `mode: 'remote'` + `syncUrl` + `sync` | constructs, connects, `isSyncEnabled()` true, no interval, `sync()` rejects `SyncNotSupported("File")` | refused, `syncUrl` message | | `libsql://` + `mode: 'remote'` + `sync`, no `syncUrl` | constructs, `isSyncEnabled()` false, `sync()` a no-op | refused, `sync` message | | `libsql://` (no mode) + `sync`, no `syncUrl` | the same | refused, `sync` message | | CONTROL `libsql://` + `mode: 'remote'` | constructs, connects, `isSyncEnabled()` false | unchanged | | RIDER `file:` + `mode: 'replica'`, no `syncUrl` | constructs as `replica`, `isSyncEnabled()` false, `sync()` a no-op | **unchanged (see the rider section below)** | | RIDER + `sync`, no `syncUrl` | the same | refused, `sync` message (the `sync` key, not the rider) | | `file:` + `sync`, no `syncUrl`, no mode | local, `sync` ignored | refused, `sync` message | ## H4: what a stored row answers at boot now `buildTursoDriverConfig` (`packages/services/service-datasource/src/turso-driver-config.ts`, read and not edited) forwards a stored row's `syncUrl`, `sync` and `mode` unparsed. PR #20199's ADR-0087 entry is semantic, so no D2 conversion rewrites such a row either. A row stored before PR #20199 with a remote url, `mode: 'remote'` and `syncUrl`, or with `sync` and no `syncUrl`, therefore reaches the constructor as written: - `factory.create` throws the refusal (`default-datasource-driver-factory.ts` for open-core, `turso-driver-factory.ts` for the host default). - `DatasourceConnectionService` catches it and records the datasource as `failed-degraded`, with "datasource NAME: connect failed — MESSAGE". - Under ADR-0062 D5, boot fails fast when objects bind to that datasource or are routed to it, or when it is boot-critical, unless `OS_ALLOW_DRIVER_CONNECT_FAILURE` is set. Otherwise it is left unconnected with a warning. - A test connection answers `ok: false`, with "Failed to build driver: MESSAGE". Before this change the same row booted, reported sync as enabled, and never synced. No in-repo caller reads `isSyncEnabled()` or calls the driver's sync outside `@objectstack/driver-turso`'s own tests. The changeset's FROM → TO paragraph carries these readings and the way out: drop `syncUrl` / `sync` from a remote config, or use a `file:` url with the remote in `syncUrl`. ## The rider stays: `mode: 'replica'` on a `file:` url with no `syncUrl` This configuration still constructs and runs as a plain local database. The table's H1 rider row shows it: a declared replica that never syncs. It is deliberately not refused here. A constructor-only refusal would make construction refuse a configuration both `TursoConfigSchema` copies accept, which reopens #19977's defect class in reverse (a datasource that authors clean and fails at boot). The parity table would go red on its row "file: under a forced mode: 'replica'", and `turso-driver-unrecognised-url-refusal.test.ts` pins `FILE: + mode 'replica', no syncUrl` as accepted. Closing it needs the spec half and the constructor half together, and the spec's accept set is outside this card, so the seat files it as its own card. The new test file pins the rider as still accepted, so whoever closes it moves that pin on purpose. ## Tests | suite at `2242ad513` | result | | --- | --- | | `@objectstack/driver-turso` vitest, whole package | 75 files · 2014 passed · 18 skipped · exit 0 | | `@objectstack/driver-turso` typecheck (`tsc --noEmit`) | exit 0; `--listFilesOnly` on the pre-merge commit shows both touched test files in the program | | `@objectstack/spec` vitest `--project local`, 3 shards | 564 files · 16640 passed · 1 todo (6026 + 5141 + 5473), exit 0 on each shard | | `@objectstack/spec` typecheck (tsc + scripts + `check:test-typecheck`) | exit 0 | The 18 skips are the parity table's forced-mode rows for the mirror, which strips `mode`: 16 before, plus the two new forced-mode `sync` rows. - **`spec/turso-config-constructor-parity.test.ts`:** the four `inert` rows flip to `ctor: 'refuse'` (three `syncUrl`, one `sync`). Four `sync` rows are added: a remote url, a forced remote, a forced replica on `file:`, and an empty `syncUrl`. The `inert` floor moves from at least 4 to exactly 0, with floors added for `syncUrl` (at least 3), `sync` (at least 5) and the sync-key refusals (at least 8). A new table, `SYNC_KEY_REFUSALS`, asserts for each of those 8 rows that the constructor's `error.message` equals the spec issue's message. - **`turso-driver-ignored-sync-key-refusal.test.ts` (new):** both refusals are asserted as the envelope (`code` + `status`) plus the message's first sentence, across `libsql://`, `https://`, `file:`, `:memory:`, forced remote, forced replica and an empty `syncUrl`. `createTursoDriver()` is covered too. Controls: forced remote with no `syncUrl` connects and reports sync off; a `file:` replica beside `syncUrl`; `sync` beside `syncUrl` under forced `mode: 'local'`; the rider. - **`packages/spec/src/data/driver/turso.test.ts`:** one assertion and its test title pinned the old clause ("never hands `syncUrl` to the remote client and runs no sync"). They now pin the new clause. This file is not named in the claim's surface; the change is the mechanical consequence of ruling 2's text edit (see Deviations). **Reverse verification**, via `scripts/ablation-replace.mjs` from the committed state (turso-driver.ts blob `afe3ad31`). The driver tests import `../turso-driver` from source, so no build is involved. Directions were predicted before each run, and all three matched: 1. `if (mode === 'remote' && config.syncUrl) {` became `if (false && …) {`, mutation landed (anchor 1 → 0, blob `afe3ad31` → `5f9aca88`). **11 failed** / 169 passed: exactly the 5 `syncUrl` cases in the new file, plus the 3 `syncUrl` rows' constructor verdicts and their 3 message pins. Restored: blob == HEAD, `git diff HEAD` empty. 2. `if (config.sync && !config.syncUrl) {` got the same mutation (blob → `32b6a28c`). **16 failed** / 164 passed: the 6 `sync` cases, plus 5 constructor verdicts and 5 message pins. Restored the same way. 3. One byte of the copy: a doubled space inside the `syncUrl` constant, after its first sentence (blob → `084c8d96`). **3 failed**: exactly the 3 `syncUrl` message pins, while every verdict and first-sentence case stayed green. This proves the byte-equality pin is what holds the copy. Restored the same way. ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, run after the last commit at `2242ad513`, lists 9 paths vs merge base `87c37aec1` and **91 commands**. Every command ran, with its exit code written to disk before any pipe. `--ran` reconciliation reads `91 derived famil(ies) accounted for — 89 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)`, with 0 UNRUN. - **NOT MEASURED (exit 3, PREREQUISITE NOT MET):** `check:dual-build-cjs-loads` (dozens of workspace packages have no `dist/`) and `check:type-check-debt` (it needs a whole-workspace build). Both need a full-workspace build, which is CI's run. - **Run twice:** `check:doc-formula-expressions` and `check:lean-entry-closure` first answered exit 3. They are exit 0 after building `@objectstack/lint` and `@objectstack/objectql` with their closures. - **Notable readings:** - `check-adr-0087-registration` → `not-required (already-registered)` for `turso-config-transport-mismatch-refused`, clause-② narrowing, BREAKING, bang; - `check-changeset-no-major` exit 0; - `check:migration-registry` → `registry.ts is current`; - `check:driver-conformance`, `check:nul-bytes`, `check:doc-authoring`, `check:test-source-alias`, `check:cross-package-test-inputs`, `check:api-surface`, `check:authorable-surface`, `check:docs`, `check:spec-changes` and `check:upgrade-guide` → exit 0. - **Roster families under a touched directory, also run:** five artifact-roster families whose roster sits under a directory this diff touches, all exit 0: `check-changeset-fixed`, spec `check:meta-url-spelling`, `check:authz-resolver`, `check:error-code-casing` and `check:filter-alias-parity`. - **Narrowed lint:** `eslint --no-inline-config --format json` over the 8 changed TS files reports 8 files, 0 errors and 0 warnings. The population is `eslint.config.mjs`'s lint object, `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`; the changeset `.md` is outside it. Invariance holds because that config never enables type-aware linting (no `parserOptions.project`, per its own header), so this diff cannot move any untouched file's verdict. The full `pnpm lint` is CI's. - **Not run locally, left to CI:** - the whole-workspace type-check lanes; - the Test Core, Dogfood and Build Core jobs; - downstream suites of `@objectstack/service-datasource`, `@objectstack/runtime` and `@objectstack/cli`. Their turso fixtures go through `buildTursoDriverConfig` or a capturing constructor and never build the real driver. The downstream real-driver constructions are both in dogfood's `date-bucket-parity-turso.test.ts`: `new TursoDriver({ url: ':memory:' })`, and at `:144` a remote `{ url: 'libsql://test-db.turso.io', authToken }`. Neither carries a sync key, so neither refusal touches them. **Driver-conformance ledger (`lanes/engine.md`):** `check:driver-conformance` read `OK — 50 covered cell(s), 0 in the DEBT ledger, 0 exempt` both before (`dbddf02c1`) and after (`2242ad513`). `driver-turso` is `ok` on all 10 case-sets both times. No movement. ## Deviations (declared) - `packages/spec/src/data/driver/turso.test.ts`: one assertion and one test title, rewritten because ruling 2's text edit makes the old pin false. It is outside the claim's listed surface ("nothing else in `packages/spec`"). Without it the spec suite goes red. - `turso-driver.ts` outside the constructor body. It carries the two module constants and a small throw helper (`refuseIgnoredSyncKey`) beside `refuseSuppliedClientTimeout`, as ruling 1 prescribes. It also extends the TSDoc on `TursoDriverConfig.syncUrl` and `.sync` so the declared config names the two new refusals. No other region of the file changed. ## Acceptance notes - **The mirror's dormant copy is now kept equal by this diff.** The driver mirror (`src/spec/turso.zod.ts`) strips `mode`, so its copy of the `syncUrl`-under-remote text is still unreachable through a parse, and no test can hold it equal. This diff applies the same one-clause edit there, so the three copies read identically today. - **The spec wording item from the report is done here** (ruling 2). - **Comments that described the ignored sync keys: corrected in patch round 1** (the amended claim 5871001040). Every comment in this diff's files that described `syncUrl` under a forced remote mode, or `sync` with no `syncUrl`, as constructed and ignored now says what holds since #20200 (list below). A `git grep -n -i ignore` over the touched files finds no such wording left. What remains is only `packages/drivers/driver-turso/README.md`'s list of constructor refusals. It is scoped to the local-engine refusals and does not name the two new ones: incomplete, not false (the review's reading), and a docs follow-up. It is not runtime text. - Loader fixtures in `@objectstack/service-datasource` (`turso-driver-config.test.ts`, the `mode: 'remote'` + `syncUrl` key-list case) spell a configuration the real constructor now refuses. They exercise `buildTursoDriverConfig` only and never construct the driver, and the package is fenced, so they are unchanged. ## Patch round 1 (text only, head `7052b9111`) The at-tier review 5870987840 PASSed `2242ad513` and escalated comments this diff made false. With the `inert` floor at 0, the driver no longer constructs and ignores anything the schemas refuse. Per the amended claim 5871001040, those comments are corrected here as text only. No logic and no test assertion changed. - **Spec `packages/spec/src/data/driver/turso.zod.ts`:** - section heading 1b "refuses or ignores" → "refuses"; - the intro "or constructs and then ignores" → "or, until #20200, constructed and then ignored"; - the `syncUrl`-under-remote bullet ("which the driver accepts and then IGNORES … That arm has no constructor refusal behind it") is now in the past tense and says the constructor refuses it too since #20200; - "Nothing the constructor accepts is refused here, the `syncUrl`-under-`mode: 'remote'` arm aside" now says that exception has been a constructor refusal too since #20200; - the superRefine comment "What the driver refuses at construction, or constructs and ignores" → "What the driver refuses at construction". - **Mirror `packages/drivers/driver-turso/src/spec/turso.zod.ts`:** section heading 2a "refuses or ignores" → "refuses"; the intro "or constructs and ignores" → "or, until #20200, constructed and ignored"; the superRefine comment corrected as in the spec. - **Spec `turso.test.ts`:** the block comment "(which it constructs and ignores)" → "(since #20200 that includes `syncUrl` under a forced `mode: 'remote'`, which it used to construct and ignore)". - **Parity test:** two comments. The header's "(or constructs and ignores)" → "(or, until #20200, constructed and ignored)", and "nothing it accepts but the declared-and-ignored keys" → "… but an `inert` row's declared-and-ignored key (none since #20200)". - **D3 entry `18.turso-config-transport-mismatch-refused`:** - its header comment now reads "the one combination the driver used to build and then ignore (syncUrl under a forced remote mode), which the constructor refuses too since #20200"; - its `reason` clause "One more it builds and then ignores … Nothing the constructor accepts is refused, that key aside" is in the past tense and bounded by #20200 ("One more it built and then ignored until #20200 …"; "Nothing the constructor accepts is refused (at #19977 that key was the one exception; since #20200 there is none)"); - nothing else in the entry moved, and `registry.ts` was regenerated by `gen:migration-registry`, its hunk equal to the entry's. - **Left as they are, each true:** "would ignore" (a conditional, naming what the refusals prevent), the `inert` mechanism's own definition (with none today), the "before" measurement table in the new test file, and `@libsql/client` ignoring `syncUrl` beside a remote url (a fact about the client). Readings at `7052b9111`. That head is the round's two commits plus a true merge of `origin/main` `8cdbe0c6e`, which regenerated `registry.ts` for its own new entry; the registry is current, with 308 semantic entries. - **CI on `2242ad513` before the push:** 35 check runs, 32 success, 3 skipped, 0 failed. Test Core 1/6, 3/6 and 5/6 and Type Check · workspace all concluded success. - **Tests:** `@objectstack/driver-turso` 75 files · 2014 passed · 18 skipped, typecheck exit 0. `@objectstack/spec` `--project local` in 3 shards: 6025 + 5141 (1 todo) + 5473 passed, exit 0 on each. A first shard-2 attempt was killed by the runner's own timeout and re-run. Spec typecheck exit 0. - **Gates:** `dispatch-gates --commands` finds 9 paths vs merge base `8cdbe0c6e` and 91 commands, all run with exit codes recorded before any pipe. `--ran` reads `91 derived famil(ies) accounted for — 89 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)`, 0 UNRUN; the two NOT MEASURED are `check:dual-build-cjs-loads` and `check:type-check-debt` (whole-workspace build; both green in CI at `2242ad513`). Also run: the five roster families under touched directories, and spec `check:generated` ("All 15 generated artifacts are up to date"), all exit 0. - **Other readings:** `check:driver-conformance` is unchanged at 50 covered, 0 DEBT. The narrowed eslint run finds 8 files, 0 errors. The control-byte scan finds nothing. --- _Generated by [Claude Code](https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent fbeb56e commit bea6d2e

9 files changed

Lines changed: 386 additions & 76 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
'@objectstack/driver-turso': minor
3+
'@objectstack/spec': patch
4+
---
5+
6+
fix(driver-turso)!: `new TursoDriver` refuses `syncUrl` under a forced `mode: 'remote'`, and `sync` with no `syncUrl`, instead of building and ignoring them
7+
8+
Clause-②: no (narrowing) — nothing is widened. No key is added, removed or renamed, and no exported symbol moves. Two driver configurations that the turso driver used to build and then ignore a key of are now refused when it is built.
9+
10+
`new TursoDriver()` accepted `syncUrl` beside a forced `mode: 'remote'`. Remote mode sends every read and write straight to `url`, and the remote client is created without `syncUrl`, so no replica is built and no sync ever runs. Measured on the built driver before this change, a `libsql://` or `file:` url under `mode: 'remote'` with `syncUrl` and `sync` constructed and connected, and `isSyncEnabled()` answered `true`. No sync interval started, and the sync call rejected with `SYNC_NOT_SUPPORTED` (`SyncNotSupported("File")` on the `file:` url). It also accepted `sync` with no `syncUrl`, in any mode, where nothing reads it. `@objectstack/spec`'s `TursoConfigSchema` already refused both at authoring. A datasource row stored before that, or a config a host builds itself, reached the constructor unparsed and ran with a sync setting that did nothing.
11+
12+
**BREAKING** accept-set narrowing on a published constructor, shipped as `minor` under the repo's launch-window convention for breaking changes (`scripts/check-changeset-no-major.mjs`). Refused now with `VALIDATION_ERROR` / 400, before any client or database is opened:
13+
14+
- `syncUrl` under a forced `mode: 'remote'`. A remote url beside `syncUrl` with no `mode` was already refused, as a replica on a remote url;
15+
- `sync` with no `syncUrl` (or with an empty one), in local, replica and remote mode alike.
16+
17+
Each refusal's message is the spec contract's issue message for that key, byte for byte, so authoring and boot say the same thing. A test holds the copies equal. The spec's `syncUrl` message said the driver "runs no sync, so the setting changes nothing". It now reads "the turso driver refuses this configuration when it starts", like its sibling refusals, and the driver's own `TursoConfigSchema` mirror follows (`@objectstack/spec` patch: message text only). The ADR-0087 entry `turso-config-transport-mismatch-refused` now also records that the constructor refuses these two shapes at boot.
18+
19+
Left accepted on purpose: `mode: 'replica'` on a `file:` url with no `syncUrl` (and no `sync`). It still runs as a plain local database. Refusing it in the constructor alone would refuse a config both schemas accept, so it is tracked separately.
20+
21+
### Migration: FROM → TO
22+
23+
| You wrote | Write instead |
24+
| --- | --- |
25+
| `url: 'libsql://my-db.turso.io', mode: 'remote', syncUrl: …` (with or without `sync`) | a remote database: drop `syncUrl` and `sync`. An embedded replica: `url: 'file:./data/replica.db', syncUrl: 'libsql://my-db.turso.io'` and no `mode` |
26+
| `url: 'file:./data/app.db', mode: 'remote', syncUrl: …` | the same two ways out |
27+
| `sync: { … }` with no `syncUrl` | name the remote in `syncUrl` (with a `file:` url), or drop `sync` |
28+
29+
A datasource row stored with one of these shapes is not re-parsed when it loads, so it now fails when the driver is built. `factory.create` throws the refusal. The connection service records the datasource as `failed-degraded` with the message, and a test connection answers `ok: false` ("Failed to build driver: …"). Under ADR-0062 D5, the boot fails fast when objects bind to that datasource or are routed to it, or when it is boot-critical, unless `OS_ALLOW_DRIVER_CONNECT_FAILURE` is set. Otherwise it is left unconnected with a warning. Before this change the same row booted, reported sync as enabled and never synced. The way out is the table above: drop `syncUrl` / `sync` from a remote config, or use a `file:` url with the remote in `syncUrl`.
30+
31+
Blast radius, measured on this tree: no example, template, published skill or hand-written doc authors either shape, and no in-repo caller reads `isSyncEnabled()` or calls the driver's sync outside `@objectstack/driver-turso`'s own tests. Whether any out-of-repo deployment declares such a config is NOT measured and is not claimed to be zero.
32+
33+
<!-- adr-0087: not-required (already-registered turso-config-transport-mismatch-refused) that entry is this family's semantic TODO (the turso transport refusals, registered with the authoring half), and this diff updates its stored-row sentence to cover the constructor half: syncUrl under a forced remote mode and sync with no syncUrl are refused at boot as well -->

‎packages/drivers/driver-turso/src/spec/turso-config-constructor-parity.test.ts‎

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,24 @@
1010
* 3. this package's own `TursoConfigSchema` mirror (`./turso.zod.ts`).
1111
*
1212
* The two schemas used to accept every combination below, so a datasource the
13-
* driver refuses at construction (or constructs and ignores) was accepted at
14-
* authoring and failed a boot later. Each row states the constructor's verdict
13+
* driver refuses at construction (or, until #20200, constructed and ignored)
14+
* was accepted at authoring and failed a boot later. Each row states the constructor's verdict
1515
* and the schemas' verdict, and the test asserts all three against it:
1616
*
1717
* - a row the constructor REFUSES is refused by both schemas, on the key the
1818
* row names, with one `custom` issue;
19-
* - a row the constructor ACCEPTS is accepted by both — except the `inert`
20-
* rows, which the constructor builds and then ignores a key of (`syncUrl`
21-
* under a forced `mode: 'remote'`; `sync` with no `syncUrl`). Those are
22-
* refused at authoring only, because a declared setting that changes
23-
* nothing is the shape ADR-0049 does not ship;
24-
* - the two schemas' messages are byte-identical wherever both judge a row.
19+
* - a row the constructor ACCEPTS is accepted by both — except an `inert`
20+
* row, one the constructor would build and then ignore a key of, refused at
21+
* authoring only. [#20200] There are none left, and the table pins that at
22+
* exactly zero: the four there were (`syncUrl` under a forced
23+
* `mode: 'remote'`; `sync` with no `syncUrl`) are refused by the
24+
* constructor now, because a declared setting that changes nothing is the
25+
* shape ADR-0049 does not ship. A new `inert` row is a new ignored key —
26+
* fix the constructor, or argue it on the card, before moving the floor;
27+
* - the two schemas' messages are byte-identical wherever both judge a row;
28+
* - [#20200] where the constructor refuses on `syncUrl` or `sync`, its message
29+
* is the spec contract's issue message, byte for byte: those two texts are
30+
* copies in `../turso-driver.ts`, and this is the pin that holds them equal.
2531
*
2632
* ⚠️ The mirror declares no `mode`, so zod strips an authored one before its
2733
* refinement runs: rows that FORCE a mode are judged by the constructor and the
@@ -69,7 +75,7 @@ interface Row {
6975
ctor: 'accept' | 'refuse';
7076
/** The key both schemas refuse on, or `undefined` when they accept. */
7177
refusedOn?: 'url' | 'syncUrl' | 'timeoutMs' | 'sync';
72-
/** The constructor accepts it and ignores a key: refused at authoring only. */
78+
/** The constructor accepts it and ignores a key: refused at authoring only. None today (#20200). */
7379
inert?: true;
7480
}
7581

@@ -134,13 +140,22 @@ const ROWS: Row[] = [
134140
{ name: 'timeoutMs beside an uppercase WS://', config: { url: 'WS://127.0.0.1:8080', timeoutMs: 5000 }, ctor: 'refuse', refusedOn: 'timeoutMs' },
135141
{ name: "timeoutMs beside wss:// under a forced mode: 'remote'", config: { url: 'wss://db.example.turso.io', timeoutMs: 5000, mode: 'remote' }, ctor: 'refuse', refusedOn: 'timeoutMs' },
136142

137-
// ── accepted by the constructor, ignored by the driver: refused at authoring ──
138-
{ name: "syncUrl under a forced mode: 'remote'", config: { url: REMOTE, mode: 'remote', syncUrl: REMOTE }, ctor: 'accept', refusedOn: 'syncUrl', inert: true },
139-
{ name: "syncUrl + sync under a forced mode: 'remote'", config: { url: REMOTE, mode: 'remote', syncUrl: REMOTE, sync: { intervalSeconds: 60 } }, ctor: 'accept', refusedOn: 'syncUrl', inert: true },
140-
{ name: "file: + syncUrl under a forced mode: 'remote'", config: { url: FILE, mode: 'remote', syncUrl: REMOTE }, ctor: 'accept', refusedOn: 'syncUrl', inert: true },
141-
{ name: 'sync with no syncUrl', config: { url: FILE, sync: { intervalSeconds: 60 } }, ctor: 'accept', refusedOn: 'sync', inert: true },
143+
// ── sync keys the driver would ignore: refused at construction and at authoring (#20200) ──
144+
{ name: "syncUrl under a forced mode: 'remote'", config: { url: REMOTE, mode: 'remote', syncUrl: REMOTE }, ctor: 'refuse', refusedOn: 'syncUrl' },
145+
{ name: "syncUrl + sync under a forced mode: 'remote'", config: { url: REMOTE, mode: 'remote', syncUrl: REMOTE, sync: { intervalSeconds: 60 } }, ctor: 'refuse', refusedOn: 'syncUrl' },
146+
{ name: "file: + syncUrl under a forced mode: 'remote'", config: { url: FILE, mode: 'remote', syncUrl: REMOTE }, ctor: 'refuse', refusedOn: 'syncUrl' },
147+
{ name: 'sync with no syncUrl', config: { url: FILE, sync: { intervalSeconds: 60 } }, ctor: 'refuse', refusedOn: 'sync' },
148+
{ name: 'sync with no syncUrl on a remote url', config: { url: REMOTE, sync: { intervalSeconds: 60 } }, ctor: 'refuse', refusedOn: 'sync' },
149+
{ name: "sync with no syncUrl under a forced mode: 'remote'", config: { url: REMOTE, mode: 'remote', sync: { onConnect: true } }, ctor: 'refuse', refusedOn: 'sync' },
150+
{ name: "sync with no syncUrl under a forced mode: 'replica'", config: { url: FILE, mode: 'replica', sync: { intervalSeconds: 60 } }, ctor: 'refuse', refusedOn: 'sync' },
151+
{ name: 'sync beside an empty syncUrl (unset)', config: { url: FILE, syncUrl: '', sync: { intervalSeconds: 60 } }, ctor: 'refuse', refusedOn: 'sync' },
142152
];
143153

154+
/** The rows the constructor refuses on a sync key: its message is a copy of the spec's (#20200). */
155+
const SYNC_KEY_REFUSALS = ROWS.filter(
156+
(r) => r.ctor === 'refuse' && (r.refusedOn === 'syncUrl' || r.refusedOn === 'sync'),
157+
);
158+
144159
/**
145160
* The driver config a datasource loader builds from an authored one — the
146161
* reading `buildTursoDriverConfig` / `resolveTursoUrl` in
@@ -159,15 +174,15 @@ function driverConfigOf(authored: Authored): TursoDriverConfig {
159174
};
160175
}
161176

162-
function constructorVerdict(authored: Authored): 'accept' | 'refuse' {
177+
function constructorVerdict(authored: Authored): { verdict: 'accept' | 'refuse'; message?: string } {
163178
try {
164179
new TursoDriver(driverConfigOf(authored));
165-
return 'accept';
180+
return { verdict: 'accept' };
166181
} catch (error) {
167182
// A refusal is the ADR-0112 envelope, never some other throw.
168183
expect((error as { code?: string }).code).toBe('VALIDATION_ERROR');
169184
expect((error as { status?: number }).status).toBe(400);
170-
return 'refuse';
185+
return { verdict: 'refuse', message: (error as Error).message };
171186
}
172187
}
173188

@@ -192,18 +207,24 @@ describe('turso config: the constructor, the spec contract and this mirror agree
192207
expect(ROWS.filter((r) => r.ctor === 'accept' && !r.refusedOn).length).toBeGreaterThanOrEqual(20);
193208
expect(ROWS.filter((r) => r.refusedOn === 'url').length).toBeGreaterThanOrEqual(25);
194209
expect(ROWS.filter((r) => r.refusedOn === 'timeoutMs').length).toBeGreaterThanOrEqual(3);
195-
expect(ROWS.filter((r) => r.inert).length).toBeGreaterThanOrEqual(4);
210+
expect(ROWS.filter((r) => r.refusedOn === 'syncUrl').length).toBeGreaterThanOrEqual(3);
211+
expect(ROWS.filter((r) => r.refusedOn === 'sync').length).toBeGreaterThanOrEqual(5);
212+
expect(SYNC_KEY_REFUSALS.length).toBeGreaterThanOrEqual(8);
213+
// [#20200] Exactly zero, not a floor: every key the constructor used to
214+
// build and ignore is refused at construction now (see the header).
215+
expect(ROWS.filter((r) => r.inert).length).toBe(0);
196216
expect(ROWS.filter((r) => !r.config.mode).length).toBeGreaterThanOrEqual(35);
197217
});
198218

199219
describe.each(ROWS)('$name', (row) => {
200220
it(`the constructor ${row.ctor}s it`, () => {
201-
expect(constructorVerdict(row.config)).toBe(row.ctor);
221+
expect(constructorVerdict(row.config).verdict).toBe(row.ctor);
202222
});
203223

224+
204225
it(row.refusedOn ? `the spec contract refuses it on \`${row.refusedOn}\`` : 'the spec contract accepts it', () => {
205226
// Authoring refuses what construction refuses, and nothing it accepts
206-
// but the declared-and-ignored keys.
227+
// but an `inert` row's declared-and-ignored key (none since #20200).
207228
expect(Boolean(row.refusedOn)).toBe(row.ctor === 'refuse' || Boolean(row.inert));
208229
const verdict = schemaVerdict(SpecTursoConfigSchema, row.config);
209230
expect(verdict.refusedOn, verdict.message).toBe(row.refusedOn);
@@ -219,4 +240,16 @@ describe('turso config: the constructor, the spec contract and this mirror agree
219240
expect(mirror).toEqual(spec);
220241
});
221242
});
243+
244+
// [#20200] The two sync refusals are copies of the spec contract's texts in
245+
// `../turso-driver.ts` (the spec keeps them module-local); this is the pin
246+
// that holds each copy equal to the schema's issue, byte for byte.
247+
describe.each(SYNC_KEY_REFUSALS)('$name', (row) => {
248+
it("the constructor's message is the spec contract's, byte for byte (#20200)", () => {
249+
const spec = schemaVerdict(SpecTursoConfigSchema, row.config);
250+
expect(spec.refusedOn).toBe(row.refusedOn);
251+
expect(spec.message).toBeTypeOf('string');
252+
expect(constructorVerdict(row.config).message).toBe(spec.message);
253+
});
254+
});
222255
});

‎packages/drivers/driver-turso/src/spec/turso.zod.ts‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,14 @@ const WASM_RETIRED =
8282
+ 'Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand.';
8383

8484
// ==========================================================================
85-
// 2a. Transport coherence — what `new TursoDriver` refuses or ignores
85+
// 2a. Transport coherence — what `new TursoDriver` refuses
8686
// ==========================================================================
8787
//
8888
// #19977. Each key below parsed on its own, so this mirror accepted
8989
// combinations the driver in this very package refuses at construction
90-
// (`VALIDATION_ERROR` / 400), or constructs and ignores. It now refuses them,
91-
// with the supported spelling in the message. The predicates and the messages
90+
// (`VALIDATION_ERROR` / 400), or, until #20200, constructed and ignored. It
91+
// now refuses them, with the supported spelling in the message. The
92+
// predicates and the messages
9293
// are the ones `@objectstack/spec`'s own turso contract
9394
// (`packages/spec/src/data/driver/turso.zod.ts`, `tursoTransportIssues`)
9495
// carries, byte for byte: the helper is internal to that package and not on
@@ -260,8 +261,8 @@ function tursoTransportIssues(cfg: TursoTransportKeys): TursoTransportIssue[] {
260261
path: 'syncUrl',
261262
message:
262263
"`syncUrl` configures an embedded replica, but `mode: 'remote'` sends every read and write "
263-
+ 'straight to `url` and builds no replica: the turso driver never hands `syncUrl` to the '
264-
+ 'remote client and runs no sync, so the setting changes nothing. For a remote database, '
264+
+ 'straight to `url` and builds no replica: the turso driver refuses this configuration when '
265+
+ 'it starts. For a remote database, '
265266
+ `drop ${syncUrlKeys}. For an embedded replica, drop \`mode\` and point \`url\` at a local `
266267
+ "file beside `syncUrl`: `url: 'file:./data/replica.db'`.",
267268
});
@@ -390,8 +391,8 @@ export const TursoConfigSchema = lazySchema(() => z.object({
390391
+ 'nothing.',
391392
});
392393
}
393-
// What the driver refuses at construction, or constructs and ignores —
394-
// see `tursoTransportIssues` above.
394+
// What the driver refuses at construction — see `tursoTransportIssues`
395+
// above.
395396
for (const issue of tursoTransportIssues(cfg)) {
396397
ctx.addIssue({ code: 'custom', path: [issue.path], message: issue.message });
397398
}

0 commit comments

Comments
 (0)