feat(device-plane): add universal adapter acceptance boundary
This commit is contained in:
@@ -2,6 +2,9 @@ import assert from "node:assert/strict";
|
||||
import { connect } from "node:net";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
DEVICE_ADAPTER_CATALOG,
|
||||
} from "../../../packages/device-adapter-catalog/src/index.mjs";
|
||||
import { createDeviceGatewayRuntime } from "../src/runtime.mjs";
|
||||
|
||||
const specificationHeader = Buffer.from(
|
||||
@@ -34,7 +37,7 @@ test("baseline health exposes no public ingress and no command transport", async
|
||||
}
|
||||
});
|
||||
|
||||
test("discovery-only ingress persists HEADER2 before acknowledging packages", async () => {
|
||||
test("telemetry ingress persists HEADER2 before acknowledging packages", async () => {
|
||||
const captured = [];
|
||||
const runtime = createDeviceGatewayRuntime({
|
||||
healthPort: 0,
|
||||
@@ -42,8 +45,14 @@ test("discovery-only ingress persists HEADER2 before acknowledging packages", as
|
||||
tcpPort: 0,
|
||||
listenEnabled: true,
|
||||
publicIngressEnabled: true,
|
||||
coreChannelAuthenticated: true,
|
||||
now: () => new Date(0x52db95de * 1000),
|
||||
onDiscovery: async (value) => captured.push(value),
|
||||
...gatewayAdapterOptions(),
|
||||
onDiscovery: async (value) => {
|
||||
captured.push(value);
|
||||
return { lifecycleState: "quarantine" };
|
||||
},
|
||||
onMessage: async (message) => acceptanceFor(message),
|
||||
});
|
||||
const addresses = await runtime.start();
|
||||
const client = await connectAndCollect(addresses.tcpAddress.port);
|
||||
@@ -70,17 +79,18 @@ test("discovery-only ingress persists HEADER2 before acknowledging packages", as
|
||||
"7B00017D",
|
||||
);
|
||||
assert.equal(runtime.status().totalDiscoveries, 1);
|
||||
assert.equal(runtime.status().totalMessagesAccepted, 1);
|
||||
assert.equal(runtime.status().totalPackagesAcknowledged, 1);
|
||||
assert.equal(runtime.status().commandTransport, "disabled");
|
||||
assert.equal(runtime.status().publicIngress, "discovery-only");
|
||||
assert.equal(runtime.status().publicIngress, "telemetry-ingest");
|
||||
|
||||
const response = await fetch(
|
||||
`http://127.0.0.1:${addresses.healthAddress.port}/healthz`,
|
||||
);
|
||||
const body = await response.json();
|
||||
assert.equal(body.framing, "verified-read-only");
|
||||
assert.equal(body.tcpListener, "discovery-only");
|
||||
assert.equal(body.publicIngress, "discovery-only");
|
||||
assert.equal(body.tcpListener, "telemetry-ingest");
|
||||
assert.equal(body.publicIngress, "telemetry-ingest");
|
||||
assert.equal(body.commandTransport, "disabled");
|
||||
} finally {
|
||||
client.socket.destroy();
|
||||
@@ -94,7 +104,12 @@ test("does not acknowledge malformed or unverified initial bytes", async () => {
|
||||
healthPort: 0,
|
||||
tcpPort: 0,
|
||||
listenEnabled: true,
|
||||
onDiscovery: async (value) => captured.push(value),
|
||||
...gatewayAdapterOptions(),
|
||||
onDiscovery: async (value) => {
|
||||
captured.push(value);
|
||||
return { lifecycleState: "quarantine" };
|
||||
},
|
||||
onMessage: async (message) => acceptanceFor(message),
|
||||
});
|
||||
const addresses = await runtime.start();
|
||||
try {
|
||||
@@ -110,14 +125,137 @@ test("does not acknowledge malformed or unverified initial bytes", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("public ingress requires an authenticated discovery sink", () => {
|
||||
test("does not acknowledge a header when Core rejects discovery", async () => {
|
||||
const runtime = createDeviceGatewayRuntime({
|
||||
healthPort: 0,
|
||||
tcpPort: 0,
|
||||
listenEnabled: true,
|
||||
...gatewayAdapterOptions(),
|
||||
onDiscovery: async () => {
|
||||
throw new Error("core_unavailable");
|
||||
},
|
||||
onMessage: async (message) => acceptanceFor(message),
|
||||
});
|
||||
const addresses = await runtime.start();
|
||||
try {
|
||||
const received = await sendAndCollect(
|
||||
addresses.tcpAddress.port,
|
||||
specificationHeader,
|
||||
);
|
||||
assert.equal(received.length, 0);
|
||||
assert.equal(runtime.status().totalDiscoveries, 0);
|
||||
assert.equal(runtime.status().totalRejected, 1);
|
||||
} finally {
|
||||
await runtime.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("does not acknowledge a package when durable Core acceptance fails", async () => {
|
||||
const runtime = createDeviceGatewayRuntime({
|
||||
healthPort: 0,
|
||||
tcpPort: 0,
|
||||
listenEnabled: true,
|
||||
now: () => new Date(0x52db95de * 1000),
|
||||
...gatewayAdapterOptions(),
|
||||
onDiscovery: async () => ({ lifecycleState: "quarantine" }),
|
||||
onMessage: async () => {
|
||||
throw new Error("core_commit_failed");
|
||||
},
|
||||
});
|
||||
const addresses = await runtime.start();
|
||||
try {
|
||||
const received = await sendAndCollect(
|
||||
addresses.tcpAddress.port,
|
||||
Buffer.concat([specificationHeader, specificationPackage]),
|
||||
);
|
||||
assert.equal(
|
||||
received.toString("hex").toUpperCase(),
|
||||
"7B0400A0DE95DB527D",
|
||||
);
|
||||
assert.equal(runtime.status().totalMessagesAccepted, 0);
|
||||
assert.equal(runtime.status().totalPackagesAcknowledged, 0);
|
||||
assert.equal(runtime.status().totalRejected, 1);
|
||||
} finally {
|
||||
await runtime.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("acknowledges an idempotent Core replay as accepted delivery", async () => {
|
||||
const runtime = createDeviceGatewayRuntime({
|
||||
healthPort: 0,
|
||||
tcpPort: 0,
|
||||
listenEnabled: true,
|
||||
now: () => new Date(0x52db95de * 1000),
|
||||
...gatewayAdapterOptions(),
|
||||
onDiscovery: async () => ({ lifecycleState: "quarantine" }),
|
||||
onMessage: async (message) => acceptanceFor(message, true),
|
||||
});
|
||||
const addresses = await runtime.start();
|
||||
try {
|
||||
const received = await sendAndCollect(
|
||||
addresses.tcpAddress.port,
|
||||
Buffer.concat([specificationHeader, specificationPackage]),
|
||||
);
|
||||
assert.equal(
|
||||
received.toString("hex").toUpperCase(),
|
||||
"7B0400A0DE95DB527D7B00017D",
|
||||
);
|
||||
assert.equal(runtime.status().totalMessagesAccepted, 1);
|
||||
assert.equal(runtime.status().totalPackagesAcknowledged, 1);
|
||||
} finally {
|
||||
await runtime.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("closes an oversized tracker buffer and releases its aggregate budget", async () => {
|
||||
const runtime = createDeviceGatewayRuntime({
|
||||
healthPort: 0,
|
||||
tcpPort: 0,
|
||||
listenEnabled: true,
|
||||
maxBufferedBytes: 1024,
|
||||
maxAggregateBufferedBytes: 1024,
|
||||
...gatewayAdapterOptions(),
|
||||
onDiscovery: async () => ({ lifecycleState: "quarantine" }),
|
||||
onMessage: async (message) => acceptanceFor(message),
|
||||
});
|
||||
const addresses = await runtime.start();
|
||||
try {
|
||||
const received = await sendAndCollect(
|
||||
addresses.tcpAddress.port,
|
||||
Buffer.alloc(1025, 0x01),
|
||||
);
|
||||
assert.equal(received.length, 0);
|
||||
assert.equal(runtime.status().totalRejected, 1);
|
||||
assert.equal(runtime.status().totalBufferedBytes, 0);
|
||||
assert.equal(runtime.status().activeSessions, 0);
|
||||
} finally {
|
||||
await runtime.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("public ingress requires both discovery and durable message acceptance sinks", () => {
|
||||
assert.throws(
|
||||
() => createDeviceGatewayRuntime({
|
||||
listenEnabled: true,
|
||||
publicIngressEnabled: true,
|
||||
coreChannelAuthenticated: true,
|
||||
tcpHost: "0.0.0.0",
|
||||
}),
|
||||
/device_gateway_core_acceptance_sink_required/,
|
||||
);
|
||||
});
|
||||
|
||||
test("public ingress cannot start on the legacy bearer HTTP Core client", () => {
|
||||
assert.throws(
|
||||
() => createDeviceGatewayRuntime({
|
||||
listenEnabled: true,
|
||||
publicIngressEnabled: true,
|
||||
tcpHost: "0.0.0.0",
|
||||
...gatewayAdapterOptions(),
|
||||
onDiscovery: async () => ({ lifecycleState: "quarantine" }),
|
||||
onMessage: async (message) => acceptanceFor(message),
|
||||
}),
|
||||
/device_gateway_discovery_sink_required/,
|
||||
/device_gateway_authenticated_core_channel_required/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -126,6 +264,9 @@ test("baseline rejects non-loopback binding", () => {
|
||||
() => createDeviceGatewayRuntime({
|
||||
listenEnabled: true,
|
||||
tcpHost: "0.0.0.0",
|
||||
...gatewayAdapterOptions(),
|
||||
onDiscovery: async () => ({ lifecycleState: "quarantine" }),
|
||||
onMessage: async (message) => acceptanceFor(message),
|
||||
}),
|
||||
/device_gateway_baseline_loopback_only/,
|
||||
);
|
||||
@@ -192,3 +333,22 @@ function sendAndCollect(port, payload) {
|
||||
socket.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function gatewayAdapterOptions() {
|
||||
return {
|
||||
adapterRegistry: DEVICE_ADAPTER_CATALOG.registry,
|
||||
protocolProfileRef: DEVICE_ADAPTER_CATALOG.defaultProfileRef,
|
||||
edgeRef: "edge:test-001",
|
||||
};
|
||||
}
|
||||
|
||||
function acceptanceFor(message, replayed = false) {
|
||||
return {
|
||||
schemaVersion: "nodedc.device-adapter-acceptance.v1",
|
||||
acceptanceRef: "acceptance:test-001",
|
||||
idempotencyKey: message.idempotencyKey,
|
||||
status: "accepted",
|
||||
replayed,
|
||||
acceptedAt: "2026-08-11T12:00:00.000Z",
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user