import test from 'node:test';
import assert from 'node:assert/strict';
import { VerifiedFeishuChannel } from '../../../src/channels/feishu/feishu-channel.mjs';
function fakeClient(overrides = {}) {
const calls = {
cards: [],
messages: [],
replies: [],
updates: [],
settings: [],
recalls: [],
reactionsAdded: [],
reactionsRemoved: [],
fileUploads: [],
imageUploads: [],
};
const client = {
cardkit: { v1: {
card: {
create: async (request) => {
calls.cards.push(request);
const cardId = calls.cards.length === 1 ? 'card-test' : `card-test-${calls.cards.length}`;
return { code: 0, data: { card_id: cardId } };
},
settings: async (request) => {
calls.settings.push(request);
return { code: 0 };
},
},
cardElement: {
content: async (request) => {
calls.updates.push(request);
return { code: 0 };
},
},
} },
im: { v1: {
file: {
create: async (request) => {
calls.fileUploads.push(request);
return { file_key: 'file-key-test' };
},
},
image: {
create: async (request) => {
calls.imageUploads.push(request);
return { data: { image_key: 'image-key-test' } };
},
},
message: {
reply: async (request) => {
calls.replies.push(request);
const messageId = calls.replies.length === 1 ? 'om-stream' : `om-stream-${calls.replies.length}`;
return { code: 0, data: { message_id: messageId } };
},
create: async (request) => {
calls.messages.push(request);
const messageId = calls.messages.length === 1 ? 'om-stream' : `om-stream-${calls.messages.length}`;
return { code: 0, data: { message_id: messageId } };
},
delete: async (request) => {
calls.recalls.push(request);
return { code: 0 };
},
},
messageReaction: {
create: async (request) => {
calls.reactionsAdded.push(request);
return { code: 0, data: { reaction_id: 'reaction-test' } };
},
delete: async (request) => {
calls.reactionsRemoved.push(request);
return { code: 0 };
},
},
} },
};
if (overrides.updateContent) client.cardkit.v1.cardElement.content = overrides.updateContent;
if (overrides.finishCard) client.cardkit.v1.card.settings = overrides.finishCard;
if (overrides.uploadFile) client.im.v1.file.create = overrides.uploadFile;
if (overrides.uploadImage) client.im.v1.image.create = overrides.uploadImage;
if (overrides.replyMessage) client.im.v1.message.reply = overrides.replyMessage;
if (overrides.createMessage) client.im.v1.message.create = overrides.createMessage;
return { client, calls };
}
function finalCardContents(calls) {
return calls.cards.map((request, index) => {
const cardId = index === 0 ? 'card-test' : `card-test-${index + 1}`;
return calls.updates.findLast((update) => update.path.card_id === cardId)?.data.content
?? JSON.parse(request.data.data).body.elements[0].content;
});
}
test('VerifiedFeishuChannel streams content and verifies terminal settings', async () => {
const { client, calls } = fakeClient();
const channel = new VerifiedFeishuChannel({ client, initialText: '正在思考…' });
const result = await channel.stream('oc_chat', {
markdown: async (controller) => {
await controller.setContent('第一段');
await controller.setContent('第一段和第二段');
},
}, { replyTo: 'om_user' });
assert.deepEqual(result, { messageId: 'om-stream', providerMessageIds: ['om-stream'] });
assert.equal(calls.replies[0].path.message_id, 'om_user');
assert.deepEqual(calls.updates.map((call) => ({
content: call.data.content,
sequence: call.data.sequence,
})), [
{ content: '第一段', sequence: 1 },
{ content: '第一段和第二段', sequence: 2 },
]);
assert.equal(calls.settings[0].data.sequence, 3);
assert.deepEqual(JSON.parse(calls.settings[0].data.settings), {
config: {
streaming_mode: false,
summary: { content: '第一段和第二段' },
},
});
assert.equal(calls.recalls.length, 0);
});
test('VerifiedFeishuChannel previews long snapshots and delivers the entire final answer in cards', async () => {
const { client, calls } = fakeClient();
const channel = new VerifiedFeishuChannel({ client });
const answer = `${'A'.repeat(28000)}\n\n ${'中'.repeat(28000)}😀\n尾声 `;
const result = await channel.stream('oc_chat', {
markdown: async (controller) => {
await controller.setContent(answer.slice(0, 28001));
const preview = calls.updates.at(-1).data.content;
assert.ok(preview.length <= 28000);
assert.match(preview, /生成完成后将分段发送完整回答/);
const updateCount = calls.updates.length;
await controller.setContent(answer);
assert.equal(calls.updates.length, updateCount, 'unchanged previews need no provider update');
assert.equal(calls.cards.length, 1, 'only the final snapshot is split into permanent cards');
},
}, { replyTo: 'om_user' });
const contents = finalCardContents(calls);
assert.equal(contents.join(''), answer, 'preserve the latest tail, newlines, and whitespace');
assert.ok(contents.every((content) => content.length <= 28000 && content.isWellFormed()));
assert.ok(calls.updates.every(({ data }) => data.content.length <= 28000));
assert.equal(result.providerMessageIds.length, contents.length);
assert.deepEqual(result.providerMessageIds, ['om-stream', 'om-stream-2', 'om-stream-3']);
assert.ok(calls.replies.every(({ path }) => path.message_id === 'om_user'));
assert.equal(calls.settings.length, contents.length);
assert.ok(calls.settings.every(({ data }) => (
JSON.parse(data.settings).config.streaming_mode === false
)));
assert.equal(calls.recalls.length, 0);
});
test('VerifiedFeishuChannel handles exact limits and Unicode without a reply target', async (t) => {
const previewLimit = 28000 - '\n\n内容较长,生成完成后将分段发送完整回答。'.length;
for (const answer of [
'a'.repeat(27999),
'a'.repeat(28000),
'a'.repeat(28001),
`${'中'.repeat(27999)}😀尾声`,
`${'a'.repeat(20000)}\n\n${'b'.repeat(12000)}`,
`${'a'.repeat(48)}😀${'b'.repeat(30000)}`,
`${'a'.repeat(previewLimit - 1)}😀${'b'.repeat(100)}`,
]) {
await t.test(`${answer.length} UTF-16 units`, async () => {
const { client, calls } = fakeClient();
const channel = new VerifiedFeishuChannel({ client });
const result = await channel.stream('oc_chat', {
markdown: async (controller) => controller.setContent(answer),
});
const contents = finalCardContents(calls);
assert.equal(contents.join(''), answer);
assert.ok(contents.every((content) => content.length <= 28000 && content.isWellFormed()));
assert.ok(calls.updates.every(({ data }) => (
data.content.length <= 28000 && data.content.isWellFormed()
)));
assert.ok(calls.settings.every(({ data }) => (
JSON.parse(data.settings).config.summary.content.isWellFormed()
)));
assert.equal(contents.length, answer.length <= 28000 ? 1 : 2);
assert.equal(result.providerMessageIds.length, contents.length);
assert.ok(calls.messages.every(({ params, data }) => (
params.receive_id_type === 'chat_id' && data.receive_id === 'oc_chat'
)));
assert.equal(calls.replies.length, 0);
assert.equal(calls.recalls.length, 0);
});
}
});
test('VerifiedFeishuChannel replaces oversized progress with a shorter final answer', async () => {
const { client, calls } = fakeClient();
const channel = new VerifiedFeishuChannel({ client });
await channel.stream('oc_chat', {
markdown: async (controller) => {
await controller.setContent('中间过程'.repeat(10000));
await controller.setContent('最终回答');
},
}, { replyTo: 'om_user' });
assert.deepEqual(finalCardContents(calls), ['最终回答']);
assert.equal(calls.replies.length, 1);
assert.equal(calls.recalls.length, 0);
});
test('VerifiedFeishuChannel finishes an empty producer and skips duplicate content updates', async () => {
const { client, calls } = fakeClient();
const channel = new VerifiedFeishuChannel({ client, initialText: '' });
await channel.stream('oc_chat', {
markdown: async (controller) => {
await controller.setContent(null);
await controller.setContent('');
},
});
assert.deepEqual(finalCardContents(calls), ['…']);
assert.equal(calls.updates.length, 0);
assert.equal(calls.settings.length, 1);
});
test('VerifiedFeishuChannel cleans up every split card on a real provider failure for text fallback', async () => {
const { client, calls } = fakeClient({
updateContent: async (request) => {
calls.updates.push(request);
return request.path.card_id === 'card-test-3'
? { code: 230099, msg: 'element update failed' }
: { code: 0 };
},
});
const channel = new VerifiedFeishuChannel({ client });
await assert.rejects(channel.stream('oc_chat', {
markdown: async (controller) => controller.setContent('a'.repeat(56001)),
}, { replyTo: 'om_user' }), /cardElement\.content failed/);
assert.equal(calls.settings.length, 2);
assert.deepEqual(calls.recalls.map(({ path }) => path.message_id), [
'om-stream', 'om-stream-2', 'om-stream-3',
]);
});
test('VerifiedFeishuChannel rejects failed updates and recalls the partial card', async () => {
const { client, calls } = fakeClient({
updateContent: async () => ({ code: 230099, msg: 'element update failed' }),
});
const channel = new VerifiedFeishuChannel({ client });
await assert.rejects(channel.stream('oc_chat', {
markdown: async (controller) => controller.setContent('最终回答'),
}, { replyTo: 'om_user' }), /cardElement\.content failed/);
assert.deepEqual(calls.recalls, [{ path: { message_id: 'om-stream' } }]);
assert.equal(calls.settings.length, 0);
});
test('VerifiedFeishuChannel rejects failed finalization and recalls the card', async () => {
const { client, calls } = fakeClient({
finishCard: async (request) => {
calls.settings.push(request);
return { code: 230099, msg: 'card finalization failed' };
},
});
const channel = new VerifiedFeishuChannel({ client });
await assert.rejects(channel.stream('oc_chat', {
markdown: async (controller) => controller.setContent('已经生成的回答'),
}, { replyTo: 'om_user' }), /card\.settings failed/);
assert.deepEqual(calls.recalls, [{ path: { message_id: 'om-stream' } }]);
assert.equal(calls.settings.length, 1);
});
test('VerifiedFeishuChannel checks reaction API results', async () => {
const { client, calls } = fakeClient();
const channel = new VerifiedFeishuChannel({ client });
const reactionId = await channel.addReaction('om_user', 'OnIt');
await channel.removeReaction('om_user', reactionId);
assert.equal(reactionId, 'reaction-test');
assert.equal(calls.reactionsAdded[0].data.reaction_type.emoji_type, 'OnIt');
assert.equal(calls.reactionsRemoved[0].path.reaction_id, 'reaction-test');
});
test('VerifiedFeishuChannel uploads a materialized result and replies with a native file message', async () => {
const { client, calls } = fakeClient();
const channel = new VerifiedFeishuChannel({ client });
const file = {
artifactId: 'artifact-html',
deliveryKey: 'delivery-html',
fileName: 'result.html',
mediaType: 'text/html',
size: 19,
bytes: Buffer.from('
result
'),
};
const receipt = await channel.sendFile('oc_chat', file, { replyTo: 'om_user' });
assert.equal(calls.fileUploads.length, 1);
assert.equal(calls.fileUploads[0].data.file_type, 'stream');
assert.equal(calls.fileUploads[0].data.file_name, 'result.html');
assert.equal(calls.fileUploads[0].data.file, file.bytes);
assert.equal(calls.replies.length, 1);
assert.equal(calls.replies[0].path.message_id, 'om_user');
assert.equal(calls.replies[0].data.msg_type, 'file');
assert.deepEqual(JSON.parse(calls.replies[0].data.content), { file_key: 'file-key-test' });
assert.match(calls.replies[0].data.uuid, /^dshim_[a-f0-9]{40}$/);
assert.deepEqual(receipt, {
schemaVersion: 1,
deliveryId: 'delivery-html',
presentation: 'feishu-file',
providerMessageIds: ['om-stream'],
artifacts: [{ artifactId: 'artifact-html', outcome: 'sent' }],
});
});
test('VerifiedFeishuChannel uploads an image and replies with a native image message', async () => {
const { client, calls } = fakeClient();
const channel = new VerifiedFeishuChannel({ client });
const file = {
artifactId: 'artifact-image',
deliveryKey: 'delivery-image',
fileName: 'result.png',
mediaType: 'image/png',
size: 12,
bytes: Buffer.from('png contents'),
};
const receipt = await channel.sendImage('oc_chat', file, { replyTo: 'om_user' });
assert.equal(calls.fileUploads.length, 0);
assert.equal(calls.imageUploads.length, 1);
assert.deepEqual(calls.imageUploads[0].data, {
image_type: 'message',
image: file.bytes,
});
assert.equal(calls.replies.length, 1);
assert.equal(calls.replies[0].path.message_id, 'om_user');
assert.equal(calls.replies[0].data.msg_type, 'image');
assert.deepEqual(JSON.parse(calls.replies[0].data.content), { image_key: 'image-key-test' });
assert.match(calls.replies[0].data.uuid, /^dshim_[a-f0-9]{40}$/);
assert.deepEqual(receipt, {
schemaVersion: 1,
deliveryId: 'delivery-image',
presentation: 'feishu-image',
providerMessageIds: ['om-stream'],
artifacts: [{ artifactId: 'artifact-image', outcome: 'sent' }],
});
});
test('VerifiedFeishuChannel sends to the current chat when no reply target exists', async () => {
const { client, calls } = fakeClient({
createMessage: async (request) => {
calls.replies.push(request);
return { code: 0, data: { message_id: 'om-created-file' } };
},
});
const channel = new VerifiedFeishuChannel({ client });
await channel.sendFile('oc_chat', {
artifactId: 'artifact-generic',
deliveryKey: 'delivery-generic',
fileName: 'result.bin',
bytes: Buffer.from('generic'),
});
assert.deepEqual(calls.replies[0].params, { receive_id_type: 'chat_id' });
assert.equal(calls.replies[0].data.receive_id, 'oc_chat');
assert.equal(calls.replies[0].data.msg_type, 'file');
});
test('VerifiedFeishuChannel retries uncertain message delivery without uploading twice', async () => {
const requests = [];
let attempts = 0;
const { client, calls } = fakeClient({
replyMessage: async (request) => {
requests.push(structuredClone(request));
attempts += 1;
return attempts === 1
? { code: 230049, msg: 'still sending' }
: { code: 0, data: { message_id: 'om-retried-file' } };
},
});
const channel = new VerifiedFeishuChannel({ client });
const receipt = await channel.sendFile('oc_chat', {
artifactId: 'artifact-retry',
deliveryKey: 'delivery-retry',
fileName: 'retry.txt',
bytes: Buffer.from('retry'),
}, { replyTo: 'om_user' });
assert.equal(calls.fileUploads.length, 1);
assert.equal(requests.length, 2);
assert.deepEqual(requests[1], requests[0]);
assert.deepEqual(receipt.providerMessageIds, ['om-retried-file']);
});
test('VerifiedFeishuChannel stops after one 230049 retry and never uploads the file twice', async () => {
const requests = [];
const { client, calls } = fakeClient({
replyMessage: async (request) => {
requests.push(structuredClone(request));
return { code: 230049, msg: 'still sending' };
},
});
const channel = new VerifiedFeishuChannel({ client });
await assert.rejects(channel.sendFile('oc_chat', {
artifactId: 'artifact-still-uncertain',
deliveryKey: 'delivery-still-uncertain',
fileName: 'uncertain.txt',
bytes: Buffer.from('uncertain'),
}, { replyTo: 'om_user' }), (error) => error.code === 'artifact-delivery-uncertain'
&& error.providerCode === 230049);
assert.equal(calls.fileUploads.length, 1);
assert.equal(requests.length, 2);
assert.deepEqual(requests[1], requests[0]);
});
test('VerifiedFeishuChannel retries an SDK-thrown 230049 with the same file key and UUID', async () => {
const requests = [];
const { client, calls } = fakeClient({
replyMessage: async (request) => {
requests.push(structuredClone(request));
if (requests.length === 1) {
const error = new Error('still sending');
error.code = 230049;
throw error;
}
return { code: 0, data: { message_id: 'om-after-thrown-230049' } };
},
});
const channel = new VerifiedFeishuChannel({ client });
const receipt = await channel.sendFile('oc_chat', {
artifactId: 'artifact-thrown-retry',
deliveryKey: 'delivery-thrown-retry',
fileName: 'thrown-retry.txt',
bytes: Buffer.from('retry'),
}, { replyTo: 'om_user' });
assert.equal(calls.fileUploads.length, 1);
assert.equal(requests.length, 2);
assert.deepEqual(requests[1], requests[0]);
assert.deepEqual(receipt.providerMessageIds, ['om-after-thrown-230049']);
});
test('VerifiedFeishuChannel bounds upload and message waits independently', async () => {
const uploadFixture = fakeClient({ uploadFile: async () => new Promise(() => {}) });
const uploadChannel = new VerifiedFeishuChannel({
client: uploadFixture.client,
fileUploadTimeoutMs: 10,
fileMessageTimeoutMs: 100,
});
await assert.rejects(uploadChannel.sendFile('oc_chat', {
artifactId: 'artifact-upload-timeout',
deliveryKey: 'delivery-upload-timeout',
fileName: 'upload-timeout.txt',
bytes: Buffer.from('timeout'),
}, { replyTo: 'om_user' }), (error) => error.code === 'artifact-provider-failed'
&& error.cause?.code === 'provider-timeout');
assert.equal(uploadFixture.calls.replies.length, 0);
let messageCalls = 0;
const messageFixture = fakeClient({
replyMessage: async () => {
messageCalls += 1;
return new Promise(() => {});
},
});
const messageChannel = new VerifiedFeishuChannel({
client: messageFixture.client,
fileUploadTimeoutMs: 100,
fileMessageTimeoutMs: 10,
});
await assert.rejects(messageChannel.sendFile('oc_chat', {
artifactId: 'artifact-message-timeout',
deliveryKey: 'delivery-message-timeout',
fileName: 'message-timeout.txt',
bytes: Buffer.from('timeout'),
}, { replyTo: 'om_user' }), (error) => error.code === 'artifact-delivery-uncertain'
&& error.cause?.code === 'provider-timeout');
assert.equal(messageFixture.calls.fileUploads.length, 1);
assert.equal(messageCalls, 1);
});
test('VerifiedFeishuChannel rejects waits longer than the 120 second operation timeout', () => {
const { client } = fakeClient();
assert.throws(
() => new VerifiedFeishuChannel({ client, fileUploadTimeoutMs: 120_001 }),
/fileUploadTimeoutMs.*120000/,
);
assert.throws(
() => new VerifiedFeishuChannel({ client, fileMessageTimeoutMs: 120_001 }),
/fileMessageTimeoutMs.*120000/,
);
});
test('VerifiedFeishuChannel stops after an in-flight upload when file delivery is cancelled', async () => {
let uploadStarted;
const started = new Promise((resolve) => { uploadStarted = resolve; });
const { client, calls } = fakeClient({
uploadFile: async () => {
uploadStarted();
return new Promise(() => {});
},
});
const channel = new VerifiedFeishuChannel({ client });
const abort = new AbortController();
const reason = new DOMException('runtime stopped', 'AbortError');
const sending = channel.sendFile('oc_chat', {
artifactId: 'artifact-cancelled-upload',
deliveryKey: 'delivery-cancelled-upload',
fileName: 'cancelled.txt',
bytes: Buffer.from('cancelled'),
}, { replyTo: 'om_user', signal: abort.signal });
await started;
abort.abort(reason);
await assert.rejects(sending, (error) => error === reason);
assert.equal(calls.replies.length, 0);
});
test('VerifiedFeishuChannel immediately preserves caller abort during an in-flight message send', async () => {
let messageStarted;
const started = new Promise((resolve) => { messageStarted = resolve; });
const { client, calls } = fakeClient({
replyMessage: async () => {
messageStarted();
return new Promise(() => {});
},
});
const channel = new VerifiedFeishuChannel({ client });
const abort = new AbortController();
const reason = new DOMException('runtime stopped', 'AbortError');
const sending = channel.sendFile('oc_chat', {
artifactId: 'artifact-cancelled-message',
deliveryKey: 'delivery-cancelled-message',
fileName: 'cancelled-message.txt',
bytes: Buffer.from('cancelled'),
}, { replyTo: 'om_user', signal: abort.signal });
await started;
abort.abort(reason);
await assert.rejects(sending, (error) => error === reason
&& error.code !== 'artifact-delivery-uncertain');
assert.equal(calls.fileUploads.length, 1);
});
test('VerifiedFeishuChannel does not retry an uncertain file message after cancellation', async () => {
const abort = new AbortController();
const reason = new DOMException('runtime stopped', 'AbortError');
const requests = [];
const { client } = fakeClient({
replyMessage: async (request) => {
requests.push(request);
abort.abort(reason);
return { code: 230049, msg: 'still sending' };
},
});
const channel = new VerifiedFeishuChannel({ client });
await assert.rejects(channel.sendFile('oc_chat', {
artifactId: 'artifact-cancelled-retry',
deliveryKey: 'delivery-cancelled-retry',
fileName: 'cancelled-retry.txt',
bytes: Buffer.from('cancelled retry'),
}, { replyTo: 'om_user', signal: abort.signal }), (error) => error === reason);
assert.equal(requests.length, 1);
});
test('VerifiedFeishuChannel lets Feishu decide empty and oversize file outcomes', async () => {
const uploads = [];
const providerCodes = [234010, 234006, 0];
const { client } = fakeClient({
uploadFile: async (request) => {
uploads.push(request);
const code = providerCodes.shift();
return code === 0 ? {} : { code };
},
});
const channel = new VerifiedFeishuChannel({ client });
const base = {
artifactId: 'artifact-invalid',
deliveryKey: 'delivery-invalid',
fileName: 'invalid.bin',
};
await assert.rejects(
channel.sendFile('oc_chat', { ...base, bytes: Buffer.alloc(0) }),
(error) => error.code === 'artifact-empty' && error.providerCode === 234010,
);
await assert.rejects(
channel.sendFile('oc_chat', { ...base, bytes: Buffer.from('provider decides') }),
(error) => error.code === 'artifact-too-large' && error.providerCode === 234006,
);
await assert.rejects(
channel.sendFile('oc_chat', { ...base, bytes: Buffer.from('valid bytes') }),
(error) => error.code === 'artifact-provider-failed' && !error.message.includes('undefined'),
);
assert.deepEqual(uploads.map((request) => request.data.file.byteLength), [0, 16, 11]);
});
test('VerifiedFeishuChannel distinguishes upload failure from uncertain final delivery', async () => {
const uploadFixture = fakeClient({
uploadFile: async () => { throw new Error('upload transport closed'); },
});
const uploadChannel = new VerifiedFeishuChannel({ client: uploadFixture.client });
await assert.rejects(uploadChannel.sendFile('oc_chat', {
artifactId: 'artifact-upload-transport',
deliveryKey: 'delivery-upload-transport',
fileName: 'upload.txt',
bytes: Buffer.from('upload'),
}, { replyTo: 'om_user' }), (error) => error.code === 'artifact-provider-failed');
assert.equal(uploadFixture.calls.replies.length, 0);
for (const replyMessage of [
async () => { throw new Error('message transport closed'); },
async () => ({ code: 0, data: {} }),
]) {
const messageFixture = fakeClient({ replyMessage });
const messageChannel = new VerifiedFeishuChannel({ client: messageFixture.client });
await assert.rejects(messageChannel.sendFile('oc_chat', {
artifactId: 'artifact-uncertain-message',
deliveryKey: 'delivery-uncertain-message',
fileName: 'message.txt',
bytes: Buffer.from('message'),
}, { replyTo: 'om_user' }), (error) => error.code === 'artifact-delivery-uncertain');
assert.equal(messageFixture.calls.fileUploads.length, 1);
}
});
test('VerifiedFeishuChannel keeps explicit final provider codes out of the uncertain bucket', async () => {
const { client, calls } = fakeClient({
replyMessage: async () => ({ code: 99991672, msg: 'missing scope' }),
});
const channel = new VerifiedFeishuChannel({ client });
await assert.rejects(channel.sendFile('oc_chat', {
artifactId: 'artifact-message-permission',
deliveryKey: 'delivery-message-permission',
fileName: 'permission.txt',
bytes: Buffer.from('permission'),
}, { replyTo: 'om_user' }), (error) => error.code === 'artifact-permission-required'
&& error.providerCode === 99991672);
assert.equal(calls.fileUploads.length, 1);
});
test('VerifiedFeishuChannel classifies missing file permission without exposing provider details', async () => {
const { client } = fakeClient({
uploadFile: async () => {
const error = new Error('raw tenant detail and private diagnostic');
error.code = 99991672;
throw error;
},
});
const channel = new VerifiedFeishuChannel({ client });
await assert.rejects(
channel.sendFile('oc_chat', {
artifactId: 'artifact-permission',
deliveryKey: 'delivery-permission',
fileName: 'result.html',
bytes: Buffer.from('result'),
}),
(error) => error.code === 'artifact-permission-required'
&& error.message.includes('im:resource permission')
&& !error.message.includes('im:resource:upload')
&& !error.message.includes('private diagnostic'),
);
});
test('VerifiedFeishuChannel recognizes the SDK array-shaped permission error', async () => {
const { client } = fakeClient({
uploadFile: async () => {
const transport = new Error('raw transport diagnostic');
transport.code = 'ERR_BAD_REQUEST';
throw [transport, {
code: 99991672,
msg: 'raw provider permission URL',
}];
},
});
const channel = new VerifiedFeishuChannel({ client });
await assert.rejects(
channel.sendFile('oc_chat', {
artifactId: 'artifact-array-permission',
deliveryKey: 'delivery-array-permission',
fileName: 'result.txt',
bytes: Buffer.from('result'),
}),
(error) => error.code === 'artifact-permission-required'
&& error.providerCode === 99991672
&& !error.message.includes('provider permission URL'),
);
});
test('rotate() finalizes the old card and carries the final answer into a new card', async () => {
const { client, calls } = fakeClient();
const channel = new VerifiedFeishuChannel({ client, initialText: '正在思考…' });
const result = await channel.stream('oc_chat', {
markdown: async (controller) => {
await controller.setContent('第一步进行中');
await controller.rotate();
// 此间隙 bridge 发出独立交互消息(此处不需要模拟)
await controller.setContent('最终回答');
},
});
assert.deepEqual(result.providerMessageIds, ['om-stream', 'om-stream-2']);
const card1 = calls.updates.filter((u) => u.path.card_id === 'card-test');
assert.ok(card1.at(-1).data.content.includes('最终结果见下方'), 'old card must carry the pointer notice');
const card2 = calls.updates.filter((u) => u.path.card_id === 'card-test-2');
assert.ok(card2.at(-1).data.content.includes('最终回答'), 'new card must carry the final answer');
assert.equal(calls.settings.length, 2, 'both cards must be finished');
});
test('rotate() degrades gracefully when finalizing the old card fails', async () => {
let cardTestUpdates = 0;
const { client, calls } = fakeClient({
updateContent: async (request) => {
if (request.path.card_id === 'card-test') {
cardTestUpdates += 1;
if (cardTestUpdates === 2) throw new Error('transient finalize failure');
}
calls.updates.push(request);
return { code: 0 };
},
});
const channel = new VerifiedFeishuChannel({ client, initialText: '正在思考…' });
const result = await channel.stream('oc_chat', {
markdown: async (controller) => {
await controller.setContent('第一步进行中');
await controller.rotate();
await controller.setContent('最终回答');
},
});
assert.deepEqual(result.providerMessageIds, ['om-stream', 'om-stream-2']);
const card2 = calls.updates.filter((u) => u.path.card_id === 'card-test-2');
assert.ok(card2.at(-1).data.content.includes('最终回答'));
});
test('rotate() keeps oversized chunked delivery on the rotated card chain', async () => {
const { client, calls } = fakeClient();
const channel = new VerifiedFeishuChannel({ client, initialText: '正在思考…' });
const huge = `${'A'.repeat(27990)}\nB`.repeat(2);
const result = await channel.stream('oc_chat', {
markdown: async (controller) => {
await controller.setContent('过程');
await controller.rotate();
await controller.setContent(huge);
},
});
assert.equal(result.providerMessageIds.length >= 3, true);
const settingsByCard = calls.settings.map((s) => s.path.card_id);
assert.deepEqual([...new Set(settingsByCard)].length, settingsByCard.length, 'each card finishes exactly once');
});