Skip to content

Test the tripwire refusal's rollback and unwrapped-error guarantee - #1546

Merged
borisno2 merged 2 commits into
mainfrom
claude/jolly-turing-869zgo
Sep 13, 2026
Merged

borisno2 merged 2 commits into
mainfrom
claude/jolly-turing-869zgo

Conversation

@borisno2

Copy link
Copy Markdown
Member

Summary

  • Adds a test on packages/core/src/context/write-transaction.test.ts proving that a tripwire refusal (UnmarkedQueryError, raised by originTripwire in beforeCompile) mid-write rolls back the write's implicit transaction and reaches the caller unwrapped.
  • The refusal is triggered by having an afterOperation hook issue a raw statement directly against context.ormHandle without entering an origin scope (no withOrigin wrap) — hooks run outside the engine's origin scope by construction (ADR-0059), so an unmarked statement issued from one is exactly what the tripwire is designed to refuse.
  • Asserts Object.getPrototypeOf(failure) === UnmarkedQueryError.prototype (prototype identity, not message matching, per the issue's acceptance criteria) and that the error is not a DatabaseError (i.e., not re-classified/wrapped by normalizeDatabaseError).
  • Asserts both the Job and Audit tables are left empty, proving the whole write (not just the refused statement) rolled back.

Why this test design

Since context.db writes now open a real implicit transaction (#1205), and the tripwire fires on any statement compiled with no origin in scope (ADR-0059), the cleanest way to reproduce "the second of several statements is refused by the tripwire" without fabricating middleware internals is to have a hook perform a second, real statement through the raw ORM handle with no origin entered — the same technique the existing "a hook's own write through ormHandle rolls back with the write" test uses, but omitting the withOrigin wrap that test relies on to avoid the refusal.

I verified the test is falsifiable against both regressions it guards:

Both were confirmed locally, then reverted before committing.

Test plan

  • New test added: a tripwire refusal mid-write rolls back and reaches the caller unwrapped
  • pnpm --filter @opensaas/stack-core test — 1758 passed, 7 skipped (no regressions)
  • pnpm lint passes
  • pnpm format / pnpm manypkg fix — no changes needed beyond the new files
  • Changeset added (@opensaas/stack-core, patch)

Closes #1209

🤖 Generated with Claude Code

https://claude.ai/code/session_01LFqWbBhZwZyqBUzLmvbcgs


Generated by Claude Code

…antee

Closes #1209.

A refusal mid-write (UnmarkedQueryError, raised by originTripwire in
beforeCompile) now has a test proving it rolls back the write's implicit
transaction and reaches the caller unwrapped, checked by prototype
identity rather than message matching. Verified falsifiable against both
the pre-#1205 no-transaction regression and a hypothetical error-wrapping
regression in normalizeDatabaseError.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFqWbBhZwZyqBUzLmvbcgs
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@changeset-bot

changeset-bot Bot commented Sep 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 910fb36

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 9 packages
Name Type
@opensaas/stack-core Patch
@opensaas/stack-auth Patch
@opensaas/stack-cli Patch
@opensaas/stack-rag Patch
@opensaas/stack-storage Patch
@opensaas/stack-tiptap Patch
@opensaas/stack-ui Patch
@opensaas/stack-storage-s3 Patch
@opensaas/stack-storage-vercel Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 13, 2026

Copy link
Copy Markdown

Deployment failed for project stack-docs with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/open-saas?upgradeToPro=build-rate-limit

Copy link
Copy Markdown
Member Author

Code review — /review (medium effort)

Reviewed the diff (packages/core/src/context/write-transaction.test.ts + the changeset) against origin/main, including tracing withOriginoriginTripwirerunWriteInTransactionrunWithTransactionBoundarynormalizeDatabaseError/classifyDriverError, and empirically verifying the test's falsifiability by mutating the source it exercises (each mutation reverted before this comment):

  • Neutered refuseUnmarkedQuery to a no-op → the new test fails with TypeError: Cannot convert undefined or null to object at the Object.getPrototypeOf(failure) line (see finding 1 below) — confirms the raw context.ormHandle.Audit.create() call really is refused by the tripwire, and only by it (the identical call succeeds one test above when wrapped in withOrigin('engine', …), which already serves as the implicit control).
  • Made runInTransaction skip opening a real transaction → the Job rows assertion fails with a persisted ship row — confirms the rollback assertion is load-bearing, not vacuously true.
  • Made the transaction-boundary rethrow a fresh plain Error with the same message instead of the original → expect(Object.getPrototypeOf(failure)).toBe(UnmarkedQueryError.prototype) fails while a message-matching assertion would not have — confirms the prototype-identity check is doing real work per User story 12's refusal rollback and unwrapped-error guarantee has no test #1209's acceptance criterion ("provable by prototype identity, not message matching").

Conclusion on the core question: the test exercises exactly what it claims. Hooks run outside the engine's origin scope by construction (origin.ts's withOrigin doc), so the raw create.call(collection, { note }) issued with no withOrigin wrap compiles with originStore.getStore() === undefined and is refused by originTripwire.beforeCompile — not for some unrelated reason (bad field, access denial, etc.). The rollback assertion is real: runWriteInTransaction's throw propagates unmodified out of runInTransaction (Prisma's $transaction callback), through runWithTransactionBoundary's throw txError (no reclassification happens outside the withOrigin('engine', …) catch, and UnmarkedQueryError doesn't match classifyDriverError's driver-error shape anyway), to the caller.

