@xema/omni-protocol 0.1.55 → 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/testing.d.ts +15 -1
- package/dist/testing.js +49 -1
- package/guide.md +39 -0
- package/package.json +1 -1
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/guide.md
CHANGED
|
@@ -2664,6 +2664,44 @@ browser that used it.
|
|
|
2664
2664
|
Task capabilities belong to each `Task`. If `hold` is omitted on one task, Omni
|
|
2665
2665
|
must not show or issue hold for that task even if another task from the same provider supports it.
|
|
2666
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
|
+
|
|
2667
2705
|
```ts
|
|
2668
2706
|
const taskCapabilities = {
|
|
2669
2707
|
channel: "voice",
|
|
@@ -4401,6 +4439,7 @@ cannot be established from TypeScript structure alone.
|
|
|
4401
4439
|
| Helper | Contract checked |
|
|
4402
4440
|
| --- | --- |
|
|
4403
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. |
|
|
4404
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. |
|
|
4405
4444
|
| `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `exerciseAdapter` result. |
|
|
4406
4445
|
| `assertAuthenticationRestoreAndExpiry(states)` | A restored authenticated session can refresh and ends in expiry. Every state is validated. |
|