diff --git a/.github/CI.md b/.github/CI.md new file mode 100644 index 00000000000..bc9564169b6 --- /dev/null +++ b/.github/CI.md @@ -0,0 +1,46 @@ +# CI selection and acceptance ownership + +The `changes` job runs Turborepo 2.9.14's `affectedTasks` query before installing application dependencies or provisioning services. `turbo.json` records package dependency propagation; `apps/sim/turbo.json` partitions the app into shared runtime, server, workspace UI, and acceptance tasks. `.github/ci-suites.json` turns those tasks into job matrices. The uploaded `ci-plan` artifact names changed files, selected tasks, attribution reasons, and any fallback. + +A PR compares its merge base with the checked-out merge commit. Main/staging pushes execute all checks; the parent `ci` workflow skips checks on `dev` pushes, while PRs targeting `dev` still run checks. A manual `checks` run executes all checks unless supplied a full base commit SHA to reproduce a scoped plan. Missing history, an empty diff, unknown files outside the workspace graph, root script/CI changes, or a planner error run every check. Missing selection outputs fail the aggregate gate. The gate also rejects an omitted dependency, any failed/cancelled job, and any skip that its explicit selection flag did not authorize. + +| Change | Expensive checks selected | +| --- | --- | +| Independent SDK | That workspace's tests and type-check; no app build, services, or browsers | +| App unit test | App test shards and test type check | +| Integration test | Both provision paths; no HTTP/browser job merely for the test edit | +| Workspace UI | Database integration, mobile, SEO and live Electron, plus app tests/build | +| Landing content | SEO, app tests/build; no database integration or Electron | +| Shared runtime or transitive dependency | All dependent boundaries | +| One E2E harness | Its acceptance group and the shared production build | + +The server boundary deliberately includes all API routes and backend domains. Shared modules and unclassified app source remain broad inputs: this is safer than pretending a route's static imports describe database, dynamic registry, or background-worker dependencies. Files read outside a package graph need explicit inputs: CLI acceptance imports `packages/sim-cli/src/embed.ts`, app tests read docs content, and live Electron and realtime acceptance cross app boundaries. When adding such a dependency, update the corresponding task inputs/dependencies and the real Git/Turbo selector scenarios. + +Integration also depends on workspace UI because the desktop executor integration suite runs the production workspace stream dispatcher against PostgreSQL and Redis. Its explicit harness inputs include the database and realtime Vitest configs and integration setup files, which runtime tasks exclude. + +SEO depends on workspace UI and server code: landing pages reuse workspace pricing and preview components, provider models/pricing, and the block registry. These directory boundaries are not isolated application dependency graphs. + +`affectedTasks` already uses task inputs. Do not enable `futureFlags.affectedUsingTaskInputs` just for this planner: that flag also changes pruning, which can pull CI-only cross-app task dependencies into production Docker contexts. + +Affected type-checks follow the package graph too; repository audits and lint run whenever the checks workflow runs. Workspace-only unit jobs use a free GitHub runner and do not start the app test shards. + +Database integration and lightweight HTTP suites run on free standard GitHub runners with matching GitHub cache setup. A same-source integration comparison of 16 jobs per provider (32 total) had a slowest shard of 6m 04s on 2-vCPU Blacksmith versus 6m 55s on free GitHub, with identical coverage. GitHub trades the observed 51 seconds for zero runner compute cost. This is one paired sample, not a long-run percentile estimate. These explicitly selected GitHub jobs do not follow `CI_PROVIDER`. + +Memory-heavy builds and browser jobs retain appropriately sized Blacksmith runners. Electron uses 4-vCPU Blacksmith: both shards passed within 14 seconds of the 8-vCPU control; the free GitHub rest shard was slower and its lease run stalled. + +Every HTTP and browser group consumes the same production build in an isolated job/database. Electron also uses that build and skips its development-route warm-up. Local HTTP and Electron development runs retain their dev-server behavior. Build inputs exclude test-only files so a harness edit can reuse an unchanged production build. + +SCIM's production app uses a `sim.ai` fixture hostname mapped to loopback, since `NEXT_PUBLIC_FORCE_HOSTED` is ignored in production. Readiness, HTTP tests, and internal API requests still connect to loopback. The SCIM suite verifies that ending an Enterprise subscription rejects both owner administration and an existing bearer token, and that restoring it restores access. + +## Coverage removed or consolidated + +- SEO is plain HTTP, independent of browser and viewport. Its complete suite runs once in the SEO job, instead of repeating in each of four mobile shards. +- The MCP suite's homepage-returns-200 control duplicated SEO's stronger real-homepage Markdown/content-negotiation check. SEO owns that positive control; MCP retains host isolation, ownership challenge, discovery, API catalogs, unknown-page/API behavior, and refusal of Markdown. +- CLI CI uses one cold/warm-up iteration and one measured iteration instead of three identical measured repetitions. Both retain run completion, selected output, file-reference size, trace, and omitted-workflow-snapshot assertions. The standalone benchmark keeps its configurable sample size. +- A precompiled Electron app needs no route-compilation warm-up, which repeated read/import/chat-switch/Stop flows before each shard. The actual acceptance tests still exercise those behaviors. + +The long-import and lease-expiration checks remain: they prove distinct survival/uncertain-outcome behavior across real process failure. Likewise, the two integration provision paths remain because migrations create constraints/triggers that schema push does not. + +Local planner validation is `bun run test:scripts scripts/ci-changes.test.ts scripts/ci-gate.test.ts`. It executes real Turbo queries in small Git fixtures; no application build or database is required. Full builds, services, browser suites, and Electron checks run on hosted CI. + +The separate `desktop-e2e` workflow retains its desktop-specific path filters. Its macOS shell smoke suite is partitioned by Playwright across three isolated runners; serial test-file groups stay intact. Packaging smoke and the weekly Electron canary remain separate. Each shard uploads its small JSON coverage/timing report. diff --git a/.github/ci-suites.json b/.github/ci-suites.json new file mode 100644 index 00000000000..8e18cf9fe18 --- /dev/null +++ b/.github/ci-suites.json @@ -0,0 +1,62 @@ +{ + "http": [ + { + "group": "scim", + "task": "ci:e2e-scim" + }, + { + "group": "cli", + "task": "ci:e2e-cli" + }, + { + "group": "stop-after", + "task": "ci:e2e-stop-after" + }, + { + "group": "desktop-inbox", + "task": "ci:e2e-desktop-inbox" + }, + { + "group": "seo", + "task": "ci:e2e-seo" + }, + { + "group": "project-files", + "task": "ci:e2e-project-files" + } + ], + "production": [ + { + "group": "mobile", + "name": "mobile, chromium, 1/2", + "browser": "chromium", + "shard": "1/2", + "artifact": "mobile-chromium-1", + "task": "ci:e2e-mobile" + }, + { + "group": "mobile", + "name": "mobile, chromium, 2/2", + "browser": "chromium", + "shard": "2/2", + "artifact": "mobile-chromium-2", + "task": "ci:e2e-mobile" + }, + { + "group": "mobile", + "name": "mobile, webkit, 1/2", + "browser": "webkit", + "shard": "1/2", + "artifact": "mobile-webkit-1", + "task": "ci:e2e-mobile" + }, + { + "group": "mobile", + "name": "mobile, webkit, 2/2", + "browser": "webkit", + "shard": "2/2", + "artifact": "mobile-webkit-2", + "task": "ci:e2e-mobile" + } + ] +} diff --git a/.github/scripts/ci-changes.sh b/.github/scripts/ci-changes.sh index c506a2149bf..f31502f4ef8 100755 --- a/.github/scripts/ci-changes.sh +++ b/.github/scripts/ci-changes.sh @@ -1,65 +1,6 @@ #!/usr/bin/env bash -# Prints which checks a pull request's changes can affect, as `=true|false` lines for -# $GITHUB_OUTPUT. A flag is `false` only when every changed file is on an allowlist whose jobs -# fully cover it. A file on no allowlist sets every flag but `desktop_live` (which keeps its own -# skip list) to `true`; a change under .github/, an empty diff, or any failure to work out the -# diff sets all of them. -# -# full integration, e2e, e2e-build, e2e-mobile: any file not allowlisted below -# test the Vitest shards and test-types: content, docs app, and desktop changes, -# plus `full` -# build the app's `next build`: content changes, plus `full` -# desktop_live the live Electron suite: any file outside its own skip list -# -# lint always runs and is not gated. -# -# Usage: ci-changes.sh -set -u - -# Posts, author profiles, and the public assets only posts reference. The test shards read them -# (lib/content/og-image.test.ts) and `next build` prerenders every post with its MDX components, -# which check:library-content compiles but never renders. Components under content/ are code. -content='^apps/sim/content/([^/]+/)*[^/]+\.mdx?$|^apps/sim/content/authors/[^/]+\.json$|^apps/sim/public/(authors|blog|changelog|library)/' -# Its own workspace tests, its type-check and docs-manifest audit in lint, and apps/sim tests -# that read its pages (tools/netsuite/netsuite.test.ts). -docs='^apps/docs/' -# Nothing outside the Electron app imports it; the desktop bridge and protocol packages are -# consumed by apps/sim and stay unlisted. -desktop='^apps/desktop/' -# Agent guidance, read only by the lint job's audits. -guidance='^(\.agents|\.claude|\.cursor)/|^([^/]+/)*(AGENTS|CLAUDE)\.md$|^README\.md$' -# The live desktop suite drives apps/desktop against the app, so it skips more than the rest. -desktop_live_skip="$guidance"'|^(apps/docs/|apps/pii/|apps/sim/content/|apps/sim/public/(authors|blog|changelog|library)/|packages/(python-sdk|ts-sdk)/)|\.mdx?$|(^|/)LICENSE$' - -emit() { - printf 'full=%s\ntest=%s\nbuild=%s\ndesktop_live=%s\n' "$1" "$2" "$3" "$4" -} -run_all() { - emit true true true true - echo "Running every check: $1" >&2 - exit 0 -} -matches() { printf '%s\n' "$names" | grep -qE "$1"; } -outside() { printf '%s\n' "$names" | grep -qvE "$1"; } - -base=${1:-} -[ -n "$base" ] || run_all 'no base commit' -git fetch --quiet --depth=1 origin "$base" || run_all "could not fetch $base" -names=$(git diff --no-renames --name-only "$base" HEAD) || run_all "could not diff against $base" -[ -n "$names" ] || run_all 'no changed files listed' -if matches '^\.github/'; then run_all 'a CI file changed'; fi - -desktop_live=false -if outside "$desktop_live_skip"; then desktop_live=true; fi -if outside "$content|$docs|$desktop|$guidance"; then - emit true true true "$desktop_live" - unlisted=$(printf '%s\n' "$names" | grep -vE "$content|$docs|$desktop|$guidance" | head -n 1) - echo "Running every check (desktop_live=$desktop_live): $unlisted is not on an allowlist" >&2 - exit 0 -fi - -test=false build=false -if matches "$content|$docs|$desktop"; then test=true; fi -if matches "$content"; then build=true; fi -emit false "$test" "$build" "$desktop_live" -echo "Scoped checks: test=$test build=$build desktop_live=$desktop_live" >&2 +# Select tasks from the committed Turborepo graph; unknown files or failed attribution run all. +# Usage: ci-changes.sh [base-sha]. No base runs the complete push/dispatch gate. +set -euo pipefail +script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) +exec bun "$script_dir/../../scripts/ci-plan.ts" "${1:-}" diff --git a/.github/scripts/ci-gate.sh b/.github/scripts/ci-gate.sh index c5303551763..38b91d37ca0 100755 --- a/.github/scripts/ci-gate.sh +++ b/.github/scripts/ci-gate.sh @@ -5,10 +5,7 @@ # # A skip passes only for a gated job whose `changes` flag is exactly `false`: # -# integration, e2e, e2e-build, e2e-mobile full -# test, test-types test -# build build -# desktop-live desktop_live +# Each job maps to its own explicit selection flag below. No inferred skip is accepted. # # Every other skip fails: an ungated job (lint, `changes` itself), a gated job whose flag is # `true`, missing, or anything else (a failed or skipped `changes` leaves no outputs). A failure @@ -24,13 +21,21 @@ if ! jq -e 'type == "object"' <<< "$needs" > /dev/null 2>&1; then fi failed="$(jq -r ' - {"integration": "full", "e2e": "full", "e2e-build": "full", "e2e-mobile": "full", "test": "test", - "test-types": "test", "build": "build", "desktop-live": "desktop_live"} as $gate + {"integration": "integration", "e2e": "http", "e2e-build": "e2e_build", "e2e-production": "production", "test": "test", + "test-types": "test_app", "build": "build", "desktop-live": "desktop_live"} as $gate + | . as $needs + | (($gate | keys) + ["changes", "lint"]) as $required + | ($required - keys) as $missing + | if ($missing | length) > 0 then + $missing[] | "\(.): missing" + else + $needs | (.changes.outputs // {}) as $flags | to_entries[] | select(.value.result != "success") | select(.value.result != "skipped" or $gate[.key] == null or $flags[$gate[.key]] != "false") | "\(.key): \(.value.result)" + end ' <<< "$needs")" if [ -n "$failed" ]; then echo "::error::Checks did not pass:" diff --git a/.github/scripts/ci-plan.gql b/.github/scripts/ci-plan.gql new file mode 100644 index 00000000000..db015cb2441 --- /dev/null +++ b/.github/scripts/ci-plan.gql @@ -0,0 +1,23 @@ +query($base: String!, $head: String!) { + packages { items { name path } } + affectedTasks(base: $base, head: $head, tasks: [ + "test", "build", "type-check", "ci:integration", "ci:desktop-live", + "ci:e2e-scim", "ci:e2e-cli", "ci:e2e-stop-after", "ci:e2e-desktop-inbox", + "ci:e2e-project-files", "ci:e2e-mobile", "ci:e2e-seo" + ]) { + items { + name + fullName + package { name } + reason { + __typename + ... on TaskFileChanged { filePath } + ... on TaskGlobalFileChanged { filePath } + ... on TaskGlobalDepsChanged { filePath } + ... on TaskDependencyTaskChanged { taskName packageName } + ... on TaskPackageDependencyChanged { packageName } + ... on TaskAllChanged { description } + } + } + } +} diff --git a/.github/scripts/http-e2e.sh b/.github/scripts/http-e2e.sh index a1d62419ca5..381c2006049 100755 --- a/.github/scripts/http-e2e.sh +++ b/.github/scripts/http-e2e.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash # Runs one end-to-end suite group over real HTTP, each against its own app: `next dev`, except -# mobile, which serves the production build the `e2e-build` job uploads with `next start` (the -# job extracts it into apps/sim/.next first). +# CI groups, which reuse the production build from `e2e-build`. +# The job extracts it into apps/sim/.next before running these suites. # -# Usage: http-e2e.sh (run from apps/sim) +# Usage: http-e2e.sh (run from apps/sim) # # The job provides DATABASE_URL, BETTER_AUTH_SECRET and ENCRYPTION_KEY; each group sets the rest of # its app's environment here. Reports and server logs land in $RUNNER_TEMP/e2e. @@ -20,7 +20,7 @@ # telemetry flush runs detached and still writes .next/dev). set -euo pipefail -group=${1:?usage: http-e2e.sh } +group=${1:?usage: http-e2e.sh } report_dir="$RUNNER_TEMP/e2e" ready_timeout_seconds=600 mkdir -p "$report_dir" @@ -33,8 +33,9 @@ realtime_pid='' realtime_tag='' realtime_log='' browsers_pid='' +local_app_url='' # `dev` or `start`; a group sets it before start_app. -app_server=dev +app_server=${E2E_APP_SERVER:-dev} finish() { local status=$? @@ -78,7 +79,8 @@ start_app() { status_log="$report_dir/$name-http-status.log" fi app_tag="$name-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$$" - export NEXT_PUBLIC_APP_URL="http://127.0.0.1:$port" + local_app_url="http://127.0.0.1:$port" + export NEXT_PUBLIC_APP_URL="${E2E_APP_PUBLIC_URL:-$local_app_url}" export BETTER_AUTH_URL="$NEXT_PUBLIC_APP_URL" export DISABLE_TELEMETRY=true NEXT_TELEMETRY_DISABLED=1 if [ "$app_server" = start ]; then @@ -96,7 +98,7 @@ start_app() { --port "$port" > "$server_log" 2>&1 & fi server_pid=$! - wait_ready "$server_pid" "$NEXT_PUBLIC_APP_URL/api/health" "$label app" + wait_ready "$server_pid" "$local_app_url/api/health" "$label app" } # start_realtime : the Socket.IO server the app's browser clients connect to, so pages @@ -143,12 +145,12 @@ case "$group" in export DB_TX_TRIPWIRE=throw export NEXT_PUBLIC_CHAT_DISABLED=true start_app scim 3017 SCIM record-http-status - SCIM_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \ + SCIM_E2E_BASE_URL="$local_app_url" \ SCIM_E2E_DATABASE_URL="$DATABASE_URL" \ SCIM_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \ SCIM_E2E_REPORT_PATH="$report_dir/scim-e2e-report.json" \ bun run test:scim:e2e - VERSION_COMPARE_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \ + VERSION_COMPARE_E2E_BASE_URL="$local_app_url" \ VERSION_COMPARE_E2E_DATABASE_URL="$DATABASE_URL" \ VERSION_COMPARE_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \ VERSION_COMPARE_E2E_REPORT_PATH="$report_dir/version-compare-http-report.json" \ @@ -174,7 +176,7 @@ case "$group" in bun --no-env-file scripts/test-mcp-host-e2e.ts CLI_LATENCY_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \ CLI_LATENCY_E2E_DATABASE_URL="$DATABASE_URL" \ - CLI_LATENCY_E2E_RUNS=3 \ + CLI_LATENCY_E2E_RUNS=1 \ CLI_LATENCY_E2E_WARMUP=1 \ CLI_LATENCY_E2E_REPORT_PATH="$report_dir/cli-run-latency-report.json" \ bun run test:cli-run-latency:e2e @@ -246,7 +248,13 @@ case "$group" in MOBILE_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \ MOBILE_E2E_REPORT_PATH="$report_dir/mobile-e2e-report.json" \ bun run test:mobile:e2e - # The same production build answers as a crawler sees it: statuses, robots, sitemap, JSON-LD. + ;; + + seo) + app_server=start + export NEXT_PUBLIC_FORCE_HOSTED=false + start_app seo 3024 'landing SEO' record-http-status + # Plain HTTP assertions have no browser-specific behavior. LANDING_SEO_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \ LANDING_SEO_E2E_REPORT_PATH="$report_dir/landing-seo-e2e-report.json" \ bun run test:landing-seo:e2e diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 062af93df7c..0cc719de1d5 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -2,42 +2,66 @@ name: checks on: workflow_call: + inputs: + base: + description: Optional comparison commit for a scoped manual check + type: string + required: false workflow_dispatch: + inputs: + base: + description: Compare to this full commit SHA (empty runs all checks) + type: string + required: false permissions: contents: read jobs: - # Which checks a pull request's changes can affect (.github/scripts/ci-changes.sh). A job is - # skipped only when the script prints `false` for its flag; an unset flag, and every push or - # dispatch (the deploy chain's checks), runs it. Every gated job needs this one, so its failure - # skips them and fails `ci` below. + # Attribute changes before dependency installation or service startup. Unknown inputs run all. changes: name: changes - runs-on: &runner-2vcpu ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }} + runs-on: &runner-2vcpu ubuntu-24.04 timeout-minutes: 5 outputs: - full: ${{ github.event_name != 'pull_request' || steps.classify.outputs.full != 'false' }} - test: ${{ github.event_name != 'pull_request' || steps.classify.outputs.test != 'false' }} - build: ${{ github.event_name != 'pull_request' || steps.classify.outputs.build != 'false' }} - desktop_live: ${{ github.event_name != 'pull_request' || steps.classify.outputs.desktop_live != 'false' }} + integration: ${{ steps.classify.outputs.integration }} + http: ${{ steps.classify.outputs.http }} + production: ${{ steps.classify.outputs.production }} + e2e_build: ${{ steps.classify.outputs.e2e_build }} + desktop_live: ${{ steps.classify.outputs.desktop_live }} + test: ${{ steps.classify.outputs.test }} + test_app: ${{ steps.classify.outputs.test_app }} + build: ${{ steps.classify.outputs.build }} + http_matrix: ${{ steps.classify.outputs.http_matrix }} + production_matrix: ${{ steps.classify.outputs.production_matrix }} + test_matrix: ${{ steps.classify.outputs.test_matrix }} + test_packages: ${{ steps.classify.outputs.test_packages }} + typecheck_packages: ${{ steps.classify.outputs.typecheck_packages }} steps: - - name: Checkout code (mirror) - if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork - uses: useblacksmith/checkout@25227e61ff9dafe400e22fa487b673eac4e4409a # v1.8.1 - with: - fetch-depth: 2 - name: Checkout code - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 with: fetch-depth: 2 - - name: Classify the pull request's changes + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 + with: + bun-version: 1.4.2 + - name: Install only the pinned task planner + continue-on-error: true + run: bun install --global "turbo@$(jq -r .devDependencies.turbo package.json)" + - name: Attribute changes to CI tasks id: classify - if: github.event_name == 'pull_request' env: - BASE: ${{ github.event.pull_request.base.sha }} + BASE: ${{ github.event.pull_request.base.sha || inputs.base }} + CI_PLAN_PATH: ${{ runner.temp }}/ci-plan.json run: bash .github/scripts/ci-changes.sh "$BASE" >> "$GITHUB_OUTPUT" + - name: Upload task attribution + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: ci-plan + path: ${{ runner.temp }}/ci-plan.json + retention-days: 7 integration: # Runs the real-infrastructure test layer: every `*.integration.ts` in packages/db and @@ -46,19 +70,18 @@ jobs: # # The two paths build different schemas (migrations add triggers, checks and NOT VALID # constraints that `db:push` does not), so each runs the whole suite. Vitest splits each - # suite's files across four shards by measured duration (DurationBalancedSequencer in vitest.shared.ts); a - # shard runs its files one at a time against its own database. Files run serially, so a shard - # barely uses more than one core: 4 vCPU is enough. - name: integration (${{ matrix.provision }}, ${{ matrix.shard }}/4) + # suite's files across eight shards by measured duration (DurationBalancedSequencer in vitest.shared.ts); a + # shard runs its files one at a time against its own database. + name: integration (${{ matrix.provision }}, ${{ matrix.shard }}/8) needs: changes - if: needs.changes.outputs.full == 'true' - runs-on: &runner-4vcpu ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} + if: needs.changes.outputs.integration == 'true' + runs-on: ubuntu-24.04 timeout-minutes: 20 strategy: fail-fast: false matrix: provision: [push, migrate] - shard: [1, 2, 3, 4] + shard: [1, 2, 3, 4, 5, 6, 7, 8] services: redis: image: redis:8.2-alpine @@ -105,24 +128,13 @@ jobs: ENCRYPTION_KEY: '0000000000000000000000000000000000000000000000000000000000000000' steps: - # A same-repository pull request checks out through a git mirror on a sticky disk, so a slow - # clone from GitHub (1 in 10 took a minute or more, up to 4) no longer sets the run's pace. - # The mirror is one disk per repository that job steps can write to, so fork pull requests - # and pushes, whose checks gate a deploy, keep the plain checkout: same trust split as the - # setup action's caches. - - &checkout-mirror - name: Checkout code (mirror) - if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork - uses: useblacksmith/checkout@25227e61ff9dafe400e22fa487b673eac4e4409a # v1.8.1 - - &checkout-plain - name: Checkout code - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork + - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 - name: Setup workspace uses: ./.github/actions/setup with: - provider: ${{ vars.CI_PROVIDER }} + provider: github - name: Provision a fresh database through the supported command working-directory: packages/db @@ -137,14 +149,14 @@ jobs: - name: Run packages/db integration tests working-directory: packages/db - run: bun run test --mode integration --shard=${{ matrix.shard }}/4 + run: bun run test --mode integration --shard=${{ matrix.shard }}/8 - name: Run apps/sim integration tests working-directory: apps/sim # A non-UTC process zone keeps timestamp-without-time-zone handling honest. env: TZ: America/Los_Angeles - run: bun run test --mode integration --shard=${{ matrix.shard }}/4 + run: bun run test --mode integration --shard=${{ matrix.shard }}/8 - name: Run apps/realtime integration tests if: matrix.shard == 1 @@ -178,28 +190,16 @@ jobs: # The suites exercise HTTP behavior rather than a provisioning path, so they run once, against # the production (migrate) path. Each group's app environment lives in http-e2e.sh. # - # SCIM's hosted suites use 8 vCPU; the other self-hosted suites fit on 4. The mobile browser - # suite runs in e2e-mobile below. + # All groups reuse the production build; compiling routes is not part of HTTP acceptance. e2e: name: e2e (${{ matrix.group }}) - needs: changes - if: needs.changes.outputs.full == 'true' - runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && matrix.runner || 'ubuntu-latest' }} + needs: [changes, e2e-build] + if: needs.changes.outputs.http == 'true' + runs-on: ubuntu-24.04 timeout-minutes: 20 strategy: fail-fast: false - matrix: - include: - - group: scim - runner: blacksmith-8vcpu-ubuntu-2404 - - group: cli - runner: blacksmith-4vcpu-ubuntu-2404 - - group: stop-after - runner: blacksmith-4vcpu-ubuntu-2404 - - group: project-files - runner: blacksmith-8vcpu-ubuntu-2404 - - group: desktop-inbox - runner: blacksmith-4vcpu-ubuntu-2404 + matrix: ${{ fromJSON(needs.changes.outputs.http_matrix) }} services: postgres: image: pgvector/pgvector:pg17 @@ -214,9 +214,9 @@ jobs: --health-interval 5s --health-timeout 5s --health-retries 10 - # Project file subscriptions and the desktop executor use Redis. + # Desktop inbox and Project Files use Redis. redis: - image: redis:8.2-alpine + image: ${{ (matrix.group == 'desktop-inbox' || matrix.group == 'project-files') && 'redis:8.2-alpine' || '' }} ports: - 6379:6379 options: >- @@ -227,22 +227,46 @@ jobs: env: DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_test BETTER_AUTH_SECRET: http-e2e-ci-secret-at-least-32-characters + E2E_APP_SERVER: start + E2E_APP_PUBLIC_URL: ${{ matrix.group == 'scim' && 'http://scim-e2e.sim.ai:3017' || '' }} + INTERNAL_API_BASE_URL: ${{ matrix.group == 'scim' && 'http://127.0.0.1:3017' || '' }} + TRUSTED_ORIGINS: ${{ matrix.group == 'scim' && 'http://127.0.0.1:3017' || '' }} + PROJECT_FILES_E2E_APP_SERVER: start ENCRYPTION_KEY: '0000000000000000000000000000000000000000000000000000000000000000' steps: - - *checkout-mirror - - *checkout-plain + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 - name: Setup workspace uses: ./.github/actions/setup with: - provider: ${{ vars.CI_PROVIDER }} + provider: github # Migrations create their own extensions, as on a fresh self-hosted install. - name: Provision the database through migrations + if: matrix.group != 'project-files' working-directory: packages/db run: bun run db:migrate + - &download-e2e-build + name: Download the production build + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: next-build + path: ${{ runner.temp }} + + # A hosted build (NEXT_PUBLIC_APP_URL on sim.ai) prerenders every post; the build production + # images use prerenders none. The URL itself is no marker: next.config's redirects name + # www.sim.ai in every build. + - &extract-e2e-build + name: Extract the production build + run: | + tar -I zstd -xf "$RUNNER_TEMP/next-build.tar.zst" -C apps/sim + jq -e '[.routes[].srcRoute] | any(. == "/blog/[slug]" or . == "/library/[slug]") | not' \ + apps/sim/.next/prerender-manifest.json > /dev/null || + { echo "::error::The build prerendered posts (a hosted build) or has no prerender manifest."; exit 1; } + # JOBS=4 caps node-gyp parallelism as docker/app.Dockerfile does (laverdet/isolated-vm#428). - name: Build native isolate addon for Project file rendering if: matrix.group == 'project-files' @@ -254,12 +278,25 @@ jobs: cd "$addon_dir" bun run install + - name: Map the hosted SCIM fixture to loopback + if: matrix.group == 'scim' + run: printf '127.0.0.1 scim-e2e.sim.ai\n' | sudo tee -a /etc/hosts + # No step timeout: the job's bound covers a hang without cutting a slow but healthy suite # short of writing its report. - name: Run end-to-end suites working-directory: apps/sim run: bash ../../.github/scripts/http-e2e.sh "${{ matrix.group }}" + - name: Upload acceptance timing and coverage reports + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: e2e-reports-${{ matrix.group }} + path: ${{ runner.temp }}/e2e/**/*.json + if-no-files-found: ignore + retention-days: 7 + - name: Upload end-to-end reports and server logs if: failure() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 @@ -269,23 +306,22 @@ jobs: if-no-files-found: ignore retention-days: 7 - # The mobile browser suite against the production build e2e-build uploads, served by - # `next start` (http-e2e.sh), one browser per job: Chromium and WebKit no longer run back to - # back, and no route waits on a dev compile. Gated like `e2e`, on a build that already ran. + # Browser suites reuse the production build, avoiding dev-route compiles. + # Each job has its own database and runtime flags; the build reads public env at startup. + # Each browser partitions the full viewport list across two shards, including future additions. # # 8 vCPU: `next start`, Postgres, the realtime server and one Playwright browser would fit a 4 # vCPU runner's 16 GB, but the suite is timing-sensitive, and CPU contention between the browser # and the server is what turns a slow tap into a flake. - e2e-mobile: - name: e2e (mobile, ${{ matrix.browser }}) + e2e-production: + name: e2e (${{ matrix.name }}) needs: [changes, e2e-build] - if: needs.changes.outputs.full == 'true' + if: needs.changes.outputs.production == 'true' runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 25 strategy: fail-fast: false - matrix: - browser: [chromium, webkit] + matrix: ${{ fromJSON(needs.changes.outputs.production_matrix) }} services: postgres: image: pgvector/pgvector:pg17 @@ -306,8 +342,20 @@ jobs: ENCRYPTION_KEY: '0000000000000000000000000000000000000000000000000000000000000000' steps: - - *checkout-mirror - - *checkout-plain + # A same-repository pull request checks out through a git mirror on a sticky disk, so a slow + # clone from GitHub (1 in 10 took a minute or more, up to 4) no longer sets the run's pace. + # The mirror is one disk per repository that job steps can write to, so fork pull requests + # and pushes, whose checks gate a deploy, keep the plain checkout: same trust split as the + # setup action's caches. + - &checkout-mirror + name: Checkout code (mirror) + if: (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork + uses: useblacksmith/checkout@25227e61ff9dafe400e22fa487b673eac4e4409a # v1.8.1 + - &checkout-plain + name: Checkout code + if: (vars.CI_PROVIDER != '' && vars.CI_PROVIDER != 'blacksmith') || github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + - name: Setup workspace uses: ./.github/actions/setup @@ -318,46 +366,43 @@ jobs: working-directory: packages/db run: bun run db:migrate - - name: Download the production build - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 - with: - name: next-build - path: ${{ runner.temp }} - - # A hosted build (NEXT_PUBLIC_APP_URL on sim.ai) prerenders every post; the build production - # images use prerenders none. The URL itself is no marker: next.config's redirects name - # www.sim.ai in every build. - - name: Extract the production build - run: | - tar -I zstd -xf "$RUNNER_TEMP/next-build.tar.zst" -C apps/sim - jq -e '[.routes[].srcRoute] | any(. == "/blog/[slug]" or . == "/library/[slug]") | not' \ - apps/sim/.next/prerender-manifest.json > /dev/null || - { echo "::error::The build prerendered posts (a hosted build) or has no prerender manifest."; exit 1; } + - *download-e2e-build + - *extract-e2e-build - name: Run end-to-end suites working-directory: apps/sim env: MOBILE_E2E_BROWSER: ${{ matrix.browser }} - run: bash ../../.github/scripts/http-e2e.sh mobile + MOBILE_E2E_SHARD: ${{ matrix.shard }} + run: bash ../../.github/scripts/http-e2e.sh "${{ matrix.group }}" - name: Upload end-to-end reports and server logs if: failure() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: http-e2e-mobile-${{ matrix.browser }} + name: http-e2e-${{ matrix.artifact }} path: ${{ runner.temp }}/e2e/ if-no-files-found: ignore retention-days: 7 - # Desktop tools in the real Electron app against a local app, on its own runner: the - # Electron app, the dev app and its realtime server together outgrow the e2e runner. + - name: Upload acceptance timing and coverage reports + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: e2e-reports-${{ matrix.artifact }} + path: ${{ runner.temp }}/e2e/**/*.json + if-no-files-found: ignore + retention-days: 7 + + # Desktop tools exercise the real Electron app against the shared production build. + # Hosted 4-vCPU runs passed both shards within 14 seconds of 8-vCPU; production peak ~4 GB. # Two runners: the lease-lapse test sleeps through a long import and then a lease (~3 min), as # long as the rest of the spec together, so on one runner it set the time for every run. desktop-live: name: desktop-live (${{ matrix.tests }}) - needs: changes + needs: [changes, e2e-build] if: needs.changes.outputs.desktop_live == 'true' - runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-latest' }} + runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 30 strategy: fail-fast: false @@ -408,22 +453,8 @@ jobs: working-directory: packages/db run: bun run db:migrate - # Turbopack's dev cache turns the spec's route warm-up from a cold compile (~4 min) into a - # restore. It is content-addressed, so a pull request's changed modules still recompile; the - # key carries the installed Next version so an upgrade starts from an empty cache, and the - # event and fork segments keep untrusted runs off the cache trusted runs read. - - name: Resolve Turbopack dev cache key - id: next-cache - run: echo "key=${GITHUB_REPOSITORY}-next-dev-desktop-live-${GITHUB_EVENT_NAME}${FORK_SUFFIX}-$(jq -r .version node_modules/next/package.json)" >> "$GITHUB_OUTPUT" - env: - FORK_SUFFIX: ${{ github.event.pull_request.head.repo.fork && '-fork' || '' }} - - - name: Mount Turbopack dev cache - uses: ./.github/actions/cache - with: - provider: ${{ vars.CI_PROVIDER }} - key: ${{ steps.next-cache.outputs.key }} - path: ./apps/sim/.next/dev + - *download-e2e-build + - *extract-e2e-build # Chat switches, Stop, sign-out, approval, the dormant-executor round trip, and background # runs across a chat switch and a network cut. The spec runs the recording proxy (the app's @@ -434,6 +465,7 @@ jobs: BETTER_AUTH_URL: http://127.0.0.1:3020 REDIS_URL: redis://127.0.0.1:6379 SIM_AGENT_API_URL: http://127.0.0.1:3022 + SIM_DESKTOP_E2E_APP_SERVER: start NEXT_PUBLIC_SOCKET_URL: http://127.0.0.1:3023 SOCKET_SERVER_URL: http://127.0.0.1:3023 NEXT_PUBLIC_FORCE_HOSTED: 'false' @@ -452,37 +484,12 @@ jobs: # every process it started has exited. realtime_tag="desktop-realtime-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$$" server_tag="desktop-tools-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$$" - # Keeps the restored cache under the cap the dev scripts apply locally. - (cd apps/sim && bun run dev:cache:cap) start_sim() { - (cd apps/sim && E2E_APP="$server_tag" exec setsid node ../../node_modules/next/dist/bin/next dev --hostname 127.0.0.1 \ + (cd apps/sim && E2E_APP="$server_tag" LOG_LEVEL=INFO exec setsid node ../../node_modules/next/dist/bin/next start --hostname 127.0.0.1 \ --port 3021 >> "$next_log" 2>&1) & server_pid=$! } - # A corrupted cache aborts Turbopack instead of falling back. Only then is the shared cache - # dropped: a failing test must not cost every later run its warm cache. - cache_broken() { - grep -qiE 'cache corruption|turbopack.*panic|panicked' "$next_log" 2>/dev/null - } - # The cache directory is a mount point: empty it rather than remove it. Absolute, because - # the EXIT trap runs after the step has moved into apps/desktop. - clear_cache() { - find "$GITHUB_WORKSPACE/apps/sim/.next/dev" -mindepth 1 -maxdepth 1 -exec rm -rf {} + - } - # A sticky disk keeps nothing from a failed job, so a clear here sticks only when the job - # passes. The marker names the cache for desktop-live-cache-reset, which empties it after - # a failed job; written on each panic, it survives the restart truncating the log. - mark_cache_broken() { - jq -r .version "$GITHUB_WORKSPACE/node_modules/next/package.json" > "$report_dir/turbopack-cache-broken" - } - # SIGINT first: `next dev` SIGKILLs its server 100ms after SIGTERM, which discards a cache - # write in flight. stop-session.sh then removes anything still running. - # SIGINT is best-effort; the cleanup always runs, since workers and the detached telemetry - # flush can outlive a server that has already exited. stop_sim() { - if kill -INT "$server_pid" 2>/dev/null; then - for _ in $(seq 1 30); do kill -0 "$server_pid" 2>/dev/null || break; sleep 1; done - fi bash "$GITHUB_WORKSPACE/.github/scripts/stop-session.sh" "$server_pid" "$server_tag" } (cd apps/realtime && PORT=3023 SIM_DB_ROLE=realtime ALLOWED_ORIGINS="$NEXT_PUBLIC_APP_URL" \ @@ -494,11 +501,6 @@ jobs: stop_sim || status=1 bash "$GITHUB_WORKSPACE/.github/scripts/stop-session.sh" "$realtime_pid" "$realtime_tag" || status=1 wait "$server_pid" "$realtime_pid" 2>/dev/null || true - if cache_broken; then - echo "::warning::Turbopack reported a broken dev cache; clearing it for the next run." - mark_cache_broken - clear_cache - fi exit "$status" } trap finish EXIT @@ -510,21 +512,10 @@ jobs: # which a Linux run does not use. (cd apps/desktop && bun run scripts/build.ts) started=$SECONDS - retried=0 until curl --fail --silent --max-time 10 http://127.0.0.1:3021/api/health > /dev/null && curl --fail --silent --max-time 10 http://127.0.0.1:3023/health > /dev/null; do if ! kill -0 "$server_pid" 2>/dev/null; then tail -n 200 "$next_log" - if [ "$retried" = 0 ] && cache_broken; then - echo "::warning::Turbopack rejected the restored dev cache; restarting from an empty cache." - bash "$GITHUB_WORKSPACE/.github/scripts/stop-session.sh" "$server_pid" "$server_tag" - mark_cache_broken - clear_cache - : > "$next_log" - retried=1 - start_sim - continue - fi exit 1 fi kill -0 "$realtime_pid" 2>/dev/null || { tail -n 200 "$report_dir/desktop-tools-realtime.log"; exit 1; } @@ -536,11 +527,21 @@ jobs: SIM_DESKTOP_E2E_SIM_URL=http://127.0.0.1:3021 \ SIM_DESKTOP_E2E_PROXY_PORT=3020 \ SIM_DESKTOP_E2E_AGENT_PORT=3022 \ + PLAYWRIGHT_JSON_OUTPUT_FILE="$report_dir/desktop-tests.json" \ SIM_DESKTOP_E2E_DATABASE_URL="$DATABASE_URL" \ SIM_DESKTOP_E2E_REDIS_URL="$REDIS_URL" \ SIM_DESKTOP_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \ xvfb-run -a -s '-screen 0 1920x1200x24' bunx playwright test e2e/desktop-tools-live-sim.spec.ts \ - ${{ matrix.filter }} --output "$report_dir/desktop-tools-results" --retries=0 + ${{ matrix.filter }} --output "$report_dir/desktop-tools-results" --retries=0 --reporter=line,json + + - name: Upload Electron coverage and timings + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: desktop-reports-${{ matrix.tests }} + path: ${{ runner.temp }}/e2e/desktop-tests.json + if-no-files-found: ignore + retention-days: 7 - name: Upload Electron E2E results and server logs if: failure() @@ -551,68 +552,12 @@ jobs: if-no-files-found: ignore retention-days: 7 - # Empties a dev cache Turbopack panicked on in a failed desktop-live run. The run's own clear is - # discarded with the rest of its failed job, so every later run would restore the same broken - # cache and fail; emptied in this passing job, the empty cache is what the next run mounts. A - # failure the cache did not cause leaves no marker, and the warm cache stays. - desktop-live-cache-reset: - name: desktop-live cache reset - needs: desktop-live - if: ${{ always() && needs.desktop-live.result == 'failure' }} - runs-on: *runner-2vcpu - timeout-minutes: 5 - steps: - - name: Download desktop-live results - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 - with: - pattern: desktop-live-e2e-results-* - path: ${{ runner.temp }}/desktop-live - merge-multiple: true - - # The marker carries only the Next version; the rest of the key comes from this run's own - # context, as in desktop-live, so a fork's marker can name no cache but its own. - - name: Resolve the broken cache's key - id: broken - env: - MARKER: ${{ runner.temp }}/desktop-live/turbopack-cache-broken - FORK_SUFFIX: ${{ github.event.pull_request.head.repo.fork && '-fork' || '' }} - run: | - if [ ! -s "$MARKER" ]; then - echo "No broken dev cache reported; keeping the warm cache." - exit 0 - fi - next_version="$(head -n 1 "$MARKER")" - if ! [[ "$next_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+[0-9A-Za-z.-]*$ ]]; then - echo "::error::The broken cache marker holds no Next version: '$next_version'" - exit 1 - fi - echo "key=${GITHUB_REPOSITORY}-next-dev-desktop-live-${GITHUB_EVENT_NAME}${FORK_SUFFIX}-${next_version}" >> "$GITHUB_OUTPUT" - - - *checkout-mirror - - *checkout-plain - - # A desktop-live leg that passed may have saved this run's actions/cache entry already; the - # suffix gives the emptied cache its own, newer entry for the next run's prefix restore. - - name: Mount the broken dev cache - if: steps.broken.outputs.key != '' - uses: ./.github/actions/cache - with: - provider: ${{ vars.CI_PROVIDER }} - key: ${{ steps.broken.outputs.key }} - path: ./apps/sim/.next/dev - save-suffix: -reset - - - name: Empty the dev cache - if: steps.broken.outputs.key != '' - run: | - mkdir -p apps/sim/.next/dev - find apps/sim/.next/dev -mindepth 1 -maxdepth 1 -exec rm -rf {} + - # Lint, audits, type-check and the schema sync check: about two minutes of mostly cached work, # kept off the test shards so neither waits on the other. lint: name: lint - runs-on: *runner-4vcpu + needs: changes + runs-on: &runner-4vcpu ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} timeout-minutes: 15 steps: @@ -624,16 +569,14 @@ jobs: # Worth stating because the failure was invisible for so long: the migration # audit read the resulting `git diff` failure as "no migrations changed" and # exited 0, so it had never actually run on a push build. - # Same mirror/plain split as the integration job's checkout. - - &checkout-mirror-depth2 - name: Checkout code (mirror) - if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork + # Same provider/trust split as the build jobs' checkout. + - name: Checkout code (mirror) + if: (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork uses: useblacksmith/checkout@25227e61ff9dafe400e22fa487b673eac4e4409a # v1.8.1 with: fetch-depth: 2 - - &checkout-plain-depth2 - name: Checkout code - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork + - name: Checkout code + if: (vars.CI_PROVIDER != '' && vars.CI_PROVIDER != 'blacksmith') || github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 with: fetch-depth: 2 @@ -744,14 +687,23 @@ jobs: - name: Migration safety (zero-downtime) audit run: bun run check:migrations "${{ steps.audit_base.outputs.ref }}" - # Every workspace, not just realtime. packages/emcn, packages/utils, + # Every affected workspace, following the same real task graph as tests. Root/unknown + # changes select all. packages/emcn, packages/utils, # apps/desktop and apps/docs had no type check in CI at all; apps/sim's # source was covered only as a side effect of `next build` in the separate # `build` job. This does NOT cover apps/sim's tests — its tsconfig # excludes *.test.ts(x), which carry ~2.2k errors; the `test-types` job # ratchets those per file instead. - - name: Type-check all workspaces - run: bunx turbo run type-check + - name: Type-check affected workspaces + env: + TYPECHECK_PACKAGES: ${{ needs.changes.outputs.typecheck_packages }} + run: | + jq -e 'type == "array" and all(.[]; type == "string")' <<< "$TYPECHECK_PACKAGES" > /dev/null + filters=() + while IFS= read -r package; do filters+=("--filter=$package"); done < <(jq -r '.[]' <<< "$TYPECHECK_PACKAGES") + if [ "${#filters[@]}" -gt 0 ]; then + bunx turbo run type-check "${filters[@]}" + fi - name: Check schema and migrations are in sync working-directory: packages/db @@ -776,7 +728,7 @@ jobs: test-types: name: test-types needs: changes - if: needs.changes.outputs.test == 'true' + if: needs.changes.outputs.test_app == 'true' runs-on: *runner-4vcpu timeout-minutes: 10 @@ -806,24 +758,32 @@ jobs: # the other workspaces. Each shard keeps its own Turbo cache: pass-through args are part of the # task hash, so a shard only ever replays its own result. test: - name: test (${{ matrix.shard }}/2) + name: test (${{ matrix.shard }}/${{ needs.changes.outputs.test_app == 'true' && '2' || '1' }}) needs: changes if: needs.changes.outputs.test == 'true' - runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }} + runs-on: ${{ needs.changes.outputs.test_app == 'true' && (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }} timeout-minutes: 15 strategy: fail-fast: false - matrix: - shard: [1, 2] + matrix: ${{ fromJSON(needs.changes.outputs.test_matrix) }} + steps: - - *checkout-mirror-depth2 - - *checkout-plain-depth2 + - name: Checkout code (mirror) + if: needs.changes.outputs.test_app == 'true' && (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork + uses: useblacksmith/checkout@25227e61ff9dafe400e22fa487b673eac4e4409a # v1.8.1 + with: + fetch-depth: 2 + - name: Checkout code + if: needs.changes.outputs.test_app != 'true' || (vars.CI_PROVIDER != '' && vars.CI_PROVIDER != 'blacksmith') || github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + fetch-depth: 2 - name: Setup workspace uses: ./.github/actions/setup with: - provider: ${{ vars.CI_PROVIDER }} + provider: ${{ needs.changes.outputs.test_app != 'true' && 'github' || vars.CI_PROVIDER }} turbo-cache-key: turbo-cache-test-${{ matrix.shard }} # cloud-review-tools.test.ts runs the real helper on the runner, which shells @@ -832,7 +792,7 @@ jobs: run: command -v rg || (sudo apt-get update && sudo apt-get install -y ripgrep) - name: Verify shell placeholder compilation in Bash - if: matrix.shard == 1 + if: matrix.shard == 1 && needs.changes.outputs.test_app == 'true' working-directory: apps/sim env: SHELL_PLACEHOLDERS_REPORT_PATH: ${{ runner.temp }}/shell-placeholders.json @@ -854,13 +814,22 @@ jobs: DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/simstudio' ENCRYPTION_KEY: '0000000000000000000000000000000000000000000000000000000000000000' # dummy key for CI only TURBO_CACHE_DIR: .turbo + TEST_APP: ${{ needs.changes.outputs.test_app }} + TEST_PACKAGES: ${{ needs.changes.outputs.test_packages }} SHARD: ${{ matrix.shard }} run: | if [ "$SHARD" = 1 ]; then bun run test:scripts - bunx turbo run test --filter='!@sim/app' + jq -e 'type == "array" and all(.[]; type == "string")' <<< "$TEST_PACKAGES" > /dev/null + filters=() + while IFS= read -r package; do filters+=("--filter=$package"); done < <(jq -r '.[]' <<< "$TEST_PACKAGES") + if [ "${#filters[@]}" -gt 0 ]; then + bunx turbo run test "${filters[@]}" --filter='!@sim/app' + fi + fi + if [ "$TEST_APP" = true ]; then + bunx turbo run test --filter=@sim/app -- --shard="$SHARD/2" fi - bunx turbo run test --filter=@sim/app -- --shard="$SHARD/2" # Next.js production build, in parallel with lint + tests. Sticky disks are # cloned from the last committed snapshot per job and committed last-writer- @@ -931,7 +900,7 @@ jobs: TURBO_CACHE_DIR: .turbo run: bunx turbo run build --filter=@sim/app - # The production build e2e-mobile serves with `next start`, built in the public-env mode + # The production build e2e-production serves with `next start`, built in the public-env mode # production images use: NEXT_PUBLIC_APP_URL is docker/app.Dockerfile's localhost default, so # every route is dynamic and NEXT_PUBLIC_* are read at start. (Images run the standalone # DOCKER_BUILD server; this is the plain `next build` output.) The `build` job above keeps the @@ -941,7 +910,7 @@ jobs: e2e-build: name: build (e2e) needs: changes - if: needs.changes.outputs.full == 'true' + if: needs.changes.outputs.e2e_build == 'true' runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-16vcpu-ubuntu-2404' || 'linux-x64-8-core' }} timeout-minutes: 25 @@ -974,16 +943,16 @@ jobs: AWS_REGION: 'us-west-2' ENCRYPTION_KEY: '0000000000000000000000000000000000000000000000000000000000000000' # dummy key for CI only TURBO_CACHE_DIR: .turbo - # This build is on the e2e-mobile critical path; its ~30 s TypeScript pass repeats what + # This build is on the e2e-production critical path; its ~30 s TypeScript pass repeats what # lint's type-check and the `build` job's own `next build` (generated route types too) # already enforce. NEXT_BUILD_SKIP_TYPE_CHECK: 'true' run: bunx turbo run build --filter=@sim/app - # Everything `next start` reads; node_modules and the source tree come from e2e-mobile's own + # Everything `next start` reads; node_modules and the source tree come from e2e-production's own # checkout and setup, at the same workspace path and lockfile. Source maps are over half the # build and only serve stack traces and devtools, which the suite never reads. - - name: Package the build for e2e-mobile + - name: Package the build for e2e-production run: >- tar -I 'zstd -T0' -cf "$RUNNER_TEMP/next-build.tar.zst" -C apps/sim --exclude .next/cache --exclude .next/trace --exclude '*.map' .next @@ -991,7 +960,7 @@ jobs: # Already zstd-compressed, so stored without zip compression. `overwrite` lets a re-run of # this job replace the earlier attempt's build; three days covers re-running a failed shard # after a weekend. - - name: Upload the build for e2e-mobile + - name: Upload the build for e2e-production uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: next-build @@ -1011,13 +980,15 @@ jobs: ci: name: ci needs: - [changes, integration, e2e, e2e-mobile, desktop-live, lint, test-types, test, build, e2e-build] + [changes, integration, e2e, e2e-production, desktop-live, lint, test-types, test, build, e2e-build] if: ${{ always() }} runs-on: *runner-2vcpu timeout-minutes: 5 steps: - - *checkout-mirror - - *checkout-plain + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + sparse-checkout: .github/scripts - name: Require every check to pass env: NEEDS: ${{ toJSON(needs) }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6482b395ea2..62879db98f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,8 +8,8 @@ name: ci # A repo variable, not a committed value: during a Blacksmith outage there is no # working CI to merge a switchover through. Only unset/'blacksmith' selects # Blacksmith; anything unrecognized selects GitHub so a typo can't queue jobs -# against the provider you're escaping. Every runs-on and both composite actions -# share this predicate and must change together. +# against the provider you're escaping. Paid build/browser jobs and their cache actions share +# this predicate. Task planning, the aggregate gate, HTTP acceptance and integration tests use free GitHub runners. # # GitHub mode is break-glass, not a peer — cold layers, slower runs. The app image # is the one job on a paid larger runner: next build needs ~32 GB and OOM-kills diff --git a/.github/workflows/desktop-e2e.yml b/.github/workflows/desktop-e2e.yml index 3acd0e4b6b4..88faa1918cd 100644 --- a/.github/workflows/desktop-e2e.yml +++ b/.github/workflows/desktop-e2e.yml @@ -1,6 +1,8 @@ name: desktop-e2e -# Smoke coverage of the real Electron shell on desktop changes, plus a weekly +# Smoke coverage of the real Electron shell on desktop changes. Three isolated runners +# partition the complete suite by Playwright test-file groups; workers remain serial per runner. +# The weekly # advisory canary against electron@latest so Chromium-cadence breakage surfaces # before an upgrade is attempted. @@ -58,10 +60,14 @@ concurrency: jobs: e2e: - name: e2e + name: e2e (${{ matrix.shard }}/3) if: github.event_name != 'schedule' runs-on: macos-26 timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3] steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 @@ -102,8 +108,9 @@ jobs: - name: Run Playwright _electron smoke suite working-directory: apps/desktop - run: bunx playwright test + run: bunx playwright test --shard=${{ matrix.shard }}/3 --reporter=line,json env: + PLAYWRIGHT_JSON_OUTPUT_FILE: test-results/desktop-smoke.json BACKGROUND_EXECUTOR_REPORT_PATH: test-results/background-executor-report.json TERMINAL_CANCEL_REPORT_PATH: test-results/terminal-cancel-report.json @@ -111,10 +118,19 @@ jobs: if: failure() uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - name: desktop-e2e-results-pinned + name: desktop-e2e-results-pinned-${{ matrix.shard }} path: apps/desktop/test-results retention-days: 7 + - name: Upload smoke coverage and timings + if: always() + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: desktop-smoke-reports-${{ matrix.shard }} + path: apps/desktop/test-results/desktop-smoke.json + if-no-files-found: ignore + retention-days: 7 + e2e-canary: name: e2e-canary if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' diff --git a/apps/desktop/e2e/desktop-tools-live-sim.spec.ts b/apps/desktop/e2e/desktop-tools-live-sim.spec.ts index 8feb023bd96..f83e6c61917 100644 --- a/apps/desktop/e2e/desktop-tools-live-sim.spec.ts +++ b/apps/desktop/e2e/desktop-tools-live-sim.spec.ts @@ -79,7 +79,8 @@ test.describe('desktop tools against a live Sim', () => { db = new SimDatabase(sim) await Promise.all([proxy.start(), agent.start()]) proxy.rewriteAnswer(REGISTRATION_PATH, executorUnavailable) - await test.step('warm up the routes the tests use', warmUp) + if (process.env.SIM_DESKTOP_E2E_APP_SERVER !== 'start') + await test.step('warm up the routes the tests use', warmUp) }) test.afterAll(async () => { diff --git a/apps/sim/ee/scim/TESTING.md b/apps/sim/ee/scim/TESTING.md index 3a12121a398..3abfe6d9d81 100644 --- a/apps/sim/ee/scim/TESTING.md +++ b/apps/sim/ee/scim/TESTING.md @@ -9,9 +9,10 @@ provisioning service. ## Requirements - Bun and the repository dependencies installed. -- A running local app configured for Enterprise SCIM, using a dedicated local +- A running local app configured for hosted Enterprise SCIM with billing enabled, using a dedicated local PostgreSQL database with all migrations applied (including `0325`). Leave Redis unconfigured so the app uses PostgreSQL for rate-limit storage. + For `next dev`, set `NEXT_PUBLIC_FORCE_HOSTED=true` and `BILLING_ENABLED=true`. - The app's local `BETTER_AUTH_SECRET`, at least 32 characters long. - An app URL using HTTP on `localhost`, `127.0.0.1`, or `::1`. The database must also use a loopback host, and its name must include `test` or `scim`. @@ -53,6 +54,8 @@ timeout. - Discovery, session-authenticated configuration, credential hashing, tenant isolation, bearer authentication, content type, and malformed JSON errors. +- Hosted Enterprise entitlement loss rejects both session-authenticated administration + and an existing bearer token; restoring the subscription restores both paths. - User and group creation, reads, replacement, PATCH, deletion, conflict errors, stable pagination, count-only queries, and group/member attribute projection. - Case-insensitive user lookup, external IDs, secondary/work/primary email @@ -78,10 +81,14 @@ timeout. The `e2e (scim)` job in `.github/workflows/checks.yml` provisions its own database through `db:migrate`, starts a local Next.js app -with hosted Enterprise configuration, and runs the HTTP suite above. It has its +with hosted Enterprise configuration, and runs the HTTP suite above. The production +app's public URL uses `scim-e2e.sim.ai`, mapped to loopback on that runner; the +tests and internal API requests still connect to `127.0.0.1`. Production ignores +`NEXT_PUBLIC_FORCE_HOSTED`, so setting that flag alone cannot provide hosted coverage. +It has its own database because the hosted app runs background usage replay against it. -Readiness is bounded to 300 seconds, since the first request cold-compiles the -app; the server is stopped when the step exits. A failure prints the server log +Readiness is bounded to 600 seconds for either production or local development +startup; the server is stopped when the step exits. A failure prints the server log tail and uploads the credential-free scenario report, an allowlist of HTTP status log lines, and the local app's server log. The CI app uses only fixture secrets. diff --git a/apps/sim/lib/mothership/tools/client/computer-tool-unload.test.ts b/apps/sim/lib/mothership/tools/client/computer-tool-unload.test.ts index cf2c384afb6..5cee9bef2e0 100644 --- a/apps/sim/lib/mothership/tools/client/computer-tool-unload.test.ts +++ b/apps/sim/lib/mothership/tools/client/computer-tool-unload.test.ts @@ -8,6 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { copilotConfirmBodySchema } from '@/lib/api/contracts/copilot' import { executeComputerToolOnClient } from '@/lib/mothership/tools/client/computer-tool-execution' import { clearUserData } from '@/stores' +import '@/stores/reset-all-stores' const native = vi.hoisted(() => ({ execute: vi.fn(), cancel: vi.fn() })) vi.mock('@/lib/computer-use/transport', () => ({ diff --git a/apps/sim/package.json b/apps/sim/package.json index 0b65be456bf..2ce1a72a2f3 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -40,7 +40,16 @@ "generate-docs": "bun run ../../scripts/generate-docs.ts", "org:consolidate-users": "bun run scripts/consolidate-users-into-organization.ts", "verify:oracledb": "node scripts/oracledb-patch-self-test.cjs --worker lib/internal/oracledb/oracle-worker.cjs", - "build:cli-runtime": "bun build ../../packages/sim-cli/src/runtime.ts --target=node --format=esm --packages=bundle --outfile=../../packages/sim-cli/dist/runtime.js" + "build:cli-runtime": "bun build ../../packages/sim-cli/src/runtime.ts --target=node --format=esm --packages=bundle --outfile=../../packages/sim-cli/dist/runtime.js", + "ci:e2e-scim": "bash ../../.github/scripts/http-e2e.sh scim", + "ci:e2e-cli": "bash ../../.github/scripts/http-e2e.sh cli", + "ci:e2e-stop-after": "bash ../../.github/scripts/http-e2e.sh stop-after", + "ci:e2e-desktop-inbox": "bash ../../.github/scripts/http-e2e.sh desktop-inbox", + "ci:e2e-project-files": "bash ../../.github/scripts/http-e2e.sh project-files", + "ci:e2e-mobile": "bash ../../.github/scripts/http-e2e.sh mobile", + "ci:e2e-seo": "bash ../../.github/scripts/http-e2e.sh seo", + "ci:integration": "vitest run --mode integration", + "ci:desktop-live": "cd ../desktop && bunx --no-install playwright test e2e/desktop-tools-live-sim.spec.ts" }, "dependencies": { "@1password/sdk": "0.5.0", diff --git a/apps/sim/scripts/test-mcp-host-e2e.ts b/apps/sim/scripts/test-mcp-host-e2e.ts index 96928c60db0..d877892880d 100644 --- a/apps/sim/scripts/test-mcp-host-e2e.ts +++ b/apps/sim/scripts/test-mcp-host-e2e.ts @@ -207,11 +207,6 @@ try { assert.match(response.headers.get('content-type') ?? '', /^text\/html/) }) - await check('a real page is not shadowed by the Markdown 404', async () => { - const { response } = await request('/', 'Mozilla/5.0', baseUrl.host, 'GET', MARKDOWN_ACCEPT) - assert.equal(response.status, 200) - }) - await check('an unknown API path keeps its JSON 404 for a Markdown client', async () => { const { response, body } = await request( '/api/not-a-real-route', diff --git a/apps/sim/scripts/test-mobile-e2e.ts b/apps/sim/scripts/test-mobile-e2e.ts index 0ebc467311b..698dd952e4c 100644 --- a/apps/sim/scripts/test-mobile-e2e.ts +++ b/apps/sim/scripts/test-mobile-e2e.ts @@ -21,6 +21,7 @@ import postgres from 'postgres' * Requires MOBILE_E2E_BASE_URL, MOBILE_E2E_DATABASE_URL, MOBILE_E2E_AUTH_SECRET, * and MOBILE_E2E_REPORT_PATH. Install Chromium and WebKit with Playwright first. * MOBILE_E2E_BROWSER and MOBILE_E2E_VIEWPORT narrow a diagnostic rerun. + * MOBILE_E2E_SHARD (index/count, one-based) partitions all viewports across CI jobs. */ const logger = createLogger('MobileE2E') const expect = browserExpect.configure({ timeout: 20_000 }) @@ -104,6 +105,12 @@ assert( !selectedViewport || viewports.some((viewport) => viewport.name === selectedViewport), 'Unknown viewport' ) +const shard = /^([1-9]\d*)\/([1-9]\d*)$/.exec(process.env.MOBILE_E2E_SHARD || '1/1') +assert(shard, 'MOBILE_E2E_SHARD must be index/count, one-based') +const shardIndex = Number(shard[1]) - 1 +const shardCount = Number(shard[2]) +assert(shardIndex < shardCount && shardCount <= viewports.length, 'Invalid viewport shard') +assert(!selectedViewport || shardCount === 1, 'Select either a viewport or a shard') interface CheckResult { name: string @@ -1278,7 +1285,8 @@ try { }) if (!browser) continue try { - for (const viewport of viewports) { + for (const [index, viewport] of viewports.entries()) { + if (index % shardCount !== shardIndex) continue if (selectedViewport && selectedViewport !== viewport.name) continue await exerciseViewport(browser, browserName, cookie, viewport) } diff --git a/apps/sim/scripts/test-project-files-e2e.ts b/apps/sim/scripts/test-project-files-e2e.ts index 20562b595b2..112b4164990 100644 --- a/apps/sim/scripts/test-project-files-e2e.ts +++ b/apps/sim/scripts/test-project-files-e2e.ts @@ -25,6 +25,8 @@ function requiredString(value: unknown): string { assert.ok(typeof value === 'string' && value.length > 0, 'Fixture response field is missing') return value } +const appServer = process.env.PROJECT_FILES_E2E_APP_SERVER ?? 'dev' +assert.ok(appServer === 'dev' || appServer === 'start', 'App server must be dev or start') const reportPath = resolve(required('PROJECT_FILES_E2E_REPORT_PATH')) const reportDir = dirname(reportPath) const adminUrl = new URL(required('PROJECT_FILES_E2E_ADMIN_DATABASE_URL')) @@ -76,7 +78,8 @@ const environment: NodeJS.ProcessEnv = { HOME: process.env.HOME, TMPDIR: process.env.TMPDIR, SYSTEMROOT: process.env.SYSTEMROOT, - NODE_ENV: 'development', + NODE_ENV: appServer === 'start' ? 'production' : 'development', + LOG_LEVEL: 'INFO', __NEXT_PROCESSED_ENV: 'true', DATABASE_URL: databaseUrl.toString(), MIGRATION_DATABASE_URL: databaseUrl.toString(), @@ -394,7 +397,7 @@ try { [ 'node', join(rootDir, 'node_modules/next/dist/bin/next'), - 'dev', + appServer, '--hostname', base.hostname, '--port', diff --git a/apps/sim/scripts/test-scim-e2e.ts b/apps/sim/scripts/test-scim-e2e.ts index bdbb789d422..a9e3d592f56 100644 --- a/apps/sim/scripts/test-scim-e2e.ts +++ b/apps/sim/scripts/test-scim-e2e.ts @@ -282,6 +282,24 @@ async function run() { } ) + await check( + 'Hosted SCIM rejects administration and bearer access after Enterprise entitlement ends', + async () => { + await admin(primary) + await scim(token, '/Users') + await sql`update subscription set status = 'canceled' where reference_id = ${primary.orgId}` + try { + const denied = await admin(primary, '', { expected: 403 }) + assert.match(string(denied.body.error), /active enterprise subscription/) + await scim(token, '/Users', { expected: 401 }) + } finally { + await sql`update subscription set status = 'active' where reference_id = ${primary.orgId}` + } + await admin(primary) + await scim(token, '/Users') + } + ) + await check( 'Protocol authentication, malformed JSON, and content-type errors are SCIM responses', async () => { diff --git a/apps/sim/turbo.json b/apps/sim/turbo.json new file mode 100644 index 00000000000..a9b72d79cb2 --- /dev/null +++ b/apps/sim/turbo.json @@ -0,0 +1,183 @@ +{ + "extends": ["//"], + "tasks": { + "ci:shared": { + "dependsOn": ["^ci:runtime"], + "inputs": [ + "$TURBO_DEFAULT$", + "!app/workspace/**", + "!app/(landing)/**", + "!app/api/**", + "!content/**", + "!tools/**", + "!providers/**", + "!blocks/**", + "!connectors/**", + "!executor/**", + "!serializer/**", + "!triggers/**", + "!background/**", + "!**/*.test.*", + "!**/*.spec.*", + "!**/*.integration.ts", + "!**/*.live.test.ts", + "!**/AGENTS.md", + "!**/CLAUDE.md", + "!scripts/test-cli-run-latency-e2e.ts", + "!scripts/test-desktop-inbox-e2e.ts", + "!scripts/test-file-*-e2e.ts", + "!scripts/test-landing-seo-e2e.ts", + "!scripts/test-mcp-host-e2e.ts", + "!scripts/test-mobile-e2e.ts", + "!scripts/test-project-file*-e2e.ts", + "!scripts/test-scim-e2e.ts", + "!scripts/test-search-*-e2e.ts", + "!scripts/test-workflow-stop-after-e2e.ts", + "!scripts/test-workflow-version-compare-e2e.ts", + "!vitest.*", + "!test-results/**" + ], + "outputs": [] + }, + "ci:server": { + "dependsOn": ["ci:shared"], + "inputs": [ + "app/api/**", + "tools/**", + "providers/**", + "blocks/**", + "connectors/**", + "executor/**", + "serializer/**", + "triggers/**", + "background/**", + "!**/*.test.*", + "!**/*.spec.*", + "!**/*.integration.ts", + "!**/*.live.test.ts", + "!**/AGENTS.md", + "!**/CLAUDE.md", + "!scripts/test-*-e2e.ts", + "!vitest.*", + "!test-results/**" + ], + "outputs": [] + }, + "ci:ui": { + "dependsOn": ["ci:shared"], + "inputs": [ + "app/workspace/**", + "!**/*.test.*", + "!**/*.spec.*", + "!**/*.integration.ts", + "!**/*.live.test.ts", + "!**/AGENTS.md", + "!**/CLAUDE.md", + "!scripts/test-*-e2e.ts", + "!vitest.*", + "!test-results/**" + ], + "outputs": [] + }, + "ci:integration": { + "dependsOn": ["ci:server", "ci:ui", "@sim/realtime#ci:runtime"], + "inputs": [ + "**/*.integration.ts", + "vitest.config.ts", + "vitest.integration.setup.ts", + "$TURBO_ROOT$/vitest.shared.ts", + "$TURBO_ROOT$/vitest.integration-durations.json", + "$TURBO_ROOT$/packages/db/**/*.integration.ts", + "$TURBO_ROOT$/packages/db/vitest.config.ts", + "$TURBO_ROOT$/packages/db/vitest.integration.setup.ts", + "$TURBO_ROOT$/apps/realtime/**/*.integration.ts", + "$TURBO_ROOT$/apps/realtime/vitest.config.ts", + "$TURBO_ROOT$/apps/realtime/vitest.integration.setup.ts" + ], + "cache": false + }, + "ci:e2e-scim": { + "dependsOn": ["ci:server"], + "inputs": ["scripts/test-scim-e2e.ts", "scripts/test-workflow-version-compare-e2e.ts"], + "cache": false + }, + "ci:e2e-cli": { + "dependsOn": ["ci:server", "sim#ci:runtime"], + "inputs": [ + "scripts/test-cli-run-latency-e2e.ts", + "scripts/test-mcp-host-e2e.ts", + "scripts/test-search-*-e2e.ts" + ], + "cache": false + }, + "ci:e2e-stop-after": { + "dependsOn": ["ci:server"], + "inputs": ["scripts/test-workflow-stop-after-e2e.ts"], + "cache": false + }, + "ci:e2e-desktop-inbox": { + "dependsOn": ["ci:server", "@sim/realtime#ci:runtime"], + "inputs": ["scripts/test-desktop-inbox-e2e.ts"], + "cache": false + }, + "ci:e2e-project-files": { + "dependsOn": ["ci:server", "@sim/realtime#ci:runtime"], + "inputs": ["scripts/test-project-file*-e2e.ts", "scripts/test-file-*-e2e.ts"], + "cache": false + }, + "ci:e2e-mobile": { + "dependsOn": ["ci:server", "ci:ui", "@sim/realtime#ci:runtime"], + "inputs": ["scripts/test-mobile-e2e.ts"], + "cache": false + }, + "ci:e2e-seo": { + "dependsOn": ["ci:server", "ci:ui"], + "inputs": ["app/(landing)/**", "content/**", "scripts/test-landing-seo-e2e.ts"], + "cache": false + }, + "ci:desktop-live": { + "dependsOn": ["ci:server", "ci:ui", "@sim/desktop#ci:runtime", "@sim/realtime#ci:runtime"], + "inputs": [ + "$TURBO_ROOT$/apps/desktop/e2e/**", + "$TURBO_ROOT$/apps/desktop/playwright.config.ts" + ], + "cache": false + }, + "test": { + "inputs": [ + "$TURBO_DEFAULT$", + "!**/*.integration.ts", + "!scripts/test-cli-run-latency-e2e.ts", + "!scripts/test-desktop-inbox-e2e.ts", + "!scripts/test-file-*-e2e.ts", + "!scripts/test-landing-seo-e2e.ts", + "!scripts/test-mcp-host-e2e.ts", + "!scripts/test-mobile-e2e.ts", + "!scripts/test-project-file*-e2e.ts", + "!scripts/test-scim-e2e.ts", + "!scripts/test-search-*-e2e.ts", + "!scripts/test-workflow-stop-after-e2e.ts", + "!scripts/test-workflow-version-compare-e2e.ts", + "$TURBO_ROOT$/vitest.shared.ts", + "$TURBO_ROOT$/apps/docs/content/**", + "!**/AGENTS.md", + "!**/CLAUDE.md" + ] + }, + "build": { + "inputs": [ + "$TURBO_DEFAULT$", + ".env*", + "!**/*.test.*", + "!**/*.spec.*", + "!**/*.integration.ts", + "!**/*.live.test.ts", + "!**/AGENTS.md", + "!**/CLAUDE.md", + "!scripts/test-*-e2e.ts", + "!vitest.*", + "!test-results/**" + ] + } + } +} diff --git a/scripts/ci-changes.test.ts b/scripts/ci-changes.test.ts index ccafa4d6ba9..c1d722cfb63 100644 --- a/scripts/ci-changes.test.ts +++ b/scripts/ci-changes.test.ts @@ -1,209 +1,334 @@ import { spawnSync } from 'node:child_process' -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' -import { dirname, join } from 'node:path' +import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { afterEach, describe, expect, it } from 'vitest' -const script = fileURLToPath(new URL('../.github/scripts/ci-changes.sh', import.meta.url)) +const root = fileURLToPath(new URL('../', import.meta.url)) +const script = resolve(root, '.github/scripts/ci-changes.sh') const fixtures: string[] = [] - -type Flags = Record<'full' | 'test' | 'build' | 'desktop_live', boolean> - -const EVERYTHING: Flags = { full: true, test: true, build: true, desktop_live: true } - +interface Plan { + flags: Record + fallback?: string + http: { group: string }[] + production: { group: string }[] + typecheckPackages: string[] + testPackages: string[] +} function git(cwd: string, ...args: string[]) { const result = spawnSync('git', args, { cwd, encoding: 'utf8' }) if (result.status !== 0) throw new Error(`git ${args.join(' ')}: ${result.stderr}`) return result.stdout.trim() } - function write(repo: string, file: string, content: string) { mkdirSync(dirname(join(repo, file)), { recursive: true }) writeFileSync(join(repo, file), content) } - -/** Builds a clone whose base commit holds `apps/sim/x.ts` and whose origin is a local bare repo. */ +/** Real Git and Turbo fixture: changed files, transitive packages, and configured task boundaries. */ function fixture() { - const root = mkdtempSync(join(tmpdir(), 'ci-changes-')) - fixtures.push(root) - const origin = join(root, 'origin.git') - const repo = join(root, 'repo') - git(root, 'init', '--quiet', '--bare', origin) - git(root, 'init', '--quiet', repo) + const repo = mkdtempSync(join(tmpdir(), 'ci-changes-')) + fixtures.push(repo) + git(repo, 'init', '--quiet') git(repo, 'config', 'user.email', 'test@example.com') git(repo, 'config', 'user.name', 'Test') git(repo, 'config', 'commit.gpgsign', 'false') - git(repo, 'remote', 'add', 'origin', origin) - write(repo, 'apps/sim/x.ts', 'export const x = 1\n'.repeat(20)) + write( + repo, + 'package.json', + JSON.stringify({ + name: 'ci-fixture', + private: true, + packageManager: 'bun@1.4.2', + workspaces: ['apps/*', 'packages/*'], + }) + ) + const packages = [ + ['apps/sim', '@sim/app', { '@sim/auth': 'workspace:*', '@sim/db': 'workspace:*' }], + ['apps/realtime', '@sim/realtime', { '@sim/auth': 'workspace:*' }], + ['apps/desktop', '@sim/desktop', {}], + ['apps/docs', 'docs', {}], + ['packages/auth', '@sim/auth', { '@sim/security': 'workspace:*' }], + ['packages/security', '@sim/security', {}], + ['packages/db', '@sim/db', {}], + ['packages/ts-sdk', 'simstudio-ts-sdk', {}], + ['packages/sim-cli', 'sim', {}], + ] as const + const appScripts = JSON.parse( + readFileSync(resolve(root, 'apps/sim/package.json'), 'utf8') + ).scripts + const workspaces: Record = { '': { name: 'ci-fixture' } } + for (const [path, name, dependencies] of packages) { + const scripts: Record = { + test: 'echo test', + build: 'echo build', + 'type-check': 'echo types', + } + if (name === '@sim/app') + for (const key of Object.keys(appScripts)) + if (key.startsWith('ci:')) scripts[key] = 'echo fixture' + write( + repo, + `${path}/package.json`, + JSON.stringify({ name, version: '1.0.0', scripts, dependencies }) + ) + write(repo, `${path}/src/index.ts`, 'export const value = 1\n') + workspaces[path] = { name, version: '1.0.0', dependencies } + } + write( + repo, + 'bun.lock', + JSON.stringify({ lockfileVersion: 1, configVersion: 1, workspaces, packages: {} }) + ) + for (const file of [ + 'turbo.json', + 'apps/sim/turbo.json', + '.github/ci-suites.json', + '.github/scripts/ci-plan.gql', + ]) { + mkdirSync(dirname(join(repo, file)), { recursive: true }) + copyFileSync(resolve(root, file), join(repo, file)) + } git(repo, 'add', '-A') git(repo, 'commit', '--quiet', '-m', 'base') - const base = git(repo, 'rev-parse', 'HEAD') - git(repo, 'push', '--quiet', 'origin', 'HEAD:refs/heads/base') - return { repo, base } + return { repo, base: git(repo, 'rev-parse', 'HEAD') } } - -function classify(repo: string, base: string): Flags { - const result = spawnSync('bash', [script, base], { cwd: repo, encoding: 'utf8' }) +function classify(repo: string, base: string): Plan { + const report = join(repo, 'plan.json') + const result = spawnSync('bash', [script, base], { + cwd: repo, + encoding: 'utf8', + env: { + ...process.env, + PATH: `${resolve(root, 'node_modules/.bin')}:${process.env.PATH}`, + CI_PLAN_PATH: report, + }, + }) expect(result.status, result.stderr).toBe(0) - const lines = result.stdout.trim().split('\n') - for (const line of lines) expect(line).toMatch(/^[a-z_]+=(true|false)$/) - const flags = Object.fromEntries(lines.map((line) => line.split('='))) - expect(lines).toHaveLength(4) - expect(Object.keys(flags).sort()).toEqual(['build', 'desktop_live', 'full', 'test']) - return { - full: flags.full === 'true', - test: flags.test === 'true', - build: flags.build === 'true', - desktop_live: flags.desktop_live === 'true', - } + const plan: Plan = JSON.parse(readFileSync(report, 'utf8')) + for (const [flag, value] of Object.entries(plan.flags)) + expect(result.stdout).toContain(`${flag}=${value}\n`) + return plan } - -/** Commits `files` on top of the base, as a pull request changing exactly those paths. */ -function classifyFiles(files: string[]): Flags { +function classifyFiles(files: string[]): Plan { const { repo, base } = fixture() for (const file of files) write(repo, file, `${file}\n`) git(repo, 'add', '-A') git(repo, 'commit', '--quiet', '-m', 'change') return classify(repo, base) } - +function groups(plan: Plan) { + return [...new Set([...plan.http, ...plan.production].map((suite) => suite.group))].sort() +} afterEach(() => { - for (const root of fixtures.splice(0)) rmSync(root, { recursive: true, force: true }) + for (const repo of fixtures.splice(0)) rmSync(repo, { recursive: true, force: true }) }) - -describe('CI change classifier', () => { - it.each<{ name: string; files: string[]; expected: Flags }>([ - { - name: '#8931 library post and its cover', - files: [ - 'apps/sim/content/library/best-ai-agents-web-scraping-market-research/index.mdx', - 'apps/sim/public/library/best-ai-agents-web-scraping-market-research/cover.jpg', - ], - expected: { full: false, test: true, build: true, desktop_live: false }, - }, - { - name: '#8927 changelog review note', - files: ['apps/sim/content/changelog/nebius-models-for-agents/production.md'], - expected: { full: false, test: true, build: true, desktop_live: false }, - }, - { - name: '#8924 changelog post with a docs page and a screenshot', - files: [ - 'apps/docs/content/docs/workflows/blocks/agent.mdx', - 'apps/sim/content/changelog/editorial-standard.md', - 'apps/sim/content/changelog/nebius-models-for-agents/index.mdx', - 'apps/sim/content/changelog/nebius-models-for-agents/production.md', - 'apps/sim/public/changelog/nebius-model-picker-v1.jpg', - ], - expected: { full: false, test: true, build: true, desktop_live: false }, - }, - { - name: 'an author profile', - files: ['apps/sim/content/authors/waleed.json', 'apps/sim/public/authors/waleed.jpg'], - expected: { full: false, test: true, build: true, desktop_live: false }, - }, - { - name: '#8767 docs app', - files: ['apps/docs/app/llms-full.txt/route.ts'], - expected: { full: false, test: true, build: false, desktop_live: false }, - }, - { - name: '#8855 desktop app', - files: ['apps/desktop/e2e/local-files.spec.ts', 'apps/desktop/src/main/menu.ts'], - expected: { full: false, test: true, build: false, desktop_live: true }, - }, - { - name: 'agent guidance only (from #8887)', - files: [ - '.agents/skills/add-block/SKILL.md', - '.claude/rules/sim-testing.md', - '.cursor/rules/sim-testing.mdc', - 'CLAUDE.md', - 'apps/sim/CLAUDE.md', - 'apps/sim/stores/AGENTS.md', - ], - expected: { full: false, test: false, build: false, desktop_live: false }, - }, - { - name: '#8897 docs page with app code', - files: [ - 'apps/docs/content/docs/platform/enterprise/whitelabeling.mdx', - 'apps/sim/app/_shell/application-loading.tsx', - ], - expected: EVERYTHING, - }, - { - name: '#8806 library post with a lockfile', - files: [ - 'apps/sim/content/library/best-ai-agent-marketplaces-template-libraries/index.mdx', - 'apps/sim/public/library/best-ai-agent-marketplaces-template-libraries/cover.jpg', - 'bun.lock', - ], - expected: EVERYTHING, - }, - { - name: '#8937 CI script with app code', - files: ['.github/scripts/http-e2e.sh', 'apps/sim/scripts/test-mobile-e2e.ts'], - expected: EVERYTHING, - }, - { - name: 'a Markdown file under .github', - files: ['.github/CONTRIBUTING.md'], - expected: EVERYTHING, - }, - { - name: '#8772 PII service, outside the live desktop suite', - files: ['apps/pii/server.py'], - expected: { full: true, test: true, build: true, desktop_live: false }, - }, - { - name: 'a component beside a blog post', - files: ['apps/sim/content/blog/v0-5/components.tsx'], - expected: { full: true, test: true, build: true, desktop_live: false }, - }, - { - name: 'a public asset outside the content folders', - files: ['apps/sim/public/landing/hero.png'], - expected: EVERYTHING, - }, - { - name: 'a README that is a test fixture', - files: ['apps/sim/tools/pitchbook/__fixtures__/README.md'], - expected: { full: true, test: true, build: true, desktop_live: false }, - }, - { - name: 'the desktop bridge package, which apps/sim imports', - files: ['packages/desktop-bridge/src/index.ts'], - expected: EVERYTHING, - }, - ])('$name', ({ files, expected }) => { - expect(classifyFiles(files)).toEqual(expected) +describe('CI task attribution through real Turborepo', () => { + it('runs SDK tests without provisioning the app, browsers, or database', () => { + const plan = classifyFiles(['packages/ts-sdk/src/index.ts']) + expect(plan.fallback).toBeUndefined() + expect(groups(plan)).toEqual([]) + expect(plan.testPackages).toEqual(['simstudio-ts-sdk']) + expect(plan.typecheckPackages).toEqual(['simstudio-ts-sdk']) + expect(plan.flags).toMatchObject({ + test: true, + test_app: false, + integration: false, + build: false, + desktop_live: false, + e2e_build: false, + }) }) - - it('runs everything when a file moves from app code into an allowlisted path', () => { + it('selects integration and browser checks for workspace code shared with desktop execution', () => { + const plan = classifyFiles([ + 'apps/sim/app/workspace/[workspaceId]/home/hooks/stream/dispatch-stream-event.ts', + ]) + expect(plan.fallback).toBeUndefined() + expect(groups(plan)).toEqual(['mobile', 'seo']) + expect(plan.flags).toMatchObject({ + desktop_live: true, + integration: true, + test_app: true, + build: true, + e2e_build: true, + }) + }) + it('selects SEO and rendering checks for content without database or Electron suites', () => { + const plan = classifyFiles(['apps/sim/content/library/example/index.mdx']) + expect(plan.fallback).toBeUndefined() + expect(groups(plan)).toEqual(['seo']) + expect(plan.flags).toMatchObject({ + integration: false, + desktop_live: false, + test_app: true, + build: true, + }) + }) + it.each([ + 'apps/sim/app/workspace/[workspaceId]/upgrade/components/comparison-table/comparison-data.ts', + 'apps/sim/providers/models.ts', + 'apps/sim/blocks/registry.ts', + ])('runs SEO when landing pages consume changed workspace or backend code: %s', (file) => { + const plan = classifyFiles([file]) + expect(plan.fallback).toBeUndefined() + expect(groups(plan)).toContain('seo') + }) + it('preserves the implicit app test dependency on docs content', () => { + const plan = classifyFiles(['apps/docs/content/docs/integrations/netsuite.mdx']) + expect(plan.fallback).toBeUndefined() + expect(groups(plan)).toEqual([]) + expect(plan.flags).toMatchObject({ + test_app: true, + build: false, + integration: false, + desktop_live: false, + }) + expect(plan.testPackages).toEqual(['docs']) + }) + it('does not build or run E2E for an app unit-test-only edit', () => { + const plan = classifyFiles(['apps/sim/lib/auth/session.test.ts']) + expect(plan.fallback).toBeUndefined() + expect(groups(plan)).toEqual([]) + expect(plan.flags).toMatchObject({ + test_app: true, + integration: false, + build: false, + desktop_live: false, + }) + }) + it('runs a new integration file even when its source domain did not change', () => { + const plan = classifyFiles(['apps/sim/lib/auth/session.integration.ts']) + expect(plan.fallback).toBeUndefined() + expect(groups(plan)).toEqual([]) + expect(plan.flags).toMatchObject({ integration: true, test_app: false, build: false }) + }) + it.each([ + 'packages/db/vitest.config.ts', + 'packages/db/vitest.integration.setup.ts', + 'apps/realtime/vitest.config.ts', + 'apps/realtime/vitest.integration.setup.ts', + ])('runs integration checks when sibling collection or setup changes: %s', (file) => { + const plan = classifyFiles([file]) + expect(plan.fallback).toBeUndefined() + expect(plan.flags.integration).toBe(true) + }) + it.each([ + ['test-mobile-e2e.ts', 'mobile'], + ['test-project-file-sharing-e2e.ts', 'project-files'], + ['test-scim-e2e.ts', 'scim'], + ['test-landing-seo-e2e.ts', 'seo'], + ])('runs the affected acceptance boundary for %s', (file, group) => { + const plan = classifyFiles([`apps/sim/scripts/${file}`]) + expect(plan.fallback).toBeUndefined() + expect(groups(plan)).toEqual([group]) + expect(plan.flags.http).toBe(group !== 'mobile') + expect(plan.flags.production).toBe(group === 'mobile') + expect(plan.flags).toMatchObject({ + test: false, + build: false, + integration: false, + e2e_build: true, + }) + }) + it('propagates a transitive security dependency to every affected boundary', () => { + const plan = classifyFiles(['packages/security/src/index.ts']) + expect(plan.fallback).toBeUndefined() + expect(groups(plan)).toEqual([ + 'cli', + 'desktop-inbox', + 'mobile', + 'project-files', + 'scim', + 'seo', + 'stop-after', + ]) + expect(plan.typecheckPackages).toContain('@sim/app') + expect(plan.typecheckPackages).toContain('@sim/realtime') + expect(plan.flags).toMatchObject({ + integration: true, + desktop_live: true, + test_app: true, + build: true, + }) + }) + it('keeps unknown app modules and non-code fixtures in the shared runtime boundary', () => { + for (const path of [ + 'apps/sim/new-domain/feature.ts', + 'apps/sim/tools/service/__fixtures__/README.md', + ]) { + const plan = classifyFiles([path]) + expect(plan.fallback).toBeUndefined() + expect(plan.flags).toMatchObject({ integration: true, desktop_live: true, e2e_build: true }) + } + }) + it.each([ + '.github/workflows/checks.yml', + 'scripts/check-example.ts', + 'unknown-root.json', + 'apps/pii/server.py', + ])('runs all checks for unclassified or orchestration changes: %s', (file) => { + const plan = classifyFiles([file]) + expect(plan.fallback).toBeTruthy() + expect(Object.values(plan.flags).every(Boolean)).toBe(true) + }) + it('keeps the implicit CLI embed dependency in HTTP acceptance', () => { + const plan = classifyFiles(['packages/sim-cli/src/embed.ts']) + expect(plan.fallback).toBeUndefined() + expect(groups(plan)).toEqual(['cli']) + expect(plan.testPackages).toEqual(['sim']) + expect(plan.flags).toMatchObject({ e2e_build: true, integration: false, desktop_live: false }) + }) + it('runs the shell execution check when its standalone harness changes', () => { + const plan = classifyFiles(['apps/sim/scripts/test-shell-placeholders-e2e.ts']) + expect(plan.flags.test_app).toBe(true) + }) + it('keeps new, unclassified acceptance harnesses in the shared boundary', () => { + const plan = classifyFiles(['apps/sim/scripts/test-new-feature-e2e.ts']) + expect(plan.flags).toMatchObject({ test_app: true, integration: true, desktop_live: true }) + }) + it('keeps guidance-only edits on the lint/audit path', () => { + const plan = classifyFiles([ + 'apps/sim/AGENTS.md', + 'packages/security/CLAUDE.md', + '.claude/rules/example.md', + ]) + expect(plan.fallback).toBeUndefined() + expect(Object.values(plan.flags).some(Boolean)).toBe(false) + }) + it('selects Electron without unrelated app suites for shell-only edits', () => { + const plan = classifyFiles(['apps/desktop/src/main/menu.ts']) + expect(plan.fallback).toBeUndefined() + expect(groups(plan)).toEqual([]) + expect(plan.flags).toMatchObject({ + desktop_live: true, + e2e_build: true, + integration: false, + test_app: false, + build: false, + }) + expect(plan.testPackages).toEqual(['@sim/desktop']) + }) + it('runs everything if the real task query cannot parse configuration', () => { const { repo, base } = fixture() - mkdirSync(join(repo, 'apps/docs'), { recursive: true }) - git(repo, 'mv', 'apps/sim/x.ts', 'apps/docs/x.ts') - git(repo, 'commit', '--quiet', '-m', 'move') - - expect(classify(repo, base)).toEqual(EVERYTHING) + write(repo, 'turbo.json', '{invalid') + git(repo, 'commit', '--quiet', '-am', 'invalid graph') + const plan = classify(repo, base) + expect(plan.fallback).toContain('turbo:') + expect(Object.values(plan.flags).every(Boolean)).toBe(true) }) - - it('runs everything when the diff is empty', () => { + it('accounts for both sides of a move out of runtime code', () => { const { repo, base } = fixture() - git(repo, 'commit', '--quiet', '--allow-empty', '-m', 'empty') - - expect(classify(repo, base)).toEqual(EVERYTHING) + git(repo, 'mv', 'apps/sim/src/index.ts', 'apps/docs/src/moved.ts') + git(repo, 'commit', '--quiet', '-am', 'move') + const plan = classify(repo, base) + expect(plan.flags).toMatchObject({ integration: true, desktop_live: true, test_app: true }) }) - - it('runs everything when the base commit cannot be fetched', () => { + it.each(['', '0'.repeat(40)])('runs all checks without a usable base: %j', (base) => { const { repo } = fixture() - expect(classify(repo, '0'.repeat(40))).toEqual(EVERYTHING) + expect(Object.values(classify(repo, base).flags).every(Boolean)).toBe(true) }) - - it('runs everything when no base commit is given', () => { - const { repo } = fixture() - expect(classify(repo, '')).toEqual(EVERYTHING) + it('runs all checks on an empty diff', () => { + const { repo, base } = fixture() + expect(Object.values(classify(repo, base).flags).every(Boolean)).toBe(true) }) }) diff --git a/scripts/ci-gate.test.ts b/scripts/ci-gate.test.ts index 91347146b0a..c017b9cf9c9 100644 --- a/scripts/ci-gate.test.ts +++ b/scripts/ci-gate.test.ts @@ -10,7 +10,7 @@ type Job = | 'integration' | 'e2e' | 'e2e-build' - | 'e2e-mobile' + | 'e2e-production' | 'desktop-live' | 'lint' | 'test' @@ -22,13 +22,22 @@ const GATED: Job[] = [ 'integration', 'e2e', 'e2e-build', - 'e2e-mobile', + 'e2e-production', 'desktop-live', 'test', 'test-types', 'build', ] -const ALL_FALSE = { full: 'false', test: 'false', build: 'false', desktop_live: 'false' } +const ALL_FALSE = { + integration: 'false', + http: 'false', + production: 'false', + e2e_build: 'false', + test: 'false', + test_app: 'false', + build: 'false', + desktop_live: 'false', +} /** * The `needs` context GitHub gives the `ci` job: every job succeeded and `changes` set `outputs`, @@ -91,26 +100,21 @@ describe('CI gate', () => { }, { name: 'a skip whose flag is missing', - outputs: { full: 'false', test: 'false', desktop_live: 'false' }, + outputs: { ...ALL_FALSE, build: undefined }, results: skipGated, reported: ['build: skipped'], }, { name: 'a skip whose flag is a misspelling', - outputs: { ...ALL_FALSE, full: 'flase' }, + outputs: { ...ALL_FALSE, production: 'flase' }, results: skipGated, - reported: [ - 'e2e-build: skipped', - 'e2e-mobile: skipped', - 'e2e: skipped', - 'integration: skipped', - ], + reported: ['e2e-production: skipped'], }, { name: 'a skip whose flag is differently cased', outputs: { ...ALL_FALSE, test: 'False' }, results: skipGated, - reported: ['test-types: skipped', 'test: skipped'], + reported: ['test: skipped'], }, { name: 'a skip whose flag is empty', @@ -132,7 +136,7 @@ describe('CI gate', () => { 'build: skipped', 'desktop-live: skipped', 'e2e-build: skipped', - 'e2e-mobile: skipped', + 'e2e-production: skipped', 'e2e: skipped', 'integration: skipped', 'test-types: skipped', @@ -147,7 +151,7 @@ describe('CI gate', () => { 'build: skipped', 'desktop-live: skipped', 'e2e-build: skipped', - 'e2e-mobile: skipped', + 'e2e-production: skipped', 'e2e: skipped', 'integration: skipped', 'test-types: skipped', @@ -163,7 +167,7 @@ describe('CI gate', () => { 'changes: failure', 'desktop-live: skipped', 'e2e-build: skipped', - 'e2e-mobile: skipped', + 'e2e-production: skipped', 'e2e: skipped', 'integration: skipped', 'test-types: skipped', @@ -178,9 +182,9 @@ describe('CI gate', () => { }, { name: 'a mobile skip after a failed e2e build, though its flag is true', - outputs: { ...ALL_FALSE, full: 'true', build: 'true' }, - results: { 'e2e-build': 'failure', 'e2e-mobile': 'skipped' }, - reported: ['e2e-build: failure', 'e2e-mobile: skipped'], + outputs: { ...ALL_FALSE, e2e_build: 'true', production: 'true', build: 'true' }, + results: { 'e2e-build': 'failure', 'e2e-production': 'skipped' }, + reported: ['e2e-build: failure', 'e2e-production: skipped'], }, { name: 'a failed job whose flag is false', @@ -213,6 +217,12 @@ describe('CI gate', () => { expect(result.reported).toEqual(reported) }) + it('fails if an expected dependency is missing from the aggregate gate', () => { + const { integration: _integration, ...context } = needs(ALL_FALSE) + expect(gate(JSON.stringify(context)).status).toBe(1) + expect(gate('{}').status).toBe(1) + }) + it.each(['', 'not json', '[]', 'null'])('fails on a needs context of %j', (context) => { const { status, stdout } = gate(context) expect(status).toBe(1) diff --git a/scripts/ci-plan.ts b/scripts/ci-plan.ts new file mode 100644 index 00000000000..65885d716a7 --- /dev/null +++ b/scripts/ci-plan.ts @@ -0,0 +1,153 @@ +import { spawnSync } from 'node:child_process' +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' + +/** Plans CI from Turborepo's task inputs without installing or executing application code. */ +interface Suite { + task: string + group: string + [key: string]: string +} +interface ChangedTask { + name: string + fullName: string + package: { name: string } + reason: Record +} +interface QueryResult { + errors?: unknown[] + data?: { + packages: { items: { name: string; path: string }[] } + affectedTasks: { items: ChangedTask[] } + } +} +const root = process.cwd() +const suites: { http: Suite[]; production: Suite[] } = JSON.parse( + readFileSync(resolve(root, '.github/ci-suites.json'), 'utf8') +) +const sourceBase = process.argv[2] +let files: string[] = [] +let tasks: ChangedTask[] = [] +let base = sourceBase +let fallback: string | undefined + +function command(program: string, args: string[]): string { + const result = spawnSync(program, args, { cwd: root, encoding: 'utf8', timeout: 60_000 }) + if (result.status !== 0) + throw new Error(`${program}: ${result.stderr || result.error || result.status}`) + return result.stdout.trim() +} + +try { + if (!sourceBase) + throw new Error('No comparison base; push and unscoped dispatch run the full suite') + if (!/^[0-9a-f]{40}$/.test(sourceBase)) + throw new Error('Comparison base must be a full commit SHA') + try { + command('git', ['cat-file', '-e', `${sourceBase}^{commit}`]) + } catch { + command('git', ['fetch', '--quiet', '--depth=2', 'origin', sourceBase]) + } + base = command('git', ['merge-base', sourceBase, 'HEAD']) + files = command('git', ['diff', '--no-renames', '--name-only', '-z', base, 'HEAD']) + .split('\0') + .filter(Boolean) + if (files.length === 0) throw new Error('Empty diff; verify the complete suite') + if (files.some((file) => /^(\.github\/|scripts\/)/.test(file))) + throw new Error('CI orchestration or root scripts changed') + const temp = mkdtempSync(join(tmpdir(), 'sim-ci-plan-')) + let result: QueryResult + try { + const variables = join(temp, 'variables.json') + writeFileSync(variables, JSON.stringify({ base, head: 'HEAD' })) + result = JSON.parse( + command('turbo', ['query', '.github/scripts/ci-plan.gql', '--variables', variables]) + ) + } finally { + rmSync(temp, { recursive: true, force: true }) + } + if ( + result.errors?.length || + !Array.isArray(result.data?.affectedTasks?.items) || + !Array.isArray(result.data?.packages?.items) + ) + throw new Error('Turborepo did not return a complete task plan') + const workspacePaths = result.data.packages.items + .filter((pkg) => pkg.name !== '//') + .map((pkg) => `${pkg.path}/`) + const guidance = /^(\.agents|\.claude|\.cursor)\/|(^|\/)(AGENTS|CLAUDE)\.md$|^README\.md$/ + const unowned = files.find( + (file) => !guidance.test(file) && !workspacePaths.some((path) => file.startsWith(path)) + ) + if (unowned) throw new Error(`Changed file outside the workspace graph: ${unowned}`) + tasks = result.data.affectedTasks.items + if ( + tasks.some( + (task) => typeof task.fullName !== 'string' || typeof task.package?.name !== 'string' + ) + ) + throw new Error('Malformed task attribution') +} catch (error) { + fallback = String(error) +} + +const selected = new Set(tasks.map((task) => task.fullName)) +const affected = (task: string) => fallback !== undefined || selected.has(`@sim/app#${task}`) +const http = suites.http.filter((suite) => affected(suite.task)) +const production = suites.production.filter((suite) => affected(suite.task)) +const desktopLive = affected('ci:desktop-live') +const appTest = affected('test') +function taskPackages(name: string, omitApp = false): string[] { + return fallback + ? ['*'] + : [ + ...new Set( + tasks + .filter((task) => task.name === name && (!omitApp || task.package.name !== '@sim/app')) + .map((task) => task.package.name) + ), + ].sort() +} +const packages = taskPackages('test', true) +const typecheckPackages = taskPackages('type-check') +const flags = { + integration: affected('ci:integration'), + http: http.length > 0, + production: production.length > 0, + e2e_build: http.length > 0 || production.length > 0 || desktopLive, + desktop_live: desktopLive, + test: appTest || packages.length > 0, + test_app: appTest, + build: affected('build'), +} +const outputs = { + ...Object.fromEntries(Object.entries(flags).map(([key, value]) => [key, String(value)])), + http_matrix: JSON.stringify({ include: http }), + production_matrix: JSON.stringify({ include: production }), + test_matrix: JSON.stringify({ shard: appTest ? [1, 2] : [1] }), + test_packages: JSON.stringify(packages), + typecheck_packages: JSON.stringify(typecheckPackages), +} +const report = { + base, + head: command('git', ['rev-parse', 'HEAD']), + files, + fallback, + tasks, + flags, + http, + production, + testPackages: packages, + typecheckPackages, +} +if (process.env.CI_PLAN_PATH) + writeFileSync(process.env.CI_PLAN_PATH, `${JSON.stringify(report, null, 2)}\n`) +process.stderr.write( + `${fallback ? `Full CI: ${fallback}` : `Affected CI: ${tasks.map((task) => task.fullName).join(', ') || 'no executable tasks'}`}\n` +) +process.stdout.write( + Object.entries(outputs) + .map(([key, value]) => `${key}=${value}\n`) + .join('') +) diff --git a/turbo.json b/turbo.json index c662535ebbe..1838c7cecd0 100644 --- a/turbo.json +++ b/turbo.json @@ -19,13 +19,36 @@ "cacheMaxAge": "7d", "cacheMaxSize": "40GB", "tasks": { + "ci:runtime": { + "dependsOn": ["^ci:runtime"], + "inputs": [ + "$TURBO_DEFAULT$", + "!**/*.test.*", + "!**/*.spec.*", + "!**/*.integration.ts", + "!**/AGENTS.md", + "!**/CLAUDE.md", + "!vitest.*", + "!test-results/**", + "!e2e/**" + ], + "outputs": [] + }, "transit": { "dependsOn": ["^transit"], "outputs": [] }, "build": { "dependsOn": ["^build"], - "inputs": ["$TURBO_DEFAULT$", ".env*"], + "inputs": [ + "$TURBO_DEFAULT$", + ".env*", + "!**/*.test.*", + "!**/*.spec.*", + "!**/*.integration.ts", + "!**/AGENTS.md", + "!**/CLAUDE.md" + ], "outputs": [".next/**", "!.next/cache/**", "dist/**"] }, "dev": { @@ -37,7 +60,13 @@ }, "test": { "dependsOn": ["^build"], - "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/vitest.shared.ts"], + "inputs": [ + "$TURBO_DEFAULT$", + "!**/*.integration.ts", + "!**/AGENTS.md", + "!**/CLAUDE.md", + "$TURBO_ROOT$/vitest.shared.ts" + ], "outputs": [] }, "lint": { diff --git a/vitest.integration-durations.json b/vitest.integration-durations.json index b42e6a1a7b0..80d52800c75 100644 --- a/vitest.integration-durations.json +++ b/vitest.integration-durations.json @@ -1,207 +1,291 @@ { - "apps/sim/app/api/auth/oauth2/token/route.integration.ts": 12.9, - "apps/sim/app/api/billing/update-cost/route.integration.ts": 2.7, - "apps/sim/app/api/copilot/chat/stream/desktop-executor.integration.ts": 7.1, - "apps/sim/app/api/knowledge/connectors/member-sync/route.integration.ts": 1.5, - "apps/sim/app/api/v1/knowledge/route.integration.ts": 2.9, - "apps/sim/background/cleanup-soft-deletes.integration.ts": 2.8, - "apps/sim/background/cleanup-table-row-ttl-predicate.integration.ts": 0.4, - "apps/sim/background/cleanup-table-row-ttl.integration.ts": 5.3, - "apps/sim/ee/access-requests/lib/application/copilot-requests.integration.ts": 4, - "apps/sim/ee/access-requests/lib/application/flow.integration.ts": 1.3, - "apps/sim/ee/access-requests/lib/impact.integration.ts": 0.6, - "apps/sim/ee/access-requests/lib/repository.integration.ts": 0.4, - "apps/sim/ee/scim/lib/managed-membership.integration.ts": 0.7, - "apps/sim/ee/workspace-forking/application/revision.integration.ts": 2.3, - "apps/sim/ee/workspace-forking/lib/lineage/fork-lock-order.integration.ts": 1.9, - "apps/sim/executor/handlers/agent/memory-harness.integration.ts": 18.5, - "apps/sim/lib/audit-logs/export.integration.ts": 2.2, - "apps/sim/lib/audit-logs/query.integration.ts": 0.5, - "apps/sim/lib/auth/lifecycle-audit.integration.ts": 4, - "apps/sim/lib/auth/oauth-provider-lifecycle.integration.ts": 1.1, - "apps/sim/lib/auth/oauth-token-family.integration.ts": 0.8, - "apps/sim/lib/auth/sim-auth-adapter.integration.ts": 0.6, - "apps/sim/lib/auth/sso/application/admit-sso-user.integration.ts": 0.6, + "apps/realtime/src/handlers/file-doc-capabilities.integration.ts": 2, + "apps/realtime/src/handlers/file-doc-editor.integration.ts": 0.5, + "apps/realtime/src/handlers/file-doc-presence.integration.ts": 1.9, + "apps/realtime/src/handlers/file-doc-project.integration.ts": 0.5, + "apps/sim/app/api/auth/oauth2/token/route.integration.ts": 5.3, + "apps/sim/app/api/billing/update-cost/route.integration.ts": 4.2, + "apps/sim/app/api/copilot/chat/stream/desktop-executor.integration.ts": 10, + "apps/sim/app/api/folders/reorder/route.integration.ts": 1.3, + "apps/sim/app/api/knowledge/connectors/member-sync/route.integration.ts": 2.3, + "apps/sim/app/api/v1/knowledge/route.integration.ts": 5.6, + "apps/sim/app/api/webhooks/agentmail/thread-scope.integration.ts": 1.3, + "apps/sim/background/cleanup-soft-deletes.integration.ts": 3.1, + "apps/sim/background/cleanup-table-row-ttl-predicate.integration.ts": 0.6, + "apps/sim/background/cleanup-table-row-ttl.integration.ts": 5.2, + "apps/sim/connectors/google-chat/google-chat.integration.ts": 1.6, + "apps/sim/ee/access-requests/lib/application/copilot-requests.integration.ts": 3.4, + "apps/sim/ee/access-requests/lib/application/flow.integration.ts": 2, + "apps/sim/ee/access-requests/lib/impact.integration.ts": 0.7, + "apps/sim/ee/access-requests/lib/repository.integration.ts": 0.9, + "apps/sim/ee/scim/lib/managed-membership.integration.ts": 0.5, + "apps/sim/ee/workspace-forking/application/revision.integration.ts": 1.7, + "apps/sim/ee/workspace-forking/lib/lineage/fork-lock-order.integration.ts": 1.8, + "apps/sim/executor/handlers/agent/memory-harness.integration.ts": 8.6, + "apps/sim/lib/api-key/byok.integration.ts": 0.8, + "apps/sim/lib/audit-logs/export.integration.ts": 3.6, + "apps/sim/lib/audit-logs/query.integration.ts": 0.7, + "apps/sim/lib/auth/lifecycle-audit.integration.ts": 5.3, + "apps/sim/lib/auth/oauth-provider-lifecycle.integration.ts": 2.1, + "apps/sim/lib/auth/oauth-token-family.integration.ts": 1.7, + "apps/sim/lib/auth/sim-auth-adapter.integration.ts": 0.9, + "apps/sim/lib/auth/sso/application/admit-sso-user.integration.ts": 0.8, "apps/sim/lib/auth/sso/application/provider-settings.integration.ts": 4.9, - "apps/sim/lib/auth/sso/primary-provider.integration.ts": 5.6, - "apps/sim/lib/auth/sso/provider-concurrency.integration.ts": 1.1, - "apps/sim/lib/billing/application/organization-billing/get-organization-plan-seats.integration.ts": 1.2, - "apps/sim/lib/billing/calculations/usage-reservation.integration.ts": 0.3, - "apps/sim/lib/billing/core/ledger-read.integration.ts": 1.8, - "apps/sim/lib/billing/core/organization-activity.integration.ts": 0.6, - "apps/sim/lib/billing/core/organization-usage-pagination.integration.ts": 0.5, - "apps/sim/lib/billing/core/plan.integration.ts": 0.4, - "apps/sim/lib/billing/core/reporting-usage-cache.integration.ts": 0.9, - "apps/sim/lib/billing/core/usage-analytics-queries.integration.ts": 0.5, - "apps/sim/lib/billing/core/usage-log.integration.ts": 14.1, + "apps/sim/lib/auth/sso/primary-provider.integration.ts": 4.8, + "apps/sim/lib/auth/sso/provider-concurrency.integration.ts": 1, + "apps/sim/lib/benchmarks/repository.integration.ts": 11.8, + "apps/sim/lib/billing/application/organization-billing/get-organization-plan-seats.integration.ts": 0.9, + "apps/sim/lib/billing/calculations/usage-reservation.integration.ts": 0.5, + "apps/sim/lib/billing/core/ledger-read.integration.ts": 2.2, + "apps/sim/lib/billing/core/organization-activity.integration.ts": 0.9, + "apps/sim/lib/billing/core/organization-usage-pagination.integration.ts": 0.4, + "apps/sim/lib/billing/core/plan.integration.ts": 0.6, + "apps/sim/lib/billing/core/reporting-usage-cache.integration.ts": 1.3, + "apps/sim/lib/billing/core/subscription.integration.ts": 0.4, + "apps/sim/lib/billing/core/usage-analytics-queries.integration.ts": 0.6, + "apps/sim/lib/billing/core/usage-log.integration.ts": 10.9, "apps/sim/lib/billing/core/usage-threshold-email.integration.ts": 0.6, - "apps/sim/lib/billing/organizations/member-limits.integration.ts": 0.8, - "apps/sim/lib/billing/webhooks/enterprise-missing-organization.integration.ts": 2.6, - "apps/sim/lib/billing/webhooks/stripe-sync-convergence.integration.ts": 2.6, - "apps/sim/lib/changelog/repository.integration.ts": 0.4, - "apps/sim/lib/core/async-jobs/backends/trigger-dev.integration.ts": 0.7, - "apps/sim/lib/core/outbox/retention.integration.ts": 0.4, - "apps/sim/lib/core/outbox/service.integration.ts": 6.1, - "apps/sim/lib/core/rate-limiter/provider-capacity.integration.ts": 0.8, - "apps/sim/lib/credential-groups/__integration__/hubspot-mcp.integration.ts": 2.3, - "apps/sim/lib/credential-groups/__integration__/organization-account-outbound.integration.ts": 1.1, - "apps/sim/lib/credentials/__integration__/copilot-credential-members.integration.ts": 3.2, - "apps/sim/lib/credentials/application/sharing.integration.ts": 1.3, - "apps/sim/lib/credentials/client-credential-accounts/vanta-token.integration.ts": 0.8, + "apps/sim/lib/billing/organizations/member-limits.integration.ts": 1.5, + "apps/sim/lib/billing/storage/project-accounting.integration.ts": 1.7, + "apps/sim/lib/billing/webhooks/enterprise-missing-organization.integration.ts": 2.8, + "apps/sim/lib/billing/webhooks/stripe-sync-convergence.integration.ts": 3.3, + "apps/sim/lib/changelog/repository.integration.ts": 1, + "apps/sim/lib/cleanup/chat-cleanup.integration.ts": 0.7, + "apps/sim/lib/computer-use/repository.integration.ts": 0.4, + "apps/sim/lib/core/async-jobs/backends/trigger-dev.integration.ts": 0.5, + "apps/sim/lib/core/config/appconfig.integration.ts": 0.6, + "apps/sim/lib/core/outbox/retention.integration.ts": 0.6, + "apps/sim/lib/core/outbox/service.integration.ts": 6.5, + "apps/sim/lib/core/rate-limiter/provider-capacity.integration.ts": 1.1, + "apps/sim/lib/credential-groups/__integration__/hubspot-mcp.integration.ts": 2.7, + "apps/sim/lib/credential-groups/__integration__/organization-account-outbound.integration.ts": 1.4, + "apps/sim/lib/credential-groups/__integration__/slack-shared-member-accounts.integration.ts": 2.7, + "apps/sim/lib/credentials/__integration__/copilot-credential-members.integration.ts": 3.5, + "apps/sim/lib/credentials/application/sharing.integration.ts": 1.5, + "apps/sim/lib/credentials/client-credential-accounts/vanta-token.integration.ts": 1.1, "apps/sim/lib/dashboards/repository.integration.ts": 0.3, - "apps/sim/lib/data-drains/service.integration.ts": 8.4, - "apps/sim/lib/data-drains/sources/coverage.integration.ts": 0.5, - "apps/sim/lib/db/advisory-locks.integration.ts": 0.6, - "apps/sim/lib/desktop/application/activity.integration.ts": 0.8, - "apps/sim/lib/desktop/application/executor.integration.ts": 1.5, - "apps/sim/lib/desktop/application/import.integration.ts": 0.9, - "apps/sim/lib/desktop/executor/bound-turn.integration.ts": 65.4, - "apps/sim/lib/desktop/executor/call-classifier.integration.ts": 0.4, - "apps/sim/lib/desktop/source-request.integration.ts": 1.6, - "apps/sim/lib/environment/execution-environment.integration.ts": 0.5, - "apps/sim/lib/execution/remote-sandbox/session-file-provenance.integration.ts": 0.5, - "apps/sim/lib/execution/remote-sandbox/session-input-certification.integration.ts": 0.4, - "apps/sim/lib/invitations/__integration__/copilot-workspace-invitations.integration.ts": 3, - "apps/sim/lib/knowledge/__integration__/application-acl.integration.ts": 5.1, + "apps/sim/lib/data-drains/service.integration.ts": 8, + "apps/sim/lib/data-drains/sources/coverage.integration.ts": 0.9, + "apps/sim/lib/db/advisory-locks.integration.ts": 1, + "apps/sim/lib/desktop/application/activity.integration.ts": 0.7, + "apps/sim/lib/desktop/application/executor.integration.ts": 2.1, + "apps/sim/lib/desktop/application/import.integration.ts": 1, + "apps/sim/lib/desktop/executor/bound-turn.integration.ts": 70.7, + "apps/sim/lib/desktop/executor/call-classifier.integration.ts": 0.9, + "apps/sim/lib/desktop/source-request.integration.ts": 2.1, + "apps/sim/lib/environment/execution-environment.integration.ts": 0.9, + "apps/sim/lib/execution/blocked-run-log.integration.ts": 1.2, + "apps/sim/lib/execution/remote-sandbox/session-file-provenance.integration.ts": 0.4, + "apps/sim/lib/execution/remote-sandbox/session-input-certification.integration.ts": 0.6, + "apps/sim/lib/execution/removal-stops.integration.ts": 6.2, + "apps/sim/lib/execution/sandbox/__integration__/content-reads.integration.ts": 4.4, + "apps/sim/lib/function-execution/overwrite-export.integration.ts": 2.9, + "apps/sim/lib/internal/file/write-copy-provenance.integration.ts": 4.6, + "apps/sim/lib/invitations/__integration__/copilot-workspace-invitations.integration.ts": 5.8, + "apps/sim/lib/knowledge/__integration__/application-acl.integration.ts": 2.1, + "apps/sim/lib/knowledge/__integration__/chunk-read-tags.integration.ts": 6.1, "apps/sim/lib/knowledge/__integration__/coda-live.integration.ts": 0, "apps/sim/lib/knowledge/__integration__/confluence-identity.integration.ts": 2, + "apps/sim/lib/knowledge/__integration__/confluence-images.integration.ts": 3.6, "apps/sim/lib/knowledge/__integration__/confluence-live.integration.ts": 0, - "apps/sim/lib/knowledge/__integration__/connector-deferral.integration.ts": 1.4, - "apps/sim/lib/knowledge/__integration__/connector-lease-pages.integration.ts": 22.4, - "apps/sim/lib/knowledge/__integration__/connector-lifecycle-locks.integration.ts": 1.7, - "apps/sim/lib/knowledge/__integration__/connector-partition-work.integration.ts": 1.4, - "apps/sim/lib/knowledge/__integration__/connector-permissions.integration.ts": 0.4, - "apps/sim/lib/knowledge/__integration__/connector-persistence-regressions.integration.ts": 3.1, - "apps/sim/lib/knowledge/__integration__/connector-save-concurrency.integration.ts": 4.3, - "apps/sim/lib/knowledge/__integration__/connector-upload.integration.ts": 1.4, - "apps/sim/lib/knowledge/__integration__/directory-sync.integration.ts": 2.4, - "apps/sim/lib/knowledge/__integration__/dormant-processing-recovery.integration.ts": 1.2, - "apps/sim/lib/knowledge/__integration__/dormant-search-processing.integration.ts": 1.6, - "apps/sim/lib/knowledge/__integration__/embedding-insert-batches.integration.ts": 1.3, - "apps/sim/lib/knowledge/__integration__/embedding-processing-recovery.integration.ts": 2.8, - "apps/sim/lib/knowledge/__integration__/execution-archive-provenance.integration.ts": 4, - "apps/sim/lib/knowledge/__integration__/external-file-provenance.integration.ts": 1.7, - "apps/sim/lib/knowledge/__integration__/filtered-search.integration.ts": 2.7, + "apps/sim/lib/knowledge/__integration__/connector-deferral.integration.ts": 1.3, + "apps/sim/lib/knowledge/__integration__/connector-lease-pages.integration.ts": 23.5, + "apps/sim/lib/knowledge/__integration__/connector-lifecycle-locks.integration.ts": 3.2, + "apps/sim/lib/knowledge/__integration__/connector-partition-work.integration.ts": 3.1, + "apps/sim/lib/knowledge/__integration__/connector-permissions.integration.ts": 0.6, + "apps/sim/lib/knowledge/__integration__/connector-persistence-regressions.integration.ts": 6.1, + "apps/sim/lib/knowledge/__integration__/connector-save-concurrency.integration.ts": 1.9, + "apps/sim/lib/knowledge/__integration__/connector-upload.integration.ts": 1.8, + "apps/sim/lib/knowledge/__integration__/content-delivery.integration.ts": 7.2, + "apps/sim/lib/knowledge/__integration__/directory-sync.integration.ts": 3.6, + "apps/sim/lib/knowledge/__integration__/dormant-processing-recovery.integration.ts": 1.5, + "apps/sim/lib/knowledge/__integration__/dormant-search-processing.integration.ts": 2.5, + "apps/sim/lib/knowledge/__integration__/embedding-insert-batches.integration.ts": 1.8, + "apps/sim/lib/knowledge/__integration__/embedding-processing-recovery.integration.ts": 4.3, + "apps/sim/lib/knowledge/__integration__/execution-archive-provenance.integration.ts": 18.6, + "apps/sim/lib/knowledge/__integration__/external-file-provenance.integration.ts": 3, + "apps/sim/lib/knowledge/__integration__/filtered-search.integration.ts": 1.7, "apps/sim/lib/knowledge/__integration__/gitlab-federated.integration.ts": 0, "apps/sim/lib/knowledge/__integration__/gitlab-live.integration.ts": 0, - "apps/sim/lib/knowledge/__integration__/gitlab-workspace.integration.ts": 3.1, + "apps/sim/lib/knowledge/__integration__/gitlab-workspace.integration.ts": 5.6, "apps/sim/lib/knowledge/__integration__/google-drive-live.integration.ts": 0, "apps/sim/lib/knowledge/__integration__/google-drive-member-live.integration.ts": 0, - "apps/sim/lib/knowledge/__integration__/google-drive-shortcuts.integration.ts": 2.4, - "apps/sim/lib/knowledge/__integration__/kb-block-search.integration.ts": 0.9, - "apps/sim/lib/knowledge/__integration__/knowledge-base-deletion.integration.ts": 0.6, - "apps/sim/lib/knowledge/__integration__/knowledge-base-list.integration.ts": 3.8, - "apps/sim/lib/knowledge/__integration__/knowledge-projection.integration.ts": 0.8, - "apps/sim/lib/knowledge/__integration__/listing-continuation.integration.ts": 24.6, - "apps/sim/lib/knowledge/__integration__/member-document-lifecycle.integration.ts": 4.2, - "apps/sim/lib/knowledge/__integration__/member-scope-renewal.integration.ts": 1.4, - "apps/sim/lib/knowledge/__integration__/ocr-input-failures.integration.ts": 1.7, - "apps/sim/lib/knowledge/__integration__/processing-lock-scope.integration.ts": 1.7, - "apps/sim/lib/knowledge/__integration__/provider-admission.integration.ts": 0.6, - "apps/sim/lib/knowledge/__integration__/provider-processing-recovery.integration.ts": 1.7, + "apps/sim/lib/knowledge/__integration__/google-drive-shortcuts.integration.ts": 4, + "apps/sim/lib/knowledge/__integration__/kb-block-search.integration.ts": 2, + "apps/sim/lib/knowledge/__integration__/keyword-ranking.integration.ts": 1.2, + "apps/sim/lib/knowledge/__integration__/knowledge-base-deletion.integration.ts": 0.7, + "apps/sim/lib/knowledge/__integration__/knowledge-base-list.integration.ts": 4.2, + "apps/sim/lib/knowledge/__integration__/knowledge-projection.integration.ts": 1, + "apps/sim/lib/knowledge/__integration__/listing-continuation.integration.ts": 23.8, + "apps/sim/lib/knowledge/__integration__/member-document-lifecycle.integration.ts": 7.5, + "apps/sim/lib/knowledge/__integration__/member-scope-renewal.integration.ts": 2.3, + "apps/sim/lib/knowledge/__integration__/member-scope-withdrawal.integration.ts": 2.3, + "apps/sim/lib/knowledge/__integration__/ocr-input-failures.integration.ts": 1.8, + "apps/sim/lib/knowledge/__integration__/processing-lock-scope.integration.ts": 1.5, + "apps/sim/lib/knowledge/__integration__/provider-admission.integration.ts": 4.2, + "apps/sim/lib/knowledge/__integration__/provider-processing-recovery.integration.ts": 2.5, "apps/sim/lib/knowledge/__integration__/providers-live.integration.ts": 0, - "apps/sim/lib/knowledge/__integration__/purged-detach-reservation.integration.ts": 4.1, + "apps/sim/lib/knowledge/__integration__/purged-detach-reservation.integration.ts": 3.6, "apps/sim/lib/knowledge/__integration__/reranker-admission.integration.ts": 0.6, "apps/sim/lib/knowledge/__integration__/scale.integration.ts": 0, - "apps/sim/lib/knowledge/__integration__/search-index-policy.integration.ts": 1.7, + "apps/sim/lib/knowledge/__integration__/search-index-policy.integration.ts": 2.5, "apps/sim/lib/knowledge/__integration__/search-latency.integration.ts": 0, - "apps/sim/lib/knowledge/__integration__/search-mcp-setup.integration.ts": 10.8, - "apps/sim/lib/knowledge/__integration__/search-reference-batching.integration.ts": 0.6, - "apps/sim/lib/knowledge/__integration__/search-source-pagination.integration.ts": 1.2, - "apps/sim/lib/knowledge/__integration__/search-source-progress.integration.ts": 2.8, - "apps/sim/lib/knowledge/__integration__/search-source-setup.integration.ts": 2.9, - "apps/sim/lib/knowledge/__integration__/slack-empty-threads.integration.ts": 2.5, + "apps/sim/lib/knowledge/__integration__/search-mcp-setup.integration.ts": 11.2, + "apps/sim/lib/knowledge/__integration__/search-reference-batching.integration.ts": 0.5, + "apps/sim/lib/knowledge/__integration__/search-source-pagination.integration.ts": 2.1, + "apps/sim/lib/knowledge/__integration__/search-source-progress.integration.ts": 6.4, + "apps/sim/lib/knowledge/__integration__/search-source-setup.integration.ts": 4.3, + "apps/sim/lib/knowledge/__integration__/slack-empty-threads.integration.ts": 4.1, "apps/sim/lib/knowledge/__integration__/slack-live.integration.ts": 0, - "apps/sim/lib/knowledge/__integration__/slack-search-turns.integration.ts": 1, - "apps/sim/lib/knowledge/__integration__/storage-accounting.integration.ts": 2.4, - "apps/sim/lib/knowledge/__integration__/storage-cleanup.integration.ts": 1.2, - "apps/sim/lib/knowledge/__integration__/stored-document-recovery.integration.ts": 16.6, - "apps/sim/lib/knowledge/__integration__/upload-read-provenance.integration.ts": 1.3, - "apps/sim/lib/knowledge/__integration__/user-document-visibility.integration.ts": 0.5, - "apps/sim/lib/knowledge/__integration__/workspace-import.integration.ts": 1.4, - "apps/sim/lib/knowledge/__integration__/workspace-kb-document-access.integration.ts": 0.9, - "apps/sim/lib/knowledge/__integration__/workspace-lifecycle.integration.ts": 3, - "apps/sim/lib/knowledge/access/group-membership.integration.ts": 0.5, - "apps/sim/lib/knowledge/access/predicate.integration.ts": 1.1, - "apps/sim/lib/knowledge/application/github-installations.integration.ts": 0.7, + "apps/sim/lib/knowledge/__integration__/slack-search-turns.integration.ts": 1.1, + "apps/sim/lib/knowledge/__integration__/storage-accounting.integration.ts": 5.5, + "apps/sim/lib/knowledge/__integration__/storage-cleanup.integration.ts": 2, + "apps/sim/lib/knowledge/__integration__/stored-document-recovery.integration.ts": 4.6, + "apps/sim/lib/knowledge/__integration__/upload-read-provenance.integration.ts": 3.4, + "apps/sim/lib/knowledge/__integration__/user-document-visibility.integration.ts": 0.9, + "apps/sim/lib/knowledge/__integration__/workspace-import.integration.ts": 2.2, + "apps/sim/lib/knowledge/__integration__/workspace-kb-document-access.integration.ts": 1, + "apps/sim/lib/knowledge/__integration__/workspace-lifecycle.integration.ts": 4.1, + "apps/sim/lib/knowledge/access/group-membership.integration.ts": 0.7, + "apps/sim/lib/knowledge/access/predicate.integration.ts": 1.7, + "apps/sim/lib/knowledge/application/github-installations.integration.ts": 1.6, "apps/sim/lib/knowledge/application/search-index.integration.ts": 0.4, - "apps/sim/lib/knowledge/application/slack-search/assistant.integration.ts": 0.8, - "apps/sim/lib/knowledge/connectors/external-directory.integration.ts": 0.2, - "apps/sim/lib/knowledge/connectors/sync-content-pass.integration.ts": 0.9, - "apps/sim/lib/knowledge/connectors/sync-persistence.integration.ts": 0.5, - "apps/sim/lib/knowledge/search/history/history.integration.ts": 0.7, - "apps/sim/lib/knowledge/tags/date-tag-condition.integration.ts": 0.8, - "apps/sim/lib/logs/execution/completion-ledger-order.integration.ts": 2.2, - "apps/sim/lib/logs/execution/start-execution.integration.ts": 1.6, - "apps/sim/lib/memory/conversation-store.integration.ts": 3.1, - "apps/sim/lib/memory/message-provenance.integration.ts": 1.7, - "apps/sim/lib/memory/summary-store.integration.ts": 0.7, - "apps/sim/lib/mothership/agent-cli/workbench-file-provenance.integration.ts": 2.2, - "apps/sim/lib/mothership/async-runs/browser-download-claim.integration.ts": 0.4, - "apps/sim/lib/mothership/async-runs/orphaned-runs.integration.ts": 14.8, - "apps/sim/lib/mothership/async-runs/tool-identity.integration.ts": 0.5, - "apps/sim/lib/mothership/billing/service-store.integration.ts": 0.6, + "apps/sim/lib/knowledge/application/slack-search/assistant.integration.ts": 0.6, + "apps/sim/lib/knowledge/connectors/external-directory.integration.ts": 0.3, + "apps/sim/lib/knowledge/connectors/sync-content-pass.integration.ts": 1.7, + "apps/sim/lib/knowledge/connectors/sync-persistence.integration.ts": 0.6, + "apps/sim/lib/knowledge/search/history/history.integration.ts": 0.9, + "apps/sim/lib/knowledge/tags/date-tag-condition.integration.ts": 1.5, + "apps/sim/lib/logs/execution/completion-ledger-order.integration.ts": 1.8, + "apps/sim/lib/logs/execution/scope-deleted.integration.ts": 2.5, + "apps/sim/lib/logs/execution/start-execution.integration.ts": 2.5, + "apps/sim/lib/logs/public-queries.integration.ts": 0.7, + "apps/sim/lib/logs/stats-queries.integration.ts": 1.5, + "apps/sim/lib/media/ffmpeg-content.integration.ts": 1, + "apps/sim/lib/memory/conversation-store.integration.ts": 2.7, + "apps/sim/lib/memory/message-provenance.integration.ts": 2.1, + "apps/sim/lib/memory/summary-store.integration.ts": 1, + "apps/sim/lib/mothership/__integration__/content-delivery-compat.integration.ts": 10, + "apps/sim/lib/mothership/__integration__/content-delivery.integration.ts": 58.4, + "apps/sim/lib/mothership/agent-cli/project-file-read.integration.ts": 30.4, + "apps/sim/lib/mothership/agent-cli/project-file-write-transport.integration.ts": 29.1, + "apps/sim/lib/mothership/agent-cli/workbench-file-provenance.integration.ts": 1.3, + "apps/sim/lib/mothership/async-runs/browser-download-claim.integration.ts": 0.7, + "apps/sim/lib/mothership/async-runs/orphaned-runs.integration.ts": 17.3, + "apps/sim/lib/mothership/async-runs/run-secret-authority.integration.ts": 0.7, + "apps/sim/lib/mothership/async-runs/tool-identity.integration.ts": 0.8, + "apps/sim/lib/mothership/billing/service-store.integration.ts": 0.9, + "apps/sim/lib/mothership/chat/application/read-recall-chats.integration.ts": 1.4, + "apps/sim/lib/mothership/chat/application/read-stream-status.integration.ts": 0.7, "apps/sim/lib/mothership/chat/application/set-effort.integration.ts": 0.7, - "apps/sim/lib/mothership/chat/mcp-servers.integration.ts": 0.8, - "apps/sim/lib/mothership/request/session/buffer-ttl.integration.ts": 12.3, - "apps/sim/lib/mothership/request/session/recovery-storm.integration.ts": 215.6, - "apps/sim/lib/mothership/request/session/replay-budget.integration.ts": 34, - "apps/sim/lib/mothership/request/session/replay-gap.integration.ts": 30.1, - "apps/sim/lib/mothership/request/session/stream-recovery.integration.ts": 6.7, - "apps/sim/lib/mothership/request/tools/workflow-client-settlement.integration.ts": 0.8, - "apps/sim/lib/mothership/tasks/application/prepare-wake.integration.ts": 10.6, - "apps/sim/lib/mothership/tools/client/desktop-tool-authorization.integration.ts": 11.9, - "apps/sim/lib/mothership/tools/client/desktop-tool-chat-view-lease.integration.ts": 7.8, - "apps/sim/lib/mothership/tools/client/desktop-tool-executor.integration.ts": 5.5, - "apps/sim/lib/mothership/tools/client/desktop-tool-pickup.integration.ts": 88.8, - "apps/sim/lib/oauth/shopify.integration.ts": 2, + "apps/sim/lib/mothership/chat/file-attachments.integration.ts": 1.3, + "apps/sim/lib/mothership/chat/mcp-servers.integration.ts": 1.3, + "apps/sim/lib/mothership/chat/project-file-context.integration.ts": 7.9, + "apps/sim/lib/mothership/chat/text-transcript.integration.ts": 0.7, + "apps/sim/lib/mothership/chat/transcript-pages.integration.ts": 4.8, + "apps/sim/lib/mothership/memories/application/resolve-access.integration.ts": 15.1, + "apps/sim/lib/mothership/memories/cleanup.integration.ts": 1.5, + "apps/sim/lib/mothership/memory/application/spaces.integration.ts": 3.7, + "apps/sim/lib/mothership/request/application/abandoned-runs.integration.ts": 13.8, + "apps/sim/lib/mothership/request/application/removal-stops-runs.integration.ts": 15.2, + "apps/sim/lib/mothership/request/lifecycle/file-owner-protocol.integration.ts": 2.1, + "apps/sim/lib/mothership/request/session/buffer-ttl.integration.ts": 12.2, + "apps/sim/lib/mothership/request/session/recovery-storm.integration.ts": 194.2, + "apps/sim/lib/mothership/request/session/replay-budget.integration.ts": 75.4, + "apps/sim/lib/mothership/request/session/replay-gap.integration.ts": 35.4, + "apps/sim/lib/mothership/request/session/stream-recovery.integration.ts": 7.2, + "apps/sim/lib/mothership/request/tools/workflow-client-settlement.integration.ts": 9.8, + "apps/sim/lib/mothership/tasks/application/prepare-wake.integration.ts": 21.3, + "apps/sim/lib/mothership/tools/client/desktop-tool-authorization.integration.ts": 17, + "apps/sim/lib/mothership/tools/client/desktop-tool-chat-view-lease.integration.ts": 10.8, + "apps/sim/lib/mothership/tools/client/desktop-tool-executor.integration.ts": 4.2, + "apps/sim/lib/mothership/tools/client/desktop-tool-pickup.integration.ts": 91.2, + "apps/sim/lib/mothership/tools/server/media/write-target-precheck.integration.ts": 1.4, + "apps/sim/lib/oauth/credential-revocation.integration.ts": 5.5, + "apps/sim/lib/oauth/shopify.integration.ts": 4.3, "apps/sim/lib/organizations/application/domain-settings.integration.ts": 0.7, - "apps/sim/lib/permission-groups/__integration__/copilot-permission-config.integration.ts": 3.1, - "apps/sim/lib/projects/__integration__/foundation.integration.ts": 3.9, - "apps/sim/lib/selectors/__integration__/copilot-nested-selectors.integration.ts": 3.6, - "apps/sim/lib/sim-search/live/managed-mcp.integration.ts": 4.9, - "apps/sim/lib/sim-search/live/provider-discovery.integration.ts": 1.1, - "apps/sim/lib/table/analytics/postgres.integration.ts": 0.5, - "apps/sim/lib/table/application/row-audit.integration.ts": 1.2, - "apps/sim/lib/table/dispatcher.integration.ts": 0.6, - "apps/sim/lib/table/row-changes.integration.ts": 0.7, - "apps/sim/lib/table/rows/writes.integration.ts": 10.7, - "apps/sim/lib/table/rows/secret-provenance.integration.ts": 2.2, - "apps/sim/lib/uploads/contexts/organization-logo/application.integration.ts": 0.6, - "apps/sim/lib/uploads/contexts/workspace/__integration__/file-names.integration.ts": 2.1, - "apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts": 6.5, - "apps/sim/lib/uploads/upload-session/workspace-file-provenance.integration.ts": 0.4, - "apps/sim/lib/webhooks/registration-store.integration.ts": 0.7, - "apps/sim/lib/webhooks/trigger-block-deployment.integration.ts": 5.4, - "apps/sim/lib/workflows/application/compare-workflow-versions.integration.ts": 1.6, - "apps/sim/lib/workflows/application/read-workflow-version.integration.ts": 1.5, - "apps/sim/lib/workflows/custom-blocks/workspace-rows.integration.ts": 1.3, - "apps/sim/lib/workflows/executor/pause-persistence.integration.ts": 5.9, - "apps/sim/lib/workflows/orchestration/create-workflow-with-state.integration.ts": 3, - "apps/sim/lib/workspace-files/__integration__/copilot-file-versions.integration.ts": 3.2, - "apps/sim/lib/workspace-files/search/chunks.integration.ts": 18, - "apps/sim/lib/workspace-files/search/dispatcher.integration.ts": 14.8, + "apps/sim/lib/permission-groups/__integration__/copilot-permission-config.integration.ts": 4.1, + "apps/sim/lib/projects/__integration__/backfill-repair.integration.ts": 44.6, + "apps/sim/lib/projects/__integration__/foundation.integration.ts": 43.4, + "apps/sim/lib/projects/files/__integration__/authorization-bulk.integration.ts": 3.9, + "apps/sim/lib/projects/files/__integration__/authorization-product.integration.ts": 5.8, + "apps/sim/lib/projects/files/__integration__/authorization.integration.ts": 7.9, + "apps/sim/lib/projects/files/__integration__/browser.integration.ts": 3.1, + "apps/sim/lib/projects/files/__integration__/content-bulk.integration.ts": 10.4, + "apps/sim/lib/projects/files/__integration__/content-product.integration.ts": 47.2, + "apps/sim/lib/projects/files/__integration__/content-rendered.integration.ts": 4.6, + "apps/sim/lib/projects/files/__integration__/content-sharing.integration.ts": 4, + "apps/sim/lib/projects/files/__integration__/content.integration.ts": 9, + "apps/sim/lib/projects/files/__integration__/documents.integration.ts": 2.2, + "apps/sim/lib/projects/files/__integration__/private-purge.integration.ts": 5.5, + "apps/sim/lib/projects/files/__integration__/retention.integration.ts": 6.4, + "apps/sim/lib/projects/files/__integration__/uploads.integration.ts": 42.3, + "apps/sim/lib/selectors/__integration__/copilot-nested-selectors.integration.ts": 6.3, + "apps/sim/lib/sim-search/live/managed-mcp.integration.ts": 6.4, + "apps/sim/lib/sim-search/live/provider-discovery.integration.ts": 1.4, + "apps/sim/lib/table/analytics/postgres.integration.ts": 0.6, + "apps/sim/lib/table/application/row-audit.integration.ts": 1.6, + "apps/sim/lib/table/dispatcher.integration.ts": 0.9, + "apps/sim/lib/table/import-provenance.integration.ts": 1.5, + "apps/sim/lib/table/row-changes.integration.ts": 1, + "apps/sim/lib/table/rows/secret-provenance.integration.ts": 4.2, + "apps/sim/lib/table/rows/writes.integration.ts": 17.5, + "apps/sim/lib/uploads/contexts/organization-logo/application.integration.ts": 0.7, + "apps/sim/lib/uploads/contexts/workspace/__integration__/creator-lifetime.integration.ts": 4.3, + "apps/sim/lib/uploads/contexts/workspace/__integration__/file-names.integration.ts": 11.6, + "apps/sim/lib/uploads/contexts/workspace/__integration__/file-versions.integration.ts": 26.3, + "apps/sim/lib/uploads/contexts/workspace/__integration__/storage-lock-order.integration.ts": 1.8, + "apps/sim/lib/uploads/upload-session/in-process-origin.integration.ts": 4.7, + "apps/sim/lib/uploads/upload-session/registration-lifetime.integration.ts": 0.6, + "apps/sim/lib/uploads/upload-session/workspace-file-provenance.integration.ts": 0.9, + "apps/sim/lib/users/deleted-identity.integration.ts": 2.3, + "apps/sim/lib/users/storage-sweep.integration.ts": 2.4, + "apps/sim/lib/webhooks/registration-store.integration.ts": 1.3, + "apps/sim/lib/webhooks/trigger-block-deployment.integration.ts": 10.4, + "apps/sim/lib/workflows/application/compare-workflow-versions.integration.ts": 1.7, + "apps/sim/lib/workflows/application/read-workflow-version.integration.ts": 2.2, + "apps/sim/lib/workflows/custom-blocks/workspace-rows.integration.ts": 2.1, + "apps/sim/lib/workflows/executor/pause-persistence.integration.ts": 10.4, + "apps/sim/lib/workflows/orchestration/create-workflow-with-state.integration.ts": 2.8, + "apps/sim/lib/workflows/persistence/custom-tools-persistence.integration.ts": 0.9, + "apps/sim/lib/workspace-files/__integration__/copilot-file-versions.integration.ts": 4.7, + "apps/sim/lib/workspace-files/delivery.integration.ts": 1.1, + "apps/sim/lib/workspace-files/search/chunks.integration.ts": 14.2, + "apps/sim/lib/workspace-files/search/dispatcher.integration.ts": 19.7, "apps/sim/lib/workspace-files/search/index-plan.integration.ts": 0.1, - "apps/sim/lib/workspaces/__integration__/fork-sync.integration.ts": 17.7, - "apps/sim/lib/workspaces/__integration__/http-cli.integration.ts": 6.7, - "apps/sim/lib/workspaces/__integration__/mapped-import.integration.ts": 4.6, - "apps/sim/lib/workspaces/__integration__/pagination.integration.ts": 0.7, - "apps/sim/lib/workspaces/__integration__/receipts.integration.ts": 0.4, - "apps/sim/lib/workspaces/organization-workspaces.integration.ts": 1, - "apps/sim/tools/powerbi/powerbi.integration.ts": 7.4, - "packages/db/embedding-width-migration.integration.ts": 0.4, - "packages/db/maintenance/search-retirement.integration.ts": 9.9, - "packages/db/member-sync-status-migration.integration.ts": 0, - "packages/db/organization-search-migration.integration.ts": 1, - "packages/db/permission-access-requests-migration.integration.ts": 0.1, - "packages/db/script-migrations/0013_backfill_legacy_knowledge_base_workspaces.integration.ts": 5.5, - "packages/db/script-migrations/0014_require_knowledge_base_owner.integration.ts": 0.1, + "apps/sim/lib/workspace-files/search/owners.integration.ts": 15.1, + "apps/sim/lib/workspaces/__integration__/fork-sync.integration.ts": 24.5, + "apps/sim/lib/workspaces/__integration__/http-cli.integration.ts": 7.7, + "apps/sim/lib/workspaces/__integration__/mapped-import.integration.ts": 8.6, + "apps/sim/lib/workspaces/__integration__/pagination.integration.ts": 1.2, + "apps/sim/lib/workspaces/__integration__/receipts.integration.ts": 0.6, + "apps/sim/lib/workspaces/__integration__/recently-deleted.integration.ts": 0.8, + "apps/sim/lib/workspaces/access/shared-resource-handoff.integration.ts": 4.5, + "apps/sim/lib/workspaces/admin-move.integration.ts": 4.6, + "apps/sim/lib/workspaces/organization-workspaces.integration.ts": 1.5, + "apps/sim/tools/powerbi/powerbi.integration.ts": 15.9, + "packages/db/embedding-width-migration.integration.ts": 0.5, + "packages/db/file-creator-lifetime.integration.ts": 0.3, + "packages/db/file-entity-ownership.integration.ts": 0.2, + "packages/db/file-folder-version-ownership.integration.ts": 0.3, + "packages/db/large-table-vacuum-throttle-migration.integration.ts": 0.3, + "packages/db/maintenance/search-retirement.integration.ts": 10.8, + "packages/db/member-sync-status-migration.integration.ts": 0.2, + "packages/db/organization-search-migration.integration.ts": 0.7, + "packages/db/permission-access-requests-migration.integration.ts": 0.2, + "packages/db/public-share-ownership.integration.ts": 43.2, + "packages/db/script-migrations/0013_backfill_legacy_knowledge_base_workspaces.integration.ts": 5.8, + "packages/db/script-migrations/0014_require_knowledge_base_owner.integration.ts": 0.4, "packages/db/script-migrations/0016_backfill_search_vectors.integration.ts": 2.2, - "packages/db/script-migrations/0018_repair_workspace_file_content_revision.integration.ts": 0.1, + "packages/db/script-migrations/0018_repair_workspace_file_content_revision.integration.ts": 0.2, "packages/db/script-migrations/0021_embedding_search_connector.integration.ts": 0.2, - "packages/db/script-migrations/0030_retire_keyword_writers.integration.ts": 2.3, - "packages/db/scripts/connector-sync-schedule-precision.integration.ts": 0, - "packages/db/scripts/database-failure-classification.integration.ts": 1.1, - "packages/db/scripts/push.integration.ts": 5.4, - "packages/db/scripts/retired-columns.integration.ts": 0.1 + "packages/db/script-migrations/0030_retire_keyword_writers.integration.ts": 2.2, + "packages/db/scripts/connector-sync-schedule-precision.integration.ts": 0.2, + "packages/db/scripts/database-failure-classification.integration.ts": 1.7, + "packages/db/scripts/file-ownership-reconciliation.integration.ts": 2.2, + "packages/db/scripts/project-authority.integration.ts": 2.2, + "packages/db/scripts/project-contract.integration.ts": 32.7, + "packages/db/scripts/push.integration.ts": 16, + "packages/db/scripts/retired-columns.integration.ts": 0.4, + "packages/db/scripts/workspace-project-column.integration.ts": 0.3 }