@openclaw-cloud/agent-controller 0.2.6 → 0.2.8

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 (83) hide show
  1. package/bin/agent-controller.js +6 -0
  2. package/dist/commands/backup-cli.d.ts +1 -0
  3. package/dist/commands/backup-cli.js +65 -0
  4. package/dist/commands/backup-cli.js.map +1 -0
  5. package/dist/commands/install.js +3 -0
  6. package/dist/commands/install.js.map +1 -1
  7. package/dist/commands/knowledge-sync-cli.d.ts +1 -0
  8. package/dist/commands/knowledge-sync-cli.js +61 -0
  9. package/dist/commands/knowledge-sync-cli.js.map +1 -0
  10. package/dist/config-file.d.ts +9 -0
  11. package/dist/config-file.js +47 -0
  12. package/dist/config-file.js.map +1 -0
  13. package/dist/connection.d.ts +1 -0
  14. package/dist/connection.js +27 -13
  15. package/dist/connection.js.map +1 -1
  16. package/dist/handlers/backup.js +7 -2
  17. package/dist/handlers/backup.js.map +1 -1
  18. package/dist/handlers/knowledge-sync.d.ts +2 -0
  19. package/dist/handlers/knowledge-sync.js +51 -0
  20. package/dist/handlers/knowledge-sync.js.map +1 -0
  21. package/dist/heartbeat.d.ts +1 -0
  22. package/dist/heartbeat.js +30 -0
  23. package/dist/heartbeat.js.map +1 -1
  24. package/dist/index.js +7 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/types.d.ts +2 -1
  27. package/package.json +6 -1
  28. package/.claude/cc-notify.sh +0 -32
  29. package/.claude/settings.json +0 -31
  30. package/.husky/pre-commit +0 -1
  31. package/BIZPLAN.md +0 -530
  32. package/CLAUDE.md +0 -172
  33. package/Dockerfile +0 -9
  34. package/__tests__/api.test.ts +0 -183
  35. package/__tests__/backup.test.ts +0 -145
  36. package/__tests__/board-handler.test.ts +0 -323
  37. package/__tests__/chat.test.ts +0 -191
  38. package/__tests__/config.test.ts +0 -100
  39. package/__tests__/connection.test.ts +0 -289
  40. package/__tests__/file-delete.test.ts +0 -90
  41. package/__tests__/file-write.test.ts +0 -119
  42. package/__tests__/gateway-adapter.test.ts +0 -366
  43. package/__tests__/gateway-client.test.ts +0 -272
  44. package/__tests__/handlers.test.ts +0 -150
  45. package/__tests__/heartbeat.test.ts +0 -124
  46. package/__tests__/onboarding.test.ts +0 -55
  47. package/__tests__/package-install.test.ts +0 -109
  48. package/__tests__/pair.test.ts +0 -60
  49. package/__tests__/self-update.test.ts +0 -123
  50. package/__tests__/stop.test.ts +0 -38
  51. package/jest.config.ts +0 -16
  52. package/src/api.ts +0 -62
  53. package/src/commands/install.ts +0 -68
  54. package/src/commands/self-update.ts +0 -43
  55. package/src/commands/uninstall.ts +0 -19
  56. package/src/config-file.ts +0 -56
  57. package/src/connection.ts +0 -203
  58. package/src/debug.ts +0 -11
  59. package/src/handlers/backup.ts +0 -101
  60. package/src/handlers/board-handler.ts +0 -155
  61. package/src/handlers/chat.ts +0 -79
  62. package/src/handlers/config.ts +0 -48
  63. package/src/handlers/deploy.ts +0 -32
  64. package/src/handlers/exec.ts +0 -32
  65. package/src/handlers/file-delete.ts +0 -46
  66. package/src/handlers/file-write.ts +0 -65
  67. package/src/handlers/knowledge-sync.ts +0 -53
  68. package/src/handlers/onboarding.ts +0 -19
  69. package/src/handlers/package-install.ts +0 -69
  70. package/src/handlers/pair.ts +0 -26
  71. package/src/handlers/restart.ts +0 -19
  72. package/src/handlers/stop.ts +0 -17
  73. package/src/heartbeat.ts +0 -110
  74. package/src/index.ts +0 -97
  75. package/src/openclaw/gateway-adapter.ts +0 -129
  76. package/src/openclaw/gateway-client.ts +0 -131
  77. package/src/openclaw/index.ts +0 -17
  78. package/src/openclaw/types.ts +0 -41
  79. package/src/platform/linux.ts +0 -108
  80. package/src/platform/macos.ts +0 -122
  81. package/src/platform/windows.ts +0 -92
  82. package/src/types.ts +0 -94
  83. package/tsconfig.json +0 -18
