lynkeus-protocol 0.2.0 → 0.3.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.d.ts +8 -0
- package/lib/index.js +1 -0
- package/lib/run.d.ts +114 -0
- package/lib/run.js +14 -0
- package/package.json +8 -4
- package/src/index.ts +6 -0
- package/src/run.ts +108 -0
- package/src/__tests__/shape.test.ts +0 -52
package/lib/index.d.ts
CHANGED
|
@@ -31,6 +31,11 @@ export type Element = {
|
|
|
31
31
|
/** Masked (`•`) when the field is a secure entry. */
|
|
32
32
|
value?: string;
|
|
33
33
|
placeholder?: string;
|
|
34
|
+
/** The size its text is set in, in points, and whether it is bold: what makes text "large" when its contrast is judged. */
|
|
35
|
+
font?: {
|
|
36
|
+
size: number;
|
|
37
|
+
bold?: boolean;
|
|
38
|
+
};
|
|
34
39
|
focused?: boolean;
|
|
35
40
|
/** Index of the nearest listed ancestor, when there is one. */
|
|
36
41
|
parent?: number;
|
|
@@ -333,6 +338,8 @@ export type Response = {
|
|
|
333
338
|
export type HelloEvent = {
|
|
334
339
|
event: 'hello';
|
|
335
340
|
protocolVersion: number;
|
|
341
|
+
/** The lynkeus-agent release in the app; absent from agents before it was sent. */
|
|
342
|
+
agentVersion?: string;
|
|
336
343
|
platform: 'ios' | 'android' | string;
|
|
337
344
|
native: boolean;
|
|
338
345
|
commands: string[];
|
|
@@ -350,3 +357,4 @@ export type AuthEvent = {
|
|
|
350
357
|
token?: string;
|
|
351
358
|
};
|
|
352
359
|
export type AgentEvent = HelloEvent | NavigationEvent;
|
|
360
|
+
export * from './run.js';
|
package/lib/index.js
CHANGED
package/lib/run.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a run of cases leaves behind: a directory anything can read without
|
|
3
|
+
* knowing how the run was made.
|
|
4
|
+
*
|
|
5
|
+
* run.json a RunManifest
|
|
6
|
+
* cases.jsonl one RunCase per line, one line per attempt, in the order they ran
|
|
7
|
+
* <case id>/… what a case's steps point at: frames, a film
|
|
8
|
+
*
|
|
9
|
+
* Whoever writes it is an engine; whoever reads it (a report, a dashboard, a
|
|
10
|
+
* script) depends on this file and on nothing else. A field is only ever
|
|
11
|
+
* added. Anything that would change the meaning of an existing field raises
|
|
12
|
+
* RUN_SCHEMA, and a reader refuses a schema it does not know.
|
|
13
|
+
*/
|
|
14
|
+
export declare const RUN_SCHEMA = 1;
|
|
15
|
+
/** Where an event was observed: inside the app, or by the backend the app talks to. */
|
|
16
|
+
export type RunEventOrigin = 'app' | 'server';
|
|
17
|
+
export type RunEvent = {
|
|
18
|
+
origin: RunEventOrigin;
|
|
19
|
+
name: string;
|
|
20
|
+
props?: Record<string, unknown>;
|
|
21
|
+
/** Who it was sent to, when the engine can tell: 'mixpanel', 'datadog', … */
|
|
22
|
+
provider?: string;
|
|
23
|
+
/** The route the app was on, for an event observed in the app. */
|
|
24
|
+
route?: string;
|
|
25
|
+
/** ISO 8601, when the source says. */
|
|
26
|
+
at?: string;
|
|
27
|
+
/** The request that caused it, for a server event: what files it under a step. */
|
|
28
|
+
requestId?: string;
|
|
29
|
+
};
|
|
30
|
+
/** `unsupported`: the host the run was on has no way to do the step; it says nothing about the app. */
|
|
31
|
+
export type RunStepStatus = 'passed' | 'failed' | 'skipped' | 'unsupported';
|
|
32
|
+
/**
|
|
33
|
+
* What the backend did for a step, from the traces it exported: the request the step's app sent, and the
|
|
34
|
+
* queries, jobs and calls out it set off. One per span, in the order they began; `depth` nests them.
|
|
35
|
+
*/
|
|
36
|
+
export type RunBackendSpan = {
|
|
37
|
+
name: string;
|
|
38
|
+
/** server, client, internal, producer or consumer, as OpenTelemetry says. */
|
|
39
|
+
kind: string;
|
|
40
|
+
/** Milliseconds after the step began. */
|
|
41
|
+
at: number;
|
|
42
|
+
ms: number;
|
|
43
|
+
depth: number;
|
|
44
|
+
/** The service that exported it, when it says. */
|
|
45
|
+
service?: string;
|
|
46
|
+
error?: string;
|
|
47
|
+
/** What it did: method, route, status, table, job class, host called. */
|
|
48
|
+
attributes?: Record<string, string | number | boolean>;
|
|
49
|
+
};
|
|
50
|
+
export type RunStep = {
|
|
51
|
+
/** The step as the case words it. */
|
|
52
|
+
step: string;
|
|
53
|
+
status: RunStepStatus;
|
|
54
|
+
ms: number;
|
|
55
|
+
error?: string;
|
|
56
|
+
/** Which part of the case it belongs to. */
|
|
57
|
+
section?: 'setup' | 'steps';
|
|
58
|
+
events?: RunEvent[];
|
|
59
|
+
/** A frame taken once the step was done, relative to the run directory. */
|
|
60
|
+
screenshot?: string;
|
|
61
|
+
/** The second of the case's film this step starts at. */
|
|
62
|
+
videoS?: number;
|
|
63
|
+
/** What the backend did for it, when the backend exports traces to the run. */
|
|
64
|
+
backend?: RunBackendSpan[];
|
|
65
|
+
};
|
|
66
|
+
export type RunCase = {
|
|
67
|
+
id: string;
|
|
68
|
+
title?: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
/** The case file, relative to where the run was started. */
|
|
71
|
+
file: string;
|
|
72
|
+
/** 1 for the first try; a case that failed and ran again has a line for each. */
|
|
73
|
+
attempt: number;
|
|
74
|
+
/** `unsupported`: one of its steps could not be done on this host, so it proved nothing here. */
|
|
75
|
+
result: 'passed' | 'failed' | 'unsupported';
|
|
76
|
+
/** ISO 8601. */
|
|
77
|
+
startedAt: string;
|
|
78
|
+
ms: number;
|
|
79
|
+
steps: RunStep[];
|
|
80
|
+
/** Why it failed, as rules read off the screen, the requests and the trace. */
|
|
81
|
+
diagnosis?: string[];
|
|
82
|
+
/** When a step failed: the screen, the requests since the case began, and why. */
|
|
83
|
+
evidence?: {
|
|
84
|
+
screen?: string;
|
|
85
|
+
requests?: string[];
|
|
86
|
+
why?: string;
|
|
87
|
+
};
|
|
88
|
+
/** The case's film, relative to the run directory. */
|
|
89
|
+
video?: string;
|
|
90
|
+
};
|
|
91
|
+
export type RunManifest = {
|
|
92
|
+
schema: typeof RUN_SCHEMA;
|
|
93
|
+
/** ISO 8601. */
|
|
94
|
+
startedAt: string;
|
|
95
|
+
/** Absent while the run is going, or when it was cut short. */
|
|
96
|
+
finishedAt?: string;
|
|
97
|
+
/** What the app said it runs on: 'ios', 'android', … */
|
|
98
|
+
platform?: string;
|
|
99
|
+
/** Whether the app ran on a device or simulator (true) or hosted without one. */
|
|
100
|
+
native?: boolean;
|
|
101
|
+
/** Where the run was made from, when it was a git checkout. */
|
|
102
|
+
git?: {
|
|
103
|
+
branch?: string;
|
|
104
|
+
commit?: string;
|
|
105
|
+
};
|
|
106
|
+
/** Cases by their last attempt. */
|
|
107
|
+
totals?: {
|
|
108
|
+
cases: number;
|
|
109
|
+
passed: number;
|
|
110
|
+
failed: number;
|
|
111
|
+
unsupported: number;
|
|
112
|
+
retried: number;
|
|
113
|
+
};
|
|
114
|
+
};
|
package/lib/run.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a run of cases leaves behind: a directory anything can read without
|
|
3
|
+
* knowing how the run was made.
|
|
4
|
+
*
|
|
5
|
+
* run.json a RunManifest
|
|
6
|
+
* cases.jsonl one RunCase per line, one line per attempt, in the order they ran
|
|
7
|
+
* <case id>/… what a case's steps point at: frames, a film
|
|
8
|
+
*
|
|
9
|
+
* Whoever writes it is an engine; whoever reads it (a report, a dashboard, a
|
|
10
|
+
* script) depends on this file and on nothing else. A field is only ever
|
|
11
|
+
* added. Anything that would change the meaning of an existing field raises
|
|
12
|
+
* RUN_SCHEMA, and a reader refuses a schema it does not know.
|
|
13
|
+
*/
|
|
14
|
+
export const RUN_SCHEMA = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lynkeus-protocol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Wire contract between an app running lynkeus-agent and whatever drives it.",
|
|
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,14 +23,17 @@
|
|
|
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/protocol"
|
|
27
28
|
},
|
|
28
29
|
"license": "MIT",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22"
|
|
32
|
+
},
|
|
29
33
|
"devDependencies": {
|
|
30
34
|
"@types/node": "^22.10.2",
|
|
31
35
|
"tsx": "^4.19.2",
|
|
32
36
|
"typescript": "^6.0.3"
|
|
33
37
|
},
|
|
34
|
-
"homepage": "https://github.com/
|
|
38
|
+
"homepage": "https://github.com/lynkeushq/lynkeus#readme"
|
|
35
39
|
}
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,8 @@ export type Element = {
|
|
|
30
30
|
/** Masked (`•`) when the field is a secure entry. */
|
|
31
31
|
value?: string;
|
|
32
32
|
placeholder?: string;
|
|
33
|
+
/** The size its text is set in, in points, and whether it is bold: what makes text "large" when its contrast is judged. */
|
|
34
|
+
font?: { size: number; bold?: boolean };
|
|
33
35
|
focused?: boolean;
|
|
34
36
|
/** Index of the nearest listed ancestor, when there is one. */
|
|
35
37
|
parent?: number;
|
|
@@ -179,6 +181,8 @@ export type Response = { id: number; result: unknown } | { id: number; error: {
|
|
|
179
181
|
export type HelloEvent = {
|
|
180
182
|
event: 'hello';
|
|
181
183
|
protocolVersion: number;
|
|
184
|
+
/** The lynkeus-agent release in the app; absent from agents before it was sent. */
|
|
185
|
+
agentVersion?: string;
|
|
182
186
|
platform: 'ios' | 'android' | string;
|
|
183
187
|
native: boolean;
|
|
184
188
|
commands: string[];
|
|
@@ -189,3 +193,5 @@ export type NavigationEvent = { event: 'navigation'; route?: string };
|
|
|
189
193
|
/** The driver's first message. `session` is random per driver process, so the app can tell a reconnect (whose resent requests it may have answered) from a new driver reusing request ids. */
|
|
190
194
|
export type AuthEvent = { event: 'auth'; session: string; token?: string };
|
|
191
195
|
export type AgentEvent = HelloEvent | NavigationEvent;
|
|
196
|
+
|
|
197
|
+
export * from './run.js';
|
package/src/run.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a run of cases leaves behind: a directory anything can read without
|
|
3
|
+
* knowing how the run was made.
|
|
4
|
+
*
|
|
5
|
+
* run.json a RunManifest
|
|
6
|
+
* cases.jsonl one RunCase per line, one line per attempt, in the order they ran
|
|
7
|
+
* <case id>/… what a case's steps point at: frames, a film
|
|
8
|
+
*
|
|
9
|
+
* Whoever writes it is an engine; whoever reads it (a report, a dashboard, a
|
|
10
|
+
* script) depends on this file and on nothing else. A field is only ever
|
|
11
|
+
* added. Anything that would change the meaning of an existing field raises
|
|
12
|
+
* RUN_SCHEMA, and a reader refuses a schema it does not know.
|
|
13
|
+
*/
|
|
14
|
+
export const RUN_SCHEMA = 1;
|
|
15
|
+
|
|
16
|
+
/** Where an event was observed: inside the app, or by the backend the app talks to. */
|
|
17
|
+
export type RunEventOrigin = 'app' | 'server';
|
|
18
|
+
|
|
19
|
+
export type RunEvent = {
|
|
20
|
+
origin: RunEventOrigin;
|
|
21
|
+
name: string;
|
|
22
|
+
props?: Record<string, unknown>;
|
|
23
|
+
/** Who it was sent to, when the engine can tell: 'mixpanel', 'datadog', … */
|
|
24
|
+
provider?: string;
|
|
25
|
+
/** The route the app was on, for an event observed in the app. */
|
|
26
|
+
route?: string;
|
|
27
|
+
/** ISO 8601, when the source says. */
|
|
28
|
+
at?: string;
|
|
29
|
+
/** The request that caused it, for a server event: what files it under a step. */
|
|
30
|
+
requestId?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** `unsupported`: the host the run was on has no way to do the step; it says nothing about the app. */
|
|
34
|
+
export type RunStepStatus = 'passed' | 'failed' | 'skipped' | 'unsupported';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What the backend did for a step, from the traces it exported: the request the step's app sent, and the
|
|
38
|
+
* queries, jobs and calls out it set off. One per span, in the order they began; `depth` nests them.
|
|
39
|
+
*/
|
|
40
|
+
export type RunBackendSpan = {
|
|
41
|
+
name: string;
|
|
42
|
+
/** server, client, internal, producer or consumer, as OpenTelemetry says. */
|
|
43
|
+
kind: string;
|
|
44
|
+
/** Milliseconds after the step began. */
|
|
45
|
+
at: number;
|
|
46
|
+
ms: number;
|
|
47
|
+
depth: number;
|
|
48
|
+
/** The service that exported it, when it says. */
|
|
49
|
+
service?: string;
|
|
50
|
+
error?: string;
|
|
51
|
+
/** What it did: method, route, status, table, job class, host called. */
|
|
52
|
+
attributes?: Record<string, string | number | boolean>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type RunStep = {
|
|
56
|
+
/** The step as the case words it. */
|
|
57
|
+
step: string;
|
|
58
|
+
status: RunStepStatus;
|
|
59
|
+
ms: number;
|
|
60
|
+
error?: string;
|
|
61
|
+
/** Which part of the case it belongs to. */
|
|
62
|
+
section?: 'setup' | 'steps';
|
|
63
|
+
events?: RunEvent[];
|
|
64
|
+
/** A frame taken once the step was done, relative to the run directory. */
|
|
65
|
+
screenshot?: string;
|
|
66
|
+
/** The second of the case's film this step starts at. */
|
|
67
|
+
videoS?: number;
|
|
68
|
+
/** What the backend did for it, when the backend exports traces to the run. */
|
|
69
|
+
backend?: RunBackendSpan[];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type RunCase = {
|
|
73
|
+
id: string;
|
|
74
|
+
title?: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
/** The case file, relative to where the run was started. */
|
|
77
|
+
file: string;
|
|
78
|
+
/** 1 for the first try; a case that failed and ran again has a line for each. */
|
|
79
|
+
attempt: number;
|
|
80
|
+
/** `unsupported`: one of its steps could not be done on this host, so it proved nothing here. */
|
|
81
|
+
result: 'passed' | 'failed' | 'unsupported';
|
|
82
|
+
/** ISO 8601. */
|
|
83
|
+
startedAt: string;
|
|
84
|
+
ms: number;
|
|
85
|
+
steps: RunStep[];
|
|
86
|
+
/** Why it failed, as rules read off the screen, the requests and the trace. */
|
|
87
|
+
diagnosis?: string[];
|
|
88
|
+
/** When a step failed: the screen, the requests since the case began, and why. */
|
|
89
|
+
evidence?: { screen?: string; requests?: string[]; why?: string };
|
|
90
|
+
/** The case's film, relative to the run directory. */
|
|
91
|
+
video?: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type RunManifest = {
|
|
95
|
+
schema: typeof RUN_SCHEMA;
|
|
96
|
+
/** ISO 8601. */
|
|
97
|
+
startedAt: string;
|
|
98
|
+
/** Absent while the run is going, or when it was cut short. */
|
|
99
|
+
finishedAt?: string;
|
|
100
|
+
/** What the app said it runs on: 'ios', 'android', … */
|
|
101
|
+
platform?: string;
|
|
102
|
+
/** Whether the app ran on a device or simulator (true) or hosted without one. */
|
|
103
|
+
native?: boolean;
|
|
104
|
+
/** Where the run was made from, when it was a git checkout. */
|
|
105
|
+
git?: { branch?: string; commit?: string };
|
|
106
|
+
/** Cases by their last attempt. */
|
|
107
|
+
totals?: { cases: number; passed: number; failed: number; unsupported: number; retried: number };
|
|
108
|
+
};
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { test } from 'node:test';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
-
|
|
8
|
-
import { PROTOCOL_VERSION } from '../index.js';
|
|
9
|
-
|
|
10
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const source = fs.readFileSync(path.join(here, '..', 'index.ts'), 'utf8');
|
|
12
|
-
const frozenFile = path.join(here, '..', '..', 'shape.json');
|
|
13
|
-
const agentProtocol = fs.readFileSync(path.join(here, '..', '..', '..', 'agent', 'src', 'protocol.ts'), 'utf8');
|
|
14
|
-
|
|
15
|
-
const SHAPES = ['ElementKind', 'HiddenReason', 'Frame', 'Element', 'Screen', 'Scope', 'Target'];
|
|
16
|
-
|
|
17
|
-
const declaration = (name: string): string => {
|
|
18
|
-
const start = source.search(new RegExp(`export type ${name}\\b`));
|
|
19
|
-
assert.notEqual(start, -1, `no export type ${name} in the protocol`);
|
|
20
|
-
let depth = 0;
|
|
21
|
-
for (let i = start; i < source.length; i++) {
|
|
22
|
-
const c = source[i];
|
|
23
|
-
if (c === '{' || c === '(') depth++;
|
|
24
|
-
else if (c === '}' || c === ')') depth--;
|
|
25
|
-
else if (c === ';' && depth === 0) return source.slice(start, i + 1);
|
|
26
|
-
}
|
|
27
|
-
return source.slice(start);
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const normalized = (text: string) =>
|
|
31
|
-
text
|
|
32
|
-
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
33
|
-
.replace(/\/\/.*$/gm, '')
|
|
34
|
-
.replace(/\s+/g, ' ')
|
|
35
|
-
.trim();
|
|
36
|
-
|
|
37
|
-
const hash = createHash('sha1')
|
|
38
|
-
.update(SHAPES.map((n) => normalized(declaration(n))).join('\n'))
|
|
39
|
-
.digest('hex');
|
|
40
|
-
const agentVersion = Number(/PROTOCOL_VERSION = (\d+)/.exec(agentProtocol)?.[1]);
|
|
41
|
-
|
|
42
|
-
test('the protocol shapes change only on purpose', () => {
|
|
43
|
-
if (process.env.UPDATE_PROTOCOL_SHAPE === '1') fs.writeFileSync(frozenFile, `${JSON.stringify({ version: PROTOCOL_VERSION, hash }, null, 2)}\n`);
|
|
44
|
-
const frozen = JSON.parse(fs.readFileSync(frozenFile, 'utf8')) as { version: number; hash: string };
|
|
45
|
-
assert.equal(
|
|
46
|
-
hash,
|
|
47
|
-
frozen.hash,
|
|
48
|
-
`${SHAPES.join(', ')} changed. A change a driver of version ${frozen.version} would misread needs PROTOCOL_VERSION bumped; an optional field it can ignore does not. Either way, record the decision with UPDATE_PROTOCOL_SHAPE=1.`,
|
|
49
|
-
);
|
|
50
|
-
assert.equal(PROTOCOL_VERSION, frozen.version, 'PROTOCOL_VERSION moved without recording the shape it belongs to (UPDATE_PROTOCOL_SHAPE=1)');
|
|
51
|
-
assert.equal(agentVersion, PROTOCOL_VERSION, "lynkeus-agent's own PROTOCOL_VERSION must equal this one");
|
|
52
|
-
});
|