@kortix/agent-tunnel 0.13.49 → 0.13.51

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
@@ -6139,7 +6139,7 @@ function acquireTunnelLease() {
6139
6139
  if (!serviceWasActive || resumed)
6140
6140
  return;
6141
6141
  resumed = true;
6142
- startService();
6142
+ resumeService();
6143
6143
  }
6144
6144
  };
6145
6145
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kortix/agent-tunnel",
3
- "version": "0.13.49",
3
+ "version": "0.13.51",
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.4.0",
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
- startService();
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
  }
@@ -117,7 +117,10 @@ function runCli(
117
117
  const child = spawn('bun', cliArgs, {
118
118
  env: {
119
119
  ...process.env,
120
- TUNNEL_API_URL: `http://localhost:${mockPort}`,
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://localhost:${permPort}`,
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://localhost:1' });
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://localhost:${emptyPort}` });
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://localhost:${emptyPort}`,
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);