$npx -y skills add haoxiang-xu/PuPu --skill pupu-test-apiUse when running QA / regression tests against PuPu, when verifying a code change actually works in the running app, or when reading PuPu UI/state without screenshotting manually. Triggers on tasks like \"test that PuPu still creates chats correctly\", \"verify the new model sele
| 1 | # PuPu Test API — Skill |
| 2 | |
| 3 | A local HTTP REST endpoint on PuPu (dev mode only, bound to `127.0.0.1`) that lets you drive PuPu like a human: create chats, switch models, send messages, read state, take screenshots. Use it to verify your code changes actually behave correctly in the running app. |
| 4 | |
| 5 | ## When to use |
| 6 | |
| 7 | - After making code changes that touch chat creation, message send, model selection, toolkits, characters, or any chat-related path — run a quick regression to confirm the happy path still works |
| 8 | - When debugging "the UI is wrong" — pull `/v1/debug/state` to see what the app thinks vs. what it shows |
| 9 | - When debugging "why didn't X happen" — pull `/v1/debug/logs` to see renderer console + main stdout (Flask logs are Phase 2) |
| 10 | - When you need to demonstrate a fix to the user with a real screenshot |
| 11 | |
| 12 | ## Pre-flight check |
| 13 | |
| 14 | Before invoking the API, confirm PuPu is running in dev mode: |
| 15 | |
| 16 | ```bash |
| 17 | ls "$HOME/Library/Application Support/pupu/test-api-port" && cat "$HOME/Library/Application Support/pupu/test-api-port" |
| 18 | ``` |
| 19 | |
| 20 | Expected: a JSON `{port, pid, started_at}`. If the file is missing, ask the user to run `npm start` in the PuPu repo. If the file exists but `pid` references a dead process, ask the user to restart PuPu. |
| 21 | |
| 22 | ## How to call |
| 23 | |
| 24 | ### Quick path: curl |
| 25 | |
| 26 | ```bash |
| 27 | PORT=$(node -e "console.log(JSON.parse(require('fs').readFileSync(process.env.HOME + '/Library/Application Support/pupu/test-api-port')).port)") |
| 28 | BASE="http://127.0.0.1:$PORT/v1" |
| 29 | |
| 30 | # Always retry 503 not_ready a couple times — it just means renderer is still initializing |
| 31 | for i in 1 2 3; do |
| 32 | S=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/debug/state") |
| 33 | [ "$S" = "200" ] && break |
| 34 | sleep 0.3 |
| 35 | done |
| 36 | ``` |
| 37 | |
| 38 | ### Better path: use the helper |
| 39 | |
| 40 | `scripts/test-api/client.mjs` does port discovery + 503 retry for you: |
| 41 | |
| 42 | ```bash |
| 43 | node -e "import('./scripts/test-api/client.mjs').then(({client}) => client.GET('/debug/state').then(s => console.log(JSON.stringify(s, null, 2))))" |
| 44 | ``` |
| 45 | |
| 46 | For a complete end-to-end smoke (create chat → send → screenshot → cleanup), run the bundled script: |
| 47 | |
| 48 | ```bash |
| 49 | node scripts/test-api/smoke.mjs |
| 50 | ``` |
| 51 | |
| 52 | ## Common recipes |
| 53 | |
| 54 | ### 1. Verify a code change didn't break message send |
| 55 | |
| 56 | ```bash |
| 57 | node -e " |
| 58 | import('./scripts/test-api/client.mjs').then(async ({client}) => { |
| 59 | const {models} = await client.GET('/catalog/models'); |
| 60 | const {chat_id} = await client.POST('/chats', {title: 'regression', model: models[0].id}); |
| 61 | const reply = await client.POST(\`/chats/\${chat_id}/messages\`, {text: 'Reply with the word OK and nothing else.'}); |
| 62 | console.log('reply:', reply.content); |
| 63 | await client.DELETE(\`/chats/\${chat_id}\`); |
| 64 | })" |
| 65 | ``` |
| 66 | |
| 67 | ### 2. Read renderer logs after a manual UI action |
| 68 | |
| 69 | ```bash |
| 70 | node -e "import('./scripts/test-api/client.mjs').then(({client}) => client.GET('/debug/logs?source=renderer&n=50').then(r => r.entries.forEach(e => console.log(\`[\${e.level}] \${e.msg}\`))))" |
| 71 | ``` |
| 72 | |
| 73 | Pass `?since=<ts_ms>` for incremental polling — keep the timestamp of the last entry you saw and ask for `since=that_ts`. |
| 74 | |
| 75 | ### 3. Snapshot UI state to debug a "why isn't this working" question |
| 76 | |
| 77 | ```bash |
| 78 | node -e "import('./scripts/test-api/client.mjs').then(({client}) => client.GET('/debug/state').then(s => console.log(JSON.stringify(s, null, 2))))" |
| 79 | ``` |
| 80 | |
| 81 | The snapshot tells you: `active_chat_id`, `current_model`, `toolkits_active`, `character_id`, `modal_open`, `is_streaming`, `route`. Compare against expected before assuming the bug is elsewhere. |
| 82 | |
| 83 | ### 4. Take a screenshot for visual confirmation |
| 84 | |
| 85 | ```bash |
| 86 | node -e "import('./scripts/test-api/client.mjs').then(async ({client}) => { const png = await client.request('GET', '/debug/screenshot'); require('fs').writeFileSync('/tmp/pupu.png', Buffer.from(png)); })" |
| 87 | ``` |
| 88 | |
| 89 | Then `Read` the file at `/tmp/pupu.png` to view it. |
| 90 | |
| 91 | ### 5. Last-mile escape hatch: arbitrary JS |
| 92 | |
| 93 | ```bash |
| 94 | node -e "import('./scripts/test-api/client.mjs').then(({client}) => client.POST('/debug/eval', {code: 'document.title', await: false}).then(r => console.log(r)))" |
| 95 | ``` |
| 96 | |
| 97 | Use only when no dedicated endpoint covers the case. Result must be JSON-serializable. |
| 98 | |
| 99 | ## Endpoint cheatsheet |
| 100 | |
| 101 | | Verb | Path | Purpose | |
| 102 | |---|---|---| |
| 103 | | POST | `/v1/chats` | Create chat (auto-activates) | |
| 104 | | GET | `/v1/chats` | List | |
| 105 | | GET | `/v1/chats/:id` | Detail with messages | |
| 106 | | POST | `/v1/chats/:id/activate` | Switch active without sending | |
| 107 | | PATCH | `/v1/chats/:id` | Rename | |
| 108 | | DELETE | `/v1/chats/:id` | Delete | |
| 109 | | POST | `/v1/chats/:id/messages` | Send (blo |