Skip to content

drivers(turso): the unbacked-conflict-target refusal has no cross-face parity pin — the two wordings can drift without a test noticing #8568

Closed

Description

@os-zhuang

Filed out of #8445. Observation-class: a test-coverage gap, no runtime defect. Unassigned.

The state after #8413 and #8445

One condition — a conflictKeys upsert whose target no unique index backs — is now refused on both TursoDriver faces, and by #5240 both must say it in one wording:

TursoDriver picks its face from url, so both can answer in one deployment and a reword of either makes the answer a property of the connection string.

The gap

Nothing compares the two. What exists:

So a future PR touching one package can drift the pair and both suites stay green. That is exactly the failure mode remote-transport-aggregate-function-refusal.test.ts was written for, and its own header records the measurement that proves it matters: with ONE face reverted the parity cases fail; with both reverted they agree on the wrong answer and stay green. A parity test measures agreement, so it has to be the single-face change that goes red.

The shape of the fix

Add a local-face case to turso-local-remote-unique-parity.test.ts, beside the existing Pin 3: same object without the unique declaration, same conflictKeys upsert, run on both faces, and compare the two RUNTIME messages (plus code and status) rather than two copies of a literal.

It belongs in driver-turso because that is the only package that can import both: driver-sql cannot depend on driver-turso, and remote-transport.ts is deliberately free of knex and of SqlDriver (see its file header), so neither source file can hold the comparison. #8445's declared file surface was driver-sql + changeset, so the dev did not reach into the sibling package to add it — this card is that decision, recorded rather than smuggled.

Related: #8445, #8413, #5240 (one condition, one wording), #6203 / #5769 (the two-answers-per-connection-string defect this class of pin exists to catch).


