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
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:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,290 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { defineAction, defineStreamAction, type RegisteredActionSpec } from '../src/action-kit';
|
||||
import { ApiHandler, type ApiActionContext } from '../src/api-handler';
|
||||
import {
|
||||
ACTION_REGISTRY,
|
||||
HANDLE_QUICK_OPERATION_ACTION,
|
||||
HANDLE_QUICK_OPERATION_ASYNC_ACTION,
|
||||
RAW_ACTION_RESERVATIONS,
|
||||
compileActionRegistry,
|
||||
type ActionGroup,
|
||||
type RawActionReservation,
|
||||
} from '../src/actions';
|
||||
import { collectActionDocs } from '../src/action-docs';
|
||||
import { okResponse } from '../src/types';
|
||||
|
||||
type ActionNames = string | readonly [string, ...string[]];
|
||||
|
||||
function normal(name: ActionNames): RegisteredActionSpec {
|
||||
return defineAction({ name, params: {}, run: () => okResponse() });
|
||||
}
|
||||
|
||||
function stream(name: ActionNames): RegisteredActionSpec {
|
||||
return defineStreamAction({ name, params: {}, run: () => okResponse() });
|
||||
}
|
||||
|
||||
function groups(...actions: RegisteredActionSpec[]): readonly ActionGroup[] {
|
||||
return [{ category: 'test', actions }];
|
||||
}
|
||||
|
||||
function conflict(
|
||||
actionGroups: readonly ActionGroup[],
|
||||
raw: readonly RawActionReservation[] = [],
|
||||
): string {
|
||||
try {
|
||||
compileActionRegistry(actionGroups, raw);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new Error('expected registry compilation to fail');
|
||||
}
|
||||
|
||||
describe('compileActionRegistry namespace conflicts', () => {
|
||||
it('rejects canonical ↔ canonical', () => {
|
||||
const message = conflict(groups(normal('shared'), normal('shared')));
|
||||
expect(message).toContain('executable name "shared"');
|
||||
expect(message).toContain(
|
||||
'canonical "shared" (name "shared", kind normal, role canonical)',
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['canonical first', [normal('shared'), normal(['owner', 'shared'])]],
|
||||
['alias first', [normal(['owner', 'shared']), normal('shared')]],
|
||||
] as const)('rejects canonical ↔ alias (%s)', (_label, specs) => {
|
||||
const message = conflict(groups(...specs));
|
||||
expect(message).toContain(
|
||||
'canonical "shared" (name "shared", kind normal, role canonical)',
|
||||
);
|
||||
expect(message).toContain('canonical "owner" (name "shared", kind normal, role alias)');
|
||||
});
|
||||
|
||||
it('rejects alias ↔ alias', () => {
|
||||
const message = conflict(groups(normal(['first', 'shared']), normal(['second', 'shared'])));
|
||||
expect(message).toContain('canonical "first" (name "shared", kind normal, role alias)');
|
||||
expect(message).toContain('canonical "second" (name "shared", kind normal, role alias)');
|
||||
});
|
||||
|
||||
it('rejects normal ↔ stream and reports both kinds', () => {
|
||||
const message = conflict(groups(normal('shared'), stream('shared')));
|
||||
expect(message).toContain(
|
||||
'canonical "shared" (name "shared", kind normal, role canonical)',
|
||||
);
|
||||
expect(message).toContain(
|
||||
'canonical "shared" (name "shared", kind stream, role canonical)',
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['canonical', normal(HANDLE_QUICK_OPERATION_ACTION), 'role canonical'],
|
||||
['alias', normal(['owner', HANDLE_QUICK_OPERATION_ACTION]), 'role alias'],
|
||||
] as const)(
|
||||
'rejects a declarative %s colliding with a reserved raw action',
|
||||
(_label, spec, role) => {
|
||||
const message = conflict(groups(spec), [
|
||||
{ name: HANDLE_QUICK_OPERATION_ACTION, canonical: HANDLE_QUICK_OPERATION_ACTION },
|
||||
]);
|
||||
expect(message).toContain(`name "${HANDLE_QUICK_OPERATION_ACTION}"`);
|
||||
expect(message).toContain(`kind normal, ${role}`);
|
||||
expect(message).toContain('kind raw, role raw');
|
||||
},
|
||||
);
|
||||
|
||||
it('reserves the async quick-operation alias against declarative names', () => {
|
||||
const message = conflict(
|
||||
groups(normal(HANDLE_QUICK_OPERATION_ASYNC_ACTION)),
|
||||
RAW_ACTION_RESERVATIONS,
|
||||
);
|
||||
expect(message).toContain(`name "${HANDLE_QUICK_OPERATION_ASYNC_ACTION}"`);
|
||||
expect(message).toContain('kind raw, role raw');
|
||||
});
|
||||
|
||||
it('rejects duplicate raw reservations', () => {
|
||||
const message = conflict(
|
||||
[],
|
||||
[
|
||||
{ name: 'raw', canonical: 'first-raw' },
|
||||
{ name: 'raw', canonical: 'second-raw' },
|
||||
],
|
||||
);
|
||||
expect(message).toContain('canonical "first-raw" (name "raw", kind raw, role raw)');
|
||||
expect(message).toContain('canonical "second-raw" (name "raw", kind raw, role raw)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('compiled production Action registry', () => {
|
||||
it('publishes a doc for every compiled action', () => {
|
||||
const registryNames = ACTION_REGISTRY.actions.map((action) => action.doc.name).sort();
|
||||
const docNames = collectActionDocs()
|
||||
.map((doc) => doc.name)
|
||||
.sort();
|
||||
expect(docNames).toEqual(registryNames);
|
||||
expect(docNames.length).toBeGreaterThan(100);
|
||||
});
|
||||
|
||||
it('resolves every executable name to exactly one canonical doc', () => {
|
||||
const docs = new Map(collectActionDocs().map((doc) => [doc.name, doc]));
|
||||
|
||||
for (const executable of ACTION_REGISTRY.executableNames) {
|
||||
expect(ACTION_REGISTRY.resolve(executable.name)).toBe(executable);
|
||||
|
||||
if (executable.kind === 'raw') {
|
||||
expect(executable.canonical).toBe(HANDLE_QUICK_OPERATION_ACTION);
|
||||
expect([
|
||||
HANDLE_QUICK_OPERATION_ACTION,
|
||||
HANDLE_QUICK_OPERATION_ASYNC_ACTION,
|
||||
]).toContain(executable.name);
|
||||
expect(docs.has(executable.canonical)).toBe(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
const doc = docs.get(executable.canonical);
|
||||
expect(doc, executable.name).toBeDefined();
|
||||
expect(executable.action.doc).toEqual(doc);
|
||||
expect(executable.action.kind).toBe(doc!.stream === true ? 'stream' : 'normal');
|
||||
expect([doc!.name, ...doc!.aliases]).toContain(executable.name);
|
||||
}
|
||||
});
|
||||
|
||||
it('binds the exact compiled executable-name/kind projection', () => {
|
||||
const api = { handle: vi.fn() } as unknown as ApiHandler;
|
||||
const bound = ACTION_REGISTRY.bind({} as ApiActionContext, api, {
|
||||
[HANDLE_QUICK_OPERATION_ACTION]: () => async () => okResponse(),
|
||||
});
|
||||
|
||||
expect([...bound.entries()].map(([name, entry]) => [name, entry.kind])).toEqual(
|
||||
ACTION_REGISTRY.executableNames.map((entry) => [entry.name, entry.kind]),
|
||||
);
|
||||
|
||||
for (const executable of ACTION_REGISTRY.executableNames) {
|
||||
expect(bound.get(executable.name)?.canonical).toBe(executable.canonical);
|
||||
}
|
||||
});
|
||||
|
||||
it('resolves the async quick-operation alias to the reserved raw handler', () => {
|
||||
const syncName = ACTION_REGISTRY.resolve(HANDLE_QUICK_OPERATION_ACTION);
|
||||
const asyncName = ACTION_REGISTRY.resolve(HANDLE_QUICK_OPERATION_ASYNC_ACTION);
|
||||
expect(syncName).toMatchObject({
|
||||
name: HANDLE_QUICK_OPERATION_ACTION,
|
||||
canonical: HANDLE_QUICK_OPERATION_ACTION,
|
||||
kind: 'raw',
|
||||
role: 'raw',
|
||||
});
|
||||
expect(asyncName).toMatchObject({
|
||||
name: HANDLE_QUICK_OPERATION_ASYNC_ACTION,
|
||||
canonical: HANDLE_QUICK_OPERATION_ACTION,
|
||||
kind: 'raw',
|
||||
role: 'raw',
|
||||
});
|
||||
});
|
||||
|
||||
it.each([HANDLE_QUICK_OPERATION_ACTION, HANDLE_QUICK_OPERATION_ASYNC_ACTION])(
|
||||
'dispatches %s to set_group_add_request',
|
||||
async (action) => {
|
||||
const handleGroupRequest = vi.fn(async () => undefined);
|
||||
const handler = new ApiHandler({ handleGroupRequest } as unknown as ApiActionContext);
|
||||
|
||||
const response = await handler.handle(action, {
|
||||
context: {
|
||||
post_type: 'request',
|
||||
request_type: 'group',
|
||||
sub_type: 'invite',
|
||||
group_id: 100000002,
|
||||
user_id: 10001,
|
||||
flag: 'invite-flag',
|
||||
},
|
||||
operation: { approve: true, remark: '' },
|
||||
});
|
||||
|
||||
expect(response.status).toBe('ok');
|
||||
expect(handleGroupRequest).toHaveBeenCalledWith('invite-flag', 'invite', true, '');
|
||||
},
|
||||
);
|
||||
|
||||
it('validates and forwards get_group_system_msg filters', async () => {
|
||||
const handleGetGroupSystemMsg = vi.fn(async () => []);
|
||||
const handler = new ApiHandler({ handleGetGroupSystemMsg } as unknown as ApiActionContext);
|
||||
|
||||
const response = await handler.handle('get_group_system_msg', {
|
||||
group_id: '100000002',
|
||||
only_pending: true,
|
||||
count: '80',
|
||||
});
|
||||
|
||||
expect(response.status).toBe('ok');
|
||||
expect(handleGetGroupSystemMsg).toHaveBeenCalledWith({
|
||||
groupId: 100000002,
|
||||
onlyPending: true,
|
||||
count: 80,
|
||||
});
|
||||
const doc = ACTION_REGISTRY.resolve('get_group_system_msg');
|
||||
|
||||
if (!doc || doc.kind === 'raw') {
|
||||
throw new Error('get_group_system_msg action missing');
|
||||
}
|
||||
|
||||
expect(doc.action.doc.params.map((param) => param.name)).toEqual([
|
||||
'group_id',
|
||||
'only_pending',
|
||||
'count',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ActionRegistry.bind', () => {
|
||||
function testRegistry(...specs: RegisteredActionSpec[]) {
|
||||
return compileActionRegistry(groups(...specs), RAW_ACTION_RESERVATIONS);
|
||||
}
|
||||
|
||||
it('binds only names declared by the injected complete registry', async () => {
|
||||
const api = new ApiHandler(
|
||||
{} as ApiActionContext,
|
||||
undefined,
|
||||
testRegistry(normal('__registry_normal__')),
|
||||
);
|
||||
expect((await api.handle('__registry_normal__', {})).status).toBe('ok');
|
||||
expect(api.isStreamAction('__registry_normal__')).toBe(false);
|
||||
expect((await api.handle('__rogue__', {})).retcode).toBe(1404);
|
||||
});
|
||||
|
||||
it('keeps stream classification bound to the sealed compiled claim', () => {
|
||||
const api = new ApiHandler(
|
||||
{} as ApiActionContext,
|
||||
undefined,
|
||||
testRegistry(stream('__registry_stream__')),
|
||||
);
|
||||
expect(api.isStreamAction('__registry_stream__')).toBe(true);
|
||||
expect(api.isStreamAction('__registry_normal__')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a missing raw factory for a reserved name', () => {
|
||||
const registry = testRegistry(normal('declared'));
|
||||
expect(() => registry.bind({} as ApiActionContext, {} as ApiHandler, {})).toThrow(
|
||||
/raw factory missing.*handle_quick_operation.*kind raw/,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects an unexpected raw factory', () => {
|
||||
const registry = compileActionRegistry(groups(normal('declared')));
|
||||
expect(() =>
|
||||
registry.bind({} as ApiActionContext, {} as ApiHandler, {
|
||||
extra: () => async () => okResponse(),
|
||||
}),
|
||||
).toThrow(/unexpected raw factory.*extra.*kind raw/);
|
||||
});
|
||||
|
||||
it('constructs an injected registry that has no raw reservation', async () => {
|
||||
const api = new ApiHandler(
|
||||
{} as ApiActionContext,
|
||||
undefined,
|
||||
compileActionRegistry(groups(normal('declared'))),
|
||||
);
|
||||
expect((await api.handle('declared', {})).status).toBe('ok');
|
||||
expect((await api.handle(HANDLE_QUICK_OPERATION_ACTION, {})).retcode).toBe(1404);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user