@vertesia/tools-sdk 1.6.0-dev.20260923.085325Z → 1.6.0-dev.20261008.064737Z

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/lib/sandbox.js ADDED
@@ -0,0 +1,35 @@
1
+ import { Hono } from 'hono';
2
+ import { AuthSession } from './auth.js';
3
+ import { createToolServer as createBaseToolServer } from './server.js';
4
+ /** Browser-bundle entrypoint. Collections take bundled data; directory loaders are Node-only. */
5
+ export * from './ActivityCollection.js';
6
+ export { AuthSession, authorizeSandbox as authorize } from './auth.js';
7
+ export * from './ContentTypesCollection.js';
8
+ export * from './InteractionCollection.js';
9
+ export * from './RenderingTemplateCollection.js';
10
+ export * from './SkillCollection.js';
11
+ export { buildAppPackage } from './server/app-package.js';
12
+ export * from './server/types.js';
13
+ /** The host supplies non-secret claims; SDK HTTP goes through the sandbox's host-mediated fetch. */
14
+ export function createToolServer(config) {
15
+ const app = new Hono();
16
+ app.use('*', async (ctx, next) => {
17
+ ctx.set('toolAuthMode', 'sandbox');
18
+ const payload = ctx.env?.sandboxSession;
19
+ if (payload) {
20
+ ctx.set('toolAuthSession', new AuthSession('sandbox', payload, {
21
+ studio: 'https://platform.invalid/studio',
22
+ store: 'https://platform.invalid/store',
23
+ token: 'https://platform.invalid/token',
24
+ }));
25
+ }
26
+ await next();
27
+ });
28
+ app.route('/', createBaseToolServer(config));
29
+ return app;
30
+ }
31
+ export * from './site/templates.js';
32
+ export * from './ToolCollection.js';
33
+ export * from './ToolRegistry.js';
34
+ export * from './types.js';
35
+ //# sourceMappingURL=sandbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox.js","sourceRoot":"","sources":["../src/sandbox.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,EAAE,gBAAgB,IAAI,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEvE,iGAAiG;AACjG,cAAc,yBAAyB,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,gBAAgB,IAAI,SAAS,EAAE,MAAM,WAAW,CAAC;AACvE,cAAc,6BAA6B,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,kCAAkC,CAAC;AACjD,cAAc,sBAAsB,CAAC;AAErC,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,cAAc,mBAAmB,CAAC;AAClC,oGAAoG;AACpG,MAAM,UAAU,gBAAgB,CAAC,MAAwB;IACrD,MAAM,GAAG,GAAG,IAAI,IAAI,EAGhB,CAAC;IACL,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QAC7B,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC;QACxC,IAAI,OAAO,EAAE,CAAC;YACV,GAAG,CAAC,GAAG,CACH,iBAAiB,EACjB,IAAI,WAAW,CAAC,SAAS,EAAE,OAAO,EAAE;gBAChC,MAAM,EAAE,iCAAiC;gBACzC,KAAK,EAAE,gCAAgC;gBACvC,KAAK,EAAE,gCAAgC;aAC1C,CAAC,CACL,CAAC;QACN,CAAC;QACD,MAAM,IAAI,EAAE,CAAC;IACjB,CAAC,CAAC,CAAC;IACH,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7C,OAAO,GAAG,CAAC;AACf,CAAC;AACD,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC"}
package/package.json CHANGED
@@ -1,34 +1,39 @@
1
1
  {
2
2
  "name": "@vertesia/tools-sdk",
3
- "version": "1.6.0-dev.20260923.085325Z",
3
+ "version": "1.6.0-dev.20261008.064737Z",
4
4
  "description": "Tools SDK - utilities for building remote tools",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
7
7
  "types": "./lib/index.d.ts",
8
8
  "files": [
9
9
  "lib",
10
- "src"
10
+ "src",
11
+ "!src/**/*.test.*"
11
12
  ],
12
13
  "license": "Apache-2.0",
13
14
  "exports": {
14
15
  ".": {
15
16
  "types": "./lib/index.d.ts",
16
17
  "default": "./lib/index.js"
18
+ },
19
+ "./sandbox": {
20
+ "types": "./lib/sandbox.d.ts",
21
+ "default": "./lib/sandbox.js"
17
22
  }
18
23
  },
19
24
  "devDependencies": {
20
- "@types/node": "^24.13.5",
25
+ "@types/node": "^24.19.0",
21
26
  "@vertesia/tsconfig": "0.1.0",
22
27
  "typescript": "7.0.2",
23
- "vitest": "5.0.1"
28
+ "vitest": "5.0.2"
24
29
  },
25
30
  "peerDependencies": {
26
- "hono": "^4.13.8"
31
+ "hono": "^4.13.13"
27
32
  },
28
33
  "dependencies": {
29
- "@llumiverse/common": "1.6.0-dev.20260923.085100Z",
30
- "@vertesia/client": "1.6.0-dev.20260923.085325Z",
31
- "@vertesia/common": "1.6.0-dev.20260923.085325Z",
34
+ "@llumiverse/common": "1.6.0-dev.20261008.064411Z",
35
+ "@vertesia/client": "1.6.0-dev.20261008.064737Z",
36
+ "@vertesia/common": "1.6.0-dev.20261008.064737Z",
32
37
  "jose": "^6.2.12",
33
38
  "zod": "4.6.5"
34
39
  },
@@ -46,13 +51,13 @@
46
51
  "agents",
47
52
  "typescript"
48
53
  ],
