From fda826446a52d00ca1ec3d4d8db4d22e3cbeaee0 Mon Sep 17 00:00:00 2001 From: woan <798680981@qq.com> Date: Fri, 29 May 2026 15:49:22 +0800 Subject: [PATCH] =?UTF-8?q?fix(ones-sync):=20step=20=E5=9B=9E=E5=86=99?= =?UTF-8?q?=E6=8C=89=20ONES=20=E5=BD=A2=E6=80=81=E5=88=97=E5=85=A8?= =?UTF-8?q?=E9=87=8F=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 经实测确认 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 --- scripts/sync-ones-results.ts | 19 ++++++++-- utils/ones-sync.ts | 68 ++++++++++++++++++++++++++++++------ 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/scripts/sync-ones-results.ts b/scripts/sync-ones-results.ts index 52e33f1..94cc418 100644 --- a/scripts/sync-ones-results.ts +++ b/scripts/sync-ones-results.ts @@ -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(); + for (const tr of testResults) { + const m = /\[ONES:(\d+)#/.exec(tr.name); + if (m) stepNums.add(parseInt(m[1], 10)); + } + const fullStepsByNumber = new Map(); + 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 兜底:仅补充锚点未覆盖的用例 diff --git a/utils/ones-sync.ts b/utils/ones-sync.ts index af15f59..9ea68af 100644 --- a/utils/ones-sync.ts +++ b/utils/ones-sync.ts @@ -14,8 +14,9 @@ export interface OnesPlanCase { export interface OnesStepResult { uuid: string; - // 执行结果填到 execute_result(step.result 是预期文本,属定义不可覆盖) - execute_result: 'passed' | 'failed' | 'skipped'; + // 执行结果填到 execute_result(step.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:] → case 级结果 - * - [ONES:#] → step 级结果,并聚合出 case 级结果 + * - [ONES:#] → 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 } = {} ): { payloads: OnesUpdatePayload[]; unanchored: TestResult[] } { const byNumber = new Map(); for (const pc of planCases) byNumber.set(pc.caseNumber, pc); - const caseResult = new Map(); - const caseSteps = new Map>(); + const caseLevel = new Map(); // caseUUID -> result (add/feature) + const runSteps = new Map>(); // caseUUID -> stepUuid -> result + const numberByUUID = new Map(); 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 }; }