monomind 1.18.4 → 1.18.6
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/package.json +4 -2
- package/packages/@monomind/cli/package.json +8 -3
- package/packages/@monomind/cli/dist/src/__tests__/._browse-adapters.test.d.ts +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-adapters.test.js +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-analyzer.test.d.ts +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-analyzer.test.js +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-builtin-handlers.test.d.ts +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-builtin-handlers.test.js +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-cdp.test.d.ts +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-cdp.test.js +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-dashboard.test.d.ts +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-dashboard.test.js +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-engine.test.d.ts +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-engine.test.js +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-expression.test.d.ts +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-expression.test.js +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-store.test.d.ts +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-store.test.js +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-workflow-types.test.d.ts +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/._browse-workflow-types.test.js +0 -0
- package/packages/@monomind/cli/dist/src/__tests__/browse-adapters.test.d.ts +0 -2
- package/packages/@monomind/cli/dist/src/__tests__/browse-adapters.test.js +0 -51
- package/packages/@monomind/cli/dist/src/__tests__/browse-analyzer.test.d.ts +0 -2
- package/packages/@monomind/cli/dist/src/__tests__/browse-analyzer.test.js +0 -68
- package/packages/@monomind/cli/dist/src/__tests__/browse-builtin-handlers.test.d.ts +0 -2
- package/packages/@monomind/cli/dist/src/__tests__/browse-builtin-handlers.test.js +0 -139
- package/packages/@monomind/cli/dist/src/__tests__/browse-cdp.test.d.ts +0 -2
- package/packages/@monomind/cli/dist/src/__tests__/browse-cdp.test.js +0 -169
- package/packages/@monomind/cli/dist/src/__tests__/browse-dashboard.test.d.ts +0 -2
- package/packages/@monomind/cli/dist/src/__tests__/browse-dashboard.test.js +0 -179
- package/packages/@monomind/cli/dist/src/__tests__/browse-engine.test.d.ts +0 -2
- package/packages/@monomind/cli/dist/src/__tests__/browse-engine.test.js +0 -122
- package/packages/@monomind/cli/dist/src/__tests__/browse-expression.test.d.ts +0 -2
- package/packages/@monomind/cli/dist/src/__tests__/browse-expression.test.js +0 -54
- package/packages/@monomind/cli/dist/src/__tests__/browse-store.test.d.ts +0 -2
- package/packages/@monomind/cli/dist/src/__tests__/browse-store.test.js +0 -99
- package/packages/@monomind/cli/dist/src/__tests__/browse-workflow-types.test.d.ts +0 -2
- package/packages/@monomind/cli/dist/src/__tests__/browse-workflow-types.test.js +0 -33
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "monomind",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.6",
|
|
4
4
|
"description": "Monomind - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
"packages/@monomind/cli/dist/**/*.json",
|
|
26
26
|
"packages/@monomind/cli/dist/**/*.svg",
|
|
27
27
|
"packages/@monomind/cli/dist/**/*.d.ts",
|
|
28
|
+
"!packages/@monomind/cli/dist/**/*.test.js",
|
|
29
|
+
"!packages/@monomind/cli/dist/**/__tests__/**",
|
|
28
30
|
"!packages/@monomind/cli/dist/**/*.map",
|
|
29
31
|
"packages/@monomind/cli/bundled-graph/**",
|
|
30
32
|
"packages/@monomind/cli/scripts/**/*.mjs",
|
|
@@ -50,7 +52,7 @@
|
|
|
50
52
|
"semver": "^7.6.0",
|
|
51
53
|
"zod": "^3.22.0",
|
|
52
54
|
"@monoes/monobrowse": "^1.0.0",
|
|
53
|
-
"@monoes/monograph": "1.2.
|
|
55
|
+
"@monoes/monograph": "1.2.10"
|
|
54
56
|
},
|
|
55
57
|
"overrides": {
|
|
56
58
|
"hono": ">=4.12.21",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monomindcli",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Monomind CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -66,6 +66,9 @@
|
|
|
66
66
|
},
|
|
67
67
|
"files": [
|
|
68
68
|
"dist",
|
|
69
|
+
"!dist/**/*.test.js",
|
|
70
|
+
"!dist/**/*.test.d.ts",
|
|
71
|
+
"!dist/**/__tests__/**",
|
|
69
72
|
"bin",
|
|
70
73
|
".claude",
|
|
71
74
|
"scripts",
|
|
@@ -88,7 +91,8 @@
|
|
|
88
91
|
"@noble/ed25519": "^2.1.0",
|
|
89
92
|
"@monoes/monograph": "workspace:*",
|
|
90
93
|
"@monoes/monobrowse": "^1.0.1",
|
|
91
|
-
"
|
|
94
|
+
"zod": "^3.25.0",
|
|
95
|
+
"yaml": "^2.8.0"
|
|
92
96
|
},
|
|
93
97
|
"optionalDependencies": {
|
|
94
98
|
"sql.js": "^1.14.1",
|
|
@@ -98,7 +102,8 @@
|
|
|
98
102
|
"@monomind/hooks": "*",
|
|
99
103
|
"@monomind/guidance": "*",
|
|
100
104
|
"@monomind/mcp": "*",
|
|
101
|
-
"@monomind/routing": "*"
|
|
105
|
+
"@monomind/routing": "*",
|
|
106
|
+
"@monomind/security": "workspace:*"
|
|
102
107
|
},
|
|
103
108
|
"publishConfig": {
|
|
104
109
|
"access": "public",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// src/__tests__/browse-adapters.test.ts
|
|
2
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
-
import { getAdapter, adapters } from '@monoes/monobrowse';
|
|
4
|
-
function mockPage(isLoggedIn, username = 'testuser') {
|
|
5
|
-
return {
|
|
6
|
-
evaluate: vi.fn().mockImplementation((expr) => {
|
|
7
|
-
if (expr.includes('querySelector'))
|
|
8
|
-
return Promise.resolve(isLoggedIn ? true : false);
|
|
9
|
-
return Promise.resolve(username);
|
|
10
|
-
}),
|
|
11
|
-
url: vi.fn().mockResolvedValue('https://example.com'),
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
describe('adapter registry', () => {
|
|
15
|
-
it('has all 6 platforms', () => {
|
|
16
|
-
expect(adapters.size).toBe(6);
|
|
17
|
-
for (const p of ['linkedin', 'instagram', 'x', 'gemini', 'google', 'microsoft']) {
|
|
18
|
-
expect(adapters.has(p)).toBe(true);
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
it('getAdapter throws for unknown platform', () => {
|
|
22
|
-
expect(() => getAdapter('myspace')).toThrow('Unknown platform');
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
describe('each adapter', () => {
|
|
26
|
-
const platforms = ['linkedin', 'instagram', 'x', 'gemini', 'google', 'microsoft'];
|
|
27
|
-
for (const platform of platforms) {
|
|
28
|
-
describe(platform, () => {
|
|
29
|
-
const adapter = getAdapter(platform);
|
|
30
|
-
it('has required fields', () => {
|
|
31
|
-
expect(typeof adapter.platform).toBe('string');
|
|
32
|
-
expect(adapter.baseURL).toMatch(/^https?:\/\//);
|
|
33
|
-
expect(Array.isArray(adapter.reservedPaths)).toBe(true);
|
|
34
|
-
expect(adapter.reservedPaths.length).toBeGreaterThan(0);
|
|
35
|
-
expect(typeof adapter.loginURL()).toBe('string');
|
|
36
|
-
});
|
|
37
|
-
it('isLoggedIn returns boolean', async () => {
|
|
38
|
-
const page = mockPage(true);
|
|
39
|
-
const result = await adapter.isLoggedIn(page);
|
|
40
|
-
expect(typeof result).toBe('boolean');
|
|
41
|
-
});
|
|
42
|
-
it('extractUsername returns string', async () => {
|
|
43
|
-
const page = mockPage(true);
|
|
44
|
-
page.evaluate = vi.fn().mockResolvedValue('johndoe');
|
|
45
|
-
const username = await adapter.extractUsername(page);
|
|
46
|
-
expect(typeof username).toBe('string');
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
//# sourceMappingURL=browse-adapters.test.js.map
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { EventEmitter } from 'events';
|
|
3
|
-
// Mock child_process.spawn so tests don't invoke the real claude CLI
|
|
4
|
-
vi.mock('child_process', async (importOriginal) => {
|
|
5
|
-
const actual = await importOriginal();
|
|
6
|
-
return { ...actual, spawn: vi.fn() };
|
|
7
|
-
});
|
|
8
|
-
async function makeSpawnMock(stdout, exitCode = 0) {
|
|
9
|
-
const { spawn } = vi.mocked(await import('child_process'));
|
|
10
|
-
spawn.mockImplementationOnce(() => {
|
|
11
|
-
const proc = new EventEmitter();
|
|
12
|
-
proc.stdout = new EventEmitter();
|
|
13
|
-
proc.stderr = new EventEmitter();
|
|
14
|
-
proc.kill = vi.fn();
|
|
15
|
-
setTimeout(() => {
|
|
16
|
-
proc.stdout.emit('data', Buffer.from(stdout));
|
|
17
|
-
proc.emit('close', exitCode);
|
|
18
|
-
}, 0);
|
|
19
|
-
return proc;
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
const VALID_ACTION_DEF = {
|
|
23
|
-
id: 'linkedin:comment_post',
|
|
24
|
-
platform: 'linkedin',
|
|
25
|
-
name: 'Comment on Post',
|
|
26
|
-
params: ['post_url', 'text'],
|
|
27
|
-
steps: [
|
|
28
|
-
{ type: 'navigate', url: '{{params.post_url}}' },
|
|
29
|
-
{ type: 'find', selectors: ['.comment-box'], as: 'box' },
|
|
30
|
-
{ type: 'click', target: '{{box}}' },
|
|
31
|
-
{ type: 'type', target: '{{box}}', text: '{{params.text}}', humanDelay: true },
|
|
32
|
-
{ type: 'wait', condition: 'network_idle', timeout: 3000 },
|
|
33
|
-
],
|
|
34
|
-
};
|
|
35
|
-
function mockPage(url = 'https://linkedin.com/feed', title = 'LinkedIn') {
|
|
36
|
-
return {
|
|
37
|
-
url: vi.fn().mockResolvedValue(url),
|
|
38
|
-
evaluate: vi.fn().mockImplementation((expr) => {
|
|
39
|
-
if (expr === 'document.title')
|
|
40
|
-
return Promise.resolve(title);
|
|
41
|
-
return Promise.resolve('[]');
|
|
42
|
-
}),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
describe('analyzePageForAction', () => {
|
|
46
|
-
let analyzePageForAction;
|
|
47
|
-
beforeEach(async () => {
|
|
48
|
-
vi.resetModules();
|
|
49
|
-
const mod = await import('@monoes/monobrowse');
|
|
50
|
-
analyzePageForAction = mod.analyzePageForAction;
|
|
51
|
-
}, 20000); // monolean: re-importing the workspace package can exceed the 10s default under full-suite parallel load
|
|
52
|
-
it('returns a valid ActionDef from mocked claude --print response', async () => {
|
|
53
|
-
await makeSpawnMock(JSON.stringify(VALID_ACTION_DEF));
|
|
54
|
-
const result = await analyzePageForAction(mockPage(), 'comment on a LinkedIn post');
|
|
55
|
-
expect(result.id).toBe('linkedin:comment_post');
|
|
56
|
-
expect(result.steps).toHaveLength(5);
|
|
57
|
-
expect(result.params).toContain('text');
|
|
58
|
-
});
|
|
59
|
-
it('throws on invalid JSON from claude', async () => {
|
|
60
|
-
await makeSpawnMock('not json at all');
|
|
61
|
-
await expect(analyzePageForAction(mockPage(), 'test')).rejects.toThrow('invalid JSON');
|
|
62
|
-
});
|
|
63
|
-
it('throws when ActionDef is missing id', async () => {
|
|
64
|
-
await makeSpawnMock(JSON.stringify({ steps: [] }));
|
|
65
|
-
await expect(analyzePageForAction(mockPage(), 'test')).rejects.toThrow('invalid ActionDef');
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
//# sourceMappingURL=browse-analyzer.test.js.map
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
// Mock fs/promises before importing the module under test
|
|
3
|
-
vi.mock('node:fs/promises', () => ({
|
|
4
|
-
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
5
|
-
readFile: vi.fn().mockRejectedValue(new Error('no file')),
|
|
6
|
-
mkdir: vi.fn().mockResolvedValue(undefined),
|
|
7
|
-
chmod: vi.fn().mockResolvedValue(undefined),
|
|
8
|
-
}));
|
|
9
|
-
vi.mock('node:fs', () => ({
|
|
10
|
-
existsSync: vi.fn().mockReturnValue(false),
|
|
11
|
-
}));
|
|
12
|
-
// Mock the browser module so gemini_image CDP path returns null immediately
|
|
13
|
-
vi.mock('../browser/index.js', () => ({
|
|
14
|
-
connectToTarget: vi.fn().mockRejectedValue(new Error('no browser')),
|
|
15
|
-
setCookies: vi.fn(),
|
|
16
|
-
openUrl: vi.fn(),
|
|
17
|
-
findByRole: vi.fn(),
|
|
18
|
-
findBySelector: vi.fn(),
|
|
19
|
-
clickElement: vi.fn(),
|
|
20
|
-
fillElement: vi.fn(),
|
|
21
|
-
pressKey: vi.fn(),
|
|
22
|
-
evaluateJs: vi.fn(),
|
|
23
|
-
getCookies: vi.fn().mockResolvedValue([]),
|
|
24
|
-
getCurrentUrl: vi.fn().mockResolvedValue(''),
|
|
25
|
-
captureScreenshot: vi.fn().mockResolvedValue(null),
|
|
26
|
-
}));
|
|
27
|
-
import { createBuiltinHandlers } from '@monoes/monobrowse';
|
|
28
|
-
import * as fsMock from 'node:fs/promises';
|
|
29
|
-
function makeItem(data = {}) {
|
|
30
|
-
return { data };
|
|
31
|
-
}
|
|
32
|
-
describe('createBuiltinHandlers', () => {
|
|
33
|
-
it('returns a Map with the expected handler keys', () => {
|
|
34
|
-
const handlers = createBuiltinHandlers();
|
|
35
|
-
expect(handlers).toBeInstanceOf(Map);
|
|
36
|
-
expect(handlers.has('action.http')).toBe(true);
|
|
37
|
-
expect(handlers.has('action.save_file')).toBe(true);
|
|
38
|
-
expect(handlers.has('action.log')).toBe(true);
|
|
39
|
-
expect(handlers.has('action.gemini_image')).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
describe('action.save_file', () => {
|
|
43
|
-
let handlers;
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
vi.clearAllMocks();
|
|
46
|
-
// existsSync stays false — no sessions file
|
|
47
|
-
handlers = createBuiltinHandlers();
|
|
48
|
-
});
|
|
49
|
-
it('writes a file for a safe relative path', async () => {
|
|
50
|
-
const handler = handlers.get('action.save_file');
|
|
51
|
-
const items = [makeItem({ hello: 'world' })];
|
|
52
|
-
const result = await handler(items, { path: 'output.txt' });
|
|
53
|
-
expect(fsMock.writeFile).toHaveBeenCalled();
|
|
54
|
-
expect(result[0].data.savedPath).toMatch(/output\.txt$/);
|
|
55
|
-
});
|
|
56
|
-
it('throws "Path traversal blocked" for a path that escapes cwd', async () => {
|
|
57
|
-
const handler = handlers.get('action.save_file');
|
|
58
|
-
const items = [makeItem()];
|
|
59
|
-
await expect(handler(items, { path: '../../etc/passwd' })).rejects.toThrow('Path traversal blocked');
|
|
60
|
-
});
|
|
61
|
-
it('writes binary content when item.binaryBase64 is set', async () => {
|
|
62
|
-
const handler = handlers.get('action.save_file');
|
|
63
|
-
const binaryItem = { data: {}, binaryBase64: Buffer.from('abc').toString('base64') };
|
|
64
|
-
const result = await handler([binaryItem], { path: 'img.png' });
|
|
65
|
-
expect(fsMock.writeFile).toHaveBeenCalled();
|
|
66
|
-
const [, content] = vi.mocked(fsMock.writeFile).mock.calls[0];
|
|
67
|
-
expect(Buffer.isBuffer(content)).toBe(true);
|
|
68
|
-
expect(result[0].data.savedPath).toMatch(/img\.png$/);
|
|
69
|
-
});
|
|
70
|
-
it('uses config.content when no field is provided', async () => {
|
|
71
|
-
const handler = handlers.get('action.save_file');
|
|
72
|
-
const items = [makeItem()];
|
|
73
|
-
const result = await handler(items, { path: 'out.txt', content: 'hello content' });
|
|
74
|
-
const [, written] = vi.mocked(fsMock.writeFile).mock.calls[0];
|
|
75
|
-
expect(written).toBe('hello content');
|
|
76
|
-
expect(result[0].data.savedPath).toMatch(/out\.txt$/);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
describe('action.gemini_image', () => {
|
|
80
|
-
let handlers;
|
|
81
|
-
beforeEach(() => {
|
|
82
|
-
vi.clearAllMocks();
|
|
83
|
-
handlers = createBuiltinHandlers();
|
|
84
|
-
});
|
|
85
|
-
it('falls back to mock mode when browser and API key are unavailable', async () => {
|
|
86
|
-
const handler = handlers.get('action.gemini_image');
|
|
87
|
-
// No GEMINI_API_KEY set, browser import throws — mock mode
|
|
88
|
-
delete process.env['GEMINI_API_KEY'];
|
|
89
|
-
delete process.env['GOOGLE_API_KEY'];
|
|
90
|
-
const items = [makeItem({ prompt: 'a beautiful sunset' })];
|
|
91
|
-
const result = await handler(items, { prompt: 'a beautiful sunset', cdpPort: 19999 });
|
|
92
|
-
expect(result[0].data.mockMode).toBe(true);
|
|
93
|
-
expect(result[0].data.prompt).toBe('a beautiful sunset');
|
|
94
|
-
});
|
|
95
|
-
it('throws "Path traversal blocked" for outputPath outside cwd', async () => {
|
|
96
|
-
const handler = handlers.get('action.gemini_image');
|
|
97
|
-
delete process.env['GEMINI_API_KEY'];
|
|
98
|
-
delete process.env['GOOGLE_API_KEY'];
|
|
99
|
-
const items = [makeItem()];
|
|
100
|
-
await expect(handler(items, { prompt: 'test', outputPath: '../../evil.png', cdpPort: 19999 })).rejects.toThrow('Path traversal blocked');
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
describe('action.http', () => {
|
|
104
|
-
let handlers;
|
|
105
|
-
const originalFetch = globalThis.fetch;
|
|
106
|
-
beforeEach(() => {
|
|
107
|
-
handlers = createBuiltinHandlers();
|
|
108
|
-
});
|
|
109
|
-
afterEach(() => {
|
|
110
|
-
globalThis.fetch = originalFetch;
|
|
111
|
-
});
|
|
112
|
-
it('fetches a URL and stores response in item.data.response', async () => {
|
|
113
|
-
const mockFetch = vi.fn().mockResolvedValue({
|
|
114
|
-
ok: true,
|
|
115
|
-
status: 200,
|
|
116
|
-
text: async () => JSON.stringify({ success: true }),
|
|
117
|
-
});
|
|
118
|
-
globalThis.fetch = mockFetch;
|
|
119
|
-
const handler = handlers.get('action.http');
|
|
120
|
-
const items = [makeItem()];
|
|
121
|
-
const result = await handler(items, { url: 'https://example.com/api', method: 'GET' });
|
|
122
|
-
expect(mockFetch).toHaveBeenCalledWith('https://example.com/api', expect.objectContaining({ method: 'GET' }));
|
|
123
|
-
expect(result[0].data.response.statusCode).toBe(200);
|
|
124
|
-
expect(result[0].data.response.json).toEqual({ success: true });
|
|
125
|
-
});
|
|
126
|
-
it('uses a custom responseField when specified', async () => {
|
|
127
|
-
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
128
|
-
ok: true,
|
|
129
|
-
status: 201,
|
|
130
|
-
text: async () => 'created',
|
|
131
|
-
});
|
|
132
|
-
const handler = handlers.get('action.http');
|
|
133
|
-
const items = [makeItem()];
|
|
134
|
-
const result = await handler(items, { url: 'https://example.com', responseField: 'myResult' });
|
|
135
|
-
expect(result[0].data.myResult).toBeDefined();
|
|
136
|
-
expect(result[0].data.myResult.statusCode).toBe(201);
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
//# sourceMappingURL=browse-builtin-handlers.test.js.map
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
// ---------------------------------------------------------------------------
|
|
3
|
-
// Fake WebSocket — mimics the Node.js EventEmitter-style API that 'ws' uses
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
class FakeWS {
|
|
6
|
-
readyState = 1; // OPEN
|
|
7
|
-
sent = [];
|
|
8
|
-
_handlers = {};
|
|
9
|
-
on(event, fn) {
|
|
10
|
-
if (!this._handlers[event])
|
|
11
|
-
this._handlers[event] = [];
|
|
12
|
-
this._handlers[event].push(fn);
|
|
13
|
-
return this;
|
|
14
|
-
}
|
|
15
|
-
send(data, cb) {
|
|
16
|
-
this.sent.push(data);
|
|
17
|
-
cb?.();
|
|
18
|
-
}
|
|
19
|
-
close() {
|
|
20
|
-
this.emit('close');
|
|
21
|
-
}
|
|
22
|
-
emit(event, ...args) {
|
|
23
|
-
for (const fn of this._handlers[event] ?? [])
|
|
24
|
-
fn(...args);
|
|
25
|
-
}
|
|
26
|
-
/** Simulate a message arriving from the browser */
|
|
27
|
-
simulateMessage(obj) {
|
|
28
|
-
this.emit('message', JSON.stringify(obj));
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Module-level reference so test helpers can access the last FakeWS instance
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
let fakeWsInstance = null;
|
|
35
|
-
// We need WebSocket to be constructable. Use vi.fn() and expose a class-like
|
|
36
|
-
// constructor that captures the instance.
|
|
37
|
-
function FakeWebSocket(_url) {
|
|
38
|
-
fakeWsInstance = new FakeWS();
|
|
39
|
-
Object.assign(this, fakeWsInstance);
|
|
40
|
-
// Proxy the FakeWS methods onto `this`
|
|
41
|
-
for (const key of Object.getOwnPropertyNames(FakeWS.prototype)) {
|
|
42
|
-
if (key !== 'constructor') {
|
|
43
|
-
this[key] = fakeWsInstance[key].bind(fakeWsInstance);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
// Also expose sent and emit directly on `this`
|
|
47
|
-
Object.defineProperty(this, 'sent', {
|
|
48
|
-
get: () => fakeWsInstance.sent,
|
|
49
|
-
configurable: true,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
vi.mock('ws', () => ({
|
|
53
|
-
WebSocket: FakeWebSocket,
|
|
54
|
-
}));
|
|
55
|
-
import { CdpClient } from '@monoes/monobrowse';
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
|
-
// Helpers
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
|
-
function openClient() {
|
|
60
|
-
const client = new CdpClient();
|
|
61
|
-
client.connect('ws://localhost:9222/devtools/page/abc');
|
|
62
|
-
const ws = fakeWsInstance;
|
|
63
|
-
ws.emit('open');
|
|
64
|
-
return { client, ws };
|
|
65
|
-
}
|
|
66
|
-
async function connectedClient() {
|
|
67
|
-
const { client, ws } = openClient();
|
|
68
|
-
await Promise.resolve();
|
|
69
|
-
return { client, ws };
|
|
70
|
-
}
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// Tests
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
describe('CdpClient', () => {
|
|
75
|
-
beforeEach(() => {
|
|
76
|
-
fakeWsInstance = null;
|
|
77
|
-
vi.clearAllMocks();
|
|
78
|
-
});
|
|
79
|
-
it('connects and resolves when the WebSocket opens', async () => {
|
|
80
|
-
const client = new CdpClient();
|
|
81
|
-
const p = client.connect('ws://localhost:9222/devtools/page/abc');
|
|
82
|
-
fakeWsInstance.emit('open');
|
|
83
|
-
await expect(p).resolves.toBeUndefined();
|
|
84
|
-
expect(client.isConnected()).toBe(true);
|
|
85
|
-
});
|
|
86
|
-
it('rejects when the WebSocket emits an error before opening', async () => {
|
|
87
|
-
const client = new CdpClient();
|
|
88
|
-
const p = client.connect('ws://localhost:9222/devtools/page/abc');
|
|
89
|
-
const err = new Error('ECONNREFUSED');
|
|
90
|
-
fakeWsInstance.emit('error', err);
|
|
91
|
-
await expect(p).rejects.toThrow('ECONNREFUSED');
|
|
92
|
-
});
|
|
93
|
-
it('sends a command and resolves with the response result', async () => {
|
|
94
|
-
const { client, ws } = await connectedClient();
|
|
95
|
-
const sendPromise = client.send('Runtime.evaluate', { expression: '1+1' });
|
|
96
|
-
const sent = JSON.parse(ws.sent[0]);
|
|
97
|
-
expect(sent.method).toBe('Runtime.evaluate');
|
|
98
|
-
ws.simulateMessage({ id: sent.id, result: { value: 2 } });
|
|
99
|
-
const result = await sendPromise;
|
|
100
|
-
expect(result.value).toBe(2);
|
|
101
|
-
});
|
|
102
|
-
it('rejects a command when the response contains a CDP error', async () => {
|
|
103
|
-
const { client, ws } = await connectedClient();
|
|
104
|
-
const sendPromise = client.send('DOM.getDocument');
|
|
105
|
-
const sent = JSON.parse(ws.sent[0]);
|
|
106
|
-
ws.simulateMessage({ id: sent.id, error: { code: -32000, message: 'Not found' } });
|
|
107
|
-
await expect(sendPromise).rejects.toThrow('CDP error -32000: Not found');
|
|
108
|
-
});
|
|
109
|
-
it('rejects when not connected', async () => {
|
|
110
|
-
const client = new CdpClient();
|
|
111
|
-
await expect(client.send('Page.navigate')).rejects.toThrow('CDP not connected');
|
|
112
|
-
});
|
|
113
|
-
it('enforces 1000 in-flight command cap', async () => {
|
|
114
|
-
const { client, ws } = await connectedClient();
|
|
115
|
-
// Override send to avoid callbacks so commands stay pending
|
|
116
|
-
vi.spyOn(ws, 'send').mockImplementation((data) => {
|
|
117
|
-
ws.sent.push(data);
|
|
118
|
-
});
|
|
119
|
-
// Fill up to 1000 pending
|
|
120
|
-
for (let i = 0; i < 1000; i++) {
|
|
121
|
-
client.send('Page.dummy').catch(() => { });
|
|
122
|
-
}
|
|
123
|
-
// 1001st command should be rejected immediately
|
|
124
|
-
await expect(client.send('Page.overflow')).rejects.toThrow('queue full');
|
|
125
|
-
});
|
|
126
|
-
it('rejects all pending commands when the connection closes', async () => {
|
|
127
|
-
const { client, ws } = await connectedClient();
|
|
128
|
-
vi.spyOn(ws, 'send').mockImplementation((data) => {
|
|
129
|
-
ws.sent.push(data);
|
|
130
|
-
// no callback — keeps command in-flight
|
|
131
|
-
});
|
|
132
|
-
const pendingPromise = client.send('Page.navigate');
|
|
133
|
-
ws.emit('close');
|
|
134
|
-
await expect(pendingPromise).rejects.toThrow('CDP connection closed');
|
|
135
|
-
expect(client.isConnected()).toBe(false);
|
|
136
|
-
});
|
|
137
|
-
it('fires event listeners when a CDP event arrives', async () => {
|
|
138
|
-
const { client, ws } = await connectedClient();
|
|
139
|
-
const listener = vi.fn();
|
|
140
|
-
client.on('Page.loadEventFired', listener);
|
|
141
|
-
ws.simulateMessage({ method: 'Page.loadEventFired', params: { timestamp: 1.23 } });
|
|
142
|
-
expect(listener).toHaveBeenCalledWith({ timestamp: 1.23 }, undefined);
|
|
143
|
-
});
|
|
144
|
-
it('enforces 100 listener cap per event', async () => {
|
|
145
|
-
const { client } = await connectedClient();
|
|
146
|
-
for (let i = 0; i < 100; i++) {
|
|
147
|
-
client.on('Page.frameNavigated', vi.fn());
|
|
148
|
-
}
|
|
149
|
-
expect(() => client.on('Page.frameNavigated', vi.fn())).toThrow('CDP event listener limit reached');
|
|
150
|
-
});
|
|
151
|
-
it('close() rejects pending commands and disconnects', async () => {
|
|
152
|
-
const { client, ws } = await connectedClient();
|
|
153
|
-
vi.spyOn(ws, 'send').mockImplementation((data) => {
|
|
154
|
-
ws.sent.push(data);
|
|
155
|
-
});
|
|
156
|
-
const p = client.send('Network.enable');
|
|
157
|
-
client.close();
|
|
158
|
-
await expect(p).rejects.toThrow('CDP connection closed');
|
|
159
|
-
expect(client.isConnected()).toBe(false);
|
|
160
|
-
});
|
|
161
|
-
it('once() resolves on the first matching event', async () => {
|
|
162
|
-
const { client, ws } = await connectedClient();
|
|
163
|
-
const oncePromise = client.once('Page.loadEventFired');
|
|
164
|
-
ws.simulateMessage({ method: 'Page.loadEventFired', params: { timestamp: 99 } });
|
|
165
|
-
const params = await oncePromise;
|
|
166
|
-
expect(params).toEqual({ timestamp: 99 });
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
//# sourceMappingURL=browse-cdp.test.js.map
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
-
import { startDashboard, getDashboard } from '@monoes/monobrowse';
|
|
3
|
-
import { WebSocket } from 'ws';
|
|
4
|
-
const PORT = 14242; // Use non-conflicting test port
|
|
5
|
-
const SSE_PORT = 14243; // Separate port for SSE streaming tests
|
|
6
|
-
afterEach(() => {
|
|
7
|
-
getDashboard()?.close();
|
|
8
|
-
});
|
|
9
|
-
function makeEvent(overrides = {}) {
|
|
10
|
-
return {
|
|
11
|
-
runId: 'run-1',
|
|
12
|
-
playbookId: 'wf-1',
|
|
13
|
-
playbookName: 'Test WF',
|
|
14
|
-
nodeId: 'node-1',
|
|
15
|
-
nodeName: 'Test Node',
|
|
16
|
-
eventType: 'step_completed',
|
|
17
|
-
timestamp: Date.now(),
|
|
18
|
-
...overrides,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
describe('startDashboard', () => {
|
|
22
|
-
it('returns a dashboard server instance', async () => {
|
|
23
|
-
const dash = startDashboard(PORT);
|
|
24
|
-
expect(dash).toBeDefined();
|
|
25
|
-
expect(dash.port).toBe(PORT);
|
|
26
|
-
});
|
|
27
|
-
it('returns the same instance on repeated calls', async () => {
|
|
28
|
-
const a = startDashboard(PORT);
|
|
29
|
-
const b = startDashboard(PORT);
|
|
30
|
-
expect(a).toBe(b);
|
|
31
|
-
});
|
|
32
|
-
it('serves HTML at GET /', async () => {
|
|
33
|
-
startDashboard(PORT);
|
|
34
|
-
const res = await fetch(`http://localhost:${PORT}/`);
|
|
35
|
-
expect(res.status).toBe(200);
|
|
36
|
-
expect(res.headers.get('content-type')).toContain('text/html');
|
|
37
|
-
const body = await res.text();
|
|
38
|
-
expect(body).toContain('monomind');
|
|
39
|
-
});
|
|
40
|
-
it('GET /runs returns JSON array', async () => {
|
|
41
|
-
const dash = startDashboard(PORT);
|
|
42
|
-
const run = { id: 'r1', playbookId: 'wf', playbookName: 'WF', status: 'completed', startedAt: Date.now(), itemsProcessed: 1, itemsTotal: 1 };
|
|
43
|
-
dash.addRunRecord(run);
|
|
44
|
-
const res = await fetch(`http://localhost:${PORT}/runs`);
|
|
45
|
-
expect(res.status).toBe(200);
|
|
46
|
-
const body = await res.json();
|
|
47
|
-
expect(body.some(r => r.id === 'r1')).toBe(true);
|
|
48
|
-
});
|
|
49
|
-
it('POST /stop/:runId marks run as stop-requested', async () => {
|
|
50
|
-
const dash = startDashboard(PORT);
|
|
51
|
-
const res = await fetch(`http://localhost:${PORT}/stop/run-abc`, { method: 'POST' });
|
|
52
|
-
expect(res.status).toBe(200);
|
|
53
|
-
expect(dash.isStopRequested('run-abc')).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
it('isStopRequested returns false for unknown runId', () => {
|
|
56
|
-
const dash = startDashboard(PORT);
|
|
57
|
-
expect(dash.isStopRequested('nonexistent')).toBe(false);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
// SSE / WebSocket streaming tests
|
|
62
|
-
//
|
|
63
|
-
// The dashboard uses WebSocket (via 'ws') when the package is available, and
|
|
64
|
-
// falls back to Server-Sent Events when it is not. Since 'ws' IS installed in
|
|
65
|
-
// this repo the server runs in WebSocket mode. We test the streaming path
|
|
66
|
-
// (broadcast + ?dir= filter) through the WebSocket interface so the tests are
|
|
67
|
-
// consistent regardless of environment.
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
/**
|
|
70
|
-
* Open a WebSocket to the dashboard, wait for the initial 'history' message,
|
|
71
|
-
* then return a helper that collects subsequent messages.
|
|
72
|
-
*/
|
|
73
|
-
function connectWs(port, dir) {
|
|
74
|
-
return new Promise((resolve, reject) => {
|
|
75
|
-
const url = dir ? `ws://127.0.0.1:${port}/?dir=${encodeURIComponent(dir)}` : `ws://127.0.0.1:${port}/`;
|
|
76
|
-
const ws = new WebSocket(url);
|
|
77
|
-
const messages = [];
|
|
78
|
-
ws.on('open', () => {
|
|
79
|
-
// history message arrives; resolve after we have it
|
|
80
|
-
ws.on('message', (data) => {
|
|
81
|
-
const parsed = JSON.parse(data.toString());
|
|
82
|
-
messages.push(parsed);
|
|
83
|
-
if (messages.length === 1 && parsed.type === 'history') {
|
|
84
|
-
resolve({ ws, messages, close: () => ws.close() });
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
ws.on('error', reject);
|
|
89
|
-
setTimeout(() => reject(new Error('WS connect timeout')), 3000);
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
describe('broadcast via WebSocket', () => {
|
|
93
|
-
it('delivers events to connected clients', async () => {
|
|
94
|
-
const dash = startDashboard(PORT);
|
|
95
|
-
// Wait a tick for the server to be ready
|
|
96
|
-
await new Promise(r => setTimeout(r, 50));
|
|
97
|
-
const { ws, messages, close } = await connectWs(PORT);
|
|
98
|
-
try {
|
|
99
|
-
// Wait for initial history message, then broadcast
|
|
100
|
-
dash.broadcast(makeEvent({ eventType: 'step_completed', nodeId: 'node-broadcast-1' }));
|
|
101
|
-
await new Promise(r => setTimeout(r, 100));
|
|
102
|
-
const events = messages.filter((m) => m.eventType !== undefined);
|
|
103
|
-
expect(events.length).toBeGreaterThan(0);
|
|
104
|
-
expect(events[0].nodeId).toBe('node-broadcast-1');
|
|
105
|
-
}
|
|
106
|
-
finally {
|
|
107
|
-
close();
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
it('?dir= filter — client with matching dir receives event', async () => {
|
|
111
|
-
const dash = startDashboard(PORT);
|
|
112
|
-
await new Promise(r => setTimeout(r, 50));
|
|
113
|
-
const dirA = '/projects/alpha';
|
|
114
|
-
const { messages, close } = await connectWs(PORT, dirA);
|
|
115
|
-
try {
|
|
116
|
-
dash.broadcast(makeEvent({ projectDir: dirA, nodeId: 'node-alpha' }));
|
|
117
|
-
await new Promise(r => setTimeout(r, 100));
|
|
118
|
-
const events = messages.filter((m) => m.eventType !== undefined);
|
|
119
|
-
expect(events.length).toBeGreaterThan(0);
|
|
120
|
-
expect(events[0].nodeId).toBe('node-alpha');
|
|
121
|
-
}
|
|
122
|
-
finally {
|
|
123
|
-
close();
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
it('?dir= filter — client with non-matching dir does NOT receive event', async () => {
|
|
127
|
-
const dash = startDashboard(PORT);
|
|
128
|
-
await new Promise(r => setTimeout(r, 50));
|
|
129
|
-
const { messages, close } = await connectWs(PORT, '/projects/beta');
|
|
130
|
-
try {
|
|
131
|
-
dash.broadcast(makeEvent({ projectDir: '/projects/alpha', nodeId: 'node-alpha-only' }));
|
|
132
|
-
await new Promise(r => setTimeout(r, 100));
|
|
133
|
-
const events = messages.filter((m) => m.eventType !== undefined);
|
|
134
|
-
expect(events.length).toBe(0);
|
|
135
|
-
}
|
|
136
|
-
finally {
|
|
137
|
-
close();
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
it('client without ?dir= receives all broadcasts', async () => {
|
|
141
|
-
const dash = startDashboard(PORT);
|
|
142
|
-
await new Promise(r => setTimeout(r, 50));
|
|
143
|
-
const { messages, close } = await connectWs(PORT);
|
|
144
|
-
try {
|
|
145
|
-
dash.broadcast(makeEvent({ projectDir: '/projects/alpha', nodeId: 'all-1' }));
|
|
146
|
-
dash.broadcast(makeEvent({ projectDir: '/projects/beta', nodeId: 'all-2' }));
|
|
147
|
-
await new Promise(r => setTimeout(r, 150));
|
|
148
|
-
const events = messages.filter((m) => m.eventType !== undefined);
|
|
149
|
-
expect(events.length).toBeGreaterThanOrEqual(2);
|
|
150
|
-
}
|
|
151
|
-
finally {
|
|
152
|
-
close();
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
describe('GET /events SSE endpoint', () => {
|
|
157
|
-
it('returns text/event-stream content-type when ws is unavailable', async () => {
|
|
158
|
-
// The dashboard is already started in ws mode on PORT.
|
|
159
|
-
// We verify the /events endpoint returns 404 in ws mode (ws is installed),
|
|
160
|
-
// which confirms the conditional branching works correctly.
|
|
161
|
-
const dash = startDashboard(PORT);
|
|
162
|
-
await new Promise(r => setTimeout(r, 50));
|
|
163
|
-
const res = await fetch(`http://127.0.0.1:${PORT}/events`, {
|
|
164
|
-
headers: { Accept: 'text/event-stream' },
|
|
165
|
-
});
|
|
166
|
-
// In ws mode the SSE endpoint is not registered — server returns 404
|
|
167
|
-
// In SSE-only mode it would return 200 with text/event-stream
|
|
168
|
-
// We test what the current mode actually does
|
|
169
|
-
if (res.status === 200) {
|
|
170
|
-
expect(res.headers.get('content-type')).toContain('text/event-stream');
|
|
171
|
-
await res.body?.cancel();
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
// ws mode: SSE not registered
|
|
175
|
-
expect(res.status).toBe(404);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
//# sourceMappingURL=browse-dashboard.test.js.map
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { runPlaybook } from '@monoes/monobrowse';
|
|
3
|
-
const triggerNode = { id: 'trigger', type: 'trigger.manual', config: {} };
|
|
4
|
-
describe('runPlaybook', () => {
|
|
5
|
-
it('runs a single-node trigger workflow', async () => {
|
|
6
|
-
const def = {
|
|
7
|
-
id: 'wf-1', name: 'Test', nodes: [triggerNode], connections: [],
|
|
8
|
-
};
|
|
9
|
-
const record = await runPlaybook(def);
|
|
10
|
-
expect(record.status).toBe('completed');
|
|
11
|
-
expect(record.playbookId).toBe('wf-1');
|
|
12
|
-
});
|
|
13
|
-
it('rejects cyclic workflow', async () => {
|
|
14
|
-
const def = {
|
|
15
|
-
id: 'cyclic', name: 'Cyclic',
|
|
16
|
-
nodes: [
|
|
17
|
-
{ id: 'a', type: 'core.set', config: {} },
|
|
18
|
-
{ id: 'b', type: 'core.set', config: {} },
|
|
19
|
-
],
|
|
20
|
-
connections: [{ from: 'a', to: 'b' }, { from: 'b', to: 'a' }],
|
|
21
|
-
};
|
|
22
|
-
await expect(runPlaybook(def)).rejects.toThrow('cycle');
|
|
23
|
-
});
|
|
24
|
-
it('calls node handlers for action nodes', async () => {
|
|
25
|
-
const handler = vi.fn().mockResolvedValue([{ data: { result: 'ok' } }]);
|
|
26
|
-
const def = {
|
|
27
|
-
id: 'wf-2', name: 'Action Test',
|
|
28
|
-
nodes: [
|
|
29
|
-
triggerNode,
|
|
30
|
-
{ id: 'act', type: 'action.linkedin.comment_post', config: { text: 'hi' } },
|
|
31
|
-
],
|
|
32
|
-
connections: [{ from: 'trigger', to: 'act' }],
|
|
33
|
-
};
|
|
34
|
-
const handlers = new Map([['action.linkedin.comment_post', handler]]);
|
|
35
|
-
const record = await runPlaybook(def, { handlers });
|
|
36
|
-
expect(handler).toHaveBeenCalled();
|
|
37
|
-
expect(record.status).toBe('completed');
|
|
38
|
-
});
|
|
39
|
-
it('emits step events in order', async () => {
|
|
40
|
-
const events = [];
|
|
41
|
-
const def = { id: 'wf-3', name: 'Events', nodes: [triggerNode], connections: [] };
|
|
42
|
-
await runPlaybook(def, { onEvent: e => events.push(e.eventType) });
|
|
43
|
-
expect(events[0]).toBe('run_started');
|
|
44
|
-
expect(events[events.length - 1]).toBe('run_completed');
|
|
45
|
-
});
|
|
46
|
-
it('skips failed node when onError is skip', async () => {
|
|
47
|
-
const handler = vi.fn().mockRejectedValue(new Error('fail'));
|
|
48
|
-
const def = {
|
|
49
|
-
id: 'wf-4', name: 'Skip',
|
|
50
|
-
nodes: [
|
|
51
|
-
triggerNode,
|
|
52
|
-
{ id: 'bad', type: 'action.x.fail', config: {}, onError: 'skip' },
|
|
53
|
-
],
|
|
54
|
-
connections: [{ from: 'trigger', to: 'bad' }],
|
|
55
|
-
};
|
|
56
|
-
const handlers = new Map([['action.x.fail', handler]]);
|
|
57
|
-
const record = await runPlaybook(def, { handlers });
|
|
58
|
-
expect(record.status).toBe('completed');
|
|
59
|
-
});
|
|
60
|
-
it('stops on node failure when onError is stop (default)', async () => {
|
|
61
|
-
const handler = vi.fn().mockRejectedValue(new Error('boom'));
|
|
62
|
-
const def = {
|
|
63
|
-
id: 'wf-5', name: 'Stop',
|
|
64
|
-
nodes: [
|
|
65
|
-
triggerNode,
|
|
66
|
-
{ id: 'bad', type: 'action.x.fail', config: {} },
|
|
67
|
-
],
|
|
68
|
-
connections: [{ from: 'trigger', to: 'bad' }],
|
|
69
|
-
};
|
|
70
|
-
const handlers = new Map([['action.x.fail', handler]]);
|
|
71
|
-
const record = await runPlaybook(def, { handlers });
|
|
72
|
-
expect(record.status).toBe('failed');
|
|
73
|
-
expect(record.error).toContain('boom');
|
|
74
|
-
});
|
|
75
|
-
it('stops when AbortSignal is aborted', async () => {
|
|
76
|
-
const controller = new AbortController();
|
|
77
|
-
controller.abort();
|
|
78
|
-
const def = { id: 'wf-6', name: 'Abort', nodes: [triggerNode], connections: [] };
|
|
79
|
-
const record = await runPlaybook(def, { signal: controller.signal });
|
|
80
|
-
expect(record.status).toBe('stopped');
|
|
81
|
-
});
|
|
82
|
-
it('core.set transforms item fields', async () => {
|
|
83
|
-
const def = {
|
|
84
|
-
id: 'wf-7', name: 'Set',
|
|
85
|
-
nodes: [
|
|
86
|
-
{ id: 'trigger', type: 'trigger.manual', config: { items: [{ data: { x: 1 } }] } },
|
|
87
|
-
{ id: 'setNode', type: 'core.set', config: { y: 'hardcoded' } },
|
|
88
|
-
],
|
|
89
|
-
connections: [{ from: 'trigger', to: 'setNode' }],
|
|
90
|
-
};
|
|
91
|
-
const events = [];
|
|
92
|
-
const record = await runPlaybook(def, { onEvent: e => events.push(e) });
|
|
93
|
-
expect(record.status).toBe('completed');
|
|
94
|
-
});
|
|
95
|
-
it('throws for unregistered action handler', async () => {
|
|
96
|
-
const def = {
|
|
97
|
-
id: 'wf-8', name: 'NoHandler',
|
|
98
|
-
nodes: [
|
|
99
|
-
triggerNode,
|
|
100
|
-
{ id: 'act', type: 'action.missing.thing', config: {} },
|
|
101
|
-
],
|
|
102
|
-
connections: [{ from: 'trigger', to: 'act' }],
|
|
103
|
-
};
|
|
104
|
-
const record = await runPlaybook(def);
|
|
105
|
-
expect(record.status).toBe('failed');
|
|
106
|
-
expect(record.error).toContain('No handler registered');
|
|
107
|
-
});
|
|
108
|
-
it('core.filter keeps matching items', async () => {
|
|
109
|
-
const def = {
|
|
110
|
-
id: 'wf-filter', name: 'Filter',
|
|
111
|
-
nodes: [
|
|
112
|
-
{ id: 'trigger', type: 'trigger.manual', config: { items: [{ data: { x: 1 } }, { data: { x: 2 } }, { data: { x: 3 } }] } },
|
|
113
|
-
{ id: 'filter', type: 'core.filter', config: { expression: '{{$json.x}}' } },
|
|
114
|
-
],
|
|
115
|
-
connections: [{ from: 'trigger', to: 'filter' }],
|
|
116
|
-
};
|
|
117
|
-
const record = await runPlaybook(def);
|
|
118
|
-
expect(record.status).toBe('completed');
|
|
119
|
-
expect(record.itemsProcessed).toBeGreaterThan(0);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
//# sourceMappingURL=browse-engine.test.js.map
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
2
|
-
import { resolveExpression, resolveConfig } from '@monoes/monobrowse';
|
|
3
|
-
const item = { data: { url: 'https://linkedin.com/post/123', comment: 'Great post!' } };
|
|
4
|
-
const nodeOutputs = {
|
|
5
|
-
'trigger': [{ data: { url: 'https://linkedin.com/post/123' } }],
|
|
6
|
-
};
|
|
7
|
-
const params = { text: 'Hello world', post_url: 'https://linkedin.com/post/123' };
|
|
8
|
-
describe('resolveExpression', () => {
|
|
9
|
-
it('resolves $json fields', () => {
|
|
10
|
-
expect(resolveExpression('{{$json.url}}', item, nodeOutputs, params)).toBe('https://linkedin.com/post/123');
|
|
11
|
-
});
|
|
12
|
-
it('resolves $env variables', () => {
|
|
13
|
-
vi.stubEnv('TEST_VAR', 'test-value');
|
|
14
|
-
expect(resolveExpression('{{$env.TEST_VAR}}', item, nodeOutputs, params)).toBe('test-value');
|
|
15
|
-
});
|
|
16
|
-
it('resolves params', () => {
|
|
17
|
-
expect(resolveExpression('{{params.text}}', item, nodeOutputs, params)).toBe('Hello world');
|
|
18
|
-
});
|
|
19
|
-
it('resolves node output references', () => {
|
|
20
|
-
expect(resolveExpression('{{$node.trigger.url}}', item, nodeOutputs, params)).toBe('https://linkedin.com/post/123');
|
|
21
|
-
});
|
|
22
|
-
it('resolves $node bracket notation', () => {
|
|
23
|
-
expect(resolveExpression('{{$node["trigger"].url}}', item, nodeOutputs, params)).toBe('https://linkedin.com/post/123');
|
|
24
|
-
});
|
|
25
|
-
it('returns raw string if no template markers', () => {
|
|
26
|
-
expect(resolveExpression('plain text', item, nodeOutputs, params)).toBe('plain text');
|
|
27
|
-
});
|
|
28
|
-
it('throws on unresolved expression', () => {
|
|
29
|
-
expect(() => resolveExpression('{{$json.missing}}', item, nodeOutputs, params)).toThrow('Unresolved');
|
|
30
|
-
});
|
|
31
|
-
it('replaces duplicate placeholders in a single template', () => {
|
|
32
|
-
const doubled = { data: { x: 'hello' } };
|
|
33
|
-
expect(resolveExpression('{{$json.x}} and {{$json.x}}', doubled, {}, {})).toBe('hello and hello');
|
|
34
|
-
});
|
|
35
|
-
it('throws when $node output array is empty', () => {
|
|
36
|
-
expect(() => resolveExpression('{{$node.empty.field}}', item, { empty: [] }, params)).toThrow('Unresolved');
|
|
37
|
-
});
|
|
38
|
-
it('throws when $node bracket field is missing', () => {
|
|
39
|
-
expect(() => resolveExpression('{{$node["trigger"].missing}}', item, nodeOutputs, params)).toThrow('Unresolved');
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
afterEach(() => {
|
|
43
|
-
vi.unstubAllEnvs();
|
|
44
|
-
});
|
|
45
|
-
describe('resolveConfig', () => {
|
|
46
|
-
it('resolves all string values in a config object', () => {
|
|
47
|
-
const config = { post_url: '{{$json.url}}', text: '{{params.text}}', count: 3 };
|
|
48
|
-
const result = resolveConfig(config, item, nodeOutputs, params);
|
|
49
|
-
expect(result.post_url).toBe('https://linkedin.com/post/123');
|
|
50
|
-
expect(result.text).toBe('Hello world');
|
|
51
|
-
expect(result.count).toBe(3);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
//# sourceMappingURL=browse-expression.test.js.map
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { readPlaybook, writePlaybookRun, listPlaybookRuns, readAction, clearRunStore } from '@monoes/monobrowse';
|
|
3
|
-
import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
const TMP = join(tmpdir(), 'browse-store-test-' + Date.now());
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
clearRunStore();
|
|
9
|
-
mkdirSync(join(TMP, 'playbooks'), { recursive: true });
|
|
10
|
-
mkdirSync(join(TMP, 'actions'), { recursive: true });
|
|
11
|
-
});
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
rmSync(TMP, { recursive: true, force: true });
|
|
14
|
-
});
|
|
15
|
-
describe('readPlaybook', () => {
|
|
16
|
-
it('parses a valid playbook JSON file', async () => {
|
|
17
|
-
const pb = {
|
|
18
|
-
id: 'test-pb',
|
|
19
|
-
name: 'Test Playbook',
|
|
20
|
-
nodes: [{ id: 'trigger', type: 'trigger.manual', config: {} }],
|
|
21
|
-
connections: [],
|
|
22
|
-
};
|
|
23
|
-
const p = join(TMP, 'playbooks', 'test.json');
|
|
24
|
-
writeFileSync(p, JSON.stringify(pb));
|
|
25
|
-
const result = await readPlaybook(p);
|
|
26
|
-
expect(result.id).toBe('test-pb');
|
|
27
|
-
expect(result.nodes).toHaveLength(1);
|
|
28
|
-
});
|
|
29
|
-
it('throws on missing file', async () => {
|
|
30
|
-
await expect(readPlaybook(join(TMP, 'nonexistent.json'))).rejects.toThrow();
|
|
31
|
-
});
|
|
32
|
-
it('throws on invalid JSON', async () => {
|
|
33
|
-
const p = join(TMP, 'playbooks', 'bad.json');
|
|
34
|
-
writeFileSync(p, 'not json');
|
|
35
|
-
await expect(readPlaybook(p)).rejects.toThrow();
|
|
36
|
-
});
|
|
37
|
-
it('throws if nodes array is missing', async () => {
|
|
38
|
-
const p = join(TMP, 'playbooks', 'no-nodes.json');
|
|
39
|
-
writeFileSync(p, JSON.stringify({ id: 'x', name: 'y', connections: [] }));
|
|
40
|
-
await expect(readPlaybook(p)).rejects.toThrow('nodes');
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
describe('readAction', () => {
|
|
44
|
-
it('parses a valid action JSON file', async () => {
|
|
45
|
-
const action = {
|
|
46
|
-
id: 'linkedin:comment_post',
|
|
47
|
-
platform: 'linkedin',
|
|
48
|
-
name: 'Comment on Post',
|
|
49
|
-
params: ['post_url', 'text'],
|
|
50
|
-
steps: [],
|
|
51
|
-
};
|
|
52
|
-
const p = join(TMP, 'actions', 'comment.json');
|
|
53
|
-
writeFileSync(p, JSON.stringify(action));
|
|
54
|
-
const result = await readAction(p);
|
|
55
|
-
expect(result.id).toBe('linkedin:comment_post');
|
|
56
|
-
expect(result.params).toContain('post_url');
|
|
57
|
-
});
|
|
58
|
-
it('throws on missing action file', async () => {
|
|
59
|
-
await expect(readAction(join(TMP, 'actions', 'nonexistent.json'))).rejects.toThrow();
|
|
60
|
-
});
|
|
61
|
-
it('throws on invalid action JSON', async () => {
|
|
62
|
-
const p = join(TMP, 'actions', 'bad.json');
|
|
63
|
-
writeFileSync(p, 'not json');
|
|
64
|
-
await expect(readAction(p)).rejects.toThrow();
|
|
65
|
-
});
|
|
66
|
-
it('throws if action steps array is missing', async () => {
|
|
67
|
-
const p = join(TMP, 'actions', 'no-steps.json');
|
|
68
|
-
writeFileSync(p, JSON.stringify({ id: 'x', platform: 'y', name: 'z', params: [] }));
|
|
69
|
-
await expect(readAction(p)).rejects.toThrow('steps');
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
describe('writePlaybookRun + listPlaybookRuns', () => {
|
|
73
|
-
it('stores and retrieves run records', async () => {
|
|
74
|
-
const record = {
|
|
75
|
-
id: 'run-1',
|
|
76
|
-
playbookId: 'pb-1',
|
|
77
|
-
playbookName: 'Test PB',
|
|
78
|
-
status: 'completed',
|
|
79
|
-
startedAt: Date.now(),
|
|
80
|
-
completedAt: Date.now() + 1000,
|
|
81
|
-
itemsProcessed: 5,
|
|
82
|
-
itemsTotal: 5,
|
|
83
|
-
};
|
|
84
|
-
await writePlaybookRun(record);
|
|
85
|
-
const runs = await listPlaybookRuns();
|
|
86
|
-
const found = runs.find(r => r.id === 'run-1');
|
|
87
|
-
expect(found).toBeDefined();
|
|
88
|
-
expect(found?.status).toBe('completed');
|
|
89
|
-
});
|
|
90
|
-
it('filters runs by playbookId', async () => {
|
|
91
|
-
const r1 = { id: 'r1', playbookId: 'pb-a', playbookName: 'A', status: 'completed', startedAt: 1, itemsProcessed: 1, itemsTotal: 1 };
|
|
92
|
-
const r2 = { id: 'r2', playbookId: 'pb-b', playbookName: 'B', status: 'failed', startedAt: 2, itemsProcessed: 0, itemsTotal: 1 };
|
|
93
|
-
await writePlaybookRun(r1);
|
|
94
|
-
await writePlaybookRun(r2);
|
|
95
|
-
const runs = await listPlaybookRuns('pb-a');
|
|
96
|
-
expect(runs.every(r => r.playbookId === 'pb-a')).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
//# sourceMappingURL=browse-store.test.js.map
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
describe('PlaybookDef', () => {
|
|
3
|
-
it('accepts a valid playbook definition', () => {
|
|
4
|
-
const wf = {
|
|
5
|
-
id: 'my-playbook',
|
|
6
|
-
name: 'My Playbook',
|
|
7
|
-
nodes: [
|
|
8
|
-
{ id: 'n1', type: 'trigger.manual', config: {} },
|
|
9
|
-
{ id: 'n2', type: 'action.linkedin.comment_post', config: { post_url: '{{$json.url}}', text: 'hi' } },
|
|
10
|
-
],
|
|
11
|
-
connections: [{ from: 'n1', to: 'n2' }],
|
|
12
|
-
};
|
|
13
|
-
expect(wf.nodes).toHaveLength(2);
|
|
14
|
-
expect(wf.connections[0].from).toBe('n1');
|
|
15
|
-
});
|
|
16
|
-
it('accepts a valid ActionDef with steps', () => {
|
|
17
|
-
const action = {
|
|
18
|
-
id: 'linkedin:comment_post',
|
|
19
|
-
platform: 'linkedin',
|
|
20
|
-
name: 'Comment on Post',
|
|
21
|
-
params: ['post_url', 'text'],
|
|
22
|
-
steps: [
|
|
23
|
-
{ type: 'navigate', url: '{{params.post_url}}' },
|
|
24
|
-
{ type: 'find', selectors: ['.comment-box', '[aria-label="Add a comment"]'], as: 'box' },
|
|
25
|
-
{ type: 'click', target: '{{box}}' },
|
|
26
|
-
{ type: 'type', target: '{{box}}', text: '{{params.text}}', humanDelay: true },
|
|
27
|
-
{ type: 'wait', condition: 'network_idle', timeout: 3000 },
|
|
28
|
-
],
|
|
29
|
-
};
|
|
30
|
-
expect(action.steps).toHaveLength(5);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
//# sourceMappingURL=browse-workflow-types.test.js.map
|