Files
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

187 lines
5.6 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
const { mockError } = vi.hoisted(() => ({ mockError: vi.fn() }))
vi.mock('@sim/logger', () => ({
createLogger: () => ({
error: mockError,
warn: vi.fn(),
info: vi.fn(),
debug: vi.fn(),
}),
}))
import {
instrumentPoolClient,
isInsideDbTransaction,
runOutsideTransactionContext,
} from './tx-tripwire'
interface FakeReserved {
unsafe: (query: string) => Promise<unknown[]>
}
function createFakeClient() {
const rootQueries: string[] = []
const reservedQueries: string[] = []
const client = instrumentPoolClient(
{
unsafe(query: string) {
rootQueries.push(query)
return Promise.resolve([])
},
// Mirrors postgres-js: begin issues its internal BEGIN through the root
// client's unsafe (the instrumented one) before running the callback on
// a reserved connection.
begin(...args: unknown[]) {
const callback = args[args.length - 1] as (reserved: FakeReserved) => unknown
const reserved: FakeReserved = {
unsafe: (query: string) => {
reservedQueries.push(query)
return Promise.resolve([])
},
}
return Promise.resolve(this.unsafe('begin')).then(() => callback(reserved))
},
},
'test-pool'
)
return { client, rootQueries, reservedQueries }
}
afterEach(() => {
vi.unstubAllEnvs()
mockError.mockClear()
})
describe('tx tripwire', () => {
it('marks the transaction context for the duration of a begin callback', async () => {
const { client } = createFakeClient()
expect(isInsideDbTransaction()).toBe(false)
await client.begin(async () => {
expect(isInsideDbTransaction()).toBe(true)
await Promise.resolve()
expect(isInsideDbTransaction()).toBe(true)
})
expect(isInsideDbTransaction()).toBe(false)
})
it('throws when the root client is queried inside a transaction callback, at any await depth', async () => {
const { client } = createFakeClient()
const deeplyNestedHelper = async () => {
await Promise.resolve()
return client.unsafe('select 1 as nested_checkout')
}
await expect(
client.begin(async () => {
await deeplyNestedHelper()
})
).rejects.toThrow(/inside a transaction callback/)
})
it('allows queries on the reserved connection inside the callback', async () => {
const { client, reservedQueries } = createFakeClient()
await client.begin(async (reserved: FakeReserved) => {
await reserved.unsafe('select 1 as tx_handle_query')
})
expect(reservedQueries).toEqual(['select 1 as tx_handle_query'])
})
it('allows root-client queries outside any transaction', async () => {
const { client, rootQueries } = createFakeClient()
await client.unsafe('select 1 as plain_query')
expect(rootQueries).toEqual(['select 1 as plain_query'])
})
it('throws when a nested transaction is opened on the root client', async () => {
const { client } = createFakeClient()
await expect(
client.begin(async () => {
await client.begin(async () => {})
})
).rejects.toThrow(/nested transaction/i)
})
it('runOutsideTransactionContext escapes lazy thenables awaited by the caller', async () => {
const { client, rootQueries } = createFakeClient()
await client.begin(async () => {
// Mirrors a drizzle query builder: no work until .then is invoked. The
// helper must assimilate it inside the exited context so the caller's
// await (inside the tx context) does not trip the wire.
const lazyQuery = {
then<T>(resolve: (value: T) => void) {
resolve(client.unsafe('select 1 as lazy_escaped') as T)
},
}
await runOutsideTransactionContext(() => lazyQuery)
})
expect(rootQueries).toEqual(['begin', 'select 1 as lazy_escaped'])
})
it('runOutsideTransactionContext escapes the context, including scheduled promises', async () => {
const { client, rootQueries } = createFakeClient()
await client.begin(async () => {
await runOutsideTransactionContext(() => {
expect(isInsideDbTransaction()).toBe(false)
return Promise.resolve().then(() => client.unsafe('select 1 as escaped_query'))
})
expect(isInsideDbTransaction()).toBe(true)
})
expect(rootQueries).toEqual(['begin', 'select 1 as escaped_query'])
})
it('DB_TX_TRIPWIRE=off disables detection', async () => {
vi.stubEnv('DB_TX_TRIPWIRE', 'off')
const { client, rootQueries } = createFakeClient()
await client.begin(async () => {
await client.unsafe('select 1 as off_mode_query')
})
expect(rootQueries).toEqual(['begin', 'select 1 as off_mode_query'])
expect(mockError).not.toHaveBeenCalled()
})
it('DB_TX_TRIPWIRE=warn logs instead of throwing and dedupes repeats', async () => {
vi.stubEnv('DB_TX_TRIPWIRE', 'warn')
const { client, rootQueries } = createFakeClient()
await client.begin(async () => {
await client.unsafe('select 1 as warn_mode_query')
await client.unsafe('select 1 as warn_mode_query')
})
expect(rootQueries).toEqual([
'begin',
'select 1 as warn_mode_query',
'select 1 as warn_mode_query',
])
expect(mockError).toHaveBeenCalledTimes(1)
expect(mockError.mock.calls[0][1]).toMatchObject({ poolName: 'test-pool' })
})
it('DB_TX_TRIPWIRE=warn reports a nested transaction exactly once', async () => {
vi.stubEnv('DB_TX_TRIPWIRE', 'warn')
const { client } = createFakeClient()
await client.begin(async () => {
await client.begin(async () => {})
})
expect(mockError).toHaveBeenCalledTimes(1)
})
})