lynkeus 0.4.0 → 0.5.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/cases/coverage.d.ts +31 -0
- package/lib/cases/coverage.js +82 -0
- package/lib/cases/draft.d.ts +21 -0
- package/lib/cases/draft.js +53 -0
- package/lib/cases/fixtures.d.ts +11 -0
- package/lib/cases/fixtures.js +59 -8
- package/lib/cases/frames.d.ts +6 -0
- package/lib/cases/frames.js +28 -0
- package/lib/cases/nativeLogs.d.ts +14 -0
- package/lib/cases/nativeLogs.js +111 -0
- package/lib/cases/runDirs.d.ts +13 -0
- package/lib/cases/runDirs.js +48 -0
- package/lib/cases/runfile.d.ts +5 -1
- package/lib/cases/runfile.js +67 -4
- package/lib/cases/runner.d.ts +56 -1
- package/lib/cases/runner.js +309 -33
- package/lib/cases/templates.d.ts +2 -2
- package/lib/cases/templates.js +20 -2
- package/lib/cases/traces.js +4 -1
- package/lib/cli.js +25 -2
- package/lib/commands/cases.d.ts +8 -0
- package/lib/commands/cases.js +102 -19
- package/lib/commands/ci.d.ts +13 -0
- package/lib/commands/ci.js +184 -0
- package/lib/commands/coverage.d.ts +1 -0
- package/lib/commands/coverage.js +151 -0
- package/lib/commands/device.js +32 -2
- package/lib/commands/drive.js +3 -1
- package/lib/commands/fixtures.d.ts +7 -0
- package/lib/commands/fixtures.js +94 -0
- package/lib/commands/headless.d.ts +11 -0
- package/lib/commands/headless.js +69 -3
- package/lib/commands/map.d.ts +2 -0
- package/lib/commands/map.js +18 -14
- package/lib/commands/os.js +43 -10
- package/lib/commands/report.d.ts +4 -0
- package/lib/commands/report.js +154 -18
- package/lib/config.d.ts +25 -0
- package/lib/device/agentVersion.d.ts +7 -0
- package/lib/device/agentVersion.js +14 -0
- package/lib/device/devices.js +6 -0
- package/lib/knowledge/analyticsTraffic.d.ts +10 -0
- package/lib/knowledge/analyticsTraffic.js +109 -0
- package/lib/knowledge/errors.js +6 -0
- package/lib/knowledge/events.d.ts +2 -0
- package/lib/knowledge/why.d.ts +2 -0
- package/lib/knowledge/why.js +1 -1
- package/lib/mcp/server.js +16 -4
- package/lib/runtime.d.ts +9 -1
- package/lib/runtime.js +25 -3
- package/package.json +7 -4
- package/src/cases/coverage.ts +99 -0
- package/src/cases/draft.ts +75 -0
- package/src/cases/fixtures.ts +57 -8
- package/src/cases/frames.ts +30 -0
- package/src/cases/nativeLogs.ts +112 -0
- package/src/cases/runDirs.ts +48 -0
- package/src/cases/runfile.ts +64 -4
- package/src/cases/runner.ts +359 -37
- package/src/cases/templates.ts +20 -2
- package/src/cases/traces.ts +3 -1
- package/src/cli.ts +25 -2
- package/src/commands/cases.ts +111 -18
- package/src/commands/ci.ts +189 -0
- package/src/commands/coverage.ts +147 -0
- package/src/commands/device.ts +27 -2
- package/src/commands/drive.ts +3 -1
- package/src/commands/fixtures.ts +102 -0
- package/src/commands/headless.ts +66 -3
- package/src/commands/map.ts +20 -15
- package/src/commands/os.ts +38 -10
- package/src/commands/report.ts +160 -16
- package/src/config.ts +15 -0
- package/src/device/agentVersion.ts +17 -0
- package/src/device/devices.ts +5 -0
- package/src/knowledge/analyticsTraffic.ts +142 -0
- package/src/knowledge/errors.ts +6 -0
- package/src/knowledge/events.ts +2 -1
- package/src/knowledge/why.ts +3 -1
- package/src/mcp/server.ts +16 -4
- package/src/runtime.ts +24 -3
package/lib/cases/runfile.js
CHANGED
|
@@ -14,8 +14,19 @@ export const toRunCase = (c, { report, attempt, startedAt }, root, dir) => {
|
|
|
14
14
|
// The runner numbers an event's step from 1, in the order the steps ran.
|
|
15
15
|
const told = (report.events ?? [])
|
|
16
16
|
.filter((e) => e.step === index + 1)
|
|
17
|
-
.map((e) => ({
|
|
18
|
-
|
|
17
|
+
.map((e) => ({
|
|
18
|
+
origin: 'app',
|
|
19
|
+
...(e.provider ? { provider: e.provider } : {}),
|
|
20
|
+
name: e.name,
|
|
21
|
+
...(e.props ? { props: e.props } : {}),
|
|
22
|
+
...(e.route ? { route: e.route } : {}),
|
|
23
|
+
}));
|
|
24
|
+
const sent = (s.sent ?? []).map((e) => ({
|
|
25
|
+
origin: e.via === 'server' ? 'server' : 'app',
|
|
26
|
+
provider: e.provider,
|
|
27
|
+
name: e.name,
|
|
28
|
+
...(e.props ? { props: e.props } : {}),
|
|
29
|
+
}));
|
|
19
30
|
const events = [...told, ...sent];
|
|
20
31
|
const screenshot = within(dir, s.screenshot);
|
|
21
32
|
return {
|
|
@@ -28,6 +39,8 @@ export const toRunCase = (c, { report, attempt, startedAt }, root, dir) => {
|
|
|
28
39
|
...(screenshot ? { screenshot } : {}),
|
|
29
40
|
...(s.videoS !== undefined ? { videoS: s.videoS } : {}),
|
|
30
41
|
...(s.backend?.length ? { backend: s.backend } : {}),
|
|
42
|
+
...(s.requests?.length ? { requests: s.requests } : {}),
|
|
43
|
+
...(s.logs?.length ? { logs: s.logs } : {}),
|
|
31
44
|
};
|
|
32
45
|
});
|
|
33
46
|
const description = typeof c.meta.description === 'string' ? c.meta.description : undefined;
|
|
@@ -36,6 +49,7 @@ export const toRunCase = (c, { report, attempt, startedAt }, root, dir) => {
|
|
|
36
49
|
id: report.id,
|
|
37
50
|
...(report.title ? { title: report.title } : {}),
|
|
38
51
|
...(description ? { description } : {}),
|
|
52
|
+
...(report.about ? { about: report.about } : {}),
|
|
39
53
|
file: path.relative(root, path.resolve(root, report.file)),
|
|
40
54
|
attempt,
|
|
41
55
|
result: report.result,
|
|
@@ -48,7 +62,7 @@ export const toRunCase = (c, { report, attempt, startedAt }, root, dir) => {
|
|
|
48
62
|
};
|
|
49
63
|
};
|
|
50
64
|
/** The branch and commit the run was made from, so a report can say which code it tested. */
|
|
51
|
-
const gitOf = (root) => {
|
|
65
|
+
export const gitOf = (root) => {
|
|
52
66
|
const ask = (...args) => {
|
|
53
67
|
try {
|
|
54
68
|
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || undefined;
|
|
@@ -61,7 +75,56 @@ const gitOf = (root) => {
|
|
|
61
75
|
if (!commit)
|
|
62
76
|
return undefined;
|
|
63
77
|
const branch = process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || ask('rev-parse', '--abbrev-ref', 'HEAD');
|
|
64
|
-
|
|
78
|
+
const remote = webOf(ask('remote', 'get-url', 'origin'));
|
|
79
|
+
// What the run had that no commit has: a report says the app was not exactly the commit it names.
|
|
80
|
+
const changed = (ask('status', '--porcelain', '--untracked-files=no') ?? '')
|
|
81
|
+
.split('\n')
|
|
82
|
+
.map((l) => l.slice(3).trim())
|
|
83
|
+
.filter(Boolean);
|
|
84
|
+
// A branch that lives only on this machine names nothing a reader can open: the run names the remote branch
|
|
85
|
+
// that has its commit instead (the one it tracks first, else any), and keeps the local name beside it.
|
|
86
|
+
const local = branch && branch !== 'HEAD' ? branch : undefined;
|
|
87
|
+
const onRemote = (name) => !!ask('rev-parse', '--verify', '--quiet', `refs/remotes/origin/${name}`);
|
|
88
|
+
const holding = remote && local && !onRemote(local) ? remoteBranchWith(ask, commit) : undefined;
|
|
89
|
+
const shown = holding ?? local;
|
|
90
|
+
const pushed = remote ? { branch: !!shown && (shown === holding || onRemote(shown)), commit: !!ask('branch', '--remotes', '--contains', commit) } : undefined;
|
|
91
|
+
return {
|
|
92
|
+
commit,
|
|
93
|
+
...(shown ? { branch: shown } : {}),
|
|
94
|
+
...(holding && local ? { localBranch: local } : {}),
|
|
95
|
+
...(remote ? { remote } : {}),
|
|
96
|
+
...(pushed ? { pushed } : {}),
|
|
97
|
+
...(changed.length ? { changed: changed.slice(0, 20), changedCount: changed.length } : {}),
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
/** The origin branch that holds `commit`: the one the checkout tracks when it does, else the first. */
|
|
101
|
+
const remoteBranchWith = (ask, commit) => {
|
|
102
|
+
const strip = (ref) => ref.replace(/^origin\//, '');
|
|
103
|
+
const upstream = ask('rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}');
|
|
104
|
+
const holders = (ask('branch', '--remotes', '--contains', commit, '--format=%(refname:short)') ?? '')
|
|
105
|
+
.split('\n')
|
|
106
|
+
.map((l) => l.trim())
|
|
107
|
+
.filter((l) => l.startsWith('origin/') && l !== 'origin/HEAD' && l !== 'origin');
|
|
108
|
+
if (upstream && holders.includes(upstream))
|
|
109
|
+
return strip(upstream);
|
|
110
|
+
return holders[0] ? strip(holders[0]) : undefined;
|
|
111
|
+
};
|
|
112
|
+
/** A remote as a web address a report can link commits to, without any credentials it carried. */
|
|
113
|
+
export const webOf = (url) => {
|
|
114
|
+
if (!url)
|
|
115
|
+
return undefined;
|
|
116
|
+
const ssh = url.match(/^[\w.-]+@([\w.-]+):(.+?)(\.git)?$/);
|
|
117
|
+
if (ssh)
|
|
118
|
+
return `https://${ssh[1]}/${ssh[2]}`;
|
|
119
|
+
try {
|
|
120
|
+
const u = new URL(url);
|
|
121
|
+
if (u.protocol !== 'https:' && u.protocol !== 'http:' && u.protocol !== 'ssh:')
|
|
122
|
+
return undefined;
|
|
123
|
+
return `https://${u.hostname}${u.pathname.replace(/\.git$/, '')}`;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
65
128
|
};
|
|
66
129
|
export class RunFile {
|
|
67
130
|
dir;
|
package/lib/cases/runner.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RequestRecord } from 'lynkeus-protocol';
|
|
1
2
|
import { type AppEvent, type RunEvent } from '../knowledge/events.js';
|
|
2
3
|
import { type Base, type Command } from '../registry.js';
|
|
3
4
|
import type { Fixtures, FixturesConfig } from './fixtures.js';
|
|
@@ -34,6 +35,8 @@ export type CasesConfig = {
|
|
|
34
35
|
/** `{ "user": "user_id" }` turns `user: u` into `user_id: <u.user_id>`, and gives a step that names no user the latest one. */
|
|
35
36
|
refs?: Record<string, string>;
|
|
36
37
|
/** Steps this project adds: `{ "app.login": [...] }`. Inside, `{{param.x}}` is what the case passed. */
|
|
38
|
+
/** The screen a case begins on, for drafts (`case new`): the first the app shows a signed-in person, say. */
|
|
39
|
+
start?: string;
|
|
37
40
|
macros?: Record<string, unknown[]>;
|
|
38
41
|
/** What the app may put in the way of a step and the steps that get past it; a step whose own words match `unless` is about it, and is left alone. */
|
|
39
42
|
interruptions?: {
|
|
@@ -41,12 +44,27 @@ export type CasesConfig = {
|
|
|
41
44
|
do: unknown[];
|
|
42
45
|
unless?: string;
|
|
43
46
|
}[];
|
|
47
|
+
/**
|
|
48
|
+
* What the backend told its own analytics while a step ran, for a report: after each app step the fixture is
|
|
49
|
+
* asked with `{ since }` (the step's start, ISO) and answers `{ events: [{ name | event, props | payload,
|
|
50
|
+
* provider? }] }`. `provider` names the service when the fixture does not (an Olympus that sends to Mixpanel).
|
|
51
|
+
*/
|
|
52
|
+
serverEvents?: {
|
|
53
|
+
fixture: string;
|
|
54
|
+
provider?: string;
|
|
55
|
+
};
|
|
56
|
+
/** The service the app's own event list feeds (its `events` command), so a report names it instead of "App". */
|
|
57
|
+
appEvents?: {
|
|
58
|
+
provider: string;
|
|
59
|
+
};
|
|
44
60
|
};
|
|
45
61
|
/** What a host's analytics capture saw a step send: the SDK it went to and the event. */
|
|
62
|
+
/** `via`: the SDK's native module (a host that stands in for it), the request it sent, or the backend's own analytics. */
|
|
46
63
|
export type SentEvent = {
|
|
47
64
|
provider: string;
|
|
48
65
|
name: string;
|
|
49
66
|
props?: Record<string, unknown>;
|
|
67
|
+
via?: 'sdk' | 'network' | 'server';
|
|
50
68
|
};
|
|
51
69
|
/** `unsupported`: the host the case ran on has no such thing to do (a real device has no mocked responses), which says nothing about the app. */
|
|
52
70
|
export type StepReport = {
|
|
@@ -62,6 +80,18 @@ export type StepReport = {
|
|
|
62
80
|
sent?: SentEvent[];
|
|
63
81
|
/** What the backend did for it, from the traces it exported to the run. */
|
|
64
82
|
backend?: BackendSpan[];
|
|
83
|
+
/** The requests the app sent while it ran, with headers and bodies (secrets hidden), when the run keeps evidence. */
|
|
84
|
+
requests?: RequestRecord[];
|
|
85
|
+
/** What the app printed while it ran: its console and the errors it did not catch, when the run keeps evidence. */
|
|
86
|
+
logs?: LogLine[];
|
|
87
|
+
};
|
|
88
|
+
/** A line the app printed: `at` in ms since the epoch, `source` where it came from (`js`, `native`, `host`). */
|
|
89
|
+
export type LogLine = {
|
|
90
|
+
at: number;
|
|
91
|
+
level: 'debug' | 'info' | 'warn' | 'error';
|
|
92
|
+
source: string;
|
|
93
|
+
text: string;
|
|
94
|
+
stack?: string;
|
|
65
95
|
};
|
|
66
96
|
export type CaseReport = {
|
|
67
97
|
id: string;
|
|
@@ -71,6 +101,17 @@ export type CaseReport = {
|
|
|
71
101
|
result: 'passed' | 'failed' | 'unsupported';
|
|
72
102
|
ms: number;
|
|
73
103
|
steps: StepReport[];
|
|
104
|
+
/**
|
|
105
|
+
* What a reader needs before the steps: what the case proves (`description`), why it matters (`why`), what
|
|
106
|
+
* has to be true when it starts (`given`), the ticket it covers and the outcome it expects. From its frontmatter.
|
|
107
|
+
*/
|
|
108
|
+
about?: {
|
|
109
|
+
description?: string;
|
|
110
|
+
why?: string;
|
|
111
|
+
given?: string;
|
|
112
|
+
ticket?: string;
|
|
113
|
+
expected?: string;
|
|
114
|
+
};
|
|
74
115
|
/** When a step failed: the screen, the requests since the mark, and why. */
|
|
75
116
|
evidence?: {
|
|
76
117
|
screen?: string;
|
|
@@ -87,10 +128,17 @@ export type CaseReport = {
|
|
|
87
128
|
diagnosis?: string[];
|
|
88
129
|
};
|
|
89
130
|
/** Where a case keeps what it can show of itself: a frame per step, a film. Each is taken only where the host can. */
|
|
131
|
+
/** `frames`: AVIF where the host writes it (a fifth of a PNG's bytes, lossy), or PNG, exact, for frames two runs are compared by. */
|
|
90
132
|
export type Evidence = {
|
|
91
133
|
dir: string;
|
|
92
134
|
screenshots?: boolean;
|
|
93
135
|
settleMs?: number;
|
|
136
|
+
/** Wait for the screen to stand still: two frames alike this many ms apart, on a host's own clock where it offers
|
|
137
|
+
* that (`still`), or taken again on a device. */
|
|
138
|
+
stillMs?: number;
|
|
139
|
+
/** A looking step on an unchanged screen shares the last frame instead of taking one. */
|
|
140
|
+
onChange?: boolean;
|
|
141
|
+
frames?: 'avif' | 'png';
|
|
94
142
|
video?: {
|
|
95
143
|
fps: number;
|
|
96
144
|
scale?: number;
|
|
@@ -115,7 +163,10 @@ export type RunOptions = {
|
|
|
115
163
|
/** Keep running a case's steps after one fails, to see everything that is wrong in one pass. */
|
|
116
164
|
continue?: boolean;
|
|
117
165
|
/** Collect the screens the app went through, for a coverage map. `fresh`: the app was started for this case. */
|
|
118
|
-
routes?: boolean | 'fresh'
|
|
166
|
+
routes?: boolean | 'fresh' /** The app's native log on a simulator or an emulator, read step by step into each step's logs. */;
|
|
167
|
+
nativeLogs?: {
|
|
168
|
+
take: () => LogLine[];
|
|
169
|
+
};
|
|
119
170
|
};
|
|
120
171
|
/** What a case's setup leaves for its steps: the aliases and what they answered, and what ran. */
|
|
121
172
|
export type Prepared = {
|
|
@@ -137,4 +188,8 @@ export declare const argvFor: (command: Command, value: StepValue) => string[];
|
|
|
137
188
|
export declare const missingFixtures: (c: Case, config: CasesConfig, known: string[]) => string[];
|
|
138
189
|
/** What the app or its host answers to a method it does not have; the name is what it lacks. */
|
|
139
190
|
export declare const lacking: (message: string) => string | undefined;
|
|
191
|
+
export declare const fileRequestsByTrace: (steps: StepReport[], spans: Map<string, {
|
|
192
|
+
index: number;
|
|
193
|
+
startedMs: number;
|
|
194
|
+
}>) => void;
|
|
140
195
|
export declare const runCase: (c: Case, options: RunOptions) => Promise<CaseReport>;
|