From 7d779fb5a5d12e3d0cf10b447546c99f0ec64b9d Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Mon, 12 Oct 2026 07:17:35 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(run-outcome):=20label=20safety=20block?= =?UTF-8?q?=20as=20=E8=A2=AB=E6=8B=A6;=20pass=20progress=20tracker=20(O113?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Safety/approval denials on incomplete runs now use 「被拦:…」 / `Blocked by: …` with the tool/command instead of looking like a normal 「卡在:」. Non-block cases (tool fail, zero writes, budget) stay 「卡在」 or omit — never mislabeled. Progress stderr (output.ts done) passes the progress completionTracker into formatProgressOutcomeLine so budget-exhausted lines match stdout, e.g. 「卡在:运行预算(read_file: …)」. Exit codes and src/safety unchanged; O111 omit-unknown preserved. --- src/cli/output.ts | 4 + src/cli/run-completion.ts | 31 +- src/cli/run-outcome.ts | 35 +- src/cli/run-progress.ts | 8 + ...o113-blocked-and-progress-tracker.spec.mjs | 379 ++++++++++++++++++ 5 files changed, 454 insertions(+), 3 deletions(-) create mode 100644 test/o113-blocked-and-progress-tracker.spec.mjs diff --git a/src/cli/output.ts b/src/cli/output.ts index a9ee9a29..c06a3547 100644 --- a/src/cli/output.ts +++ b/src/cli/output.ts @@ -1039,11 +1039,15 @@ export function createCliRunRenderer(options: CliRunRendererOptions = {}) { if (unfinished) { // O99 / O98: progress-path footer via formatOutcomeLine (never empty). // Exit code is owned by oneshot resolveRunOutcome. + // O113: pass the progress completionTracker so budget/turn-limit + // stderr matches stdout (e.g. 「卡在:运行预算(read_file: …)」). const zh = isZhLocale(); + const prog = ensureProgress(); stderrLine( `${mark('fail')} ${formatProgressOutcomeLine({ stopReason, finalText: event.result?.response ?? '', + tracker: prog.completionTracker(), zh, })}` ); diff --git a/src/cli/run-completion.ts b/src/cli/run-completion.ts index 247a84b3..10439ddc 100644 --- a/src/cli/run-completion.ts +++ b/src/cli/run-completion.ts @@ -49,6 +49,11 @@ export interface RunCompletionTracker { wroteFiles: boolean; /** Last failed tool step, e.g. `exec: npm run check`. */ lastFailedStep?: string; + /** + * O113: last tool step blocked by safety / approval (not a normal tool fail). + * When set, the incomplete last line uses 「被拦:…」 / `Blocked by: …`. + */ + lastBlockedStep?: string; /** Last tool step that started, e.g. `multi_edit: src/cli/hooks.ts`. */ lastStep?: string; /** Latest MOSS_GOAL_VERIFY_CMD result (O98 — must feed the tracker). */ @@ -140,6 +145,23 @@ function execFailed(result: string): boolean { return /\bexit(?:ed)?(?:\s+code)?[\s:=]+[1-9]\d*\b/i.test(result); } +/** + * O113: true when a tool_end result is a safety / approval / policy block + * (not a normal tool failure like exit 1 or missing file). + */ +export function isSafetyOrApprovalBlockResult(result: string | undefined): boolean { + if (!result) return false; + const head = result.slice(0, 600); + return ( + /^\s*Command blocked:/i.test(head) || + /^\s*Blocked dangerous command:/i.test(head) || + /^\s*已拦截/u.test(head) || + /^\s*Blocked\b/i.test(head) || + /^\s*Tool execution denied\b/i.test(head) || + /^\s*Operation blocked by workspace policy\b/i.test(head) + ); +} + /** Feed one agent event into the tracker. Safe to call for every event. */ export function noteRunCompletionEvent(tracker: RunCompletionTracker, event: MossAgentEvent): void { if (event.type === 'tool_start') { @@ -153,7 +175,14 @@ export function noteRunCompletionEvent(tracker: RunCompletionTracker, event: Mos const input = started?.input ?? {}; const toolName = started?.toolName ?? event.toolName; const failed = event.isError || (EXEC_TOOLS.has(toolName) && execFailed(event.result)); - if (failed) tracker.lastFailedStep = describeStep(toolName, input); + if (failed) { + const step = describeStep(toolName, input); + tracker.lastFailedStep = step; + // O113: remember safety/approval blocks separately so the last line can say 被拦. + if (isSafetyOrApprovalBlockResult(event.result)) { + tracker.lastBlockedStep = step; + } + } if (FILE_WRITE_TOOLS.has(toolName) && !failed) tracker.wroteFiles = true; if (toolName === 'run_tests') diff --git a/src/cli/run-outcome.ts b/src/cli/run-outcome.ts index 0ea59e43..ac8a0cff 100644 --- a/src/cli/run-outcome.ts +++ b/src/cli/run-outcome.ts @@ -57,6 +57,11 @@ export interface RunOutcome { stuckAt?: string; /** Where the run got stuck (Chinese), when known. */ stuckAtZh?: string; + /** + * O113: when true, the stuck half uses 「被拦:…」 / `Blocked by: …` + * (safety/approval block). Otherwise 「卡在:…」 / `stuck at: …`. + */ + stuckWasBlocked?: boolean; } export interface ResolveRunOutcomeInput { @@ -233,7 +238,11 @@ export function resolveRunOutcome(input: ResolveRunOutcomeInput): RunOutcome { // O111: unfinished class (turn/budget/…) plus the last tool step when both // are known, e.g. `run budget (read_file: src/a.ts)`; either alone otherwise. + // O113: a safety/approval block alone uses 「被拦」; budget/turn-limit classes + // stay 「卡在」 even when a blocked tool also ran (do not mislabel budget). + let stuckWasBlocked = false; if (!stuckAt) { + const blockedStep = tracker.lastBlockedStep; const step = tracker.lastFailedStep ?? tracker.lastStep; const cls = input.runError ? stuckClassFromUnfinished(input.runError) : undefined; if (cls && step) { @@ -242,6 +251,10 @@ export function resolveRunOutcome(input: ResolveRunOutcomeInput): RunOutcome { } else if (cls) { stuckAt = cls.en; stuckAtZh = cls.zh; + } else if (blockedStep) { + stuckAt = blockedStep; + stuckAtZh = blockedStep; + stuckWasBlocked = true; } else if (step) { stuckAt = step; stuckAtZh = step; @@ -260,6 +273,7 @@ export function resolveRunOutcome(input: ResolveRunOutcomeInput): RunOutcome { exitCode, ...(stuckAt ? { stuckAt } : {}), ...(stuckAtZh ? { stuckAtZh } : {}), + ...(stuckWasBlocked ? { stuckWasBlocked: true } : {}), }; } @@ -272,8 +286,17 @@ function head(label: string, reason: string | undefined, zh: boolean): string { return zh ? `${label}:${reason}` : `${label}: ${reason}`; } -function appendStuckHalf(base: string, stuck: string | undefined, zh: boolean): string { +function appendStuckHalf( + base: string, + stuck: string | undefined, + zh: boolean, + blocked = false +): string { if (!stuck) return base; + // O113: safety/approval block → 「被拦:…」 / `Blocked by: …` + if (blocked) { + return zh ? `${base};被拦:${stuck}` : `${base}; Blocked by: ${stuck}`; + } return zh ? `${base};卡在:${stuck}` : `${base}; stuck at: ${stuck}`; } @@ -317,6 +340,9 @@ export function formatOutcomeLine(outcome: RunOutcome, lang: OutcomeLang): strin // O111: no placeholder stuck step — omit the stuck half when unset. const stuckRaw = zh ? outcome.stuckAtZh : outcome.stuckAt; const stuck = stuckRaw?.replace(/\s+/g, ' ').trim() || undefined; + // O113: incomplete + safety block uses 「被拦」 half; Stop-hook `blocked` + // status already carries 「被拦」/Blocked in the head — keep stuck half as 卡在. + const blockedHalf = outcome.stuckWasBlocked === true && outcome.status === 'incomplete'; switch (outcome.status) { case 'completed': return head(zh ? '完成' : 'Completed', reason, zh); @@ -328,6 +354,11 @@ export function formatOutcomeLine(outcome: RunOutcome, lang: OutcomeLang): strin return appendStuckHalf(head(zh ? '被拦' : 'Blocked', reason, zh), stuck, zh); case 'incomplete': default: - return appendStuckHalf(head(zh ? '未完成' : 'Incomplete', reason, zh), stuck, zh); + return appendStuckHalf( + head(zh ? '未完成' : 'Incomplete', reason, zh), + stuck, + zh, + blockedHalf + ); } } diff --git a/src/cli/run-progress.ts b/src/cli/run-progress.ts index 771f6b01..f22371b2 100644 --- a/src/cli/run-progress.ts +++ b/src/cli/run-progress.ts @@ -238,6 +238,11 @@ export interface RunProgressTracker { dispose(): void; /** Test helper: emitted lines in order. */ lines(): readonly ProgressLine[]; + /** + * O113: the O98 completion tracker fed by onEvent — pass to + * {@link formatProgressOutcomeLine} so stderr conclusions share lastStep. + */ + completionTracker(): RunCompletionTracker; } /** @@ -449,5 +454,8 @@ export function createRunProgressTracker( lines() { return emitted; }, + completionTracker() { + return completionTracker; + }, }; } diff --git a/test/o113-blocked-and-progress-tracker.spec.mjs b/test/o113-blocked-and-progress-tracker.spec.mjs new file mode 100644 index 00000000..789e045c --- /dev/null +++ b/test/o113-blocked-and-progress-tracker.spec.mjs @@ -0,0 +1,379 @@ +#!/usr/bin/env node +/** + * O113: safety/approval incomplete last line → 「被拦:…」/`Blocked by: …`; + * progress stderr must pass completionTracker so budget lines carry the tool step. + * Spec: /workspace/specs/O113.md (A–D + UX/QA). Mislabel = BLOCK. + */ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + createRunCompletionTracker, + isSafetyOrApprovalBlockResult, + noteRunCompletionEvent, +} from '../dist/cli/run-completion.js'; +import { formatOutcomeLine, resolveRunOutcome } from '../dist/cli/run-outcome.js'; +import { createRunProgressTracker, formatProgressOutcomeLine } from '../dist/cli/run-progress.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, '..'); + +function banUnknown(line, label) { + assert.doesNotMatch(line, /未知|unknown|Unknown/i, `${label}: no 未知/unknown — ${line}`); + assert.doesNotMatch(line, /卡在:\s*(;|$)/, `${label}: empty 卡在 — ${line}`); + assert.doesNotMatch(line, /stuck at:\s*(;|$)/, `${label}: empty stuck at — ${line}`); +} + +function banBlockedMislabel(line, label) { + assert.doesNotMatch(line, /被拦|Blocked by/i, `${label}: must NOT say 被拦/Blocked by — ${line}`); +} + +function feedBlock(t, toolName, command, result) { + noteRunCompletionEvent(t, { + type: 'tool_start', + toolName, + toolCallId: 'b1', + input: { command }, + }); + noteRunCompletionEvent(t, { + type: 'tool_end', + toolName, + toolCallId: 'b1', + result, + isError: true, + }); +} + +function feedFail(t, toolName, detail, result = 'Error: no such file') { + const input = toolName === 'read_file' ? { path: detail } : { command: detail }; + noteRunCompletionEvent(t, { + type: 'tool_start', + toolName, + toolCallId: 'f1', + input, + }); + noteRunCompletionEvent(t, { + type: 'tool_end', + toolName, + toolCallId: 'f1', + result, + isError: true, + }); +} + +// ── Detector ────────────────────────────────────────────────────────────── +{ + assert.equal( + isSafetyOrApprovalBlockResult('Blocked dangerous command: 禁止递归删除根目录或用户目录'), + true + ); + assert.equal( + isSafetyOrApprovalBlockResult( + '已拦截:Blocked dangerous command: 禁止递归删除根目录或用户目录' + ), + true + ); + assert.equal(isSafetyOrApprovalBlockResult('Tool execution denied: User denied exec.'), true); + assert.equal(isSafetyOrApprovalBlockResult('Command blocked: rm -rf /'), true); + assert.equal( + isSafetyOrApprovalBlockResult( + 'Operation blocked by workspace policy. The tool did not inspect.' + ), + true + ); + assert.equal(isSafetyOrApprovalBlockResult('Blocked by read-only safety mode.'), true); + // Non-blocks + assert.equal(isSafetyOrApprovalBlockResult('Error: ENOENT: no such file'), false); + assert.equal(isSafetyOrApprovalBlockResult('exit_code: 1\nfailed'), false); + assert.equal(isSafetyOrApprovalBlockResult('Command failed (exit 1)'), false); + assert.equal(isSafetyOrApprovalBlockResult('failed'), false); + assert.equal(isSafetyOrApprovalBlockResult(undefined), false); +} + +// ── A. Safety block → 「被拦:」/`Blocked by:` + tool/command ────────────── +{ + const t = createRunCompletionTracker(); + feedBlock(t, 'exec', 'rm -rf /', 'Blocked dangerous command: 禁止递归删除根目录或用户目录'); + assert.equal(t.lastBlockedStep, 'exec: rm -rf /'); + const o = resolveRunOutcome({ + tracker: t, + finalText: 'I cannot do that.', + request: 'Fix src/cli/hooks.ts by cleaning /tmp', + }); + assert.equal(o.status, 'incomplete'); + assert.equal(o.exitCode, 9); + assert.equal(o.stuckWasBlocked, true); + assert.equal(o.stuckAt, 'exec: rm -rf /'); + const zh = formatOutcomeLine(o, 'zh'); + const en = formatOutcomeLine(o, 'en'); + assert.match(zh, /被拦:exec: rm -rf \//); + assert.match(en, /Blocked by: exec: rm -rf \//); + assert.doesNotMatch(zh, /卡在:/); + assert.doesNotMatch(en, /stuck at:/); + banUnknown(zh, 'A zh'); + banUnknown(en, 'A en'); + // Same structure EN/ZH + assert.equal(/被拦:/.test(zh), /Blocked by:/.test(en)); +} + +// A2. ZH-prefixed block result still marks blocked +{ + const t = createRunCompletionTracker(); + feedBlock( + t, + 'exec', + 'rm -rf /', + '已拦截:Blocked dangerous command: 禁止递归删除根目录或用户目录' + ); + const o = resolveRunOutcome({ + tracker: t, + finalText: '不行', + request: '删掉根目录下的危险脚本并修复 hooks.ts', + }); + assert.equal(o.stuckWasBlocked, true); + assert.match(formatOutcomeLine(o, 'zh'), /被拦:exec: rm -rf \//); + assert.match(formatOutcomeLine(o, 'en'), /Blocked by: exec: rm -rf \//); +} + +// A3. Revert-red: if stuckWasBlocked is ignored, line falls back to 卡在 only +{ + const fake = { + status: 'incomplete', + reason: 'no files were modified', + reasonZh: '没有改动任何文件', + exitCode: 9, + stuckAt: 'exec: rm -rf /', + stuckAtZh: 'exec: rm -rf /', + stuckWasBlocked: true, + }; + const withFix = formatOutcomeLine(fake, 'zh'); + assert.match(withFix, /被拦:/); + // Simulate removing the blocked-half branch: appendStuckHalf without blocked → 卡在 + const withoutFix = `未完成:没有改动任何文件;卡在:exec: rm -rf /`; + assert.doesNotMatch(withoutFix, /被拦:/); + assert.notEqual( + withFix, + withoutFix, + 'A3: dropping stuckWasBlocked must change the line (revert-red)' + ); +} + +// ── B. Non-block cases must NOT say 被拦 / Blocked by (mislabel = BLOCK) ── +{ + // B1 tool fail + const tFail = createRunCompletionTracker(); + feedFail(tFail, 'read_file', 'src/nope.ts'); + const oFail = resolveRunOutcome({ + tracker: tFail, + finalText: 'missing', + request: 'Fix src/a.ts', + }); + assert.equal(oFail.stuckWasBlocked, undefined); + const zhFail = formatOutcomeLine(oFail, 'zh'); + const enFail = formatOutcomeLine(oFail, 'en'); + assert.match(zhFail, /卡在:read_file: src\/nope\.ts/); + assert.match(enFail, /stuck at: read_file: src\/nope\.ts/); + banBlockedMislabel(zhFail, 'B1 zh'); + banBlockedMislabel(enFail, 'B1 en'); + + // B2 zero writes + const oZero = resolveRunOutcome({ + tracker: createRunCompletionTracker(), + finalText: 'Done.', + request: 'Fix the timeout in src/cli/hooks.ts', + }); + assert.equal(oZero.stuckAt, undefined); + banBlockedMislabel(formatOutcomeLine(oZero, 'zh'), 'B2 zh'); + banBlockedMislabel(formatOutcomeLine(oZero, 'en'), 'B2 en'); + assert.doesNotMatch(formatOutcomeLine(oZero, 'zh'), /卡在:/); + + // B3 budget + const tBudget = createRunCompletionTracker(); + noteRunCompletionEvent(tBudget, { + type: 'tool_start', + toolName: 'read_file', + toolCallId: 'r1', + input: { path: 'src/a.ts' }, + }); + const oBudget = resolveRunOutcome({ + tracker: tBudget, + finalText: 'x', + runError: 'run budget exceeded before finishing', + }); + assert.equal(oBudget.stuckWasBlocked, undefined); + const zhB = formatOutcomeLine(oBudget, 'zh'); + const enB = formatOutcomeLine(oBudget, 'en'); + assert.match(zhB, /卡在:运行预算(read_file: src\/a\.ts)/); + assert.match(enB, /stuck at: run budget \(read_file: src\/a\.ts\)/); + banBlockedMislabel(zhB, 'B3 zh'); + banBlockedMislabel(enB, 'B3 en'); + + // B4 Q&A completed + const oOk = resolveRunOutcome({ + tracker: createRunCompletionTracker(), + finalText: '2+2=4', + request: 'What is 2+2?', + }); + assert.equal(oOk.status, 'completed'); + assert.equal(oOk.exitCode, 0); + banBlockedMislabel(formatOutcomeLine(oOk, 'zh'), 'B4 zh'); + banBlockedMislabel(formatOutcomeLine(oOk, 'en'), 'B4 en'); + + // B5 budget + prior block still uses 卡在 (class path; do not mislabel budget) + const tMix = createRunCompletionTracker(); + feedBlock(tMix, 'exec', 'rm -rf /', 'Blocked dangerous command: nope'); + noteRunCompletionEvent(tMix, { + type: 'tool_start', + toolName: 'read_file', + toolCallId: 'r2', + input: { path: 'src/b.ts' }, + }); + const oMix = resolveRunOutcome({ + tracker: tMix, + finalText: 'x', + runError: 'run budget exceeded before finishing', + }); + assert.equal(oMix.stuckWasBlocked, undefined); + banBlockedMislabel(formatOutcomeLine(oMix, 'zh'), 'B5 zh'); + banBlockedMislabel(formatOutcomeLine(oMix, 'en'), 'B5 en'); + assert.match(formatOutcomeLine(oMix, 'zh'), /卡在:运行预算/); +} + +// ── C. Budget: stdout ↔ stderr progress line share tracker step ─────────── +{ + const mk = () => { + const t = createRunCompletionTracker(); + noteRunCompletionEvent(t, { + type: 'tool_start', + toolName: 'read_file', + toolCallId: 'r1', + input: { path: 'src/a.ts' }, + }); + return t; + }; + const stdoutO = resolveRunOutcome({ + tracker: mk(), + finalText: 'x', + runError: 'Run budget exceeded (tool_calls). Partial output above is the completed work.', + }); + const withTracker = formatProgressOutcomeLine({ + stopReason: 'budget_tool_calls', + finalText: 'x', + tracker: mk(), + zh: true, + }); + const withoutTracker = formatProgressOutcomeLine({ + stopReason: 'budget_tool_calls', + finalText: 'x', + zh: true, + }); + // With tracker: step present (matches stdout shape) + assert.match(withTracker, /卡在:运行预算(read_file: src\/a\.ts)/); + assert.match(formatOutcomeLine(stdoutO, 'zh'), /卡在:运行预算(read_file: src\/a\.ts)/); + assert.equal( + withTracker.includes('read_file: src/a.ts'), + formatOutcomeLine(stdoutO, 'zh').includes('read_file: src/a.ts'), + 'C: stdout and stderr-with-tracker must both carry the tool step' + ); + // Without tracker: class only — revert-red if someone drops tracker passing + assert.match(withoutTracker, /卡在:运行预算/); + assert.doesNotMatch(withoutTracker, /read_file/); + assert.notEqual( + withTracker, + withoutTracker, + 'C revert-red: removing tracker from formatProgressOutcomeLine must change the line' + ); + + const enWith = formatProgressOutcomeLine({ + stopReason: 'budget_tool_calls', + finalText: 'x', + tracker: mk(), + zh: false, + }); + const enWithout = formatProgressOutcomeLine({ + stopReason: 'budget_tool_calls', + finalText: 'x', + zh: false, + }); + assert.match(enWith, /stuck at: run budget \(read_file: src\/a\.ts\)/); + assert.doesNotMatch(enWithout, /read_file/); + banBlockedMislabel(withTracker, 'C zh'); + banBlockedMislabel(enWith, 'C en'); +} + +// C2. progress tracker from onEvent feeds completionTracker() (output.ts path) +{ + const lines = []; + const prog = createRunProgressTracker({ + sink: { + write(line) { + lines.push(line); + }, + }, + zh: true, + emitToolBoundaries: false, + }); + prog.onEvent({ + type: 'tool_start', + toolName: 'read_file', + toolCallId: 'r1', + input: { path: 'src/a.ts' }, + }); + const tracked = prog.completionTracker(); + assert.equal(tracked.lastStep, 'read_file: src/a.ts'); + const line = formatProgressOutcomeLine({ + stopReason: 'budget_tool_calls', + finalText: 'x', + tracker: tracked, + zh: true, + }); + assert.match(line, /卡在:运行预算(read_file: src\/a\.ts)/); + prog.dispose(); +} + +// C3. Source guard: output.ts must pass tracker: prog.completionTracker() +{ + const src = fs.readFileSync(path.join(ROOT, 'src/cli/output.ts'), 'utf8'); + assert.match( + src, + /formatProgressOutcomeLine\(\{[\s\S]*?tracker:\s*prog\.completionTracker\(\)/, + 'C3 revert-red: output.ts done branch must pass tracker: prog.completionTracker()' + ); +} + +// C4. Source guard: stuckWasBlocked / Blocked by in run-outcome +{ + const src = fs.readFileSync(path.join(ROOT, 'src/cli/run-outcome.ts'), 'utf8'); + assert.match(src, /stuckWasBlocked/, 'A revert-red: stuckWasBlocked must exist'); + assert.match(src, /Blocked by:/, 'A revert-red: Blocked by: wording must exist'); + assert.match(src, /被拦:/, 'A revert-red: 被拦: wording must exist'); + const completion = fs.readFileSync(path.join(ROOT, 'src/cli/run-completion.ts'), 'utf8'); + assert.match(completion, /lastBlockedStep/, 'A revert-red: lastBlockedStep must exist'); + assert.match(completion, /isSafetyOrApprovalBlockResult/, 'A revert-red: detector must exist'); +} + +// ── D. Exit / O111 / Stop-hook blocked head unchanged ───────────────────── +{ + // Stop hook still uses status blocked head 「被拦:」/`Blocked:` (O98) + const oStop = resolveRunOutcome({ + tracker: createRunCompletionTracker(), + finalText: 'done', + blocked: true, + blockedReason: 'Stop hook wants more work', + }); + assert.equal(oStop.status, 'blocked'); + assert.equal(oStop.exitCode, 9); + assert.match(formatOutcomeLine(oStop, 'zh'), /^被拦:/); + assert.match(formatOutcomeLine(oStop, 'en'), /^Blocked: /); + // stuck half for Stop remains 卡在 (head already says 被拦) + assert.match(formatOutcomeLine(oStop, 'zh'), /卡在:Stop 钩子/); + + // O111 omit empty stuck + const bare = { status: 'incomplete', reason: '', reasonZh: '', exitCode: 9 }; + assert.equal(formatOutcomeLine(bare, 'zh'), '未完成'); + assert.equal(formatOutcomeLine(bare, 'en'), 'Incomplete'); + banUnknown(formatOutcomeLine(bare, 'zh'), 'D omit'); +} + +console.log('[PASS] o113-blocked-and-progress-tracker'); From 5ee7a98dc505c18910d91f2ccb64044e14d9cfba Mon Sep 17 00:00:00 2001 From: QiaolongLi1201 Date: Mon, 12 Oct 2026 07:20:51 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test:=20track=20O91=20Stop=20zh=20display?= =?UTF-8?q?=20name=20(=E5=81=9C=E6=AD=A2=E5=89=8D=E9=92=A9=E5=AD=90)=20in?= =?UTF-8?q?=20O111/O113=20specs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/o111-stuck-no-unknown.spec.mjs | 4 ++-- test/o113-blocked-and-progress-tracker.spec.mjs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/o111-stuck-no-unknown.spec.mjs b/test/o111-stuck-no-unknown.spec.mjs index f8affa6e..bbc5c710 100755 --- a/test/o111-stuck-no-unknown.spec.mjs +++ b/test/o111-stuck-no-unknown.spec.mjs @@ -147,10 +147,10 @@ function failTool(t, name = 'exec', cmd = 'npm run check') { }); assert.equal(o.status, 'blocked'); assert.equal(o.stuckAt, 'Stop hook'); - assert.equal(o.stuckAtZh, 'Stop 钩子'); + assert.equal(o.stuckAtZh, '停止前钩子'); // O91 zh display name const zh = formatOutcomeLine(o, 'zh'); const en = formatOutcomeLine(o, 'en'); - assert.match(zh, /^被拦:.*;卡在:Stop 钩子$/); + assert.match(zh, /^被拦:.*;卡在:停止前钩子$/); assert.match(en, /^Blocked: .*stuck at: Stop hook$/); banUnknown(zh, 'B4 zh'); banUnknown(en, 'B4 en'); diff --git a/test/o113-blocked-and-progress-tracker.spec.mjs b/test/o113-blocked-and-progress-tracker.spec.mjs index 789e045c..0cfcfbce 100644 --- a/test/o113-blocked-and-progress-tracker.spec.mjs +++ b/test/o113-blocked-and-progress-tracker.spec.mjs @@ -367,7 +367,7 @@ function feedFail(t, toolName, detail, result = 'Error: no such file') { assert.match(formatOutcomeLine(oStop, 'zh'), /^被拦:/); assert.match(formatOutcomeLine(oStop, 'en'), /^Blocked: /); // stuck half for Stop remains 卡在 (head already says 被拦) - assert.match(formatOutcomeLine(oStop, 'zh'), /卡在:Stop 钩子/); + assert.match(formatOutcomeLine(oStop, 'zh'), /卡在:停止前钩子$/); // O111 omit empty stuck const bare = { status: 'incomplete', reason: '', reasonZh: '', exitCode: 9 };