@kortix/agent-tunnel 0.13.49 → 0.13.50
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/dist/agent-cli.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kortix/agent-tunnel",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.50",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/kortix-ai/suna.git",
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
"build": "bun build src/agent/cli.ts --target=node --outfile=dist/agent-cli.js --banner='#!/usr/bin/env node' && bun build src/client/cli.ts --target=node --outfile=dist/client-cli.js --banner='#!/usr/bin/env node' && node -e \"const fs=require('node:fs'); fs.chmodSync('dist/agent-cli.js', 0o755); fs.chmodSync('dist/client-cli.js', 0o755);\"",
|
|
33
33
|
"prepack": "bun run build",
|
|
34
34
|
"typecheck": "tsc --noEmit",
|
|
35
|
-
"test": "bun test"
|
|
35
|
+
"test": "bun test --timeout ${KORTIX_TEST_TIMEOUT_MS:-15000}"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"hono": "^4.
|
|
38
|
+
"hono": "^4.13.7",
|
|
39
39
|
"ws": "^8.18.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { platform, tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type ServicePaths,
|
|
8
|
+
getServicePaths,
|
|
9
|
+
getServiceStatus,
|
|
10
|
+
installService,
|
|
11
|
+
runnerPartsFor,
|
|
12
|
+
} from './service';
|
|
13
|
+
import { acquireTunnelLease } from './service-control';
|
|
14
|
+
import { type ServiceDriver, serviceDriverFor, supervisor } from './service-drivers';
|
|
15
|
+
|
|
16
|
+
const realRun = supervisor.run;
|
|
17
|
+
|
|
18
|
+
let calls: string[] = [];
|
|
19
|
+
let home = '';
|
|
20
|
+
let paths: ServicePaths | undefined;
|
|
21
|
+
let driver: ServiceDriver | undefined;
|
|
22
|
+
let savedHomeEnv: string | undefined;
|
|
23
|
+
|
|
24
|
+
/** The supervisor verb that (re)starts the service, per platform. */
|
|
25
|
+
const startVerb = (): RegExp =>
|
|
26
|
+
platform() === 'win32'
|
|
27
|
+
? /schtasks\.exe \/Run/
|
|
28
|
+
: platform() === 'darwin'
|
|
29
|
+
? /launchctl (bootstrap|kickstart)/
|
|
30
|
+
: /systemctl --user (restart|enable --now|start) /;
|
|
31
|
+
|
|
32
|
+
describe('tunnel lease', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
savedHomeEnv = process.env.AGENT_TUNNEL_HOME;
|
|
35
|
+
home = mkdtempSync(join(tmpdir(), 'agent-tunnel-lease-'));
|
|
36
|
+
// A non-default home derives a unique service label, so the unit file the
|
|
37
|
+
// test writes under the real home directory never touches a real service.
|
|
38
|
+
process.env.AGENT_TUNNEL_HOME = home;
|
|
39
|
+
paths = getServicePaths();
|
|
40
|
+
const resolvedDriver = serviceDriverFor(platform());
|
|
41
|
+
if (!resolvedDriver) throw new Error(`no service driver for ${platform()}`);
|
|
42
|
+
driver = resolvedDriver;
|
|
43
|
+
calls = [];
|
|
44
|
+
// ponytail: every supervisor command succeeds — the drivers' failure and
|
|
45
|
+
// retry paths are service-drivers.test.ts's job.
|
|
46
|
+
supervisor.run = (command, args) => {
|
|
47
|
+
calls.push([command, ...args].join(' '));
|
|
48
|
+
return { ok: true, detail: command === 'powershell.exe' ? 'Running' : '' };
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
supervisor.run = realRun;
|
|
54
|
+
if (driver && paths) rmSync(driver.unitPath(paths), { force: true });
|
|
55
|
+
if (home) rmSync(home, { recursive: true, force: true });
|
|
56
|
+
if (savedHomeEnv === undefined) delete process.env.AGENT_TUNNEL_HOME;
|
|
57
|
+
else process.env.AGENT_TUNNEL_HOME = savedHomeEnv;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** Installs a real unit for the temp home, then rewrites it with a stale runner. */
|
|
61
|
+
function driftUnit() {
|
|
62
|
+
installService();
|
|
63
|
+
if (!driver || !paths) throw new Error('beforeEach did not initialise the fixture');
|
|
64
|
+
// The same home, an old runner: what an app update that moved the bundle
|
|
65
|
+
// leaves on disk.
|
|
66
|
+
const staleRunner = runnerPartsFor('/defunct/agent-cli.js', { execPath: '/bin/sh' }, paths);
|
|
67
|
+
writeFileSync(driver.unitPath(paths), driver.render(paths, staleRunner), { mode: 0o600 });
|
|
68
|
+
calls.length = 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test('resume reinstalls a drifted unit before starting it', () => {
|
|
72
|
+
driftUnit();
|
|
73
|
+
expect(getServiceStatus().upToDate).toBe(false);
|
|
74
|
+
|
|
75
|
+
const lease = acquireTunnelLease();
|
|
76
|
+
expect(lease.serviceWasActive).toBe(true);
|
|
77
|
+
lease.resumeService();
|
|
78
|
+
|
|
79
|
+
// The resume went through the drift-safe path: the unit on disk is what an
|
|
80
|
+
// install would write now, and the supervisor was asked to run the service.
|
|
81
|
+
expect(getServiceStatus().upToDate).toBe(true);
|
|
82
|
+
expect(calls.some((line) => startVerb().test(line))).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('resume starts a fresh unit and acts at most once', () => {
|
|
86
|
+
installService();
|
|
87
|
+
calls.length = 0;
|
|
88
|
+
expect(getServiceStatus().upToDate).toBe(true);
|
|
89
|
+
|
|
90
|
+
const lease = acquireTunnelLease();
|
|
91
|
+
lease.resumeService();
|
|
92
|
+
const callsAfterFirstResume = calls.length;
|
|
93
|
+
|
|
94
|
+
lease.resumeService();
|
|
95
|
+
expect(calls.length).toBe(callsAfterFirstResume);
|
|
96
|
+
expect(getServiceStatus().upToDate).toBe(true);
|
|
97
|
+
expect(calls.some((line) => startVerb().test(line))).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
pauseService,
|
|
6
6
|
restartService,
|
|
7
7
|
resumeService,
|
|
8
|
-
startService,
|
|
9
8
|
stopService,
|
|
10
9
|
uninstallService,
|
|
11
10
|
} from './service';
|
|
@@ -34,7 +33,9 @@ export function acquireTunnelLease(): TunnelLease {
|
|
|
34
33
|
resumeService() {
|
|
35
34
|
if (!serviceWasActive || resumed) return;
|
|
36
35
|
resumed = true;
|
|
37
|
-
|
|
36
|
+
// The drift-safe resume: a unit an app update has moved is reinstalled
|
|
37
|
+
// before it starts, or the service would run a runner that is gone.
|
|
38
|
+
resumeService();
|
|
38
39
|
},
|
|
39
40
|
};
|
|
40
41
|
}
|
package/src/client/cli.test.ts
CHANGED
|
@@ -117,7 +117,10 @@ function runCli(
|
|
|
117
117
|
const child = spawn('bun', cliArgs, {
|
|
118
118
|
env: {
|
|
119
119
|
...process.env,
|
|
120
|
-
|
|
120
|
+
// The mock agent is loopback by address: `localhost` does not resolve
|
|
121
|
+
// on a platform sandbox, which turns every row here into
|
|
122
|
+
// "Unable to connect" instead of the response under test.
|
|
123
|
+
TUNNEL_API_URL: `http://127.0.0.1:${mockPort}`,
|
|
121
124
|
TUNNEL_TOKEN: 'test-token',
|
|
122
125
|
TUNNEL_ID: '',
|
|
123
126
|
...envOverrides,
|
|
@@ -320,7 +323,7 @@ describe('Agent Tunnel CLI', () => {
|
|
|
320
323
|
|
|
321
324
|
test('permission denied returns structured response', async () => {
|
|
322
325
|
const r = await runCli('cua_list_apps', undefined, {
|
|
323
|
-
TUNNEL_API_URL: `http://
|
|
326
|
+
TUNNEL_API_URL: `http://127.0.0.1:${permPort}`,
|
|
324
327
|
});
|
|
325
328
|
expect(r.json!.success).toBe(false);
|
|
326
329
|
expect(r.json!.permissionRequired).toBe(true);
|
|
@@ -330,7 +333,7 @@ describe('Agent Tunnel CLI', () => {
|
|
|
330
333
|
|
|
331
334
|
describe('server unreachable', () => {
|
|
332
335
|
test('status with dead server returns error JSON', async () => {
|
|
333
|
-
const r = await runCli('status', undefined, { TUNNEL_API_URL: 'http://
|
|
336
|
+
const r = await runCli('status', undefined, { TUNNEL_API_URL: 'http://127.0.0.1:1' });
|
|
334
337
|
expect(r.exitCode).toBe(1);
|
|
335
338
|
expect(r.json!.success).toBe(false);
|
|
336
339
|
});
|
|
@@ -357,7 +360,7 @@ describe('Agent Tunnel CLI', () => {
|
|
|
357
360
|
});
|
|
358
361
|
|
|
359
362
|
test('status with no connections returns empty list', async () => {
|
|
360
|
-
const r = await runCli('status', undefined, { TUNNEL_API_URL: `http://
|
|
363
|
+
const r = await runCli('status', undefined, { TUNNEL_API_URL: `http://127.0.0.1:${emptyPort}` });
|
|
361
364
|
expect(r.exitCode).toBe(0);
|
|
362
365
|
expect(r.json!.success).toBe(true);
|
|
363
366
|
expect((r.json!.connections as unknown[]).length).toBe(0);
|
|
@@ -365,7 +368,7 @@ describe('Agent Tunnel CLI', () => {
|
|
|
365
368
|
|
|
366
369
|
test('fs_read with no connections returns error', async () => {
|
|
367
370
|
const r = await runCli('fs_read', '{"path":"/tmp/x"}', {
|
|
368
|
-
TUNNEL_API_URL: `http://
|
|
371
|
+
TUNNEL_API_URL: `http://127.0.0.1:${emptyPort}`,
|
|
369
372
|
});
|
|
370
373
|
expect(r.exitCode).toBe(1);
|
|
371
374
|
expect(r.json!.success).toBe(false);
|