Skip to content

Commit a3b1d26

Browse files
os-zhuangclaude
andauthored
test(driver-turso): pin the unbacked-conflict-target refusal across both faces (#8568) (#8579)
The refusal is pinned on the remote face here and on the local face in driver-sql's own suite, each against its OWN literal — so a reword of one package that updates its own literal drifts the pair with every suite green. TursoDriver picks its face from url, so that drift makes the answer to one condition a property of the connection string (#5240). Adds a local-face case beside Pin 3 that raises the same condition on both faces and compares the two RUNTIME errors to each other (message, code, status), plus a two-face positive control so the agreement cannot be bought by refusing every conflictKeys upsert. Claude-Session: https://claude.ai/code/session_01VoxQqG5FiUHZKCST7KDoZC Co-authored-by: Claude <noreply@anthropic.com>
1 parent 916279e commit a3b1d26

1 file changed

Lines changed: 117 additions & 1 deletion

File tree

packages/drivers/driver-turso/src/turso-local-remote-unique-parity.test.ts

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
* reads them. The divergence itself is the defect, so the divergence is what is
3434
* pinned: same declaration, same write, both faces, one assertion.
3535
*
36-
* # The three pins, and what each one alone would miss
36+
* # The four pins, and what each one alone would miss
3737
*
3838
* - **The DDL pin** asserts the UNIQUE index the remote face emits for a
3939
* `unique: true` column, by name and by key. Without it a future rewrite of
@@ -49,10 +49,20 @@
4949
* `toThrow()` here is blind in both directions: it stays green on the raw
5050
* `SqliteError` this card exists to remove.
5151
*
52+
* - **The parity pin** [#8568] asserts the two faces' refusals against EACH
53+
* OTHER — message, `code` and `status` — on one condition raised twice.
54+
* Every pin above it, and #8445's local-face suite, is single-face and pins
55+
* its own wording as a LITERAL: a reword of one package with its own literal
56+
* updated alongside it drifts the pair while every suite stays green. That
57+
* is the state this file was written to make impossible, reached from the
58+
* one direction it had left open.
59+
*
5260
* The refusal pin carries a **positive control** beside it: with the unique
5361
* index present, the same `conflictKeys` upsert MERGES. Without that half, a
5462
* transport that refused every `conflictKeys` upsert unconditionally would pass
5563
* the refusal pin — and would have broken the capability instead of fixing it.
64+
* The parity pin carries the same control for the same reason, doubled: two
65+
* faces that refused everything would agree perfectly.
5666
*
5767
* # Reverse verification, direction predicted BEFORE it was run
5868
*
@@ -63,6 +73,24 @@
6373
* The refusal pin's negative half stays GREEN across the revert: its table
6474
* never had a unique index to lose, which is what makes it the pin for the
6575
* already-created-table case rather than a second copy of the DDL pin.
76+
*
77+
* The parity pin [#8568] has TWO legs to predict, because two-way redness is
78+
* the entire property being bought: rewording ONLY `driver-sql`'s helper must
79+
* turn it red, and rewording ONLY `driver-turso`'s must turn it red as well. A
80+
* pin that reddens on one leg only is the one-way pin this card replaced. Both
81+
* legs were measured (`1 failed | 9 passed` each, the failure being the message
82+
* comparison in both). The REMOTE leg is the one worth reading: under it every
83+
* pre-existing pin in this file stayed GREEN, the refusal pin included — its
84+
* assertions are token regexes (`/crm_contact_plain/`, `/email/`, `/unique/i`)
85+
* that a reword preserving those words walks straight past, which is exactly
86+
* how the drift could have landed unnoticed.
87+
*
88+
* ⚠️ One measured trap for whoever runs that verification again: the LOCAL face
89+
* arrives here through the BUILT `@objectstack/driver-sql` (this package
90+
* resolves the workspace dependency to its `dist`, and there is no vitest alias
91+
* to `src`). A reworded `sql-driver.ts` therefore changes nothing until that
92+
* package is rebuilt — leg 1 ran GREEN against a stale `dist` before the
93+
* rebuild, which reads exactly like a pin that does not work.
6694
*/
6795

6896
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
@@ -277,4 +305,92 @@ describe('[#8413] `unique` is enforced on BOTH TursoDriver faces', () => {
277305
expect(rows[0].title).toBe('second');
278306
});
279307
});
308+
309+
// ─────────────────────────────────────────────────────────────────────
310+
// Pin 4 — the two refusals held against EACH OTHER
311+
// ─────────────────────────────────────────────────────────────────────
312+
313+
describe('[#8568] the unbacked-conflict-target refusal is ONE answer across both faces', () => {
314+
/**
315+
* Pin 3 above asserts the refusal on the REMOTE face; #8445's
316+
* `sql-driver-upsert-conflict-target-envelope.test.ts` asserts it on the
317+
* LOCAL one. Both are single-face by construction, and each pins its own
318+
* wording as a LITERAL — so a reword of one package, with its own literal
319+
* updated in the same commit, drifts the pair while every suite stays
320+
* green. `TursoDriver` picks its face from `url`, so that drift makes the
321+
* answer to one condition a property of the connection string (#5240: one
322+
* condition, one wording; the defect class of #6203 / #5769).
323+
*
324+
* ⚠️ The assertion below is therefore two RUNTIME answers compared to EACH
325+
* OTHER — never each against a literal. The literal form is what this pin
326+
* exists to replace: it passes forever while the faces diverge. A parity
327+
* test measures agreement, so the change that must turn it red is the
328+
* SINGLE-face one, and reverse verification has to be run on each face
329+
* separately to prove it (both legs measured — see the header's reverse
330+
* verification section, including the stale-`dist` trap on the local leg).
331+
*/
332+
it('answers the same message, `code` and `status` on the local and the remote face', async () => {
333+
await local.initObjects([{ ...CONTACT_NO_UNIQUE, fields: { ...CONTACT_NO_UNIQUE.fields } }]);
334+
await remote.initObjects([{ ...CONTACT_NO_UNIQUE, fields: { ...CONTACT_NO_UNIQUE.fields } }]);
335+
336+
// One condition, raised twice: the same object with no `unique`
337+
// declaration, the same `conflictKeys` upsert, once per face.
338+
const localErr = await captureError(() =>
339+
local.upsert(CONTACT_NO_UNIQUE.name, { email: 'a@b.com', title: 'x' }, ['email'], { bypassTenantAudit: true }),
340+
);
341+
const remoteErr = await captureError(() =>
342+
remote.upsert(CONTACT_NO_UNIQUE.name, { email: 'a@b.com', title: 'x' }, ['email']),
343+
);
344+
345+
// Neither face may ACCEPT it — without this guard a face that stopped
346+
// refusing would reach the comparison below holding `null`, and the
347+
// message equality would report a `TypeError` rather than the divergence.
348+
expect({ local: localErr !== null, remote: remoteErr !== null }).toEqual({ local: true, remote: true });
349+
350+
// The comparison this pin is for. `toBe` between two runtime values:
351+
// rewording either compiler alone turns it red, whichever one it is.
352+
expect(localErr!.message).toBe(remoteErr!.message);
353+
expect(localErr!.code).toBe(remoteErr!.code);
354+
expect(localErr!.status).toBe(remoteErr!.status);
355+
356+
// …and the agreement is ANCHORED, because agreement alone is satisfied by
357+
// both faces regressing together: two raw SqliteErrors agree on
358+
// `code: 'SQLITE_ERROR'` and on `status: undefined` just as well. What is
359+
// worth pinning is agreement ON THE ADR-0112 ENVELOPE. This is a value
360+
// assertion, not a wording literal — an identical reword of BOTH faces
361+
// keeps #5240 satisfied and is meant to stay green here.
362+
expect(localErr!.code).toBe(StandardErrorCode.enum.VALIDATION_ERROR);
363+
expect(localErr!.status).toBe(400);
364+
365+
// Both faces also keep the SQLite ground truth as `cause` rather than
366+
// replacing it — the same shape on both, so an operator debugging either
367+
// deployment reads the same two layers.
368+
for (const err of [localErr!, remoteErr!]) {
369+
expect(String((err as unknown as { cause?: Error }).cause?.message)).toMatch(
370+
/ON CONFLICT clause does not match/i,
371+
);
372+
}
373+
});
374+
375+
/**
376+
* The positive control for the parity pin, mirroring Pin 3's: a pair of
377+
* faces that refused EVERY `conflictKeys` upsert would agree perfectly and
378+
* satisfy the assertion above. Agreement is only worth pinning while the
379+
* capability still works on both — so the backed target must MERGE on both.
380+
*/
381+
it('and both faces still MERGE when a declared unique index does back the target', async () => {
382+
await local.initObjects([{ ...CONTACT, fields: { ...CONTACT.fields } }]);
383+
await remote.initObjects([{ ...CONTACT, fields: { ...CONTACT.fields } }]);
384+
385+
for (const title of ['first', 'second']) {
386+
await local.upsert(CONTACT.name, { email: 'a@b.com', title }, ['email'], { bypassTenantAudit: true });
387+
await remote.upsert(CONTACT.name, { email: 'a@b.com', title }, ['email']);
388+
}
389+
390+
expect(await local.count(CONTACT.name, {})).toBe(1);
391+
expect(await remote.count(CONTACT.name, {})).toBe(1);
392+
expect((await local.find(CONTACT.name, {}))[0].title).toBe('second');
393+
expect((await remote.find(CONTACT.name, {}))[0].title).toBe('second');
394+
});
395+
});
280396
});

0 commit comments

Comments
 (0)