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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions .claude/skills/run-emulate/SKILL.md
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.
117 changes: 117 additions & 0 deletions .claude/skills/run-emulate/smoke.sh
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
Comment thread
amondnet marked this conversation as resolved.

ROOT=$(cd "$(dirname "$0")/../../.." && pwd)
BASE_PORT=${1:-4300}
KAKAO=$BASE_PORT NAVER=$((BASE_PORT+1)) TOSS=$((BASE_PORT+2))
Comment thread
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; }
Comment thread
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
Comment thread
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 ]]