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).
181 lines
5.4 KiB
TypeScript
181 lines
5.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { IOneBotNetworkAdapter, NetworkReloadType, OneBotNetworkManager } from '../src/network';
|
|
import type { NetworkAdapterContext } from '../src/network';
|
|
import type { JsonObject, NetworkBase } from '../src/types';
|
|
import type { DispatchPayload } from '../src/event-filter';
|
|
|
|
interface FakeNetworkConfig extends NetworkBase {
|
|
// Allows tests to influence behavior via config:
|
|
failOnEvent?: boolean;
|
|
}
|
|
|
|
class FakeAdapter extends IOneBotNetworkAdapter<FakeNetworkConfig> {
|
|
events: JsonObject[] = [];
|
|
opens = 0;
|
|
closes = 0;
|
|
reloads = 0;
|
|
// Last dispatch payload object identity, to assert it was reused per emit.
|
|
lastPayloads: DispatchPayload[] = [];
|
|
|
|
open(): void {
|
|
this.opens++;
|
|
if (this.config.enabled !== false) this.isEnabled = true;
|
|
}
|
|
close(): void {
|
|
this.closes++;
|
|
this.isEnabled = false;
|
|
}
|
|
reload(next: FakeNetworkConfig): NetworkReloadType {
|
|
this.reloads++;
|
|
this.config = structuredClone(next);
|
|
if (next.enabled === false && this.isEnabled) {
|
|
this.close();
|
|
return NetworkReloadType.Closed;
|
|
}
|
|
if (next.enabled !== false && !this.isEnabled) {
|
|
this.open();
|
|
return NetworkReloadType.Opened;
|
|
}
|
|
return NetworkReloadType.Normal;
|
|
}
|
|
onEvent(event: JsonObject, payload: DispatchPayload): void {
|
|
this.events.push(event);
|
|
this.lastPayloads.push(payload);
|
|
if (this.config.failOnEvent) throw new Error('boom');
|
|
}
|
|
}
|
|
|
|
const NULL_CTX: NetworkAdapterContext = {
|
|
uin: '10001',
|
|
api: { handle: async () => ({ status: 'failed', retcode: 0, data: null }), processRequest: async () => '' } as never,
|
|
buildLifecycleEvent: () => ({}),
|
|
buildHeartbeatEvent: () => ({}),
|
|
};
|
|
|
|
function makeAdapter(name: string, partial: Partial<FakeNetworkConfig> = {}): FakeAdapter {
|
|
const config: FakeNetworkConfig = {
|
|
name,
|
|
enabled: true,
|
|
messageFormat: 'array',
|
|
reportSelfMessage: false,
|
|
...partial,
|
|
};
|
|
return new FakeAdapter(name, config, NULL_CTX);
|
|
}
|
|
|
|
const SAMPLE_EVENT: JsonObject = {
|
|
time: 1,
|
|
self_id: 10001,
|
|
post_type: 'message',
|
|
message_type: 'private',
|
|
sub_type: 'friend',
|
|
message_id: 1,
|
|
user_id: 22222,
|
|
message: [{ type: 'text', data: { text: 'hi' } }],
|
|
raw_message: 'hi',
|
|
font: 0,
|
|
sender: { user_id: 22222, nickname: 'peer', sex: 'unknown', age: 0 },
|
|
};
|
|
|
|
describe('OneBotNetworkManager', () => {
|
|
it('opens and emits to every active adapter in parallel', async () => {
|
|
const mgr = new OneBotNetworkManager();
|
|
const a = makeAdapter('a');
|
|
const b = makeAdapter('b');
|
|
mgr.register(a);
|
|
mgr.register(b);
|
|
await mgr.openAll();
|
|
|
|
expect(a.opens).toBe(1);
|
|
expect(b.opens).toBe(1);
|
|
|
|
await mgr.emitEvent(SAMPLE_EVENT);
|
|
expect(a.events).toEqual([SAMPLE_EVENT]);
|
|
expect(b.events).toEqual([SAMPLE_EVENT]);
|
|
// Both adapters should receive the same payload reference (built once).
|
|
expect(a.lastPayloads[0]).toBe(b.lastPayloads[0]);
|
|
});
|
|
|
|
it('skips inactive adapters during emit', async () => {
|
|
const mgr = new OneBotNetworkManager();
|
|
const active = makeAdapter('active');
|
|
const idle = makeAdapter('idle', { enabled: false });
|
|
mgr.register(active);
|
|
mgr.register(idle);
|
|
await mgr.openAll();
|
|
|
|
await mgr.emitEvent(SAMPLE_EVENT);
|
|
expect(active.events).toHaveLength(1);
|
|
expect(idle.events).toHaveLength(0);
|
|
});
|
|
|
|
it('isolates adapter errors so one bad adapter does not block others', async () => {
|
|
const mgr = new OneBotNetworkManager();
|
|
const ok = makeAdapter('ok');
|
|
const bad = makeAdapter('bad', { failOnEvent: true });
|
|
mgr.register(ok);
|
|
mgr.register(bad);
|
|
await mgr.openAll();
|
|
|
|
await mgr.emitEvent(SAMPLE_EVENT);
|
|
// The good adapter still saw the event.
|
|
expect(ok.events).toHaveLength(1);
|
|
// The bad adapter logged the throw but didn't crash the manager.
|
|
expect(bad.events).toHaveLength(1);
|
|
});
|
|
|
|
it('replaces an existing adapter under the same name and closes the old one', async () => {
|
|
const mgr = new OneBotNetworkManager();
|
|
const first = makeAdapter('dup');
|
|
mgr.register(first);
|
|
await mgr.openAll();
|
|
|
|
const second = makeAdapter('dup');
|
|
mgr.register(second);
|
|
|
|
expect(mgr.get('dup')).toBe(second);
|
|
expect(first.closes).toBe(1);
|
|
});
|
|
|
|
it('closeOne shuts down a single adapter and removes it', async () => {
|
|
const mgr = new OneBotNetworkManager();
|
|
const a = makeAdapter('a');
|
|
const b = makeAdapter('b');
|
|
mgr.register(a);
|
|
mgr.register(b);
|
|
await mgr.openAll();
|
|
|
|
await mgr.closeOne('a');
|
|
expect(mgr.has('a')).toBe(false);
|
|
expect(a.closes).toBe(1);
|
|
expect(b.closes).toBe(0);
|
|
expect(mgr.hasActiveAdapters()).toBe(true);
|
|
});
|
|
|
|
it('closeAll clears every adapter', async () => {
|
|
const mgr = new OneBotNetworkManager();
|
|
const a = makeAdapter('a');
|
|
const b = makeAdapter('b');
|
|
mgr.register(a);
|
|
mgr.register(b);
|
|
await mgr.openAll();
|
|
|
|
await mgr.closeAll();
|
|
expect(mgr.list()).toHaveLength(0);
|
|
expect(a.closes).toBe(1);
|
|
expect(b.closes).toBe(1);
|
|
});
|
|
|
|
it('hasActiveAdapters reflects current state', async () => {
|
|
const mgr = new OneBotNetworkManager();
|
|
expect(mgr.hasActiveAdapters()).toBe(false);
|
|
const a = makeAdapter('a');
|
|
mgr.register(a);
|
|
expect(mgr.hasActiveAdapters()).toBe(false);
|
|
await mgr.openAll();
|
|
expect(mgr.hasActiveAdapters()).toBe(true);
|
|
await mgr.closeAll();
|
|
expect(mgr.hasActiveAdapters()).toBe(false);
|
|
});
|
|
});
|