@xema/omni-protocol 0.1.11 → 0.1.13
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 +4 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +12 -0
- package/dist/testing.d.ts +21 -1
- package/dist/testing.js +137 -5
- package/dist/validation.js +9 -0
- package/guide.md +23 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,7 +58,10 @@ validateSnapshot(snapshot, manifest, "snapshot", { self: identity.id, capabiliti
|
|
|
58
58
|
`exerciseAdapter` validates the manifest, opens an authenticated session, connects, checks
|
|
59
59
|
required capability methods, subscribes, validates the snapshot, every delivered event, and every
|
|
60
60
|
authentication state published during the run — against the latest login — states a capacity,
|
|
61
|
-
then unsubscribes and disconnects.
|
|
61
|
+
then unsubscribes and disconnects. `result.notExercised` names what the run never reached — each
|
|
62
|
+
optional part of a task, of the break state and roster, each contribution, each event type — so a
|
|
63
|
+
clean result is read for what it covers and not for the whole contract; `assertReached(result,
|
|
64
|
+
subjects)` is the paired assertion.
|
|
62
65
|
|
|
63
66
|
```ts
|
|
64
67
|
const result = await exerciseAdapter(adapter, context, { collectOnly: true });
|
package/dist/index.d.ts
CHANGED
|
@@ -137,7 +137,11 @@ export interface AuthenticationFailure {
|
|
|
137
137
|
}
|
|
138
138
|
/** What a lead may do with their team. Declared by presence. */
|
|
139
139
|
export interface TeamCapabilities {
|
|
140
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* This lead may act on their team's breaks through `executeTeamBreak` -- place, release, decide,
|
|
142
|
+
* set policy -- as far as the provider supports; a command it lacks answers
|
|
143
|
+
* `omni.capability-not-enabled`. Requires `executeTeamBreak`.
|
|
144
|
+
*/
|
|
141
145
|
breakControl?: true;
|
|
142
146
|
/** This lead may join a member's call on request. Requires `executeTeamConsult`. */
|
|
143
147
|
consultControl?: true;
|
|
@@ -998,6 +1002,13 @@ export interface BrowserSessionKeyInput {
|
|
|
998
1002
|
taskType: string;
|
|
999
1003
|
browser: TaskBrowser;
|
|
1000
1004
|
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Whether two logins declare the same capabilities, field by field. The comparison an adapter
|
|
1007
|
+
* makes before republishing `authenticated`: capabilities are current, not fixed, and the natural
|
|
1008
|
+
* guard -- comparing the identity -- never fires on a demotion, because the thing that changed is
|
|
1009
|
+
* not the thing being compared. Key order does not matter, and `team: {}` is not `team` absent.
|
|
1010
|
+
*/
|
|
1011
|
+
export declare function sameCapabilities(a: SessionCapabilities, b: SessionCapabilities): boolean;
|
|
1001
1012
|
/**
|
|
1002
1013
|
* The storage-profile key a reusing browser shares, or `undefined` where it shares nothing.
|
|
1003
1014
|
*
|
package/dist/index.js
CHANGED
|
@@ -128,6 +128,18 @@ export const HANDLING_STEPS_WITH_A_PERSON = [
|
|
|
128
128
|
export function handlingStepExpectsAPerson(step) {
|
|
129
129
|
return HANDLING_STEPS_WITH_A_PERSON.includes(step);
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Whether two logins declare the same capabilities, field by field. The comparison an adapter
|
|
133
|
+
* makes before republishing `authenticated`: capabilities are current, not fixed, and the natural
|
|
134
|
+
* guard -- comparing the identity -- never fires on a demotion, because the thing that changed is
|
|
135
|
+
* not the thing being compared. Key order does not matter, and `team: {}` is not `team` absent.
|
|
136
|
+
*/
|
|
137
|
+
export function sameCapabilities(a, b) {
|
|
138
|
+
return a.breaks === b.breaks &&
|
|
139
|
+
(a.team === undefined) === (b.team === undefined) &&
|
|
140
|
+
a.team?.breakControl === b.team?.breakControl &&
|
|
141
|
+
a.team?.consultControl === b.team?.consultControl;
|
|
142
|
+
}
|
|
131
143
|
/**
|
|
132
144
|
* The storage-profile key a reusing browser shares, or `undefined` where it shares nothing.
|
|
133
145
|
*
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type TaskCompletion, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext, type Manifest } from "./index.js";
|
|
1
|
+
import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type TaskCompletion, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext, 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
|
+
/**
|
|
5
|
+
* A part of the contract a run may never reach: state nothing obliges an adapter to publish, so
|
|
6
|
+
* a fixture without it exercises none of its rules and passes clean. One subject per family of
|
|
7
|
+
* rules -- each optional part of a task, each optional part of the break state and roster, each
|
|
8
|
+
* declared contribution, and each event type.
|
|
9
|
+
*/
|
|
10
|
+
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.handlingHistory", "task.consultation", "task.lead", "task.assisting", "task.dispositions", "task.destinations", "task.custom", "break.reasons", "break.imposed", "team.members", "team.requests", "contacts", "scheduledActivities"];
|
|
11
|
+
export type ContractSubject = (typeof STATE_SUBJECTS)[number] | `event.${ProviderEvent["type"]}`;
|
|
4
12
|
export interface AdapterContractResult {
|
|
5
13
|
events: ProviderEventEnvelope[];
|
|
6
14
|
/** The state the session was restored with at sign-in. */
|
|
@@ -12,6 +20,13 @@ export interface AdapterContractResult {
|
|
|
12
20
|
login: AuthenticationState;
|
|
13
21
|
/** True only when every unsubscribe, `disconnect()`, and `close()` settled without throwing. */
|
|
14
22
|
disconnectWasClean: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* What the run never reached, and so what a clean `violations` says nothing about. Nothing here
|
|
25
|
+
* is a violation -- an adapter with no team has nothing to exercise -- but a fixture with no
|
|
26
|
+
* tasks exercises no task rule, and an adapter's own test asserts that the subjects it meant to
|
|
27
|
+
* reach are absent from this list.
|
|
28
|
+
*/
|
|
29
|
+
notExercised: readonly ContractSubject[];
|
|
15
30
|
/** Every violation observed. Non-empty only when `collectOnly` suppressed the throw. */
|
|
16
31
|
violations: readonly ProtocolViolation[];
|
|
17
32
|
}
|
|
@@ -59,6 +74,11 @@ export declare function assertCommandRefusedAfterWithdrawal(result: {
|
|
|
59
74
|
code: string;
|
|
60
75
|
};
|
|
61
76
|
}): void;
|
|
77
|
+
/**
|
|
78
|
+
* Throws unless the run reached every subject named: the paired assertion beside a clean result,
|
|
79
|
+
* so a fixture that never produced a roster cannot pass a test that meant to check one.
|
|
80
|
+
*/
|
|
81
|
+
export declare function assertReached(result: AdapterContractResult, subjects: readonly ContractSubject[]): void;
|
|
62
82
|
/** Validates duplicate delivery and returns the event sequence Omni applies once per ID. */
|
|
63
83
|
export declare function assertDuplicateEventDelivery<C extends Channel>(envelopes: readonly ProviderEventEnvelope<C>[]): ProviderEventEnvelope<C>[];
|
|
64
84
|
/** Validates an authoritative reconnect snapshot containing assignments missed while offline. */
|
package/dist/testing.js
CHANGED
|
@@ -1,6 +1,128 @@
|
|
|
1
|
-
import { browserSessionKey, } from "./index.js";
|
|
1
|
+
import { browserSessionKey, sameCapabilities, } from "./index.js";
|
|
2
2
|
import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateManifest, validateSnapshot, } from "./validation.js";
|
|
3
3
|
export { ProtocolConformanceError, assertNoViolations } from "./validation.js";
|
|
4
|
+
/**
|
|
5
|
+
* A part of the contract a run may never reach: state nothing obliges an adapter to publish, so
|
|
6
|
+
* a fixture without it exercises none of its rules and passes clean. One subject per family of
|
|
7
|
+
* rules -- each optional part of a task, each optional part of the break state and roster, each
|
|
8
|
+
* declared contribution, and each event type.
|
|
9
|
+
*/
|
|
10
|
+
const STATE_SUBJECTS = [
|
|
11
|
+
"tasks",
|
|
12
|
+
"task.browsers",
|
|
13
|
+
"task.attributes",
|
|
14
|
+
"task.handlingHistory",
|
|
15
|
+
"task.consultation",
|
|
16
|
+
"task.lead",
|
|
17
|
+
"task.assisting",
|
|
18
|
+
"task.dispositions",
|
|
19
|
+
"task.destinations",
|
|
20
|
+
"task.custom",
|
|
21
|
+
"break.reasons",
|
|
22
|
+
"break.imposed",
|
|
23
|
+
"team.members",
|
|
24
|
+
"team.requests",
|
|
25
|
+
"contacts",
|
|
26
|
+
"scheduledActivities",
|
|
27
|
+
];
|
|
28
|
+
// Pinned to the event union the way validation pins its closed sets: a type added to
|
|
29
|
+
// `ProviderEvent` without a row here, or a row it lacks, is a compile error.
|
|
30
|
+
const EVENT_TYPES = {
|
|
31
|
+
snapshot: true, "provider-status": true, "break-state": true, "task-offered": true, "task-updated": true,
|
|
32
|
+
"task-media-ended": true, "task-ended": true, announcement: true, "provider-summary": true,
|
|
33
|
+
"team-updated": true, "contacts-updated": true, "calendar-updated": true,
|
|
34
|
+
};
|
|
35
|
+
const CONTRACT_SUBJECTS = [
|
|
36
|
+
...STATE_SUBJECTS,
|
|
37
|
+
...Object.keys(EVENT_TYPES).map(type => `event.${type}`),
|
|
38
|
+
];
|
|
39
|
+
// What the run observed. The input is untrusted and has already been reported on, so nothing
|
|
40
|
+
// here assumes its shape; a subject is reached only by something the element rules would see.
|
|
41
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
42
|
+
const some = (value) => Array.isArray(value) && value.length > 0;
|
|
43
|
+
function observeTask(value, seen) {
|
|
44
|
+
if (!isRecord(value))
|
|
45
|
+
return;
|
|
46
|
+
seen.add("tasks");
|
|
47
|
+
if (some(value.browsers))
|
|
48
|
+
seen.add("task.browsers");
|
|
49
|
+
if (some(value.attributes))
|
|
50
|
+
seen.add("task.attributes");
|
|
51
|
+
if (some(value.handlingHistory))
|
|
52
|
+
seen.add("task.handlingHistory");
|
|
53
|
+
if (value.consultation !== undefined)
|
|
54
|
+
seen.add("task.consultation");
|
|
55
|
+
if (value.lead !== undefined)
|
|
56
|
+
seen.add("task.lead");
|
|
57
|
+
if (value.assisting !== undefined)
|
|
58
|
+
seen.add("task.assisting");
|
|
59
|
+
const capabilities = isRecord(value.capabilities) ? value.capabilities : {};
|
|
60
|
+
if (isRecord(capabilities.dispositions))
|
|
61
|
+
seen.add("task.dispositions");
|
|
62
|
+
if (isRecord(capabilities.blindTransfer) && some(capabilities.blindTransfer.destinations))
|
|
63
|
+
seen.add("task.destinations");
|
|
64
|
+
if (some(capabilities.custom))
|
|
65
|
+
seen.add("task.custom");
|
|
66
|
+
}
|
|
67
|
+
function observeBreak(value, seen) {
|
|
68
|
+
if (!isRecord(value))
|
|
69
|
+
return;
|
|
70
|
+
if (some(value.reasons))
|
|
71
|
+
seen.add("break.reasons");
|
|
72
|
+
if (value.imposed !== undefined)
|
|
73
|
+
seen.add("break.imposed");
|
|
74
|
+
}
|
|
75
|
+
function observeTeam(value, seen) {
|
|
76
|
+
if (!isRecord(value))
|
|
77
|
+
return;
|
|
78
|
+
if (some(value.members))
|
|
79
|
+
seen.add("team.members");
|
|
80
|
+
if (some(value.requests))
|
|
81
|
+
seen.add("team.requests");
|
|
82
|
+
}
|
|
83
|
+
function observeSnapshot(value, seen) {
|
|
84
|
+
if (!isRecord(value))
|
|
85
|
+
return;
|
|
86
|
+
if (Array.isArray(value.tasks))
|
|
87
|
+
value.tasks.forEach(task => observeTask(task, seen));
|
|
88
|
+
observeBreak(value.break, seen);
|
|
89
|
+
observeTeam(value.team, seen);
|
|
90
|
+
if (some(value.contacts))
|
|
91
|
+
seen.add("contacts");
|
|
92
|
+
if (some(value.scheduledActivities))
|
|
93
|
+
seen.add("scheduledActivities");
|
|
94
|
+
}
|
|
95
|
+
function observeEvent(envelope, seen) {
|
|
96
|
+
const event = isRecord(envelope) ? envelope.event : undefined;
|
|
97
|
+
if (!isRecord(event))
|
|
98
|
+
return;
|
|
99
|
+
if (typeof event.type === "string" && event.type in EVENT_TYPES)
|
|
100
|
+
seen.add(`event.${event.type}`);
|
|
101
|
+
switch (event.type) {
|
|
102
|
+
case "snapshot":
|
|
103
|
+
observeSnapshot(event.snapshot, seen);
|
|
104
|
+
break;
|
|
105
|
+
case "break-state":
|
|
106
|
+
observeBreak(event.break, seen);
|
|
107
|
+
break;
|
|
108
|
+
case "task-offered":
|
|
109
|
+
case "task-updated":
|
|
110
|
+
observeTask(event.task, seen);
|
|
111
|
+
break;
|
|
112
|
+
case "team-updated":
|
|
113
|
+
observeTeam(event.team, seen);
|
|
114
|
+
break;
|
|
115
|
+
case "contacts-updated":
|
|
116
|
+
if (some(event.contacts))
|
|
117
|
+
seen.add("contacts");
|
|
118
|
+
break;
|
|
119
|
+
case "calendar-updated":
|
|
120
|
+
if (some(event.scheduledActivities))
|
|
121
|
+
seen.add("scheduledActivities");
|
|
122
|
+
break;
|
|
123
|
+
default: break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
4
126
|
/**
|
|
5
127
|
* Adapter conformance exercise: validates the manifest, opens an authenticated session,
|
|
6
128
|
* connects, checks that every method the declarations require is implemented, subscribes,
|
|
@@ -16,6 +138,7 @@ export { ProtocolConformanceError, assertNoViolations } from "./validation.js";
|
|
|
16
138
|
export async function exerciseAdapter(adapter, context, options = {}) {
|
|
17
139
|
const violations = [...validateManifest(adapter.manifest)];
|
|
18
140
|
const events = [];
|
|
141
|
+
const seen = new Set();
|
|
19
142
|
const storedSecrets = new Map();
|
|
20
143
|
const authentication = await adapter.createAuthenticationSession({
|
|
21
144
|
...context,
|
|
@@ -104,6 +227,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
104
227
|
requireMethod(live, "openMedia", "the manifest channel is voice");
|
|
105
228
|
const eventIds = new Set();
|
|
106
229
|
unsubscribe = connection.subscribe(envelope => {
|
|
230
|
+
observeEvent(envelope, seen);
|
|
107
231
|
violations.push(...validateEventEnvelope(envelope, adapter.manifest, "event", reader()));
|
|
108
232
|
if (typeof envelope?.id === "string") {
|
|
109
233
|
if (eventIds.has(envelope.id))
|
|
@@ -113,6 +237,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
113
237
|
events.push(envelope);
|
|
114
238
|
});
|
|
115
239
|
const snapshot = await connection.snapshot();
|
|
240
|
+
observeSnapshot(snapshot, seen);
|
|
116
241
|
violations.push(...validateSnapshot(snapshot, adapter.manifest, "snapshot", reader()));
|
|
117
242
|
requireCapabilityMethods(live, current().capabilities);
|
|
118
243
|
if (publishesUserIds(snapshot))
|
|
@@ -169,6 +294,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
169
294
|
events: events,
|
|
170
295
|
authenticationState: authenticationState,
|
|
171
296
|
login: (login ?? authenticationState),
|
|
297
|
+
notExercised: CONTRACT_SUBJECTS.filter(subject => !seen.has(subject)),
|
|
172
298
|
disconnectWasClean,
|
|
173
299
|
violations,
|
|
174
300
|
};
|
|
@@ -187,10 +313,6 @@ function publishesUserIds(snapshot) {
|
|
|
187
313
|
return false;
|
|
188
314
|
return snapshot.tasks.some(task => Array.isArray(task?.handlingHistory) && task.handlingHistory.some(step => step?.by !== undefined));
|
|
189
315
|
}
|
|
190
|
-
const sameCapabilities = (a, b) => a.breaks === b.breaks &&
|
|
191
|
-
(a.team === undefined) === (b.team === undefined) &&
|
|
192
|
-
a.team?.breakControl === b.team?.breakControl &&
|
|
193
|
-
a.team?.consultControl === b.team?.consultControl;
|
|
194
316
|
/**
|
|
195
317
|
* `refreshing` carries the identity and capabilities of the login it refreshes. A change to
|
|
196
318
|
* either is published as `authenticated`; a different identity is a new login.
|
|
@@ -296,6 +418,16 @@ export function assertCommandRefusedAfterWithdrawal(result) {
|
|
|
296
418
|
throw new Error(`A command after its capability was withdrawn fails with omni.capability-not-enabled, not ${result.failure?.code}`);
|
|
297
419
|
}
|
|
298
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* Throws unless the run reached every subject named: the paired assertion beside a clean result,
|
|
423
|
+
* so a fixture that never produced a roster cannot pass a test that meant to check one.
|
|
424
|
+
*/
|
|
425
|
+
export function assertReached(result, subjects) {
|
|
426
|
+
const missed = subjects.filter(subject => result.notExercised.includes(subject));
|
|
427
|
+
if (missed.length > 0) {
|
|
428
|
+
throw new Error(`The exercise never reached ${missed.join(", ")}: its clean result says nothing about them`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
299
431
|
/** Validates duplicate delivery and returns the event sequence Omni applies once per ID. */
|
|
300
432
|
export function assertDuplicateEventDelivery(envelopes) {
|
|
301
433
|
const byId = new Map();
|
package/dist/validation.js
CHANGED
|
@@ -811,6 +811,15 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
|
|
|
811
811
|
// Presence is the permission, and it cuts both ways: data a provider never declared a
|
|
812
812
|
// capability for is data Omni would show against a control the agent does not have.
|
|
813
813
|
const idle = isPlainObject(manifest) && isPlainObject(manifest.idleCapabilities) ? manifest.idleCapabilities : {};
|
|
814
|
+
// And it cuts the other way too: a declared contribution is required, `[]` included. A snapshot
|
|
815
|
+
// that omits one the manifest declares has not cleared it, it has said nothing, and Omni would
|
|
816
|
+
// go on showing whatever it held.
|
|
817
|
+
if (snapshot.contacts === undefined && idle.contacts === true) {
|
|
818
|
+
into.add("snapshot.contacts.required", `${path}.contacts`, "the manifest declares contacts, so every snapshot carries the contribution: [] when there are none");
|
|
819
|
+
}
|
|
820
|
+
if (snapshot.scheduledActivities === undefined && idle.calendar === true) {
|
|
821
|
+
into.add("snapshot.calendar.required", `${path}.scheduledActivities`, "the manifest declares calendar, so every snapshot carries the contribution: [] when there are none");
|
|
822
|
+
}
|
|
814
823
|
if (snapshot.contacts !== undefined) {
|
|
815
824
|
into.require(idle.contacts === true, "snapshot.contacts.capability", `${path}.contacts`, "contacts require the contacts idle capability");
|
|
816
825
|
if (Array.isArray(snapshot.contacts)) {
|
package/guide.md
CHANGED
|
@@ -1352,7 +1352,7 @@ them from what arrives later.
|
|
|
1352
1352
|
| --- | --- |
|
|
1353
1353
|
| `breaks` | This login may request a break. Requires the four break methods on the connection. |
|
|
1354
1354
|
| `team` | This login leads a team. The provider publishes a `TeamRoster` to it on every snapshot — `[]` when nobody is in it — and to nobody else. |
|
|
1355
|
-
| `team.breakControl` | This lead
|
|
1355
|
+
| `team.breakControl` | This lead may act on their team's breaks through `executeTeamBreak` — place, release, decide, set policy — as far as the provider supports; a command it lacks answers `omni.capability-not-enabled`. Omni asks for a decision only against a member whose `break` is `awaiting-decision`, so a provider that grants on request is never asked to decide. Requires `executeTeamBreak`. |
|
|
1356
1356
|
| `team.consultControl` | This lead may join a member's call on request. Requires `executeTeamConsult`. |
|
|
1357
1357
|
|
|
1358
1358
|
A session action is available only when both the capability and Omni provisioning permit it.
|
|
@@ -1366,7 +1366,10 @@ roster arrives, and nothing for anybody else — and withdraws it on the next re
|
|
|
1366
1366
|
capability goes. A command that arrives after its capability was withdrawn is answered `failed`
|
|
1367
1367
|
with `omni.capability-not-enabled`: the provider names it, so Omni never has to infer from a
|
|
1368
1368
|
capability change it may not have rendered yet that "you are no longer a lead" is the message
|
|
1369
|
-
rather than "that did not work".
|
|
1369
|
+
rather than "that did not work". One trap for adapter authors: the natural guard on a republish
|
|
1370
|
+
compares the identity, and a demotion does not touch it — compare the capabilities, field by
|
|
1371
|
+
field, which is what `sameCapabilities()` does. The thing that changed is not the thing you are
|
|
1372
|
+
comparing, and `assertCapabilityWithdrawal` is the test that catches a guard which never fires.
|
|
1370
1373
|
|
|
1371
1374
|
### Starting authentication
|
|
1372
1375
|
|
|
@@ -3138,6 +3141,12 @@ Use it for every `UserId` — `handlingHistory[].by`, roster members, `memberId`
|
|
|
3138
3141
|
lead command, `ImposedBreak.by`. A bare one is only ever compared against another from the **same** provider; anything
|
|
3139
3142
|
wider goes through this key.
|
|
3140
3143
|
|
|
3144
|
+
### `sameCapabilities(a, b)`
|
|
3145
|
+
|
|
3146
|
+
Whether two logins declare the same capabilities, field by field — key order aside, and with
|
|
3147
|
+
`team: {}` distinct from `team` absent. It is the comparison an adapter makes before republishing
|
|
3148
|
+
`authenticated`, and the one `exerciseAdapter` holds `refreshing` to.
|
|
3149
|
+
|
|
3141
3150
|
## Runtime validation
|
|
3142
3151
|
|
|
3143
3152
|
Structural rules in this document are executable through the runtime validators Omni applies to
|
|
@@ -3148,7 +3157,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
3148
3157
|
| --- | --- |
|
|
3149
3158
|
| `validateManifest(manifest)` | Identity, protocol-version interoperability, authentication methods, and idle-capability shapes. |
|
|
3150
3159
|
| `validateTask(task, { channel })` | Identity, channel agreement, phase, completion allowance, capability shapes, custom controls, and browsers. |
|
|
3151
|
-
| `validateSnapshot(snapshot, manifest)` | Status, break state, break reasons, team roster, and every task, contact, and activity, including idle-capability gating. |
|
|
3160
|
+
| `validateSnapshot(snapshot, manifest)` | Status, break state, break reasons, team roster, and every task, contact, and activity, including idle-capability gating both ways: a contribution the manifest never declared is refused, and one it declares is required, `[]` included. |
|
|
3152
3161
|
| `validateEventEnvelope(envelope, manifest)` | Envelope identity, timestamp, and the payload for each event type. |
|
|
3153
3162
|
| `validateContact(contact)` | Contact field shapes and attribute keys. Every field is optional, so this checks what is present rather than what is missing. |
|
|
3154
3163
|
| `validateScheduledActivity(activity)` | Required activity fields and start/end ordering. |
|
|
@@ -3204,6 +3213,15 @@ latest the session published during the run, which differs only when the adapter
|
|
|
3204
3213
|
`authentication.refreshing.identity`, a changed capability set `authentication.refreshing.capabilities`.
|
|
3205
3214
|
A capability granted by a later login requires its methods just as one declared at sign-in does.
|
|
3206
3215
|
|
|
3216
|
+
`result.notExercised` lists what the run never reached — one subject per family of rules: each
|
|
3217
|
+
optional part of a task (`task.browsers`, `task.handlingHistory`, `task.lead`, …), the break's
|
|
3218
|
+
`reasons` and `imposed`, the roster's `members` and `requests`, each declared contribution, and
|
|
3219
|
+
each event type (`event.task-ended`, …) — and so what a clean `violations` says nothing about.
|
|
3220
|
+
Nothing there is a violation: an adapter with no team has nothing to exercise. But a fixture with
|
|
3221
|
+
no tasks exercises no task rule, and a pass over it reads as coverage it is not.
|
|
3222
|
+
`assertReached(result, subjects)` is the paired assertion: it throws naming every subject the run
|
|
3223
|
+
never met, so a test that meant to check a roster cannot pass on a fixture that never produced one.
|
|
3224
|
+
|
|
3207
3225
|
Three properties of the harness matter to adapter authors:
|
|
3208
3226
|
|
|
3209
3227
|
- **Violations are collected, never thrown from inside the subscribe listener.** Throwing there
|
|
@@ -3224,7 +3242,8 @@ cannot be established from TypeScript structure alone.
|
|
|
3224
3242
|
| Helper | Contract checked |
|
|
3225
3243
|
| --- | --- |
|
|
3226
3244
|
| `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. |
|
|
3227
|
-
| `assertCommandRefusedAfterWithdrawal(result)` | A command that arrives after its capability was withdrawn fails with `omni.capability-not-enabled`, named by the provider. |
|
|
3245
|
+
| `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. |
|
|
3246
|
+
| `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `exerciseAdapter` result. |
|
|
3228
3247
|
| `assertAuthenticationRestoreAndExpiry(states)` | A restored authenticated session can refresh and ends in expiry. Every state is validated. |
|
|
3229
3248
|
| `assertReconnectWithMissedAssignments(before, reconnect, ids)` | A reconnect snapshot restores assignments received while offline. |
|
|
3230
3249
|
| `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
|