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,40 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { haGetStates, haCallService, haGetHistory, haAutomation, configureHomeAssistant } from '../src/tools/builtin/home-assistant';
|
|
3
|
+
|
|
4
|
+
describe('Home Assistant Tools', () => {
|
|
5
|
+
it('ha_get_states fails without config', async () => {
|
|
6
|
+
const r = await haGetStates.execute({});
|
|
7
|
+
expect(r.isError).toBe(true);
|
|
8
|
+
expect(r.content).toContain('not configured');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('ha_call_service fails without config', async () => {
|
|
12
|
+
const r = await haCallService.execute({ domain: 'light', service: 'turn_on', entity_id: 'light.living' });
|
|
13
|
+
expect(r.isError).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('ha_get_history fails without config', async () => {
|
|
17
|
+
const r = await haGetHistory.execute({ entity_id: 'sensor.temp' });
|
|
18
|
+
expect(r.isError).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('ha_automation list fails without config', async () => {
|
|
22
|
+
const r = await haAutomation.execute({ action: 'list' });
|
|
23
|
+
expect(r.isError).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('ha_automation requires automation_id for trigger', async () => {
|
|
27
|
+
configureHomeAssistant({ url: 'http://localhost:8123', token: 'test' });
|
|
28
|
+
// Will fail on fetch but tests the validation path
|
|
29
|
+
const r = await haAutomation.execute({ action: 'trigger' });
|
|
30
|
+
expect(r.isError).toBe(true);
|
|
31
|
+
expect(r.content).toContain('automation_id required');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('all HA tools have correct names', () => {
|
|
35
|
+
expect(haGetStates.name).toBe('ha_get_states');
|
|
36
|
+
expect(haCallService.name).toBe('ha_call_service');
|
|
37
|
+
expect(haGetHistory.name).toBe('ha_get_history');
|
|
38
|
+
expect(haAutomation.name).toBe('ha_automation');
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { HookManager, ALL_HOOK_EVENTS } from '../src/core/hooks';
|
|
3
|
+
import type { HookContext, HookEvent } from '../src/core/hooks';
|
|
4
|
+
|
|
5
|
+
describe('HookManager', () => {
|
|
6
|
+
it('should have 14 hook events defined', () => {
|
|
7
|
+
// 7 before/after pairs (message/tool/llm/send/learn/recall) = 12 + on:error/start/stop = 15
|
|
8
|
+
expect(ALL_HOOK_EVENTS.length).toBe(15);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should register and run hooks', async () => {
|
|
12
|
+
const mgr = new HookManager();
|
|
13
|
+
let called = false;
|
|
14
|
+
mgr.register('before:message', () => { called = true; });
|
|
15
|
+
await mgr.run('before:message');
|
|
16
|
+
expect(called).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should run hooks in priority order', async () => {
|
|
20
|
+
const mgr = new HookManager();
|
|
21
|
+
const order: number[] = [];
|
|
22
|
+
mgr.register('before:tool', () => { order.push(2); }, { priority: 200 });
|
|
23
|
+
mgr.register('before:tool', () => { order.push(1); }, { priority: 50 });
|
|
24
|
+
mgr.register('before:tool', () => { order.push(3); }, { priority: 300 });
|
|
25
|
+
await mgr.run('before:tool');
|
|
26
|
+
expect(order).toEqual([1, 2, 3]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should allow context modification', async () => {
|
|
30
|
+
const mgr = new HookManager();
|
|
31
|
+
mgr.register('before:llm', (ctx) => ({ ...ctx, modified: true }));
|
|
32
|
+
mgr.register('before:llm', (ctx) => ({ ...ctx, extra: 'data' }));
|
|
33
|
+
const result = await mgr.run('before:llm', { original: true });
|
|
34
|
+
expect(result.original).toBe(true);
|
|
35
|
+
expect(result.modified).toBe(true);
|
|
36
|
+
expect(result.extra).toBe('data');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should unregister hooks', async () => {
|
|
40
|
+
const mgr = new HookManager();
|
|
41
|
+
let count = 0;
|
|
42
|
+
const id = mgr.register('after:message', () => { count++; });
|
|
43
|
+
await mgr.run('after:message');
|
|
44
|
+
expect(count).toBe(1);
|
|
45
|
+
expect(mgr.unregister(id)).toBe(true);
|
|
46
|
+
await mgr.run('after:message');
|
|
47
|
+
expect(count).toBe(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should list registered hooks', () => {
|
|
51
|
+
const mgr = new HookManager();
|
|
52
|
+
mgr.register('on:error', () => {}, { name: 'error-logger', priority: 10 });
|
|
53
|
+
const list = mgr.getRegistered('on:error');
|
|
54
|
+
expect(list).toHaveLength(1);
|
|
55
|
+
expect(list[0].name).toBe('error-logger');
|
|
56
|
+
expect(list[0].priority).toBe(10);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should clear hooks', () => {
|
|
60
|
+
const mgr = new HookManager();
|
|
61
|
+
mgr.register('on:start', () => {});
|
|
62
|
+
mgr.register('on:stop', () => {});
|
|
63
|
+
mgr.clear('on:start');
|
|
64
|
+
expect(mgr.hasHooks('on:start')).toBe(false);
|
|
65
|
+
expect(mgr.hasHooks('on:stop')).toBe(true);
|
|
66
|
+
mgr.clear();
|
|
67
|
+
expect(mgr.hasHooks('on:stop')).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should handle async hooks', async () => {
|
|
71
|
+
const mgr = new HookManager();
|
|
72
|
+
mgr.register('before:send', async (ctx) => {
|
|
73
|
+
await new Promise(r => setTimeout(r, 5));
|
|
74
|
+
return { ...ctx, async: true };
|
|
75
|
+
});
|
|
76
|
+
const result = await mgr.run('before:send', {});
|
|
77
|
+
expect(result.async).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { IDEBridge } from '../src/core/ide-bridge';
|
|
3
|
+
|
|
4
|
+
describe('IDEBridge', () => {
|
|
5
|
+
it('should create with vscode config', () => {
|
|
6
|
+
const bridge = new IDEBridge({ editor: 'vscode' });
|
|
7
|
+
expect(bridge).toBeInstanceOf(IDEBridge);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should create with jetbrains config', () => {
|
|
11
|
+
const bridge = new IDEBridge({ editor: 'jetbrains', workspacePath: '/tmp' });
|
|
12
|
+
expect(bridge).toBeInstanceOf(IDEBridge);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('getDiagnostics returns empty array (stub)', async () => {
|
|
16
|
+
const bridge = new IDEBridge({ editor: 'vscode' });
|
|
17
|
+
const diags = await bridge.getDiagnostics();
|
|
18
|
+
expect(diags).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('getOpenFiles returns empty array (stub)', async () => {
|
|
22
|
+
const bridge = new IDEBridge({ editor: 'zed' });
|
|
23
|
+
const files = await bridge.getOpenFiles();
|
|
24
|
+
expect(files).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('getSelection returns null (stub)', async () => {
|
|
28
|
+
const bridge = new IDEBridge({ editor: 'cursor' });
|
|
29
|
+
const sel = await bridge.getSelection();
|
|
30
|
+
expect(sel).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('applyEdit throws for non-empty edits (stub)', async () => {
|
|
34
|
+
const bridge = new IDEBridge({ editor: 'vscode' });
|
|
35
|
+
await expect(bridge.applyEdit('test.ts', [{ range: { startLine: 1, startColumn: 0, endLine: 1, endColumn: 5 }, newText: 'hi' }]))
|
|
36
|
+
.rejects.toThrow('extension');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { ImageGenerator } from '../src/tools/image-generator';
|
|
3
|
+
|
|
4
|
+
describe('ImageGenerator', () => {
|
|
5
|
+
describe('detectProvider', () => {
|
|
6
|
+
it('returns null when no keys configured', () => {
|
|
7
|
+
const gen = new ImageGenerator({});
|
|
8
|
+
expect(gen.detectProvider()).toBeNull();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('detects dalle when openai key set', () => {
|
|
12
|
+
const gen = new ImageGenerator({ openaiApiKey: 'sk-test' });
|
|
13
|
+
expect(gen.detectProvider()).toBe('dalle');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('detects stable-diffusion when SD URL set', () => {
|
|
17
|
+
const gen = new ImageGenerator({ sdApiUrl: 'http://localhost:7860' });
|
|
18
|
+
expect(gen.detectProvider()).toBe('stable-diffusion');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('detects replicate when token set', () => {
|
|
22
|
+
const gen = new ImageGenerator({ replicateApiKey: 'r8_test' });
|
|
23
|
+
expect(gen.detectProvider()).toBe('replicate');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('prefers dalle over others', () => {
|
|
27
|
+
const gen = new ImageGenerator({ openaiApiKey: 'sk-test', sdApiUrl: 'http://localhost:7860' });
|
|
28
|
+
expect(gen.detectProvider()).toBe('dalle');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('getStatus', () => {
|
|
33
|
+
it('returns provider status', () => {
|
|
34
|
+
const gen = new ImageGenerator({ openaiApiKey: 'sk-test' });
|
|
35
|
+
const status = gen.getStatus();
|
|
36
|
+
expect(status.configured).toBe(true);
|
|
37
|
+
expect(status.providers).toHaveLength(3);
|
|
38
|
+
expect(status.providers.find(p => p.name === 'dalle')?.configured).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('shows unconfigured when no keys', () => {
|
|
42
|
+
const gen = new ImageGenerator({});
|
|
43
|
+
const status = gen.getStatus();
|
|
44
|
+
expect(status.configured).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('generate', () => {
|
|
49
|
+
it('returns error when no provider configured', async () => {
|
|
50
|
+
const gen = new ImageGenerator({});
|
|
51
|
+
const result = await gen.generate('a cat');
|
|
52
|
+
expect(result.success).toBe(false);
|
|
53
|
+
expect(result.error).toContain('No image generation provider configured');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns error for unknown provider', async () => {
|
|
57
|
+
const gen = new ImageGenerator({});
|
|
58
|
+
const result = await gen.generate('a cat', { provider: 'unknown' });
|
|
59
|
+
expect(result.success).toBe(false);
|
|
60
|
+
expect(result.error).toContain('Unknown provider');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns error when dalle key missing', async () => {
|
|
64
|
+
const gen = new ImageGenerator({});
|
|
65
|
+
const result = await gen.generate('a cat', { provider: 'dalle' });
|
|
66
|
+
expect(result.success).toBe(false);
|
|
67
|
+
expect(result.error).toContain('OPENAI_API_KEY');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns error when SD URL missing', async () => {
|
|
71
|
+
const gen = new ImageGenerator({});
|
|
72
|
+
const result = await gen.generate('a cat', { provider: 'stable-diffusion' });
|
|
73
|
+
expect(result.success).toBe(false);
|
|
74
|
+
expect(result.error).toContain('SD_API_URL');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns error when replicate key missing', async () => {
|
|
78
|
+
const gen = new ImageGenerator({});
|
|
79
|
+
const result = await gen.generate('a cat', { provider: 'replicate' });
|
|
80
|
+
expect(result.success).toBe(false);
|
|
81
|
+
expect(result.error).toContain('REPLICATE_API_TOKEN');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { NodeNetwork } from '../src/core/node-network';
|
|
3
|
+
|
|
4
|
+
describe('NodeNetwork', () => {
|
|
5
|
+
let network: NodeNetwork;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => { network = new NodeNetwork(); });
|
|
8
|
+
|
|
9
|
+
it('should add a node', () => {
|
|
10
|
+
const node = network.addNode({ name: 'pi-1', type: 'pi', host: '192.168.1.10' });
|
|
11
|
+
expect(node.id).toBeTruthy();
|
|
12
|
+
expect(node.name).toBe('pi-1');
|
|
13
|
+
expect(network.listNodes().length).toBe(1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should remove a node', () => {
|
|
17
|
+
const node = network.addNode({ name: 'test' });
|
|
18
|
+
network.removeNode(node.id);
|
|
19
|
+
expect(network.listNodes().length).toBe(0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should throw on removing unknown node', () => {
|
|
23
|
+
expect(() => network.removeNode('unknown')).toThrow('not found');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should get node by id', () => {
|
|
27
|
+
const node = network.addNode({ name: 'desktop-1' });
|
|
28
|
+
expect(network.getNode(node.id)?.name).toBe('desktop-1');
|
|
29
|
+
expect(network.getNode('nonexistent')).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should pair a node', async () => {
|
|
33
|
+
const node = network.addNode({ name: 'phone', type: 'phone', status: 'pairing' });
|
|
34
|
+
const result = await network.pair(node.id, 'ABC123');
|
|
35
|
+
expect(result).toBe(true);
|
|
36
|
+
expect(network.getNode(node.id)?.status).toBe('online');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should fail pairing with short code', async () => {
|
|
40
|
+
const node = network.addNode({ name: 'phone', status: 'pairing' });
|
|
41
|
+
const result = await network.pair(node.id, 'ab');
|
|
42
|
+
expect(result).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should send command to online node', async () => {
|
|
46
|
+
const node = network.addNode({ name: 'vps', status: 'online' });
|
|
47
|
+
const result = await network.sendCommand(node.id, 'uptime');
|
|
48
|
+
expect(result.command).toBe('uptime');
|
|
49
|
+
expect(result.status).toBe('sent');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should throw sending to offline node', async () => {
|
|
53
|
+
const node = network.addNode({ name: 'offline', status: 'offline' });
|
|
54
|
+
await expect(network.sendCommand(node.id, 'test')).rejects.toThrow('offline');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should broadcast to online nodes', async () => {
|
|
58
|
+
network.addNode({ name: 'n1', status: 'online' });
|
|
59
|
+
network.addNode({ name: 'n2', status: 'online' });
|
|
60
|
+
network.addNode({ name: 'n3', status: 'offline' });
|
|
61
|
+
const results = await network.broadcast('ping');
|
|
62
|
+
expect(results.size).toBe(2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should health check all nodes', async () => {
|
|
66
|
+
network.addNode({ name: 'up', status: 'online' });
|
|
67
|
+
network.addNode({ name: 'down', status: 'offline' });
|
|
68
|
+
const health = await network.healthCheck();
|
|
69
|
+
expect(health.size).toBe(2);
|
|
70
|
+
const vals = Array.from(health.values());
|
|
71
|
+
expect(vals).toContain(true);
|
|
72
|
+
expect(vals).toContain(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { ProfileManager } from '../src/core/profiles';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
|
|
7
|
+
describe('ProfileManager', () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
let pm: ProfileManager;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
tmpDir = path.join(os.tmpdir(), `opc-profiles-test-${Date.now()}`);
|
|
13
|
+
pm = new ProfileManager(tmpDir);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should create a profile', () => {
|
|
21
|
+
const p = pm.create('test-profile', { model: 'gpt-4o' });
|
|
22
|
+
expect(p.name).toBe('test-profile');
|
|
23
|
+
expect(p.config.model).toBe('gpt-4o');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should throw on duplicate create', () => {
|
|
27
|
+
pm.create('dup');
|
|
28
|
+
expect(() => pm.create('dup')).toThrow('already exists');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should list profiles', () => {
|
|
32
|
+
pm.create('a');
|
|
33
|
+
pm.create('b');
|
|
34
|
+
const list = pm.list();
|
|
35
|
+
expect(list).toHaveLength(2);
|
|
36
|
+
expect(list.map(p => p.name).sort()).toEqual(['a', 'b']);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should switch profiles', () => {
|
|
40
|
+
pm.create('prof1');
|
|
41
|
+
pm.create('prof2');
|
|
42
|
+
pm.switch('prof1');
|
|
43
|
+
expect(pm.current().name).toBe('prof1');
|
|
44
|
+
pm.switch('prof2');
|
|
45
|
+
expect(pm.current().name).toBe('prof2');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should delete a non-current profile', () => {
|
|
49
|
+
pm.create('keeper');
|
|
50
|
+
pm.create('goner');
|
|
51
|
+
pm.switch('keeper');
|
|
52
|
+
pm.delete('goner');
|
|
53
|
+
expect(pm.list()).toHaveLength(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should throw when deleting current profile', () => {
|
|
57
|
+
pm.create('active');
|
|
58
|
+
pm.switch('active');
|
|
59
|
+
expect(() => pm.delete('active')).toThrow('Cannot delete');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
rlRecordTrajectory, rlEvaluateOutcome, rlGetBestStrategy, rlCompareStrategies,
|
|
4
|
+
rlGenerateTrainingData, rlRewardSignal, rlExplorationSuggest, rlUpdatePolicy,
|
|
5
|
+
rlGetStatistics, rlResetEpisode,
|
|
6
|
+
} from '../src/tools/builtin/rl-tools';
|
|
7
|
+
|
|
8
|
+
describe('RL Tools', () => {
|
|
9
|
+
beforeEach(async () => { await rlResetEpisode.execute({}); });
|
|
10
|
+
|
|
11
|
+
it('rl_record_trajectory records actions', async () => {
|
|
12
|
+
const r = await rlRecordTrajectory.execute({ taskType: 'search', action: 'web_search' });
|
|
13
|
+
const data = JSON.parse(r.content);
|
|
14
|
+
expect(data.episodeId).toBeTruthy();
|
|
15
|
+
expect(data.actionsRecorded).toBe(1);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('rl_evaluate_outcome scores episode', async () => {
|
|
19
|
+
await rlRecordTrajectory.execute({ taskType: 'search', action: 'web_search' });
|
|
20
|
+
const r = await rlEvaluateOutcome.execute({ outcome: 'success' });
|
|
21
|
+
const data = JSON.parse(r.content);
|
|
22
|
+
expect(data.score).toBe(1);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('rl_get_best_strategy returns null when no data', async () => {
|
|
26
|
+
const r = await rlGetBestStrategy.execute({ taskType: 'nonexistent_xyz' });
|
|
27
|
+
expect(JSON.parse(r.content).strategy).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('rl_compare_strategies returns breakdown', async () => {
|
|
31
|
+
await rlRecordTrajectory.execute({ taskType: 'coding', action: 'write', outcome: 'success' });
|
|
32
|
+
await rlResetEpisode.execute({});
|
|
33
|
+
await rlRecordTrajectory.execute({ taskType: 'coding', action: 'debug', outcome: 'failure' });
|
|
34
|
+
const r = await rlCompareStrategies.execute({ taskType: 'coding' });
|
|
35
|
+
const data = JSON.parse(r.content);
|
|
36
|
+
expect(data.total).toBe(2);
|
|
37
|
+
expect(data.breakdown.successes).toBe(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('rl_generate_training_data exports JSONL', async () => {
|
|
41
|
+
await rlRecordTrajectory.execute({ taskType: 'test', action: 'action1', outcome: 'success' });
|
|
42
|
+
const r = await rlGenerateTrainingData.execute({ taskType: 'test' });
|
|
43
|
+
expect(r.content).toContain('action1');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('rl_reward_signal records reward', async () => {
|
|
47
|
+
await rlRecordTrajectory.execute({ taskType: 'test', action: 'act' });
|
|
48
|
+
const r = await rlRewardSignal.execute({ reward: 1.5 });
|
|
49
|
+
const data = JSON.parse(r.content);
|
|
50
|
+
expect(data.reward).toBe(1.5);
|
|
51
|
+
expect(data.totalReward).toBe(1.5);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('rl_reward_signal fails without episode', async () => {
|
|
55
|
+
const r = await rlRewardSignal.execute({ reward: 1 });
|
|
56
|
+
expect(r.isError).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('rl_exploration_suggest returns suggestions', async () => {
|
|
60
|
+
await rlRecordTrajectory.execute({ taskType: 'explore', action: 'a' });
|
|
61
|
+
await rlResetEpisode.execute({});
|
|
62
|
+
await rlRecordTrajectory.execute({ taskType: 'explore', action: 'b' });
|
|
63
|
+
const r = await rlExplorationSuggest.execute({ taskType: 'explore', currentAction: 'a' });
|
|
64
|
+
const data = JSON.parse(r.content);
|
|
65
|
+
expect(data.suggestions).toContain('b');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('rl_update_policy updates weights', async () => {
|
|
69
|
+
const r = await rlUpdatePolicy.execute({ taskType: 'code', action: 'refactor', weight: 2 });
|
|
70
|
+
const data = JSON.parse(r.content);
|
|
71
|
+
expect(data.weights.refactor).toBe(2);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('rl_get_statistics returns stats', async () => {
|
|
75
|
+
await rlRecordTrajectory.execute({ taskType: 'stats_test', action: 'a', outcome: 'success' });
|
|
76
|
+
const r = await rlGetStatistics.execute({ taskType: 'stats_test' });
|
|
77
|
+
const data = JSON.parse(r.content);
|
|
78
|
+
expect(data.stats_test.total).toBe(1);
|
|
79
|
+
expect(data.stats_test.success).toBe(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('rl_reset_episode clears state', async () => {
|
|
83
|
+
await rlRecordTrajectory.execute({ taskType: 'test', action: 'a' });
|
|
84
|
+
const r = await rlResetEpisode.execute({});
|
|
85
|
+
expect(JSON.parse(r.content).hadActiveEpisode).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('trajectory recording accumulates actions', async () => {
|
|
89
|
+
await rlRecordTrajectory.execute({ taskType: 'multi', action: 'step1' });
|
|
90
|
+
const r = await rlRecordTrajectory.execute({ taskType: 'multi', action: 'step2' });
|
|
91
|
+
expect(JSON.parse(r.content).actionsRecorded).toBe(2);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { SandboxManager } from '../src/core/sandbox';
|
|
3
|
+
|
|
4
|
+
describe('SandboxManager (Remote)', () => {
|
|
5
|
+
it('should create with default local backend', () => {
|
|
6
|
+
const sm = new SandboxManager();
|
|
7
|
+
expect(sm).toBeDefined();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should exec local command', async () => {
|
|
11
|
+
const sm = new SandboxManager({ backend: 'local' });
|
|
12
|
+
const result = await sm.exec('echo hello');
|
|
13
|
+
expect(result.exitCode).toBe(0);
|
|
14
|
+
expect(result.stdout).toContain('hello');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should return non-zero exit code on failure', async () => {
|
|
18
|
+
const sm = new SandboxManager({ backend: 'local' });
|
|
19
|
+
const result = await sm.exec('exit 1');
|
|
20
|
+
expect(result.exitCode).not.toBe(0);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should throw on docker without image', async () => {
|
|
24
|
+
const sm = new SandboxManager({ backend: 'docker' });
|
|
25
|
+
await expect(sm.exec('echo hi')).rejects.toThrow('Docker image is required');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should throw on ssh without host', async () => {
|
|
29
|
+
const sm = new SandboxManager({ backend: 'ssh' });
|
|
30
|
+
await expect(sm.exec('echo hi')).rejects.toThrow('SSH host and user are required');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should upload locally (copy)', async () => {
|
|
34
|
+
const sm = new SandboxManager({ backend: 'local' });
|
|
35
|
+
const fs = await import('fs');
|
|
36
|
+
const path = await import('path');
|
|
37
|
+
const os = await import('os');
|
|
38
|
+
const src = path.join(os.tmpdir(), 'sandbox-test-src.txt');
|
|
39
|
+
const dst = path.join(os.tmpdir(), 'sandbox-test-dst.txt');
|
|
40
|
+
fs.writeFileSync(src, 'test content');
|
|
41
|
+
await sm.upload(src, dst);
|
|
42
|
+
expect(fs.readFileSync(dst, 'utf-8')).toBe('test content');
|
|
43
|
+
fs.unlinkSync(src);
|
|
44
|
+
fs.unlinkSync(dst);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import { SecretsManager } from '../src/security/secrets';
|
|
3
|
+
import { existsSync, unlinkSync, mkdirSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
|
|
7
|
+
const testDir = join(tmpdir(), 'opc-secrets-test-' + Date.now());
|
|
8
|
+
let counter = 0;
|
|
9
|
+
function testPath() { return join(testDir, `secrets-${++counter}.enc`); }
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
// cleanup
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('SecretsManager', () => {
|
|
16
|
+
it('should create new store and set/get secrets', () => {
|
|
17
|
+
const fp = testPath();
|
|
18
|
+
const mgr = new SecretsManager({ password: 'test123', filePath: fp });
|
|
19
|
+
mgr.set('API_KEY', 'sk-abc123');
|
|
20
|
+
expect(mgr.get('API_KEY')).toBe('sk-abc123');
|
|
21
|
+
expect(existsSync(fp)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should persist and reload secrets', () => {
|
|
25
|
+
const fp = testPath();
|
|
26
|
+
const mgr1 = new SecretsManager({ password: 'pw', filePath: fp });
|
|
27
|
+
mgr1.set('TOKEN', 'xyz');
|
|
28
|
+
const mgr2 = new SecretsManager({ password: 'pw', filePath: fp });
|
|
29
|
+
expect(mgr2.get('TOKEN')).toBe('xyz');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should fail with wrong password', () => {
|
|
33
|
+
const fp = testPath();
|
|
34
|
+
const mgr = new SecretsManager({ password: 'right', filePath: fp });
|
|
35
|
+
mgr.set('KEY', 'val');
|
|
36
|
+
expect(() => new SecretsManager({ password: 'wrong', filePath: fp })).toThrow();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should delete secrets', () => {
|
|
40
|
+
const fp = testPath();
|
|
41
|
+
const mgr = new SecretsManager({ password: 'pw', filePath: fp });
|
|
42
|
+
mgr.set('A', '1');
|
|
43
|
+
expect(mgr.delete('A')).toBe(true);
|
|
44
|
+
expect(mgr.get('A')).toBeUndefined();
|
|
45
|
+
expect(mgr.delete('nonexistent')).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should list secret keys', () => {
|
|
49
|
+
const fp = testPath();
|
|
50
|
+
const mgr = new SecretsManager({ password: 'pw', filePath: fp });
|
|
51
|
+
mgr.set('A', '1');
|
|
52
|
+
mgr.set('B', '2');
|
|
53
|
+
expect(mgr.list().sort()).toEqual(['A', 'B']);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should check has', () => {
|
|
57
|
+
const fp = testPath();
|
|
58
|
+
const mgr = new SecretsManager({ password: 'pw', filePath: fp });
|
|
59
|
+
mgr.set('X', 'y');
|
|
60
|
+
expect(mgr.has('X')).toBe(true);
|
|
61
|
+
expect(mgr.has('Z')).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should inject secrets into env object', () => {
|
|
65
|
+
const fp = testPath();
|
|
66
|
+
const mgr = new SecretsManager({ password: 'pw', filePath: fp });
|
|
67
|
+
mgr.set('DB_HOST', 'localhost');
|
|
68
|
+
mgr.set('DB_PASS', 'secret');
|
|
69
|
+
const env: Record<string, string | undefined> = { PATH: '/usr/bin' };
|
|
70
|
+
mgr.inject(env, ['DB_HOST']);
|
|
71
|
+
expect(env.DB_HOST).toBe('localhost');
|
|
72
|
+
expect(env.DB_PASS).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should inject all secrets when no keys specified', () => {
|
|
76
|
+
const fp = testPath();
|
|
77
|
+
const mgr = new SecretsManager({ password: 'pw', filePath: fp });
|
|
78
|
+
mgr.set('A', '1');
|
|
79
|
+
mgr.set('B', '2');
|
|
80
|
+
const env: Record<string, string | undefined> = {};
|
|
81
|
+
mgr.inject(env);
|
|
82
|
+
expect(env.A).toBe('1');
|
|
83
|
+
expect(env.B).toBe('2');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should rotate password', () => {
|
|
87
|
+
const fp = testPath();
|
|
88
|
+
const mgr = new SecretsManager({ password: 'old', filePath: fp });
|
|
89
|
+
mgr.set('KEY', 'value');
|
|
90
|
+
mgr.rotate('new');
|
|
91
|
+
// Old password should fail
|
|
92
|
+
expect(() => new SecretsManager({ password: 'old', filePath: fp })).toThrow();
|
|
93
|
+
// New password should work
|
|
94
|
+
const mgr2 = new SecretsManager({ password: 'new', filePath: fp });
|
|
95
|
+
expect(mgr2.get('KEY')).toBe('value');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should export and import', () => {
|
|
99
|
+
const fp1 = testPath();
|
|
100
|
+
const mgr = new SecretsManager({ password: 'pw', filePath: fp1 });
|
|
101
|
+
mgr.set('SECRET', 'data');
|
|
102
|
+
const exported = mgr.exportEncrypted();
|
|
103
|
+
const fp2 = testPath();
|
|
104
|
+
const mgr2 = SecretsManager.importEncrypted(exported, 'pw', fp2);
|
|
105
|
+
expect(mgr2.get('SECRET')).toBe('data');
|
|
106
|
+
});
|
|
107
|
+
});
|