## E3a 并发多控制端(Hub 槽位池) 过去:一个被控端只有一条 Hub 连接 -> 同时只能一个控制端(否则 E_NODE_BUSY)。 现在: - Hub 侧 agents 表改为「node_id -> 槽位池」,每个槽位服务一个控制端: 有空闲槽位直接配对;不足且未达上限则向被控端请求扩容(need_slot,3s 超时); 仍不满足才返回 E_NODE_BUSY。新增配置 max_slots_per_node(默认 3,设 1 即退回独占语义) - 被控端侧 hub-mode 重写为槽位池:常备 2 条连接(warm_slots 可调), 并在某槽位被占用(收到 hello)时预热下一条直到 max_slots —— 复用既有协议信号,不新增控制消息;每条槽位独立重连与退避 - Hub 列表视图聚合为「一节点一行」并带 slots / paired_slots - e2e 新增并发用例:两个控制端同时握手 + 交替并发调用,均成功(旧版会失败) ## E3b 设备分组 - 客户端配置新增 groups(组名 -> 设备名数组;成员可写 @其它组名,最多展开 3 层防环) - CLI:nodeagent group add|remove|list;fanout --nodes @组名 按组并发下发 - 真机验证:group add 本机 win -> fanout system.info --nodes @本机 正常 ## E3c 权限隔离(部分完成) - 能力级隔离沿用被控端 ACL(按 client_id,已含 allow/deny/glob/生效时段/IP 白黑名单/按能力限速) - Hub 侧「按设备/按控制端授权」尚未实现(当前 Hub 视角为单一令牌),已记入路线图待办 ## 测试 - 单测 73 -> 79(新增设备分组 6 例:展开 / 嵌套 / 去重 / 未知 / 兼容旧配置 / 环引用收敛) - e2e 21/21(含扩展后的 Hub 并发双控制端用例)
60 lines
2.0 KiB
JavaScript
60 lines
2.0 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { resolveNodeSelector } from '../../packages/client/dist/index.js';
|
|
|
|
const cfg = {
|
|
client_id: 'mac_01',
|
|
current: 'win_a',
|
|
nodes: {
|
|
win_a: { host: '10.0.0.1', port: 8765 },
|
|
win_b: { host: '10.0.0.2', port: 8765 },
|
|
mac_b: { host: '10.0.0.3', port: 8765 },
|
|
},
|
|
groups: {
|
|
办公: ['win_a', 'win_b'],
|
|
全部: ['@办公', 'mac_b'],
|
|
},
|
|
};
|
|
|
|
test('分组:@组名 展开为设备列表', () => {
|
|
const r = resolveNodeSelector(cfg, ['@办公']);
|
|
assert.deepEqual(r.nodes, ['win_a', 'win_b']);
|
|
assert.deepEqual(r.resolvedGroups['办公'], ['win_a', 'win_b']);
|
|
});
|
|
|
|
test('分组:支持嵌套引用(@全部 含 @办公)', () => {
|
|
const r = resolveNodeSelector(cfg, ['@全部']);
|
|
assert.deepEqual(r.nodes, ['win_a', 'win_b', 'mac_b']);
|
|
});
|
|
|
|
test('分组:与设备名混用且去重', () => {
|
|
const r = resolveNodeSelector(cfg, ['win_b', '@办公']);
|
|
assert.deepEqual(r.nodes, ['win_b', 'win_a'], '先出现的先保留,重复项剔除');
|
|
});
|
|
|
|
test('分组:未知组展开为空(由调用方报错),未知设备名原样保留', () => {
|
|
const r = resolveNodeSelector(cfg, ['@不存在']);
|
|
assert.deepEqual(r.nodes, []);
|
|
assert.deepEqual(r.resolvedGroups['不存在'], []);
|
|
const r2 = resolveNodeSelector(cfg, ['ghost']);
|
|
assert.deepEqual(r2.nodes, ['ghost']);
|
|
});
|
|
|
|
test('分组:无 groups 字段时退化为原样返回(向后兼容旧配置)', () => {
|
|
const legacy = { client_id: 'c', current: 'win_a', nodes: { win_a: { host: 'h', port: 1 } } };
|
|
const r = resolveNodeSelector(legacy, ['win_a']);
|
|
assert.deepEqual(r.nodes, ['win_a']);
|
|
});
|
|
|
|
test('分组:环形引用不无限递归(深度上限 3)', () => {
|
|
const cyc = {
|
|
client_id: 'c',
|
|
current: 'a',
|
|
nodes: { a: { host: 'h', port: 1 } },
|
|
groups: { X: ['@Y'], Y: ['@X', 'a'] },
|
|
};
|
|
const r = resolveNodeSelector(cyc, ['@X']);
|
|
assert.ok(r.nodes.includes('a'), '应能收敛到真实设备');
|
|
assert.ok(r.nodes.length < 50, '不应因环引用爆炸');
|
|
});
|