Recipe: smoke-test GitHub inbox sync through studio MCP
Use this when you need to prove the external-intake path without touching the network or a real GitHub account. The smoke starts a real studio MCP server over stdio, opens a driving session, syncs fake GitHub issue/PR rows into that session's inbox, checks the global work queue, teleports to the selected notification, and renders the reacquired TUI frame.
The fake gh script lives under .artifacts/ so it is disposable and never committed:
mkdir -p .artifacts/mcp-test/fake-bin
cat > .artifacts/mcp-test/fake-bin/gh <<'SH'
#!/bin/sh
if [ "$1" = "--version" ]; then
echo "gh version fake"
exit 0
fi
if [ "$1" = "issue" ] && [ "$2" = "list" ]; then
cat <<'JSON'
[{"number":7,"title":"Assigned issue","url":"https://github.com/acme/repo/issues/7","assignees":[{"login":"brad"}]}]
JSON
exit 0
fi
if [ "$1" = "pr" ] && [ "$2" = "list" ]; then
cat <<'JSON'
[{"number":42,"title":"Review this","url":"https://github.com/acme/repo/pull/42","author":{"login":"alice"}}]
JSON
exit 0
fi
echo "unexpected fake gh command: $*" >&2
exit 1
SH
chmod +x .artifacts/mcp-test/fake-bin/gh
Then run the MCP smoke with that fake gh first on PATH:
PATH="$PWD/.artifacts/mcp-test/fake-bin:$PATH" \
GOCACHE="${KITSOKI_GOCACHE:-/private/tmp/kitsoki-gocache}" \
go run ./cmd/kitsoki mcp-test \
--timeout 20s \
--server-arg mcp \
--server-arg --stories-dir --server-arg ./stories \
--server-arg --db --server-arg .artifacts/mcp-test/github-inbox.db \
--calls '[
{
"tool": "session.new",
"args": {
"story_path": "testdata/apps/cloak/app.yaml",
"key": "github-inbox-smoke"
},
"expect": {
"structuredContent.state": "foyer"
}
},
{
"tool": "inbox.sync_github",
"args": {
"handle": "github-inbox-smoke",
"repo": "acme/repo",
"limit": 10,
"teleport_state": "foyer"
},
"expect": {
"structuredContent.fetched": 2,
"structuredContent.inserted": 2,
"structuredContent.skipped": 0,
"structuredContent.items.1.origin_ref": "github:acme/repo/pr/42",
"structuredContent.items.1.url": "https://github.com/acme/repo/pull/42"
},
"save": {
"pr_notification_id": "structuredContent.items.1.notification_id"
}
},
{
"tool": "studio.work",
"expect": {
"structuredContent.summary.notifications_unread": 2,
"structuredContent.summary.notifications_action_required": 2,
"structuredContent.items.0.origin_url": "https://github.com/acme/repo/issues/7",
"structuredContent.items.0.reacquire.tool": "session.teleport"
}
},
{
"tool": "session.teleport",
"args": {
"handle": "github-inbox-smoke",
"notification_id": "${pr_notification_id}"
},
"expect": {
"structuredContent.outcome.state": "foyer"
}
},
{
"tool": "session.inspect",
"args": {
"handle": "github-inbox-smoke"
},
"expect": {
"structuredContent.async.notifications_unread": 1,
"structuredContent.notifications.0.origin_url": "https://github.com/acme/repo/issues/7"
}
},
{
"tool": "render.tui",
"args": {
"handle": "github-inbox-smoke"
},
"expect": {
"structuredContent.frame.metadata.state": "foyer"
}
}
]'
Live web and TUI sessions also poll this same GitHub intake path every five minutes while a session is open. This MCP smoke calls inbox.sync_github explicitly so the proof is deterministic and does not wait on a wall-clock poll interval.
Expected proof:
inbox.sync_githubinserts one assigned issue and one review-requested PR.inbox.sync_githubreturns the PR deep link andstudio.workranks both GitHub rows as unread action-required work.session.teleportreacquires the PR notification by id.session.inspectshows one unread notification left and still exposes the PRorigin_urlfor focused context.render.tuiconfirms the operator-visible frame is reacquired.