feat(realtime): add printer-approval-v1 and the PRINTERS frame for extension 2.4.0
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 3s
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1b3648d9421543350067e95e54959dfb471b26d5] (pull_request) Successful in 5s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI / secrets (pull_request) Successful in 11s
QRun-IO/ci-policy: Shared Node CI / verify (pull_request) Successful in 33s
QRun-IO/ci-policy: Shared Node CI / gate (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / node-8 (pull_request) Successful in 44s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 4s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 3s
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1b3648d9421543350067e95e54959dfb471b26d5] (pull_request) Successful in 5s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI / secrets (pull_request) Successful in 11s
QRun-IO/ci-policy: Shared Node CI / verify (pull_request) Successful in 33s
QRun-IO/ci-policy: Shared Node CI / gate (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / node-8 (pull_request) Successful in 44s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 4s
Add the printer-approval-v1 HELLO capability (maxItems 5), extend ControlCapability and the historical control-poll/management capability arrays to four entries, and define RealtimePrinters, RealtimePrintersPayload and PrinterApprovalState with the union entry. The server sends PRINTERS only on connections that advertised the capability, after each accepted INVENTORY and on every approval change; it is not acknowledged and carries no job data. Add the PRINTERS, five-capability hello and 2.4.0 discovery fixtures, cover the new frame in the directional and negative verifier checks, document the frame, the 2.4.0 registry row and the negotiation rule, and record distribution 1.1.0 as unreleased. Wire payload v1, the subprotocol and every 2.3.0 fixture are unchanged. 79 definitions compile and 82 fixtures pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: James Maes <james@kof22.com>
This commit is contained in:
1 parent
afc0c145ac
commit
ac835f8b93
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