@hover-dev/core 0.16.0 → 0.18.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/engine.d.ts +28 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +27 -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/playwright/launchChrome.d.ts +18 -0
- package/dist/playwright/launchChrome.d.ts.map +1 -1
- package/dist/playwright/launchChrome.js +46 -3
- 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/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/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/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 +71 -41
- package/dist/specs/pageObjectManifest.d.ts +3 -1
- package/dist/specs/pageObjectManifest.d.ts.map +1 -1
- package/dist/specs/pageObjectManifest.js +24 -19
- 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/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 +8 -6
- package/dist/specs/text.d.ts.map +1 -1
- package/dist/specs/text.js +10 -7
- package/dist/specs/writeSpec.d.ts +62 -1
- package/dist/specs/writeSpec.d.ts.map +1 -1
- package/dist/specs/writeSpec.js +596 -21
- package/package.json +9 -29
- package/dist/agents/aider.d.ts +0 -16
- package/dist/agents/aider.d.ts.map +0 -1
- package/dist/agents/aider.js +0 -161
- package/dist/agents/argv.d.ts +0 -11
- package/dist/agents/argv.d.ts.map +0 -1
- package/dist/agents/argv.js +0 -23
- package/dist/agents/claude.d.ts +0 -3
- package/dist/agents/claude.d.ts.map +0 -1
- package/dist/agents/claude.js +0 -195
- package/dist/agents/codex.d.ts +0 -19
- package/dist/agents/codex.d.ts.map +0 -1
- package/dist/agents/codex.js +0 -216
- package/dist/agents/cursor.d.ts +0 -18
- package/dist/agents/cursor.d.ts.map +0 -1
- package/dist/agents/cursor.js +0 -220
- package/dist/agents/detect.d.ts +0 -46
- package/dist/agents/detect.d.ts.map +0 -1
- package/dist/agents/detect.js +0 -80
- package/dist/agents/gemini.d.ts +0 -17
- package/dist/agents/gemini.d.ts.map +0 -1
- package/dist/agents/gemini.js +0 -186
- package/dist/agents/index.d.ts +0 -6
- package/dist/agents/index.d.ts.map +0 -1
- package/dist/agents/index.js +0 -5
- package/dist/agents/invoke.d.ts +0 -12
- package/dist/agents/invoke.d.ts.map +0 -1
- package/dist/agents/invoke.js +0 -96
- package/dist/agents/qwen.d.ts +0 -17
- package/dist/agents/qwen.d.ts.map +0 -1
- package/dist/agents/qwen.js +0 -172
- package/dist/agents/registry.d.ts +0 -19
- package/dist/agents/registry.d.ts.map +0 -1
- package/dist/agents/registry.js +0 -34
- package/dist/agents/shared.d.ts +0 -28
- package/dist/agents/shared.d.ts.map +0 -1
- package/dist/agents/shared.js +0 -35
- package/dist/agents/types.d.ts +0 -186
- package/dist/agents/types.d.ts.map +0 -1
- package/dist/agents/types.js +0 -23
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -2
- package/dist/mcp/sourceFence.d.ts +0 -23
- package/dist/mcp/sourceFence.d.ts.map +0 -1
- package/dist/mcp/sourceFence.js +0 -75
- package/dist/mcp/sourceServer.d.ts +0 -3
- package/dist/mcp/sourceServer.d.ts.map +0 -1
- package/dist/mcp/sourceServer.js +0 -116
- package/dist/playwright/cdpStatus.d.ts +0 -29
- package/dist/playwright/cdpStatus.d.ts.map +0 -1
- package/dist/playwright/cdpStatus.js +0 -119
- package/dist/playwright/preflight.d.ts +0 -31
- package/dist/playwright/preflight.d.ts.map +0 -1
- package/dist/playwright/preflight.js +0 -82
- package/dist/playwright/preflightCache.d.ts +0 -27
- package/dist/playwright/preflightCache.d.ts.map +0 -1
- package/dist/playwright/preflightCache.js +0 -21
- package/dist/playwright/raiseWindow.d.ts +0 -10
- package/dist/playwright/raiseWindow.d.ts.map +0 -1
- package/dist/playwright/raiseWindow.js +0 -158
- package/dist/playwright/resolveMcpConfig.d.ts +0 -55
- package/dist/playwright/resolveMcpConfig.d.ts.map +0 -1
- package/dist/playwright/resolveMcpConfig.js +0 -66
- package/dist/plugin-api.d.ts +0 -235
- package/dist/plugin-api.d.ts.map +0 -1
- package/dist/plugin-api.js +0 -52
- package/dist/runSession.d.ts +0 -42
- package/dist/runSession.d.ts.map +0 -1
- package/dist/runSession.js +0 -81
- 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/service/cdpHandlers.d.ts +0 -44
- package/dist/service/cdpHandlers.d.ts.map +0 -1
- package/dist/service/cdpHandlers.js +0 -85
- package/dist/service/cdpHint.d.ts +0 -48
- package/dist/service/cdpHint.d.ts.map +0 -1
- package/dist/service/cdpHint.js +0 -216
- package/dist/service/conventions.d.ts +0 -8
- package/dist/service/conventions.d.ts.map +0 -1
- package/dist/service/conventions.js +0 -42
- package/dist/service/saveHandlers.d.ts +0 -52
- package/dist/service/saveHandlers.d.ts.map +0 -1
- package/dist/service/saveHandlers.js +0 -75
- package/dist/service/types.d.ts +0 -58
- package/dist/service/types.d.ts.map +0 -1
- package/dist/service/types.js +0 -26
- package/dist/service.d.ts +0 -50
- package/dist/service.d.ts.map +0 -1
- package/dist/service.js +0 -1065
- 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/extractPageObjects.d.ts +0 -18
- package/dist/specs/extractPageObjects.d.ts.map +0 -1
- package/dist/specs/extractPageObjects.js +0 -98
- 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/optimizeSpecWithAgent.d.ts +0 -11
- package/dist/specs/optimizeSpecWithAgent.d.ts.map +0 -1
- package/dist/specs/optimizeSpecWithAgent.js +0 -40
- package/dist/specs/writeCaseCsv.d.ts +0 -28
- package/dist/specs/writeCaseCsv.d.ts.map +0 -1
- package/dist/specs/writeCaseCsv.js +0 -134
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Captured-step type.
|
|
3
|
-
*
|
|
4
|
-
* NOTE: Save-as-Skill (writing `.claude/skills/<slug>/SKILL.md` for agent
|
|
5
|
-
* replay) was retired — `spec` + ⟳ Re-record covers intent-driven replay, and
|
|
6
|
-
* "skill" collided with Claude Code's own skills concept. All that remains
|
|
7
|
-
* here is `SkillStep`: the serialized message shape from the widget's
|
|
8
|
-
* localStorage, which the whole spec pipeline (writeSpec, sidecar, listSpecs,
|
|
9
|
-
* Page-Object extraction) consumes as `SpecStep`. The file keeps its path so
|
|
10
|
-
* the many `import { SkillStep } from '../skills/writeSkill.js'` call sites
|
|
11
|
-
* don't churn; renaming to a neutral module is a separate mechanical pass.
|
|
12
|
-
*/
|
|
13
|
-
/**
|
|
14
|
-
* Serialized message shape from the widget's localStorage. Matches the
|
|
15
|
-
* `state.messages` schema in packages/widget-bootstrap/src/widget/client.js.
|
|
16
|
-
*/
|
|
17
|
-
export interface SkillStep {
|
|
18
|
-
kind: 'user' | 'system' | 'step' | 'ai' | 'done';
|
|
19
|
-
text?: string;
|
|
20
|
-
tool?: string;
|
|
21
|
-
input?: unknown;
|
|
22
|
-
isError?: boolean;
|
|
23
|
-
turns?: number;
|
|
24
|
-
costUsd?: number;
|
|
25
|
-
summary?: string;
|
|
26
|
-
}
|
|
27
|
-
//# sourceMappingURL=writeSkill.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"writeSkill.d.ts","sourceRoot":"","sources":["../../src/skills/writeSkill.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;;GAGG;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"}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Captured-step type.
|
|
3
|
-
*
|
|
4
|
-
* NOTE: Save-as-Skill (writing `.claude/skills/<slug>/SKILL.md` for agent
|
|
5
|
-
* replay) was retired — `spec` + ⟳ Re-record covers intent-driven replay, and
|
|
6
|
-
* "skill" collided with Claude Code's own skills concept. All that remains
|
|
7
|
-
* here is `SkillStep`: the serialized message shape from the widget's
|
|
8
|
-
* localStorage, which the whole spec pipeline (writeSpec, sidecar, listSpecs,
|
|
9
|
-
* Page-Object extraction) consumes as `SpecStep`. The file keeps its path so
|
|
10
|
-
* the many `import { SkillStep } from '../skills/writeSkill.js'` call sites
|
|
11
|
-
* don't churn; renaming to a neutral module is a separate mechanical pass.
|
|
12
|
-
*/
|
|
13
|
-
export {};
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export interface ExtractedPage {
|
|
2
|
-
className: string;
|
|
3
|
-
methodName: string;
|
|
4
|
-
fileName: string;
|
|
5
|
-
/** Absolute path written. */
|
|
6
|
-
path: string;
|
|
7
|
-
/** Slugs of the specs that share this flow. */
|
|
8
|
-
specs: string[];
|
|
9
|
-
}
|
|
10
|
-
export interface ExtractResult {
|
|
11
|
-
pages: ExtractedPage[];
|
|
12
|
-
/** Absolute path of the written fixtures.ts, or null when nothing extracted. */
|
|
13
|
-
fixturesPath: string | null;
|
|
14
|
-
}
|
|
15
|
-
export declare function extractPageObjects(devRoot: string, opts?: {
|
|
16
|
-
minSpecs?: number;
|
|
17
|
-
}): Promise<ExtractResult>;
|
|
18
|
-
//# sourceMappingURL=extractPageObjects.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"extractPageObjects.d.ts","sourceRoot":"","sources":["../../src/specs/extractPageObjects.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,gFAAgF;IAChF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,MAAM,EACf,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/B,OAAO,CAAC,aAAa,CAAC,CA4CxB"}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stage 3 (F4): extract Page Objects + a fixtures entry point from flows shared
|
|
3
|
-
* across saved specs.
|
|
4
|
-
*
|
|
5
|
-
* Reads detectSharedFlows (>= 3 specs sharing an entry prefix — the scaffold's
|
|
6
|
-
* 3-use threshold), generates a `pages/<Name>.ts` per flow, and (re)writes a
|
|
7
|
-
* single `fixtures.ts` that registers each Page Object via `base.extend`. New
|
|
8
|
-
* specs `import { test, expect } from './fixtures'` and consume e.g.
|
|
9
|
-
* `async ({ page, loginPage }) => …`.
|
|
10
|
-
*
|
|
11
|
-
* Manual trigger (Stage 3b): invoked by a CLI command, not on every save, and
|
|
12
|
-
* it never rewrites already-committed specs (D6) — it only emits the shared
|
|
13
|
-
* pages/ + fixtures.ts going forward.
|
|
14
|
-
*/
|
|
15
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
16
|
-
import { join } from 'node:path';
|
|
17
|
-
import { detectSharedFlows } from './detectSharedFlows.js';
|
|
18
|
-
import { generatePageObject } from './generatePageObject.js';
|
|
19
|
-
import { writePageObjectManifest } from './pageObjectManifest.js';
|
|
20
|
-
export async function extractPageObjects(devRoot, opts = {}) {
|
|
21
|
-
// 3-use threshold for extraction; lower thresholds only *report* (Stage 2).
|
|
22
|
-
const flows = await detectSharedFlows(devRoot, { minSpecs: opts.minSpecs ?? 3 });
|
|
23
|
-
if (flows.length === 0)
|
|
24
|
-
return { pages: [], fixturesPath: null };
|
|
25
|
-
const testsDir = join(devRoot, '__vibe_tests__');
|
|
26
|
-
const pagesDir = join(testsDir, 'pages');
|
|
27
|
-
await mkdir(pagesDir, { recursive: true });
|
|
28
|
-
const pages = [];
|
|
29
|
-
const entries = [];
|
|
30
|
-
const usedNames = new Set();
|
|
31
|
-
for (const flow of flows) {
|
|
32
|
-
const probe = generatePageObject(flow.prefixSteps);
|
|
33
|
-
const className = uniqueName(probe.className, usedNames);
|
|
34
|
-
const po = className === probe.className
|
|
35
|
-
? probe
|
|
36
|
-
: generatePageObject(flow.prefixSteps, { className });
|
|
37
|
-
const path = join(pagesDir, po.fileName);
|
|
38
|
-
await writeFile(path, po.source, 'utf-8');
|
|
39
|
-
pages.push({
|
|
40
|
-
className: po.className,
|
|
41
|
-
methodName: po.methodName,
|
|
42
|
-
fileName: po.fileName,
|
|
43
|
-
path,
|
|
44
|
-
specs: flow.specs,
|
|
45
|
-
});
|
|
46
|
-
entries.push({
|
|
47
|
-
className: po.className,
|
|
48
|
-
methodName: po.methodName,
|
|
49
|
-
fixtureName: fixtureName(po.className),
|
|
50
|
-
fileName: po.fileName,
|
|
51
|
-
signatures: flow.signatures,
|
|
52
|
-
specs: flow.specs,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
const fixturesPath = join(testsDir, 'fixtures.ts');
|
|
56
|
-
await writeFile(fixturesPath, renderFixtures(pages), 'utf-8');
|
|
57
|
-
// Manifest lets writeSpec match a new spec's prefix to a Page Object and
|
|
58
|
-
// consume it (Stage 3c) without re-running detection.
|
|
59
|
-
await writePageObjectManifest(devRoot, entries);
|
|
60
|
-
return { pages, fixturesPath };
|
|
61
|
-
}
|
|
62
|
-
function renderFixtures(pages) {
|
|
63
|
-
const lines = [];
|
|
64
|
-
lines.push(`import { test as base } from '@playwright/test';`);
|
|
65
|
-
for (const p of pages) {
|
|
66
|
-
lines.push(`import { ${p.className} } from './pages/${p.className}';`);
|
|
67
|
-
}
|
|
68
|
-
lines.push('');
|
|
69
|
-
lines.push('/**');
|
|
70
|
-
lines.push(' * Generated by Hover — Page Object fixtures lifted from flows shared');
|
|
71
|
-
lines.push(" * across specs. In a new spec: `import { test, expect } from './fixtures';`");
|
|
72
|
-
lines.push(' * then consume e.g. `async ({ page, loginPage }) => …`.');
|
|
73
|
-
lines.push(' */');
|
|
74
|
-
const typeMembers = pages.map(p => `${fixtureName(p.className)}: ${p.className}`).join('; ');
|
|
75
|
-
lines.push(`export const test = base.extend<{ ${typeMembers} }>({`);
|
|
76
|
-
for (const p of pages) {
|
|
77
|
-
lines.push(` ${fixtureName(p.className)}: async ({ page }, use) => {`);
|
|
78
|
-
lines.push(` await use(new ${p.className}(page));`);
|
|
79
|
-
lines.push(` },`);
|
|
80
|
-
}
|
|
81
|
-
lines.push(`});`);
|
|
82
|
-
lines.push('');
|
|
83
|
-
lines.push(`export { expect } from '@playwright/test';`);
|
|
84
|
-
lines.push('');
|
|
85
|
-
return lines.join('\n');
|
|
86
|
-
}
|
|
87
|
-
/** Class name -> fixture key: LoginPage -> loginPage. */
|
|
88
|
-
function fixtureName(className) {
|
|
89
|
-
return className.charAt(0).toLowerCase() + className.slice(1);
|
|
90
|
-
}
|
|
91
|
-
function uniqueName(base, used) {
|
|
92
|
-
let name = base;
|
|
93
|
-
let n = 2;
|
|
94
|
-
while (used.has(name))
|
|
95
|
-
name = `${base}${n++}`;
|
|
96
|
-
used.add(name);
|
|
97
|
-
return name;
|
|
98
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { type OptimizationSuggestion } from './optimizationSuggestion.js';
|
|
2
|
-
export interface SpecSummary {
|
|
3
|
-
/** Path-relative slug, e.g. `login-and-counter`. Identifies the spec. */
|
|
4
|
-
slug: string;
|
|
5
|
-
/** Absolute path to the .spec.ts file. */
|
|
6
|
-
path: string;
|
|
7
|
-
/** `Original prompt:` parsed from the JSDoc header. `null` for
|
|
8
|
-
* hand-authored specs that have no header — they list but can't be
|
|
9
|
-
* re-recorded automatically. */
|
|
10
|
-
originalPrompt: string | null;
|
|
11
|
-
/** First line of `Outcome:` from the JSDoc header, if present. */
|
|
12
|
-
outcome: string | null;
|
|
13
|
-
/** Number of `Steps:` lines parsed (informational only). */
|
|
14
|
-
stepCount: number;
|
|
15
|
-
/** File mtime in ms — used to show "saved 2 hours ago" in the UI. */
|
|
16
|
-
mtimeMs: number;
|
|
17
|
-
/** Whether a structured `.hover/<slug>.json` sidecar exists. The widget
|
|
18
|
-
* gates the optimization pass on this — without a captured session there's
|
|
19
|
-
* no observed feedback for the LLM to add assertions from. */
|
|
20
|
-
hasSidecar: boolean;
|
|
21
|
-
/** Count of `// hover:optimizable` markers the deterministic translator left
|
|
22
|
-
* — interactions it couldn't fully translate single-step. >0 is a strong
|
|
23
|
-
* signal to run the optimization pass (or add a seed). */
|
|
24
|
-
optimizableCount: number;
|
|
25
|
-
/** The default-off "review optimization?" nudge (F7/D10): suggested + reasons,
|
|
26
|
-
* derived from optimizable markers + relevant seeds. */
|
|
27
|
-
optimization: OptimizationSuggestion;
|
|
28
|
-
}
|
|
29
|
-
export interface SpecHeader {
|
|
30
|
-
/** Raw text of `Original prompt:` line, or null when absent. */
|
|
31
|
-
originalPrompt: string | null;
|
|
32
|
-
/** First line of `Outcome:`. */
|
|
33
|
-
outcome: string | null;
|
|
34
|
-
/** Step lines from the `Steps:` block, in order. */
|
|
35
|
-
steps: string[];
|
|
36
|
-
/** Lines from the `Expected:` block, in order. */
|
|
37
|
-
expected: string[];
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Parse the JSDoc header that `writeSpec.ts` emits. Tolerant of:
|
|
41
|
-
* - Specs without any JSDoc (returns all-null).
|
|
42
|
-
* - Hand-edited specs where users reordered or trimmed sections.
|
|
43
|
-
* - Long prompts that wrap across lines (we take only the first line).
|
|
44
|
-
*/
|
|
45
|
-
export declare function parseSpecHeader(source: string): SpecHeader;
|
|
46
|
-
/**
|
|
47
|
-
* List every `*.spec.ts` file under `<devRoot>/__vibe_tests__/` with its
|
|
48
|
-
* parsed header. Returns newest-first by mtime so the widget overlay shows
|
|
49
|
-
* recently-saved specs at the top.
|
|
50
|
-
*/
|
|
51
|
-
export declare function listSpecs(devRoot: string): Promise<SpecSummary[]>;
|
|
52
|
-
//# sourceMappingURL=listSpecs.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"listSpecs.d.ts","sourceRoot":"","sources":["../../src/specs/listSpecs.ts"],"names":[],"mappings":"AAoBA,OAAO,EAA0B,KAAK,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAGlG,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb;;qCAEiC;IACjC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,kEAAkE;IAClE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;IAChB;;mEAE+D;IAC/D,UAAU,EAAE,OAAO,CAAC;IACpB;;+DAE2D;IAC3D,gBAAgB,EAAE,MAAM,CAAC;IACzB;6DACyD;IACzD,YAAY,EAAE,sBAAsB,CAAC;CACtC;AAED,MAAM,WAAW,UAAU;IACzB,gEAAgE;IAChE,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,gCAAgC;IAChC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,oDAAoD;IACpD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,kDAAkD;IAClD,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAsB1D;AA4BD;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CA2DvE"}
|
package/dist/specs/listSpecs.js
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* List + parse Hover-generated Playwright specs under `<devRoot>/__vibe_tests__/`.
|
|
3
|
-
*
|
|
4
|
-
* Used by:
|
|
5
|
-
* - The widget's "Specs" overlay tab (server pushes a SpecSummary[] list).
|
|
6
|
-
* - The CLI's `hover re-record <spec>` subcommand (parses one spec for its
|
|
7
|
-
* `Original prompt:` JSDoc header).
|
|
8
|
-
*
|
|
9
|
-
* Hand-authored specs (no Hover JSDoc header) are listed but reported with
|
|
10
|
-
* `originalPrompt: null` — the UI / CLI surfaces that "this spec can't be
|
|
11
|
-
* re-recorded automatically; the natural-language intent isn't recorded."
|
|
12
|
-
*
|
|
13
|
-
* Shares the SpecSummary row shape the widget's Specs tab renders.
|
|
14
|
-
*/
|
|
15
|
-
import { readdir, readFile } from 'node:fs/promises';
|
|
16
|
-
import { stat } from 'node:fs/promises';
|
|
17
|
-
import { existsSync } from 'node:fs';
|
|
18
|
-
import { join } from 'node:path';
|
|
19
|
-
import { countOptimizableMarkers } from './writeSpec.js';
|
|
20
|
-
import { readSeeds, relevantSeeds } from './seeds.js';
|
|
21
|
-
import { optimizationSuggestion } from './optimizationSuggestion.js';
|
|
22
|
-
/**
|
|
23
|
-
* Parse the JSDoc header that `writeSpec.ts` emits. Tolerant of:
|
|
24
|
-
* - Specs without any JSDoc (returns all-null).
|
|
25
|
-
* - Hand-edited specs where users reordered or trimmed sections.
|
|
26
|
-
* - Long prompts that wrap across lines (we take only the first line).
|
|
27
|
-
*/
|
|
28
|
-
export function parseSpecHeader(source) {
|
|
29
|
-
// JSDoc block right after the @playwright/test import (or at file top).
|
|
30
|
-
// We don't require it to be the very first JSDoc — there could be a
|
|
31
|
-
// banner comment from a linter. We DO require it to appear before the
|
|
32
|
-
// first `test(` / `test.describe(` so that long file footers can't
|
|
33
|
-
// confuse the parser.
|
|
34
|
-
const beforeFirstTest = source.split(/^\s*(?:test|test\.describe)\s*\(/m)[0] ?? source;
|
|
35
|
-
const blockMatch = beforeFirstTest.match(/\/\*\*([\s\S]*?)\*\//);
|
|
36
|
-
if (!blockMatch) {
|
|
37
|
-
return { originalPrompt: null, outcome: null, steps: [], expected: [] };
|
|
38
|
-
}
|
|
39
|
-
const block = blockMatch[1];
|
|
40
|
-
const originalPrompt = extractScalar(block, /^\s*\*\s*Original prompt:\s*(.+?)\s*$/m);
|
|
41
|
-
const outcome = extractScalar(block, /^\s*\*\s*Outcome:\s*(.+?)\s*$/m);
|
|
42
|
-
return {
|
|
43
|
-
originalPrompt,
|
|
44
|
-
outcome,
|
|
45
|
-
steps: extractList(block, /^\s*\*\s*Steps:\s*$/m),
|
|
46
|
-
expected: extractList(block, /^\s*\*\s*Expected:\s*$/m),
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function extractScalar(block, re) {
|
|
50
|
-
const m = block.match(re);
|
|
51
|
-
return m ? m[1].trim() : null;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Extract a JSDoc list-style block. Given a header regex matching "Steps:"
|
|
55
|
-
* or "Expected:", read subsequent ` * <indented line>` lines until the next
|
|
56
|
-
* top-level marker (blank ` *` line or another `Section:` header).
|
|
57
|
-
*/
|
|
58
|
-
function extractList(block, headerRe) {
|
|
59
|
-
const match = block.match(headerRe);
|
|
60
|
-
if (!match)
|
|
61
|
-
return [];
|
|
62
|
-
const start = (match.index ?? 0) + match[0].length;
|
|
63
|
-
const tail = block.slice(start);
|
|
64
|
-
const lines = [];
|
|
65
|
-
for (const raw of tail.split('\n')) {
|
|
66
|
-
// Stop at a blank JSDoc line (` *` only) or another `Section:` header.
|
|
67
|
-
if (/^\s*\*\s*$/.test(raw))
|
|
68
|
-
break;
|
|
69
|
-
if (/^\s*\*\s*\w[\w ]*:\s*$/.test(raw) || /^\s*\*\s*\w[\w ]*:\s/.test(raw))
|
|
70
|
-
break;
|
|
71
|
-
const m = raw.match(/^\s*\*\s*(?:[•\-\*\d.]\s*)*(.+?)\s*$/);
|
|
72
|
-
if (m && m[1])
|
|
73
|
-
lines.push(m[1]);
|
|
74
|
-
}
|
|
75
|
-
return lines;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* List every `*.spec.ts` file under `<devRoot>/__vibe_tests__/` with its
|
|
79
|
-
* parsed header. Returns newest-first by mtime so the widget overlay shows
|
|
80
|
-
* recently-saved specs at the top.
|
|
81
|
-
*/
|
|
82
|
-
export async function listSpecs(devRoot) {
|
|
83
|
-
const root = join(devRoot, '__vibe_tests__');
|
|
84
|
-
let entries;
|
|
85
|
-
try {
|
|
86
|
-
entries = await readdir(root);
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
return [];
|
|
90
|
-
}
|
|
91
|
-
// Seeds are devRoot-wide; read once and reuse for every spec's suggestion.
|
|
92
|
-
const seeds = await readSeeds(devRoot);
|
|
93
|
-
const summaries = [];
|
|
94
|
-
for (const entry of entries) {
|
|
95
|
-
if (!entry.endsWith('.spec.ts'))
|
|
96
|
-
continue;
|
|
97
|
-
const path = join(root, entry);
|
|
98
|
-
let content;
|
|
99
|
-
let mtimeMs = 0;
|
|
100
|
-
try {
|
|
101
|
-
content = await readFile(path, 'utf-8');
|
|
102
|
-
const st = await stat(path);
|
|
103
|
-
mtimeMs = st.mtimeMs;
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
const header = parseSpecHeader(content);
|
|
109
|
-
const slug = entry.replace(/\.spec\.ts$/, '');
|
|
110
|
-
const sidecarPath = join(root, '.hover', `${slug}.json`);
|
|
111
|
-
const hasSidecar = existsSync(sidecarPath);
|
|
112
|
-
const optimizableCount = countOptimizableMarkers(content);
|
|
113
|
-
// Which seeds could plausibly apply, from the sidecar's captured tools.
|
|
114
|
-
let relevantSeedNames = [];
|
|
115
|
-
if (hasSidecar && seeds.length > 0) {
|
|
116
|
-
try {
|
|
117
|
-
const sc = JSON.parse(await readFile(sidecarPath, 'utf-8'));
|
|
118
|
-
const tools = new Set((sc.steps ?? []).filter(s => s.kind === 'step' && s.tool).map(s => s.tool));
|
|
119
|
-
relevantSeedNames = relevantSeeds(seeds, tools).map(s => s.name);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
/* malformed sidecar — treat as no relevant seeds */
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
summaries.push({
|
|
126
|
-
slug,
|
|
127
|
-
path,
|
|
128
|
-
originalPrompt: header.originalPrompt,
|
|
129
|
-
outcome: header.outcome,
|
|
130
|
-
stepCount: header.steps.length,
|
|
131
|
-
mtimeMs,
|
|
132
|
-
hasSidecar,
|
|
133
|
-
optimizableCount,
|
|
134
|
-
optimization: optimizationSuggestion({ hasSidecar, optimizableCount, relevantSeedNames }),
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
summaries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
138
|
-
return summaries;
|
|
139
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The default-off "should we nudge the user to optimize this spec?" signal
|
|
3
|
-
* (F7 / D10). Optimization never runs automatically; instead, when a saved spec
|
|
4
|
-
* has a clear improvable shape, the widget surfaces a "review optimization?"
|
|
5
|
-
* prompt. This computes that decision + human-readable reasons.
|
|
6
|
-
*
|
|
7
|
-
* Pure function so it's trivially testable; `listSpecs` gathers the inputs
|
|
8
|
-
* (optimizable-marker count, sidecar presence, relevant seed names) and attaches
|
|
9
|
-
* the result to each SpecSummary.
|
|
10
|
-
*/
|
|
11
|
-
export interface OptimizationSuggestion {
|
|
12
|
-
/** Whether to nudge the user to run the optimization pass on this spec. */
|
|
13
|
-
suggested: boolean;
|
|
14
|
-
/** Human-readable reasons, for the widget tooltip / prompt. Empty when not
|
|
15
|
-
* suggested. */
|
|
16
|
-
reasons: string[];
|
|
17
|
-
}
|
|
18
|
-
export declare function optimizationSuggestion(args: {
|
|
19
|
-
/** Whether a `.hover/<slug>.json` sidecar exists. */
|
|
20
|
-
hasSidecar: boolean;
|
|
21
|
-
/** Count of `// hover:optimizable` markers in the spec. */
|
|
22
|
-
optimizableCount: number;
|
|
23
|
-
/** Names of seeds whose signature is relevant to this spec's tools. */
|
|
24
|
-
relevantSeedNames: string[];
|
|
25
|
-
}): OptimizationSuggestion;
|
|
26
|
-
//# sourceMappingURL=optimizationSuggestion.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"optimizationSuggestion.d.ts","sourceRoot":"","sources":["../../src/specs/optimizationSuggestion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,WAAW,sBAAsB;IACrC,2EAA2E;IAC3E,SAAS,EAAE,OAAO,CAAC;IACnB;qBACiB;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,qDAAqD;IACrD,UAAU,EAAE,OAAO,CAAC;IACpB,2DAA2D;IAC3D,gBAAgB,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B,GAAG,sBAAsB,CAqBzB"}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The default-off "should we nudge the user to optimize this spec?" signal
|
|
3
|
-
* (F7 / D10). Optimization never runs automatically; instead, when a saved spec
|
|
4
|
-
* has a clear improvable shape, the widget surfaces a "review optimization?"
|
|
5
|
-
* prompt. This computes that decision + human-readable reasons.
|
|
6
|
-
*
|
|
7
|
-
* Pure function so it's trivially testable; `listSpecs` gathers the inputs
|
|
8
|
-
* (optimizable-marker count, sidecar presence, relevant seed names) and attaches
|
|
9
|
-
* the result to each SpecSummary.
|
|
10
|
-
*/
|
|
11
|
-
export function optimizationSuggestion(args) {
|
|
12
|
-
const { hasSidecar, optimizableCount, relevantSeedNames } = args;
|
|
13
|
-
const reasons = [];
|
|
14
|
-
// The optimization pass reads the sidecar (observed feedback, captured steps);
|
|
15
|
-
// without one there's nothing to optimize from. Matches the widget's Optimize
|
|
16
|
-
// gate, so we never suggest what can't be acted on.
|
|
17
|
-
if (!hasSidecar)
|
|
18
|
-
return { suggested: false, reasons };
|
|
19
|
-
if (optimizableCount > 0) {
|
|
20
|
-
const n = optimizableCount;
|
|
21
|
-
reasons.push(`${n} interaction${n === 1 ? '' : 's'} couldn't be fully translated — the optimization pass can complete ${n === 1 ? 'it' : 'them'}`);
|
|
22
|
-
}
|
|
23
|
-
if (relevantSeedNames.length > 0) {
|
|
24
|
-
const k = relevantSeedNames.length;
|
|
25
|
-
reasons.push(`${k} seed${k === 1 ? '' : 's'} may apply: ${relevantSeedNames.join(', ')}`);
|
|
26
|
-
}
|
|
27
|
-
return { suggested: reasons.length > 0, reasons };
|
|
28
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { type OptimizeResult } from './optimizeSpec.js';
|
|
2
|
-
export interface OptimizeAgentOptions {
|
|
3
|
-
agentId: string;
|
|
4
|
-
model?: string;
|
|
5
|
-
maxBudgetUsd?: number;
|
|
6
|
-
/** Optional model API key, injected into the spawned CLI's env. */
|
|
7
|
-
apiKey?: string;
|
|
8
|
-
signal?: AbortSignal;
|
|
9
|
-
}
|
|
10
|
-
export declare function optimizeSpecWithAgent(devRoot: string, slug: string, opts: OptimizeAgentOptions): Promise<OptimizeResult>;
|
|
11
|
-
//# sourceMappingURL=optimizeSpecWithAgent.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"optimizeSpecWithAgent.d.ts","sourceRoot":"","sources":["../../src/specs/optimizeSpecWithAgent.ts"],"names":[],"mappings":"AAUA,OAAO,EAAgB,KAAK,cAAc,EAAmB,MAAM,mBAAmB,CAAC;AAEvF,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,cAAc,CAAC,CA4BzB"}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wires optimizeSpec's injected codegen call to a real agent via invokeAgent,
|
|
3
|
-
* in "codegen mode": no MCP, no browser tools, the agent's own built-in tools
|
|
4
|
-
* disallowed — it just reads the prompt and emits the improved spec as text.
|
|
5
|
-
*
|
|
6
|
-
* Kept separate from optimizeSpec.ts so the core (prompt / extract / validate /
|
|
7
|
-
* write) stays a pure, spawn-free module that tests import directly.
|
|
8
|
-
*/
|
|
9
|
-
import { invokeAgent } from '../agents/invoke.js';
|
|
10
|
-
import { getAgent } from '../agents/registry.js';
|
|
11
|
-
import { optimizeSpec } from './optimizeSpec.js';
|
|
12
|
-
export async function optimizeSpecWithAgent(devRoot, slug, opts) {
|
|
13
|
-
const descriptor = getAgent(opts.agentId);
|
|
14
|
-
// Codegen mode: deny the agent's built-in tools so it answers with text only;
|
|
15
|
-
// pass no mcpConfig / allowedTools so it never reaches a browser.
|
|
16
|
-
const disallowedTools = descriptor?.defaultDisallowedTools
|
|
17
|
-
? [...descriptor.defaultDisallowedTools]
|
|
18
|
-
: undefined;
|
|
19
|
-
const runCodegen = async (prompt) => {
|
|
20
|
-
let streamed = '';
|
|
21
|
-
let summary = '';
|
|
22
|
-
for await (const ev of invokeAgent({
|
|
23
|
-
agentId: opts.agentId,
|
|
24
|
-
prompt,
|
|
25
|
-
model: opts.model,
|
|
26
|
-
maxBudgetUsd: opts.maxBudgetUsd,
|
|
27
|
-
apiKey: opts.apiKey,
|
|
28
|
-
signal: opts.signal,
|
|
29
|
-
disallowedTools,
|
|
30
|
-
})) {
|
|
31
|
-
if (ev.kind === 'text' && ev.text)
|
|
32
|
-
streamed += `${ev.text}\n`;
|
|
33
|
-
else if (ev.kind === 'session_end' && ev.summary)
|
|
34
|
-
summary = ev.summary;
|
|
35
|
-
}
|
|
36
|
-
// Prefer the final result summary; fall back to streamed text blocks.
|
|
37
|
-
return summary || streamed;
|
|
38
|
-
};
|
|
39
|
-
return optimizeSpec(devRoot, slug, runCodegen);
|
|
40
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { SkillStep } from '../skills/writeSkill.js';
|
|
2
|
-
import type { SpecAssertion } from './writeSpec.js';
|
|
3
|
-
export declare class CaseCsvExistsError extends Error {
|
|
4
|
-
readonly slug: string;
|
|
5
|
-
readonly path: string;
|
|
6
|
-
constructor(slug: string, path: string);
|
|
7
|
-
}
|
|
8
|
-
export interface WriteCaseCsvOptions {
|
|
9
|
-
devRoot: string;
|
|
10
|
-
name: string;
|
|
11
|
-
description?: string;
|
|
12
|
-
steps: SkillStep[];
|
|
13
|
-
assertions?: SpecAssertion[];
|
|
14
|
-
/** Optional Jira project key prefix (e.g. "PROJ"). Goes into Labels so
|
|
15
|
-
* the importer can route the test cases without rewriting the CSV.
|
|
16
|
-
* Stripped of whitespace; if empty, no project label is added. */
|
|
17
|
-
jiraProjectKey?: string;
|
|
18
|
-
/** Free-form labels appended after the default "hover" label. Split
|
|
19
|
-
* on commas/whitespace and lowercased. */
|
|
20
|
-
labels?: string;
|
|
21
|
-
overwrite?: boolean;
|
|
22
|
-
}
|
|
23
|
-
export interface WriteCaseCsvResult {
|
|
24
|
-
path: string;
|
|
25
|
-
slug: string;
|
|
26
|
-
}
|
|
27
|
-
export declare function writeCaseCsv(opts: WriteCaseCsvOptions): Promise<WriteCaseCsvResult>;
|
|
28
|
-
//# sourceMappingURL=writeCaseCsv.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"writeCaseCsv.d.ts","sourceRoot":"","sources":["../../src/specs/writeCaseCsv.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGpD,qBAAa,kBAAmB,SAAQ,KAAK;aACf,IAAI,EAAE,MAAM;aAAkB,IAAI,EAAE,MAAM;gBAA1C,IAAI,EAAE,MAAM,EAAkB,IAAI,EAAE,MAAM;CAIvE;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;IAC7B;;uEAEmE;IACnE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;+CAC2C;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;CAAE;AAEnE,wBAAsB,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAiBzF"}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Save a completed Hover session as an Xray-compatible test case CSV
|
|
3
|
-
* (one file per session, multi-row layout — one row per replayable step).
|
|
4
|
-
*
|
|
5
|
-
* Target: Atlassian Marketplace's #1 test management for Jira (Xray,
|
|
6
|
-
* ~10M users, ~100M test cases / month). The same CSV imports cleanly
|
|
7
|
-
* into Zephyr Scale and the original Jira issue importer with minor
|
|
8
|
-
* column mapping, so this is the broadest single-format hand-off into
|
|
9
|
-
* a team's test management.
|
|
10
|
-
*
|
|
11
|
-
* Schema (Xray Test Case Importer — multi-row layout):
|
|
12
|
-
*
|
|
13
|
-
* Issue Id unique grouping key, repeated on every row of the
|
|
14
|
-
* same test case. We use the slug.
|
|
15
|
-
* Summary the test case title; set on the FIRST row only.
|
|
16
|
-
* Test Type "Manual" for everything Hover emits.
|
|
17
|
-
* Priority "Medium" by default; PMs can edit post-import.
|
|
18
|
-
* Labels space-separated; "hover" plus the user-supplied set.
|
|
19
|
-
* Action one human-readable imperative per row. Reuses the
|
|
20
|
-
* humanSteps helper that also feeds the .spec.ts JSDoc.
|
|
21
|
-
* Expected Result attached to the LAST row of the case. Carries
|
|
22
|
-
* assertion hints if present, else the agent's
|
|
23
|
-
* done-summary first sentence.
|
|
24
|
-
*
|
|
25
|
-
* The "Issue Id" column is what tells Xray's importer that consecutive
|
|
26
|
-
* rows belong to the same test, even though only the first row has a
|
|
27
|
-
* Summary. The "Test Type" column tells it to instantiate the Manual
|
|
28
|
-
* Test issue type and use the Step / Expected Result fields.
|
|
29
|
-
*/
|
|
30
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
31
|
-
import { existsSync } from 'node:fs';
|
|
32
|
-
import { join } from 'node:path';
|
|
33
|
-
import { humanSteps } from './humanSteps.js';
|
|
34
|
-
import { slugify, firstSentence } from './text.js';
|
|
35
|
-
export class CaseCsvExistsError extends Error {
|
|
36
|
-
slug;
|
|
37
|
-
path;
|
|
38
|
-
constructor(slug, path) {
|
|
39
|
-
super(`Test case CSV "${slug}" already exists at ${path}`);
|
|
40
|
-
this.slug = slug;
|
|
41
|
-
this.path = path;
|
|
42
|
-
this.name = 'CaseCsvExistsError';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
export async function writeCaseCsv(opts) {
|
|
46
|
-
const slug = slugify(opts.name);
|
|
47
|
-
if (!slug)
|
|
48
|
-
throw new Error('case name must contain at least one alphanumeric character');
|
|
49
|
-
if (!opts.steps.some(s => s.kind === 'step')) {
|
|
50
|
-
throw new Error('case must contain at least one tool step to describe');
|
|
51
|
-
}
|
|
52
|
-
const dir = join(opts.devRoot, '__vibe_tests__');
|
|
53
|
-
const path = join(dir, `${slug}.case.csv`);
|
|
54
|
-
if (!opts.overwrite && existsSync(path)) {
|
|
55
|
-
throw new CaseCsvExistsError(slug, path);
|
|
56
|
-
}
|
|
57
|
-
await mkdir(dir, { recursive: true });
|
|
58
|
-
const csv = renderCsv(slug, opts);
|
|
59
|
-
await writeFile(path, csv, 'utf-8');
|
|
60
|
-
return { path, slug };
|
|
61
|
-
}
|
|
62
|
-
// ───────── helpers ─────────
|
|
63
|
-
function renderCsv(slug, opts) {
|
|
64
|
-
const rows = buildRows(slug, opts);
|
|
65
|
-
// CRLF row terminator — what Excel / Numbers / Xray's importer all
|
|
66
|
-
// assume by default. Comma column delimiter, fields with commas or
|
|
67
|
-
// newlines get wrapped in double-quotes (escaped by doubling).
|
|
68
|
-
const header = ['Issue Id', 'Summary', 'Test Type', 'Priority', 'Labels', 'Action', 'Expected Result'];
|
|
69
|
-
const lines = [header.map(escapeField).join(',')];
|
|
70
|
-
for (const r of rows)
|
|
71
|
-
lines.push(r.map(escapeField).join(','));
|
|
72
|
-
return lines.join('\r\n') + '\r\n';
|
|
73
|
-
}
|
|
74
|
-
function buildRows(slug, opts) {
|
|
75
|
-
const actions = humanSteps(opts.steps);
|
|
76
|
-
const summary = opts.description?.trim() || opts.name;
|
|
77
|
-
const expectedTail = expectedFor(opts.assertions ?? [], opts.steps);
|
|
78
|
-
const labels = buildLabels(opts.jiraProjectKey, opts.labels);
|
|
79
|
-
// Multi-row layout: one row per Action. First row carries the
|
|
80
|
-
// test-case-level fields (Summary, Test Type, Priority, Labels); the
|
|
81
|
-
// rest carry only the Issue Id + Action so Xray groups them.
|
|
82
|
-
if (actions.length === 0) {
|
|
83
|
-
// Defensive — writeCaseCsv() already throws on no replayable steps,
|
|
84
|
-
// but keep a single-row fallback so the file is still well-formed.
|
|
85
|
-
return [[slug, summary, 'Manual', 'Medium', labels, '(no replayable steps were captured)', expectedTail]];
|
|
86
|
-
}
|
|
87
|
-
const rows = [];
|
|
88
|
-
actions.forEach((action, i) => {
|
|
89
|
-
const isFirst = i === 0;
|
|
90
|
-
const isLast = i === actions.length - 1;
|
|
91
|
-
rows.push([
|
|
92
|
-
slug,
|
|
93
|
-
isFirst ? summary : '',
|
|
94
|
-
isFirst ? 'Manual' : '',
|
|
95
|
-
isFirst ? 'Medium' : '',
|
|
96
|
-
isFirst ? labels : '',
|
|
97
|
-
action,
|
|
98
|
-
isLast ? expectedTail : '',
|
|
99
|
-
]);
|
|
100
|
-
});
|
|
101
|
-
return rows;
|
|
102
|
-
}
|
|
103
|
-
function expectedFor(assertions, steps) {
|
|
104
|
-
if (assertions.length > 0) {
|
|
105
|
-
return assertions.map(a => `• ${a.hint ?? a.code}`).join('\n');
|
|
106
|
-
}
|
|
107
|
-
const done = [...steps].reverse().find(s => s.kind === 'done');
|
|
108
|
-
if (done?.summary) {
|
|
109
|
-
return firstSentence(done.summary);
|
|
110
|
-
}
|
|
111
|
-
return '';
|
|
112
|
-
}
|
|
113
|
-
function buildLabels(jiraProjectKey, labels) {
|
|
114
|
-
const set = new Set(['hover']);
|
|
115
|
-
if (jiraProjectKey?.trim())
|
|
116
|
-
set.add(jiraProjectKey.trim().toLowerCase());
|
|
117
|
-
if (labels?.trim()) {
|
|
118
|
-
labels.split(/[\s,]+/).filter(Boolean).forEach(l => set.add(l.toLowerCase()));
|
|
119
|
-
}
|
|
120
|
-
// Xray and Jira both accept space-separated labels in a single cell.
|
|
121
|
-
return [...set].join(' ');
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* RFC 4180 escaping: any field containing a quote, comma, CR, or LF
|
|
125
|
-
* gets wrapped in double-quotes; embedded quotes are doubled.
|
|
126
|
-
*/
|
|
127
|
-
function escapeField(value) {
|
|
128
|
-
if (value === '')
|
|
129
|
-
return '';
|
|
130
|
-
if (/[",\r\n]/.test(value)) {
|
|
131
|
-
return `"${value.replace(/"/g, '""')}"`;
|
|
132
|
-
}
|
|
133
|
-
return value;
|
|
134
|
-
}
|