fix(review): print review reject/apply output in English (#836) (#859)

* 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.
This commit is contained in:
Leo Camus
2026-09-28 10:41:32 +08:00
committed by GitHub
parent a8ab8e00f7
commit 78477fa16a
3 changed files with 124 additions and 7 deletions
+117
View File
@@ -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<typeof vi.spyOn>;
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<string, unknown>): 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);
});
});
+1 -1
View File
@@ -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 模式 ───────────────────────────────────────
+6 -6
View File
@@ -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<void> {
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<void> {
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;
}