From 925e99ee8beced312193a98a1c8e5a10b6c02a51 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Sat, 4 Jul 2026 12:03:56 +0900 Subject: [PATCH 1/2] chore: add run-emulate skill with curl smoke driver --- .claude/skills/run-emulate/SKILL.md | 119 ++++++++++++++++++++++++++++ .claude/skills/run-emulate/smoke.sh | 99 +++++++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 .claude/skills/run-emulate/SKILL.md create mode 100755 .claude/skills/run-emulate/smoke.sh diff --git a/.claude/skills/run-emulate/SKILL.md b/.claude/skills/run-emulate/SKILL.md new file mode 100644 index 0000000..5824eba --- /dev/null +++ b/.claude/skills/run-emulate/SKILL.md @@ -0,0 +1,119 @@ +--- +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`), Toss +payment create → confirm (`"status":"DONE"`), Firebase `accounts:signUp`, +Supabase PostgREST select, Asana workspaces, Linear GraphQL teams, and the +programmatic `createEmulator()` API. Prints `passed: 8 failed: 0` and the +server-log path on success. + +### 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`, 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. diff --git a/.claude/skills/run-emulate/smoke.sh b/.claude/skills/run-emulate/smoke.sh new file mode 100755 index 0000000..57ecab1 --- /dev/null +++ b/.claude/skills/run-emulate/smoke.sh @@ -0,0 +1,99 @@ +#!/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)) +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 + 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 +} +trap cleanup EXIT + +echo "== build" +(cd "$ROOT" && bun run build >/dev/null) || { echo "build failed"; exit 1; } + +echo "== seed + start (ports $BASE_PORT-$LINEAR, workdir $WORKDIR)" +(cd "$WORKDIR" && bun "$CLI" init >/dev/null) +(cd "$WORKDIR" && 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 + +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 "== 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" <&1) +check "createEmulator supabase" '"title":"prog"' "$PROG" + +echo +echo "passed: $PASS failed: $FAIL (server log: $WORKDIR/server.log)" +[[ $FAIL == 0 ]] From 50f6e3f7c56a8c2c9dc58d577cf6845904f736b6 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Sat, 4 Jul 2026 12:13:54 +0900 Subject: [PATCH 2/2] chore: apply AI code review suggestions --- .claude/skills/run-emulate/SKILL.md | 16 ++++++++++------ .claude/skills/run-emulate/smoke.sh | 24 +++++++++++++++++++++--- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/.claude/skills/run-emulate/SKILL.md b/.claude/skills/run-emulate/SKILL.md index 5824eba..f6ecbbb 100644 --- a/.claude/skills/run-emulate/SKILL.md +++ b/.claude/skills/run-emulate/SKILL.md @@ -37,11 +37,12 @@ bun run build # turbo run build → packages/emulate/dist/index.js (the C ``` 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`), Toss -payment create → confirm (`"status":"DONE"`), Firebase `accounts:signUp`, -Supabase PostgREST select, Asana workspaces, Linear GraphQL teams, and the -programmatic `createEmulator()` API. Prints `passed: 8 failed: 0` and the -server-log path on success. +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 @@ -58,7 +59,10 @@ curl -s -X POST http://localhost:4300/oauth/token \ ``` Seeded credentials from `emulate init` (see the generated yaml for all): -Kakao `kakao_rest_api_key_example`/`kakao_client_secret_example`, Toss secret +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`. diff --git a/.claude/skills/run-emulate/smoke.sh b/.claude/skills/run-emulate/smoke.sh index 57ecab1..97cf016 100755 --- a/.claude/skills/run-emulate/smoke.sh +++ b/.claude/skills/run-emulate/smoke.sh @@ -25,6 +25,8 @@ check() { # check 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 @@ -32,8 +34,11 @@ echo "== build" (cd "$ROOT" && bun run build >/dev/null) || { echo "build failed"; exit 1; } echo "== seed + start (ports $BASE_PORT-$LINEAR, workdir $WORKDIR)" -(cd "$WORKDIR" && bun "$CLI" init >/dev/null) -(cd "$WORKDIR" && bun "$CLI" start --port "$BASE_PORT" --seed emulate.config.yaml > server.log 2>&1) & +(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 @@ -52,6 +57,15 @@ 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}') @@ -95,5 +109,9 @@ PROG=$(bun "$WORKDIR/prog.ts" 2>&1) check "createEmulator supabase" '"title":"prog"' "$PROG" echo -echo "passed: $PASS failed: $FAIL (server log: $WORKDIR/server.log)" +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 ]]