@@ -1,150 +0,0 @@
1
- import { handleExec } from '../src/handlers/exec';
2
- import { handleRestart } from '../src/handlers/restart';
3
- import { handleDeploy } from '../src/handlers/deploy';
4
- import { handleConfig } from '../src/handlers/config';
5
- import { handlePair } from '../src/handlers/pair';
6
- import { handleStop } from '../src/handlers/stop';
7
- import type { AgentCommand } from '../src/types';
8
- import childProcess from 'node:child_process';
9
- import fs from 'node:fs/promises';
10
-
11
- jest.mock('node:child_process');
12
- jest.mock('node:fs/promises');
13
-
14
- const mockExec = childProcess.exec as unknown as jest.Mock;
15
-
16
- function fakeExec(error: Error | null, stdout: string, stderr: string) {
17
- mockExec.mockImplementation((_cmd: string, _opts: unknown, cb: Function) => {
18
- cb(error, Buffer.from(stdout), Buffer.from(stderr));
19
- });
20
- }
21
-
22
- describe('handleExec', () => {
23
- beforeEach(() => jest.clearAllMocks());
24
-
25
- it('executes a command and returns stdout', async () => {
26
- fakeExec(null, 'hello\n', '');
27
- const cmd: AgentCommand = { id: '1', type: 'exec', payload: { command: 'echo hello' } };
28
- const res = await handleExec(cmd);
29
- expect(res.success).toBe(true);
30
- expect(res.data?.stdout).toBe('hello\n');
31
- expect(res.data?.exitCode).toBe(0);
32
- });
33
-
34
- it('returns error when command fails', async () => {
35
- const err = new Error('command failed');
36
- (err as any).code = 127;
37
- fakeExec(err, '', 'not found');
38
- const cmd: AgentCommand = { id: '2', type: 'exec', payload: { command: 'bad' } };
39
- const res = await handleExec(cmd);
40
- expect(res.success).toBe(false);
41
- expect(res.error).toBe('command failed');
42
- });
43
-
44
- it('rejects missing command payload', async () => {
45
- const cmd: AgentCommand = { id: '3', type: 'exec', payload: {} };
46
- const res = await handleExec(cmd);
47
- expect(res.success).toBe(false);
48
- expect(res.error).toContain('Missing');
49
- });
50
- });
51
-
52
- describe('handleRestart', () => {
53
- beforeEach(() => jest.clearAllMocks());
54
-
55
- it('calls openclaw gateway restart', async () => {
56
- fakeExec(null, 'restarted', '');
57
- const cmd: AgentCommand = { id: '4', type: 'restart', payload: {} };
58
- const res = await handleRestart(cmd);
59
- expect(res.success).toBe(true);
60
- expect(mockExec).toHaveBeenCalledWith('openclaw gateway restart', expect.any(Object), expect.any(Function));
61
- });
62
-
63
- it('returns error on restart failure', async () => {
64
- fakeExec(new Error('restart failed'), '', 'err');
65
- const cmd: AgentCommand = { id: '5', type: 'restart', payload: {} };
66
- const res = await handleRestart(cmd);
67
- expect(res.success).toBe(false);
68
- });
69
- });
70
-
71
- describe('handleDeploy', () => {
72
- beforeEach(() => jest.clearAllMocks());
73
-
74
- it('installs and restarts on success', async () => {
75
- let callCount = 0;
76
- mockExec.mockImplementation((_cmd: string, _opts: unknown, cb: Function) => {
77
- callCount++;
78
- cb(null, Buffer.from(`step${callCount}`), Buffer.from(''));
79
- });
80
- const cmd: AgentCommand = { id: '6', type: 'deploy', payload: {} };
81
- const res = await handleDeploy(cmd);
82
- expect(res.success).toBe(true);
83
- expect(mockExec).toHaveBeenCalledTimes(2);
84
- });
85
-
86
- it('fails if install fails', async () => {
87
- fakeExec(new Error('npm error'), '', 'err');
88
- const cmd: AgentCommand = { id: '7', type: 'deploy', payload: {} };
89
- const res = await handleDeploy(cmd);
90
- expect(res.success).toBe(false);
91
- expect(res.error).toContain('Install failed');
92
- });
93
- });
94
-
95
- describe('handleConfig', () => {
96
- beforeEach(() => jest.clearAllMocks());
97
-
98
- it('writes config and restarts', async () => {
99
- (fs.mkdir as jest.Mock).mockResolvedValue(undefined);
100
- (fs.writeFile as jest.Mock).mockResolvedValue(undefined);
101
- fakeExec(null, 'ok', '');
102
- const cmd: AgentCommand = { id: '8', type: 'config', payload: { filename: 'test.yml', content: 'key: val' } };
103
- const res = await handleConfig(cmd);
104
- expect(res.success).toBe(true);
105
- expect(fs.writeFile).toHaveBeenCalled();
106
- });
107
-
108
- it('rejects missing payload', async () => {
109
- const cmd: AgentCommand = { id: '9', type: 'config', payload: {} };
110
- const res = await handleConfig(cmd);
111
- expect(res.success).toBe(false);
112
- expect(res.error).toContain('Missing');
113
- });
114
- });
115
-
116
- describe('handlePair', () => {
117
- it('returns paired response', async () => {
118
- const cmd: AgentCommand = { id: '10', type: 'pair', payload: { token: 'abc', targetId: 'target-1' } };
119
- const res = await handlePair(cmd);
120
- expect(res.success).toBe(true);
121
- expect(res.data?.paired).toBe(true);
122
- expect(res.data?.targetId).toBe('target-1');
123
- });
124
-
125
- it('rejects missing pair params', async () => {
126
- const cmd: AgentCommand = { id: '11', type: 'pair', payload: {} };
127
- const res = await handlePair(cmd);
128
- expect(res.success).toBe(false);
129
- });
130
- });
131
-
132
- describe('handleStop', () => {
133
- beforeEach(() => {
134
- jest.useFakeTimers();
135
- jest.spyOn(process, 'exit').mockImplementation(() => undefined as never);
136
- });
137
- afterEach(() => {
138
- jest.useRealTimers();
139
- jest.restoreAllMocks();
140
- });
141
-
142
- it('returns success and schedules shutdown', async () => {
143
- const cmd: AgentCommand = { id: '12', type: 'stop', payload: {} };
144
- const res = await handleStop(cmd);
145
- expect(res.success).toBe(true);
146
- expect(res.data?.message).toContain('gracefully');
147
- jest.advanceTimersByTime(600);
148
- expect(process.exit).toHaveBeenCalledWith(0);
149
- });
150
- });
@@ -1,124 +0,0 @@
1
- import os from 'node:os';
2
-
3
- import { getMetrics, getLastMessageAt, startHeartbeat } from '../src/heartbeat';
4
- import type { AgentApi } from '../src/api';
5
-
6
- jest.mock('node:fs/promises', () => ({
7
- readdir: jest.fn(),
8
- stat: jest.fn(),
9
- }));
10
-
11
- import fsMock from 'node:fs/promises';
12
- const fsPromises = fsMock as jest.Mocked<typeof fsMock>;
13
-
14
- describe('getLastMessageAt', () => {
15
- const origEnv = process.env.OPENCLAW_STATE_DIR;
16
-
17
- afterEach(() => {
18
- process.env.OPENCLAW_STATE_DIR = origEnv;
19
- jest.clearAllMocks();
20
- });
21
-
22
- it('returns null when sessions directory does not exist', async () => {
23
- fsPromises.readdir.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
24
- const result = await getLastMessageAt();
25
- expect(result).toBeNull();
26
- });
27
-
28
- it('returns null when no .jsonl files are present', async () => {
29
- fsPromises.readdir.mockResolvedValue(['session.txt', 'other.json'] as unknown as Awaited<ReturnType<typeof fsPromises.readdir>>);
30
- const result = await getLastMessageAt();
31
- expect(result).toBeNull();
32
- });
33
-
34
- it('returns ISO string of the most recent mtime when .jsonl files are present', async () => {
35
- process.env.OPENCLAW_STATE_DIR = '/test/state';
36
- fsPromises.readdir.mockResolvedValue(['a.jsonl', 'b.jsonl'] as unknown as Awaited<ReturnType<typeof fsPromises.readdir>>);
37
- const t1 = new Date('2024-01-01T10:00:00.000Z').getTime();
38
- const t2 = new Date('2024-01-02T12:00:00.000Z').getTime();
39
- fsPromises.stat
40
- .mockResolvedValueOnce({ mtime: new Date(t1) } as unknown as Awaited<ReturnType<typeof fsPromises.stat>>)
41
- .mockResolvedValueOnce({ mtime: new Date(t2) } as unknown as Awaited<ReturnType<typeof fsPromises.stat>>);
42
- const result = await getLastMessageAt();
43
- expect(result).toBe('2024-01-02T12:00:00.000Z');
44
- });
45
-
46
- it('does not throw on any error', async () => {
47
- fsPromises.readdir.mockRejectedValue(new Error('unexpected'));
48
- await expect(getLastMessageAt()).resolves.toBeNull();
49
- });
50
- });
51
-
52
- describe('getMetrics', () => {
53
- it('returns cpu, mem, uptime', () => {
54
- const metrics = getMetrics();
55
- expect(metrics).toHaveProperty('cpu');
56
- expect(metrics).toHaveProperty('mem');
57
- expect(metrics).toHaveProperty('uptime');
58
- expect(typeof metrics.cpu).toBe('number');
59
- expect(typeof metrics.mem).toBe('number');
60
- expect(typeof metrics.uptime).toBe('number');
61
- expect(metrics.cpu).toBeGreaterThanOrEqual(0);
62
- expect(metrics.cpu).toBeLessThanOrEqual(1);
63
- expect(metrics.mem).toBeGreaterThanOrEqual(0);
64
- expect(metrics.mem).toBeLessThanOrEqual(1);
65
- });
66
- });
67
-
68
- function makeApi(publishHeartbeat = jest.fn().mockResolvedValue(undefined)): AgentApi {
69
- return {
70
- get: jest.fn(),
71
- post: jest.fn(),
72
- publishResponse: jest.fn(),
73
- publishHeartbeat,
74
- } as unknown as AgentApi;
75
- }
76
-
77
- describe('startHeartbeat', () => {
78
- beforeEach(() => {
79
- jest.useFakeTimers();
80
- });
81
-
82
- afterEach(() => {
83
- jest.useRealTimers();
84
- });
85
-
86
- it('publishes heartbeat immediately and at intervals', async () => {
87
- const publishHeartbeat = jest.fn().mockResolvedValue(undefined);
88
- const api = makeApi(publishHeartbeat);
89
-
90
- const timer = startHeartbeat({ api, agentId: 'agent-42' });
91
- // Flush microtasks so async getLastMessageAt() resolves before api.publishHeartbeat is called
92
- await Promise.resolve();
93
- await Promise.resolve();
94
-
95
- // Immediate call
96
- expect(publishHeartbeat).toHaveBeenCalledTimes(1);
97
- expect(publishHeartbeat).toHaveBeenCalledWith(
98
- 'agent-42',
99
- expect.objectContaining({
100
- agentId: 'agent-42',
101
- type: 'heartbeat',
102
- }),
103
- );
104
-
105
- // After 30s
106
- jest.advanceTimersByTime(30_000);
107
- await Promise.resolve();
108
- await Promise.resolve();
109
- expect(publishHeartbeat).toHaveBeenCalledTimes(2);
110
-
111
- clearInterval(timer);
112
- });
113
-
114
- it('handles publish errors gracefully', () => {
115
- const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
116
- const api = makeApi(jest.fn().mockRejectedValue(new Error('network fail')));
117
-
118
- const timer = startHeartbeat({ api, agentId: 'agent-42' });
119
-
120
- // Should not throw
121
- clearInterval(timer);
122
- consoleSpy.mockRestore();
123
- });
124
- });
@@ -1,55 +0,0 @@
1
- import { handleOnboardingComplete } from '../src/handlers/onboarding';
2
- import type { AgentCommand } from '../src/types';
3
- import childProcess from 'node:child_process';
4
-
5
- jest.mock('node:child_process');
6
-
7
- const mockExec = childProcess.exec as unknown as jest.Mock;
8
-
9
- function fakeExec(error: Error | null, stdout = '', stderr = '') {
10
- mockExec.mockImplementation((_cmd: string, _opts: unknown, cb: Function) => {
11
- cb(error, Buffer.from(stdout), Buffer.from(stderr));
12
- });
13
- }
14
-
15
- const baseCmd: AgentCommand = { id: '1', type: 'onboarding_complete', payload: { agentId: 'agent-123' } };
16
-
17
- describe('handleOnboardingComplete', () => {
18
- beforeEach(() => {
19
- jest.clearAllMocks();
20
- });
21
-
22
- it('triggers "openclaw gateway restart" and returns success', async () => {
23
- fakeExec(null, 'restarted', '');
24
- const res = await handleOnboardingComplete(baseCmd);
25
- expect(res.success).toBe(true);
26
- expect(res.id).toBe('1');
27
- expect(res.type).toBe('onboarding_complete');
28
- expect(mockExec).toHaveBeenCalledWith(
29
- 'openclaw gateway restart',
30
- expect.any(Object),
31
- expect.any(Function),
32
- );
33
- });
34
-
35
- it('returns success:false with error message when restart fails', async () => {
36
- fakeExec(new Error('restart failed'), '', 'stderr output');
37
- const res = await handleOnboardingComplete({ ...baseCmd, id: '2' });
38
- expect(res.success).toBe(false);
39
- expect(res.error).toBe('restart failed');
40
- expect(res.id).toBe('2');
41
- });
42
-
43
- it('includes stdout and stderr in data', async () => {
44
- fakeExec(null, 'gateway restarted ok', 'some warning');
45
- const res = await handleOnboardingComplete(baseCmd);
46
- expect(res.data?.stdout).toBe('gateway restarted ok');
47
- expect(res.data?.stderr).toBe('some warning');
48
- });
49
-
50
- it('does not set error field on success', async () => {
51
- fakeExec(null);
52
- const res = await handleOnboardingComplete(baseCmd);
53
- expect(res.error).toBeUndefined();
54
- });
55
- });
@@ -1,109 +0,0 @@
1
- import { handlePackageInstall } from '../src/handlers/package-install';
2
- import type { AgentCommand } from '../src/types';
3
- import childProcess from 'node:child_process';
4
-
5
- jest.mock('node:child_process');
6
-
7
- const mockExec = childProcess.exec as unknown as jest.Mock;
8
-
9
- function fakeExec(error: Error | null) {
10
- mockExec.mockImplementation((_cmd: string, _opts: unknown, cb: Function) => {
11
- cb(error, Buffer.from(''), Buffer.from(''));
12
- });
13
- }
14
-
15
- const baseCmd: AgentCommand = { id: '1', type: 'package_install', payload: {} };
16
-
17
- describe('handlePackageInstall', () => {
18
- beforeEach(() => {
19
- jest.clearAllMocks();
20
- });
21
-
22
- it('returns success with empty installed when no packages provided', async () => {
23
- const cmd: AgentCommand = { ...baseCmd, payload: { packages: {} } };
24
- const res = await handlePackageInstall(cmd);
25
- expect(res.success).toBe(true);
26
- expect(res.data?.installed).toEqual({ apt: [], npm: [], pip: [] });
27
- expect(res.data?.errors).toEqual([]);
28
- expect(mockExec).not.toHaveBeenCalled();
29
- });
30
-
31
- it('returns success with empty installed when packages key is missing', async () => {
32
- const cmd: AgentCommand = { ...baseCmd, payload: {} };
33
- const res = await handlePackageInstall(cmd);
34
- expect(res.success).toBe(true);
35
- expect(res.data?.installed).toEqual({ apt: [], npm: [], pip: [] });
36
- expect(mockExec).not.toHaveBeenCalled();
37
- });
38
-
39
- it('installs apt packages and runs apt-get install -y per package', async () => {
40
- fakeExec(null);
41
- const cmd: AgentCommand = { ...baseCmd, payload: { packages: { apt: ['curl', 'git'] } } };
42
- const res = await handlePackageInstall(cmd);
43
- expect(res.success).toBe(true);
44
- expect((res.data?.installed as any).apt).toEqual(['curl', 'git']);
45
- expect(mockExec).toHaveBeenCalledWith('apt-get install -y curl', expect.any(Object), expect.any(Function));
46
- expect(mockExec).toHaveBeenCalledWith('apt-get install -y git', expect.any(Object), expect.any(Function));
47
- });
48
-
49
- it('installs npm packages with npm install -g', async () => {
50
- fakeExec(null);
51
- const cmd: AgentCommand = { ...baseCmd, payload: { packages: { npm: ['typescript'] } } };
52
- const res = await handlePackageInstall(cmd);
53
- expect(res.success).toBe(true);
54
- expect((res.data?.installed as any).npm).toEqual(['typescript']);
55
- expect(mockExec).toHaveBeenCalledWith('npm install -g typescript', expect.any(Object), expect.any(Function));
56
- });
57
-
58
- it('installs pip packages with pip install', async () => {
59
- fakeExec(null);
60
- const cmd: AgentCommand = { ...baseCmd, payload: { packages: { pip: ['requests'] } } };
61
- const res = await handlePackageInstall(cmd);
62
- expect(res.success).toBe(true);
63
- expect((res.data?.installed as any).pip).toEqual(['requests']);
64
- expect(mockExec).toHaveBeenCalledWith('pip install requests', expect.any(Object), expect.any(Function));
65
- });
66
-
67
- it('continues with other packages when one npm package fails (partial failure)', async () => {
68
- let call = 0;
69
- mockExec.mockImplementation((_cmd: string, _opts: unknown, cb: Function) => {
70
- call === 0
71
- ? cb(new Error('package not found'), Buffer.from(''), Buffer.from(''))
72
- : cb(null, Buffer.from(''), Buffer.from(''));
73
- call++;
74
- });
75
- const cmd: AgentCommand = { ...baseCmd, payload: { packages: { npm: ['bad-pkg', 'typescript'] } } };
76
- const res = await handlePackageInstall(cmd);
77
- expect(res.success).toBe(false);
78
- expect((res.data?.installed as any).npm).toEqual(['typescript']);
79
- expect((res.data?.errors as any[]).length).toBe(1);
80
- expect((res.data?.errors as any[])[0].name).toBe('bad-pkg');
81
- expect((res.data?.errors as any[])[0].error).toContain('package not found');
82
- });
83
-
84
- it('collects errors across all package types when all fail', async () => {
85
- fakeExec(new Error('install failed'));
86
- const cmd: AgentCommand = {
87
- ...baseCmd,
88
- payload: { packages: { apt: ['pkg1'], npm: ['pkg2'], pip: ['pkg3'] } },
89
- };
90
- const res = await handlePackageInstall(cmd);
91
- expect(res.success).toBe(false);
92
- expect((res.data?.errors as any[]).length).toBe(3);
93
- expect((res.data?.installed as any)).toEqual({ apt: [], npm: [], pip: [] });
94
- });
95
-
96
- it('succeeds with success:true when all packages across all types install', async () => {
97
- fakeExec(null);
98
- const cmd: AgentCommand = {
99
- ...baseCmd,
100
- payload: { packages: { apt: ['curl'], npm: ['ts-node'], pip: ['boto3'] } },
101
- };
102
- const res = await handlePackageInstall(cmd);
103
- expect(res.success).toBe(true);
104
- expect((res.data?.errors as any[]).length).toBe(0);
105
- expect((res.data?.installed as any).apt).toEqual(['curl']);
106
- expect((res.data?.installed as any).npm).toEqual(['ts-node']);
107
- expect((res.data?.installed as any).pip).toEqual(['boto3']);
108
- });
109
- });
@@ -1,60 +0,0 @@
1
- import { handlePair } from '../src/handlers/pair';
2
- import type { AgentCommand } from '../src/types';
3
-
4
- describe('handlePair', () => {
5
- const originalEnv = process.env;
6
-
7
- beforeEach(() => {
8
- process.env = { ...originalEnv, AGENT_ID: 'agent-uuid-123' };
9
- });
10
-
11
- afterEach(() => {
12
- process.env = originalEnv;
13
- });
14
-
15
- it('rejects when token is missing', async () => {
16
- const cmd: AgentCommand = { id: '1', type: 'pair', payload: { targetId: 'target-1' } };
17
- const res = await handlePair(cmd);
18
- expect(res.success).toBe(false);
19
- expect(res.error).toContain('Missing');
20
- });
21
-
22
- it('rejects when targetId is missing', async () => {
23
- const cmd: AgentCommand = { id: '2', type: 'pair', payload: { token: 'abc' } };
24
- const res = await handlePair(cmd);
25
- expect(res.success).toBe(false);
26
- expect(res.error).toContain('Missing');
27
- });
28
-
29
- it('rejects when both token and targetId are missing', async () => {
30
- const cmd: AgentCommand = { id: '3', type: 'pair', payload: {} };
31
- const res = await handlePair(cmd);
32
- expect(res.success).toBe(false);
33
- expect(res.error).toContain('Missing');
34
- });
35
-
36
- it('returns paired:true with targetId and agentId from env', async () => {
37
- const cmd: AgentCommand = {
38
- id: '4',
39
- type: 'pair',
40
- payload: { token: 'my-token', targetId: 'target-42' },
41
- };
42
- const res = await handlePair(cmd);
43
- expect(res.success).toBe(true);
44
- expect(res.data?.paired).toBe(true);
45
- expect(res.data?.targetId).toBe('target-42');
46
- expect(res.data?.agentId).toBe('agent-uuid-123');
47
- });
48
-
49
- it('returns agentId as undefined when AGENT_ID env var is not set', async () => {
50
- delete process.env.AGENT_ID;
51
- const cmd: AgentCommand = {
52
- id: '5',
53
- type: 'pair',
54
- payload: { token: 'tok', targetId: 'target-1' },
55
- };
56
- const res = await handlePair(cmd);
57
- expect(res.success).toBe(true);
58
- expect(res.data?.agentId).toBeUndefined();
59
- });
60
- });
@@ -1,123 +0,0 @@
1
- import { selfUpdate } from '../src/commands/self-update';
2
- import childProcess from 'node:child_process';
3
-
4
- jest.mock('node:child_process');
5
-
6
- const mockExec = childProcess.exec as unknown as jest.Mock;
7
-
8
- type ExecFakeResult = { error: Error | null; stdout: string };
9
-
10
- function fakeExecSequence(results: ExecFakeResult[]) {
11
- let call = 0;
12
- mockExec.mockImplementation((_cmd: string, _opts: unknown, cb: Function) => {
13
- const result = results[call++] ?? { error: new Error('unexpected call'), stdout: '' };
14
- cb(result.error, Buffer.from(result.stdout), Buffer.from(''));
15
- });
16
- }
17
-
18
- describe('selfUpdate', () => {
19
- let exitSpy: jest.SpyInstance;
20
- let logSpy: jest.SpyInstance;
21
- let errorSpy: jest.SpyInstance;
22
-
23
- beforeEach(() => {
24
- jest.clearAllMocks();
25
- exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined as never);
26
- logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
27
- errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
28
- });
29
-
30
- afterEach(() => {
31
- jest.restoreAllMocks();
32
- });
33
-
34
- it('prints "Already up to date" and does not exit when version matches', async () => {
35
- fakeExecSequence([{ error: null, stdout: '1.2.3' }]);
36
-
37
- await selfUpdate('1.2.3');
38
-
39
- expect(mockExec).toHaveBeenCalledTimes(1);
40
- expect(mockExec).toHaveBeenCalledWith(
41
- 'npm view @openclaw-cloud/agent-controller version',
42
- expect.any(Object),
43
- expect.any(Function),
44
- );
45
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Already up to date'));
46
- expect(exitSpy).not.toHaveBeenCalled();
47
- });
48
-
49
- it('installs latest and calls process.exit(0) when a new version is available', async () => {
50
- fakeExecSequence([
51
- { error: null, stdout: '1.3.0' },
52
- { error: null, stdout: '' },
53
- ]);
54
-
55
- await selfUpdate('1.2.3');
56
-
57
- expect(mockExec).toHaveBeenCalledTimes(2);
58
- expect(mockExec).toHaveBeenNthCalledWith(
59
- 1,
60
- 'npm view @openclaw-cloud/agent-controller version',
61
- expect.any(Object),
62
- expect.any(Function),
63
- );
64
- expect(mockExec).toHaveBeenNthCalledWith(
65
- 2,
66
- 'npm install -g @openclaw-cloud/agent-controller@latest',
67
- expect.any(Object),
68
- expect.any(Function),
69
- );
70
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Updating from 1.2.3 to 1.3.0'));
71
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Successfully updated to 1.3.0'));
72
- expect(exitSpy).toHaveBeenCalledWith(0);
73
- });
74
-
75
- it('calls process.exit(1) when npm view fails', async () => {
76
- fakeExecSequence([{ error: new Error('network error'), stdout: '' }]);
77
-
78
- await selfUpdate('1.2.3');
79
-
80
- expect(exitSpy).toHaveBeenCalledWith(1);
81
- expect(errorSpy).toHaveBeenCalledWith(
82
- expect.stringContaining('Failed to fetch latest version'),
83
- 'network error',
84
- );
85
- expect(mockExec).toHaveBeenCalledTimes(1);
86
- });
87
-
88
- it('calls process.exit(1) when npm install fails', async () => {
89
- fakeExecSequence([
90
- { error: null, stdout: '1.3.0' },
91
- { error: new Error('npm install failed'), stdout: '' },
92
- ]);
93
-
94
- await selfUpdate('1.2.3');
95
-
96
- expect(exitSpy).toHaveBeenCalledWith(1);
97
- expect(errorSpy).toHaveBeenCalledWith(
98
- expect.stringContaining('Update failed'),
99
- 'npm install failed',
100
- );
101
- });
102
-
103
- it('logs current and latest versions before updating', async () => {
104
- fakeExecSequence([
105
- { error: null, stdout: '2.0.0' },
106
- { error: null, stdout: '' },
107
- ]);
108
-
109
- await selfUpdate('1.0.0');
110
-
111
- expect(logSpy).toHaveBeenCalledWith('Current version: 1.0.0');
112
- expect(logSpy).toHaveBeenCalledWith('Latest version: 2.0.0');
113
- });
114
-
115
- it('trims whitespace from npm view output before comparing', async () => {
116
- fakeExecSequence([{ error: null, stdout: '1.2.3\n' }]);
117
-
118
- await selfUpdate('1.2.3');
119
-
120
- expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Already up to date'));
121
- expect(exitSpy).not.toHaveBeenCalled();
122
- });
123
- });
@@ -1,38 +0,0 @@
1
- import { handleStop } from '../src/handlers/stop';
2
- import type { AgentCommand } from '../src/types';
3
-
4
- describe('handleStop', () => {
5
- beforeEach(() => {
6
- jest.useFakeTimers();
7
- jest.spyOn(process, 'exit').mockImplementation(() => undefined as never);
8
- });
9
-
10
- afterEach(() => {
11
- jest.useRealTimers();
12
- jest.restoreAllMocks();
13
- });
14
-
15
- it('returns success response with graceful shutdown message', async () => {
16
- const cmd: AgentCommand = { id: '1', type: 'stop', payload: {} };
17
- const res = await handleStop(cmd);
18
- expect(res.success).toBe(true);
19
- expect(res.data?.message).toContain('gracefully');
20
- expect(res.id).toBe('1');
21
- expect(res.type).toBe('stop');
22
- });
23
-
24
- it('does not call process.exit before 500ms', async () => {
25
- const cmd: AgentCommand = { id: '2', type: 'stop', payload: {} };
26
- await handleStop(cmd);
27
- jest.advanceTimersByTime(499);
28
- expect(process.exit).not.toHaveBeenCalled();
29
- });
30
-
31
- it('calls process.exit(0) after 500ms', async () => {
32
- const cmd: AgentCommand = { id: '3', type: 'stop', payload: {} };
33
- await handleStop(cmd);
34
- expect(process.exit).not.toHaveBeenCalled();
35
- jest.advanceTimersByTime(500);
36
- expect(process.exit).toHaveBeenCalledWith(0);
37
- });
38
- });
package/jest.config.ts DELETED
@@ -1,16 +0,0 @@
1
- import type { Config } from 'jest';
2
-
3
- const config: Config = {
4
- preset: 'ts-jest',
5
- testEnvironment: 'node',
6
- roots: ['<rootDir>/__tests__'],
7
- moduleFileExtensions: ['ts', 'js', 'json'],
8
- transform: {
9
- '^.+\\.ts$': ['ts-jest', { useESM: false, tsconfig: { module: 'commonjs' } }],
10
- },
11
- moduleNameMapper: {
12
- '^(\\.{1,2}/.*)\\.js$': '$1',
13
- },
14
- };
15
-
16
- export default config;