Skip to content

Latest commit

 

History

History
178 lines (133 loc) · 8.41 KB

File metadata and controls

178 lines (133 loc) · 8.41 KB

prem-socket-test-bot

Minimal Azure SignalR socket-mode test client for a local APX (bot API service) instance. No Teams app and no inbound HTTP endpoint — it only connects out to Azure SignalR and exercises the full APX bot socket-mode flow, driven by directives in the activity payload.

It exercises both legs of socket mode:

  • Invoke round-trip — APX dispatches an invoke; the bot replies with a real Bot Framework invokeResponse over the socket (APX routes it back through its Redis backplane).
  • One-way activity + Ack — the socket equivalent of a classic bot's onMessageActivity: APX delivers a message/typing activity and the bot returns a delivery Ack.

What it does

  1. POST /v3/websockets/connect on the local APX BotNotifications role -> { url, accessToken, sessionId, expiresIn }.
  2. Opens a SignalR WebSocket to the negotiated Azure SignalR url and joins the bot's group bot_<botKey>.
  3. Receives APX->bot frames on the activity client method (a SocketActivityEnvelope).
  4. For invokes (type:"invoke"), computes the real Bot Framework response for the invoke name (src/invokeResponses.ts) and replies with connection.send("invokeResponse", frame) — correlation key is envelopeId.
  5. For ack-required one-way activities, runs its onMessageActivity-equivalent handler and sends connection.send("Ack", …).
  6. The connection is resilient (re-negotiates through APX on restart / token expiry) and single-instance (a PID lockfile kills any prior copy on start, so orphans can't pile up).

Prerequisites

  • Node 18+ (uses global fetch).
  • A local APX built in DEBUG with the BotNotifications role running (serves negotiate, the upstream webhook, and the DEBUG test endpoints).
  • An Azure SignalR resource in Serverless mode, with its Upstream endpoint pointed at your APX (via a tunnel). See the full E2E setup guide linked at the bottom.
  • (Optional) a local Redis to exercise APX's reply backplane — ./start-local-redis.ps1.

Quick start

Option A — F5 (Teams Toolkit, recommended; self-provisions an Entra app)

Open the folder in VS Code and run "Run socket bot (local)" (F5). The .vscode task chain:

  1. Validate prerequisites — Node + Microsoft 365 sign-in.
  2. Provision (teamsapp provision --env local) — aadApp/create: when BOT_ID in env/.env.local is empty, mints a new Microsoft Entra app (its client id becomes the socket botKey); when set, reuses it. Writes BOT_ID/secret/tenant back to the env.
  3. Deploy (teamsapp deploy --env local) — writes the runtime env to .localConfigs.
  4. Startnpm run dev:teamsfx launches the bot under nodemon; it negotiates with APX, connects to Azure SignalR, and is ready on the socket.

There is no manifest, app package, or messaging endpoint — only the bot identity is provisioned.

Option B — manual

npm install
cp .env.example .env            # set BOT_KEY (= bot MSA AppId) and APX_BASE_URL
# local APX uses a dev cert. PowerShell:  $env:NODE_TLS_REJECT_UNAUTHORIZED = "0"
npm run dev                     # nodemon + ts-node (auto-restart on save)
# or one-shot:  npm run build && npm start

Expected output:

prem-socket-test-bot starting. apx=https://localhost:444 botKey=<appId> connections=1 ...
[conn1] connected. sessionId=... expiresIn=3600s

and in the APX log: WebSocket session allocated. botKey=<appId> sessionId=....


Configuration

.env (manual path) / env/.env.local (F5 path) / .localConfigs (generated by F5):

var meaning
APX_BASE_URL local APX BotNotifications role, e.g. https://localhost:444 (negotiate + invoke + upstream all live there)
BOT_ID / BOT_KEY the bot's MSA AppId = APX's socket key (group bot_<key>), echoed on replies. BOT_ID (F5) takes precedence over BOT_KEY.
CONNECTIONS parallel sockets for one bot (>1 exercises group fan-out duplicate delivery + dedup)
DEFAULT_DIRECTIVE behavior when an invoke carries no value.directive
RENEGOTIATE_FRACTION re-negotiate at this fraction of the token lifetime (default 0.8)
NODE_TLS_REJECT_UNAUTHORIZED=0 accept the local APX dev cert

Secrets are never committed. .env, .localConfigs, env/.env.local, and env/.env.local.user (the provisioned bot id + password) are gitignored. Only the .env.example / env/.env.local.sample templates are tracked.


Local Redis (optional, for the reply backplane)

APX routes invoke replies through Redis when ForceRedisReplyPath is on. To run a local Redis so that path is genuinely exercised:

./start-local-redis.ps1          # downloads a portable Windows Redis on first run; starts it on 6379
# stop later:  Get-Process redis-server | Stop-Process

APX picks it up automatically via its LocalRedisConnectionString (defaults to localhost:6379).


Driving scenarios

Per-activity behavior comes from payload.value.directive. The easiest way to fire scenarios is APX's DEBUG-only test endpoints (no auth, dispatch straight to the bot's group). With BOTKEY=<your bot MSA AppId> and BASE=https://localhost:444:

