@xema/omni-protocol 0.1.111 → 0.1.113

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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { browserSessionKey, effectiveLevels, sameCapabilities, } from "./index.js";
2
- import { assertNoViolations, validateAuthenticationState, validateBreakTransition, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateUserDetails, observeRules, ruleEvaluated, validateHostMute, validateHostRecording, validateLoginStore, validateHistoryReport, validateManifest, validateResult, validateSnapshot, validateTask, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
2
+ import { assertNoViolations, validateAuthenticationState, validateBreakTransition, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateUserDetails, observeRules, ruleTested, validateHostMute, validateHostRecording, validateLoginStore, validateHistoryReport, 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
@@ -212,9 +212,9 @@ function observeEvent(envelope, seen) {
212
212
  * for a synchronous emitter, and is swallowed as an unhandled rejection for an
213
213
  * asynchronous one — which would let a non-conforming async adapter pass.
214
214
  */
215
- export async function exerciseAdapter(adapter, context, options = {}) {
216
- const rulesEvaluated = new Set();
217
- const stopObserving = observeRules(rule => { rulesEvaluated.add(rule); });
215
+ export async function testAdapter(adapter, context, options = {}) {
216
+ const rulesTested = new Set();
217
+ const stopObserving = observeRules(rule => { rulesTested.add(rule); });
218
218
  const violations = [...validateManifest(adapter.manifest)];
219
219
  const events = [];
220
220
  const seen = new Set();
@@ -388,7 +388,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
388
388
  const tellRefused = (report) => {
389
389
  if (typeof live.refused !== "function")
390
390
  return;
391
- ruleEvaluated("connection.refused.rejected");
391
+ ruleTested("connection.refused.rejected");
392
392
  try {
393
393
  live.refused(report);
394
394
  }
@@ -398,7 +398,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
398
398
  };
399
399
  const eventPayloads = new Map();
400
400
  let capacityStated;
401
- // The drive waits on events: each waiter is offered every envelope as it lands.
401
+ // The test waits on events: each waiter is offered every envelope as it lands.
402
402
  const waiters = new Set();
403
403
  const onEnvelope = (envelope) => {
404
404
  observeEvent(envelope, seen);
@@ -408,10 +408,10 @@ export async function exerciseAdapter(adapter, context, options = {}) {
408
408
  tellRefused({ artefact: "event", envelopeId: typeof envelope?.id === "string" ? envelope.id : undefined, violations: broken });
409
409
  // A diagnostic is informational to a host and a failure to a conformance run: the platform
410
410
  // under test broke a rule the adapter relies on, and a green result must not paper over it.
411
- // A reload in life has no first connection: the page is gone. The drive keeps its first
411
+ // A reload in life has no first connection: the page is gone. The test keeps its first
412
412
  // connection up while the second reads, so a platform that pushes the open task to the new
413
413
  // client pushes it to the old one too, and an adapter that calls a re-offer of answered work a
414
- // defect is right to. That diagnostic is the drive's own artefact, and is not counted while the
414
+ // defect is right to. That diagnostic is the test's own artefact, and is not counted while the
415
415
  // second adapter is up; every other moment it is.
416
416
  if (isRecord(envelope?.event) && envelope.event.type === "diagnostic") {
417
417
  violations.push({ rule: "diagnostic.raised", path: "event.diagnostic",
@@ -469,7 +469,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
469
469
  waiter(envelope);
470
470
  };
471
471
  unsubscribe = connection.subscribe(onEnvelope);
472
- // A host reload is the first client dying and a second coming up in its place. The drive hands
472
+ // A host reload is the first client dying and a second coming up in its place. The test hands
473
473
  // the run over: the first connection is unsubscribed, disconnected and its session closed before
474
474
  // the second connects, and the run continues on the second -- so there is no first client for a
475
475
  // platform to push the open task back to, and nothing to exempt.
@@ -519,8 +519,8 @@ export async function exerciseAdapter(adapter, context, options = {}) {
519
519
  // holds its connect snapshot to no team until it is sent again, after its capacity.
520
520
  leadFeaturesOn = false;
521
521
  const restated = await second.setCapacity({ count: capacityStated ?? 1 });
522
- violations.push(...validateResult(restated, "setCapacity", "drive.reload.setCapacity"));
523
- await switchOnLeadFeatures(second, "drive.reload");
522
+ violations.push(...validateResult(restated, "setCapacity", "test.reload.setCapacity"));
523
+ await switchOnLeadFeatures(second, "test.reload");
524
524
  },
525
525
  };
526
526
  // Every snapshot the run reads, the connect snapshot and a reloaded client's, is read the same
@@ -578,7 +578,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
578
578
  continue;
579
579
  }
580
580
  // Evaluated where a superseded event was held: with none, the question was never asked.
581
- ruleEvaluated("snapshot.accounts.task", "snapshot.accounts.ended");
581
+ ruleTested("snapshot.accounts.task", "snapshot.accounts.ended");
582
582
  if ((event.type === "task-offered" || event.type === "task-updated") && isRecord(event.task) && typeof event.task.assignmentId === "string" && !carried.has(event.task.assignmentId)) {
583
583
  violations.push({ rule: "snapshot.accounts.task", path: "snapshot.tasks",
584
584
  message: `${String(event.task.assignmentId)} (${event.task.assignmentId}) was published while the snapshot was read and the snapshot does not carry it: a snapshot accounts for everything the adapter emitted before it resolved` });
@@ -596,7 +596,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
596
596
  // ready, and on every change. An adapter that never asked cannot have.
597
597
  // The obligation has two halves: read the report before declaring the agent ready, and hear of
598
598
  // every change after. An adapter that did one and not the other consulted the host once, or never.
599
- ruleEvaluated("connection.host.consulted", "connection.host.subscribed");
599
+ ruleTested("connection.host.consulted", "connection.host.subscribed");
600
600
  if (adapter.manifest.channel === "voice" && !consulted.report) {
601
601
  violations.push({ rule: "connection.host.consulted", path: "connection.host",
602
602
  message: "a voice adapter reads the host's report before it declares the agent ready to its platform, and this one never asked" });
@@ -642,8 +642,8 @@ export async function exerciseAdapter(adapter, context, options = {}) {
642
642
  if (switched === undefined)
643
643
  return;
644
644
  violations.push(...validateResult(switched, "executeTeam", `${at}.executeTeam`));
645
- ruleEvaluated("team.required");
646
- const ms = options.driveTimeoutMs ?? 5000;
645
+ ruleTested("team.required");
646
+ const ms = options.timeoutMs ?? 5000;
647
647
  const isBaseline = (envelope) => isRecord(envelope?.event) && envelope.event.type === "team-updated";
648
648
  const baseline = events.slice(from).some(isBaseline) || await new Promise(resolve => {
649
649
  const timer = setTimeout(() => { waiters.delete(waiter); resolve(false); }, ms);
@@ -661,19 +661,19 @@ export async function exerciseAdapter(adapter, context, options = {}) {
661
661
  message: `the application switched the team feature on and no team-updated followed within ${ms}ms: the whole team is owed once it is on, members: [] when nobody is in it` });
662
662
  };
663
663
  await switchOnLeadFeatures(live, "connection");
664
- if (options.drive) {
664
+ if (options.withCall) {
665
665
  const localAudio = isRecord(first) && isRecord(first.audio) && isRecord(first.audio.input) && first.audio.input.status === "available"
666
666
  ? first.audio.input.localAudio : undefined;
667
- violations.push(...await driveOneCall({
667
+ violations.push(...await testOneCall({
668
668
  connection: live, manifest: adapter.manifest, channel: adapter.manifest.channel, softphone, snapshot, events, waiters, stream, localAudio,
669
669
  everHeldForTask: (taskId) => [...watched.everHeld].some(key => namesTask(key, taskId)), snapshotRead,
670
- timeoutMs: options.driveTimeoutMs ?? 5000,
670
+ timeoutMs: options.timeoutMs ?? 5000,
671
671
  context: connected, secrets: authenticationSecrets, reader, rebuild: options.rebuild, held: watched.held, handOver,
672
672
  streams: { stream, breaks, team },
673
673
  }));
674
- // Every offer is owed an ending. The drive ends the call it drove; anything else the
674
+ // Every offer is owed an ending. The test ends the call it drove; anything else the
675
675
  // provider offered and left open is an assignment whose ending never came.
676
- ruleEvaluated("stream.taskOffered.unended");
676
+ ruleTested("stream.taskOffered.unended");
677
677
  for (const id of stream.unended()) {
678
678
  violations.push({ rule: "stream.taskOffered.unended", path: "event.task-ended",
679
679
  message: `${id} was offered and never ended: every offer is owed a task-ended, whatever became of the call` });
@@ -750,10 +750,10 @@ export async function exerciseAdapter(adapter, context, options = {}) {
750
750
  events: events,
751
751
  authenticationState: authenticationState,
752
752
  login: (login ?? authenticationState),
753
- notExercised: CONTRACT_SUBJECTS.filter(subject => !seen.has(subject)),
753
+ notTested: CONTRACT_SUBJECTS.filter(subject => !seen.has(subject)),
754
754
  disconnectWasClean,
755
755
  violations,
756
- rulesEvaluated,
756
+ rulesTested,
757
757
  };
758
758
  }
759
759
  /**
@@ -1036,7 +1036,7 @@ export function assertTaskCapabilityWithdrawal(tasks, manifest, command) {
1036
1036
  * so a fixture that never produced a team member list cannot pass a test that meant to check one.
1037
1037
  */
1038
1038
  export function assertReached(result, subjects) {
1039
- const missed = subjects.filter(subject => result.notExercised.includes(subject));
1039
+ const missed = subjects.filter(subject => result.notTested.includes(subject));
1040
1040
  if (missed.length > 0) {
1041
1041
  throw new Error(`The exercise never reached ${missed.join(", ")}: its clean result says nothing about them`);
1042
1042
  }
@@ -1139,6 +1139,36 @@ export class TaskStream {
1139
1139
  endedAssignments = new Set();
1140
1140
  /** Every assignment a `task-offered` introduced, as against one a snapshot carried: each is owed its ending. */
1141
1141
  offered = new Set();
1142
+ /** The last seconds-left value the stream read per task and field, with the provider instant it was published at. */
1143
+ countdowns = new Map();
1144
+ /**
1145
+ * A seconds-left value is the provider's arithmetic at the instant of the publication, never a
1146
+ * value copied from an earlier one: between two publications the countdown loses at least the
1147
+ * seconds the provider's own clock says passed, within a second of rounding.
1148
+ */
1149
+ holdCountdowns(id, task, publishedAt, at, refuse) {
1150
+ const atMs = typeof publishedAt === "string" ? Date.parse(publishedAt) : Number.NaN;
1151
+ if (!isRecord(task) || Number.isNaN(atMs))
1152
+ return;
1153
+ const known = this.countdowns.get(id) ?? new Map();
1154
+ for (const field of ["expiresInSeconds", "previewEndsInSeconds", "wrapEndsInSeconds"]) {
1155
+ const value = task[field];
1156
+ if (typeof value !== "number") {
1157
+ known.delete(field);
1158
+ continue;
1159
+ }
1160
+ const before = known.get(field);
1161
+ if (before !== undefined) {
1162
+ ruleTested("stream.countdown.copied");
1163
+ const elapsed = Math.floor((atMs - before.atMs) / 1000);
1164
+ if (elapsed > 0 && value > before.value - elapsed + 1) {
1165
+ refuse("stream.countdown.copied", `${at}.${field}`, `${id} said ${field} ${before.value} at ${new Date(before.atMs).toISOString()} and ${value} at ${new Date(atMs).toISOString()}: ${elapsed}s passed on the provider's clock and the countdown lost fewer; seconds left are worked out at each publication, never copied from the one before`);
1166
+ }
1167
+ }
1168
+ known.set(field, { value, atMs });
1169
+ }
1170
+ this.countdowns.set(id, known);
1171
+ }
1142
1172
  // Every dial the stream can place an outcome against: one the host said it placed, or one a
1143
1173
  // task carried on `onCall` or in its record -- which is how a dial made before a take-over is known
1144
1174
  // to whoever holds the task now. `answered` or `ended` once its outcome arrived, since it comes once.
@@ -1235,11 +1265,16 @@ export class TaskStream {
1235
1265
  resync(snapshot, at) {
1236
1266
  const found = [];
1237
1267
  const refuse = (rule, where, message) => found.push({ rule, path: where, message });
1238
- ruleEvaluated("stream.snapshot.capabilitySource", "stream.snapshot.history", "stream.snapshot.phase", "stream.snapshot.audio");
1268
+ ruleTested("stream.snapshot.capabilitySource", "stream.snapshot.history", "stream.snapshot.phase", "stream.snapshot.audio");
1239
1269
  if (isRecord(snapshot) && Array.isArray(snapshot.tasks)) {
1240
1270
  snapshot.tasks.forEach((task, index) => {
1241
1271
  if (!isRecord(task) || typeof task.assignmentId !== "string")
1242
1272
  return;
1273
+ // A snapshot from a provider with a clock is a publication at providerTime; the countdowns it carries are held to it.
1274
+ if (typeof snapshot.providerTime === "string")
1275
+ this.holdCountdowns(task.assignmentId, task, snapshot.providerTime, `${at}.tasks[${index}]`, refuse);
1276
+ else
1277
+ this.countdowns.delete(task.assignmentId);
1243
1278
  const was = this.tasks.get(task.assignmentId);
1244
1279
  if (was === undefined)
1245
1280
  return;
@@ -1296,7 +1331,7 @@ export class TaskStream {
1296
1331
  found.push(...this.resync(event.snapshot, `${at}.snapshot`));
1297
1332
  break;
1298
1333
  case "task-offered": {
1299
- ruleEvaluated("stream.taskOffered.duplicate");
1334
+ ruleTested("stream.taskOffered.duplicate");
1300
1335
  if (id === undefined)
1301
1336
  break;
1302
1337
  // An assignment is introduced once: not while it is open, and never again once it has ended,
@@ -1308,18 +1343,20 @@ export class TaskStream {
1308
1343
  this.offered.add(id);
1309
1344
  this.tasks.set(id, TaskStream.stated(event.task));
1310
1345
  this.noteDials(event.task);
1346
+ this.holdCountdowns(id, event.task, isRecord(envelope) ? envelope.occurredAt : undefined, `${at}.task`, refuse);
1311
1347
  break;
1312
1348
  }
1313
1349
  case "task-updated":
1314
1350
  if (id === undefined)
1315
1351
  break;
1316
- ruleEvaluated("stream.taskUpdated.unknown");
1352
+ ruleTested("stream.taskUpdated.unknown");
1317
1353
  if (known === undefined) {
1318
1354
  refuse("stream.taskUpdated.unknown", `${at}.task.assignmentId`, `${id} was never offered or carried on a snapshot`);
1319
1355
  break;
1320
1356
  }
1321
1357
  // The rules about a known task are evaluated only once there is one.
1322
- ruleEvaluated("stream.taskUpdated.capabilitySource", "stream.taskUpdated.phase", "stream.taskUpdated.audioOpen", "stream.taskUpdated.history", "stream.taskAudioEnded.follow", "stream.taskUpdated.audio", "stream.taskUpdated.stage", "stream.taskUpdated.stage.lingering");
1358
+ this.holdCountdowns(id, event.task, isRecord(envelope) ? envelope.occurredAt : undefined, `${at}.task`, refuse);
1359
+ ruleTested("stream.taskUpdated.capabilitySource", "stream.taskUpdated.phase", "stream.taskUpdated.audioOpen", "stream.taskUpdated.history", "stream.taskAudioEnded.follow", "stream.taskUpdated.audio", "stream.taskUpdated.stage", "stream.taskUpdated.stage.lingering");
1323
1360
  // Terms once read stay read. A re-read that fails is not a new fact about the task, so the
1324
1361
  // last statement stands and the failure is a diagnostic; not-yet-read is a place a task
1325
1362
  // starts from, never one it returns to.
@@ -1351,7 +1388,7 @@ export class TaskStream {
1351
1388
  }
1352
1389
  // A task completes after its audio ends, never around it: an update that moves a task to
1353
1390
  // completing while the stream holds its audio as started is a call whose audio never ended,
1354
- // whoever caused the ending -- the drive's end-call, a take-over, the provider's own hand.
1391
+ // whoever caused the ending -- the test's end-call, a take-over, the provider's own hand.
1355
1392
  {
1356
1393
  const to = isRecord(event.task) ? String(event.task.phase) : "";
1357
1394
  if (to === "completing" && known.audio === "started" && known.phase !== "completing") {
@@ -1391,7 +1428,7 @@ export class TaskStream {
1391
1428
  this.noteDials(event.task);
1392
1429
  break;
1393
1430
  case "task-audio-started":
1394
- ruleEvaluated("stream.taskAudioStarted.beforeWork", "stream.taskAudioStarted.duplicate", "stream.taskAudio.channel", "stream.assignment.ended", "stream.assignment.unknown");
1431
+ ruleTested("stream.taskAudioStarted.beforeWork", "stream.taskAudioStarted.duplicate", "stream.taskAudio.channel", "stream.assignment.ended", "stream.assignment.unknown");
1395
1432
  if (id === undefined)
1396
1433
  break;
1397
1434
  if (known === undefined) {
@@ -1413,7 +1450,7 @@ export class TaskStream {
1413
1450
  known.audio = "started";
1414
1451
  break;
1415
1452
  case "task-audio-ended":
1416
- ruleEvaluated("stream.taskAudioEnded.beforeWork", "stream.taskAudioEnded.silent", "stream.taskAudio.channel", "stream.assignment.ended", "stream.assignment.unknown");
1453
+ ruleTested("stream.taskAudioEnded.beforeWork", "stream.taskAudioEnded.silent", "stream.taskAudio.channel", "stream.assignment.ended", "stream.assignment.unknown");
1417
1454
  if (id === undefined)
1418
1455
  break;
1419
1456
  if (known === undefined) {
@@ -1433,7 +1470,7 @@ export class TaskStream {
1433
1470
  known.audio = "ended";
1434
1471
  break;
1435
1472
  case "task-ended":
1436
- ruleEvaluated("stream.assignment.ended", "stream.assignment.unknown");
1473
+ ruleTested("stream.assignment.ended", "stream.assignment.unknown");
1437
1474
  if (id === undefined)
1438
1475
  break;
1439
1476
  if (known === undefined) {
@@ -1443,7 +1480,7 @@ export class TaskStream {
1443
1480
  // A voice task ends after its audio ends, never around it, whatever the outcome: a take-over
1444
1481
  // and a lead leaving included. An ending with the audio still up leaves the host holding an
1445
1482
  // open audio session and an open leg on a task that no longer exists.
1446
- ruleEvaluated("stream.taskEnded.audioOpen");
1483
+ ruleTested("stream.taskEnded.audioOpen");
1447
1484
  if (known.audio === "started") {
1448
1485
  refuse("stream.taskEnded.audioOpen", `${at}.assignmentId`, `${id} ended with its audio still started: the audio ends first, on task-audio-ended, whatever the outcome`);
1449
1486
  }
@@ -1451,7 +1488,7 @@ export class TaskStream {
1451
1488
  // provider's word that interaction ended and the moment the wrap allowance starts. A provider
1452
1489
  // that completes the task itself with an allowance to run has to have started the clock:
1453
1490
  // completed from in-progress, the allowance it stated was never given.
1454
- ruleEvaluated("stream.taskEnded.unwrapped");
1491
+ ruleTested("stream.taskEnded.unwrapped");
1455
1492
  // An agent's own complete cuts the wrap short from wherever the task stands, so that ending says by agent and is not this.
1456
1493
  if (known.channel !== "voice" && known.completionMode === "provider-automatic" && known.wrapAllowance !== undefined && known.wrapAllowance > 0
1457
1494
  && known.phase !== "completing" && isRecord(event.outcome) && event.outcome.type === "completed" && event.outcome.by === "provider") {
@@ -1464,7 +1501,7 @@ export class TaskStream {
1464
1501
  // An outcome ends a dial somebody placed, once. The task it names may already have ended;
1465
1502
  // a dial placed late routinely outlives its call, which is why the dial has its own identity,
1466
1503
  // and why the outcome names the assignment: the host routes it to that life, ended or not.
1467
- ruleEvaluated("stream.assignment.unknown");
1504
+ ruleTested("stream.assignment.unknown");
1468
1505
  if (typeof event.assignmentId === "string" && !this.assignments.has(event.assignmentId)) {
1469
1506
  refuse("stream.assignment.unknown", `${at}.assignmentId`, `${event.assignmentId} is not an assignment this stream has seen`);
1470
1507
  }
@@ -1490,24 +1527,24 @@ export class TaskStream {
1490
1527
  * One ordinary lifecycle on the first task offered, each step held to the rules a host holds a
1491
1528
  * provider to: the command validated against the task as published, the result validated for the
1492
1529
  * method, and the event the provider owes in return awaited -- a step that never arrives is a
1493
- * violation naming what was owed. The drive stops where the task offers no way on (no `endCall`,
1530
+ * violation naming what was owed. The test stops where the task offers no way on (no `endCall`,
1494
1531
  * a provider that completes for itself) and says nothing about what it could not reach.
1495
1532
  */
1496
- async function driveOneCall(drive) {
1533
+ async function testOneCall(call) {
1497
1534
  const found = [];
1498
1535
  const refuse = (rule, path, message) => found.push({ rule, path, message });
1499
1536
  const isTask = (value) => isRecord(value) && typeof value.assignmentId === "string";
1500
1537
  // Wait for an envelope that satisfies `matches`, looking first at what already arrived.
1501
- const waitFor = (what, matches, from, within = { ms: drive.timeoutMs, onExpiry: () => refuse("drive.timeout", "drive", `the provider owed ${what} within ${drive.timeoutMs}ms and it never arrived`) }) => new Promise(resolve => {
1502
- for (let index = from; index < drive.events.length; index += 1) {
1503
- const hit = matches(drive.events[index]);
1538
+ const waitFor = (what, matches, from, within = { ms: call.timeoutMs, onExpiry: () => refuse("test.timeout", "test", `the provider owed ${what} within ${call.timeoutMs}ms and it never arrived`) }) => new Promise(resolve => {
1539
+ for (let index = from; index < call.events.length; index += 1) {
1540
+ const hit = matches(call.events[index]);
1504
1541
  if (hit !== undefined) {
1505
1542
  resolve({ found: hit, at: index + 1 });
1506
1543
  return;
1507
1544
  }
1508
1545
  }
1509
1546
  const timer = setTimeout(() => {
1510
- drive.waiters.delete(waiter);
1547
+ call.waiters.delete(waiter);
1511
1548
  within.onExpiry();
1512
1549
  resolve(undefined);
1513
1550
  }, within.ms);
@@ -1516,45 +1553,45 @@ async function driveOneCall(drive) {
1516
1553
  if (hit === undefined)
1517
1554
  return;
1518
1555
  clearTimeout(timer);
1519
- drive.waiters.delete(waiter);
1520
- resolve({ found: hit, at: drive.events.length });
1556
+ call.waiters.delete(waiter);
1557
+ resolve({ found: hit, at: call.events.length });
1521
1558
  };
1522
- drive.waiters.add(waiter);
1559
+ call.waiters.add(waiter);
1523
1560
  });
1524
1561
  // The call's audio session, opened on the first client and opened again on a reloaded second.
1525
1562
  let session;
1526
1563
  const recordSurvivesReload = async (at) => {
1527
1564
  // Each reload rule is marked where it is decided, never up front: a rebuild that throws has looked at nothing else.
1528
- ruleEvaluated("drive.reload.rejected");
1565
+ ruleTested("test.reload.rejected");
1529
1566
  let again;
1530
1567
  try {
1531
- again = drive.rebuild();
1568
+ again = call.rebuild();
1532
1569
  }
1533
1570
  catch (error) {
1534
- refuse("drive.reload.rejected", "drive.reload", `building the adapter again threw: ${String(error)}`);
1571
+ refuse("test.reload.rejected", "test.reload", `building the adapter again threw: ${String(error)}`);
1535
1572
  return;
1536
1573
  }
1537
1574
  // The same provider, built again: a different manifest is a different adapter, and proves nothing about this one.
1538
- ruleEvaluated("drive.reload.manifest");
1539
- if (again.manifest.id !== drive.manifest.id) {
1540
- refuse("drive.reload.manifest", "drive.reload.manifest", `rebuild returned an adapter for ${String(again.manifest.id)}; the reload is of ${String(drive.manifest.id)}`);
1575
+ ruleTested("test.reload.manifest");
1576
+ if (again.manifest.id !== call.manifest.id) {
1577
+ refuse("test.reload.manifest", "test.reload.manifest", `rebuild returned an adapter for ${String(again.manifest.id)}; the reload is of ${String(call.manifest.id)}`);
1541
1578
  return;
1542
1579
  }
1543
1580
  // The first client dies first, as it does on a reload: from here the run has no connection until the second stands.
1544
- ruleEvaluated("drive.reload.handover");
1545
- if (!await drive.handOver.down()) {
1546
- refuse("drive.reload.handover", "drive.reload", "taking the first client down threw: an unsubscribe, disconnect() or close() failed");
1581
+ ruleTested("test.reload.handover");
1582
+ if (!await call.handOver.down()) {
1583
+ refuse("test.reload.handover", "test.reload", "taking the first client down threw: an unsubscribe, disconnect() or close() failed");
1547
1584
  }
1548
1585
  try {
1549
- const restoredSession = await again.createAuthenticationSession({ ...drive.context, secrets: drive.secrets });
1586
+ const restoredSession = await again.createAuthenticationSession({ ...call.context, secrets: call.secrets });
1550
1587
  // The reload is a restore before it is anything else: the second session stands authenticated
1551
1588
  // as the same person, from the secrets alone, or nothing it reads afterwards is this login's.
1552
1589
  const restored = await restoredSession.state();
1553
- found.push(...validateAuthenticationState(restored, "drive.reload.login"));
1554
- ruleEvaluated("drive.reload.login");
1555
- const same = isRecord(restored) && restored.status === "authenticated" && isRecord(restored.identity) && restored.identity.id === drive.reader().self;
1590
+ found.push(...validateAuthenticationState(restored, "test.reload.login"));
1591
+ ruleTested("test.reload.login");
1592
+ const same = isRecord(restored) && restored.status === "authenticated" && isRecord(restored.identity) && restored.identity.id === call.reader().self;
1556
1593
  if (!same) {
1557
- refuse("drive.reload.login", "drive.reload.login", `a second adapter built from the same login and secrets did not restore it: the session says ${String(isRecord(restored) ? restored.status : restored)}${isRecord(restored) && isRecord(restored.identity) ? ` as ${String(restored.identity.id)}` : ""}, and the login is ${String(drive.reader().self)}`);
1594
+ refuse("test.reload.login", "test.reload.login", `a second adapter built from the same login and secrets did not restore it: the session says ${String(isRecord(restored) ? restored.status : restored)}${isRecord(restored) && isRecord(restored.identity) ? ` as ${String(restored.identity.id)}` : ""}, and the login is ${String(call.reader().self)}`);
1558
1595
  // A reload that cannot restore the login is a sign-in screen, not a connection: the run ends here.
1559
1596
  try {
1560
1597
  await restoredSession.close();
@@ -1562,27 +1599,27 @@ async function driveOneCall(drive) {
1562
1599
  catch { /* the session that would not restore is closed as far as it can be */ }
1563
1600
  return;
1564
1601
  }
1565
- const second = await again.connect(drive.context);
1602
+ const second = await again.connect(call.context);
1566
1603
  // From here the second client is the run's connection, held to everything the first was on
1567
1604
  // connect: what it publishes is validated as before.
1568
- await drive.handOver.up(restoredSession, second);
1569
- drive.connection = second;
1605
+ await call.handOver.up(restoredSession, second);
1606
+ call.connection = second;
1570
1607
  const snapshot = await second.snapshot();
1571
1608
  // The second adapter's snapshot is read as the connect snapshot was, and the streams take it
1572
1609
  // as the state now the way they take any resync: held to what they knew, then replaced. A reload
1573
1610
  // is a place the stream's rules keep working, not one where they all stop.
1574
- await drive.snapshotRead(second, snapshot, "drive.reload.snapshot");
1575
- found.push(...drive.streams.stream.resync(snapshot, "drive.reload.snapshot"));
1576
- found.push(...drive.streams.breaks.seed(snapshot));
1577
- drive.streams.team.seed(snapshot);
1611
+ await call.snapshotRead(second, snapshot, "test.reload.snapshot");
1612
+ found.push(...call.streams.stream.resync(snapshot, "test.reload.snapshot"));
1613
+ found.push(...call.streams.breaks.seed(snapshot));
1614
+ call.streams.team.seed(snapshot);
1578
1615
  const carried = isRecord(snapshot) && Array.isArray(snapshot.tasks)
1579
1616
  ? snapshot.tasks.find(task => isRecord(task) && task.assignmentId === taskId) : undefined;
1580
- ruleEvaluated("drive.reload.snapshot");
1617
+ ruleTested("test.reload.snapshot");
1581
1618
  if (carried === undefined) {
1582
- refuse("drive.reload.snapshot", "drive.reload.snapshot", `a second adapter built from the same login does not carry ${taskId} on its snapshot, and the task is still open`);
1619
+ refuse("test.reload.snapshot", "test.reload.snapshot", `a second adapter built from the same login does not carry ${taskId} on its snapshot, and the task is still open`);
1583
1620
  return;
1584
1621
  }
1585
- ruleEvaluated("drive.reload.history");
1622
+ ruleTested("test.reload.history");
1586
1623
  const history = carried.history;
1587
1624
  const steps = isRecord(history) && Array.isArray(history.steps) ? history.steps : [];
1588
1625
  // A record once read is not unread across a reload either: every entry the first adapter published is here.
@@ -1591,41 +1628,41 @@ async function driveOneCall(drive) {
1591
1628
  const lost = wasRead.filter(entry => !steps.some(now => isRecord(now) && now.step === entry.step && now.at === entry.at))
1592
1629
  .map(entry => `${String(entry.step)}@${String(entry.at)}`);
1593
1630
  if (lost.length > 0) {
1594
- refuse("drive.reload.history", "drive.reload.history", `a second adapter built from the same login carries ${taskId} without ${lost.join(", ")}, which the first had published: a record once read is not unread`);
1631
+ refuse("test.reload.history", "test.reload.history", `a second adapter built from the same login carries ${taskId} without ${lost.join(", ")}, which the first had published: a record once read is not unread`);
1595
1632
  }
1596
1633
  const leg = steps.find(entry => isRecord(entry) && entry.step === "muted" && entry.at === at);
1597
1634
  if (leg === undefined) {
1598
- refuse("drive.reload.history", "drive.reload.history", `a second adapter built from the same login carries ${taskId} without the muted leg at ${at}: the record was composed in memory and died with the adapter; the login's store is where it lives`);
1635
+ refuse("test.reload.history", "test.reload.history", `a second adapter built from the same login carries ${taskId} without the muted leg at ${at}: the record was composed in memory and died with the adapter; the login's store is where it lives`);
1599
1636
  }
1600
1637
  else if (leg.mutedBy !== "host") {
1601
- refuse("drive.reload.history", "drive.reload.history", `the reloaded record's muted leg at ${at} says mutedBy ${String(leg.mutedBy)}; the host reported host`);
1638
+ refuse("test.reload.history", "test.reload.history", `the reloaded record's muted leg at ${at} says mutedBy ${String(leg.mutedBy)}; the host reported host`);
1602
1639
  }
1603
- // The drive goes on with the task as the reloaded client holds it. The first client's audio
1640
+ // The test goes on with the task as the reloaded client holds it. The first client's audio
1604
1641
  // session died with it: a host opens audio on a task arriving with audio started on a
1605
- // snapshot, so the drive opens it again on the second and holds the answer as it held the first.
1642
+ // snapshot, so the test opens it again on the second and holds the answer as it held the first.
1606
1643
  task = carried;
1607
- if (drive.softphone && carried.audio === "started") {
1644
+ if (call.softphone && carried.audio === "started") {
1608
1645
  if (session !== undefined && typeof session.close === "function") {
1609
1646
  try {
1610
1647
  session.close();
1611
1648
  }
1612
1649
  catch {
1613
- refuse("drive.openAudio.close", "drive.openAudio", "the first client's audio session threw on close");
1650
+ refuse("test.openAudio.close", "test.openAudio", "the first client's audio session threw on close");
1614
1651
  }
1615
1652
  }
1616
1653
  session = undefined;
1617
- ruleEvaluated("drive.reload.openAudio");
1618
- const reopened = await second.openAudio?.({ assignmentId: taskId, localAudio: drive.localAudio });
1619
- const malformed = validateResult(reopened, "openAudio", "drive.reload.openAudio");
1654
+ ruleTested("test.reload.openAudio");
1655
+ const reopened = await second.openAudio?.({ assignmentId: taskId, localAudio: call.localAudio });
1656
+ const malformed = validateResult(reopened, "openAudio", "test.reload.openAudio");
1620
1657
  found.push(...malformed);
1621
1658
  if (malformed.length === 0 && isRecord(reopened) && reopened.status === "opened")
1622
1659
  session = reopened.audio;
1623
1660
  else if (malformed.length === 0)
1624
- refuse("drive.reload.openAudio", "drive.reload.openAudio", `a second adapter built from the same login could not open the audio of ${taskId}, which its own snapshot carries with audio started`);
1661
+ refuse("test.reload.openAudio", "test.reload.openAudio", `a second adapter built from the same login could not open the audio of ${taskId}, which its own snapshot carries with audio started`);
1625
1662
  }
1626
1663
  }
1627
1664
  catch (error) {
1628
- refuse("drive.reload.rejected", "drive.reload", `the second adapter rejected rather than answered: ${String(error)}`);
1665
+ refuse("test.reload.rejected", "test.reload", `the second adapter rejected rather than answered: ${String(error)}`);
1629
1666
  }
1630
1667
  };
1631
1668
  // A task's keys are gone with the task. A task-scoped key carries the task id, so the watched
@@ -1638,28 +1675,28 @@ async function driveOneCall(drive) {
1638
1675
  // A task's key names the task delimited, not as a run of characters inside another id, and the
1639
1676
  // rule is evaluated only where the store was ever seen to hold such a key: an adapter whose keys
1640
1677
  // name nothing the harness can see leaves the rule unasked, which the result says.
1641
- const retained = [...drive.held].filter(key => namesTask(key, taskId));
1678
+ const retained = [...call.held].filter(key => namesTask(key, taskId));
1642
1679
  retainedAtEnd = new Set(retained);
1643
- if (drive.everHeldForTask(taskId))
1644
- ruleEvaluated("drive.store.retained");
1680
+ if (call.everHeldForTask(taskId))
1681
+ ruleTested("test.store.retained");
1645
1682
  if (retained.length > 0) {
1646
- refuse("drive.store.retained", "drive.store", `${taskId} has ended and the login's store still holds ${retained.join(", ")}: a task's keys go with the task, or the next offer of the same id inherits them`);
1683
+ refuse("test.store.retained", "test.store", `${taskId} has ended and the login's store still holds ${retained.join(", ")}: a task's keys go with the task, or the next offer of the same id inherits them`);
1647
1684
  }
1648
1685
  };
1649
1686
  // A key written about the task after its end -- a persist hung off a timer, a late command -- is the
1650
- // same hazard arriving later, so the store is read once more when the drive is done.
1687
+ // same hazard arriving later, so the store is read once more when the test is done.
1651
1688
  const nothingLate = () => {
1652
1689
  if (retainedAtEnd === undefined)
1653
1690
  return;
1654
- ruleEvaluated("drive.store.late");
1655
- const late = [...drive.held].filter(key => namesTask(key, taskId) && !retainedAtEnd.has(key));
1691
+ ruleTested("test.store.late");
1692
+ const late = [...call.held].filter(key => namesTask(key, taskId) && !retainedAtEnd.has(key));
1656
1693
  if (late.length > 0) {
1657
- refuse("drive.store.late", "drive.store", `${taskId} had ended with its keys gone, and the login's store now holds ${late.join(", ")}: something wrote about the task after its end`);
1694
+ refuse("test.store.late", "test.store", `${taskId} had ended with its keys gone, and the login's store now holds ${late.join(", ")}: something wrote about the task after its end`);
1658
1695
  }
1659
1696
  };
1660
1697
  // The task, as first seen: on the snapshot, or on the first offer.
1661
1698
  let cursor = 0;
1662
- let task = isRecord(drive.snapshot) && Array.isArray(drive.snapshot.tasks) ? drive.snapshot.tasks.find(isTask) : undefined;
1699
+ let task = isRecord(call.snapshot) && Array.isArray(call.snapshot.tasks) ? call.snapshot.tasks.find(isTask) : undefined;
1663
1700
  if (task === undefined) {
1664
1701
  const offered = await waitFor("a task-offered", envelope => {
1665
1702
  const event = envelope.event;
@@ -1670,46 +1707,46 @@ async function driveOneCall(drive) {
1670
1707
  task = offered.found;
1671
1708
  cursor = offered.at;
1672
1709
  }
1673
- const taskId = task.assignmentId;
1710
+ let taskId = task.assignmentId;
1674
1711
  const latestTask = () => task;
1675
- const updated = (until, what) => waitFor(what, envelope => {
1712
+ const updated = (until, what, within) => waitFor(what, envelope => {
1676
1713
  const event = envelope.event;
1677
1714
  if ((event.type === "task-updated" || event.type === "task-offered") && isTask(event.task) && event.task.assignmentId === taskId) {
1678
1715
  task = event.task;
1679
1716
  return until(event.task) ? event.task : undefined;
1680
1717
  }
1681
1718
  return undefined;
1682
- }, cursor).then(hit => { if (hit)
1719
+ }, cursor, within).then(hit => { if (hit)
1683
1720
  cursor = hit.at; return hit; });
1684
- const ended = (within) => waitFor("a task-ended for the driven task", envelope => {
1721
+ const ended = (within) => waitFor("a task-ended for the tested task", envelope => {
1685
1722
  const event = envelope.event;
1686
1723
  return event.type === "task-ended" && event.assignmentId === taskId ? event : undefined;
1687
1724
  }, cursor, within);
1688
- // Every command the drive sends is validated against the task as published, and its answer for its method.
1689
- ruleEvaluated("drive.timeout");
1725
+ // Every command the test sends is validated against the task as published, and its answer for its method.
1726
+ ruleTested("test.timeout");
1690
1727
  const send = async (command, dialId) => {
1691
- const own = validateTaskCommand(command, latestTask(), `drive.command.${String(command.type)}`, {
1692
- levels: effectiveLevels(drive.manifest.orgLevels).map(level => level.id),
1693
- dialOutcomesDeclared: drive.manifest.dialOutcomes !== undefined,
1694
- calendarDeclared: drive.manifest.idleCapabilities?.calendar === true,
1695
- autoAcceptTasks: drive.context.autoAcceptTasks,
1728
+ const own = validateTaskCommand(command, latestTask(), `test.command.${String(command.type)}`, {
1729
+ levels: effectiveLevels(call.manifest.orgLevels).map(level => level.id),
1730
+ dialOutcomesDeclared: call.manifest.dialOutcomes !== undefined,
1731
+ calendarDeclared: call.manifest.idleCapabilities?.calendar === true,
1732
+ autoAcceptTasks: call.context.autoAcceptTasks,
1696
1733
  });
1697
1734
  found.push(...own);
1698
1735
  if (own.length > 0)
1699
1736
  return undefined;
1700
- ruleEvaluated("drive.command.rejected");
1737
+ ruleTested("test.command.rejected");
1701
1738
  let result;
1702
1739
  try {
1703
- result = await drive.connection.execute({ assignmentId: taskId, command });
1740
+ result = await call.connection.execute({ assignmentId: taskId, command });
1704
1741
  }
1705
1742
  catch (error) {
1706
- refuse("drive.command.rejected", `drive.command.${String(command.type)}`, `execute rejected rather than answered: ${String(error)}`);
1743
+ refuse("test.command.rejected", `test.command.${String(command.type)}`, `execute rejected rather than answered: ${String(error)}`);
1707
1744
  return undefined;
1708
1745
  }
1709
- ruleEvaluated("drive.command.failed");
1710
- found.push(...validateResult(result, "execute", `drive.command.${String(command.type)}.result`, dialId));
1746
+ ruleTested("test.command.failed");
1747
+ found.push(...validateResult(result, "execute", `test.command.${String(command.type)}.result`, dialId));
1711
1748
  if (isRecord(result) && result.status === "failed") {
1712
- refuse("drive.command.failed", `drive.command.${String(command.type)}`, `the provider refused ${String(command.type)} on a task that offered it: ${String(isRecord(result.failure) ? result.failure.code : result.failure)}`);
1749
+ refuse("test.command.failed", `test.command.${String(command.type)}`, `the provider refused ${String(command.type)} on a task that offered it: ${String(isRecord(result.failure) ? result.failure.code : result.failure)}`);
1713
1750
  return undefined;
1714
1751
  }
1715
1752
  return isRecord(result) ? result : undefined;
@@ -1721,39 +1758,107 @@ async function driveOneCall(drive) {
1721
1758
  };
1722
1759
  // Audio may arrive any time after the accept, before or after the task's own update says
1723
1760
  // in-progress: the event is the provider's word that the audio should attach, never a reply to
1724
- // openAudio, so the drive looks for it from the accept rather than from the last update it read.
1725
- const acceptedAt = drive.events.length;
1761
+ // openAudio, so the test looks for it from the accept rather than from the last update it read.
1762
+ // 0. A deadline stated is a deadline kept. Where the first offer says how long it has, within
1763
+ // what the test will wait, the test leaves it alone and expects the provider to end it expired
1764
+ // when it runs out; the next offer is the one tested. Offers with no deadline, or a longer one,
1765
+ // leave this rule unreached, and the result says so.
1766
+ const settleMs = Number(call.manifest.settleMs);
1767
+ const lapse = latestTask().phase === "pending" ? latestTask().expiresInSeconds : undefined;
1768
+ if (typeof lapse === "number" && lapse * 1000 <= call.timeoutMs) {
1769
+ ruleTested("test.offer.expired");
1770
+ const lapsedId = taskId;
1771
+ let missed = false;
1772
+ const ending = await waitFor(`the expired ending of ${lapsedId}`, envelope => {
1773
+ const event = envelope.event;
1774
+ return event.type === "task-ended" && event.assignmentId === lapsedId ? event : undefined;
1775
+ }, cursor, { ms: lapse * 1000 + settleMs, onExpiry: () => { missed = true; } });
1776
+ if (ending === undefined) {
1777
+ if (missed)
1778
+ refuse("test.offer.expired", "test.offer", `${lapsedId} was offered with ${lapse}s to answer and ${lapse * 1000 + settleMs}ms later it has not ended: a deadline stated is a deadline kept, and the offer ends expired naming pending`);
1779
+ return found;
1780
+ }
1781
+ const outcome = isRecord(ending.found.outcome) ? ending.found.outcome : {};
1782
+ if (!(outcome.type === "expired" && outcome.phase === "pending")) {
1783
+ refuse("test.offer.expired", "test.offer", `${lapsedId} lapsed unanswered and ended ${String(outcome.type)}: an offer nobody answered before its deadline ends expired naming pending`);
1784
+ }
1785
+ // The next offer is the one tested; a provider with nothing more to offer ends the test here.
1786
+ const next = await waitFor("a task-offered after the lapsed one", envelope => {
1787
+ const event = envelope.event;
1788
+ return event.type === "task-offered" && isTask(event.task) && event.task.assignmentId !== lapsedId ? event.task : undefined;
1789
+ }, ending.at);
1790
+ if (next === undefined)
1791
+ return found;
1792
+ task = next.found;
1793
+ cursor = next.at;
1794
+ taskId = task.assignmentId;
1795
+ }
1796
+ const acceptedAt = call.events.length;
1726
1797
  // 1. Accept the offer, if it is one.
1727
1798
  if (latestTask().phase === "pending") {
1728
- if (await send({ type: drive.channel === "voice" ? "answer" : "accept" }) === undefined)
1799
+ if (await send({ type: call.channel === "voice" ? "answer" : "accept" }) === undefined)
1729
1800
  return found;
1730
1801
  if (await updated(t => t.phase !== "pending", "the task leaving pending after it was accepted") === undefined)
1731
1802
  return found;
1732
1803
  }
1733
- // 2. A preview: press Call, which is a dial.
1804
+ // 2. A preview: press Call, which is a dial. Where the preview says how long it has, within what
1805
+ // the test will wait, the deadline is kept to its atDeadline first: under provider-dials the
1806
+ // provider dials when it runs out and the test presses nothing; under host-dials and waits the
1807
+ // preview stands until then, and the test presses Call at zero as the desk would.
1734
1808
  if (latestTask().phase === "preview") {
1735
- const dialId = `drive-${taskId}`;
1736
- drive.stream.dialled(dialId);
1737
- if (await send({ type: "dial", dialId }, dialId) === undefined)
1738
- return found;
1739
- if (await updated(t => t.phase === "in-progress" || t.phase === "completing", "the task leaving preview after Call") === undefined)
1809
+ const left = latestTask().previewEndsInSeconds;
1810
+ const atDeadline = latestTask().atDeadline;
1811
+ // Under provider-dials the provider places the call, and the desk presses nothing.
1812
+ let pressCall = true;
1813
+ if (typeof left === "number" && left * 1000 <= call.timeoutMs && typeof atDeadline === "string") {
1814
+ ruleTested("test.preview.deadline");
1815
+ const ringing = (t) => t.phase !== "preview"
1816
+ || (Array.isArray(t.onCall) && t.onCall.some(who => isRecord(who) && who.role === "party" && who.stage === "ringing"));
1817
+ if (atDeadline === "provider-dials") {
1818
+ let missed = false;
1819
+ const dialled = await updated(ringing, "the provider dialling at the preview's deadline", { ms: left * 1000 + settleMs, onExpiry: () => { missed = true; } });
1820
+ if (dialled === undefined) {
1821
+ if (!missed)
1822
+ return found;
1823
+ refuse("test.preview.deadline", "test.preview", `${taskId} is a preview under provider-dials with ${left}s left, and the provider did not dial when it ran out: the party rings on onCall, or the task leaves preview`);
1824
+ return found;
1825
+ }
1826
+ pressCall = false;
1827
+ }
1828
+ else {
1829
+ // Nothing may move the preview before its deadline under host-dials or waits: the test
1830
+ // waits the seconds out and expects the task where it left it.
1831
+ const moved = await updated(t => t.phase !== "preview", "nothing", { ms: left * 1000, onExpiry: () => undefined });
1832
+ if (moved !== undefined) {
1833
+ refuse("test.preview.deadline", "test.preview", `${taskId} is a preview under ${atDeadline} with ${left}s left, and the provider moved it to ${String(moved.found.phase)} before the deadline: under ${atDeadline} the preview stands until the desk dials, or for as long as the agent needs`);
1834
+ return found;
1835
+ }
1836
+ }
1837
+ }
1838
+ if (pressCall && latestTask().phase === "preview") {
1839
+ const dialId = `test-${taskId}`;
1840
+ call.stream.dialled(dialId);
1841
+ if (await send({ type: "dial", dialId }, dialId) === undefined)
1842
+ return found;
1843
+ }
1844
+ if (latestTask().phase === "preview" && await updated(t => t.phase === "in-progress" || t.phase === "completing", pressCall ? "the task leaving preview after Call" : "the task leaving preview after the provider's dial") === undefined)
1740
1845
  return found;
1741
1846
  }
1742
1847
  // The other direction of step 4, wherever the task stands outside the interaction phases with the
1743
1848
  // control still declared: a host holds it back (command.phase.interaction), and an adapter that
1744
- // receives it anyway must refuse it -- so the drive sends it past the validator and expects failed.
1849
+ // receives it anyway must refuse it -- so the test sends it past the validator and expects failed.
1745
1850
  const holdRefusedOutsideInteraction = async () => {
1746
- ruleEvaluated("drive.command.rejected", "drive.command.interaction");
1851
+ ruleTested("test.command.rejected", "test.command.interaction");
1747
1852
  const phase = String(latestTask().phase);
1748
1853
  let answer;
1749
1854
  try {
1750
- answer = await drive.connection.execute({ assignmentId: taskId, command: { type: "hold" } });
1855
+ answer = await call.connection.execute({ assignmentId: taskId, command: { type: "hold" } });
1751
1856
  }
1752
1857
  catch (error) {
1753
- refuse("drive.command.rejected", "drive.command.hold", `execute rejected rather than answered: ${String(error)}`);
1858
+ refuse("test.command.rejected", "test.command.hold", `execute rejected rather than answered: ${String(error)}`);
1754
1859
  }
1755
1860
  if (isRecord(answer) && answer.status !== "failed") {
1756
- refuse("drive.command.interaction", "drive.command.hold", `the provider applied hold on a ${phase} task: a control on the contact belongs to in-progress or paused, and the adapter is the second gate`);
1861
+ refuse("test.command.interaction", "test.command.hold", `the provider applied hold on a ${phase} task: a control on the contact belongs to in-progress or paused, and the adapter is the second gate`);
1757
1862
  }
1758
1863
  };
1759
1864
  if (latestTask().phase === "confirmed") {
@@ -1763,9 +1868,9 @@ async function driveOneCall(drive) {
1763
1868
  return found;
1764
1869
  }
1765
1870
  // 3. On a softphone, the audio arrives and the host opens it.
1766
- if (drive.softphone && latestTask().phase === "in-progress") {
1871
+ if (call.softphone && latestTask().phase === "in-progress") {
1767
1872
  if (latestTask().audio !== "started") {
1768
- const started = await waitFor("task-audio-started for the driven task", envelope => {
1873
+ const started = await waitFor("task-audio-started for the tested task", envelope => {
1769
1874
  const event = envelope.event;
1770
1875
  return event.type === "task-audio-started" && event.assignmentId === taskId ? event : undefined;
1771
1876
  }, acceptedAt);
@@ -1773,15 +1878,15 @@ async function driveOneCall(drive) {
1773
1878
  return found;
1774
1879
  cursor = Math.max(cursor, started.at);
1775
1880
  }
1776
- const opened = await drive.connection.openAudio?.({ assignmentId: taskId, localAudio: drive.localAudio });
1777
- const malformed = validateResult(opened, "openAudio", "drive.openAudio");
1881
+ const opened = await call.connection.openAudio?.({ assignmentId: taskId, localAudio: call.localAudio });
1882
+ const malformed = validateResult(opened, "openAudio", "test.openAudio");
1778
1883
  found.push(...malformed);
1779
1884
  if (malformed.length === 0 && isRecord(opened) && opened.status === "opened")
1780
1885
  session = opened.audio;
1781
1886
  else if (malformed.length === 0 && isRecord(opened))
1782
- refuse("drive.openAudio.unavailable", "drive.openAudio", "a softphone login's adapter could not open the call's audio");
1887
+ refuse("test.openAudio.unavailable", "test.openAudio", "a softphone login's adapter could not open the call's audio");
1783
1888
  }
1784
- // 3b. The microphone is the host's. With the audio open, the drive mutes it for a moment and
1889
+ // 3b. The microphone is the host's. With the audio open, the test mutes it for a moment and
1785
1890
  // reports the leg the provider's record would otherwise miss -- begun, then ended -- and
1786
1891
  // expects each report recorded. If the provider restates the task's record afterwards, the leg is in it.
1787
1892
  const canonicalTimes = new Map();
@@ -1793,34 +1898,34 @@ async function driveOneCall(drive) {
1793
1898
  ? history.steps.find(entry => isRecord(entry) && entry.step === "muted" && entry.at === canonicalTimes.get(at)) : undefined;
1794
1899
  return typeof leg?.seconds === "number" ? leg.seconds : undefined;
1795
1900
  };
1796
- const canRecordMute = session !== undefined && typeof session.setMuted === "function" && typeof drive.connection.recordStep === "function";
1901
+ const canRecordMute = session !== undefined && typeof session.setMuted === "function" && typeof call.connection.recordStep === "function";
1797
1902
  const report = async (body) => {
1798
- ruleEvaluated("drive.recordStep.rejected", "drive.recordStep.failed");
1799
- // The drive holds its own report to the contract before it crosses, as a host must.
1903
+ ruleTested("test.recordStep.rejected", "test.recordStep.failed");
1904
+ // The test holds its own report to the contract before it crosses, as a host must.
1800
1905
  const leg = { assignmentId: taskId, step: "muted", mutedBy: "host", ...body };
1801
- const own = validateHistoryReport(leg, "drive.recordStep.report", drive.manifest);
1906
+ const own = validateHistoryReport(leg, "test.recordStep.report", call.manifest);
1802
1907
  found.push(...own);
1803
1908
  if (own.length > 0)
1804
1909
  return;
1805
1910
  let answer;
1806
1911
  try {
1807
- answer = await drive.connection.recordStep(leg);
1912
+ answer = await call.connection.recordStep(leg);
1808
1913
  }
1809
1914
  catch (error) {
1810
- refuse("drive.recordStep.rejected", "drive.recordStep", `recordStep rejected rather than answered: ${String(error)}`);
1915
+ refuse("test.recordStep.rejected", "test.recordStep", `recordStep rejected rather than answered: ${String(error)}`);
1811
1916
  return;
1812
1917
  }
1813
- const resultErrors = validateResult(answer, "recordStep", "drive.recordStep.result");
1918
+ const resultErrors = validateResult(answer, "recordStep", "test.recordStep.result");
1814
1919
  found.push(...resultErrors);
1815
1920
  if (!resultErrors.length && isRecord(answer) && answer.status === "recorded" && typeof answer.at === "string" && typeof body.at === "string") {
1816
1921
  const prior = canonicalTimes.get(body.at);
1817
1922
  if (prior !== undefined && prior !== answer.at)
1818
- refuse("drive.recordStep.identity", "drive.recordStep.result.at", "one reported leg must retain its provider-assigned history instant");
1923
+ refuse("test.recordStep.identity", "test.recordStep.result.at", "one reported leg must retain its provider-assigned history instant");
1819
1924
  else
1820
1925
  canonicalTimes.set(body.at, answer.at);
1821
1926
  }
1822
1927
  if (isRecord(answer) && answer.status === "failed") {
1823
- refuse("drive.recordStep.failed", "drive.recordStep", `the provider refused to record the host's muted leg: ${String(isRecord(answer.failure) ? answer.failure.code : answer.failure)}`);
1928
+ refuse("test.recordStep.failed", "test.recordStep", `the provider refused to record the host's muted leg: ${String(isRecord(answer.failure) ? answer.failure.code : answer.failure)}`);
1824
1929
  }
1825
1930
  };
1826
1931
  const setMuted = (muted) => {
@@ -1831,7 +1936,7 @@ async function driveOneCall(drive) {
1831
1936
  session.setMuted(muted);
1832
1937
  }
1833
1938
  catch {
1834
- refuse("drive.openAudio.setMuted", "drive.openAudio", `the audio session threw on setMuted(${String(muted)})`);
1939
+ refuse("test.openAudio.setMuted", "test.openAudio", `the audio session threw on setMuted(${String(muted)})`);
1835
1940
  }
1836
1941
  };
1837
1942
  if (canRecordMute) {
@@ -1847,16 +1952,16 @@ async function driveOneCall(drive) {
1847
1952
  // 3c. A host reload destroys the adapter object and keeps the login's store. Built again from
1848
1953
  // the same login, a second adapter carries the task and the leg -- from its platform, or from
1849
1954
  // the store -- or it composed the record in memory and the record died with it.
1850
- if (drive.rebuild !== undefined && canonicalTimes.has(at)) {
1955
+ if (call.rebuild !== undefined && canonicalTimes.has(at)) {
1851
1956
  await recordSurvivesReload(canonicalTimes.get(at));
1852
- // A reload that stood no client ends the run: there is nothing left to drive.
1853
- if (!drive.handOver.isLive())
1957
+ // A reload that stood no client ends the run: there is nothing left to call.
1958
+ if (!call.handOver.isLive())
1854
1959
  return found;
1855
1960
  }
1856
1961
  }
1857
1962
  // 4. Hold and resume, where offered.
1858
1963
  if (latestTask().phase === "in-progress" && offers("hold")) {
1859
- if (await send({ type: drive.channel === "chat" ? "pause" : "hold" }) !== undefined) {
1964
+ if (await send({ type: call.channel === "chat" ? "pause" : "hold" }) !== undefined) {
1860
1965
  if (await updated(t => t.phase === "paused", "the task pausing after hold") !== undefined) {
1861
1966
  if (await send({ type: "resume" }) !== undefined) {
1862
1967
  await updated(t => t.phase === "in-progress", "the task resuming after resume");
@@ -1868,14 +1973,14 @@ async function driveOneCall(drive) {
1868
1973
  // calendar-updated that shows it is owed within the manifest's bound. The time is the provider's
1869
1974
  // own: a day after the instant it published the offer, so no clock of the runner's is read.
1870
1975
  if (latestTask().phase === "in-progress" && offers("schedule")) {
1871
- const publishedAt = drive.events.slice(0, cursor).reverse().find(envelope => {
1976
+ const publishedAt = call.events.slice(0, cursor).reverse().find(envelope => {
1872
1977
  const event = envelope.event;
1873
1978
  return (event.type === "task-offered" || event.type === "task-updated") && isTask(event.task) && event.task.assignmentId === taskId;
1874
1979
  })?.occurredAt;
1875
1980
  const at = new Date(Date.parse(String(publishedAt ?? "2026-01-01T00:00:00Z")) + 24 * 60 * 60 * 1000).toISOString();
1876
1981
  if (await send({ type: "schedule", at, note: "Follow-up promised on the call" }) !== undefined) {
1877
- ruleEvaluated("drive.schedule.unsettled");
1878
- const settle = Number(drive.manifest.settleMs);
1982
+ ruleTested("test.schedule.unsettled");
1983
+ const settle = Number(call.manifest.settleMs);
1879
1984
  const carries = (calendar) => Array.isArray(calendar) && calendar.some(activity => isRecord(activity) && activity.startsAt === at);
1880
1985
  let unsettled = false;
1881
1986
  const shown = await waitFor("a calendar-updated carrying the follow-up", envelope => {
@@ -1885,13 +1990,13 @@ async function driveOneCall(drive) {
1885
1990
  if (shown === undefined && unsettled) {
1886
1991
  let resync;
1887
1992
  try {
1888
- resync = await drive.connection.snapshot();
1993
+ resync = await call.connection.snapshot();
1889
1994
  }
1890
1995
  catch (error) {
1891
- refuse("drive.command.rejected", "drive.schedule", `snapshot() after an unsettled schedule rejected: ${String(error)}`);
1996
+ refuse("test.command.rejected", "test.schedule", `snapshot() after an unsettled schedule rejected: ${String(error)}`);
1892
1997
  }
1893
1998
  if (!(isRecord(resync) && carries(resync.calendar))) {
1894
- refuse("drive.schedule.unsettled", "drive.schedule", `schedule was applied and ${settle}ms later no calendar-updated carries a follow-up at ${at}, and a snapshot does not either: applied says the follow-up is on the calendar, and it is not`);
1999
+ refuse("test.schedule.unsettled", "test.schedule", `schedule was applied and ${settle}ms later no calendar-updated carries a follow-up at ${at}, and a snapshot does not either: applied says the follow-up is on the calendar, and it is not`);
1895
2000
  }
1896
2001
  }
1897
2002
  }
@@ -1899,9 +2004,9 @@ async function driveOneCall(drive) {
1899
2004
  // 5. End the call, where the agent may -- with the microphone muted, as agents do. The leg left
1900
2005
  // open at audio end is the provider's to close, since the provider knows the instant the audio
1901
2006
  // ended; the host's own closing report can only follow what it hears, is answered recorded, and
1902
- // changes nothing. So the drive mutes, does not report the end before end-call, and reports it
2007
+ // changes nothing. So the test mutes, does not report the end before end-call, and reports it
1903
2008
  // after the audio has ended.
1904
- if (drive.channel === "voice" && latestTask().phase === "in-progress" && offers("endCall")) {
2009
+ if (call.channel === "voice" && latestTask().phase === "in-progress" && offers("endCall")) {
1905
2010
  let openLeg;
1906
2011
  if (canRecordMute && session !== undefined) {
1907
2012
  // The task as published before the call ends: the completing publication that follows is the restatement held to carrying this leg.
@@ -1911,7 +2016,7 @@ async function driveOneCall(drive) {
1911
2016
  }
1912
2017
  if (await send({ type: "end-call" }) !== undefined) {
1913
2018
  // A task that completes with its audio still up is refused by the stream as it passes
1914
- // (stream.taskUpdated.audioOpen); the drive does not also wait out the clock for an ending
2019
+ // (stream.taskUpdated.audioOpen); the test does not also wait out the clock for an ending
1915
2020
  // that is not coming, so the run reads by the rule and not by its timeout.
1916
2021
  const audioEnded = await waitFor("task-audio-ended after end-call", envelope => {
1917
2022
  const event = envelope.event;
@@ -1943,11 +2048,11 @@ async function driveOneCall(drive) {
1943
2048
  session.close();
1944
2049
  }
1945
2050
  catch {
1946
- refuse("drive.openAudio.close", "drive.openAudio", "the audio session threw on close");
2051
+ refuse("test.openAudio.close", "test.openAudio", "the audio session threw on close");
1947
2052
  }
1948
2053
  }
1949
2054
  // 6. Complete, under either mode: the agent may finish early and the provider is free to end at
1950
- // once. Only a task with no wrap at all has nothing to complete, and there the drive waits for the
2055
+ // once. Only a task with no wrap at all has nothing to complete, and there the test waits for the
1951
2056
  // provider. A conversation has no audio to end and no completing phase to wait for: a chat or an
1952
2057
  // email, and a voice task offering no end-call, is completed from where it stands.
1953
2058
  const completable = latestTask().phase === "completing" || latestTask().phase === "in-progress" || latestTask().phase === "paused";
@@ -1961,8 +2066,8 @@ async function driveOneCall(drive) {
1961
2066
  // applied says the provider has completed the task, and its ending follows within the bound
1962
2067
  // the manifest stated. Past it the host resyncs: a snapshot still carrying the task is a task
1963
2068
  // held open by a provider that said it was done, and the desk shows it as unsettled.
1964
- ruleEvaluated("drive.completion.unsettled");
1965
- const settle = Number(drive.manifest.settleMs);
2069
+ ruleTested("test.completion.unsettled");
2070
+ const settle = Number(call.manifest.settleMs);
1966
2071
  let unsettled = false;
1967
2072
  const end = await ended({ ms: settle, onExpiry: () => { unsettled = true; } });
1968
2073
  if (end !== undefined)
@@ -1970,13 +2075,13 @@ async function driveOneCall(drive) {
1970
2075
  else if (unsettled) {
1971
2076
  let resync;
1972
2077
  try {
1973
- resync = await drive.connection.snapshot();
2078
+ resync = await call.connection.snapshot();
1974
2079
  }
1975
2080
  catch (error) {
1976
- refuse("drive.command.rejected", "drive.completion", `snapshot() after an unsettled completion rejected: ${String(error)}`);
2081
+ refuse("test.command.rejected", "test.completion", `snapshot() after an unsettled completion rejected: ${String(error)}`);
1977
2082
  }
1978
2083
  const still = isRecord(resync) && Array.isArray(resync.tasks) && resync.tasks.some(t => isRecord(t) && t.assignmentId === taskId);
1979
- refuse("drive.completion.unsettled", "drive.completion", still
2084
+ refuse("test.completion.unsettled", "test.completion", still
1980
2085
  ? `complete was applied and ${settle}ms later the task-ended has not come and a snapshot still carries ${taskId}: applied says the provider completed the task, and it has not`
1981
2086
  : `complete was applied and ${settle}ms later the task-ended has not come; a snapshot no longer carries ${taskId}, so the ending was owed and never sent`);
1982
2087
  }
@@ -1987,10 +2092,10 @@ async function driveOneCall(drive) {
1987
2092
  await storeReleased();
1988
2093
  }
1989
2094
  // The record kept by the provider has every leg the host reported, wherever the provider restated it:
1990
- // the last publication of the task is read, whether or not the drive was waiting on it.
2095
+ // the last publication of the task is read, whether or not the test was waiting on it.
1991
2096
  const lastPublished = () => {
1992
- for (let index = drive.events.length - 1; index >= 0; index -= 1) {
1993
- const event = drive.events[index].event;
2097
+ for (let index = call.events.length - 1; index >= 0; index -= 1) {
2098
+ const event = call.events[index].event;
1994
2099
  if ((event.type === "task-updated" || event.type === "task-offered") && isTask(event.task) && event.task.assignmentId === taskId)
1995
2100
  return event.task;
1996
2101
  }
@@ -2007,15 +2112,15 @@ async function driveOneCall(drive) {
2007
2112
  const { at } = reported;
2008
2113
  const leg = history.steps.find(entry => isRecord(entry) && entry.step === "muted" && entry.at === at);
2009
2114
  if (leg === undefined) {
2010
- refuse("drive.recordStep.history", "drive.recordStep", `the provider restated the task's record after the host reported a muted leg at ${at}, and the leg is not in it`);
2115
+ refuse("test.recordStep.history", "test.recordStep", `the provider restated the task's record after the host reported a muted leg at ${at}, and the leg is not in it`);
2011
2116
  }
2012
2117
  else if (leg.mutedBy !== "host") {
2013
- refuse("drive.recordStep.history", "drive.recordStep", `the record's muted leg at ${at} says mutedBy ${String(leg.mutedBy)}; the host reported host, and the record keeps the host's word`);
2118
+ refuse("test.recordStep.history", "test.recordStep", `the record's muted leg at ${at} says mutedBy ${String(leg.mutedBy)}; the host reported host, and the record keeps the host's word`);
2014
2119
  }
2015
2120
  else if (reported.closedAs !== undefined) {
2016
- ruleEvaluated("drive.recordStep.overwritten");
2121
+ ruleTested("test.recordStep.overwritten");
2017
2122
  if (leg.seconds !== reported.closedAs) {
2018
- refuse("drive.recordStep.overwritten", "drive.recordStep", `the provider closed the leg at ${at} with ${reported.closedAs}s at audio end and the record now says ${String(leg.seconds)}s: the host's closing report changes nothing`);
2123
+ refuse("test.recordStep.overwritten", "test.recordStep", `the provider closed the leg at ${at} with ${reported.closedAs}s at audio end and the record now says ${String(leg.seconds)}s: the host's closing report changes nothing`);
2019
2124
  }
2020
2125
  }
2021
2126
  }
@@ -2064,7 +2169,7 @@ export class TeamStream {
2064
2169
  case "team-member-updated":
2065
2170
  case "team-member-removed":
2066
2171
  case "team-policies-updated": {
2067
- ruleEvaluated("stream.team.baseline");
2172
+ ruleTested("stream.team.baseline");
2068
2173
  if (this.members === undefined) {
2069
2174
  return [{ rule: "stream.team.baseline", path: `${at}.type`,
2070
2175
  message: `${event.type} arrived before the team: team-updated carries the whole team first, and it changes one member at a time after that` }];
@@ -2075,7 +2180,7 @@ export class TeamStream {
2075
2180
  return [];
2076
2181
  }
2077
2182
  if (event.type === "team-member-removed") {
2078
- ruleEvaluated("stream.teamMember.unknown");
2183
+ ruleTested("stream.teamMember.unknown");
2079
2184
  const id = String(event.memberId);
2080
2185
  if (!this.members.has(id)) {
2081
2186
  return [{ rule: "stream.teamMember.unknown", path: `${at}.memberId`, message: `${id} is not a member the team carried: a removal takes off somebody the lead was given` }];
@@ -2168,7 +2273,7 @@ function watchStore(store) {
2168
2273
  },
2169
2274
  };
2170
2275
  }
2171
- /** A login store that lives in memory for the test: what most adapter tests hand `exerciseAdapter` as `context.store`. */
2276
+ /** A login store that lives in memory for the test: what most adapter tests hand `testAdapter` as `context.store`. */
2172
2277
  export function memoryStore() {
2173
2278
  const kept = new Map();
2174
2279
  return {