2026-01-14 17:58:45 +08:00
|
|
|
import assert from 'node:assert/strict';
|
|
|
|
|
import { HeartbeatProcessor } from '../src/processor/heartbeatProcessor.js';
|
2026-01-17 18:37:44 +08:00
|
|
|
import { RedisIntegration } from '../src/redis/redisIntegration.js';
|
2026-01-14 17:58:45 +08:00
|
|
|
|
|
|
|
|
describe('HeartbeatProcessor smoke', () => {
|
|
|
|
|
it('decodes JSON buffer into object', () => {
|
|
|
|
|
const processor = new HeartbeatProcessor(
|
|
|
|
|
{ batchSize: 100, batchTimeout: 1000 },
|
|
|
|
|
{ insertHeartbeatEvents: async () => {} }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const payload = { ts_ms: 1700000000123, hotel_id: 1, room_id: 2, device_id: 'd', ip: '127.0.0.1', power_state: 1, guest_type: 0, cardless_state: 0, service_mask: 1, pms_state: 1, carbon_state: 0, device_count: 1, comm_seq: 1 };
|
|
|
|
|
const message = { value: Buffer.from(JSON.stringify(payload), 'utf8') };
|
|
|
|
|
const decoded = processor.unpackMessage(message);
|
|
|
|
|
assert.equal(decoded.hotel_id, 1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('accepts camelCase fields via normalizeHeartbeat', () => {
|
|
|
|
|
const processor = new HeartbeatProcessor(
|
|
|
|
|
{ batchSize: 100, batchTimeout: 1000 },
|
|
|
|
|
{ insertHeartbeatEvents: async () => {} }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const payload = { tsMs: 1700000000123, hotelId: 1, roomId: 2, deviceId: 'd', ip: '127.0.0.1', powerState: 1, guestType: 0, cardlessState: 0, serviceMask: 1, pmsState: 1, carbonState: 0, deviceCount: 1, commSeq: 1 };
|
|
|
|
|
assert.equal(processor.validateData(payload), true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-17 18:37:44 +08:00
|
|
|
describe('RedisIntegration protocol', () => {
|
|
|
|
|
it('writes heartbeat to 项目心跳 LIST', async () => {
|
|
|
|
|
const redis = new RedisIntegration({
|
|
|
|
|
enabled: true,
|
|
|
|
|
projectName: 'BLS主机心跳日志',
|
|
|
|
|
apiBaseUrl: 'http://127.0.0.1:3000',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const calls = { rPush: [], lTrim: [] };
|
|
|
|
|
redis.client = {
|
|
|
|
|
isReady: true,
|
|
|
|
|
rPush: async (key, value) => {
|
|
|
|
|
calls.rPush.push({ key, value });
|
|
|
|
|
},
|
|
|
|
|
lTrim: async (key, start, stop) => {
|
|
|
|
|
calls.lTrim.push({ key, start, stop });
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const before = Date.now();
|
|
|
|
|
await redis.writeHeartbeat();
|
|
|
|
|
const after = Date.now();
|
|
|
|
|
|
|
|
|
|
assert.equal(calls.rPush.length, 1);
|
|
|
|
|
assert.equal(calls.rPush[0].key, '项目心跳');
|
|
|
|
|
const payload = JSON.parse(calls.rPush[0].value);
|
|
|
|
|
assert.equal(payload.projectName, 'BLS主机心跳日志');
|
|
|
|
|
assert.equal(payload.apiBaseUrl, 'http://127.0.0.1:3000');
|
|
|
|
|
assert.equal(typeof payload.lastActiveAt, 'number');
|
|
|
|
|
assert.ok(payload.lastActiveAt >= before && payload.lastActiveAt <= after);
|
|
|
|
|
|
|
|
|
|
assert.equal(calls.lTrim.length, 1);
|
|
|
|
|
assert.deepEqual(calls.lTrim[0], { key: '项目心跳', start: -2000, stop: -1 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('caches heartbeat when redis is not ready and flushes later', async () => {
|
|
|
|
|
const redis = new RedisIntegration({
|
|
|
|
|
enabled: true,
|
|
|
|
|
projectName: 'BLS主机心跳日志',
|
|
|
|
|
apiBaseUrl: 'http://127.0.0.1:3000',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const calls = { rPush: [], lTrim: [] };
|
|
|
|
|
redis.client = {
|
|
|
|
|
isReady: false,
|
|
|
|
|
connect: async () => {},
|
|
|
|
|
rPush: async (key, value) => {
|
|
|
|
|
calls.rPush.push({ key, value });
|
|
|
|
|
},
|
|
|
|
|
lTrim: async (key, start, stop) => {
|
|
|
|
|
calls.lTrim.push({ key, start, stop });
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await redis.writeHeartbeat();
|
|
|
|
|
assert.ok(redis._pendingHeartbeat);
|
|
|
|
|
|
|
|
|
|
redis.client.isReady = true;
|
|
|
|
|
await redis.flushPendingHeartbeat();
|
|
|
|
|
|
|
|
|
|
assert.equal(redis._pendingHeartbeat, null);
|
|
|
|
|
assert.equal(calls.rPush.length, 1);
|
|
|
|
|
assert.equal(calls.rPush[0].key, '项目心跳');
|
|
|
|
|
const payload = JSON.parse(calls.rPush[0].value);
|
|
|
|
|
assert.equal(payload.projectName, 'BLS主机心跳日志');
|
|
|
|
|
assert.equal(payload.apiBaseUrl, 'http://127.0.0.1:3000');
|
|
|
|
|
assert.equal(typeof payload.lastActiveAt, 'number');
|
|
|
|
|
assert.equal(calls.lTrim.length, 1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('buffers console logs when redis is not ready', async () => {
|
|
|
|
|
const redis = new RedisIntegration({
|
|
|
|
|
enabled: true,
|
|
|
|
|
projectName: 'BLS主机心跳日志',
|
|
|
|
|
apiBaseUrl: 'http://127.0.0.1:3000',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const calls = { rPush: [], lTrim: [] };
|
|
|
|
|
redis.client = {
|
|
|
|
|
isReady: false,
|
|
|
|
|
connect: async () => {},
|
|
|
|
|
rPush: async (key, ...values) => {
|
|
|
|
|
calls.rPush.push({ key, values });
|
|
|
|
|
},
|
|
|
|
|
lTrim: async (key, start, stop) => {
|
|
|
|
|
calls.lTrim.push({ key, start, stop });
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await redis.info('hello', { module: 'test' });
|
|
|
|
|
assert.equal(redis._pendingConsoleLogs.length, 1);
|
|
|
|
|
|
|
|
|
|
redis.client.isReady = true;
|
|
|
|
|
await redis.flushPendingConsoleLogs();
|
|
|
|
|
|
|
|
|
|
assert.equal(redis._pendingConsoleLogs.length, 0);
|
|
|
|
|
assert.equal(calls.rPush.length, 1);
|
|
|
|
|
assert.equal(calls.rPush[0].key, 'BLS主机心跳日志_项目控制台');
|
|
|
|
|
assert.equal(calls.rPush[0].values.length, 1);
|
|
|
|
|
const entry = JSON.parse(calls.rPush[0].values[0]);
|
|
|
|
|
assert.equal(entry.level, 'info');
|
|
|
|
|
assert.equal(entry.message, 'hello');
|
|
|
|
|
assert.equal(entry.metadata.module, 'test');
|
|
|
|
|
});
|
|
|
|
|
});
|