# Invoke round-trip (name = real Bot Framework invoke type; directive controls transport)
curl -sk -X POST "$BASE/v3/websockets/testinvoke?botKey=$BOTKEY&directive=ok&name=composeExtension/query&timeoutSeconds=25" --data ""
curl -sk -X POST "$BASE/v3/websockets/testinvoke?botKey=$BOTKEY&directive=dupe&name=task/fetch&timeoutSeconds=25"  --data ""
curl -sk -X POST "$BASE/v3/websockets/testinvoke?botKey=$BOTKEY&directive=drop&name=task/fetch&timeoutSeconds=6"   --data ""

# One-way activity + Ack (onMessageActivity equivalent)
curl -sk -X POST "$BASE/v3/websockets/testactivity?botKey=$BOTKEY&type=message&directive=ok&text=hi&timeoutSeconds=10"   --data ""
curl -sk -X POST "$BASE/v3/websockets/testactivity?botKey=$BOTKEY&type=message&directive=error&timeoutSeconds=6" --data ""

Invoke directives:

directive bot behavior exercises
ok reply 200 + the real BF response for name happy-path round-trip
error reply 500 + error body bot-error status passthrough
dupe reply twice APX first-reply-wins dedup
delay (delayMs, default 27000) sleep then reply APX deadline -> HTTP fallback
drop no reply timeout -> HTTP fallback

Activity (onMessageActivity) directives:

directive bot behavior result
ok handler succeeds -> send Ack delivery confirmed (Success)
error / drop handler "throws" / dropped -> no Ack APX Timeout (received, not acknowledged)
delay Ack after delayMs Success if within the deadline

Recognized invoke names (real Bot Framework shapes in src/invokeResponses.ts): composeExtension/query, composeExtension/submitAction, task/fetch, task/submit, adaptiveCard/action, signin/verifyState, signin/tokenExchange, message/submitAction.


Logs

The bot logs the full flow so you can trace it against the APX console:

[recv] <-- APX  envelopeId=... type=invoke ...
[recv]   full activity payload={"name":"composeExtension/query","type":"invoke",...}
[invoke] HANDLE name=... directive=ok -> computed status=200 body=...
[reply] --> APX  invokeResponse envelopeId=... status=200 body=...
[msg]  onMessageActivity (socket) type=message directive=ok ...      # one-way activities
[ack]  --> APX  Ack envelopeId=... (sending delivery confirmation)

Notes

  • Single instance: each start SIGTERMs any prior bot process (PID lockfile in %TEMP%), so orphaned node processes can't accumulate and double-ack. The --inspect=9239 address already in use warning at startup is harmless (the inspector just doesn't attach).
  • Resilient: the socket survives APX restarts — it re-negotiates and rejoins automatically.
  • Wire parity: APX serializes the activity so the bot receives it byte-identical to the HTTP send-to-bot form (camelCase, nulls stripped); the bot's full activity payload log line should match APX's Activity PARITY wire payload line exactly.
  • Negotiate uses the DEBUG ?botKey= override; for an authenticated run, add a Bot Framework JWT Authorization: Bearer header in the negotiate call.

For the complete local end-to-end walkthrough (Azure SignalR + Upstream + tunnel + APX config + Redis verification), see the Bot Socket Mode - Local E2E Setup guide.