@hover-dev/core 0.26.0 → 0.27.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/dist/cloud.d.ts +9 -0
- package/dist/cloud.d.ts.map +1 -1
- package/dist/cloud.js +9 -0
- package/dist/dashboard.d.ts +100 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +133 -0
- package/package.json +5 -1
package/dist/cloud.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RunFailure } from './specs/runFailures.js';
|
|
2
|
+
import type { DashboardData } from './dashboard.js';
|
|
2
3
|
export declare const DEFAULT_CLOUD_URL = "https://cloud.gethover.dev";
|
|
3
4
|
export interface CloudCredentials {
|
|
4
5
|
/** Personal access token minted at cloud Settings → Access tokens. */
|
|
@@ -24,6 +25,9 @@ export interface CloudHealRequest {
|
|
|
24
25
|
branch: string | null;
|
|
25
26
|
commitSha: string | null;
|
|
26
27
|
ciUrl: string | null;
|
|
28
|
+
/** Environment the spec drifted on — heal against THIS env's URL (matched
|
|
29
|
+
* to `.hover/environments.json`), not localhost. Null on older runs. */
|
|
30
|
+
environment?: string | null;
|
|
27
31
|
createdAt: string;
|
|
28
32
|
};
|
|
29
33
|
}
|
|
@@ -50,6 +54,11 @@ export declare function fetchHealRequests(creds: CloudCredentials, opts?: {
|
|
|
50
54
|
/** Mark a request routed (a local heal picked it up) or dismissed / reopened.
|
|
51
55
|
* `healed` is not writable — only CI seeing the spec pass closes a request. */
|
|
52
56
|
export declare function updateHealRequest(creds: CloudCredentials, id: string, status: 'routed' | 'dismissed' | 'open', fetchImpl?: typeof fetch): Promise<void>;
|
|
57
|
+
/** One project's dashboard, computed cloud-side from ingested CI runs — the
|
|
58
|
+
* same `DashboardData` shape the local gatherer builds from `.hover/runs`, so
|
|
59
|
+
* a dashboard surface can swap data sources without a UI change. `repo` is the
|
|
60
|
+
* GitHub `owner/name` the project was created with. */
|
|
61
|
+
export declare function fetchDashboard(creds: CloudCredentials, repo: string, fetchImpl?: typeof fetch): Promise<DashboardData>;
|
|
53
62
|
/** The heal slug for a queued request (`checkout.spec.ts` → `checkout`) — what
|
|
54
63
|
* `/mcp__hover__heal <slug>` takes. */
|
|
55
64
|
export declare function healSlug(specFile: string): string;
|
package/dist/cloud.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../src/cloud.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../src/cloud.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,eAAO,MAAM,iBAAiB,+BAA+B,CAAC;AAE9D,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,GAAG,EAAE,MAAM,CAAC;CACb;AAED;6EAC6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,CAAC;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACpD,GAAG,EAAE;QACH,EAAE,EAAE,MAAM,CAAC;QACX,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB;iFACyE;QACzE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,wBAAgB,eAAe,CAAC,IAAI,GAAE,MAAkB,GAAG,MAAM,CAEhE;AAED;2EAC2E;AAC3E,wBAAgB,oBAAoB,CAClC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,IAAI,CAAC,EAAE,MAAM,GACZ,gBAAgB,GAAG,IAAI,CAczB;AAED;+EAC+E;AAC/E,wBAAgB,qBAAqB,CACnC,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACtC,IAAI,GAAE,MAAkB,GACvB,MAAM,CAOR;AAED,gFAAgF;AAChF,qBAAa,aAAc,SAAQ,KAAK;IAE7B,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM;CAIlB;AAuBD,uEAAuE;AACvE,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,gBAAgB,EACvB,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,KAAK,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,EACzF,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAY7B;AAED;gFACgF;AAChF,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,gBAAgB,EACvB,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,QAAQ,GAAG,WAAW,GAAG,MAAM,EACvC,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,IAAI,CAAC,CAOf;AAED;;;wDAGwD;AACxD,wBAAsB,cAAc,CAClC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,aAAa,CAAC,CASxB;AAED;wCACwC;AACxC,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGjD"}
|
package/dist/cloud.js
CHANGED
|
@@ -85,6 +85,15 @@ export async function fetchHealRequests(creds, opts = {}, fetchImpl = fetch) {
|
|
|
85
85
|
export async function updateHealRequest(creds, id, status, fetchImpl = fetch) {
|
|
86
86
|
await cloudJson(creds, `/api/v1/heal-requests/${id}`, { method: 'PATCH', body: JSON.stringify({ status }) }, fetchImpl);
|
|
87
87
|
}
|
|
88
|
+
/** One project's dashboard, computed cloud-side from ingested CI runs — the
|
|
89
|
+
* same `DashboardData` shape the local gatherer builds from `.hover/runs`, so
|
|
90
|
+
* a dashboard surface can swap data sources without a UI change. `repo` is the
|
|
91
|
+
* GitHub `owner/name` the project was created with. */
|
|
92
|
+
export async function fetchDashboard(creds, repo, fetchImpl = fetch) {
|
|
93
|
+
const qs = `?${new URLSearchParams({ repo })}`;
|
|
94
|
+
const data = await cloudJson(creds, `/api/v1/dashboard${qs}`, {}, fetchImpl);
|
|
95
|
+
return data.dashboard;
|
|
96
|
+
}
|
|
88
97
|
/** The heal slug for a queued request (`checkout.spec.ts` → `checkout`) — what
|
|
89
98
|
* `/mcp__hover__heal <slug>` takes. */
|
|
90
99
|
export function healSlug(specFile) {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard data contract (v1) + the pure helpers that compute it.
|
|
3
|
+
*
|
|
4
|
+
* ONE shape feeds every dashboard surface: the VS Code webview panel builds it
|
|
5
|
+
* from local `.hover/runs/*.json` (see vscode-ext/src/dashboardView.ts) and
|
|
6
|
+
* Hover Cloud's `GET /api/v1/dashboard` emits the same shape from ingested CI
|
|
7
|
+
* runs — so the panel can swap data sources without a UI change. The cloud
|
|
8
|
+
* route (Hover-cloud repo, apps/dashboard/app/api/v1/dashboard/route.ts)
|
|
9
|
+
* mirrors this contract; change it here first, then there.
|
|
10
|
+
*
|
|
11
|
+
* Everything in this module is pure (no vscode, no fs) so the extension, the
|
|
12
|
+
* MCP server, and tests can all use it.
|
|
13
|
+
*/
|
|
14
|
+
export type Status = 'pass' | 'fail' | 'flaky';
|
|
15
|
+
/** How many recent runs a dashboard shows (the run-history strip width). */
|
|
16
|
+
export declare const MAX_RUNS = 14;
|
|
17
|
+
export interface SpecRow {
|
|
18
|
+
/** Spec basename, e.g. `checkout.spec.ts` — the row identity. */
|
|
19
|
+
name: string;
|
|
20
|
+
/** Absolute local fsPath for open/run actions. Local source only; cloud → null. */
|
|
21
|
+
path: string | null;
|
|
22
|
+
/** Repo-relative spec file path. Cloud source only; local → omitted. */
|
|
23
|
+
specFile?: string;
|
|
24
|
+
/** Folder group under `__vibe_tests__/` ('' = top level). */
|
|
25
|
+
group: string;
|
|
26
|
+
security: boolean;
|
|
27
|
+
/** One cell per entry in `runs` (null = spec not part of that run). */
|
|
28
|
+
cells: (Status | null)[];
|
|
29
|
+
/** Inconsistent across the window — a candidate for 🏥 Heal. */
|
|
30
|
+
flaky: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface DashboardRun {
|
|
33
|
+
id: string;
|
|
34
|
+
/** ISO-ish timestamp label (local: the run filename stamp; cloud: createdAt). */
|
|
35
|
+
ts: string;
|
|
36
|
+
/** Deep link back to the user's CI. Cloud source only. */
|
|
37
|
+
ciUrl?: string | null;
|
|
38
|
+
branch?: string | null;
|
|
39
|
+
/** Which environment the run targeted (`local` / `staging` / `prod` / `ci`) —
|
|
40
|
+
* the Environments-view id the reporter tagged the run with. Cloud source only. */
|
|
41
|
+
environment?: string | null;
|
|
42
|
+
}
|
|
43
|
+
export interface DashboardData {
|
|
44
|
+
hasRuns: boolean;
|
|
45
|
+
tiles: {
|
|
46
|
+
specs: number;
|
|
47
|
+
/** Pass rate (%) of the latest run; null when there are no runs. */
|
|
48
|
+
passRate: number | null;
|
|
49
|
+
flaky: number;
|
|
50
|
+
/** 7-day agent token spend. Local source only; cloud → null. */
|
|
51
|
+
tokens7d: number | null;
|
|
52
|
+
};
|
|
53
|
+
runs: DashboardRun[];
|
|
54
|
+
rows: SpecRow[];
|
|
55
|
+
}
|
|
56
|
+
/** One run as a computable unit: its column header + per-spec statuses. The
|
|
57
|
+
* local gatherer parses these from `.hover/runs/*.json`; `dashboardRunSlices`
|
|
58
|
+
* recovers them from a cloud `DashboardData`. */
|
|
59
|
+
export interface RunSlice extends DashboardRun {
|
|
60
|
+
specs: Record<string, Status>;
|
|
61
|
+
}
|
|
62
|
+
/** Where a spec row's file lives — local absolute path and/or repo-relative. */
|
|
63
|
+
export interface SpecFileRef {
|
|
64
|
+
path: string | null;
|
|
65
|
+
specFile?: string;
|
|
66
|
+
}
|
|
67
|
+
/** Worst-wins ranking when a single run file reports several specs in one file. */
|
|
68
|
+
export declare function worse(a: Status | undefined, b: Status): Status;
|
|
69
|
+
/** Flaky = a spec that both passed and failed across the window (or a run marked
|
|
70
|
+
* it flaky on retry). Shared by the per-row flag and the aggregate tile. */
|
|
71
|
+
export declare function cellsFlaky(cells: (Status | null)[]): boolean;
|
|
72
|
+
/** Security specs get a shield in every surface — one naming rule. */
|
|
73
|
+
export declare function isSecuritySpec(name: string): boolean;
|
|
74
|
+
/** Folder segments between the nearest `__vibe_tests__` and the file (excl. the
|
|
75
|
+
* filename), joined by '/'. `__vibe_tests__/auth/login.spec.ts` → 'auth'.
|
|
76
|
+
* Works on both absolute fsPaths and repo-relative paths. */
|
|
77
|
+
export declare function specGroup(path: string): string;
|
|
78
|
+
/** Recover per-run slices from a `DashboardData` (rows/cells → specs maps) —
|
|
79
|
+
* how a cloud dashboard becomes mergeable with locally-gathered runs. */
|
|
80
|
+
export declare function dashboardRunSlices(data: DashboardData): RunSlice[];
|
|
81
|
+
/** The GitHub Actions run id inside a CI deep link, or null. Matches the
|
|
82
|
+
* `ci-<id>.json` ledger naming the extension's CI sync uses — the dedup key
|
|
83
|
+
* between a locally-synced CI run and the same run ingested by the cloud. */
|
|
84
|
+
export declare function actionsRunId(ciUrl: string | null | undefined): string | null;
|
|
85
|
+
/** Merge locally-gathered runs with cloud-ingested ones into one chronological
|
|
86
|
+
* timeline, newest-last, capped at MAX_RUNS. A CI run present on both sides
|
|
87
|
+
* (a `ci-<id>` local ledger vs the cloud run's `ciUrl`) keeps the CLOUD copy —
|
|
88
|
+
* it carries the real timestamp + deep link. Unparseable timestamps sort
|
|
89
|
+
* oldest, preserving their relative order (Array.sort is stable). */
|
|
90
|
+
export declare function mergeRunSlices(local: RunSlice[], cloud: RunSlice[]): RunSlice[];
|
|
91
|
+
/** Assemble the final `DashboardData` from run slices (chronological) + the
|
|
92
|
+
* known spec files. `files` seeds the rows (a catalogue spec with no runs yet
|
|
93
|
+
* still shows) and supplies open/run paths; specs seen only in run history get
|
|
94
|
+
* a path-less row. `specCount` overrides the specs tile when the caller has a
|
|
95
|
+
* better catalogue count than `files.size`. */
|
|
96
|
+
export declare function buildDashboard(runs: RunSlice[], files: Map<string, SpecFileRef>, tokens7d: number | null, specCount?: number): DashboardData;
|
|
97
|
+
/** Parse a Playwright json report into { specBasename → status }. Defensive:
|
|
98
|
+
* an unexpected shape just yields no entries. */
|
|
99
|
+
export declare function parsePlaywrightRun(json: unknown): Record<string, Status>;
|
|
100
|
+
//# sourceMappingURL=dashboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard data contract (v1) + the pure helpers that compute it.
|
|
3
|
+
*
|
|
4
|
+
* ONE shape feeds every dashboard surface: the VS Code webview panel builds it
|
|
5
|
+
* from local `.hover/runs/*.json` (see vscode-ext/src/dashboardView.ts) and
|
|
6
|
+
* Hover Cloud's `GET /api/v1/dashboard` emits the same shape from ingested CI
|
|
7
|
+
* runs — so the panel can swap data sources without a UI change. The cloud
|
|
8
|
+
* route (Hover-cloud repo, apps/dashboard/app/api/v1/dashboard/route.ts)
|
|
9
|
+
* mirrors this contract; change it here first, then there.
|
|
10
|
+
*
|
|
11
|
+
* Everything in this module is pure (no vscode, no fs) so the extension, the
|
|
12
|
+
* MCP server, and tests can all use it.
|
|
13
|
+
*/
|
|
14
|
+
/** How many recent runs a dashboard shows (the run-history strip width). */
|
|
15
|
+
export const MAX_RUNS = 14;
|
|
16
|
+
/** Worst-wins ranking when a single run file reports several specs in one file. */
|
|
17
|
+
export function worse(a, b) {
|
|
18
|
+
const rank = { pass: 0, flaky: 1, fail: 2 };
|
|
19
|
+
if (!a)
|
|
20
|
+
return b;
|
|
21
|
+
return rank[b] > rank[a] ? b : a;
|
|
22
|
+
}
|
|
23
|
+
/** Flaky = a spec that both passed and failed across the window (or a run marked
|
|
24
|
+
* it flaky on retry). Shared by the per-row flag and the aggregate tile. */
|
|
25
|
+
export function cellsFlaky(cells) {
|
|
26
|
+
const seen = new Set(cells.filter(Boolean));
|
|
27
|
+
return seen.has('flaky') || (seen.has('pass') && seen.has('fail'));
|
|
28
|
+
}
|
|
29
|
+
/** Security specs get a shield in every surface — one naming rule. */
|
|
30
|
+
export function isSecuritySpec(name) {
|
|
31
|
+
return name.endsWith('.api-test.spec.ts');
|
|
32
|
+
}
|
|
33
|
+
/** Folder segments between the nearest `__vibe_tests__` and the file (excl. the
|
|
34
|
+
* filename), joined by '/'. `__vibe_tests__/auth/login.spec.ts` → 'auth'.
|
|
35
|
+
* Works on both absolute fsPaths and repo-relative paths. */
|
|
36
|
+
export function specGroup(path) {
|
|
37
|
+
const parts = path.split(/[\\/]/);
|
|
38
|
+
const idx = parts.lastIndexOf('__vibe_tests__');
|
|
39
|
+
return idx >= 0 ? parts.slice(idx + 1, -1).join('/') : '';
|
|
40
|
+
}
|
|
41
|
+
/** Recover per-run slices from a `DashboardData` (rows/cells → specs maps) —
|
|
42
|
+
* how a cloud dashboard becomes mergeable with locally-gathered runs. */
|
|
43
|
+
export function dashboardRunSlices(data) {
|
|
44
|
+
return data.runs.map((run, i) => {
|
|
45
|
+
const specs = {};
|
|
46
|
+
for (const row of data.rows) {
|
|
47
|
+
const cell = row.cells[i];
|
|
48
|
+
if (cell)
|
|
49
|
+
specs[row.name] = cell;
|
|
50
|
+
}
|
|
51
|
+
return { ...run, specs };
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** The GitHub Actions run id inside a CI deep link, or null. Matches the
|
|
55
|
+
* `ci-<id>.json` ledger naming the extension's CI sync uses — the dedup key
|
|
56
|
+
* between a locally-synced CI run and the same run ingested by the cloud. */
|
|
57
|
+
export function actionsRunId(ciUrl) {
|
|
58
|
+
return ciUrl?.match(/\/actions\/runs\/(\d+)/)?.[1] ?? null;
|
|
59
|
+
}
|
|
60
|
+
/** Merge locally-gathered runs with cloud-ingested ones into one chronological
|
|
61
|
+
* timeline, newest-last, capped at MAX_RUNS. A CI run present on both sides
|
|
62
|
+
* (a `ci-<id>` local ledger vs the cloud run's `ciUrl`) keeps the CLOUD copy —
|
|
63
|
+
* it carries the real timestamp + deep link. Unparseable timestamps sort
|
|
64
|
+
* oldest, preserving their relative order (Array.sort is stable). */
|
|
65
|
+
export function mergeRunSlices(local, cloud) {
|
|
66
|
+
const cloudCiIds = new Set(cloud.map((r) => actionsRunId(r.ciUrl)).filter(Boolean));
|
|
67
|
+
const keptLocal = local.filter((r) => !cloudCiIds.has(r.id.replace(/^ci-/, '')));
|
|
68
|
+
const key = (r) => {
|
|
69
|
+
const t = Date.parse(r.ts);
|
|
70
|
+
return Number.isNaN(t) ? 0 : t;
|
|
71
|
+
};
|
|
72
|
+
return [...keptLocal, ...cloud].sort((a, b) => key(a) - key(b)).slice(-MAX_RUNS);
|
|
73
|
+
}
|
|
74
|
+
/** Assemble the final `DashboardData` from run slices (chronological) + the
|
|
75
|
+
* known spec files. `files` seeds the rows (a catalogue spec with no runs yet
|
|
76
|
+
* still shows) and supplies open/run paths; specs seen only in run history get
|
|
77
|
+
* a path-less row. `specCount` overrides the specs tile when the caller has a
|
|
78
|
+
* better catalogue count than `files.size`. */
|
|
79
|
+
export function buildDashboard(runs, files, tokens7d, specCount = files.size) {
|
|
80
|
+
const names = new Set(files.keys());
|
|
81
|
+
for (const r of runs)
|
|
82
|
+
for (const k of Object.keys(r.specs))
|
|
83
|
+
names.add(k);
|
|
84
|
+
const rows = [...names]
|
|
85
|
+
.sort((a, b) => a.localeCompare(b))
|
|
86
|
+
.map((name) => {
|
|
87
|
+
const ref = files.get(name);
|
|
88
|
+
const cells = runs.map((r) => r.specs[name] ?? null);
|
|
89
|
+
return {
|
|
90
|
+
name,
|
|
91
|
+
path: ref?.path ?? null,
|
|
92
|
+
...(ref?.specFile ? { specFile: ref.specFile } : {}),
|
|
93
|
+
group: specGroup(ref?.path ?? ref?.specFile ?? ''),
|
|
94
|
+
security: isSecuritySpec(name),
|
|
95
|
+
cells,
|
|
96
|
+
flaky: cellsFlaky(cells),
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
let passRate = null;
|
|
100
|
+
const last = runs[runs.length - 1];
|
|
101
|
+
if (last) {
|
|
102
|
+
const vals = Object.values(last.specs);
|
|
103
|
+
if (vals.length)
|
|
104
|
+
passRate = Math.round((vals.filter((s) => s === 'pass').length / vals.length) * 100);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
hasRuns: runs.length > 0,
|
|
108
|
+
tiles: { specs: specCount, passRate, flaky: rows.filter((r) => r.flaky).length, tokens7d },
|
|
109
|
+
runs: runs.map(({ specs: _specs, ...run }) => run),
|
|
110
|
+
rows,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** Parse a Playwright json report into { specBasename → status }. Defensive:
|
|
114
|
+
* an unexpected shape just yields no entries. */
|
|
115
|
+
export function parsePlaywrightRun(json) {
|
|
116
|
+
const out = {};
|
|
117
|
+
const visit = (suite, inherited) => {
|
|
118
|
+
const file = suite.file ?? inherited;
|
|
119
|
+
for (const raw of suite.specs ?? []) {
|
|
120
|
+
const spec = raw;
|
|
121
|
+
const key = (file ?? spec.file ?? 'unknown').split(/[\\/]/).pop() ?? 'unknown';
|
|
122
|
+
let status = spec.ok ? 'pass' : 'fail';
|
|
123
|
+
if (spec.ok && (spec.tests ?? []).some((t) => t.status === 'flaky'))
|
|
124
|
+
status = 'flaky';
|
|
125
|
+
out[key] = worse(out[key], status);
|
|
126
|
+
}
|
|
127
|
+
for (const child of suite.suites ?? [])
|
|
128
|
+
visit(child, file);
|
|
129
|
+
};
|
|
130
|
+
for (const s of json?.suites ?? [])
|
|
131
|
+
visit(s);
|
|
132
|
+
return out;
|
|
133
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hover-dev/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Hover's local Node service: agent invocation, Playwright CDP preflight, WebSocket bridge.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Hyperyond",
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
"./cloud": {
|
|
39
39
|
"types": "./dist/cloud.d.ts",
|
|
40
40
|
"import": "./dist/cloud.js"
|
|
41
|
+
},
|
|
42
|
+
"./dashboard": {
|
|
43
|
+
"types": "./dist/dashboard.d.ts",
|
|
44
|
+
"import": "./dist/dashboard.js"
|
|
41
45
|
}
|
|
42
46
|
},
|
|
43
47
|
"files": [
|