csspilot 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +755 -172
- package/dist/index.js +755 -172
- package/dist/skills/testpilot/manifest.yaml +1 -1
- package/dist/skills/testpilot/workflows/analyze-result.md +6 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10450,7 +10450,7 @@ function makeCiCommand() {
|
|
|
10450
10450
|
|
|
10451
10451
|
// src/commands/init/doctor.ts
|
|
10452
10452
|
import { existsSync as existsSync7 } from "node:fs";
|
|
10453
|
-
import { join as
|
|
10453
|
+
import { join as join24 } from "node:path";
|
|
10454
10454
|
|
|
10455
10455
|
// ../../packages/core/src/errors/index.ts
|
|
10456
10456
|
var TestPilotError = class extends Error {
|
|
@@ -15513,7 +15513,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
|
|
|
15513
15513
|
var NONE = 0;
|
|
15514
15514
|
var ASSUMED = 1;
|
|
15515
15515
|
var PROVEN = 2;
|
|
15516
|
-
function isRecursive(inst, stack,
|
|
15516
|
+
function isRecursive(inst, stack, resolve6) {
|
|
15517
15517
|
const cached2 = recursive.get(inst);
|
|
15518
15518
|
if (cached2 !== void 0)
|
|
15519
15519
|
return cached2 ? PROVEN : NONE;
|
|
@@ -15523,7 +15523,7 @@ function isRecursive(inst, stack, resolve5) {
|
|
|
15523
15523
|
let result = NONE;
|
|
15524
15524
|
const check2 = (child) => {
|
|
15525
15525
|
if (result !== PROVEN && child?._zod) {
|
|
15526
|
-
const answer = isRecursive(child, stack,
|
|
15526
|
+
const answer = isRecursive(child, stack, resolve6);
|
|
15527
15527
|
if (answer > result)
|
|
15528
15528
|
result = answer;
|
|
15529
15529
|
}
|
|
@@ -15534,7 +15534,7 @@ function isRecursive(inst, stack, resolve5) {
|
|
|
15534
15534
|
const desc = Object.getOwnPropertyDescriptor(sh, key);
|
|
15535
15535
|
if (spread && !desc.enumerable)
|
|
15536
15536
|
continue;
|
|
15537
|
-
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack,
|
|
15537
|
+
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve6) : NONE;
|
|
15538
15538
|
if (child > answer)
|
|
15539
15539
|
answer = child;
|
|
15540
15540
|
}
|
|
@@ -15601,7 +15601,7 @@ function isRecursive(inst, stack, resolve5) {
|
|
|
15601
15601
|
break;
|
|
15602
15602
|
// `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
|
|
15603
15603
|
case "lazy": {
|
|
15604
|
-
const inner = def._cachedInner ?? (
|
|
15604
|
+
const inner = def._cachedInner ?? (resolve6 ? inst._zod.innerType : void 0);
|
|
15605
15605
|
merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
|
|
15606
15606
|
break;
|
|
15607
15607
|
}
|
|
@@ -30341,7 +30341,7 @@ function execToString(command, args) {
|
|
|
30341
30341
|
}
|
|
30342
30342
|
|
|
30343
30343
|
// ../../packages/sdk/src/client.ts
|
|
30344
|
-
import { isAbsolute as isAbsolute3, join as
|
|
30344
|
+
import { isAbsolute as isAbsolute3, join as join19 } from "node:path";
|
|
30345
30345
|
|
|
30346
30346
|
// ../../packages/sdk/src/cases.ts
|
|
30347
30347
|
import { mkdir as mkdir3, readFile as readFile3, readdir, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
|
|
@@ -30715,12 +30715,62 @@ async function writeCaseFile(file2, source, options = {}) {
|
|
|
30715
30715
|
}
|
|
30716
30716
|
|
|
30717
30717
|
// ../../packages/sdk/src/reports.ts
|
|
30718
|
-
import { readFile as
|
|
30719
|
-
import { join as
|
|
30718
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
30719
|
+
import { join as join18 } from "node:path";
|
|
30720
30720
|
|
|
30721
30721
|
// ../../packages/reporter/src/report.ts
|
|
30722
30722
|
import { writeFile as writeFile4 } from "node:fs/promises";
|
|
30723
|
-
import { join as
|
|
30723
|
+
import { dirname as dirname2, join as join8, resolve as resolve3 } from "node:path";
|
|
30724
|
+
|
|
30725
|
+
// ../../packages/reporter/src/classify.ts
|
|
30726
|
+
var FAILURE_CATEGORIES = [
|
|
30727
|
+
{ id: "assert", label: "\u65AD\u8A00\u5931\u8D25" },
|
|
30728
|
+
{ id: "element", label: "\u5143\u7D20\u672A\u627E\u5230" },
|
|
30729
|
+
{ id: "adapter", label: "Adapter \u95EE\u9898" },
|
|
30730
|
+
{ id: "env", label: "\u73AF\u5883 / \u8FDE\u63A5" },
|
|
30731
|
+
{ id: "timeout", label: "\u8D85\u65F6" },
|
|
30732
|
+
{ id: "other", label: "\u5176\u4ED6" }
|
|
30733
|
+
];
|
|
30734
|
+
function categoryLabel(id) {
|
|
30735
|
+
return FAILURE_CATEGORIES.find((item) => item.id === id)?.label ?? id;
|
|
30736
|
+
}
|
|
30737
|
+
var RULES = [
|
|
30738
|
+
{ id: "assert", pattern: /断言失败|AssertionError|expect\(.+\)\.to/i },
|
|
30739
|
+
{ id: "element", pattern: /未找到元素|等待元素超时|waiting for locator|waiting for selector|element not found/i },
|
|
30740
|
+
{ id: "adapter", pattern: /未注册 target|ADAPTER_NOT_FOUND|无法定位 TestPilot Skill/i },
|
|
30741
|
+
{ id: "env", pattern: /net::ERR|ECONNREFUSED|Connection closed|Failed to launch|服务端口|不可达|ENOTFOUND|EACCES/i },
|
|
30742
|
+
{ id: "timeout", pattern: /timeout|超时/i }
|
|
30743
|
+
];
|
|
30744
|
+
function classifyFailure(message) {
|
|
30745
|
+
if (!message) return "other";
|
|
30746
|
+
for (const rule of RULES) {
|
|
30747
|
+
if (rule.pattern.test(message)) return rule.id;
|
|
30748
|
+
}
|
|
30749
|
+
return "other";
|
|
30750
|
+
}
|
|
30751
|
+
|
|
30752
|
+
// ../../packages/reporter/src/history.ts
|
|
30753
|
+
import { readFile as readFile4, readdir as readdir2 } from "node:fs/promises";
|
|
30754
|
+
import { join as join7 } from "node:path";
|
|
30755
|
+
async function collectHistory(runsRoot2, skipRunId, limit = 12) {
|
|
30756
|
+
const entries = (await readdir2(runsRoot2).catch(() => [])).filter((name) => /^\d{8}-\d{3}$/.test(name) && name !== skipRunId).sort().slice(-limit);
|
|
30757
|
+
const points = [];
|
|
30758
|
+
for (const runId of entries) {
|
|
30759
|
+
try {
|
|
30760
|
+
const summary = JSON.parse(await readFile4(join7(runsRoot2, runId, "result.json"), "utf8"));
|
|
30761
|
+
points.push({
|
|
30762
|
+
runId,
|
|
30763
|
+
startedAt: summary.startedAt,
|
|
30764
|
+
status: summary.status ?? "unknown",
|
|
30765
|
+
passed: summary.totals?.passed ?? 0,
|
|
30766
|
+
failed: summary.totals?.failed ?? 0,
|
|
30767
|
+
durationMs: summary.durationMs
|
|
30768
|
+
});
|
|
30769
|
+
} catch {
|
|
30770
|
+
}
|
|
30771
|
+
}
|
|
30772
|
+
return points;
|
|
30773
|
+
}
|
|
30724
30774
|
|
|
30725
30775
|
// ../../packages/reporter/src/summary.ts
|
|
30726
30776
|
function toSummaryView(summary) {
|
|
@@ -30739,40 +30789,201 @@ function toSummaryView(summary) {
|
|
|
30739
30789
|
}
|
|
30740
30790
|
|
|
30741
30791
|
// ../../packages/reporter/src/report.ts
|
|
30742
|
-
|
|
30792
|
+
var HISTORY_LIMIT = 12;
|
|
30793
|
+
async function writeReports(runDir, summary, options = {}) {
|
|
30743
30794
|
const dir = resolve3(runDir);
|
|
30744
|
-
const
|
|
30745
|
-
const
|
|
30746
|
-
const
|
|
30747
|
-
|
|
30748
|
-
|
|
30795
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
30796
|
+
const history = options.history ?? await collectHistory(dirname2(dir), summary.runId, options.historyLimit ?? HISTORY_LIMIT);
|
|
30797
|
+
const failures = [];
|
|
30798
|
+
const cases = summary.cases.map((item) => {
|
|
30799
|
+
const categories = /* @__PURE__ */ new Set();
|
|
30800
|
+
const steps = item.steps.map((step) => {
|
|
30801
|
+
const category = step.status === "failed" ? classifyFailure(step.error) : void 0;
|
|
30802
|
+
if (category) {
|
|
30803
|
+
categories.add(category);
|
|
30804
|
+
failures.push({
|
|
30805
|
+
category,
|
|
30806
|
+
label: categoryLabel(category),
|
|
30807
|
+
caseId: item.caseId,
|
|
30808
|
+
index: step.index,
|
|
30809
|
+
message: step.error ?? ""
|
|
30810
|
+
});
|
|
30811
|
+
}
|
|
30812
|
+
return { ...step, category };
|
|
30813
|
+
});
|
|
30814
|
+
return { ...item, steps, categories: [...categories] };
|
|
30815
|
+
});
|
|
30816
|
+
const categoryLabels = {};
|
|
30817
|
+
for (const item of FAILURE_CATEGORIES) categoryLabels[item.id] = item.label;
|
|
30818
|
+
const data = {
|
|
30819
|
+
generatedAt,
|
|
30820
|
+
summary: { ...summary, cases },
|
|
30821
|
+
history,
|
|
30822
|
+
failures,
|
|
30823
|
+
view: toSummaryView(summary),
|
|
30824
|
+
categoryLabels
|
|
30825
|
+
};
|
|
30826
|
+
const jsonPath = join8(dir, "report.json");
|
|
30827
|
+
const htmlPath = join8(dir, "report.html");
|
|
30828
|
+
await writeFile4(jsonPath, JSON.stringify(data, null, 2), "utf8");
|
|
30829
|
+
await writeFile4(htmlPath, renderHtml(data), "utf8");
|
|
30749
30830
|
return { json: jsonPath, html: htmlPath };
|
|
30750
30831
|
}
|
|
30751
|
-
function
|
|
30752
|
-
const
|
|
30832
|
+
function categoryChips(data) {
|
|
30833
|
+
const counts = /* @__PURE__ */ new Map();
|
|
30834
|
+
for (const failure2 of data.failures) {
|
|
30835
|
+
counts.set(failure2.category, (counts.get(failure2.category) ?? 0) + 1);
|
|
30836
|
+
}
|
|
30837
|
+
const chips = [
|
|
30838
|
+
'<span class="chip active" data-group="status" data-value="all">\u5168\u90E8\u72B6\u6001</span>',
|
|
30839
|
+
'<span class="chip" data-group="status" data-value="failed">\u4EC5\u5931\u8D25</span>'
|
|
30840
|
+
];
|
|
30841
|
+
for (const item of FAILURE_CATEGORIES) {
|
|
30842
|
+
const count = counts.get(item.id) ?? 0;
|
|
30843
|
+
if (count > 0) {
|
|
30844
|
+
chips.push(
|
|
30845
|
+
`<span class="chip" data-group="cat" data-value="${item.id}">${escapeHtml(item.label)}(${count})</span>`
|
|
30846
|
+
);
|
|
30847
|
+
}
|
|
30848
|
+
}
|
|
30849
|
+
return chips.join("\n");
|
|
30850
|
+
}
|
|
30851
|
+
function renderHtml(data) {
|
|
30852
|
+
const view = toSummaryView(data.summary);
|
|
30853
|
+
const summary = data.summary;
|
|
30854
|
+
const trendSvg = renderTrend(data);
|
|
30753
30855
|
const casesHtml = summary.cases.map((item) => caseHtml(item)).join("\n");
|
|
30856
|
+
const chips = categoryChips(data);
|
|
30754
30857
|
return `<!doctype html>
|
|
30755
30858
|
<html lang="zh-CN">
|
|
30756
30859
|
<head>
|
|
30757
30860
|
<meta charset="utf-8" />
|
|
30758
30861
|
<title>TestPilot Report ${escapeHtml(summary.runId)}</title>
|
|
30759
30862
|
<style>
|
|
30760
|
-
|
|
30761
|
-
|
|
30762
|
-
|
|
30763
|
-
.
|
|
30764
|
-
|
|
30765
|
-
|
|
30766
|
-
|
|
30767
|
-
|
|
30768
|
-
|
|
30769
|
-
|
|
30863
|
+
:root { --ok:#1a7f37; --fail:#cf222e; --skip:#6e7781; --line:#d0d7de; --muted:#59636e; }
|
|
30864
|
+
* { box-sizing: border-box; }
|
|
30865
|
+
body { font-family: system-ui, "Segoe UI", sans-serif; margin: 0; color: #1f2328; background: #f6f8fa; }
|
|
30866
|
+
.wrap { max-width: 1080px; margin: 0 auto; padding: 20px 16px 48px; }
|
|
30867
|
+
h1 { font-size: 20px; margin: 8px 0; } h2 { font-size: 15px; margin: 24px 0 8px; }
|
|
30868
|
+
.badge { display: inline-block; padding: 2px 10px; border-radius: 10px; color: #fff; font-size: 12px; }
|
|
30869
|
+
.passed { background: var(--ok); } .failed { background: var(--fail); } .skipped { background: var(--skip); }
|
|
30870
|
+
code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
|
|
30871
|
+
.cards { display: flex; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
|
|
30872
|
+
.card { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 16px; min-width: 130px; }
|
|
30873
|
+
.card b { font-size: 20px; display: block; }
|
|
30874
|
+
.card span { color: var(--muted); font-size: 12px; }
|
|
30875
|
+
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 10px 0; }
|
|
30876
|
+
.chip { border: 1px solid var(--line); background: #fff; border-radius: 14px; padding: 3px 12px; font-size: 12px; cursor: pointer; }
|
|
30877
|
+
.chip.active { background: #0969da; color: #fff; border-color: #0969da; }
|
|
30878
|
+
input#q { flex: 1; min-width: 180px; padding: 5px 10px; border: 1px solid var(--line); border-radius: 6px; font-size: 13px; }
|
|
30879
|
+
.btn { border: 1px solid var(--line); background: #fff; border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
|
|
30880
|
+
#count { color: var(--muted); font-size: 12px; margin-left: auto; }
|
|
30881
|
+
details.case { background: #fff; border: 1px solid var(--line); border-radius: 8px; margin: 10px 0; }
|
|
30882
|
+
details.case > summary { cursor: pointer; padding: 10px 14px; font-size: 14px; list-style: none; }
|
|
30883
|
+
details.case > summary::-webkit-details-marker { display: none; }
|
|
30884
|
+
details.case > summary:hover { background: #f6f8fa; }
|
|
30885
|
+
table { border-collapse: collapse; width: 100%; font-size: 13px; }
|
|
30886
|
+
th, td { border-top: 1px solid var(--line); padding: 6px 10px; text-align: left; vertical-align: top; }
|
|
30887
|
+
td.num { text-align: right; white-space: nowrap; }
|
|
30888
|
+
.error { color: var(--fail); white-space: pre-wrap; }
|
|
30889
|
+
a { color: #0969da; text-decoration: none; }
|
|
30890
|
+
.trend { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 12px; overflow-x: auto; }
|
|
30891
|
+
.muted { color: var(--muted); }
|
|
30892
|
+
.hide { display: none !important; }
|
|
30893
|
+
.meta { color: var(--muted); font-size: 12px; margin: 6px 0; }
|
|
30770
30894
|
</style>
|
|
30771
30895
|
</head>
|
|
30772
30896
|
<body>
|
|
30773
|
-
<
|
|
30774
|
-
<
|
|
30775
|
-
${
|
|
30897
|
+
<div class="wrap">
|
|
30898
|
+
<h1>TestPilot \u6D4B\u8BD5\u62A5\u544A <span class="badge ${summary.status}">${summary.status}</span></h1>
|
|
30899
|
+
<p class="meta">run <code>${escapeHtml(summary.runId)}</code> \xB7 \u751F\u6210\u4E8E ${escapeHtml(data.generatedAt)} \xB7 \u8017\u65F6 ${(view.durationMs / 1e3).toFixed(1)}s</p>
|
|
30900
|
+
|
|
30901
|
+
<div class="cards">
|
|
30902
|
+
<div class="card"><b>${view.cases.passed}<span style="color:var(--ok)"> \u2713</span></b><span>\u901A\u8FC7\u7528\u4F8B</span></div>
|
|
30903
|
+
<div class="card"><b>${view.cases.failed}<span style="color:var(--fail)"> \u2717</span></b><span>\u5931\u8D25\u7528\u4F8B</span></div>
|
|
30904
|
+
<div class="card"><b>${view.steps.passed}<span style="color:var(--ok)"> \u2713</span></b><span>\u901A\u8FC7\u6B65\u9AA4</span></div>
|
|
30905
|
+
<div class="card"><b>${view.steps.failed}<span style="color:var(--fail)"> \u2717</span></b><span>\u5931\u8D25\u6B65\u9AA4</span></div>
|
|
30906
|
+
<div class="card"><b>${view.steps.skipped} \u21B7</b><span>\u8DF3\u8FC7\u6B65\u9AA4</span></div>
|
|
30907
|
+
</div>
|
|
30908
|
+
|
|
30909
|
+
<h2>\u5386\u53F2\u8D8B\u52BF(\u6700\u8FD1 ${data.history.length + 1} \u6B21\u8FD0\u884C)</h2>
|
|
30910
|
+
<div class="trend">${trendSvg}</div>
|
|
30911
|
+
|
|
30912
|
+
<h2>\u5931\u8D25\u5206\u7C7B</h2>
|
|
30913
|
+
<div class="toolbar" id="cat-chips">${chips}</div>
|
|
30914
|
+
|
|
30915
|
+
<h2>\u7528\u4F8B</h2>
|
|
30916
|
+
<div class="toolbar">
|
|
30917
|
+
<input id="q" placeholder="\u641C\u7D22\u7528\u4F8B id / \u540D\u79F0 / \u6587\u4EF6\u2026" />
|
|
30918
|
+
<button class="btn" id="expand">\u5C55\u5F00\u5168\u90E8</button>
|
|
30919
|
+
<button class="btn" id="collapse">\u6536\u8D77\u5168\u90E8</button>
|
|
30920
|
+
<span id="count"></span>
|
|
30921
|
+
</div>
|
|
30922
|
+
<div id="cases">${casesHtml}</div>
|
|
30923
|
+
</div>
|
|
30924
|
+
|
|
30925
|
+
<script id="report-data" type="application/json">${embedJson(data)}</script>
|
|
30926
|
+
<script>
|
|
30927
|
+
(function () {
|
|
30928
|
+
var data = JSON.parse(document.getElementById('report-data').textContent);
|
|
30929
|
+
var state = { status: 'all', cat: 'all', q: '' };
|
|
30930
|
+
var labels = data.categoryLabels || {};
|
|
30931
|
+
|
|
30932
|
+
function setChip(group, value) {
|
|
30933
|
+
document.querySelectorAll('.chip[data-group="' + group + '"]').forEach(function (chip) {
|
|
30934
|
+
chip.classList.toggle('active', chip.getAttribute('data-value') === value);
|
|
30935
|
+
});
|
|
30936
|
+
}
|
|
30937
|
+
|
|
30938
|
+
function matches(card) {
|
|
30939
|
+
if (state.status !== 'all' && card.getAttribute('data-status') !== state.status) return false;
|
|
30940
|
+
if (state.cat !== 'all' && (',' + (card.getAttribute('data-cats') || '') + ',').indexOf(',' + state.cat + ',') === -1) return false;
|
|
30941
|
+
if (state.q) {
|
|
30942
|
+
var text = (card.getAttribute('data-search') || '').toLowerCase();
|
|
30943
|
+
if (text.indexOf(state.q.toLowerCase()) === -1) return false;
|
|
30944
|
+
}
|
|
30945
|
+
return true;
|
|
30946
|
+
}
|
|
30947
|
+
|
|
30948
|
+
function apply() {
|
|
30949
|
+
var cards = document.querySelectorAll('details.case');
|
|
30950
|
+
var shown = 0;
|
|
30951
|
+
cards.forEach(function (card) {
|
|
30952
|
+
var show = matches(card);
|
|
30953
|
+
card.classList.toggle('hide', !show);
|
|
30954
|
+
if (show) shown++;
|
|
30955
|
+
});
|
|
30956
|
+
var count = document.getElementById('count');
|
|
30957
|
+
if (count) count.textContent = shown + ' / ' + cards.length + ' \u7528\u4F8B';
|
|
30958
|
+
}
|
|
30959
|
+
|
|
30960
|
+
document.querySelectorAll('.chip').forEach(function (chip) {
|
|
30961
|
+
chip.addEventListener('click', function () {
|
|
30962
|
+
var group = chip.getAttribute('data-group');
|
|
30963
|
+
var value = chip.getAttribute('data-value');
|
|
30964
|
+
if (group === 'status') state.status = value;
|
|
30965
|
+
if (group === 'cat') state.cat = value;
|
|
30966
|
+
setChip(group, value);
|
|
30967
|
+
apply();
|
|
30968
|
+
});
|
|
30969
|
+
});
|
|
30970
|
+
var q = document.getElementById('q');
|
|
30971
|
+
if (q) q.addEventListener('input', function () { state.q = q.value.trim(); apply(); });
|
|
30972
|
+
var expand = document.getElementById('expand');
|
|
30973
|
+
if (expand) expand.addEventListener('click', function () {
|
|
30974
|
+
document.querySelectorAll('details.case').forEach(function (c) { c.open = true; });
|
|
30975
|
+
});
|
|
30976
|
+
var collapse = document.getElementById('collapse');
|
|
30977
|
+
if (collapse) collapse.addEventListener('click', function () {
|
|
30978
|
+
document.querySelectorAll('details.case').forEach(function (c) { c.open = false; });
|
|
30979
|
+
});
|
|
30980
|
+
|
|
30981
|
+
function failedSteps(card) {
|
|
30982
|
+
return data.summary.cases.find(function (c) { return c.caseId === card; });
|
|
30983
|
+
}
|
|
30984
|
+
apply();
|
|
30985
|
+
})();
|
|
30986
|
+
</script>
|
|
30776
30987
|
</body>
|
|
30777
30988
|
</html>`;
|
|
30778
30989
|
}
|
|
@@ -30783,18 +30994,27 @@ function caseHtml(item) {
|
|
|
30783
30994
|
if (item.video) links.push(`<a href="${escapeHtml(item.video)}">\u89C6\u9891</a>`);
|
|
30784
30995
|
if (item.trace) links.push(`<a href="${escapeHtml(item.trace)}">Trace</a>`);
|
|
30785
30996
|
const linksLine = links.length > 0 ? `<p>${links.join(" \xB7 ")}</p>` : "";
|
|
30997
|
+
const search = escapeHtml(`${item.caseId} ${item.caseName} ${item.file}`).replaceAll('"', """);
|
|
30998
|
+
const cats = escapeHtml(item.categories.join(","));
|
|
30786
30999
|
return `
|
|
30787
|
-
<
|
|
30788
|
-
|
|
30789
|
-
${
|
|
30790
|
-
<
|
|
30791
|
-
|
|
30792
|
-
|
|
30793
|
-
|
|
31000
|
+
<details class="case" data-status="${item.status}" data-cats="${cats}" data-search="${search}">
|
|
31001
|
+
<summary>
|
|
31002
|
+
<span class="badge ${item.status}">${item.status}</span>
|
|
31003
|
+
<b>${escapeHtml(item.caseId)}</b> \xB7 ${escapeHtml(item.caseName)}
|
|
31004
|
+
<span class="muted">\xB7 ${item.steps.length} \u6B65 \xB7 ${(item.durationMs / 1e3).toFixed(1)}s</span>
|
|
31005
|
+
</summary>
|
|
31006
|
+
${errorLine}
|
|
31007
|
+
${linksLine}
|
|
31008
|
+
<table>
|
|
31009
|
+
<tr><th>#</th><th>target</th><th>action</th><th>\u72B6\u6001</th><th>\u8017\u65F6</th><th>\u8BE6\u60C5</th></tr>
|
|
31010
|
+
${rows}
|
|
31011
|
+
</table>
|
|
31012
|
+
</details>`;
|
|
30794
31013
|
}
|
|
30795
31014
|
function stepHtml(step) {
|
|
30796
31015
|
const details = [];
|
|
30797
31016
|
if (step.error) details.push(`<span class="error">${escapeHtml(step.error)}</span>`);
|
|
31017
|
+
if (step.category) details.push(`<span class="muted">\u5206\u7C7B: ${escapeHtml(categoryLabel(step.category))}</span>`);
|
|
30798
31018
|
if (step.extracted) {
|
|
30799
31019
|
for (const [name, value] of Object.entries(step.extracted)) {
|
|
30800
31020
|
details.push(`\u53D8\u91CF ${escapeHtml(name)} = <code>${escapeHtml(value)}</code>`);
|
|
@@ -30810,21 +31030,344 @@ function stepHtml(step) {
|
|
|
30810
31030
|
<td>${details.join("<br />") || "-"}</td>
|
|
30811
31031
|
</tr>`;
|
|
30812
31032
|
}
|
|
31033
|
+
function renderTrend(data) {
|
|
31034
|
+
const points = [
|
|
31035
|
+
...data.history.map((h) => ({ runId: h.runId, passed: h.passed, failed: h.failed, status: h.status })),
|
|
31036
|
+
{
|
|
31037
|
+
runId: data.summary.runId,
|
|
31038
|
+
passed: data.summary.totals.passed,
|
|
31039
|
+
failed: data.summary.totals.failed,
|
|
31040
|
+
status: data.summary.status
|
|
31041
|
+
}
|
|
31042
|
+
];
|
|
31043
|
+
if (points.length === 0) return '<p class="muted">\u6682\u65E0\u5386\u53F2\u6570\u636E</p>';
|
|
31044
|
+
const max = Math.max(1, ...points.map((p) => p.passed + p.failed));
|
|
31045
|
+
const barW = 34;
|
|
31046
|
+
const gap = 14;
|
|
31047
|
+
const chartH = 90;
|
|
31048
|
+
const labelH = 34;
|
|
31049
|
+
const width = points.length * (barW + gap) + gap;
|
|
31050
|
+
const height = chartH + labelH;
|
|
31051
|
+
const bars = points.map((point, index) => {
|
|
31052
|
+
const total = point.passed + point.failed;
|
|
31053
|
+
const scale = (count) => total === 0 ? 0 : count / max * chartH;
|
|
31054
|
+
const passedH = scale(point.passed);
|
|
31055
|
+
const failedH = scale(point.failed);
|
|
31056
|
+
const x = gap + index * (barW + gap);
|
|
31057
|
+
const passedY = chartH - passedH;
|
|
31058
|
+
const failedY = passedY - failedH;
|
|
31059
|
+
const tip = `${point.runId}: \u2713${point.passed} \u2717${point.failed}`;
|
|
31060
|
+
return `<g>
|
|
31061
|
+
<title>${escapeHtml(tip)}</title>
|
|
31062
|
+
<rect x="${x}" y="${chartH + 4}" width="${barW}" height="1" fill="var(--line)"></rect>
|
|
31063
|
+
${passedH > 0 ? `<rect x="${x}" y="${passedY.toFixed(1)}" width="${barW}" height="${passedH.toFixed(1)}" fill="var(--ok)" rx="2"></rect>` : ""}
|
|
31064
|
+
${failedH > 0 ? `<rect x="${x}" y="${failedY.toFixed(1)}" width="${barW}" height="${failedH.toFixed(1)}" fill="var(--fail)" rx="2"></rect>` : ""}
|
|
31065
|
+
<text x="${x + barW / 2}" y="${chartH + 20}" font-size="9" fill="var(--muted)" text-anchor="middle">${escapeHtml(shortId(point.runId))}</text>
|
|
31066
|
+
</g>`;
|
|
31067
|
+
}).join("\n");
|
|
31068
|
+
return `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">${bars}</svg>`;
|
|
31069
|
+
}
|
|
31070
|
+
function shortId(runId) {
|
|
31071
|
+
return runId.replace(/^\d{8}-/, "");
|
|
31072
|
+
}
|
|
30813
31073
|
function escapeHtml(input2) {
|
|
30814
31074
|
return input2.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
30815
31075
|
}
|
|
31076
|
+
function embedJson(data) {
|
|
31077
|
+
return JSON.stringify(data).replaceAll("<", "\\u003c");
|
|
31078
|
+
}
|
|
31079
|
+
|
|
31080
|
+
// ../../packages/reporter/src/summary-report.ts
|
|
31081
|
+
import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
|
|
31082
|
+
import { join as join9, resolve as resolve4 } from "node:path";
|
|
31083
|
+
function escapeHtml2(value) {
|
|
31084
|
+
return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
31085
|
+
}
|
|
31086
|
+
function pct(value) {
|
|
31087
|
+
return (value * 100).toFixed(1) + "%";
|
|
31088
|
+
}
|
|
31089
|
+
function buildSummaryData(runs) {
|
|
31090
|
+
const executions = [];
|
|
31091
|
+
const runPoints = [];
|
|
31092
|
+
for (const { runId, summary } of runs) {
|
|
31093
|
+
for (const item of summary.cases) {
|
|
31094
|
+
const platforms = [...new Set(item.steps.map((step) => step.target))];
|
|
31095
|
+
const failedStep = item.steps.find((step) => step.status === "failed");
|
|
31096
|
+
const category = item.status === "failed" ? classifyFailure(failedStep?.error) : void 0;
|
|
31097
|
+
executions.push({
|
|
31098
|
+
runId,
|
|
31099
|
+
caseId: item.caseId,
|
|
31100
|
+
caseName: item.caseName,
|
|
31101
|
+
status: item.status,
|
|
31102
|
+
category,
|
|
31103
|
+
platforms
|
|
31104
|
+
});
|
|
31105
|
+
}
|
|
31106
|
+
runPoints.push({
|
|
31107
|
+
runId,
|
|
31108
|
+
startedAt: summary.startedAt,
|
|
31109
|
+
status: summary.status,
|
|
31110
|
+
passed: summary.totals.passed,
|
|
31111
|
+
failed: summary.totals.failed,
|
|
31112
|
+
passRate: summary.totals.cases > 0 ? summary.totals.passed / summary.totals.cases : 0
|
|
31113
|
+
});
|
|
31114
|
+
}
|
|
31115
|
+
const byCase = /* @__PURE__ */ new Map();
|
|
31116
|
+
for (const exec2 of executions) {
|
|
31117
|
+
let agg = byCase.get(exec2.caseId);
|
|
31118
|
+
if (!agg) {
|
|
31119
|
+
agg = {
|
|
31120
|
+
caseId: exec2.caseId,
|
|
31121
|
+
caseName: exec2.caseName,
|
|
31122
|
+
platforms: [],
|
|
31123
|
+
runs: 0,
|
|
31124
|
+
passed: 0,
|
|
31125
|
+
failed: 0,
|
|
31126
|
+
passRate: 0,
|
|
31127
|
+
lastStatus: "unknown",
|
|
31128
|
+
flaky: false
|
|
31129
|
+
};
|
|
31130
|
+
byCase.set(exec2.caseId, agg);
|
|
31131
|
+
}
|
|
31132
|
+
agg.runs++;
|
|
31133
|
+
if (exec2.status === "passed") agg.passed++;
|
|
31134
|
+
else agg.failed++;
|
|
31135
|
+
for (const platform of exec2.platforms) {
|
|
31136
|
+
if (!agg.platforms.includes(platform)) agg.platforms.push(platform);
|
|
31137
|
+
}
|
|
31138
|
+
}
|
|
31139
|
+
for (const agg of byCase.values()) {
|
|
31140
|
+
const last = [...executions].reverse().find((exec2) => exec2.caseId === agg.caseId);
|
|
31141
|
+
agg.lastStatus = last?.status ?? "unknown";
|
|
31142
|
+
agg.lastCategory = last?.category;
|
|
31143
|
+
agg.passRate = agg.runs > 0 ? agg.passed / agg.runs : 0;
|
|
31144
|
+
agg.flaky = agg.failed > 0 && agg.passed > 0;
|
|
31145
|
+
}
|
|
31146
|
+
const cases = [...byCase.values()];
|
|
31147
|
+
const flaky = cases.filter((item) => item.flaky);
|
|
31148
|
+
const categories = FAILURE_CATEGORIES.map((item) => ({
|
|
31149
|
+
id: item.id,
|
|
31150
|
+
label: item.label,
|
|
31151
|
+
count: executions.filter((exec2) => exec2.status === "failed" && exec2.category === item.id).length
|
|
31152
|
+
})).filter((item) => item.count > 0);
|
|
31153
|
+
const durations = runs.map((item) => item.summary.durationMs).filter((value) => typeof value === "number");
|
|
31154
|
+
return {
|
|
31155
|
+
window: {
|
|
31156
|
+
runs: runs.length,
|
|
31157
|
+
from: runPoints[0]?.startedAt,
|
|
31158
|
+
to: runPoints.at(-1)?.startedAt
|
|
31159
|
+
},
|
|
31160
|
+
kpis: {
|
|
31161
|
+
runs: runs.length,
|
|
31162
|
+
executions: executions.length,
|
|
31163
|
+
overallRate: executions.length > 0 ? executions.filter((exec2) => exec2.status === "passed").length / executions.length : 0,
|
|
31164
|
+
avgDurationMs: durations.length > 0 ? durations.reduce((sum, value) => sum + value, 0) / durations.length : 0,
|
|
31165
|
+
flakyCount: flaky.length
|
|
31166
|
+
},
|
|
31167
|
+
runs: runPoints,
|
|
31168
|
+
cases,
|
|
31169
|
+
platforms: [...new Set(cases.flatMap((item) => item.platforms))].sort(),
|
|
31170
|
+
categories,
|
|
31171
|
+
categoryLabels: Object.fromEntries(FAILURE_CATEGORIES.map((item) => [item.id, item.label]))
|
|
31172
|
+
};
|
|
31173
|
+
}
|
|
31174
|
+
function renderSummaryTrend(runs) {
|
|
31175
|
+
if (runs.length === 0) return '<p class="muted">\u7EDF\u8BA1\u7A97\u53E3\u5185\u6CA1\u6709\u8FD0\u884C\u6570\u636E</p>';
|
|
31176
|
+
const barW = 30;
|
|
31177
|
+
const gap = 8;
|
|
31178
|
+
const chartH = 96;
|
|
31179
|
+
const width = runs.length * (barW + gap) + gap;
|
|
31180
|
+
const height = chartH + 30;
|
|
31181
|
+
const max = Math.max(1, ...runs.map((run) => run.passed + run.failed));
|
|
31182
|
+
const bars = runs.map((run, index) => {
|
|
31183
|
+
const x = gap + index * (barW + gap);
|
|
31184
|
+
const passedH = run.passed / max * chartH;
|
|
31185
|
+
const failedH = run.failed / max * chartH;
|
|
31186
|
+
const passedY = chartH - passedH;
|
|
31187
|
+
const failedY = passedY - failedH;
|
|
31188
|
+
const total = run.passed + run.failed;
|
|
31189
|
+
const rate = total > 0 ? Math.round(run.passed / total * 100) : 0;
|
|
31190
|
+
const label = run.runId.replace(/^\d{8}-/, "");
|
|
31191
|
+
return "<g><title>" + escapeHtml2(run.runId) + ": \u2713" + run.passed + " \u2717" + run.failed + '</title><rect x="' + x + '" y="' + passedY.toFixed(1) + '" width="' + barW + '" height="' + passedH.toFixed(1) + '" fill="#1a7f37" rx="2"></rect><rect x="' + x + '" y="' + failedY.toFixed(1) + '" width="' + barW + '" height="' + failedH.toFixed(1) + '" fill="#cf222e" rx="2"></rect><text x="' + (x + barW / 2) + '" y="' + (chartH + 12) + '" font-size="8" fill="#59636e" text-anchor="middle">' + escapeHtml2(label) + '</text><text x="' + (x + barW / 2) + '" y="' + (chartH + 24) + '" font-size="8" fill="#59636e" text-anchor="middle">' + rate + "%</text></g>";
|
|
31192
|
+
}).join("\n");
|
|
31193
|
+
return '<svg width="' + width + '" height="' + height + '" viewBox="0 0 ' + width + " " + height + '" xmlns="http://www.w3.org/2000/svg">' + bars + "</svg>";
|
|
31194
|
+
}
|
|
31195
|
+
function renderSummaryHtml(data) {
|
|
31196
|
+
const kpis = data.kpis;
|
|
31197
|
+
const rateText = pct(kpis.overallRate);
|
|
31198
|
+
const trendSvg = renderSummaryTrend(data.runs);
|
|
31199
|
+
const catBars = data.categories.map(
|
|
31200
|
+
(item) => '<div class="hbar-row" data-cat="' + item.id + '" title="\u70B9\u51FB\u7B5B\u9009\u7528\u4F8B\u660E\u7EC6"><span class="hbar-label">' + escapeHtml2(item.label) + '</span><span class="hbar-track"><span class="hbar" style="width:' + (kpis.executions > 0 ? Math.round(item.count / kpis.executions * 100) : 0) + '%"></span></span><span class="hbar-num">' + item.count + "</span></div>"
|
|
31201
|
+
).join("\n");
|
|
31202
|
+
const catHtml = catBars || '<p class="muted">\u7EDF\u8BA1\u7A97\u53E3\u5185\u65E0\u5931\u8D25</p>';
|
|
31203
|
+
const flaky = data.cases.filter((item) => item.flaky);
|
|
31204
|
+
const flakyRows = flaky.map(
|
|
31205
|
+
(item) => '<tr data-platform="' + escapeHtml2(item.platforms.join(" ")) + '" data-q="' + escapeHtml2(item.caseId + " " + item.caseName) + '"><td><code class="flaky-link" data-q="' + escapeHtml2(item.caseId) + '">' + escapeHtml2(item.caseId) + "</code></td><td>" + escapeHtml2(item.caseName) + "</td><td>" + escapeHtml2(item.platforms.join(" + ")) + '</td><td class="num">' + item.failed + " / " + item.runs + '</td><td class="num">' + pct(item.passRate) + '</td><td><span class="badge ' + item.lastStatus + '">' + item.lastStatus + "</span></td></tr>"
|
|
31206
|
+
).join("\n");
|
|
31207
|
+
const platformChips = ["all", ...data.platforms].map(
|
|
31208
|
+
(platform) => '<span class="chip' + (platform === "all" ? " active" : "") + '" data-platform="' + platform + '">' + (platform === "all" ? "\u5168\u90E8\u5E73\u53F0" : platform) + "</span>"
|
|
31209
|
+
).join("\n");
|
|
31210
|
+
const detailRows = data.cases.map(
|
|
31211
|
+
(item) => '<tr data-platform="' + escapeHtml2(item.platforms.join(" ")) + '" data-cat="' + escapeHtml2(item.lastCategory ?? "") + '" data-q="' + escapeHtml2(item.caseId + " " + item.caseName) + '" data-pass="' + item.passRate + '" data-runs="' + item.runs + '" data-failed="' + item.failed + '"><td><code>' + escapeHtml2(item.caseId) + "</code></td><td>" + escapeHtml2(item.caseName) + "</td><td>" + escapeHtml2(item.platforms.join(" + ")) + '</td><td class="num">' + item.runs + '</td><td class="num">' + item.passed + '</td><td class="num">' + item.failed + '</td><td><span class="mini-track"><span class="mini ' + (item.failed > 0 ? "warn" : "ok") + '" style="width:' + Math.round(item.passRate * 60) + 'px"></span></span> ' + pct(item.passRate) + "</td><td>" + (item.lastCategory ? escapeHtml2(categoryLabel(item.lastCategory)) : "-") + '</td><td><span class="badge ' + item.lastStatus + '">' + item.lastStatus + "</span></td></tr>"
|
|
31212
|
+
).join("\n");
|
|
31213
|
+
const range = (data.window.from ?? "").slice(0, 10) + " \u2192 " + (data.window.to ?? "").slice(0, 10);
|
|
31214
|
+
return `<!doctype html>
|
|
31215
|
+
<html lang="zh-CN">
|
|
31216
|
+
<head>
|
|
31217
|
+
<meta charset="utf-8" />
|
|
31218
|
+
<title>TestPilot \u6C47\u603B\u62A5\u544A</title>
|
|
31219
|
+
<style>
|
|
31220
|
+
:root { --ok:#1a7f37; --fail:#cf222e; --line:#d0d7de; --muted:#59636e; }
|
|
31221
|
+
* { box-sizing: border-box; }
|
|
31222
|
+
body { font-family: system-ui, "Segoe UI", sans-serif; margin: 0; color: #1f2328; background: #f6f8fa; }
|
|
31223
|
+
.wrap { max-width: 1120px; margin: 0 auto; padding: 20px 16px 56px; }
|
|
31224
|
+
h1 { font-size: 21px; margin: 8px 0; } h2 { font-size: 15px; margin: 26px 0 8px; }
|
|
31225
|
+
.meta { color: var(--muted); font-size: 12px; margin: 4px 0 12px; }
|
|
31226
|
+
.cards { display: flex; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
|
|
31227
|
+
.card { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 12px 18px; min-width: 140px; }
|
|
31228
|
+
.card b { font-size: 22px; display: block; } .card span { color: var(--muted); font-size: 12px; }
|
|
31229
|
+
.row2 { display: flex; gap: 14px; flex-wrap: wrap; align-items: stretch; }
|
|
31230
|
+
.panel { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 14px; }
|
|
31231
|
+
.panel h3 { margin: 0 0 8px; font-size: 13px; color: var(--muted); font-weight: 500; }
|
|
31232
|
+
.trend { overflow-x: auto; flex: 1; min-width: 320px; }
|
|
31233
|
+
table { border-collapse: collapse; width: 100%; font-size: 13px; background: #fff; }
|
|
31234
|
+
th, td { border-bottom: 1px solid var(--line); padding: 7px 10px; text-align: left; }
|
|
31235
|
+
th { color: var(--muted); font-weight: 500; cursor: pointer; white-space: nowrap; }
|
|
31236
|
+
th.num, td.num { text-align: right; }
|
|
31237
|
+
code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; cursor: pointer; }
|
|
31238
|
+
.badge { display: inline-block; padding: 1px 9px; border-radius: 10px; color: #fff; font-size: 11px; }
|
|
31239
|
+
.passed { background: #1a7f37; } .failed { background: #cf222e; } .unknown { background: #6e7781; }
|
|
31240
|
+
.toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 10px 0; }
|
|
31241
|
+
.chip { border: 1px solid var(--line); background: #fff; border-radius: 14px; padding: 3px 12px; font-size: 12px; cursor: pointer; }
|
|
31242
|
+
.chip.active { background: #0969da; color: #fff; border-color: #0969da; }
|
|
31243
|
+
input#q { flex: 1; min-width: 160px; padding: 5px 10px; border: 1px solid var(--line); border-radius: 6px; font-size: 13px; }
|
|
31244
|
+
#count { color: var(--muted); font-size: 12px; margin-left: auto; }
|
|
31245
|
+
.hbar-row { display: flex; align-items: center; gap: 10px; margin: 7px 0; cursor: pointer; }
|
|
31246
|
+
.hbar-label { width: 90px; font-size: 13px; } .hbar-num { width: 30px; text-align: right; font-size: 13px; }
|
|
31247
|
+
.hbar-track { flex: 1; background: #eef1f4; border-radius: 5px; height: 12px; overflow: hidden; }
|
|
31248
|
+
.hbar { display: block; height: 100%; background: var(--fail); }
|
|
31249
|
+
.mini-track { display: inline-block; width: 60px; height: 7px; background: #eef1f4; border-radius: 4px; overflow: hidden; vertical-align: middle; }
|
|
31250
|
+
.mini { display: block; height: 100%; } .mini.ok { background: var(--ok); } .mini.warn { background: #bf8700; }
|
|
31251
|
+
.muted { color: var(--muted); } .hide { display: none !important; }
|
|
31252
|
+
</style>
|
|
31253
|
+
</head>
|
|
31254
|
+
<body>
|
|
31255
|
+
<div class="wrap">
|
|
31256
|
+
<h1>TestPilot \u6C47\u603B\u62A5\u544A</h1>
|
|
31257
|
+
<p class="meta">\u7EDF\u8BA1\u8303\u56F4:\u6700\u8FD1 ${data.window.runs} \u6B21\u8FD0\u884C(${escapeHtml2(range)})\xB7 \u751F\u6210\u4E8E ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}</p>
|
|
31258
|
+
|
|
31259
|
+
<div class="cards">
|
|
31260
|
+
<div class="card"><b>${kpis.runs}</b><span>\u8FD0\u884C\u6B21\u6570</span></div>
|
|
31261
|
+
<div class="card"><b>${kpis.executions}</b><span>\u7528\u4F8B\u6267\u884C\u6B21\u6570</span></div>
|
|
31262
|
+
<div class="card"><b>${rateText}</b><span>\u6574\u4F53\u901A\u8FC7\u7387</span></div>
|
|
31263
|
+
<div class="card"><b>${(kpis.avgDurationMs / 1e3).toFixed(1)}s</b><span>\u5E73\u5747\u8FD0\u884C\u8017\u65F6</span></div>
|
|
31264
|
+
<div class="card"><b>${kpis.flakyCount}</b><span>Flaky \u7528\u4F8B</span></div>
|
|
31265
|
+
</div>
|
|
31266
|
+
|
|
31267
|
+
<h2>\u8D8B\u52BF(\u6BCF\u6B21\u8FD0\u884C\u7ED3\u679C\u4E0E\u901A\u8FC7\u7387,\u60AC\u6D6E\u770B\u8BE6\u60C5)</h2>
|
|
31268
|
+
<div class="panel trend">${trendSvg}</div>
|
|
31269
|
+
|
|
31270
|
+
<h2>\u5931\u8D25\u5206\u7C7B\u5206\u5E03(\u70B9\u51FB\u7B5B\u9009\u660E\u7EC6)</h2>
|
|
31271
|
+
<div class="panel">${catHtml}</div>
|
|
31272
|
+
|
|
31273
|
+
<h2>Flaky \u699C(\u53C8\u8FC7\u53C8\u6302;\u70B9\u7528\u4F8B\u540D\u5B9A\u4F4D\u660E\u7EC6)</h2>
|
|
31274
|
+
<div class="panel"><table>
|
|
31275
|
+
<tr><th>\u7528\u4F8B</th><th>\u540D\u79F0</th><th>\u5E73\u53F0</th><th class="num">\u5931\u8D25 / \u53C2\u4E0E</th><th class="num">\u901A\u8FC7\u7387</th><th>\u6700\u8FD1\u72B6\u6001</th></tr>
|
|
31276
|
+
${flakyRows || '<tr><td colspan="6" class="muted">\u7EDF\u8BA1\u7A97\u53E3\u5185\u65E0 Flaky \u7528\u4F8B</td></tr>'}
|
|
31277
|
+
</table></div>
|
|
31278
|
+
|
|
31279
|
+
<h2>\u7528\u4F8B\u660E\u7EC6</h2>
|
|
31280
|
+
<div class="toolbar">
|
|
31281
|
+
${platformChips}
|
|
31282
|
+
<input id="q" placeholder="\u641C\u7D22\u7528\u4F8B id / \u540D\u79F0\u2026" />
|
|
31283
|
+
<span id="count"></span>
|
|
31284
|
+
</div>
|
|
31285
|
+
<table id="detail">
|
|
31286
|
+
<thead><tr>
|
|
31287
|
+
<th>\u7528\u4F8B</th><th>\u540D\u79F0</th><th>\u5E73\u53F0</th>
|
|
31288
|
+
<th class="num" data-sort="runs">\u8FD0\u884C\u6B21\u6570</th><th class="num" data-sort="passed">\u901A\u8FC7</th><th class="num" data-sort="failed">\u5931\u8D25</th>
|
|
31289
|
+
<th data-sort="passRate">\u901A\u8FC7\u7387 \u25BE</th><th>\u6700\u8FD1\u5206\u7C7B</th><th>\u6700\u8FD1\u72B6\u6001</th>
|
|
31290
|
+
</tr></thead>
|
|
31291
|
+
<tbody>${detailRows}</tbody>
|
|
31292
|
+
</table>
|
|
31293
|
+
</div>
|
|
31294
|
+
|
|
31295
|
+
<script id="summary-data" type="application/json">${JSON.stringify(data).replaceAll("<", "\\u003c")}</script>
|
|
31296
|
+
<script>
|
|
31297
|
+
(function () {
|
|
31298
|
+
var rows = Array.prototype.slice.call(document.querySelectorAll('#detail tbody tr'));
|
|
31299
|
+
var state = { platform: 'all', cat: 'all', q: '', sortKey: 'passRate', sortDir: -1 };
|
|
31300
|
+
|
|
31301
|
+
function apply() {
|
|
31302
|
+
var shown = 0;
|
|
31303
|
+
rows.forEach(function (row) {
|
|
31304
|
+
var show = true;
|
|
31305
|
+
if (state.platform !== 'all' && row.getAttribute('data-platform').split(' ').indexOf(state.platform) === -1) show = false;
|
|
31306
|
+
if (state.cat !== 'all' && (row.getAttribute('data-cat') || '') !== state.cat) show = false;
|
|
31307
|
+
if (state.q && row.getAttribute('data-q').toLowerCase().indexOf(state.q.toLowerCase()) === -1) show = false;
|
|
31308
|
+
row.classList.toggle('hide', !show);
|
|
31309
|
+
if (show) shown++;
|
|
31310
|
+
});
|
|
31311
|
+
document.getElementById('count').textContent = shown + ' / ' + rows.length + ' \u7528\u4F8B';
|
|
31312
|
+
}
|
|
31313
|
+
|
|
31314
|
+
document.querySelectorAll('.chip[data-platform]').forEach(function (chip) {
|
|
31315
|
+
chip.addEventListener('click', function () {
|
|
31316
|
+
document.querySelectorAll('.chip[data-platform]').forEach(function (c) { c.classList.remove('active'); });
|
|
31317
|
+
chip.classList.add('active');
|
|
31318
|
+
state.platform = chip.getAttribute('data-platform');
|
|
31319
|
+
apply();
|
|
31320
|
+
});
|
|
31321
|
+
});
|
|
31322
|
+
document.querySelectorAll('.hbar-row').forEach(function (bar) {
|
|
31323
|
+
bar.addEventListener('click', function () {
|
|
31324
|
+
state.cat = bar.getAttribute('data-cat');
|
|
31325
|
+
apply();
|
|
31326
|
+
});
|
|
31327
|
+
});
|
|
31328
|
+
var q = document.getElementById('q');
|
|
31329
|
+
q.addEventListener('input', function () { state.q = q.value.trim(); apply(); });
|
|
31330
|
+
|
|
31331
|
+
document.querySelectorAll('#detail th[data-sort]').forEach(function (th) {
|
|
31332
|
+
th.addEventListener('click', function () {
|
|
31333
|
+
var key = th.getAttribute('data-sort');
|
|
31334
|
+
state.sortDir = state.sortKey === key ? -state.sortDir : -1;
|
|
31335
|
+
state.sortKey = key;
|
|
31336
|
+
var sorted = rows.slice().sort(function (a, b) {
|
|
31337
|
+
return (Number(a.getAttribute('data-' + key)) - Number(b.getAttribute('data-' + key))) * state.sortDir;
|
|
31338
|
+
});
|
|
31339
|
+
var tbody = document.querySelector('#detail tbody');
|
|
31340
|
+
sorted.forEach(function (row) { tbody.appendChild(row); });
|
|
31341
|
+
});
|
|
31342
|
+
});
|
|
31343
|
+
|
|
31344
|
+
apply();
|
|
31345
|
+
})();
|
|
31346
|
+
</script>
|
|
31347
|
+
</body>
|
|
31348
|
+
</html>`;
|
|
31349
|
+
}
|
|
31350
|
+
async function writeSummaryReport(artifactsDir, data) {
|
|
31351
|
+
const dir = resolve4(artifactsDir);
|
|
31352
|
+
await mkdir4(dir, { recursive: true });
|
|
31353
|
+
const htmlPath = join9(dir, "summary.html");
|
|
31354
|
+
const jsonPath = join9(dir, "summary.json");
|
|
31355
|
+
await writeFile5(jsonPath, JSON.stringify(data, null, 2), "utf8");
|
|
31356
|
+
await writeFile5(htmlPath, renderSummaryHtml(data), "utf8");
|
|
31357
|
+
return { html: htmlPath, json: jsonPath };
|
|
31358
|
+
}
|
|
30816
31359
|
|
|
30817
31360
|
// ../../packages/sdk/src/runs.ts
|
|
30818
|
-
import { readFile as
|
|
30819
|
-
import { isAbsolute as isAbsolute2, join as
|
|
31361
|
+
import { readFile as readFile6, readdir as readdir4 } from "node:fs/promises";
|
|
31362
|
+
import { isAbsolute as isAbsolute2, join as join17 } from "node:path";
|
|
30820
31363
|
|
|
30821
31364
|
// ../../packages/execution-engine/src/artifacts/index.ts
|
|
30822
|
-
import { mkdir as
|
|
30823
|
-
import { join as
|
|
31365
|
+
import { mkdir as mkdir9 } from "node:fs/promises";
|
|
31366
|
+
import { join as join13 } from "node:path";
|
|
30824
31367
|
|
|
30825
31368
|
// ../../packages/evidence/src/log.ts
|
|
30826
|
-
import { appendFile, mkdir as
|
|
30827
|
-
import { dirname as
|
|
31369
|
+
import { appendFile, mkdir as mkdir5 } from "node:fs/promises";
|
|
31370
|
+
import { dirname as dirname3 } from "node:path";
|
|
30828
31371
|
var RunLogger = class {
|
|
30829
31372
|
constructor(filePath) {
|
|
30830
31373
|
this.filePath = filePath;
|
|
@@ -30839,7 +31382,7 @@ var RunLogger = class {
|
|
|
30839
31382
|
return this.write("ERROR", message);
|
|
30840
31383
|
}
|
|
30841
31384
|
async write(level, message) {
|
|
30842
|
-
await
|
|
31385
|
+
await mkdir5(dirname3(this.filePath), { recursive: true });
|
|
30843
31386
|
const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level} ${message}
|
|
30844
31387
|
`;
|
|
30845
31388
|
await appendFile(this.filePath, line, "utf8");
|
|
@@ -30847,32 +31390,32 @@ var RunLogger = class {
|
|
|
30847
31390
|
};
|
|
30848
31391
|
|
|
30849
31392
|
// ../../packages/evidence/src/screenshot.ts
|
|
30850
|
-
import { mkdir as
|
|
30851
|
-
import { dirname as
|
|
31393
|
+
import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
|
|
31394
|
+
import { dirname as dirname4, join as join10 } from "node:path";
|
|
30852
31395
|
async function saveScreenshot(dir, fileName, data) {
|
|
30853
|
-
const file2 =
|
|
30854
|
-
await
|
|
30855
|
-
await
|
|
31396
|
+
const file2 = join10(dir, fileName);
|
|
31397
|
+
await mkdir6(dirname4(file2), { recursive: true });
|
|
31398
|
+
await writeFile6(file2, data);
|
|
30856
31399
|
return file2;
|
|
30857
31400
|
}
|
|
30858
31401
|
|
|
30859
31402
|
// ../../packages/evidence/src/trace.ts
|
|
30860
|
-
import { mkdir as
|
|
30861
|
-
import { dirname as
|
|
31403
|
+
import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
|
|
31404
|
+
import { dirname as dirname5, join as join11 } from "node:path";
|
|
30862
31405
|
async function saveTrace(dir, fileName, data) {
|
|
30863
|
-
const file2 =
|
|
30864
|
-
await
|
|
30865
|
-
await
|
|
31406
|
+
const file2 = join11(dir, fileName);
|
|
31407
|
+
await mkdir7(dirname5(file2), { recursive: true });
|
|
31408
|
+
await writeFile7(file2, data);
|
|
30866
31409
|
return file2;
|
|
30867
31410
|
}
|
|
30868
31411
|
|
|
30869
31412
|
// ../../packages/evidence/src/video.ts
|
|
30870
|
-
import { mkdir as
|
|
30871
|
-
import { dirname as
|
|
31413
|
+
import { mkdir as mkdir8, writeFile as writeFile8 } from "node:fs/promises";
|
|
31414
|
+
import { dirname as dirname6, join as join12 } from "node:path";
|
|
30872
31415
|
async function saveVideo(dir, fileName, data) {
|
|
30873
|
-
const file2 =
|
|
30874
|
-
await
|
|
30875
|
-
await
|
|
31416
|
+
const file2 = join12(dir, fileName);
|
|
31417
|
+
await mkdir8(dirname6(file2), { recursive: true });
|
|
31418
|
+
await writeFile8(file2, data);
|
|
30876
31419
|
return file2;
|
|
30877
31420
|
}
|
|
30878
31421
|
|
|
@@ -30880,11 +31423,11 @@ async function saveVideo(dir, fileName, data) {
|
|
|
30880
31423
|
var ArtifactManager = class {
|
|
30881
31424
|
constructor(runDir) {
|
|
30882
31425
|
this.runDir = runDir;
|
|
30883
|
-
this.screenshotsDir =
|
|
30884
|
-
this.videosDir =
|
|
30885
|
-
this.tracesDir =
|
|
30886
|
-
this.logsDir =
|
|
30887
|
-
this.logger = new RunLogger(
|
|
31426
|
+
this.screenshotsDir = join13(runDir, "screenshots");
|
|
31427
|
+
this.videosDir = join13(runDir, "videos");
|
|
31428
|
+
this.tracesDir = join13(runDir, "traces");
|
|
31429
|
+
this.logsDir = join13(runDir, "logs");
|
|
31430
|
+
this.logger = new RunLogger(join13(this.logsDir, "run.log"));
|
|
30888
31431
|
}
|
|
30889
31432
|
screenshotsDir;
|
|
30890
31433
|
videosDir;
|
|
@@ -30892,8 +31435,8 @@ var ArtifactManager = class {
|
|
|
30892
31435
|
logsDir;
|
|
30893
31436
|
logger;
|
|
30894
31437
|
async prepare() {
|
|
30895
|
-
await
|
|
30896
|
-
await
|
|
31438
|
+
await mkdir9(this.screenshotsDir, { recursive: true });
|
|
31439
|
+
await mkdir9(this.logsDir, { recursive: true });
|
|
30897
31440
|
}
|
|
30898
31441
|
/** 保存步骤截图,返回相对 run 目录的路径(screenshots/xxx.png,统一正斜杠供报告链接使用) */
|
|
30899
31442
|
async saveStepScreenshot(fileName, data) {
|
|
@@ -30957,8 +31500,8 @@ var ExecutionContext = class {
|
|
|
30957
31500
|
};
|
|
30958
31501
|
|
|
30959
31502
|
// ../../packages/execution-engine/src/lifecycle/index.ts
|
|
30960
|
-
import { mkdir as
|
|
30961
|
-
import { join as
|
|
31503
|
+
import { mkdir as mkdir10, readdir as readdir3 } from "node:fs/promises";
|
|
31504
|
+
import { join as join14 } from "node:path";
|
|
30962
31505
|
function dateStamp(date5 = /* @__PURE__ */ new Date()) {
|
|
30963
31506
|
const year = date5.getFullYear();
|
|
30964
31507
|
const month = String(date5.getMonth() + 1).padStart(2, "0");
|
|
@@ -30967,7 +31510,7 @@ function dateStamp(date5 = /* @__PURE__ */ new Date()) {
|
|
|
30967
31510
|
}
|
|
30968
31511
|
async function allocateRunId(runsRoot2) {
|
|
30969
31512
|
const stamp = dateStamp();
|
|
30970
|
-
const existing = await
|
|
31513
|
+
const existing = await readdir3(runsRoot2).catch(() => []);
|
|
30971
31514
|
let max = 0;
|
|
30972
31515
|
for (const name of existing) {
|
|
30973
31516
|
const match = /^(\d{8})-(\d{3})$/.exec(name);
|
|
@@ -30978,14 +31521,14 @@ async function allocateRunId(runsRoot2) {
|
|
|
30978
31521
|
return `${stamp}-${String(max + 1).padStart(3, "0")}`;
|
|
30979
31522
|
}
|
|
30980
31523
|
async function ensureRunDir(runsRoot2, runId) {
|
|
30981
|
-
const runDir =
|
|
30982
|
-
await
|
|
31524
|
+
const runDir = join14(runsRoot2, runId);
|
|
31525
|
+
await mkdir10(runDir, { recursive: true });
|
|
30983
31526
|
return runDir;
|
|
30984
31527
|
}
|
|
30985
31528
|
|
|
30986
31529
|
// ../../packages/execution-engine/src/events/index.ts
|
|
30987
|
-
import { appendFile as appendFile2, mkdir as
|
|
30988
|
-
import { dirname as
|
|
31530
|
+
import { appendFile as appendFile2, mkdir as mkdir11 } from "node:fs/promises";
|
|
31531
|
+
import { dirname as dirname7 } from "node:path";
|
|
30989
31532
|
var EventBus = class {
|
|
30990
31533
|
subscribers = [];
|
|
30991
31534
|
subscribe(subscriber) {
|
|
@@ -30999,7 +31542,7 @@ var EventBus = class {
|
|
|
30999
31542
|
};
|
|
31000
31543
|
function ndjsonSubscriber(filePath) {
|
|
31001
31544
|
return async (event) => {
|
|
31002
|
-
await
|
|
31545
|
+
await mkdir11(dirname7(filePath), { recursive: true });
|
|
31003
31546
|
await appendFile2(filePath, `${JSON.stringify(event)}
|
|
31004
31547
|
`, "utf8");
|
|
31005
31548
|
};
|
|
@@ -31355,8 +31898,8 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
|
|
|
31355
31898
|
}
|
|
31356
31899
|
|
|
31357
31900
|
// ../../packages/execution-engine/src/engine/test-runner.ts
|
|
31358
|
-
import { writeFile as
|
|
31359
|
-
import { join as
|
|
31901
|
+
import { writeFile as writeFile9 } from "node:fs/promises";
|
|
31902
|
+
import { join as join15, resolve as resolve5 } from "node:path";
|
|
31360
31903
|
var TestRunner = class {
|
|
31361
31904
|
resolver;
|
|
31362
31905
|
runsRoot;
|
|
@@ -31366,7 +31909,7 @@ var TestRunner = class {
|
|
|
31366
31909
|
accounts;
|
|
31367
31910
|
constructor(options) {
|
|
31368
31911
|
this.resolver = options.resolver;
|
|
31369
|
-
this.runsRoot =
|
|
31912
|
+
this.runsRoot = resolve5(options.runsRoot ?? join15(process.cwd(), ".testpilot", "artifacts", "runs"));
|
|
31370
31913
|
this.customRunId = options.runId;
|
|
31371
31914
|
this.extraSubscriber = options.onEvent;
|
|
31372
31915
|
this.signal = options.signal;
|
|
@@ -31379,7 +31922,7 @@ var TestRunner = class {
|
|
|
31379
31922
|
const artifacts = new ArtifactManager(runDir);
|
|
31380
31923
|
await artifacts.prepare();
|
|
31381
31924
|
const bus = new EventBus();
|
|
31382
|
-
bus.subscribe(ndjsonSubscriber(
|
|
31925
|
+
bus.subscribe(ndjsonSubscriber(join15(runDir, "events.ndjson")));
|
|
31383
31926
|
if (this.extraSubscriber) bus.subscribe(this.extraSubscriber);
|
|
31384
31927
|
await bus.emit({ type: "run-started", runId, totalCases: cases.length, ts: startedAt.toISOString() });
|
|
31385
31928
|
await artifacts.logger.info(`run ${runId} started with ${cases.length} case(s)`);
|
|
@@ -31412,7 +31955,7 @@ var TestRunner = class {
|
|
|
31412
31955
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
31413
31956
|
});
|
|
31414
31957
|
await artifacts.logger.info(`run ${runId} finished: ${summary.status}`);
|
|
31415
|
-
await
|
|
31958
|
+
await writeFile9(join15(runDir, RESULT_FILE), JSON.stringify(summary, null, 2), "utf8");
|
|
31416
31959
|
return summary;
|
|
31417
31960
|
}
|
|
31418
31961
|
};
|
|
@@ -31446,9 +31989,9 @@ async function openCaseContext(bundle, options, evidenceTmpDir) {
|
|
|
31446
31989
|
|
|
31447
31990
|
// ../../packages/adapter-playwright/src/page.ts
|
|
31448
31991
|
import { existsSync as existsSync2 } from "node:fs";
|
|
31449
|
-
import { mkdtemp, readFile as
|
|
31992
|
+
import { mkdtemp, readFile as readFile5, rm } from "node:fs/promises";
|
|
31450
31993
|
import { tmpdir } from "node:os";
|
|
31451
|
-
import { join as
|
|
31994
|
+
import { join as join16 } from "node:path";
|
|
31452
31995
|
var PlaywrightAdapter = class {
|
|
31453
31996
|
constructor(bundle, options = {}) {
|
|
31454
31997
|
this.bundle = bundle;
|
|
@@ -31462,7 +32005,7 @@ var PlaywrightAdapter = class {
|
|
|
31462
32005
|
/** 开始用例级取证:为本用例创建独立 context(录屏 + trace) */
|
|
31463
32006
|
async startEvidence(_caseId) {
|
|
31464
32007
|
await this.closeContextQuietly();
|
|
31465
|
-
const evidenceTmpDir = await mkdtemp(
|
|
32008
|
+
const evidenceTmpDir = await mkdtemp(join16(tmpdir(), "testpilot-pw-"));
|
|
31466
32009
|
this.current = {
|
|
31467
32010
|
...await openCaseContext(this.bundle, this.options, evidenceTmpDir),
|
|
31468
32011
|
evidenceTmpDir
|
|
@@ -31473,12 +32016,12 @@ var PlaywrightAdapter = class {
|
|
|
31473
32016
|
if (!this.current) return {};
|
|
31474
32017
|
const { context, page, evidenceTmpDir } = this.current;
|
|
31475
32018
|
this.current = void 0;
|
|
31476
|
-
const traceZipPath =
|
|
32019
|
+
const traceZipPath = join16(evidenceTmpDir ?? tmpdir(), "trace.zip");
|
|
31477
32020
|
await context.tracing.stop({ path: traceZipPath }).catch(() => void 0);
|
|
31478
32021
|
const recordedVideoPath = await page.video()?.path().catch(() => void 0);
|
|
31479
32022
|
await context.close().catch(() => void 0);
|
|
31480
|
-
const video = recordedVideoPath && existsSync2(recordedVideoPath) ? await
|
|
31481
|
-
const trace = evidenceTmpDir && existsSync2(traceZipPath) ? await
|
|
32023
|
+
const video = recordedVideoPath && existsSync2(recordedVideoPath) ? await readFile5(recordedVideoPath) : void 0;
|
|
32024
|
+
const trace = evidenceTmpDir && existsSync2(traceZipPath) ? await readFile5(traceZipPath) : void 0;
|
|
31482
32025
|
if (evidenceTmpDir) await rm(evidenceTmpDir, { recursive: true, force: true }).catch(() => void 0);
|
|
31483
32026
|
return { video, trace };
|
|
31484
32027
|
}
|
|
@@ -31582,7 +32125,7 @@ var automatorLauncher = {
|
|
|
31582
32125
|
}
|
|
31583
32126
|
};
|
|
31584
32127
|
function delay(ms) {
|
|
31585
|
-
return new Promise((
|
|
32128
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
31586
32129
|
}
|
|
31587
32130
|
async function connectWithRetry(wsEndpoint, timeoutMs) {
|
|
31588
32131
|
const startedAt = Date.now();
|
|
@@ -31942,7 +32485,7 @@ function createDefaultAdapters(config2) {
|
|
|
31942
32485
|
async function runCases(options = {}) {
|
|
31943
32486
|
const root = resolveRoot(options.root);
|
|
31944
32487
|
const config2 = await loadProjectConfig(root);
|
|
31945
|
-
const inputs = options.paths && options.paths.length > 0 ? options.paths.map((input2) => isAbsolute2(input2) ? input2 :
|
|
32488
|
+
const inputs = options.paths && options.paths.length > 0 ? options.paths.map((input2) => isAbsolute2(input2) ? input2 : join17(root, input2)) : [join17(root, config2.casesDir)];
|
|
31946
32489
|
const cases = await collectCases(inputs, { root });
|
|
31947
32490
|
if (cases.length === 0) {
|
|
31948
32491
|
throw new RunError("ENO_CASES", `\u672A\u627E\u5230\u7528\u4F8B\u6587\u4EF6(\u67E5\u627E:${inputs.join(", ")})`);
|
|
@@ -31968,7 +32511,7 @@ async function runCases(options = {}) {
|
|
|
31968
32511
|
];
|
|
31969
32512
|
const runner = new TestRunner({
|
|
31970
32513
|
resolver,
|
|
31971
|
-
runsRoot:
|
|
32514
|
+
runsRoot: join17(root, RUNS_DIR),
|
|
31972
32515
|
runId: options.runId,
|
|
31973
32516
|
onEvent: options.onEvent,
|
|
31974
32517
|
signal: options.signal,
|
|
@@ -31980,14 +32523,14 @@ async function runCases(options = {}) {
|
|
|
31980
32523
|
return { summary, invalid, tagFiltered, missingAccounts };
|
|
31981
32524
|
}
|
|
31982
32525
|
function runsRoot(root) {
|
|
31983
|
-
return
|
|
32526
|
+
return join17(resolveRoot(root), RUNS_DIR);
|
|
31984
32527
|
}
|
|
31985
32528
|
async function listRuns(options = {}) {
|
|
31986
32529
|
const base = runsRoot(options.root);
|
|
31987
|
-
const entries = await
|
|
32530
|
+
const entries = await readdir4(base).catch(() => []);
|
|
31988
32531
|
const metas = [];
|
|
31989
32532
|
for (const entry of entries.filter((name) => /^\d{8}-\d{3}$/.test(name)).sort().reverse()) {
|
|
31990
|
-
metas.push({ ...await readRunMeta(base, entry), runId: entry, dir:
|
|
32533
|
+
metas.push({ ...await readRunMeta(base, entry), runId: entry, dir: join17(base, entry) });
|
|
31991
32534
|
}
|
|
31992
32535
|
return metas;
|
|
31993
32536
|
}
|
|
@@ -31997,22 +32540,22 @@ async function latestRunId(options = {}) {
|
|
|
31997
32540
|
}
|
|
31998
32541
|
async function getRun(runId, options = {}) {
|
|
31999
32542
|
const base = runsRoot(options.root);
|
|
32000
|
-
const file2 =
|
|
32543
|
+
const file2 = join17(base, runId, RESULT_FILE);
|
|
32001
32544
|
try {
|
|
32002
|
-
return JSON.parse(await
|
|
32545
|
+
return JSON.parse(await readFile6(file2, "utf8"));
|
|
32003
32546
|
} catch {
|
|
32004
32547
|
throw new RunError("ENO_CASES", `\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${file2}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
|
|
32005
32548
|
}
|
|
32006
32549
|
}
|
|
32007
32550
|
async function getRunEvents(runId, options = {}) {
|
|
32008
|
-
const file2 =
|
|
32009
|
-
const source = await
|
|
32551
|
+
const file2 = join17(runsRoot(options.root), runId, "events.ndjson");
|
|
32552
|
+
const source = await readFile6(file2, "utf8").catch(() => "");
|
|
32010
32553
|
return source.split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
|
|
32011
32554
|
}
|
|
32012
32555
|
async function readRunMeta(base, runId) {
|
|
32013
32556
|
try {
|
|
32014
32557
|
const summary = JSON.parse(
|
|
32015
|
-
await
|
|
32558
|
+
await readFile6(join17(base, runId, RESULT_FILE), "utf8")
|
|
32016
32559
|
);
|
|
32017
32560
|
return {
|
|
32018
32561
|
status: summary.status,
|
|
@@ -32033,25 +32576,47 @@ async function generateReport(runId, options = {}) {
|
|
|
32033
32576
|
if (!id) {
|
|
32034
32577
|
throw new Error("\u672A\u627E\u5230\u8FD0\u884C\u8BB0\u5F55(\u5148\u8FD0\u884C\u4E00\u6B21\u7528\u4F8B)");
|
|
32035
32578
|
}
|
|
32036
|
-
const runDir =
|
|
32579
|
+
const runDir = join18(resolveRoot(options.root), RUNS_DIR, id);
|
|
32037
32580
|
let summary;
|
|
32038
32581
|
try {
|
|
32039
|
-
summary = JSON.parse(await
|
|
32582
|
+
summary = JSON.parse(await readFile7(join18(runDir, "result.json"), "utf8"));
|
|
32040
32583
|
} catch {
|
|
32041
|
-
throw new Error(`\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${
|
|
32584
|
+
throw new Error(`\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${join18(runDir, "result.json")}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
|
|
32042
32585
|
}
|
|
32043
32586
|
const written = await writeReports(runDir, summary);
|
|
32044
32587
|
return { runId: id, json: written.json, html: written.html };
|
|
32045
32588
|
}
|
|
32589
|
+
async function generateSummaryReport(options = {}) {
|
|
32590
|
+
const root = resolveRoot(options.root);
|
|
32591
|
+
const limit = options.last && options.last > 0 ? options.last : 30;
|
|
32592
|
+
const metas = await listRuns({ root });
|
|
32593
|
+
if (metas.length === 0) {
|
|
32594
|
+
throw new Error("\u7EDF\u8BA1\u7A97\u53E3\u5185\u6CA1\u6709\u4EFB\u4F55\u8FD0\u884C\u8BB0\u5F55(\u5148\u8FD0\u884C npx csspilot run)");
|
|
32595
|
+
}
|
|
32596
|
+
const runs = [];
|
|
32597
|
+
for (const meta3 of metas.slice(0, limit)) {
|
|
32598
|
+
try {
|
|
32599
|
+
runs.push({ runId: meta3.runId, summary: await getRun(meta3.runId, { root }) });
|
|
32600
|
+
} catch {
|
|
32601
|
+
}
|
|
32602
|
+
}
|
|
32603
|
+
if (runs.length === 0) {
|
|
32604
|
+
throw new Error("\u7EDF\u8BA1\u7A97\u53E3\u5185\u7684\u8FD0\u884C\u7ED3\u679C\u5747\u4E0D\u53EF\u8BFB");
|
|
32605
|
+
}
|
|
32606
|
+
const data = buildSummaryData(runs);
|
|
32607
|
+
const artifactsDir = join18(root, ".testpilot", "artifacts");
|
|
32608
|
+
const written = await writeSummaryReport(artifactsDir, data);
|
|
32609
|
+
return { html: written.html, json: written.json, data, window: { runs: runs.length } };
|
|
32610
|
+
}
|
|
32046
32611
|
async function readReport(runId, options = {}) {
|
|
32047
|
-
const file2 =
|
|
32048
|
-
const source = await
|
|
32612
|
+
const file2 = join18(resolveRoot(options.root), RUNS_DIR, runId, "report.json");
|
|
32613
|
+
const source = await readFile7(file2, "utf8").catch(() => void 0);
|
|
32049
32614
|
return source ? JSON.parse(source) : void 0;
|
|
32050
32615
|
}
|
|
32051
32616
|
|
|
32052
32617
|
// ../../packages/sdk/src/client.ts
|
|
32053
32618
|
function resolveInput(root, input2) {
|
|
32054
|
-
return isAbsolute3(input2) ? input2 :
|
|
32619
|
+
return isAbsolute3(input2) ? input2 : join19(root, input2);
|
|
32055
32620
|
}
|
|
32056
32621
|
var TestPilotClient = class {
|
|
32057
32622
|
/** 业务项目根目录(绝对路径) */
|
|
@@ -32116,6 +32681,10 @@ var TestPilotClient = class {
|
|
|
32116
32681
|
generateReport(runId) {
|
|
32117
32682
|
return generateReport(runId, { root: this.root });
|
|
32118
32683
|
}
|
|
32684
|
+
/** 生成跨 run 汇总报告(summary.html + summary.json) */
|
|
32685
|
+
generateSummaryReport(options = {}) {
|
|
32686
|
+
return generateSummaryReport({ ...options, root: this.root });
|
|
32687
|
+
}
|
|
32119
32688
|
/** 读取已生成的报告 */
|
|
32120
32689
|
readReport(runId) {
|
|
32121
32690
|
return readReport(runId, { root: this.root });
|
|
@@ -32124,12 +32693,12 @@ var TestPilotClient = class {
|
|
|
32124
32693
|
|
|
32125
32694
|
// src/commands/init/adapter-detector.ts
|
|
32126
32695
|
import { existsSync as existsSync4, readFileSync } from "node:fs";
|
|
32127
|
-
import { join as
|
|
32696
|
+
import { join as join20 } from "node:path";
|
|
32128
32697
|
async function detectAdapters(root, projectType) {
|
|
32129
32698
|
const required2 = projectType === "hybrid" ? ["playwright", "wechatide"] : projectType === "wechat-miniapp" ? ["wechatide"] : projectType === "web" ? ["playwright"] : [];
|
|
32130
32699
|
const pkg = readPackageJsonDeps(root);
|
|
32131
32700
|
const hasPlaywrightDep = pkg !== void 0 && ("playwright" in pkg || "@playwright/test" in pkg);
|
|
32132
|
-
const playwrightInstalled = hasPlaywrightDep || existsSync4(
|
|
32701
|
+
const playwrightInstalled = hasPlaywrightDep || existsSync4(join20(root, "node_modules", "playwright"));
|
|
32133
32702
|
const devtoolsCli = await findWechatDevToolsCli().catch(() => void 0);
|
|
32134
32703
|
return [
|
|
32135
32704
|
{
|
|
@@ -32147,7 +32716,7 @@ async function detectAdapters(root, projectType) {
|
|
|
32147
32716
|
];
|
|
32148
32717
|
}
|
|
32149
32718
|
function readPackageJsonDeps(root) {
|
|
32150
|
-
const file2 =
|
|
32719
|
+
const file2 = join20(root, "package.json");
|
|
32151
32720
|
if (!existsSync4(file2)) return void 0;
|
|
32152
32721
|
try {
|
|
32153
32722
|
const pkg = JSON.parse(readFileSync(file2, "utf8"));
|
|
@@ -32158,13 +32727,13 @@ function readPackageJsonDeps(root) {
|
|
|
32158
32727
|
}
|
|
32159
32728
|
|
|
32160
32729
|
// src/commands/init/environment-initializer.ts
|
|
32161
|
-
import { readFile as
|
|
32162
|
-
import { join as
|
|
32730
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
32731
|
+
import { join as join21 } from "node:path";
|
|
32163
32732
|
var ENV_REF_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
32164
32733
|
async function collectEnvVarRefs(root) {
|
|
32165
32734
|
let text;
|
|
32166
32735
|
try {
|
|
32167
|
-
text = await
|
|
32736
|
+
text = await readFile8(join21(root, "testpilot.yaml"), "utf8");
|
|
32168
32737
|
} catch {
|
|
32169
32738
|
return [];
|
|
32170
32739
|
}
|
|
@@ -32181,7 +32750,7 @@ async function collectEnvVarRefs(root) {
|
|
|
32181
32750
|
|
|
32182
32751
|
// src/commands/init/project-detector.ts
|
|
32183
32752
|
import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
|
|
32184
|
-
import { basename, join as
|
|
32753
|
+
import { basename, join as join22 } from "node:path";
|
|
32185
32754
|
|
|
32186
32755
|
// src/lib/git.ts
|
|
32187
32756
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -32218,7 +32787,7 @@ var WEB_DEP_MARKERS = [
|
|
|
32218
32787
|
];
|
|
32219
32788
|
var E2E_DIR_CANDIDATES = ["tests/e2e", "test/e2e", "e2e", "cypress"];
|
|
32220
32789
|
function readPackageJson(root) {
|
|
32221
|
-
const file2 =
|
|
32790
|
+
const file2 = join22(root, "package.json");
|
|
32222
32791
|
if (!existsSync5(file2)) return void 0;
|
|
32223
32792
|
try {
|
|
32224
32793
|
return JSON.parse(readFileSync2(file2, "utf8"));
|
|
@@ -32229,8 +32798,8 @@ function readPackageJson(root) {
|
|
|
32229
32798
|
function detectProjectType(root) {
|
|
32230
32799
|
const pkg = readPackageJson(root);
|
|
32231
32800
|
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
32232
|
-
const isWechat = existsSync5(
|
|
32233
|
-
const isWeb = existsSync5(
|
|
32801
|
+
const isWechat = existsSync5(join22(root, "project.config.json")) || existsSync5(join22(root, "app.json")) || existsSync5(join22(root, "miniprogram", "app.json"));
|
|
32802
|
+
const isWeb = existsSync5(join22(root, "index.html")) || WEB_DEP_MARKERS.some((marker) => deps?.[marker] !== void 0) || [".ts", ".tsx", ".js", ".jsx"].some((ext) => existsSync5(join22(root, "src", `index${ext}`)));
|
|
32234
32803
|
if (isWechat && isWeb) return "hybrid";
|
|
32235
32804
|
if (isWechat) return "wechat-miniapp";
|
|
32236
32805
|
if (isWeb) return "web";
|
|
@@ -32239,9 +32808,9 @@ function detectProjectType(root) {
|
|
|
32239
32808
|
async function detectProject(root) {
|
|
32240
32809
|
const pkg = readPackageJson(root);
|
|
32241
32810
|
const type = detectProjectType(root);
|
|
32242
|
-
const language = existsSync5(
|
|
32243
|
-
const packageManager = existsSync5(
|
|
32244
|
-
const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync5(
|
|
32811
|
+
const language = existsSync5(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync5(join22(root, "src")) ? "JavaScript" : "Unknown";
|
|
32812
|
+
const packageManager = existsSync5(join22(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync5(join22(root, "yarn.lock")) ? "yarn" : existsSync5(join22(root, "package-lock.json")) ? "npm" : "npx";
|
|
32813
|
+
const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync5(join22(root, dir)));
|
|
32245
32814
|
return {
|
|
32246
32815
|
root,
|
|
32247
32816
|
name: pkg?.name ?? basename(root),
|
|
@@ -32251,8 +32820,8 @@ async function detectProject(root) {
|
|
|
32251
32820
|
git: await readGitInfo(root),
|
|
32252
32821
|
hasTestsE2e: existingE2eDirs.includes("tests/e2e"),
|
|
32253
32822
|
existingE2eDirs,
|
|
32254
|
-
hasTestpilotConfig: existsSync5(
|
|
32255
|
-
hasProjectLink: existsSync5(
|
|
32823
|
+
hasTestpilotConfig: existsSync5(join22(root, "testpilot.yaml")),
|
|
32824
|
+
hasProjectLink: existsSync5(join22(root, ".testpilot", "project.json"))
|
|
32256
32825
|
};
|
|
32257
32826
|
}
|
|
32258
32827
|
function defaultAdapterFor(type) {
|
|
@@ -32268,8 +32837,8 @@ function adaptersFor(type) {
|
|
|
32268
32837
|
|
|
32269
32838
|
// src/commands/init/skill-installer.ts
|
|
32270
32839
|
import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
|
|
32271
|
-
import { access as access2, cp, mkdir as
|
|
32272
|
-
import { dirname as
|
|
32840
|
+
import { access as access2, cp, mkdir as mkdir12, writeFile as writeFile10 } from "node:fs/promises";
|
|
32841
|
+
import { dirname as dirname8, join as join23 } from "node:path";
|
|
32273
32842
|
import { fileURLToPath } from "node:url";
|
|
32274
32843
|
var CORE_FILES = ["SKILL.md", "manifest.yaml"];
|
|
32275
32844
|
var CORE_DIRS = ["rules", "workflows"];
|
|
@@ -32282,7 +32851,7 @@ async function exists3(path) {
|
|
|
32282
32851
|
}
|
|
32283
32852
|
}
|
|
32284
32853
|
function readManifestVersion(dir) {
|
|
32285
|
-
const manifestPath =
|
|
32854
|
+
const manifestPath = join23(dir, "manifest.yaml");
|
|
32286
32855
|
if (!existsSync6(manifestPath)) return void 0;
|
|
32287
32856
|
return /^version:\s*([^\s#]+)/m.exec(readFileSync3(manifestPath, "utf8"))?.[1];
|
|
32288
32857
|
}
|
|
@@ -32296,25 +32865,25 @@ function resolveSkillSourceDir() {
|
|
|
32296
32865
|
}
|
|
32297
32866
|
async function copyCoreFiles(src, dest) {
|
|
32298
32867
|
for (const name of CORE_FILES) {
|
|
32299
|
-
await cp(
|
|
32868
|
+
await cp(join23(src, name), join23(dest, name), { recursive: true, force: true });
|
|
32300
32869
|
}
|
|
32301
32870
|
for (const dir of CORE_DIRS) {
|
|
32302
|
-
if (existsSync6(
|
|
32303
|
-
await cp(
|
|
32871
|
+
if (existsSync6(join23(src, dir))) {
|
|
32872
|
+
await cp(join23(src, dir), join23(dest, dir), { recursive: true, force: true });
|
|
32304
32873
|
}
|
|
32305
32874
|
}
|
|
32306
32875
|
}
|
|
32307
32876
|
async function syncSkillFiles(root, options = {}) {
|
|
32308
|
-
const dest =
|
|
32877
|
+
const dest = join23(root, ".agents", "skills", "testpilot");
|
|
32309
32878
|
const bundledVersion = readManifestVersion(resolveSkillSourceDir() ?? "");
|
|
32310
|
-
if (!await exists3(
|
|
32879
|
+
if (!await exists3(join23(dest, "manifest.yaml"))) {
|
|
32311
32880
|
const src = resolveSkillSourceDir();
|
|
32312
32881
|
if (!src) {
|
|
32313
32882
|
throw new Error(
|
|
32314
32883
|
"\u65E0\u6CD5\u5B9A\u4F4D TestPilot Skill \u6E90\u76EE\u5F55;\u53EF\u901A\u8FC7\u73AF\u5883\u53D8\u91CF TESTPILOT_SKILL_DIR \u6307\u5B9A skills/testpilot \u8DEF\u5F84"
|
|
32315
32884
|
);
|
|
32316
32885
|
}
|
|
32317
|
-
await
|
|
32886
|
+
await mkdir12(dirname8(dest), { recursive: true });
|
|
32318
32887
|
await cp(src, dest, { recursive: true });
|
|
32319
32888
|
return { core: "created", bundledVersion, dest };
|
|
32320
32889
|
}
|
|
@@ -32335,14 +32904,14 @@ async function syncSkillFiles(root, options = {}) {
|
|
|
32335
32904
|
};
|
|
32336
32905
|
}
|
|
32337
32906
|
async function installSkill(root, info, adapters) {
|
|
32338
|
-
const dest =
|
|
32907
|
+
const dest = join23(root, ".agents", "skills", "testpilot");
|
|
32339
32908
|
const sync = await syncSkillFiles(root);
|
|
32340
32909
|
const core = sync.core === "created" ? "created" : sync.core === "updated" ? "updated" : "reused";
|
|
32341
32910
|
const coreVersion = sync.core === "updated" || sync.core === "created" ? { from: sync.installedVersion, to: sync.bundledVersion } : void 0;
|
|
32342
32911
|
const config2 = await loadTestpilotConfig(root).catch(() => void 0);
|
|
32343
32912
|
const defaultEnvironment = typeof config2?.environment?.default === "string" ? config2.environment.default : "\u672A\u8BBE\u7F6E";
|
|
32344
32913
|
const defaultWorkspace = config2?.workspace?.default ?? "\u672A\u8BBE\u7F6E(\u53EF\u5728 yaml workspace.default \u914D\u7F6E)";
|
|
32345
|
-
const refsDir =
|
|
32914
|
+
const refsDir = join23(dest, "references");
|
|
32346
32915
|
const files = [
|
|
32347
32916
|
["project.md", () => renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace)],
|
|
32348
32917
|
["test-conventions.md", renderTestConventionsMd],
|
|
@@ -32350,10 +32919,10 @@ async function installSkill(root, info, adapters) {
|
|
|
32350
32919
|
];
|
|
32351
32920
|
let references = "reused";
|
|
32352
32921
|
for (const [name, render] of files) {
|
|
32353
|
-
const file2 =
|
|
32922
|
+
const file2 = join23(refsDir, name);
|
|
32354
32923
|
if (await exists3(file2)) continue;
|
|
32355
|
-
await
|
|
32356
|
-
await
|
|
32924
|
+
await mkdir12(refsDir, { recursive: true });
|
|
32925
|
+
await writeFile10(file2, render(), "utf8");
|
|
32357
32926
|
references = "created";
|
|
32358
32927
|
}
|
|
32359
32928
|
return coreVersion ? { core, coreVersion, references, dest } : { core, references, dest };
|
|
@@ -32428,27 +32997,27 @@ async function runDoctor(root) {
|
|
|
32428
32997
|
const projectItems = [
|
|
32429
32998
|
ok(`Project ${info.name}(${info.type})`),
|
|
32430
32999
|
info.git.commit ? ok(`Git repository(${info.git.branch ?? "detached"} \xB7 ${info.git.commit.slice(0, 7)})`) : warn("Git repository \u672A\u68C0\u6D4B\u5230", "Case Metadata \u65E0\u6CD5\u540C\u6B65\u5230 Server"),
|
|
32431
|
-
existsSync7(
|
|
32432
|
-
existsSync7(
|
|
33000
|
+
existsSync7(join24(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
|
|
33001
|
+
existsSync7(join24(root, "testpilot.yaml")) ? ok("testpilot.yaml") : error62("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210")
|
|
32433
33002
|
];
|
|
32434
33003
|
push("Project", projectItems);
|
|
32435
33004
|
const config2 = await loadTestpilotConfig(root).catch(() => void 0);
|
|
32436
33005
|
const casesDir = config2?.casesDir ?? "tests/e2e/cases";
|
|
32437
|
-
const cases = await collectCases([
|
|
33006
|
+
const cases = await collectCases([join24(root, casesDir)], { root }).catch(() => []);
|
|
32438
33007
|
const invalidCount = cases.filter((item) => !item.valid).length;
|
|
32439
|
-
const testDir =
|
|
33008
|
+
const testDir = join24(root, "tests", "e2e");
|
|
32440
33009
|
push("Test", [
|
|
32441
33010
|
existsSync7(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
|
|
32442
|
-
existsSync7(
|
|
33011
|
+
existsSync7(join24(root, casesDir)) ? ok(`cases directory(${casesDir})`) : error62(`cases directory(${casesDir})`, "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
|
|
32443
33012
|
cases.length === 0 ? warn("Case validation \u8DF3\u8FC7(\u672A\u627E\u5230\u7528\u4F8B)", "\u5728 tests/e2e/cases \u521B\u5EFA Case") : invalidCount === 0 ? ok(`Case validation(${cases.length} \u4E2A\u7528\u4F8B\u5168\u90E8\u6709\u6548)`) : error62(`Case validation(${invalidCount}/${cases.length} \u4E2A\u7528\u4F8B\u6821\u9A8C\u5931\u8D25)`, "\u8FD0\u884C npx csspilot validate \u67E5\u770B\u8BE6\u60C5")
|
|
32444
33013
|
]);
|
|
32445
|
-
const skillDir =
|
|
33014
|
+
const skillDir = join24(root, ".agents", "skills", "testpilot");
|
|
32446
33015
|
const installedSkillVersion = readManifestVersion(skillDir);
|
|
32447
33016
|
const skillSource = resolveSkillSourceDir();
|
|
32448
33017
|
const bundledSkillVersion = skillSource ? readManifestVersion(skillSource) : void 0;
|
|
32449
33018
|
const skillItems = [
|
|
32450
33019
|
installedSkillVersion ? ok(`TestPilot Skill(.agents/skills/testpilot,v${installedSkillVersion})`) : error62("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5"),
|
|
32451
|
-
existsSync7(
|
|
33020
|
+
existsSync7(join24(skillDir, "references", "project.md")) ? ok("Project testing guide(references/)") : warn("Project testing guide \u7F3A\u5931", "references/ \u7531 init \u751F\u6210;\u53EF\u91CD\u65B0\u8FD0\u884C npx csspilot init")
|
|
32452
33021
|
];
|
|
32453
33022
|
if (installedSkillVersion && bundledSkillVersion && installedSkillVersion !== bundledSkillVersion) {
|
|
32454
33023
|
skillItems.push(
|
|
@@ -32540,11 +33109,11 @@ function makeDoctorCommand() {
|
|
|
32540
33109
|
}
|
|
32541
33110
|
|
|
32542
33111
|
// src/commands/init/index.ts
|
|
32543
|
-
import { join as
|
|
33112
|
+
import { join as join27 } from "node:path";
|
|
32544
33113
|
|
|
32545
33114
|
// src/commands/init/config-initializer.ts
|
|
32546
|
-
import { access as access3, mkdir as
|
|
32547
|
-
import { basename as basename2, join as
|
|
33115
|
+
import { access as access3, mkdir as mkdir13, readFile as readFile9, writeFile as writeFile11 } from "node:fs/promises";
|
|
33116
|
+
import { basename as basename2, join as join25 } from "node:path";
|
|
32548
33117
|
async function exists4(path) {
|
|
32549
33118
|
try {
|
|
32550
33119
|
await access3(path);
|
|
@@ -32618,42 +33187,42 @@ STAGING_BASE_URL=https://staging.example.com
|
|
|
32618
33187
|
`;
|
|
32619
33188
|
}
|
|
32620
33189
|
async function ensureGitignoreEntry(root, entry, comment = "TestPilot \u672C\u5730\u73AF\u5883\u53D8\u91CF") {
|
|
32621
|
-
const gitignorePath =
|
|
33190
|
+
const gitignorePath = join25(root, ".gitignore");
|
|
32622
33191
|
if (!await exists4(gitignorePath)) {
|
|
32623
|
-
await
|
|
33192
|
+
await writeFile11(gitignorePath, `# ${comment}
|
|
32624
33193
|
${entry}
|
|
32625
33194
|
`, "utf8");
|
|
32626
33195
|
return "created";
|
|
32627
33196
|
}
|
|
32628
|
-
const content = await
|
|
33197
|
+
const content = await readFile9(gitignorePath, "utf8");
|
|
32629
33198
|
if (content.split(/\r?\n/).some((item) => item.trim() === entry)) {
|
|
32630
33199
|
return "exists";
|
|
32631
33200
|
}
|
|
32632
33201
|
const separator = content.endsWith("\n") || content === "" ? "" : "\n";
|
|
32633
|
-
await
|
|
33202
|
+
await writeFile11(gitignorePath, `${content}${separator}# ${comment}
|
|
32634
33203
|
${entry}
|
|
32635
33204
|
`, "utf8");
|
|
32636
33205
|
return "updated";
|
|
32637
33206
|
}
|
|
32638
33207
|
async function initTestPilotConfig(root, info, adapters) {
|
|
32639
|
-
const yamlPath =
|
|
33208
|
+
const yamlPath = join25(root, "testpilot.yaml");
|
|
32640
33209
|
let yaml = "reused";
|
|
32641
33210
|
if (!await exists4(yamlPath)) {
|
|
32642
|
-
await
|
|
33211
|
+
await writeFile11(yamlPath, renderTestpilotYaml(info), "utf8");
|
|
32643
33212
|
yaml = "created";
|
|
32644
33213
|
}
|
|
32645
|
-
const envExamplePath =
|
|
33214
|
+
const envExamplePath = join25(root, ".env.example");
|
|
32646
33215
|
let envExample = "reused";
|
|
32647
33216
|
if (!await exists4(envExamplePath)) {
|
|
32648
|
-
await
|
|
33217
|
+
await writeFile11(envExamplePath, renderDotEnvExample(), "utf8");
|
|
32649
33218
|
envExample = "created";
|
|
32650
33219
|
}
|
|
32651
33220
|
const gitignore = await ensureGitignoreEntry(root, ".env");
|
|
32652
|
-
const runtimeDir =
|
|
32653
|
-
await
|
|
32654
|
-
const keep =
|
|
33221
|
+
const runtimeDir = join25(root, ".testpilot", "artifacts");
|
|
33222
|
+
await mkdir13(runtimeDir, { recursive: true });
|
|
33223
|
+
const keep = join25(runtimeDir, ".gitkeep");
|
|
32655
33224
|
if (!await exists4(keep)) {
|
|
32656
|
-
await
|
|
33225
|
+
await writeFile11(keep, "", "utf8");
|
|
32657
33226
|
}
|
|
32658
33227
|
const existing = await readProjectLink(root);
|
|
32659
33228
|
const defaultAdapter = defaultAdapterFor(info.type);
|
|
@@ -32674,7 +33243,7 @@ async function initTestPilotConfig(root, info, adapters) {
|
|
|
32674
33243
|
const projectLinkFile = await writeProjectLink(link, root);
|
|
32675
33244
|
return {
|
|
32676
33245
|
yaml,
|
|
32677
|
-
runtimeDir:
|
|
33246
|
+
runtimeDir: join25(root, ".testpilot"),
|
|
32678
33247
|
projectLink: link,
|
|
32679
33248
|
projectLinkFile,
|
|
32680
33249
|
envExample,
|
|
@@ -32710,13 +33279,13 @@ function randomId() {
|
|
|
32710
33279
|
|
|
32711
33280
|
// src/commands/init/test-structure.ts
|
|
32712
33281
|
import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
|
|
32713
|
-
import { mkdir as
|
|
32714
|
-
import { join as
|
|
33282
|
+
import { mkdir as mkdir14, writeFile as writeFile12 } from "node:fs/promises";
|
|
33283
|
+
import { join as join26 } from "node:path";
|
|
32715
33284
|
async function initTestStructure(root, _info) {
|
|
32716
33285
|
const dirs = {
|
|
32717
|
-
cases:
|
|
32718
|
-
fixtures:
|
|
32719
|
-
data:
|
|
33286
|
+
cases: join26(root, "tests", "e2e", "cases"),
|
|
33287
|
+
fixtures: join26(root, "tests", "e2e", "fixtures"),
|
|
33288
|
+
data: join26(root, "tests", "e2e", "data")
|
|
32720
33289
|
};
|
|
32721
33290
|
const existed = {
|
|
32722
33291
|
cases: existsSync8(dirs.cases),
|
|
@@ -32724,11 +33293,11 @@ async function initTestStructure(root, _info) {
|
|
|
32724
33293
|
data: existsSync8(dirs.data)
|
|
32725
33294
|
};
|
|
32726
33295
|
for (const dir of Object.values(dirs)) {
|
|
32727
|
-
await
|
|
33296
|
+
await mkdir14(dir, { recursive: true });
|
|
32728
33297
|
}
|
|
32729
33298
|
for (const [name, dir] of Object.entries(dirs)) {
|
|
32730
33299
|
if (!existed[name] && readdirSync2(dir).length === 0) {
|
|
32731
|
-
await
|
|
33300
|
+
await writeFile12(join26(dir, ".gitkeep"), "", "utf8").catch(() => void 0);
|
|
32732
33301
|
}
|
|
32733
33302
|
}
|
|
32734
33303
|
return {
|
|
@@ -32762,7 +33331,7 @@ async function runInit(root, options = {}) {
|
|
|
32762
33331
|
console.log(
|
|
32763
33332
|
config2.yaml === "created" ? "\u2713 \u9879\u76EE\u914D\u7F6E testpilot.yaml" : "\u2713 \u9879\u76EE\u914D\u7F6E testpilot.yaml(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
|
|
32764
33333
|
);
|
|
32765
|
-
console.log(`\u2713 Runtime \u76EE\u5F55 ${
|
|
33334
|
+
console.log(`\u2713 Runtime \u76EE\u5F55 ${join27(".testpilot", "artifacts")}`);
|
|
32766
33335
|
const serverMode = config2.projectLink.serverUrl ? `Server:${config2.projectLink.serverUrl}` : "\u672C\u5730\u6A21\u5F0F";
|
|
32767
33336
|
console.log(`\u2713 \u9879\u76EE\u5173\u8054 ${config2.projectLink.projectId}(${serverMode})`);
|
|
32768
33337
|
console.log(
|
|
@@ -32786,7 +33355,7 @@ async function runInit(root, options = {}) {
|
|
|
32786
33355
|
);
|
|
32787
33356
|
} else {
|
|
32788
33357
|
console.log(
|
|
32789
|
-
skill.core === "created" ? `\u2713 Skill \u5B89\u88C5 ${
|
|
33358
|
+
skill.core === "created" ? `\u2713 Skill \u5B89\u88C5 ${join27(".agents", "skills", "testpilot")}` : "\u2713 Skill \u5B89\u88C5 .agents/skills/testpilot/(\u5DF2\u5B58\u5728,\u8DF3\u8FC7)"
|
|
32790
33359
|
);
|
|
32791
33360
|
}
|
|
32792
33361
|
console.log(
|
|
@@ -32863,9 +33432,20 @@ function makeListCommand() {
|
|
|
32863
33432
|
// src/commands/report.ts
|
|
32864
33433
|
import { exec } from "node:child_process";
|
|
32865
33434
|
function makeReportCommand() {
|
|
32866
|
-
return new Command("report").description("\u751F\u6210\u6D4B\u8BD5\u62A5\u544A(JSON + HTML)").option("--run <id>", "\u8FD0\u884C\u8BB0\u5F55 ID,\u9ED8\u8BA4\u6700\u8FD1\u4E00\u6B21").option("--open", "\u751F\u6210\u540E\u5728\u6D4F\u89C8\u5668\u6253\u5F00", false).action(async (options) => {
|
|
33435
|
+
return new Command("report").description("\u751F\u6210\u6D4B\u8BD5\u62A5\u544A(JSON + HTML)").option("--run <id>", "\u8FD0\u884C\u8BB0\u5F55 ID,\u9ED8\u8BA4\u6700\u8FD1\u4E00\u6B21").option("--open", "\u751F\u6210\u540E\u5728\u6D4F\u89C8\u5668\u6253\u5F00", false).option("--summary", "\u751F\u6210\u8DE8 run \u6C47\u603B\u62A5\u544A(summary.html + summary.json)", false).option("--last <n>", "\u6C47\u603B\u7EDF\u8BA1\u7A97\u53E3:\u6700\u8FD1 N \u6B21\u8FD0\u884C", "30").action(async (options) => {
|
|
32867
33436
|
const client = new TestPilotClient();
|
|
32868
33437
|
try {
|
|
33438
|
+
if (options.summary) {
|
|
33439
|
+
const result = await client.generateSummaryReport({
|
|
33440
|
+
last: Number(options.last) > 0 ? Number(options.last) : void 0
|
|
33441
|
+
});
|
|
33442
|
+
console.log(
|
|
33443
|
+
`\u2713 summary.html ${result.html}(\u7EDF\u8BA1\u6700\u8FD1 ${result.window.runs} \u6B21\u8FD0\u884C,\u6574\u4F53\u901A\u8FC7\u7387 ${(result.data.kpis.overallRate * 100).toFixed(1)}%)`
|
|
33444
|
+
);
|
|
33445
|
+
console.log(`\u2713 summary.json ${result.json}`);
|
|
33446
|
+
if (options.open) await openInBrowser(result.html);
|
|
33447
|
+
return;
|
|
33448
|
+
}
|
|
32869
33449
|
const report = await client.generateReport(options.run);
|
|
32870
33450
|
console.log(`\u2713 report.json ${report.json}`);
|
|
32871
33451
|
console.log(`\u2713 report.html ${report.html}`);
|
|
@@ -32884,7 +33464,7 @@ function openInBrowser(file2) {
|
|
|
32884
33464
|
}
|
|
32885
33465
|
|
|
32886
33466
|
// src/commands/run.ts
|
|
32887
|
-
import { join as
|
|
33467
|
+
import { join as join28 } from "node:path";
|
|
32888
33468
|
function makeRunCommand() {
|
|
32889
33469
|
return new Command("run").description("\u6267\u884C\u6D4B\u8BD5\u7528\u4F8B(\u9ED8\u8BA4\u8FD0\u884C testpilot.yaml \u914D\u7F6E\u7684 Case)").argument("[paths...]", "\u7528\u4F8B\u6587\u4EF6\u6216\u76EE\u5F55,\u9ED8\u8BA4\u6309\u9879\u76EE\u914D\u7F6E").option("--tag <tag>", "\u6309\u6807\u7B7E\u8FC7\u6EE4\u7528\u4F8B").action(async (paths, options) => {
|
|
32890
33470
|
const client = new TestPilotClient();
|
|
@@ -32908,7 +33488,7 @@ function makeRunCommand() {
|
|
|
32908
33488
|
console.log(
|
|
32909
33489
|
`Run ${summary.runId}${summary.cancelled ? "(\u5DF2\u53D6\u6D88)" : ""}: ${summary.status.toUpperCase()} \u2014 \u7528\u4F8B \u2713${summary.totals.passed} \u2717${summary.totals.failed},\u6B65\u9AA4 \u2713${summary.totals.stepsPassed} \u2717${summary.totals.stepsFailed} \u21B7${summary.totals.stepsSkipped}${notes ? `(${notes})` : ""}`
|
|
32910
33490
|
);
|
|
32911
|
-
console.log(`\u4EA7\u7269\u76EE\u5F55:${
|
|
33491
|
+
console.log(`\u4EA7\u7269\u76EE\u5F55:${join28(".testpilot", "artifacts", "runs", summary.runId)}`);
|
|
32912
33492
|
console.log("\u67E5\u770B\u62A5\u544A:npx csspilot report");
|
|
32913
33493
|
if (summary.status === "failed") process.exitCode = 1;
|
|
32914
33494
|
} catch (err) {
|
|
@@ -33019,12 +33599,12 @@ function makeSyncMetadataCommand() {
|
|
|
33019
33599
|
}
|
|
33020
33600
|
|
|
33021
33601
|
// src/commands/update.ts
|
|
33022
|
-
import { writeFile as
|
|
33023
|
-
import { join as
|
|
33602
|
+
import { writeFile as writeFile13 } from "node:fs/promises";
|
|
33603
|
+
import { join as join29 } from "node:path";
|
|
33024
33604
|
function makeUpdateCommand() {
|
|
33025
33605
|
const update = new Command("update").description("\u540C\u6B65 TestPilot \u63A5\u5165\u7269\u5230\u5F53\u524D CLI \u7248\u672C(Skill / .env.example;\u4E0D\u89E6\u78B0 .env \u4E0E\u7528\u4F8B)").option("--force", "Skill \u7248\u672C\u4E00\u81F4\u65F6\u4E5F\u5F3A\u5236\u8986\u76D6\u672C\u5730\u4FEE\u6539", false).action(async (options) => {
|
|
33026
33606
|
const root = process.cwd();
|
|
33027
|
-
const dest =
|
|
33607
|
+
const dest = join29(".agents", "skills", "testpilot");
|
|
33028
33608
|
const skill = await syncSkillFiles(root, { force: options.force });
|
|
33029
33609
|
switch (skill.core) {
|
|
33030
33610
|
case "created":
|
|
@@ -33038,7 +33618,7 @@ function makeUpdateCommand() {
|
|
|
33038
33618
|
default:
|
|
33039
33619
|
console.log(`\u2713 Skill \u5DF2\u662F\u6700\u65B0 v${skill.bundledVersion ?? "?"}`);
|
|
33040
33620
|
}
|
|
33041
|
-
await
|
|
33621
|
+
await writeFile13(join29(root, ".env.example"), renderDotEnvExample(), "utf8");
|
|
33042
33622
|
console.log("\u2713 .env.example \u5DF2\u540C\u6B65\u4E3A\u5F53\u524D\u7248\u672C\u7684\u793A\u4F8B(.env \u4E0D\u53D7\u5F71\u54CD)");
|
|
33043
33623
|
const gitignore = await ensureGitignoreEntry(root, ".env");
|
|
33044
33624
|
console.log(
|
|
@@ -33085,10 +33665,13 @@ function makeValidateCommand() {
|
|
|
33085
33665
|
}
|
|
33086
33666
|
|
|
33087
33667
|
// src/cli.ts
|
|
33088
|
-
var CLI_VERSION = "0.
|
|
33668
|
+
var CLI_VERSION = "0.5.0";
|
|
33089
33669
|
async function createCli() {
|
|
33090
33670
|
const program2 = new Command();
|
|
33091
|
-
program2.name("csspilot").description("TestPilot \u2014 AI Native \u8DE8\u7AEF\u4E1A\u52A1\u6D4B\u8BD5\u57FA\u7840\u8BBE\u65BD CLI").version(CLI_VERSION).
|
|
33671
|
+
program2.name("csspilot").description("TestPilot \u2014 AI Native \u8DE8\u7AEF\u4E1A\u52A1\u6D4B\u8BD5\u57FA\u7840\u8BBE\u65BD CLI").version(CLI_VERSION, "-v, --version", "output the current version").option("-V", "output the current version", () => {
|
|
33672
|
+
console.log(CLI_VERSION);
|
|
33673
|
+
process.exit(0);
|
|
33674
|
+
}).addCommand(makeInitCommand()).addCommand(makeValidateCommand()).addCommand(makeListCommand()).addCommand(makeRunCommand()).addCommand(makeReportCommand()).addCommand(makeSyncMetadataCommand()).addCommand(makeDoctorCommand()).addCommand(makeUpdateCommand()).addCommand(makeCiCommand());
|
|
33092
33675
|
return program2;
|
|
33093
33676
|
}
|
|
33094
33677
|
export {
|