@advicenxt/sbp-server 0.1.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/benchmarks/bench.ts +272 -0
- package/dist/auth.d.ts +20 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +69 -0
- package/dist/auth.js.map +1 -0
- package/dist/blackboard.d.ts +84 -0
- package/dist/blackboard.d.ts.map +1 -0
- package/dist/blackboard.js +502 -0
- package/dist/blackboard.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +102 -0
- package/dist/cli.js.map +1 -0
- package/dist/conditions.d.ts +27 -0
- package/dist/conditions.d.ts.map +1 -0
- package/dist/conditions.js +240 -0
- package/dist/conditions.js.map +1 -0
- package/dist/decay.d.ts +21 -0
- package/dist/decay.d.ts.map +1 -0
- package/dist/decay.js +88 -0
- package/dist/decay.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/rate-limiter.d.ts +21 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +75 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/server.d.ts +63 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +401 -0
- package/dist/server.js.map +1 -0
- package/dist/store.d.ts +54 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +55 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +247 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +296 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +205 -0
- package/dist/validation.js.map +1 -0
- package/eslint.config.js +26 -0
- package/package.json +66 -0
- package/src/auth.ts +89 -0
- package/src/blackboard.test.ts +287 -0
- package/src/blackboard.ts +651 -0
- package/src/cli.ts +116 -0
- package/src/conditions.ts +305 -0
- package/src/conformance.test.ts +686 -0
- package/src/decay.ts +103 -0
- package/src/index.ts +24 -0
- package/src/rate-limiter.ts +104 -0
- package/src/server.integration.test.ts +436 -0
- package/src/server.ts +500 -0
- package/src/store.ts +108 -0
- package/src/types.ts +314 -0
- package/src/validation.ts +251 -0
- package/tsconfig.eslint.json +5 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SBP Input Validation
|
|
3
|
+
* Zod schemas for all JSON-RPC params and protocol messages
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// DECAY MODEL SCHEMAS
|
|
8
|
+
// ============================================================================
|
|
9
|
+
const ExponentialDecaySchema = z.object({
|
|
10
|
+
type: z.literal("exponential"),
|
|
11
|
+
half_life_ms: z.number().positive(),
|
|
12
|
+
});
|
|
13
|
+
const LinearDecaySchema = z.object({
|
|
14
|
+
type: z.literal("linear"),
|
|
15
|
+
rate_per_ms: z.number().positive(),
|
|
16
|
+
});
|
|
17
|
+
const StepDecaySchema = z.object({
|
|
18
|
+
type: z.literal("step"),
|
|
19
|
+
steps: z
|
|
20
|
+
.array(z.object({
|
|
21
|
+
at_ms: z.number().nonnegative(),
|
|
22
|
+
intensity: z.number().min(0).max(1),
|
|
23
|
+
}))
|
|
24
|
+
.min(1),
|
|
25
|
+
});
|
|
26
|
+
const ImmortalDecaySchema = z.object({
|
|
27
|
+
type: z.literal("immortal"),
|
|
28
|
+
});
|
|
29
|
+
const DecayModelSchema = z.discriminatedUnion("type", [
|
|
30
|
+
ExponentialDecaySchema,
|
|
31
|
+
LinearDecaySchema,
|
|
32
|
+
StepDecaySchema,
|
|
33
|
+
ImmortalDecaySchema,
|
|
34
|
+
]);
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// TAG FILTER SCHEMAS
|
|
37
|
+
// ============================================================================
|
|
38
|
+
const TagFilterSchema = z
|
|
39
|
+
.object({
|
|
40
|
+
any: z.array(z.string()).optional(),
|
|
41
|
+
all: z.array(z.string()).optional(),
|
|
42
|
+
none: z.array(z.string()).optional(),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// CONDITION SCHEMAS
|
|
47
|
+
// ============================================================================
|
|
48
|
+
const ThresholdConditionSchema = z.object({
|
|
49
|
+
type: z.literal("threshold"),
|
|
50
|
+
trail: z.string().min(1),
|
|
51
|
+
signal_type: z.string().min(1),
|
|
52
|
+
tags: TagFilterSchema.optional(),
|
|
53
|
+
aggregation: z.enum(["sum", "max", "avg", "count", "any"]),
|
|
54
|
+
operator: z.enum([">=", ">", "<=", "<", "==", "!="]),
|
|
55
|
+
value: z.number(),
|
|
56
|
+
});
|
|
57
|
+
const RateConditionSchema = z.object({
|
|
58
|
+
type: z.literal("rate"),
|
|
59
|
+
trail: z.string().min(1),
|
|
60
|
+
signal_type: z.string().min(1),
|
|
61
|
+
metric: z.enum(["emissions_per_second", "intensity_delta"]),
|
|
62
|
+
window_ms: z.number().positive(),
|
|
63
|
+
operator: z.enum([">=", ">", "<=", "<"]),
|
|
64
|
+
value: z.number(),
|
|
65
|
+
});
|
|
66
|
+
const PatternConditionSchema = z.object({
|
|
67
|
+
type: z.literal("pattern"),
|
|
68
|
+
sequence: z
|
|
69
|
+
.array(z.object({
|
|
70
|
+
trail: z.string().min(1),
|
|
71
|
+
signal_type: z.string().min(1),
|
|
72
|
+
min_intensity: z.number().min(0).max(1).optional(),
|
|
73
|
+
}))
|
|
74
|
+
.min(1),
|
|
75
|
+
window_ms: z.number().positive(),
|
|
76
|
+
ordered: z.boolean().optional(),
|
|
77
|
+
});
|
|
78
|
+
// Recursive condition schema using z.lazy for composite self-reference
|
|
79
|
+
const ScentConditionSchema = z.lazy(() => z.union([
|
|
80
|
+
ThresholdConditionSchema,
|
|
81
|
+
z.object({
|
|
82
|
+
type: z.literal("composite"),
|
|
83
|
+
operator: z.enum(["and", "or", "not"]),
|
|
84
|
+
conditions: z.array(ScentConditionSchema).min(1),
|
|
85
|
+
}),
|
|
86
|
+
RateConditionSchema,
|
|
87
|
+
PatternConditionSchema,
|
|
88
|
+
]));
|
|
89
|
+
// ============================================================================
|
|
90
|
+
// OPERATION PARAM SCHEMAS
|
|
91
|
+
// ============================================================================
|
|
92
|
+
export const EmitParamsSchema = z.object({
|
|
93
|
+
trail: z.string().min(1, "Trail must be a non-empty string"),
|
|
94
|
+
type: z.string().min(1, "Type must be a non-empty string"),
|
|
95
|
+
intensity: z.number().min(0).max(1, "Intensity must be between 0 and 1"),
|
|
96
|
+
decay: DecayModelSchema.optional(),
|
|
97
|
+
payload: z.record(z.unknown()).optional(),
|
|
98
|
+
tags: z.array(z.string()).optional(),
|
|
99
|
+
merge_strategy: z.enum(["reinforce", "replace", "max", "add", "new"]).optional(),
|
|
100
|
+
source_agent: z.string().optional(),
|
|
101
|
+
});
|
|
102
|
+
export const SniffParamsSchema = z.object({
|
|
103
|
+
trails: z.array(z.string()).optional(),
|
|
104
|
+
types: z.array(z.string()).optional(),
|
|
105
|
+
min_intensity: z.number().min(0).max(1).optional(),
|
|
106
|
+
max_age_ms: z.number().positive().optional(),
|
|
107
|
+
tags: TagFilterSchema.optional(),
|
|
108
|
+
limit: z.number().int().positive().max(10000).optional(),
|
|
109
|
+
include_evaporated: z.boolean().optional(),
|
|
110
|
+
});
|
|
111
|
+
export const RegisterScentParamsSchema = z.object({
|
|
112
|
+
scent_id: z.string().min(1, "Scent ID must be a non-empty string"),
|
|
113
|
+
agent_endpoint: z.string().optional().default(""),
|
|
114
|
+
condition: ScentConditionSchema,
|
|
115
|
+
cooldown_ms: z.number().int().nonnegative().optional(),
|
|
116
|
+
activation_payload: z.record(z.unknown()).optional(),
|
|
117
|
+
trigger_mode: z.enum(["level", "edge_rising", "edge_falling"]).optional(),
|
|
118
|
+
hysteresis: z.number().nonnegative().optional(),
|
|
119
|
+
max_execution_ms: z.number().positive().optional(),
|
|
120
|
+
context_trails: z.array(z.string()).optional(),
|
|
121
|
+
});
|
|
122
|
+
export const DeregisterScentParamsSchema = z.object({
|
|
123
|
+
scent_id: z.string().min(1),
|
|
124
|
+
});
|
|
125
|
+
export const EvaporateParamsSchema = z.object({
|
|
126
|
+
trail: z.string().optional(),
|
|
127
|
+
types: z.array(z.string()).optional(),
|
|
128
|
+
older_than_ms: z.number().positive().optional(),
|
|
129
|
+
below_intensity: z.number().min(0).max(1).optional(),
|
|
130
|
+
tags: TagFilterSchema.optional(),
|
|
131
|
+
});
|
|
132
|
+
export const InspectParamsSchema = z.object({
|
|
133
|
+
include: z.array(z.enum(["trails", "scents", "stats"])).optional(),
|
|
134
|
+
});
|
|
135
|
+
export const SubscribeParamsSchema = z.object({
|
|
136
|
+
scent_id: z.string().min(1),
|
|
137
|
+
});
|
|
138
|
+
export const UnsubscribeParamsSchema = z.object({
|
|
139
|
+
scent_id: z.string().min(1),
|
|
140
|
+
});
|
|
141
|
+
// ============================================================================
|
|
142
|
+
// JSON-RPC ENVELOPE SCHEMA
|
|
143
|
+
// ============================================================================
|
|
144
|
+
export const JsonRpcRequestSchema = z.object({
|
|
145
|
+
jsonrpc: z.literal("2.0"),
|
|
146
|
+
id: z.union([z.string(), z.number()]),
|
|
147
|
+
method: z.string().min(1),
|
|
148
|
+
params: z.unknown().optional().default({}),
|
|
149
|
+
});
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// VALIDATION HELPER
|
|
152
|
+
// ============================================================================
|
|
153
|
+
/** Map of method names to their param schemas */
|
|
154
|
+
const METHOD_SCHEMAS = {
|
|
155
|
+
"sbp/emit": EmitParamsSchema,
|
|
156
|
+
"sbp/sniff": SniffParamsSchema,
|
|
157
|
+
"sbp/register_scent": RegisterScentParamsSchema,
|
|
158
|
+
"sbp/deregister_scent": DeregisterScentParamsSchema,
|
|
159
|
+
"sbp/evaporate": EvaporateParamsSchema,
|
|
160
|
+
"sbp/inspect": InspectParamsSchema,
|
|
161
|
+
"sbp/subscribe": SubscribeParamsSchema,
|
|
162
|
+
"sbp/unsubscribe": UnsubscribeParamsSchema,
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Validate a JSON-RPC request envelope.
|
|
166
|
+
* Returns the parsed request or a JSON-RPC error.
|
|
167
|
+
*/
|
|
168
|
+
export function validateEnvelope(body) {
|
|
169
|
+
const result = JsonRpcRequestSchema.safeParse(body);
|
|
170
|
+
if (!result.success) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
error: {
|
|
174
|
+
code: -32600,
|
|
175
|
+
message: "Invalid JSON-RPC request",
|
|
176
|
+
data: result.error.issues.map((i) => ({ path: i.path.join("."), message: i.message })),
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return { ok: true, request: result.data };
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Validate method-specific params.
|
|
184
|
+
* Returns the parsed params or a JSON-RPC error.
|
|
185
|
+
*/
|
|
186
|
+
export function validateParams(method, params) {
|
|
187
|
+
const schema = METHOD_SCHEMAS[method];
|
|
188
|
+
if (!schema) {
|
|
189
|
+
// Unknown methods are handled by the RPC router, not here
|
|
190
|
+
return { ok: true, params };
|
|
191
|
+
}
|
|
192
|
+
const result = schema.safeParse(params);
|
|
193
|
+
if (!result.success) {
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
error: {
|
|
197
|
+
code: -32602,
|
|
198
|
+
message: `Invalid params for ${method}`,
|
|
199
|
+
data: result.error.issues.map((i) => ({ path: i.path.join("."), message: i.message })),
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return { ok: true, params: result.data };
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"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"}
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import eslint from '@eslint/js';
|
|
2
|
+
import tseslint from 'typescript-eslint';
|
|
3
|
+
import globals from 'globals';
|
|
4
|
+
|
|
5
|
+
export default tseslint.config(
|
|
6
|
+
eslint.configs.recommended,
|
|
7
|
+
...tseslint.configs.recommended,
|
|
8
|
+
{
|
|
9
|
+
languageOptions: {
|
|
10
|
+
globals: {
|
|
11
|
+
...globals.node,
|
|
12
|
+
},
|
|
13
|
+
parserOptions: {
|
|
14
|
+
project: './tsconfig.eslint.json',
|
|
15
|
+
tsconfigRootDir: import.meta.dirname,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
rules: {
|
|
19
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
20
|
+
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
ignores: ['dist/**', 'node_modules/**', 'coverage/**'],
|
|
25
|
+
}
|
|
26
|
+
);
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@advicenxt/sbp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Stigmergic Blackboard Protocol - Server Implementation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"sbp-server": "./dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "tsx watch src/cli.ts",
|
|
20
|
+
"start": "node dist/cli.js",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"lint": "eslint src",
|
|
24
|
+
"prepublishOnly": "npm run build"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"sbp",
|
|
28
|
+
"stigmergy",
|
|
29
|
+
"blackboard",
|
|
30
|
+
"multi-agent",
|
|
31
|
+
"coordination",
|
|
32
|
+
"pheromone"
|
|
33
|
+
],
|
|
34
|
+
"author": "SBP Contributors",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/AdviceNXT/sbp.git",
|
|
38
|
+
"directory": "packages/server"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://sbp.dev",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/AdviceNXT/sbp/issues"
|
|
43
|
+
},
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"fastify": "^4.26.0",
|
|
47
|
+
"uuid": "^13.0.0",
|
|
48
|
+
"zod": "^3.22.4"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@eslint/js": "^9.39.2",
|
|
52
|
+
"@types/node": "^20.11.0",
|
|
53
|
+
"@types/uuid": "^11.0.0",
|
|
54
|
+
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
55
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
56
|
+
"eslint": "^9.39.2",
|
|
57
|
+
"globals": "^14.0.0",
|
|
58
|
+
"tsx": "^4.7.0",
|
|
59
|
+
"typescript": "^5.3.3",
|
|
60
|
+
"typescript-eslint": "^8.54.0",
|
|
61
|
+
"vitest": "^1.2.0"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=18.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/auth.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SBP Authentication Middleware
|
|
3
|
+
* Basic API key authentication for the SBP server
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { FastifyRequest, FastifyReply, HookHandlerDoneFunction } from "fastify";
|
|
7
|
+
|
|
8
|
+
export interface AuthOptions {
|
|
9
|
+
/** List of valid API keys */
|
|
10
|
+
apiKeys?: string[];
|
|
11
|
+
/** Whether authentication is required (default: false) */
|
|
12
|
+
requireAuth?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Paths that skip authentication */
|
|
16
|
+
const PUBLIC_PATHS = ["/health"];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a Fastify onRequest hook for API key authentication.
|
|
20
|
+
*
|
|
21
|
+
* Checks the `Authorization: Bearer <key>` header against the
|
|
22
|
+
* configured list of API keys. Returns 401 with SBP error code
|
|
23
|
+
* -32005 (UNAUTHORIZED) if the key is missing or invalid.
|
|
24
|
+
*/
|
|
25
|
+
export function createAuthHook(options: AuthOptions) {
|
|
26
|
+
const { apiKeys = [], requireAuth = false } = options;
|
|
27
|
+
|
|
28
|
+
return function authHook(
|
|
29
|
+
request: FastifyRequest,
|
|
30
|
+
reply: FastifyReply,
|
|
31
|
+
done: HookHandlerDoneFunction
|
|
32
|
+
): void {
|
|
33
|
+
// Skip auth if not required
|
|
34
|
+
if (!requireAuth || apiKeys.length === 0) {
|
|
35
|
+
done();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Skip auth for public paths and OPTIONS
|
|
40
|
+
const url = request.url.split("?")[0];
|
|
41
|
+
if (PUBLIC_PATHS.includes(url) || request.method === "OPTIONS") {
|
|
42
|
+
done();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Extract bearer token
|
|
47
|
+
const authHeader = request.headers.authorization;
|
|
48
|
+
if (!authHeader) {
|
|
49
|
+
reply.status(401).send({
|
|
50
|
+
jsonrpc: "2.0",
|
|
51
|
+
id: null,
|
|
52
|
+
error: {
|
|
53
|
+
code: -32005,
|
|
54
|
+
message: "Unauthorized: Missing Authorization header",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const parts = authHeader.split(" ");
|
|
61
|
+
if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") {
|
|
62
|
+
reply.status(401).send({
|
|
63
|
+
jsonrpc: "2.0",
|
|
64
|
+
id: null,
|
|
65
|
+
error: {
|
|
66
|
+
code: -32005,
|
|
67
|
+
message: "Unauthorized: Invalid Authorization header format. Expected: Bearer <api-key>",
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const token = parts[1];
|
|
74
|
+
if (!apiKeys.includes(token)) {
|
|
75
|
+
reply.status(401).send({
|
|
76
|
+
jsonrpc: "2.0",
|
|
77
|
+
id: null,
|
|
78
|
+
error: {
|
|
79
|
+
code: -32005,
|
|
80
|
+
message: "Unauthorized: Invalid API key",
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Valid key — attach agent info from token position (optional extension point)
|
|
87
|
+
done();
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SBP Server Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
6
|
+
import { Blackboard } from "../src/blackboard.js";
|
|
7
|
+
|
|
8
|
+
describe("Blackboard", () => {
|
|
9
|
+
let bb: Blackboard;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
bb = new Blackboard();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("emit", () => {
|
|
16
|
+
it("should create a new pheromone", () => {
|
|
17
|
+
const result = bb.emit({
|
|
18
|
+
trail: "test.signals",
|
|
19
|
+
type: "event",
|
|
20
|
+
intensity: 0.8,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(result.action).toBe("created");
|
|
24
|
+
expect(result.new_intensity).toBe(0.8);
|
|
25
|
+
expect(result.pheromone_id).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should reinforce existing pheromone with same payload", () => {
|
|
29
|
+
bb.emit({
|
|
30
|
+
trail: "test.signals",
|
|
31
|
+
type: "event",
|
|
32
|
+
intensity: 0.5,
|
|
33
|
+
payload: { id: 1 },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const result = bb.emit({
|
|
37
|
+
trail: "test.signals",
|
|
38
|
+
type: "event",
|
|
39
|
+
intensity: 0.8,
|
|
40
|
+
payload: { id: 1 },
|
|
41
|
+
merge_strategy: "reinforce",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(result.action).toBe("reinforced");
|
|
45
|
+
expect(result.new_intensity).toBe(0.8);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should create new pheromone with merge_strategy=new", () => {
|
|
49
|
+
bb.emit({
|
|
50
|
+
trail: "test.signals",
|
|
51
|
+
type: "event",
|
|
52
|
+
intensity: 0.5,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
bb.emit({
|
|
56
|
+
trail: "test.signals",
|
|
57
|
+
type: "event",
|
|
58
|
+
intensity: 0.8,
|
|
59
|
+
merge_strategy: "new",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(bb.size).toBe(2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should clamp intensity to [0, 1]", () => {
|
|
66
|
+
const result1 = bb.emit({
|
|
67
|
+
trail: "test",
|
|
68
|
+
type: "a",
|
|
69
|
+
intensity: 1.5,
|
|
70
|
+
});
|
|
71
|
+
expect(result1.new_intensity).toBe(1);
|
|
72
|
+
|
|
73
|
+
const result2 = bb.emit({
|
|
74
|
+
trail: "test",
|
|
75
|
+
type: "b",
|
|
76
|
+
intensity: -0.5,
|
|
77
|
+
});
|
|
78
|
+
expect(result2.new_intensity).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("sniff", () => {
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
bb.emit({ trail: "market.signals", type: "volatility", intensity: 0.8 });
|
|
85
|
+
bb.emit({ trail: "market.signals", type: "momentum", intensity: 0.5 });
|
|
86
|
+
bb.emit({ trail: "market.orders", type: "large", intensity: 0.6 });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should return all pheromones when no filter", () => {
|
|
90
|
+
const result = bb.sniff();
|
|
91
|
+
expect(result.pheromones.length).toBe(3);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should filter by trail", () => {
|
|
95
|
+
const result = bb.sniff({ trails: ["market.signals"] });
|
|
96
|
+
expect(result.pheromones.length).toBe(2);
|
|
97
|
+
expect(result.pheromones.every((p) => p.trail === "market.signals")).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should filter by type", () => {
|
|
101
|
+
const result = bb.sniff({ types: ["volatility"] });
|
|
102
|
+
expect(result.pheromones.length).toBe(1);
|
|
103
|
+
expect(result.pheromones[0].type).toBe("volatility");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should filter by min_intensity", () => {
|
|
107
|
+
const result = bb.sniff({ min_intensity: 0.7 });
|
|
108
|
+
expect(result.pheromones.length).toBe(1);
|
|
109
|
+
expect(result.pheromones[0].current_intensity).toBeGreaterThanOrEqual(0.7);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should return aggregates", () => {
|
|
113
|
+
const result = bb.sniff();
|
|
114
|
+
expect(result.aggregates["market.signals/volatility"]).toBeDefined();
|
|
115
|
+
expect(result.aggregates["market.signals/volatility"].count).toBe(1);
|
|
116
|
+
expect(result.aggregates["market.signals/volatility"].max_intensity).toBeCloseTo(0.8, 1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should sort by intensity descending", () => {
|
|
120
|
+
const result = bb.sniff();
|
|
121
|
+
for (let i = 1; i < result.pheromones.length; i++) {
|
|
122
|
+
expect(result.pheromones[i - 1].current_intensity).toBeGreaterThanOrEqual(
|
|
123
|
+
result.pheromones[i].current_intensity
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("registerScent", () => {
|
|
130
|
+
it("should register a new scent", () => {
|
|
131
|
+
const result = bb.registerScent({
|
|
132
|
+
scent_id: "test-scent",
|
|
133
|
+
agent_endpoint: "http://localhost:8080",
|
|
134
|
+
condition: {
|
|
135
|
+
type: "threshold",
|
|
136
|
+
trail: "test",
|
|
137
|
+
signal_type: "event",
|
|
138
|
+
aggregation: "max",
|
|
139
|
+
operator: ">=",
|
|
140
|
+
value: 0.5,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(result.status).toBe("registered");
|
|
145
|
+
expect(result.scent_id).toBe("test-scent");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should update existing scent", () => {
|
|
149
|
+
bb.registerScent({
|
|
150
|
+
scent_id: "test-scent",
|
|
151
|
+
agent_endpoint: "http://localhost:8080",
|
|
152
|
+
condition: {
|
|
153
|
+
type: "threshold",
|
|
154
|
+
trail: "test",
|
|
155
|
+
signal_type: "event",
|
|
156
|
+
aggregation: "max",
|
|
157
|
+
operator: ">=",
|
|
158
|
+
value: 0.5,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const result = bb.registerScent({
|
|
163
|
+
scent_id: "test-scent",
|
|
164
|
+
agent_endpoint: "http://localhost:9090",
|
|
165
|
+
condition: {
|
|
166
|
+
type: "threshold",
|
|
167
|
+
trail: "test",
|
|
168
|
+
signal_type: "event",
|
|
169
|
+
aggregation: "max",
|
|
170
|
+
operator: ">=",
|
|
171
|
+
value: 0.7,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
expect(result.status).toBe("updated");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should report current condition state", () => {
|
|
179
|
+
bb.emit({ trail: "test", type: "event", intensity: 0.8 });
|
|
180
|
+
|
|
181
|
+
const result = bb.registerScent({
|
|
182
|
+
scent_id: "test-scent",
|
|
183
|
+
agent_endpoint: "http://localhost:8080",
|
|
184
|
+
condition: {
|
|
185
|
+
type: "threshold",
|
|
186
|
+
trail: "test",
|
|
187
|
+
signal_type: "event",
|
|
188
|
+
aggregation: "max",
|
|
189
|
+
operator: ">=",
|
|
190
|
+
value: 0.5,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(result.current_condition_state.met).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe("deregisterScent", () => {
|
|
199
|
+
it("should deregister existing scent", () => {
|
|
200
|
+
bb.registerScent({
|
|
201
|
+
scent_id: "test-scent",
|
|
202
|
+
agent_endpoint: "http://localhost:8080",
|
|
203
|
+
condition: {
|
|
204
|
+
type: "threshold",
|
|
205
|
+
trail: "test",
|
|
206
|
+
signal_type: "event",
|
|
207
|
+
aggregation: "max",
|
|
208
|
+
operator: ">=",
|
|
209
|
+
value: 0.5,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const result = bb.deregisterScent({ scent_id: "test-scent" });
|
|
214
|
+
expect(result.status).toBe("deregistered");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should return not_found for unknown scent", () => {
|
|
218
|
+
const result = bb.deregisterScent({ scent_id: "unknown" });
|
|
219
|
+
expect(result.status).toBe("not_found");
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("evaporate", () => {
|
|
224
|
+
beforeEach(() => {
|
|
225
|
+
bb.emit({ trail: "a", type: "x", intensity: 0.1 });
|
|
226
|
+
bb.emit({ trail: "a", type: "y", intensity: 0.5 });
|
|
227
|
+
bb.emit({ trail: "b", type: "x", intensity: 0.8 });
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should evaporate by trail", () => {
|
|
231
|
+
const result = bb.evaporate({ trail: "a" });
|
|
232
|
+
expect(result.evaporated_count).toBe(2);
|
|
233
|
+
expect(bb.size).toBe(1);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should evaporate by intensity threshold", () => {
|
|
237
|
+
const result = bb.evaporate({ below_intensity: 0.3 });
|
|
238
|
+
expect(result.evaporated_count).toBe(1);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("gc", () => {
|
|
243
|
+
it("should remove evaporated pheromones", async () => {
|
|
244
|
+
// Create pheromone with very short half-life
|
|
245
|
+
bb.emit({
|
|
246
|
+
trail: "test",
|
|
247
|
+
type: "event",
|
|
248
|
+
intensity: 0.5,
|
|
249
|
+
decay: { type: "linear", rate_per_ms: 1 }, // Decays to 0 in 500ms
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(bb.size).toBe(1);
|
|
253
|
+
|
|
254
|
+
// Wait for decay
|
|
255
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
256
|
+
|
|
257
|
+
const removed = bb.gc();
|
|
258
|
+
expect(removed).toBe(1);
|
|
259
|
+
expect(bb.size).toBe(0);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe("inspect", () => {
|
|
264
|
+
it("should return stats", () => {
|
|
265
|
+
bb.emit({ trail: "test", type: "event", intensity: 0.5 });
|
|
266
|
+
bb.registerScent({
|
|
267
|
+
scent_id: "s1",
|
|
268
|
+
agent_endpoint: "http://localhost",
|
|
269
|
+
condition: { type: "threshold", trail: "test", signal_type: "event", aggregation: "max", operator: ">=", value: 0.1 },
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const result = bb.inspect({ include: ["stats"] });
|
|
273
|
+
expect(result.stats).toBeDefined();
|
|
274
|
+
expect(result.stats!.total_pheromones).toBe(1);
|
|
275
|
+
expect(result.stats!.total_scents).toBe(1);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("should return trails info", () => {
|
|
279
|
+
bb.emit({ trail: "a", type: "x", intensity: 0.5 });
|
|
280
|
+
bb.emit({ trail: "a", type: "y", intensity: 0.3 });
|
|
281
|
+
|
|
282
|
+
const result = bb.inspect({ include: ["trails"] });
|
|
283
|
+
expect(result.trails).toBeDefined();
|
|
284
|
+
expect(result.trails!.find((t) => t.name === "a")?.pheromone_count).toBe(2);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
});
|