lynkeus 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/cases/ahead.d.ts +31 -0
- package/lib/cases/ahead.js +90 -0
- package/lib/cases/fixtures.d.ts +12 -0
- package/lib/cases/fixtures.js +32 -0
- package/lib/cases/flakes.d.ts +28 -0
- package/lib/cases/flakes.js +61 -0
- package/lib/cases/format.d.ts +5 -0
- package/lib/cases/format.js +30 -4
- package/lib/cases/portClaim.d.ts +10 -0
- package/lib/cases/portClaim.js +55 -0
- package/lib/cases/runfile.d.ts +24 -0
- package/lib/cases/runfile.js +108 -0
- package/lib/cases/runner.d.ts +82 -3
- package/lib/cases/runner.js +394 -40
- package/lib/cases/templates.d.ts +2 -2
- package/lib/cases/templates.js +14 -4
- package/lib/cases/traces.d.ts +58 -0
- package/lib/cases/traces.js +250 -0
- package/lib/cli.js +28 -7
- package/lib/commands/cases.js +411 -42
- package/lib/commands/check.d.ts +6 -0
- package/lib/commands/check.js +8 -3
- package/lib/commands/doctor.js +8 -2
- package/lib/commands/drive.js +32 -8
- package/lib/commands/headless.js +132 -8
- package/lib/commands/init.d.ts +29 -0
- package/lib/commands/init.js +295 -33
- package/lib/commands/report.d.ts +97 -0
- package/lib/commands/report.js +228 -0
- package/lib/config.d.ts +26 -1
- package/lib/config.js +30 -0
- package/lib/device/agentVersion.d.ts +7 -0
- package/lib/device/agentVersion.js +12 -0
- package/lib/device/devices.d.ts +5 -0
- package/lib/device/devices.js +27 -0
- package/lib/device/driver.js +2 -2
- package/lib/flow/runner.js +33 -1
- package/lib/flow/types.d.ts +2 -1
- package/lib/knowledge/contrast.d.ts +45 -0
- package/lib/knowledge/contrast.js +114 -0
- package/lib/knowledge/errors.js +1 -1
- package/lib/knowledge/lint.d.ts +2 -0
- package/lib/knowledge/lint.js +13 -3
- package/lib/mcp/server.js +31 -2
- package/lib/registry.d.ts +5 -0
- package/lib/targets.d.ts +10 -1
- package/lib/targets.js +16 -3
- package/package.json +10 -6
- package/src/cases/ahead.ts +111 -0
- package/src/cases/fixtures.ts +37 -0
- package/src/cases/flakes.ts +67 -0
- package/src/cases/format.ts +31 -6
- package/src/cases/portClaim.ts +52 -0
- package/src/cases/runfile.ts +119 -0
- package/src/cases/runner.ts +454 -46
- package/src/cases/templates.ts +14 -4
- package/src/cases/traces.ts +298 -0
- package/src/cli.ts +29 -7
- package/src/commands/cases.ts +428 -41
- package/src/commands/check.ts +11 -4
- package/src/commands/doctor.ts +9 -2
- package/src/commands/drive.ts +31 -8
- package/src/commands/headless.ts +143 -8
- package/src/commands/init.ts +338 -30
- package/src/commands/report.ts +272 -0
- package/src/config.ts +39 -2
- package/src/device/agentVersion.ts +15 -0
- package/src/device/devices.ts +28 -0
- package/src/device/driver.ts +2 -2
- package/src/flow/runner.ts +35 -1
- package/src/flow/types.ts +2 -2
- package/src/knowledge/contrast.ts +135 -0
- package/src/knowledge/errors.ts +1 -1
- package/src/knowledge/lint.ts +15 -4
- package/src/mcp/server.ts +31 -2
- package/src/registry.ts +2 -0
- package/src/targets.ts +17 -3
- package/lib/__tests__/args.test.d.ts +0 -1
- package/lib/__tests__/args.test.js +0 -69
- package/lib/__tests__/check.test.d.ts +0 -1
- package/lib/__tests__/check.test.js +0 -60
- package/lib/__tests__/init.test.d.ts +0 -1
- package/lib/__tests__/init.test.js +0 -42
- package/lib/__tests__/record.test.d.ts +0 -1
- package/lib/__tests__/record.test.js +0 -44
- package/lib/__tests__/snapshot.test.d.ts +0 -1
- package/lib/__tests__/snapshot.test.js +0 -47
- package/lib/__tests__/targets.test.d.ts +0 -1
- package/lib/__tests__/targets.test.js +0 -127
- package/lib/cases/__tests__/format.test.d.ts +0 -1
- package/lib/cases/__tests__/format.test.js +0 -61
- package/lib/cases/__tests__/runner.test.d.ts +0 -1
- package/lib/cases/__tests__/runner.test.js +0 -188
- package/lib/knowledge/__tests__/edges.test.d.ts +0 -1
- package/lib/knowledge/__tests__/edges.test.js +0 -25
- package/lib/knowledge/__tests__/events.test.d.ts +0 -1
- package/lib/knowledge/__tests__/events.test.js +0 -39
- package/lib/knowledge/__tests__/extract.test.d.ts +0 -1
- package/lib/knowledge/__tests__/extract.test.js +0 -69
- package/lib/knowledge/__tests__/frontier.test.d.ts +0 -1
- package/lib/knowledge/__tests__/frontier.test.js +0 -57
- package/lib/knowledge/__tests__/funnels.test.d.ts +0 -1
- package/lib/knowledge/__tests__/funnels.test.js +0 -40
- package/lib/knowledge/__tests__/lint.test.d.ts +0 -1
- package/lib/knowledge/__tests__/lint.test.js +0 -53
- package/lib/knowledge/__tests__/perf.test.d.ts +0 -1
- package/lib/knowledge/__tests__/perf.test.js +0 -34
- package/lib/knowledge/__tests__/plan.test.d.ts +0 -1
- package/lib/knowledge/__tests__/plan.test.js +0 -35
- package/lib/knowledge/__tests__/query.test.d.ts +0 -1
- package/lib/knowledge/__tests__/query.test.js +0 -55
- package/lib/knowledge/__tests__/screens.test.d.ts +0 -1
- package/lib/knowledge/__tests__/screens.test.js +0 -30
- package/lib/knowledge/__tests__/why.test.d.ts +0 -1
- package/lib/knowledge/__tests__/why.test.js +0 -28
- package/src/__tests__/args.test.ts +0 -82
- package/src/__tests__/check.test.ts +0 -71
- package/src/__tests__/init.test.ts +0 -52
- package/src/__tests__/record.test.ts +0 -49
- package/src/__tests__/snapshot.test.ts +0 -51
- package/src/__tests__/targets.test.ts +0 -132
- package/src/cases/__tests__/format.test.ts +0 -76
- package/src/cases/__tests__/runner.test.ts +0 -230
- package/src/knowledge/__tests__/edges.test.ts +0 -34
- package/src/knowledge/__tests__/events.test.ts +0 -45
- package/src/knowledge/__tests__/extract.test.ts +0 -88
- package/src/knowledge/__tests__/frontier.test.ts +0 -77
- package/src/knowledge/__tests__/funnels.test.ts +0 -58
- package/src/knowledge/__tests__/lint.test.ts +0 -66
- package/src/knowledge/__tests__/perf.test.ts +0 -41
- package/src/knowledge/__tests__/plan.test.ts +0 -52
- package/src/knowledge/__tests__/query.test.ts +0 -74
- package/src/knowledge/__tests__/screens.test.ts +0 -37
- package/src/knowledge/__tests__/why.test.ts +0 -40
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
|
|
4
|
-
import type { ObservedEdge } from '../edges.js';
|
|
5
|
-
import { describePlan, plan } from '../plan.js';
|
|
6
|
-
import type { ScreenGraph } from '../types.js';
|
|
7
|
-
|
|
8
|
-
const graph = (routes: string[]): ScreenGraph => ({
|
|
9
|
-
app: 'test',
|
|
10
|
-
root: '/tmp',
|
|
11
|
-
extractedAt: '2026-01-01',
|
|
12
|
-
screens: Object.fromEntries(routes.map((route) => [route, { route, component: route, file: null, declaredIn: 'nav.tsx' }])),
|
|
13
|
-
files: {},
|
|
14
|
-
warnings: [],
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const edge = (e: Partial<ObservedEdge> & Pick<ObservedEdge, 'from' | 'to' | 'via'>): ObservedEdge => ({ seen: 1, last: '2026-01-01', ...e });
|
|
18
|
-
|
|
19
|
-
test('a sheet is a place the directions can reach', () => {
|
|
20
|
-
const steps = plan(graph(['Settings.Main', 'Account.Delete']), 'Settings.Main', 'Account.Delete', [
|
|
21
|
-
edge({ from: 'Settings.Main', to: 'Settings.Main', via: 'account-button', toLayer: 'account-button' }),
|
|
22
|
-
edge({ from: 'Settings.Main', to: 'Account.Delete', via: 'delete-account-button', fromLayer: 'account-button' }),
|
|
23
|
-
]);
|
|
24
|
-
|
|
25
|
-
assert.deepEqual(
|
|
26
|
-
steps?.map((s) => [s.from, s.press, s.to]),
|
|
27
|
-
[
|
|
28
|
-
['Settings.Main', 'account-button', 'Settings.Main#account-button'],
|
|
29
|
-
['Settings.Main#account-button', 'delete-account-button', 'Account.Delete'],
|
|
30
|
-
],
|
|
31
|
-
);
|
|
32
|
-
assert.match(describePlan(steps!), /opens a layer/);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('what a screen offers does not apply while a sheet covers it', () => {
|
|
36
|
-
const steps = plan(graph(['Settings.Main', 'Profile.Edit']), 'Settings.Main#account-button', 'Profile.Edit', [
|
|
37
|
-
edge({ from: 'Settings.Main', to: 'Profile.Edit', via: 'profile-button' }),
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
assert.equal(steps, null);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('asking for a route accepts arriving with something open over it', () => {
|
|
44
|
-
const steps = plan(graph(['Settings.Main', 'Checkout.Main']), 'Settings.Main', 'Checkout.Main', [
|
|
45
|
-
edge({ from: 'Settings.Main', to: 'Checkout.Main', via: 'send-button', toLayer: 'send-button' }),
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
|
-
assert.deepEqual(
|
|
49
|
-
steps?.map((s) => s.to),
|
|
50
|
-
['Checkout.Main#send-button'],
|
|
51
|
-
);
|
|
52
|
-
});
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
|
|
4
|
-
import { screensAffectedBy } from '../query.js';
|
|
5
|
-
import type { ScreenGraph } from '../types.js';
|
|
6
|
-
|
|
7
|
-
const graph = {
|
|
8
|
-
app: 'test',
|
|
9
|
-
root: 'Root',
|
|
10
|
-
extractedAt: '',
|
|
11
|
-
warnings: [],
|
|
12
|
-
screens: {
|
|
13
|
-
'A.Main': { route: 'A.Main', component: 'A', file: 'app/a.tsx', declaredIn: 'nav.tsx' },
|
|
14
|
-
'B.Main': { route: 'B.Main', component: 'B', file: 'app/b.tsx', declaredIn: 'nav.tsx' },
|
|
15
|
-
},
|
|
16
|
-
files: {
|
|
17
|
-
'app/a.tsx': { imports: ['app/shared.tsx'] },
|
|
18
|
-
'app/b.tsx': { imports: [] },
|
|
19
|
-
'app/shared.tsx': { imports: [] },
|
|
20
|
-
},
|
|
21
|
-
} as unknown as ScreenGraph;
|
|
22
|
-
|
|
23
|
-
test('a changed screen file is affected at distance 0', () => {
|
|
24
|
-
const r = screensAffectedBy(graph, ['app/a.tsx']);
|
|
25
|
-
assert.deepEqual(
|
|
26
|
-
r.map((x) => [x.route, x.distance]),
|
|
27
|
-
[['A.Main', 0]],
|
|
28
|
-
);
|
|
29
|
-
});
|
|
30
|
-
test('a shared file reaches its importer at distance 1, naming the changed file', () => {
|
|
31
|
-
const r = screensAffectedBy(graph, ['app/shared.tsx']);
|
|
32
|
-
assert.deepEqual(
|
|
33
|
-
r.map((x) => [x.route, x.distance, x.via]),
|
|
34
|
-
[['A.Main', 1, 'app/shared.tsx']],
|
|
35
|
-
);
|
|
36
|
-
});
|
|
37
|
-
test('results are nearest first', () => {
|
|
38
|
-
const g = {
|
|
39
|
-
...graph,
|
|
40
|
-
screens: { ...graph.screens, 'C.Main': { route: 'C.Main', component: 'C', file: 'app/c.tsx', declaredIn: 'nav.tsx' } },
|
|
41
|
-
files: { ...graph.files, 'app/c.tsx': { imports: ['app/a.tsx'] } },
|
|
42
|
-
} as unknown as ScreenGraph;
|
|
43
|
-
const r = screensAffectedBy(g, ['app/shared.tsx']);
|
|
44
|
-
assert.deepEqual(
|
|
45
|
-
r.map((x) => [x.route, x.distance]),
|
|
46
|
-
[
|
|
47
|
-
['A.Main', 1],
|
|
48
|
-
['C.Main', 2],
|
|
49
|
-
],
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
test('an unrelated change affects nothing', () => {
|
|
53
|
-
assert.deepEqual(screensAffectedBy(graph, ['app/unrelated.tsx']), []);
|
|
54
|
-
});
|
|
55
|
-
test('a barrel reaches who imports through it only when the barrel itself changed', () => {
|
|
56
|
-
const g = {
|
|
57
|
-
...graph,
|
|
58
|
-
files: {
|
|
59
|
-
'app/a.tsx': { imports: ['app/hooks/useThing.ts'], barrels: ['app/hooks/index.ts'] },
|
|
60
|
-
'app/b.tsx': { imports: ['app/hooks/useOther.ts'], barrels: ['app/hooks/index.ts'] },
|
|
61
|
-
'app/hooks/index.ts': { imports: ['app/hooks/useThing.ts', 'app/hooks/useOther.ts'] },
|
|
62
|
-
'app/hooks/useThing.ts': { imports: [] },
|
|
63
|
-
'app/hooks/useOther.ts': { imports: [] },
|
|
64
|
-
},
|
|
65
|
-
} as unknown as ScreenGraph;
|
|
66
|
-
assert.deepEqual(
|
|
67
|
-
screensAffectedBy(g, ['app/hooks/useThing.ts']).map((x) => x.route),
|
|
68
|
-
['A.Main'],
|
|
69
|
-
);
|
|
70
|
-
assert.deepEqual(
|
|
71
|
-
screensAffectedBy(g, ['app/hooks/index.ts']).map((x) => x.route),
|
|
72
|
-
['A.Main', 'B.Main'],
|
|
73
|
-
);
|
|
74
|
-
});
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { test } from 'node:test';
|
|
6
|
-
|
|
7
|
-
import type { Screen } from 'lynkeus-protocol';
|
|
8
|
-
|
|
9
|
-
import { diffGeometry, record } from '../screens.js';
|
|
10
|
-
|
|
11
|
-
const f = (x: number, y: number, w: number, h: number) => ({ x, y, w, h });
|
|
12
|
-
|
|
13
|
-
test('drift within the tolerance is not a change', () => {
|
|
14
|
-
assert.deepEqual(diffGeometry({ a: f(10, 10, 100, 40) }, { a: f(12, 8, 102, 41) }), []);
|
|
15
|
-
});
|
|
16
|
-
test('a move past the tolerance is reported with both positions', () => {
|
|
17
|
-
assert.deepEqual(diffGeometry({ a: f(10, 10, 100, 40) }, { a: f(10, 60, 100, 40) }), ['#a moved (10,10)→(10,60)']);
|
|
18
|
-
});
|
|
19
|
-
test('a resize is reported', () => {
|
|
20
|
-
assert.deepEqual(diffGeometry({ a: f(0, 0, 100, 40) }, { a: f(0, 0, 100, 80) }), ['#a resized 100×40→100×80']);
|
|
21
|
-
});
|
|
22
|
-
test('an element that collapsed to zero is named as such', () => {
|
|
23
|
-
assert.deepEqual(diffGeometry({ a: f(0, 0, 100, 40) }, { a: f(0, 0, 100, 0) }), ['#a collapsed to 100×0 (was 100×40)']);
|
|
24
|
-
});
|
|
25
|
-
test('elements only on one side are not geometry changes', () => {
|
|
26
|
-
assert.deepEqual(diffGeometry({ a: f(0, 0, 1, 1) }, { b: f(0, 0, 1, 1) }), []);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('a recorded screen keeps what is presented, not what another tab or a covered screen holds', () => {
|
|
30
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'lynkeus-screens-'));
|
|
31
|
-
const el = (i: number, testId: string, hidden?: string) => ({ i, kind: 'button', frame: f(0, i * 50, 100, 40), enabled: true, depth: 0, testId, hidden });
|
|
32
|
-
const screen = (elements: object[]) =>
|
|
33
|
-
({ route: 'Settings.Main', path: ['Settings.Main'], commit: 1, busy: false, window: { w: 390, h: 844 }, costMs: 1, elements }) as unknown as Screen;
|
|
34
|
-
record(dir, screen([el(0, 'save'), el(1, 'other-tab', 'a11y'), el(2, 'slide', 'inert')]));
|
|
35
|
-
const change = record(dir, screen([el(0, 'save'), el(2, 'slide', 'inert')]));
|
|
36
|
-
assert.equal(change, null);
|
|
37
|
-
});
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
|
|
4
|
-
import { explain } from '../why.js';
|
|
5
|
-
import type { Element, RequestRecord, Screen, TraceEvent } from 'lynkeus-protocol';
|
|
6
|
-
|
|
7
|
-
const el = (p: Partial<Element>, i: number): Element =>
|
|
8
|
-
({ i, kind: 'button', frame: { x: 0, y: 0, w: 10, h: 10 }, enabled: true, depth: 1, tag: i + 1, ...p }) as Element;
|
|
9
|
-
const screen = (elements: Element[], route?: string): Screen => ({ route, path: route ? [route] : [], elements, commit: 5, costMs: 1 }) as unknown as Screen;
|
|
10
|
-
const req = (status: number | undefined, url: string, error?: string): RequestRecord => ({ id: 1, method: 'GET', url, status, startedAt: 0, ms: 10, error });
|
|
11
|
-
const rules = (f: ReturnType<typeof explain>) => f.map((x) => x.rule);
|
|
12
|
-
|
|
13
|
-
test('a dead navigator and a gone session are named', () => {
|
|
14
|
-
assert.deepEqual(rules(explain({ screen: screen([]), requests: [], inFlight: 0 })), ['dead-navigator']);
|
|
15
|
-
assert.ok(
|
|
16
|
-
rules(explain({ screen: screen([el({}, 0)], 'Home'), requests: [req(401, 'http://x/a'), req(401, 'http://x/b')], inFlight: 0 })).includes('session-gone'),
|
|
17
|
-
);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test('a covered target, a near text and a capped list', () => {
|
|
21
|
-
const s = screen([el({ testId: 'submit', covered: true }, 0), el({ kind: 'text', text: 'Café Ünïcode wörks here' }, 1)], 'Home');
|
|
22
|
-
assert.ok(rules(explain({ screen: s, requests: [], inFlight: 0, target: { testId: 'submit' } })).includes('covered'));
|
|
23
|
-
assert.ok(rules(explain({ screen: s, requests: [], inFlight: 0, target: { text: 'Cafe Unicode works here' } })).includes('near-match'));
|
|
24
|
-
assert.ok(rules(explain({ screen: s, requests: [], inFlight: 0, target: { text: 'cafe unicode works here' } })).includes('near-match'));
|
|
25
|
-
assert.ok(!rules(explain({ screen: s, requests: [], inFlight: 0, target: { text: 'A completely different sentence' } })).includes('near-match'));
|
|
26
|
-
const list = screen(
|
|
27
|
-
Array.from({ length: 12 }, (_, i) => el({ testId: `row-${i}-item`, text: `Item${i}` }, i)),
|
|
28
|
-
'Search',
|
|
29
|
-
);
|
|
30
|
-
assert.ok(rules(explain({ screen: list, requests: [req(200, 'http://x/search')], inFlight: 0, target: { text: 'itemabc' } })).includes('capped-list'));
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test('backend and loading conditions', () => {
|
|
34
|
-
const s = screen([el({}, 0)], 'Home');
|
|
35
|
-
assert.ok(rules(explain({ screen: s, requests: [req(500, 'http://x/api/a')], inFlight: 0 })).includes('backend-error'));
|
|
36
|
-
assert.ok(rules(explain({ screen: s, requests: [req(200, 'http://x/a')], inFlight: 2, target: { testId: 'x' } })).includes('in-flight'));
|
|
37
|
-
const stall: TraceEvent = { seq: 1, t: 1, kind: 'stall', renders: 2000, windowMs: 3000, component: 'Feed' };
|
|
38
|
-
assert.ok(rules(explain({ screen: s, requests: [], inFlight: 0, trace: [stall] })).includes('render-loop'));
|
|
39
|
-
assert.deepEqual(rules(explain({ screen: s, requests: [req(200, 'http://x/a')], inFlight: 0 })), []);
|
|
40
|
-
});
|