Generated by Claude Code

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 13, 2026
  2. added
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 13, 2026
  3. added theissue type on Aug 13, 2026
  4. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Triage: promoted finding → pm:queue at first touch; lands in packages/drivers/driver-turso (turso-local-remote-unique-parity.test.ts) → domain:drivers, type Task.

    Rationale: this is a test-only pin with a fully specified shape (add a local-face case beside the existing remote-only Pin 3, compare the two RUNTIME messages plus code+status instead of two literals) and no open question — exactly the "test-only pin, nothing to ask" class that skips the findings holding pattern. The invariant it protects (#5240: one condition, one wording across both TursoDriver faces) is currently guarded one-way only, so a reword of either package drifts the pair with every suite green.

    Serial-constraint note for the executing seat: same package family as #8567 (both grew out of #8445) — do not run them in one batch if #8567's chosen home ends up touching driver-turso's parity suite; otherwise they are file-disjoint (#8567 is driver-sql/types-side, this card is driver-turso tests only).

    Size/model suggestion: S (mechanical, gate-judged), mode:subagent, model: sonnet.

    Triage seat Routine, session session_01KKqo1q7JddqjVbabF1ujMj.


    Generated by Claude Code

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 13, 2026
  6. self-assigned this
    on Aug 13, 2026
  7. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop, drivers lane
    Session: session_01VoxQqG5FiUHZKCST7KDoZC
    Branch: claude/issue-8568-cross-face-refusal-parity-pin
    Worktree: objectstack-issue-8568
    Domain: domain:drivers
    File surface: packages/drivers/driver-turso/src/turso-local-remote-unique-parity.test.ts + .changeset/ or the skip-changeset label (test-only — the dev decides and states which) (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus — deviating from triage's sonnet, reasoned below
    Serial constraints cleared: this file was last moved by #8413 (1d1ca8e99, MERGED). #8567 is pm:queue but unclaimed and undispatched, so no overlap today; per triage's note the two stay file-disjoint unless #8567's chosen home reaches this suite — if I dispatch #8567 later I will hold it serial against this branch rather than batch them.

    Premise verified on origin/main @ 2342ee42

    • The parity suite exists with 8 pins; the refusal pin at :223 ("answers a real code and status — never a raw SqliteError") is the remote-only one this card extends.
    • driver-turso depends on @objectstack/driver-sql (workspace:*) — confirmed in its package.json. That is the fact the card's placement argument rests on: this package can import both faces, and driver-sql cannot import back.

    Why opus rather than triage's sonnet

    Triage graded it "mechanical, gate-judged". I don't think the failure mode is a missed gate. The entire product of this card is a pin that actually tests the invariant — and a wrong pin is green. The specific way to get it wrong is well-marked: asserting each face's message against its own literal instead of asserting the two runtime messages against each other. The literal form passes forever while the two faces drift, which is the exact defect being fixed, re-created inside its own fix.

    That is judgment in the design, not correctness a gate adjudicates — the S-but-not-mechanical case that takes M treatment. This card exists because a previous pin was subtly one-way; spending a tier to avoid producing a second one is the cheaper side of that trade.


    Generated by Claude Code

  8. os-zhuang commented on Aug 13, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 8568,
      "status": "done",
      "branch": "claude/issue-8568-cross-face-refusal-parity-pin",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/8579",
      "premise_still_valid": true,
      "summary": "Added the cross-face parity pin as Pin 4 of packages/drivers/driver-turso/src/turso-local-remote-unique-parity.test.ts: one condition (a conflictKeys upsert on an object with no unique declaration) raised on BOTH TursoDriver faces, with the two RUNTIME errors compared to EACH OTHER — expect(localErr!.message).toBe(remoteErr!.message) plus the same form for .code and .status (file lines 352-354) — never each against its own literal. Two guards make the agreement mean something: a refusal guard before the comparison (so a face that stopped refusing reports the divergence rather than a TypeError on null), and an anchor on the agreed value (VALIDATION_ERROR / 400 and the SQLite text surviving as cause on both faces), because two raw SqliteErrors would otherwise agree perfectly on code 'SQLITE_ERROR' and status undefined. A two-face positive control mirrors Pin 3's: both faces must still MERGE on a backed target, so the pin cannot be satisfied by refusing every conflictKeys upsert. Test-only: git diff origin/main --stat is the one test file, 117 insertions, 1 deletion (the header's 'three pins' heading); sql-driver.ts and remote-transport.ts are untouched. The existing 8 pins are unmodified.",
      "tests": "AFFECTED PACKAGE, on the final tree — pnpm --filter @objectstack/driver-turso test: 'Test Files  37 passed (37) / Tests  986 passed (986)'; pnpm --filter @objectstack/driver-turso typecheck: 'tsc --noEmit' exited clean with no diagnostics. Parity file alone before the reverse legs: 'Test Files  1 passed (1) / Tests  10 passed (10)' (8 pre-existing pins + 2 new). Build closure first, per the fresh-worktree rule: pnpm --filter '@objectstack/driver-turso^...' build. REVERSE VERIFICATION, direction predicted per leg BEFORE running, both legs run from the COMMITTED state and restored with git checkout HEAD -- path. LEG 1 (reword LOCAL face only, driver-sql refuseUnbackedConflictTarget, 'Cannot upsert into' -> 'Refusing to upsert into'): parity pin RED — \"AssertionError: expected 'Refusing to upsert into \\\"crm_contact_…' to be 'Cannot upsert into \\\"crm_contact_plain…' // Object.is equality\" at src/turso-local-remote-unique-parity.test.ts:333, totals 'Tests  1 failed | 9 passed (10)'; the 8 pre-existing pins and the new positive control stayed GREEN. In the same leg #8445's own suite also went red — 'x opens with the remote refusal's first sentence, word for word', 'Tests  1 failed | 5 passed (6)' — which is the literal a drifting author updates in the same commit. LEG 2 (restore, then reword REMOTE face only, driver-turso remote-transport.ts, same substitution): parity pin RED again — \"AssertionError: expected 'Cannot upsert into \\\"crm_contact_plain…' to be 'Refusing to upsert into \\\"crm_contact_…'\", totals 'Tests  1 failed | 9 passed (10)'; and the reading that matters — EVERY pre-existing pin stayed green, Pin 3's remote refusal included, because its assertions are token regexes (/crm_contact_plain/, /email/, /unique/i) that this reword preserves; #8445's suite stayed fully green at 'Tests  6 passed (6)', being blind to the remote face by construction. Two-way redness confirmed. MEASURED TRAP, now recorded in the file header: the local face arrives through the BUILT @objectstack/driver-sql (driver-turso resolves the workspace dep to its dist; no vitest alias to src), so leg 1 first ran GREEN ('Tests 10 passed (10)') against a stale dist and only went red after pnpm --filter @objectstack/driver-sql build — a false green that reads exactly like a pin that does not work. GATES — derived by node scripts/pm/dispatch-gates.mjs against the actual changed path. It named, path-matched: check:test-source-alias, check:type-source-resolution [both lint.yml]; convention-matched (adds/edits a test file): check:query-options-erasure, check:type-check-coverage; plus the standing any-edit gate check:nul-bytes. RAN, all green: check:nul-bytes 'OK (scanned 7735 text file(s) ... no raw ASCII control bytes)' (+ its 75-assertion self-test); check:test-source-alias 'OK — 72 packages with tests scanned'; check:type-source-resolution 'OK — 76 packages with a tsconfig.json scanned'; check:query-options-erasure 'ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new ... test surface: 240 site(s) in 47 file(s) — at the ceiling ... baseline key set verified against ec74646: no files added'. NOT RUN, stated rather than implied: check:type-check-coverage — its input for this change is the package's test-layer typecheck, which is clean above, so its ratchet cannot have moved; CI's required TypeScript Type Check job carries it. Control-byte self-scan beyond the gate on the changed file: grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' returned no matches. CI at report time (not waited on, per the report-at-draft-PR-time rule): Check Changeset success (after the label), Check PR Size success, Console Pin Freshness success, ADR maintainer approval success, 'No other open PR may claim the same issue' success, 'Part-of PR must not also close its card' success; ESLint, TypeScript Type Check, Test Core and the Dogfood gates still in_progress.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Changeset decision — the skip-changeset label, no changeset file. The diff is one test file; nothing user-visible ships, so a declaring changeset would announce a release note for a change no consumer can observe, and check-empty-changeset bans the stub form. Label applied by me right after the PR existed, written as the union with what the bots had already put there, then read back once they settled: ["size/m","skip-changeset"]. Check Changeset concluded success at 23:19:56, after the label write. Reviewers should not look for a missing .changeset/*.md.

    Sanitizer check (requested). No damage found on #8568: the issue body, triage comment 5287058071 and the claim comment 5287308858 all read complete, each ending in its intact attribution footer, with no truncated prose and no <+letter spans that the body sanitizer would have eaten. The &#39; sequences visible in the issue and PR bodies through the GitHub MCP read path are the documented read-path artifact, not stored damage. One caveat on method: gh is not installed in this container, so the cross-check was done through the MCP read path plus the PR body read back after creation (its fenced expect(...) block survived byte-for-byte), not through a second independent client.

    Scope. Test-only, as dispatched — no production code touched, no fork encountered. The two deliberate rewords used for reverse verification were restored from the commit and verified: git diff origin/main --stat shows only the parity test file, and git worktree remove succeeded unforced on a clean tree.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions