lynkeus-client 0.2.0 → 0.4.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.
- package/lib/index.js +7 -1
- package/package.json +9 -5
- package/src/index.ts +11 -1
- package/src/__tests__/server.test.ts +0 -93
package/lib/index.js
CHANGED
|
@@ -45,7 +45,13 @@ export class AgentServer extends EventEmitter {
|
|
|
45
45
|
server.on('connection', (socket) => this.#attach(socket));
|
|
46
46
|
await new Promise((resolve, reject) => {
|
|
47
47
|
server.once('listening', () => resolve());
|
|
48
|
-
server.once('error',
|
|
48
|
+
server.once('error', (error) => {
|
|
49
|
+
this.#server = null;
|
|
50
|
+
if (error.code !== 'EADDRINUSE')
|
|
51
|
+
return reject(error);
|
|
52
|
+
// Sharing the port would hand the app to whichever driver it dials next: two runs' steps mixed.
|
|
53
|
+
reject(new AgentError(`Port ${this.port} is already taken: another lynkeus run is probably still driving the app there. Stop it, or use another port (LYNKEUS_PORT, --ports).`, 'port-in-use'));
|
|
54
|
+
});
|
|
49
55
|
});
|
|
50
56
|
}
|
|
51
57
|
async close() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lynkeus-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Node driver for apps running lynkeus-agent: attach, read the screen, press, type, wait.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"lib",
|
|
16
|
-
"src"
|
|
16
|
+
"src",
|
|
17
|
+
"!**/__tests__"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
|
19
20
|
"build": "tsc -p tsconfig.json",
|
|
@@ -22,12 +23,15 @@
|
|
|
22
23
|
},
|
|
23
24
|
"repository": {
|
|
24
25
|
"type": "git",
|
|
25
|
-
"url": "git+https://github.com/
|
|
26
|
+
"url": "git+https://github.com/lynkeushq/lynkeus.git",
|
|
26
27
|
"directory": "packages/client"
|
|
27
28
|
},
|
|
28
29
|
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22"
|
|
32
|
+
},
|
|
29
33
|
"dependencies": {
|
|
30
|
-
"lynkeus-protocol": "^0.
|
|
34
|
+
"lynkeus-protocol": "^0.4.0",
|
|
31
35
|
"ws": "^8.18.0"
|
|
32
36
|
},
|
|
33
37
|
"devDependencies": {
|
|
@@ -36,5 +40,5 @@
|
|
|
36
40
|
"tsx": "^4.19.2",
|
|
37
41
|
"typescript": "^6.0.3"
|
|
38
42
|
},
|
|
39
|
-
"homepage": "https://github.com/
|
|
43
|
+
"homepage": "https://github.com/lynkeushq/lynkeus#readme"
|
|
40
44
|
}
|
package/src/index.ts
CHANGED
|
@@ -72,7 +72,17 @@ export class AgentServer extends EventEmitter {
|
|
|
72
72
|
server.on('connection', (socket) => this.#attach(socket));
|
|
73
73
|
await new Promise<void>((resolve, reject) => {
|
|
74
74
|
server.once('listening', () => resolve());
|
|
75
|
-
server.once('error',
|
|
75
|
+
server.once('error', (error: NodeJS.ErrnoException) => {
|
|
76
|
+
this.#server = null;
|
|
77
|
+
if (error.code !== 'EADDRINUSE') return reject(error);
|
|
78
|
+
// Sharing the port would hand the app to whichever driver it dials next: two runs' steps mixed.
|
|
79
|
+
reject(
|
|
80
|
+
new AgentError(
|
|
81
|
+
`Port ${this.port} is already taken: another lynkeus run is probably still driving the app there. Stop it, or use another port (LYNKEUS_PORT, --ports).`,
|
|
82
|
+
'port-in-use',
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
});
|
|
76
86
|
});
|
|
77
87
|
}
|
|
78
88
|
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { after, test } from 'node:test';
|
|
3
|
-
|
|
4
|
-
import { WebSocket } from 'ws';
|
|
5
|
-
|
|
6
|
-
import { AgentError, AgentServer } from '../index.js';
|
|
7
|
-
|
|
8
|
-
let port = 18_500;
|
|
9
|
-
const servers: AgentServer[] = [];
|
|
10
|
-
after(async () => {
|
|
11
|
-
for (const s of servers) await s.close();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
const serve = async (options: ConstructorParameters<typeof AgentServer>[0] = {}) => {
|
|
15
|
-
const server = new AgentServer({ port: port++, ...options });
|
|
16
|
-
servers.push(server);
|
|
17
|
-
await server.listen();
|
|
18
|
-
return server;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
type Message = { event?: string; token?: string; id?: number; method?: string };
|
|
22
|
-
|
|
23
|
-
const fakeApp = async (server: AgentServer, answer: (m: Message) => unknown = () => ({ ok: true })) => {
|
|
24
|
-
const socket = new WebSocket(`ws://127.0.0.1:${server.port}`);
|
|
25
|
-
const seen: Message[] = [];
|
|
26
|
-
socket.on('message', (data) => {
|
|
27
|
-
const m = JSON.parse(String(data)) as Message;
|
|
28
|
-
seen.push(m);
|
|
29
|
-
if (m.event === 'auth') socket.send(JSON.stringify({ event: 'hello', protocolVersion: 1, platform: 'ios', native: false, commands: [] }));
|
|
30
|
-
else if (m.id !== undefined) {
|
|
31
|
-
const reply = answer(m);
|
|
32
|
-
if (reply !== undefined) socket.send(JSON.stringify({ id: m.id, ...(reply as object) }));
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
await new Promise((r) => socket.once('open', r));
|
|
36
|
-
return { socket, seen };
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
test('an app that dials in is sent the token first and is ready once it says hello', async () => {
|
|
40
|
-
const server = await serve({ token: 'secret' });
|
|
41
|
-
const app = await fakeApp(server);
|
|
42
|
-
const hello = await server.waitForApp(2000);
|
|
43
|
-
assert.equal(hello.platform, 'ios');
|
|
44
|
-
assert.equal(server.connected, true);
|
|
45
|
-
assert.equal(app.seen[0]?.event, 'auth');
|
|
46
|
-
assert.equal(app.seen[0]?.token, 'secret');
|
|
47
|
-
app.socket.close();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('a call gets the result the app sends back, and an error as an AgentError with its code', async () => {
|
|
51
|
-
const server = await serve();
|
|
52
|
-
const app = await fakeApp(server, (m) => (m.method === 'ping' ? { result: { pong: true } } : { error: { message: 'Unknown method', code: 'unknown' } }));
|
|
53
|
-
await server.waitForApp(2000);
|
|
54
|
-
assert.deepEqual(await server.call('ping'), { pong: true });
|
|
55
|
-
await assert.rejects(server.call('nope'), (e: unknown) => e instanceof AgentError && e.code === 'unknown');
|
|
56
|
-
app.socket.close();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('a call the app never answers times out with the timeout code', async () => {
|
|
60
|
-
const server = await serve();
|
|
61
|
-
const app = await fakeApp(server, () => undefined);
|
|
62
|
-
await server.waitForApp(2000);
|
|
63
|
-
await assert.rejects(server.call('screen', undefined, 100), (e: unknown) => e instanceof AgentError && e.code === 'timeout');
|
|
64
|
-
app.socket.close();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('with no app attached, waiting says so and a call refuses at once', async () => {
|
|
68
|
-
const server = await serve();
|
|
69
|
-
await assert.rejects(server.waitForApp(50), /No app attached/);
|
|
70
|
-
await assert.rejects(server.call('ping'), /No app attached/);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('a call in flight when the app drops is resent to the app that comes back', async () => {
|
|
74
|
-
const server = await serve({ reconnectGraceMs: 1000 });
|
|
75
|
-
const first = await fakeApp(server, () => undefined);
|
|
76
|
-
await server.waitForApp(2000);
|
|
77
|
-
const pending = server.call('screen', undefined, 3000);
|
|
78
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
79
|
-
first.socket.close();
|
|
80
|
-
const second = await fakeApp(server, () => ({ result: { route: 'Settings.Main' } }));
|
|
81
|
-
assert.deepEqual(await pending, { route: 'Settings.Main' });
|
|
82
|
-
second.socket.close();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('a call in flight fails as disconnected when the app does not come back in time', async () => {
|
|
86
|
-
const server = await serve({ reconnectGraceMs: 100 });
|
|
87
|
-
const app = await fakeApp(server, () => undefined);
|
|
88
|
-
await server.waitForApp(2000);
|
|
89
|
-
const pending = server.call('screen', undefined, 3000);
|
|
90
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
91
|
-
app.socket.close();
|
|
92
|
-
await assert.rejects(pending, (e: unknown) => e instanceof AgentError && e.code === 'disconnected');
|
|
93
|
-
});
|