Skip to content

Commit 2f76202

Browse files
committed
Merge staging and advance Project enforcement migrations
2 parents 5351d14 + caa925a commit 2f76202

228 files changed

Lines changed: 132689 additions & 1812 deletions

File tree

Some content is hidden

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

‎.agents/skills/add-enrichment/SKILL.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Because enrichments run on Sim's hosted keys by default, **every provider tool y
2222

2323
## Architecture (what you're plugging into)
2424

25-
- **`enrichments/types.ts`** — `EnrichmentConfig { id, name, description, icon, inputs, outputs, providers }` and `EnrichmentProvider { id, label, toolId, buildParams, projectFailure, mapOutput }` — `toolProvider` fills `projectFailure` with the standard HTTP projection; override it only for a provider whose failure shape is nonstandard (see `enrichments/provider-failures/`). Providers are **plain data** (no `@/tools` import) so the catalog stays client-safe.
25+
- **`enrichments/types.ts`** — `EnrichmentConfig { id, name, description, icon, inputs, outputs, providers }` and `EnrichmentProvider { id, label, toolId, buildParams, projectFailure, mapOutput, isVerified? }` — `toolProvider` fills `projectFailure` with the standard HTTP projection; override it only for a provider whose failure shape is nonstandard (see `enrichments/provider-failures/`). Providers are **plain data** (no `@/tools` import) so the catalog stays client-safe.
2626
- **`enrichments/providers.ts`** — `toolProvider(...)` (typed passthrough) plus shared input helpers: `str(v)`, `normalizeDomain(v)`, `firstNonEmpty(arr)`, `splitName(fullName)`, and `projectEnrichmentProviderFailure`.
2727
- **`enrichments/run.ts`** — the server-only cascade runner. Calls `executeTool(provider.toolId, { ...params, _context: { workspaceId, userId } })`, accumulates hosted-key cost, returns the first non-empty mapped result. **You do not edit this** — it works for any registry entry.
2828
- **`enrichments/registry.ts`** — `ENRICHMENT_REGISTRY` / `ALL_ENRICHMENTS` / `getEnrichment`. Register new entries here.
@@ -37,7 +37,9 @@ For each output the enrichment produces, decide which existing tool provides it.
3737
- Its `params` accept what you can derive from table columns (read the tool's `params`).
3838
- Its `outputs` / `transformResponse` actually expose the field you need (read the real output shape — don't assume).
3939

40-
Order providers **cheapest / most-likely-to-hit first**; the cascade stops at the first non-empty result. Apollo and LinkedIn APIs are not hosted-safe (ToS) — never call them as providers.
40+
Order providers by **accuracy first, then price per hit**. Order decides which answer wins when two providers would disagree, so a cheaper but less accurate provider ahead of a better one trades quality for cost; reorder for price only with benchmark evidence. Among similarly accurate providers, put the ones that bill on a miss or charge a high price per hit after the cheaper, free-on-miss ones (check each vendor's billing docs and the tool's `hosting.pricing`); never move a more accurate provider behind a less accurate one to save cost. The cascade stops at the first non-empty, verified result. Apollo and LinkedIn APIs are not hosted-safe (ToS) — never call them as providers.
41+
42+
Prefer a vendor's own verified-only option (e.g. Prospeo `only_verified_email`) when it turns an unverified result into a free miss. Otherwise, when a provider can return an unconfirmed hit (a catch-all guess, a `risky` status), declare `isVerified(output)`: an unconfirmed hit is held while later providers look for a confirmed one, and fills the cell only if none does. `isVerified: false` marks a provider that never confirms its hits (a database record); it runs only while nothing is held.
4143

4244
## Step 2: Verify hosted-key support — chain to `/add-hosted-key` if missing
4345

@@ -136,7 +138,8 @@ export const ENRICHMENT_REGISTRY: EnrichmentRegistry = {
136138

137139
- [ ] Each output mapped to a real tool field (verified against the tool's `params`/`outputs`)
138140
- [ ] **Every provider tool has a `hosting` block — ran `/add-hosted-key` for any that didn't**
139-
- [ ] Providers ordered cheapest / most-likely-first; Apollo/LinkedIn not used
141+
- [ ] Providers ordered accuracy-first, then price per hit (billing on a miss checked against vendor docs); Apollo/LinkedIn not used
142+
- [ ] Providers that can return an unconfirmed hit declare `isVerified`
140143
- [ ] Enrichment file is client-safe (no `@/tools` import); uses `toolProvider` + shared helpers
141144
- [ ] `buildParams` returns `null` on insufficient inputs; `mapOutput` returns `null` on a miss
142145
- [ ] Registered in `enrichments/registry.ts`

‎.claude/rules/sim-testing.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ contracts, and demonstrated regressions.
3333
| Suffix | Needs | Run with | In CI |
3434
|--------|-------|----------|-------|
3535
| `*.test.ts(x)` | nothing; global mocks from `vitest.setup.ts` | `bun run --cwd <workspace> test` | `test` jobs |
36-
| `*.integration.ts` | real PostgreSQL (`TEST_DATABASE_URL`), optionally Redis (`TEST_REDIS_URL`) | `bun run --cwd <workspace> test --mode integration` | `integration` jobs, by glob (sharded per provisioning path) |
36+
| `*.integration.ts` | real PostgreSQL (`TEST_DATABASE_URL`), Redis (`TEST_REDIS_URL`; optional locally, required in CI) | `bun run --cwd <workspace> test --mode integration` | `integration` jobs, by glob (sharded per provisioning path) |
3737
| `*.live.test.ts` | provider APIs, hosted sandboxes, local runtimes, or sibling checkouts | `bun run --cwd apps/sim test --mode live <file>` | never |
3838
| `apps/desktop/e2e/*.spec.ts` | the packaged Electron app | Playwright | desktop E2E workflow |
3939
| `apps/sim/scripts/test-*-e2e.ts` | a running app over HTTP | its `package.json` script when one exists (`bun run test:scim:e2e`; `test:workflow-version-compare:e2e` adds `--no-env-file`), else `bun scripts/test-<suite>-e2e.ts` from apps/sim | `e2e` jobs (`.github/scripts/http-e2e.sh`) |

‎.cursor/rules/sim-testing.mdc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ contracts, and demonstrated regressions.
3131
| Suffix | Needs | Run with | In CI |
3232
|--------|-------|----------|-------|
3333
| `*.test.ts(x)` | nothing; global mocks from `vitest.setup.ts` | `bun run --cwd <workspace> test` | `test` jobs |
34-
| `*.integration.ts` | real PostgreSQL (`TEST_DATABASE_URL`), optionally Redis (`TEST_REDIS_URL`) | `bun run --cwd <workspace> test --mode integration` | `integration` jobs, by glob (sharded per provisioning path) |
34+
| `*.integration.ts` | real PostgreSQL (`TEST_DATABASE_URL`), Redis (`TEST_REDIS_URL`; optional locally, required in CI) | `bun run --cwd <workspace> test --mode integration` | `integration` jobs, by glob (sharded per provisioning path) |
3535
| `*.live.test.ts` | provider APIs, hosted sandboxes, local runtimes, or sibling checkouts | `bun run --cwd apps/sim test --mode live <file>` | never |
3636
| `apps/desktop/e2e/*.spec.ts` | the packaged Electron app | Playwright | desktop E2E workflow |
3737
| `apps/sim/scripts/test-*-e2e.ts` | a running app over HTTP | its `package.json` script when one exists (`bun run test:scim:e2e`; `test:workflow-version-compare:e2e` adds `--no-env-file`), else `bun scripts/test-<suite>-e2e.ts` from apps/sim | `e2e` jobs (`.github/scripts/http-e2e.sh`) |

‎.github/scripts/ci-changes.sh‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env bash
2+
# Prints which checks a pull request's changes can affect, as `<flag>=true|false` lines for
3+
# $GITHUB_OUTPUT. A flag is `false` only when every changed file is on an allowlist whose jobs
4+
# fully cover it. A file on no allowlist sets every flag but `desktop_live` (which keeps its own
5+
# skip list) to `true`; a change under .github/, an empty diff, or any failure to work out the
6+
# diff sets all of them.
7+
#
8+
# full integration, e2e, e2e-build, e2e-mobile: any file not allowlisted below
9+
# test the Vitest shards: content, docs app, and desktop changes, plus `full`
10+
# build the app's `next build`: content changes, plus `full`
11+
# desktop_live the live Electron suite: any file outside its own skip list
12+
#
13+
# lint always runs and is not gated.
14+
#
15+
# Usage: ci-changes.sh <base-sha>
16+
set -u
17+
18+
# Posts, author profiles, and the public assets only posts reference. The test shards read them
19+
# (lib/content/og-image.test.ts) and `next build` prerenders every post with its MDX components,
20+
# which check:library-content compiles but never renders. Components under content/ are code.
21+
content='^apps/sim/content/([^/]+/)*[^/]+\.mdx?$|^apps/sim/content/authors/[^/]+\.json$|^apps/sim/public/(authors|blog|changelog|library)/'
22+
# Its own workspace tests, its type-check and docs-manifest audit in lint, and apps/sim tests
23+
# that read its pages (tools/netsuite/netsuite.test.ts).
24+
docs='^apps/docs/'
25+
# Nothing outside the Electron app imports it; the desktop bridge and protocol packages are
26+
# consumed by apps/sim and stay unlisted.
27+
desktop='^apps/desktop/'
28+
# Agent guidance, read only by the lint job's audits.
29+
guidance='^(\.agents|\.claude|\.cursor)/|^([^/]+/)*(AGENTS|CLAUDE)\.md$|^README\.md$'
30+
# The live desktop suite drives apps/desktop against the app, so it skips more than the rest.
31+
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$'
32+
33+
emit() {
34+
printf 'full=%s\ntest=%s\nbuild=%s\ndesktop_live=%s\n' "$1" "$2" "$3" "$4"
35+
}
36+
run_all() {
37+
emit true true true true
38+
echo "Running every check: $1" >&2
39+
exit 0
40+
}
41+
matches() { printf '%s\n' "$names" | grep -qE "$1"; }
42+
outside() { printf '%s\n' "$names" | grep -qvE "$1"; }
43+
44+
base=${1:-}
45+
[ -n "$base" ] || run_all 'no base commit'
46+
git fetch --quiet --depth=1 origin "$base" || run_all "could not fetch $base"
47+
names=$(git diff --no-renames --name-only "$base" HEAD) || run_all "could not diff against $base"
48+
[ -n "$names" ] || run_all 'no changed files listed'
49+
if matches '^\.github/'; then run_all 'a CI file changed'; fi
50+
51+
desktop_live=false
52+
if outside "$desktop_live_skip"; then desktop_live=true; fi
53+
if outside "$content|$docs|$desktop|$guidance"; then
54+
emit true true true "$desktop_live"
55+
unlisted=$(printf '%s\n' "$names" | grep -vE "$content|$docs|$desktop|$guidance" | head -n 1)
56+
echo "Running every check (desktop_live=$desktop_live): $unlisted is not on an allowlist" >&2
57+
exit 0
58+
fi
59+
60+
test=false build=false
61+
if matches "$content|$docs|$desktop"; then test=true; fi
62+
if matches "$content"; then build=true; fi
63+
emit false "$test" "$build" "$desktop_live"
64+
echo "Scoped checks: test=$test build=$build desktop_live=$desktop_live" >&2

‎.github/scripts/ci-gate.sh‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env bash
2+
# The `ci` job's verdict: the single required check, so it must never pass a run whose checks did
3+
# not all pass. Reads the job's `needs` context (`toJSON(needs)`) from $NEEDS and exits non-zero,
4+
# naming each offending job, unless every job succeeded or was skipped as `changes` intended.
5+
#
6+
# A skip passes only for a gated job whose `changes` flag is exactly `false`:
7+
#
8+
# integration, e2e, e2e-build, e2e-mobile full
9+
# test test
10+
# build build
11+
# desktop-live desktop_live
12+
#
13+
# Every other skip fails: an ungated job (lint, `changes` itself), a gated job whose flag is
14+
# `true`, missing, or anything else (a failed or skipped `changes` leaves no outputs). A failure
15+
# or a cancellation always fails.
16+
#
17+
# Usage: NEEDS='<toJSON(needs)>' ci-gate.sh
18+
set -euo pipefail
19+
20+
needs=${NEEDS:-}
21+
if ! jq -e 'type == "object"' <<< "$needs" > /dev/null 2>&1; then
22+
echo "::error::The needs context is not a JSON object: '$needs'"
23+
exit 1
24+
fi
25+
26+
failed="$(jq -r '
27+
{"integration": "full", "e2e": "full", "e2e-build": "full", "e2e-mobile": "full", "test": "test",
28+
"build": "build", "desktop-live": "desktop_live"} as $gate
29+
| (.changes.outputs // {}) as $flags
30+
| to_entries[]
31+
| select(.value.result != "success")
32+
| select(.value.result != "skipped" or $gate[.key] == null or $flags[$gate[.key]] != "false")
33+
| "\(.key): \(.value.result)"
34+
' <<< "$needs")"
35+
if [ -n "$failed" ]; then
36+
echo "::error::Checks did not pass:"
37+
echo "$failed"
38+
exit 1
39+
fi
40+
echo "All checks passed."

‎.github/scripts/desktop-live-changes.sh‎

Lines changed: 0 additions & 24 deletions
This file was deleted.

‎.github/scripts/http-e2e.sh‎

Lines changed: 85 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
#!/usr/bin/env bash
2-
# Runs one end-to-end suite group over real HTTP, each against its own `next dev` app.
2+
# Runs one end-to-end suite group over real HTTP, each against its own app: `next dev`, except
3+
# mobile, which serves the production build the `e2e-build` job uploads with `next start` (the
4+
# job extracts it into apps/sim/.next first).
35
#
46
# Usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|mobile> (run from apps/sim)
57
#
68
# The job provides DATABASE_URL, BETTER_AUTH_SECRET and ENCRYPTION_KEY; each group sets the rest of
79
# its app's environment here. Reports and server logs land in $RUNNER_TEMP/e2e.
810
#
9-
# The first request cold-compiles the app under Turbopack, which takes 42-150s on CI runners, so
10-
# the readiness deadline only has to catch a hung boot: an exited server fails immediately, and
11-
# either way the server log tail lands in the job log.
11+
# Under `next dev` the first request cold-compiles the app under Turbopack, which takes 42-150s on
12+
# CI runners, so the readiness deadline only has to catch a hung boot: an exited server fails
13+
# immediately, and either way the server log tail lands in the job log.
1214
#
1315
# Each app starts from an empty Turbopack dev cache: a cache written under other NEXT_PUBLIC_*
1416
# values, by a server that `next dev` SIGKILLs 100ms after SIGTERM, can panic Turbopack or wedge a
@@ -26,19 +28,43 @@ server_pid=''
2628
app_tag=''
2729
server_log=''
2830
status_log=''
31+
realtime_pid=''
32+
realtime_tag=''
33+
realtime_log=''
34+
browsers_pid=''
35+
# `dev` or `start`; a group sets it before start_app.
36+
app_server=dev
2937

3038
finish() {
3139
local status=$?
3240
if [ -n "$server_pid" ]; then
3341
bash "$GITHUB_WORKSPACE/.github/scripts/stop-session.sh" "$server_pid" "$app_tag" || status=1
3442
wait "$server_pid" 2>/dev/null || true
35-
if [ -n "$status_log" ]; then
43+
if [ -n "$status_log" ] && [ "$app_server" = start ]; then
44+
# `next start` logs no requests; RouteHandler's closing line for each API request carries
45+
# its status (an unhandled error's line is a 500 without one) and duration.
46+
jq -Rr 'fromjson? | objects | select(.module == "RouteHandler" and .duration != null
47+
and ((.path // "") | startswith("/api/")))
48+
| "\(.method) \(.path) \(.status // 500) in \(.duration)ms"' \
49+
"$server_log" > "$status_log"
50+
elif [ -n "$status_log" ]; then
3651
awk '/^ (GET|POST|PUT|PATCH|DELETE|HEAD) \/api\// { print }' "$server_log" > "$status_log"
3752
fi
3853
if [ "$status" -ne 0 ]; then
3954
tail -n 200 "$server_log"
4055
fi
4156
fi
57+
if [ -n "$realtime_pid" ]; then
58+
bash "$GITHUB_WORKSPACE/.github/scripts/stop-session.sh" "$realtime_pid" "$realtime_tag" || status=1
59+
wait "$realtime_pid" 2>/dev/null || true
60+
if [ "$status" -ne 0 ]; then
61+
tail -n 100 "$realtime_log"
62+
fi
63+
fi
64+
if [ -n "$browsers_pid" ]; then
65+
kill -- "-$browsers_pid" 2>/dev/null || true
66+
wait "$browsers_pid" 2>/dev/null || true
67+
fi
4268
exit "$status"
4369
}
4470
trap finish EXIT
@@ -54,24 +80,54 @@ start_app() {
5480
export NEXT_PUBLIC_APP_URL="http://127.0.0.1:$port"
5581
export BETTER_AUTH_URL="$NEXT_PUBLIC_APP_URL"
5682
export DISABLE_TELEMETRY=true NEXT_TELEMETRY_DISABLED=1
57-
rm -rf .next/dev
58-
E2E_APP="$app_tag" setsid node ../../node_modules/next/dist/bin/next dev --hostname 127.0.0.1 \
59-
--port "$port" > "$server_log" 2>&1 &
83+
if [ "$app_server" = start ]; then
84+
if [ ! -f .next/BUILD_ID ]; then
85+
echo "::error::No production build in apps/sim/.next for next start."
86+
exit 1
87+
fi
88+
# Production logs only errors by default; INFO writes the RouteHandler lines the status log
89+
# reads.
90+
E2E_APP="$app_tag" LOG_LEVEL=INFO setsid node ../../node_modules/next/dist/bin/next start \
91+
--hostname 127.0.0.1 --port "$port" > "$server_log" 2>&1 &
92+
else
93+
rm -rf .next/dev
94+
E2E_APP="$app_tag" setsid node ../../node_modules/next/dist/bin/next dev --hostname 127.0.0.1 \
95+
--port "$port" > "$server_log" 2>&1 &
96+
fi
6097
server_pid=$!
98+
wait_ready "$server_pid" "$NEXT_PUBLIC_APP_URL/api/health" "$label app"
99+
}
100+
101+
# start_realtime <name> <port>: the Socket.IO server the app's browser clients connect to, so pages
102+
# render connected rather than under a "Reconnecting..." notification. Start after start_app, which
103+
# exports the app URL the server allows as an origin; NEXT_PUBLIC_SOCKET_URL must already be set
104+
# when the app starts.
105+
start_realtime() {
106+
local name=$1 port=$2
107+
realtime_tag="$name-realtime-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$$"
108+
realtime_log="$report_dir/$name-realtime.log"
109+
(cd ../realtime && PORT="$port" E2E_APP="$realtime_tag" exec setsid bun run start \
110+
> "$realtime_log" 2>&1) &
111+
realtime_pid=$!
112+
wait_ready "$realtime_pid" "http://127.0.0.1:$port/health" "$name realtime server"
113+
}
61114

115+
# wait_ready <pid> <health-url> <label>
116+
wait_ready() {
117+
local pid=$1 url=$2 label=$3
62118
local started=$SECONDS
63-
until curl --fail --silent --max-time 10 "$NEXT_PUBLIC_APP_URL/api/health" > /dev/null; do
64-
if ! kill -0 "$server_pid" 2>/dev/null; then
65-
echo "::error::Local $label app exited during startup."
119+
until curl --fail --silent --max-time 10 "$url" > /dev/null; do
120+
if ! kill -0 "$pid" 2>/dev/null; then
121+
echo "::error::Local $label exited during startup."
66122
exit 1
67123
fi
68124
if [ $((SECONDS - started)) -ge "$ready_timeout_seconds" ]; then
69-
echo "::error::Local $label app did not become ready within $ready_timeout_seconds seconds."
125+
echo "::error::Local $label did not become ready within $ready_timeout_seconds seconds."
70126
exit 1
71127
fi
72128
sleep 2
73129
done
74-
echo "Local $label app ready after $((SECONDS - started))s"
130+
echo "Local $label ready after $((SECONDS - started))s"
75131
}
76132

77133
case "$group" in
@@ -155,12 +211,26 @@ case "$group" in
155211
bun run test:desktop-inbox:e2e
156212
;;
157213

214+
# MOBILE_E2E_BROWSER (chromium or webkit) narrows the job to one browser; unset runs both.
158215
mobile)
216+
app_server=start
159217
export NEXT_PUBLIC_FORCE_HOSTED=false
160218
export INTERNAL_API_SECRET=mobile-http-ci-local-secret-at-least-32-characters
161-
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--max-old-space-size=12288"
162-
bunx --no-install playwright install --with-deps chromium webkit
219+
export NEXT_PUBLIC_SOCKET_URL=http://127.0.0.1:3025 SOCKET_SERVER_URL=http://127.0.0.1:3025
220+
read -ra browsers <<< "${MOBILE_E2E_BROWSER:-chromium webkit}"
221+
# The browsers install while the app boots, in their own process group so finish() can stop them.
222+
browsers_log="$report_dir/mobile-playwright-install.log"
223+
setsid --wait bunx --no-install playwright install --with-deps "${browsers[@]}" > "$browsers_log" 2>&1 &
224+
browsers_pid=$!
163225
start_app mobile 3024 'mobile browser' record-http-status
226+
start_realtime mobile 3025
227+
if ! wait "$browsers_pid"; then
228+
browsers_pid=''
229+
tail -n 100 "$browsers_log"
230+
echo "::error::Playwright browser install failed."
231+
exit 1
232+
fi
233+
browsers_pid=''
164234
MOBILE_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
165235
MOBILE_E2E_DATABASE_URL="$DATABASE_URL" \
166236
MOBILE_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \

0 commit comments

Comments
 (0)