feat: establish standalone Device Core repository
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
createCoreDiscoveryClient,
|
||||
createCoreGatewayClient,
|
||||
} from "../src/core-client.mjs";
|
||||
|
||||
const gatewayToken = "test-only-gateway-token-with-32-bytes";
|
||||
|
||||
test("posts a discovery through the authenticated internal Core boundary", async () => {
|
||||
let captured;
|
||||
const observe = createCoreDiscoveryClient({
|
||||
coreUrl: "http://device-control-core:18120",
|
||||
gatewayToken,
|
||||
fetchImpl: async (url, options) => {
|
||||
captured = { url, options };
|
||||
return new Response(JSON.stringify({
|
||||
ok: true,
|
||||
discovery: {
|
||||
lifecycleState: "quarantine",
|
||||
commandTransport: "disabled",
|
||||
},
|
||||
}), {
|
||||
status: 201,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
},
|
||||
});
|
||||
const signal = {
|
||||
schemaVersion: "nodedc.device.discovery-signal.v1",
|
||||
sessionRef: "session:test",
|
||||
};
|
||||
const discovery = await observe(signal);
|
||||
assert.equal(
|
||||
captured.url,
|
||||
"http://device-control-core:18120/internal/v1/device-discoveries:observe",
|
||||
);
|
||||
assert.equal(
|
||||
captured.options.headers.Authorization,
|
||||
`Bearer ${gatewayToken}`,
|
||||
);
|
||||
assert.deepEqual(JSON.parse(captured.options.body), signal);
|
||||
assert.equal(discovery.lifecycleState, "quarantine");
|
||||
});
|
||||
|
||||
test("fails closed when Core does not return an accepted discovery view", async () => {
|
||||
const observe = createCoreDiscoveryClient({
|
||||
coreUrl: "http://device-control-core:18120",
|
||||
gatewayToken,
|
||||
fetchImpl: async () => new Response(JSON.stringify({
|
||||
ok: true,
|
||||
discovery: {
|
||||
lifecycleState: "observed",
|
||||
commandTransport: "disabled",
|
||||
},
|
||||
}), { status: 200 }),
|
||||
});
|
||||
await assert.rejects(
|
||||
() => observe({ schemaVersion: "test" }),
|
||||
/device_gateway_core_ingest_contract_invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts a package only through the explicit Core acceptance contract", async () => {
|
||||
let captured;
|
||||
const client = createCoreGatewayClient({
|
||||
coreUrl: "http://device-control-core:18120",
|
||||
gatewayToken,
|
||||
fetchImpl: async (url, options) => {
|
||||
captured = { url, options };
|
||||
const message = JSON.parse(options.body);
|
||||
return new Response(JSON.stringify({
|
||||
ok: true,
|
||||
acceptance: {
|
||||
schemaVersion: "nodedc.device-adapter-acceptance.v1",
|
||||
acceptanceRef: "acceptance:test-001",
|
||||
idempotencyKey: message.idempotencyKey,
|
||||
status: "accepted",
|
||||
replayed: false,
|
||||
acceptedAt: "2026-08-11T12:00:00.000Z",
|
||||
},
|
||||
}), { status: 201 });
|
||||
},
|
||||
});
|
||||
const message = { idempotencyKey: `sha256:${"a".repeat(64)}` };
|
||||
const acceptance = await client.acceptMessage(message);
|
||||
assert.equal(
|
||||
captured.url,
|
||||
"http://device-control-core:18120/internal/v1/gateway/messages:accept",
|
||||
);
|
||||
assert.equal(acceptance.status, "accepted");
|
||||
assert.equal(acceptance.idempotencyKey, message.idempotencyKey);
|
||||
});
|
||||
|
||||
test("rejects a mismatched or non-durable Core package response", async () => {
|
||||
const client = createCoreGatewayClient({
|
||||
coreUrl: "http://device-control-core:18120",
|
||||
gatewayToken,
|
||||
fetchImpl: async () => new Response(JSON.stringify({
|
||||
ok: true,
|
||||
acceptance: {
|
||||
schemaVersion: "nodedc.device-adapter-acceptance.v1",
|
||||
acceptanceRef: "acceptance:test-001",
|
||||
idempotencyKey: `sha256:${"b".repeat(64)}`,
|
||||
status: "accepted",
|
||||
replayed: false,
|
||||
acceptedAt: "2026-08-11T12:00:00.000Z",
|
||||
},
|
||||
}), { status: 201 }),
|
||||
});
|
||||
await assert.rejects(
|
||||
() => client.acceptMessage({
|
||||
idempotencyKey: `sha256:${"a".repeat(64)}`,
|
||||
}),
|
||||
/device_gateway_core_acceptance_mismatch/,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
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 {
|
||||
createControlCoreApp,
|
||||
} from "../../device-control-core/src/app.mjs";
|
||||
import { createCoreGatewayClient } from "../src/core-client.mjs";
|
||||
import { createDeviceGatewayRuntime } from "../src/runtime.mjs";
|
||||
|
||||
const gatewayToken = "test-only-gateway-token-with-32-bytes";
|
||||
const identifierPepper = "test-only-identifier-pepper-with-32-bytes";
|
||||
const specificationHeader = Buffer.from(
|
||||
"FF23E9EF782DE7120300",
|
||||
"hex",
|
||||
);
|
||||
const specificationPackage = Buffer.from(
|
||||
"5B01010000FBDEC251EC5D",
|
||||
"hex",
|
||||
);
|
||||
|
||||
test("B2 HEADER2 becomes a persisted masked quarantine discovery before ACK", async () => {
|
||||
let stored;
|
||||
let storedMessage;
|
||||
const core = createControlCoreApp({
|
||||
discoveryIngestEnabled: true,
|
||||
gatewayToken,
|
||||
identifierPepper,
|
||||
repository: {
|
||||
health: async () => "ready",
|
||||
upsertQuarantineDiscovery: async (value) => {
|
||||
stored = value;
|
||||
return {
|
||||
created: true,
|
||||
value: {
|
||||
...value.safeView,
|
||||
discoveryRef: "discovery:integration-001",
|
||||
},
|
||||
};
|
||||
},
|
||||
acceptAdapterMessage: async (value) => {
|
||||
storedMessage = value;
|
||||
return {
|
||||
acceptance: {
|
||||
schemaVersion: "nodedc.device-adapter-acceptance.v1",
|
||||
acceptanceRef: "acceptance:integration-001",
|
||||
idempotencyKey: value.safeView.idempotencyKey,
|
||||
status: "accepted",
|
||||
replayed: false,
|
||||
acceptedAt: "2026-08-11T12:00:00.000Z",
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
await listen(core);
|
||||
const coreAddress = core.address();
|
||||
const observe = createCoreGatewayClient({
|
||||
coreUrl: `http://127.0.0.1:${coreAddress.port}`,
|
||||
gatewayToken,
|
||||
});
|
||||
const gateway = createDeviceGatewayRuntime({
|
||||
healthPort: 0,
|
||||
tcpHost: "0.0.0.0",
|
||||
tcpPort: 0,
|
||||
listenEnabled: true,
|
||||
publicIngressEnabled: true,
|
||||
coreChannelAuthenticated: true,
|
||||
adapterRegistry: DEVICE_ADAPTER_CATALOG.registry,
|
||||
protocolProfileRef: DEVICE_ADAPTER_CATALOG.defaultProfileRef,
|
||||
edgeRef: "edge:integration-001",
|
||||
now: () => new Date(0x52db95de * 1000),
|
||||
onDiscovery: observe.observeDiscovery,
|
||||
onMessage: observe.acceptMessage,
|
||||
onCommandStatus: async () => undefined,
|
||||
});
|
||||
const addresses = await gateway.start();
|
||||
try {
|
||||
const response = await exchange(
|
||||
addresses.tcpAddress.port,
|
||||
Buffer.concat([specificationHeader, specificationPackage]),
|
||||
13,
|
||||
);
|
||||
assert.equal(
|
||||
response.toString("hex").toUpperCase(),
|
||||
"7B0400A0DE95DB527D7B00017D",
|
||||
);
|
||||
assert.match(stored.identifierDigest, /^hmac-sha256:[a-f0-9]{64}$/);
|
||||
assert.equal(stored.safeView.lifecycleState, "quarantine");
|
||||
assert.equal(stored.safeView.identifier.masked, "***********7769");
|
||||
assert.equal(stored.safeView.commandTransport, "disabled");
|
||||
assert.equal(
|
||||
JSON.stringify(stored).includes("865209039777769"),
|
||||
false,
|
||||
);
|
||||
assert.equal(storedMessage.safeView.messageType, "telemetry.package");
|
||||
assert.equal(storedMessage.safeView.identifier.masked, "***********7769");
|
||||
assert.equal(
|
||||
JSON.stringify(storedMessage).includes("865209039777769"),
|
||||
false,
|
||||
);
|
||||
} finally {
|
||||
await gateway.stop();
|
||||
await close(core);
|
||||
}
|
||||
});
|
||||
|
||||
function listen(server) {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function close(server) {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
server.closeAllConnections?.();
|
||||
});
|
||||
}
|
||||
|
||||
function exchange(port, payload, expectedBytes) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
let byteLength = 0;
|
||||
const socket = connect({ host: "127.0.0.1", port }, () => {
|
||||
socket.write(payload);
|
||||
});
|
||||
socket.on("data", (chunk) => {
|
||||
chunks.push(chunk);
|
||||
byteLength += chunk.length;
|
||||
if (byteLength >= expectedBytes) {
|
||||
socket.destroy();
|
||||
resolve(Buffer.concat(chunks, byteLength));
|
||||
}
|
||||
});
|
||||
socket.on("error", reject);
|
||||
socket.on("close", () => {
|
||||
if (byteLength < expectedBytes) {
|
||||
reject(new Error(
|
||||
`device_gateway_test_socket_closed_early:${byteLength}/${expectedBytes}`,
|
||||
));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
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(
|
||||
"FF23E9EF782DE7120300",
|
||||
"hex",
|
||||
);
|
||||
const specificationPackage = Buffer.from(
|
||||
"5B01010000FBDEC251EC5D",
|
||||
"hex",
|
||||
);
|
||||
|
||||
test("baseline health exposes no public ingress and no command transport", async () => {
|
||||
const runtime = createDeviceGatewayRuntime({
|
||||
healthPort: 0,
|
||||
listenEnabled: false,
|
||||
});
|
||||
const addresses = await runtime.start();
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://127.0.0.1:${addresses.healthAddress.port}/healthz`,
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const body = await response.json();
|
||||
assert.equal(body.publicIngress, "disabled");
|
||||
assert.equal(body.commandTransport, "disabled");
|
||||
assert.equal(body.tcpListener, "disabled");
|
||||
assert.equal(addresses.tcpAddress, null);
|
||||
} finally {
|
||||
await runtime.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("telemetry ingress persists HEADER2 before acknowledging packages", async () => {
|
||||
const captured = [];
|
||||
const runtime = createDeviceGatewayRuntime({
|
||||
healthPort: 0,
|
||||
tcpHost: "0.0.0.0",
|
||||
tcpPort: 0,
|
||||
listenEnabled: true,
|
||||
publicIngressEnabled: true,
|
||||
coreChannelAuthenticated: true,
|
||||
now: () => new Date(0x52db95de * 1000),
|
||||
...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);
|
||||
try {
|
||||
client.socket.write(specificationHeader.subarray(0, 4));
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(client.bytes().length, 0);
|
||||
|
||||
client.socket.write(specificationHeader.subarray(4));
|
||||
await client.waitForBytes(9);
|
||||
assert.equal(
|
||||
client.bytes().subarray(0, 9).toString("hex").toUpperCase(),
|
||||
"7B0400A0DE95DB527D",
|
||||
);
|
||||
assert.equal(captured.length, 1);
|
||||
assert.equal(captured[0].identifier.value, "865209039777769");
|
||||
assert.equal(captured[0].evidence.framingStatus, "verified");
|
||||
assert.equal(captured[0].commandTransport, undefined);
|
||||
|
||||
client.socket.write(specificationPackage);
|
||||
await client.waitForBytes(13);
|
||||
assert.equal(
|
||||
client.bytes().subarray(9).toString("hex").toUpperCase(),
|
||||
"7B00017D",
|
||||
);
|
||||
assert.equal(runtime.status().totalDiscoveries, 1);
|
||||
assert.equal(runtime.status().totalMessagesAccepted, 1);
|
||||
assert.equal(runtime.status().totalPackagesAcknowledged, 1);
|
||||
assert.equal(runtime.status().commandTransport, "typed-service-ping-v1");
|
||||
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, "telemetry-ingest");
|
||||
assert.equal(body.publicIngress, "telemetry-ingest");
|
||||
assert.equal(body.commandTransport, "typed-service-ping-v1");
|
||||
} finally {
|
||||
client.socket.destroy();
|
||||
await runtime.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("dispatches a typed service ping on the next telemetry package and records SERV OK", async () => {
|
||||
const statuses = [];
|
||||
const runtime = createDeviceGatewayRuntime({
|
||||
healthPort: 0,
|
||||
tcpPort: 0,
|
||||
listenEnabled: true,
|
||||
...gatewayAdapterOptions(),
|
||||
onDiscovery: async () => ({ lifecycleState: "claimed" }),
|
||||
onMessage: async (message) => ({
|
||||
...acceptanceFor(message),
|
||||
commandOffer: {
|
||||
commandRef: "command:11111111-1111-4111-8111-111111111111",
|
||||
commandType: "service.ping",
|
||||
accessCode: "123456",
|
||||
transportMessageRef: "edge-command:22222222-2222-4222-8222-222222222222",
|
||||
},
|
||||
}),
|
||||
onCommandStatus: async (status) => statuses.push(status),
|
||||
});
|
||||
const addresses = await runtime.start();
|
||||
const client = await connectAndCollect(addresses.tcpAddress.port);
|
||||
try {
|
||||
client.socket.write(Buffer.concat([specificationHeader, specificationPackage]));
|
||||
await client.waitForBytes(28);
|
||||
assert.equal(
|
||||
client.bytes().subarray(13).toString("ascii"),
|
||||
"123456*SERV*1.1",
|
||||
);
|
||||
client.socket.write(Buffer.from("SERV OK", "ascii"));
|
||||
await waitFor(() => statuses.length === 1);
|
||||
assert.deepEqual(statuses[0], {
|
||||
commandRef: "command:11111111-1111-4111-8111-111111111111",
|
||||
transportMessageRef: "edge-command:22222222-2222-4222-8222-222222222222",
|
||||
lifecycleState: "acknowledged",
|
||||
resultCode: "serv_ok",
|
||||
observedAt: statuses[0].observedAt,
|
||||
sessionRef: statuses[0].sessionRef,
|
||||
adapterProfileRef: "arusnavi.b2.internal.v1",
|
||||
});
|
||||
} finally {
|
||||
client.socket.destroy();
|
||||
await runtime.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("does not acknowledge malformed or unverified initial bytes", async () => {
|
||||
const captured = [];
|
||||
const runtime = createDeviceGatewayRuntime({
|
||||
healthPort: 0,
|
||||
tcpPort: 0,
|
||||
listenEnabled: true,
|
||||
...gatewayAdapterOptions(),
|
||||
onDiscovery: async (value) => {
|
||||
captured.push(value);
|
||||
return { lifecycleState: "quarantine" };
|
||||
},
|
||||
onMessage: async (message) => acceptanceFor(message),
|
||||
});
|
||||
const addresses = await runtime.start();
|
||||
try {
|
||||
const received = await sendAndCollect(
|
||||
addresses.tcpAddress.port,
|
||||
Buffer.from("not-a-b2-header", "utf8"),
|
||||
);
|
||||
assert.equal(received.length, 0);
|
||||
assert.equal(captured.length, 0);
|
||||
assert.equal(runtime.status().totalRejected, 1);
|
||||
} finally {
|
||||
await runtime.stop();
|
||||
}
|
||||
});
|
||||
|
||||
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_authenticated_core_channel_required/,
|
||||
);
|
||||
});
|
||||
|
||||
test("baseline rejects non-loopback binding", () => {
|
||||
assert.throws(
|
||||
() => createDeviceGatewayRuntime({
|
||||
listenEnabled: true,
|
||||
tcpHost: "0.0.0.0",
|
||||
...gatewayAdapterOptions(),
|
||||
onDiscovery: async () => ({ lifecycleState: "quarantine" }),
|
||||
onMessage: async (message) => acceptanceFor(message),
|
||||
}),
|
||||
/device_gateway_baseline_loopback_only/,
|
||||
);
|
||||
});
|
||||
|
||||
test("container health may bind all interfaces while TCP stays disabled", async () => {
|
||||
const runtime = createDeviceGatewayRuntime({
|
||||
healthHost: "0.0.0.0",
|
||||
healthPort: 0,
|
||||
listenEnabled: false,
|
||||
});
|
||||
const addresses = await runtime.start();
|
||||
try {
|
||||
assert.equal(addresses.healthAddress.address, "0.0.0.0");
|
||||
assert.equal(addresses.tcpAddress, null);
|
||||
assert.equal(runtime.status().publicIngress, "disabled");
|
||||
} finally {
|
||||
await runtime.stop();
|
||||
}
|
||||
});
|
||||
|
||||
function connectAndCollect(port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
let byteLength = 0;
|
||||
const waiters = [];
|
||||
const socket = connect({ host: "127.0.0.1", port }, () => {
|
||||
resolve({
|
||||
socket,
|
||||
bytes: () => Buffer.concat(chunks, byteLength),
|
||||
waitForBytes: (minimum) => {
|
||||
if (byteLength >= minimum) return Promise.resolve();
|
||||
return new Promise((waitResolve, waitReject) => {
|
||||
waiters.push({ minimum, waitResolve, waitReject });
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
socket.on("data", (chunk) => {
|
||||
chunks.push(chunk);
|
||||
byteLength += chunk.length;
|
||||
for (let index = waiters.length - 1; index >= 0; index -= 1) {
|
||||
if (byteLength >= waiters[index].minimum) {
|
||||
waiters[index].waitResolve();
|
||||
waiters.splice(index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
socket.on("error", (error) => {
|
||||
for (const waiter of waiters.splice(0)) waiter.waitReject(error);
|
||||
reject(error);
|
||||
});
|
||||
socket.on("close", () => {
|
||||
for (const waiter of waiters.splice(0)) {
|
||||
waiter.waitReject(new Error("device_gateway_test_socket_closed"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendAndCollect(port, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
const socket = connect({ host: "127.0.0.1", port }, () => {
|
||||
socket.end(payload);
|
||||
});
|
||||
socket.on("data", (chunk) => chunks.push(chunk));
|
||||
socket.on("close", () => resolve(Buffer.concat(chunks)));
|
||||
socket.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function gatewayAdapterOptions() {
|
||||
return {
|
||||
adapterRegistry: DEVICE_ADAPTER_CATALOG.registry,
|
||||
protocolProfileRef: DEVICE_ADAPTER_CATALOG.defaultProfileRef,
|
||||
edgeRef: "edge:test-001",
|
||||
onCommandStatus: async () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
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",
|
||||
};
|
||||
}
|
||||
|
||||
async function waitFor(predicate, timeoutMs = 1_000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (!predicate()) {
|
||||
if (Date.now() >= deadline) throw new Error("test_wait_timeout");
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user