Merge pull request 'feat(realtime): printer-approval-v1 and the PRINTERS frame for extension 2.4.0' (#11) from feat/printer-approval-20261105 into main
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Failing after 2s
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1b3648d9421543350067e95e54959dfb471b26d5] (push) Successful in 11s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
QRun-IO/ci-policy: Shared Node CI / verify (push) Failing after 1s
QRun-IO/ci-policy: Shared Node CI / secrets (push) Failing after 1s
QRun-IO/ci-policy: Shared Node CI / gate (push) Failing after 1s
QRun-IO/ci-policy: ci / node-8 (push) Failing after 1m26s
QRun-IO/ci-policy: ci / ci-result (push) Failing after 1s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Failing after 2s
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1b3648d9421543350067e95e54959dfb471b26d5] (push) Successful in 11s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
QRun-IO/ci-policy: Shared Node CI / verify (push) Failing after 1s
QRun-IO/ci-policy: Shared Node CI / secrets (push) Failing after 1s
QRun-IO/ci-policy: Shared Node CI / gate (push) Failing after 1s
QRun-IO/ci-policy: ci / node-8 (push) Failing after 1m26s
QRun-IO/ci-policy: ci / ci-result (push) Failing after 1s
This commit was merged in pull request #11.
This commit is contained in:
13 files changed
+223
-14
No files matched your search
@@ -1,5 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## 1.1.0 — unreleased
|
||||
|
||||
Additive contract for printing semantic extension **2.4.0**. Wire payload stays
|
||||
**v1** and the subprotocol stays `qqq-printing.v1`; every 2.3.0 fixture is unchanged.
|
||||
|
||||
- Add the `printer-approval-v1` capability to `RealtimeHelloPayload` (`maxItems` 5)
|
||||
and `ControlCapability`; the historical control poll and management
|
||||
`Device.control` accept four non-realtime capabilities.
|
||||
- Add the server → client `PRINTERS` frame (`RealtimePrinters`,
|
||||
`RealtimePrintersPayload`, `PrinterApprovalState`) carrying the complete approval
|
||||
state of the device's latest accepted inventory; sent only to connections that
|
||||
advertised `printer-approval-v1`, after each accepted INVENTORY and on every
|
||||
approval change. Not acknowledged, not durable, no job or document data.
|
||||
- Add `realtime-printers.message.json`, a five-capability hello and a 2.4.0
|
||||
discovery fixture; the verifiers cover the directional frame, bounds and
|
||||
required fields. See [REALTIME.md](docs/REALTIME.md#printer-approval) and the
|
||||
[version registry](docs/VERSIONS.md).
|
||||
|
||||
## 1.0.0
|
||||
|
||||
Distribution version 1.0.0 of the contract and fixtures from 0.4.0-rc.1. The wire
|
||||
|
||||
@@ -13,6 +13,13 @@ granting device management. Use the stable cohort above for these profiles;
|
||||
older 0.3.2 clients do not support them. Wire version 1 and the client-initiated
|
||||
realtime transport stay the same.
|
||||
|
||||
Source version **1.1.0** (unreleased) adds printing semantic extension **2.4.0**:
|
||||
the `printer-approval-v1` HELLO capability and the server → client `PRINTERS`
|
||||
frame, which reports each exposed printer's server approval over the
|
||||
[realtime channel](docs/REALTIME.md#printer-approval). Clients advertise it only
|
||||
after discovering a stable 2.4+ extension; a 2.3.0 server or a client without the
|
||||
capability keeps the 2.3.0 wire unchanged.
|
||||
|
||||
## Current connection and delivery
|
||||
|
||||
1. Configure the QQQ server origin in the Windows app. Discover its UI/UX API and sign in using the server's configured authentication method.
|
||||
|
||||
+24
-1
@@ -32,7 +32,9 @@ The client opens one persistent WebSocket to the configured QQQ origin:
|
||||
`X-QQQ-Device-Id`, `X-QQQ-Device-Token`, `X-QQQ-Client: api/qqq-print-agent`
|
||||
and the canonical same-origin `Origin`.
|
||||
- Capability: `realtime-v1` is mandatory in `HELLO`. Optional command capabilities
|
||||
are the existing `REFRESH_INVENTORY` and `CANCEL_SPOOL`.
|
||||
are the existing `REFRESH_INVENTORY` and `CANCEL_SPOOL`; optional feature
|
||||
capabilities are `printer-media-v1` (extension 2.3.0) and `printer-approval-v1`
|
||||
(extension 2.4.0), each gated by [discovery](VERSIONS.md#negotiation-rules).
|
||||
|
||||
Do not add credentials to URLs, subprotocols, logs or frame payloads. Refuse
|
||||
cross-origin redirects and arbitrary content URLs. The existing session is
|
||||
@@ -68,6 +70,7 @@ compression that could bypass decoded-size limits are not supported.
|
||||
| Server → client | DESIRED_STATE | Existing `DeviceDesiredState` directly |
|
||||
| Server → client | SETTINGS | `RealtimeSettings` directly |
|
||||
| Server → client | JOB_AVAILABLE | Empty object; a coalescible wake-up, not a job or claim |
|
||||
| Server → client | PRINTERS | `printers`: complete `printerId`/`enabled` approval of the latest accepted inventory; only after `printer-approval-v1` |
|
||||
| Server → client | COMMAND | Existing `DeviceCommand` directly |
|
||||
| Client → server | JOB_EVENT | `jobId`, existing `JobEvent` as `event` |
|
||||
| Client → server | COMMAND_RECEIPT | `commandId`, existing `DeviceCommandReceipt` as `receipt` |
|
||||
@@ -97,6 +100,26 @@ refreshing inventory or querying jobs/controls. Only one serialized writer owns
|
||||
socket sends; the receive/control path must not await document download,
|
||||
rendering, printer submission or UI work.
|
||||
|
||||
### Printer approval
|
||||
|
||||
Extension 2.4.0 adds `printer-approval-v1`. A client advertises it only after
|
||||
discovering a stable 2.4+ extension within major two. The server then sends
|
||||
PRINTERS (`RealtimePrinters`) on that connection, through the same serialized
|
||||
writer, after it has processed an accepted INVENTORY from that connection
|
||||
(after that INVENTORY's ACK) and whenever the approval of any printer belonging
|
||||
to the device changes by any server-side path. Nothing else triggers it; there
|
||||
is no timer and the client does not acknowledge it.
|
||||
|
||||
The payload lists every printer in the device's latest accepted inventory, for
|
||||
this device only, as `printerId` plus boolean `enabled`, at most 200 entries.
|
||||
`enabled:false` means the server knows the printer and has not approved it. A
|
||||
printer the device no longer exposes is not listed; an empty inventory yields
|
||||
`{ "printers": [] }`. Each frame replaces the previous one entirely, so frames
|
||||
are idempotent and coalescible like JOB_AVAILABLE. The frame is not a durable
|
||||
event and carries no job or document data. A client that did not advertise the
|
||||
capability never receives it and keeps reading its existing authorized catalog
|
||||
for approval; a server never sends it to a connection that did not advertise it.
|
||||
|
||||
## Ordering, synchronization and replay
|
||||
|
||||
1. Authenticate the connection, then require HELLO before other application work.
|
||||
|
||||
+13
-4
@@ -5,7 +5,7 @@ compare them as if they were one version sequence. See the [stable cohort](INTEG
|
||||
|
||||
| Number | Meaning | Current |
|
||||
| --- | --- | --- |
|
||||
| Protocol distribution | Version of the specification and fixture archive | 1.0.0 |
|
||||
| Protocol distribution | Version of the specification and fixture archive | 1.1.0 (unreleased; 1.0.0 published) |
|
||||
| Wire payload version | Shape of the JSON messages in this repository | 1 |
|
||||
| WebSocket subprotocol | Framing and message envelope | `qqq-printing.v1` |
|
||||
| Printing semantic extension | Server feature level discovered at `/qqq/v1/version` | see below |
|
||||
@@ -21,22 +21,31 @@ additive and keeps the earlier behavior.
|
||||
| 2.1.0 | Record, table-list and text print processes | server documentation |
|
||||
| 2.2.0 | Render-to-PDF process, header and footer values, content sections. No media negotiation | server documentation |
|
||||
| 2.3.0 | Least-privilege operator catalog, submission and request lookup; reported queue-default media and `printer-media-v1`; opt-in retained-source facade | [PRINTER-MEDIA.md](PRINTER-MEDIA.md) |
|
||||
| 2.4.0 | `printer-approval-v1` and the server → client `PRINTERS` frame reporting each exposed printer's approval | [REALTIME.md](REALTIME.md#printer-approval) |
|
||||
|
||||
## Negotiation rules
|
||||
|
||||
- A client advertises a capability only when it supports it **and** the discovered
|
||||
extension is a stable `major.minor.patch` version in major 2 at or above the
|
||||
feature minimum. `printer-media-v1` has minimum 2.3.0.
|
||||
feature minimum. `printer-media-v1` has minimum 2.3.0; `printer-approval-v1`
|
||||
has minimum 2.4.0.
|
||||
- A client facing 2.0, 2.1 or 2.2 stays a three-capability peer (`realtime-v1`,
|
||||
`REFRESH_INVENTORY`, `CANCEL_SPOOL`) and sends no media inventory.
|
||||
`REFRESH_INVENTORY`, `CANCEL_SPOOL`) and sends no media inventory. A client
|
||||
facing 2.3 is at most a four-capability peer and receives no `PRINTERS` frame.
|
||||
- A server sends `PRINTERS` only on a connection whose HELLO advertised
|
||||
`printer-approval-v1`, whatever extension version it serves; a client that did
|
||||
not advertise it keeps its own approval fallback.
|
||||
- A server must not let a client that did not advertise `printer-media-v1` claim a
|
||||
job that carries a media selection, whatever extension version that client
|
||||
discovered.
|
||||
- Capabilities are re-evaluated on each new socket, so a server upgrade from 2.0
|
||||
to 2.3, or a downgrade from 2.3 to 2.2, takes effect at the next reconnect
|
||||
to 2.4, or a downgrade from 2.4 to 2.2, takes effect at the next reconnect
|
||||
without a polling schedule.
|
||||
- Pre-release, ambiguous or malformed versions (`2.3.0-rc.1`, `2.3`, `02.3.0`,
|
||||
four-part values) never enable a feature.
|
||||
|
||||
QBit 1.0.0 advertises native printing extension 2.3.0. Windows 1.0.1 is the paired
|
||||
stable client; their product versions do not replace capability discovery.
|
||||
QBit 1.1.0 advertises 2.4.0 and pairs with Windows 1.2.0; a 2.4.0 server still
|
||||
serves 1.0.1/1.1.x clients as four-capability peers, and a 1.2.0 client facing
|
||||
2.3.0 does not advertise `printer-approval-v1`.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "printing",
|
||||
"version": "2.4.0",
|
||||
"pathPrefix": "/qbit/printing"
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"type": "HELLO",
|
||||
"messageId": "hello-connection-1",
|
||||
"payload": {
|
||||
"agentVersion": "1.2.0",
|
||||
"capabilities": [
|
||||
"realtime-v1",
|
||||
"REFRESH_INVENTORY",
|
||||
"CANCEL_SPOOL",
|
||||
"printer-media-v1",
|
||||
"printer-approval-v1"
|
||||
],
|
||||
"lastAppliedRevision": 0,
|
||||
"lastSettingsRevision": 0,
|
||||
"limits": {
|
||||
"maxPdfBytes": 536870912,
|
||||
"maxZplBytes": 104857600,
|
||||
"maxFrameBytes": 16777216,
|
||||
"maxQueuedFrames": 4096,
|
||||
"maxQueuedFrameBytes": 67108864,
|
||||
"maxConcurrentDownloads": 16,
|
||||
"maxConcurrentPrints": 16,
|
||||
"maxStagedBytes": 4294967296,
|
||||
"maxCopies": 32767
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"type": "PRINTERS",
|
||||
"messageId": "printers-connection-1",
|
||||
"payload": {
|
||||
"printers": [
|
||||
{
|
||||
"printerId": "laser-example",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"printerId": "label-example",
|
||||
"enabled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@qrun-io/qqq-print-protocol",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@qrun-io/qqq-print-protocol",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "10.0.1",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qrun-io/qqq-print-protocol",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"description": "Versioned wire contract and compatibility fixtures for QQQ cloud printing.",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -993,7 +993,8 @@
|
||||
"enum": [
|
||||
"REFRESH_INVENTORY",
|
||||
"CANCEL_SPOOL",
|
||||
"printer-media-v1"
|
||||
"printer-media-v1",
|
||||
"printer-approval-v1"
|
||||
]
|
||||
},
|
||||
"ProtocolVersionResponse": {
|
||||
@@ -1060,7 +1061,7 @@
|
||||
"items": {
|
||||
"$ref": "#/$defs/ControlCapability"
|
||||
},
|
||||
"maxItems": 3,
|
||||
"maxItems": 4,
|
||||
"uniqueItems": true
|
||||
},
|
||||
"lastPollAt": {
|
||||
@@ -1099,7 +1100,7 @@
|
||||
"items": {
|
||||
"$ref": "#/$defs/ControlCapability"
|
||||
},
|
||||
"maxItems": 3,
|
||||
"maxItems": 4,
|
||||
"uniqueItems": true
|
||||
},
|
||||
"lastAppliedRevision": {
|
||||
@@ -1718,11 +1719,12 @@
|
||||
"realtime-v1",
|
||||
"REFRESH_INVENTORY",
|
||||
"CANCEL_SPOOL",
|
||||
"printer-media-v1"
|
||||
"printer-media-v1",
|
||||
"printer-approval-v1"
|
||||
]
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 4,
|
||||
"maxItems": 5,
|
||||
"uniqueItems": true,
|
||||
"contains": {
|
||||
"const": "realtime-v1"
|
||||
@@ -2068,6 +2070,61 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PrinterApprovalState": {
|
||||
"description": "Server approval of one printer in the device's latest accepted inventory; enabled:false means known but not approved.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"printerId",
|
||||
"enabled"
|
||||
],
|
||||
"properties": {
|
||||
"printerId": {
|
||||
"$ref": "#/$defs/Id"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"RealtimePrintersPayload": {
|
||||
"description": "Complete approval state of every printer the device currently exposes; each frame replaces the previous one.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"printers"
|
||||
],
|
||||
"properties": {
|
||||
"printers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/PrinterApprovalState"
|
||||
},
|
||||
"maxItems": 200
|
||||
}
|
||||
},
|
||||
"additionalProperties": true
|
||||
},
|
||||
"RealtimePrinters": {
|
||||
"description": "Sent only on connections whose HELLO advertised printer-approval-v1; not acknowledged.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"type",
|
||||
"messageId",
|
||||
"payload"
|
||||
],
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "PRINTERS"
|
||||
},
|
||||
"messageId": {
|
||||
"$ref": "#/$defs/Id"
|
||||
},
|
||||
"payload": {
|
||||
"$ref": "#/$defs/RealtimePrintersPayload"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"RealtimeCommand": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -2184,6 +2241,9 @@
|
||||
{
|
||||
"$ref": "#/$defs/RealtimeJobAvailable"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/RealtimePrinters"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/RealtimeCommand"
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// Shared correspondence between wire examples and schema definitions.
|
||||
export const fixtureTypes = {
|
||||
"printing-realtime-extension.discovery.json": "PrintingRealtimeExtensionDiscovery",
|
||||
"printing-realtime-extension-240.discovery.json": "PrintingRealtimeExtensionDiscovery",
|
||||
"version-legacy.response.json": "ProtocolVersionResponse",
|
||||
"version-controls.response.json": "ProtocolVersionResponse",
|
||||
"control-poll.request.json": "DeviceControlPollRequest",
|
||||
@@ -82,4 +83,6 @@ export const fixtureTypes = {
|
||||
"operator-printers.response.json": "OperatorPrinterList",
|
||||
"operator-submit.request.json": "OperatorSubmitJobRequest",
|
||||
"realtime-media-hello.message.json": "RealtimeHello",
|
||||
"realtime-approval-hello.message.json": "RealtimeHello",
|
||||
"realtime-printers.message.json": "RealtimePrinters",
|
||||
};
|
||||
@@ -27,6 +27,10 @@ for(const version of ["1.0.0", "1.1.0", "3.0.0", "2.0", "2.01.0", "2.0.0-rc.1",
|
||||
}
|
||||
assert(!valid("PrintingRealtimeExtensionDiscovery", { ...extension, name: "another-extension" }));
|
||||
assert(!valid("PrintingRealtimeExtensionDiscovery", { ...extension, pathPrefix: "/printing/v1" }));
|
||||
const approvalExtension = fixture("printing-realtime-extension-240.discovery.json");
|
||||
assert(valid("PrintingRealtimeExtensionDiscovery", approvalExtension), ajv.errorsText());
|
||||
assert.equal(approvalExtension["version"], "2.4.0", "printer-approval-v1 has feature minimum 2.4.0.");
|
||||
assert.deepEqual({ ...approvalExtension, version: extension["version"] }, extension, "2.4.0 keeps the 2.x descriptor name and path prefix.");
|
||||
|
||||
/*******************************************************************************
|
||||
** Cross-field policy relationships require semantic checks beyond JSON Schema.
|
||||
@@ -91,8 +95,23 @@ assert(!valid("RealtimeHello", { ...hello, messageId: "hello:unsafe-prefix" }));
|
||||
assert(!valid("RealtimeHello", { ...hello, token: "not-a-real-token" }));
|
||||
assert(!valid("RealtimeClientMessage", { type: "RUN_SHELL", messageId: "unsupported-1", payload: {} }));
|
||||
|
||||
/*******************************************************************************
|
||||
** Capability hellos: the 2.3.0 four-capability and 2.4.0 five-capability peers
|
||||
** both remain valid; the list is bounded, unique and closed.
|
||||
*******************************************************************************/
|
||||
const mediaHello = /** @type {typeof hello} */ (/** @type {unknown} */ (fixture("realtime-media-hello.message.json")));
|
||||
const approvalHello = /** @type {typeof import("../fixtures/realtime-approval-hello.message.json")} */ (/** @type {unknown} */ (fixture("realtime-approval-hello.message.json")));
|
||||
assert(valid("RealtimeHello", mediaHello), ajv.errorsText());
|
||||
assert(valid("RealtimeHello", approvalHello), ajv.errorsText());
|
||||
assert.equal(approvalHello.payload.capabilities.length, 5);
|
||||
assert(approvalHello.payload.capabilities.includes("printer-approval-v1"));
|
||||
assert.deepEqual(approvalHello.payload.limits, hello.payload.limits, "Approval negotiation does not change declared ceilings.");
|
||||
assert(!valid("RealtimeHello", { ...approvalHello, payload: { ...approvalHello.payload, capabilities: [...approvalHello.payload.capabilities, "printer-approval-v1"] } }), "Duplicate capability accepted");
|
||||
assert(!valid("RealtimeHello", { ...approvalHello, payload: { ...approvalHello.payload, capabilities: [...approvalHello.payload.capabilities, "printer-approval-v2"] } }), "Unknown sixth capability accepted");
|
||||
assert(!valid("RealtimeHello", { ...hello, payload: { ...hello.payload, capabilities: ["printer-approval-v1"] } }), "realtime-v1 remains mandatory");
|
||||
|
||||
const clientFixtures = ["hello", "state-applied", "job-event", "command-receipt", "heartbeat", "inventory", "ping"];
|
||||
const serverFixtures = ["welcome", "desired-state", "job-available", "command", "settings", "ack", "error", "pong"];
|
||||
const serverFixtures = ["welcome", "desired-state", "job-available", "printers", "command", "settings", "ack", "error", "pong"];
|
||||
for(const [files, accepted, rejected] of /** @type {[string[],string,string][]} */ ([
|
||||
[clientFixtures, "RealtimeClientMessage", "RealtimeServerMessage"],
|
||||
[serverFixtures, "RealtimeServerMessage", "RealtimeClientMessage"],
|
||||
@@ -110,6 +129,26 @@ for(const [files, accepted, rejected] of /** @type {[string[],string,string][]}
|
||||
assert(!valid(accepted, { ...message, contentUrl: "https://unrelated.invalid/document" }));
|
||||
}
|
||||
}
|
||||
/*******************************************************************************
|
||||
** PRINTERS carries the complete approval state of the latest accepted inventory;
|
||||
** entries are bounded, both fields are required and the envelope stays closed.
|
||||
*******************************************************************************/
|
||||
const printers = /** @type {typeof import("../fixtures/realtime-printers.message.json")} */ (/** @type {unknown} */ (fixture("realtime-printers.message.json")));
|
||||
assert.equal(printers.payload.printers.length, 2);
|
||||
assert.deepEqual(printers.payload.printers.map(printer => printer.enabled), [true, false], "Fixture shows one approved and one awaiting printer.");
|
||||
assert(valid("RealtimePrinters", { ...printers, payload: { printers: [] } }), "An empty inventory yields an empty approval list.");
|
||||
assert(valid("RealtimePrinters", { ...printers, payload: { printers: Array(200).fill(printers.payload.printers[0]) } }));
|
||||
assert(!valid("RealtimePrinters", { ...printers, payload: { printers: Array(201).fill(printers.payload.printers[0]) } }));
|
||||
assert(valid("RealtimePrinters", { ...printers, payload: { ...printers.payload, futureField: true } }), "Additive payload fields remain compatible.");
|
||||
assert(valid("RealtimePrinters", { ...printers, payload: { printers: [{ ...printers.payload.printers[0], futureField: true }] } }));
|
||||
assert(!valid("RealtimePrinters", { ...printers, payload: { printers: [{ printerId: "laser-example" }] } }), "enabled is required");
|
||||
assert(!valid("RealtimePrinters", { ...printers, payload: { printers: [{ enabled: true }] } }), "printerId is required");
|
||||
assert(!valid("RealtimePrinters", { ...printers, payload: { printers: [{ printerId: "laser-example", enabled: "true" }] } }), "enabled must be boolean");
|
||||
assert(!valid("RealtimePrinters", { ...printers, payload: { printers: [{ printerId: "", enabled: true }] } }));
|
||||
assert(!valid("RealtimePrinters", { ...printers, payload: {} }), "printers list is required");
|
||||
assert(!valid("RealtimePrinters", { ...printers, type: "JOB_AVAILABLE" }));
|
||||
assert(!valid("RealtimeJobAvailable", { ...printers, type: "JOB_AVAILABLE" }), "PRINTERS payload is not the empty wake-up");
|
||||
|
||||
const event = fixture("realtime-job-event.message.json");
|
||||
assert.deepEqual(fixture("realtime-job-event-replay.message.json"), event, "Lost ACK replay preserves the exact envelope.");
|
||||
assert.equal((/** @type {typeof import("../fixtures/realtime-ack.message.json")} */ (/** @type {unknown} */ (fixture("realtime-ack.message.json")))).payload.acknowledgedMessageId, event["messageId"]);
|
||||
|
||||
@@ -77,6 +77,8 @@ const poll = control("control-poll.request.json");
|
||||
assert(!validControl("DeviceControlPollRequest", { ...poll, controlVersion: 2 }));
|
||||
assert(!validControl("DeviceControlPollRequest", { ...poll, capabilities: ["CANCEL_SPOOL", "CANCEL_SPOOL"] }));
|
||||
assert(!validControl("DeviceControlPollRequest", { ...poll, capabilities: ["RESTART_SERVICE"] }));
|
||||
assert(validControl("DeviceControlPollRequest", { ...poll, capabilities: ["REFRESH_INVENTORY", "CANCEL_SPOOL", "printer-media-v1", "printer-approval-v1"] }), "Historical control poll accepts every non-realtime capability.");
|
||||
assert(!validControl("DeviceControlPollRequest", { ...poll, capabilities: ["REFRESH_INVENTORY", "CANCEL_SPOOL", "printer-media-v1", "printer-approval-v1", "printer-approval-v1"] }));
|
||||
assert(!validControl("DeviceControlPollRequest", { ...poll, lastAppliedRevision: -1 }));
|
||||
assert(!validControl("DeviceControlPollRequest", { ...poll, lastAppliedRevision: 9007199254740992 }));
|
||||
const pollResult = /** @type {typeof import("../fixtures/control-poll.response.json")} */ (/** @type {unknown} */ (control("control-poll.response.json")));
|
||||
|
||||
Reference in new issue
Block a user