@capagents/forgeqe 1.1.1 → 1.1.2
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/HOW_TO_USE.md +20 -4
- package/README.md +27 -1
- package/docs/user-guide.html +24 -4
- package/evals/prompts/brain/capability-question.yaml +12 -0
- package/evals/prompts/brain/failed-runs-cause.yaml +12 -0
- package/evals/prompts/brain/release-route.yaml +14 -0
- package/evals/prompts/brain/report-request.yaml +13 -0
- package/evals/prompts/brain/secret-request.yaml +9 -0
- package/evals/prompts/explore-charters/lockout.yaml +18 -0
- package/evals/prompts/explore-charters/transfer-limit.yaml +16 -0
- package/evals/prompts/refine/current-release.yaml +26 -0
- package/evals/prompts/refine/missing-item.yaml +10 -0
- package/evals/prompts/refine/question-stays-question.yaml +13 -0
- package/evals/prompts/refine/report-request.yaml +19 -0
- package/forgeqe.example.yaml +39 -0
- package/package.json +1 -1
- package/prompts/README.md +28 -7
- package/prompts/api-scenarios.md +56 -24
- package/prompts/api-tests.md +67 -51
- package/prompts/brain.md +277 -268
- package/prompts/browser-capture.md +8 -7
- package/prompts/browser-case-report.md +13 -12
- package/prompts/browser-checks.md +17 -18
- package/prompts/browser-efficiency.md +4 -1
- package/prompts/browser-error-recovery.md +5 -4
- package/prompts/browser-extra.md +1 -0
- package/prompts/browser-goal-verification.md +8 -7
- package/prompts/browser-skill-select.md +28 -1
- package/prompts/conventions.md +10 -0
- package/prompts/domain-rule.md +17 -14
- package/prompts/eval-judge.md +36 -13
- package/prompts/explore-charters.md +53 -22
- package/prompts/find-tests.md +45 -30
- package/prompts/heal.md +42 -32
- package/prompts/pr-review.md +44 -28
- package/prompts/refine.md +73 -40
- package/prompts/skill-rule.md +4 -1
- package/prompts/test-agent.md +61 -45
- package/prompts/test-reviewer.md +64 -37
- package/prompts/test-rules.md +59 -56
- package/py/forgeqe_engine/browser.py +111 -1
- package/py/forgeqe_engine/capture.py +8 -1
- package/py/forgeqe_engine/checks.py +20 -8
- package/py/forgeqe_engine/prompts.py +9 -0
- package/py/forgeqe_engine/report.py +15 -8
- package/py/forgeqe_engine/service.py +50 -28
- package/py/forgeqe_engine/skills.py +31 -7
- package/src/auth/signin.ts +30 -1
- package/src/bi.ts +899 -0
- package/src/childEnv.ts +4 -0
- package/src/cli/help.ts +10 -2
- package/src/cli/index.ts +114 -10
- package/src/config.ts +149 -0
- package/src/engine/bridge.ts +32 -2
- package/src/evals/prompts.ts +288 -0
- package/src/mcp/server.ts +8 -0
- package/src/mcp/tools.ts +4 -0
- package/src/prompts.ts +6 -3
- package/src/promptstyle.ts +159 -0
- package/src/run.ts +1 -0
- package/src/ste.ts +8 -2
package/src/bi.ts
ADDED
|
@@ -0,0 +1,899 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
import type { Readiness } from "./cases/readiness.ts"
|
|
6
|
+
import { adoSettings } from "./cases/ado.ts"
|
|
7
|
+
import { FAILURE_ACTION } from "./cases/failure.ts"
|
|
8
|
+
import { http } from "./cases/http.ts"
|
|
9
|
+
import { jiraApi } from "./cases/jira.ts"
|
|
10
|
+
import type { CaseResult, WriteBack } from "./cases/types.ts"
|
|
11
|
+
import type { AppConfig, BiConfig, BiDatasetConfig } from "./config.ts"
|
|
12
|
+
import { counts as suiteCounts } from "./suite/report.ts"
|
|
13
|
+
|
|
14
|
+
type BuildOverrides = { evidenceDir?: string; dataDir?: string; buildDir?: string }
|
|
15
|
+
type Paths = {
|
|
16
|
+
evidenceDir: string
|
|
17
|
+
dataDir: string
|
|
18
|
+
buildDir: string
|
|
19
|
+
sourceDir: string
|
|
20
|
+
pagesDir: string
|
|
21
|
+
externalDataDir: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type SuiteFile = {
|
|
25
|
+
name?: string
|
|
26
|
+
startedAt?: string
|
|
27
|
+
ms?: number
|
|
28
|
+
stopped?: boolean
|
|
29
|
+
results?: CaseResult[]
|
|
30
|
+
writeBack?: WriteBack[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type SuiteRow = {
|
|
34
|
+
suite_id: string
|
|
35
|
+
suite_scope: "suite" | "monitor"
|
|
36
|
+
suite_name: string
|
|
37
|
+
started_at: string
|
|
38
|
+
duration_ms: number
|
|
39
|
+
total_cases: number
|
|
40
|
+
passed_cases: number
|
|
41
|
+
failed_cases: number
|
|
42
|
+
error_cases: number
|
|
43
|
+
stopped_cases: number
|
|
44
|
+
coded_cases: number
|
|
45
|
+
coded_passed_cases: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type CaseRow = {
|
|
49
|
+
suite_id: string
|
|
50
|
+
suite_scope: "suite" | "monitor"
|
|
51
|
+
case_id: string
|
|
52
|
+
case_title: string
|
|
53
|
+
case_source: string
|
|
54
|
+
case_status: string
|
|
55
|
+
started_at: string
|
|
56
|
+
finished_at: string
|
|
57
|
+
duration_ms: number
|
|
58
|
+
tokens: number
|
|
59
|
+
final_url: string
|
|
60
|
+
run_dir: string
|
|
61
|
+
group: string
|
|
62
|
+
tags: string
|
|
63
|
+
requirements: string
|
|
64
|
+
failure_type: string
|
|
65
|
+
failure_action: string
|
|
66
|
+
bug_label: string
|
|
67
|
+
bug_url: string
|
|
68
|
+
code_status: string
|
|
69
|
+
code_test_file: string
|
|
70
|
+
code_command: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type StepRow = {
|
|
74
|
+
suite_id: string
|
|
75
|
+
suite_scope: "suite" | "monitor"
|
|
76
|
+
case_id: string
|
|
77
|
+
step_number: number
|
|
78
|
+
action: string
|
|
79
|
+
data: string
|
|
80
|
+
expected: string
|
|
81
|
+
status: string
|
|
82
|
+
actual: string
|
|
83
|
+
cause: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type WriteBackRow = {
|
|
87
|
+
suite_id: string
|
|
88
|
+
suite_scope: "suite" | "monitor"
|
|
89
|
+
case_id: string
|
|
90
|
+
source: string
|
|
91
|
+
text: string
|
|
92
|
+
url: string
|
|
93
|
+
error: string
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type ReadinessRow = {
|
|
97
|
+
report_id: string
|
|
98
|
+
scope: string
|
|
99
|
+
verdict: string
|
|
100
|
+
pass_rate: number | ""
|
|
101
|
+
stories: number
|
|
102
|
+
untested_stories: number
|
|
103
|
+
cases: number
|
|
104
|
+
automated_cases: number
|
|
105
|
+
executed_cases: number
|
|
106
|
+
passed_cases: number
|
|
107
|
+
failed_cases: number
|
|
108
|
+
blocked_cases: number
|
|
109
|
+
not_run_cases: number
|
|
110
|
+
flaky_cases: number
|
|
111
|
+
quarantined_cases: number
|
|
112
|
+
open_bugs: number
|
|
113
|
+
blockers: string
|
|
114
|
+
risks: string
|
|
115
|
+
notes: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type ReadinessStoryRow = {
|
|
119
|
+
report_id: string
|
|
120
|
+
story_id: number
|
|
121
|
+
story_type: string
|
|
122
|
+
story_title: string
|
|
123
|
+
state: string
|
|
124
|
+
url: string
|
|
125
|
+
area: string
|
|
126
|
+
case_count: number
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
type ReadinessCaseRow = {
|
|
130
|
+
report_id: string
|
|
131
|
+
story_id: number
|
|
132
|
+
case_id: number
|
|
133
|
+
case_title: string
|
|
134
|
+
automated: string
|
|
135
|
+
outcome: string
|
|
136
|
+
from: string
|
|
137
|
+
when: string
|
|
138
|
+
flaky: string
|
|
139
|
+
quarantined: string
|
|
140
|
+
url: string
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
type ExternalDatasetRow = {
|
|
144
|
+
dataset_id: string
|
|
145
|
+
title: string
|
|
146
|
+
source_kind: string
|
|
147
|
+
row_count: number
|
|
148
|
+
refreshed_at: string
|
|
149
|
+
page: string
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
type ExternalExport = { row: ExternalDatasetRow; files: string[] }
|
|
153
|
+
|
|
154
|
+
export type BiSyncResult = {
|
|
155
|
+
evidenceDir: string
|
|
156
|
+
dataDir: string
|
|
157
|
+
buildDir: string
|
|
158
|
+
suites: number
|
|
159
|
+
caseResults: number
|
|
160
|
+
readinessReports: number
|
|
161
|
+
externalDatasets: number
|
|
162
|
+
dataFiles: string[]
|
|
163
|
+
pageFiles: string[]
|
|
164
|
+
summaryFile: string
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export type BiBuildResult = BiSyncResult & { indexFile: string }
|
|
168
|
+
|
|
169
|
+
export async function initBiProject(cfg: AppConfig, overrides: BuildOverrides = {}): Promise<{ evidenceDir: string; files: string[] }> {
|
|
170
|
+
const paths = resolvePaths(cfg.bi || defaultBiConfig(cfg), overrides)
|
|
171
|
+
await ensureEvidenceProject(paths.evidenceDir)
|
|
172
|
+
const files = writeManagedEvidenceFiles(cfg, paths)
|
|
173
|
+
return { evidenceDir: paths.evidenceDir, files }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function syncBiData(cfg: AppConfig, overrides: BuildOverrides = {}): Promise<BiSyncResult> {
|
|
177
|
+
const bi = cfg.bi || defaultBiConfig(cfg)
|
|
178
|
+
const paths = resolvePaths(bi, overrides)
|
|
179
|
+
await ensureEvidenceProject(paths.evidenceDir)
|
|
180
|
+
const pageFiles = writeManagedEvidenceFiles({ ...cfg, bi }, paths)
|
|
181
|
+
|
|
182
|
+
mkdirSync(paths.dataDir, { recursive: true })
|
|
183
|
+
mkdirSync(paths.buildDir, { recursive: true })
|
|
184
|
+
mkdirSync(paths.externalDataDir, { recursive: true })
|
|
185
|
+
|
|
186
|
+
const suites = collectSuiteData(cfg)
|
|
187
|
+
const readiness = collectReadiness(cfg)
|
|
188
|
+
const external = await exportExternalDatasets({ ...cfg, bi }, paths)
|
|
189
|
+
|
|
190
|
+
const dataFiles = [
|
|
191
|
+
writeCsv(path.join(paths.dataDir, "suites.csv"), SUITE_HEADERS, suites.suites),
|
|
192
|
+
writeCsv(path.join(paths.dataDir, "case_results.csv"), CASE_HEADERS, suites.cases),
|
|
193
|
+
writeCsv(path.join(paths.dataDir, "step_results.csv"), STEP_HEADERS, suites.steps),
|
|
194
|
+
writeCsv(path.join(paths.dataDir, "writebacks.csv"), WRITEBACK_HEADERS, suites.writebacks),
|
|
195
|
+
writeCsv(path.join(paths.dataDir, "readiness.csv"), READINESS_HEADERS, readiness.summary),
|
|
196
|
+
writeCsv(path.join(paths.dataDir, "readiness_stories.csv"), READINESS_STORY_HEADERS, readiness.stories),
|
|
197
|
+
writeCsv(path.join(paths.dataDir, "readiness_cases.csv"), READINESS_CASE_HEADERS, readiness.cases),
|
|
198
|
+
writeCsv(path.join(paths.dataDir, "external_datasets.csv"), EXTERNAL_HEADERS, external.map((e) => e.row)),
|
|
199
|
+
...external.flatMap((e) => e.files),
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
const summaryFile = path.join(paths.buildDir, "summary.md")
|
|
203
|
+
writeFileSync(summaryFile, summaryMarkdown({ ...cfg, bi }, paths, suites, readiness, external), "utf8")
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
evidenceDir: paths.evidenceDir,
|
|
207
|
+
dataDir: paths.dataDir,
|
|
208
|
+
buildDir: paths.buildDir,
|
|
209
|
+
suites: suites.suites.length,
|
|
210
|
+
caseResults: suites.cases.length,
|
|
211
|
+
readinessReports: readiness.summary.length,
|
|
212
|
+
externalDatasets: external.length,
|
|
213
|
+
dataFiles,
|
|
214
|
+
pageFiles,
|
|
215
|
+
summaryFile,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function buildBi(cfg: AppConfig, overrides: BuildOverrides = {}): Promise<BiBuildResult> {
|
|
220
|
+
const synced = await syncBiData(cfg, overrides)
|
|
221
|
+
await runEvidence(["build"], synced.evidenceDir, { EVIDENCE_BUILD_DIR: synced.buildDir })
|
|
222
|
+
return { ...synced, indexFile: path.join(synced.buildDir, "index.html") }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function resolvePaths(cfg: BiConfig, overrides: BuildOverrides): Paths {
|
|
226
|
+
const evidenceDir = path.resolve(overrides.evidenceDir || cfg.evidenceDir)
|
|
227
|
+
const dataDir = path.resolve(overrides.dataDir || cfg.dataDir)
|
|
228
|
+
const buildDir = path.resolve(overrides.buildDir || cfg.buildDir)
|
|
229
|
+
return {
|
|
230
|
+
evidenceDir,
|
|
231
|
+
dataDir,
|
|
232
|
+
buildDir,
|
|
233
|
+
sourceDir: path.join(evidenceDir, "sources", "forgeqe"),
|
|
234
|
+
pagesDir: path.join(evidenceDir, "pages"),
|
|
235
|
+
externalDataDir: path.join(dataDir, "external"),
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function defaultBiConfig(cfg: AppConfig): BiConfig {
|
|
240
|
+
const evidenceDir = path.resolve("evidence")
|
|
241
|
+
return {
|
|
242
|
+
title: "ForgeQE BI",
|
|
243
|
+
evidenceDir,
|
|
244
|
+
dataDir: path.join(evidenceDir, "sources", "forgeqe", "data"),
|
|
245
|
+
buildDir: path.join(cfg.export.outDir, "bi"),
|
|
246
|
+
datasets: [],
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function ensureEvidenceProject(dir: string): Promise<void> {
|
|
251
|
+
if (existsSync(path.join(dir, "evidence.config.yaml"))) return
|
|
252
|
+
mkdirSync(path.dirname(dir), { recursive: true })
|
|
253
|
+
await runEvidence(["init", dir], process.cwd())
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function writeManagedEvidenceFiles(cfg: AppConfig, paths: Paths): string[] {
|
|
257
|
+
const bi = cfg.bi || defaultBiConfig(cfg)
|
|
258
|
+
mkdirSync(paths.sourceDir, { recursive: true })
|
|
259
|
+
mkdirSync(paths.pagesDir, { recursive: true })
|
|
260
|
+
|
|
261
|
+
const files: string[] = []
|
|
262
|
+
files.push(writeText(path.join(paths.sourceDir, "connection.yaml"), `name: forgeqe\ntype: duckdb\noptions:\n filename: ":memory:"\n`))
|
|
263
|
+
|
|
264
|
+
const rel = (file: string) => sqlPath(path.relative(paths.sourceDir, file))
|
|
265
|
+
const managedQueries: Array<[string, string]> = [
|
|
266
|
+
["suites.sql", `select * from read_csv_auto('${rel(path.join(paths.dataDir, "suites.csv"))}', header = true)`],
|
|
267
|
+
["case_results.sql", `select * from read_csv_auto('${rel(path.join(paths.dataDir, "case_results.csv"))}', header = true)`],
|
|
268
|
+
["step_results.sql", `select * from read_csv_auto('${rel(path.join(paths.dataDir, "step_results.csv"))}', header = true)`],
|
|
269
|
+
["writebacks.sql", `select * from read_csv_auto('${rel(path.join(paths.dataDir, "writebacks.csv"))}', header = true)`],
|
|
270
|
+
["readiness.sql", `select * from read_csv_auto('${rel(path.join(paths.dataDir, "readiness.csv"))}', header = true)`],
|
|
271
|
+
["readiness_stories.sql", `select * from read_csv_auto('${rel(path.join(paths.dataDir, "readiness_stories.csv"))}', header = true)`],
|
|
272
|
+
["readiness_cases.sql", `select * from read_csv_auto('${rel(path.join(paths.dataDir, "readiness_cases.csv"))}', header = true)`],
|
|
273
|
+
["external_datasets.sql", `select * from read_csv_auto('${rel(path.join(paths.dataDir, "external_datasets.csv"))}', header = true)`],
|
|
274
|
+
]
|
|
275
|
+
for (const dataset of bi.datasets) {
|
|
276
|
+
managedQueries.push([`${dataset.id}.sql`, `select * from read_csv_auto('${rel(path.join(paths.externalDataDir, `${dataset.id}.csv`))}', header = true)`])
|
|
277
|
+
}
|
|
278
|
+
for (const [name, sql] of managedQueries) files.push(writeText(path.join(paths.sourceDir, name), `${sql}\n`))
|
|
279
|
+
|
|
280
|
+
files.push(writeText(path.join(paths.pagesDir, "home.md"), homePage({ ...cfg, bi })))
|
|
281
|
+
files.push(writeText(path.join(paths.pagesDir, "forgeqe-runs.md"), runsPage()))
|
|
282
|
+
files.push(writeText(path.join(paths.pagesDir, "forgeqe-readiness.md"), readinessPage()))
|
|
283
|
+
files.push(writeText(path.join(paths.pagesDir, "forgeqe-external.md"), externalPage()))
|
|
284
|
+
for (const dataset of bi.datasets) {
|
|
285
|
+
files.push(writeText(path.join(paths.pagesDir, `forgeqe-dataset-${dataset.id}.md`), datasetPage(dataset)))
|
|
286
|
+
}
|
|
287
|
+
return files
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function homePage(cfg: AppConfig): string {
|
|
291
|
+
const bi = cfg.bi || defaultBiConfig(cfg)
|
|
292
|
+
const datasetLinks = bi.datasets.length
|
|
293
|
+
? bi.datasets.map((d) => `- [${d.title}](./forgeqe-dataset-${d.id})`).join("\n")
|
|
294
|
+
: "- No external datasets are configured yet. Add them under `bi.datasets` in `forgeqe.yaml`."
|
|
295
|
+
return [
|
|
296
|
+
"# ForgeQE BI",
|
|
297
|
+
"",
|
|
298
|
+
"This static site is generated by `forgeqe bi build`. ForgeQE exports CSV datasets, Evidence queries them with DuckDB, and the build writes static HTML with no always-on backend.",
|
|
299
|
+
"",
|
|
300
|
+
"## Pages",
|
|
301
|
+
"",
|
|
302
|
+
"- [Suite and monitor history](./forgeqe-runs)",
|
|
303
|
+
"- [Release readiness history](./forgeqe-readiness)",
|
|
304
|
+
"- [External datasets](./forgeqe-external)",
|
|
305
|
+
datasetLinks,
|
|
306
|
+
"",
|
|
307
|
+
sqlBlock(
|
|
308
|
+
"suite_summary",
|
|
309
|
+
`select
|
|
310
|
+
count(*) as suites,
|
|
311
|
+
coalesce(sum(total_cases), 0) as total_cases,
|
|
312
|
+
coalesce(sum(passed_cases), 0) as passed_cases,
|
|
313
|
+
coalesce(sum(failed_cases), 0) as failed_cases,
|
|
314
|
+
coalesce(sum(error_cases), 0) as error_cases
|
|
315
|
+
from forgeqe.suites`,
|
|
316
|
+
),
|
|
317
|
+
"",
|
|
318
|
+
"{% table data=\"suite_summary\" /%}",
|
|
319
|
+
"",
|
|
320
|
+
"## Case results by source",
|
|
321
|
+
"",
|
|
322
|
+
sqlBlock(
|
|
323
|
+
"cases_by_source",
|
|
324
|
+
`select
|
|
325
|
+
case_source,
|
|
326
|
+
count(*) as total_results,
|
|
327
|
+
sum(case when case_status = 'passed' then 1 else 0 end) as passed_results,
|
|
328
|
+
sum(case when case_status = 'failed' then 1 else 0 end) as failed_results,
|
|
329
|
+
sum(case when case_status = 'error' then 1 else 0 end) as error_results
|
|
330
|
+
from forgeqe.case_results
|
|
331
|
+
group by 1
|
|
332
|
+
order by total_results desc, case_source`,
|
|
333
|
+
),
|
|
334
|
+
"",
|
|
335
|
+
"{% table data=\"cases_by_source\" /%}",
|
|
336
|
+
"",
|
|
337
|
+
"## External datasets",
|
|
338
|
+
"",
|
|
339
|
+
sqlBlock(
|
|
340
|
+
"external_datasets_home",
|
|
341
|
+
`select dataset_id, title, source_kind, row_count, refreshed_at, page
|
|
342
|
+
from forgeqe.external_datasets
|
|
343
|
+
order by title`,
|
|
344
|
+
),
|
|
345
|
+
"",
|
|
346
|
+
"{% table data=\"external_datasets_home\" /%}",
|
|
347
|
+
"",
|
|
348
|
+
].join("\n")
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function runsPage(): string {
|
|
352
|
+
return [
|
|
353
|
+
"# Suite and Monitor History",
|
|
354
|
+
"",
|
|
355
|
+
sqlBlock(
|
|
356
|
+
"suite_timeline",
|
|
357
|
+
`select
|
|
358
|
+
started_at,
|
|
359
|
+
suite_scope,
|
|
360
|
+
suite_name,
|
|
361
|
+
total_cases,
|
|
362
|
+
passed_cases,
|
|
363
|
+
failed_cases,
|
|
364
|
+
error_cases,
|
|
365
|
+
stopped_cases,
|
|
366
|
+
duration_ms
|
|
367
|
+
from forgeqe.suites
|
|
368
|
+
order by started_at desc`,
|
|
369
|
+
),
|
|
370
|
+
"",
|
|
371
|
+
"{% table data=\"suite_timeline\" /%}",
|
|
372
|
+
"",
|
|
373
|
+
"## Slowest case results",
|
|
374
|
+
"",
|
|
375
|
+
sqlBlock(
|
|
376
|
+
"slowest_cases",
|
|
377
|
+
`select
|
|
378
|
+
suite_id,
|
|
379
|
+
case_id,
|
|
380
|
+
case_title,
|
|
381
|
+
case_source,
|
|
382
|
+
case_status,
|
|
383
|
+
duration_ms,
|
|
384
|
+
tokens
|
|
385
|
+
from forgeqe.case_results
|
|
386
|
+
order by duration_ms desc
|
|
387
|
+
limit 50`,
|
|
388
|
+
),
|
|
389
|
+
"",
|
|
390
|
+
"{% table data=\"slowest_cases\" /%}",
|
|
391
|
+
"",
|
|
392
|
+
].join("\n")
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function readinessPage(): string {
|
|
396
|
+
return [
|
|
397
|
+
"# Release Readiness History",
|
|
398
|
+
"",
|
|
399
|
+
sqlBlock(
|
|
400
|
+
"readiness_summary",
|
|
401
|
+
`select
|
|
402
|
+
report_id,
|
|
403
|
+
scope,
|
|
404
|
+
verdict,
|
|
405
|
+
pass_rate,
|
|
406
|
+
stories,
|
|
407
|
+
cases,
|
|
408
|
+
open_bugs,
|
|
409
|
+
blockers,
|
|
410
|
+
risks
|
|
411
|
+
from forgeqe.readiness
|
|
412
|
+
order by report_id desc`,
|
|
413
|
+
),
|
|
414
|
+
"",
|
|
415
|
+
"{% table data=\"readiness_summary\" /%}",
|
|
416
|
+
"",
|
|
417
|
+
"## Stories in the latest readiness reports",
|
|
418
|
+
"",
|
|
419
|
+
sqlBlock(
|
|
420
|
+
"readiness_story_table",
|
|
421
|
+
`select
|
|
422
|
+
report_id,
|
|
423
|
+
story_id,
|
|
424
|
+
story_type,
|
|
425
|
+
story_title,
|
|
426
|
+
state,
|
|
427
|
+
area,
|
|
428
|
+
case_count
|
|
429
|
+
from forgeqe.readiness_stories
|
|
430
|
+
order by report_id desc, story_id`,
|
|
431
|
+
),
|
|
432
|
+
"",
|
|
433
|
+
"{% table data=\"readiness_story_table\" /%}",
|
|
434
|
+
"",
|
|
435
|
+
].join("\n")
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function externalPage(): string {
|
|
439
|
+
return [
|
|
440
|
+
"# External Datasets",
|
|
441
|
+
"",
|
|
442
|
+
"ForgeQE refreshes these datasets from Azure DevOps or Jira during `forgeqe bi sync` and `forgeqe bi build`.",
|
|
443
|
+
"",
|
|
444
|
+
sqlBlock(
|
|
445
|
+
"external_datasets_table",
|
|
446
|
+
`select dataset_id, title, source_kind, row_count, refreshed_at, page
|
|
447
|
+
from forgeqe.external_datasets
|
|
448
|
+
order by title`,
|
|
449
|
+
),
|
|
450
|
+
"",
|
|
451
|
+
"{% table data=\"external_datasets_table\" /%}",
|
|
452
|
+
"",
|
|
453
|
+
].join("\n")
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function datasetPage(dataset: BiDatasetConfig): string {
|
|
457
|
+
return [
|
|
458
|
+
`# ${dataset.title}`,
|
|
459
|
+
"",
|
|
460
|
+
`Source kind: \`${dataset.kind}\``,
|
|
461
|
+
"",
|
|
462
|
+
sqlBlock("dataset_stats", `select count(*) as rows from forgeqe.${dataset.id}`),
|
|
463
|
+
"",
|
|
464
|
+
"{% table data=\"dataset_stats\" /%}",
|
|
465
|
+
"",
|
|
466
|
+
sqlBlock("dataset_preview", `select * from forgeqe.${dataset.id} limit 100`),
|
|
467
|
+
"",
|
|
468
|
+
"{% table data=\"dataset_preview\" /%}",
|
|
469
|
+
"",
|
|
470
|
+
].join("\n")
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function collectSuiteData(cfg: AppConfig): { suites: SuiteRow[]; cases: CaseRow[]; steps: StepRow[]; writebacks: WriteBackRow[] } {
|
|
474
|
+
const suites: SuiteRow[] = []
|
|
475
|
+
const cases: CaseRow[] = []
|
|
476
|
+
const steps: StepRow[] = []
|
|
477
|
+
const writebacks: WriteBackRow[] = []
|
|
478
|
+
for (const [root, scope] of [
|
|
479
|
+
[cfg.suite.outDir, "suite" as const],
|
|
480
|
+
[cfg.monitor.outDir, "monitor" as const],
|
|
481
|
+
] as Array<[string, "suite" | "monitor"]>) {
|
|
482
|
+
for (const dir of suiteDirs(root)) {
|
|
483
|
+
const suite = readJson<SuiteFile>(path.join(dir, "suite.json"))
|
|
484
|
+
if (!suite) continue
|
|
485
|
+
const results = suite.results || []
|
|
486
|
+
const c = suiteCounts(results)
|
|
487
|
+
suites.push({
|
|
488
|
+
suite_id: path.basename(dir),
|
|
489
|
+
suite_scope: scope,
|
|
490
|
+
suite_name: suite.name || path.basename(dir),
|
|
491
|
+
started_at: suite.startedAt || "",
|
|
492
|
+
duration_ms: Number(suite.ms || 0),
|
|
493
|
+
total_cases: c.total,
|
|
494
|
+
passed_cases: c.passed,
|
|
495
|
+
failed_cases: c.failed,
|
|
496
|
+
error_cases: c.errors,
|
|
497
|
+
stopped_cases: c.stopped,
|
|
498
|
+
coded_cases: c.coded,
|
|
499
|
+
coded_passed_cases: c.codePassed,
|
|
500
|
+
})
|
|
501
|
+
for (const result of results) {
|
|
502
|
+
cases.push({
|
|
503
|
+
suite_id: path.basename(dir),
|
|
504
|
+
suite_scope: scope,
|
|
505
|
+
case_id: result.case.id,
|
|
506
|
+
case_title: result.case.title,
|
|
507
|
+
case_source: result.case.source,
|
|
508
|
+
case_status: result.status,
|
|
509
|
+
started_at: result.startedAt || "",
|
|
510
|
+
finished_at: result.finishedAt || "",
|
|
511
|
+
duration_ms: Number(result.ms || 0),
|
|
512
|
+
tokens: Number(result.tokens || 0),
|
|
513
|
+
final_url: result.finalUrl || "",
|
|
514
|
+
run_dir: result.runDir || "",
|
|
515
|
+
group: result.case.group || "",
|
|
516
|
+
tags: (result.case.tags || []).join("; "),
|
|
517
|
+
requirements: (result.case.requirements || []).join("; "),
|
|
518
|
+
failure_type: result.failure?.type || "",
|
|
519
|
+
failure_action: result.failure ? FAILURE_ACTION[result.failure.type] : "",
|
|
520
|
+
bug_label: result.bug?.label || "",
|
|
521
|
+
bug_url: result.bug?.url || "",
|
|
522
|
+
code_status: result.code?.status || "",
|
|
523
|
+
code_test_file: result.code?.testFile || "",
|
|
524
|
+
code_command: result.code?.command || "",
|
|
525
|
+
})
|
|
526
|
+
const verdicts = new Map((result.stepResults || []).map((v) => [v.step, v]))
|
|
527
|
+
result.case.steps.forEach((step, i) => {
|
|
528
|
+
const verdict = verdicts.get(i + 1)
|
|
529
|
+
steps.push({
|
|
530
|
+
suite_id: path.basename(dir),
|
|
531
|
+
suite_scope: scope,
|
|
532
|
+
case_id: result.case.id,
|
|
533
|
+
step_number: i + 1,
|
|
534
|
+
action: step.action,
|
|
535
|
+
data: step.data || "",
|
|
536
|
+
expected: step.expected || "",
|
|
537
|
+
status: verdict?.status || "",
|
|
538
|
+
actual: verdict?.actual || "",
|
|
539
|
+
cause: verdict?.cause || "",
|
|
540
|
+
})
|
|
541
|
+
})
|
|
542
|
+
}
|
|
543
|
+
for (const post of suite.writeBack || []) {
|
|
544
|
+
writebacks.push({
|
|
545
|
+
suite_id: path.basename(dir),
|
|
546
|
+
suite_scope: scope,
|
|
547
|
+
case_id: post.caseId || "",
|
|
548
|
+
source: post.source,
|
|
549
|
+
text: post.text,
|
|
550
|
+
url: post.url || "",
|
|
551
|
+
error: post.error || "",
|
|
552
|
+
})
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
suites.sort((a, b) => a.started_at.localeCompare(b.started_at))
|
|
557
|
+
cases.sort((a, b) => a.started_at.localeCompare(b.started_at) || a.case_id.localeCompare(b.case_id))
|
|
558
|
+
return { suites, cases, steps, writebacks }
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function collectReadiness(cfg: AppConfig): { summary: ReadinessRow[]; stories: ReadinessStoryRow[]; cases: ReadinessCaseRow[] } {
|
|
562
|
+
const out = { summary: [] as ReadinessRow[], stories: [] as ReadinessStoryRow[], cases: [] as ReadinessCaseRow[] }
|
|
563
|
+
const root = path.join(cfg.export.outDir, "readiness")
|
|
564
|
+
if (!existsSync(root)) return out
|
|
565
|
+
for (const name of readdirSync(root).sort()) {
|
|
566
|
+
const report = readJson<Readiness>(path.join(root, name, "readiness.json"))
|
|
567
|
+
if (!report?.scope || !report.counts) continue
|
|
568
|
+
out.summary.push({
|
|
569
|
+
report_id: name,
|
|
570
|
+
scope: report.scope,
|
|
571
|
+
verdict: report.verdict,
|
|
572
|
+
pass_rate: report.passRate ?? "",
|
|
573
|
+
stories: report.counts.stories,
|
|
574
|
+
untested_stories: report.counts.untested,
|
|
575
|
+
cases: report.counts.cases,
|
|
576
|
+
automated_cases: report.counts.automated,
|
|
577
|
+
executed_cases: report.counts.executed,
|
|
578
|
+
passed_cases: report.counts.passed,
|
|
579
|
+
failed_cases: report.counts.failed,
|
|
580
|
+
blocked_cases: report.counts.blocked,
|
|
581
|
+
not_run_cases: report.counts.notRun,
|
|
582
|
+
flaky_cases: report.counts.flaky,
|
|
583
|
+
quarantined_cases: report.counts.quarantined,
|
|
584
|
+
open_bugs: report.bugs.length,
|
|
585
|
+
blockers: report.blockers.join(" | "),
|
|
586
|
+
risks: report.risks.join(" | "),
|
|
587
|
+
notes: report.notes.join(" | "),
|
|
588
|
+
})
|
|
589
|
+
for (const story of report.stories) {
|
|
590
|
+
out.stories.push({
|
|
591
|
+
report_id: name,
|
|
592
|
+
story_id: story.id,
|
|
593
|
+
story_type: story.type,
|
|
594
|
+
story_title: story.title,
|
|
595
|
+
state: story.state,
|
|
596
|
+
url: story.url,
|
|
597
|
+
area: story.area || "",
|
|
598
|
+
case_count: story.cases.length,
|
|
599
|
+
})
|
|
600
|
+
for (const tc of story.cases) {
|
|
601
|
+
out.cases.push({
|
|
602
|
+
report_id: name,
|
|
603
|
+
story_id: story.id,
|
|
604
|
+
case_id: tc.id,
|
|
605
|
+
case_title: tc.title,
|
|
606
|
+
automated: tc.automated ? "yes" : "no",
|
|
607
|
+
outcome: tc.outcome,
|
|
608
|
+
from: tc.from || "",
|
|
609
|
+
when: tc.when || "",
|
|
610
|
+
flaky: tc.flaky ? "yes" : "no",
|
|
611
|
+
quarantined: tc.quarantined ? "yes" : "no",
|
|
612
|
+
url: tc.url,
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return out
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function exportExternalDatasets(cfg: AppConfig, paths: Paths): Promise<ExternalExport[]> {
|
|
621
|
+
const bi = cfg.bi || defaultBiConfig(cfg)
|
|
622
|
+
const exports: ExternalExport[] = []
|
|
623
|
+
for (const dataset of bi.datasets) {
|
|
624
|
+
if (dataset.kind === "ado_wiql") {
|
|
625
|
+
const rows = await fetchAdoRows(cfg, dataset)
|
|
626
|
+
const file = path.join(paths.externalDataDir, `${dataset.id}.csv`)
|
|
627
|
+
exports.push({
|
|
628
|
+
row: { dataset_id: dataset.id, title: dataset.title, source_kind: dataset.kind, row_count: rows.length, refreshed_at: new Date().toISOString(), page: `forgeqe-dataset-${dataset.id}` },
|
|
629
|
+
files: [writeCsv(file, ADO_DATASET_HEADERS, rows)],
|
|
630
|
+
})
|
|
631
|
+
} else {
|
|
632
|
+
const rows = await fetchJiraRows(cfg, dataset)
|
|
633
|
+
const file = path.join(paths.externalDataDir, `${dataset.id}.csv`)
|
|
634
|
+
exports.push({
|
|
635
|
+
row: { dataset_id: dataset.id, title: dataset.title, source_kind: dataset.kind, row_count: rows.length, refreshed_at: new Date().toISOString(), page: `forgeqe-dataset-${dataset.id}` },
|
|
636
|
+
files: [writeCsv(file, JIRA_DATASET_HEADERS, rows)],
|
|
637
|
+
})
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return exports
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async function fetchAdoRows(cfg: AppConfig, dataset: Extract<BiDatasetConfig, { kind: "ado_wiql" }>): Promise<Record<string, string | number>[]> {
|
|
644
|
+
const { auth, org, project: configured } = adoSettings(cfg.sources)
|
|
645
|
+
const project = dataset.project || configured
|
|
646
|
+
const base = project ? `${org}/${encodeURIComponent(project)}` : org
|
|
647
|
+
const { data } = await http<{ workItems?: { id: number }[]; workItemRelations?: { source?: { id?: number } | null; target?: { id?: number } | null }[] }>(
|
|
648
|
+
`${base}/_apis/wit/wiql?api-version=7.1&$top=500`,
|
|
649
|
+
{ auth, body: { query: dataset.wiql } },
|
|
650
|
+
)
|
|
651
|
+
const ids = [...new Set([...(data.workItems || []).map((w) => w.id), ...(data.workItemRelations || []).flatMap((r) => [r.source?.id, r.target?.id]).filter((id): id is number => typeof id === "number")])].slice(0, 500)
|
|
652
|
+
if (!ids.length) return []
|
|
653
|
+
const fields = ["System.Id", "System.WorkItemType", "System.Title", "System.State", "System.AssignedTo", "System.ChangedDate", "System.IterationPath", "System.TeamProject"]
|
|
654
|
+
const rows: Record<string, string | number>[] = []
|
|
655
|
+
for (let i = 0; i < ids.length; i += 200) {
|
|
656
|
+
const batch = await http<{ value?: { id: number; fields: Record<string, unknown> }[] }>(`${org}/_apis/wit/workitemsbatch?api-version=7.1`, { auth, body: { ids: ids.slice(i, i + 200), fields } })
|
|
657
|
+
for (const item of batch.data.value || []) {
|
|
658
|
+
rows.push({
|
|
659
|
+
id: item.id,
|
|
660
|
+
project: String(item.fields["System.TeamProject"] || project || ""),
|
|
661
|
+
type: String(item.fields["System.WorkItemType"] || ""),
|
|
662
|
+
title: String(item.fields["System.Title"] || ""),
|
|
663
|
+
state: String(item.fields["System.State"] || ""),
|
|
664
|
+
assigned_to: displayName(item.fields["System.AssignedTo"]),
|
|
665
|
+
changed_at: String(item.fields["System.ChangedDate"] || ""),
|
|
666
|
+
iteration: String(item.fields["System.IterationPath"] || ""),
|
|
667
|
+
})
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return rows
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function fetchJiraRows(cfg: AppConfig, dataset: Extract<BiDatasetConfig, { kind: "jira_jql" }>): Promise<Record<string, string>[]> {
|
|
674
|
+
const jira = jiraApi(cfg.sources)
|
|
675
|
+
const fields = "summary,status,statuscategory,issuetype,priority,assignee,reporter,labels,components,parent,created,updated"
|
|
676
|
+
const rows: Record<string, string>[] = []
|
|
677
|
+
if (jira.cloud) {
|
|
678
|
+
let next: string | undefined
|
|
679
|
+
do {
|
|
680
|
+
const q = new URLSearchParams({ jql: dataset.jql, fields, maxResults: "100", ...(next ? { nextPageToken: next } : {}) })
|
|
681
|
+
const { data } = await http<{ issues?: any[]; nextPageToken?: string; isLast?: boolean }>(`${jira.api}/search/jql?${q}`, { auth: jira.auth })
|
|
682
|
+
for (const issue of data.issues || []) rows.push(jiraRow(jira.url, issue))
|
|
683
|
+
next = data.isLast === false || data.nextPageToken ? data.nextPageToken : undefined
|
|
684
|
+
} while (next)
|
|
685
|
+
} else {
|
|
686
|
+
let startAt = 0
|
|
687
|
+
for (;;) {
|
|
688
|
+
const q = new URLSearchParams({ jql: dataset.jql, fields, maxResults: "100", startAt: String(startAt) })
|
|
689
|
+
const { data } = await http<{ issues?: any[]; total?: number }>(`${jira.api}/search?${q}`, { auth: jira.auth })
|
|
690
|
+
const page = data.issues || []
|
|
691
|
+
for (const issue of page) rows.push(jiraRow(jira.url, issue))
|
|
692
|
+
startAt += page.length
|
|
693
|
+
if (!page.length || startAt >= Number(data.total || 0)) break
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return rows
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function jiraRow(baseUrl: string, issue: Record<string, any>): Record<string, string> {
|
|
700
|
+
const fields = (issue.fields || {}) as Record<string, any>
|
|
701
|
+
return {
|
|
702
|
+
key: String(issue.key || ""),
|
|
703
|
+
summary: String(fields.summary || ""),
|
|
704
|
+
status: String(fields.status?.name || ""),
|
|
705
|
+
status_category: String(fields.status?.statusCategory?.name || fields.statuscategory?.name || ""),
|
|
706
|
+
issue_type: String(fields.issuetype?.name || ""),
|
|
707
|
+
priority: String(fields.priority?.name || ""),
|
|
708
|
+
assignee: displayName(fields.assignee),
|
|
709
|
+
reporter: displayName(fields.reporter),
|
|
710
|
+
parent_key: String(fields.parent?.key || ""),
|
|
711
|
+
parent_summary: String(fields.parent?.fields?.summary || ""),
|
|
712
|
+
labels: (Array.isArray(fields.labels) ? fields.labels : []).join("; "),
|
|
713
|
+
components: (Array.isArray(fields.components) ? fields.components.map((c: { name?: string }) => c.name).filter(Boolean) : []).join("; "),
|
|
714
|
+
created_at: String(fields.created || ""),
|
|
715
|
+
updated_at: String(fields.updated || ""),
|
|
716
|
+
url: issue.key ? `${baseUrl}/browse/${issue.key}` : "",
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function displayName(value: unknown): string {
|
|
721
|
+
return value && typeof value === "object" ? String((value as { displayName?: string; name?: string }).displayName || (value as { name?: string }).name || "") : String(value || "")
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function suiteDirs(root: string): string[] {
|
|
725
|
+
if (!existsSync(root)) return []
|
|
726
|
+
return readdirSync(root)
|
|
727
|
+
.map((name) => path.join(root, name))
|
|
728
|
+
.filter((dir) => existsSync(path.join(dir, "suite.json")))
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function writeText(file: string, body: string): string {
|
|
732
|
+
mkdirSync(path.dirname(file), { recursive: true })
|
|
733
|
+
writeFileSync(file, body, "utf8")
|
|
734
|
+
return file
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function writeCsv(file: string, headers: readonly string[], rows: Record<string, unknown>[]): string {
|
|
738
|
+
mkdirSync(path.dirname(file), { recursive: true })
|
|
739
|
+
const lines = [headers.join(","), ...rows.map((row) => headers.map((h) => csvCell(row[h])).join(","))]
|
|
740
|
+
writeFileSync(file, `${lines.join("\n")}\n`, "utf8")
|
|
741
|
+
return file
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function csvCell(value: unknown): string {
|
|
745
|
+
const text =
|
|
746
|
+
value === undefined || value === null
|
|
747
|
+
? ""
|
|
748
|
+
: typeof value === "string"
|
|
749
|
+
? value
|
|
750
|
+
: typeof value === "number" || typeof value === "boolean"
|
|
751
|
+
? String(value)
|
|
752
|
+
: Array.isArray(value)
|
|
753
|
+
? value.join("; ")
|
|
754
|
+
: JSON.stringify(value)
|
|
755
|
+
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function readJson<T>(file: string): T | null {
|
|
759
|
+
if (!existsSync(file)) return null
|
|
760
|
+
try {
|
|
761
|
+
return JSON.parse(readFileSync(file, "utf8")) as T
|
|
762
|
+
} catch {
|
|
763
|
+
return null
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function sqlPath(file: string): string {
|
|
768
|
+
return file.split(path.sep).join("/").replace(/'/g, "''")
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function sqlBlock(name: string, sql: string): string {
|
|
772
|
+
return `\`\`\`sql ${name}\n${sql}\n\`\`\``
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function summaryMarkdown(
|
|
776
|
+
cfg: AppConfig,
|
|
777
|
+
paths: Paths,
|
|
778
|
+
suites: { suites: SuiteRow[]; cases: CaseRow[]; steps: StepRow[]; writebacks: WriteBackRow[] },
|
|
779
|
+
readiness: { summary: ReadinessRow[] },
|
|
780
|
+
external: ExternalExport[],
|
|
781
|
+
): string {
|
|
782
|
+
const bi = cfg.bi || defaultBiConfig(cfg)
|
|
783
|
+
return [
|
|
784
|
+
`# ${bi.title}`,
|
|
785
|
+
"",
|
|
786
|
+
`Generated at ${new Date().toISOString()}.`,
|
|
787
|
+
"",
|
|
788
|
+
`- Evidence project: \`${paths.evidenceDir}\``,
|
|
789
|
+
`- Static build: \`${paths.buildDir}\``,
|
|
790
|
+
`- Data export: \`${paths.dataDir}\``,
|
|
791
|
+
`- Suite runs exported: ${suites.suites.length}`,
|
|
792
|
+
`- Case results exported: ${suites.cases.length}`,
|
|
793
|
+
`- Release readiness reports exported: ${readiness.summary.length}`,
|
|
794
|
+
`- External datasets exported: ${external.length}`,
|
|
795
|
+
"",
|
|
796
|
+
external.length ? "## External datasets" : "## External datasets",
|
|
797
|
+
"",
|
|
798
|
+
...(external.length ? external.map((d) => `- ${d.row.title} (\`${d.row.dataset_id}\`): ${d.row.row_count} row${d.row.row_count === 1 ? "" : "s"}`) : ["- None configured. Add datasets under `bi.datasets` in `forgeqe.yaml`."]),
|
|
799
|
+
"",
|
|
800
|
+
].join("\n")
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
async function runEvidence(args: string[], cwd: string, extraEnv: Record<string, string> = {}): Promise<void> {
|
|
804
|
+
const evidence = Bun.which("evidence")
|
|
805
|
+
const command = evidence ? evidence : Bun.which("npx")
|
|
806
|
+
if (!command) throw new Error("Evidence CLI not found and npx is unavailable. Install Node.js or the Evidence CLI first.")
|
|
807
|
+
const fullArgs = evidence ? args : ["-p", "evidence", "evidence", ...args]
|
|
808
|
+
await new Promise<void>((resolve, reject) => {
|
|
809
|
+
const child = spawn(command, fullArgs, {
|
|
810
|
+
cwd,
|
|
811
|
+
stdio: "inherit",
|
|
812
|
+
shell: process.platform === "win32",
|
|
813
|
+
env: { ...process.env, DO_NOT_TRACK: "1", EVIDENCE_TELEMETRY_DISABLED: "1", ...extraEnv },
|
|
814
|
+
})
|
|
815
|
+
child.on("error", reject)
|
|
816
|
+
child.on("exit", (code) => (code === 0 ? resolve() : reject(new Error(`Evidence command failed (${[command, ...fullArgs].join(" ")}).`))))
|
|
817
|
+
})
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const SUITE_HEADERS = [
|
|
821
|
+
"suite_id",
|
|
822
|
+
"suite_scope",
|
|
823
|
+
"suite_name",
|
|
824
|
+
"started_at",
|
|
825
|
+
"duration_ms",
|
|
826
|
+
"total_cases",
|
|
827
|
+
"passed_cases",
|
|
828
|
+
"failed_cases",
|
|
829
|
+
"error_cases",
|
|
830
|
+
"stopped_cases",
|
|
831
|
+
"coded_cases",
|
|
832
|
+
"coded_passed_cases",
|
|
833
|
+
] as const
|
|
834
|
+
|
|
835
|
+
const CASE_HEADERS = [
|
|
836
|
+
"suite_id",
|
|
837
|
+
"suite_scope",
|
|
838
|
+
"case_id",
|
|
839
|
+
"case_title",
|
|
840
|
+
"case_source",
|
|
841
|
+
"case_status",
|
|
842
|
+
"started_at",
|
|
843
|
+
"finished_at",
|
|
844
|
+
"duration_ms",
|
|
845
|
+
"tokens",
|
|
846
|
+
"final_url",
|
|
847
|
+
"run_dir",
|
|
848
|
+
"group",
|
|
849
|
+
"tags",
|
|
850
|
+
"requirements",
|
|
851
|
+
"failure_type",
|
|
852
|
+
"failure_action",
|
|
853
|
+
"bug_label",
|
|
854
|
+
"bug_url",
|
|
855
|
+
"code_status",
|
|
856
|
+
"code_test_file",
|
|
857
|
+
"code_command",
|
|
858
|
+
] as const
|
|
859
|
+
|
|
860
|
+
const STEP_HEADERS = [
|
|
861
|
+
"suite_id",
|
|
862
|
+
"suite_scope",
|
|
863
|
+
"case_id",
|
|
864
|
+
"step_number",
|
|
865
|
+
"action",
|
|
866
|
+
"data",
|
|
867
|
+
"expected",
|
|
868
|
+
"status",
|
|
869
|
+
"actual",
|
|
870
|
+
"cause",
|
|
871
|
+
] as const
|
|
872
|
+
|
|
873
|
+
const WRITEBACK_HEADERS = ["suite_id", "suite_scope", "case_id", "source", "text", "url", "error"] as const
|
|
874
|
+
const READINESS_HEADERS = [
|
|
875
|
+
"report_id",
|
|
876
|
+
"scope",
|
|
877
|
+
"verdict",
|
|
878
|
+
"pass_rate",
|
|
879
|
+
"stories",
|
|
880
|
+
"untested_stories",
|
|
881
|
+
"cases",
|
|
882
|
+
"automated_cases",
|
|
883
|
+
"executed_cases",
|
|
884
|
+
"passed_cases",
|
|
885
|
+
"failed_cases",
|
|
886
|
+
"blocked_cases",
|
|
887
|
+
"not_run_cases",
|
|
888
|
+
"flaky_cases",
|
|
889
|
+
"quarantined_cases",
|
|
890
|
+
"open_bugs",
|
|
891
|
+
"blockers",
|
|
892
|
+
"risks",
|
|
893
|
+
"notes",
|
|
894
|
+
] as const
|
|
895
|
+
const READINESS_STORY_HEADERS = ["report_id", "story_id", "story_type", "story_title", "state", "url", "area", "case_count"] as const
|
|
896
|
+
const READINESS_CASE_HEADERS = ["report_id", "story_id", "case_id", "case_title", "automated", "outcome", "from", "when", "flaky", "quarantined", "url"] as const
|
|
897
|
+
const EXTERNAL_HEADERS = ["dataset_id", "title", "source_kind", "row_count", "refreshed_at", "page"] as const
|
|
898
|
+
const ADO_DATASET_HEADERS = ["id", "project", "type", "title", "state", "assigned_to", "changed_at", "iteration"] as const
|
|
899
|
+
const JIRA_DATASET_HEADERS = ["key", "summary", "status", "status_category", "issue_type", "priority", "assignee", "reporter", "parent_key", "parent_summary", "labels", "components", "created_at", "updated_at", "url"] as const
|