Skip to content

Commit 7a18b96

Browse files
committed
Merge branch 'staging' into dev
2 parents 1745793 + 2d94b37 commit 7a18b96

1,884 files changed

Lines changed: 149009 additions & 41954 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/commands/you-might-not-need-a-callback.md

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,34 @@ User arguments: $ARGUMENTS
1616
Read before analyzing:
1717
1. https://react.dev/reference/react/useCallback — official docs on when useCallback is actually needed
1818

19+
## The one rule that matters
20+
21+
`useCallback` is only useful when **something observes the reference**. Ask: does anything care if this function gets a new identity on re-render?
22+
23+
Observers that care about reference stability:
24+
- A `useEffect` that lists the function in its deps array
25+
- A `useMemo` that lists the function in its deps array
26+
- Another `useCallback` that lists the function in its deps array
27+
- A child component wrapped in `React.memo` that receives the function as a prop
28+
29+
If none of those apply — if the function is only called inline, or passed to a non-memoized child, or assigned to a native element event — the reference is unobserved and `useCallback` adds overhead with zero benefit.
30+
1931
## Anti-patterns to detect
2032

21-
1. **useCallback on functions not passed as props or deps**: No benefit if only called within the same component.
22-
2. **useCallback with deps that change every render**: Memoization is wasted.
23-
3. **useCallback on handlers passed to native elements**: `<button onClick={fn}>` doesn't benefit from stable references.
24-
4. **useCallback wrapping functions that return new objects/arrays**: Memoization at the wrong level.
25-
5. **useCallback with empty deps when deps are needed**: Stale closures.
26-
6. **Pairing useCallback + React.memo unnecessarily**: Only optimize when you've measured a problem.
27-
7. **useCallback in hooks that don't need stable references**: Not every hook return needs memoization.
33+
1. **No observer tracks the reference**: The function is only called inline in the same component, or passed to a non-memoized child, or used as a native element handler (`<button onClick={fn}>`). Nothing re-runs or bails out based on reference identity. Remove `useCallback`.
34+
2. **useCallback with deps that change every render**: If a dep is a plain object/array created inline, or state that changes on every interaction, memoization buys nothing — the function gets a new identity anyway.
35+
3. **useCallback on handlers passed only to native elements**: `<button onClick={fn}>` — React never does reference equality on native element props. No benefit.
36+
4. **useCallback wrapping functions that return new objects/arrays**: Stable function identity, unstable return value — memoization is at the wrong level. Use `useMemo` on the return value instead, or restructure.
37+
5. **useCallback with empty deps when deps are needed**: Stale closure — reads initial values forever. This is a correctness bug, not just a performance issue.
38+
6. **Pairing useCallback + React.memo on trivially cheap renders**: If the child renders in < 1ms and re-renders rarely, the memo infrastructure costs more than it saves.
39+
40+
## Patterns that ARE correct — do not flag
2841

29-
Note: This codebase uses a ref pattern for stable callbacks (`useRef` + empty deps). That pattern is correct — don't flag it.
42+
- `useCallback` whose result is in a `useEffect` dep array — prevents the effect from re-running on every render
43+
- `useCallback` whose result is in a `useMemo` dep array — prevents the memo from recomputing on every render
44+
- `useCallback` whose result is a dep of another `useCallback` — stabilises a callback chain
45+
- `useCallback` passed to a `React.memo`-wrapped child — the whole point of the pattern
46+
- This codebase's ref pattern: `useRef` + callback with empty deps that reads the ref inside — correct, do not flag
3047

3148
## Steps
3249

.claude/rules/global.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
# Global Standards
22

