fix(ones-sync): step 回写按 ONES 形态列全量 step

经实测确认 ONES 写接口形态:case result 可保持 to_do,只在跑过的 step 上
填 execute_result,未跑 step 仅列 uuid,且需列出该用例全部 step。

- OnesStepResult.execute_result 改为可选(未跑 step 只带 uuid)
- buildAnchoredPayloads 支持 fullStepsByNumber:列全量 step,
  全部跑完才聚合 case 结果(passed/failed),否则保持 to_do
- fetchCaseSteps: 取用例全部 step uuid
- sync 脚本对含 step 锚点的用例先拉全量 step 列表再回写

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
woan 2026-05-29 15:49:22 +08:00
parent 88435b0ffc
commit fda826446a
2 changed files with 74 additions and 13 deletions

View File

@ -20,6 +20,7 @@ import {
matchResults,
buildAnchoredPayloads,
postPayloads,
fetchCaseSteps,
OnesUpdatePayload,
} from '../utils/ones-sync';
import { execSync } from 'child_process';
@ -88,9 +89,23 @@ function main() {
// 3. 匹配:锚点优先(支持 step 级),无锚点回退 LCS
console.log(`\n[3/4] 匹配 (锚点优先 + LCS 兜底) ...`);
const { payloads: anchored, unanchored } = buildAnchoredPayloads(planCases, testResults, executor);
// 含 step 锚点的用例需列全量 step → 先取其完整 step 列表
const stepNums = new Set<number>();
for (const tr of testResults) {
const m = /\[ONES:(\d+)#/.exec(tr.name);
if (m) stepNums.add(parseInt(m[1], 10));
}
const fullStepsByNumber = new Map<number, string[]>();
for (const num of stepNums) fullStepsByNumber.set(num, fetchCaseSteps(num));
const { payloads: anchored, unanchored } = buildAnchoredPayloads(
planCases,
testResults,
executor,
{ fullStepsByNumber }
);
const anchoredUUIDs = new Set(anchored.map(p => p.uuid));
const stepCount = anchored.reduce((n, p) => n + p.steps.length, 0);
const stepCount = anchored.reduce((n, p) => n + p.steps.filter(s => s.execute_result).length, 0);
console.log(` 锚点匹配: ${anchored.length} 用例 (含 ${stepCount} 个 step 级结果)`);
// LCS 兜底:仅补充锚点未覆盖的用例

View File

@ -14,8 +14,9 @@ export interface OnesPlanCase {
export interface OnesStepResult {
uuid: string;
// 执行结果填到 execute_resultstep.result 是预期文本,属定义不可覆盖)
execute_result: 'passed' | 'failed' | 'skipped';
// 执行结果填到 execute_resultstep.result 是预期文本,属定义不可覆盖)。
// 未执行的 step 只列 uuid、不带 execute_result。
execute_result?: 'passed' | 'failed' | 'skipped';
actual_result?: string;
}
@ -140,22 +141,43 @@ function lcs(a: string, b: string): number {
return dp[m][n];
}
/**
* step uuid step
*/
export function fetchCaseSteps(caseNumber: number): string[] {
try {
const out = execSync(`${ONES_CLI} testcase case search --key ${caseNumber}`, {
encoding: 'utf-8',
timeout: 30000,
});
const c = JSON.parse(out).cases?.[0];
return (c?.steps || []).map((s: any) => s.uuid);
} catch {
return [];
}
}
/**
* payload case step
* - [ONES:<num>] case
* - [ONES:<num>#<stepUuid>] step case
* - [ONES:<num>#<stepUuid>] step
* matchResults LCS
*
* opts.fullStepsByNumber: 用例号 step uuid ONES step
* execute_result uuid case to_do
*/
export function buildAnchoredPayloads(
planCases: OnesPlanCase[],
testResults: TestResult[],
executor: string
executor: string,
opts: { fullStepsByNumber?: Map<number, string[]> } = {}
): { payloads: OnesUpdatePayload[]; unanchored: TestResult[] } {
const byNumber = new Map<number, OnesPlanCase>();
for (const pc of planCases) byNumber.set(pc.caseNumber, pc);
const caseResult = new Map<string, SyncStatus>();
const caseSteps = new Map<string, Map<string, OnesStepResult>>();
const caseLevel = new Map<string, SyncStatus>(); // caseUUID -> result (add/feature)
const runSteps = new Map<string, Map<string, OnesStepResult>>(); // caseUUID -> stepUuid -> result
const numberByUUID = new Map<string, number>();
const unanchored: TestResult[] = [];
for (const tr of testResults) {
@ -166,23 +188,47 @@ export function buildAnchoredPayloads(
}
const pc = byNumber.get(parseInt(m[1], 10));
if (!pc) continue; // 锚点用例不在本计划内
numberByUUID.set(pc.caseUUID, pc.caseNumber);
const status = toStatus(tr.status);
caseResult.set(pc.caseUUID, mergeStatus(caseResult.get(pc.caseUUID), status));
if (m[2]) {
if (!caseSteps.has(pc.caseUUID)) caseSteps.set(pc.caseUUID, new Map());
caseSteps.get(pc.caseUUID)!.set(m[2], {
if (!runSteps.has(pc.caseUUID)) runSteps.set(pc.caseUUID, new Map());
runSteps.get(pc.caseUUID)!.set(m[2], {
uuid: m[2],
execute_result: status,
actual_result: (tr.detail || '').slice(0, 500),
});
} else {
caseLevel.set(pc.caseUUID, mergeStatus(caseLevel.get(pc.caseUUID), status));
}
}
const payloads: OnesUpdatePayload[] = [];
for (const [caseUUID, result] of caseResult) {
const steps = caseSteps.has(caseUUID) ? Array.from(caseSteps.get(caseUUID)!.values()) : [];
// step 级用例:列全量 step未跑的仅 uuid全部跑完才聚合 case 结果
for (const [caseUUID, stepMap] of runSteps) {
const num = numberByUUID.get(caseUUID)!;
const full = opts.fullStepsByNumber?.get(num);
let steps: OnesStepResult[];
let allRun: boolean;
if (full && full.length) {
steps = full.map((uuid) => stepMap.get(uuid) ?? { uuid });
allRun = full.every((uuid) => stepMap.has(uuid));
} else {
steps = Array.from(stepMap.values());
allRun = true; // 无全量列表时,以已有结果为准
}
let agg: SyncStatus | undefined;
for (const s of stepMap.values()) if (s.execute_result) agg = mergeStatus(agg, s.execute_result);
const result: OnesUpdatePayload['result'] = allRun ? agg ?? 'to_do' : 'to_do';
payloads.push({ uuid: caseUUID, executor, note: '', result, steps });
}
// case 级用例(添加/功能,无 step 结果)
for (const [caseUUID, result] of caseLevel) {
if (runSteps.has(caseUUID)) continue;
payloads.push({ uuid: caseUUID, executor, note: '', result, steps: [] });
}
return { payloads, unanchored };
}