mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement(codebase): migrate tests to dbChainMock, extract react-query hooks (#4235)
* improvement(codebase): migrate tests to dbChainMock, extract react-query hooks Migrate 97 test files to centralized dbChainMock/dbChainMockFns helpers from @sim/testing — removes hoisted chain-wiring boilerplate. Extend dbChainMock to cover insert/update/delete/transaction/execute patterns. Extract useGitHubStars and useVoiceSettings react-query hooks from inline fetches. Centralize additional mocks (authMockFns, hybridAuthMockFns) and update docs. * fix(github-stars): centralize fallback via initialData, remove stale constants Move the placeholder star count into useGitHubStars as initialData with initialDataUpdatedAt: 0 so `data` is always a narrowed string while still refetching on mount. Fixes two Bugbot issues: stale '25.8k' in chat.tsx (vs '27.8k' in navbar) and empty-string return in fetchGitHubStars that bypassed `??` fallbacks in consumers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(testing): wire dbChainMock.db to shared transaction and execute fns dbChainMock.db.transaction was an inline vi.fn() separate from the exported dbChainMockFns.transaction, so dbChainMockFns.transaction.mockResolvedValueOnce and assertions silently targeted the wrong instance. dbChainMock.db also omitted execute, so tests for any module that calls db.execute (logging-session, table service, billing balance) would throw TypeError. Both mocks now reference the module-level constants so overrides and resetDbChainMock affect the same fn. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(chat,testing): memoize welcome message and add selectDistinct to dbChainMock.db Why: - Welcome ChatMessage was rebuilt inline each render, producing a fresh timestamp and new array identity — cascading to ChatMessageContainer and VoiceInterface props on every tick. - dbChainMockFns exports selectDistinct/selectDistinctOn but the dbChainMock.db object omitted them, so tests that stub those builders hit undefined on the mocked module. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(chat): re-attach scroll listener once container mounts The scroll effect's empty dep array meant it ran only on the first render, when `chatConfig` is still loading and the component returns `<ChatLoadingState />` — so `messagesContainerRef.current` was null and the listener was never attached. Depend on the gating conditions that control which tree renders, so the effect re-runs once the real container is in the DOM (and re-attaches when toggling in/out of voice mode). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(chat): reset chat state on identifier change via key prop Keying `<ChatClient>` on `identifier` guarantees a full remount on route transitions between chats, so `conversationId`, `messages`, and every other piece of local state start fresh — no reset effect required. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
1ced54a77c
commit
5cf7e8d546
@@ -56,6 +56,149 @@ export function createMockSqlOperators() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-wired chain of vi.fn()s for drizzle-style DB queries.
|
||||
*
|
||||
* Each builder step is a stable, module-level `vi.fn()` — safe to reference
|
||||
* inside hoisted `vi.mock()` factories (same pattern as `authMockFns`). Chains
|
||||
* are wired at module load time:
|
||||
*
|
||||
* - `select().from().where()` → returns a builder with `.limit` / `.orderBy` /
|
||||
* `.returning` / `.groupBy` terminals
|
||||
* - `select().from().innerJoin()|leftJoin()` → returns the same where-builder
|
||||
* - `insert().values().returning()` / `update().set().where()` / `delete().where()`
|
||||
*
|
||||
* Terminals (`limit`, `orderBy`, `returning`, `groupBy`, `values`) default to
|
||||
* resolving `[]` (or `undefined` for `values`). Override per-test with
|
||||
* `dbChainMockFns.limit.mockResolvedValueOnce([...])`.
|
||||
*
|
||||
* `vi.clearAllMocks()` clears call history but preserves default wiring. Tests
|
||||
* that replace a wiring with `mockReturnValue(...)` (not `...Once`) must re-wire
|
||||
* in their own `beforeEach`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { dbChainMock, dbChainMockFns } from '@sim/testing'
|
||||
* vi.mock('@sim/db', () => dbChainMock)
|
||||
*
|
||||
* it('finds rows', async () => {
|
||||
* dbChainMockFns.limit.mockResolvedValueOnce([{ id: 'w-1' }])
|
||||
* // ... exercise code that hits db.select().from().where().limit() ...
|
||||
* expect(dbChainMockFns.where).toHaveBeenCalled()
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
const limit = vi.fn(() => Promise.resolve([] as unknown[]))
|
||||
const orderBy = vi.fn(() => Promise.resolve([] as unknown[]))
|
||||
const returning = vi.fn(() => Promise.resolve([] as unknown[]))
|
||||
const groupBy = vi.fn(() => Promise.resolve([] as unknown[]))
|
||||
const execute = vi.fn(() => Promise.resolve([] as unknown[]))
|
||||
|
||||
const onConflictDoUpdate = vi.fn(() => ({ returning }) as unknown as Promise<void>)
|
||||
const onConflictDoNothing = vi.fn(() => ({ returning }) as unknown as Promise<void>)
|
||||
|
||||
const whereBuilder = () => ({ limit, orderBy, returning, groupBy })
|
||||
const where = vi.fn(whereBuilder)
|
||||
|
||||
const joinBuilder = (): { where: typeof where; innerJoin: any; leftJoin: any } => ({
|
||||
where,
|
||||
innerJoin,
|
||||
leftJoin,
|
||||
})
|
||||
const innerJoin: ReturnType<typeof vi.fn> = vi.fn(joinBuilder)
|
||||
const leftJoin: ReturnType<typeof vi.fn> = vi.fn(joinBuilder)
|
||||
const from = vi.fn(joinBuilder)
|
||||
|
||||
const select = vi.fn(() => ({ from }))
|
||||
const selectDistinct = vi.fn(() => ({ from }))
|
||||
const selectDistinctOn = vi.fn(() => ({ from }))
|
||||
const values = vi.fn(() => ({ returning, onConflictDoUpdate, onConflictDoNothing }))
|
||||
const insert = vi.fn(() => ({ values }))
|
||||
const set = vi.fn(() => ({ where }))
|
||||
const update = vi.fn(() => ({ set }))
|
||||
const del = vi.fn(() => ({ where }))
|
||||
const transaction: ReturnType<typeof vi.fn> = vi.fn(
|
||||
async (cb: (tx: typeof dbChainMock.db) => unknown) => cb(dbChainMock.db)
|
||||
)
|
||||
|
||||
export const dbChainMockFns = {
|
||||
select,
|
||||
selectDistinct,
|
||||
selectDistinctOn,
|
||||
from,
|
||||
where,
|
||||
limit,
|
||||
orderBy,
|
||||
returning,
|
||||
innerJoin,
|
||||
leftJoin,
|
||||
groupBy,
|
||||
execute,
|
||||
insert,
|
||||
values,
|
||||
onConflictDoUpdate,
|
||||
onConflictDoNothing,
|
||||
update,
|
||||
set,
|
||||
delete: del,
|
||||
transaction,
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-applies the default chain wiring to every `dbChainMockFns` entry. Call
|
||||
* this in `beforeEach` (after `vi.clearAllMocks()`) if any test uses
|
||||
* `mockReturnValue` / `mockResolvedValue` (permanent overrides) — this
|
||||
* guarantees the next test starts with fresh defaults.
|
||||
*
|
||||
* Not needed if tests exclusively use the `...Once` variants, since those
|
||||
* auto-expire after one call.
|
||||
*/
|
||||
export function resetDbChainMock(): void {
|
||||
select.mockImplementation(() => ({ from }))
|
||||
selectDistinct.mockImplementation(() => ({ from }))
|
||||
selectDistinctOn.mockImplementation(() => ({ from }))
|
||||
from.mockImplementation(joinBuilder)
|
||||
innerJoin.mockImplementation(joinBuilder)
|
||||
leftJoin.mockImplementation(joinBuilder)
|
||||
where.mockImplementation(whereBuilder)
|
||||
insert.mockImplementation(() => ({ values }))
|
||||
values.mockImplementation(() => ({ returning, onConflictDoUpdate, onConflictDoNothing }))
|
||||
onConflictDoUpdate.mockImplementation(() => ({ returning }) as unknown as Promise<void>)
|
||||
onConflictDoNothing.mockImplementation(() => ({ returning }) as unknown as Promise<void>)
|
||||
update.mockImplementation(() => ({ set }))
|
||||
set.mockImplementation(() => ({ where }))
|
||||
del.mockImplementation(() => ({ where }))
|
||||
limit.mockImplementation(() => Promise.resolve([] as unknown[]))
|
||||
orderBy.mockImplementation(() => Promise.resolve([] as unknown[]))
|
||||
returning.mockImplementation(() => Promise.resolve([] as unknown[]))
|
||||
groupBy.mockImplementation(() => Promise.resolve([] as unknown[]))
|
||||
execute.mockImplementation(() => Promise.resolve([] as unknown[]))
|
||||
transaction.mockImplementation(async (cb: (tx: typeof dbChainMock.db) => unknown) =>
|
||||
cb(dbChainMock.db)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Static mock module for `@sim/db` backed by `dbChainMockFns`.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* vi.mock('@sim/db', () => dbChainMock)
|
||||
* ```
|
||||
*/
|
||||
export const dbChainMock = {
|
||||
db: {
|
||||
select,
|
||||
selectDistinct,
|
||||
selectDistinctOn,
|
||||
insert,
|
||||
update,
|
||||
delete: del,
|
||||
execute,
|
||||
transaction,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock database connection.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { vi } from 'vitest'
|
||||
import { authMockFns } from './auth.mock'
|
||||
|
||||
/**
|
||||
* Auth type constants matching `@/lib/auth/hybrid` AuthType. Included in
|
||||
@@ -10,14 +11,35 @@ const AuthTypeMock = {
|
||||
INTERNAL_JWT: 'internal_jwt',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Default session-delegating implementation for `checkSessionOrInternalAuth`.
|
||||
* Mirrors the real function's session-auth path so tests that only mock
|
||||
* `getSession` (via `authMockFns.mockGetSession`) continue to work when the
|
||||
* hybrid module is globally mocked.
|
||||
*/
|
||||
const defaultCheckSessionOrInternalAuth = async () => {
|
||||
const session = await authMockFns.mockGetSession()
|
||||
if (session?.user?.id) {
|
||||
return {
|
||||
success: true,
|
||||
userId: session.user.id,
|
||||
userName: session.user.name,
|
||||
userEmail: session.user.email,
|
||||
authType: AuthTypeMock.SESSION,
|
||||
}
|
||||
}
|
||||
return { success: false, error: 'Unauthorized' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Controllable mock functions for `@/lib/auth/hybrid`. Override per-test with
|
||||
* `hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValueOnce(...)`.
|
||||
*/
|
||||
export const hybridAuthMockFns = {
|
||||
mockCheckHybridAuth: vi.fn(),
|
||||
mockCheckSessionOrInternalAuth: vi.fn(),
|
||||
mockCheckHybridAuth: vi.fn(defaultCheckSessionOrInternalAuth),
|
||||
mockCheckSessionOrInternalAuth: vi.fn(defaultCheckSessionOrInternalAuth),
|
||||
mockCheckInternalAuth: vi.fn(),
|
||||
mockHasExternalApiCredentials: vi.fn(() => false),
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,4 +55,5 @@ export const hybridAuthMock = {
|
||||
checkHybridAuth: hybridAuthMockFns.mockCheckHybridAuth,
|
||||
checkSessionOrInternalAuth: hybridAuthMockFns.mockCheckSessionOrInternalAuth,
|
||||
checkInternalAuth: hybridAuthMockFns.mockCheckInternalAuth,
|
||||
hasExternalApiCredentials: hybridAuthMockFns.mockHasExternalApiCredentials,
|
||||
}
|
||||
|
||||
@@ -43,7 +43,10 @@ export {
|
||||
createMockSql,
|
||||
createMockSqlOperators,
|
||||
databaseMock,
|
||||
dbChainMock,
|
||||
dbChainMockFns,
|
||||
drizzleOrmMock,
|
||||
resetDbChainMock,
|
||||
} from './database.mock'
|
||||
// Encryption mocks
|
||||
export { encryptionMock, encryptionMockFns } from './encryption.mock'
|
||||
|
||||
Reference in New Issue
Block a user