49
- "gitHead": "8732631fd8099bdcab4f5375da2309f3cff34e19",
54
+ "gitHead": "8ee3a33e9a7c3dbcd13937e971cbdcc5fad56717",
50
55
  "scripts": {
51
56
  "lint": "biome lint src",
52
57
  "test": "vitest run",
53
- "typecheck:test": "tsc -p tsconfig.test.json --noEmit",
58
+ "typecheck:test": "tsc --checkers 1 -p tsconfig.test.json --noEmit",
54
59
  "clean:lib": "rimraf ./lib ./tsconfig.tsbuildinfo",
55
- "build": "pnpm run clean:lib && tsc -p tsconfig.json",
60
+ "build": "pnpm run clean:lib && tsc --checkers 1 -p tsconfig.json",
56
61
  "clean": "rimraf ./node_modules ./lib ./tsconfig.tsbuildinfo"
57
62
  }
58
63
  }
@@ -1,5 +1,3 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
- import { join } from 'node:path';
3
1
  import type { ToolDefinition } from '@llumiverse/common';
4
2
  import type { AgentToolDefinition } from '@vertesia/common';
5
3
  import type { Context } from 'hono';
@@ -427,64 +425,3 @@ export async function loadSkillFromFile(
427
425
  const contentType: SkillContentType = filePath.endsWith('.jst') ? 'jst' : 'md';
428
426
  return parseSkillFile(content, contentType);
429
427
  }
430
-
431
- /**
432
- * Load all skills from a directory.
433
- * Scans for subdirectories containing SKILL.md files.
434
- *
435
- * Directory structure:
436
- * ```
437
- * skills/
438
- * nagare/
439
- * fund-onboarding/
440
- * SKILL.md
441
- * monte-carlo/
442
- * SKILL.md
443
- * ```
444
- *
445
- * @param dirPath - Path to the skills collection directory
446
- * @returns Array of parsed skill definitions
447
- */
448
- export function loadSkillsFromDirectory(dirPath: string): SkillDefinition[] {
449
- const skills: SkillDefinition[] = [];
450
-
451
- let entries: string[];
452
- try {
453
- entries = readdirSync(dirPath);
454
- } catch {
455
- console.warn(`Could not read skills directory: ${dirPath}`);
456
- return skills;
457
- }
458
-
459
- for (const entry of entries) {
460
- const entryPath = join(dirPath, entry);
461
-
462
- try {
463
- const stat = statSync(entryPath);
464
- if (!stat.isDirectory()) continue;
465
-
466
- // Look for SKILL.md or SKILL.jst
467
- const mdPath = join(entryPath, 'SKILL.md');
468
- const jstPath = join(entryPath, 'SKILL.jst');
469
-
470
- let content: string | undefined;
471
- let contentType: SkillContentType = 'md';
472
-
473
- if (existsSync(mdPath)) {
474
- content = readFileSync(mdPath, 'utf-8');
475
- contentType = 'md';
476
- } else if (existsSync(jstPath)) {
477
- content = readFileSync(jstPath, 'utf-8');
478
- contentType = 'jst';
479
- }
480
-
481
- if (content) {
482
- skills.push(parseSkillFile(content, contentType));
483
- }
484
- } catch (err) {
485
- console.warn(`Error loading skill from ${entryPath}:`, err);
486
- }
487
- }
488
-
489
- return skills;
490
- }
@@ -1,6 +1,3 @@
1
- import { existsSync, readdirSync, statSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { pathToFileURL } from 'node:url';
4
1
  import type { AgentToolDefinition } from '@vertesia/common';
5
2
  import type { Context } from 'hono';
6
3
  import { HTTPException } from 'hono/http-exception';
@@ -158,68 +155,3 @@ function readPayload(ctx: Context): ToolExecutionPayload {
158
155
  'Invalid or missing tool execution payload. Expected { tool_use: { id, tool_name, tool_input? }, metadata? }',
159
156
  });
