collabSupport/services/voiceDiag/README.md
jmcqueen 2eb31a2ddc Add /voicediag rules-engine command with 9 per-user calling checks
Introduces a new diagnostic command that walks a registry of check
modules against a store user's Webex Calling configuration and
surfaces per-issue adaptive-card remediation for the fixable ones.

Checks (services/voiceDiag/checks/): dnd, callForwarding, callWaiting,
callIntercept, voicemail, hoteling, executiveAssistant,
outgoingPermission, phoneOnline. Remediations offered for DND,
forwarding, waiting, and intercept.

Uses the /v1/people/{id}/features/* admin surface (spark-admin:people_read
+ spark-admin:people_write scopes we already hold) — the earlier
telephony/config/people/*/callSettings/* path scheme returns 404 from
the Webex gateway and is not a live surface. Runner distinguishes
routing-404s ("URL moved") from "not applicable" 404s ("no calling
license") via the response body.

Arg parser accepts detail/detailed/--detail/--detailed and normalises
macOS smart-dashes so --detailed doesn't die when auto-correct
turns it into an em-dash.

Wires a VOICEDIAG_ACTIONS dispatcher in index.js mirroring the IGMP
branch, and registers /voicediag in commands/registry.js. 170 tests
pass (52 new: 39 check + 12 renderer + 5 arg-normalization).

Docs updated in .env.example, services/phoneService.js:467, and a new
services/voiceDiag/README.md that includes a "how to add a check"
recipe plus a note on the earlier wrong URL scheme.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 18:51:31 -04:00

130 lines
4.9 KiB
Markdown

# /voicediag
Rules-engine style diagnostic for the store's Webex Calling
per-user configuration. The orchestrator (`voiceDiagService.js`)
walks a registry of check modules, gathers their `CheckResult`
objects, and hands them to the renderer + adaptive-card layer in
`commands/voiceDiag.js`.
## Command
```
/voicediag <storeNum> default: hides OK checks, posts fixable cards
/voicediag <storeNum> detail include OK checks + expand every details block
/voicediag <storeNum> --only dnd,callForwarding
/voicediag list-checks enumerate every registered check + its scope
```
HTTP path: `GET /voicediag?storeNum=<n>[&detailed=true][&only=dnd,callWaiting]`.
HTTP callers get the markdown snapshot only — remediation cards are
chat-only.
## Prerequisites
All checks call `/v1/people/{personId}/features/{feature}` (with a
few Webex-namespace variants noted inline in each check module).
The scopes required are:
- `spark-admin:people_read` — every read (all checks)
- `spark-admin:people_write` — remediations (disable DND, clear
forwarding, enable call waiting, disable call intercept)
These are the same scopes `/webexhost` and `/offboarduser` already
use, so no operator work is needed to enable `/voicediag` on an
existing deployment. If the token ever loses those scopes,
each check self-reports as `skipped: scope missing` — the run still
completes, and the phone-online summary keeps working.
### About the URL scheme
Early drafts of this feature documented `/v1/telephony/config/people/{id}/callSettings/*`.
That path family returns 404 "no static resource" from the Webex API
gateway — it is not a live surface. The correct admin path is
`/v1/people/{id}/features/{feature}`, per Webex's User Call Settings
docs. A couple of features use different feature-name segments than
their check id — most notably `intercept` (not `callIntercept`);
see each check module for the exact endpoint. The runner
distinguishes routing-404s ("URL moved, update the check") from
"not applicable" 404s ("this person isn't a calling user") using
the `no static resource` marker in the response body.
## Adding a new check
1. Create `services/voiceDiag/checks/<newCheck>.js` and export a
descriptor:
```js
export const myNewCheck = {
id: 'myNew',
label: 'My New Check',
requires: ['personId'], // subset of ['personId','phoneStatus','telephonyProfile']
scope: 'spark-admin:people_read',
async run(ctx) {
const data = await ctx.webex.request('GET', `people/${ctx.personId}/features/whatever`);
// ...evaluate...
return {
status: 'warn', // 'ok' | 'warn' | 'error' | 'skipped'
message: 'Human sentence for the chat row.',
details: { relevant: 'facts' },
remediation: { // optional
action: 'fix_my_thing', // globally unique remediation id
title: 'Fix My Thing',
summary: 'One-line description of what the fix will do.',
payload: { personId: ctx.personId, personLabel: ctx.personLabel, storeNum: ctx.storeNum },
},
};
},
remediations: { // optional
async fix_my_thing(bot, data, requester) {
// do the PUT, audit, reply
},
},
};
```
2. Register it in `services/voiceDiag/checks/index.js` at the
position you want the renderer to output it.
3. Add a `describe`-level block to `tests/voiceDiag.checks.test.js`
covering the enabled / disabled / 403 branches.
That's it — no changes to the runner, renderer, command handler,
`index.js` dispatcher, or `commands/registry.js`. The single
`confirm_voicediag` / `cancel_voicediag` action namespace routes
through the shared dispatcher, which looks the remediation up in the
central registry built at module load. Remediation id collisions
throw at import so you'll notice immediately.
### CheckResult shape
```ts
type Severity = 'ok' | 'warn' | 'error' | 'skipped';
type Remediation = {
action: string; // globally unique across all checks
title: string; // button label
summary?: string; // optional card body line
payload: Record<string, unknown>; // handed to remediations[action](bot, payload, requester)
};
type CheckResult = {
id: string; // set from check.id automatically
label: string; // set from check.label automatically
status: Severity;
message: string;
details?: object | null; // shown under `detail` mode
remediation?: Remediation | null;
};
```
### Error handling
Never throw from `run()`. The runner wraps every check in a
try/catch and converts:
- HTTP 401/403/404 → `status: 'skipped'` with a "missing scope"
hint pointing at `check.scope`.
- Any other error → `status: 'error'` with the API message.
So if you _can't_ reason about a specific 5xx, just let it bubble —
the runner will surface it.