Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -182,17 +182,17 @@ jobs:
# without `--coverage`. See the Codecov note below.
#
# apps/sim runs only its first shard here; `test-shard` below runs the
# other. That suite is bound by the single Vite server thread that feeds
# others. That suite is bound by the single Vite server thread that feeds
# every worker — wall time is flat from 4 to 13 workers — so a bigger
# runner buys nothing and a second runner halves it.
# runner buys nothing and each extra runner takes a proportional slice.
- name: Run tests
env:
NODE_OPTIONS: '--no-warnings --max-old-space-size=8192'
NEXT_PUBLIC_APP_URL: 'https://www.sim.ai'
DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/simstudio'
ENCRYPTION_KEY: '0000000000000000000000000000000000000000000000000000000000000000' # dummy key for CI only
TURBO_CACHE_DIR: .turbo
SIM_TEST_SHARD: 1/2
SIM_TEST_SHARD: 1/3
run: bun run test

- name: Check schema and migrations are in sync
Expand All @@ -208,15 +208,20 @@ jobs:
fi
echo "✅ Schema and migrations are in sync"

# The second half of apps/sim's Vitest suite. Everything else — lint, the
# audits, type-check, the other workspaces' suites — lives in `test-build`
# with shard 1; this job exists only because that suite cannot go faster on
# one machine (see the "Run tests" note there). The Turbo cache disk gets
# its own key so the two shards' entries do not evict each other.
# The remaining shards of apps/sim's Vitest suite. Everything else — lint,
# the audits, type-check, the other workspaces' suites — lives in
# `test-build` with shard 1; these jobs exist only because that suite cannot
# go faster on one machine (see the "Run tests" note there). Three shards
# put each runner at roughly the fixed cost of checkout + install. The Turbo
# cache disk gets its own key so the shards' entries do not evict each other.
test-shard:
name: Test (shard 2)
name: Test (shard ${{ matrix.shard }})
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
shard: [2, 3]

steps:
- name: Checkout code
Expand Down Expand Up @@ -250,7 +255,7 @@ jobs:
uses: ./.github/actions/cache-mount
with:
provider: ${{ vars.CI_PROVIDER }}
key: ${{ github.repository }}-turbo-cache-shard-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}
key: ${{ github.repository }}-turbo-cache-shard-${{ matrix.shard }}-${{ github.event_name }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}
path: ./.turbo

- name: Install dependencies
Expand All @@ -259,14 +264,14 @@ jobs:
- name: Install ripgrep
run: command -v rg || (sudo apt-get update && sudo apt-get install -y ripgrep)

- name: Run tests (apps/sim shard 2/2)
- name: Run tests (apps/sim shard ${{ matrix.shard }}/3)
env:
NODE_OPTIONS: '--no-warnings --max-old-space-size=8192'
NEXT_PUBLIC_APP_URL: 'https://www.sim.ai'
DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/simstudio'
ENCRYPTION_KEY: '0000000000000000000000000000000000000000000000000000000000000000' # dummy key for CI only
TURBO_CACHE_DIR: .turbo
SIM_TEST_SHARD: 2/2
SIM_TEST_SHARD: ${{ matrix.shard }}/3
run: bunx turbo run test --filter=@sim/app