160
157
  }
161
-
162
- /**
163
- * Load all tools from a directory.
164
- * Scans for .js files and imports tools that match naming convention.
165
- *
166
- * Directory structure:
167
- * ```
168
- * collection/
169
- * tools/
170
- * SearchFundsTool.js # exports SearchFundsTool
171
- * GetFundDetailsTool.js # exports GetFundDetailsTool
172
- * ```
173
- *
174
- * Naming convention: File should export a Tool with name matching *Tool pattern.
175
- *
176
- * @param toolsDir - Path to the tools directory (e.g., /path/to/collection/tools)
177
- * @returns Promise resolving to array of Tool objects
178
- */
179
- export async function loadToolsFromDirectory(toolsDir: string): Promise<Tool[]> {
180
- const tools: Tool[] = [];
181
-
182
- if (!existsSync(toolsDir)) {
183
- console.warn(`Tools directory not found: ${toolsDir}`);
184
- return tools;
185
- }
186
-
187
- let entries: string[];
188
- try {
189
- entries = readdirSync(toolsDir);
190
- } catch {
191
- console.warn(`Could not read tools directory: ${toolsDir}`);
192
- return tools;
193
- }
194
-
195
- for (const entry of entries) {
196
- // Only process .js and .ts files that end with Tool
197
- if (!entry.endsWith('Tool.js') && !entry.endsWith('Tool.ts')) continue;
198
- if (entry.endsWith('.d.ts')) continue;
199
-
200
- const entryPath = join(toolsDir, entry);
201
-
202
- try {
203
- const stat = statSync(entryPath);
204
- if (!stat.isFile()) continue;
205
-
206
- // Dynamic import - need file:// URL for ESM
207
- const fileUrl = pathToFileURL(entryPath).href;
208
- const module = await import(fileUrl);
209
-
210
- // Find exported Tool (named export matching filename or any Tool export)
211
- const baseName = entry.replace(/\.(js|ts)$/, '');
212
- const tool = module[baseName] || module.default;
213
-
214
- if (tool && typeof tool.name === 'string' && typeof tool.run === 'function') {
215
- tools.push(tool);
216
- } else {
217
- console.warn(`No valid Tool export found in ${entry}`);
218
- }
219
- } catch (err) {
220
- console.warn(`Error loading tool from ${entry}:`, err);
221
- }
222
- }
223
-
224
- return tools;
225
- }
package/src/auth.ts CHANGED
@@ -49,7 +49,22 @@ export interface ToolContext {
49
49
  runId?: string;
50
50
  }
51
51
 