Findings

  1. Minor — opaque failure mode if the regression under test ever recurs (write-transaction.test.ts:262). If the write ever silently stops being refused (tripwire regresses), .then(() => undefined, ...) resolves failure to undefined, and Object.getPrototypeOf(undefined) throws before the expect runs — confirmed above. CI still fails, but with a confusing crash instead of "expected UnmarkedQueryError, got success." A expect(failure).toBeInstanceOf(Error) (or similar) guard before the prototype check would give a clearer signal.
  2. Nit — duplication (write-transaction.test.ts:224-235 vs. 169-180). writeWithNoOrigin is a near-verbatim copy of writeThroughHandle in the test immediately above, differing only in the withOrigin('engine', …) wrap. Not a bug, just maintenance surface — a shared rawAuditCreate(collection, note) helper that each test wraps differently would remove it.

No correctness bugs found, and no coverage gap for the specific guarantee #1209 asks for (rollback + unwrapped, proven by prototype identity). Test suite run locally: all 7 tests in the file pass.


Generated by Claude Code

- Assert failure is an Error before checking its prototype, so a tripwire
  regression fails cleanly instead of crashing on
  Object.getPrototypeOf(undefined).
- Extract writeAuditRow(context, note, { marked }) so the marked and
  unmarked raw-write tests share one helper instead of two near-identical
  copies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFqWbBhZwZyqBUzLmvbcgs

Copy link
Copy Markdown
Member Author

Thanks for the review — both findings addressed in 910fb36:

  • Added expect(failure).toBeInstanceOf(Error) before the prototype-identity check, so a tripwire regression fails the assertion cleanly instead of crashing on Object.getPrototypeOf(undefined).
  • Extracted a shared writeAuditRow(context, note, { marked }) helper so the marked (existing) and unmarked (new) raw-write tests no longer carry two near-identical copies of the same helper.

pnpm --filter @opensaas/stack-core test, pnpm lint, and pnpm format all still pass clean.


Generated by Claude Code

Copy link
Copy Markdown
Member Author

The Vercel status check is failing with "Deployment rate limited — retry in 24 hours" (api-deployments-free-per-day). This is the docs-site preview deployment hitting Vercel's free-tier daily deployment cap — this PR touches only packages/core/src/context/write-transaction.test.ts and a changeset, nothing under docs/, so the failure isn't caused by this change. It's an account-level quota exhausted by the day's overall deployment volume across the repo, not a per-PR/per-commit condition, so re-running it now would not help (it will only succeed once the 24-hour window resets) — there's no fix to port here. Leaving this PR watched; will keep an eye on it independent of this check.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Core Package Coverage (./packages/core)

Status Category Percentage Covered / Total
🟢 Lines 93.11% (🎯 81%) 3814 / 4096
🟢 Statements 91.1% (🎯 76%) 4304 / 4724
🟢 Functions 95.43% (🎯 78%) 837 / 877
🟢 Branches 85.77% (🎯 71%) 2894 / 3374
File CoverageNo changed files found.
Generated in workflow #2489 for commit 910fb36 by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for UI Package Coverage (./packages/ui)

Status Category Percentage Covered / Total
🔵 Lines 78.7% 244 / 310
🔵 Statements 78.43% 251 / 320
🔵 Functions 69.81% 74 / 106
🔵 Branches 67.51% 160 / 237
File CoverageNo changed files found.
Generated in workflow #2489 for commit 910fb36 by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for CLI Package Coverage (./packages/cli)

Status Category Percentage Covered / Total
🔵 Lines 82.28% 1992 / 2421
🔵 Statements 82% 2142 / 2612
🔵 Functions 87.91% 342 / 389
🔵 Branches 75.17% 1066 / 1418
File CoverageNo changed files found.
Generated in workflow #2489 for commit 910fb36 by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Auth Package Coverage (./packages/auth)

Status Category Percentage Covered / Total
🔵 Lines 91.2% 280 / 307
🔵 Statements 89.94% 313 / 348
🔵 Functions 96.05% 73 / 76
🔵 Branches 82.38% 262 / 318
File CoverageNo changed files found.
Generated in workflow #2489 for commit 910fb36 by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Storage Package Coverage (./packages/storage)

Status Category Percentage Covered / Total
🔵 Lines 90.11% 301 / 334
🔵 Statements 90.27% 334 / 370
🔵 Functions 96.9% 94 / 97
🔵 Branches 86.44% 319 / 369
File CoverageNo changed files found.
Generated in workflow #2489 for commit 910fb36 by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for RAG Package Coverage (./packages/rag)

Status Category Percentage Covered / Total
🔵 Lines 91.98% 597 / 649
🔵 Statements 91.35% 655 / 717
🔵 Functions 97.54% 119 / 122
🔵 Branches 85.65% 424 / 495
File CoverageNo changed files found.
Generated in workflow #2489 for commit 910fb36 by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Storage S3 Package Coverage (./packages/storage-s3)

Status Category Percentage Covered / Total
🔵 Lines 100% 40 / 40
🔵 Statements 100% 40 / 40
🔵 Functions 100% 9 / 9
🔵 Branches 100% 19 / 19
File CoverageNo changed files found.
Generated in workflow #2489 for commit 910fb36 by the Vitest Coverage Report Action

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for Storage Vercel Package Coverage (./packages/storage-vercel)

Status Category Percentage Covered / Total
🔵 Lines 100% 68 / 68
🔵 Statements 100% 71 / 71
🔵 Functions 100% 15 / 15
🔵 Branches 97.87% 46 / 47
File CoverageNo changed files found.
Generated in workflow #2489 for commit 910fb36 by the Vitest Coverage Report Action

@borisno2
borisno2 merged commit 721b7c0 into main Sep 13, 2026
6 of 7 checks passed
@borisno2
borisno2 deleted the claude/jolly-turing-869zgo branch September 13, 2026 21:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

User story 12's refusal rollback and unwrapped-error guarantee has no test

2 participants