131 lines
3.9 KiB
TypeScript
131 lines
3.9 KiB
TypeScript
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||
import { join, relative } from "node:path";
|
||
|
||
import type { InvalidArticleSample, RunSummary, SampleResult } from "./types";
|
||
|
||
interface AggregateInput {
|
||
startedAt: string;
|
||
finishedAt: string;
|
||
provider: string;
|
||
model: string;
|
||
baseURL: string;
|
||
reportDir: string;
|
||
samples: SampleResult[];
|
||
invalidSamples: InvalidArticleSample[];
|
||
}
|
||
|
||
export function writeSampleResult(reportDir: string, result: SampleResult) {
|
||
const sampleDir = join(reportDir, "samples", result.slug);
|
||
mkdirSync(sampleDir, { recursive: true });
|
||
writeFileSync(
|
||
join(sampleDir, "result.json"),
|
||
`${JSON.stringify(result, null, 2)}\n`,
|
||
);
|
||
}
|
||
|
||
export function readSampleResults(reportDir: string): SampleResult[] {
|
||
const samplesDir = join(reportDir, "samples");
|
||
try {
|
||
return readdirSync(samplesDir, { withFileTypes: true })
|
||
.filter((entry) => entry.isDirectory())
|
||
.map((entry) => join(samplesDir, entry.name, "result.json"))
|
||
.map(
|
||
(filePath) => JSON.parse(readFileSync(filePath, "utf8")) as SampleResult,
|
||
)
|
||
.sort((left, right) => left.file.localeCompare(right.file));
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
export function aggregateSummary(input: AggregateInput): RunSummary {
|
||
const passed = input.samples.filter((sample) => sample.status === "passed").length;
|
||
const failed = input.samples.filter((sample) => sample.status === "failed").length;
|
||
const skipped =
|
||
input.samples.filter((sample) => sample.status === "skipped").length +
|
||
input.invalidSamples.length;
|
||
|
||
return {
|
||
started_at: input.startedAt,
|
||
finished_at: input.finishedAt,
|
||
mode: "live",
|
||
provider: input.provider,
|
||
model: input.model,
|
||
base_url: input.baseURL,
|
||
report_dir: input.reportDir,
|
||
totals: { passed, failed, skipped },
|
||
samples: input.samples,
|
||
invalid_samples: input.invalidSamples,
|
||
};
|
||
}
|
||
|
||
export function writeSummaryFiles(reportDir: string, summary: RunSummary) {
|
||
mkdirSync(reportDir, { recursive: true });
|
||
writeFileSync(
|
||
join(reportDir, "summary.json"),
|
||
`${JSON.stringify(summary, null, 2)}\n`,
|
||
);
|
||
writeFileSync(join(reportDir, "summary.md"), renderSummaryMarkdown(summary));
|
||
}
|
||
|
||
export function redactSensitiveText(
|
||
text: string,
|
||
secrets: Array<string | undefined>,
|
||
) {
|
||
let redacted = text
|
||
.replace(/(x-api-key:\s*)([^\s]+)/gi, "$1[REDACTED]")
|
||
.replace(/(authorization:\s*bearer\s+)([^\s]+)/gi, "$1[REDACTED]")
|
||
.replace(
|
||
/((?:DEEPSEEK|OPENAI|API)_?[A-Z_]*KEY=)"?([^"\s]+)"?/g,
|
||
'$1"[REDACTED]"',
|
||
);
|
||
|
||
for (const secret of secrets) {
|
||
if (!secret) continue;
|
||
redacted = redacted.split(secret).join("[REDACTED]");
|
||
}
|
||
|
||
return redacted;
|
||
}
|
||
|
||
function renderSummaryMarkdown(summary: RunSummary) {
|
||
const lines = [
|
||
"# GEO 样例文章 E2E 测试报告",
|
||
"",
|
||
`- 模式:${summary.mode}`,
|
||
`- Provider:${summary.provider}`,
|
||
`- Model:${summary.model}`,
|
||
`- Base URL:${summary.base_url}`,
|
||
`- 报告目录:${summary.report_dir}`,
|
||
`- 结果:通过 ${summary.totals.passed},失败 ${summary.totals.failed},跳过 ${summary.totals.skipped}`,
|
||
"",
|
||
"| 样例 | 状态 | QA | 耗时 | 失败分类 |",
|
||
"| --- | --- | --- | ---: | --- |",
|
||
...summary.samples
|
||
.map((sample) =>
|
||
[
|
||
sample.name,
|
||
sample.status,
|
||
sample.qa_status ?? "",
|
||
`${Math.round(sample.duration_ms / 1000)}s`,
|
||
sample.failure_category ?? "",
|
||
].join(" | "),
|
||
)
|
||
.map((row) => `| ${row} |`),
|
||
];
|
||
|
||
if (summary.invalid_samples.length > 0) {
|
||
lines.push("", "## 无效样例", "");
|
||
for (const sample of summary.invalid_samples) {
|
||
lines.push(`- ${sample.fileName}: ${sample.reason}`);
|
||
}
|
||
}
|
||
|
||
lines.push("");
|
||
return lines.join("\n");
|
||
}
|
||
|
||
export function relativeArtifactPath(reportDir: string, artifactPath: string) {
|
||
return relative(reportDir, artifactPath);
|
||
}
|