52
+ /** Sandbox authorization never accepts a credential supplied by the guest request. */
53
+ export async function authorizeSandbox(
54
+ ctx: Context,
55
+ _endpointOverrides?: EndpointOverrides,
56
+ _toolContext?: ToolContext,
57
+ ) {
58
+ const session = ctx.get('toolAuthSession') as AuthSession | undefined;
59
+ if (!session) throw new HTTPException(401, { message: 'Missing sandbox host session' });
60
+ return session;
61
+ }
62
+
52
63
  export async function authorize(ctx: Context, endpointOverrides?: EndpointOverrides, toolContext?: ToolContext) {
64
+ if (ctx.get('toolAuthMode') === 'sandbox') return authorizeSandbox(ctx);
65
+ // A sandbox host can bind a session in application middleware. HTTP headers cannot set this value.
66
+ const boundSession = ctx.get('toolAuthSession') as AuthSession | undefined;
67
+ if (boundSession) return boundSession;
53
68
  const auth = ctx.req.header('Authorization');
54
69
  if (!auth) {
55
70
  throw new HTTPException(401, {
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export { AuthSession, authorize } from './auth.js';
3
3
  export * from './ContentTypesCollection.js';
4
4
  export { copyRuntimeAssets } from './copy-assets.js';
5
5
  export * from './InteractionCollection.js';
6
+ export { loadSkillsFromDirectory, loadToolsFromDirectory } from './node-loaders.js';
6
7
  export * from './RenderingTemplateCollection.js';
7
8
  export * from './SkillCollection.js';
8
9
  export type { BuildAppPackageOptions } from './server/app-package.js';
@@ -0,0 +1,131 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { parseSkillFile } from './SkillCollection.js';
5
+ import type { SkillContentType, SkillDefinition, Tool } from './types.js';
6
+
7
+ /**
8
+ * Load all tools from a directory.
9
+ * Scans for .js files and imports tools that match naming convention.
10
+ *
11
+ * Directory structure:
12
+ * ```
13
+ * collection/
14
+ * tools/
15
+ * SearchFundsTool.js # exports SearchFundsTool
16
+ * GetFundDetailsTool.js # exports GetFundDetailsTool
17
+ * ```
18
+ *
19
+ * Naming convention: File should export a Tool with name matching *Tool pattern.
20
+ *
21
+ * @param toolsDir - Path to the tools directory (e.g., /path/to/collection/tools)
22
+ * @returns Promise resolving to array of Tool objects
23
+ */
24
+ export async function loadToolsFromDirectory(toolsDir: string): Promise<Tool[]> {
25
+ const tools: Tool[] = [];
26
+
27
+ if (!existsSync(toolsDir)) {
28
+ console.warn(`Tools directory not found: ${toolsDir}`);
29
+ return tools;
30
+ }
31
+
32
+ let entries: string[];
33
+ try {
34
+ entries = readdirSync(toolsDir);
35
+ } catch {
36
+ console.warn(`Could not read tools directory: ${toolsDir}`);
37
+ return tools;
38
+ }
39
+
40
+ for (const entry of entries) {
41
+ // Only process .js and .ts files that end with Tool
42
+ if (!entry.endsWith('Tool.js') && !entry.endsWith('Tool.ts')) continue;
43
+ if (entry.endsWith('.d.ts')) continue;
44
+
45
+ const entryPath = join(toolsDir, entry);
46
+
47
+ try {
48
+ const stat = statSync(entryPath);
49
+ if (!stat.isFile()) continue;
50
+
51
+ // Dynamic import - need file:// URL for ESM
52
+ const fileUrl = pathToFileURL(entryPath).href;
53
+ const module = await import(fileUrl);
54
+
55
+ // Find exported Tool (named export matching filename or any Tool export)
56
+ const baseName = entry.replace(/\.(js|ts)$/, '');
57
+ const tool = module[baseName] || module.default;
58
+
59
+ if (tool && typeof tool.name === 'string' && typeof tool.run === 'function') {
60
+ tools.push(tool);
61
+ } else {
62
+ console.warn(`No valid Tool export found in ${entry}`);
63
+ }
64
+ } catch (err) {
65
+ console.warn(`Error loading tool from ${entry}:`, err);
66
+ }
67
+ }
68
+
69
+ return tools;
70
+ }
71
+
72
+ /**
73
+ * Load all skills from a directory.
74
+ * Scans for subdirectories containing SKILL.md files.
75
+ *
76
+ * Directory structure:
77
+ * ```
78
+ * skills/
79
+ * nagare/
80
+ * fund-onboarding/
81
+ * SKILL.md
82
+ * monte-carlo/
83
+ * SKILL.md
84
+ * ```
85
+ *
86
+ * @param dirPath - Path to the skills collection directory
87
+ * @returns Array of parsed skill definitions
88
+ */
89
+ export function loadSkillsFromDirectory(dirPath: string): SkillDefinition[] {
90
+ const skills: SkillDefinition[] = [];
91
+
92
+ let entries: string[];
93
+ try {
94
+ entries = readdirSync(dirPath);
95
+ } catch {
96
+ console.warn(`Could not read skills directory: ${dirPath}`);
97
+ return skills;
98
+ }
99
+
100
+ for (const entry of entries) {
101
+ const entryPath = join(dirPath, entry);
102
+
103
+ try {
104
+ const stat = statSync(entryPath);
105
+ if (!stat.isDirectory()) continue;
106
+
107
+ // Look for SKILL.md or SKILL.jst
108
+ const mdPath = join(entryPath, 'SKILL.md');
109
+ const jstPath = join(entryPath, 'SKILL.jst');
110
+
111
+ let content: string | undefined;
112
+ let contentType: SkillContentType = 'md';
113
+
114
+ if (existsSync(mdPath)) {
115
+ content = readFileSync(mdPath, 'utf-8');
116
+ contentType = 'md';
117
+ } else if (existsSync(jstPath)) {
118
+ content = readFileSync(jstPath, 'utf-8');
119
+ contentType = 'jst';
120
+ }
121
+
122
+ if (content) {
123
+ skills.push(parseSkillFile(content, contentType));
124
+ }
125
+ } catch (err) {
126
+ console.warn(`Error loading skill from ${entryPath}:`, err);
127
+ }
128
+ }
129
+
130
+ return skills;
131
+ }
package/src/sandbox.ts ADDED
@@ -0,0 +1,44 @@
1
+ import type { AuthTokenPayload } from '@vertesia/common';
2
+ import { Hono } from 'hono';
3
+ import { AuthSession } from './auth.js';
4
+ import type { ToolServerConfig } from './server/types.js';
5
+ import { createToolServer as createBaseToolServer } from './server.js';
6
+
7
+ /** Browser-bundle entrypoint. Collections take bundled data; directory loaders are Node-only. */
8
+ export * from './ActivityCollection.js';
9
+ export { AuthSession, authorizeSandbox as authorize } from './auth.js';
10
+ export * from './ContentTypesCollection.js';
11
+ export * from './InteractionCollection.js';
12
+ export * from './RenderingTemplateCollection.js';
13
+ export * from './SkillCollection.js';
14
+ export type { BuildAppPackageOptions } from './server/app-package.js';
15
+ export { buildAppPackage } from './server/app-package.js';
16
+ export * from './server/types.js';
17
+ /** The host supplies non-secret claims; SDK HTTP goes through the sandbox's host-mediated fetch. */
18
+ export function createToolServer(config: ToolServerConfig) {
19
+ const app = new Hono<{
20
+ Bindings: { sandboxSession?: AuthTokenPayload };
21
+ Variables: { toolAuthSession: AuthSession; toolAuthMode: 'sandbox' };
22
+ }>();
23
+ app.use('*', async (ctx, next) => {
24
+ ctx.set('toolAuthMode', 'sandbox');
25
+ const payload = ctx.env?.sandboxSession;
26
+ if (payload) {
27
+ ctx.set(
28
+ 'toolAuthSession',
29
+ new AuthSession('sandbox', payload, {
30
+ studio: 'https://platform.invalid/studio',
31
+ store: 'https://platform.invalid/store',
32
+ token: 'https://platform.invalid/token',
33
+ }),
34
+ );
35
+ }
36
+ await next();
37
+ });
38
+ app.route('/', createBaseToolServer(config));
39
+ return app;
40
+ }
41
+ export * from './site/templates.js';
42
+ export * from './ToolCollection.js';
43
+ export * from './ToolRegistry.js';
44
+ export * from './types.js';
@@ -1,162 +0,0 @@
1
- import type { RemoteActivityExecutionPayload } from '@vertesia/common';
2
- import type { Context } from 'hono';
3
- import { beforeEach, describe, expect, it, vi } from 'vitest';
4
- import { ActivityCollection, type ActivityDefinition } from './ActivityCollection.js';
5
-
6
- type MockContext = Context & {
7
- json: ReturnType<typeof vi.fn>;
8
- req: { header: ReturnType<typeof vi.fn>; url: string };
9
- set: ReturnType<typeof vi.fn>;
10
- };
11
-
12
- vi.mock('./auth.js', () => ({
13
- authorize: vi.fn().mockResolvedValue({ token: 'test-token' }),
14
- }));
15
-
16
- const mockActivity: ActivityDefinition = {
17
- name: 'analyze_sentiment',
18
- title: 'Analyze Sentiment',
19
- description: 'Analyzes text sentiment',
20
- input_schema: {
21
- type: 'object',
22
- properties: { text: { type: 'string' } },
23
- required: ['text'],
24
- },
25
- output_schema: {
26
- type: 'object',
27
- properties: { score: { type: 'number' } },
28
- },
29
- run: vi.fn().mockResolvedValue({ score: 0.95 }),
30
- };
31
-
32
- const mockFailingActivity: ActivityDefinition = {
33
- name: 'fail_always',
34
- description: 'Always fails',
35
- run: vi.fn().mockRejectedValue(new Error('Intentional failure')),
36
- };
37
-
38
- function createCollection(activities: ActivityDefinition[] = [mockActivity]) {
39
- return new ActivityCollection({
40
- name: 'test-collection',
41
- title: 'Test Collection',
42
- description: 'A test collection',
43
- activities,
44
- });
45
- }
46
-
47
- beforeEach(() => {
48
- vi.clearAllMocks();
49
- });
50
-
51
- describe('ActivityCollection', () => {
52
- describe('getActivityDefinitions', () => {
53
- it('returns correct metadata for all registered activities', () => {
54
- const coll = createCollection([mockActivity, mockFailingActivity]);
55
- const defs = coll.getActivityDefinitions();
56
-
57
- expect(defs).toHaveLength(2);
58
- expect(defs[0]).toEqual({
59
- name: 'analyze_sentiment',
60
- title: 'Analyze Sentiment',
61
- description: 'Analyzes text sentiment',
62
- input_schema: mockActivity.input_schema,
63
- output_schema: mockActivity.output_schema,
64
- });
65
- expect(defs[1]).toEqual({
66
- name: 'fail_always',
67
- title: undefined,
68
- description: 'Always fails',
69
- input_schema: undefined,
70
- output_schema: undefined,
71
- });
72
- });
73
-
74
- it('returns empty array when no activities registered', () => {
75
- const coll = createCollection([]);
76
- expect(coll.getActivityDefinitions()).toEqual([]);
77
- });
78
- });
79
-
80
- describe('getActivity', () => {
81
- it('returns the activity by name', () => {
82
- const coll = createCollection();
83
- const activity = coll.getActivity('analyze_sentiment');
84
- expect(activity.name).toBe('analyze_sentiment');
85
- expect(activity.run).toBeDefined();
86
- });
87
-
88
- it('throws 404 for unknown activity name', () => {
89
- const coll = createCollection();
90
- expect(() => coll.getActivity('nonexistent')).toThrow();
91
- });
92
- });
93
-
94
- describe('iterator', () => {
95
- it('iterates over all activities', () => {
96
- const coll = createCollection([mockActivity, mockFailingActivity]);
97
- const names = [...coll].map((a) => a.name);
98
- expect(names).toEqual(['analyze_sentiment', 'fail_always']);
99
- });
100
- });
101
-
102
- describe('execute', () => {
103
- const createMockContext = (): MockContext => {
104
- const jsonFn = vi.fn().mockReturnThis();
105
- return {
106
- json: jsonFn,
107
- req: {
108
- header: vi.fn().mockReturnValue('Bearer test-token'),
109
- url: 'http://localhost/api/activities',
110
- },
111
- set: vi.fn(),
112
- } as unknown as MockContext;
113
- };
114
-
115
- const payload: RemoteActivityExecutionPayload = {
116
- activity_name: 'analyze_sentiment',
117
- params: { text: 'Hello world' },
118
- metadata: {
119
- app_install_id: 'install-1',
120
- },
121
- };
122
-
123
- it('calls the correct activity handler and returns result', async () => {
124
- const coll = createCollection();
125
- const ctx = createMockContext();
126
-
127
- await coll.execute(ctx, payload);
128
-
129
- expect(mockActivity.run).toHaveBeenCalledWith(payload, expect.objectContaining({ token: 'test-token' }));
130
- expect(ctx.json).toHaveBeenCalledWith({
131
- result: { score: 0.95 },
132
- is_error: false,
133
- });
134
- });
135
-
136
- it('returns is_error with message when activity throws', async () => {
137
- const coll = createCollection([mockFailingActivity]);
138
- const ctx = createMockContext();
139
-
140
- const failPayload: RemoteActivityExecutionPayload = {
141
- ...payload,
142
- activity_name: 'fail_always',
143
- };
144
-
145
- await coll.execute(ctx, failPayload);
146
-
147
- expect(ctx.json).toHaveBeenCalledWith({ result: null, is_error: true, error: 'Intentional failure' }, 500);
148
- });
149
-
150
- it('throws 404 when activity_name is not found', async () => {
151
- const coll = createCollection();
152
- const ctx = createMockContext();
153
-
154
- const unknownPayload: RemoteActivityExecutionPayload = {
155
- ...payload,
156
- activity_name: 'nonexistent',
157
- };
158
-
159
- await expect(coll.execute(ctx, unknownPayload)).rejects.toThrow();
160
- });
161
- });
162
- });