mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-10 00:00:46 +08:00
Complete unified IM bot integration
This commit is contained in:
parent
ef426d54c5
commit
8996476693
11 changed files with 803 additions and 389 deletions
185
test/client-lifecycle.test.mjs
Normal file
185
test/client-lifecycle.test.mjs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { mergeFeishuSnapshotState } from '../plugin-src/client/channels/feishu/index.js';
|
||||
import { mergeWeixinProvisioningSnapshot } from '../plugin-src/client/channels/weixin/index.js';
|
||||
import {
|
||||
createAnimationFrameScheduler,
|
||||
createPollScheduler,
|
||||
} from '../plugin-src/client/lifecycle.js';
|
||||
|
||||
function deferred() {
|
||||
let resolve;
|
||||
const promise = new Promise((done) => {
|
||||
resolve = done;
|
||||
});
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
function fakeTimers() {
|
||||
let nextId = 1;
|
||||
const pending = new Map();
|
||||
return {
|
||||
setTimeout(callback) {
|
||||
const id = nextId;
|
||||
nextId += 1;
|
||||
pending.set(id, callback);
|
||||
return id;
|
||||
},
|
||||
clearTimeout(id) {
|
||||
pending.delete(id);
|
||||
},
|
||||
runNext() {
|
||||
const entry = pending.entries().next().value;
|
||||
if (!entry) return false;
|
||||
const [id, callback] = entry;
|
||||
pending.delete(id);
|
||||
callback();
|
||||
return true;
|
||||
},
|
||||
get size() {
|
||||
return pending.size;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('disposed Weixin polling cannot schedule a zombie timer after a late RPC', async () => {
|
||||
const timers = fakeTimers();
|
||||
const response = deferred();
|
||||
const scheduler = createPollScheduler({
|
||||
setTimeoutFn: (callback) => timers.setTimeout(callback),
|
||||
clearTimeoutFn: (id) => timers.clearTimeout(id),
|
||||
});
|
||||
let polls = 0;
|
||||
const poll = async () => {
|
||||
polls += 1;
|
||||
await response.promise;
|
||||
scheduler.schedule(poll, 1_000);
|
||||
};
|
||||
|
||||
scheduler.schedule(poll, 0);
|
||||
assert.equal(timers.runNext(), true);
|
||||
assert.equal(polls, 1);
|
||||
assert.equal(timers.size, 0);
|
||||
|
||||
scheduler.dispose();
|
||||
response.resolve();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
|
||||
assert.equal(timers.size, 0);
|
||||
assert.equal(scheduler.schedule(poll, 1_000), false);
|
||||
});
|
||||
|
||||
test('disposing an animation-frame scheduler cancels callbacks queued by a removed tab', () => {
|
||||
let nextId = 1;
|
||||
const pending = new Map();
|
||||
const cancelled = [];
|
||||
const scheduler = createAnimationFrameScheduler({
|
||||
requestFrame(callback) {
|
||||
const id = nextId;
|
||||
nextId += 1;
|
||||
pending.set(id, callback);
|
||||
return id;
|
||||
},
|
||||
cancelFrame(id) {
|
||||
cancelled.push(id);
|
||||
pending.delete(id);
|
||||
},
|
||||
});
|
||||
let ran = false;
|
||||
|
||||
scheduler.schedule(() => {
|
||||
ran = true;
|
||||
});
|
||||
scheduler.dispose();
|
||||
|
||||
assert.deepEqual(cancelled, [1]);
|
||||
assert.equal(pending.size, 0);
|
||||
assert.equal(ran, false);
|
||||
assert.equal(scheduler.schedule(() => {}), false);
|
||||
});
|
||||
|
||||
test('keyed animation frames keep only the latest announcement', () => {
|
||||
let nextId = 1;
|
||||
const pending = new Map();
|
||||
const cancelled = [];
|
||||
const scheduler = createAnimationFrameScheduler({
|
||||
requestFrame(callback) {
|
||||
const id = nextId;
|
||||
nextId += 1;
|
||||
pending.set(id, callback);
|
||||
return id;
|
||||
},
|
||||
cancelFrame(id) {
|
||||
cancelled.push(id);
|
||||
pending.delete(id);
|
||||
},
|
||||
});
|
||||
const announcements = [];
|
||||
|
||||
scheduler.schedule(() => announcements.push('old'), 'announcement');
|
||||
scheduler.schedule(() => announcements.push('new'), 'announcement');
|
||||
|
||||
assert.deepEqual(cancelled, [1]);
|
||||
assert.deepEqual([...pending.keys()], [2]);
|
||||
const latest = pending.get(2);
|
||||
pending.delete(2);
|
||||
latest();
|
||||
assert.deepEqual(announcements, ['new']);
|
||||
scheduler.dispose();
|
||||
});
|
||||
|
||||
test('periodic snapshots cannot restore locally cancelled Weixin or Feishu provisioning', () => {
|
||||
const weixinProvisioning = {
|
||||
attemptId: 'wx_attempt_stale',
|
||||
status: 'pending',
|
||||
expiresAt: 2_000,
|
||||
};
|
||||
assert.equal(mergeWeixinProvisioningSnapshot(
|
||||
null,
|
||||
weixinProvisioning,
|
||||
{ restoreProvisioning: false },
|
||||
), null);
|
||||
assert.equal(
|
||||
mergeWeixinProvisioningSnapshot(
|
||||
null,
|
||||
weixinProvisioning,
|
||||
{ restoreProvisioning: true },
|
||||
)?.attemptId,
|
||||
'wx_attempt_stale',
|
||||
);
|
||||
|
||||
const current = {
|
||||
phase: 'ready',
|
||||
revision: 3,
|
||||
bots: [],
|
||||
totals: { configured: 0, connected: 0 },
|
||||
provisioning: null,
|
||||
pageError: null,
|
||||
statusError: null,
|
||||
};
|
||||
const snapshot = {
|
||||
revision: 4,
|
||||
state: 'provisioning',
|
||||
bots: [],
|
||||
totals: { configured: 0, connected: 0 },
|
||||
provisioning: {
|
||||
attemptId: 'fs_attempt_stale',
|
||||
expiresAt: 2_000,
|
||||
},
|
||||
};
|
||||
assert.equal(mergeFeishuSnapshotState(
|
||||
current,
|
||||
snapshot,
|
||||
{ restoreProvisioning: false, now: 1_000 },
|
||||
).provisioning, null);
|
||||
assert.equal(
|
||||
mergeFeishuSnapshotState(
|
||||
current,
|
||||
snapshot,
|
||||
{ restoreProvisioning: true, now: 1_000 },
|
||||
).provisioning.attemptId,
|
||||
'fs_attempt_stale',
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue