SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

+275
View File
@@ -0,0 +1,275 @@
import { describe, expect, it, vi } from 'vitest';
import { callTool, computeTools } from '../src/tools';
import { ACTIONS } from '../src/generated/catalog';
import type { ActionClient, OneBotEnvelope } from '../src/client';
import { ActionCallError } from '../src/results';
const docs = { mode: 'docs' as const };
function client(
reply: OneBotEnvelope = { status: 'ok', retcode: 0, data: { message_id: -123 } },
): ActionClient {
return {
call: vi.fn(async () => reply),
callStream: vi.fn(async () => ({ frame_count: 1, frames: [], terminal: reply })),
};
}
function text(result: Awaited<ReturnType<typeof callTool>>) {
expect(result.content[0].type).toBe('text');
return result.content[0].type === 'text' ? result.content[0].text : '';
}
describe('additive structured results', () => {
it.each(['list_actions', 'list_categories', 'get_action', 'search_actions'])(
'keeps the original text payload for %s',
async (name) => {
const result = await callTool(name, { name: 'get_status', query: 'get_status' }, docs);
expect(result.structuredContent.result).toEqual(JSON.parse(text(result)));
expect(result.structuredContent.diagnostics).toEqual({ outcome: 'success' });
expect(
computeTools('docs').find((tool) => tool.name === name)?.outputSchema,
).toBeDefined();
},
);
it.each([
{ status: 'ok', retcode: 0, data: { message_id: -123 } },
{ status: 'failed', retcode: 1404, wording: 'not found' },
{ status: 'async', retcode: 1 },
])('preserves the OneBot envelope and isError contract ($status)', async (reply) => {
const result = await callTool(
'invoke_action',
{ action: 'send_group_msg' },
{ mode: 'write', client: client(reply) },
);
expect(text(result)).toBe(JSON.stringify(reply, null, 2));
expect(result.isError).toBeUndefined();
expect(result.structuredContent.result).toEqual(reply);
expect(result.structuredContent.diagnostics.outcome).toBe(
reply.status === 'ok' ? 'success' : reply.status === 'failed' ? 'failed' : 'unknown',
);
});
it('reports permission failures without contacting OneBot', async () => {
const onebot = client();
const result = await callTool(
'query_action',
{ action: 'send_group_msg' },
{ mode: 'read', client: onebot },
);
expect(result.isError).toBe(true);
expect(result.structuredContent.diagnostics).toMatchObject({
outcome: 'failed',
kind: 'permission',
});
expect(onebot.call).not.toHaveBeenCalled();
});
it.each(['timeout', 'cancelled', 'authentication', 'transport', 'invalid_response'] as const)(
'makes %s observable without claiming a write was rolled back',
async (kind) => {
const onebot = client();
onebot.call = vi.fn(async () => {
throw new ActionCallError('request failed', kind);
});
const result = await callTool(
'invoke_action',
{ action: 'send_group_msg' },
{ mode: 'write', client: onebot },
);
expect(result.isError).toBe(true);
expect(result.structuredContent.diagnostics).toMatchObject({
outcome: 'unknown',
kind,
});
expect(result.structuredContent.diagnostics.next_step).toContain('不代表撤销');
expect(onebot.call).toHaveBeenCalledTimes(1);
},
);
it('does not report an incomplete response as successful', async () => {
const onebot = client();
onebot.call = async () => JSON.parse('{"data":null}');
const result = await callTool(
'query_action',
{ action: 'get_status' },
{ mode: 'read', client: onebot },
);
expect(result.structuredContent.diagnostics).toMatchObject({
outcome: 'unknown',
kind: 'invalid_response',
});
expect(text(result)).toBe('{\n "data": null\n}');
});
it('keeps malformed peer fields in the original payload without breaking the diagnostics schema', async () => {
const onebot = client();
onebot.call = async () =>
JSON.parse('{"status":"failed","retcode":1400,"wording":{"unexpected":true}}');
const result = await callTool(
'query_action',
{ action: 'get_status' },
{ mode: 'read', client: onebot },
);
expect(result.structuredContent.diagnostics.message).toBe('retcode=1400');
expect(JSON.parse(text(result)).wording).toEqual({ unexpected: true });
});
});
describe('optional discovery controls', () => {
it('keeps legacy matching, ordering and response fields by default', async () => {
const result = await callTool('search_actions', { query: '群 禁言' }, docs);
expect(JSON.parse(text(result))).toEqual({ count: 0, actions: [] });
const all = JSON.parse(text(await callTool('list_actions', {}, docs)));
expect(Object.keys(all)).toEqual(['count', 'actions']);
expect(all.actions.map((action: { name: string }) => action.name)).toEqual(
ACTIONS.map((action) => action.name),
);
});
it.each([
['群 禁言', 'set_group_ban'],
['历史消息', 'get_group_msg_history'],
['私聊 history', 'get_friend_msg_history'],
['group mute', 'set_group_ban'],
])('finds %s with optional keyword matching', async (query, expected) => {
const data = JSON.parse(
text(await callTool('search_actions', { query, match: 'keywords' }, docs)),
);
expect(data.actions.map((action: { name: string }) => action.name)).toContain(expected);
});
it('ranks an exact alias first and combines filters', async () => {
const exact = JSON.parse(
text(
await callTool('search_actions', { query: 'nc_get_rkey', match: 'keywords' }, docs),
),
);
expect(exact.actions[0].name).toBe('get_rkey');
const filtered = JSON.parse(
text(
await callTool(
'search_actions',
{
query: '群',
match: 'keywords',
category: '群管理',
read_only: false,
stream: false,
},
docs,
),
),
);
expect(filtered.count).toBeGreaterThan(0);
expect(
filtered.actions.every(
(action: { category: string; readOnly: boolean; stream: boolean }) =>
action.category === '群管理' && !action.readOnly && !action.stream,
),
).toBe(true);
});
it('paginates deterministically with explicit totals and termination', async () => {
const names: string[] = [];
let offset: number | null = 0;
while (offset !== null) {
const data = JSON.parse(
text(await callTool('list_actions', { limit: 37, offset }, docs)),
);
expect(data.total).toBe(ACTIONS.length);
expect(data.count).toBe(data.actions.length);
names.push(...data.actions.map((action: { name: string }) => action.name));
offset = data.next_offset;
}
expect(names).toEqual(ACTIONS.map((action) => action.name));
const beyond = JSON.parse(text(await callTool('list_actions', { offset: 9999 }, docs)));
expect(beyond).toEqual({ count: 0, actions: [], total: ACTIONS.length, next_offset: null });
});
it.each([
{ limit: 0 },
{ limit: 201 },
{ offset: -1 },
{ offset: 0.5 },
{ stream: 'true' },
{ read_only: 1 },
{ match: 'typo' },
])('rejects invalid new options (%j)', async (options) => {
const result = await callTool('search_actions', { query: '', ...options }, docs);
expect(result.isError).toBe(true);
expect(result.structuredContent.diagnostics.kind).toBe('invalid_arguments');
});
});
describe('runtime overview and caller cancellation', () => {
it('does not contact OneBot for a local overview, even if a client exists in docs mode', async () => {
const onebot = client();
const result = await callTool('get_runtime_info', {}, { ...docs, client: onebot });
expect(result.structuredContent.result).toMatchObject({
mode: 'docs',
execution_enabled: false,
write_enabled: false,
transport: null,
connection_checked: false,
catalog: { action_count: ACTIONS.length, server_support: 'unknown' },
});
expect(onebot.call).not.toHaveBeenCalled();
const denied = await callTool(
'get_runtime_info',
{ check_connection: true },
{ ...docs, client: onebot },
);
expect(denied.isError).toBe(true);
expect(onebot.call).not.toHaveBeenCalled();
});
it('only performs the three explicit read-only checks and preserves individual failures', async () => {
const onebot = client();
const controller = new AbortController();
onebot.call = vi.fn(async (action, _params, options) => {
expect(options?.signal).toBe(controller.signal);
return action === 'get_status'
? { status: 'failed', retcode: 1401, wording: 'unauthorized' }
: { status: 'ok', retcode: 0, data: { action } };
});
const result = await callTool(
'get_runtime_info',
{ check_connection: true },
{ mode: 'read', client: onebot, signal: controller.signal },
);
expect(vi.mocked(onebot.call).mock.calls.map(([action]) => action)).toEqual([
'get_login_info',
'get_status',
'get_version_info',
]);
expect(result.structuredContent.diagnostics.outcome).toBe('failed');
expect(result.structuredContent.result).toMatchObject({
checks: {
get_status: { result: { retcode: 1401 }, diagnostics: { outcome: 'failed' } },
},
});
});
it('forwards ordinary request cancellation and does not start pre-cancelled calls', async () => {
const controller = new AbortController();
const onebot = client();
await callTool(
'query_action',
{ action: 'get_status' },
{ mode: 'read', client: onebot, signal: controller.signal },
);
expect(onebot.call).toHaveBeenCalledWith('get_status', {}, { signal: controller.signal });
controller.abort();
const result = await callTool(
'query_action',
{ action: 'get_status' },
{ mode: 'read', client: onebot, signal: controller.signal },
);
expect(onebot.call).toHaveBeenCalledTimes(1);
expect(result.structuredContent.diagnostics.kind).toBe('cancelled');
});
});