33
## Logging
4-
Import `createLogger` from `sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`.
4+
Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`. Inside API routes wrapped with `withRouteHandler`, loggers automatically include the request ID.
5+
6+
## API Route Handlers
7+
All API route handlers must be wrapped with `withRouteHandler` from `@/lib/core/utils/with-route-handler`. Never export a bare `async function GET/POST/...` — always use `export const METHOD = withRouteHandler(...)`.
58

69
## Comments
710
Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.
@@ -10,7 +13,7 @@ Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.
1013
Never update global styles. Keep all styling local to components.
1114

1215
## ID Generation
13-
Never use `crypto.randomUUID()`, `nanoid`, or the `uuid` package directly. Use the utilities from `@/lib/core/utils/uuid`:
16+
Never use `crypto.randomUUID()`, `nanoid`, or the `uuid` package directly. Use the utilities from `@sim/utils/id`:
1417

1518
- `generateId()` — UUID v4, use by default
1619
- `generateShortId(size?)` — short URL-safe ID (default 21 chars), for compact identifiers
@@ -24,11 +27,32 @@ import { v4 as uuidv4 } from 'uuid'
2427
const id = crypto.randomUUID()
2528

2629
// ✓ Good
27-
import { generateId, generateShortId } from '@/lib/core/utils/uuid'
30+
import { generateId, generateShortId } from '@sim/utils/id'
2831
const uuid = generateId()
2932
const shortId = generateShortId()
3033
const tiny = generateShortId(8)
3134
```
3235

36+
## Common Utilities
37+
Use shared helpers from `@sim/utils` instead of writing inline implementations:
38+
39+
- `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
40+
- `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
41+
- `toError(value).message` — get error message safely. Never write `e instanceof Error ? e.message : String(e)`
42+
43+
```typescript
44+
// ✗ Bad
45+
await new Promise(resolve => setTimeout(resolve, 1000))
46+
const msg = error instanceof Error ? error.message : String(error)
47+
const err = error instanceof Error ? error : new Error(String(error))
48+
49+
// ✓ Good
50+
import { sleep } from '@sim/utils/helpers'
51+
import { toError } from '@sim/utils/errors'
52+
await sleep(1000)
53+
const msg = toError(error).message
54+
const err = toError(error)
55+
```
56+
3357
## Package Manager
3458
Use `bun` and `bunx`, not `npm` and `npx`.

.claude/rules/sim-testing.md

Lines changed: 88 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ Use Vitest. Test files: `feature.ts` → `feature.test.ts`
1313
These modules are mocked globally — do NOT re-mock them in test files unless you need to override behavior:
1414

1515
- `@sim/db``databaseMock`
16+
- `@sim/db/schema``schemaMock`
1617
- `drizzle-orm``drizzleOrmMock`
1718
- `@sim/logger``loggerMock`
19+
- `@/lib/auth``authMock`
20+
- `@/lib/auth/hybrid``hybridAuthMock` (with default session-delegating behavior)
21+
- `@/lib/core/utils/request``requestUtilsMock`
1822
- `@/stores/console/store`, `@/stores/terminal`, `@/stores/execution/store`
1923
- `@/blocks/registry`
2024
- `@trigger.dev/sdk`
@@ -102,10 +106,6 @@ vi.mock('@/lib/workspaces/utils', () => ({
102106
}))
103107
```
104108

105-
### NEVER use `mockAuth()`, `mockConsoleLogger()`, or `setupCommonApiMocks()` from `@sim/testing`
106-
107-
These helpers internally use `vi.doMock()` which is slow. Use direct `vi.hoisted()` + `vi.mock()` instead.
108-
109109
### Mock heavy transitive dependencies
110110

111111
If a module under test imports `@/blocks` (200+ files), `@/tools/registry`, or other heavy modules, mock them:
@@ -135,83 +135,129 @@ await new Promise(r => setTimeout(r, 1))
135135
vi.useFakeTimers()
136136
```
137137

