From 78477fa16ab2d8fdaeb4bd84cd1fbd3091427db3 Mon Sep 17 00:00:00 2001 From: Leo Camus Date: Mon, 28 Sep 2026 04:41:32 +0200 Subject: [PATCH] fix(review): print review reject/apply output in English (#836) (#859) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(review): print review reject/apply output in English (#836) teamai review --reject and --apply printed Chinese strings in their console output: 已拒绝 for rejected, 应用失败 for apply failed, and Chinese error reasons (缺失, 为空, 不存在, 不支持自动应用). Issue #836 established that CLI output must be English, and #840 fixed the same class of bug in cache-cmd and import-local; review-cmd was missed. Translate the six user-facing strings in applyOne's error reasons and reviewCmd's reject/apply messages. Code comments are left as-is — they are not user-facing. Add three tests that assert the English text and no CJK characters in the output; all three fail before this fix. * fix(review): update existing test to expect English output The domain-drift apply test in review-cmd.test.ts still asserted the Chinese string '不支持' which was translated in the previous commit. Update the assertion to match the English output. --- src/__tests__/review-cmd-output.test.ts | 117 ++++++++++++++++++++++++ src/__tests__/review-cmd.test.ts | 2 +- src/review-cmd.ts | 12 +-- 3 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 src/__tests__/review-cmd-output.test.ts diff --git a/src/__tests__/review-cmd-output.test.ts b/src/__tests__/review-cmd-output.test.ts new file mode 100644 index 00000000..e4c458f6 --- /dev/null +++ b/src/__tests__/review-cmd-output.test.ts @@ -0,0 +1,117 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; + +// Stub chalk so the test sees raw text, not ANSI codes. +vi.mock('chalk', () => ({ + default: { + yellow: (s: string) => s, + red: (s: string) => s, + green: (s: string) => s, + cyan: (s: string) => s, + dim: (s: string) => s, + bold: (s: string) => s, + }, +})); + +import { reviewCmd } from '../review-cmd.js'; + +const CJK = /[\u3000-\u303f\u4e00-\u9fff\uff00-\uffef]/; + +let tmpDir: string; +let consoleSpy: ReturnType; + +beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'teamai-review-')); + consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); +}); + +afterEach(() => { + consoleSpy.mockRestore(); + fs.rmSync(tmpDir, { recursive: true, force: true }); +}); + +function seedReviewItem(item: Record): void { + const reviewPath = path.join(tmpDir, '.teamai', 'pending-review.jsonl'); + fs.mkdirSync(path.dirname(reviewPath), { recursive: true }); + fs.appendFileSync(reviewPath, JSON.stringify(item) + '\n'); +} + +describe('review-cmd English output (#836)', () => { + it('reject prints English, not Chinese', async () => { + seedReviewItem({ + id: 'test-item', + ts: new Date().toISOString(), + kind: 'codebase-section', + source: 'test', + target: { file: 'x.md', section: 'a' }, + payload: { content: 'body' }, + risk: 'low', + }); + + const origCwd = process.cwd(); + process.chdir(tmpDir); + try { + await reviewCmd({ idArg: 'test-item', reject: true }); + } finally { + process.chdir(origCwd); + } + + const output = consoleSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(output).toContain('rejected'); + expect(output).not.toMatch(CJK); + }); + + it('apply-failure reason is English when target.section is missing', async () => { + seedReviewItem({ + id: 'no-section', + ts: new Date().toISOString(), + kind: 'codebase-section', + source: 'test', + target: { file: 'x.md' }, + payload: { content: 'body' }, + risk: 'low', + }); + + const origCwd = process.cwd(); + process.chdir(tmpDir); + try { + await reviewCmd({ idArg: 'no-section', apply: true }); + } finally { + process.chdir(origCwd); + } + + const output = consoleSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(output).toContain('apply failed'); + expect(output).toContain('target.section is missing'); + expect(output).not.toMatch(CJK); + }); + + it('apply-failure reason is English when payload.content is empty', async () => { + const targetFile = path.join(tmpDir, 'target.md'); + fs.writeFileSync(targetFile, '## Section\nBody\n'); + seedReviewItem({ + id: 'empty-payload', + ts: new Date().toISOString(), + kind: 'codebase-section', + source: 'test', + target: { file: targetFile, section: 's' }, + payload: {}, + risk: 'low', + }); + + const origCwd = process.cwd(); + process.chdir(tmpDir); + try { + await reviewCmd({ idArg: 'empty-payload', apply: true }); + } finally { + process.chdir(origCwd); + } + + const output = consoleSpy.mock.calls.map((c) => c.join(' ')).join('\n'); + expect(output).toContain('apply failed'); + expect(output).toContain('payload.content is empty'); + expect(output).not.toMatch(CJK); + }); +}); diff --git a/src/__tests__/review-cmd.test.ts b/src/__tests__/review-cmd.test.ts index cec17cba..ef49472d 100644 --- a/src/__tests__/review-cmd.test.ts +++ b/src/__tests__/review-cmd.test.ts @@ -142,7 +142,7 @@ describe('review-cmd', () => { expect(removePendingReview).not.toHaveBeenCalled(); const output = consoleSpy.mock.calls.map((c) => c.join(' ')).join('\n'); - expect(output).toContain('不支持'); + expect(output).toContain('does not support auto-apply'); }); // ── reject 模式 ─────────────────────────────────────── diff --git a/src/review-cmd.ts b/src/review-cmd.ts index 994a2811..f30491b8 100644 --- a/src/review-cmd.ts +++ b/src/review-cmd.ts @@ -125,23 +125,23 @@ async function applyOne( item: PendingReviewItem, ): Promise<{ ok: boolean; reason?: string }> { if (item.kind !== 'codebase-section') { - return { ok: false, reason: `kind ${item.kind} 不支持自动应用,请人工处理` }; + return { ok: false, reason: `kind ${item.kind} does not support auto-apply; please handle manually` }; } const { file, section } = item.target; if (!section) { - return { ok: false, reason: 'target.section 缺失' }; + return { ok: false, reason: 'target.section is missing' }; } const filePath = path.isAbsolute(file) ? file : path.join(cwd, file); if (!await fs.pathExists(filePath)) { - return { ok: false, reason: `目标文件不存在:${filePath}` }; + return { ok: false, reason: `target file not found: ${filePath}` }; } const oldMd = await fs.readFile(filePath, 'utf8'); const body = String(item.payload['content'] ?? ''); if (!body) { - return { ok: false, reason: 'payload.content 为空' }; + return { ok: false, reason: 'payload.content is empty' }; } try { @@ -236,7 +236,7 @@ export async function reviewCmd(opts: ReviewCmdOptions): Promise { console.log(JSON.stringify({ ok: true, action: 'reject', id: idArg })); return; } - console.log(chalk.yellow(`[review] 已拒绝:${idArg}`)); + console.log(chalk.yellow(`[review] rejected: ${idArg}`)); return; } @@ -256,7 +256,7 @@ export async function reviewCmd(opts: ReviewCmdOptions): Promise { if (result.ok) { console.log(chalk.green(`[review] applied: ${idArg} → ${item.target.file}`)); } else { - console.log(chalk.red(`[review] 应用失败:${idArg} — ${result.reason}`)); + console.log(chalk.red(`[review] apply failed: ${idArg} — ${result.reason}`)); } return; }