opc-agent 3.0.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +30 -24
  2. package/dist/channels/dingtalk.d.ts +17 -0
  3. package/dist/channels/dingtalk.js +38 -0
  4. package/dist/channels/googlechat.d.ts +14 -0
  5. package/dist/channels/googlechat.js +37 -0
  6. package/dist/channels/imessage.d.ts +13 -0
  7. package/dist/channels/imessage.js +28 -0
  8. package/dist/channels/irc.d.ts +20 -0
  9. package/dist/channels/irc.js +71 -0
  10. package/dist/channels/line.d.ts +14 -0
  11. package/dist/channels/line.js +28 -0
  12. package/dist/channels/matrix.d.ts +15 -0
  13. package/dist/channels/matrix.js +28 -0
  14. package/dist/channels/mattermost.d.ts +18 -0
  15. package/dist/channels/mattermost.js +49 -0
  16. package/dist/channels/msteams.d.ts +14 -0
  17. package/dist/channels/msteams.js +28 -0
  18. package/dist/channels/nostr.d.ts +14 -0
  19. package/dist/channels/nostr.js +28 -0
  20. package/dist/channels/qq.d.ts +15 -0
  21. package/dist/channels/qq.js +28 -0
  22. package/dist/channels/signal.d.ts +14 -0
  23. package/dist/channels/signal.js +28 -0
  24. package/dist/channels/sms.d.ts +15 -0
  25. package/dist/channels/sms.js +28 -0
  26. package/dist/channels/twitch.d.ts +17 -0
  27. package/dist/channels/twitch.js +59 -0
  28. package/dist/channels/voice-call.d.ts +27 -0
  29. package/dist/channels/voice-call.js +82 -0
  30. package/dist/channels/whatsapp.d.ts +14 -0
  31. package/dist/channels/whatsapp.js +28 -0
  32. package/dist/cli.js +36 -0
  33. package/dist/core/api-server.d.ts +25 -0
  34. package/dist/core/api-server.js +286 -0
  35. package/dist/core/audio.d.ts +50 -0
  36. package/dist/core/audio.js +68 -0
  37. package/dist/core/context-discovery.d.ts +16 -0
  38. package/dist/core/context-discovery.js +107 -0
  39. package/dist/core/context-refs.d.ts +29 -0
  40. package/dist/core/context-refs.js +162 -0
  41. package/dist/core/gateway.d.ts +53 -0
  42. package/dist/core/gateway.js +80 -0
  43. package/dist/core/heartbeat.d.ts +19 -0
  44. package/dist/core/heartbeat.js +50 -0
  45. package/dist/core/hooks.d.ts +28 -0
  46. package/dist/core/hooks.js +82 -0
  47. package/dist/core/ide-bridge.d.ts +53 -0
  48. package/dist/core/ide-bridge.js +97 -0
  49. package/dist/core/node-network.d.ts +23 -0
  50. package/dist/core/node-network.js +77 -0
  51. package/dist/core/profiles.d.ts +27 -0
  52. package/dist/core/profiles.js +131 -0
  53. package/dist/core/sandbox.d.ts +25 -0
  54. package/dist/core/sandbox.js +84 -1
  55. package/dist/core/session-manager.d.ts +33 -0
  56. package/dist/core/session-manager.js +157 -0
  57. package/dist/core/vision.d.ts +45 -0
  58. package/dist/core/vision.js +177 -0
  59. package/dist/index.d.ts +64 -1
  60. package/dist/index.js +86 -3
  61. package/dist/memory/context-compressor.d.ts +43 -0
  62. package/dist/memory/context-compressor.js +167 -0
  63. package/dist/memory/index.d.ts +4 -0
  64. package/dist/memory/index.js +5 -1
  65. package/dist/memory/user-profiler.d.ts +50 -0
  66. package/dist/memory/user-profiler.js +201 -0
  67. package/dist/schema/oad.d.ts +12 -12
  68. package/dist/security/approvals.d.ts +53 -0
  69. package/dist/security/approvals.js +115 -0
  70. package/dist/security/elevated.d.ts +41 -0
  71. package/dist/security/elevated.js +89 -0
  72. package/dist/security/index.d.ts +6 -0
  73. package/dist/security/index.js +7 -1
  74. package/dist/security/secrets.d.ts +34 -0
  75. package/dist/security/secrets.js +115 -0
  76. package/dist/tools/builtin/browser.d.ts +47 -0
  77. package/dist/tools/builtin/browser.js +284 -0
  78. package/dist/tools/builtin/home-assistant.d.ts +12 -0
  79. package/dist/tools/builtin/home-assistant.js +126 -0
  80. package/dist/tools/builtin/index.d.ts +6 -1
  81. package/dist/tools/builtin/index.js +18 -2
  82. package/dist/tools/builtin/rl-tools.d.ts +13 -0
  83. package/dist/tools/builtin/rl-tools.js +228 -0
  84. package/dist/tools/builtin/vision.d.ts +6 -0
  85. package/dist/tools/builtin/vision.js +61 -0
  86. package/package.json +2 -2
  87. package/src/channels/dingtalk.ts +46 -0
  88. package/src/channels/googlechat.ts +42 -0
  89. package/src/channels/imessage.ts +32 -0
  90. package/src/channels/irc.ts +82 -0
  91. package/src/channels/line.ts +33 -0
  92. package/src/channels/matrix.ts +34 -0
  93. package/src/channels/mattermost.ts +57 -0
  94. package/src/channels/msteams.ts +33 -0
  95. package/src/channels/nostr.ts +33 -0
  96. package/src/channels/qq.ts +34 -0
  97. package/src/channels/signal.ts +33 -0
  98. package/src/channels/sms.ts +34 -0
  99. package/src/channels/twitch.ts +65 -0
  100. package/src/channels/voice-call.ts +100 -0
  101. package/src/channels/whatsapp.ts +33 -0
  102. package/src/cli.ts +40 -0
  103. package/src/core/api-server.ts +277 -0
  104. package/src/core/audio.ts +98 -0
  105. package/src/core/context-discovery.ts +85 -0
  106. package/src/core/context-refs.ts +140 -0
  107. package/src/core/gateway.ts +106 -0
  108. package/src/core/heartbeat.ts +51 -0
  109. package/src/core/hooks.ts +105 -0
  110. package/src/core/ide-bridge.ts +133 -0
  111. package/src/core/node-network.ts +86 -0
  112. package/src/core/profiles.ts +122 -0
  113. package/src/core/sandbox.ts +100 -0
  114. package/src/core/session-manager.ts +137 -0
  115. package/src/core/vision.ts +180 -0
  116. package/src/index.ts +84 -1
  117. package/src/memory/context-compressor.ts +189 -0
  118. package/src/memory/index.ts +4 -0
  119. package/src/memory/user-profiler.ts +215 -0
  120. package/src/security/approvals.ts +143 -0
  121. package/src/security/elevated.ts +105 -0
  122. package/src/security/index.ts +6 -0
  123. package/src/security/secrets.ts +129 -0
  124. package/src/tools/builtin/browser.ts +299 -0
  125. package/src/tools/builtin/home-assistant.ts +116 -0
  126. package/src/tools/builtin/index.ts +9 -2
  127. package/src/tools/builtin/rl-tools.ts +243 -0
  128. package/src/tools/builtin/vision.ts +64 -0
  129. package/tests/api-server.test.ts +148 -0
  130. package/tests/approvals.test.ts +89 -0
  131. package/tests/audio.test.ts +40 -0
  132. package/tests/browser.test.ts +179 -0
  133. package/tests/builtin-tools.test.ts +83 -83
  134. package/tests/channels-extra.test.ts +45 -0
  135. package/tests/context-compressor.test.ts +172 -0
  136. package/tests/context-refs.test.ts +121 -0
  137. package/tests/elevated.test.ts +69 -0
  138. package/tests/gateway.test.ts +63 -71
  139. package/tests/home-assistant.test.ts +40 -0
  140. package/tests/hooks.test.ts +79 -0
  141. package/tests/ide-bridge.test.ts +38 -0
  142. package/tests/node-network.test.ts +74 -0
  143. package/tests/profiles.test.ts +61 -0
  144. package/tests/rl-tools.test.ts +93 -0
  145. package/tests/sandbox-manager.test.ts +46 -0
  146. package/tests/secrets.test.ts +107 -0
  147. package/tests/tools/builtin-extended.test.ts +138 -138
  148. package/tests/user-profiler.test.ts +169 -0
  149. package/tests/v090-features.test.ts +254 -0
  150. package/tests/vision.test.ts +61 -0
  151. package/tests/voice-call.test.ts +47 -0