138-
## Mock Pattern Reference
138+
## Centralized Mocks (prefer over local declarations)
139+
140+
`@sim/testing` exports ready-to-use mock modules for common dependencies. Import and pass directly to `vi.mock()` — no `vi.hoisted()` boilerplate needed. Each paired `*MockFns` object exposes the underlying `vi.fn()`s for per-test overrides.
141+
142+
| Module mocked | Import | Factory form |
143+
|---|---|---|
144+
| `@/app/api/auth/oauth/utils` | `authOAuthUtilsMock`, `authOAuthUtilsMockFns` | `vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)` |
145+
| `@/app/api/knowledge/utils` | `knowledgeApiUtilsMock`, `knowledgeApiUtilsMockFns` | `vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)` |
146+
| `@/app/api/workflows/utils` | `workflowsApiUtilsMock`, `workflowsApiUtilsMockFns` | `vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)` |
147+
| `@/lib/audit/log` | `auditMock`, `auditMockFns` | `vi.mock('@/lib/audit/log', () => auditMock)` |
148+
| `@/lib/auth` | `authMock`, `authMockFns` | `vi.mock('@/lib/auth', () => authMock)` |
149+
| `@/lib/auth/hybrid` | `hybridAuthMock`, `hybridAuthMockFns` | `vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)` |
150+
| `@/lib/copilot/request/http` | `copilotHttpMock`, `copilotHttpMockFns` | `vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)` |
151+
| `@/lib/core/config/env` | `envMock`, `createEnvMock(overrides)` | `vi.mock('@/lib/core/config/env', () => envMock)` |
152+
| `@/lib/core/config/feature-flags` | `featureFlagsMock` | `vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock)` |
153+
| `@/lib/core/config/redis` | `redisConfigMock`, `redisConfigMockFns` | `vi.mock('@/lib/core/config/redis', () => redisConfigMock)` |
154+
| `@/lib/core/security/encryption` | `encryptionMock`, `encryptionMockFns` | `vi.mock('@/lib/core/security/encryption', () => encryptionMock)` |
155+
| `@/lib/core/security/input-validation.server` | `inputValidationMock`, `inputValidationMockFns` | `vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock)` |
156+
| `@/lib/core/utils/request` | `requestUtilsMock`, `requestUtilsMockFns` | `vi.mock('@/lib/core/utils/request', () => requestUtilsMock)` |
157+
| `@/lib/core/utils/urls` | `urlsMock`, `urlsMockFns` | `vi.mock('@/lib/core/utils/urls', () => urlsMock)` |
158+
| `@/lib/execution/preprocessing` | `executionPreprocessingMock`, `executionPreprocessingMockFns` | `vi.mock('@/lib/execution/preprocessing', () => executionPreprocessingMock)` |
159+
| `@/lib/logs/execution/logging-session` | `loggingSessionMock`, `loggingSessionMockFns`, `LoggingSessionMock` | `vi.mock('@/lib/logs/execution/logging-session', () => loggingSessionMock)` |
160+
| `@/lib/workflows/orchestration` | `workflowsOrchestrationMock`, `workflowsOrchestrationMockFns` | `vi.mock('@/lib/workflows/orchestration', () => workflowsOrchestrationMock)` |
161+
| `@/lib/workflows/persistence/utils` | `workflowsPersistenceUtilsMock`, `workflowsPersistenceUtilsMockFns` | `vi.mock('@/lib/workflows/persistence/utils', () => workflowsPersistenceUtilsMock)` |
162+
| `@/lib/workflows/utils` | `workflowsUtilsMock`, `workflowsUtilsMockFns` | `vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)` |
163+
| `@/lib/workspaces/permissions/utils` | `permissionsMock`, `permissionsMockFns` | `vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)` |
164+
| `@sim/db/schema` | `schemaMock` | `vi.mock('@sim/db/schema', () => schemaMock)` |
139165

140166
### Auth mocking (API routes)
141167

142168
```typescript
143-
const { mockGetSession } = vi.hoisted(() => ({
144-
mockGetSession: vi.fn(),
145-
}))
169+
import { authMock, authMockFns } from '@sim/testing'
170+
import { beforeEach, describe, expect, it, vi } from 'vitest'
146171

147-
vi.mock('@/lib/auth', () => ({
148-
auth: { api: { getSession: vi.fn() } },
149-
getSession: mockGetSession,
150-
}))
172+
vi.mock('@/lib/auth', () => authMock)
151173

152-
// In tests:
153-
mockGetSession.mockResolvedValue({ user: { id: 'user-1', email: 'test@example.com' } })
154-
mockGetSession.mockResolvedValue(null) // unauthenticated
174+
import { GET } from '@/app/api/my-route/route'
175+
176+
beforeEach(() => {
177+
vi.clearAllMocks()
178+
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
179+
})
155180
```
156181

