-
Notifications
You must be signed in to change notification settings - Fork 0
chore: add run-emulate skill with curl smoke driver #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| --- | ||
| name: run-emulate | ||
| description: >- | ||
| Build, run, smoke-test, and drive the emulate CLI — local HTTP emulators for | ||
| Kakao, Naver, Toss Payments, Firebase, Supabase, Asana, and Linear. Use when | ||
| asked to run/start the emulators, verify a change against the live servers, | ||
| exercise an OAuth/payment flow with curl, or run the smoke driver. | ||
| --- | ||
|
|
||
| # Run emulate | ||
|
|
||
| `@pleaseai/emulate` (packages/emulate) is a CLI that starts one HTTP server | ||
| per emulated service on sequential ports. It is driven with `curl` — there is | ||
| no GUI. The committed driver `.claude/skills/run-emulate/smoke.sh` builds, | ||
| launches everything seeded, exercises one real flow per service, and exits | ||
| non-zero on failure. | ||
|
|
||
| All paths below are relative to the repo root. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - bun (pinned via mise: `mise install`; any recent bun also works) | ||
| - No OS packages needed — servers are plain `Bun.serve`-style HTTP. | ||
|
|
||
| ## Build | ||
|
|
||
| ```bash | ||
| bun install | ||
| bun run build # turbo run build → packages/emulate/dist/index.js (the CLI) | ||
| ``` | ||
|
|
||
| ## Run (agent path) — the smoke driver | ||
|
|
||
| ```bash | ||
| .claude/skills/run-emulate/smoke.sh # ports 4300-4306 | ||
| .claude/skills/run-emulate/smoke.sh 5300 # alternate base port | ||
| ``` | ||
|
|
||
| It builds, runs `emulate init` in a temp dir, starts all 7 services with that | ||
| seed, then verifies: Kakao OAuth (authorize → token → `/v2/user/me`), Naver | ||
| OAuth (authorize → token → `/v1/nid/me`), Toss payment create → confirm | ||
| (`"status":"DONE"`), Firebase `accounts:signUp`, Supabase PostgREST select, | ||
| Asana workspaces, Linear GraphQL teams, and the programmatic | ||
| `createEmulator()` API. Prints `passed: 10 failed: 0` on success; on failure | ||
| it keeps the temp workdir and prints the `server.log` path for debugging. | ||
|
|
||
| ### Driving a running server manually | ||
|
|
||
| ```bash | ||
| # start seeded in some scratch dir (foreground; Ctrl+C to stop) | ||
| bun packages/emulate/dist/index.js init # writes emulate.config.yaml | ||
| bun packages/emulate/dist/index.js start --port 4300 --seed emulate.config.yaml | ||
|
|
||
| # example: full Kakao OAuth flow against port 4300 | ||
| LOC=$(curl -s -o /dev/null -w '%{redirect_url}' "http://localhost:4300/oauth/authorize?client_id=kakao_rest_api_key_example&redirect_uri=http://localhost:3000/api/auth/callback/kakao&response_type=code&user_id=1001") | ||
| CODE=${LOC##*code=} | ||
| curl -s -X POST http://localhost:4300/oauth/token \ | ||
| -d "grant_type=authorization_code&client_id=kakao_rest_api_key_example&client_secret=kakao_client_secret_example&redirect_uri=http://localhost:3000/api/auth/callback/kakao&code=$CODE" | ||
| ``` | ||
|
|
||
| Seeded credentials from `emulate init` (see the generated yaml for all): | ||
| Kakao `kakao_rest_api_key_example`/`kakao_client_secret_example`, Naver | ||
| `naver_client_id_example`/`naver_client_secret_example` (authorize requires | ||
| `state=`; seeded users get auto-generated ids `naver_user_001`, `_002`, … | ||
| usable as the `?user=` auto-approve param), Toss secret | ||
| key `test_sk_example` (Basic auth, `key:` base64), Firebase API key | ||
| `firebase_api_key_example`, Supabase `supabase_anon_key_example`, Asana | ||
| bearer `test_token_admin` (top-level `tokens:` seed), Linear `lin_api_test`. | ||
|
|
||
| ## Direct invocation (most PRs need only this) | ||
|
|
||
| Each service package is a library; `bun test` in it runs against in-process | ||
| servers. To poke internals without the CLI, import the source directly — | ||
| bun runs the TS as-is, but the script must live **inside the repo** (module | ||
| resolution; see Gotchas): | ||
|
|
||
| ```bash | ||
| cat > prog.smoke.ts <<'EOF' # at repo root — must be inside the repo | ||
| import { createEmulator } from './packages/emulate/src/api.ts' | ||
| const e = await createEmulator({ service: 'supabase', port: 4444, | ||
| seed: { supabase: { anon_key: 'k', tables: { todos: [{ id: 1, title: 'x', completed: false }] } } } }) | ||
| console.log(await (await fetch(`${e.url}/rest/v1/todos?select=*`, { headers: { apikey: 'k' } })).json()) | ||
| await e.close() | ||
| EOF | ||
| bun prog.smoke.ts && rm prog.smoke.ts | ||
| ``` | ||
|
|
||
| ## Test | ||
|
|
||
| ```bash | ||
| bun run test # turbo run test — all packages | ||
| (cd packages/kakao && bun test) # one package | ||
| ``` | ||
|
|
||
| ## Gotchas | ||
|
|
||
| - **Unseeded server = 401 everywhere.** `emulate start` with no `--seed` | ||
| boots fine but has zero app keys: Kakao authorize returns 401 KOE101 as an | ||
| HTML error page. Always `emulate init` + `--seed emulate.config.yaml`. | ||
| - **Kakao token exchange requires `client_secret`** when the seeded app | ||
| defines one (init's does) — omitting it gives `KOE010 client_secret does | ||
| not match`, even though the README's minimal example skips it. | ||
| - **Linear auth uses the seeded `api_keys` value** (`lin_api_test`), sent as | ||
| `Authorization: lin_api_test` or `Authorization: Bearer lin_api_test` — | ||
| both verified. Wrong key → GraphQL `AUTHENTICATION_ERROR`, not HTTP 401. | ||
| - **Workspace imports only resolve inside the repo.** Root `node_modules/ | ||
| @pleaseai/` does not link the workspace packages; a script outside the repo | ||
| cannot `import '@pleaseai/emulate'` (Cannot find module). Run scripts from | ||
| within the repo and/or import `packages/emulate/src/api.ts` by path. | ||
| - **`GET /` returns 404 on every service** — that's the liveness signal the | ||
| driver uses; don't read it as "server broken". | ||
| - In zsh, `echo ===` fails with `== not found` (zsh `=`-expansion) — quote | ||
| separators when curling interactively from zsh. | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| - `error: Cannot find module '@pleaseai/emulate'` → script is outside the | ||
| repo; move it inside or import by relative path (Gotchas above). | ||
| - `{"error":"invalid_client"...KOE010}` → add | ||
| `client_secret=kakao_client_secret_example` to the token POST. | ||
| - 401 + HTML titled `KOE101 | emulate` → server started without `--seed`. | ||
| - Port already in use → a previous run is still alive: | ||
| `pkill -f 'emulate/dist/index.js'`, or pass a different base port. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| #!/usr/bin/env bash | ||
| # Smoke driver for the emulate CLI: builds, launches all service emulators | ||
| # seeded with `emulate init` data, exercises one real flow per service over | ||
| # HTTP, checks the programmatic createEmulator() API, then shuts down. | ||
| # | ||
| # Usage: .claude/skills/run-emulate/smoke.sh [BASE_PORT] | ||
| # Exit 0 = all checks passed. Server log: $WORKDIR/server.log (path printed). | ||
| set -u | ||
|
|
||
| ROOT=$(cd "$(dirname "$0")/../../.." && pwd) | ||
| BASE_PORT=${1:-4300} | ||
| KAKAO=$BASE_PORT NAVER=$((BASE_PORT+1)) TOSS=$((BASE_PORT+2)) | ||
|
amondnet marked this conversation as resolved.
|
||
| FIREBASE=$((BASE_PORT+3)) SUPABASE=$((BASE_PORT+4)) ASANA=$((BASE_PORT+5)) LINEAR=$((BASE_PORT+6)) | ||
| WORKDIR=$(mktemp -d "${TMPDIR:-/tmp}/emulate-smoke.XXXXXX") | ||
| PASS=0 FAIL=0 | ||
| CLI="$ROOT/packages/emulate/dist/index.js" | ||
|
|
||
| check() { # check <name> <expected-substring> <actual> | ||
| if [[ "$3" == *"$2"* ]]; then | ||
| echo " ok $1"; PASS=$((PASS+1)) | ||
| else | ||
| echo " FAIL $1 — expected substring '$2', got: ${3:0:200}"; FAIL=$((FAIL+1)) | ||
| fi | ||
| } | ||
|
|
||
| cleanup() { | ||
| [[ -n "${SERVER_PID:-}" ]] && kill "$SERVER_PID" 2>/dev/null | ||
| # Keep the workdir (seed + server.log) only when something failed, for debugging. | ||
| [[ $FAIL == 0 && -d "$WORKDIR" ]] && rm -rf "$WORKDIR" | ||
| } | ||
| trap cleanup EXIT | ||
|
|
||
| echo "== build" | ||
| (cd "$ROOT" && bun run build >/dev/null) || { echo "build failed"; exit 1; } | ||
|
amondnet marked this conversation as resolved.
|
||
|
|
||
| echo "== seed + start (ports $BASE_PORT-$LINEAR, workdir $WORKDIR)" | ||
| (cd "$WORKDIR" && bun "$CLI" init >/dev/null) || { echo "emulate init failed"; exit 1; } | ||
| # exec so $! is the bun process itself, not a wrapper subshell — otherwise | ||
| # cleanup kills the subshell and leaks the server, which keeps the port and | ||
| # stale state (e.g. EMAIL_EXISTS on the firebase check) for the next run. | ||
| (cd "$WORKDIR" && exec bun "$CLI" start --port "$BASE_PORT" --seed emulate.config.yaml > server.log 2>&1) & | ||
| SERVER_PID=$! | ||
|
|
||
| for i in $(seq 1 30); do | ||
| curl -s -m1 -o /dev/null "http://localhost:$LINEAR/" && break | ||
| sleep 0.5 | ||
| [[ $i == 30 ]] && { echo "server did not come up"; cat "$WORKDIR/server.log"; exit 1; } | ||
| done | ||
|
amondnet marked this conversation as resolved.
|
||
|
|
||
| echo "== kakao oauth (authorize -> token -> /v2/user/me)" | ||
| LOC=$(curl -s -m3 -o /dev/null -w '%{redirect_url}' "http://localhost:$KAKAO/oauth/authorize?client_id=kakao_rest_api_key_example&redirect_uri=http://localhost:3000/api/auth/callback/kakao&response_type=code&user_id=1001") | ||
| CODE=${LOC##*code=} | ||
| TOK=$(curl -s -m3 -X POST "http://localhost:$KAKAO/oauth/token" \ | ||
| -d "grant_type=authorization_code&client_id=kakao_rest_api_key_example&client_secret=kakao_client_secret_example&redirect_uri=http://localhost:3000/api/auth/callback/kakao&code=$CODE") | ||
| check "kakao token" '"access_token"' "$TOK" | ||
| AT=$(echo "$TOK" | sed -E 's/.*"access_token":"([^"]+)".*/\1/') | ||
| ME=$(curl -s -m3 "http://localhost:$KAKAO/v2/user/me" -H "Authorization: Bearer $AT") | ||
| check "kakao /v2/user/me" '"email":"hong@example.com"' "$ME" | ||
|
|
||
| echo "== naver oauth (authorize -> token -> /v1/nid/me)" | ||
| NLOC=$(curl -s -m3 -o /dev/null -w '%{redirect_url}' "http://localhost:$NAVER/oauth2.0/authorize?client_id=naver_client_id_example&redirect_uri=http://localhost:3000/api/auth/callback/naver&response_type=code&state=smoke&user=naver_user_001") | ||
| NCODE=$(echo "$NLOC" | sed -E 's/.*[?&]code=([^&]+).*/\1/') | ||
| NTOK=$(curl -s -m3 "http://localhost:$NAVER/oauth2.0/token?grant_type=authorization_code&client_id=naver_client_id_example&client_secret=naver_client_secret_example&code=$NCODE&state=smoke") | ||
| check "naver token" '"access_token"' "$NTOK" | ||
| NAT=$(echo "$NTOK" | sed -E 's/.*"access_token":"([^"]+)".*/\1/') | ||
| NME=$(curl -s -m3 "http://localhost:$NAVER/v1/nid/me" -H "Authorization: Bearer $NAT") | ||
| check "naver /v1/nid/me" '"email":"hong@example.com"' "$NME" | ||
|
|
||
| echo "== tosspayments (create -> confirm)" | ||
| P=$(curl -s -m3 -X POST "http://localhost:$TOSS/internal/payments" -H 'content-type: application/json' \ | ||
| -d '{"orderId":"smoke-order","orderName":"Smoke order","amount":11000}') | ||
| PK=$(echo "$P" | sed -E 's/.*"paymentKey":"([^"]+)".*/\1/') | ||
| CONF=$(curl -s -m3 -X POST "http://localhost:$TOSS/v1/payments/confirm" \ | ||
| -H "Authorization: Basic $(printf 'test_sk_example:' | base64)" -H 'content-type: application/json' \ | ||
| -d "{\"paymentKey\":\"$PK\",\"orderId\":\"smoke-order\",\"amount\":11000}") | ||
| check "toss confirm" '"status":"DONE"' "$CONF" | ||
|
|
||
| echo "== firebase (accounts:signUp)" | ||
| FB=$(curl -s -m3 -X POST "http://localhost:$FIREBASE/v1/accounts:signUp?key=firebase_api_key_example" \ | ||
| -H 'content-type: application/json' -d '{"email":"smoke@example.com","password":"secret12","returnSecureToken":true}') | ||
| check "firebase signUp" '"idToken"' "$FB" | ||
|
|
||
| echo "== supabase (PostgREST select)" | ||
| SB=$(curl -s -m3 "http://localhost:$SUPABASE/rest/v1/todos?select=*" -H "apikey: supabase_anon_key_example") | ||
| check "supabase todos" '"title"' "$SB" | ||
|
|
||
| echo "== asana (workspaces)" | ||
| AS=$(curl -s -m3 "http://localhost:$ASANA/api/1.0/workspaces" -H 'Authorization: Bearer test_token_admin') | ||
| check "asana workspaces" '"resource_type":"workspace"' "$AS" | ||
|
|
||
| echo "== linear (GraphQL teams)" | ||
| LN=$(curl -s -m3 -X POST "http://localhost:$LINEAR/graphql" -H 'content-type: application/json' \ | ||
| -H 'Authorization: lin_api_test' -d '{"query":"{ teams { nodes { key name } } }"}') | ||
| check "linear teams" '"key":"ENG"' "$LN" | ||
|
|
||
| echo "== programmatic createEmulator()" | ||
| cat > "$WORKDIR/prog.ts" <<EOF | ||
| import { createEmulator } from '$ROOT/packages/emulate/src/api.ts' | ||
| const e = await createEmulator({ | ||
| service: 'supabase', | ||
| port: $((BASE_PORT + 100)), | ||
| seed: { supabase: { anon_key: 'k', tables: { todos: [{ id: 1, title: 'prog', completed: false }] } } }, | ||
| }) | ||
| const r = await fetch(\`\${e.url}/rest/v1/todos?select=*\`, { headers: { apikey: 'k' } }) | ||
| console.log(JSON.stringify(await r.json())) | ||
| await e.close() | ||
| EOF | ||
| PROG=$(bun "$WORKDIR/prog.ts" 2>&1) | ||
| check "createEmulator supabase" '"title":"prog"' "$PROG" | ||
|
|
||
| echo | ||
| if [[ $FAIL == 0 ]]; then | ||
| echo "passed: $PASS failed: 0" | ||
| else | ||
| echo "passed: $PASS failed: $FAIL (workdir kept for debugging: $WORKDIR/server.log)" | ||
| fi | ||
| [[ $FAIL == 0 ]] | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.