@@ -1,71 +1,63 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { ToolGateway } from '../src/tools/gateway';
3
- import type { ToolGatewayConfig } from '../src/tools/gateway';
4
-
5
- const baseConfig: ToolGatewayConfig = {
6
- enabled: true,
7
- endpoint: 'https://gateway.example.com',
8
- apiKey: 'test-key',
9
- };
10
-
11
- describe('ToolGateway', () => {
12
- it('should load default tools when connect fails', async () => {
13
- vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network error')));
14
- const gw = new ToolGateway(baseConfig);
15
- await gw.connect();
16
- expect(gw.isConnected).toBe(false);
17
- expect(gw.toolCount).toBe(4);
18
- expect(gw.listTools().map((t) => t.name)).toContain('gateway:web-search');
19
- vi.unstubAllGlobals();
20
- });
21
-
22
- it('should filter tools by enabledTools config', async () => {
23
- vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('fail')));
24
- const gw = new ToolGateway({ ...baseConfig, enabledTools: ['web-search', 'tts'] });
25
- await gw.connect();
26
- expect(gw.toolCount).toBe(2);
27
- vi.unstubAllGlobals();
28
- });
29
-
30
- it('should parse gateway discovery response', async () => {
31
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
32
- ok: true,
33
- json: async () => ({
34
- tools: [
35
- { name: 'web-search', description: 'Search', inputSchema: {}, available: true },
36
- ],
37
- }),
38
- }));
39
- const gw = new ToolGateway(baseConfig);
40
- await gw.connect();
41
- expect(gw.isConnected).toBe(true);
42
- expect(gw.toolCount).toBe(1);
43
- vi.unstubAllGlobals();
44
- });
45
-
46
- it('should return MCPTool instances from getTools()', async () => {
47
- vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('fail')));
48
- const gw = new ToolGateway(baseConfig);
49
- await gw.connect();
50
- const tools = gw.getTools();
51
- expect(tools.length).toBe(4);
52
- expect(tools[0]).toHaveProperty('execute');
53
- expect(tools[0]).toHaveProperty('name');
54
- vi.unstubAllGlobals();
55
- });
56
-
57
- it('should handle invoke errors gracefully', async () => {
58
- vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('timeout')));
59
- const gw = new ToolGateway(baseConfig);
60
- const result = await gw.invokeTool('web-search', { query: 'test' });
61
- expect(result.isError).toBe(true);
62
- expect(result.content).toContain('timeout');
63
- vi.unstubAllGlobals();
64
- });
65
-
66
- it('should not discover tools when disabled', async () => {
67
- const gw = new ToolGateway({ ...baseConfig, enabled: false });
68
- await gw.connect();
69
- expect(gw.toolCount).toBe(0);
70
- });
71
- });
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { Gateway } from '../src/core/gateway';
3
+
4
+ describe('Gateway', () => {
5
+ let gw: Gateway;
6
+
7
+ beforeEach(() => {
8
+ gw = new Gateway({
9
+ port: 3000,
10
+ agents: [{ id: 'agent-1', name: 'Test Agent' }],
11
+ channels: [{ id: 'ch-1', type: 'web' }],
12
+ });
13
+ });
14
+
15
+ it('should start and stop', async () => {
16
+ await gw.start();
17
+ expect(gw.getStatus().agents).toBe(1);
18
+ await gw.stop();
19
+ });
20
+
21
+ it('should throw on double start', async () => {
22
+ await gw.start();
23
+ await expect(gw.start()).rejects.toThrow('already running');
24
+ await gw.stop();
25
+ });
26
+
27
+ it('should route messages', async () => {
28
+ await gw.start();
29
+ const agentId = await gw.routeMessage({ id: '1', content: 'hi', channel: 'web', timestamp: Date.now() }, 'web');
30
+ expect(agentId).toBe('agent-1');
31
+ await gw.stop();
32
+ });
33
+
34
+ it('should add and remove agents', async () => {
35
+ gw.addAgent({ id: 'agent-2', name: 'Agent 2' });
36
+ expect(gw.getStatus().agents).toBe(2);
37
+ gw.removeAgent('agent-2');
38
+ expect(gw.getStatus().agents).toBe(1);
39
+ });
40
+
41
+ it('should throw removing unknown agent', () => {
42
+ expect(() => gw.removeAgent('unknown')).toThrow('not found');
43
+ });
44
+
45
+ it('should track status', async () => {
46
+ await gw.start();
47
+ const status = gw.getStatus();
48
+ expect(status.agents).toBe(1);
49
+ expect(status.channels).toBe(1);
50
+ expect(status.messagesProcessed).toBe(0);
51
+ expect(status.uptime).toBeGreaterThanOrEqual(0);
52
+ await gw.stop();
53
+ });
54
+
55
+ it('should report metrics', async () => {
56
+ await gw.start();
57
+ await gw.routeMessage({ id: '1', content: 'test', channel: 'web', timestamp: Date.now() }, 'web');
58
+ const metrics = gw.getMetrics();
59
+ expect(metrics.messagesPerMinute).toBeGreaterThan(0);
60
+ expect(metrics.errorRate).toBe(0);
61
+ await gw.stop();
62
+ });
63
+ });
@@ -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,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
+ });