Phase 3 of the 4-package split (RoadMap #3). Moves the entire `packages/core/src/onebot/` subtree (43 source files + 20 test files spanning network adapters, action handlers, event converters, message parser, message store, media store, instance + manager) into the new `@snowluma/onebot` workspace package. The remaining `@snowluma/core` now hosts only the api-layer (Bridge class + 13 Api classes), the hook tooling, the WebUI server, and the runnable `index.ts`. Why --- - The OneBot adapter is the public-facing OneBot11 protocol surface; it has its own version cadence and its own consumers (third-party bots talking OneBot WS / HTTP). Splitting it lets that cadence move independently of the Api-on-ctx layer. - The two were sharing the `packages/core/` build pipeline only because they happened to live in the same directory. Nothing in the api-layer reaches into OneBot internals. - Mirrors NapCat / Lagrange.OneBot's repo split (their OneBot adapter is a separate assembly from the Core protocol library). Layout ------ Moved via `git mv` (history preserved): packages/core/src/onebot/* → @snowluma/onebot/src/<x> packages/core/tests/onebot/* (6 files) → @snowluma/onebot/tests/ packages/core/tests/{message-parser,message-store,event-pipeline, media-store,media-url-resolver,event-converter, http-server-hot-reload,http-server-content-type, api-handler-logging,message-id,group-msg-emoji-like, config,media-indexer,network-manager}.test.ts (14 files) → @snowluma/onebot/tests/ What stays in @snowluma/core ---------------------------- - `bridge/{bridge,bridge-interface,bridge-context,manager}.ts` - `bridge/apis/*` (the 13 typed Api classes) - `hook/*` (debugger/injector — tooling for the core protocol layer) - `webui/*` (admin UI server — application wiring, not the OneBot adapter itself) - `index.ts` (application entry that wires BridgeManager + OneBotManager + HookManager + WebUI together) Changes ------- - New package `packages/onebot/` (package.json with subpath exports mirroring legacy `core/src/onebot/<x>` 1:1, tsconfig.json, vitest.config.ts with `SNOWLUMA_LOG_LEVEL=debug` env so the action- dispatch logging tests see debug-level subscribers — copied verbatim from core's vitest.config.ts). - @snowluma/core grows new subpath exports for the api-layer files OneBot's type-only imports need: `./bridge`, `./bridge-interface`, `./bridge-context`, `./manager`, `./apis`. - `@snowluma/onebot` depends on `@snowluma/bridge`, `@snowluma/common`, `@snowluma/core`, `@snowluma/proto-defs`, `@snowluma/proton`, `@snowluma/websocket`. - `@snowluma/core` depends on `@snowluma/onebot` (for index.ts + webui/server.ts wiring). - 100+ import sites rewritten via sed: * OneBot → core: `'../bridge/bridge'` → `'@snowluma/core/bridge'`, `'../bridge/manager'` → `'@snowluma/core/manager'`, `'../bridge/bridge-interface'` → `'@snowluma/core/bridge-interface'`, `'@/bridge/bridge'` (vite path alias) → `'@snowluma/core/bridge'`. * Core → OneBot: `'./onebot/manager'` → `'@snowluma/onebot/manager'`, `'../onebot/config'` → `'@snowluma/onebot/config'`, etc. * Test relative paths: `'../../src/onebot/<x>'` → `'../src/<x>'` after the test file move (one level shallower). - New `packages/onebot/src/env.d.ts` declaring `__APP_VERSION__` and `__BUILD_WEBUI__` (the vite-injected globals `info.ts` reads). Workspace cycle --------------- core ↔ onebot is allowed by pnpm workspaces. The runtime direction is one-way: core → onebot (index.ts constructs OneBotManager, webui/server reads OneBot config). OneBot only reaches into core for TYPES (Bridge, BridgeInterface, BridgeManager, ClientKeyInfo, DownloadRKeyInfo); those imports use `import type` so they compile out at runtime and don't introduce a real cycle. Verification ------------ - Full workspace `pnpm -r typecheck`: all 11 projects clean. - `@snowluma/core` test suite: 302/302 pass (33 test files). - `@snowluma/onebot` test suite: 250/250 pass (20 test files; pre-existing `network-manager.test.ts` native-addon failure unchanged — same baseline as before the split). - Combined: 552/552 — identical pass count to the pre-Phase-3 baseline. - Vite build with stubbed native binary: clean, 2.84 MB bundle / 224 KB gzip (identical envelope).
131 lines
4.8 KiB
TypeScript
131 lines
4.8 KiB
TypeScript
// Tests for ApiHandler's central dispatch logging.
|
|
// Verifies that every action call emits a debug entry line, failures
|
|
// fold into a warn line with stack, and unknown actions are surfaced.
|
|
// Also covers the summarizeParams helper used to keep the entry log
|
|
// from blowing the line width.
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { ApiHandler } from '../src/api-handler';
|
|
import { subscribeLogs, type LogEntry } from '@snowluma/common/logger';
|
|
import { summarizeParams } from '@snowluma/common/log-summary';
|
|
|
|
// Minimal context — we never reach the real action handlers; we
|
|
// register our own via registerAction.
|
|
function emptyContext(): any {
|
|
return {
|
|
bridge: {},
|
|
getLoginInfo: () => ({ userId: 0, nickname: '' }),
|
|
isOnline: () => true,
|
|
getMessage: () => null,
|
|
getMessageMeta: () => null,
|
|
sendPrivateMessage: async () => ({ messageId: 0 }),
|
|
sendGroupMessage: async () => ({ messageId: 0 }),
|
|
deleteMessage: async () => {},
|
|
canSendImage: () => true,
|
|
canSendRecord: () => true,
|
|
getFriendList: async () => [],
|
|
getImageInfo: async () => null,
|
|
getRecordInfo: async () => null,
|
|
};
|
|
}
|
|
|
|
let captured: LogEntry[];
|
|
let unsub: () => void;
|
|
const savedLogLevel = process.env.SNOWLUMA_LOG_LEVEL;
|
|
|
|
beforeEach(() => {
|
|
process.env.SNOWLUMA_LOG_LEVEL = 'debug';
|
|
captured = [];
|
|
unsub = subscribeLogs((e) => captured.push(e));
|
|
});
|
|
|
|
afterEach(() => {
|
|
unsub();
|
|
if (savedLogLevel === undefined) delete process.env.SNOWLUMA_LOG_LEVEL;
|
|
else process.env.SNOWLUMA_LOG_LEVEL = savedLogLevel;
|
|
});
|
|
|
|
describe('summarizeParams', () => {
|
|
it('renders flat scalar fields as k=v pairs', () => {
|
|
const out = summarizeParams({ group_id: 12345, auto_escape: false, foo: 'bar' });
|
|
expect(out).toBe('group_id=12345 auto_escape=false foo="bar"');
|
|
});
|
|
|
|
it('truncates long string values with an ellipsis', () => {
|
|
const long = 'x'.repeat(80);
|
|
const out = summarizeParams({ s: long });
|
|
expect(out).toMatch(/^s="x{40}\.\.\."$/);
|
|
});
|
|
|
|
it('collapses arrays and nested objects rather than dumping them', () => {
|
|
const out = summarizeParams({ messages: [1, 2, 3, 4], meta: { a: 1, b: 2 } });
|
|
expect(out).toBe('messages=[len=4] meta={...}');
|
|
});
|
|
|
|
it('caps the total line and tags the cut with ellipsis', () => {
|
|
const params: Record<string, number> = {};
|
|
for (let i = 0; i < 30; i++) params[`field_with_some_padding_${i}`] = i;
|
|
const out = summarizeParams(params);
|
|
expect(out.length).toBeLessThan(220);
|
|
expect(out.endsWith('...')).toBe(true);
|
|
});
|
|
|
|
it('handles null / undefined inputs gracefully', () => {
|
|
expect(summarizeParams(null)).toBe('{}');
|
|
expect(summarizeParams(undefined)).toBe('{}');
|
|
});
|
|
});
|
|
|
|
describe('ApiHandler dispatch logging', () => {
|
|
it('emits a debug entry under [Bridge.Action] when an action is called', async () => {
|
|
const handler = new ApiHandler(emptyContext(), 12345);
|
|
handler.registerAction('echo', async () => ({ status: 'ok', retcode: 0, data: null }));
|
|
|
|
await handler.handle('echo', { group_id: 67890, message: [1, 2, 3] });
|
|
|
|
const entry = captured.find((e) => e.scope === 'Bridge.Action' && e.level === 'debug');
|
|
expect(entry).toBeDefined();
|
|
expect(entry!.uin).toBe(12345);
|
|
expect(entry!.message).toContain('echo');
|
|
expect(entry!.message).toContain('group_id=67890');
|
|
expect(entry!.message).toContain('message=[len=3]');
|
|
});
|
|
|
|
it('emits a warn line with the error stack when the handler throws', async () => {
|
|
const handler = new ApiHandler(emptyContext(), 12345);
|
|
handler.registerAction('boom', async () => {
|
|
throw new Error('kapow');
|
|
});
|
|
|
|
const result = await handler.handle('boom', {});
|
|
expect(result.status).toBe('failed');
|
|
|
|
const warn = captured.find((e) => e.scope === 'Bridge.Action' && e.level === 'warn');
|
|
expect(warn).toBeDefined();
|
|
expect(warn!.message).toContain('boom');
|
|
expect(warn!.message).toContain('kapow');
|
|
// stack contains the test file path; just check that something stack-shaped is appended
|
|
expect(warn!.message).toMatch(/at\s+/);
|
|
});
|
|
|
|
it('logs unknown actions at debug level', async () => {
|
|
const handler = new ApiHandler(emptyContext(), 99);
|
|
await handler.handle('not_a_real_action', {});
|
|
|
|
const entry = captured.find((e) => e.scope === 'Bridge.Action' && e.message.includes('unknown action'));
|
|
expect(entry).toBeDefined();
|
|
expect(entry!.level).toBe('debug');
|
|
});
|
|
|
|
it('falls back to the module-level logger (no uin slot) when uin is omitted', async () => {
|
|
const handler = new ApiHandler(emptyContext());
|
|
handler.registerAction('ping', async () => ({ status: 'ok', retcode: 0, data: null }));
|
|
|
|
await handler.handle('ping', {});
|
|
|
|
const entry = captured.find((e) => e.scope === 'Bridge.Action' && e.level === 'debug');
|
|
expect(entry).toBeDefined();
|
|
expect(entry!.uin).toBeUndefined();
|
|
});
|
|
});
|