dsh-im-ops/test/channels/feishu/plugin-host.test.mjs
2026-08-23 20:02:06 +08:00

1310 lines
44 KiB
JavaScript

import assert from 'node:assert/strict';
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import test from 'node:test';
import {
FEISHU_APP_ID_REF,
FEISHU_APP_SECRET_REF,
FEISHU_ENDPOINTS,
FEISHU_MULTI_ENDPOINTS,
apply,
createDshCredentialStore,
createProductionController,
createProvisioningBackedController,
} from '../../../plugin-src/host/channels/feishu/index.mjs';
const signal = () => new AbortController().signal;
function status(overrides = {}) {
return {
phase: 'unconfigured',
connected: false,
configured: false,
bot: null,
registration: { state: 'idle', attempt: 0, updatedAt: 100 },
connection: {
ready: false,
feishuLongConnectionState: 'idle',
harnessReachable: false,
},
error: null,
...overrides,
};
}
async function rpcFixture(controller) {
let registration;
let disposed = false;
const ctx = {
connection: {
rpc: {
handle(channel, handler, options) {
registration = { channel, handler, options };
return async () => { disposed = true; };
},
},
},
};
const dispose = await apply(ctx, { controller });
return {
dispose,
get registration() { return registration; },
get disposed() { return disposed; },
};
}
test('Host plugin registers the real rc.6 Connection RPC shape as loopback-only', async () => {
const controller = {
status: async () => status(),
startRegistration: async () => status(),
cancelRegistration: async () => status(),
disconnect: async () => status(),
};
const fx = await rpcFixture(controller);
assert.equal(fx.registration.channel, '/feishu');
assert.deepEqual(fx.registration.options, { authority: 'loopback' });
const result = await fx.registration.handler(FEISHU_ENDPOINTS.status, {}, signal());
assert.equal(result.ok, true);
assert.equal(result.value.state, 'disconnected');
assert.equal(result.value.connected, false);
assert.equal(result.value.configured, false);
assert.equal(result.value.bot.name, '飞书机器人');
assert.equal(result.value.health.status, 'offline');
assert.equal('registration' in result.value, false);
await fx.dispose();
assert.equal(fx.disposed, true);
});
test('Host exposes configured offline as a redacted capability fact', async () => {
const secret = 'offline-secret-must-stay-host-only';
const controller = {
status: async () => status({
phase: 'disconnected',
configured: true,
bot: { name: '北汇星河助手', appSecret: secret },
credentials: { client_secret: secret },
connection: {
ready: false,
feishuLongConnectionState: 'failed',
harnessReachable: true,
token: secret,
},
}),
startRegistration: async () => status(),
cancelRegistration: async () => status(),
reconnect: async () => status({ configured: true }),
disconnect: async () => status(),
};
const fx = await rpcFixture(controller);
const result = await fx.registration.handler(FEISHU_ENDPOINTS.status, {}, signal());
assert.equal(result.ok, true);
assert.equal(result.value.state, 'disconnected');
assert.equal(result.value.connected, false);
assert.equal(result.value.configured, true);
assert.equal(result.value.health.status, 'offline');
assert.doesNotMatch(JSON.stringify(result), new RegExp(secret));
assert.equal('credentials' in result.value, false);
});
test('Host exposes only browser-safe Agent Preset fields in status and update responses', async () => {
let selectedPreset = 'marketing-jeep';
const calls = [];
const current = () => status({
schemaVersion: 2,
revision: 3,
configured: true,
agentPresetCatalog: {
defaultId: 'standard',
root: 'private-preset-root',
failures: [{ message: 'private-catalog-failure' }],
items: [
{
id: 'standard',
name: 'Standard',
path: 'private-standard-path',
trust: 'private-trust-level',
},
{
id: 'marketing-jeep',
label: '营销吉普',
error: { message: 'private-preset-error' },
},
{
id: 'broken-preset',
label: 'Broken',
broken: { message: 'private-broken-reason' },
},
{ id: 'INVALID', label: 'invalid-item-must-be-filtered' },
{ id: 'standard', label: 'duplicate-item-must-be-filtered' },
],
},
bots: [{
botId: 'bot_safe',
phase: 'connected',
connected: true,
configured: true,
agentPreset: selectedPreset,
bot: { name: '安全机器人', domain: 'feishu' },
connection: {
ready: true,
feishuLongConnectionState: 'connected',
harnessReachable: true,
},
}],
});
const controller = {
status: async () => current(),
startRegistration: async () => current(),
cancelRegistration: async () => current(),
disconnect: async () => status(),
updateAgentPreset: async (botId, agentPreset) => {
calls.push({ botId, agentPreset });
selectedPreset = agentPreset;
return current();
},
};
const fx = await rpcFixture(controller);
const listed = await fx.registration.handler(FEISHU_ENDPOINTS.status, {}, signal());
assert.equal(listed.ok, true);
assert.equal(listed.value.bots[0].agentPreset, 'marketing-jeep');
assert.deepEqual(listed.value.agentPresetCatalog, {
defaultId: 'standard',
items: [
{ id: 'standard', label: 'Standard' },
{ id: 'marketing-jeep', label: '营销吉普' },
],
});
assert.doesNotMatch(
JSON.stringify(listed),
/private-preset-root|private-catalog-failure|private-standard-path|private-trust-level|private-preset-error|private-broken-reason|invalid-item|duplicate-item/,
);
const cleared = await fx.registration.handler(
FEISHU_ENDPOINTS.setAgentPreset,
{ botId: 'bot_safe', agentPreset: null },
signal(),
);
assert.equal(cleared.ok, true);
assert.deepEqual(calls, [{ botId: 'bot_safe', agentPreset: null }]);
assert.equal(cleared.value.bots[0].agentPreset, null);
assert.deepEqual(cleared.value.agentPresetCatalog, listed.value.agentPresetCatalog);
assert.doesNotMatch(JSON.stringify(cleared), /private-|invalid-item|duplicate-item/);
await fx.dispose();
});
test('Host validates and updates the Feishu group response mode', async () => {
let mode = 'mention';
const current = () => status({
schemaVersion: 2,
revision: 4,
configured: true,
bots: [{
botId: 'bot_mode',
phase: 'connected',
connected: true,
configured: true,
groupResponseMode: mode,
bot: { name: '模式机器人', domain: 'feishu' },
connection: {
ready: true,
feishuLongConnectionState: 'connected',
harnessReachable: true,
},
}],
});
const controller = {
status: async () => current(),
startRegistration: async () => current(),
cancelRegistration: async () => current(),
disconnect: async () => status(),
updateGroupResponseMode: async (botId, value) => {
assert.equal(botId, 'bot_mode');
mode = value;
return current();
},
};
const fx = await rpcFixture(controller);
const updated = await fx.registration.handler(
FEISHU_ENDPOINTS.setGroupResponseMode,
{ botId: 'bot_mode', groupResponseMode: 'all' },
signal(),
);
assert.equal(updated.ok, true);
assert.equal(updated.value.bots[0].groupResponseMode, 'all');
const invalid = await fx.registration.handler(
FEISHU_ENDPOINTS.setGroupResponseMode,
{ botId: 'bot_mode', groupResponseMode: 'sometimes' },
signal(),
);
assert.equal(invalid.ok, false);
assert.equal(invalid.error.code, 'bad-request');
await fx.dispose();
});
test('RPC dispatch matches every endpoint in client/api.js', async () => {
const calls = [];
let current = status({
phase: 'registering',
registration: {
state: 'qr_ready',
attempt: 7,
updatedAt: 100,
qrCodeUrl: 'https://accounts.feishu.cn/device',
expiresAt: Date.now() + 60_000,
},
});
const controller = {
status: async () => current,
startRegistration: async () => { calls.push('begin'); return current; },
cancelRegistration: async () => {
calls.push('cancel');
current = status({ registration: { state: 'cancelled', attempt: 7, updatedAt: 200 } });
return current;
},
reconnect: async () => { calls.push('test'); return current; },
bindCredentials: async ({ appId, appSecret }) => {
calls.push(`bind:${appId}:${appSecret}`);
return current;
},
disconnect: async () => { calls.push('disconnect'); return status(); },
};
const fx = await rpcFixture(controller);
const begun = await fx.registration.handler(
FEISHU_ENDPOINTS.beginProvisioning,
{ locale: 'zh-CN' },
signal(),
);
assert.equal(begun.ok, true);
assert.equal(begun.value.attemptId, '7');
assert.match(begun.value.qrCodeDataUrl, /^data:image\/png;base64,/);
const polled = await fx.registration.handler(
FEISHU_ENDPOINTS.pollProvisioning,
{ attemptId: '7' },
signal(),
);
assert.equal(polled.ok, true);
assert.equal(polled.value.status, 'pending');
const tested = await fx.registration.handler(FEISHU_ENDPOINTS.testConnection, {}, signal());
assert.equal(tested.ok, true);
const bound = await fx.registration.handler(
FEISHU_ENDPOINTS.bindCredentials,
{ appId: 'cli_manual', appSecret: 'manual-private-secret' },
signal(),
);
assert.equal(bound.ok, true);
assert.doesNotMatch(JSON.stringify(bound), /manual-private-secret|appSecret/);
const cancelled = await fx.registration.handler(
FEISHU_ENDPOINTS.cancelProvisioning,
{ attemptId: '7' },
signal(),
);
assert.equal(cancelled.ok, true);
assert.equal(cancelled.value.status, 'failed');
const disconnected = await fx.registration.handler(
FEISHU_ENDPOINTS.disconnect,
{ removeCredentials: true },
signal(),
);
assert.equal(disconnected.ok, true);
assert.deepEqual(calls, [
'begin', 'test', 'bind:cli_manual:manual-private-secret', 'cancel', 'disconnect',
]);
const attemptedSecret = await fx.registration.handler(
FEISHU_ENDPOINTS.beginProvisioning,
{ appSecret: 'must-not-cross-browser-boundary' },
signal(),
);
assert.equal(attemptedSecret.ok, false);
assert.equal(attemptedSecret.error.code, 'bad-request');
assert.doesNotMatch(JSON.stringify(attemptedSecret), /must-not-cross-browser-boundary/);
});
test('callback repair begins for exactly one bot and returns only a safe official QR projection', async () => {
const calls = [];
const repair = status({
schemaVersion: 2,
phase: 'registering',
configured: true,
registration: {
state: 'qr_ready',
attempt: 'reg_repair',
operation: 'callback_repair',
botId: 'bot_target',
qrCodeUrl: 'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_target&addons=encoded&user_code=opaque',
expiresAt: Date.now() + 60_000,
},
bots: [{
botId: 'bot_target',
connected: true,
configured: true,
bot: { name: '目标机器人', appIdMasked: 'cli_tar••••rget' },
connection: { ready: true, feishuLongConnectionState: 'connected', harnessReachable: true },
}],
});
const controller = {
status: async () => repair,
registrationStatus: async () => repair,
startRegistration: async () => status(),
startCallbackRepair: async (botId) => { calls.push(botId); return repair; },
cancelRegistration: async () => repair,
disconnect: async () => status(),
};
const fx = await rpcFixture(controller);
const result = await fx.registration.handler(
FEISHU_ENDPOINTS.beginCallbackRepair,
{ botId: 'bot_target' },
signal(),
);
assert.equal(result.ok, true);
assert.deepEqual(calls, ['bot_target']);
assert.equal(result.value.operation, 'callback_repair');
assert.equal(result.value.botId, 'bot_target');
assert.equal(
result.value.verificationUrl,
'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_target&addons=encoded&user_code=opaque',
);
assert.match(result.value.qrCodeDataUrl, /^data:image\/png;base64,/);
assert.doesNotMatch(JSON.stringify(result), /client_secret|appSecret/);
const restored = await fx.registration.handler(FEISHU_ENDPOINTS.status, {}, signal());
assert.equal(restored.value.provisioning.operation, 'callback_repair');
assert.equal(restored.value.provisioning.botId, 'bot_target');
for (const payload of [
{},
{ botId: '../target' },
{ botId: 'bot_target', appSecret: 'must-not-leak' },
]) {
const invalid = await fx.registration.handler(
FEISHU_ENDPOINTS.beginCallbackRepair,
payload,
signal(),
);
assert.equal(invalid.ok, false);
assert.equal(invalid.error.code, 'bad-request');
assert.doesNotMatch(JSON.stringify(invalid), /must-not-leak|\.\.\/target/);
}
await fx.dispose();
});
test('group-message permission begins for one existing bot and returns a safe official QR projection', async () => {
const calls = [];
const permission = status({
schemaVersion: 2,
phase: 'registering',
configured: true,
registration: {
state: 'qr_ready',
attempt: 'reg_group_permission',
operation: 'group_message_permission',
botId: 'bot_target',
qrCodeUrl: 'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_target&addons=encoded&user_code=opaque',
expiresAt: Date.now() + 60_000,
},
bots: [{
botId: 'bot_target',
connected: true,
configured: true,
groupResponseMode: 'mention',
groupMessagePermissionGranted: false,
bot: { name: '目标机器人', appIdMasked: 'cli_tar••••rget', domain: 'feishu' },
connection: { ready: true, feishuLongConnectionState: 'connected', harnessReachable: true },
}],
});
const controller = {
status: async () => permission,
registrationStatus: async () => permission,
startRegistration: async () => status(),
startGroupMessagePermission: async (botId) => { calls.push(botId); return permission; },
cancelRegistration: async () => permission,
disconnect: async () => status(),
};
const fx = await rpcFixture(controller);
const result = await fx.registration.handler(
FEISHU_ENDPOINTS.beginGroupMessagePermission,
{ botId: 'bot_target' },
signal(),
);
assert.equal(result.ok, true);
assert.deepEqual(calls, ['bot_target']);
assert.equal(result.value.operation, 'group_message_permission');
assert.equal(result.value.botId, 'bot_target');
assert.equal(
result.value.verificationUrl,
'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_target&addons=encoded&user_code=opaque',
);
assert.match(result.value.qrCodeDataUrl, /^data:image\/png;base64,/);
assert.doesNotMatch(JSON.stringify(result), /client_secret|appSecret/);
const restored = await fx.registration.handler(FEISHU_ENDPOINTS.status, {}, signal());
assert.equal(restored.value.provisioning.operation, 'group_message_permission');
assert.equal(restored.value.provisioning.botId, 'bot_target');
assert.equal(restored.value.bots[0].groupMessagePermissionGranted, false);
for (const payload of [
{},
{ botId: '../target' },
{ botId: 'bot_target', appSecret: 'must-not-leak' },
]) {
const invalid = await fx.registration.handler(
FEISHU_ENDPOINTS.beginGroupMessagePermission,
payload,
signal(),
);
assert.equal(invalid.ok, false);
assert.equal(invalid.error.code, 'bad-request');
assert.doesNotMatch(JSON.stringify(invalid), /must-not-leak|\.\.\/target/);
}
await fx.dispose();
});
test('status preserves a submitted callback repair attempt after its QR URL is discarded', async () => {
const secret = 'must-never-cross-the-rpc-boundary';
const saving = status({
schemaVersion: 2,
phase: 'connecting',
configured: true,
registration: {
state: 'saving',
attempt: 'reg_committed',
operation: 'callback_repair',
botId: 'bot_target',
},
bots: [{
botId: 'bot_target',
configured: true,
bot: { name: '目标机器人', domain: 'feishu', appSecret: secret },
}],
});
const controller = {
status: async () => saving,
startRegistration: async () => status(),
cancelRegistration: async () => saving,
disconnect: async () => status(),
};
const fx = await rpcFixture(controller);
const restored = await fx.registration.handler(FEISHU_ENDPOINTS.status, {}, signal());
assert.equal(restored.ok, true);
assert.equal(restored.value.state, 'connecting');
assert.deepEqual(
{
attemptId: restored.value.provisioning.attemptId,
operation: restored.value.provisioning.operation,
botId: restored.value.provisioning.botId,
submitted: restored.value.provisioning.submitted,
},
{
attemptId: 'reg_committed',
operation: 'callback_repair',
botId: 'bot_target',
submitted: true,
},
);
assert.equal(restored.value.provisioning.verificationUrl, undefined);
assert.equal(restored.value.provisioning.qrCodeDataUrl, undefined);
assert.doesNotMatch(JSON.stringify(restored), new RegExp(secret));
await fx.dispose();
});
test('callback repair failures cross RPC only as fixed safe public errors', async () => {
const expected = new Map([
['repair_app_mismatch', 'The authorized Feishu app does not match the selected bot.'],
['repair_domain_mismatch', 'The authorized Feishu tenant does not match the selected bot.'],
['repair_owner_mismatch', 'The authorizing Feishu account is not an owner of the selected bot.'],
['repair_target_changed', 'The selected bot changed while repair was in progress. Start the repair again.'],
['credential_update_failed', 'Unable to store the repaired Feishu credentials.'],
['repair_connection_failed', 'The callback update was accepted, but the selected bot could not reconnect.'],
['card_action_probe_send_failed', 'The callback update was accepted, but the verification card could not be sent.'],
['card_action_probe_unavailable', 'The selected bot is not connected, so its card button cannot be verified.'],
['card_action_probe_timeout', 'Feishu accepted the update, but the card button was not verified in time. Start the repair again and click the test button within two minutes.'],
]);
for (const [code, message] of expected) {
const failed = status({
phase: 'error',
registration: {
state: 'error',
attempt: 'reg_failed',
operation: 'callback_repair',
botId: 'bot_target',
error: { code, message: 'secret=must-not-cross' },
},
});
const controller = {
status: async () => failed,
startRegistration: async () => status(),
cancelRegistration: async () => failed,
disconnect: async () => status(),
};
const fx = await rpcFixture(controller);
const result = await fx.registration.handler(FEISHU_ENDPOINTS.status, {}, signal());
assert.deepEqual(result.value.error, { code, message });
assert.doesNotMatch(JSON.stringify(result), /must-not-cross/);
await fx.dispose();
}
});
test('callback repair refuses placeholder, non-SDK, and non-official verification links', async () => {
for (const qrCodeUrl of [
'https://open.feishu.cn/page/launcher?tp=sdk&clientID=%7B%7Bclient_id%7D%7D',
'https://open.feishu.cn/page/launcher?tp=card&clientID=cli_target',
'https://evil.example/device?tp=sdk&clientID=cli_target',
'http://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_target',
'https://accounts.feishu.cn/device?tp=sdk&clientID=cli_target',
'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_target&addons=x&createOnly=true',
]) {
const repair = status({
phase: 'registering',
configured: true,
registration: {
state: 'qr_ready',
attempt: 'reg_unsafe',
operation: 'callback_repair',
botId: 'bot_target',
qrCodeUrl,
expiresAt: Date.now() + 60_000,
},
});
const controller = {
status: async () => repair,
registrationStatus: async () => repair,
startRegistration: async () => status(),
startCallbackRepair: async () => repair,
cancelRegistration: async () => repair,
disconnect: async () => status(),
};
const fx = await rpcFixture(controller);
const result = await fx.registration.handler(
FEISHU_ENDPOINTS.beginCallbackRepair,
{ botId: 'bot_target' },
signal(),
);
assert.equal(result.ok, false);
assert.equal(result.error.code, 'internal');
assert.equal(JSON.stringify(result).includes(qrCodeUrl), false);
await fx.dispose();
}
});
test('callback repair cannot be cancelled after configuration enters saving', async () => {
let cancels = 0;
const saving = status({
phase: 'connecting',
configured: true,
registration: {
state: 'saving',
attempt: 'reg_committed',
operation: 'callback_repair',
botId: 'bot_target',
},
});
const controller = {
status: async () => saving,
registrationStatus: async () => saving,
startRegistration: async () => status(),
cancelRegistration: async () => { cancels += 1; return saving; },
disconnect: async () => status(),
};
const fx = await rpcFixture(controller);
const result = await fx.registration.handler(
FEISHU_ENDPOINTS.cancelProvisioning,
{ attemptId: 'reg_committed' },
signal(),
);
assert.equal(result.ok, true);
assert.equal(result.value.status, 'connecting');
assert.equal(result.value.operation, 'callback_repair');
assert.equal(result.value.botId, 'bot_target');
assert.equal(cancels, 1);
const polled = await fx.registration.handler(
FEISHU_ENDPOINTS.pollProvisioning,
{ attemptId: 'reg_committed' },
signal(),
);
assert.equal(polled.ok, true);
assert.equal(polled.value.status, 'connecting');
await fx.dispose();
});
test('connection.test does not restart an already healthy long connection', async () => {
let reconnects = 0;
const healthy = status({
phase: 'connected',
connected: true,
configured: true,
connection: {
ready: true,
feishuLongConnectionState: 'connected',
harnessReachable: true,
},
});
const controller = {
status: async () => healthy,
startRegistration: async () => healthy,
cancelRegistration: async () => healthy,
reconnect: async () => { reconnects += 1; return healthy; },
disconnect: async () => status(),
};
const fx = await rpcFixture(controller);
const result = await fx.registration.handler(FEISHU_ENDPOINTS.testConnection, {}, signal());
assert.equal(result.ok, true);
assert.equal(result.value.connected, true);
assert.equal(reconnects, 0);
});
test('provision.begin waits through starting until onQRCodeReady is observable', async () => {
let current = status({
phase: 'registering',
registration: { state: 'starting', attempt: 3, updatedAt: 100 },
});
const controller = {
status: async () => current,
startRegistration: async () => {
setTimeout(() => {
current = status({
phase: 'registering',
registration: {
state: 'qr_ready',
attempt: 3,
updatedAt: 110,
qrCodeUrl: 'https://accounts.feishu.cn/async-qr',
expiresAt: Date.now() + 60_000,
},
});
}, 5);
return current;
},
cancelRegistration: async () => status(),
disconnect: async () => status(),
};
const fx = await rpcFixture(controller);
const begun = await fx.registration.handler(
FEISHU_ENDPOINTS.beginProvisioning,
{ locale: 'zh-CN' },
signal(),
);
assert.equal(begun.ok, true);
assert.equal(begun.value.attemptId, '3');
assert.equal(begun.value.verificationUrl, 'https://accounts.feishu.cn/async-qr');
assert.match(begun.value.qrCodeDataUrl, /^data:image\/png;base64,/);
});
test('cancelling during credential activation tears down the eventual runtime', async () => {
const calls = [];
const current = status({
phase: 'connecting',
configured: true,
registration: { state: 'saving', attempt: 11, updatedAt: 100 },
});
const controller = {
status: async () => current,
startRegistration: async () => current,
cancelRegistration: async () => { calls.push('cancel-only'); return current; },
disconnect: async () => { calls.push('disconnect'); return status(); },
};
const fx = await rpcFixture(controller);
const result = await fx.registration.handler(
FEISHU_ENDPOINTS.cancelProvisioning,
{ attemptId: '11' },
signal(),
);
assert.equal(result.ok, true);
assert.deepEqual(calls, ['disconnect']);
});
test('status returns qrCodeDataUrl while dropping all credential-shaped fields', async () => {
const secret = 'sdk-super-secret-value';
const controller = {
status: async () => status({
phase: 'registering',
appSecret: secret,
credentials: { client_secret: secret },
registration: {
state: 'qr_ready',
attempt: 1,
qrCodeUrl: 'https://accounts.feishu.cn/device',
expiresAt: Date.now() + 60_000,
client_secret: secret,
},
connection: {
ready: true,
connected: false,
state: 'connecting',
token: secret,
},
}),
startRegistration: async () => status(),
cancelRegistration: async () => status(),
disconnect: async () => status(),
};
const fx = await rpcFixture(controller);
const result = await fx.registration.handler(FEISHU_ENDPOINTS.status, {}, signal());
assert.equal(result.ok, true);
assert.equal(result.value.state, 'provisioning');
assert.equal(result.value.provisioning.verificationUrl, 'https://accounts.feishu.cn/device');
assert.match(result.value.provisioning.qrCodeDataUrl, /^data:image\/png;base64,/);
assert.doesNotMatch(JSON.stringify(result), /sdk-super-secret-value|client_secret|credentials|token/);
});
test('polling a new QR stays pending when another bot is already connected', async () => {
const current = status({
schemaVersion: 2,
phase: 'registering',
connected: false,
configured: true,
registration: {
state: 'qr_ready',
attempt: 'reg_new_bot',
qrCodeUrl: 'https://accounts.feishu.cn/new-bot',
expiresAt: Date.now() + 60_000,
},
bots: [{
botId: 'bot_existing',
phase: 'connected',
connected: true,
configured: true,
bot: { name: '现有机器人', appIdMasked: 'cli_old••••1234' },
connection: {
ready: true,
feishuLongConnectionState: 'connected',
harnessReachable: true,
},
}],
});
const controller = {
status: async () => current,
registrationStatus: async () => current,
startRegistration: async () => current,
cancelRegistration: async () => current,
disconnect: async () => status(),
};
const fx = await rpcFixture(controller);
const result = await fx.registration.handler(
FEISHU_ENDPOINTS.pollProvisioning,
{ attemptId: 'reg_new_bot' },
signal(),
);
assert.equal(result.ok, true);
assert.equal(result.value.status, 'pending');
assert.equal('botId' in result.value, false);
assert.equal('connection' in result.value, false);
});
test('multi-bot RPC operations require a botId and never expose host-only fields', async () => {
const calls = [];
const multi = status({
schemaVersion: 2,
revision: 9,
configured: true,
bots: [{
botId: 'bot_safe',
phase: 'connected',
connected: true,
configured: true,
secretRef: 'DSH_FEISHU_APP_SECRET_PRIVATE',
ownerOpenIds: ['ou_private'],
bot: {
name: '安全机器人',
appId: 'cli_raw_private',
appIdMasked: 'cli_safe••••1234',
domain: 'feishu',
},
connection: {
ready: true,
feishuLongConnectionState: 'connected',
harnessReachable: true,
token: 'private-token',
},
}],
});
const controller = {
status: async () => multi,
startRegistration: async () => multi,
cancelRegistration: async () => multi,
disconnect: async () => status(),
reconnectBot: async (botId) => { calls.push(['reconnect', botId]); return multi; },
sendConnectionTest: async (botId) => { calls.push(['test-message', botId]); },
disconnectBot: async (botId) => { calls.push(['disconnect', botId]); return multi; },
deleteBot: async (botId) => { calls.push(['delete', botId]); return status({ schemaVersion: 2, bots: [] }); },
};
const fx = await rpcFixture(controller);
for (const [endpoint, payload] of [
[FEISHU_MULTI_ENDPOINTS.reconnectBot, { botId: 'bot_safe', sendTest: true }],
[FEISHU_MULTI_ENDPOINTS.disconnectBot, { botId: 'bot_safe' }],
[FEISHU_MULTI_ENDPOINTS.deleteBot, { botId: 'bot_safe', confirm: true }],
]) {
const result = await fx.registration.handler(endpoint, payload, signal());
assert.equal(result.ok, true);
assert.doesNotMatch(JSON.stringify(result), /DSH_FEISHU_APP_SECRET|ou_private|cli_raw_private|private-token/);
if (endpoint === FEISHU_MULTI_ENDPOINTS.reconnectBot) {
assert.deepEqual(result.value.testMessage, { sent: true });
}
}
assert.deepEqual(calls, [
['reconnect', 'bot_safe'],
['test-message', 'bot_safe'],
['disconnect', 'bot_safe'],
['delete', 'bot_safe'],
]);
const invalid = await fx.registration.handler(
FEISHU_MULTI_ENDPOINTS.deleteBot,
{ botId: '../private', confirm: true },
signal(),
);
assert.equal(invalid.ok, false);
assert.equal(invalid.error.code, 'bad-request');
assert.doesNotMatch(JSON.stringify(invalid), /\.\.\/private/);
});
test('Feishu reconnect test delivery is best-effort and requires the target bot to be connected', async () => {
let connected = true;
let sendCalls = 0;
const snapshot = () => status({
schemaVersion: 2,
revision: 1,
configured: true,
bots: [{
botId: 'bot_safe',
phase: connected ? 'connected' : 'disconnected',
connected,
configured: true,
bot: { name: '安全机器人', appIdMasked: 'cli_safe••••1234', domain: 'feishu' },
connection: {
ready: connected,
feishuLongConnectionState: connected ? 'connected' : 'idle',
harnessReachable: connected,
},
}],
});
const controller = {
status: async () => snapshot(),
startRegistration: async () => snapshot(),
cancelRegistration: async () => snapshot(),
disconnect: async () => status(),
reconnectBot: async () => snapshot(),
sendConnectionTest: async () => {
sendCalls += 1;
throw new Error('private Feishu provider failure');
},
};
const fx = await rpcFixture(controller);
const failedSend = await fx.registration.handler(
FEISHU_MULTI_ENDPOINTS.reconnectBot,
{ botId: 'bot_safe', sendTest: true },
signal(),
);
assert.equal(failedSend.ok, true);
assert.deepEqual(failedSend.value.testMessage, {
sent: false,
code: 'test-message-failed',
});
assert.doesNotMatch(JSON.stringify(failedSend), /private Feishu provider failure/);
assert.equal(sendCalls, 1);
connected = false;
const unavailable = await fx.registration.handler(
FEISHU_MULTI_ENDPOINTS.reconnectBot,
{ botId: 'bot_safe', sendTest: true },
signal(),
);
assert.equal(unavailable.ok, true);
assert.deepEqual(unavailable.value.testMessage, {
sent: false,
code: 'test-target-unavailable',
});
assert.equal(sendCalls, 1);
const invalid = await fx.registration.handler(
FEISHU_MULTI_ENDPOINTS.reconnectBot,
{ botId: 'bot_safe', sendTest: 'yes' },
signal(),
);
assert.equal(invalid.ok, false);
assert.equal(invalid.error.code, 'bad-request');
await fx.dispose();
});
test('Feishu RPC never sends a connection test after reconnect is cancelled', async () => {
let resolveReconnect;
let sendCalls = 0;
const reconnect = new Promise((resolve) => { resolveReconnect = resolve; });
const controller = {
status: async () => status(),
startRegistration: async () => status(),
cancelRegistration: async () => status(),
disconnect: async () => status(),
reconnectBot: async () => reconnect,
sendConnectionTest: async () => { sendCalls += 1; },
};
const fx = await rpcFixture(controller);
const abort = new AbortController();
const result = fx.registration.handler(FEISHU_MULTI_ENDPOINTS.reconnectBot, {
botId: 'bot_safe',
sendTest: true,
}, abort.signal);
abort.abort();
resolveReconnect(status({
schemaVersion: 2,
bots: [{ botId: 'bot_safe', connected: true }],
}));
assert.equal((await result).error.code, 'cancelled');
assert.equal(sendCalls, 0);
await fx.dispose();
});
test('dependency failures and AbortSignal use valid RpcResult error branches', async () => {
const secret = 'should-never-be-reflected';
const controller = {
status: async () => { throw new Error(`SDK failed with ${secret}`); },
startRegistration: async () => status(),
cancelRegistration: async () => status(),
disconnect: async () => status(),
};
const fx = await rpcFixture(controller);
const failure = await fx.registration.handler(FEISHU_ENDPOINTS.status, {}, signal());
assert.deepEqual(failure, {
ok: false,
error: {
code: 'internal',
message: 'The Feishu integration operation failed.',
details: {},
},
});
assert.doesNotMatch(JSON.stringify(failure), new RegExp(secret));
const abort = new AbortController();
abort.abort();
const cancelled = await fx.registration.handler(FEISHU_ENDPOINTS.status, {}, abort.signal);
assert.equal(cancelled.ok, false);
assert.equal(cancelled.error.code, 'cancelled');
const unknown = await fx.registration.handler('credentials.read', {}, signal());
assert.equal(unknown.ok, false);
assert.equal(unknown.error.code, 'bad-request');
});
test('provisioning callback stores credentials and connects before connected is visible', async () => {
const secret = 'host-only-app-secret';
let onCredentials;
let registrationState = 'idle';
let configured = false;
let connectionStatus = {
ready: false,
feishuLongConnectionState: 'idle',
harnessReachable: false,
};
const saved = [];
const connectedWith = [];
const controller = createProvisioningBackedController({
createProvisioningManager(callbacks) {
onCredentials = callbacks.onCredentials;
return {
start() { registrationState = 'qr_ready'; return this.status(); },
status() {
return {
state: registrationState,
attempt: 1,
updatedAt: 100,
...(registrationState === 'qr_ready'
? { qrCodeUrl: 'https://accounts.feishu.cn/qr', expiresAt: Date.now() + 60_000 }
: {}),
};
},
cancel() { registrationState = 'cancelled'; return this.status(); },
};
},
credentialStore: {
async save(credentials) { saved.push(credentials); configured = true; },
async clear() { configured = false; },
async configured() { return configured; },
},
connectionManager: {
async connect(credentials) {
connectedWith.push(credentials);
connectionStatus = {
ready: true,
feishuLongConnectionState: 'connected',
harnessReachable: true,
};
},
async disconnect() {
connectionStatus = {
ready: false,
feishuLongConnectionState: 'idle',
harnessReachable: false,
};
},
status: () => connectionStatus,
},
});
const fx = await rpcFixture(controller);
const begun = await fx.registration.handler(
FEISHU_ENDPOINTS.beginProvisioning,
{ locale: 'zh-CN' },
signal(),
);
assert.equal(begun.ok, true);
await onCredentials({
client_id: 'cli_created',
client_secret: secret,
user_info: { open_id: 'ou_owner' },
});
registrationState = 'succeeded';
const ready = await fx.registration.handler(FEISHU_ENDPOINTS.status, {}, signal());
assert.equal(ready.ok, true);
assert.equal(ready.value.state, 'connected');
assert.equal(ready.value.connected, true);
assert.equal(saved[0].appSecret, secret);
assert.equal(connectedWith[0].appSecret, secret);
assert.doesNotMatch(JSON.stringify(ready), new RegExp(secret));
const disconnected = await fx.registration.handler(
FEISHU_ENDPOINTS.disconnect,
{ removeCredentials: true },
signal(),
);
assert.equal(disconnected.ok, true);
assert.equal(disconnected.value.state, 'disconnected');
assert.equal(configured, false);
});
test('DSH credential adapter stores refs off the browser plane and clears them', async () => {
const values = new Map();
const provider = {
async resolve(ref) {
return values.has(ref) ? { value: values.get(ref), source: 'file' } : undefined;
},
async describe(ref) {
return { configured: values.has(ref), source: values.has(ref) ? 'file' : undefined, writable: true };
},
async set(ref, value) { values.set(ref, value); },
async unset(ref) { values.delete(ref); },
};
const store = createDshCredentialStore(provider);
await store.save({ appId: 'cli_created', appSecret: 'stored-secret' });
assert.equal(values.get(FEISHU_APP_ID_REF), 'cli_created');
assert.equal(values.get(FEISHU_APP_SECRET_REF), 'stored-secret');
assert.equal(await store.configured(), true);
await store.clear();
assert.equal(values.size, 0);
assert.equal(await store.configured(), false);
});
test('production assembly needs only ctx credentials and the active DSH webServer', async () => {
const constructed = {};
const httpInstance = { request: async () => ({}) };
const wsAgent = {
addRequest() {},
destroy() { constructed.wsAgentDestroyed = true; },
};
class FakeConfigStore {
constructor(path) { constructed.configPath = path; }
async load() { return this; }
}
class FakeStateStore {
constructor(path) {
constructed.statePath = path;
(constructed.statePaths ??= []).push(path);
}
async load() { return this; }
}
class FakeHarness {
constructor(options) { constructed.harness = options; }
async ensureRunning() { constructed.harnessReadyChecks = (constructed.harnessReadyChecks ?? 0) + 1; }
stopManagedProcess() { constructed.harnessStopped = true; }
}
class FakeRuntime {
constructor(options) { constructed.runtime = options; }
}
class FakeController {
constructor(options) { constructed.controller = options; }
async initialize() { constructed.initialized = true; }
status() { return { totals: { configured: 0, connected: 0 } }; }
async close() { constructed.closed = true; }
}
const credentials = {};
const production = await createProductionController({
credentials,
webServer: { port: 43123, host: '127.0.0.1' },
logger: console,
}, {
dshHome: '/tmp/dsh-feishu-host-test',
workspace: '/tmp/dsh-feishu-workspace',
}, {
lark: { registerApp: async () => ({}), defaultHttpInstance: httpInstance },
Controller: FakeController,
ConfigStore: FakeConfigStore,
StateStore: FakeStateStore,
HarnessClient: FakeHarness,
FeishuRuntime: FakeRuntime,
verifyFeishuApp: async (options) => {
constructed.verifyOptions = options;
return {};
},
proxyEnv: { HTTPS_PROXY: 'http://proxy.test:8080' },
createProxyAgent: (proxyUrl) => {
constructed.wsAgentCreated = (constructed.wsAgentCreated ?? 0) + 1;
constructed.wsProxyUrl = proxyUrl;
return wsAgent;
},
});
assert.equal(constructed.initialized, undefined);
await production.ready;
assert.equal(constructed.initialized, true);
assert.equal(constructed.harnessReadyChecks, 1);
assert.equal(constructed.controller.credentials, credentials);
await constructed.controller.verifyApp({ appId: 'cli_verify', appSecret: 'verify-secret' });
assert.equal(constructed.verifyOptions.httpInstance, httpInstance);
assert.equal(constructed.wsAgentCreated, 1);
assert.equal(constructed.wsProxyUrl, 'http://proxy.test:8080');
assert.equal(String(constructed.harness.baseUrl), 'http://127.0.0.1:43123/');
assert.equal(constructed.harness.autostart, false);
assert.match(constructed.configPath, /integrations\/dsh-feishu\/config\.json$/);
await constructed.controller.createRuntime({
config: {
appId: 'cli_created',
domain: 'feishu',
ownerOpenId: 'ou_owner',
},
appSecret: 'host-only',
});
assert.match(constructed.statePath, /integrations\/dsh-feishu\/state\.json$/);
assert.equal(constructed.runtime.appSecret, 'host-only');
assert.equal(constructed.runtime.wsAgent, wsAgent);
const repair = { start() {}, status() {}, cancel() {} };
await constructed.controller.createRuntime({
botId: 'bot_alpha',
config: {
id: 'bot_alpha',
appId: 'cli_alpha',
secretRef: 'DSH_FEISHU_APP_SECRET_ALPHA',
domain: 'feishu',
ownerOpenIds: ['ou_alpha'],
},
appSecret: 'alpha-secret',
repair,
});
const alphaState = constructed.runtime.state;
assert.equal(constructed.runtime.botId, 'bot_alpha');
assert.equal(constructed.runtime.repair, repair);
assert.equal(Object.hasOwn(constructed.runtime, 'outboundArtifactsEnabled'), false);
await constructed.controller.createRuntime({
botId: 'bot_beta',
config: {
id: 'bot_beta',
appId: 'cli_beta',
secretRef: 'DSH_FEISHU_APP_SECRET_BETA',
domain: 'feishu',
ownerOpenIds: ['ou_beta'],
},
appSecret: 'beta-secret',
});
const betaState = constructed.runtime.state;
assert.equal(Object.hasOwn(constructed.runtime, 'outboundArtifactsEnabled'), false);
assert.notEqual(alphaState, betaState);
assert.ok(constructed.statePaths.some((path) => /bots\/bot_alpha\/state\.json$/.test(path)));
assert.ok(constructed.statePaths.some((path) => /bots\/bot_beta\/state\.json$/.test(path)));
await production.close();
assert.equal(constructed.closed, true);
assert.equal(constructed.harnessStopped, true);
assert.equal(constructed.wsAgentDestroyed, true);
});
test('a corrupt legacy state file cannot prevent a healthy v2 bot from starting', async () => {
const dataDir = await mkdtemp(join(tmpdir(), 'dsh-feishu-production-state-isolation-'));
await mkdir(dataDir, { recursive: true });
await writeFile(join(dataDir, 'state.json'), '{corrupt legacy state');
const legacy = {
id: 'bot_legacy',
appId: 'cli_legacy',
secretRef: 'DSH_FEISHU_APP_SECRET',
ownerOpenIds: ['ou_legacy'],
domain: 'feishu',
botName: '旧机器人',
};
const healthy = {
id: 'bot_healthy',
appId: 'cli_healthy',
secretRef: 'DSH_FEISHU_APP_SECRET_HEALTHY',
ownerOpenIds: ['ou_healthy'],
domain: 'feishu',
botName: '健康机器人',
};
await writeFile(join(dataDir, 'config.json'), JSON.stringify({ version: 2, bots: [legacy, healthy] }));
const secrets = new Map([
[legacy.secretRef, 'legacy-secret'],
[healthy.secretRef, 'healthy-secret'],
]);
class FakeRuntime {
#status = { ready: false, feishuLongConnectionState: 'idle', harnessReachable: false };
get status() { return structuredClone(this.#status); }
async start() {
this.#status = { ready: true, feishuLongConnectionState: 'connected', harnessReachable: true };
}
async stop() {
this.#status = { ready: false, feishuLongConnectionState: 'idle', harnessReachable: false };
}
}
class FakeHarness {
async ensureRunning() {}
stopManagedProcess() {}
}
const production = await createProductionController({
credentials: {
async resolve(ref) { return secrets.has(ref) ? { value: secrets.get(ref) } : undefined; },
async set(ref, value) { secrets.set(ref, value); },
async unset(ref) { secrets.delete(ref); },
},
webServer: { port: 43124 },
logger: console,
}, { dataDir, workspace: dataDir }, {
lark: { registerApp: async () => ({}) },
HarnessClient: FakeHarness,
FeishuRuntime: FakeRuntime,
verifyFeishuApp: async () => ({}),
proxyEnv: {},
});
await production.ready;
const statusValue = await production.controller.status();
assert.equal(statusValue.bots.find((entry) => entry.botId === 'bot_legacy').phase, 'error');
assert.equal(statusValue.bots.find((entry) => entry.botId === 'bot_healthy').connected, true);
assert.equal(statusValue.totals.connected, 1);
await production.close();
});