182+
Only define a local `vi.mock('@/lib/auth', ...)` if the module under test consumes exports outside the centralized shape (e.g., `auth.api.verifyOneTimeToken`, `auth.api.resetPassword`).
183+
157184
### Hybrid auth mocking
158185

159186
```typescript
160-
const { mockCheckSessionOrInternalAuth } = vi.hoisted(() => ({
161-
mockCheckSessionOrInternalAuth: vi.fn(),
162-
}))
187+
import { hybridAuthMock, hybridAuthMockFns } from '@sim/testing'
163188

164-
vi.mock('@/lib/auth/hybrid', () => ({
165-
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
166-
}))
189+
vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)
167190

168191
// In tests:
169-
mockCheckSessionOrInternalAuth.mockResolvedValue({
192+
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
170193
success: true, userId: 'user-1', authType: 'session',
171194
})
172195
```
173196

174197
### Database chain mocking
175198

199+
Use the centralized `dbChainMock` + `dbChainMockFns` helpers — no `vi.hoisted()` or chain-wiring boilerplate needed.
200+
176201
```typescript
177-
const { mockSelect, mockFrom, mockWhere } = vi.hoisted(() => ({
178-
mockSelect: vi.fn(),
179-
mockFrom: vi.fn(),
180-
mockWhere: vi.fn(),
181-
}))
202+
import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing'
182203

183-
vi.mock('@sim/db', () => ({
184-
db: { select: mockSelect },
185-
}))
204+
vi.mock('@sim/db', () => dbChainMock)
205+
// Spread for custom exports: vi.mock('@sim/db', () => ({ ...dbChainMock, myTable: {...} }))
186206

187207
beforeEach(() => {
188-
mockSelect.mockReturnValue({ from: mockFrom })
189-
mockFrom.mockReturnValue({ where: mockWhere })
190-
mockWhere.mockResolvedValue([{ id: '1', name: 'test' }])
208+
vi.clearAllMocks()
209+
resetDbChainMock() // only needed if tests use permanent (non-`Once`) overrides
210+
})
211+
212+
it('reads a row', async () => {
213+
dbChainMockFns.limit.mockResolvedValueOnce([{ id: '1', name: 'test' }])
214+
// exercise code that hits db.select().from().where().limit()
215+
expect(dbChainMockFns.where).toHaveBeenCalled()
191216
})
192217
```
193218

219+
**Default chains supported:**
220+
- `select()/selectDistinct()/selectDistinctOn() → from() → where()/innerJoin()/leftJoin() → where() → limit()/orderBy()/returning()/groupBy()/for()`
221+
- `insert() → values() → returning()/onConflictDoUpdate()/onConflictDoNothing()`
222+
- `update() → set() → where() → limit()/orderBy()/returning()/for()`
223+
- `delete() → where() → limit()/orderBy()/returning()/for()`
224+
- `db.execute()` resolves `[]`
225+
- `db.transaction(cb)` calls cb with `dbChainMock.db`
226+
227+
`.for('update')` (Postgres row-level locking) is supported on `where`
228+
builders. It returns a thenable with `.limit` / `.orderBy` / `.returning` /
229+
`.groupBy` attached, so both `await .where().for('update')` (terminal) and
230+
`await .where().for('update').limit(1)` (chained) work. Override the terminal
231+
result with `dbChainMockFns.for.mockResolvedValueOnce([...])`; for the chained
232+
form, mock the downstream terminal (e.g. `dbChainMockFns.limit.mockResolvedValueOnce([...])`).
233+
234+
All terminals default to `Promise.resolve([])`. Override per-test with `dbChainMockFns.<terminal>.mockResolvedValueOnce(...)`.
235+
236+
Use `resetDbChainMock()` in `beforeEach` only when tests replace wiring with `.mockReturnValue` / `.mockResolvedValue` (permanent). Tests using only `...Once` variants don't need it.
237+
194238
## @sim/testing Package
195239

196240
Always prefer over local test data.
197241

198242
| Category | Utilities |
199243
|----------|-----------|
200-
| **Mocks** | `loggerMock`, `databaseMock`, `drizzleOrmMock`, `setupGlobalFetchMock()` |
244+
| **Module mocks** | See "Centralized Mocks" table above |
245+
| **Logger helpers** | `loggerMock`, `createMockLogger()`, `getLoggerCalls()`, `clearLoggerMocks()` |
246+
| **Database helpers** | `databaseMock`, `drizzleOrmMock`, `createMockDb()`, `createMockSql()`, `createMockSqlOperators()` |
247+
| **Fetch helpers** | `setupGlobalFetchMock()`, `createMockFetch()`, `createMockResponse()`, `mockFetchError()` |
201248
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutionContext()` |
202249
| **Builders** | `WorkflowBuilder`, `ExecutionContextBuilder` |
203250
| **Assertions** | `expectWorkflowAccessGranted()`, `expectBlockExecuted()` |
204-
| **Requests** | `createMockRequest()`, `createEnvMock()` |
251+
| **Requests** | `createMockRequest()`, `createMockFormDataRequest()` |
205252

206253
## Rules Summary
207254

208255
1. `@vitest-environment node` unless DOM is required
209-
2. `vi.hoisted()` + `vi.mock()` + static imports — never `vi.resetModules()` + `vi.doMock()` + dynamic imports
210-
3. `vi.mock()` calls before importing mocked modules
211-
4. `@sim/testing` utilities over local mocks
256+
2. Prefer centralized mocks from `@sim/testing` (see table above) over local `vi.hoisted()` + `vi.mock()` boilerplate
257+
3. `vi.hoisted()` + `vi.mock()` + static imports — never `vi.resetModules()` + `vi.doMock()` + dynamic imports
258+
4. `vi.mock()` calls before importing mocked modules
212259
5. `beforeEach(() => vi.clearAllMocks())` to reset state — no redundant `afterEach`
213260
6. No `vi.importActual()` — mock everything explicitly
214-
7. No `mockAuth()`, `mockConsoleLogger()`, `setupCommonApiMocks()` — use direct mocks
215-
8. Mock heavy deps (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them
216-
9. Use absolute imports in test files
217-
10. Avoid real timers — use 1ms delays or `vi.useFakeTimers()`
261+
7. Mock heavy deps (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them
262+
8. Use absolute imports in test files
263+
9. Avoid real timers — use 1ms delays or `vi.useFakeTimers()`

.cursor/rules/global.mdc

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.
1717
Never update global styles. Keep all styling local to components.
1818

1919
## ID Generation
20-
Never use `crypto.randomUUID()`, `nanoid`, or the `uuid` package directly. Use the utilities from `@/lib/core/utils/uuid`:
20+
Never use `crypto.randomUUID()`, `nanoid`, or the `uuid` package directly. Use the utilities from `@sim/utils/id`:
2121

2222
- `generateId()` — UUID v4, use by default
2323
- `generateShortId(size?)` — short URL-safe ID (default 21 chars), for compact identifiers
@@ -31,11 +31,32 @@ import { v4 as uuidv4 } from 'uuid'
3131
const id = crypto.randomUUID()
3232

3333
// ✓ Good
34-
import { generateId, generateShortId } from '@/lib/core/utils/uuid'
34+
import { generateId, generateShortId } from '@sim/utils/id'
3535
const uuid = generateId()
3636
const shortId = generateShortId()
3737
const tiny = generateShortId(8)
3838
```
3939

40+
## Common Utilities
41+
Use shared helpers from `@sim/utils` instead of writing inline implementations:
42+
43+
- `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
44+
- `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
45+
- `toError(value).message` — get error message safely. Never write `e instanceof Error ? e.message : String(e)`
46+
47+
```typescript
48+
// ✗ Bad
49+
await new Promise(resolve => setTimeout(resolve, 1000))
50+
const msg = error instanceof Error ? error.message : String(error)
51+
const err = error instanceof Error ? error : new Error(String(error))
52+
53+
// ✓ Good
54+
import { sleep } from '@sim/utils/helpers'
55+
import { toError } from '@sim/utils/errors'
56+
await sleep(1000)
57+
const msg = toError(error).message
58+
const err = toError(error)
59+
```
60+
4061
## Package Manager
4162
Use `bun` and `bunx`, not `npm` and `npx`.

0 commit comments

Comments
 (0)