mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-18 04:50:44 +08:00
test(dingtalk): cover PR #76 mentions and credit contributor
Add regression coverage for group mention chunks, private replies, mention payloads, and batch reply failure handling. Credit divingleee in package contributors and rebuild the checked-in bundles.
This commit is contained in:
parent
799391552e
commit
092d2cbb9c
5 changed files with 259 additions and 154 deletions
|
|
@ -601,6 +601,10 @@ var package_default = {
|
||||||
{
|
{
|
||||||
name: "C3H3-AI",
|
name: "C3H3-AI",
|
||||||
url: "https://github.com/C3H3-AI"
|
url: "https://github.com/C3H3-AI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "divingleee",
|
||||||
|
url: "https://github.com/divingleee"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
license: "MIT",
|
license: "MIT",
|
||||||
|
|
|
||||||
308
lib/index.js
308
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -30,6 +30,10 @@
|
||||||
{
|
{
|
||||||
"name": "C3H3-AI",
|
"name": "C3H3-AI",
|
||||||
"url": "https://github.com/C3H3-AI"
|
"url": "https://github.com/C3H3-AI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "divingleee",
|
||||||
|
"url": "https://github.com/divingleee"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,42 @@ test('session replies use the fixed token endpoint, reject redirects, and cache
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('session text replies include populated mention targets and omit empty mentions', async () => {
|
||||||
|
const sent = [];
|
||||||
|
const api = createDingtalkApi({
|
||||||
|
fetchImpl: async (url, options) => {
|
||||||
|
if (url.toString().includes('/oauth2/accessToken')) {
|
||||||
|
return jsonResponse({ accessToken: 'access-token', expireIn: 7_200 });
|
||||||
|
}
|
||||||
|
sent.push(JSON.parse(options.body));
|
||||||
|
return jsonResponse({ errcode: 0 });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
for (const at of [
|
||||||
|
{ atUserIds: ['staff-one'] },
|
||||||
|
{ atMobiles: ['13800000000'] },
|
||||||
|
{ isAtAll: true },
|
||||||
|
{},
|
||||||
|
{ atUserIds: [], atMobiles: [], isAtAll: false },
|
||||||
|
]) {
|
||||||
|
await api.sendText({
|
||||||
|
clientId: 'ding-client',
|
||||||
|
clientSecret: 'host-only-secret',
|
||||||
|
sessionWebhook: 'https://oapi.dingtalk.com/robot/reply?ticket=mentions',
|
||||||
|
text: '回答',
|
||||||
|
at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
assert.deepEqual(sent.map((body) => body.at), [
|
||||||
|
{ atUserIds: ['staff-one'] },
|
||||||
|
{ atMobiles: ['13800000000'] },
|
||||||
|
{ isAtAll: true },
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
]);
|
||||||
|
for (const body of sent) assert.equal(body.text.content, '回答');
|
||||||
|
});
|
||||||
|
|
||||||
test('DingTalk adds, recalls, and replaces its native status reactions', async () => {
|
test('DingTalk adds, recalls, and replaces its native status reactions', async () => {
|
||||||
const calls = [];
|
const calls = [];
|
||||||
const fetchImpl = async (url, options) => {
|
const fetchImpl = async (url, options) => {
|
||||||
|
|
|
||||||
|
|
@ -2425,6 +2425,67 @@ test('DingTalk clears a private batch after turn-stopped without suggesting a ba
|
||||||
assert.match(sent.at(-1), /当前没有待提交的批量内容/);
|
assert.match(sent.at(-1), /当前没有待提交的批量内容/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('DingTalk mentions the sender in every group text chunk but not in private replies', async (t) => {
|
||||||
|
for (const conversationType of ['1', '2', 2]) {
|
||||||
|
await t.test(`conversationType=${JSON.stringify(conversationType)}`, async () => {
|
||||||
|
const sent = [];
|
||||||
|
const bridge = new DingtalkHarnessBridge({
|
||||||
|
api: { sendText: async (request) => sent.push(request) },
|
||||||
|
clientId: 'ding-client',
|
||||||
|
clientSecret: 'host-secret',
|
||||||
|
harness: { ensureRunning: async () => true },
|
||||||
|
state: stateFixture().state,
|
||||||
|
maxMessageChars: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
await bridge.accept(message('mention-status', '/status', {
|
||||||
|
conversationType,
|
||||||
|
isInAtList: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert.ok(sent.length > 1);
|
||||||
|
assert.equal(sent.map(({ text }) => text).join(''), '钉钉机器人与 DeepSeek Harness 连接正常。');
|
||||||
|
for (const request of sent) {
|
||||||
|
assert.deepEqual(request.at, String(conversationType) === '2'
|
||||||
|
? { atUserIds: ['staff-approved'] }
|
||||||
|
: undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DingTalk batch reply failures attempt a safe fallback and settle even if it also fails', async (t) => {
|
||||||
|
for (const conversationType of ['1', '2']) {
|
||||||
|
for (const fallbackFails of [false, true]) {
|
||||||
|
await t.test(`conversationType=${conversationType}, fallbackFails=${fallbackFails}`, async () => {
|
||||||
|
const sent = [];
|
||||||
|
const bridge = new DingtalkHarnessBridge({
|
||||||
|
api: {
|
||||||
|
sendText: async (request) => {
|
||||||
|
sent.push(request);
|
||||||
|
if (sent.length === 1 || fallbackFails) throw new Error('private delivery failure');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
clientId: 'ding-client',
|
||||||
|
clientSecret: 'host-secret',
|
||||||
|
harness: { ask: async () => assert.fail('batch acknowledgement must not call Harness') },
|
||||||
|
state: stateFixture().state,
|
||||||
|
logger: { error() {} },
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.doesNotReject(bridge.accept(message('batch-reply-failure', '/batch', {
|
||||||
|
conversationType,
|
||||||
|
isInAtList: true,
|
||||||
|
})));
|
||||||
|
|
||||||
|
assert.equal(sent.length, 2);
|
||||||
|
assert.match(sent[1].text, /参考号:MF-[A-F0-9]{8}/);
|
||||||
|
assert.doesNotMatch(sent[1].text, /private delivery failure|ReferenceError/);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('DingTalk group batch commands are rejected without reaching Harness', async () => {
|
test('DingTalk group batch commands are rejected without reaching Harness', async () => {
|
||||||
const fixture = stateFixture();
|
const fixture = stateFixture();
|
||||||
const sent = [];
|
const sent = [];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue