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:
Waleed
2026-04-19 23:05:06 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 1ced54a77c
commit 5cf7e8d546
100 changed files with 976 additions and 1455 deletions
+143
View File
@@ -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.
*/
+25 -2
View File
@@ -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,
}
+3
View File
@@ -43,7 +43,10 @@ export {
createMockSql,
createMockSqlOperators,
databaseMock,
dbChainMock,
dbChainMockFns,
drizzleOrmMock,
resetDbChainMock,
} from './database.mock'
// Encryption mocks
export { encryptionMock, encryptionMockFns } from './encryption.mock'