dsh-im-ops/test/channels/shared/access-grant.test.mjs
oliver bcbe85fdb1
Some checks failed
CI / Test, build, and verify package (push) Has been cancelled
Make WhatsApp member presets follow channel mapping by default.
Empty member/group Agent Preset now resolves to the bot workspace mapping (not Host global); settings label is Follow channel mapping, and /new createSession carries that mapping explicitly.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 23:11:33 +08:00

311 lines
9.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import assert from 'node:assert/strict';
import test from 'node:test';
import {
approverPhonesForPending,
emptyAccessGrant,
enqueueAccessPending,
ensureOwnerGlobalAdmin,
evaluateAccessGrant,
findPendingByNotifyMessageId,
formatPendingNotifyBody,
migrateAccessPolicyToGrant,
normalizeAccessPhone,
parseApprovalIntent,
parsePendingIdFromNotifyText,
resolveAccessPending,
upsertAccessContact,
resolveAccessAgentPreset,
resolveChatAgentPreset,
validateAccessGrant,
} from '../../../src/channels/shared/access-grant.mjs';
const GROUP_A = '120363111111111111@g.us';
const GROUP_B = '120363222222222222@g.us';
function grant(overrides = {}) {
return validateAccessGrant({
version: 1,
globalAdmins: ['8618111111111'],
directMembers: [],
groups: {},
pending: [],
contacts: [],
...overrides,
});
}
test('ACCESS_GRANT_GROUP_JID_PATTERN accepts legacy hyphen group JIDs', () => {
const legacy = '123456789-120363000000000000@g.us';
assert.doesNotThrow(() => validateAccessGrant({
...emptyAccessGrant(),
globalAdmins: ['8618111111111'],
groups: {
[legacy]: { admins: [], members: [] },
},
}));
});
test('normalizeAccessPhone accepts E.164, JIDs, and rejects LID servers', () => {
assert.equal(normalizeAccessPhone('+86 181-4238-7786'), '8618142387786');
assert.equal(normalizeAccessPhone('8618142387786@s.whatsapp.net'), '8618142387786');
assert.equal(normalizeAccessPhone('12345:12@c.us'), '12345');
assert.equal(normalizeAccessPhone('123456789012345@lid'), null);
assert.equal(normalizeAccessPhone('12'), null);
});
test('ensureOwnerGlobalAdmin seeds linked account and refuses empty admins on validate', () => {
const seeded = ensureOwnerGlobalAdmin(null, '8618111111111@s.whatsapp.net');
assert.deepEqual(seeded.globalAdmins, ['8618111111111']);
assert.throws(
() => validateAccessGrant({ ...emptyAccessGrant(), globalAdmins: [] }),
/全局管理员/,
);
});
test('migrateAccessPolicyToGrant copies allowlist phones into directMembers only', () => {
const migrated = migrateAccessPolicyToGrant({
direct: { allowlist: { users: [{ id: '8618222222222@s.whatsapp.net', canExecuteCommands: true }] } },
group: { allowlist: { users: [{ id: '+86 183-3333-3333', canExecuteCommands: false }] } },
}, null, '8618111111111');
assert.deepEqual(migrated.globalAdmins, ['8618111111111']);
assert.deepEqual(
migrated.directMembers.map((m) => m.phone).sort(),
['8618222222222', '8618333333333'],
);
assert.equal(Object.keys(migrated.groups).length, 0);
});
test('evaluateAccessGrant keeps DM and group scopes isolated', () => {
const doc = grant({
directMembers: [{ phone: '8618222222222', canExecuteCommands: true }],
groups: {
[GROUP_A]: {
admins: ['8618333333333'],
members: [{ phone: '8618444444444', canExecuteCommands: false }],
},
},
});
assert.equal(evaluateAccessGrant(doc, {
scene: 'direct', phone: '8618111111111',
}).reason, 'global-admin');
assert.equal(evaluateAccessGrant(doc, {
scene: 'direct', phone: '8618222222222',
}).allowed, true);
assert.equal(evaluateAccessGrant(doc, {
scene: 'direct', phone: '8618333333333',
}).allowed, false);
assert.equal(evaluateAccessGrant(doc, {
scene: 'group', phone: '8618444444444', groupJid: GROUP_A,
}).reason, 'group-member');
assert.equal(evaluateAccessGrant(doc, {
scene: 'group', phone: '8618444444444', groupJid: GROUP_B,
}).allowed, false);
assert.equal(evaluateAccessGrant(doc, {
scene: 'group', phone: '8618222222222', groupJid: GROUP_A,
}).allowed, false);
assert.equal(evaluateAccessGrant(doc, {
scene: 'group',
phone: '8618444444444',
groupJid: GROUP_A,
isCommand: true,
}).reason, 'command-not-allowed');
});
test('pending notify falls back to global admins when group has no admins', () => {
const doc = grant({
groups: {
[GROUP_A]: { admins: [], members: [] },
},
});
assert.deepEqual(
approverPhonesForPending(doc, { kind: 'group', groupJid: GROUP_A }),
['8618111111111'],
);
assert.deepEqual(
approverPhonesForPending(grant({
groups: { [GROUP_A]: { admins: ['8618555555555'], members: [] } },
}), { kind: 'group', groupJid: GROUP_A }),
['8618555555555'],
);
});
test('enqueue + approve places member into the matching grant bucket', () => {
let doc = grant();
const { grant: withPending, pending, created } = enqueueAccessPending(doc, {
kind: 'group',
groupJid: GROUP_A,
phone: '8618666666666',
pushName: 'Alice',
requestText: 'hello',
}, { idFactory: () => 'p_test_approve_1' });
assert.equal(created, true);
assert.equal(pending.id, 'p_test_approve_1');
doc = withPending;
const reused = enqueueAccessPending(doc, {
kind: 'group',
groupJid: GROUP_A,
phone: '8618666666666',
});
assert.equal(reused.created, false);
const { grant: approved } = resolveAccessPending(doc, {
pendingId: pending.id,
action: 'approve',
resolvedByPhone: '8618111111111',
});
assert.equal(approved.groups[GROUP_A].members[0].phone, '8618666666666');
assert.equal(approved.directMembers.length, 0);
assert.equal(approved.pending[0].status, 'approved');
});
test('contacts upsert merges lid and phone sightings', () => {
let doc = grant();
doc = upsertAccessContact(doc, {
lid: '999@lid',
pushName: 'Bob',
scene: 'group',
groupJid: GROUP_A,
});
doc = upsertAccessContact(doc, {
phone: '8618777777777',
lid: '999@lid',
pushName: 'Bobby',
scene: 'direct',
});
assert.equal(doc.contacts.length, 1);
assert.equal(doc.contacts[0].phone, '8618777777777');
assert.deepEqual(doc.contacts[0].lids, ['999@lid']);
assert.deepEqual(doc.contacts[0].scenes.slice().sort(), ['direct', 'group']);
});
test('quote approval helpers parse intent, pending id, and notify refs', () => {
assert.equal(parseApprovalIntent('同意 顺便说一声'), 'approve');
assert.equal(parseApprovalIntent('拒绝'), 'deny');
assert.equal(parseApprovalIntent('maybe later'), null);
const body = formatPendingNotifyBody({
id: 'p_abc123',
kind: 'direct',
phone: '8618888888888',
pushName: 'Carol',
requestText: 'hi',
});
assert.match(body, /请求编号:p_abc123/);
assert.equal(parsePendingIdFromNotifyText(body), 'p_abc123');
const enBody = formatPendingNotifyBody({
id: 'p_abc123',
kind: 'direct',
phone: '8618888888888',
pushName: 'Carol',
requestText: 'hi',
}, (text, params) => {
const map = {
'[dsh-im-ops] 访问申请': '[dsh-im-ops] Access request',
'私聊': 'Direct chat',
'场景:{scene}': 'Scene: {scene}',
'申请人:{who}': 'Requester: {who}',
'请求编号:{id}': 'Request: {id}',
'原文:{text}': 'Original message: {text}',
'请引用本条消息回复「同意」或「拒绝」。': 'Quote this message…',
'未知': 'unknown',
};
const translated = map[text] ?? text;
if (params == null) return translated;
return translated.replace(/\{(\w+)\}/g, (match, name) =>
Object.hasOwn(params, name) ? String(params[name]) : match);
});
assert.match(enBody, /Request: p_abc123/);
assert.equal(parsePendingIdFromNotifyText(enBody), 'p_abc123');
const withRef = {
...grant(),
pending: [{
id: 'p_abc123',
kind: 'direct',
phone: '8618888888888',
createdAt: new Date().toISOString(),
status: 'pending',
unresolved: false,
notifyRefs: [{ adminPhone: '8618111111111', providerMessageId: 'MSG1' }],
}],
};
assert.equal(findPendingByNotifyMessageId(withRef, 'MSG1')?.id, 'p_abc123');
});
test('resolveAccessAgentPreset prefers direct-member and group overrides over channel mapping', () => {
const doc = grant({
directMembers: [
{ phone: '8618222222222', canExecuteCommands: true, agentPreset: 'ops-user' },
{ phone: '8618333333333', canExecuteCommands: true },
],
groups: {
[GROUP_A]: {
title: 'A',
agentPreset: 'ops-group',
admins: [],
members: [{ phone: '8618444444444', canExecuteCommands: true }],
},
},
});
assert.equal(resolveAccessAgentPreset(doc, {
kind: 'direct',
phone: '8618222222222',
}), 'ops-user');
assert.equal(resolveAccessAgentPreset(doc, {
kind: 'direct',
phone: '8618333333333',
}), null);
assert.equal(resolveAccessAgentPreset(doc, {
kind: 'group',
groupJid: GROUP_A,
}), 'ops-group');
assert.equal(resolveAccessAgentPreset(doc, {
kind: 'group',
groupJid: GROUP_B,
}), null);
});
test('resolveChatAgentPreset falls back to bot/channel mapping, not Host global', () => {
const doc = grant({
directMembers: [
{ phone: '8618222222222', canExecuteCommands: true, agentPreset: 'ops-user' },
{ phone: '8618333333333', canExecuteCommands: true },
],
});
assert.equal(resolveChatAgentPreset(doc, {
kind: 'direct',
phone: '8618222222222',
}, 'wa-mapped'), 'ops-user');
assert.equal(resolveChatAgentPreset(doc, {
kind: 'direct',
phone: '8618333333333',
}, 'wa-mapped'), 'wa-mapped');
assert.equal(resolveChatAgentPreset(doc, {
kind: 'direct',
phone: '8618333333333',
}, null), null);
assert.equal(resolveChatAgentPreset(null, {
kind: 'direct',
phone: '8618333333333',
}, 'wa-mapped'), 'wa-mapped');
});
test('validateAccessGrant keeps optional agentPreset on members and groups', () => {
const doc = grant({
directMembers: [{ phone: '8618222222222', canExecuteCommands: false, agentPreset: 'desk' }],
groups: {
[GROUP_A]: {
agentPreset: 'night',
admins: ['8618111111111'],
members: [],
},
},
});
assert.equal(doc.directMembers[0].agentPreset, 'desk');
assert.equal(doc.groups[GROUP_A].agentPreset, 'night');
assert.throws(() => grant({
directMembers: [{ phone: '8618222222222', canExecuteCommands: true, agentPreset: 'BAD Preset' }],
}), /Agent Preset/);
});