# Next.js production build, in parallel with lint + tests. Sticky disks are
Expand Down
1 change: 0 additions & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
"devDependencies": {
"@sim/tsconfig": "workspace:*",
"@tailwindcss/postcss": "^4.0.12",
"@types/mdx": "^2.0.13",
"@types/node": "24.2.1",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.0.4",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @vitest-environment jsdom
* @vitest-environment node
*
* Guards against drift between the code-block language picker and the Prism grammars actually
* registered by CodeBlockHighlight: every selectable language must have a registered grammar, or it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,41 +157,6 @@ describe('agent-stream applier', () => {
expect(freshText).toContain('Gamma paragraph')
})

it('preserves a concurrent peer edit to a region the agent snapshot does not include', () => {
// This is the core "AI as a CRDT peer" guarantee: the agent relays only its OWN delta (computed
// against a private shadow), never a whole-document reconcile that would revert a peer's edit.
const { editor, doc } = track(makeCollabEditor())

const session = beginAgentStream(editor)!
applyAgentStreamFrame(editor, session, 'Alpha paragraph.\n\nBeta paragraph.')

// A peer edits the FIRST paragraph directly on the shared doc — the agent's later snapshot still
// carries the ORIGINAL first paragraph (it was built from the base, before this edit).
const peer = new Y.Doc()
Y.applyUpdate(peer, Y.encodeStateAsUpdate(doc))
const peerFrag = peer.getXmlFragment('default')
peer.transact(() => {
const firstPara = peerFrag.get(0) as Y.XmlElement
const textNode = firstPara.get(0) as Y.XmlText
textNode.insert(textNode.toString().length, ' EDITED')
})
Y.applyUpdate(doc, Y.encodeStateAsUpdate(peer, Y.encodeStateVector(doc)))
peer.destroy()

// The agent appends a third paragraph. Its snapshot's first paragraph is the stale original, but the
// shadow-relayed delta only inserts the new paragraph — so the peer's " EDITED" must survive.
applyAgentStreamFrame(
editor,
session,
'Alpha paragraph.\n\nBeta paragraph.\n\nGamma paragraph.'
)
endAgentStream(session)

const live = doc.getXmlFragment('default').toString()
expect(live).toContain('EDITED')
expect(live).toContain('Gamma paragraph')
})

it('reuses cached binding metadata across frames, still emitting minimal per-frame deltas', () => {
// The binding `meta` is built ONCE (first frame) and reused — `updateYFragment` maintains it in place,
// so we skip an O(doc) `initProseMirrorDoc` rebuild per frame. This guards that caching preserves the
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @vitest-environment jsdom
* @vitest-environment node
*
* Dragging an image to reposition it inside a document must MOVE it, not import it again.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @vitest-environment jsdom
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @vitest-environment jsdom
*/
import { Editor } from '@tiptap/core'
import { afterEach, describe, expect, it } from 'vitest'
import { afterAll, describe, expect, it } from 'vitest'
import { createMarkdownContentExtensions } from './extensions'
import { parseMarkdownToDoc, serializeMarkdownBody, splitMarkdownBlocks } from './markdown-parse'
import { isRoundTripSafe } from './round-trip-safety'
Expand All @@ -12,19 +12,20 @@ const isEmptyPara = (n: { type?: string; content?: unknown[] }): boolean =>
n.type === 'paragraph' && !n.content?.length

let editor: Editor | null = null
afterEach(() => {
afterAll(() => {
editor?.destroy()
editor = null
})

/** The current whole-document path: parse markdown in one shot, serialize back. */
/**
* The current whole-document path: parse markdown in one shot, serialize back. One editor serves
* every call — `setContent` replaces the document wholesale, so a fresh instance per call only adds
* the cost of building the view, which the property tests below paid hundreds of times over.
*/
function oneShot(body: string): string {
editor = new Editor({ extensions: createMarkdownContentExtensions() })
editor ??= new Editor({ extensions: createMarkdownContentExtensions() })
editor.commands.setContent(body, { contentType: 'markdown' })
const out = editor.getMarkdown()
editor.destroy()
editor = null
return out
return editor.getMarkdown()
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @vitest-environment jsdom
* @vitest-environment node
*/

import { resolveDesktopZoom } from '@sim/desktop-bridge'
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/workspace/[workspaceId]/logs/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @vitest-environment jsdom
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { resolveLogWorkflowId, workflowEditorPath } from './utils'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @vitest-environment jsdom
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'

Expand Down
2 changes: 1 addition & 1 deletion apps/sim/blocks/blocks/mothership.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Blimp } from '@sim/emcn'
import { Blimp } from '@sim/emcn/icons'
import type { BlockConfig } from '@/blocks/types'
import type { ToolResponse } from '@/tools/types'

Expand Down
7 changes: 6 additions & 1 deletion apps/sim/executor/utils/block-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ import type { SerializedBlock } from '@/serializer/types'

/**
* These assertions are about what the real block registry publishes, so the global stub — which
* returns one mock block with no outputs — would make every case here pass vacuously.
* returns one mock block with no outputs — would make every case here pass vacuously. Only the
* generic webhook block is read, so only it is registered.
*/
vi.unmock('@/blocks/registry')
vi.mock('@/blocks/registry-maps', async () => {
const { partialBlockRegistry } = await import('@sim/testing/mocks/block-registry.mock')
return partialBlockRegistry(await import('@/blocks/blocks/generic_webhook'))
})

function triggerBlock(type: string, params: Record<string, unknown> = {}): SerializedBlock {
return {
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/hooks/queries/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { createLogger } from '@sim/logger'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { requestJson } from '@/lib/api/client/request'
import {
type ContractBodyInput,
removeWorkspaceEnvironmentContract,
savePersonalEnvironmentContract,
upsertWorkspaceEnvironmentContract,
} from '@/lib/api/contracts'
} from '@/lib/api/contracts/environment'
import type { ContractBodyInput } from '@/lib/api/contracts/types'
import type { WorkspaceEnvironmentData } from '@/lib/environment/api'
import { fetchPersonalEnvironment, fetchWorkspaceEnvironment } from '@/lib/environment/api'
import { invalidateSelectorQueries } from '@/hooks/queries/utils/selector-keys'
Expand Down
31 changes: 16 additions & 15 deletions apps/sim/lib/auth/sso-trust.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@
* domain-verification proof entirely.
*/
import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing'
import { afterAll, expect, it, vi } from 'vitest'

// Structurally slow — it imports the entire Better Auth module graph — so under a fully-parallel local run this file
// blows the default timeout while passing in isolation and on CI. Give it a
// real budget instead of letting machine load decide the verdict.
vi.setConfig({ testTimeout: 30_000 })
import { afterAll, beforeAll, expect, it, vi } from 'vitest'

const { ssoOptions } = vi.hoisted(() => ({
ssoOptions: { current: undefined as Record<string, unknown> | undefined },
Expand All @@ -28,24 +23,30 @@ vi.mock('@better-auth/sso', () => ({

setEnvFlags({ isSsoEnabled: true })

afterAll(resetEnvFlagsMock)

it('never trusts the IdP-supplied email_verified claim for SSO linking', async () => {
/**
* Structurally slow — it imports the entire Better Auth module graph — so under
* a fully-parallel local run this import blows the default budget while passing
* in isolation and on CI. The plugin options are captured once at module
* evaluation, so every assertion reads the same object: import once, outside
* any per-test budget, with a real budget of its own instead of letting machine
* load decide the verdict.
*/
beforeAll(async () => {
await import('@/lib/auth/auth')
}, 30_000)

afterAll(resetEnvFlagsMock)

it('never trusts the IdP-supplied email_verified claim for SSO linking', () => {
expect(ssoOptions.current).toBeDefined()
expect(ssoOptions.current?.trustEmailVerified).toBe(false)
})

it('keeps domain verification as the sole SSO linking trust source', async () => {
await import('@/lib/auth/auth')

it('keeps domain verification as the sole SSO linking trust source', () => {
expect(ssoOptions.current?.domainVerification).toEqual({ enabled: true })
})

it('disables Better Auth membership writes so Sim owns JIT admission', async () => {
await import('@/lib/auth/auth')

it('disables Better Auth membership writes so Sim owns JIT admission', () => {
expect(ssoOptions.current?.organizationProvisioning).toEqual({
disabled: true,
defaultRole: 'member',
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/collab-doc/merge.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @vitest-environment jsdom
* @vitest-environment node
*/
import { FILE_DOC_SEED } from '@sim/realtime-protocol/file-doc'
import { describe, expect, it } from 'vitest'
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/collab-doc/persist.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @vitest-environment jsdom
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as Y from 'yjs'
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/collab-doc/seed.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @vitest-environment jsdom
* @vitest-environment node
*/
import { FILE_DOC_SEED } from '@sim/realtime-protocol/file-doc'
import { getSchema } from '@tiptap/core'
Expand Down
3 changes: 3 additions & 0 deletions apps/sim/lib/copilot/chat/payload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ vi.mock('@/tools/registry', () => ({
},
}))

/** Denied-operation projection walks the block map only for blocks the mocked tool list never names. */
vi.mock('@/blocks/registry-maps', () => ({ BLOCK_REGISTRY: {}, BLOCK_META_REGISTRY: {} }))

vi.mock('@/tools/utils', () => ({
getLatestVersionTools: vi.fn((input) => input),
stripVersionSuffix: vi.fn((toolId: string) => toolId),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ import {
permissionGroupScopeMockFns,
resetPermissionGroupScopeMock,
workflowAuthzMockFns,
workflowsUtilsMock,
} from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { ChatContext } from '@/stores/panel'

vi.mock('@/lib/permission-groups/config-scope.server', () => permissionGroupScopeMock)

/** Folder listing is untouched by `@log` mentions; the real module drags in the block and trigger registries. */
vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)

import { processContextsServer } from '@/lib/copilot/chat/process-contents'
import { DEFAULT_PERMISSION_GROUP_CONFIG } from '@/lib/permission-groups/fields'

Expand Down
6 changes: 6 additions & 0 deletions apps/sim/lib/copilot/request/go/stream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import {
MothershipStreamV1ToolPhase,
} from '@/lib/copilot/generated/mothership-stream-v1'

/** Table side effects are not exercised here, and the real module loads the table application layer. */
vi.mock('@/lib/copilot/request/tools/tables', () => ({
maybeWriteOutputToTable: vi.fn(async (_toolName, _params, result) => result),
maybeWriteReadCsvToTable: vi.fn(async (_toolName, _params, result) => result),
}))

vi.mock('@/lib/copilot/request/session', async () => {
const actual = await vi.importActual<typeof import('@/lib/copilot/request/session')>(
'@/lib/copilot/request/session'
Expand Down
6 changes: 6 additions & 0 deletions apps/sim/lib/copilot/request/handlers/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ vi.mock('@/lib/copilot/async-runs/repository', () => ({
claimWorkflowToolExecution,
}))

/** Table side effects are not exercised here, and the real module loads the table application layer. */
vi.mock('@/lib/copilot/request/tools/tables', () => ({
maybeWriteOutputToTable: vi.fn(async (_toolName, _params, result) => result),
maybeWriteReadCsvToTable: vi.fn(async (_toolName, _params, result) => result),
}))

vi.mock('@/lib/copilot/request/tools/client', () => ({
waitForClientToolCompletion,
waitForToolCompletion,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import { MothershipStreamV1CompletionStatus } from '@/lib/copilot/generated/mothership-stream-v1'
import { createStreamingContext } from '@/lib/copilot/request/context/request-context'

/** Table side effects are not exercised here, and the real module loads the table application layer. */
vi.mock('@/lib/copilot/request/tools/tables', () => ({
maybeWriteOutputToTable: vi.fn(async (_toolName, _params, result) => result),
maybeWriteReadCsvToTable: vi.fn(async (_toolName, _params, result) => result),
}))

import { makeResumeLegContext, mergeResumeLegOutputs } from '@/lib/copilot/request/lifecycle/run'

// Guards the makeResumeLegContext / mergeResumeLegOutputs contract: the two MUST
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/copilot/server/agent-url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const { envMock } = vi.hoisted(() => ({
},
}))

vi.mock('@/lib/api/contracts', () => ({
vi.mock('@/lib/api/contracts/user', () => ({
mothershipEnvironmentSchema: {
safeParse: (value: unknown) =>
['default', 'dev', 'staging', 'prod'].includes(String(value))
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/copilot/server/agent-url.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { db } from '@sim/db'
import { settings, user } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type MothershipEnvironment, mothershipEnvironmentSchema } from '@/lib/api/contracts'
import { type MothershipEnvironment, mothershipEnvironmentSchema } from '@/lib/api/contracts/user'
Comment thread
waleedlatif1 marked this conversation as resolved.
import { SIM_AGENT_API_URL, SIM_AGENT_API_URL_DEFAULT } from '@/lib/copilot/constants'
import { env } from '@/lib/core/config/env'

Expand Down
Loading
Loading