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