openvibers/openvibe.run Commits

Report Run command delivery and safe retry (#255)

OP OpenVibers committed 2 days ago · 29a0fd7566 · 2 files, +13 −2

* Report Run desk command delivery

* Merge remote-tracking branch 'origin/main' into cx/codes-r2-11-run

* Check safe and unsafe Run command retries

Changes

ddiff --git a/server/sessions/service.js b/server/sessions/service.js
iindex 4545943..623bd66 100644
---- a/server/sessions/service.js
++++ b/server/sessions/service.js
@@@ -256,7 +256,7 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), log
const r = await desks.act(row.machine, row.desk, bare(row.owner), { id, kind: cmd.kind, value: cmd.value });
if (r === null) {
const ended = await finish(row, 'host_lost', 'its machine no longer holds the desk', { release: false });
-- return { session_id: row.id, id, result: 'refused', code: 'run.session.host_lost', reason: 'the session\'s machine went away', state: ended ? ended.state : 'ended' };
++ return { session_id: row.id, id, result: 'refused', code: 'run.session.host_lost', reason: 'the session\'s machine went away', state: ended ? ended.state : 'ended', delivery: 'not_delivered', retry: { safe: true, reason: 'The machine no longer holds this desk' } };
}
const renewed = { ...lease, expires_ms: now() + HOLDER_MS };
if (renewed.expires_ms !== lease.expires_ms) await store.update(db, row.id, { control_lease: renewed, last_used_at: now(), updated_at: now() });
@@@ -265,6 +265,12 @@ function createSessions({ config, db, desks, outbox, now = () => Date.now(), log
if (r.code != null) out.code = String(r.code).slice(0, 80);
if (r.reason != null) out.reason = String(r.reason).slice(0, 1000);
if (r.data && typeof r.data === 'object' && out.result === 'ack') out.data = r.data;
++ // An older Bot may omit delivery. Never infer that a timeout or refusal is safe to repeat.
++ out.delivery = ['delivered', 'not_delivered', 'uncertain'].includes(r.delivery) ? r.delivery
++ : ['ack', 'nack'].includes(out.result) ? 'delivered' : 'uncertain';
++ out.retry = { safe: out.delivery === 'not_delivered', reason: out.delivery === 'not_delivered'
++ ? 'Bot did not deliver the command' : out.delivery === 'uncertain'
++ ? 'The machine may already have run this command' : 'The machine already answered this command' };
await logAction(row, out.id, cmd, caller, out.result, out.code || null);
return out;
}
ddiff --git a/test/sessions.test.js b/test/sessions.test.js
iindex bb26d3a..ee95921 100644
---- a/test/sessions.test.js
++++ b/test/sessions.test.js
@@@ -41,6 +41,7 @@ function fakeDesks() {
async get(machine, desk) { calls.push(['get', machine, desk]); return leases.get(`${machine} ${desk}`) || null; },
async act(machine, desk, subject, cmd) {
calls.push(['act', machine, desk, subject, cmd]);
++ if (cmd.id === 'not-delivered') return { machine_id: machine, desk, id: cmd.id, result: 'refused', delivery: 'not_delivered', retry: { safe: true } };
if (!leases.has(`${machine} ${desk}`)) return null;
if (cmd.value && cmd.value.action === 'active_origin') return { machine_id: machine, desk, id: cmd.id, result: 'ack', data: { origin: 'https://example.org' } };
if (cmd.value && cmd.value.action === 'blind_fill') return { machine_id: machine, desk, id: cmd.id, result: 'ack', data: { outcome: 'ok' } };
@@@ -112,7 +113,9 @@ function fakeDesks() {
await check('commands reach its desk; anyone else\'s call is 404', async () => {
const r = await t.call('POST', `/api/v1/sessions/${ses.id}/commands`, { ...as(ada), body: { id: 'c1', kind: 'browser', value: { action: 'open', url: 'https://example.org/' } } });
assert.strictEqual(r.status, 200, r.text);
-- valid(r.json, 'run.session-control-result@1');
++ const { delivery, retry, ...releasedShape } = r.json;
++ valid(releasedShape, 'run.session-control-result@1'); // pinned Contracts predates the additive fields
++ assert.deepStrictEqual([delivery, retry.safe], ['delivered', false]);
assert.deepStrictEqual([r.json.result, r.json.state, r.json.data.title], ['ack', 'running', 'Example']);
const act = desks.calls.filter((c) => c[0] === 'act').at(-1);
assert.deepStrictEqual([act[1], act[2], act[4].kind], [OPENVIBE, ses.placement.desk, 'browser']);
@@@ -131,6 +134,8 @@ function fakeDesks() {
assert.strictEqual(timeline.json.actions[0].action, 'open');
assert.ok(!JSON.stringify(timeline.json).includes('https://example.org'), 'timeline omits command values and URLs');
assert.strictEqual((await t.call('GET', `/api/v1/sessions/${ses.id}/timeline`, as(bea))).status, 404);
++ const refused = await t.call('POST', `/api/v1/sessions/${ses.id}/commands`, { ...as(ada), body: { id: 'not-delivered', kind: 'browser', value: { action: 'read' } } });
++ assert.deepStrictEqual([refused.json.delivery, refused.json.retry.safe], ['not_delivered', true]);
});
await check('the B3 control lease hands off to an agent, refuses the former holder, and owner take-back wins', async () => {