@republicroad/zen-udf 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +13 -0
- package/README.md +80 -0
- package/docs/naming.md +40 -0
- package/package.json +24 -0
- package/src/context-hardening.test.ts +93 -0
- package/src/contrib/crypto.test.ts +60 -0
- package/src/contrib/crypto.ts +74 -0
- package/src/contrib/custom-list-query.ts +51 -0
- package/src/contrib/debug.ts +42 -0
- package/src/contrib/debugui.test.ts +10 -0
- package/src/contrib/debugui.ts +27 -0
- package/src/contrib/http-guard.test.ts +116 -0
- package/src/contrib/http.test.ts +230 -0
- package/src/contrib/http.ts +283 -0
- package/src/contrib/ip-location.ts +82 -0
- package/src/contrib/legacy-graph-acceptance.test.ts +67 -0
- package/src/contrib/rate-store-conformance.ts +68 -0
- package/src/contrib/rate-store.test.ts +30 -0
- package/src/contrib/rate-window.ts +195 -0
- package/src/contrib/rebuilt-functions.test.ts +60 -0
- package/src/contrib/roster.test.ts +27 -0
- package/src/contrib/roster.ts +38 -0
- package/src/decision-cache.test.ts +122 -0
- package/src/decision-cache.ts +104 -0
- package/src/decision-runtime.test.ts +154 -0
- package/src/engine-cache-semantics.test.ts +87 -0
- package/src/engine.ts +474 -0
- package/src/exec-context.test.ts +52 -0
- package/src/exec-context.ts +32 -0
- package/src/execution-spec.test.ts +173 -0
- package/src/index.ts +43 -0
- package/src/limiter.ts +70 -0
- package/src/reference.ts +34 -0
- package/src/register.test.ts +60 -0
- package/src/register.ts +515 -0
- package/src/roster.test.ts +96 -0
- package/src/roster.ts +129 -0
- package/src/sanitize.test.ts +65 -0
- package/src/udf-pack.test.ts +107 -0
- package/src/udf-trace.test.ts +137 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { DecisionRuntime } from './engine.ts';
|
|
4
|
+
import { runWithExecContext } from './exec-context.ts';
|
|
5
|
+
import { InMemoryConcurrencyLimiter, NoopConcurrencyLimiter } from './limiter.ts';
|
|
6
|
+
import { UdfRegistry } from './register.ts';
|
|
7
|
+
|
|
8
|
+
/** 带 customNode 的图:expr 调用 target_udf,结果写 out 字段 */
|
|
9
|
+
const graph = (id: string, expr = 'target_udf;;p1;;p2;;p3') => ({
|
|
10
|
+
id,
|
|
11
|
+
nodes: [
|
|
12
|
+
{ id: 'in', type: 'inputNode', name: 'Request' },
|
|
13
|
+
{
|
|
14
|
+
id: 'c1',
|
|
15
|
+
type: 'customNode',
|
|
16
|
+
name: 'custom',
|
|
17
|
+
content: {
|
|
18
|
+
kind: 'UDF',
|
|
19
|
+
config: { expressions: [{ id: 'e1', key: 'out', value: expr }] },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
{ id: 'out', type: 'outputNode', name: 'Response' },
|
|
23
|
+
],
|
|
24
|
+
edges: [
|
|
25
|
+
{ id: 'ed1', sourceId: 'in', targetId: 'c1', type: 'edge' },
|
|
26
|
+
{ id: 'ed2', sourceId: 'c1', targetId: 'out', type: 'edge' },
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const makeRegistry = (fn: (kwargs: Record<string, unknown>) => unknown): UdfRegistry => {
|
|
31
|
+
const registry = new UdfRegistry();
|
|
32
|
+
registry.registerFunction(
|
|
33
|
+
fn,
|
|
34
|
+
'target',
|
|
35
|
+
{
|
|
36
|
+
description: 'target udf',
|
|
37
|
+
parametersSchema: {
|
|
38
|
+
properties: {
|
|
39
|
+
p1: { type: 'string', title: 'P1' },
|
|
40
|
+
p2: { type: 'integer', title: 'P2' },
|
|
41
|
+
p3: { type: 'string', title: 'P3', default: '' },
|
|
42
|
+
timeout: { type: 'integer', title: 'Timeout', default: 0 },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
'target_udf',
|
|
47
|
+
);
|
|
48
|
+
return registry;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const evalGraph = async (runtime: DecisionRuntime, input: unknown) =>
|
|
52
|
+
runWithExecContext({ tenantId: 't-1' }, () => runtime.evaluateAsync('k', input));
|
|
53
|
+
|
|
54
|
+
describe('执行规范 §6.1 参数校验', () => {
|
|
55
|
+
test('必填位置参数缺失 → INVALID_PARAM 结构化错误', async () => {
|
|
56
|
+
const registry = makeRegistry((kwargs) => ({ echo: kwargs?.p1 ?? null }));
|
|
57
|
+
const runtime = new DecisionRuntime({ registry });
|
|
58
|
+
await runWithExecContext({ tenantId: 't-1' }, () => {
|
|
59
|
+
runtime.createDecisionWithCacheKey('k', graph('g'));
|
|
60
|
+
return Promise.resolve();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// p2 为必填(无默认值),表达式传 null
|
|
64
|
+
const result = await evalGraph(runtime, { p1: 'a', p2: null, p3: 'c' });
|
|
65
|
+
const out = (result.result as { out?: { error?: { code?: string; issues?: string[] } } }).out;
|
|
66
|
+
expect(out?.error?.code).toBe('INVALID_PARAM');
|
|
67
|
+
expect(JSON.stringify(out?.error?.issues)).toContain('p2 is required');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('有默认值的参数缺失不报错(funcBindParams 回退)', async () => {
|
|
71
|
+
const registry = makeRegistry((kwargs) => ({ got: kwargs?.p1 ?? null, timeout: kwargs?.timeout ?? null }));
|
|
72
|
+
const runtime = new DecisionRuntime({ registry });
|
|
73
|
+
await runWithExecContext({ tenantId: 't-1' }, () => {
|
|
74
|
+
runtime.createDecisionWithCacheKey('k', graph('g'));
|
|
75
|
+
return Promise.resolve();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const result = await evalGraph(runtime, { p1: 'a', p2: 2, p3: 'c' });
|
|
79
|
+
const out = (result.result as { out?: { got?: string } }).out;
|
|
80
|
+
expect(out?.got).toBe('a');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('执行规范 §6.2 超时约定', () => {
|
|
85
|
+
test('kwargs.timeout 超时返回 UDF_TIMEOUT 结构化错误', async () => {
|
|
86
|
+
const registry = makeRegistry(async (kwargs) => {
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
88
|
+
return { done: true, input: kwargs?.p1 ?? null };
|
|
89
|
+
});
|
|
90
|
+
// timeout 声明为 schema 最后一个参数(默认 0 = 不限时),表达式第 4 位传入
|
|
91
|
+
const runtime = new DecisionRuntime({ registry });
|
|
92
|
+
await runWithExecContext({ tenantId: 't-1' }, () => {
|
|
93
|
+
runtime.createDecisionWithCacheKey('k', graph('g-timeout', 'target_udf;;p1;;p2;;p3;;timeout'));
|
|
94
|
+
return Promise.resolve();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const result = await evalGraph(runtime, { p1: 'a', p2: 1, p3: 'b', timeout: 80 });
|
|
98
|
+
const out = (result.result as { out?: { error?: { code?: string; message?: string } } }).out;
|
|
99
|
+
expect(out?.error?.code).toBe('UDF_TIMEOUT');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('未超时则正常返回', async () => {
|
|
103
|
+
const registry = makeRegistry(async (kwargs) => {
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
105
|
+
return { done: true, input: kwargs?.p1 ?? null };
|
|
106
|
+
});
|
|
107
|
+
const runtime = new DecisionRuntime({ registry });
|
|
108
|
+
await runWithExecContext({ tenantId: 't-1' }, () => {
|
|
109
|
+
runtime.createDecisionWithCacheKey('k', graph('g-ok', 'target_udf;;p1;;p2;;p3;;timeout'));
|
|
110
|
+
return Promise.resolve();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const result = await evalGraph(runtime, { p1: 'ok', p2: 1, p3: 'b', timeout: 1000 });
|
|
114
|
+
const out = (result.result as { out?: { done?: boolean; input?: string } }).out;
|
|
115
|
+
expect(out?.done).toBe(true);
|
|
116
|
+
expect(out?.input).toBe('ok');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('执行规范 §6.3 并发闸', () => {
|
|
121
|
+
test('InMemoryConcurrencyLimiter 串行化同租户调用', async () => {
|
|
122
|
+
let active = 0;
|
|
123
|
+
let peak = 0;
|
|
124
|
+
const registry = new UdfRegistry();
|
|
125
|
+
registry.registerFunction(async function target_udf() {
|
|
126
|
+
active += 1;
|
|
127
|
+
peak = Math.max(peak, active);
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
129
|
+
active -= 1;
|
|
130
|
+
return { ok: true };
|
|
131
|
+
}, 'target');
|
|
132
|
+
|
|
133
|
+
const limiter = new InMemoryConcurrencyLimiter(1);
|
|
134
|
+
const runtime = new DecisionRuntime({ registry, limiter });
|
|
135
|
+
await runWithExecContext({ tenantId: 't-1' }, () => {
|
|
136
|
+
runtime.createDecisionWithCacheKey('k', graph('g'));
|
|
137
|
+
return Promise.resolve();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const [a, b] = await Promise.all([
|
|
141
|
+
runWithExecContext({ tenantId: 't-1' }, () => runtime.evaluateAsync('k', { p1: 'a', p2: 1, p3: 'c' })),
|
|
142
|
+
runWithExecContext({ tenantId: 't-1' }, () => runtime.evaluateAsync('k', { p1: 'b', p2: 2, p3: 'c' })),
|
|
143
|
+
]);
|
|
144
|
+
expect(peak).toBe(1);
|
|
145
|
+
expect((a.result as { out?: { ok?: boolean } }).out?.ok).toBe(true);
|
|
146
|
+
expect((b.result as { out?: { ok?: boolean } }).out?.ok).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('NoopConcurrencyLimiter 不限制并发', async () => {
|
|
150
|
+
let active = 0;
|
|
151
|
+
let peak = 0;
|
|
152
|
+
const registry = new UdfRegistry();
|
|
153
|
+
registry.registerFunction(async function target_udf() {
|
|
154
|
+
active += 1;
|
|
155
|
+
peak = Math.max(peak, active);
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
157
|
+
active -= 1;
|
|
158
|
+
return { ok: true };
|
|
159
|
+
}, 'target');
|
|
160
|
+
|
|
161
|
+
const runtime = new DecisionRuntime({ registry, limiter: new NoopConcurrencyLimiter() });
|
|
162
|
+
await runWithExecContext({ tenantId: 't-1' }, () => {
|
|
163
|
+
runtime.createDecisionWithCacheKey('k', graph('g'));
|
|
164
|
+
return Promise.resolve();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await Promise.all([
|
|
168
|
+
runWithExecContext({ tenantId: 't-1' }, () => runtime.evaluateAsync('k', { p1: 'a', p2: 1, p3: 'c' })),
|
|
169
|
+
runWithExecContext({ tenantId: 't-1' }, () => runtime.evaluateAsync('k', { p1: 'b', p2: 2, p3: 'c' })),
|
|
170
|
+
]);
|
|
171
|
+
expect(peak).toBe(2);
|
|
172
|
+
});
|
|
173
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// 参考函数域装载:import 本包根即向 globalUdfRegistry 注册 contrib 参考域
|
|
2
|
+
// (与 0.1.x 行为一致)。需要空注册表(实例隔离)时传 `new UdfRegistry()`,
|
|
3
|
+
// 并按需用 loadReferenceInto 装载参考域。
|
|
4
|
+
import './reference.ts';
|
|
5
|
+
|
|
6
|
+
export { DecisionRuntime, type DecisionRuntimeOptions, type UdfTrace } from './engine.ts';
|
|
7
|
+
export {
|
|
8
|
+
DecisionCache,
|
|
9
|
+
type CacheMetricsSnapshot,
|
|
10
|
+
type DecisionCacheEntry,
|
|
11
|
+
type DecisionCacheOptions,
|
|
12
|
+
} from './decision-cache.ts';
|
|
13
|
+
export { getExecContext, runWithExecContext, type ExecContext } from './exec-context.ts';
|
|
14
|
+
export { InMemoryConcurrencyLimiter, NoopConcurrencyLimiter, type ConcurrencyLimiter } from './limiter.ts';
|
|
15
|
+
export { registerRoster, listRosters, getRoster, deleteRoster, queryRoster, type Roster } from './roster.ts';
|
|
16
|
+
export {
|
|
17
|
+
UdfRegistry,
|
|
18
|
+
globalUdfRegistry,
|
|
19
|
+
registerUdf,
|
|
20
|
+
createExtRegister,
|
|
21
|
+
createUdfRegistry,
|
|
22
|
+
validatePack,
|
|
23
|
+
type UdfPack,
|
|
24
|
+
type CreateUdfRegistryOptions,
|
|
25
|
+
type ContribToolDef,
|
|
26
|
+
type ContribDef,
|
|
27
|
+
type UdfSchema,
|
|
28
|
+
type UdfSchemaParameter,
|
|
29
|
+
type JsonSchema,
|
|
30
|
+
type JsonSchemaProperty,
|
|
31
|
+
type CustomFunctionTool,
|
|
32
|
+
type CustomNodeNamespace,
|
|
33
|
+
} from './register.ts';
|
|
34
|
+
export { loadReferenceInto, referenceDomains } from './reference.ts';
|
|
35
|
+
export { configureHttpUdf, type EgressGuard, type SecretResolver } from './contrib/http.ts';
|
|
36
|
+
export {
|
|
37
|
+
setRateStore,
|
|
38
|
+
getRateStore,
|
|
39
|
+
InMemoryRateStore,
|
|
40
|
+
type RateStore,
|
|
41
|
+
type RateCommonResult,
|
|
42
|
+
type GroupDistinctCommonResult,
|
|
43
|
+
} from './contrib/rate-window.ts';
|
package/src/limiter.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 并发闸端口(执行规范 §6.3):per-tenant UDF 并发上限,防单租户打满
|
|
3
|
+
* evaluate worker 线程池。机制在本仓(内存参考实现 + 接口契约),真实语义
|
|
4
|
+
* (分布式/Redis 化配额)由宿主(verdict)实现注入。
|
|
5
|
+
*/
|
|
6
|
+
export interface ConcurrencyLimiter {
|
|
7
|
+
/** 获取 key(通常是 tenantId)的一个槽位;返回释放函数,必须调用恰好一次。实现须 FIFO 公平 */
|
|
8
|
+
acquire(key: string): Promise<() => void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SlotState {
|
|
12
|
+
active: number;
|
|
13
|
+
/** 等待队列(FIFO):resolve 后调用方获得槽位 */
|
|
14
|
+
queue: Array<() => void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 进程内信号量参考实现:每 key 最多 maxPerKey 个并发,FIFO 排队 */
|
|
18
|
+
export class InMemoryConcurrencyLimiter implements ConcurrencyLimiter {
|
|
19
|
+
private readonly slots = new Map<string, SlotState>();
|
|
20
|
+
|
|
21
|
+
constructor(private readonly maxPerKey = 5) {
|
|
22
|
+
this.maxPerKey = Math.max(1, maxPerKey);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async acquire(key: string): Promise<() => void> {
|
|
26
|
+
let state = this.slots.get(key);
|
|
27
|
+
if (!state) {
|
|
28
|
+
state = { active: 0, queue: [] };
|
|
29
|
+
this.slots.set(key, state);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (state.active < this.maxPerKey) {
|
|
33
|
+
state.active += 1;
|
|
34
|
+
return this.releaseOf(key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
state!.queue.push(() => resolve(this.releaseOf(key)));
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private releaseOf(key: string): () => void {
|
|
43
|
+
return () => {
|
|
44
|
+
const state = this.slots.get(key);
|
|
45
|
+
if (!state) return;
|
|
46
|
+
const next = state.queue.shift();
|
|
47
|
+
if (next) {
|
|
48
|
+
// 槽位直接移交队首(active 数不变)
|
|
49
|
+
next();
|
|
50
|
+
} else {
|
|
51
|
+
state.active -= 1;
|
|
52
|
+
if (state.active === 0 && state.queue.length === 0) {
|
|
53
|
+
this.slots.delete(key);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 观测:某 key 当前活跃并发数(测试/指标用) */
|
|
60
|
+
activeOf(key: string): number {
|
|
61
|
+
return this.slots.get(key)?.active ?? 0;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 空实现:不限并发(DecisionRuntime 缺省,零开销) */
|
|
66
|
+
export class NoopConcurrencyLimiter implements ConcurrencyLimiter {
|
|
67
|
+
async acquire(_key: string): Promise<() => void> {
|
|
68
|
+
return () => {};
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/reference.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// 参考函数域装载(builtin: 'reference' 的实现核心)。
|
|
2
|
+
// import 本模块即向 globalUdfRegistry 注册全部 contrib 工具(各 contrib 模块的
|
|
3
|
+
// 模块级 defineContrib 副作用);需要装载到独立实例(多租户/多运行时隔离)时,
|
|
4
|
+
// 调用 loadReferenceInto(registry)。业务 UDF 包(verdict 侧)不应依赖本模块。
|
|
5
|
+
import cryptoTools from './contrib/crypto.ts';
|
|
6
|
+
import customListQueryTools from './contrib/custom-list-query.ts';
|
|
7
|
+
import debugTools from './contrib/debug.ts';
|
|
8
|
+
import debuguiTools from './contrib/debugui.ts';
|
|
9
|
+
import httpTools from './contrib/http.ts';
|
|
10
|
+
import ipLocationTools from './contrib/ip-location.ts';
|
|
11
|
+
import rateWindowTools from './contrib/rate-window.ts';
|
|
12
|
+
import rosterTools from './contrib/roster.ts';
|
|
13
|
+
import { type ContribToolDef, type UdfRegistry, globalUdfRegistry } from './register.ts';
|
|
14
|
+
|
|
15
|
+
/** 参考域清单:[namespace, tools]——namespace 与 contrib 文件名约定一致 */
|
|
16
|
+
export const referenceDomains: Array<[string, ContribToolDef[]]> = [
|
|
17
|
+
['crypto', cryptoTools],
|
|
18
|
+
['custom-list-query', customListQueryTools],
|
|
19
|
+
['debug', debugTools],
|
|
20
|
+
['debugui', debuguiTools],
|
|
21
|
+
['http', httpTools],
|
|
22
|
+
['ip-location', ipLocationTools],
|
|
23
|
+
['rate-window', rateWindowTools],
|
|
24
|
+
['roster', rosterTools],
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/** 将参考函数域注册到任意注册表(实例隔离场景用;global 的注册由 import 副作用完成) */
|
|
28
|
+
export const loadReferenceInto = (registry: UdfRegistry): void => {
|
|
29
|
+
for (const [namespace, tools] of referenceDomains) {
|
|
30
|
+
registry.registerTools(tools, namespace);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export { globalUdfRegistry };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { UdfRegistry } from './register.ts';
|
|
4
|
+
|
|
5
|
+
describe('UdfRegistry namespace collision(U3 起跨名硬失败)', () => {
|
|
6
|
+
test('函数名与自身 namespace 同名放行(遗留契约:namespace 优先)', () => {
|
|
7
|
+
const manager = new UdfRegistry();
|
|
8
|
+
expect(() => manager.registerFunction(() => null, 'roster', undefined, 'roster')).not.toThrow();
|
|
9
|
+
expect(manager.udfFunctionSchema('roster')?.namespace).toBe('roster');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('函数名与现有 namespace 同名时抛错', () => {
|
|
13
|
+
const manager = new UdfRegistry();
|
|
14
|
+
manager.registerFunction(() => null, 'counter', undefined, 'rate_1h');
|
|
15
|
+
expect(() => manager.registerFunction(() => null, 'debug', undefined, 'counter')).toThrow(/'counter'/);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('namespace 与现有函数同名时抛错', () => {
|
|
19
|
+
const manager = new UdfRegistry();
|
|
20
|
+
manager.registerFunction(() => null, 'debug', undefined, 'roster');
|
|
21
|
+
expect(() => manager.registerFunction(() => null, 'roster', undefined, 'query_list')).toThrow(/namespace 'roster'/);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('force 逃生口可显式接管撞名', () => {
|
|
25
|
+
const manager = new UdfRegistry();
|
|
26
|
+
manager.registerFunction(() => null, 'counter', undefined, 'rate_1h');
|
|
27
|
+
expect(() => manager.registerFunction(() => null, 'debug', undefined, 'counter', { force: true })).not.toThrow();
|
|
28
|
+
expect(manager.udfFunctionSchema('counter')).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('无碰撞时正常注册', () => {
|
|
32
|
+
const manager = new UdfRegistry();
|
|
33
|
+
expect(() => manager.registerFunction(() => null, 'debug', undefined, 'inout')).not.toThrow();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('registerTools 批量注册到指定 namespace', () => {
|
|
37
|
+
const manager = new UdfRegistry();
|
|
38
|
+
manager.registerTools(
|
|
39
|
+
[
|
|
40
|
+
{
|
|
41
|
+
name: 'tool_a',
|
|
42
|
+
description: 'a',
|
|
43
|
+
parametersSchema: { properties: {}, title: 'tool_a', type: 'object' },
|
|
44
|
+
returnsSchema: { type: 'null' },
|
|
45
|
+
fn: () => 1,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'tool_b',
|
|
49
|
+
description: 'b',
|
|
50
|
+
parametersSchema: { properties: {}, title: 'tool_b', type: 'object' },
|
|
51
|
+
returnsSchema: { type: 'null' },
|
|
52
|
+
fn: () => 2,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
'pack-ns',
|
|
56
|
+
);
|
|
57
|
+
expect(manager.udfFunctionSchema('tool_a')?.namespace).toBe('pack-ns');
|
|
58
|
+
expect(manager.udfFunctionSchema('tool_b')?.namespace).toBe('pack-ns');
|
|
59
|
+
});
|
|
60
|
+
});
|