opc-agent 3.0.1 → 4.0.1
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/README.md +404 -74
- package/README.zh-CN.md +82 -0
- package/dist/channels/dingtalk.d.ts +17 -0
- package/dist/channels/dingtalk.js +38 -0
- package/dist/channels/googlechat.d.ts +14 -0
- package/dist/channels/googlechat.js +37 -0
- package/dist/channels/imessage.d.ts +13 -0
- package/dist/channels/imessage.js +28 -0
- package/dist/channels/irc.d.ts +20 -0
- package/dist/channels/irc.js +71 -0
- package/dist/channels/line.d.ts +14 -0
- package/dist/channels/line.js +28 -0
- package/dist/channels/matrix.d.ts +15 -0
- package/dist/channels/matrix.js +28 -0
- package/dist/channels/mattermost.d.ts +18 -0
- package/dist/channels/mattermost.js +49 -0
- package/dist/channels/msteams.d.ts +14 -0
- package/dist/channels/msteams.js +28 -0
- package/dist/channels/nostr.d.ts +14 -0
- package/dist/channels/nostr.js +28 -0
- package/dist/channels/qq.d.ts +15 -0
- package/dist/channels/qq.js +28 -0
- package/dist/channels/signal.d.ts +14 -0
- package/dist/channels/signal.js +28 -0
- package/dist/channels/sms.d.ts +15 -0
- package/dist/channels/sms.js +28 -0
- package/dist/channels/twitch.d.ts +17 -0
- package/dist/channels/twitch.js +59 -0
- package/dist/channels/voice-call.d.ts +27 -0
- package/dist/channels/voice-call.js +82 -0
- package/dist/channels/whatsapp.d.ts +14 -0
- package/dist/channels/whatsapp.js +28 -0
- package/dist/cli/chat.d.ts +2 -0
- package/dist/cli/chat.js +134 -0
- package/dist/cli/setup.d.ts +4 -0
- package/dist/cli/setup.js +303 -0
- package/dist/cli.js +142 -6
- package/dist/core/api-server.d.ts +25 -0
- package/dist/core/api-server.js +286 -0
- package/dist/core/audio.d.ts +50 -0
- package/dist/core/audio.js +68 -0
- package/dist/core/context-discovery.d.ts +16 -0
- package/dist/core/context-discovery.js +107 -0
- package/dist/core/context-refs.d.ts +29 -0
- package/dist/core/context-refs.js +162 -0
- package/dist/core/gateway.d.ts +53 -0
- package/dist/core/gateway.js +80 -0
- package/dist/core/heartbeat.d.ts +19 -0
- package/dist/core/heartbeat.js +50 -0
- package/dist/core/hooks.d.ts +28 -0
- package/dist/core/hooks.js +82 -0
- package/dist/core/ide-bridge.d.ts +53 -0
- package/dist/core/ide-bridge.js +97 -0
- package/dist/core/node-network.d.ts +23 -0
- package/dist/core/node-network.js +77 -0
- package/dist/core/profiles.d.ts +27 -0
- package/dist/core/profiles.js +131 -0
- package/dist/core/sandbox.d.ts +25 -0
- package/dist/core/sandbox.js +84 -1
- package/dist/core/session-manager.d.ts +33 -0
- package/dist/core/session-manager.js +157 -0
- package/dist/core/vision.d.ts +45 -0
- package/dist/core/vision.js +177 -0
- package/dist/hub/brain-seed.d.ts +14 -0
- package/dist/hub/brain-seed.js +77 -0
- package/dist/hub/client.d.ts +25 -0
- package/dist/hub/client.js +44 -0
- package/dist/index.d.ts +66 -1
- package/dist/index.js +95 -3
- package/dist/memory/context-compressor.d.ts +43 -0
- package/dist/memory/context-compressor.js +167 -0
- package/dist/memory/index.d.ts +4 -0
- package/dist/memory/index.js +5 -1
- package/dist/memory/user-profiler.d.ts +50 -0
- package/dist/memory/user-profiler.js +201 -0
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.js +54 -1
- package/dist/scheduler/cron-engine.d.ts +41 -0
- package/dist/scheduler/cron-engine.js +200 -0
- package/dist/scheduler/index.d.ts +3 -0
- package/dist/scheduler/index.js +7 -0
- package/dist/schema/oad.d.ts +12 -12
- package/dist/security/approvals.d.ts +53 -0
- package/dist/security/approvals.js +115 -0
- package/dist/security/elevated.d.ts +41 -0
- package/dist/security/elevated.js +89 -0
- package/dist/security/index.d.ts +6 -0
- package/dist/security/index.js +7 -1
- package/dist/security/secrets.d.ts +34 -0
- package/dist/security/secrets.js +115 -0
- package/dist/skills/builtin/index.d.ts +6 -0
- package/dist/skills/builtin/index.js +402 -0
- package/dist/skills/marketplace.d.ts +30 -0
- package/dist/skills/marketplace.js +142 -0
- package/dist/skills/types.d.ts +34 -0
- package/dist/skills/types.js +16 -0
- package/dist/studio/server.d.ts +25 -0
- package/dist/studio/server.js +780 -0
- package/dist/studio/templates-data.d.ts +21 -0
- package/dist/studio/templates-data.js +148 -0
- package/dist/studio-ui/index.html +2502 -1073
- package/dist/tools/builtin/browser.d.ts +47 -0
- package/dist/tools/builtin/browser.js +284 -0
- package/dist/tools/builtin/home-assistant.d.ts +12 -0
- package/dist/tools/builtin/home-assistant.js +126 -0
- package/dist/tools/builtin/index.d.ts +7 -1
- package/dist/tools/builtin/index.js +23 -2
- package/dist/tools/builtin/rl-tools.d.ts +13 -0
- package/dist/tools/builtin/rl-tools.js +228 -0
- package/dist/tools/builtin/vision.d.ts +6 -0
- package/dist/tools/builtin/vision.js +61 -0
- package/dist/tools/builtin/web-search.d.ts +9 -0
- package/dist/tools/builtin/web-search.js +150 -0
- package/dist/tools/document-processor.d.ts +39 -0
- package/dist/tools/document-processor.js +188 -0
- package/dist/tools/image-generator.d.ts +42 -0
- package/dist/tools/image-generator.js +136 -0
- package/dist/tools/web-scraper.d.ts +20 -0
- package/dist/tools/web-scraper.js +148 -0
- package/dist/tools/web-search.d.ts +51 -0
- package/dist/tools/web-search.js +152 -0
- package/install.ps1 +154 -0
- package/install.sh +164 -0
- package/package.json +63 -52
- package/src/channels/dingtalk.ts +46 -0
- package/src/channels/googlechat.ts +42 -0
- package/src/channels/imessage.ts +32 -0
- package/src/channels/irc.ts +82 -0
- package/src/channels/line.ts +33 -0
- package/src/channels/matrix.ts +34 -0
- package/src/channels/mattermost.ts +57 -0
- package/src/channels/msteams.ts +33 -0
- package/src/channels/nostr.ts +33 -0
- package/src/channels/qq.ts +34 -0
- package/src/channels/signal.ts +33 -0
- package/src/channels/sms.ts +34 -0
- package/src/channels/twitch.ts +65 -0
- package/src/channels/voice-call.ts +100 -0
- package/src/channels/whatsapp.ts +33 -0
- package/src/cli/chat.ts +99 -0
- package/src/cli/setup.ts +314 -0
- package/src/cli.ts +148 -6
- package/src/core/api-server.ts +277 -0
- package/src/core/audio.ts +98 -0
- package/src/core/context-discovery.ts +85 -0
- package/src/core/context-refs.ts +140 -0
- package/src/core/gateway.ts +106 -0
- package/src/core/heartbeat.ts +51 -0
- package/src/core/hooks.ts +105 -0
- package/src/core/ide-bridge.ts +133 -0
- package/src/core/node-network.ts +86 -0
- package/src/core/profiles.ts +122 -0
- package/src/core/sandbox.ts +100 -0
- package/src/core/session-manager.ts +137 -0
- package/src/core/vision.ts +180 -0
- package/src/hub/brain-seed.ts +54 -0
- package/src/hub/client.ts +60 -0
- package/src/index.ts +86 -1
- package/src/memory/context-compressor.ts +189 -0
- package/src/memory/index.ts +4 -0
- package/src/memory/user-profiler.ts +215 -0
- package/src/providers/index.ts +64 -1
- package/src/scheduler/cron-engine.ts +191 -0
- package/src/scheduler/index.ts +2 -0
- package/src/security/approvals.ts +143 -0
- package/src/security/elevated.ts +105 -0
- package/src/security/index.ts +6 -0
- package/src/security/secrets.ts +129 -0
- package/src/skills/builtin/index.ts +408 -0
- package/src/skills/marketplace.ts +113 -0
- package/src/skills/types.ts +42 -0
- package/src/studio/server.ts +1591 -791
- package/src/studio/templates-data.ts +178 -0
- package/src/studio-ui/index.html +2502 -1073
- package/src/tools/builtin/browser.ts +299 -0
- package/src/tools/builtin/home-assistant.ts +116 -0
- package/src/tools/builtin/index.ts +37 -28
- package/src/tools/builtin/rl-tools.ts +243 -0
- package/src/tools/builtin/vision.ts +64 -0
- package/src/tools/builtin/web-search.ts +126 -0
- package/src/tools/document-processor.ts +213 -0
- package/src/tools/image-generator.ts +150 -0
- package/src/tools/web-scraper.ts +179 -0
- package/src/tools/web-search.ts +180 -0
- package/tests/api-server.test.ts +148 -0
- package/tests/approvals.test.ts +89 -0
- package/tests/audio.test.ts +40 -0
- package/tests/browser.test.ts +179 -0
- package/tests/builtin-tools.test.ts +83 -83
- package/tests/channels-extra.test.ts +45 -0
- package/tests/context-compressor.test.ts +172 -0
- package/tests/context-refs.test.ts +121 -0
- package/tests/cron-engine.test.ts +101 -0
- package/tests/document-processor.test.ts +69 -0
- package/tests/e2e-nocode.test.ts +442 -0
- package/tests/elevated.test.ts +69 -0
- package/tests/gateway.test.ts +63 -71
- package/tests/home-assistant.test.ts +40 -0
- package/tests/hooks.test.ts +79 -0
- package/tests/ide-bridge.test.ts +38 -0
- package/tests/image-generator.test.ts +84 -0
- package/tests/node-network.test.ts +74 -0
- package/tests/profiles.test.ts +61 -0
- package/tests/rl-tools.test.ts +93 -0
- package/tests/sandbox-manager.test.ts +46 -0
- package/tests/secrets.test.ts +107 -0
- package/tests/settings-api.test.ts +148 -0
- package/tests/setup.test.ts +73 -0
- package/tests/studio.test.ts +402 -229
- package/tests/tools/builtin-extended.test.ts +138 -138
- package/tests/user-profiler.test.ts +169 -0
- package/tests/v090-features.test.ts +254 -0
- package/tests/vision.test.ts +61 -0
- package/tests/voice-call.test.ts +47 -0
- package/tests/voice-interaction.test.ts +38 -0
- package/tests/web-search.test.ts +155 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ExecApprovalManager } from '../src/security/approvals';
|
|
3
|
+
|
|
4
|
+
describe('ExecApprovalManager', () => {
|
|
5
|
+
it('should default to elevated-only policy', () => {
|
|
6
|
+
const mgr = new ExecApprovalManager();
|
|
7
|
+
expect(mgr.getPolicy()).toBe('elevated-only');
|
|
8
|
+
mgr.destroy();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should require approval for elevated commands in elevated-only mode', () => {
|
|
12
|
+
const mgr = new ExecApprovalManager({ policy: 'elevated-only' });
|
|
13
|
+
expect(mgr.needsApproval('ls', true)).toBe(true);
|
|
14
|
+
expect(mgr.needsApproval('ls', false)).toBe(false);
|
|
15
|
+
mgr.destroy();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should always require approval in always mode', () => {
|
|
19
|
+
const mgr = new ExecApprovalManager({ policy: 'always' });
|
|
20
|
+
expect(mgr.needsApproval('ls', false)).toBe(true);
|
|
21
|
+
mgr.destroy();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should never require approval in never mode', () => {
|
|
25
|
+
const mgr = new ExecApprovalManager({ policy: 'never' });
|
|
26
|
+
expect(mgr.needsApproval('rm -rf /', true)).toBe(false);
|
|
27
|
+
mgr.destroy();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should skip approval for allowlisted commands', () => {
|
|
31
|
+
const mgr = new ExecApprovalManager({ policy: 'allowlist', allowedCommands: ['git ', 'npm test'] });
|
|
32
|
+
expect(mgr.needsApproval('git pull', false)).toBe(false);
|
|
33
|
+
expect(mgr.needsApproval('rm -rf /', false)).toBe(true);
|
|
34
|
+
mgr.destroy();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should create and approve requests', () => {
|
|
38
|
+
const mgr = new ExecApprovalManager();
|
|
39
|
+
const req = mgr.request('sudo reboot', true);
|
|
40
|
+
expect(req.status).toBe('pending');
|
|
41
|
+
expect(mgr.getPending()).toHaveLength(1);
|
|
42
|
+
const approved = mgr.approve(req.id, 'admin');
|
|
43
|
+
expect(approved.status).toBe('approved');
|
|
44
|
+
expect(mgr.getPending()).toHaveLength(0);
|
|
45
|
+
expect(mgr.getHistory()).toHaveLength(1);
|
|
46
|
+
mgr.destroy();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should deny requests', () => {
|
|
50
|
+
const mgr = new ExecApprovalManager();
|
|
51
|
+
const req = mgr.request('rm -rf /', true);
|
|
52
|
+
const denied = mgr.deny(req.id, 'admin', 'too dangerous');
|
|
53
|
+
expect(denied.status).toBe('denied');
|
|
54
|
+
expect(denied.reason).toBe('too dangerous');
|
|
55
|
+
mgr.destroy();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should throw on double approve', () => {
|
|
59
|
+
const mgr = new ExecApprovalManager();
|
|
60
|
+
const req = mgr.request('test', false);
|
|
61
|
+
mgr.approve(req.id, 'admin');
|
|
62
|
+
expect(() => mgr.approve(req.id, 'admin')).toThrow();
|
|
63
|
+
mgr.destroy();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should expire pending requests', () => {
|
|
67
|
+
const mgr = new ExecApprovalManager({ expiryMs: 1 });
|
|
68
|
+
const req = mgr.request('test', false);
|
|
69
|
+
// Wait a tick then check
|
|
70
|
+
return new Promise<void>(resolve => {
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
mgr.checkExpiry();
|
|
73
|
+
expect(mgr.getPending()).toHaveLength(0);
|
|
74
|
+
const found = mgr.getRequest(req.id);
|
|
75
|
+
expect(found?.status).toBe('expired');
|
|
76
|
+
mgr.destroy();
|
|
77
|
+
resolve();
|
|
78
|
+
}, 10);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should fire onRequest callback', () => {
|
|
83
|
+
let called = false;
|
|
84
|
+
const mgr = new ExecApprovalManager({ onRequest: () => { called = true; } });
|
|
85
|
+
mgr.request('test', false);
|
|
86
|
+
expect(called).toBe(true);
|
|
87
|
+
mgr.destroy();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { AudioProcessor } from '../src/core/audio';
|
|
3
|
+
|
|
4
|
+
describe('AudioProcessor', () => {
|
|
5
|
+
it('should detect WAV format', () => {
|
|
6
|
+
const wav = Buffer.from('RIFF\x00\x00\x00\x00WAVEfmt ');
|
|
7
|
+
expect(AudioProcessor.detectFormat(wav)).toBe('wav');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should detect MP3 format (ID3)', () => {
|
|
11
|
+
const mp3 = Buffer.from('ID3\x04\x00\x00\x00\x00\x00\x00');
|
|
12
|
+
expect(AudioProcessor.detectFormat(mp3)).toBe('mp3');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should return unknown for unrecognized format', () => {
|
|
16
|
+
const unknown = Buffer.from('NOTAFORMAT');
|
|
17
|
+
expect(AudioProcessor.detectFormat(unknown)).toBe('unknown');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should split buffer into chunks', () => {
|
|
21
|
+
const buf = Buffer.alloc(100, 0x42);
|
|
22
|
+
const chunks = AudioProcessor.split(buf, 30);
|
|
23
|
+
expect(chunks).toHaveLength(4);
|
|
24
|
+
expect(chunks[0].length).toBe(30);
|
|
25
|
+
expect(chunks[3].length).toBe(10);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should throw without provider on transcribe', async () => {
|
|
29
|
+
const proc = new AudioProcessor();
|
|
30
|
+
await expect(proc.transcribe(Buffer.alloc(0))).rejects.toThrow('No transcribe provider');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should call transcribe provider', async () => {
|
|
34
|
+
const proc = new AudioProcessor({
|
|
35
|
+
transcribe: async () => ({ text: 'hello world' }),
|
|
36
|
+
});
|
|
37
|
+
const result = await proc.transcribe(Buffer.alloc(10));
|
|
38
|
+
expect(result.text).toBe('hello world');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { BrowserManager } from '../src/tools/builtin/browser';
|
|
3
|
+
|
|
4
|
+
// Mock playwright
|
|
5
|
+
const mockPage = {
|
|
6
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
7
|
+
title: vi.fn().mockResolvedValue('Test Page'),
|
|
8
|
+
url: vi.fn().mockReturnValue('https://example.com'),
|
|
9
|
+
innerText: vi.fn().mockResolvedValue('Hello World'),
|
|
10
|
+
click: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
fill: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
screenshot: vi.fn().mockResolvedValue(Buffer.from('fake-png')),
|
|
13
|
+
$$eval: vi.fn().mockResolvedValue([]),
|
|
14
|
+
evaluate: vi.fn().mockResolvedValue('result'),
|
|
15
|
+
waitForSelector: vi.fn().mockResolvedValue(true),
|
|
16
|
+
goBack: vi.fn().mockResolvedValue(undefined),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const mockContext = {
|
|
20
|
+
newPage: vi.fn().mockResolvedValue(mockPage),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const mockBrowser = {
|
|
24
|
+
newContext: vi.fn().mockResolvedValue(mockContext),
|
|
25
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const mockPlaywright = () => ({
|
|
29
|
+
chromium: {
|
|
30
|
+
launch: vi.fn().mockResolvedValue(mockBrowser),
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('BrowserManager', () => {
|
|
35
|
+
let manager: BrowserManager;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
manager = new BrowserManager(mockPlaywright);
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(async () => {
|
|
43
|
+
await manager.close();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('lazy initializes browser on first call', async () => {
|
|
47
|
+
await manager.ensureBrowser();
|
|
48
|
+
expect(mockBrowser.newContext).toHaveBeenCalledOnce();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('reuses browser on subsequent calls', async () => {
|
|
52
|
+
await manager.ensureBrowser();
|
|
53
|
+
await manager.ensureBrowser();
|
|
54
|
+
expect(mockBrowser.newContext).toHaveBeenCalledOnce();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('navigate returns title, text, url', async () => {
|
|
58
|
+
const result = await manager.navigate('https://example.com');
|
|
59
|
+
expect(result.title).toBe('Test Page');
|
|
60
|
+
expect(result.text).toBe('Hello World');
|
|
61
|
+
expect(result.url).toBe('https://example.com');
|
|
62
|
+
expect(mockPage.goto).toHaveBeenCalledWith('https://example.com', expect.any(Object));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('click calls page.click with selector', async () => {
|
|
66
|
+
await manager.click('#btn');
|
|
67
|
+
expect(mockPage.click).toHaveBeenCalledWith('#btn', expect.any(Object));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('type calls page.fill', async () => {
|
|
71
|
+
await manager.type('#input', 'hello');
|
|
72
|
+
expect(mockPage.fill).toHaveBeenCalledWith('#input', 'hello', expect.any(Object));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('screenshot returns base64', async () => {
|
|
76
|
+
const result = await manager.screenshot();
|
|
77
|
+
expect(typeof result).toBe('string');
|
|
78
|
+
expect(result).toBe(Buffer.from('fake-png').toString('base64'));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('extract returns text, links, images', async () => {
|
|
82
|
+
mockPage.$$eval.mockResolvedValueOnce(['https://link1.com']).mockResolvedValueOnce(['img1.png']);
|
|
83
|
+
const result = await manager.extract();
|
|
84
|
+
expect(result.text).toBe('Hello World');
|
|
85
|
+
expect(result.links).toEqual(['https://link1.com']);
|
|
86
|
+
expect(result.images).toEqual(['img1.png']);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('scroll calls evaluate with direction', async () => {
|
|
90
|
+
await manager.scroll('down', 300);
|
|
91
|
+
expect(mockPage.evaluate).toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('back calls goBack', async () => {
|
|
95
|
+
await manager.back();
|
|
96
|
+
expect(mockPage.goBack).toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('evaluate runs script', async () => {
|
|
100
|
+
mockPage.evaluate.mockResolvedValueOnce(42);
|
|
101
|
+
const result = await manager.evaluate('1+1');
|
|
102
|
+
expect(result).toBe(42);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('getImages calls $$eval', async () => {
|
|
106
|
+
mockPage.$$eval.mockResolvedValueOnce([{ src: 'a.png', alt: 'A' }]);
|
|
107
|
+
const images = await manager.getImages();
|
|
108
|
+
expect(images).toEqual([{ src: 'a.png', alt: 'A' }]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('waitFor returns true when found', async () => {
|
|
112
|
+
const result = await manager.waitFor('.test');
|
|
113
|
+
expect(result).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('waitFor returns false on timeout', async () => {
|
|
117
|
+
mockPage.waitForSelector.mockRejectedValueOnce(new Error('timeout'));
|
|
118
|
+
const result = await manager.waitFor('.missing', 100);
|
|
119
|
+
expect(result).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('close cleans up browser', async () => {
|
|
123
|
+
await manager.ensureBrowser();
|
|
124
|
+
await manager.close();
|
|
125
|
+
expect(mockBrowser.close).toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('close is safe to call without browser', async () => {
|
|
129
|
+
await manager.close(); // should not throw
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('throws helpful error when playwright not installed', async () => {
|
|
133
|
+
const noPlaywright = new BrowserManager();
|
|
134
|
+
await expect(noPlaywright.ensureBrowser()).rejects.toThrow('Install playwright: npm i playwright');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('Browser tool parameter validation', () => {
|
|
139
|
+
it('browser_navigate requires url', async () => {
|
|
140
|
+
const { browserNavigateTool } = await import('../src/tools/builtin/browser');
|
|
141
|
+
const result = await browserNavigateTool.execute({});
|
|
142
|
+
expect(result.isError).toBe(true);
|
|
143
|
+
expect(result.content).toContain('url');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('browser_click requires selector', async () => {
|
|
147
|
+
const { browserClickTool } = await import('../src/tools/builtin/browser');
|
|
148
|
+
const result = await browserClickTool.execute({});
|
|
149
|
+
expect(result.isError).toBe(true);
|
|
150
|
+
expect(result.content).toContain('selector');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('browser_type requires selector and text', async () => {
|
|
154
|
+
const { browserTypeTool } = await import('../src/tools/builtin/browser');
|
|
155
|
+
const result = await browserTypeTool.execute({});
|
|
156
|
+
expect(result.isError).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('browser_eval requires script', async () => {
|
|
160
|
+
const { browserEvalTool } = await import('../src/tools/builtin/browser');
|
|
161
|
+
const result = await browserEvalTool.execute({});
|
|
162
|
+
expect(result.isError).toBe(true);
|
|
163
|
+
expect(result.content).toContain('script');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('browser_wait requires selector', async () => {
|
|
167
|
+
const { browserWaitTool } = await import('../src/tools/builtin/browser');
|
|
168
|
+
const result = await browserWaitTool.execute({});
|
|
169
|
+
expect(result.isError).toBe(true);
|
|
170
|
+
expect(result.content).toContain('selector');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('browser_scroll requires direction', async () => {
|
|
174
|
+
const { browserScrollTool } = await import('../src/tools/builtin/browser');
|
|
175
|
+
const result = await browserScrollTool.execute({});
|
|
176
|
+
expect(result.isError).toBe(true);
|
|
177
|
+
expect(result.content).toContain('direction');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
-
import { mkdtempSync, rmSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import { tmpdir } from 'os';
|
|
5
|
-
import { getBuiltinTools, getBuiltinToolsByName } from '../src/tools/builtin';
|
|
6
|
-
import { fileTool, shellTool, datetimeTool } from '../src/tools/builtin';
|
|
7
|
-
|
|
8
|
-
describe('getBuiltinTools', () => {
|
|
9
|
-
it('returns
|
|
10
|
-
const tools = getBuiltinTools();
|
|
11
|
-
expect(tools).toHaveLength(
|
|
12
|
-
const names = tools.map(t => t.name);
|
|
13
|
-
expect(names).toContain('file_operations');
|
|
14
|
-
expect(names).toContain('web_fetch');
|
|
15
|
-
expect(names).toContain('shell_exec');
|
|
16
|
-
expect(names).toContain('datetime');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('getBuiltinToolsByName filters correctly', () => {
|
|
20
|
-
const tools = getBuiltinToolsByName(['datetime', 'file_operations']);
|
|
21
|
-
expect(tools).toHaveLength(2);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('getBuiltinToolsByName with no args returns all', () => {
|
|
25
|
-
expect(getBuiltinToolsByName()).toHaveLength(
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe('file_operations tool', () => {
|
|
30
|
-
// file tool resolves paths relative to cwd, so use relative paths from a temp dir
|
|
31
|
-
// Actually, it uses process.cwd() as workspace. Let's just test with paths relative to cwd.
|
|
32
|
-
const testFile = `tmp-test-${Date.now()}.txt`;
|
|
33
|
-
|
|
34
|
-
afterAll(() => {
|
|
35
|
-
try { require('fs').unlinkSync(testFile); } catch {}
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('write and read a file', async () => {
|
|
39
|
-
const writeRes = await fileTool.execute({ action: 'write', path: testFile, content: 'hello' });
|
|
40
|
-
expect(writeRes.isError).toBe(false);
|
|
41
|
-
|
|
42
|
-
const readRes = await fileTool.execute({ action: 'read', path: testFile });
|
|
43
|
-
expect(readRes.isError).toBe(false);
|
|
44
|
-
expect(readRes.content).toBe('hello');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('list files', async () => {
|
|
48
|
-
const res = await fileTool.execute({ action: 'list', path: '.' });
|
|
49
|
-
expect(res.isError).toBe(false);
|
|
50
|
-
expect(res.content).toContain('package.json');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('exists check', async () => {
|
|
54
|
-
const res = await fileTool.execute({ action: 'exists', path: testFile });
|
|
55
|
-
expect(res.content).toBe('true');
|
|
56
|
-
|
|
57
|
-
const res2 = await fileTool.execute({ action: 'exists', path: 'nope-does-not-exist.txt' });
|
|
58
|
-
expect(res2.content).toBe('false');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('rejects path outside workspace', async () => {
|
|
62
|
-
const res = await fileTool.execute({ action: 'read', path: '../../etc/passwd' });
|
|
63
|
-
expect(res.isError).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe('datetime tool', () => {
|
|
68
|
-
it('returns valid JSON with iso field', async () => {
|
|
69
|
-
const res = await datetimeTool.execute({});
|
|
70
|
-
expect(res.isError).toBe(false);
|
|
71
|
-
const parsed = JSON.parse(res.content);
|
|
72
|
-
expect(parsed.iso).toBeDefined();
|
|
73
|
-
expect(new Date(parsed.iso).toISOString()).toBe(parsed.iso);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe('shell_exec tool', () => {
|
|
78
|
-
it('runs a command', async () => {
|
|
79
|
-
const res = await shellTool.execute({ command: 'echo hello' });
|
|
80
|
-
expect(res.isError).toBe(false);
|
|
81
|
-
expect(res.content).toContain('hello');
|
|
82
|
-
});
|
|
83
|
-
});
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { getBuiltinTools, getBuiltinToolsByName } from '../src/tools/builtin';
|
|
6
|
+
import { fileTool, shellTool, datetimeTool } from '../src/tools/builtin';
|
|
7
|
+
|
|
8
|
+
describe('getBuiltinTools', () => {
|
|
9
|
+
it('returns 17 tools', () => {
|
|
10
|
+
const tools = getBuiltinTools();
|
|
11
|
+
expect(tools).toHaveLength(31);
|
|
12
|
+
const names = tools.map(t => t.name);
|
|
13
|
+
expect(names).toContain('file_operations');
|
|
14
|
+
expect(names).toContain('web_fetch');
|
|
15
|
+
expect(names).toContain('shell_exec');
|
|
16
|
+
expect(names).toContain('datetime');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('getBuiltinToolsByName filters correctly', () => {
|
|
20
|
+
const tools = getBuiltinToolsByName(['datetime', 'file_operations']);
|
|
21
|
+
expect(tools).toHaveLength(2);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('getBuiltinToolsByName with no args returns all', () => {
|
|
25
|
+
expect(getBuiltinToolsByName()).toHaveLength(31);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('file_operations tool', () => {
|
|
30
|
+
// file tool resolves paths relative to cwd, so use relative paths from a temp dir
|
|
31
|
+
// Actually, it uses process.cwd() as workspace. Let's just test with paths relative to cwd.
|
|
32
|
+
const testFile = `tmp-test-${Date.now()}.txt`;
|
|
33
|
+
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
try { require('fs').unlinkSync(testFile); } catch {}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('write and read a file', async () => {
|
|
39
|
+
const writeRes = await fileTool.execute({ action: 'write', path: testFile, content: 'hello' });
|
|
40
|
+
expect(writeRes.isError).toBe(false);
|
|
41
|
+
|
|
42
|
+
const readRes = await fileTool.execute({ action: 'read', path: testFile });
|
|
43
|
+
expect(readRes.isError).toBe(false);
|
|
44
|
+
expect(readRes.content).toBe('hello');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('list files', async () => {
|
|
48
|
+
const res = await fileTool.execute({ action: 'list', path: '.' });
|
|
49
|
+
expect(res.isError).toBe(false);
|
|
50
|
+
expect(res.content).toContain('package.json');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('exists check', async () => {
|
|
54
|
+
const res = await fileTool.execute({ action: 'exists', path: testFile });
|
|
55
|
+
expect(res.content).toBe('true');
|
|
56
|
+
|
|
57
|
+
const res2 = await fileTool.execute({ action: 'exists', path: 'nope-does-not-exist.txt' });
|
|
58
|
+
expect(res2.content).toBe('false');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('rejects path outside workspace', async () => {
|
|
62
|
+
const res = await fileTool.execute({ action: 'read', path: '../../etc/passwd' });
|
|
63
|
+
expect(res.isError).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('datetime tool', () => {
|
|
68
|
+
it('returns valid JSON with iso field', async () => {
|
|
69
|
+
const res = await datetimeTool.execute({});
|
|
70
|
+
expect(res.isError).toBe(false);
|
|
71
|
+
const parsed = JSON.parse(res.content);
|
|
72
|
+
expect(parsed.iso).toBeDefined();
|
|
73
|
+
expect(new Date(parsed.iso).toISOString()).toBe(parsed.iso);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('shell_exec tool', () => {
|
|
78
|
+
it('runs a command', async () => {
|
|
79
|
+
const res = await shellTool.execute({ command: 'echo hello' });
|
|
80
|
+
expect(res.isError).toBe(false);
|
|
81
|
+
expect(res.content).toContain('hello');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { WhatsAppChannel } from '../src/channels/whatsapp';
|
|
3
|
+
import { SignalChannel } from '../src/channels/signal';
|
|
4
|
+
import { MatrixChannel } from '../src/channels/matrix';
|
|
5
|
+
import { IMessageChannel } from '../src/channels/imessage';
|
|
6
|
+
import { LINEChannel } from '../src/channels/line';
|
|
7
|
+
import { MSTeamsChannel } from '../src/channels/msteams';
|
|
8
|
+
import { QQChannel } from '../src/channels/qq';
|
|
9
|
+
import { NostrChannel } from '../src/channels/nostr';
|
|
10
|
+
import { SMSChannel } from '../src/channels/sms';
|
|
11
|
+
|
|
12
|
+
describe('Additional Channels', () => {
|
|
13
|
+
it('should create WhatsApp channel', () => {
|
|
14
|
+
const ch = new WhatsAppChannel({ phoneNumber: '+1234567890' });
|
|
15
|
+
expect(ch.type).toBe('whatsapp');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should throw on start without deps', async () => {
|
|
19
|
+
const ch = new SignalChannel();
|
|
20
|
+
await expect(ch.start()).rejects.toThrow(/Install/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should create Matrix channel', () => {
|
|
24
|
+
const ch = new MatrixChannel({ homeserverUrl: 'https://matrix.org' });
|
|
25
|
+
expect(ch.type).toBe('matrix');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should create all channel types', () => {
|
|
29
|
+
const channels = [
|
|
30
|
+
new IMessageChannel(),
|
|
31
|
+
new LINEChannel(),
|
|
32
|
+
new MSTeamsChannel(),
|
|
33
|
+
new QQChannel(),
|
|
34
|
+
new NostrChannel(),
|
|
35
|
+
new SMSChannel(),
|
|
36
|
+
];
|
|
37
|
+
const types = channels.map(c => c.type);
|
|
38
|
+
expect(types).toEqual(['imessage', 'line', 'msteams', 'qq', 'nostr', 'sms']);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should throw on send before start', async () => {
|
|
42
|
+
const ch = new WhatsAppChannel();
|
|
43
|
+
await expect(ch.send('123', 'hello')).rejects.toThrow(/not yet connected/);
|
|
44
|
+
});
|
|
45
|
+
});
|