@xema/omni-protocol 0.1.112 → 0.1.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/index.d.ts +5 -2
- package/dist/testing.d.ts +13 -13
- package/dist/testing.js +198 -176
- package/dist/validation.d.ts +2 -2
- package/dist/validation.js +11 -9
- package/guide/agent.md +12 -9
- package/guide/breaks.md +1 -1
- package/guide/phone.md +1 -1
- package/guide/voice.md +8 -1
- package/guide.md +52 -50
- package/package.json +1 -1
package/dist/validation.d.ts
CHANGED
|
@@ -12,8 +12,8 @@ export declare function assertNoViolations(violations: readonly ProtocolViolatio
|
|
|
12
12
|
* rule never evaluated is a visible gap, not a pass. The harness registers one for each exercise.
|
|
13
13
|
*/
|
|
14
14
|
export declare function observeRules(listener: (rule: string) => void): () => void;
|
|
15
|
-
/** Says a rule was evaluated, for code that decides outside a collector: the stream, the
|
|
16
|
-
export declare function
|
|
15
|
+
/** Says a rule was evaluated, for code that decides outside a collector: the stream, the test. */
|
|
16
|
+
export declare function ruleTested(...rules: readonly string[]): void;
|
|
17
17
|
/** Every field is optional, so this checks what is present rather than what is missing. */
|
|
18
18
|
export declare function validateContact(contact: unknown, path?: string): ProtocolViolation[];
|
|
19
19
|
export declare function validateScheduledActivity(activity: unknown, path?: string): ProtocolViolation[];
|
package/dist/validation.js
CHANGED
|
@@ -167,8 +167,8 @@ export function observeRules(listener) {
|
|
|
167
167
|
ruleListeners.add(listener);
|
|
168
168
|
return () => { ruleListeners.delete(listener); };
|
|
169
169
|
}
|
|
170
|
-
/** Says a rule was evaluated, for code that decides outside a collector: the stream, the
|
|
171
|
-
export function
|
|
170
|
+
/** Says a rule was evaluated, for code that decides outside a collector: the stream, the test. */
|
|
171
|
+
export function ruleTested(...rules) {
|
|
172
172
|
for (const rule of rules)
|
|
173
173
|
for (const listener of ruleListeners)
|
|
174
174
|
listener(rule);
|
|
@@ -176,11 +176,11 @@ export function ruleEvaluated(...rules) {
|
|
|
176
176
|
class Collector {
|
|
177
177
|
violations = [];
|
|
178
178
|
add(rule, path, message) {
|
|
179
|
-
|
|
179
|
+
ruleTested(rule);
|
|
180
180
|
this.violations.push({ rule, path, message });
|
|
181
181
|
}
|
|
182
182
|
require(condition, rule, path, message) {
|
|
183
|
-
|
|
183
|
+
ruleTested(rule);
|
|
184
184
|
if (!condition)
|
|
185
185
|
this.violations.push({ rule, path, message });
|
|
186
186
|
return Boolean(condition);
|
|
@@ -719,7 +719,7 @@ const levelIds = (levels) => levels ?? DEFAULT_LEVEL_IDS;
|
|
|
719
719
|
* formatting hides it; anything else by its text, case aside.
|
|
720
720
|
*/
|
|
721
721
|
function validateNothingLeaksInto(task, locked, path, into) {
|
|
722
|
-
|
|
722
|
+
ruleTested("task.locked.leak");
|
|
723
723
|
const carries = (text, value) => {
|
|
724
724
|
if (typeof text !== "string")
|
|
725
725
|
return false;
|
|
@@ -1160,7 +1160,7 @@ function validateTaskInto(task, context, path, into) {
|
|
|
1160
1160
|
// that created it, and says so whatever the provisioning: the desk shows no Accept for a call
|
|
1161
1161
|
// the agent placed. The provisioning governs work the queue routes, and nothing else.
|
|
1162
1162
|
const originated = task.phase === "pending" && originatedByTheAgent(task);
|
|
1163
|
-
|
|
1163
|
+
ruleTested("task.acceptance.originated");
|
|
1164
1164
|
if (task.acceptance !== undefined) {
|
|
1165
1165
|
into.oneOf(task.acceptance, ACCEPTANCE_MODES, "task.acceptance", `${path}.acceptance`);
|
|
1166
1166
|
if (task.phase !== "pending") {
|
|
@@ -1355,7 +1355,7 @@ export function validateBreakStatus(state, tasks, path = "snapshot") {
|
|
|
1355
1355
|
// The converse: a committed break with nothing outstanding has begun. starting-after-task beside
|
|
1356
1356
|
// no task is a break waiting on work that does not exist, and an empty list reading as "still
|
|
1357
1357
|
// finishing" is the plausible nought.
|
|
1358
|
-
|
|
1358
|
+
ruleTested("break.starting-after-task.tasks");
|
|
1359
1359
|
if (isPlainObject(state) && state.status === "starting-after-task"
|
|
1360
1360
|
&& Array.isArray(tasks) && tasks.length === 0) {
|
|
1361
1361
|
into.add("break.starting-after-task.tasks", `${path}.break.status`, "a break starting after the task waits on a task, and the snapshot carries none: with nothing outstanding the break is on-break");
|
|
@@ -1619,7 +1619,7 @@ function validateBreakState(value, path, into) {
|
|
|
1619
1619
|
// A break in effect, or about to be, on a provider that publishes reasons is on one of them: an
|
|
1620
1620
|
// forced one included, since the lead's force-break command named it. A break of no kind is a break whose
|
|
1621
1621
|
// rules -- who may listen through it, whether it counts -- nobody can apply.
|
|
1622
|
-
|
|
1622
|
+
ruleTested("break.activeReasonId.required");
|
|
1623
1623
|
if (seen.size > 0 && (value.status === "on-break" || value.status === "starting-after-task")) {
|
|
1624
1624
|
into.require(typeof value.activeReasonId === "string" && value.activeReasonId.length > 0, "break.activeReasonId.required", `${path}.activeReasonId`, `a break ${describeValue(value.status)} on a provider that publishes reasons names the one it is on`);
|
|
1625
1625
|
}
|
|
@@ -2758,6 +2758,8 @@ export function validateTaskCommand(command, task, path = "command", taskContext
|
|
|
2758
2758
|
case "end-call":
|
|
2759
2759
|
offered("endCall");
|
|
2760
2760
|
interaction();
|
|
2761
|
+
// Ending my part with the caller on hold leaves them in the hold, with nobody coming back to them.
|
|
2762
|
+
into.require(task.phase !== "paused", "command.endCall.held", path, "the caller is on hold: resume before ending your part");
|
|
2761
2763
|
break;
|
|
2762
2764
|
case "terminate-call":
|
|
2763
2765
|
offered("terminateCall");
|
|
@@ -3140,7 +3142,7 @@ const filled = (v) => typeof v === "string" && v.trim().length > 0;
|
|
|
3140
3142
|
function collector() {
|
|
3141
3143
|
const violations = [];
|
|
3142
3144
|
const check = (ok, rule, path, message) => {
|
|
3143
|
-
|
|
3145
|
+
ruleTested(`recording.${rule}`);
|
|
3144
3146
|
if (!ok)
|
|
3145
3147
|
violations.push({ rule: `recording.${rule}`, path, message });
|
|
3146
3148
|
return Boolean(ok);
|
package/guide/agent.md
CHANGED
|
@@ -234,7 +234,7 @@ would otherwise find the next customer under a reused handle and act on them. A
|
|
|
234
234
|
of the task the adapter keeps, not a field held in memory beside it: it lives in the login's
|
|
235
235
|
`store` with the task, or is derived from a platform handle that itself never reuses, so a rebuilt
|
|
236
236
|
adapter comes back with the same assignment on the same task, or every late event it was minted to
|
|
237
|
-
catch mismatches and the second adapter's snapshot no longer carries the task (`
|
|
237
|
+
catch mismatches and the second adapter's snapshot no longer carries the task (`test.reload.snapshot`).
|
|
238
238
|
|
|
239
239
|
Assignment, acceptance, and progress are distinct. Acceptance follows `autoAcceptTasks` and the
|
|
240
240
|
task's `acceptance`, moving the task from `pending` to `confirmed`. The provider reports
|
|
@@ -369,7 +369,7 @@ Migration from the earlier spellings:
|
|
|
369
369
|
| command.complete.mode | `command.complete.wrapAllowance`: `complete` under either mode, refused only on `provider-automatic` with no wrap |
|
|
370
370
|
| expiresInSeconds on task-offered | `Task.expiresInSeconds`, in `pending` (`task.pending.expiresInSeconds`, `.unexpected`; `event.taskOffered.expiresInSeconds.unexpected`) |
|
|
371
371
|
| the expired outcome naming preview | gone: nothing expires a preview; withdrawn, it is `cancelled` by the provider |
|
|
372
|
-
| schedule on voice alone | `schedule` on every channel, under a manifest that declares `calendar` (`task.capability.calendar.required`), bounded by `settleMs` (`
|
|
372
|
+
| schedule on voice alone | `schedule` on every channel, under a manifest that declares `calendar` (`task.capability.calendar.required`), bounded by `settleMs` (`test.schedule.unsettled`) |
|
|
373
373
|
| a forced break requesting breaks on the other providers | `setCapacity({ count: 0 })` on every other usable provider (`assertForcedBreakStopsTheRest`) |
|
|
374
374
|
| end-forced-break on a break the platform imposed | refused: `team.command.endForcedBreak.provider`, `omni.break-forced-by-provider` |
|
|
375
375
|
| ForcedBreak.expectedDurationMs | `expectedEndsInSeconds`: seconds left, restated (`break.forced.expectedEndsInSeconds`, `.expectedDurationMs.renamed`); the command keeps `expectedDurationMs` |
|
|
@@ -383,6 +383,8 @@ Migration from the earlier spellings:
|
|
|
383
383
|
| two clients on one login | the later wins; the first is ended with `recovery: "displaced"` |
|
|
384
384
|
| the phone unspoken | `PhoneState` -- the device, its mute, its channels -- on `Snapshot.phone`, `phone-updated` and `TeamMember.phone`, from a manifest declaring `phoneStatus` (`snapshot.phone.required`, `.unexpected`, `phone.*`, `phone.channel.*`, `event.phone.capability`) |
|
|
385
385
|
| the agent's readiness for the next call read into setCapacity | the agent's own queue: `capabilities.nextCall`, `requestNextCall`/`cancelNextCall`/`releaseLinedUp`, `Snapshot.nextCall` and `linedUp`, the `next-call` and `lined-up` events (`snapshot.nextCall.idle`, `nextCall.*`, `linedUp.*`) |
|
|
386
|
+
| a harness that exercised an adapter, drove a call and evaluated rules | the harness reads as a test: `testAdapter`, `withCall`, `timeoutMs`, `notTested`, `rulesTested`, rules `test.*` (`test.timeout`, `test.offer.expired`, `test.preview.deadline`, ...) |
|
|
387
|
+
| an `end-call` while the caller was on hold | refused: a `paused` task is resumed first (`command.endCall.held`), the desk shows End call disabled on hold, the provider answers `failed`, and the test names an adapter that ends from hold (`test.command.held`); `terminate-call` is not gated, it ends the caller's channel too |
|
|
386
388
|
|
|
387
389
|
Update producers, consumers, saved task snapshots, and validation-rule assertions together.
|
|
388
390
|
History and report rule names use `history` and `historyReport`; assignment rules use
|
|
@@ -592,7 +594,7 @@ duration with a later report. A later agent mute is a new leg, never a reopening
|
|
|
592
594
|
leg, on its own clock, written when the provider takes the leg into its record, never an echo of
|
|
593
595
|
the `at` the host reported: the host keeps the answer as the leg's identity from then on and names
|
|
594
596
|
it on the closing report, and a provider that echoes the host's instant has assigned nothing
|
|
595
|
-
(`
|
|
597
|
+
(`test.recordStep.identity`). Correlate the provider-published `muted` entry using that
|
|
596
598
|
provider-selected `at` acknowledged for the current report key; a final `seconds` closes that leg
|
|
597
599
|
under the history contract.
|
|
598
600
|
This is identity matching, not reconciliation between clocks. An old closed entry, an unrelated
|
|
@@ -818,10 +820,10 @@ words are close in English and far apart on the desk: `nobody` is silence, `not-
|
|
|
818
820
|
standing notice.
|
|
819
821
|
|
|
820
822
|
One consequence for whoever builds the agent application: because a conformance run fails on `not-yet-read`, a
|
|
821
|
-
conformant adapter never shows an agent application `not-yet-read` under test, and a clean `
|
|
823
|
+
conformant adapter never shows an agent application `not-yet-read` under test, and a clean `testAdapter`
|
|
822
824
|
result says nothing about how the agent application renders it. That rendering is tested against a fixture --
|
|
823
825
|
a task published under `not-yet-read` shows the notice, the same task under `queue` shows none --
|
|
824
|
-
and never against an adapter, even in principle. `
|
|
826
|
+
and never against an adapter, even in principle. `testAdapter` treats a
|
|
825
827
|
task published under `not-yet-read` as a violation (`capabilitySource.notYetRead`), as it treats
|
|
826
828
|
a diagnostic: a conformance run against a platform that cannot say what it permits fails loudly
|
|
827
829
|
rather than passing with a note.
|
|
@@ -919,9 +921,9 @@ on the calendar.
|
|
|
919
921
|
follows within the manifest's `settleMs`, as a `task-ended` follows an applied `complete`. Past
|
|
920
922
|
the bound the agent application calls `snapshot()`: a calendar carrying an activity at that instant
|
|
921
923
|
clears the wait; one without it shows the follow-up as unsettled -- "Scheduled... the provider has
|
|
922
|
-
not confirmed" -- naming the command. The
|
|
924
|
+
not confirmed" -- naming the command. The test schedules one follow-up where the task offers it,
|
|
923
925
|
at a time it takes from the provider's own publication, and holds the provider to the same bound
|
|
924
|
-
(`
|
|
926
|
+
(`test.schedule.unsettled`).
|
|
925
927
|
|
|
926
928
|
### Custom capabilities
|
|
927
929
|
|
|
@@ -1004,7 +1006,7 @@ declared:
|
|
|
1004
1006
|
| Command | What makes it available |
|
|
1005
1007
|
| --- | --- |
|
|
1006
1008
|
| `answer`, `accept` | Nothing. A task that was offered can be accepted, or offering it meant nothing. |
|
|
1007
|
-
| `end-call` | The `endCall` capability: ends my part, the caller continues. |
|
|
1009
|
+
| `end-call` | The `endCall` capability: ends my part, the caller continues. Never from hold: a `paused` task is resumed first (`command.endCall.held`), or the caller is left in the hold. |
|
|
1008
1010
|
| `terminate-call` | The `terminateCall` capability: ends the whole call, every channel on it. |
|
|
1009
1011
|
| `conference` with `action: "remove"` | The `conference` capability, and somebody else on the call: a remove that would leave the agent alone is `end-call`, and a provider answers it `failed`. |
|
|
1010
1012
|
| `decline` | The `decline` capability on any channel, **and** Omni local policy permitting it. One word for refusing an offer, whatever the channel. |
|
|
@@ -1019,7 +1021,8 @@ declared:
|
|
|
1019
1021
|
|
|
1020
1022
|
**A control on the contact belongs to the interaction phases**, `in-progress` and `paused`:
|
|
1021
1023
|
`hold`, `resume` and `pause`, `end-call` and `terminate-call`, every `conference` action, and
|
|
1022
|
-
every `lead-assist` action; `schedule` alone reaches into `completing
|
|
1024
|
+
every `lead-assist` action; `schedule` alone reaches into `completing`, and `end-call` alone stops
|
|
1025
|
+
short of `paused` (`command.endCall.held`), since ending my part from hold leaves the caller in it. These controls act within this agent's current interaction.
|
|
1023
1026
|
In `completing`, that interaction has ended and its wrap work remains. The caller may still be in
|
|
1024
1027
|
an IVR, queue or another agent's interaction, and other channels may remain connected. Completion
|
|
1025
1028
|
of this interaction does not establish that the caller or bridge ended. The capability stays
|
package/guide/breaks.md
CHANGED
|
@@ -243,7 +243,7 @@ Capacity supersedes rather than accumulates: the latest value is the ceiling, an
|
|
|
243
243
|
ordinary as an increase. A provider whose ceiling can only rise -- one that keeps the highest count
|
|
244
244
|
it was ever told, or returns early on a small one -- cannot be told to take less work, and an agent application
|
|
245
245
|
taking capacity away is answered `applied` while the work keeps coming. The harness moves the axis
|
|
246
|
-
both ways after the
|
|
246
|
+
both ways after the test, two then one then nought, and an offer after a lower count is caught
|
|
247
247
|
against it (`stream.taskOffered.overCapacity`).
|
|
248
248
|
|
|
249
249
|
**Capacity gates what the provider assigns, not what the agent starts.** A call placed from the
|
package/guide/phone.md
CHANGED
|
@@ -62,7 +62,7 @@ report is a task fact or a capacity fact; it is what the station can do, and the
|
|
|
62
62
|
is the one that knows whether an agent without a microphone, or without a speaker, can work.
|
|
63
63
|
|
|
64
64
|
The agent application's own obligations here — asking at connect, never publishing `not-asked` when it does,
|
|
65
|
-
publishing a state and not a flicker — are Omni's tests' to hold. `
|
|
65
|
+
publishing a state and not a flicker — are Omni's tests' to hold. `testAdapter` holds the
|
|
66
66
|
other side: it validates the shape of whatever agent application a test hands the adapter, requires `audio` on
|
|
67
67
|
a softphone login and none elsewhere -- not on a desk phone, not off voice
|
|
68
68
|
(`context.host.audio.required` / `.unexpected`), holds `host.mute` to the same line (`host.mute.required`
|
package/guide/voice.md
CHANGED
|
@@ -131,7 +131,7 @@ stream that drops, a track that ends, a transport that disconnects, a microphone
|
|
|
131
131
|
endpoint re-registering change nothing about the task until the provider says so. Structurally:
|
|
132
132
|
`task-audio-started` and `task-audio-ended` alternate on a task whose work has begun, audio ends
|
|
133
133
|
only where it arrived, what follows the audio ending is `completing` or `task-ended`, and every
|
|
134
|
-
task is introduced once — `
|
|
134
|
+
task is introduced once — `testAdapter` holds the stream to that from the connect snapshot on,
|
|
135
135
|
and `assertAudioFollowsTheTask` holds any sequence.
|
|
136
136
|
|
|
137
137
|
## Connecting back during completion
|
|
@@ -258,6 +258,13 @@ provider. Two of them end the agent's part; they differ in what happens to the c
|
|
|
258
258
|
conferenced colleague, a lead who joined -- end, and the caller continues on the path the
|
|
259
259
|
provider decides: another IVR, a queue, a survey, or the end of the call. Gated by `endCall`.
|
|
260
260
|
|
|
261
|
+
**Never from hold.** An `end-call` while the task is `paused` would leave the caller in the hold
|
|
262
|
+
with nobody coming back to them, and the provider's path for a caller who continues never begins.
|
|
263
|
+
So the caller is taken off hold first: the desk holds the command back while the task is `paused`
|
|
264
|
+
(`command.endCall.held`) and shows End call disabled on hold, and a provider that receives it
|
|
265
|
+
anyway answers `failed`. Resume, then end. `terminate-call` needs no such gate: it ends the
|
|
266
|
+
caller's channel with the rest, held or not, and nobody is left waiting.
|
|
267
|
+
|
|
261
268
|
**`terminate-call` ends the whole call.** Every channel on it ends at the provider: the agent's,
|
|
262
269
|
the caller's, any colleague the agent added, and anyone else on the call. Nobody is left on the
|
|
263
270
|
line. Gated by `terminateCall`, a separate permission, since ending a customer's call is a
|
package/guide.md
CHANGED
|
@@ -1646,9 +1646,9 @@ reload is the first client dying and a second coming up for the same login, and
|
|
|
1646
1646
|
publishes the same truth about the same task as the first did, or the first was publishing
|
|
1647
1647
|
something it made up: an assignment minted for the connection, audio remembered as a boolean, a
|
|
1648
1648
|
phone the second never read. Three pieces of exactly that state were found in one adapter in an
|
|
1649
|
-
afternoon, and none of it was visible until the adapter was built twice. The
|
|
1649
|
+
afternoon, and none of it was visible until the adapter was built twice. The test's `rebuild` is the test of this
|
|
1650
1650
|
principle: given a way to build the adapter again, the run hands the login over and holds the
|
|
1651
|
-
second to what the first published (`
|
|
1651
|
+
second to what the first published (`test.reload.snapshot`, `.history`, `.openAudio`, and the
|
|
1652
1652
|
stream's own rules across the resync). An adapter that cannot be built twice against its platform
|
|
1653
1653
|
and publish the same task is not a conformant adapter yet, whatever a single run says.
|
|
1654
1654
|
|
|
@@ -2180,7 +2180,7 @@ Creates one live provider connection for the signed-in agent.
|
|
|
2180
2180
|
| `autoAcceptTasks` | Agent local policy policy relayed to the provider at login, stated by the agent application on every connection and never assumed from its absence. When `true`, a pending task states its `acceptance`; when `false`, every task requires agent acceptance. Fixed for this connection, like everything else here: the provider states or omits `acceptance` by the value it was sent, and Omni validates by that same value, not by a policy that has since moved — a change reaches the provider through a fresh `connect()`. |
|
|
2181
2181
|
| `timeZone` | The same value passed as `AuthenticationContext.timeZone`. The provider stores it on the agent and carries it on the identity. See **The agent's day**. |
|
|
2182
2182
|
| `phone` | The same value passed as `AuthenticationContext.phone`: how this login hears its calls. The type ties `host.mute` to it: a `softphone` login's agent application states what its Mute does, and a desk-phone or conversation login's agent application cannot, so the omission is a compile error rather than a live seat's discovery. See **The station is the agent application's** in `guide/phone.md`. |
|
|
2183
|
-
| `store` | The login's operational store, kept by the agent application for the life of the login, across a reload of the agent application, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the interaction legs an agent application reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store an agent application may clear aggressively. **A task's keys carry its assignment id and go with the task**: a key written about a task names the assignment id in the key, and is deleted before the task's end is published, so that nothing of a closed task survives its ending and nothing is left for whatever comes next -- a record with legs the agent application never reported against it. A login-scoped key carries no assignment id and outlives any task. The harness requires the store of every connection (`store.shape`, `store.get`, `.set`, `.delete`), watches the one it hands over, and names a task's key still held after `task-ended` (`
|
|
2183
|
+
| `store` | The login's operational store, kept by the agent application for the life of the login, across a reload of the agent application, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the interaction legs an agent application reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store an agent application may clear aggressively. **A task's keys carry its assignment id and go with the task**: a key written about a task names the assignment id in the key, and is deleted before the task's end is published, so that nothing of a closed task survives its ending and nothing is left for whatever comes next -- a record with legs the agent application never reported against it. A login-scoped key carries no assignment id and outlives any task. The harness requires the store of every connection (`store.shape`, `store.get`, `.set`, `.delete`), watches the one it hands over, and names a task's key still held after `task-ended` (`test.store.retained`) or written about the task after its end -- a persist hung off a timer that saw the task as it was (`test.store.late`). The ordinary late write is the agent application's, not the adapter's: an agent application reports a leg without waiting for the answer, so an unmute can follow `complete` by a tick, and the adapter answers a report about a task that has ended `failed` with `omni.assignment-not-found` and writes nothing -- the agent application ends its own open legs at the task's end, so such a report is the agent application's error to see, and an agent application reads every `recordStep` answer and awaits the one for the leg it closes at a task's end, the only moment a refusal is expected, since a report nobody waits for is an error nobody can see; an adapter that names no task in its keys gets no cleanup check, which is a gap rather than a pass, never an exemption: the obligation is that nothing of a closed task survives its ending, and an adapter that keeps every open task in one login-scoped value owes exactly that inside the value, where the harness cannot look. One key per task, named for it, is the shape the harness can hold, and the shape to reach for. The store lists nothing, so an adapter that needs to find its tasks keeps a login-scoped index of ids beside them; at the task's end it deletes the body first and reindexes after, since a crash between the two then leaves an index naming a task with no body, which a reader skips, where the other order leaves a body for a task that has ended, which is the hazard itself. A reader of the index tolerates an id with no body as an ending that was underway, not as corruption. |
|
|
2184
2184
|
| `host` | The agent application's report of the agent's station — devices, permissions, network — to consult before declaring the agent ready to the platform, and on every change. See **The agent application reports, the adapter decides** in `guide/phone.md`. |
|
|
2185
2185
|
| `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
|
|
2186
2186
|
| `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
|
|
@@ -2426,12 +2426,12 @@ update against the last publication, and on a snapshot from a provider with a cl
|
|
|
2426
2426
|
overruns; that is the true value, not a copy.
|
|
2427
2427
|
|
|
2428
2428
|
**A deadline stated is a deadline kept.** The provider that says how long an offer has ends it
|
|
2429
|
-
`expired` when it runs out, and the
|
|
2430
|
-
`expiresInSeconds` within the
|
|
2431
|
-
the `expired` ending within the seconds plus `settleMs`, then
|
|
2432
|
-
(`
|
|
2429
|
+
`expired` when it runs out, and the test holds it to that: where the first offer carries
|
|
2430
|
+
`expiresInSeconds` within the test's timeout, the test leaves that offer unanswered and expects
|
|
2431
|
+
the `expired` ending within the seconds plus `settleMs`, then tests the next offer
|
|
2432
|
+
(`test.offer.expired`). A preview's deadline is held to its `atDeadline` the same way: under
|
|
2433
2433
|
`provider-dials` the provider dials when it runs out, under `host-dials` and `waits` the preview
|
|
2434
|
-
stands until the desk dials (`
|
|
2434
|
+
stands until the desk dials (`test.preview.deadline`).
|
|
2435
2435
|
|
|
2436
2436
|
#### Optional periodic provider time checks and agent application estimates
|
|
2437
2437
|
|
|
@@ -2611,7 +2611,7 @@ break, which says so with `forced`: in effect at once, or `starting-after-task`
|
|
|
2611
2611
|
finishes the call they are on (`stream.breakState.commitBeforeGrant`); and a break never moves backwards —
|
|
2612
2612
|
from `on-break` or `starting-after-task` to a grant or a request, or from `granted` to
|
|
2613
2613
|
`awaiting-approval` — a new request passes through `not-requested` (`stream.breakState.backwards`).
|
|
2614
|
-
`
|
|
2614
|
+
`testAdapter` holds the stream to that from the connect snapshot on;
|
|
2615
2615
|
`assertBreakFollowsItsRequests` holds any sequence.
|
|
2616
2616
|
|
|
2617
2617
|
Adapters and hosts can call `validateBreakTransition(before, after)` from the package’s validation entry point
|
|
@@ -2675,7 +2675,7 @@ snapshots until it ends.
|
|
|
2675
2675
|
**Every offer is owed an ending.** An assignment the provider introduced is an assignment it ends,
|
|
2676
2676
|
with `task-ended` and an outcome, whatever became of the call: answered and completed, declined,
|
|
2677
2677
|
withdrawn, abandoned in the ring, lapsed, taken over. An offer that is simply never mentioned
|
|
2678
|
-
again leaves the desk holding a task nobody will close, and the
|
|
2678
|
+
again leaves the desk holding a task nobody will close, and the test names it
|
|
2679
2679
|
(`stream.taskOffered.unended`).
|
|
2680
2680
|
|
|
2681
2681
|
### `task-updated`
|
|
@@ -2730,8 +2730,8 @@ manifest's `settleMs`. A provider never answers `applied` for a completion it ha
|
|
|
2730
2730
|
performed. Past the bound the agent application calls `snapshot()`: a snapshot still carrying the task is a task
|
|
2731
2731
|
held open by a provider that said it was done, and the desk shows it as unsettled -- "Completing...
|
|
2732
2732
|
the provider has not confirmed" -- naming the command; a snapshot no longer carrying it clears the
|
|
2733
|
-
task, since the ending was owed and lost. The
|
|
2734
|
-
(`
|
|
2733
|
+
task, since the ending was owed and lost. The test holds a provider to the same bound
|
|
2734
|
+
(`test.completion.unsettled`). The `task-audio-ended` event and the `completing` phase are likewise
|
|
2735
2735
|
non-terminal. A replacement
|
|
2736
2736
|
snapshot that no longer contains the task also clears it. Repeated `task-ended` delivery with the
|
|
2737
2737
|
same envelope ID is harmless, and a `task-ended` naming an assignment that has already ended is
|
|
@@ -2762,7 +2762,7 @@ already done the safe thing before it speaks, and nothing in the task or break m
|
|
|
2762
2762
|
a diagnostic. It exists so that the loudness reaches a person — an agent application renders each one where the
|
|
2763
2763
|
agent works and keeps a count an operator can read, because a healthy workspace over a shouting
|
|
2764
2764
|
console is the silence problem one layer up. One event per occurrence; the agent application counts, the
|
|
2765
|
-
provider does not batch. `
|
|
2765
|
+
provider does not batch. `testAdapter` treats a diagnostic delivered during a run as a
|
|
2766
2766
|
violation (`diagnostic.raised`): a conformance run against a platform that is breaking its rules
|
|
2767
2767
|
fails loudly rather than passing with a note. A task published under `capabilitySource:
|
|
2768
2768
|
"not-yet-read"` is the same kind of fact, stated on the task instead, and fails a run the same way
|
|
@@ -2875,7 +2875,7 @@ wider goes through this key.
|
|
|
2875
2875
|
|
|
2876
2876
|
Whether two logins declare the same capabilities, field by field — key order aside, and with
|
|
2877
2877
|
`team: {}` distinct from `team` absent. It is the comparison an adapter makes before republishing
|
|
2878
|
-
`authenticated`, and the one `
|
|
2878
|
+
`authenticated`, and the one `testAdapter` holds `refreshing` to.
|
|
2879
2879
|
|
|
2880
2880
|
## Runtime validation
|
|
2881
2881
|
|
|
@@ -2900,9 +2900,9 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
2900
2900
|
| `validateHostReport(report)` | The agent application's own report as published to an adapter: `online`, and where there is audio, an input that is `available` with the microphone and `flowing`, or `unavailable` with a reason and the failure that says why, and an output that is `available` or `unavailable` with its failure. The harness validates whatever agent application a test hands the adapter; `stillHost(report)` builds one that never changes. |
|
|
2901
2901
|
| `validateHostMute(mute, softphone)` | What the agent application's Mute does, stated on a softphone login and nowhere else: `stream` or `station` (`host.mute`), required where the agent application holds a microphone (`host.mute.required`) and refused where it does not (`host.mute.unexpected`). The harness holds `ConnectContext.host.mute` to it. |
|
|
2902
2902
|
| `validateLoginStore(store)` | The login's store the agent application hands every connection: an object with `get`, `set` and `delete` (`store.shape`, `store.get`, `.set`, `.delete`). The harness holds `ConnectContext.store` to it. |
|
|
2903
|
-
| `validateCapacity(capacity)` | What the agent application states as capacity: a whole number of zero or more (`capacity.count`), zero being agent application-stopped. The harness states one on connect and two, one and zero after the
|
|
2903
|
+
| `validateCapacity(capacity)` | What the agent application states as capacity: a whole number of zero or more (`capacity.count`), zero being agent application-stopped. The harness states one on connect and two, one and zero after the test, each answered `applied`, and holds any offer to the count in force (`stream.taskOffered.overCapacity`). |
|
|
2904
2904
|
| `validateAuthenticationResult(result, method)` | What `start()` or `complete()` answered: a challenge or a rejection, a login or a rejection. A rejection's failure is held to its rules -- an `omni.` code the contract lists, and `omni.phone-not-permitted` never retryable, since the agent's station is configuration. `validateAuthenticationFailure(failure)` is the same check on a failure alone. |
|
|
2905
|
-
| `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a conference's item, a schedule's time, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.interaction` for every control on the call or the conversation), and the state that has to stand: a lead requested, somebody else still on the call (`command.conference.remove.alone`). An agent application validates before sending and an adapter before acting. |
|
|
2905
|
+
| `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a conference's item, a schedule's time, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.interaction` for every control on the call or the conversation), and the state that has to stand: a lead requested, somebody else still on the call (`command.conference.remove.alone`), the caller off hold before the agent's part ends (`command.endCall.held`). An agent application validates before sending and an adapter before acting. |
|
|
2906
2906
|
| `validateResult(result, method)` | What a connection method answered: the status it gives, a failure where the status says so and nowhere else, the failure's shape, and that an `omni.` code is one this contract names. |
|
|
2907
2907
|
| `validateAuthenticationState(state)` | The identity each state must carry, the capabilities a usable login declares, and the expiry that only `authenticated` may. Omni applies it to every state a session publishes — the republished as much as the first. |
|
|
2908
2908
|
|
|
@@ -2917,7 +2917,7 @@ carries that agent reports `team.member.self`, on a `team-member-updated` and a
|
|
|
2917
2917
|
reports `team.unentitled`, and a team event to such a login reports `event.team.capability`; given
|
|
2918
2918
|
`leadFeatures: true`, a snapshot without a team reports `team.required`, and given `leadFeatures: false`,
|
|
2919
2919
|
anything of the team reaching the lead reports `team.unexpected` or `event.team.features`. Without them those rules are not checked, because they cannot be.
|
|
2920
|
-
`
|
|
2920
|
+
`testAdapter` passes all three, holding the switch off until it has sent it. Given the manifest, a
|
|
2921
2921
|
snapshot or event that carries an instant the desk renders as a running duration under a manifest
|
|
2922
2922
|
without `timeCheck` reports `manifest.timeCheck.required`.
|
|
2923
2923
|
|
|
@@ -2950,9 +2950,9 @@ A refusal has an aftermath on the desk and a report to the provider, and both ar
|
|
|
2950
2950
|
side. The harness tells an adapter under test exactly as an agent application does
|
|
2951
2951
|
(`connection.refused.required`, `connection.refused.rejected`).
|
|
2952
2952
|
|
|
2953
|
-
##
|
|
2953
|
+
## Testing an adapter
|
|
2954
2954
|
|
|
2955
|
-
### `
|
|
2955
|
+
### `testAdapter(adapter, context, options?)`
|
|
2956
2956
|
|
|
2957
2957
|
Adapter conformance exercise from `@xema/omni-protocol/testing`.
|
|
2958
2958
|
|
|
@@ -2966,7 +2966,7 @@ By default it throws `ProtocolConformanceError` listing every violation. Pass
|
|
|
2966
2966
|
`{ collectOnly: true }` to receive them on the result instead:
|
|
2967
2967
|
|
|
2968
2968
|
```ts
|
|
2969
|
-
const result = await
|
|
2969
|
+
const result = await testAdapter(adapter, context, { collectOnly: true });
|
|
2970
2970
|
expect(result.violations).toEqual([]);
|
|
2971
2971
|
expect(result.disconnectWasClean).toBe(true);
|
|
2972
2972
|
```
|
|
@@ -2978,7 +2978,7 @@ latest the session published during the run, which differs only when the adapter
|
|
|
2978
2978
|
`authentication.refreshing.identity`, a changed capability set `authentication.refreshing.capabilities`.
|
|
2979
2979
|
A capability granted by a later login requires its methods just as one declared at sign-in does.
|
|
2980
2980
|
|
|
2981
|
-
`result.
|
|
2981
|
+
`result.notTested` lists what the run never reached — one subject per family of rules: each
|
|
2982
2982
|
optional part of a task (`task.browsers`, `task.history`, `task.leadAssist`, …), the break's
|
|
2983
2983
|
`reasons` and `forced`, the team member list's `members` and a member's `request`, each declared contribution, and
|
|
2984
2984
|
each event type (`event.task-ended`, …) — and so what a clean `violations` says nothing about.
|
|
@@ -2987,52 +2987,52 @@ no tasks exercises no task rule, and a pass over it reads as coverage it is not.
|
|
|
2987
2987
|
|
|
2988
2988
|
**A static run never answers a call.** It states a capacity and disconnects, so everything
|
|
2989
2989
|
downstream of `answer` -- the room, the stage, audio, every phase past `pending`, every dial
|
|
2990
|
-
outcome -- stays in `
|
|
2991
|
-
in each adopter's own tests. `{
|
|
2990
|
+
outcome -- stays in `notTested` for every adapter, and a rule about a live call is enforced only
|
|
2991
|
+
in each adopter's own tests. `{ withCall: true }` closes that: the exercise takes the first task the
|
|
2992
2992
|
provider offers through one ordinary lifecycle -- accept it, wait for its audio and open it on a
|
|
2993
2993
|
softphone, hold and resume where the task offers `hold`, end the call where it offers `endCall`,
|
|
2994
2994
|
complete it with an outcome where the agent completes -- and holds every step to the rules a
|
|
2995
2995
|
agent application holds a provider to. Each command is validated against the task as published
|
|
2996
|
-
(`
|
|
2997
|
-
violation (`
|
|
2998
|
-
(`
|
|
2996
|
+
(`test.command.*`), each answer for its method, a refusal of a control the task offered is a
|
|
2997
|
+
violation (`test.command.failed`), and an event the provider owes and never sends is one too
|
|
2998
|
+
(`test.timeout`, after `timeoutMs`, 5000 by default). A first offer that says how long it has is left to
|
|
2999
2999
|
lapse and expected to end `expired`, and a preview's deadline is held to its `atDeadline`
|
|
3000
|
-
(`
|
|
3001
|
-
clock**. Around the
|
|
3000
|
+
(`test.offer.expired`, `test.preview.deadline`); see **Every screen counts from the provider's
|
|
3001
|
+
clock**. Around the test the exercise holds
|
|
3002
3002
|
the run to what an agent application holds a provider to between commands: an offer before the agent application stated
|
|
3003
3003
|
capacity, or beyond the count with nothing dialled on it, is named
|
|
3004
3004
|
(`stream.taskOffered.beforeCapacity`, `.overCapacity`); every user the snapshot names is looked up
|
|
3005
3005
|
through `getUserDetails` and the answer held to the shape, nobody unasked, nobody described as
|
|
3006
3006
|
nothing (`getUserDetails.user.*`, `getUserDetails.unasked`, `connection.getUserDetails.empty`); and
|
|
3007
3007
|
the second adapter a `rebuild` gives comes up signed in as the same login from the secrets alone
|
|
3008
|
-
before it reads anything (`
|
|
3008
|
+
before it reads anything (`test.reload.login`), and is then held to everything the first was on
|
|
3009
3009
|
connect: the methods its declarations call for, the agent application's report, a capacity stated to it, its
|
|
3010
3010
|
snapshot read as the connect snapshot was, and its audio opened afresh on a task carried with
|
|
3011
3011
|
`audio: "started"`, since the first client's session died with it. The result also says which rules the
|
|
3012
|
-
run evaluated, pass or fail, in `
|
|
3012
|
+
run evaluated, pass or fail, in `rulesTested`: the validators' as each was applied, the stream's
|
|
3013
3013
|
as each case was considered, so a test that needs a rule to have run asserts it there rather than
|
|
3014
3014
|
inferring it from an empty `violations`, and a rule absent from it was never looked at, which is a
|
|
3015
3015
|
gap and not a pass. With the audio open on a softphone,
|
|
3016
|
-
the
|
|
3017
|
-
expecting each report `recorded` with the provider-selected history `at` (`
|
|
3016
|
+
the test mutes it for one second and reports the leg through `recordStep`, begun and then ended,
|
|
3017
|
+
expecting each report `recorded` with the provider-selected history `at` (`test.recordStep.failed`, `.rejected`, `result.recordStep.at`); then it mutes again and
|
|
3018
3018
|
ends the call muted, as agents do, so the leg is open when the audio ends, the provider closes it
|
|
3019
3019
|
in the completing publication or the open entry is refused (`task.history.muted.open`),
|
|
3020
|
-
and the
|
|
3021
|
-
a record restated afterwards with that leg's duration altered is named (`
|
|
3020
|
+
and the test's closing report after the audio ended is expected `recorded` and to change nothing:
|
|
3021
|
+
a record restated afterwards with that leg's duration altered is named (`test.recordStep.overwritten`).
|
|
3022
3022
|
Where the provider restates the task's record afterwards, each leg is in it or the hole is named
|
|
3023
|
-
(`
|
|
3024
|
-
does, the
|
|
3025
|
-
disconnected and its session closed (`
|
|
3023
|
+
(`test.recordStep.history`). Given `rebuild`, a way to build the adapter again as an agent application reload
|
|
3024
|
+
does, the test reloads the agent application as a reload happens: the first client is unsubscribed,
|
|
3025
|
+
disconnected and its session closed (`test.reload.handover`), and only then is a second adapter
|
|
3026
3026
|
built and connected for the same login with the same context and the same `store`; its snapshot
|
|
3027
3027
|
must carry the task with that leg at its acknowledged provider timestamp and the reported actor, and the run goes on with the second as its
|
|
3028
3028
|
connection to the end. A platform that holds the record hands it back, and an adapter that composed
|
|
3029
|
-
the record in memory has nothing and is named (`
|
|
3029
|
+
the record in memory has nothing and is named (`test.reload.snapshot`, `test.reload.history`,
|
|
3030
3030
|
`.rejected`). The second adapter's snapshot is taken as any resync is: held to what the stream knew
|
|
3031
3031
|
before it replaces it, so a phase gone backwards, a record that shrank or audio forgotten on a task
|
|
3032
3032
|
still at work is named by the stream's own rules (`stream.snapshot.phase`, `.history`,
|
|
3033
3033
|
`.audio`) and a reload is a place those rules keep working, not one where they stop. The second
|
|
3034
3034
|
adapter is held to what the first was: the same provider
|
|
3035
|
-
(`
|
|
3035
|
+
(`test.reload.manifest`), a snapshot that stands as any snapshot must, and a record that lost none
|
|
3036
3036
|
of the entries the first had published -- a record once read is not unread across a reload either.
|
|
3037
3037
|
A reconnect on the same adapter object would prove nothing, since an in-process adapter's memory
|
|
3038
3038
|
survives it; only a second object separates kept-in-the-store from never-lost. And because the
|
|
@@ -3041,31 +3041,33 @@ open task back to, and nothing about the reload is exempt from any rule. The sec
|
|
|
3041
3041
|
`secrets`, so the reload is a restore before it is anything else: an adapter that holds a session
|
|
3042
3042
|
has already put what would rebuild it into the secrets store, from the moment it was handed one,
|
|
3043
3043
|
not only when a flow completes -- the session is the adapter's, the store is the agent application's, and a
|
|
3044
|
-
agent application reload is exactly when no flow will run. And once the task has ended, the store the
|
|
3044
|
+
agent application reload is exactly when no flow will run. And once the task has ended, the store the test
|
|
3045
3045
|
handed the adapter holds no key naming the task's assignment id, delimited, never as a run of characters inside
|
|
3046
|
-
another id (`
|
|
3046
|
+
another id (`test.store.retained`); an adapter whose keys never named the task leaves the rule
|
|
3047
3047
|
unevaluated, and the result says so rather than passing it: a task's keys go
|
|
3048
3048
|
with the task, or whatever comes next inherits them. Outside the
|
|
3049
3049
|
interaction phases with `hold` still declared -- in
|
|
3050
3050
|
`confirmed`, where the provider publishes it, and in `completing` once this agent's interaction has ended -- the
|
|
3051
|
-
|
|
3051
|
+
test sends `hold` past the validator that would hold it back, and expects `failed`: the adapter
|
|
3052
3052
|
is the second gate on a control on the contact, and one that applies it outside this task's
|
|
3053
|
-
current interaction is named (`
|
|
3053
|
+
current interaction is named (`test.command.interaction`). With the task on hold, the test sends `end-call` past the
|
|
3054
|
+
validator the same way and expects `failed`, naming an adapter that ends the agent's part with the caller in the
|
|
3055
|
+
hold (`test.command.held`). The test cannot put a task into a phase the provider
|
|
3054
3056
|
never publishes, so a provider that goes straight from `pending` to `in-progress` is checked in
|
|
3055
3057
|
`completing` alone. A task the agent completes is
|
|
3056
|
-
completed from wherever it stands once the
|
|
3058
|
+
completed from wherever it stands once the test has nothing left to do on it -- from `completing`
|
|
3057
3059
|
after an `end-call`, or from `in-progress` where there is no call to end, which is every chat and
|
|
3058
3060
|
email and a voice task offering no `endCall` -- so a conversation reaches its end as a call does.
|
|
3059
|
-
The
|
|
3061
|
+
The test stops where the task offers no way on and says nothing about what it could not reach. A
|
|
3060
3062
|
softphone adapter written for a browser needs its audio APIs supplied by whatever runs the
|
|
3061
|
-
harness: the
|
|
3063
|
+
harness: the test opens audio outside a browser, and a provider that quietly stopped carrying audio
|
|
3062
3064
|
where `AudioContext` was missing would be lying about the one thing the channel is for. It is off by default because it issues
|
|
3063
3065
|
commands against whatever platform the adapter is connected to: turn it on against a test backend.
|
|
3064
3066
|
|
|
3065
3067
|
```ts
|
|
3066
|
-
const
|
|
3067
|
-
expect(
|
|
3068
|
-
assertReached(
|
|
3068
|
+
const tested = await testAdapter(adapter, context, { collectOnly: true, withCall: true });
|
|
3069
|
+
expect(tested.violations).toEqual([]);
|
|
3070
|
+
assertReached(tested, ["task.onCall", "task.audio", "event.task-ended"]);
|
|
3069
3071
|
```
|
|
3070
3072
|
`assertReached(result, subjects)` is the paired assertion: it throws naming every subject the run
|
|
3071
3073
|
never met, so a test that meant to check a team member list cannot pass on a fixture that never produced one.
|
|
@@ -3096,7 +3098,7 @@ cannot be established from TypeScript structure alone.
|
|
|
3096
3098
|
| `assertCapabilityWithdrawal(states, snapshot, manifest)` | A capability withdrawn by a later `authenticated` state is gone from the next snapshot: no team member list for a login that no longer leads. Every state is validated on the way, `refreshing` must carry the login over, and the sequence passes only through usable states. |
|
|
3097
3099
|
| `assertTaskCapabilityWithdrawal(tasks, manifest, command)` | A capability withdrawn by a republish of the task is gone from the task: every task in the sequence is validated, all carry the offer's id, at least one capability the offer declared is absent at the end (a locked control is present, not withdrawn), and `command` is clean against the first task and refused against the last for want of a withdrawn capability and nothing else. Pair it with `assertCommandRefusedAfterWithdrawal` on the provider's answer. |
|
|
3098
3100
|
| `assertCommandRefusedAfterWithdrawal(result)` | A command that arrives after its capability was withdrawn fails with `omni.capability-not-enabled`, named by the provider. The same assertion serves a command the provider never supported under a capability it declares. |
|
|
3099
|
-
| `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `
|
|
3101
|
+
| `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `testAdapter` result. |
|
|
3100
3102
|
| `assertAuthenticationRestoreAndExpiry(states)` | A restored authenticated session can refresh and ends in expiry. Every state is validated. |
|
|
3101
3103
|
| `assertReconnectWithMissedAssignments(before, reconnect, ids)` | A reconnect snapshot restores assignments received while offline. |
|
|
3102
3104
|
| `stillHost(report?, guarantees?, mute?)` | An agent application that reports one thing and never changes, for a test context: `{ online: true }` by default, a report with audio for a softphone voice adapter, and never for a desk phone. |
|