@hover-dev/core 0.15.0 → 0.17.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/README.md +26 -55
- package/dist/agentDirectives.d.ts +55 -0
- package/dist/agentDirectives.d.ts.map +1 -0
- package/dist/agentDirectives.js +276 -0
- package/dist/agents/claude.d.ts.map +1 -1
- package/dist/agents/claude.js +28 -3
- package/dist/agents/codex.d.ts.map +1 -1
- package/dist/agents/codex.js +38 -18
- package/dist/agents/gemini.d.ts.map +1 -1
- package/dist/agents/gemini.js +3 -14
- package/dist/agents/invoke.d.ts.map +1 -1
- package/dist/agents/invoke.js +3 -6
- package/dist/agents/qwen.d.ts.map +1 -1
- package/dist/agents/qwen.js +3 -14
- package/dist/agents/registry.d.ts.map +1 -1
- package/dist/agents/registry.js +0 -4
- package/dist/agents/shared.d.ts +28 -0
- package/dist/agents/shared.d.ts.map +1 -0
- package/dist/agents/shared.js +35 -0
- package/dist/agents/types.d.ts +19 -11
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/engine.d.ts +53 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +78 -0
- package/dist/mcp/actuateServer.d.ts +3 -0
- package/dist/mcp/actuateServer.d.ts.map +1 -0
- package/dist/mcp/actuateServer.js +594 -0
- package/dist/mcp/sourceFence.d.ts +23 -0
- package/dist/mcp/sourceFence.d.ts.map +1 -0
- package/dist/mcp/sourceFence.js +79 -0
- package/dist/mcp/sourceServer.d.ts +3 -0
- package/dist/mcp/sourceServer.d.ts.map +1 -0
- package/dist/mcp/sourceServer.js +191 -0
- package/dist/memory/businessMemory.d.ts +29 -0
- package/dist/memory/businessMemory.d.ts.map +1 -0
- package/dist/memory/businessMemory.js +125 -0
- package/dist/modes.d.ts +39 -0
- package/dist/modes.d.ts.map +1 -0
- package/dist/modes.js +34 -0
- package/dist/playwright/cdpStatus.d.ts +0 -15
- package/dist/playwright/cdpStatus.d.ts.map +1 -1
- package/dist/playwright/cdpStatus.js +0 -67
- package/dist/playwright/launchChrome.d.ts +18 -0
- package/dist/playwright/launchChrome.d.ts.map +1 -1
- package/dist/playwright/launchChrome.js +46 -3
- package/dist/playwright/preflight.d.ts.map +1 -1
- package/dist/playwright/preflight.js +6 -1
- package/dist/playwright/resolveMcpConfig.d.ts +12 -0
- package/dist/playwright/resolveMcpConfig.d.ts.map +1 -1
- package/dist/playwright/resolveMcpConfig.js +36 -5
- package/dist/plugin-api.d.ts +35 -26
- package/dist/plugin-api.d.ts.map +1 -1
- package/dist/plugin-api.js +2 -2
- package/dist/qa/candidates.d.ts +32 -0
- package/dist/qa/candidates.d.ts.map +1 -0
- package/dist/qa/candidates.js +20 -0
- package/dist/qa/classify.d.ts +38 -0
- package/dist/qa/classify.d.ts.map +1 -0
- package/dist/qa/classify.js +138 -0
- package/dist/qa/intensity.d.ts +33 -0
- package/dist/qa/intensity.d.ts.map +1 -0
- package/dist/qa/intensity.js +25 -0
- package/dist/qa/qaReport.d.ts +19 -0
- package/dist/qa/qaReport.d.ts.map +1 -0
- package/dist/qa/qaReport.js +50 -0
- package/dist/runSession.d.ts +14 -3
- package/dist/runSession.d.ts.map +1 -1
- package/dist/runSession.js +31 -11
- package/dist/service/cdpHandlers.d.ts +3 -27
- package/dist/service/cdpHandlers.d.ts.map +1 -1
- package/dist/service/cdpHandlers.js +6 -53
- package/dist/service/cdpHint.d.ts +21 -28
- package/dist/service/cdpHint.d.ts.map +1 -1
- package/dist/service/cdpHint.js +106 -164
- package/dist/service/relayHandlers.d.ts +28 -0
- package/dist/service/relayHandlers.d.ts.map +1 -0
- package/dist/service/relayHandlers.js +105 -0
- package/dist/service/saveHandlers.d.ts +1 -3
- package/dist/service/saveHandlers.d.ts.map +1 -1
- package/dist/service/saveHandlers.js +17 -15
- package/dist/service/types.d.ts +108 -8
- package/dist/service/types.d.ts.map +1 -1
- package/dist/service.d.ts +13 -3
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +1022 -236
- package/dist/sessions/sessions.d.ts +125 -0
- package/dist/sessions/sessions.d.ts.map +1 -0
- package/dist/sessions/sessions.js +175 -0
- package/dist/specs/authFixture.d.ts +30 -0
- package/dist/specs/authFixture.d.ts.map +1 -0
- package/dist/specs/authFixture.js +145 -0
- package/dist/specs/businessMap.d.ts +29 -0
- package/dist/specs/businessMap.d.ts.map +1 -0
- package/dist/specs/businessMap.js +95 -0
- package/dist/specs/detectSharedFlows.d.ts +1 -1
- package/dist/specs/detectSharedFlows.d.ts.map +1 -1
- package/dist/specs/detectSharedFlows.js +20 -21
- package/dist/specs/generatePageObject.d.ts +1 -1
- package/dist/specs/generatePageObject.d.ts.map +1 -1
- package/dist/specs/healPrompt.d.ts +19 -0
- package/dist/specs/healPrompt.d.ts.map +1 -0
- package/dist/specs/healPrompt.js +48 -0
- package/dist/specs/humanSteps.d.ts +4 -8
- package/dist/specs/humanSteps.d.ts.map +1 -1
- package/dist/specs/humanSteps.js +6 -1
- package/dist/specs/optimizeSpec.d.ts +15 -8
- package/dist/specs/optimizeSpec.d.ts.map +1 -1
- package/dist/specs/optimizeSpec.js +98 -46
- package/dist/specs/optimizeSpecWithAgent.d.ts +0 -2
- package/dist/specs/optimizeSpecWithAgent.d.ts.map +1 -1
- package/dist/specs/optimizeSpecWithAgent.js +0 -1
- package/dist/specs/pageObjectManifest.d.ts +3 -1
- package/dist/specs/pageObjectManifest.d.ts.map +1 -1
- package/dist/specs/pageObjectManifest.js +13 -9
- package/dist/specs/replayGrounded.d.ts +45 -0
- package/dist/specs/replayGrounded.d.ts.map +1 -0
- package/dist/specs/replayGrounded.js +155 -0
- package/dist/specs/runFailures.d.ts +34 -0
- package/dist/specs/runFailures.d.ts.map +1 -0
- package/dist/specs/runFailures.js +93 -0
- package/dist/specs/seeds.d.ts +16 -15
- package/dist/specs/seeds.d.ts.map +1 -1
- package/dist/specs/seeds.js +86 -54
- package/dist/specs/sidecar.d.ts +34 -6
- package/dist/specs/sidecar.d.ts.map +1 -1
- package/dist/specs/sidecar.js +79 -9
- package/dist/specs/softBatch.d.ts +14 -0
- package/dist/specs/softBatch.d.ts.map +1 -0
- package/dist/specs/softBatch.js +177 -0
- package/dist/specs/specStep.d.ts +21 -0
- package/dist/specs/specStep.d.ts.map +1 -0
- package/dist/specs/specStep.js +1 -0
- package/dist/specs/text.d.ts +19 -0
- package/dist/specs/text.d.ts.map +1 -0
- package/dist/specs/text.js +27 -0
- package/dist/specs/writeSpec.d.ts +62 -1
- package/dist/specs/writeSpec.d.ts.map +1 -1
- package/dist/specs/writeSpec.js +598 -30
- package/package.json +10 -10
- package/dist/agents/aider.d.ts +0 -16
- package/dist/agents/aider.d.ts.map +0 -1
- package/dist/agents/aider.js +0 -169
- package/dist/agents/cursor.d.ts +0 -18
- package/dist/agents/cursor.d.ts.map +0 -1
- package/dist/agents/cursor.js +0 -229
- package/dist/playwright/raiseWindow.d.ts +0 -10
- package/dist/playwright/raiseWindow.d.ts.map +0 -1
- package/dist/playwright/raiseWindow.js +0 -139
- package/dist/scripts/bench-multi-tab.d.ts +0 -2
- package/dist/scripts/bench-multi-tab.d.ts.map +0 -1
- package/dist/scripts/bench-multi-tab.js +0 -192
- package/dist/scripts/bench-ttfb.d.ts +0 -2
- package/dist/scripts/bench-ttfb.d.ts.map +0 -1
- package/dist/scripts/bench-ttfb.js +0 -127
- package/dist/scripts/start-chrome.d.ts +0 -3
- package/dist/scripts/start-chrome.d.ts.map +0 -1
- package/dist/scripts/start-chrome.js +0 -23
- package/dist/skills/writeSkill.d.ts +0 -27
- package/dist/skills/writeSkill.d.ts.map +0 -1
- package/dist/skills/writeSkill.js +0 -13
- package/dist/specs/listSpecs.d.ts +0 -52
- package/dist/specs/listSpecs.d.ts.map +0 -1
- package/dist/specs/listSpecs.js +0 -139
- package/dist/specs/optimizationSuggestion.d.ts +0 -26
- package/dist/specs/optimizationSuggestion.d.ts.map +0 -1
- package/dist/specs/optimizationSuggestion.js +0 -28
- package/dist/specs/writeCaseCsv.d.ts +0 -28
- package/dist/specs/writeCaseCsv.d.ts.map +0 -1
- package/dist/specs/writeCaseCsv.js +0 -140
package/dist/specs/sidecar.js
CHANGED
|
@@ -1,29 +1,62 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Structured-session sidecar for a generated spec.
|
|
3
3
|
*
|
|
4
|
-
* Every saved spec gets a companion
|
|
4
|
+
* Every saved spec gets a companion `.hover/sidecars/<slug>.json` holding
|
|
5
5
|
* the original structured `SpecStep[]` (plus assertions + metadata). The
|
|
6
6
|
* `.spec.ts` is the human / CI artifact; this sidecar is the machine record
|
|
7
7
|
* that downstream work reads instead of parsing generated code:
|
|
8
8
|
* - F4 cross-session extraction signature-matches `steps` across sidecars.
|
|
9
9
|
* - F7 optimization pass feeds the draft + this sidecar to the LLM.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Home is the project-root `.hover/` directory (the same home as
|
|
12
|
+
* `.hover/conventions.md`) — Hover-derived data lives outside
|
|
13
|
+
* `__vibe_tests__/`, which stays 100% user-owned Playwright code. Sidecars
|
|
14
|
+
* historically lived nested at `__vibe_tests__/.hover/<slug>.json`; readers
|
|
15
|
+
* fall back to that legacy path and lazily copy-forward, so pre-existing
|
|
16
|
+
* projects keep working without a migration step.
|
|
13
17
|
*/
|
|
14
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
18
|
+
import { mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
15
19
|
import { join } from 'node:path';
|
|
16
20
|
/** Current sidecar schema version. Bump when the shape changes so readers
|
|
17
21
|
* (Stage 2 detection, Stage 7 optimization) can migrate or skip cleanly. */
|
|
18
22
|
export const SIDECAR_VERSION = 1;
|
|
19
|
-
/**
|
|
20
|
-
*
|
|
23
|
+
/** Project-root `.hover/` directory — the single home for Hover-derived data
|
|
24
|
+
* (sidecars, runs, rules, conventions). */
|
|
25
|
+
export function hoverDir(devRoot) {
|
|
26
|
+
return join(devRoot, '.hover');
|
|
27
|
+
}
|
|
28
|
+
/** Sanitize an id segment for use as a directory name (conversation / run id). */
|
|
29
|
+
export function safeSeg(s) {
|
|
30
|
+
return (s || '').replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'unknown';
|
|
31
|
+
}
|
|
32
|
+
/** Per-run home: `<devRoot>/.hover/conversations/<conversationId>/<runId>/`.
|
|
33
|
+
* Everything a single agent run produces — meta.json (the ledger record),
|
|
34
|
+
* report.md (QA), screenshots/ — lives here, grouped under its conversation so
|
|
35
|
+
* deleting a conversation is one `rm -rf .hover/conversations/<conversationId>`.
|
|
36
|
+
* (Distinct from `.hover/runs/`, which is the Playwright spec-run-results
|
|
37
|
+
* ledger written by ▶ Run.) */
|
|
38
|
+
export function conversationsDir(devRoot) {
|
|
39
|
+
return join(hoverDir(devRoot), 'conversations');
|
|
40
|
+
}
|
|
41
|
+
export function conversationDir(devRoot, conversationId) {
|
|
42
|
+
return join(conversationsDir(devRoot), safeSeg(conversationId));
|
|
43
|
+
}
|
|
44
|
+
export function runDir(devRoot, conversationId, runId) {
|
|
45
|
+
return join(conversationDir(devRoot, conversationId), safeSeg(runId));
|
|
46
|
+
}
|
|
47
|
+
/** Sidecar directory: `<devRoot>/.hover/sidecars`. Outside `__vibe_tests__/`,
|
|
48
|
+
* so Playwright's default `*.spec.ts` glob trivially never reaches it. */
|
|
21
49
|
export function sidecarDir(devRoot) {
|
|
50
|
+
return join(hoverDir(devRoot), 'sidecars');
|
|
51
|
+
}
|
|
52
|
+
/** Pre-relocation sidecar home (`__vibe_tests__/.hover/`). Read-only fallback;
|
|
53
|
+
* nothing writes here anymore. */
|
|
54
|
+
export function legacySidecarDir(devRoot) {
|
|
22
55
|
return join(devRoot, '__vibe_tests__', '.hover');
|
|
23
56
|
}
|
|
24
|
-
/** Write the structured-session sidecar at `.hover/<slug>.json`.
|
|
25
|
-
* the data minus the stamped fields (`version`, `createdAt`),
|
|
26
|
-
* function fills. Returns the absolute path written. */
|
|
57
|
+
/** Write the structured-session sidecar at `.hover/sidecars/<slug>.json`.
|
|
58
|
+
* Caller passes the data minus the stamped fields (`version`, `createdAt`),
|
|
59
|
+
* which this function fills. Returns the absolute path written. */
|
|
27
60
|
export async function writeSidecar(devRoot, data) {
|
|
28
61
|
const dir = sidecarDir(devRoot);
|
|
29
62
|
await mkdir(dir, { recursive: true });
|
|
@@ -36,3 +69,40 @@ export async function writeSidecar(devRoot, data) {
|
|
|
36
69
|
await writeFile(path, JSON.stringify(sidecar, null, 2) + '\n', 'utf-8');
|
|
37
70
|
return path;
|
|
38
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Read one sidecar by slug, with legacy fallback + lazy copy-forward: when a
|
|
74
|
+
* sidecar only exists at the pre-relocation `__vibe_tests__/.hover/` path it
|
|
75
|
+
* is parsed from there and best-effort re-written into `.hover/sidecars/` so
|
|
76
|
+
* the next read hits the new home. Returns `null` when absent or malformed.
|
|
77
|
+
*/
|
|
78
|
+
export async function readSidecar(devRoot, slug) {
|
|
79
|
+
const current = await parseSidecarFile(join(sidecarDir(devRoot), `${slug}.json`));
|
|
80
|
+
if (current)
|
|
81
|
+
return current;
|
|
82
|
+
const legacy = await parseSidecarFile(join(legacySidecarDir(devRoot), `${slug}.json`));
|
|
83
|
+
if (legacy) {
|
|
84
|
+
// Copy-forward, best effort — a read must never fail because the
|
|
85
|
+
// migration write did.
|
|
86
|
+
try {
|
|
87
|
+
const dir = sidecarDir(devRoot);
|
|
88
|
+
await mkdir(dir, { recursive: true });
|
|
89
|
+
await writeFile(join(dir, `${slug}.json`), JSON.stringify(legacy, null, 2) + '\n', 'utf-8');
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
/* leave it in the legacy home */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return legacy;
|
|
96
|
+
}
|
|
97
|
+
/** Parse one sidecar file, or `null` when absent / not JSON. Deliberately
|
|
98
|
+
* lenient on shape (an empty `{}` still counts as "a sidecar exists") —
|
|
99
|
+
* consumers that need `steps`/`slug` filter for themselves. */
|
|
100
|
+
export async function parseSidecarFile(path) {
|
|
101
|
+
try {
|
|
102
|
+
const sc = JSON.parse(await readFile(path, 'utf-8'));
|
|
103
|
+
return sc && typeof sc === 'object' ? sc : null;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** A trailing run with fewer than this many assertions is left alone —
|
|
2
|
+
* `expect.soft` only earns its keep when ≥2 failures could be collected. */
|
|
3
|
+
export declare const MIN_RUN = 2;
|
|
4
|
+
export interface SoftBatchResult {
|
|
5
|
+
/** The (possibly) rewritten source. */
|
|
6
|
+
code: string;
|
|
7
|
+
/** Whether anything changed. */
|
|
8
|
+
changed: boolean;
|
|
9
|
+
/** How many assertions were softened across all tests. */
|
|
10
|
+
softened: number;
|
|
11
|
+
}
|
|
12
|
+
/** Run the soft-batch step over a spec's source text. Pure: text in, text out. */
|
|
13
|
+
export declare function softBatch(source: string): SoftBatchResult;
|
|
14
|
+
//# sourceMappingURL=softBatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"softBatch.d.ts","sourceRoot":"","sources":["../../src/specs/softBatch.ts"],"names":[],"mappings":"AAgCA;6EAC6E;AAC7E,eAAO,MAAM,OAAO,IAAI,CAAC;AAEzB,MAAM,WAAW,eAAe;IAC9B,uCAAuC;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,kFAAkF;AAClF,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAuBzD"}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* soft-batch — a deterministic finishing step of the optimization pass.
|
|
3
|
+
*
|
|
4
|
+
* After the LLM optimize pass decides WHAT to assert (it reads the observed
|
|
5
|
+
* feedback and adds the trailing "Then" assertions), this step applies the safe
|
|
6
|
+
* mechanical rewrite: the maximal *trailing run* of independent assertions in
|
|
7
|
+
* each `test()` body goes from `expect(...)` to `expect.soft(...)`, so a
|
|
8
|
+
* field-audit spec reports every failing field in one run instead of halting on
|
|
9
|
+
* the first. This is the "LLM decides, AST executes" split — the LLM never
|
|
10
|
+
* reprints the file to do it, so it can't drift; ts-morph applies the soften
|
|
11
|
+
* surgically and every untouched node stays byte-identical.
|
|
12
|
+
*
|
|
13
|
+
* Two assertion shapes are recognised, because that is what specs look like
|
|
14
|
+
* after optimize:
|
|
15
|
+
* A. a bare `await expect(...)....` statement;
|
|
16
|
+
* B. a Hover `await test.step('Then · …', async () => { await expect(...) })`
|
|
17
|
+
* closure whose body is nothing but assertions — Hover emits one such step
|
|
18
|
+
* per observed-feedback assertion, AFTER all the action steps.
|
|
19
|
+
*
|
|
20
|
+
* Safety guard (why this is deterministic and never changes test semantics):
|
|
21
|
+
* we only ever soften an assertion that sits in the trailing run — the suffix
|
|
22
|
+
* of the test body that is assertions all the way down, with no action after
|
|
23
|
+
* it. A *gating* assertion — one a later action depends on — is by construction
|
|
24
|
+
* followed by that action, so it never lands in the trailing run and is never
|
|
25
|
+
* softened. Softening only changes how failures are *reported* (all collected
|
|
26
|
+
* vs. stop-on-first), never whether the test passes. We require ≥2 assertions
|
|
27
|
+
* in the run: soft buys nothing for a single one. `expect.soft` collects across
|
|
28
|
+
* the whole test regardless of `test.step` nesting, so softening step-wrapped
|
|
29
|
+
* assertions is sound.
|
|
30
|
+
*/
|
|
31
|
+
import { Project, SyntaxKind, Node } from 'ts-morph';
|
|
32
|
+
/** A trailing run with fewer than this many assertions is left alone —
|
|
33
|
+
* `expect.soft` only earns its keep when ≥2 failures could be collected. */
|
|
34
|
+
export const MIN_RUN = 2;
|
|
35
|
+
/** Run the soft-batch step over a spec's source text. Pure: text in, text out. */
|
|
36
|
+
export function softBatch(source) {
|
|
37
|
+
const project = new Project({
|
|
38
|
+
useInMemoryFileSystem: true,
|
|
39
|
+
compilerOptions: { allowJs: true },
|
|
40
|
+
});
|
|
41
|
+
const sf = project.createSourceFile('__spec.ts', source, { overwrite: true });
|
|
42
|
+
// Collect every assertion to soften first (across all test bodies), then
|
|
43
|
+
// apply — each edit is a localized identifier replace, so sibling targets
|
|
44
|
+
// stay valid.
|
|
45
|
+
const targets = [];
|
|
46
|
+
for (const body of testBodies(sf)) {
|
|
47
|
+
const run = trailingAssertionRun(body.getStatements());
|
|
48
|
+
const asserts = run.flatMap(bareAssertionsIn);
|
|
49
|
+
if (asserts.length >= MIN_RUN)
|
|
50
|
+
targets.push(...asserts);
|
|
51
|
+
}
|
|
52
|
+
let softened = 0;
|
|
53
|
+
for (const stmt of targets) {
|
|
54
|
+
if (soften(stmt))
|
|
55
|
+
softened++;
|
|
56
|
+
}
|
|
57
|
+
return { code: sf.getFullText(), changed: softened > 0, softened };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Yield the block body of every `test(...)` call (including `test.only` /
|
|
61
|
+
* `.skip` / `.fixme`), but NOT `test.describe` / hooks — those wrap tests, they
|
|
62
|
+
* aren't a test body.
|
|
63
|
+
*/
|
|
64
|
+
function testBodies(sf) {
|
|
65
|
+
const bodies = [];
|
|
66
|
+
for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
67
|
+
if (!isTestCall(call.getExpression()))
|
|
68
|
+
continue;
|
|
69
|
+
const cb = call.getArguments().at(-1);
|
|
70
|
+
if (!cb)
|
|
71
|
+
continue;
|
|
72
|
+
if (Node.isArrowFunction(cb) || Node.isFunctionExpression(cb)) {
|
|
73
|
+
const block = cb.getBody();
|
|
74
|
+
if (Node.isBlock(block))
|
|
75
|
+
bodies.push(block);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return bodies;
|
|
79
|
+
}
|
|
80
|
+
/** `test` | `test.only` | `test.skip` | `test.fixme` — not `test.describe` or hooks. */
|
|
81
|
+
function isTestCall(callee) {
|
|
82
|
+
if (Node.isIdentifier(callee))
|
|
83
|
+
return callee.getText() === 'test';
|
|
84
|
+
if (Node.isPropertyAccessExpression(callee)) {
|
|
85
|
+
const base = callee.getExpression();
|
|
86
|
+
if (!Node.isIdentifier(base) || base.getText() !== 'test')
|
|
87
|
+
return false;
|
|
88
|
+
return ['only', 'skip', 'fixme'].includes(callee.getName());
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
/** The longest suffix of `statements` that are all assertion units (a bare
|
|
93
|
+
* assertion, or a `test.step` whose body is only assertions). */
|
|
94
|
+
function trailingAssertionRun(statements) {
|
|
95
|
+
const run = [];
|
|
96
|
+
for (let i = statements.length - 1; i >= 0; i--) {
|
|
97
|
+
if (!isAssertionUnit(statements[i]))
|
|
98
|
+
break;
|
|
99
|
+
run.unshift(statements[i]);
|
|
100
|
+
}
|
|
101
|
+
return run;
|
|
102
|
+
}
|
|
103
|
+
/** A statement that contributes only assertions: a bare assertion (case A) or
|
|
104
|
+
* an assertion-only `test.step` closure (case B). */
|
|
105
|
+
function isAssertionUnit(stmt) {
|
|
106
|
+
return isBareAssertion(stmt) || bareAssertionsInStep(stmt) !== null;
|
|
107
|
+
}
|
|
108
|
+
/** The bare-assertion statements a unit contains: itself (case A) or the
|
|
109
|
+
* assertions inside its `test.step` closure (case B). */
|
|
110
|
+
function bareAssertionsIn(stmt) {
|
|
111
|
+
if (isBareAssertion(stmt))
|
|
112
|
+
return [stmt];
|
|
113
|
+
return bareAssertionsInStep(stmt) ?? [];
|
|
114
|
+
}
|
|
115
|
+
/** True if the statement is `(await) expect(...)....` — its call chain bottoms
|
|
116
|
+
* out at an `expect` identifier. */
|
|
117
|
+
function isBareAssertion(stmt) {
|
|
118
|
+
if (!Node.isExpressionStatement(stmt))
|
|
119
|
+
return false;
|
|
120
|
+
let expr = stmt.getExpression();
|
|
121
|
+
if (Node.isAwaitExpression(expr))
|
|
122
|
+
expr = expr.getExpression();
|
|
123
|
+
return leftmostBase(expr) === 'expect';
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* If `stmt` is `await test.step(label, async () => { …only assertions… })`,
|
|
127
|
+
* return those inner assertion statements; otherwise null. The closure body
|
|
128
|
+
* must be non-empty and contain ONLY bare assertions — a step that also acts
|
|
129
|
+
* (a "When" with a check) is not a pure assertion unit and is left alone.
|
|
130
|
+
*/
|
|
131
|
+
function bareAssertionsInStep(stmt) {
|
|
132
|
+
if (!Node.isExpressionStatement(stmt))
|
|
133
|
+
return null;
|
|
134
|
+
let expr = stmt.getExpression();
|
|
135
|
+
if (Node.isAwaitExpression(expr))
|
|
136
|
+
expr = expr.getExpression();
|
|
137
|
+
if (!Node.isCallExpression(expr))
|
|
138
|
+
return null;
|
|
139
|
+
const callee = expr.getExpression();
|
|
140
|
+
if (!Node.isPropertyAccessExpression(callee) ||
|
|
141
|
+
callee.getName() !== 'step' ||
|
|
142
|
+
callee.getExpression().getText() !== 'test') {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const cb = expr.getArguments().at(-1);
|
|
146
|
+
if (!cb || !(Node.isArrowFunction(cb) || Node.isFunctionExpression(cb)))
|
|
147
|
+
return null;
|
|
148
|
+
const block = cb.getBody();
|
|
149
|
+
if (!Node.isBlock(block))
|
|
150
|
+
return null;
|
|
151
|
+
const inner = block.getStatements();
|
|
152
|
+
if (inner.length === 0 || !inner.every(isBareAssertion))
|
|
153
|
+
return null;
|
|
154
|
+
return inner;
|
|
155
|
+
}
|
|
156
|
+
/** Descend a call/member chain to its leftmost identifier, e.g.
|
|
157
|
+
* `expect(x).toHaveText(y)` → "expect", `page.goto('/')` → "page". */
|
|
158
|
+
function leftmostBase(node) {
|
|
159
|
+
let cur = node;
|
|
160
|
+
while (cur && (Node.isCallExpression(cur) || Node.isPropertyAccessExpression(cur))) {
|
|
161
|
+
cur = cur.getExpression();
|
|
162
|
+
}
|
|
163
|
+
return cur && Node.isIdentifier(cur) ? cur.getText() : null;
|
|
164
|
+
}
|
|
165
|
+
/** Rewrite the `expect(` call inside a bare assertion statement to
|
|
166
|
+
* `expect.soft(`. Skips ones already soft (their callee is `expect.soft`, not
|
|
167
|
+
* the bare identifier `expect`). Returns whether a change was made. */
|
|
168
|
+
function soften(stmt) {
|
|
169
|
+
for (const call of stmt.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
170
|
+
const callee = call.getExpression();
|
|
171
|
+
if (Node.isIdentifier(callee) && callee.getText() === 'expect') {
|
|
172
|
+
callee.replaceWithText('expect.soft');
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The serialized captured-step shape the whole spec pipeline (writeSpec,
|
|
3
|
+
* sidecar, Page-Object extraction, the session record) consumes.
|
|
4
|
+
*
|
|
5
|
+
* One entry per recorded chat message: a user prompt, an agent narration, a
|
|
6
|
+
* `browser_*` / control-MCP tool call (`step`), or the terminal `done` summary.
|
|
7
|
+
* (Formerly `SkillStep` in specs/specStep.ts, back when a run could be saved
|
|
8
|
+
* as a `.claude/skills/<slug>/SKILL.md` — that feature was retired; only the
|
|
9
|
+
* type survived, now relocated here.)
|
|
10
|
+
*/
|
|
11
|
+
export interface SkillStep {
|
|
12
|
+
kind: 'user' | 'system' | 'step' | 'ai' | 'done';
|
|
13
|
+
text?: string;
|
|
14
|
+
tool?: string;
|
|
15
|
+
input?: unknown;
|
|
16
|
+
isError?: boolean;
|
|
17
|
+
turns?: number;
|
|
18
|
+
costUsd?: number;
|
|
19
|
+
summary?: string;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=specStep.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"specStep.d.ts","sourceRoot":"","sources":["../../src/specs/specStep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small text helpers for the spec emitter.
|
|
3
|
+
*
|
|
4
|
+
* Hoisted here so writeSpec's JSDoc header derives a slug and a one-sentence
|
|
5
|
+
* "Expected" line through one shared implementation.
|
|
6
|
+
*/
|
|
7
|
+
/** Lowercase, hyphenate, and trim a display name into a filesystem-safe slug. */
|
|
8
|
+
export declare function slugify(name: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* The first sentence of a done-summary, trimmed. Agents sometimes ramble; the
|
|
11
|
+
* Expected blocks only want the leading sentence. Splits on the gap that
|
|
12
|
+
* follows sentence-ending punctuation — Latin (`.`, `!`, `?`) AND CJK
|
|
13
|
+
* (`。`, `!`, `?`) so a Chinese summary doesn't dump its whole multi-line body
|
|
14
|
+
* (which then broke the JSDoc block). Also cuts at the first hard line break, so
|
|
15
|
+
* a summary that runs straight into a `\n\n- bullet` list keeps only the lead
|
|
16
|
+
* sentence. A summary with no such break comes back trimmed in full.
|
|
17
|
+
*/
|
|
18
|
+
export declare function firstSentence(summary: string): string;
|
|
19
|
+
//# sourceMappingURL=text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/specs/text.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,iFAAiF;AACjF,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM5C;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGrD"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small text helpers for the spec emitter.
|
|
3
|
+
*
|
|
4
|
+
* Hoisted here so writeSpec's JSDoc header derives a slug and a one-sentence
|
|
5
|
+
* "Expected" line through one shared implementation.
|
|
6
|
+
*/
|
|
7
|
+
/** Lowercase, hyphenate, and trim a display name into a filesystem-safe slug. */
|
|
8
|
+
export function slugify(name) {
|
|
9
|
+
return name
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.trim()
|
|
12
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
13
|
+
.replace(/^-+|-+$/g, '');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The first sentence of a done-summary, trimmed. Agents sometimes ramble; the
|
|
17
|
+
* Expected blocks only want the leading sentence. Splits on the gap that
|
|
18
|
+
* follows sentence-ending punctuation — Latin (`.`, `!`, `?`) AND CJK
|
|
19
|
+
* (`。`, `!`, `?`) so a Chinese summary doesn't dump its whole multi-line body
|
|
20
|
+
* (which then broke the JSDoc block). Also cuts at the first hard line break, so
|
|
21
|
+
* a summary that runs straight into a `\n\n- bullet` list keeps only the lead
|
|
22
|
+
* sentence. A summary with no such break comes back trimmed in full.
|
|
23
|
+
*/
|
|
24
|
+
export function firstSentence(summary) {
|
|
25
|
+
const bySentence = summary.split(/(?<=[.!?。!?])\s+/)[0] ?? summary;
|
|
26
|
+
return bySentence.split(/\n/)[0]?.trim() ?? summary.trim();
|
|
27
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SkillStep } from '../
|
|
1
|
+
import type { SkillStep } from '../specs/specStep.js';
|
|
2
2
|
export type SpecStep = SkillStep;
|
|
3
3
|
/**
|
|
4
4
|
* Marker the deterministic translator leaves where a captured action is a real
|
|
@@ -24,6 +24,18 @@ export declare class SpecExistsError extends Error {
|
|
|
24
24
|
readonly path: string;
|
|
25
25
|
constructor(slug: string, path: string);
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* A credential to keep out of the generated spec. The deterministic translator
|
|
29
|
+
* replaces any fill value that exactly equals `value` with a
|
|
30
|
+
* `process.env.<envVar>` reference — so the literal password/username never
|
|
31
|
+
* lands in the spec source, the JSDoc header, OR the `.hover/` sidecar. The
|
|
32
|
+
* value comes from the caller (the editor resolves an `@account` mention from
|
|
33
|
+
* its vault); core only uses it to match-and-replace, never to write.
|
|
34
|
+
*/
|
|
35
|
+
export interface Redaction {
|
|
36
|
+
value: string;
|
|
37
|
+
envVar: string;
|
|
38
|
+
}
|
|
27
39
|
export interface WriteSpecOptions {
|
|
28
40
|
devRoot: string;
|
|
29
41
|
name: string;
|
|
@@ -31,10 +43,51 @@ export interface WriteSpecOptions {
|
|
|
31
43
|
steps: SpecStep[];
|
|
32
44
|
assertions?: SpecAssertion[];
|
|
33
45
|
overwrite?: boolean;
|
|
46
|
+
/** Credentials to parameterize into `process.env.<envVar>` references. */
|
|
47
|
+
redactions?: Redaction[];
|
|
48
|
+
/** The run's target URL (the active environment / dev origin). Used to
|
|
49
|
+
* guarantee the spec opens the app: if the captured session has NO
|
|
50
|
+
* navigation (the agent connected to an already-open tab and never called
|
|
51
|
+
* browser_navigate), a leading `page.goto()` is synthesized from this, and
|
|
52
|
+
* it's the fallback origin for the scaffolded playwright config. */
|
|
53
|
+
startUrl?: string;
|
|
54
|
+
/** Recon-discovered reset recipe for the run's environment (debt-2 reproducible
|
|
55
|
+
* state isolation). When tier 1, a shared `support/resetState.ts` helper is
|
|
56
|
+
* generated and called in a `beforeEach` so the spec re-enters from a clean
|
|
57
|
+
* client state every run. Tier 2/3 (backend-state) emit no reset here. (Engine
|
|
58
|
+
* shape, decoupled from the extension's ResetRecipe — only tier/keys matter.) */
|
|
59
|
+
resetRecipe?: {
|
|
60
|
+
tier: number;
|
|
61
|
+
storageKeys?: string[];
|
|
62
|
+
hook?: string;
|
|
63
|
+
};
|
|
64
|
+
/** Auth-as-fixture (debt 3): engage the fixture EVEN WHEN a user playwright.config
|
|
65
|
+
* already exists — i.e. the user approved Hover editing their config (Stage 4).
|
|
66
|
+
* Without it, an existing config keeps login inline (Hover never edits a user's
|
|
67
|
+
* config unprompted). When true, writeSpec also applies the setup-project edit
|
|
68
|
+
* to the config. No effect when no login prefix is detected. */
|
|
69
|
+
authFixture?: boolean;
|
|
34
70
|
}
|
|
35
71
|
export interface WriteSpecResult {
|
|
72
|
+
/** Primary written file (the first flow when split, else the single spec). */
|
|
36
73
|
path: string;
|
|
37
74
|
slug: string;
|
|
75
|
+
/** Every file written — one per `mark_flow` feature when the run was split. */
|
|
76
|
+
files: {
|
|
77
|
+
path: string;
|
|
78
|
+
slug: string;
|
|
79
|
+
flow: string;
|
|
80
|
+
}[];
|
|
81
|
+
/** Auth-as-fixture (debt 3, Stage 4): a login was detected but a USER
|
|
82
|
+
* playwright.config already exists, so Hover left login inline rather than
|
|
83
|
+
* edit their config unprompted. This is the proposed edit to offer for
|
|
84
|
+
* approval — on approval the caller re-saves with `authFixture: true`, which
|
|
85
|
+
* applies it. Absent when there's no login, no user config, or the config
|
|
86
|
+
* can't be safely edited (already has `projects`). */
|
|
87
|
+
authFixtureOffer?: {
|
|
88
|
+
configPath: string;
|
|
89
|
+
proposedConfig: string;
|
|
90
|
+
};
|
|
38
91
|
}
|
|
39
92
|
export declare function writeSpec(opts: WriteSpecOptions): Promise<WriteSpecResult>;
|
|
40
93
|
/**
|
|
@@ -68,6 +121,14 @@ export declare function blockScope(lines: string[]): string[];
|
|
|
68
121
|
* trailing role keyword is the convention Playwright MCP uses.
|
|
69
122
|
*/
|
|
70
123
|
export declare function selectorFromDescription(desc: string, pageVar?: string): string;
|
|
124
|
+
/**
|
|
125
|
+
* browser_select_option always targets a native `<select>` — whose ARIA role
|
|
126
|
+
* is `combobox`. The agent's description is usually the label ("marital
|
|
127
|
+
* status"), with no role keyword, so selectorFromDescription would fall back to
|
|
128
|
+
* getByText and match the *label text*, not the control — and `.selectOption()`
|
|
129
|
+
* on a text node throws. Force the combobox role by accessible name instead.
|
|
130
|
+
*/
|
|
131
|
+
export declare function selectorForSelect(desc: string, pageVar?: string): string;
|
|
71
132
|
/**
|
|
72
133
|
* Form fields from browser_fill_form have a `name` that's typically the
|
|
73
134
|
* accessible name / label / aria-label. getByLabel is the right primitive.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writeSpec.d.ts","sourceRoot":"","sources":["../../src/specs/writeSpec.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"writeSpec.d.ts","sourceRoot":"","sources":["../../src/specs/writeSpec.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAatD,MAAM,MAAM,QAAQ,GAAG,SAAS,CAAC;AAEjC;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AAEzD;;0DAE0D;AAC1D,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE9D;AA2CD,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAgB,SAAQ,KAAK;aACZ,IAAI,EAAE,MAAM;aAAkB,IAAI,EAAE,MAAM;gBAA1C,IAAI,EAAE,MAAM,EAAkB,IAAI,EAAE,MAAM;CAIvE;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IACzB;;;;yEAIqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;sFAIkF;IAClF,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;qEAIiE;IACjE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAiDD,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtD;;;;;2DAKuD;IACvD,gBAAgB,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;CACnE;AAQD,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAchF;AA+mBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAM9E;AAED;;oCAEoC;AACpC,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CAqB9E;AA0DD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CAMxE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CAQ1F"}
|