@xema/omni-protocol 0.1.54 → 0.1.56
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/dist/index.d.ts +1 -1
- package/dist/index.js +1 -0
- package/dist/testing.d.ts +15 -1
- package/dist/testing.js +49 -1
- package/dist/validation.d.ts +7 -0
- package/dist/validation.js +56 -8
- package/guide.md +61 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1394,7 +1394,7 @@ export interface ProviderEventEnvelope<C extends Channel = Channel> {
|
|
|
1394
1394
|
occurredAt: IsoTimestamp;
|
|
1395
1395
|
event: ProviderEvent<C>;
|
|
1396
1396
|
}
|
|
1397
|
-
export declare const OMNI_FAILURE_CODES: readonly ["omni.not-authenticated", "omni.capability-not-enabled", "omni.task-not-found", "omni.destination-not-permitted", "omni.rate-limited", "omni.unavailable", "omni.break-already-committed"];
|
|
1397
|
+
export declare const OMNI_FAILURE_CODES: readonly ["omni.not-authenticated", "omni.capability-not-enabled", "omni.task-not-found", "omni.destination-not-permitted", "omni.phone-not-permitted", "omni.rate-limited", "omni.unavailable", "omni.break-already-committed"];
|
|
1398
1398
|
export type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
|
|
1399
1399
|
export interface Connection<C extends Channel = Channel> {
|
|
1400
1400
|
snapshot(): Snapshot<C> | Promise<Snapshot<C>>;
|
package/dist/index.js
CHANGED
package/dist/testing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type TaskCompletion, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext, type Host, type HostGuarantees, type HostReport, type Manifest, type ProviderEvent } from "./index.js";
|
|
1
|
+
import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type Task, type TaskCompletion, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext, type Host, type HostGuarantees, type HostReport, type Manifest, type ProviderEvent } from "./index.js";
|
|
2
2
|
import { type ProtocolViolation } from "./validation.js";
|
|
3
3
|
export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation } from "./validation.js";
|
|
4
4
|
/**
|
|
@@ -84,6 +84,20 @@ export declare function assertCommandRefusedAfterWithdrawal(result: {
|
|
|
84
84
|
code: string;
|
|
85
85
|
};
|
|
86
86
|
}): void;
|
|
87
|
+
/**
|
|
88
|
+
* A task's capabilities are current, not fixed. A provider whose platform withdraws a permission
|
|
89
|
+
* mid-task republishes the task with the set as it now stands, and a command under the withdrawn
|
|
90
|
+
* capability is refused from then on. `tasks` is what the provider published for one task, first
|
|
91
|
+
* to last -- the offer, then each republish -- every one validated against the manifest, all the
|
|
92
|
+
* same id, with at least one capability offered by the first absent from the last. A locked
|
|
93
|
+
* control is present without permission, not withdrawn: it is still drawn, so it does not count.
|
|
94
|
+
* `command` is the command a host issues under a withdrawn capability: it must be clean against
|
|
95
|
+
* the first task and refused against the last for want of that capability and for nothing else.
|
|
96
|
+
* That pairing is what proves the refusal comes from the withdrawal rather than from a command
|
|
97
|
+
* that was never issuable. Pair it with `assertCommandRefusedAfterWithdrawal` on the provider's
|
|
98
|
+
* answer to the same command.
|
|
99
|
+
*/
|
|
100
|
+
export declare function assertTaskCapabilityWithdrawal(tasks: readonly Task[], manifest: Manifest, command: Record<string, unknown>): void;
|
|
87
101
|
/**
|
|
88
102
|
* Throws unless the run reached every subject named: the paired assertion beside a clean result,
|
|
89
103
|
* so a fixture that never produced a roster cannot pass a test that meant to check one.
|
package/dist/testing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { browserSessionKey, effectiveLevels, sameCapabilities, } from "./index.js";
|
|
2
|
-
import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateManifest, validateResult, validateSnapshot, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
|
|
2
|
+
import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateManifest, validateResult, validateSnapshot, validateTask, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
|
|
3
3
|
export { ProtocolConformanceError, assertNoViolations } from "./validation.js";
|
|
4
4
|
/**
|
|
5
5
|
* A part of the contract a run may never reach: state nothing obliges an adapter to publish, so
|
|
@@ -575,6 +575,54 @@ export function assertCommandRefusedAfterWithdrawal(result) {
|
|
|
575
575
|
throw new Error(`A command after its capability was withdrawn fails with omni.capability-not-enabled, not ${result.failure?.code}`);
|
|
576
576
|
}
|
|
577
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* A task's capabilities are current, not fixed. A provider whose platform withdraws a permission
|
|
580
|
+
* mid-task republishes the task with the set as it now stands, and a command under the withdrawn
|
|
581
|
+
* capability is refused from then on. `tasks` is what the provider published for one task, first
|
|
582
|
+
* to last -- the offer, then each republish -- every one validated against the manifest, all the
|
|
583
|
+
* same id, with at least one capability offered by the first absent from the last. A locked
|
|
584
|
+
* control is present without permission, not withdrawn: it is still drawn, so it does not count.
|
|
585
|
+
* `command` is the command a host issues under a withdrawn capability: it must be clean against
|
|
586
|
+
* the first task and refused against the last for want of that capability and for nothing else.
|
|
587
|
+
* That pairing is what proves the refusal comes from the withdrawal rather than from a command
|
|
588
|
+
* that was never issuable. Pair it with `assertCommandRefusedAfterWithdrawal` on the provider's
|
|
589
|
+
* answer to the same command.
|
|
590
|
+
*/
|
|
591
|
+
export function assertTaskCapabilityWithdrawal(tasks, manifest, command) {
|
|
592
|
+
const first = tasks[0];
|
|
593
|
+
const last = tasks.at(-1);
|
|
594
|
+
if (tasks.length < 2 || first === undefined || last === undefined) {
|
|
595
|
+
throw new Error("Task capability withdrawal needs the task as offered and at least one republish of it");
|
|
596
|
+
}
|
|
597
|
+
const context = {
|
|
598
|
+
channel: manifest.channel,
|
|
599
|
+
levels: effectiveLevels(manifest.orgLevels).map(level => level.id),
|
|
600
|
+
dialOutcomesDeclared: manifest.dialOutcomes !== undefined,
|
|
601
|
+
};
|
|
602
|
+
tasks.forEach((task, index) => {
|
|
603
|
+
assertNoViolations(validateTask(task, context, `tasks[${index}]`), `Task capability withdrawal: tasks[${index}] must be a task the wire could carry`);
|
|
604
|
+
if (task.id !== first.id) {
|
|
605
|
+
throw new Error(`Task capability withdrawal must keep the task: tasks[${index}] is ${task.id}, and the offer was ${first.id}`);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
const withdrawn = Object.keys(first.capabilities).filter(name => last.capabilities[name] === undefined);
|
|
609
|
+
if (withdrawn.length === 0) {
|
|
610
|
+
throw new Error("Task capability withdrawal must end with at least one capability the offer declared withdrawn; a locked control is present without permission, not withdrawn");
|
|
611
|
+
}
|
|
612
|
+
const before = validateTaskCommand(command, first, "command");
|
|
613
|
+
if (before.length > 0) {
|
|
614
|
+
throw new Error(`Task capability withdrawal: the command must be issuable against the task as offered, or its later refusal proves nothing (${before.map(v => v.rule).join(", ")})`);
|
|
615
|
+
}
|
|
616
|
+
const after = validateTaskCommand(command, last, "command");
|
|
617
|
+
const forWant = after.filter(v => withdrawn.some(name => v.rule === `command.capability.${name}`));
|
|
618
|
+
if (forWant.length === 0) {
|
|
619
|
+
throw new Error(`Task capability withdrawal: the command must be refused against the republished task for want of ${withdrawn.join(" or ")}; it was ${after.length === 0 ? "accepted" : `refused for ${after.map(v => v.rule).join(", ")}`}`);
|
|
620
|
+
}
|
|
621
|
+
const other = after.filter(v => !forWant.includes(v));
|
|
622
|
+
if (other.length > 0) {
|
|
623
|
+
throw new Error(`Task capability withdrawal: the republished task refuses the command for more than the withdrawal (${other.map(v => v.rule).join(", ")}); change one thing at a time`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
578
626
|
/**
|
|
579
627
|
* Throws unless the run reached every subject named: the paired assertion beside a clean result,
|
|
580
628
|
* so a fixture that never produced a roster cannot pass a test that meant to check one.
|
package/dist/validation.d.ts
CHANGED
|
@@ -91,4 +91,11 @@ export interface LoginValidationContext {
|
|
|
91
91
|
/** The level ids in force, from the manifest. The defaults when absent. */
|
|
92
92
|
levels?: readonly string[];
|
|
93
93
|
}
|
|
94
|
+
export declare function validateAuthenticationFailure(failure: unknown, path?: string): ProtocolViolation[];
|
|
95
|
+
/**
|
|
96
|
+
* What `start()` or `complete()` answered: a challenge or a rejection from `start`, a login or a
|
|
97
|
+
* rejection from `complete`. A rejection carries a failure the host can show, and a login is held
|
|
98
|
+
* to the same rules as any `authenticated` state.
|
|
99
|
+
*/
|
|
100
|
+
export declare function validateAuthenticationResult(result: unknown, method: "start" | "complete", path?: string, context?: LoginValidationContext): ProtocolViolation[];
|
|
94
101
|
export declare function validateAuthenticationState(state: unknown, path?: string, context?: LoginValidationContext): ProtocolViolation[];
|
package/dist/validation.js
CHANGED
|
@@ -2053,6 +2053,61 @@ function validateMonitorControl(value, path, into) {
|
|
|
2053
2053
|
});
|
|
2054
2054
|
into.require(value.includes("monitor"), "authentication.capability.team.monitorControl.monitor", path, "whisper and barge begin from a monitor: a lead who may listen in any mode may monitor");
|
|
2055
2055
|
}
|
|
2056
|
+
/**
|
|
2057
|
+
* A failure an authentication reports: on a `rejected` start or completion, or on an `expired`
|
|
2058
|
+
* state. A provider names its own codes freely; an `omni.` code is the contract's and has to be one
|
|
2059
|
+
* it lists. `omni.phone-not-permitted` is never retryable: the agent's station is administrator
|
|
2060
|
+
* configuration, and trying again does not reconfigure it.
|
|
2061
|
+
*/
|
|
2062
|
+
function validateAuthenticationFailureInto(value, path, into) {
|
|
2063
|
+
if (!isPlainObject(value)) {
|
|
2064
|
+
into.add("authentication.failure.shape", path, "a failure must be an object");
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
if (into.filled(value.code, "authentication.failure.code", `${path}.code`, "a failure needs a code") && value.code.startsWith("omni.")) {
|
|
2068
|
+
into.require(OMNI_FAILURE_CODES.includes(value.code), "failure.code.unknown", `${path}.code`, `${String(value.code)} is not a failure code this contract defines`);
|
|
2069
|
+
}
|
|
2070
|
+
into.filled(value.message, "authentication.failure.message", `${path}.message`, "a failure needs a message");
|
|
2071
|
+
into.require(typeof value.retryable === "boolean", "authentication.failure.retryable", `${path}.retryable`, "a failure must say whether it is retryable");
|
|
2072
|
+
if (value.code === "omni.phone-not-permitted") {
|
|
2073
|
+
into.require(value.retryable === false, "authentication.failure.phone.retryable", `${path}.retryable`, "a phone the platform does not permit for this agent is configuration: trying again does not change it, so the refusal is not retryable");
|
|
2074
|
+
}
|
|
2075
|
+
if (value.retryAfterMs !== undefined) {
|
|
2076
|
+
into.require(typeof value.retryAfterMs === "number" && value.retryAfterMs >= 0, "authentication.failure.retryAfterMs", `${path}.retryAfterMs`, "retryAfterMs must be a non-negative number when present");
|
|
2077
|
+
}
|
|
2078
|
+
if (value.field !== undefined)
|
|
2079
|
+
into.filled(value.field, "authentication.failure.field", `${path}.field`, "field names a declared credentials field when present");
|
|
2080
|
+
}
|
|
2081
|
+
export function validateAuthenticationFailure(failure, path = "failure") {
|
|
2082
|
+
const into = new Collector();
|
|
2083
|
+
validateAuthenticationFailureInto(failure, path, into);
|
|
2084
|
+
return into.violations;
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* What `start()` or `complete()` answered: a challenge or a rejection from `start`, a login or a
|
|
2088
|
+
* rejection from `complete`. A rejection carries a failure the host can show, and a login is held
|
|
2089
|
+
* to the same rules as any `authenticated` state.
|
|
2090
|
+
*/
|
|
2091
|
+
export function validateAuthenticationResult(result, method, path = "result", context = {}) {
|
|
2092
|
+
const into = new Collector();
|
|
2093
|
+
if (!isPlainObject(result)) {
|
|
2094
|
+
into.add("authentication.result.shape", path, `${method} must answer an object`);
|
|
2095
|
+
return into.violations;
|
|
2096
|
+
}
|
|
2097
|
+
if (result.status === "rejected") {
|
|
2098
|
+
validateAuthenticationFailureInto(result.failure, `${path}.failure`, into);
|
|
2099
|
+
}
|
|
2100
|
+
else if (method === "start" && result.status === "interaction-required") {
|
|
2101
|
+
into.require(isPlainObject(result.challenge), "authentication.result.challenge", `${path}.challenge`, "interaction-required carries the challenge to put to the person");
|
|
2102
|
+
}
|
|
2103
|
+
else if (method === "complete" && result.status === "authenticated") {
|
|
2104
|
+
into.violations.push(...validateAuthenticationState(result, path, context));
|
|
2105
|
+
}
|
|
2106
|
+
else {
|
|
2107
|
+
into.add("authentication.result.status", `${path}.status`, `${method} answers ${method === "start" ? "interaction-required" : "authenticated"} or rejected, not ${String(result.status)}`);
|
|
2108
|
+
}
|
|
2109
|
+
return into.violations;
|
|
2110
|
+
}
|
|
2056
2111
|
export function validateAuthenticationState(state, path = "authentication", context = {}) {
|
|
2057
2112
|
const into = new Collector();
|
|
2058
2113
|
if (!isPlainObject(state)) {
|
|
@@ -2072,14 +2127,7 @@ export function validateAuthenticationState(state, path = "authentication", cont
|
|
|
2072
2127
|
if (state.identity !== undefined)
|
|
2073
2128
|
validateUser(state.identity, "authentication.identity", `${path}.identity`, into);
|
|
2074
2129
|
if (state.failure !== undefined) {
|
|
2075
|
-
|
|
2076
|
-
into.add("authentication.failure.shape", `${path}.failure`, "a failure must be an object when present");
|
|
2077
|
-
}
|
|
2078
|
-
else {
|
|
2079
|
-
into.filled(state.failure.code, "authentication.failure.code", `${path}.failure.code`, "a failure needs a code");
|
|
2080
|
-
into.filled(state.failure.message, "authentication.failure.message", `${path}.failure.message`, "a failure needs a message");
|
|
2081
|
-
into.require(typeof state.failure.retryable === "boolean", "authentication.failure.retryable", `${path}.failure.retryable`, "a failure must say whether it is retryable");
|
|
2082
|
-
}
|
|
2130
|
+
validateAuthenticationFailureInto(state.failure, `${path}.failure`, into);
|
|
2083
2131
|
}
|
|
2084
2132
|
}
|
|
2085
2133
|
else {
|
package/guide.md
CHANGED
|
@@ -1120,6 +1120,7 @@ const OMNI_FAILURE_CODES = [
|
|
|
1120
1120
|
"omni.capability-not-enabled",
|
|
1121
1121
|
"omni.task-not-found",
|
|
1122
1122
|
"omni.destination-not-permitted",
|
|
1123
|
+
"omni.phone-not-permitted",
|
|
1123
1124
|
"omni.rate-limited",
|
|
1124
1125
|
"omni.unavailable",
|
|
1125
1126
|
"omni.break-already-committed",
|
|
@@ -2663,6 +2664,44 @@ browser that used it.
|
|
|
2663
2664
|
Task capabilities belong to each `Task`. If `hold` is omitted on one task, Omni
|
|
2664
2665
|
must not show or issue hold for that task even if another task from the same provider supports it.
|
|
2665
2666
|
|
|
2667
|
+
**A capability is a property of the task, and of nothing the task names.** A provider derives it
|
|
2668
|
+
from no other source -- not the queue the task came from, not the login, not its own configuration
|
|
2669
|
+
-- because each of those answers a question about this task from somewhere that does not know
|
|
2670
|
+
about this task, and a client with two sources and a rule for choosing between them is the shape
|
|
2671
|
+
that produces two consumers disagreeing about one fact. The task is the one source, and the
|
|
2672
|
+
provider puts on the task what the platform permits for it.
|
|
2673
|
+
|
|
2674
|
+
**A permission that changes while the task is open is republished on the task at the moment it
|
|
2675
|
+
changes.** An agent on a billing dispute may refund up to their own limit and no further. The
|
|
2676
|
+
customer wants more, the agent asks for a lead, and the moment the lead joins the call the task is
|
|
2677
|
+
republished with the Refund control the agent could not have a minute ago; when the lead leaves, it
|
|
2678
|
+
is republished without it. Nothing about the agent changed -- who is on the call did -- and the
|
|
2679
|
+
task said so both times, at the moment it became true. A call moves queues the same way: a caller
|
|
2680
|
+
identified as a priority customer is transferred to the priority queue, and the task arrives under
|
|
2681
|
+
that queue's terms -- a longer wrap allowance, a discount control the general queue never offered --
|
|
2682
|
+
restated on the task at the hand-over, not inferred from the queue it left. A capability stated
|
|
2683
|
+
once at offer or answer and never corrected is a fact with a shelf life and no expiry, and a host
|
|
2684
|
+
draws a control the provider will now refuse -- or withholds one the agent now has. So the provider republishes
|
|
2685
|
+
the task, with its capabilities as they now stand, and a host treats the last statement as current
|
|
2686
|
+
rather than re-deriving anything. This is the same shape as a room left full after the call ends
|
|
2687
|
+
-- a field describing the present, carried past the moment it stopped being true -- carried past
|
|
2688
|
+
this time not by a spread but by nobody sending the correction. A provider whose platform can
|
|
2689
|
+
change a permission mid-task and does not republish is in breach, however conformant its offer was.
|
|
2690
|
+
A capability set can shrink as well as grow, and a host that has only ever seen it grow meets a
|
|
2691
|
+
control that was there a moment ago and is gone; a command that arrives after its capability was
|
|
2692
|
+
withdrawn is refused with a reason, never acted on. The asymmetry decides which direction a
|
|
2693
|
+
provider gets right first: a control gained late is a nicety, a control withdrawn and not
|
|
2694
|
+
republished is a button that fails when pressed. `assertTaskCapabilityWithdrawal` is the test for that
|
|
2695
|
+
direction: the task as offered and as republished, and one command that was issuable under the
|
|
2696
|
+
first and is refused under the last for want of the capability withdrawn -- and nothing else.
|
|
2697
|
+
|
|
2698
|
+
**An empty capability set is a statement, not a shrug.** `capabilities: {}` says the platform
|
|
2699
|
+
permits nothing on this task. A provider that has not yet learned what the platform permits -- a
|
|
2700
|
+
queue's configuration that has not reached it -- knows nothing of the kind, and must not publish
|
|
2701
|
+
the task as if it did: it holds the task until it knows, or reports the gap as a `diagnostic`,
|
|
2702
|
+
because "no queue governs this call" is a fact and "the configuration has not arrived" is a fault,
|
|
2703
|
+
and a host cannot tell them apart from a value that carries neither.
|
|
2704
|
+
|
|
2666
2705
|
```ts
|
|
2667
2706
|
const taskCapabilities = {
|
|
2668
2707
|
channel: "voice",
|
|
@@ -3799,6 +3838,21 @@ that is a telephone. The harness requires no `openMedia` of such an adapter and
|
|
|
3799
3838
|
this wire never asks the agent for it: a host declares `phone` and nothing more. What a platform
|
|
3800
3839
|
asks on surfaces of its own is its own decision.
|
|
3801
3840
|
|
|
3841
|
+
**A declared phone the platform does not permit for this agent is a login that cannot be
|
|
3842
|
+
established.** The manifest says what the platform can do; the agent's record, kept by an
|
|
3843
|
+
administrator, says what this agent is configured for, and the two can disagree with the host's
|
|
3844
|
+
declaration -- a softphone declared for an agent whose record says desk phone. Honouring that
|
|
3845
|
+
declaration on a switch that allows one registration per endpoint would evict the handset: the
|
|
3846
|
+
agent's phone stops ringing and their calls land in a tab, and nobody is told. So the provider
|
|
3847
|
+
refuses the login at authentication, with `omni.phone-not-permitted` and a message the host shows
|
|
3848
|
+
-- "this agent is configured for a desk phone" -- and refusing is correct, not an override of the
|
|
3849
|
+
host. It is a refusal, never a negotiation afterwards and never a quiet substitution, and it is
|
|
3850
|
+
never retryable: trying again does not reconfigure the agent, and a host validating the answer
|
|
3851
|
+
(`validateAuthenticationResult`) refuses a phone refusal marked otherwise
|
|
3852
|
+
(`authentication.failure.phone.retryable`). And **a
|
|
3853
|
+
provider never makes a declared phone true by changing the platform's configuration**: the record
|
|
3854
|
+
is the administrator's, and a login is not a request to reconfigure an agent.
|
|
3855
|
+
|
|
3802
3856
|
### Opening the audio
|
|
3803
3857
|
|
|
3804
3858
|
`openMedia` hands Omni the remote audio for one task. Every adapter whose manifest lists
|
|
@@ -3963,6 +4017,7 @@ react rather than only display the message:
|
|
|
3963
4017
|
| `omni.not-authenticated` | The provider session is no longer usable. The adapter has published `expired` at or before this answer — the state is what Omni surfaces reauthentication from; the code says why this action failed, and is never the only signal. |
|
|
3964
4018
|
| `omni.capability-not-enabled` | The action targets a capability this task, manifest, or login did not declare — including a lead command from a login whose `capabilities` no longer carry it. |
|
|
3965
4019
|
| `omni.task-not-found` | The provider-local task id is unknown, typically after the task already ended. |
|
|
4020
|
+
| `omni.phone-not-permitted` | The host declared a `phone` the platform does not permit for this agent -- a softphone for an agent configured for a desk phone, or the reverse. The login is refused at authentication, and the provider never reconfigures the agent to make the declaration true. See **How the agent hears the call**. |
|
|
3966
4021
|
| `omni.destination-not-permitted` | The dialled number, or the `destinationId` named, is not one the provider offers this agent. |
|
|
3967
4022
|
| `omni.rate-limited` | The action was throttled. Pair with `retryAfterMs`. |
|
|
3968
4023
|
| `omni.unavailable` | The provider is temporarily unable to serve the action, including any command sent while `transport-status` is not `active`. |
|
|
@@ -4270,6 +4325,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
4270
4325
|
| `validateHostGuarantees(guarantees)` | What a host promises: only the guarantees this contract names, each declared by presence and never `false`. The harness validates the guarantees of whatever host a test hands the adapter. |
|
|
4271
4326
|
| `validateHandlingReport(report, path?, manifest?)` | What the host reports of a leg it performed, for an adapter to check before forwarding: a task, a step, when it began, a positive `seconds` where stated, and an explicit `ended` that carries the final duration. Given the manifest, a running report is refused unless it declares `runningStepReports`. |
|
|
4272
4327
|
| `validateHostReport(report)` | The host'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 host a test hands the adapter; `stillHost(report)` builds one that never changes. |
|
|
4328
|
+
| `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. |
|
|
4273
4329
|
| `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a transfer's item, 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.*`), and the state that has to stand: a consulted entry, a lead requested, somebody else still on the call (`command.conference.remove.alone`). A host validates before sending and an adapter before acting. |
|
|
4274
4330
|
| `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. |
|
|
4275
4331
|
| `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. |
|
|
@@ -4358,6 +4414,10 @@ assertReached(driven, ["task.onCall", "task.media", "event.task-ended"]);
|
|
|
4358
4414
|
```
|
|
4359
4415
|
`assertReached(result, subjects)` is the paired assertion: it throws naming every subject the run
|
|
4360
4416
|
never met, so a test that meant to check a roster cannot pass on a fixture that never produced one.
|
|
4417
|
+
It reads like a guarantee and is a claim the adopter keeps making: it catches an adapter that
|
|
4418
|
+
stopped reaching a subject, not a list that stopped asking, so a list can rot to nothing and stay
|
|
4419
|
+
green. Keep it honest with a control beside it -- one subject the run genuinely cannot reach,
|
|
4420
|
+
asserted to throw -- so the assertion is shown to be looking rather than agreeing.
|
|
4361
4421
|
|
|
4362
4422
|
Three properties of the harness matter to adapter authors:
|
|
4363
4423
|
|
|
@@ -4379,6 +4439,7 @@ cannot be established from TypeScript structure alone.
|
|
|
4379
4439
|
| Helper | Contract checked |
|
|
4380
4440
|
| --- | --- |
|
|
4381
4441
|
| `assertCapabilityWithdrawal(states, snapshot, manifest)` | A capability withdrawn by a later `authenticated` state is gone from the next snapshot: no roster for a login that no longer leads, no requests for one that may no longer join. Every state is validated on the way, `refreshing` must carry the login over, and the sequence passes only through usable states. |
|
|
4442
|
+
| `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. |
|
|
4382
4443
|
| `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. |
|
|
4383
4444
|
| `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `exerciseAdapter` result. |
|
|
4384
4445
|
| `assertAuthenticationRestoreAndExpiry(states)` | A restored authenticated session can refresh and ends in expiry. Every state is validated. |
|