3ad29356c9
集成通用机器人数值接口、本机控制桥、LeRobot 插件和统一键盘遥操作。采用离线 CoACD 全臂碰撞配方 revision 4、局部装配区切分与结构自接触,限制直接关节位姿写入并保留安全看门狗。同步版本号、变更记录、来源许可证和兼容性验证。
119 lines
4.4 KiB
TypeScript
119 lines
4.4 KiB
TypeScript
import fixture from '../../../contracts/fixtures/single-joint.json';
|
|
import { RobotRuntime } from './RobotRuntime';
|
|
import type { RobotAdapter } from './types';
|
|
import { validateDescriptor, validateValues } from './validation';
|
|
|
|
function setup() {
|
|
const descriptor = validateDescriptor(fixture.descriptor);
|
|
let clock = 0,
|
|
paused = false,
|
|
measured = 0.12;
|
|
const adapter: RobotAdapter = {
|
|
describe: () => descriptor,
|
|
readObservation: () => ({ 'slider.position': measured }),
|
|
validateAction: (v) => validateValues(v, descriptor.actionChannels, true),
|
|
applyAction: vi.fn((v) => validateValues(v, descriptor.actionChannels, true)),
|
|
safeStop: vi.fn(),
|
|
reset: () => {
|
|
measured = 0;
|
|
},
|
|
dispose: vi.fn(),
|
|
};
|
|
const fault = vi.fn(() => {
|
|
paused = true;
|
|
});
|
|
const runtime = new RobotRuntime(
|
|
adapter,
|
|
'sim-test',
|
|
() => paused,
|
|
fault,
|
|
() => clock,
|
|
);
|
|
runtime.authorize();
|
|
const identity = runtime.claim('lease-test');
|
|
const action = (seq = 1) => ({ ...fixture.validAction, ...identity, actionSeq: seq });
|
|
return {
|
|
runtime,
|
|
adapter,
|
|
fault,
|
|
action,
|
|
tick: (ms: number) => {
|
|
clock += ms;
|
|
},
|
|
pause: () => {
|
|
paused = true;
|
|
},
|
|
};
|
|
}
|
|
describe('RobotRuntime bounded mailbox', () => {
|
|
it('applies only at a step, confirms after the step, observations are not targets', async () => {
|
|
const s = setup();
|
|
let confirmed = false;
|
|
const ack = s.runtime.enqueue(s.action()).then((r) => {
|
|
confirmed = true;
|
|
return r;
|
|
});
|
|
expect(s.adapter.applyAction).not.toHaveBeenCalled();
|
|
expect(s.runtime.beforeStep()).toBe(true);
|
|
expect(s.adapter.applyAction).toHaveBeenCalledOnce();
|
|
expect(confirmed).toBe(false);
|
|
s.runtime.afterStep(0.002);
|
|
expect((await ack).values['slider.position']).toBe(0.3);
|
|
expect(s.runtime.latest()?.values['slider.position']).toBe(0.12);
|
|
expect(s.runtime.latest()?.appliedActionSeq).toBe(1);
|
|
});
|
|
it('only newest pending target survives and old sequences cannot replay', async () => {
|
|
const s = setup();
|
|
const old = s.runtime.enqueue(s.action()).catch((e) => e.code);
|
|
const next = s.runtime.enqueue(s.action(2));
|
|
expect(await old).toBe('SUPERSEDED');
|
|
await expect(s.runtime.enqueue(s.action())).rejects.toMatchObject({ code: 'STALE' });
|
|
s.runtime.beforeStep();
|
|
s.runtime.afterStep(0.002);
|
|
expect((await next).actionSeq).toBe(2);
|
|
});
|
|
it('wall clock watchdog cancels stale actions before the first resumed step', async () => {
|
|
const s = setup();
|
|
const ack = s.runtime.enqueue(s.action()).catch((e) => e.code);
|
|
s.tick(501);
|
|
expect(s.runtime.beforeStep()).toBe(false);
|
|
expect(await ack).toBe('DISCONNECTED');
|
|
expect(s.adapter.applyAction).not.toHaveBeenCalled();
|
|
expect(s.fault).toHaveBeenCalledOnce();
|
|
expect(s.runtime.status().enabled).toBe(false);
|
|
await expect(s.runtime.enqueue(s.action(2))).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
|
|
});
|
|
it('a socket event delivered after freeze cannot renew an expired lease', async () => {
|
|
const s = setup();
|
|
s.runtime.capture(0);
|
|
s.tick(501);
|
|
expect(s.runtime.status().observationAgeMs).toBe(501);
|
|
await expect(s.runtime.enqueue(s.action())).rejects.toMatchObject({ code: 'TIMEOUT' });
|
|
expect(s.adapter.applyAction).not.toHaveBeenCalled();
|
|
expect(s.fault).toHaveBeenCalledOnce();
|
|
expect(s.runtime.status().enabled).toBe(false);
|
|
});
|
|
it('reset/dispose reject outstanding requests and invalidate epoch', async () => {
|
|
const s = setup();
|
|
const pending = s.runtime.enqueue(s.action()).catch((e) => e.code);
|
|
s.runtime.reset();
|
|
expect(await pending).toBe('DISCONNECTED');
|
|
expect(s.runtime.status().modelEpoch).toBe(1);
|
|
s.runtime.authorize();
|
|
s.runtime.claim('new-lease');
|
|
await expect(s.runtime.enqueue(s.action(2))).rejects.toMatchObject({ code: 'STALE' });
|
|
s.runtime.dispose();
|
|
s.runtime.dispose();
|
|
expect(s.adapter.dispose).toHaveBeenCalledOnce();
|
|
});
|
|
it('paused/multiple owners denied and nonowner revocation does not write', async () => {
|
|
const s = setup();
|
|
expect(() => s.runtime.claim('other')).toThrow(/已有/);
|
|
s.pause();
|
|
await expect(s.runtime.enqueue(s.action())).rejects.toMatchObject({ code: 'PAUSED' });
|
|
vi.mocked(s.adapter.safeStop).mockClear();
|
|
s.runtime.revoke('not owner', false);
|
|
expect(s.adapter.safeStop).not.toHaveBeenCalled();
|
|
});
|
|
});
|