From e1ab19afc93a2c4756340171aa7985bda80fa469 Mon Sep 17 00:00:00 2001 From: liberxue <5831248+Liberxue@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:57:36 +0800 Subject: [PATCH 1/3] docs(i18n): add comment conventions and message extraction scripts Introduce unified English comment rules (docs/COMMENT_CONVENTIONS.md) and a set of i18n tooling scripts for extracting, checking, and repairing message keys/tokens across the workspace. --- docs/COMMENT_CONVENTIONS.md | 96 ++++++++++++++++++++++ scripts/i18n_check_keys.mjs | 46 +++++++++++ scripts/i18n_check_tokens.mjs | 47 +++++++++++ scripts/i18n_extract.mjs | 91 +++++++++++++++++++++ scripts/i18n_fallbacks.mjs | 73 +++++++++++++++++ scripts/i18n_merge_en.mjs | 51 ++++++++++++ scripts/i18n_pivot.mjs | 77 ++++++++++++++++++ scripts/i18n_repair.mjs | 146 ++++++++++++++++++++++++++++++++++ 8 files changed, 627 insertions(+) create mode 100644 docs/COMMENT_CONVENTIONS.md create mode 100644 scripts/i18n_check_keys.mjs create mode 100644 scripts/i18n_check_tokens.mjs create mode 100644 scripts/i18n_extract.mjs create mode 100644 scripts/i18n_fallbacks.mjs create mode 100644 scripts/i18n_merge_en.mjs create mode 100644 scripts/i18n_pivot.mjs create mode 100644 scripts/i18n_repair.mjs diff --git a/docs/COMMENT_CONVENTIONS.md b/docs/COMMENT_CONVENTIONS.md new file mode 100644 index 00000000..c13dcfcd --- /dev/null +++ b/docs/COMMENT_CONVENTIONS.md @@ -0,0 +1,96 @@ +# Comment Conventions + +Unified rules for comments in this workspace. The goal is consistent, English, +rustdoc-friendly comments across every crate. + +## 1. Comment forms + +| Syntax | Use for | +| ----------- | --------------------------------------------------- | +| `//!` | Module- or crate-level docs (placed at the top). | +| `///` | Documentation for the item that follows it. | +| `//` | Inline notes, `TODO`, rationale — not public API. | +| `// TODO:` | Pending work (English). Prefer an issue link. | + +- Use `///` / `//!` for anything that is part of the public surface (exported + types, functions, methods, modules). Use `//` for internal reasoning. +- Never use `/* ... */` block comments for doc text; keep prose as line comments + so `cargo doc` picks it up. Inline `/* */` is fine for temporarily disabling code. + +## 2. Language + +- All **documentation text** is written in **English**: `//`, `///`, `//!` comment + lines, `utoipa` `description = "..."` attributes, and `#[error(...)]` / + `#[ignore = "..."]` / `#[schema(...)]` messages. +- Do not write documentation in Chinese, Pinyin, or mixed languages. +- **Out of scope:** string literals that are test fixtures or mock data (e.g. + `"登录"`, `"张三"` in `#[test]` / `r#"..."#` bodies). These intentionally exercise + i18n and are left as-is. + +## 3. Style + +- **Capitalize the first word.** End every sentence with a period (`."). +- **One space** after `//`, `///`, `//!`: `/// Returns the count.` +- Use **third-person or imperative** summary sentences: + - `/// Returns the number of active users.` + - `/// Parses the raw payload into a `Proposal`.` + - Avoid `This function returns...`; lead with the action. +- Keep comments **concise**; do not restate what the code obviously does. + Explain *why*, constraints, and non-obvious behavior. +- For struct/enum fields, prefer a noun phrase ending in a period: + `/// The user's display name.` + +## 4. Code and identifiers + +- Wrap types, functions, modules, and paths in backticks: + `Vec`, `parse()`, `crate::domain::Proposal`, `Result`. +- Wrap inline code snippets in fenced blocks when longer than a line: + + ```rust + let req = Proposal::new(..)?; + ``` + +## 5. Markdown + +- Doc comments (`///`, `//!`) accept Markdown: lists, links, tables, code fences. +- For lists, use `-` and a blank line before the first item only when the list + needs a lead-in sentence. + +## 6. Examples + +```rust +//! HTTP adapters for the requirement crate. +//! Exposes REST endpoints backed by the PostgreSQL repository. + +/// Creates a new requirement from the given draft. +/// +/// Returns an error if the title is empty after trimming. +pub fn create(draft: Draft) -> Result { + // Reject empty titles early to avoid a round-trip to the DB. + if draft.title.trim().is_empty() { + return Err(Error::EmptyTitle); + } + // ... +} +``` + +## 7. Checking + +To confirm no Chinese remains in comment lines: + +```bash +# crude check: flag any CJK outside of string literals in // lines +python3 - <<'PY' +import os, re +def cjk(s): return any('\u4e00' <= c <= '\u9fff' for c in s) +for root, _, fs in os.walk('crates'): + for f in fs: + if not f.endswith('.rs'): + continue + for i, line in enumerate(open(os.path.join(root, f), encoding='utf-8', errors='ignore'), 1): + s = re.sub(r'"[^"]*"', '""', line) + s = re.sub(r"'[^']*'", "''", s) + if cjk(s) and re.match(r'\s*//', line): + print(f'{os.path.join(root, f)}:{i}: {line.rstrip()}') +PY +``` diff --git a/scripts/i18n_check_keys.mjs b/scripts/i18n_check_keys.mjs new file mode 100644 index 00000000..e123e4e8 --- /dev/null +++ b/scripts/i18n_check_keys.mjs @@ -0,0 +1,46 @@ +#!/usr/bin/env node +// Find i18n keys referenced by the app but absent from the DICT in web/src/i18n.tsx. +// Such keys silently fall back to their hardcoded Chinese literal in every language, +// which is invisible in a build and only shows up when someone switches locale. +// Covers both `t('key', '中文')` calls and the `label: ['key', '中文']` tables used by +// AppShell's nav and similar module-scope metadata. +// Usage: node scripts/i18n_check_keys.mjs +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const SRC = path.join(ROOT, 'web/src') +const base = JSON.parse(fs.readFileSync(path.join(ROOT, '.i18n-work/base.json'), 'utf8')) + +const files = [] +;(function walk(dir) { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, e.name) + if (e.isDirectory()) { if (e.name !== 'locales') walk(p) } else if (/\.tsx?$/.test(e.name)) files.push(p) + } +})(SRC) + +const RE_T = /\bt\(\s*(['"])((?:\\.|(?!\1).)*)\1/g +const RE_LABEL = /label:\s*\[\s*(['"])((?:\\.|(?!\1).)*)\1/g + +const used = new Map() +for (const f of files) { + const s = fs.readFileSync(f, 'utf8') + for (const re of [RE_T, RE_LABEL]) { + re.lastIndex = 0 + let m + while ((m = re.exec(s))) { + const key = m[2] + // Skip dynamically built keys and bare words that aren't dotted key names. + if (key.includes('${') || !key.includes('.')) continue + if (!used.has(key)) used.set(key, []) + used.get(key).push(path.relative(ROOT, f)) + } + } +} + +const missing = [...used.keys()].filter((k) => !base[k]).sort() +console.log('keys referenced:', used.size, '| missing from DICT:', missing.length) +for (const k of missing) console.log(` ${k} <- ${[...new Set(used.get(k))].join(', ')}`) +process.exit(missing.length ? 1 : 0) diff --git a/scripts/i18n_check_tokens.mjs b/scripts/i18n_check_tokens.mjs new file mode 100644 index 00000000..115c899f --- /dev/null +++ b/scripts/i18n_check_tokens.mjs @@ -0,0 +1,47 @@ +#!/usr/bin/env node +// Verify that every locale translation preserves the placeholder tokens of its source. +// Tokens: ${...} template expressions and {name} interpolation slots. +// Usage: node scripts/i18n_check_tokens.mjs [--json] +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const base = JSON.parse(fs.readFileSync(path.join(ROOT, '.i18n-work/base.json'), 'utf8')) +const LOC = path.join(ROOT, 'web/src/locales') +const LANGS = ['ja', 'ko', 'fr', 'de', 'es', 'pt', 'ru', 'it', 'tr', 'vi', 'th', 'id', 'nl', 'pl', 'ar'] + +const TOKEN = /\$\{[^}]*\}|\{\w+\}/g +const tokens = (s) => (String(s || '').match(TOKEN) || []).slice().sort() +const eq = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]) + +const locales = Object.fromEntries(LANGS.map((l) => [l, JSON.parse(fs.readFileSync(path.join(LOC, `${l}.json`), 'utf8'))])) + +const bad = [] +for (const [key, v] of Object.entries(base)) { + const want = tokens(v.en && v.en.trim() ? v.en : v.zh) + if (!want.length) continue + for (const l of LANGS) { + const got = tokens(locales[l][key]) + if (!eq(want, got)) bad.push({ key, lang: l, want, got, text: locales[l][key] }) + } +} + +const byKey = new Map() +for (const b of bad) { + if (!byKey.has(b.key)) byKey.set(b.key, []) + byKey.get(b.key).push(b) +} + +if (process.argv.includes('--json')) { + console.log(JSON.stringify([...byKey.keys()], null, 2)) +} else { + console.log('keys with token mismatch:', byKey.size, '/ occurrences:', bad.length) + for (const [key, list] of byKey) { + const src = base[key].en || base[key].zh + console.log(`\n${key} src=${JSON.stringify(src)} want=${JSON.stringify(list[0].want)}`) + console.log(' langs:', list.map((x) => x.lang).join(' ')) + console.log(' e.g.', list[0].lang, '=', JSON.stringify(list[0].text)) + } +} +process.exit(byKey.size ? 1 : 0) diff --git a/scripts/i18n_extract.mjs b/scripts/i18n_extract.mjs new file mode 100644 index 00000000..2f201791 --- /dev/null +++ b/scripts/i18n_extract.mjs @@ -0,0 +1,91 @@ +#!/usr/bin/env node +// Extract the DICT object literal from web/src/i18n.tsx into JSON, and split +// it into fixed-size chunk files for parallel translation. +// +// Usage: node scripts/i18n_extract.mjs [chunkSize] +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const SRC = path.join(ROOT, 'web/src/i18n.tsx') +const WORK = path.join(ROOT, '.i18n-work') + +const CHUNK = Number(process.argv[2] || 100) + +const text = fs.readFileSync(SRC, 'utf8') + +// Locate `const DICT ... = {` and its matching depth-0 closing brace. +const declIdx = text.indexOf('const DICT') +if (declIdx < 0) throw new Error('const DICT not found') +const eqIdx = text.indexOf('=', declIdx) +const openIdx = text.indexOf('{', eqIdx) +if (openIdx < 0) throw new Error('DICT opening brace not found') + +// Brace scan that respects quotes, template literals and comments. +function findClose(s, start) { + let depth = 0 + let i = start + while (i < s.length) { + const c = s[i] + if (c === '/' && s[i + 1] === '/') { + i = s.indexOf('\n', i) + if (i < 0) break + continue + } + if (c === '/' && s[i + 1] === '*') { + i = s.indexOf('*/', i) + 2 + continue + } + if (c === "'" || c === '"' || c === '`') { + const q = c + i++ + while (i < s.length) { + if (s[i] === '\\') { i += 2; continue } + if (s[i] === q) break + i++ + } + i++ + continue + } + if (c === '{') depth++ + else if (c === '}') { + depth-- + if (depth === 0) return i + } + i++ + } + throw new Error('unbalanced braces') +} + +const closeIdx = findClose(text, openIdx) +const body = text.slice(openIdx, closeIdx + 1) + +// The body is a plain JS object literal (no TS annotations inside), so it can +// be evaluated directly. This handles escapes/quotes correctly. +const dict = new Function('return ' + body)() + +const keys = Object.keys(dict) +fs.mkdirSync(WORK, { recursive: true }) +fs.writeFileSync(path.join(WORK, 'base.json'), JSON.stringify(dict, null, 2)) + +// Only keys with both zh and en can be translated; the rest need their source +// text repaired first (see scripts/i18n_fallbacks.mjs). +const ready = keys.filter((k) => dict[k].zh && dict[k].en) +const pending = keys.filter((k) => !dict[k].zh || !dict[k].en) + +// Chunk files contain only zh/en source text — the translation input. +let n = 0 +for (let i = 0; i < ready.length; i += CHUNK) { + const out = {} + for (const k of ready.slice(i, i + CHUNK)) out[k] = { zh: dict[k].zh, en: dict[k].en } + const id = String(n).padStart(2, '0') + fs.writeFileSync(path.join(WORK, `in.${id}.json`), JSON.stringify(out, null, 2)) + n++ +} + +console.log(`keys=${keys.length} ready=${ready.length} pending=${pending.length}`) +console.log(`chunks=${n} chunkSize=${CHUNK}`) +console.log(`dictRange=[${openIdx},${closeIdx}]`) +if (pending.length) fs.writeFileSync(path.join(WORK, 'pending.json'), JSON.stringify(pending, null, 2)) +fs.writeFileSync(path.join(WORK, 'missing-zh.json'), JSON.stringify(keys.filter((k) => !dict[k].zh), null, 2)) diff --git a/scripts/i18n_fallbacks.mjs b/scripts/i18n_fallbacks.mjs new file mode 100644 index 00000000..51db7a63 --- /dev/null +++ b/scripts/i18n_fallbacks.mjs @@ -0,0 +1,73 @@ +#!/usr/bin/env node +// Scan web/src for `t('key', 'fallback')` call sites and report the fallback +// literal for each key. Used to recover the zh source text for DICT entries +// whose zh field is empty. +// +// Usage: node scripts/i18n_fallbacks.mjs [keysJson] > out.json +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const SRC = path.join(ROOT, 'web/src') + +function walk(dir, acc = []) { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, e.name) + if (e.isDirectory()) walk(p, acc) + else if (/\.(tsx?|jsx?)$/.test(e.name)) acc.push(p) + } + return acc +} + +// t('key', 'fallback') — the optional third `vars` argument means the call may +// continue with a comma instead of closing here. +const RE = /\bt\(\s*(['"])((?:\\.|(?!\1).)*)\1\s*,\s*(['"`])((?:\\.|(?!\3)[\s\S])*)\3\s*[,)]/g + +// Module-scope tables that cannot call the hook pair the key with its Chinese +// source instead, e.g. `{ labelKey: 'rag.stepRerank', labelZh: '重排' }`. +const RE_PAIR = /\bl(?:abel)?Key\s*:\s*(['"])((?:\\.|(?!\1).)*)\1\s*,\s*l(?:abel)?Zh\s*:\s*(['"])((?:\\.|(?!\3).)*)\3/g + +const found = {} +const conflicts = {} +for (const file of walk(SRC)) { + const text = fs.readFileSync(file, 'utf8') + for (const re of [RE, RE_PAIR]) { + re.lastIndex = 0 + let m + while ((m = re.exec(text))) { + const key = m[2] + const raw = m[4] + // Unescape the JS string literal. + let val + try { + val = JSON.parse('"' + raw.replace(/"/g, '\\"').replace(/\\'/g, "'") + '"') + } catch { + val = raw + } + if (found[key] && found[key] !== val) { + ;(conflicts[key] ||= new Set()).add(found[key]).add(val) + } + found[key] ??= val + } + } +} + +const wanted = process.argv[2] ? JSON.parse(fs.readFileSync(process.argv[2], 'utf8')) : null +const out = {} +for (const [k, v] of Object.entries(found)) { + if (wanted && !wanted.includes(k)) continue + out[k] = v +} + +process.stdout.write(JSON.stringify(out, null, 2) + '\n') +const conflictKeys = Object.keys(conflicts).filter((k) => !wanted || wanted.includes(k)) +if (conflictKeys.length) { + console.error(`conflicting fallbacks for ${conflictKeys.length} keys:`) + for (const k of conflictKeys) console.error(` ${k}: ${[...conflicts[k]].join(' | ')}`) +} +console.error(`resolved ${Object.keys(out).length} keys`) +if (wanted) { + const missing = wanted.filter((k) => !(k in out)) + if (missing.length) console.error(`no call site for ${missing.length}: ${missing.join(', ')}`) +} diff --git a/scripts/i18n_merge_en.mjs b/scripts/i18n_merge_en.mjs new file mode 100644 index 00000000..3e559270 --- /dev/null +++ b/scripts/i18n_merge_en.mjs @@ -0,0 +1,51 @@ +#!/usr/bin/env node +// Fill empty `en: ''` values in web/src/i18n.tsx DICT from translation outputs. +// Sources: .i18n-work/out.enonly.json (flat key -> en) +// .i18n-work/out.pend*.json (key -> { en, ja, ... }) +// Usage: node scripts/i18n_merge_en.mjs [--dry] +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const WORK = path.join(ROOT, '.i18n-work') +const FILE = path.join(ROOT, 'web/src/i18n.tsx') +const DRY = process.argv.includes('--dry') + +const en = {} +const read = (f) => JSON.parse(fs.readFileSync(path.join(WORK, f), 'utf8')) + +for (const f of fs.readdirSync(WORK)) { + if (f === 'out.enonly.json') { + for (const [k, v] of Object.entries(read(f))) if (typeof v === 'string' && v.trim()) en[k] = v + } else if (/^out\.pend[A-Z]\.json$/.test(f)) { + for (const [k, v] of Object.entries(read(f))) if (v && typeof v.en === 'string' && v.en.trim()) en[k] = v.en + } +} +console.log('en translations available:', Object.keys(en).length) + +const lines = fs.readFileSync(FILE, 'utf8').split('\n') +const ENTRY = /^(\s*)(['"])((?:\\.|(?!\2).)*)\2\s*:\s*\{(.*)\}(,?)\s*$/ +const EMPTY_EN = /\ben\s*:\s*(['"])\1/ + +let filled = 0 +const missed = [] +for (let i = 0; i < lines.length; i++) { + const m = ENTRY.exec(lines[i]) + if (!m) continue + const key = m[3] + if (!(key in en)) continue + if (!EMPTY_EN.test(m[4])) continue + const body = m[4].replace(EMPTY_EN, `en: ${JSON.stringify(en[key])}`) + lines[i] = `${m[1]}${m[2]}${key}${m[2]}: {${body}}${m[5]}` + filled++ +} +for (const k of Object.keys(en)) { + if (!lines.some((l) => l.includes(`${JSON.stringify(en[k])}`))) missed.push(k) +} + +console.log('en filled in i18n.tsx:', filled) +if (missed.length) console.log('not applied (check manually):', missed.length, missed.slice(0, 10)) +if (DRY) { console.log('(dry run, nothing written)'); process.exit(0) } +fs.writeFileSync(FILE, lines.join('\n')) +console.log('written', FILE) diff --git a/scripts/i18n_pivot.mjs b/scripts/i18n_pivot.mjs new file mode 100644 index 00000000..6d0f1621 --- /dev/null +++ b/scripts/i18n_pivot.mjs @@ -0,0 +1,77 @@ +#!/usr/bin/env node +// Pivot the per-chunk translation outputs in .i18n-work/out.*.json +// (keyed by dot.key -> { lang: text }) into one file per language at +// web/src/locales/.json (keyed by dot.key -> text). +// +// Reports coverage against the DICT keys in .i18n-work/base.json so gaps are +// visible instead of silently shipping English. +// +// Usage: node scripts/i18n_pivot.mjs +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const WORK = path.join(ROOT, '.i18n-work') +const OUT = path.join(ROOT, 'web/src/locales') + +const LANGS = ['ja', 'ko', 'fr', 'de', 'es', 'pt', 'ru', 'it', 'tr', 'vi', 'th', 'id', 'nl', 'pl', 'ar'] + +const base = JSON.parse(fs.readFileSync(path.join(WORK, 'base.json'), 'utf8')) +const allKeys = Object.keys(base) + +const tables = Object.fromEntries(LANGS.map((l) => [l, {}])) +const seen = new Set() +const problems = [] + +// out.enonly.json is a flat key->en map consumed by i18n_merge_en.mjs, not a locale chunk +const files = fs.readdirSync(WORK).filter((f) => /^out\..*\.json$/.test(f) && f !== 'out.enonly.json').sort() +for (const f of files) { + let data + try { + data = JSON.parse(fs.readFileSync(path.join(WORK, f), 'utf8')) + } catch (e) { + problems.push(`${f}: unparseable (${e.message})`) + continue + } + for (const [key, byLang] of Object.entries(data)) { + if (!(key in base)) { + problems.push(`${f}: unknown key ${key}`) + continue + } + seen.add(key) + for (const l of LANGS) { + const v = byLang?.[l] + if (typeof v !== 'string' || !v.trim()) { + problems.push(`${f}: ${key} missing ${l}`) + continue + } + const trimmed = v.trim() + // A translation that still equals the Chinese source means the model + // echoed the input instead of translating it. + if (l !== 'ja' && l !== 'ko' && trimmed === base[key].zh) { + problems.push(`${f}: ${key} [${l}] untranslated (equals zh)`) + } + tables[l][key] = trimmed + } + } +} + +fs.mkdirSync(OUT, { recursive: true }) +for (const l of LANGS) { + // Sort keys so regenerating one language produces a stable diff. + const sorted = Object.fromEntries(Object.keys(tables[l]).sort().map((k) => [k, tables[l][k]])) + fs.writeFileSync(path.join(OUT, `${l}.json`), JSON.stringify(sorted, null, 2) + '\n') +} + +const missing = allKeys.filter((k) => !seen.has(k)) +console.log(`chunk files : ${files.length}`) +console.log(`dict keys : ${allKeys.length}`) +console.log(`translated : ${seen.size}`) +console.log(`missing : ${missing.length}`) +for (const l of LANGS) console.log(` ${l}: ${Object.keys(tables[l]).length}`) +if (missing.length) fs.writeFileSync(path.join(WORK, 'untranslated.json'), JSON.stringify(missing, null, 2)) +if (problems.length) { + fs.writeFileSync(path.join(WORK, 'problems.txt'), problems.join('\n') + '\n') + console.log(`problems : ${problems.length} (see .i18n-work/problems.txt)`) +} diff --git a/scripts/i18n_repair.mjs b/scripts/i18n_repair.mjs new file mode 100644 index 00000000..e666d460 --- /dev/null +++ b/scripts/i18n_repair.mjs @@ -0,0 +1,146 @@ +#!/usr/bin/env node +// Reconcile web/src/i18n.tsx's DICT with the `t('key', 'fallback')` call sites +// in web/src: +// +// 1. Entries whose `zh` is empty get it filled from the call-site fallback. +// 2. Keys used at a call site but absent from DICT are appended. +// 3. Any non-zh/en language fields still inlined in DICT are lifted out into +// .i18n-work/out.existing.json so the pivot step can fold them into +// web/src/locales/ instead of leaving them stranded in the .tsx. +// +// zh/en stay inline in DICT — they are the source of truth. Everything else +// belongs in the locale files. +// +// Usage: node scripts/i18n_repair.mjs [--dry] +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { execFileSync } from 'node:child_process' + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const SRC = path.join(ROOT, 'web/src/i18n.tsx') +const WORK = path.join(ROOT, '.i18n-work') +const DRY = process.argv.includes('--dry') + +const SOURCE_LANGS = new Set(['zh', 'en']) + +// --- call-site fallbacks ----------------------------------------------------- +const fallbacks = JSON.parse( + execFileSync('node', [path.join(ROOT, 'scripts/i18n_fallbacks.mjs')], { encoding: 'utf8' }), +) + +// --- rewrite DICT entry lines ------------------------------------------------ +const text = fs.readFileSync(SRC, 'utf8') +const lines = text.split('\n') + +// A couple of entries with long values are wrapped across several lines, so the +// per-line rewrite below cannot see them. Evaluate the whole object literal to +// get the authoritative key set — otherwise those keys look absent and get +// appended a second time. +const dictKeys = new Set(Object.keys(JSON.parse(fs.readFileSync(path.join(WORK, 'base.json'), 'utf8')))) + +// ` 'key': { zh: '...', en: '...' },` +const ENTRY = /^(\s*)((['"])(?:\\.|(?!\3).)*\3)\s*:\s*(\{.*\})\s*,?\s*$/ + +/** Serialise a string as a JS literal, preferring single quotes. */ +function q(s) { + const esc = s.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/\r/g, '\\r') + if (!esc.includes("'")) return `'${esc}'` + if (!esc.includes('"')) return `"${esc}"` + return `'${esc.replace(/'/g, "\\'")}'` +} + +const lifted = {} // key -> { lang: text } for languages other than zh/en +const seen = new Set() +const filledZh = [] +const stillMissing = [] +let liftedCount = 0 + +for (let i = 0; i < lines.length; i++) { + const m = ENTRY.exec(lines[i]) + if (!m) continue + const [, indent, rawKey, , objSrc] = m + let key + let obj + try { + key = new Function('return ' + rawKey)() + obj = new Function('return ' + objSrc)() + } catch { + continue + } + if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) continue + seen.add(key) + + // Lift out non-source languages. + const extra = {} + for (const [l, v] of Object.entries(obj)) { + if (SOURCE_LANGS.has(l)) continue + if (typeof v === 'string' && v.trim()) extra[l] = v.trim() + } + if (Object.keys(extra).length) { + lifted[key] = extra + liftedCount++ + } + + let zh = typeof obj.zh === 'string' ? obj.zh : '' + const en = typeof obj.en === 'string' ? obj.en : '' + if (!zh && fallbacks[key]) { + zh = fallbacks[key] + filledZh.push(key) + } + if (!zh || !en) stillMissing.push(key) + + // Leave already-correct lines byte-identical so the diff only shows real + // changes rather than a wholesale requoting of the file. + const untouched = + !Object.keys(extra).length && zh === obj.zh && Object.keys(obj).length === 2 && 'zh' in obj && 'en' in obj + if (untouched) continue + + lines[i] = `${indent}${rawKey}: { zh: ${q(zh)}, en: ${q(en)} },` +} + +// --- append keys that exist only at call sites ------------------------------- +const added = Object.keys(fallbacks).filter((k) => !seen.has(k) && !dictKeys.has(k)).sort() +if (added.length) { + // Find the depth-0 closing brace of the DICT object literal. + let close = -1 + let depth = 0 + let started = false + for (let i = 0; i < lines.length; i++) { + if (!started && !/^const DICT/.test(lines[i])) continue + started = true + for (const c of lines[i]) { + if (c === '{') depth++ + else if (c === '}') { + depth-- + if (depth === 0) { + close = i + break + } + } + } + if (close >= 0) break + } + if (close < 0) throw new Error('could not locate end of DICT') + const block = [ + ' // Keys recovered from t() call sites that had no dictionary entry.', + ...added.map((k) => ` ${q(k)}: { zh: ${q(fallbacks[k])}, en: '' },`), + ] + lines.splice(close, 0, ...block) + stillMissing.push(...added) +} + +if (!DRY) { + fs.writeFileSync(SRC, lines.join('\n')) + fs.mkdirSync(WORK, { recursive: true }) + fs.writeFileSync(path.join(WORK, 'out.existing.json'), JSON.stringify(lifted, null, 2)) + fs.writeFileSync(path.join(WORK, 'needs-source.json'), JSON.stringify([...new Set(stillMissing)].sort(), null, 2)) +} + +console.log(`call sites : ${Object.keys(fallbacks).length}`) +console.log(`dict entries : ${seen.size}`) +console.log(`zh filled in : ${filledZh.length}`) +console.log(`keys appended : ${added.length}`) +console.log(`langs lifted out: ${liftedCount} entries -> .i18n-work/out.existing.json`) +console.log(`still incomplete: ${new Set(stillMissing).size} -> .i18n-work/needs-source.json`) +if (added.length) console.log(` appended: ${added.slice(0, 12).join(', ')}${added.length > 12 ? ' …' : ''}`) From 02730f2e1e0f0a334909ff7e88e47442098757ae Mon Sep 17 00:00:00 2001 From: liberxue <5831248+Liberxue@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:58:23 +0800 Subject: [PATCH 2/3] refactor: convert Chinese doc comments and test fixtures to English Apply the new comment conventions across all crates: translate rustdoc comments, utoipa descriptions, and error/ignore messages to English, and update test fixtures accordingly. String literals that intentionally exercise i18n (e.g. mock data) are left as-is per docs/COMMENT_CONVENTIONS.md. --- crates/agent-runtime/src/backend.rs | 17 +-- crates/agent-runtime/src/client.rs | 10 +- crates/agent-runtime/src/events.rs | 8 +- crates/agent-runtime/src/git.rs | 4 +- crates/agent-runtime/src/main.rs | 49 ++++---- crates/agent-runtime/src/models.rs | 4 +- crates/api-definition/src/adapters/http.rs | 64 +++++----- crates/api-definition/src/adapters/pg.rs | 43 ++++--- .../src/application/add_api_case.rs | 8 +- .../src/application/add_api_mock.rs | 6 +- .../src/application/create_api_case.rs | 6 +- .../src/application/create_api_definition.rs | 2 +- .../src/application/import_api_definitions.rs | 17 ++- .../src/application/import_schedule.rs | 4 +- .../src/application/update_api_definition.rs | 10 +- crates/api-definition/src/domain/api_case.rs | 4 +- .../src/domain/api_definition.rs | 6 +- crates/api-definition/src/domain/api_mock.rs | 4 +- .../api-definition/src/domain/api_module.rs | 6 +- crates/api-definition/src/domain/api_view.rs | 8 +- crates/api-definition/src/domain/import.rs | 12 +- .../api-definition/src/domain/import_har.rs | 11 +- .../src/domain/import_jmeter.rs | 10 +- .../src/domain/import_metersphere.rs | 19 +-- .../src/domain/import_postman.rs | 6 +- .../src/domain/import_schedule.rs | 4 +- crates/api-definition/tests/architecture.rs | 6 +- crates/api-runner/src/domain/runner.rs | 116 ++++++++++-------- crates/api-runner/tests/architecture.rs | 6 +- crates/api-scenario/src/adapters/http.rs | 38 ++++-- crates/api-scenario/src/adapters/pg.rs | 6 +- .../src/application/copy_scenario.rs | 12 +- .../src/application/create_scenario.rs | 4 +- crates/api-scenario/tests/architecture.rs | 6 +- crates/api-test/src/adapters/http.rs | 17 +-- crates/api-test/src/adapters/pg.rs | 6 +- .../api-test/src/adapters/report_archive.rs | 2 +- .../src/application/manage_resource_pool.rs | 12 +- crates/api-test/src/domain/resource_pool.rs | 4 +- crates/api-test/tests/architecture.rs | 6 +- crates/bug/src/adapters/http.rs | 10 +- crates/bug/src/adapters/pg.rs | 16 +-- crates/bug/src/application/create_bug.rs | 18 ++- crates/bug/src/domain/bug.rs | 6 +- crates/bug/tests/architecture.rs | 6 +- crates/case-management/Cargo.toml | 4 +- crates/case-management/src/adapters/http.rs | 24 ++-- .../case-management/src/application/cases.rs | 41 ++++--- crates/case-management/src/domain/case.rs | 10 +- crates/case-management/tests/architecture.rs | 6 +- crates/case/src/adapters/http.rs | 16 +-- crates/case/src/adapters/pg.rs | 2 +- crates/case/src/application/delete_review.rs | 2 +- crates/case/src/application/submit_review.rs | 2 +- .../src/application/update_review_meta.rs | 6 +- crates/case/src/domain/review.rs | 2 +- crates/case/tests/architecture.rs | 6 +- crates/comment/src/adapters/http.rs | 6 +- crates/comment/src/adapters/pg.rs | 6 +- crates/comment/src/application/add_comment.rs | 5 +- crates/comment/src/domain/comment.rs | 5 +- crates/comment/tests/architecture.rs | 6 +- crates/delivery/src/adapters/agent_http.rs | 4 +- crates/delivery/src/adapters/http.rs | 8 +- crates/delivery/src/adapters/local.rs | 4 +- crates/delivery/src/adapters/pg.rs | 17 +-- .../src/application/delivery_service.rs | 6 +- crates/delivery/src/domain/delivery.rs | 6 +- crates/delivery/src/domain/event.rs | 6 +- crates/delivery/tests/architecture.rs | 6 +- crates/design/src/adapters/pg.rs | 14 +-- .../src/application/proposal_service.rs | 15 ++- crates/design/src/domain/proposal.rs | 14 +-- crates/design/tests/architecture.rs | 6 +- crates/environment/src/adapters/http.rs | 2 +- crates/environment/tests/architecture.rs | 6 +- crates/follow/src/adapters/http.rs | 2 +- crates/follow/src/adapters/pg.rs | 2 +- crates/follow/tests/architecture.rs | 6 +- crates/migrate/tests/migration_versions.rs | 11 +- crates/mock-runtime/tests/architecture.rs | 6 +- crates/notice/src/adapters/http.rs | 4 +- crates/notice/src/adapters/pg.rs | 4 +- crates/notice/src/adapters/robot_sender.rs | 2 +- crates/notice/src/application/notifier.rs | 11 +- crates/notice/src/domain/notice.rs | 5 +- crates/notice/tests/architecture.rs | 6 +- .../src/application/delivery_feedback.rs | 6 +- crates/orchestrator/src/judges.rs | 6 +- crates/orchestrator/tests/architecture.rs | 6 +- crates/perf/src/adapters/engine.rs | 4 +- crates/perf/tests/architecture.rs | 6 +- crates/perf/tests/load_e2e.rs | 5 +- crates/perf/tests/probe_load_e2e.rs | 5 +- crates/pool-runner/src/protocol.rs | 5 +- crates/probe/src/domain/probe.rs | 17 +-- crates/probe/src/plugins/ssh.rs | 16 +-- crates/probe/tests/architecture.rs | 6 +- crates/probe/tests/mysql_live.rs | 15 ++- crates/probe/tests/redis_live.rs | 15 ++- crates/probe/tests/websocket_e2e.rs | 12 +- crates/project/src/adapters/http.rs | 2 +- crates/project/src/adapters/member_http.rs | 2 +- crates/project/src/adapters/pg.rs | 2 +- crates/project/src/adapters/pg_template.rs | 12 +- crates/project/src/adapters/template_http.rs | 10 +- .../src/application/template_service.rs | 20 +-- crates/project/src/domain/template.rs | 6 +- crates/project/tests/architecture.rs | 6 +- crates/rag/src/adapters.rs | 6 +- crates/rag/src/http.rs | 4 +- crates/requirement/src/adapters/http.rs | 104 ++++++++-------- crates/requirement/src/adapters/in_memory.rs | 10 +- crates/requirement/src/adapters/pg.rs | 52 ++++---- .../src/application/create_requirement.rs | 58 ++++----- .../src/application/requirement_admin.rs | 76 ++++++------ crates/requirement/src/domain/requirement.rs | 37 +++--- crates/requirement/tests/architecture.rs | 6 +- crates/runner/src/adapters/http.rs | 17 +-- crates/runner/src/application/mod.rs | 17 +-- crates/runner/src/domain/agent.rs | 11 +- crates/runner/tests/architecture.rs | 6 +- crates/runner/tests/pg_protocol.rs | 2 +- crates/server/src/breakdown_route.rs | 16 +-- crates/server/src/case_drafter.rs | 4 +- crates/server/src/decomposition_run.rs | 6 +- crates/server/src/import_scheduler.rs | 22 ++-- crates/server/src/judge.rs | 9 +- crates/server/src/llm.rs | 66 +++++----- crates/server/src/main.rs | 10 +- crates/server/src/mcp_tools.rs | 2 +- crates/server/src/openapi.rs | 4 +- crates/server/src/orchestration.rs | 12 +- crates/server/src/perf_run.rs | 16 ++- crates/server/src/plan_run.rs | 14 ++- crates/server/src/planner.rs | 4 +- crates/server/src/pool_runner_ws.rs | 2 +- crates/server/src/prd_draft_route.rs | 21 ++-- crates/server/src/references_route.rs | 2 +- crates/server/src/routes.rs | 4 +- crates/server/src/scenario_run.rs | 2 +- crates/server/tests/scenarios.rs | 114 +++++++++-------- crates/shepherd-cli/src/client.rs | 86 ++++++------- crates/shepherd-cli/src/commands/agent.rs | 17 +-- crates/shepherd-cli/src/commands/apidef.rs | 18 +-- crates/shepherd-cli/src/commands/apikey.rs | 4 +- crates/shepherd-cli/src/commands/auth.rs | 6 +- crates/shepherd-cli/src/commands/comment.rs | 2 +- crates/shepherd-cli/src/commands/debug.rs | 4 +- crates/shepherd-cli/src/commands/fcase.rs | 2 +- crates/shepherd-cli/src/commands/import.rs | 4 +- crates/shepherd-cli/src/commands/llm.rs | 2 +- crates/shepherd-cli/src/commands/mcp.rs | 4 +- crates/shepherd-cli/src/commands/notice.rs | 8 +- .../shepherd-cli/src/commands/oidc_admin.rs | 2 +- crates/shepherd-cli/src/commands/pfile.rs | 21 ++-- crates/shepherd-cli/src/commands/plan.rs | 2 +- crates/shepherd-cli/src/commands/root.rs | 16 +-- crates/shepherd-cli/src/commands/scenario.rs | 4 +- crates/shepherd-cli/src/main.rs | 6 +- crates/skill/src/adapters/http.rs | 10 +- crates/skill/src/adapters/pg.rs | 12 +- crates/skill/src/application/create_skill.rs | 2 +- crates/skill/src/application/skill_admin.rs | 12 +- crates/skill/src/domain/skill.rs | 6 +- crates/skill/tests/architecture.rs | 6 +- .../src/adapters/apikey_http.rs | 2 +- crates/system-setting/src/adapters/http.rs | 4 +- .../src/adapters/llm_model_http.rs | 2 +- crates/system-setting/src/adapters/oidc.rs | 4 +- crates/system-setting/src/adapters/pg.rs | 14 +-- .../src/ports/user_directory.rs | 2 +- crates/system-setting/tests/architecture.rs | 2 +- crates/task/src/adapters/heuristic_planner.rs | 17 +-- crates/task/src/adapters/http.rs | 4 +- crates/task/src/adapters/pg.rs | 4 +- crates/task/src/application/breakdown.rs | 7 +- crates/task/src/application/task_admin.rs | 14 ++- crates/task/tests/architecture.rs | 6 +- crates/test-plan/src/adapters/http.rs | 62 ++++++---- crates/test-plan/src/adapters/pg.rs | 20 +-- .../test-plan/src/application/create_plan.rs | 25 ++-- .../test-plan/src/application/manage_plan.rs | 14 +-- .../test-plan/src/application/plan_cases.rs | 6 +- .../src/application/plan_planning.rs | 31 ++--- .../src/application/plan_statistics.rs | 12 +- .../test-plan/src/application/scheduling.rs | 2 +- crates/test-plan/src/domain/plan.rs | 6 +- crates/test-plan/tests/architecture.rs | 6 +- crates/verification/src/adapters/http.rs | 4 +- crates/verification/src/adapters/pg.rs | 10 +- .../src/application/verification_admin.rs | 10 +- .../verification/src/domain/verification.rs | 23 +++- crates/verification/tests/architecture.rs | 6 +- 194 files changed, 1399 insertions(+), 1057 deletions(-) diff --git a/crates/agent-runtime/src/backend.rs b/crates/agent-runtime/src/backend.rs index 267ff443..83c313c4 100644 --- a/crates/agent-runtime/src/backend.rs +++ b/crates/agent-runtime/src/backend.rs @@ -194,7 +194,7 @@ impl CliAgentBackend for ClaudeBackend { } sink.emit(ExecEvent::new( "TIMEOUT", - &format!("超时 {}s,已终止", self.timeout.as_secs()), + &format!("timed out after {}s, terminated", self.timeout.as_secs()), )) .await; return Err(BackendError::Run(format!( @@ -266,7 +266,8 @@ impl CliAgentBackend for GenericCliBackend { ) -> Result { let (program, args) = self.cmd.split_first().ok_or_else(|| BackendError::Run("empty cmd".into()))?; - sink.emit(ExecEvent::new("DECISION", &format!("调用 {} 执行任务", self.name))).await; + sink.emit(ExecEvent::new("DECISION", &format!("invoking {} to run the task", self.name))) + .await; let mut cmd = Command::new(program); cmd.current_dir(cwd) .args(args) @@ -310,7 +311,7 @@ pub struct MockBackend { impl Default for MockBackend { fn default() -> Self { - Self { output: "## Mock 输出\n(agent-runtime mock backend)".to_string() } + Self { output: "## Mock output\n(agent-runtime mock backend)".to_string() } } } @@ -326,7 +327,7 @@ impl CliAgentBackend for MockBackend { _cwd: &str, sink: &dyn ProgressSink, ) -> Result { - sink.emit(ExecEvent::new("DECISION", "mock backend 运行中")).await; + sink.emit(ExecEvent::new("DECISION", "mock backend running")).await; Ok(self.output.clone()) } } @@ -392,12 +393,12 @@ mod tests { async fn generic_backend_runs_cmd_and_captures_stdout() { let b = GenericCliBackend { name: "codex", - cmd: vec!["/bin/sh".into(), "-c".into(), "printf '## codex 设计'".into(), "_".into()], + cmd: vec!["/bin/sh".into(), "-c".into(), "printf '## codex design'".into(), "_".into()], timeout: TEST_TIMEOUT, }; let sink = RecSink::default(); let out = b.execute("the prompt", ".", &sink).await.expect("run"); - assert_eq!(out, "## codex 设计"); + assert_eq!(out, "## codex design"); assert_eq!( sink.events.lock().unwrap_or_else(std::sync::PoisonError::into_inner)[0].kind, "DECISION" @@ -408,12 +409,12 @@ mod tests { async fn claude_backend_streams_events_and_returns_result() { let path = write_script( "claude", - "#!/bin/sh\ncat >/dev/null\nprintf '%s\\n' '{\"type\":\"assistant\",\"message\":{\"content\":[{\"type\":\"tool_use\",\"name\":\"Edit\",\"input\":{\"file_path\":\"a.rs\"}}]}}' '{\"type\":\"result\",\"is_error\":false,\"result\":\"完成\"}'\n", + "#!/bin/sh\ncat >/dev/null\nprintf '%s\\n' '{\"type\":\"assistant\",\"message\":{\"content\":[{\"type\":\"tool_use\",\"name\":\"Edit\",\"input\":{\"file_path\":\"a.rs\"}}]}}' '{\"type\":\"result\",\"is_error\":false,\"result\":\"done\"}'\n", ); let b = ClaudeBackend { bin: path.to_string_lossy().to_string(), timeout: TEST_TIMEOUT }; let sink = RecSink::default(); let out = b.execute("do it", ".", &sink).await.expect("run"); - assert_eq!(out, "完成"); + assert_eq!(out, "done"); let evs = sink.events.lock().unwrap_or_else(std::sync::PoisonError::into_inner); assert_eq!(evs.len(), 1); assert_eq!(evs[0], ExecEvent::new("FILE_CHANGE", "Edit a.rs")); diff --git a/crates/agent-runtime/src/client.rs b/crates/agent-runtime/src/client.rs index 917a2654..2c872fcd 100644 --- a/crates/agent-runtime/src/client.rs +++ b/crates/agent-runtime/src/client.rs @@ -28,7 +28,7 @@ where Ok(v) => return Ok(v), Err(e) if i == attempts => return Err(e), Err(e) => { - tracing::warn!("{label} 第 {i}/{attempts} 次失败,{delay:?} 后重试: {e}"); + tracing::warn!("{label} attempt {i}/{attempts} failed, retrying in {delay:?}: {e}"); tokio::time::sleep(delay).await; delay = (delay * 2).min(Duration::from_secs(10)); } @@ -53,7 +53,7 @@ impl ServerClient { let revoked = status == reqwest::StatusCode::UNAUTHORIZED; if revoked { tracing::error!( - "API key 已吊销或无效(HTTP 401):请管理员在 POST /system/apikey 重新签发,并更新 SHEPHERD_AGENT_KEY" + "API key revoked or invalid (HTTP 401): ask an admin to reissue it via POST /system/apikey and update SHEPHERD_AGENT_KEY" ); } revoked @@ -62,7 +62,7 @@ impl ServerClient { /// Stand-in for error_for_status: maps 401 to a readable "key revoked" error, keeps status errors otherwise. fn ensure_ok(&self, resp: reqwest::Response) -> anyhow::Result { if self.key_revoked(resp.status()) { - anyhow::bail!("API key 已吊销或无效"); + anyhow::bail!("API key revoked or invalid"); } Ok(resp.error_for_status()?) } @@ -92,7 +92,7 @@ impl ServerClient { .await? .status(); if self.key_revoked(code) { - anyhow::bail!("API key 已吊销或无效"); + anyhow::bail!("API key revoked or invalid"); } Ok(code != reqwest::StatusCode::NOT_FOUND) } @@ -233,7 +233,7 @@ mod tests { assert_eq!( req.headers()["authorization"], "Bearer sak_0123456789abcdef.deadbeef", - "静态 key 应原样作为 bearer 附带" + "static key should be attached verbatim as bearer" ); } diff --git a/crates/agent-runtime/src/events.rs b/crates/agent-runtime/src/events.rs index d06658ff..21611871 100644 --- a/crates/agent-runtime/src/events.rs +++ b/crates/agent-runtime/src/events.rs @@ -68,7 +68,7 @@ fn parse_assistant(v: &serde_json::Value) -> Vec { .and_then(|x| x.as_str()) .unwrap_or(""); if is_test_command(cmd) { - ExecEvent::new("TEST_RESULT", &format!("运行测试: {}", clip(cmd, 120))) + ExecEvent::new("TEST_RESULT", &format!("run tests: {}", clip(cmd, 120))) } else { ExecEvent::new("TOOL_CALL", &format!("$ {}", clip(cmd, 120))) } @@ -189,8 +189,8 @@ mod tests { let bash = r#"{"type":"assistant","message":{"content":[{"type":"tool_use","name":"Bash","input":{"command":"ls -la"}}]}}"#; assert_eq!(parse_claude_line(bash), vec![ExecEvent::new("TOOL_CALL", "$ ls -la")]); - let text = r#"{"type":"assistant","message":{"content":[{"type":"text","text":"用 argon2 哈希"}]}}"#; - assert_eq!(parse_claude_line(text), vec![ExecEvent::new("DECISION", "用 argon2 哈希")]); + let text = r#"{"type":"assistant","message":{"content":[{"type":"text","text":"hash with argon2"}]}}"#; + assert_eq!(parse_claude_line(text), vec![ExecEvent::new("DECISION", "hash with argon2")]); } #[test] @@ -198,7 +198,7 @@ mod tests { let bash = r#"{"type":"assistant","message":{"content":[{"type":"tool_use","name":"Bash","input":{"command":"cargo test -p auth"}}]}}"#; assert_eq!( parse_claude_line(bash), - vec![ExecEvent::new("TEST_RESULT", "运行测试: cargo test -p auth")] + vec![ExecEvent::new("TEST_RESULT", "run tests: cargo test -p auth")] ); } diff --git a/crates/agent-runtime/src/git.rs b/crates/agent-runtime/src/git.rs index f4ac69bd..7661d0fe 100644 --- a/crates/agent-runtime/src/git.rs +++ b/crates/agent-runtime/src/git.rs @@ -118,7 +118,7 @@ mod tests { run(&cwd, &["commit", "-q", "-m", "init"]).await; assert!(snapshot(&cwd, "att-1", "t").await.is_none()); std::fs::write(dir.join("a.txt"), "v2 changed").expect("w"); - let snap = snapshot(&cwd, "att-abc-def", "实现登录").await.expect("snapshot"); + let snap = snapshot(&cwd, "att-abc-def", "implement login").await.expect("snapshot"); assert!(snap.reference.starts_with("git://shepherd/deliver/att@")); let branch = git(&cwd, &["rev-parse", "shepherd/deliver/att"]).await; assert!(branch.is_some(), "branch created"); @@ -141,7 +141,7 @@ mod tests { let wt = add_worktree(&base, "att-xyz-9", None).await.expect("worktree"); std::fs::write(std::path::Path::new(&wt).join("f.txt"), "agent edit").expect("w"); - let snap = snapshot(&wt, "att-xyz-9", "实现").await.expect("snapshot"); + let snap = snapshot(&wt, "att-xyz-9", "implement").await.expect("snapshot"); assert!(snap.reference.starts_with("git://shepherd/deliver/att@")); assert_eq!(std::fs::read_to_string(dir.join("f.txt")).unwrap(), "base"); assert!(git(&base, &["rev-parse", "shepherd/deliver/att"]).await.is_some()); diff --git a/crates/agent-runtime/src/main.rs b/crates/agent-runtime/src/main.rs index 955517ef..b330bf35 100644 --- a/crates/agent-runtime/src/main.rs +++ b/crates/agent-runtime/src/main.rs @@ -65,8 +65,8 @@ impl Config { fn agent_key(raw: Option) -> anyhow::Result { raw.filter(|s| !s.trim().is_empty()).ok_or_else(|| { anyhow::anyhow!( - "缺少 SHEPHERD_AGENT_KEY:agent-runtime 只接受 API key 认证。\ - 请设置 SHEPHERD_AGENT_KEY=sak_…;key 可在 个人中心 → API KEY 或 POST /system/apikey 签发" + "missing SHEPHERD_AGENT_KEY: agent-runtime only accepts API key authentication. \ + Set SHEPHERD_AGENT_KEY=sak_…; a key can be issued from Profile → API KEY or POST /system/apikey" ) }) } @@ -132,17 +132,19 @@ async fn handle( let prompt = spec.to_prompt(); let mode = if spec.is_design() { "design" } else { "implement" }; tracing::info!(attempt = %spec.attempt_id, executor = %spec.executor, mode, - backend = backend.cli_name(), "认领任务"); + backend = backend.cli_name(), "claimed task"); if spec.is_design() { match backend.execute(&prompt, base_workdir, &NoopSink).await { Ok(doc) => match client.post_design(&spec.attempt_id, &doc).await { - Ok(()) => tracing::info!(proposal = %spec.attempt_id, "设计稿已回填 → 待审"), + Ok(()) => { + tracing::info!(proposal = %spec.attempt_id, "design draft posted back → pending review") + } Err(e) => { - tracing::error!(proposal = %spec.attempt_id, "回填设计稿最终失败(已重试): {e}") + tracing::error!(proposal = %spec.attempt_id, "posting the design draft back finally failed (after retries): {e}") } }, - Err(e) => tracing::warn!(proposal = %spec.attempt_id, "设计起草失败: {e}"), + Err(e) => tracing::warn!(proposal = %spec.attempt_id, "design drafting failed: {e}"), } return; } @@ -157,21 +159,24 @@ async fn handle( match git::snapshot(run_dir, &spec.attempt_id, &spec.title).await { Some(s) => { let stat: String = s.stat.replace('\n', ";").chars().take(300).collect(); - (s.reference, format!("变更:{stat} | {summary}")) - } - None => { - (format!("runtime://{}", spec.attempt_id), format!("(无代码变动){summary}")) + (s.reference, format!("changes: {stat} | {summary}")) } + None => ( + format!("runtime://{}", spec.attempt_id), + format!("(no code changes){summary}"), + ), }; match client.complete(&spec.attempt_id, "DIFF", &reference, &summary).await { - Ok(()) => tracing::info!(attempt = %spec.attempt_id, %reference, "交付完成"), + Ok(()) => { + tracing::info!(attempt = %spec.attempt_id, %reference, "delivery complete") + } Err(e) => tracing::error!(attempt = %spec.attempt_id, - "交付上报最终失败(已重试,server 将到点 reclaim): {e}"), + "delivery report finally failed (after retries; the server will reclaim on timeout): {e}"), } } Err(e) => { if let Err(e2) = client.fail(&spec.attempt_id, &e.to_string()).await { - tracing::error!(attempt = %spec.attempt_id, "失败上报也失败(已重试): {e2} / 原因 {e}"); + tracing::error!(attempt = %spec.attempt_id, "reporting the failure also failed (after retries): {e2} / cause {e}"); } } } @@ -206,7 +211,7 @@ async fn main() -> anyhow::Result<()> { match connect(&cfg).await { Ok(pair) => break pair, Err(e) => { - tracing::warn!(base = %cfg.base, "连接 server 失败,5s 后重试: {e}"); + tracing::warn!(base = %cfg.base, "failed to connect to the server, retrying in 5s: {e}"); tokio::select! { _ = wait_shutdown(&mut sd) => return Ok(()), _ = tokio::time::sleep(Duration::from_secs(5)) => {} @@ -216,7 +221,7 @@ async fn main() -> anyhow::Result<()> { }; let runtime_id = Arc::new(Mutex::new(id0.clone())); tracing::info!(base = %cfg.base, caps = ?cfg.caps, concurrency = cfg.concurrency, runtime_id = %id0, - "agent-runtime 上线"); + "agent-runtime online"); let hb = { let (client, rid, name, caps, every) = @@ -260,7 +265,7 @@ async fn main() -> anyhow::Result<()> { } Ok(None) => drop(permit), Err(e) => { - tracing::warn!("认领出错: {e}"); + tracing::warn!("claim failed: {e}"); drop(permit); tokio::time::sleep(Duration::from_secs(2)).await; } @@ -273,17 +278,17 @@ async fn main() -> anyhow::Result<()> { tracing::info!( inflight, drain_secs = cfg.drain_timeout.as_secs(), - "收到关停信号,等待在飞任务收尾…" + "shutdown signal received, waiting for in-flight tasks to finish…" ); match tokio::time::timeout(cfg.drain_timeout, sem.acquire_many(mc)).await { - Ok(_) => tracing::info!("在飞任务已全部收尾"), + Ok(_) => tracing::info!("all in-flight tasks finished"), Err(_) => tracing::warn!( stuck = cfg.concurrency - sem.available_permits(), - "排空超时,强制退出(server 将到点 reclaim 未完成任务)" + "drain timed out, forcing exit (the server will reclaim unfinished tasks on timeout)" ), } } - tracing::info!("agent-runtime 优雅退出"); + tracing::info!("agent-runtime shut down gracefully"); Ok(()) } @@ -301,8 +306,8 @@ mod tests { for raw in [None, Some(String::new()), Some(" ".into())] { let err = agent_key(raw).expect_err("blank/missing key must fail startup"); let msg = err.to_string(); - assert!(msg.contains("SHEPHERD_AGENT_KEY"), "要点名缺的环境变量: {msg}"); - assert!(msg.contains("/system/apikey"), "要给签发指引: {msg}"); + assert!(msg.contains("SHEPHERD_AGENT_KEY"), "must name the missing env var: {msg}"); + assert!(msg.contains("/system/apikey"), "must give issuance guidance: {msg}"); } } } diff --git a/crates/agent-runtime/src/models.rs b/crates/agent-runtime/src/models.rs index 33c7139b..0391980a 100644 --- a/crates/agent-runtime/src/models.rs +++ b/crates/agent-runtime/src/models.rs @@ -62,12 +62,12 @@ mod tests { #[test] fn deserializes_claim_and_detects_design() { let s = spec( - r#"{"attemptId":"a1","title":"登录","description":"做","executor":"CLAUDE_CODE","context":"design","acceptanceCriteria":["c1"]}"#, + r#"{"attemptId":"a1","title":"login","description":"do it","executor":"CLAUDE_CODE","context":"design","acceptanceCriteria":["c1"]}"#, ); assert_eq!(s.attempt_id, "a1"); assert!(s.is_design()); let p = s.to_prompt(); - assert!(p.contains("# Task: 登录")); + assert!(p.contains("# Task: login")); assert!(p.contains("- c1")); assert!(!p.contains("Context: design")); } diff --git a/crates/api-definition/src/adapters/http.rs b/crates/api-definition/src/adapters/http.rs index 74406263..6b98768d 100644 --- a/crates/api-definition/src/adapters/http.rs +++ b/crates/api-definition/src/adapters/http.rs @@ -1398,7 +1398,7 @@ async fn import_definitions( ApiViewUpdateBody, ApiViewResponse )), - tags((name = "api-definition", description = "接口定义")) + tags((name = "api-definition", description = "API definition")) )] struct ApiDoc; pub fn openapi() -> utoipa::openapi::OpenApi { @@ -1462,7 +1462,7 @@ mod tests { .clone() .oneshot(post( "/api/definition", - r#"{"projectId":"p1","name":"登录","method":"POST","path":"/login"}"#, + r#"{"projectId":"p1","name":"login","method":"POST","path":"/login"}"#, Some(t), )) .await @@ -1479,7 +1479,7 @@ mod tests { .clone() .oneshot(post( "/api/definition", - r#"{"projectId":"p1","name":"登录","method":"post","path":"/login"}"#, + r#"{"projectId":"p1","name":"login","method":"post","path":"/login"}"#, Some(&t), )) .await @@ -1608,7 +1608,7 @@ mod tests { .clone() .oneshot(post( &format!("/api/definition/{id}/case"), - r#"{"name":"用例","method":"POST","url":"/login","assertions":[]}"#, + r#"{"name":"case","method":"POST","url":"/login","assertions":[]}"#, Some(&t), )) .await @@ -1661,21 +1661,21 @@ mod tests { .clone() .oneshot(put( &format!("/api/definition/{id}"), - r#"{"name":"登录v2","method":"put","path":"/v2/login"}"#, + r#"{"name":"login v2","method":"put","path":"/v2/login"}"#, Some(&t), )) .await .expect("resp"); assert_eq!(resp.status(), StatusCode::OK); let v = json_body(resp).await; - assert_eq!(v["name"], "登录v2"); + assert_eq!(v["name"], "login v2"); assert_eq!(v["method"], "PUT"); assert_eq!(v["path"], "/v2/login"); let resp = app.oneshot(get(&format!("/api/definition/{id}"), Some(&t))).await.expect("resp"); let v = json_body(resp).await; - assert_eq!(v["name"], "登录v2"); + assert_eq!(v["name"], "login v2"); assert_eq!(v["path"], "/v2/login"); } @@ -1689,7 +1689,7 @@ mod tests { .expect("resp"); assert_eq!(resp.status(), StatusCode::OK); let v = json_body(resp).await; - assert_eq!(v["name"], "登录"); + assert_eq!(v["name"], "login"); assert_eq!(v["method"], "POST"); assert_eq!(v["path"], "/login2"); } @@ -1747,7 +1747,7 @@ mod tests { .clone() .oneshot(post( &format!("/api/definition/{id}/case"), - r#"{"name":"用例","method":"POST","url":"/login","assertions":[]}"#, + r#"{"name":"case","method":"POST","url":"/login","assertions":[]}"#, Some(&t), )) .await @@ -1769,7 +1769,7 @@ mod tests { let resp = app .oneshot(post( "/api/definition/ghost/case", - r#"{"name":"用例","method":"GET","url":"/x"}"#, + r#"{"name":"case","method":"GET","url":"/x"}"#, Some(&t), )) .await @@ -1784,7 +1784,7 @@ mod tests { let resp = app .oneshot(post( &format!("/api/definition/{id}/case"), - r#"{"name":"用例","method":"GET","url":"/x","assertions":{}}"#, + r#"{"name":"case","method":"GET","url":"/x","assertions":{}}"#, Some(&t), )) .await @@ -1799,7 +1799,7 @@ mod tests { .clone() .oneshot(post( "/api/case", - r#"{"projectId":"p1","name":"用例","method":"post","url":"/login"}"#, + r#"{"projectId":"p1","name":"case","method":"post","url":"/login"}"#, Some(&t), )) .await @@ -1818,7 +1818,7 @@ mod tests { .clone() .oneshot(post( "/api/case", - r#"{"projectId":"p1","apiDefinitionId":"def-9","name":"用例","method":"GET","url":"/x","assertions":[]}"#, + r#"{"projectId":"p1","apiDefinitionId":"def-9","name":"case","method":"GET","url":"/x","assertions":[]}"#, Some(&t), )) .await @@ -1834,7 +1834,7 @@ mod tests { let resp = app .oneshot(post( "/api/case", - r#"{"projectId":"p1","name":"用例","method":"GET","url":"/x"}"#, + r#"{"projectId":"p1","name":"case","method":"GET","url":"/x"}"#, None, )) .await @@ -1852,7 +1852,7 @@ mod tests { let resp = app .oneshot(post( "/api/case", - r#"{"projectId":"p1","name":"用例","method":"GET","url":"/x"}"#, + r#"{"projectId":"p1","name":"case","method":"GET","url":"/x"}"#, Some(&token), )) .await @@ -1866,7 +1866,7 @@ mod tests { let resp = app .oneshot(post( "/api/case", - r#"{"projectId":"p1","name":"用例","method":"GET","url":"/x","assertions":{}}"#, + r#"{"projectId":"p1","name":"case","method":"GET","url":"/x","assertions":{}}"#, Some(&t), )) .await @@ -1918,7 +1918,7 @@ mod tests { .clone() .oneshot(post( &format!("/api/definition/{id}/mock"), - r#"{"name":"挡板","responseStatus":404,"enabled":false}"#, + r#"{"name":"stub","responseStatus":404,"enabled":false}"#, Some(&t), )) .await @@ -1943,7 +1943,7 @@ mod tests { let resp = app .oneshot(post( &format!("/api/definition/{id}/mock"), - r#"{"name":"挡板","responseStatus":700}"#, + r#"{"name":"stub","responseStatus":700}"#, Some(&t), )) .await @@ -1955,7 +1955,7 @@ mod tests { async fn add_mock_to_missing_definition_404() { let (app, t) = app().await; let resp = app - .oneshot(post("/api/definition/ghost/mock", r#"{"name":"挡板"}"#, Some(&t))) + .oneshot(post("/api/definition/ghost/mock", r#"{"name":"stub"}"#, Some(&t))) .await .expect("resp"); assert_eq!(resp.status(), StatusCode::NOT_FOUND); @@ -1967,7 +1967,7 @@ mod tests { let id = create_definition_returns_id(&app, &t).await; let resp = app .clone() - .oneshot(post(&format!("/api/definition/{id}/mock"), r#"{"name":"挡板"}"#, Some(&t))) + .oneshot(post(&format!("/api/definition/{id}/mock"), r#"{"name":"stub"}"#, Some(&t))) .await .expect("resp"); let mock_id = json_body(resp).await["id"].as_str().expect("id").to_string(); @@ -1976,7 +1976,7 @@ mod tests { .clone() .oneshot(put( &format!("/api/mock/{mock_id}"), - r#"{"name":"改名","responseStatus":500,"enabled":false}"#, + r#"{"name":"rename","responseStatus":500,"enabled":false}"#, Some(&t), )) .await @@ -1986,7 +1986,7 @@ mod tests { let resp = app.oneshot(get(&format!("/api/definition/{id}/mock"), Some(&t))).await.expect("resp"); let v = json_body(resp).await; - assert_eq!(v[0]["name"], "改名"); + assert_eq!(v[0]["name"], "rename"); assert_eq!(v[0]["responseStatus"], 500); assert_eq!(v[0]["enabled"], false); } @@ -1997,7 +1997,7 @@ mod tests { let id = create_definition_returns_id(&app, &t).await; let resp = app .clone() - .oneshot(post(&format!("/api/definition/{id}/mock"), r#"{"name":"挡板"}"#, Some(&t))) + .oneshot(post(&format!("/api/definition/{id}/mock"), r#"{"name":"stub"}"#, Some(&t))) .await .expect("resp"); let mock_id = json_body(resp).await["id"].as_str().expect("id").to_string(); @@ -2025,7 +2025,7 @@ mod tests { .clone() .oneshot(post( "/api/api-view", - r#"{"projectId":"p1","name":"我的视图","config":{"pageSize":10},"shared":true}"#, + r#"{"projectId":"p1","name":"my view","config":{"pageSize":10},"shared":true}"#, Some(t), )) .await @@ -2042,12 +2042,12 @@ mod tests { // Partial update: only name changes; config/shared keep their prior values. let resp = app .clone() - .oneshot(put(&format!("/api/api-view/{id}"), r#"{"name":" 改名视图 "}"#, Some(&t))) + .oneshot(put(&format!("/api/api-view/{id}"), r#"{"name":" rename view "}"#, Some(&t))) .await .expect("resp"); assert_eq!(resp.status(), StatusCode::OK); let v = json_body(resp).await; - assert_eq!(v["name"], "改名视图"); + assert_eq!(v["name"], "rename view"); assert_eq!(v["config"], serde_json::json!({"pageSize": 10})); assert_eq!(v["shared"], true); @@ -2063,14 +2063,14 @@ mod tests { .expect("resp"); assert_eq!(resp.status(), StatusCode::OK); let v = json_body(resp).await; - assert_eq!(v["name"], "改名视图"); + assert_eq!(v["name"], "rename view"); assert_eq!(v["config"], serde_json::json!({"pageSize": 50})); assert_eq!(v["shared"], false); // List endpoint reflects the updated values. let resp = app.oneshot(get("/api/api-view?projectId=p1", Some(&t))).await.expect("resp"); let list = json_body(resp).await; - assert_eq!(list[0]["name"], "改名视图"); + assert_eq!(list[0]["name"], "rename view"); assert_eq!(list[0]["shared"], false); } @@ -2104,7 +2104,11 @@ mod tests { let resp = app .clone() - .oneshot(put(&format!("/api/api-view/{id}"), r#"{"name":"越权改名"}"#, Some(&other))) + .oneshot(put( + &format!("/api/api-view/{id}"), + r#"{"name":"unauthorized rename"}"#, + Some(&other), + )) .await .expect("resp"); assert_eq!(resp.status(), StatusCode::NOT_FOUND); @@ -2113,7 +2117,7 @@ mod tests { let resp = app.oneshot(get("/api/api-view?projectId=p1", Some(&owner))).await.expect("resp"); let list = json_body(resp).await; - assert_eq!(list[0]["name"], "我的视图"); + assert_eq!(list[0]["name"], "my view"); } #[tokio::test] diff --git a/crates/api-definition/src/adapters/pg.rs b/crates/api-definition/src/adapters/pg.rs index e9d45b6a..5f757250 100644 --- a/crates/api-definition/src/adapters/pg.rs +++ b/crates/api-definition/src/adapters/pg.rs @@ -674,7 +674,7 @@ mod tests { use crate::domain::ApiProtocol; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_definition_case_mock_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -686,18 +686,18 @@ mod tests { let repo = PgApiDefinitionRepository::new(pool.clone()); - let nd = NewApiDefinition::new("p1", "登录", ApiProtocol::Http, "POST", "/login") + let nd = NewApiDefinition::new("p1", "login", ApiProtocol::Http, "POST", "/login") .expect("valid"); let def = repo.insert_definition(&nd).await.expect("insert def"); assert_eq!(def.method, "POST"); - assert_eq!(repo.get_definition(&def.id).await.expect("get").expect("some").name, "登录"); + assert_eq!(repo.get_definition(&def.id).await.expect("get").expect("some").name, "login"); assert_eq!(repo.list_definitions("p1").await.expect("list").len(), 1); assert!(repo.get_definition("ghost").await.expect("get").is_none()); let nc = NewApiCase::new( &def.id, "p1", - "用例", + "case", "POST", "/login", Some("{}".into()), @@ -710,9 +710,16 @@ mod tests { assert_eq!(cases.len(), 1); assert!(cases[0].assertions.is_array()); - let standalone = - NewApiCase::new("", "p1", "独立用例", "GET", "/ping", None, serde_json::json!([])) - .expect("valid"); + let standalone = NewApiCase::new( + "", + "p1", + "standalone case", + "GET", + "/ping", + None, + serde_json::json!([]), + ) + .expect("valid"); repo.insert_case(&standalone).await.expect("insert standalone"); assert_eq!(repo.count_cases_by_project("p1").await.expect("count"), 2); let page = repo.list_cases_by_project("p1", 0, 1).await.expect("page"); @@ -722,7 +729,7 @@ mod tests { let nm = NewApiMock::new( &def.id, - "挡板", + "stub", serde_json::json!({"path": "/login"}), 200, Some("ok".into()), @@ -737,7 +744,7 @@ mod tests { let nm2 = NewApiMock::new( &def.id, - "改名挡板", + "renamed stub", serde_json::json!({"path": "/logout"}), 404, Some("nope".into()), @@ -747,7 +754,7 @@ mod tests { assert!(repo.update_mock(&mock.id, &nm2).await.expect("update mock")); let mocks = repo.list_mocks(&def.id).await.expect("list mocks"); assert_eq!(mocks.len(), 1); - assert_eq!(mocks[0].name, "改名挡板"); + assert_eq!(mocks[0].name, "renamed stub"); assert_eq!(mocks[0].response_status, 404); assert!(!mocks[0].enabled); assert_eq!(mocks[0].match_rule, serde_json::json!({"path": "/logout"})); @@ -755,7 +762,7 @@ mod tests { } #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_view_update_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -764,37 +771,37 @@ mod tests { let repo = PgApiDefinitionRepository::new(pool.clone()); - let nv = NewApiView::new("p1", "u1", "我的视图", serde_json::json!({"pageSize": 10}), true) + let nv = NewApiView::new("p1", "u1", "my view", serde_json::json!({"pageSize": 10}), true) .expect("valid"); let view = repo.insert_view(&nv).await.expect("insert view"); // Partial update: only name changes; config/shared keep their prior values. - let patch = ApiViewPatch::new(Some("改名视图"), None, None).expect("patch"); + let patch = ApiViewPatch::new(Some("rename view"), None, None).expect("patch"); let updated = repo.update_view(&view.id, "u1", &patch).await.expect("update").expect("owner match"); - assert_eq!(updated.name, "改名视图"); + assert_eq!(updated.name, "rename view"); assert_eq!(updated.config, serde_json::json!({"pageSize": 10})); assert!(updated.shared); // Full update of all fields. let patch = ApiViewPatch::new( - Some("再改名"), + Some("renamed again"), Some(serde_json::json!({"pageSize": 50})), Some(false), ) .expect("patch"); let updated = repo.update_view(&view.id, "u1", &patch).await.expect("update").expect("owner match"); - assert_eq!(updated.name, "再改名"); + assert_eq!(updated.name, "renamed again"); assert_eq!(updated.config, serde_json::json!({"pageSize": 50})); assert!(!updated.shared); // Neither a non-owner nor a nonexistent id matches. - let patch = ApiViewPatch::new(Some("越权"), None, None).expect("patch"); + let patch = ApiViewPatch::new(Some("unauthorized"), None, None).expect("patch"); assert!(repo.update_view(&view.id, "u2", &patch).await.expect("update").is_none()); assert!(repo.update_view("ghost", "u1", &patch).await.expect("update").is_none()); let views = repo.list_views("p1", "u1").await.expect("list"); assert_eq!(views.len(), 1); - assert_eq!(views[0].name, "再改名"); + assert_eq!(views[0].name, "renamed again"); } } diff --git a/crates/api-definition/src/application/add_api_case.rs b/crates/api-definition/src/application/add_api_case.rs index 5f440208..f69d5ba7 100644 --- a/crates/api-definition/src/application/add_api_case.rs +++ b/crates/api-definition/src/application/add_api_case.rs @@ -78,14 +78,14 @@ mod tests { async fn adds_case_and_inherits_project() { let repo = Arc::new(InMemoryApiDefinitionRepository::new()); let def = CreateApiDefinitionUseCase::new(repo.clone()) - .execute("p1", "登录", ApiProtocol::Http, "POST", "/login", "u1") + .execute("p1", "login", ApiProtocol::Http, "POST", "/login", "u1") .await .expect("ok"); let uc = AddApiCaseUseCase::new(repo); let c = uc .execute( &def.id, - "用例", + "case", "POST", "/login", None, @@ -106,7 +106,7 @@ mod tests { let err = uc .execute( "ghost", - "用例", + "case", "GET", "/x", None, @@ -130,7 +130,7 @@ mod tests { let err = uc .execute( &def.id, - "用例", + "case", "GET", "/x", None, diff --git a/crates/api-definition/src/application/add_api_mock.rs b/crates/api-definition/src/application/add_api_mock.rs index ef883ceb..18c25238 100644 --- a/crates/api-definition/src/application/add_api_mock.rs +++ b/crates/api-definition/src/application/add_api_mock.rs @@ -85,7 +85,7 @@ mod tests { let m = uc .execute( &def.id, - "挡板", + "stub", serde_json::json!({}), 200, None, @@ -106,7 +106,7 @@ mod tests { let err = uc .execute( "ghost", - "挡板", + "stub", serde_json::json!({}), 200, None, @@ -130,7 +130,7 @@ mod tests { let err = uc .execute( &def.id, - "挡板", + "stub", serde_json::json!({}), 700, None, diff --git a/crates/api-definition/src/application/create_api_case.rs b/crates/api-definition/src/application/create_api_case.rs index 263b57ff..89efd57c 100644 --- a/crates/api-definition/src/application/create_api_case.rs +++ b/crates/api-definition/src/application/create_api_case.rs @@ -55,7 +55,7 @@ mod tests { .execute( "p1", None, - "用例", + "case", "post", "/login", None, @@ -77,7 +77,7 @@ mod tests { .execute( "p1", Some("def-1"), - "用例", + "case", "GET", "/x", None, @@ -97,7 +97,7 @@ mod tests { .execute( "p1", None, - "用例", + "case", "GET", "/x", None, diff --git a/crates/api-definition/src/application/create_api_definition.rs b/crates/api-definition/src/application/create_api_definition.rs index ffbe12e0..67923a44 100644 --- a/crates/api-definition/src/application/create_api_definition.rs +++ b/crates/api-definition/src/application/create_api_definition.rs @@ -48,7 +48,7 @@ mod tests { let repo = Arc::new(InMemoryApiDefinitionRepository::new()); let uc = CreateApiDefinitionUseCase::new(repo); let d = - uc.execute("p1", "登录", ApiProtocol::Http, "get", "/login", "u1").await.expect("ok"); + uc.execute("p1", "login", ApiProtocol::Http, "get", "/login", "u1").await.expect("ok"); assert_eq!(d.method, "GET"); assert_eq!(d.protocol, ApiProtocol::Http); } diff --git a/crates/api-definition/src/application/import_api_definitions.rs b/crates/api-definition/src/application/import_api_definitions.rs index d912f1bb..ee6d3e08 100644 --- a/crates/api-definition/src/application/import_api_definitions.rs +++ b/crates/api-definition/src/application/import_api_definitions.rs @@ -99,7 +99,12 @@ impl ImportApiDefinitionsUseCase { if self.repo.update_definition_spec(id, &new_def.spec).await.is_ok() { let _ = self .repo - .record_definition_change(id, "UPDATE_SPEC", "OpenAPI 导入覆盖规格", "") + .record_definition_change( + id, + "UPDATE_SPEC", + "OpenAPI import overwrote spec", + "", + ) .await; if sync_module { let mid = self @@ -137,7 +142,7 @@ impl ImportApiDefinitionsUseCase { def.module_id = Some(mid.to_string()); } } - let case_name = format!("{} 默认用例", api.name); + let case_name = format!("{} default case", api.name); if let Ok(case) = NewApiCase::new( &def.id, project_id, @@ -196,7 +201,7 @@ mod tests { let doc = json!({ "openapi": "3.0.0", "paths": { - "/login": { "post": { "summary": "登录" } }, + "/login": { "post": { "summary": "login" } }, "/users": { "get": { "operationId": "listUsers" } } } }); @@ -221,9 +226,9 @@ mod tests { let doc = json!({ "openapi": "3.0.0", "paths": { - "/login": { "post": { "summary": "登录", "tags": ["auth"] } }, - "/logout": { "post": { "summary": "登出", "tags": ["auth"] } }, - "/users": { "get": { "summary": "列用户", "tags": ["user"] } }, + "/login": { "post": { "summary": "login", "tags": ["auth"] } }, + "/logout": { "post": { "summary": "logout", "tags": ["auth"] } }, + "/users": { "get": { "summary": "list users", "tags": ["user"] } }, "/ping": { "get": { "summary": "ping" } } } }); diff --git a/crates/api-definition/src/application/import_schedule.rs b/crates/api-definition/src/application/import_schedule.rs index 2f6a38ce..f25f4bf8 100644 --- a/crates/api-definition/src/application/import_schedule.rs +++ b/crates/api-definition/src/application/import_schedule.rs @@ -99,9 +99,9 @@ mod tests { assert_eq!(uc.list_enabled().await.unwrap().len(), 0); assert_eq!(uc.list_by_project("p1").await.unwrap().len(), 1); - uc.record_run(&s.id, "新增 3 / 覆盖 1 / 跳过 0", "alice").await.expect("record"); + uc.record_run(&s.id, "added 3 / overwritten 1 / skipped 0", "alice").await.expect("record"); let got = uc.get(&s.id).await.unwrap().unwrap(); - assert_eq!(got.last_result, "新增 3 / 覆盖 1 / 跳过 0"); + assert_eq!(got.last_result, "added 3 / overwritten 1 / skipped 0"); assert_eq!(got.last_run_by, "alice"); uc.delete(&s.id).await.expect("delete"); diff --git a/crates/api-definition/src/application/update_api_definition.rs b/crates/api-definition/src/application/update_api_definition.rs index 96dcb719..4a936970 100644 --- a/crates/api-definition/src/application/update_api_definition.rs +++ b/crates/api-definition/src/application/update_api_definition.rs @@ -49,7 +49,7 @@ mod tests { async fn seed(repo: Arc) -> String { let uc = CreateApiDefinitionUseCase::new(repo); - uc.execute("p1", "登录", ApiProtocol::Http, "get", "/login", "u1").await.expect("seed").id + uc.execute("p1", "login", ApiProtocol::Http, "get", "/login", "u1").await.expect("seed").id } #[tokio::test] @@ -57,9 +57,11 @@ mod tests { let repo = Arc::new(InMemoryApiDefinitionRepository::new()); let id = seed(repo.clone()).await; let uc = UpdateApiDefinitionUseCase::new(repo); - let d = - uc.execute(&id, " 登录v2 ", ApiProtocol::Http, "post", "/v2/login").await.expect("ok"); - assert_eq!(d.name, "登录v2"); + let d = uc + .execute(&id, " login v2 ", ApiProtocol::Http, "post", "/v2/login") + .await + .expect("ok"); + assert_eq!(d.name, "login v2"); assert_eq!(d.method, "POST"); assert_eq!(d.path, "/v2/login"); assert_eq!(d.project_id, "p1"); diff --git a/crates/api-definition/src/domain/api_case.rs b/crates/api-definition/src/domain/api_case.rs index 5fb16834..6dfb5d82 100644 --- a/crates/api-definition/src/domain/api_case.rs +++ b/crates/api-definition/src/domain/api_case.rs @@ -133,14 +133,14 @@ mod tests { let c = NewApiCase::new( "def1", "p1", - " 用例 ", + " case ", "post", " /login ", Some("{}".into()), serde_json::json!([{"type": "status", "value": 200}]), ) .expect("ok"); - assert_eq!(c.name, "用例"); + assert_eq!(c.name, "case"); assert_eq!(c.method, "POST"); assert_eq!(c.url, "/login"); } diff --git a/crates/api-definition/src/domain/api_definition.rs b/crates/api-definition/src/domain/api_definition.rs index 320f7412..3e54fa0d 100644 --- a/crates/api-definition/src/domain/api_definition.rs +++ b/crates/api-definition/src/domain/api_definition.rs @@ -178,8 +178,8 @@ mod tests { #[test] fn new_definition_uppercases_method_and_defaults_draft() { let d = - NewApiDefinition::new("p1", " 登录 ", ApiProtocol::Http, "get", "/login").expect("ok"); - assert_eq!(d.name, "登录"); + NewApiDefinition::new("p1", " login ", ApiProtocol::Http, "get", "/login").expect("ok"); + assert_eq!(d.name, "login"); assert_eq!(d.method, "GET"); assert_eq!(d.status, ApiStatus::Draft); } @@ -210,7 +210,7 @@ mod tests { #[test] fn non_http_protocol_ignores_method() { - let d = NewApiDefinition::new("p1", "查询", ApiProtocol::Sql, "", "SELECT 1").expect("ok"); + let d = NewApiDefinition::new("p1", "query", ApiProtocol::Sql, "", "SELECT 1").expect("ok"); assert_eq!(d.protocol, ApiProtocol::Sql); assert_eq!(d.method, ""); } diff --git a/crates/api-definition/src/domain/api_mock.rs b/crates/api-definition/src/domain/api_mock.rs index 16307de9..da8b8e85 100644 --- a/crates/api-definition/src/domain/api_mock.rs +++ b/crates/api-definition/src/domain/api_mock.rs @@ -95,14 +95,14 @@ mod tests { fn new_mock_ok() { let m = NewApiMock::new( "def1", - " 挡板 ", + " stub ", serde_json::json!({"path": "/x"}), 200, Some("{}".into()), true, ) .expect("ok"); - assert_eq!(m.name, "挡板"); + assert_eq!(m.name, "stub"); assert_eq!(m.response_status, 200); assert!(m.enabled); } diff --git a/crates/api-definition/src/domain/api_module.rs b/crates/api-definition/src/domain/api_module.rs index da09e22f..c4743454 100644 --- a/crates/api-definition/src/domain/api_module.rs +++ b/crates/api-definition/src/domain/api_module.rs @@ -42,10 +42,10 @@ mod tests { #[test] fn new_module_trims_and_normalizes_parent() { - let m = NewApiModule::new("p1", Some(""), " 登录域 ").expect("ok"); - assert_eq!(m.name, "登录域"); + let m = NewApiModule::new("p1", Some(""), " login module ").expect("ok"); + assert_eq!(m.name, "login module"); assert_eq!(m.parent_id, None); - let m2 = NewApiModule::new("p1", Some("mod-1"), "子模块").expect("ok"); + let m2 = NewApiModule::new("p1", Some("mod-1"), "child module").expect("ok"); assert_eq!(m2.parent_id.as_deref(), Some("mod-1")); } diff --git a/crates/api-definition/src/domain/api_view.rs b/crates/api-definition/src/domain/api_view.rs index 4f4720eb..86073fee 100644 --- a/crates/api-definition/src/domain/api_view.rs +++ b/crates/api-definition/src/domain/api_view.rs @@ -85,8 +85,8 @@ mod tests { #[test] fn new_view_ok_and_defaults_config() { let v = - NewApiView::new("p1", "u1", " 我的视图 ", serde_json::json!("bad"), true).expect("ok"); - assert_eq!(v.name, "我的视图"); + NewApiView::new("p1", "u1", " my view ", serde_json::json!("bad"), true).expect("ok"); + assert_eq!(v.name, "my view"); assert_eq!(v.config, serde_json::json!({})); assert!(v.shared); } @@ -99,8 +99,8 @@ mod tests { #[test] fn patch_trims_name_and_keeps_absent_fields() { - let p = ApiViewPatch::new(Some(" 新名字 "), None, Some(false)).expect("ok"); - assert_eq!(p.name.as_deref(), Some("新名字")); + let p = ApiViewPatch::new(Some(" new name "), None, Some(false)).expect("ok"); + assert_eq!(p.name.as_deref(), Some("new name")); assert_eq!(p.config, None); assert_eq!(p.shared, Some(false)); let empty = ApiViewPatch::new(None, None, None).expect("ok"); diff --git a/crates/api-definition/src/domain/import.rs b/crates/api-definition/src/domain/import.rs index 4241d087..e8d5c8d7 100644 --- a/crates/api-definition/src/domain/import.rs +++ b/crates/api-definition/src/domain/import.rs @@ -56,7 +56,7 @@ pub fn parse_import( ImportFormat::Har => super::import_har::parse_har(doc), ImportFormat::Jmeter => { let xml = doc.as_str().ok_or_else(|| { - ApiDefinitionError::BadImport("jmeter 导入需原始 .jmx 文本".into()) + ApiDefinitionError::BadImport("jmeter import requires raw .jmx text".into()) })?; super::import_jmeter::parse_jmeter(xml) } @@ -471,7 +471,7 @@ mod tests { let doc = json!({ "openapi": "3.0.0", "paths": { - "/login": { "post": { "summary": "登录" } }, + "/login": { "post": { "summary": "login" } }, "/users": { "get": { "operationId": "listUsers" }, "post": {} @@ -482,7 +482,7 @@ mod tests { assert_eq!(apis.len(), 3); assert_eq!( (apis[0].name.as_str(), apis[0].method.as_str(), apis[0].path.as_str()), - ("登录", "POST", "/login") + ("login", "POST", "/login") ); assert_eq!(apis[1].name, "listUsers"); assert_eq!(apis[1].method, "GET"); @@ -539,7 +539,7 @@ mod tests { fn parameters_become_spec_kv_with_required_in_desc() { let doc = json!({ "paths": { "/u": { "get": { "summary": "u", "parameters": [ - { "name": "projectId", "in": "query", "required": true, "description": "项目", "schema": {"type": "string"} }, + { "name": "projectId", "in": "query", "required": true, "description": "project", "schema": {"type": "string"} }, { "name": "X-Token", "in": "header", "required": false, "schema": {"type": "string"} }, { "name": "id", "in": "path", "required": true, "schema": {"type": "integer"} } ] } } } @@ -548,7 +548,7 @@ mod tests { let q = api.spec["requestQuery"].as_array().unwrap(); assert_eq!(q[0]["name"], "projectId"); assert!(q[0]["desc"].as_str().unwrap().contains("必填")); - assert!(q[0]["desc"].as_str().unwrap().contains("项目")); + assert!(q[0]["desc"].as_str().unwrap().contains("project")); let h = api.spec["requestHeaders"].as_array().unwrap(); assert!(h[0]["desc"].as_str().unwrap().contains("选填")); let r = api.spec["restParams"].as_array().unwrap(); @@ -562,7 +562,7 @@ mod tests { "type": "object", "required": ["username"], "properties": { "username": {"type": "string"}, "remember": {"type": "boolean"} } } } }, - "paths": { "/login": { "post": { "summary": "登录", + "paths": { "/login": { "post": { "summary": "login", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LoginBody" } } } }, "responses": { "200": { "content": { "application/json": { "schema": { "type": "object", "required": ["token"], "properties": { "token": {"type": "string"} } diff --git a/crates/api-definition/src/domain/import_har.rs b/crates/api-definition/src/domain/import_har.rs index 9e33bc6a..33e6ff09 100644 --- a/crates/api-definition/src/domain/import_har.rs +++ b/crates/api-definition/src/domain/import_har.rs @@ -8,11 +8,10 @@ use crate::domain::import::{ }; pub fn parse_har(doc: &Value) -> Result, ApiDefinitionError> { - let entries = doc - .get("log") - .and_then(|l| l.get("entries")) - .and_then(|v| v.as_array()) - .ok_or_else(|| ApiDefinitionError::BadImport("har: 缺少 `log.entries` 数组".into()))?; + let entries = + doc.get("log").and_then(|l| l.get("entries")).and_then(|v| v.as_array()).ok_or_else( + || ApiDefinitionError::BadImport("har: missing `log.entries` array".into()), + )?; let mut out = Vec::new(); let mut seen: HashSet<(String, String)> = HashSet::new(); @@ -23,7 +22,7 @@ pub fn parse_har(doc: &Value) -> Result, ApiDefinitionError> { } } if out.is_empty() { - return Err(ApiDefinitionError::BadImport("har: 未找到任何请求".into())); + return Err(ApiDefinitionError::BadImport("har: no requests found".into())); } Ok(out) } diff --git a/crates/api-definition/src/domain/import_jmeter.rs b/crates/api-definition/src/domain/import_jmeter.rs index 00e523cc..d7851f56 100644 --- a/crates/api-definition/src/domain/import_jmeter.rs +++ b/crates/api-definition/src/domain/import_jmeter.rs @@ -26,7 +26,7 @@ pub fn parse_jmeter(xml: &str) -> Result, ApiDefinitionError> { loop { match reader.read_event_into(&mut buf) { Err(e) => { - return Err(ApiDefinitionError::BadImport(format!("jmeter: XML 解析失败:{e}"))) + return Err(ApiDefinitionError::BadImport(format!("jmeter: XML parse failed: {e}"))) } Ok(Event::Eof) => break, Ok(Event::Start(e)) => match e.local_name().as_ref() { @@ -109,7 +109,7 @@ pub fn parse_jmeter(xml: &str) -> Result, ApiDefinitionError> { } if out.is_empty() { - return Err(ApiDefinitionError::BadImport("jmeter: 未找到 HTTP 取样器".into())); + return Err(ApiDefinitionError::BadImport("jmeter: no HTTP sampler found".into())); } Ok(out) } @@ -265,7 +265,7 @@ mod tests { const JMX: &str = r#" - + @@ -281,7 +281,7 @@ mod tests { POST true - + @@ -303,7 +303,7 @@ mod tests { let login = apis.iter().find(|a| a.path == "/login").expect("login"); assert_eq!(login.method, "POST"); - assert_eq!(login.name, "登录"); + assert_eq!(login.name, "login"); assert_eq!(login.spec["bodyType"], "json"); assert!(login.case_body.as_deref().unwrap().contains("admin")); assert_eq!(login.spec["requestQuery"].as_array().unwrap().len(), 0); diff --git a/crates/api-definition/src/domain/import_metersphere.rs b/crates/api-definition/src/domain/import_metersphere.rs index 16e6c69e..c8433a49 100644 --- a/crates/api-definition/src/domain/import_metersphere.rs +++ b/crates/api-definition/src/domain/import_metersphere.rs @@ -6,8 +6,9 @@ use crate::domain::import::{ }; pub fn parse_metersphere(doc: &Value) -> Result, ApiDefinitionError> { - let list = locate_list(doc) - .ok_or_else(|| ApiDefinitionError::BadImport("metersphere: 未找到接口列表".into()))?; + let list = locate_list(doc).ok_or_else(|| { + ApiDefinitionError::BadImport("metersphere: interface list not found".into()) + })?; let mut out = Vec::new(); for item in list { @@ -16,7 +17,7 @@ pub fn parse_metersphere(doc: &Value) -> Result, ApiDefinitionE } } if out.is_empty() { - return Err(ApiDefinitionError::BadImport("metersphere: 未找到任何有效接口".into())); + return Err(ApiDefinitionError::BadImport("metersphere: no valid interface found".into())); } Ok(out) } @@ -172,17 +173,17 @@ mod tests { "projectName": "demo", "data": [ { - "name": "登录", + "name": "login", "method": "post", "path": "/login", - "modulePath": "/认证/登录态", + "modulePath": "/auth/login-state", "request": "{\"headers\":[{\"name\":\"X-Token\",\"value\":\"t\",\"enable\":true}],\"arguments\":[{\"name\":\"from\",\"value\":\"web\"}],\"body\":{\"raw\":\"{\\\"u\\\":\\\"a\\\"}\"}}" }, { - "name": "列用户", + "name": "list users", "method": "GET", "path": "/users?page=1", - "moduleName": "用户" + "moduleName": "users" } ] }); @@ -190,14 +191,14 @@ mod tests { assert_eq!(apis.len(), 2); let login = apis.iter().find(|a| a.path == "/login").expect("login"); assert_eq!(login.method, "POST"); - assert_eq!(login.module.as_deref(), Some("认证")); + assert_eq!(login.module.as_deref(), Some("auth")); assert_eq!(login.spec["requestHeaders"][0]["name"], "X-Token"); assert_eq!(login.spec["requestQuery"][0]["name"], "from"); assert_eq!(login.spec["bodyType"], "json"); assert!(login.case_body.as_deref().unwrap().contains("\"u\"")); let users = apis.iter().find(|a| a.path == "/users").expect("users"); - assert_eq!(users.module.as_deref(), Some("用户")); + assert_eq!(users.module.as_deref(), Some("users")); assert_eq!(users.spec["requestQuery"][0]["name"], "page"); } diff --git a/crates/api-definition/src/domain/import_postman.rs b/crates/api-definition/src/domain/import_postman.rs index 0cf10c66..c76bfcd0 100644 --- a/crates/api-definition/src/domain/import_postman.rs +++ b/crates/api-definition/src/domain/import_postman.rs @@ -9,14 +9,14 @@ pub fn parse_postman(doc: &Value) -> Result, ApiDefinitionError let items = doc .get("item") .and_then(|v| v.as_array()) - .ok_or_else(|| ApiDefinitionError::BadImport("postman: 缺少 `item` 数组".into()))?; + .ok_or_else(|| ApiDefinitionError::BadImport("postman: missing `item` array".into()))?; let mut out = Vec::new(); for it in items { walk(it, None, &mut out); } if out.is_empty() { - return Err(ApiDefinitionError::BadImport("postman: 未找到任何请求".into())); + return Err(ApiDefinitionError::BadImport("postman: no requests found".into())); } Ok(out) } @@ -177,7 +177,7 @@ mod tests { "info": { "name": "demo" }, "item": [ { "name": "auth", "item": [ - { "name": "登录", "request": { + { "name": "login", "request": { "method": "post", "header": [{ "key": "Content-Type", "value": "application/json" }], "url": { "raw": "{{base}}/login?from=web", "path": ["login"], "query": [{ "key": "from", "value": "web" }] }, diff --git a/crates/api-definition/src/domain/import_schedule.rs b/crates/api-definition/src/domain/import_schedule.rs index 05c6770c..199baa4b 100644 --- a/crates/api-definition/src/domain/import_schedule.rs +++ b/crates/api-definition/src/domain/import_schedule.rs @@ -79,7 +79,7 @@ mod tests { fn base() -> NewImportSchedule { NewImportSchedule { project_id: " p1 ".into(), - name: " 每日同步 ".into(), + name: " daily sync ".into(), format: "Postman".into(), source_url: " https://h/c.json ".into(), auth_token: String::new(), @@ -102,7 +102,7 @@ mod tests { assert_eq!(s.cron, "0 0 2 * * *"); assert_eq!(s.format, "postman"); assert_eq!(s.module_id, None); - assert_eq!(s.name, "每日同步"); + assert_eq!(s.name, "daily sync"); } #[test] diff --git a/crates/api-definition/tests/architecture.rs b/crates/api-definition/tests/architecture.rs index 782cc5fc..2ea8b518 100644 --- a/crates/api-definition/tests/architecture.rs +++ b/crates/api-definition/tests/architecture.rs @@ -21,7 +21,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/api-runner/src/domain/runner.rs b/crates/api-runner/src/domain/runner.rs index 19bc7d90..7d950133 100644 --- a/crates/api-runner/src/domain/runner.rs +++ b/crates/api-runner/src/domain/runner.rs @@ -183,11 +183,10 @@ impl Assertion { vars: &std::collections::BTreeMap, ) -> Option { match self { - Assertion::StatusIs(want) => { - (resp.status != *want).then(|| format!("status: 期望 {want},实际 {}", resp.status)) - } + Assertion::StatusIs(want) => (resp.status != *want) + .then(|| format!("status: expected {want}, actual {}", resp.status)), Assertion::BodyContains(needle) => { - (!resp.body.contains(needle)).then(|| format!("body 不含子串: {needle}")) + (!resp.body.contains(needle)).then(|| format!("body missing substring: {needle}")) } Assertion::HeaderEquals { name, value } => { let got = resp @@ -196,10 +195,10 @@ impl Assertion { .find(|(k, _)| k.eq_ignore_ascii_case(name)) .map(|(_, v)| v.as_str()); (got != Some(value.as_str())) - .then(|| format!("header {name}: 期望 {value},实际 {got:?}")) + .then(|| format!("header {name}: expected {value}, actual {got:?}")) } Assertion::JsonFieldEquals { pointer, expected } => match json { - None => Some("body 不是合法 JSON".to_string()), + None => Some("body is not valid JSON".to_string()), Some(v) => { let got = v.pointer(pointer); let matches = match got { @@ -209,13 +208,14 @@ impl Assertion { .unwrap_or(false), None => false, }; - (!matches).then(|| format!("json {pointer}: 期望 {expected},实际 {got:?}")) + (!matches) + .then(|| format!("json {pointer}: expected {expected}, actual {got:?}")) } }, Assertion::ResponseCode { condition, expected } => { let actual = resp.status.to_string(); (!condition.matches(&actual, expected)) - .then(|| format!("status {condition:?}: 期望 {expected},实际 {actual}")) + .then(|| format!("status {condition:?}: expected {expected}, actual {actual}")) } Assertion::ResponseHeader { name, condition, expected } => { let got = resp @@ -224,33 +224,37 @@ impl Assertion { .find(|(k, _)| k.eq_ignore_ascii_case(name)) .map(|(_, v)| v.as_str()) .unwrap_or(""); - (!condition.matches(got, expected)) - .then(|| format!("header {name} {condition:?}: 期望 {expected},实际 {got:?}")) + (!condition.matches(got, expected)).then(|| { + format!("header {name} {condition:?}: expected {expected}, actual {got:?}") + }) } Assertion::ResponseBody { condition, expected } => (!condition .matches(&resp.body, expected)) - .then(|| format!("body {condition:?}: 期望 {expected}")), + .then(|| format!("body {condition:?}: expected {expected}")), Assertion::JsonPath { path, condition, expected } => match json { - None => Some("body 不是合法 JSON".to_string()), + None => Some("body is not valid JSON".to_string()), Some(v) => { let pointer = json_path_to_pointer(path); match v.pointer(&pointer) { - None => Some(format!("json {path}: 路径不存在")), + None => Some(format!("json {path}: path not found")), Some(found) => { let actual = json_value_to_string(found); (!condition.matches(&actual, expected)).then(|| { - format!("json {path} {condition:?}: 期望 {expected},实际 {actual}") + format!( + "json {path} {condition:?}: expected {expected}, actual {actual}" + ) }) } } } }, Assertion::ResponseTime { max_ms } => (resp.elapsed_ms > *max_ms) - .then(|| format!("耗时: 期望 ≤{max_ms}ms,实际 {}ms", resp.elapsed_ms)), + .then(|| format!("elapsed: expected ≤{max_ms}ms, actual {}ms", resp.elapsed_ms)), Assertion::Variable { name, condition, expected } => { let actual = vars.get(name).map(String::as_str).unwrap_or(""); - (!condition.matches(actual, expected)) - .then(|| format!("变量 {name} {condition:?}: 期望 {expected},实际 {actual:?}")) + (!condition.matches(actual, expected)).then(|| { + format!("variable {name} {condition:?}: expected {expected}, actual {actual:?}") + }) } } } @@ -322,26 +326,26 @@ pub struct AssertionReport { fn cond_label(c: &MatchCondition) -> String { use MatchCondition::*; match c { - Equals => "等于", - NotEquals => "不等于", - Contains => "包含", - NotContains => "不包含", - StartWith => "开头为", - EndWith => "结尾为", - Empty => "为空", - NotEmpty => "非空", - Regex => "正则", - Gt => "大于", + Equals => "equals", + NotEquals => "not equals", + Contains => "contains", + NotContains => "not contains", + StartWith => "starts with", + EndWith => "ends with", + Empty => "is empty", + NotEmpty => "not empty", + Regex => "regex", + Gt => "greater than", GtOrEquals => "≥", - Lt => "小于", + Lt => "less than", LtOrEquals => "≤", - LengthEquals => "长度=", - LengthNotEquals => "长度≠", - LengthGt => "长度>", - LengthGtOrEquals => "长度≥", - LengthLt => "长度<", - LengthLtOrEquals => "长度≤", - Unchecked => "不校验", + LengthEquals => "length =", + LengthNotEquals => "length ≠", + LengthGt => "length >", + LengthGtOrEquals => "length ≥", + LengthLt => "length <", + LengthLtOrEquals => "length ≤", + Unchecked => "unchecked", } .to_string() } @@ -389,37 +393,43 @@ pub fn evaluate_detailed_with_vars( let passed = reason.is_none(); let (item, condition, expected, actual) = match a { Assertion::StatusIs(n) => ( - "状态码".to_string(), - "等于".to_string(), + "status code".to_string(), + "equals".to_string(), n.to_string(), resp.status.to_string(), ), - Assertion::BodyContains(s) => { - ("响应体".to_string(), "包含".to_string(), s.clone(), truncate(&resp.body, 60)) - } - Assertion::HeaderEquals { name, value } => { - (format!("响应头[{name}]"), "等于".to_string(), value.clone(), header(name)) - } + Assertion::BodyContains(s) => ( + "response body".to_string(), + "contains".to_string(), + s.clone(), + truncate(&resp.body, 60), + ), + Assertion::HeaderEquals { name, value } => ( + format!("response header[{name}]"), + "equals".to_string(), + value.clone(), + header(name), + ), Assertion::JsonFieldEquals { pointer, expected } => ( format!("JSON {pointer}"), - "等于".to_string(), + "equals".to_string(), expected.clone(), json_at(pointer), ), Assertion::ResponseCode { condition, expected } => ( - "状态码".to_string(), + "status code".to_string(), cond_label(condition), expected.clone(), resp.status.to_string(), ), Assertion::ResponseHeader { name, condition, expected } => ( - format!("响应头[{name}]"), + format!("response header[{name}]"), cond_label(condition), expected.clone(), header(name), ), Assertion::ResponseBody { condition, expected } => ( - "响应体".to_string(), + "response body".to_string(), cond_label(condition), expected.clone(), truncate(&resp.body, 60), @@ -431,13 +441,13 @@ pub fn evaluate_detailed_with_vars( json_at(&json_path_to_pointer(path)), ), Assertion::ResponseTime { max_ms } => ( - "响应耗时(ms)".to_string(), + "response time (ms)".to_string(), "≤".to_string(), max_ms.to_string(), resp.elapsed_ms.to_string(), ), Assertion::Variable { name, condition, expected } => ( - format!("变量[{name}]"), + format!("variable[{name}]"), cond_label(condition), expected.clone(), vars.get(name).cloned().unwrap_or_default(), @@ -608,14 +618,14 @@ mod tests { ]; let reports = evaluate_detailed(&a, &r); assert_eq!(reports.len(), 3); - assert_eq!(reports[0].item, "状态码"); - assert_eq!(reports[0].condition, "等于"); + assert_eq!(reports[0].item, "status code"); + assert_eq!(reports[0].condition, "equals"); assert_eq!(reports[0].expected, "200"); assert_eq!(reports[0].actual, "200"); assert!(reports[0].passed); assert!(!reports[1].passed); - assert!(reports[1].reason.contains("期望 500")); - assert_eq!(reports[2].item, "响应体"); + assert!(reports[1].reason.contains("expected 500")); + assert_eq!(reports[2].item, "response body"); assert!(reports[2].passed); } diff --git a/crates/api-runner/tests/architecture.rs b/crates/api-runner/tests/architecture.rs index 96e1b43b..0410bdcc 100644 --- a/crates/api-runner/tests/architecture.rs +++ b/crates/api-runner/tests/architecture.rs @@ -24,7 +24,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/api-scenario/src/adapters/http.rs b/crates/api-scenario/src/adapters/http.rs index 569a972b..4858cf5e 100644 --- a/crates/api-scenario/src/adapters/http.rs +++ b/crates/api-scenario/src/adapters/http.rs @@ -416,7 +416,12 @@ async fn copy_scenario( Ok(s) => { let _ = st .repo - .record_change(&s.id, "CREATE", Some(&format!("复制自 {id}")), Some(&user.user_id)) + .record_change( + &s.id, + "CREATE", + Some(&format!("copied from {id}")), + Some(&user.user_id), + ) .await; (StatusCode::CREATED, Json(ScenarioResponse::from(s))).into_response() } @@ -523,7 +528,12 @@ async fn update_scenario( Ok(Some(s)) => { let _ = st .repo - .record_change(&id, "UPDATE", Some("更新基本信息/参数/设置"), Some(&user.user_id)) + .record_change( + &id, + "UPDATE", + Some("updated basic info / params / settings"), + Some(&user.user_id), + ) .await; (StatusCode::OK, Json(ScenarioResponse::from(s))).into_response() } @@ -552,7 +562,7 @@ async fn reorder_steps( Ok(()) => { let _ = st .repo - .record_change(&id, "REORDER", Some("调整步骤顺序"), Some(&user.user_id)) + .record_change(&id, "REORDER", Some("reordered steps"), Some(&user.user_id)) .await; StatusCode::NO_CONTENT.into_response() } @@ -573,7 +583,7 @@ async fn delete_step( Ok(true) => { let _ = st .repo - .record_change(&id, "DELETE_STEP", Some("删除步骤"), Some(&user.user_id)) + .record_change(&id, "DELETE_STEP", Some("deleted step"), Some(&user.user_id)) .await; StatusCode::NO_CONTENT.into_response() } @@ -593,8 +603,10 @@ async fn delete_scenario( } match st.repo.delete_scenario(&id).await { Ok(true) => { - let _ = - st.repo.record_change(&id, "DELETE", Some("删除场景"), Some(&user.user_id)).await; + let _ = st + .repo + .record_change(&id, "DELETE", Some("deleted scenario"), Some(&user.user_id)) + .await; StatusCode::NO_CONTENT.into_response() } Ok(false) => (StatusCode::NOT_FOUND, "scenario not found").into_response(), @@ -671,7 +683,7 @@ async fn add_step( .record_change( &id, "ADD_STEP", - Some(&format!("新增步骤 {}", req.kind)), + Some(&format!("added step {}", req.kind)), Some(&user.user_id), ) .await; @@ -716,7 +728,7 @@ async fn update_step( .record_change( &id, "UPDATE_STEP", - Some(&format!("更新步骤 {}", req.kind)), + Some(&format!("updated step {}", req.kind)), Some(&user.user_id), ) .await; @@ -893,7 +905,7 @@ async fn purge_scenario( ScenarioExecutionDto, ScenarioExecutionPageResponse )), - tags((name = "api-scenario", description = "接口场景编排")) + tags((name = "api-scenario", description = "API scenario orchestration")) )] struct ApiDoc; pub fn openapi() -> utoipa::openapi::OpenApi { @@ -950,7 +962,11 @@ mod tests { async fn create_scenario_id(app: &Router, t: &str) -> String { let resp = app .clone() - .oneshot(post("/api/scenario", r#"{"projectId":"p1","name":"下单"}"#, Some(t))) + .oneshot(post( + "/api/scenario", + r#"{"projectId":"p1","name":"order placement"}"#, + Some(t), + )) .await .expect("resp"); assert_eq!(resp.status(), StatusCode::CREATED); @@ -1220,7 +1236,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::CREATED); let v = json_body(resp).await; assert_ne!(v["id"].as_str().expect("id"), id); - assert_eq!(v["name"], "下单_copy"); + assert_eq!(v["name"], "order placement_copy"); assert_eq!(v["steps"][0]["kind"], "CASE"); assert_eq!(v["steps"][0]["caseId"], "case-7"); } diff --git a/crates/api-scenario/src/adapters/pg.rs b/crates/api-scenario/src/adapters/pg.rs index facc7238..c1a0da7e 100644 --- a/crates/api-scenario/src/adapters/pg.rs +++ b/crates/api-scenario/src/adapters/pg.rs @@ -496,7 +496,7 @@ mod tests { use super::*; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_scenario_steps_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -509,7 +509,7 @@ mod tests { let repo = PgApiScenarioRepository::new(pool.clone()); let scenario = repo - .insert_scenario(&NewApiScenario::new("p1", "下单链路").expect("valid")) + .insert_scenario(&NewApiScenario::new("p1", "order placement flow").expect("valid")) .await .expect("insert"); assert_eq!(scenario.status, ScenarioStatus::Draft); @@ -566,7 +566,7 @@ mod tests { } #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_execution_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); diff --git a/crates/api-scenario/src/application/copy_scenario.rs b/crates/api-scenario/src/application/copy_scenario.rs index 4c3530ee..e0159e17 100644 --- a/crates/api-scenario/src/application/copy_scenario.rs +++ b/crates/api-scenario/src/application/copy_scenario.rs @@ -70,12 +70,12 @@ mod tests { async fn seed(repo: Arc) -> String { let create = CreateScenarioUseCase::new(repo.clone()); - let s = create.execute("p1", "下单链路", Some("alice")).await.expect("create"); + let s = create.execute("p1", "order placement flow", Some("alice")).await.expect("create"); repo.update_scenario( &s.id, - "下单链路", + "order placement flow", "DEBUGGING", - &serde_json::json!({ "priority": "P1", "tags": ["核心"] }), + &serde_json::json!({ "priority": "P1", "tags": ["core"] }), ) .await .expect("meta"); @@ -105,7 +105,7 @@ mod tests { let copy = uc.execute(&src, None, Some("bob")).await.expect("copy"); assert_ne!(copy.id, src); - assert_eq!(copy.name, "下单链路_copy"); + assert_eq!(copy.name, "order placement flow_copy"); assert_eq!(copy.project_id, "p1"); assert_eq!(copy.status.as_str(), "DEBUGGING"); assert_eq!(copy.meta["priority"], "P1"); @@ -123,8 +123,8 @@ mod tests { let repo: Arc = Arc::new(InMemoryApiScenarioRepository::new()); let src = seed(repo.clone()).await; let uc = CopyScenarioUseCase::new(repo); - let copy = uc.execute(&src, Some(" 回归_下单 "), None).await.expect("copy"); - assert_eq!(copy.name, "回归_下单"); + let copy = uc.execute(&src, Some(" regression_order "), None).await.expect("copy"); + assert_eq!(copy.name, "regression_order"); } #[tokio::test] diff --git a/crates/api-scenario/src/application/create_scenario.rs b/crates/api-scenario/src/application/create_scenario.rs index f5a79bc7..529dd120 100644 --- a/crates/api-scenario/src/application/create_scenario.rs +++ b/crates/api-scenario/src/application/create_scenario.rs @@ -43,8 +43,8 @@ mod tests { async fn creates_scenario_with_empty_steps() { let repo = InMemoryApiScenarioRepository::new(); let uc = CreateScenarioUseCase::new(Arc::new(repo)); - let s = uc.execute("p1", "下单链路", None).await.expect("ok"); - assert_eq!(s.name, "下单链路"); + let s = uc.execute("p1", "order placement flow", None).await.expect("ok"); + assert_eq!(s.name, "order placement flow"); assert_eq!(s.project_id, "p1"); assert!(s.steps.is_empty()); } diff --git a/crates/api-scenario/tests/architecture.rs b/crates/api-scenario/tests/architecture.rs index 782cc5fc..2ea8b518 100644 --- a/crates/api-scenario/tests/architecture.rs +++ b/crates/api-scenario/tests/architecture.rs @@ -21,7 +21,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/api-test/src/adapters/http.rs b/crates/api-test/src/adapters/http.rs index bc9d0406..037ce095 100644 --- a/crates/api-test/src/adapters/http.rs +++ b/crates/api-test/src/adapters/http.rs @@ -219,7 +219,7 @@ fn default_page_size() -> u32 { get, path = "/api/case/{caseId}/executions", tag = "api-test", - params(("caseId" = String, Path, description = "用例 id"), CaseExecutionQuery), + params(("caseId" = String, Path, description = "Case id"), CaseExecutionQuery), responses((status = 200, body = CaseExecutionPageResponse), (status = 400), (status = 403)), security(("bearer" = [])) )] @@ -476,7 +476,7 @@ async fn delete_resource_pool( ResourcePoolBody, ResourcePoolResponse )), - tags((name = "api-test", description = "接口批量执行")) + tags((name = "api-test", description = "API batch execution")) )] struct ApiDoc; pub fn openapi() -> utoipa::openapi::OpenApi { @@ -795,11 +795,12 @@ mod tests { #[tokio::test] async fn create_pool_with_permission_201() { let (app, t) = pool_app().await; - let resp = app.oneshot(pool_post(r#"{"name":"本地池"}"#, Some(&t))).await.expect("resp"); + let resp = + app.oneshot(pool_post(r#"{"name":"local pool"}"#, Some(&t))).await.expect("resp"); assert_eq!(resp.status(), StatusCode::CREATED); let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX).await.expect("body"); let v: serde_json::Value = serde_json::from_slice(&bytes).expect("json"); - assert_eq!(v["name"], "本地池"); + assert_eq!(v["name"], "local pool"); assert_eq!(v["enabled"], true); assert!(v["id"].as_str().expect("id").starts_with('p')); } @@ -868,7 +869,7 @@ mod tests { #[tokio::test] async fn create_carries_extended_fields() { let (app, t) = pool_app().await; - let body = r#"{"name":"k8s池","poolType":"Kubernetes","maxConcurrency":10,"description":"d","allOrg":false,"orgIds":["org-1"],"serverUrl":"http://ms","config":{"namespace":"ns"}}"#; + let body = r#"{"name":"k8s pool","poolType":"Kubernetes","maxConcurrency":10,"description":"d","allOrg":false,"orgIds":["org-1"],"serverUrl":"http://ms","config":{"namespace":"ns"}}"#; let resp = app.oneshot(pool_post(body, Some(&t))).await.expect("resp"); assert_eq!(resp.status(), StatusCode::CREATED); let v = json_of(resp).await; @@ -884,7 +885,7 @@ mod tests { async fn update_then_delete_roundtrip() { let (app, t) = pool_app().await; let created = json_of( - app.clone().oneshot(pool_post(r#"{"name":"池"}"#, Some(&t))).await.expect("resp"), + app.clone().oneshot(pool_post(r#"{"name":"pool"}"#, Some(&t))).await.expect("resp"), ) .await; let id = created["id"].as_str().expect("id").to_string(); @@ -894,12 +895,12 @@ mod tests { .uri(format!("/api/resource-pool/{id}")) .header("content-type", "application/json") .header("authorization", format!("Bearer {t}")) - .body(Body::from(r#"{"name":"改名","enabled":false}"#)) + .body(Body::from(r#"{"name":"rename","enabled":false}"#)) .expect("req"); let resp = app.clone().oneshot(put).await.expect("resp"); assert_eq!(resp.status(), StatusCode::OK); let v = json_of(resp).await; - assert_eq!(v["name"], "改名"); + assert_eq!(v["name"], "rename"); assert_eq!(v["enabled"], false); let del = Request::builder() diff --git a/crates/api-test/src/adapters/pg.rs b/crates/api-test/src/adapters/pg.rs index a7cf79b7..134b0a88 100644 --- a/crates/api-test/src/adapters/pg.rs +++ b/crates/api-test/src/adapters/pg.rs @@ -789,7 +789,7 @@ mod tests { } #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_pool_resolution_and_dispatch() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -800,7 +800,7 @@ mod tests { .expect("truncate"); sqlx::raw_sql( "INSERT INTO ms_resource_pool (id,name,enabled,deleted) VALUES \ - ('pool1','可用',true,false),('pool2','禁用',false,false); \ + ('pool1','available',true,false),('pool2','disabled',false,false); \ INSERT INTO ms_project_api_config (project_id, default_pool_id) VALUES \ ('proj1','pool1'),('proj2',NULL);", ) @@ -854,7 +854,7 @@ mod tests { } #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_case_execution_query_pagination() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); diff --git a/crates/api-test/src/adapters/report_archive.rs b/crates/api-test/src/adapters/report_archive.rs index 844664a9..7c427de0 100644 --- a/crates/api-test/src/adapters/report_archive.rs +++ b/crates/api-test/src/adapters/report_archive.rs @@ -162,7 +162,7 @@ mod tests { resp_size: Some(34), body: Some("ok".to_string()), headers: vec![], - assertions: serde_json::json!([{"item": "状态码", "passed": true}]), + assertions: serde_json::json!([{"item": "status code", "passed": true}]), extractions: serde_json::json!([["token", "abc"]]), req_method: Some("GET".to_string()), req_url: Some("http://x/healthz".to_string()), diff --git a/crates/api-test/src/application/manage_resource_pool.rs b/crates/api-test/src/application/manage_resource_pool.rs index b03451e8..9a6ac28e 100644 --- a/crates/api-test/src/application/manage_resource_pool.rs +++ b/crates/api-test/src/application/manage_resource_pool.rs @@ -160,12 +160,12 @@ mod tests { let create = CreateResourcePoolUseCase::new(admin.clone()); let list = ListResourcePoolsUseCase::new(admin); - let p = create.execute(draft(" 本地池 ")).await.expect("created"); + let p = create.execute(draft(" local pool ")).await.expect("created"); assert_eq!(p.id, "p1"); - assert_eq!(p.name, "本地池"); + assert_eq!(p.name, "local pool"); assert!(p.enabled); - create.execute(draft("远端池")).await.expect("created"); + create.execute(draft("remote pool")).await.expect("created"); let all = list.execute().await.expect("listed"); assert_eq!(all.len(), 2); } @@ -185,13 +185,13 @@ mod tests { let create = CreateResourcePoolUseCase::new(admin.clone()); let edit = EditResourcePoolUseCase::new(admin.clone()); - let p = create.execute(draft("池")).await.expect("created"); + let p = create.execute(draft("pool")).await.expect("created"); let updated = edit - .update(&p.id, ResourcePoolDraft { enabled: false, ..draft("池改名") }) + .update(&p.id, ResourcePoolDraft { enabled: false, ..draft("pool renamed") }) .await .expect("ok") .expect("found"); - assert_eq!(updated.name, "池改名"); + assert_eq!(updated.name, "pool renamed"); assert!(!updated.enabled); assert!(edit.update("nope", draft("x")).await.expect("ok").is_none()); diff --git a/crates/api-test/src/domain/resource_pool.rs b/crates/api-test/src/domain/resource_pool.rs index 69dc41c1..b724a650 100644 --- a/crates/api-test/src/domain/resource_pool.rs +++ b/crates/api-test/src/domain/resource_pool.rs @@ -107,8 +107,8 @@ mod tests { #[test] fn trims_and_accepts_name_with_default_type() { - let p = NewResourcePool::new(draft(" 本地池 ")).expect("ok"); - assert_eq!(p.name, "本地池"); + let p = NewResourcePool::new(draft(" local pool ")).expect("ok"); + assert_eq!(p.name, "local pool"); assert!(p.enabled); assert_eq!(p.pool_type, "Node"); } diff --git a/crates/api-test/tests/architecture.rs b/crates/api-test/tests/architecture.rs index daffa2d8..49adec54 100644 --- a/crates/api-test/tests/architecture.rs +++ b/crates/api-test/tests/architecture.rs @@ -21,7 +21,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/bug/src/adapters/http.rs b/crates/bug/src/adapters/http.rs index 7f2dc1ed..d117e741 100644 --- a/crates/bug/src/adapters/http.rs +++ b/crates/bug/src/adapters/http.rs @@ -196,7 +196,7 @@ struct ListBugsQuery { #[utoipa::path( get, path = "/bug", tag = "bug", - params(("projectId" = String, Query, description = "项目 ID")), + params(("projectId" = String, Query, description = "Project ID")), responses((status = 200, body = [BugResponse]), (status = 403)), security(("bearer" = [])) )] @@ -515,7 +515,7 @@ async fn list_bugs_by_plan( } #[derive(OpenApi)] -#[openapi(paths(create_bug, list_bugs, update_bug, change_status, set_custom_fields, list_followers, follow_bug, unfollow_bug, list_relations, link_relation, unlink_relation, list_bugs_by_plan), components(schemas(CreateBugRequest, UpdateBugRequest, ChangeStatusRequest, SetCustomFieldsRequest, BugResponse, FollowRequest, FollowersResponse, LinkRelationRequest, RelationItem, RelationsResponse)), tags((name = "bug", description = "缺陷管理")))] +#[openapi(paths(create_bug, list_bugs, update_bug, change_status, set_custom_fields, list_followers, follow_bug, unfollow_bug, list_relations, link_relation, unlink_relation, list_bugs_by_plan), components(schemas(CreateBugRequest, UpdateBugRequest, ChangeStatusRequest, SetCustomFieldsRequest, BugResponse, FollowRequest, FollowersResponse, LinkRelationRequest, RelationItem, RelationsResponse)), tags((name = "bug", description = "Defect management")))] struct ApiDoc; pub fn openapi() -> utoipa::openapi::OpenApi { ApiDoc::openapi() @@ -750,7 +750,7 @@ mod tests { .clone() .oneshot(put_req( &format!("/bug/{id}/custom-fields"), - r#"{"customFields":{"env":"prod","多选":"a,b"}}"#, + r#"{"customFields":{"env":"prod","multi-select":"a,b"}}"#, Some(&t), )) .await @@ -758,13 +758,13 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX).await.expect("body"); let v: serde_json::Value = serde_json::from_slice(&bytes).expect("json"); - assert_eq!(v["customFields"], serde_json::json!({"env": "prod", "多选": "a,b"})); + assert_eq!(v["customFields"], serde_json::json!({"env": "prod", "multi-select": "a,b"})); // List reads back the replaced fields. let resp = app.clone().oneshot(get("/bug?projectId=p1", Some(&t))).await.expect("resp"); let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX).await.expect("body"); let v: serde_json::Value = serde_json::from_slice(&bytes).expect("json"); - assert_eq!(v[0]["customFields"], serde_json::json!({"env": "prod", "多选": "a,b"})); + assert_eq!(v[0]["customFields"], serde_json::json!({"env": "prod", "multi-select": "a,b"})); // Blank key → 400; missing bug → 404. assert_eq!( diff --git a/crates/bug/src/adapters/pg.rs b/crates/bug/src/adapters/pg.rs index fa33793f..70f2741c 100644 --- a/crates/bug/src/adapters/pg.rs +++ b/crates/bug/src/adapters/pg.rs @@ -313,7 +313,7 @@ mod tests { use super::*; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_status_flow_assembled_and_bug_status_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -326,7 +326,7 @@ mod tests { sqlx::raw_sql( "INSERT INTO ms_status_item (id, project_id, name, internal) VALUES \ - ('NEW','p1','新建',true),('RESOLVED','p1','已解决',true),('CLOSED','p1','已关闭',true); \ + ('NEW','p1','New',true),('RESOLVED','p1','Resolved',true),('CLOSED','p1','Closed',true); \ INSERT INTO ms_status_flow (project_id, from_id, to_id) VALUES \ ('p1','NEW','RESOLVED'),('p1','RESOLVED','CLOSED');", ) @@ -344,12 +344,12 @@ mod tests { let cf = |pairs: &[(&str, &str)]| -> BTreeMap { pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect() }; - let nb = NewBug::new("p1", "登录崩溃") + let nb = NewBug::new("p1", "login crash") .expect("valid") .with_created_by(Some("alice")) .with_severity(Some("P1".into())) .with_handler(Some("bob".into())) - .with_custom_fields(cf(&[("severity", "P0"), ("多选", "a,b")])); + .with_custom_fields(cf(&[("severity", "P0"), ("multi-select", "a,b")])); let bug = repo.insert(&nb, "NEW").await.expect("insert"); let got = repo.get(&bug.id).await.expect("get").expect("some"); assert_eq!(got.status, "NEW"); @@ -359,7 +359,7 @@ mod tests { assert_eq!(got.updated_by.as_deref(), Some("alice")); assert!(got.updated_at.is_some()); // Custom fields read back as written. - assert_eq!(got.custom_fields, cf(&[("severity", "P0"), ("多选", "a,b")])); + assert_eq!(got.custom_fields, cf(&[("severity", "P0"), ("multi-select", "a,b")])); repo.set_status(&bug.id, "RESOLVED", Some("carol")).await.expect("set"); let after = repo.get(&bug.id).await.expect("get").expect("some"); @@ -368,11 +368,11 @@ mod tests { // Meta update replaces severity/handler and stamps the operator. let updated = repo - .update_meta(&bug.id, "登录崩溃(改)", Some("P0"), None, Some("dave")) + .update_meta(&bug.id, "login crash (modified)", Some("P0"), None, Some("dave")) .await .expect("meta") .expect("some"); - assert_eq!(updated.title, "登录崩溃(改)"); + assert_eq!(updated.title, "login crash (modified)"); assert_eq!(updated.severity.as_deref(), Some("P0")); assert_eq!(updated.handler, None); assert_eq!(updated.updated_by.as_deref(), Some("dave")); @@ -409,7 +409,7 @@ mod tests { let listed = repo.list("p1").await.expect("list"); assert_eq!(listed.len(), 1); assert_eq!(listed[0].id, bug.id); - assert_eq!(listed[0].title, "登录崩溃(改)"); + assert_eq!(listed[0].title, "login crash (modified)"); assert!(repo.list("other").await.expect("list").is_empty()); assert!(repo.get("ghost").await.expect("get").is_none()); diff --git a/crates/bug/src/application/create_bug.rs b/crates/bug/src/application/create_bug.rs index 87960c4d..8508fe86 100644 --- a/crates/bug/src/application/create_bug.rs +++ b/crates/bug/src/application/create_bug.rs @@ -71,11 +71,11 @@ mod tests { let repo = InMemoryBugRepository::with_default_flow("p1"); let uc = CreateBugUseCase::new(Arc::new(repo)); let bug = uc - .execute("p1", "登录崩溃", "NEW", Some("alice"), None, None, &BTreeMap::new()) + .execute("p1", "login crash", "NEW", Some("alice"), None, None, &BTreeMap::new()) .await .expect("ok"); assert_eq!(bug.status, "NEW"); - assert_eq!(bug.title, "登录崩溃"); + assert_eq!(bug.title, "login crash"); assert_eq!(bug.created_by.as_deref(), Some("alice")); // Insert seeds the audit pair from the creator. assert_eq!(bug.updated_by.as_deref(), Some("alice")); @@ -88,7 +88,15 @@ mod tests { let repo = InMemoryBugRepository::with_default_flow("p1"); let uc = CreateBugUseCase::new(Arc::new(repo)); let bug = uc - .execute("p1", "登录崩溃", "NEW", None, Some(" p0 "), Some(" bob "), &BTreeMap::new()) + .execute( + "p1", + "login crash", + "NEW", + None, + Some(" p0 "), + Some(" bob "), + &BTreeMap::new(), + ) .await .expect("ok"); assert_eq!(bug.severity.as_deref(), Some("P0")); @@ -111,12 +119,12 @@ mod tests { let repo = InMemoryBugRepository::with_default_flow("p1"); let uc = CreateBugUseCase::new(Arc::new(repo)); let raw = BTreeMap::from([(" severity ".to_string(), "P0".to_string())]); - let bug = uc.execute("p1", "登录崩溃", "NEW", None, None, None, &raw).await.expect("ok"); + let bug = uc.execute("p1", "login crash", "NEW", None, None, None, &raw).await.expect("ok"); assert_eq!(bug.custom_fields, BTreeMap::from([("severity".to_string(), "P0".to_string())])); // Blank key is a validation error. let bad = BTreeMap::from([(" ".to_string(), "v".to_string())]); assert_eq!( - uc.execute("p1", "又崩了", "NEW", None, None, None, &bad).await.unwrap_err(), + uc.execute("p1", "crashed again", "NEW", None, None, None, &bad).await.unwrap_err(), CreateBugError::Validation(BugError::EmptyCustomFieldKey) ); } diff --git a/crates/bug/src/domain/bug.rs b/crates/bug/src/domain/bug.rs index 08f3c4b2..9af9a28c 100644 --- a/crates/bug/src/domain/bug.rs +++ b/crates/bug/src/domain/bug.rs @@ -179,9 +179,9 @@ mod tests { fn flow() -> StatusFlowGraph { StatusFlowGraph::new( vec![ - StatusItem::new("NEW", "新建", true), - StatusItem::new("RESOLVED", "已解决", true), - StatusItem::new("CLOSED", "已关闭", true), + StatusItem::new("NEW", "New", true), + StatusItem::new("RESOLVED", "Resolved", true), + StatusItem::new("CLOSED", "Closed", true), ], vec![("NEW".into(), "RESOLVED".into()), ("RESOLVED".into(), "CLOSED".into())], ) diff --git a/crates/bug/tests/architecture.rs b/crates/bug/tests/architecture.rs index 782cc5fc..2ea8b518 100644 --- a/crates/bug/tests/architecture.rs +++ b/crates/bug/tests/architecture.rs @@ -21,7 +21,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/case-management/Cargo.toml b/crates/case-management/Cargo.toml index e4185edb..94dc8f2c 100644 --- a/crates/case-management/Cargo.toml +++ b/crates/case-management/Cargo.toml @@ -17,14 +17,14 @@ sqlx = { workspace = true, features = ["runtime-tokio", "postgres", "json"], opt axum = { workspace = true, optional = true } utoipa = { workspace = true, optional = true } webauth = { workspace = true, optional = true } -serde = { workspace = true, optional = true } +serde = { workspace = true } rust_xlsxwriter = { version = "0.79", optional = true } # calamine 0.36 (quick-xml 0.41) needs Rust 1.88; 0.35 is the newest for our 1.86 pin. calamine = { version = "0.35", optional = true } [features] pg = ["dep:sqlx"] -http = ["dep:axum", "dep:serde", "dep:webauth", "webauth/http", "dep:utoipa", "dep:rust_xlsxwriter", "dep:calamine"] +http = ["dep:axum", "dep:webauth", "webauth/http", "dep:utoipa", "dep:rust_xlsxwriter", "dep:calamine"] [dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/case-management/src/adapters/http.rs b/crates/case-management/src/adapters/http.rs index 6ffc8d76..dbc96f49 100644 --- a/crates/case-management/src/adapters/http.rs +++ b/crates/case-management/src/adapters/http.rs @@ -641,7 +641,7 @@ async fn remove_case_dependency( } } -#[utoipa::path(get, path = "/functional-case/export", tag = "functional-case", params(ProjectQuery), responses((status = 200, description = "xlsx 文件"), (status = 403)), security(("bearer" = [])))] +#[utoipa::path(get, path = "/functional-case/export", tag = "functional-case", params(ProjectQuery), responses((status = 200, description = "xlsx file"), (status = 403)), security(("bearer" = [])))] async fn export_cases( user: AuthUser, State(st): State, @@ -677,7 +677,7 @@ struct ImportResult { imported: usize, } -#[utoipa::path(post, path = "/functional-case/import", tag = "functional-case", params(ProjectQuery), request_body(content = Vec, description = "xlsx 文件字节"), responses((status = 200, body = ImportResult), (status = 400), (status = 403)), security(("bearer" = [])))] +#[utoipa::path(post, path = "/functional-case/import", tag = "functional-case", params(ProjectQuery), request_body(content = Vec, description = "xlsx file bytes"), responses((status = 200, body = ImportResult), (status = 400), (status = 403)), security(("bearer" = [])))] async fn import_cases( user: AuthUser, State(st): State, @@ -735,7 +735,7 @@ fn rows_to_xlsx(rows: &[Vec]) -> Result, rust_xlsxwriter::XlsxEr CaseBody, CaseResponse, ImportResult, CaseChangeDto, CaseBugDto, CaseReviewDto, CasePlanDto, CaseDependencyDto, DependencyBody )), - tags((name = "functional-case", description = "功能用例")) + tags((name = "functional-case", description = "Functional cases")) )] struct ApiDoc; @@ -786,7 +786,7 @@ mod tests { .clone() .oneshot(post( "/functional-case", - r#"{"projectId":"p1","name":"登录成功","module":"登录","customFields":{"owner":"alice"}}"#, + r#"{"projectId":"p1","name":"login success","module":"login","customFields":{"owner":"alice"}}"#, Some(&t), )) .await @@ -814,8 +814,8 @@ mod tests { let (app, t) = app("FUNCTIONAL_CASE:READ+ADD").await; let rows = vec![ vec!["名称".to_string(), "优先级".to_string(), "owner".to_string()], - vec!["登录成功".into(), "P0".into(), "alice".into()], - vec!["密码错误".into(), "P1".into(), "bob".into()], + vec!["login success".into(), "P0".into(), "alice".into()], + vec!["wrong password".into(), "P1".into(), "bob".into()], ]; let xlsx = rows_to_xlsx(&rows).expect("xlsx"); let req = Request::builder() @@ -853,7 +853,11 @@ mod tests { let (app, t) = app("FUNCTIONAL_CASE:READ+ADD+UPDATE+DELETE").await; let resp = app .clone() - .oneshot(post("/functional-case", r#"{"projectId":"p1","name":"登录成功"}"#, Some(&t))) + .oneshot(post( + "/functional-case", + r#"{"projectId":"p1","name":"login success"}"#, + Some(&t), + )) .await .expect("resp"); assert_eq!(resp.status(), StatusCode::CREATED); @@ -868,7 +872,9 @@ mod tests { .uri(format!("/functional-case/{id}")) .header("content-type", "application/json") .header("authorization", format!("Bearer {t}")) - .body(Body::from(r#"{"projectId":"p1","name":"登录失败","priority":"P0"}"#.to_string())) + .body(Body::from( + r#"{"projectId":"p1","name":"login failure","priority":"P0"}"#.to_string(), + )) .expect("req"); let resp = app.clone().oneshot(put).await.expect("resp"); assert_eq!(resp.status(), StatusCode::OK); @@ -876,7 +882,7 @@ mod tests { let b = axum::body::to_bytes(resp.into_body(), usize::MAX).await.expect("body"); serde_json::from_slice(&b).expect("json") }; - assert_eq!(updated["name"], "登录失败"); + assert_eq!(updated["name"], "login failure"); assert_eq!(updated["priority"], "P0"); let del = Request::builder() diff --git a/crates/case-management/src/application/cases.rs b/crates/case-management/src/application/cases.rs index 446cd321..3b410b1b 100644 --- a/crates/case-management/src/application/cases.rs +++ b/crates/case-management/src/application/cases.rs @@ -328,7 +328,7 @@ mod tests { project_id: "p1".into(), num: 0, name: name.into(), - module: "登录".into(), + module: "login".into(), priority: "P2".into(), status: "PREPARED".into(), tags: Vec::new(), @@ -346,12 +346,21 @@ mod tests { let create = CreateCaseUseCase::new(repo.clone()); let list = ListCasesUseCase::new(repo); create - .execute("p1", "登录成功", "登录", "P0", "", BTreeMap::new(), Vec::new(), Some("alice")) + .execute( + "p1", + "login success", + "login", + "P0", + "", + BTreeMap::new(), + Vec::new(), + Some("alice"), + ) .await .expect("created"); let all = list.execute("p1").await.expect("listed"); assert_eq!(all.len(), 1); - assert_eq!(all[0].name, "登录成功"); + assert_eq!(all[0].name, "login success"); assert_eq!(all[0].status, "PREPARED"); assert_eq!(all[0].created_by.as_deref(), Some("alice")); } @@ -359,14 +368,14 @@ mod tests { #[test] fn cases_from_rows_is_inverse_of_export() { let cases = vec![ - case("c1", "用例1", &[("owner", "alice")]), - case("c2", "用例2", &[("sprint", "S1")]), + case("c1", "case 1", &[("owner", "alice")]), + case("c2", "case 2", &[("sprint", "S1")]), ]; let rows = export_rows(&cases); let news = cases_from_rows("p1", &rows); assert_eq!(news.len(), 2); - assert_eq!(news[0].name, "用例1"); - assert_eq!(news[0].module, "登录"); + assert_eq!(news[0].name, "case 1"); + assert_eq!(news[0].module, "login"); assert_eq!(news[0].custom_fields.get("owner").map(String::as_str), Some("alice")); assert_eq!(news[1].custom_fields.get("sprint").map(String::as_str), Some("S1")); } @@ -375,12 +384,12 @@ mod tests { fn cases_from_rows_skips_empty_name() { let rows = vec![ vec!["名称".into(), "模块".into()], - vec!["".into(), "登录".into()], - vec!["有效用例".into(), "登录".into()], + vec!["".into(), "login".into()], + vec!["valid case".into(), "login".into()], ]; let news = cases_from_rows("p1", &rows); assert_eq!(news.len(), 1); - assert_eq!(news[0].name, "有效用例"); + assert_eq!(news[0].name, "valid case"); } #[tokio::test] @@ -390,8 +399,8 @@ mod tests { let list = ListCasesUseCase::new(repo); let rows = vec![ vec!["名称".into(), "优先级".into(), "owner".into()], - vec!["登录成功".into(), "P0".into(), "alice".into()], - vec!["密码错误".into(), "P1".into(), "bob".into()], + vec!["login success".into(), "P0".into(), "alice".into()], + vec!["wrong password".into(), "P1".into(), "bob".into()], ]; assert_eq!(import.execute("p1", &rows, Some("importer")).await.expect("import"), 2); let all = list.execute("p1").await.expect("list"); @@ -404,12 +413,12 @@ mod tests { #[test] fn export_rows_unions_custom_fields() { let cases = vec![ - case("c1", "用例1", &[("owner", "alice")]), - case("c2", "用例2", &[("sprint", "S1")]), + case("c1", "case 1", &[("owner", "alice")]), + case("c2", "case 2", &[("sprint", "S1")]), ]; let rows = export_rows(&cases); assert_eq!(rows[0], vec!["ID", "名称", "模块", "优先级", "状态", "owner", "sprint"]); - assert_eq!(rows[1], vec!["c1", "用例1", "登录", "P2", "PREPARED", "alice", ""]); - assert_eq!(rows[2], vec!["c2", "用例2", "登录", "P2", "PREPARED", "", "S1"]); + assert_eq!(rows[1], vec!["c1", "case 1", "login", "P2", "PREPARED", "alice", ""]); + assert_eq!(rows[2], vec!["c2", "case 2", "login", "P2", "PREPARED", "", "S1"]); } } diff --git a/crates/case-management/src/domain/case.rs b/crates/case-management/src/domain/case.rs index b864fb93..83da9484 100644 --- a/crates/case-management/src/domain/case.rs +++ b/crates/case-management/src/domain/case.rs @@ -110,8 +110,8 @@ mod tests { #[test] fn defaults_priority_and_status() { - let c = - NewFunctionalCase::new("p1", "登录成功", "", "", "", fields(), Vec::new()).expect("ok"); + let c = NewFunctionalCase::new("p1", "login success", "", "", "", fields(), Vec::new()) + .expect("ok"); assert_eq!(c.priority, "P2"); assert_eq!(c.status, "PREPARED"); assert_eq!(c.custom_fields["owner"], "alice"); @@ -121,8 +121,8 @@ mod tests { fn keeps_given_values_and_trims() { let c = NewFunctionalCase::new( " p1 ", - " 用例 ", - "登录", + " case ", + "login", "P0", "REVIEWING", fields(), @@ -130,7 +130,7 @@ mod tests { ) .expect("ok"); assert_eq!(c.project_id, "p1"); - assert_eq!(c.name, "用例"); + assert_eq!(c.name, "case"); assert_eq!(c.priority, "P0"); assert_eq!(c.status, "REVIEWING"); } diff --git a/crates/case-management/tests/architecture.rs b/crates/case-management/tests/architecture.rs index 8b415f6e..a983ab39 100644 --- a/crates/case-management/tests/architecture.rs +++ b/crates/case-management/tests/architecture.rs @@ -24,7 +24,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/case/src/adapters/http.rs b/crates/case/src/adapters/http.rs index 85dcd216..320c2680 100644 --- a/crates/case/src/adapters/http.rs +++ b/crates/case/src/adapters/http.rs @@ -375,7 +375,7 @@ async fn submit_review( } #[derive(OpenApi)] -#[openapi(paths(submit_review, create_review, update_review, list_reviews, get_review, delete_review_handler), components(schemas(SubmitRequest, SubmitResponse, CreateReviewRequest, UpdateReviewRequest, CreatedReview, ReviewSummaryResponse, ReviewCaseStatusResponse, ReviewDetailResponse)), tags((name = "case", description = "用例评审")))] +#[openapi(paths(submit_review, create_review, update_review, list_reviews, get_review, delete_review_handler), components(schemas(SubmitRequest, SubmitResponse, CreateReviewRequest, UpdateReviewRequest, CreatedReview, ReviewSummaryResponse, ReviewCaseStatusResponse, ReviewDetailResponse)), tags((name = "case", description = "Case review")))] struct ApiDoc; pub fn openapi() -> utoipa::openapi::OpenApi { ApiDoc::openapi() @@ -515,8 +515,8 @@ mod tests { let (app, t) = app_with(PassRule::Single, 1).await; let create_body = r#"{"projectId":"p1","passRule":"SINGLE","reviewerCount":1, - "caseIds":["c1","c2"],"name":"v1.0 版本用例评审","description":"首轮", - "tags":["冒烟"],"moduleId":"m1","startAt":"2026-07-01T00:00:00Z","endAt":"2026-07-31T00:00:00Z"}"#; + "caseIds":["c1","c2"],"name":"v1.0 release case review","description":"first round", + "tags":["smoke"],"moduleId":"m1","startAt":"2026-07-01T00:00:00Z","endAt":"2026-07-31T00:00:00Z"}"#; let resp = app .clone() .oneshot(json_req("POST", "/case-review", create_body, &t)) @@ -534,15 +534,15 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); let list = body_json(resp).await; let item = &list.as_array().expect("array")[0]; - assert_eq!(item["name"], "v1.0 版本用例评审"); - assert_eq!(item["description"], "首轮"); - assert_eq!(item["tags"][0], "冒烟"); + assert_eq!(item["name"], "v1.0 release case review"); + assert_eq!(item["description"], "first round"); + assert_eq!(item["tags"][0], "smoke"); assert_eq!(item["moduleId"], "m1"); assert_eq!(item["status"], "IN_PROGRESS"); assert_eq!(item["total"], 2); assert_eq!(item["createdBy"], "admin"); - let put_body = r#"{"name":"v2 评审","description":"二轮","tags":["回归"], + let put_body = r#"{"name":"v2 review","description":"second round","tags":["regression"], "moduleId":null,"passRule":"MULTIPLE","reviewerCount":2}"#; let resp = app .clone() @@ -555,7 +555,7 @@ mod tests { app.oneshot(json_req("GET", "/case-review?projectId=p1", "", &t)).await.expect("resp"); let list = body_json(resp).await; let item = &list.as_array().expect("array")[0]; - assert_eq!(item["name"], "v2 评审"); + assert_eq!(item["name"], "v2 review"); assert_eq!(item["passRule"], "MULTIPLE"); assert_eq!(item["reviewerCount"], 2); assert!(item["moduleId"].is_null()); diff --git a/crates/case/src/adapters/pg.rs b/crates/case/src/adapters/pg.rs index d70f54db..9a24e109 100644 --- a/crates/case/src/adapters/pg.rs +++ b/crates/case/src/adapters/pg.rs @@ -287,7 +287,7 @@ mod tests { use super::*; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_history_ordering_and_status_upsert() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); diff --git a/crates/case/src/application/delete_review.rs b/crates/case/src/application/delete_review.rs index 3719eb73..982a7c10 100644 --- a/crates/case/src/application/delete_review.rs +++ b/crates/case/src/application/delete_review.rs @@ -42,7 +42,7 @@ mod tests { reviewer_count: 1, case_ids: vec!["c1".into()], created_by: "admin".into(), - meta: ReviewMeta { name: "v1 评审".into(), ..ReviewMeta::default() }, + meta: ReviewMeta { name: "v1 review".into(), ..ReviewMeta::default() }, }) .await .expect("create"); diff --git a/crates/case/src/application/submit_review.rs b/crates/case/src/application/submit_review.rs index f7ee86d7..177a9f76 100644 --- a/crates/case/src/application/submit_review.rs +++ b/crates/case/src/application/submit_review.rs @@ -106,7 +106,7 @@ mod tests { uc.execute("rev1", "c1", Verdict::new("u1", Pass, None).expect("v")).await.expect("ok"); let status = uc - .execute("rev1", "c1", Verdict::new("u2", UnPass, Some("步骤缺失")).expect("v")) + .execute("rev1", "c1", Verdict::new("u2", UnPass, Some("missing steps")).expect("v")) .await .expect("ok"); assert_eq!(status, UnPass); diff --git a/crates/case/src/application/update_review_meta.rs b/crates/case/src/application/update_review_meta.rs index 89bd0874..e45d899c 100644 --- a/crates/case/src/application/update_review_meta.rs +++ b/crates/case/src/application/update_review_meta.rs @@ -72,7 +72,7 @@ mod tests { reviewer_count: 1, case_ids: vec!["c1".into()], created_by: "admin".into(), - meta: meta("v1 评审"), + meta: meta("v1 review"), }) .await .expect("create"); @@ -83,7 +83,7 @@ mod tests { async fn updates_meta_and_setting() { let (repo, id) = seeded_repo().await; let uc = UpdateReviewMetaUseCase::new(repo.clone()); - let mut u = update("v2 评审", "MULTIPLE", 3); + let mut u = update("v2 review", "MULTIPLE", 3); u.meta.description = "desc".into(); u.meta.tags = vec!["t1".into()]; u.meta.module_id = Some("m1".into()); @@ -93,7 +93,7 @@ mod tests { let list = repo.list_reviews("p1").await.expect("list"); let s = &list[0]; - assert_eq!(s.meta.name, "v2 评审"); + assert_eq!(s.meta.name, "v2 review"); assert_eq!(s.meta.description, "desc"); assert_eq!(s.meta.tags, vec!["t1".to_string()]); assert_eq!(s.meta.module_id.as_deref(), Some("m1")); diff --git a/crates/case/src/domain/review.rs b/crates/case/src/domain/review.rs index 7770fd21..95153b59 100644 --- a/crates/case/src/domain/review.rs +++ b/crates/case/src/domain/review.rs @@ -184,7 +184,7 @@ mod tests { #[test] fn un_pass_with_content_ok_and_pass_needs_no_content() { - assert!(Verdict::new("u1", UnPass, Some("理由")).is_ok()); + assert!(Verdict::new("u1", UnPass, Some("reason")).is_ok()); assert!(Verdict::new("u1", Pass, None).is_ok()); } diff --git a/crates/case/tests/architecture.rs b/crates/case/tests/architecture.rs index 782cc5fc..2ea8b518 100644 --- a/crates/case/tests/architecture.rs +++ b/crates/case/tests/architecture.rs @@ -21,7 +21,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/comment/src/adapters/http.rs b/crates/comment/src/adapters/http.rs index 9eecc595..f0d0b0a8 100644 --- a/crates/comment/src/adapters/http.rs +++ b/crates/comment/src/adapters/http.rs @@ -178,7 +178,7 @@ async fn delete_comment( } #[derive(OpenApi)] -#[openapi(paths(add_comment, list_comments, delete_comment), components(schemas(AddCommentRequest, CommentResponse)), tags((name = "comment", description = "通用评论")))] +#[openapi(paths(add_comment, list_comments, delete_comment), components(schemas(AddCommentRequest, CommentResponse)), tags((name = "comment", description = "Generic comments")))] struct ApiDoc; pub fn openapi() -> utoipa::openapi::OpenApi { ApiDoc::openapi() @@ -237,7 +237,7 @@ mod tests { .clone() .oneshot(post( "/comment", - r#"{"targetType":"BUG","targetId":"b1","content":"复现步骤"}"#, + r#"{"targetType":"BUG","targetId":"b1","content":"reproduction steps"}"#, Some(t), )) .await @@ -259,7 +259,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); let v = json(resp).await; assert_eq!(v.as_array().expect("arr").len(), 1); - assert_eq!(v[0]["content"], "复现步骤"); + assert_eq!(v[0]["content"], "reproduction steps"); } #[tokio::test] diff --git a/crates/comment/src/adapters/pg.rs b/crates/comment/src/adapters/pg.rs index 3c439439..8c5e8c1d 100644 --- a/crates/comment/src/adapters/pg.rs +++ b/crates/comment/src/adapters/pg.rs @@ -93,7 +93,7 @@ mod tests { use super::*; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_comment_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -101,13 +101,13 @@ mod tests { sqlx::raw_sql("TRUNCATE ms_comment").execute(&pool).await.expect("truncate"); let repo = PgCommentRepository::new(pool.clone()); - let nc = NewComment::new("BUG", "b1", "登录崩溃复现", "admin").expect("valid"); + let nc = NewComment::new("BUG", "b1", "login crash reproduction", "admin").expect("valid"); let c = repo.insert(&nc).await.expect("insert"); assert!(!c.created_at.is_empty()); let list = repo.list("BUG", "b1").await.expect("list"); assert_eq!(list.len(), 1); - assert_eq!(list[0].content, "登录崩溃复现"); + assert_eq!(list[0].content, "login crash reproduction"); repo.soft_delete(&c.id).await.expect("delete"); assert!(repo.list("BUG", "b1").await.expect("list").is_empty()); diff --git a/crates/comment/src/application/add_comment.rs b/crates/comment/src/application/add_comment.rs index ebe3cea8..8861af11 100644 --- a/crates/comment/src/application/add_comment.rs +++ b/crates/comment/src/application/add_comment.rs @@ -43,9 +43,10 @@ mod tests { #[tokio::test] async fn adds_valid_comment() { let uc = AddCommentUseCase::new(Arc::new(InMemoryCommentRepository::new())); - let c = uc.execute("BUG", "b1", "复现步骤如下", "admin").await.expect("ok"); + let c = + uc.execute("BUG", "b1", "reproduction steps as follows", "admin").await.expect("ok"); assert_eq!(c.target_id, "b1"); - assert_eq!(c.content, "复现步骤如下"); + assert_eq!(c.content, "reproduction steps as follows"); assert_eq!(c.author, "admin"); } diff --git a/crates/comment/src/domain/comment.rs b/crates/comment/src/domain/comment.rs index fe4a2a6e..a020ec87 100644 --- a/crates/comment/src/domain/comment.rs +++ b/crates/comment/src/domain/comment.rs @@ -58,10 +58,11 @@ mod tests { #[test] fn new_comment_trims_and_accepts_valid() { - let c = NewComment::new("BUG", "b1", " 看起来是空指针 ", " admin ").expect("valid"); + let c = NewComment::new("BUG", "b1", " looks like a null pointer ", " admin ") + .expect("valid"); assert_eq!(c.target_type, "BUG"); assert_eq!(c.target_id, "b1"); - assert_eq!(c.content, "看起来是空指针"); + assert_eq!(c.content, "looks like a null pointer"); assert_eq!(c.author, "admin"); } diff --git a/crates/comment/tests/architecture.rs b/crates/comment/tests/architecture.rs index 782cc5fc..2ea8b518 100644 --- a/crates/comment/tests/architecture.rs +++ b/crates/comment/tests/architecture.rs @@ -21,7 +21,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/delivery/src/adapters/agent_http.rs b/crates/delivery/src/adapters/agent_http.rs index 4d361281..60feac40 100644 --- a/crates/delivery/src/adapters/agent_http.rs +++ b/crates/delivery/src/adapters/agent_http.rs @@ -211,8 +211,8 @@ mod tests { "status":"completed", "deliverable":{"kind":"DIFF","reference":"branch:y","summary":"ok"}, "events":[ - {"kind":"DECISION","message":"用 argon2","detail":"PHC"}, - {"kind":"TEST_RESULT","message":"12/12 通过"} + {"kind":"DECISION","message":"using argon2","detail":"PHC"}, + {"kind":"TEST_RESULT","message":"12/12 passed"} ] })) }), diff --git a/crates/delivery/src/adapters/http.rs b/crates/delivery/src/adapters/http.rs index 54f6c081..a0e70106 100644 --- a/crates/delivery/src/adapters/http.rs +++ b/crates/delivery/src/adapters/http.rs @@ -563,7 +563,7 @@ async fn delete_attempt( #[openapi( paths(dispatch, list_by_task, get_attempt, report_running, complete, fail, record_event, list_events, list_tasks, collab_stats, stop, delete_attempt), components(schemas(DispatchBody, RunningBody, CompleteBody, FailBody, EventBody, EventResponse, DeliverableResponse, AttemptResponse, TaskItemResponse, TaskPageResponse, CollabRequirementItem, CollabDayItem, CollabStatsResponse, StopBody)), - tags((name = "delivery", description = "交付执行(AI 执行者)")) + tags((name = "delivery", description = "Delivery execution (AI executor)")) )] struct ApiDoc; pub fn openapi() -> utoipa::openapi::OpenApi { @@ -722,7 +722,7 @@ mod tests { .oneshot(req( "POST", &format!("/delivery/{id}/events"), - r#"{"kind":"DECISION","message":"选用 argon2","detail":"PHC"}"#, + r#"{"kind":"DECISION","message":"uses argon2","detail":"PHC"}"#, Some(&t), )) .await @@ -747,7 +747,7 @@ mod tests { assert_eq!(list.status(), StatusCode::OK); let arr = json(list).await; assert_eq!(arr.as_array().expect("a").len(), 2); - assert_eq!(arr[0]["message"], "选用 argon2"); + assert_eq!(arr[0]["message"], "uses argon2"); assert_eq!( app.oneshot(req( @@ -837,7 +837,7 @@ mod tests { .oneshot(req( "POST", &format!("/delivery/{id}/stop"), - r#"{"reason":"手动停止"}"#, + r#"{"reason":"manual stop"}"#, Some(&t), )) .await diff --git a/crates/delivery/src/adapters/local.rs b/crates/delivery/src/adapters/local.rs index 631eb573..34b46e99 100644 --- a/crates/delivery/src/adapters/local.rs +++ b/crates/delivery/src/adapters/local.rs @@ -219,7 +219,7 @@ mod tests { #[tokio::test] async fn streams_events_then_completes_with_result() { - let script = r#"cat >/dev/null; printf '{"event":"DECISION","message":"用 argon2"}\n{"event":"FILE_CHANGE","message":"edit auth.rs"}\n{"reference":"branch:x","summary":"done"}\n'"#; + let script = r#"cat >/dev/null; printf '{"event":"DECISION","message":"using argon2"}\n{"event":"FILE_CHANGE","message":"edit auth.rs"}\n{"reference":"branch:x","summary":"done"}\n'"#; let exec = LocalCommandAgentExecutor::new( vec!["/bin/sh".into(), "-c".into(), script.into()], vec!["/bin/sh".into(), "-c".into(), "true".into()], @@ -235,7 +235,7 @@ mod tests { } let events = sink.events.lock().unwrap_or_else(std::sync::PoisonError::into_inner); assert_eq!(events.len(), 2); - assert_eq!(events[0], (EventKind::Decision, "用 argon2".to_string())); + assert_eq!(events[0], (EventKind::Decision, "using argon2".to_string())); assert_eq!(events[1].0, EventKind::FileChange); } diff --git a/crates/delivery/src/adapters/pg.rs b/crates/delivery/src/adapters/pg.rs index 9ee256e3..51ac5161 100644 --- a/crates/delivery/src/adapters/pg.rs +++ b/crates/delivery/src/adapters/pg.rs @@ -369,7 +369,7 @@ mod tests { use crate::domain::DeliverableKind; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_attempt_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -395,7 +395,7 @@ mod tests { } #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_list_tasks_carries_basic_info() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -404,11 +404,11 @@ mod tests { sqlx::query(&format!("TRUNCATE {t} CASCADE")).execute(&pool).await.expect("truncate"); } - sqlx::query("INSERT INTO ms_requirement (id, project_id, title, status) VALUES ('r1','p1','登录模块','DRAFT')") + sqlx::query("INSERT INTO ms_requirement (id, project_id, title, status) VALUES ('r1','p1','login module','DRAFT')") .execute(&pool).await.expect("req"); sqlx::query("INSERT INTO ms_task_decomposition (id, requirement_id, requirement_version) VALUES ('d1','r1',1)") .execute(&pool).await.expect("decomp"); - sqlx::query("INSERT INTO ms_task (decomposition_id, id, title, description, status) VALUES ('d1','t1','实现登录接口','支持手机号+验证码登录','RUNNING')") + sqlx::query("INSERT INTO ms_task (decomposition_id, id, title, description, status) VALUES ('d1','t1','implement login API','supports phone number + verification code login','RUNNING')") .execute(&pool).await.expect("task"); let repo = PgDeliveryRepository::new(pool.clone()); @@ -421,9 +421,12 @@ mod tests { .expect("list_tasks"); assert_eq!(page.total, 2); let linked = page.items.iter().find(|r| r.attempt.task_id == "t1").expect("t1 row"); - assert_eq!(linked.title.as_deref(), Some("实现登录接口")); - assert_eq!(linked.description.as_deref(), Some("支持手机号+验证码登录")); - assert_eq!(linked.module.as_deref(), Some("登录模块")); + assert_eq!(linked.title.as_deref(), Some("implement login API")); + assert_eq!( + linked.description.as_deref(), + Some("supports phone number + verification code login") + ); + assert_eq!(linked.module.as_deref(), Some("login module")); let orphan = page.items.iter().find(|r| r.attempt.task_id == "t9").expect("t9 row"); assert_eq!(orphan.title, None); assert_eq!(orphan.description, None); diff --git a/crates/delivery/src/application/delivery_service.rs b/crates/delivery/src/application/delivery_service.rs index 2f0783d3..873becad 100644 --- a/crates/delivery/src/application/delivery_service.rs +++ b/crates/delivery/src/application/delivery_service.rs @@ -413,7 +413,7 @@ mod tests { Arc::new(EchoAgentExecutor::new()), ); let a = s - .dispatch("d1", "t1", "实现登录", "", &[], "CLAUDE_CODE", None, None, None) + .dispatch("d1", "t1", "implement login", "", &[], "CLAUDE_CODE", None, None, None) .await .expect("dispatch"); let events = s.events(&a.id).await.expect("events"); @@ -429,9 +429,9 @@ mod tests { .await .expect("dispatch"); - s.record_event(&a.id, "DECISION", "选用 argon2", Some("PHC 格式")).await.expect("ev1"); + s.record_event(&a.id, "DECISION", "uses argon2", Some("PHC format")).await.expect("ev1"); s.record_event(&a.id, "FILE_CHANGE", "edit auth.rs", None).await.expect("ev2"); - s.record_event(&a.id, "TEST_RESULT", "cargo test 通过", None).await.expect("ev3"); + s.record_event(&a.id, "TEST_RESULT", "cargo test passed", None).await.expect("ev3"); let events = s.events(&a.id).await.expect("events"); assert_eq!(events.len(), 3); diff --git a/crates/delivery/src/domain/delivery.rs b/crates/delivery/src/domain/delivery.rs index bf94e6cc..c6b6771e 100644 --- a/crates/delivery/src/domain/delivery.rs +++ b/crates/delivery/src/domain/delivery.rs @@ -290,14 +290,14 @@ mod tests { #[test] fn stop_from_dispatched_or_running_then_terminal() { let mut a = attempt(); - a.stop("用户停止").expect("stop"); + a.stop("stopped by user").expect("stop"); assert_eq!(a.status, AttemptStatus::Stopped); - assert_eq!(a.error.as_deref(), Some("用户停止")); + assert_eq!(a.error.as_deref(), Some("stopped by user")); assert!(a.status.is_terminal()); let mut b = attempt(); b.start_running("r").expect("run"); - assert!(b.stop("中途停止").is_ok()); + assert!(b.stop("stopped midway").is_ok()); assert!(a.stop("again").is_err()); } diff --git a/crates/delivery/src/domain/event.rs b/crates/delivery/src/domain/event.rs index 4474f6d8..6d754e50 100644 --- a/crates/delivery/src/domain/event.rs +++ b/crates/delivery/src/domain/event.rs @@ -78,10 +78,10 @@ mod tests { EventError::EmptyMessage ); let e = - NewExecutionEvent::new(EventKind::Decision, " 选用 argon2 ", Some(" 因为 PHC ")) + NewExecutionEvent::new(EventKind::Decision, " uses argon2 ", Some(" because PHC ")) .expect("ok"); - assert_eq!(e.message, "选用 argon2"); - assert_eq!(e.detail.as_deref(), Some("因为 PHC")); + assert_eq!(e.message, "uses argon2"); + assert_eq!(e.detail.as_deref(), Some("because PHC")); let e2 = NewExecutionEvent::new(EventKind::Log, "x", Some(" ")).expect("ok"); assert_eq!(e2.detail, None); } diff --git a/crates/delivery/tests/architecture.rs b/crates/delivery/tests/architecture.rs index 643f0b7d..0dd3436b 100644 --- a/crates/delivery/tests/architecture.rs +++ b/crates/delivery/tests/architecture.rs @@ -24,7 +24,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/design/src/adapters/pg.rs b/crates/design/src/adapters/pg.rs index 6f84d665..f1bf8242 100644 --- a/crates/design/src/adapters/pg.rs +++ b/crates/design/src/adapters/pg.rs @@ -92,7 +92,7 @@ mod tests { use super::*; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_proposal_lifecycle_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -110,27 +110,27 @@ mod tests { sqlx::query("TRUNCATE ms_design_proposal").execute(&pool).await.expect("truncate"); let repo = PgProposalRepository::new(pool.clone()); - let mut p = repo.create("req-1", "登录改造设计").await.expect("create"); + let mut p = repo.create("req-1", "login revamp design").await.expect("create"); assert_eq!(p.status, ProposalStatus::Drafting); - p.submit_design("## 方案 v1").expect("submit"); + p.submit_design("## proposal v1").expect("submit"); repo.save(&p).await.expect("save1"); - p.request_changes("补充错误码").expect("reject"); + p.request_changes("add error codes").expect("reject"); repo.save(&p).await.expect("save2"); let got = repo.get(&p.id).await.expect("get").expect("some"); assert_eq!(got.status, ProposalStatus::ChangesRequested); assert_eq!(got.revision, 1); - assert_eq!(got.review_comment.as_deref(), Some("补充错误码")); + assert_eq!(got.review_comment.as_deref(), Some("add error codes")); let mut p = got; - p.submit_design("## 方案 v2 含错误码").expect("submit2"); + p.submit_design("## proposal v2 with error codes").expect("submit2"); p.approve().expect("approve"); repo.save(&p).await.expect("save3"); let final_p = repo.get(&p.id).await.expect("get2").expect("some"); assert_eq!(final_p.status, ProposalStatus::Approved); - assert_eq!(final_p.design_doc.as_deref(), Some("## 方案 v2 含错误码")); + assert_eq!(final_p.design_doc.as_deref(), Some("## proposal v2 with error codes")); assert!(final_p.review_comment.is_none()); assert_eq!(repo.list_by_requirement("req-1").await.expect("list").len(), 1); } diff --git a/crates/design/src/application/proposal_service.rs b/crates/design/src/application/proposal_service.rs index 54049823..cd95b2fe 100644 --- a/crates/design/src/application/proposal_service.rs +++ b/crates/design/src/application/proposal_service.rs @@ -119,13 +119,13 @@ mod tests { #[tokio::test] async fn full_lifecycle_with_revision() { let s = svc(); - let p = s.create("req-1", "登录改造").await.expect("create"); + let p = s.create("req-1", "login revamp").await.expect("create"); assert_eq!(p.status, ProposalStatus::Drafting); let p = s.submit_design(&p.id, "v1").await.expect("submit"); assert_eq!(p.status, ProposalStatus::PendingReview); - let p = s.request_changes(&p.id, "补充失败分支").await.expect("reject"); + let p = s.request_changes(&p.id, "add failure branches").await.expect("reject"); assert_eq!(p.status, ProposalStatus::ChangesRequested); assert_eq!(p.revision, 1); @@ -170,16 +170,19 @@ mod tests { let s = ProposalService::new(Arc::new(InMemoryProposalRepository::new())) .with_drafter(drafter.clone()); - let p = s.create("req-9", "支付重构").await.expect("create"); + let p = s.create("req-9", "payment refactor").await.expect("create"); assert_eq!( drafter.drafted.lock().unwrap_or_else(std::sync::PoisonError::into_inner).as_slice(), std::slice::from_ref(&p.id) ); assert_eq!(p.status, ProposalStatus::Drafting); - let p = s.submit_design(&p.id, "## 架构\n拆分支付域").await.expect("callback"); + let p = s + .submit_design(&p.id, "## Architecture\nsplit the payment domain") + .await + .expect("callback"); assert_eq!(p.status, ProposalStatus::PendingReview); - assert_eq!(p.design_doc.as_deref(), Some("## 架构\n拆分支付域")); + assert_eq!(p.design_doc.as_deref(), Some("## Architecture\nsplit the payment domain")); } #[tokio::test] @@ -207,7 +210,7 @@ mod tests { let trig = Arc::new(RecordingTrigger::default()); let s = ProposalService::new(Arc::new(InMemoryProposalRepository::new())) .with_breakdown_trigger(trig.clone()); - let p = s.create("req-7", "设计").await.expect("create"); + let p = s.create("req-7", "design").await.expect("create"); s.submit_design(&p.id, "doc").await.expect("submit"); s.approve(&p.id).await.expect("approve"); diff --git a/crates/design/src/domain/proposal.rs b/crates/design/src/domain/proposal.rs index 1eac3bba..9ecadead 100644 --- a/crates/design/src/domain/proposal.rs +++ b/crates/design/src/domain/proposal.rs @@ -124,7 +124,7 @@ mod tests { use super::*; fn draft() -> Proposal { - Proposal::new("p1", "req-1", "登录改造设计").expect("new") + Proposal::new("p1", "req-1", "login revamp design").expect("new") } #[test] @@ -141,9 +141,9 @@ mod tests { #[test] fn happy_path_draft_review_approve() { let mut p = draft(); - p.submit_design("## 方案\n用 JWT").expect("submit"); + p.submit_design("## proposal\nuses JWT").expect("submit"); assert_eq!(p.status, ProposalStatus::PendingReview); - assert_eq!(p.design_doc.as_deref(), Some("## 方案\n用 JWT")); + assert_eq!(p.design_doc.as_deref(), Some("## proposal\nuses JWT")); p.approve().expect("approve"); assert_eq!(p.status, ProposalStatus::Approved); assert!(p.status.is_terminal()); @@ -153,14 +153,14 @@ mod tests { fn reject_then_revise_loop() { let mut p = draft(); p.submit_design("v1").expect("submit v1"); - p.request_changes("缺少错误码设计").expect("reject"); + p.request_changes("missing error code design").expect("reject"); assert_eq!(p.status, ProposalStatus::ChangesRequested); assert_eq!(p.revision, 1); - assert_eq!(p.review_comment.as_deref(), Some("缺少错误码设计")); - p.submit_design("v2 含错误码").expect("submit v2"); + assert_eq!(p.review_comment.as_deref(), Some("missing error code design")); + p.submit_design("v2 with error codes").expect("submit v2"); assert_eq!(p.status, ProposalStatus::PendingReview); assert!(p.review_comment.is_none()); - assert_eq!(p.design_doc.as_deref(), Some("v2 含错误码")); + assert_eq!(p.design_doc.as_deref(), Some("v2 with error codes")); p.approve().expect("approve"); assert_eq!(p.status, ProposalStatus::Approved); assert_eq!(p.revision, 1); diff --git a/crates/design/tests/architecture.rs b/crates/design/tests/architecture.rs index 7e5ce513..e3c29d59 100644 --- a/crates/design/tests/architecture.rs +++ b/crates/design/tests/architecture.rs @@ -25,7 +25,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/environment/src/adapters/http.rs b/crates/environment/src/adapters/http.rs index 67fe62ea..1aefea3b 100644 --- a/crates/environment/src/adapters/http.rs +++ b/crates/environment/src/adapters/http.rs @@ -228,7 +228,7 @@ async fn delete_environment( #[openapi( paths(create_environment, list_environments, get_environment, update_environment, delete_environment), components(schemas(EnvironmentResponse, EnvironmentBody, HeaderDto)), - tags((name = "environment", description = "环境")) + tags((name = "environment", description = "Environment")) )] struct ApiDoc; diff --git a/crates/environment/tests/architecture.rs b/crates/environment/tests/architecture.rs index 782cc5fc..2ea8b518 100644 --- a/crates/environment/tests/architecture.rs +++ b/crates/environment/tests/architecture.rs @@ -21,7 +21,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/follow/src/adapters/http.rs b/crates/follow/src/adapters/http.rs index 20587d38..e0313306 100644 --- a/crates/follow/src/adapters/http.rs +++ b/crates/follow/src/adapters/http.rs @@ -161,7 +161,7 @@ async fn mine( #[openapi( paths(follow, unfollow, status, mine), components(schemas(FollowBody, FollowStatus, MineResponse)), - tags((name = "follow", description = "关注人(通用)")) + tags((name = "follow", description = "Followers (generic)")) )] struct ApiDoc; pub fn openapi() -> utoipa::openapi::OpenApi { diff --git a/crates/follow/src/adapters/pg.rs b/crates/follow/src/adapters/pg.rs index 993c3a7e..6b9d319d 100644 --- a/crates/follow/src/adapters/pg.rs +++ b/crates/follow/src/adapters/pg.rs @@ -103,7 +103,7 @@ mod tests { use super::*; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_follow_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); diff --git a/crates/follow/tests/architecture.rs b/crates/follow/tests/architecture.rs index 7e5ce513..e3c29d59 100644 --- a/crates/follow/tests/architecture.rs +++ b/crates/follow/tests/architecture.rs @@ -25,7 +25,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/migrate/tests/migration_versions.rs b/crates/migrate/tests/migration_versions.rs index 27e3ee7b..a4a4b50d 100644 --- a/crates/migrate/tests/migration_versions.rs +++ b/crates/migrate/tests/migration_versions.rs @@ -26,19 +26,20 @@ fn sql_files() -> Vec { fn migration_versions_are_unique() { let mut by_version: HashMap> = HashMap::new(); for name in sql_files() { - let v = - version_of(&name).unwrap_or_else(|| panic!("迁移文件缺少数字版本前缀(NNNN_…):{name}")); + let v = version_of(&name).unwrap_or_else(|| { + panic!("migration file lacks a numeric version prefix (NNNN_…): {name}") + }); by_version.entry(v).or_default().push(name); } let mut dups: Vec<_> = by_version .iter() .filter(|(_, files)| files.len() > 1) - .map(|(v, files)| format!("版本 {v}: {files:?}")) + .map(|(v, files)| format!("version {v}: {files:?}")) .collect(); dups.sort(); assert!( dups.is_empty(), - "迁移版本号重复 —— sqlx 会静默丢失其中的迁移!请重编号:\n{}", + "duplicate migration versions — sqlx will silently drop one of them! renumber them:\n{}", dups.join("\n") ); } @@ -46,7 +47,7 @@ fn migration_versions_are_unique() { #[test] fn migration_names_well_formed() { let bad: Vec = sql_files().into_iter().filter(|n| version_of(n).is_none()).collect(); - assert!(bad.is_empty(), "迁移文件名应为 `NNNN_描述.sql`:{bad:?}"); + assert!(bad.is_empty(), "migration file names must be `NNNN_description.sql`: {bad:?}"); } #[test] diff --git a/crates/mock-runtime/tests/architecture.rs b/crates/mock-runtime/tests/architecture.rs index 4d60fc1f..45e97c6f 100644 --- a/crates/mock-runtime/tests/architecture.rs +++ b/crates/mock-runtime/tests/architecture.rs @@ -24,7 +24,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/notice/src/adapters/http.rs b/crates/notice/src/adapters/http.rs index d53ee436..90b59e63 100644 --- a/crates/notice/src/adapters/http.rs +++ b/crates/notice/src/adapters/http.rs @@ -536,7 +536,7 @@ async fn delete_rule( } #[derive(OpenApi)] -#[openapi(paths(list_notices, unread_count, mark_read, read_all, list_robots, create_robot, update_robot, delete_robot, test_robot, list_rules, create_rule, update_rule, delete_rule), components(schemas(NoticeResponse, NoticePageResponse, UnreadCountResponse, RobotResponse, RobotBody, RobotTestResponse, RuleResponse, RuleBody)), tags((name = "notice", description = "站内信")))] +#[openapi(paths(list_notices, unread_count, mark_read, read_all, list_robots, create_robot, update_robot, delete_robot, test_robot, list_rules, create_rule, update_rule, delete_rule), components(schemas(NoticeResponse, NoticePageResponse, UnreadCountResponse, RobotResponse, RobotBody, RobotTestResponse, RuleResponse, RuleBody)), tags((name = "notice", description = "Notifications")))] struct ApiDoc; pub fn openapi() -> utoipa::openapi::OpenApi { ApiDoc::openapi() @@ -583,7 +583,7 @@ mod tests { project_id: "p1".into(), category: category.into(), event_type: "BUG_ASSIGNED".into(), - title: "登录页崩溃".into(), + title: "login page crash".into(), content: "NEW".into(), resource_type: "BUG".into(), resource_id: "b1".into(), diff --git a/crates/notice/src/adapters/pg.rs b/crates/notice/src/adapters/pg.rs index a764a2fd..bca38ba1 100644 --- a/crates/notice/src/adapters/pg.rs +++ b/crates/notice/src/adapters/pg.rs @@ -226,7 +226,7 @@ mod tests { use super::*; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_notice_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -239,7 +239,7 @@ mod tests { receivers: vec!["u1".into(), "u2".into()], category: "BUG".into(), event_type: "BUG_ASSIGNED".into(), - title: "登录页崩溃".into(), + title: "login page crash".into(), content: String::new(), resource_type: "BUG".into(), resource_id: "b1".into(), diff --git a/crates/notice/src/adapters/robot_sender.rs b/crates/notice/src/adapters/robot_sender.rs index 518e3a20..f2df5060 100644 --- a/crates/notice/src/adapters/robot_sender.rs +++ b/crates/notice/src/adapters/robot_sender.rs @@ -1,5 +1,5 @@ //! Webhook robot sender: formats a text message per platform and posts it. -//! DingTalk supports the optional "加签" mode: HMAC-SHA256 over "{ts}\n{secret}" +//! DingTalk supports the optional "signed" mode: HMAC-SHA256 over "{ts}\n{secret}" //! keyed by the secret, base64 then URL-encoded, appended as timestamp+sign. use std::time::Duration; diff --git a/crates/notice/src/application/notifier.rs b/crates/notice/src/application/notifier.rs index da4b06f2..25ffadcf 100644 --- a/crates/notice/src/application/notifier.rs +++ b/crates/notice/src/application/notifier.rs @@ -195,7 +195,7 @@ mod tests { project_id: "p1".into(), category: category.into(), event_type: event_type.into(), - title: "登录页崩溃".into(), + title: "login page crash".into(), content: String::new(), resource_type: "BUG".into(), resource_id: "b1".into(), @@ -265,7 +265,8 @@ mod tests { #[tokio::test] async fn mentions_resolve_to_user_ids_and_set_at_mention() { let (store, notifier) = services(); - let n = notifier.notify_mentions("@admin @ghost 看一下", event("BUG", "MENTIONED")).await; + let n = + notifier.notify_mentions("@admin @ghost take a look", event("BUG", "MENTIONED")).await; assert_eq!(n, 1); let page = store .list(&ListQuery { @@ -296,7 +297,7 @@ mod rule_tests { project_id: "p1".into(), category: "BUG".into(), event_type: event_type.into(), - title: "登录页崩溃".into(), + title: "login page crash".into(), content: String::new(), resource_type: "BUG".into(), resource_id: "b1".into(), @@ -407,7 +408,7 @@ mod rule_tests { let sent = wait_sent(&f.sender, 1).await; assert_eq!(sent.len(), 1); assert_eq!(sent[0].0, robot_id); - assert_eq!(sent[0].1, "登录页崩溃 / admin"); + assert_eq!(sent[0].1, "login page crash / admin"); } #[tokio::test] @@ -419,7 +420,7 @@ mod rule_tests { assert_eq!(inbox_total(&f.store).await, 1); let sent = wait_sent(&f.sender, 1).await; // Default template: title + operator + time. - assert!(sent[0].1.contains("登录页崩溃") && sent[0].1.contains("admin")); + assert!(sent[0].1.contains("login page crash") && sent[0].1.contains("admin")); } #[tokio::test] diff --git a/crates/notice/src/domain/notice.rs b/crates/notice/src/domain/notice.rs index bf58fc7c..c93c46b0 100644 --- a/crates/notice/src/domain/notice.rs +++ b/crates/notice/src/domain/notice.rs @@ -149,7 +149,10 @@ mod tests { #[test] fn parse_mentions_extracts_names() { - assert_eq!(parse_mentions("cc @admin 和 @u-admin, 看下"), vec!["admin", "u-admin"]); + assert_eq!( + parse_mentions("cc @admin and @u-admin, please check"), + vec!["admin", "u-admin"] + ); assert_eq!(parse_mentions("@a.b. end"), vec!["a.b"]); assert_eq!(parse_mentions("mail me at x@example.com"), vec!["example.com"]); assert!(parse_mentions("no mentions @ all").is_empty()); diff --git a/crates/notice/tests/architecture.rs b/crates/notice/tests/architecture.rs index 7e5ce513..e3c29d59 100644 --- a/crates/notice/tests/architecture.rs +++ b/crates/notice/tests/architecture.rs @@ -25,7 +25,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/orchestrator/src/application/delivery_feedback.rs b/crates/orchestrator/src/application/delivery_feedback.rs index 17d46182..73baec52 100644 --- a/crates/orchestrator/src/application/delivery_feedback.rs +++ b/crates/orchestrator/src/application/delivery_feedback.rs @@ -250,7 +250,7 @@ mod tests { async fn terminal_auto_links_task_criteria_before_sync() { let task = Arc::new(FakeTask { map: vec![("d1".into(), "req1".into(), 1)], - criteria: vec!["登录成功".into()], + criteria: vec!["login success".into()], ..Default::default() }); let verif = Arc::new(FakeVerif { @@ -264,7 +264,7 @@ mod tests { .expect("ok"); let linked = verif.linked.lock().unwrap_or_else(std::sync::PoisonError::into_inner); assert_eq!(linked.len(), 1); - assert_eq!(linked[0], ("v1/t1".to_string(), vec!["登录成功".to_string()])); + assert_eq!(linked[0], ("v1/t1".to_string(), vec!["login success".to_string()])); assert!(verif.synced.lock().unwrap_or_else(std::sync::PoisonError::into_inner)[0].1); } @@ -342,7 +342,7 @@ mod tests { let mut n = self.calls.lock().unwrap_or_else(std::sync::PoisonError::into_inner); *n += 1; if *n >= self.fix_after { - Ok(dv("branch:fixed", "已据反馈补齐")) + Ok(dv("branch:fixed", "completed per feedback")) } else { Ok(dv("branch:retry", "")) } diff --git a/crates/orchestrator/src/judges.rs b/crates/orchestrator/src/judges.rs index 16eaf11a..3f7e70b1 100644 --- a/crates/orchestrator/src/judges.rs +++ b/crates/orchestrator/src/judges.rs @@ -17,11 +17,11 @@ pub struct RuleJudge; impl Judge for RuleJudge { async fn judge(&self, _criteria: &[String], deliverable: &DeliverableView) -> Verdict { if deliverable.reference.trim().is_empty() { - Verdict { passed: false, reason: "交付物缺少 reference".into() } + Verdict { passed: false, reason: "deliverable is missing reference".into() } } else if deliverable.summary.trim().is_empty() { - Verdict { passed: false, reason: "交付物缺少 summary".into() } + Verdict { passed: false, reason: "deliverable is missing summary".into() } } else { - Verdict { passed: true, reason: "交付物完整".into() } + Verdict { passed: true, reason: "deliverable is complete".into() } } } } diff --git a/crates/orchestrator/tests/architecture.rs b/crates/orchestrator/tests/architecture.rs index 7239dd5a..f1302698 100644 --- a/crates/orchestrator/tests/architecture.rs +++ b/crates/orchestrator/tests/architecture.rs @@ -25,7 +25,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/perf/src/adapters/engine.rs b/crates/perf/src/adapters/engine.rs index 9f03cab2..d2ee9dac 100644 --- a/crates/perf/src/adapters/engine.rs +++ b/crates/perf/src/adapters/engine.rs @@ -110,8 +110,8 @@ mod tests { // n=usize::MAX keeps the fast spin loop always succeeding, so the failure threshold never trips. let exec = Arc::new(EveryNthFails { calls: AtomicUsize::new(0), n: usize::MAX }); let report = run_load(&plan, exec).await; - assert!(report.total > 0, "时长模式应至少跑若干次: {report:?}"); - assert!(report.elapsed_ms >= 70, "耗时应≈时长: {}", report.elapsed_ms); + assert!(report.total > 0, "duration mode should run at least a few times: {report:?}"); + assert!(report.elapsed_ms >= 70, "elapsed should be ≈ the duration: {}", report.elapsed_ms); assert_eq!(report.failed, 0); } } diff --git a/crates/perf/tests/architecture.rs b/crates/perf/tests/architecture.rs index 4d60fc1f..45e97c6f 100644 --- a/crates/perf/tests/architecture.rs +++ b/crates/perf/tests/architecture.rs @@ -24,7 +24,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/perf/tests/load_e2e.rs b/crates/perf/tests/load_e2e.rs index b524352c..ce7eaca4 100644 --- a/crates/perf/tests/load_e2e.rs +++ b/crates/perf/tests/load_e2e.rs @@ -50,7 +50,10 @@ async fn native_load_run_produces_report() { assert_eq!(report.total, 30); assert_eq!(report.success + report.failed, 30); - assert!(report.failed >= 1, "每 10 次一个 500,应有失败: {report:?}"); + assert!( + report.failed >= 1, + "one 500 every 10 requests, so there should be failures: {report:?}" + ); assert!(report.throughput_rps > 0.0); assert!(report.latency.p50 <= report.latency.p95); assert!(report.latency.p95 <= report.latency.max); diff --git a/crates/perf/tests/probe_load_e2e.rs b/crates/perf/tests/probe_load_e2e.rs index 708194b0..9512cfb7 100644 --- a/crates/perf/tests/probe_load_e2e.rs +++ b/crates/perf/tests/probe_load_e2e.rs @@ -52,7 +52,10 @@ async fn perf_through_probe_registry_produces_report() { assert_eq!(report.total, 30); assert_eq!(report.success + report.failed, 30); - assert!(report.failed >= 1, "每 10 次一个 500,断言应判失败: {report:?}"); + assert!( + report.failed >= 1, + "one 500 every 10 requests, so the assertion should fail: {report:?}" + ); assert!(report.throughput_rps > 0.0); assert!(report.latency.p50 <= report.latency.p95); assert!(report.latency.p95 <= report.latency.max); diff --git a/crates/pool-runner/src/protocol.rs b/crates/pool-runner/src/protocol.rs index 4c625b44..0e99d2ca 100644 --- a/crates/pool-runner/src/protocol.rs +++ b/crates/pool-runner/src/protocol.rs @@ -170,11 +170,12 @@ mod tests { #[test] fn hello_accepts_pool_name_without_id() { let m: RunnerMsg = - serde_json::from_str(r#"{"type":"hello","poolName":"本地池","name":"n"}"#).expect("de"); + serde_json::from_str(r#"{"type":"hello","poolName":"local pool","name":"n"}"#) + .expect("de"); match m { RunnerMsg::Hello { pool_id, pool_name, .. } => { assert_eq!(pool_id, None); - assert_eq!(pool_name.as_deref(), Some("本地池")); + assert_eq!(pool_name.as_deref(), Some("local pool")); } other => panic!("expected hello, got {other:?}"), } diff --git a/crates/probe/src/domain/probe.rs b/crates/probe/src/domain/probe.rs index e95a7216..a3249ef5 100644 --- a/crates/probe/src/domain/probe.rs +++ b/crates/probe/src/domain/probe.rs @@ -39,20 +39,20 @@ impl ProbeAssertion { if raw.transport_ok { None } else { - Some(format!("传输失败: {}", raw.error.as_deref().unwrap_or("unknown"))) + Some(format!("transport failed: {}", raw.error.as_deref().unwrap_or("unknown"))) } } Self::StatusIs(want) => match raw.status { Some(got) if got == *want => None, - Some(got) => Some(format!("status: 期望 {want},实际 {got}")), - None => Some(format!("status: 期望 {want},无状态码")), + Some(got) => Some(format!("status: expected {want}, actual {got}")), + None => Some(format!("status: expected {want}, no status code")), }, Self::OutputContains(sub) => { let out = raw.output.as_deref().unwrap_or(""); if out.contains(sub.as_str()) { None } else { - Some(format!("output 不含子串: {sub}")) + Some(format!("output does not contain substring: {sub}")) } } Self::OutputEquals(want) => { @@ -60,14 +60,14 @@ impl ProbeAssertion { if out == want { None } else { - Some("output 不等于期望值".to_string()) + Some("output does not equal the expected value".to_string()) } } Self::LatencyUnderMs(max) => { if raw.latency_ms <= *max { None } else { - Some(format!("latency {}ms 超过 {max}ms", raw.latency_ms)) + Some(format!("latency {}ms exceeds {max}ms", raw.latency_ms)) } } } @@ -94,7 +94,8 @@ impl ProbeOutcome { pub fn from_raw(raw: RawProbe, assertions: &[ProbeAssertion]) -> Self { let mut failures = evaluate(assertions, &raw); if !raw.transport_ok && !assertions.iter().any(|a| matches!(a, ProbeAssertion::Success)) { - failures.push(format!("传输失败: {}", raw.error.as_deref().unwrap_or("unknown"))); + failures + .push(format!("transport failed: {}", raw.error.as_deref().unwrap_or("unknown"))); } Self { success: raw.transport_ok && failures.is_empty(), @@ -146,7 +147,7 @@ mod tests { RawProbe { transport_ok: false, error: Some("refused".into()), ..Default::default() }; let f = evaluate(&[ProbeAssertion::Success], &raw); assert_eq!(f.len(), 1); - assert!(f[0].contains("传输失败")); + assert!(f[0].contains("transport failed")); assert!(!ProbeOutcome::from_raw(raw, &[]).success); } diff --git a/crates/probe/src/plugins/ssh.rs b/crates/probe/src/plugins/ssh.rs index 8225eb12..286f0073 100644 --- a/crates/probe/src/plugins/ssh.rs +++ b/crates/probe/src/plugins/ssh.rs @@ -35,21 +35,23 @@ async fn run_ssh( password: &str, ) -> Result<(String, i64), String> { let (host, port) = match target.rsplit_once(':') { - Some((h, p)) => (h.to_string(), p.parse::().map_err(|_| "端口非法".to_string())?), + Some((h, p)) => (h.to_string(), p.parse::().map_err(|_| "invalid port".to_string())?), None => (target.to_string(), 22u16), }; let config = Arc::new(client::Config::default()); let mut handle = client::connect(config, (host.as_str(), port), AcceptAll) .await - .map_err(|e| format!("连接失败: {e}"))?; - let authed = - handle.authenticate_password(user, password).await.map_err(|e| format!("认证错误: {e}"))?; + .map_err(|e| format!("connection failed: {e}"))?; + let authed = handle + .authenticate_password(user, password) + .await + .map_err(|e| format!("authentication error: {e}"))?; if !authed.success() { - return Err("认证失败(用户名/密码)".to_string()); + return Err("authentication failed (username/password)".to_string()); } let mut channel = - handle.channel_open_session().await.map_err(|e| format!("开通道失败: {e}"))?; - channel.exec(true, command).await.map_err(|e| format!("执行失败: {e}"))?; + handle.channel_open_session().await.map_err(|e| format!("failed to open channel: {e}"))?; + channel.exec(true, command).await.map_err(|e| format!("execution failed: {e}"))?; let mut out: Vec = Vec::new(); let mut code: i64 = 0; diff --git a/crates/probe/tests/architecture.rs b/crates/probe/tests/architecture.rs index 2847fcac..1c35ab20 100644 --- a/crates/probe/tests/architecture.rs +++ b/crates/probe/tests/architecture.rs @@ -25,7 +25,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 plugins)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in plugins)", + path.display() + ); } } } diff --git a/crates/probe/tests/mysql_live.rs b/crates/probe/tests/mysql_live.rs index b414238f..96c94b9b 100644 --- a/crates/probe/tests/mysql_live.rs +++ b/crates/probe/tests/mysql_live.rs @@ -18,29 +18,32 @@ fn req(stmt: &str, assertions: Vec) -> ProbeRequest { } #[tokio::test] -#[ignore = "需要 MySQL"] +#[ignore = "requires MySQL"] async fn select_and_ddl_through_registry() { let reg = default_registry(); - assert!(reg.protocols().contains(&"mysql".to_string()), "mysql 插件应在注册表"); + assert!( + reg.protocols().contains(&"mysql".to_string()), + "mysql plugin should be in the registry" + ); let out = reg.dispatch(&req("SELECT 1", vec![ProbeAssertion::Success])).await; - assert!(out.success, "SELECT 1 应成功: {out:?}"); + assert!(out.success, "SELECT 1 should succeed: {out:?}"); let _ = reg.dispatch(&req("DROP TABLE IF EXISTS shep_probe_t", vec![])).await; let out = reg .dispatch(&req("CREATE TABLE shep_probe_t (id INT)", vec![ProbeAssertion::Success])) .await; - assert!(out.success, "建表应成功: {out:?}"); + assert!(out.success, "CREATE TABLE should succeed: {out:?}"); let out = reg .dispatch(&req( "INSERT INTO shep_probe_t VALUES (1)", vec![ProbeAssertion::OutputContains("rows_affected=1".into())], )) .await; - assert!(out.success, "插入一行应 rows_affected=1: {out:?}"); + assert!(out.success, "inserting one row should give rows_affected=1: {out:?}"); let out = reg.dispatch(&req("NOT SQL", vec![])).await; - assert!(!out.success, "非法语句应失败: {out:?}"); + assert!(!out.success, "an invalid statement should fail: {out:?}"); let _ = reg.dispatch(&req("DROP TABLE IF EXISTS shep_probe_t", vec![])).await; } diff --git a/crates/probe/tests/redis_live.rs b/crates/probe/tests/redis_live.rs index f28464c2..525777fc 100644 --- a/crates/probe/tests/redis_live.rs +++ b/crates/probe/tests/redis_live.rs @@ -17,10 +17,13 @@ fn req(cmd: &str, assertions: Vec) -> ProbeRequest { } #[tokio::test] -#[ignore = "需要 Redis"] +#[ignore = "requires Redis"] async fn ping_set_get_through_registry() { let reg = default_registry(); - assert!(reg.protocols().contains(&"redis".to_string()), "redis 插件应在注册表"); + assert!( + reg.protocols().contains(&"redis".to_string()), + "redis plugin should be in the registry" + ); let out = reg .dispatch(&req( @@ -28,21 +31,21 @@ async fn ping_set_get_through_registry() { vec![ProbeAssertion::Success, ProbeAssertion::OutputEquals("PONG".into())], )) .await; - assert!(out.success, "PING 应成功: {out:?}"); + assert!(out.success, "PING should succeed: {out:?}"); let out = reg .dispatch(&req("SET shep_probe_k hello", vec![ProbeAssertion::OutputEquals("OK".into())])) .await; - assert!(out.success, "SET 应回 OK: {out:?}"); + assert!(out.success, "SET should return OK: {out:?}"); let out = reg .dispatch(&req("GET shep_probe_k", vec![ProbeAssertion::OutputContains("hello".into())])) .await; - assert!(out.success, "GET 应回 hello: {out:?}"); + assert!(out.success, "GET should return hello: {out:?}"); assert_eq!(out.output.as_deref(), Some("hello")); let out = reg .dispatch(&req("GET shep_probe_absent", vec![ProbeAssertion::OutputContains("x".into())])) .await; - assert!(!out.success, "缺失键不应命中断言: {out:?}"); + assert!(!out.success, "a missing key should not satisfy the assertion: {out:?}"); } diff --git a/crates/probe/tests/websocket_e2e.rs b/crates/probe/tests/websocket_e2e.rs index c882b850..f4d033be 100644 --- a/crates/probe/tests/websocket_e2e.rs +++ b/crates/probe/tests/websocket_e2e.rs @@ -32,7 +32,10 @@ async fn spawn_echo() -> String { async fn ws_probe_echo_through_registry() { let base = spawn_echo().await; let reg = default_registry(); - assert!(reg.protocols().contains(&"websocket".to_string()), "websocket 插件应在注册表"); + assert!( + reg.protocols().contains(&"websocket".to_string()), + "websocket plugin should be in the registry" + ); let out = reg .dispatch(&ProbeRequest { @@ -46,7 +49,10 @@ async fn ws_probe_echo_through_registry() { ], }) .await; - assert!(out.success, "echo 应回原文且断言通过: {out:?}"); + assert!( + out.success, + "echo should return the original payload and assertions should pass: {out:?}" + ); assert_eq!(out.output.as_deref(), Some("hello probe")); let out = reg @@ -58,6 +64,6 @@ async fn ws_probe_echo_through_registry() { assertions: vec![ProbeAssertion::Success], }) .await; - assert!(out.success, "握手应成功: {out:?}"); + assert!(out.success, "handshake should succeed: {out:?}"); assert_eq!(out.output.as_deref(), Some("connected")); } diff --git a/crates/project/src/adapters/http.rs b/crates/project/src/adapters/http.rs index eeb2aee9..c04ad84b 100644 --- a/crates/project/src/adapters/http.rs +++ b/crates/project/src/adapters/http.rs @@ -137,7 +137,7 @@ async fn list_projects( } #[derive(OpenApi)] -#[openapi(paths(create_project, list_projects), components(schemas(CreateProjectRequest, ProjectResponse, PageResponse)), tags((name = "project", description = "项目管理")))] +#[openapi(paths(create_project, list_projects), components(schemas(CreateProjectRequest, ProjectResponse, PageResponse)), tags((name = "project", description = "Project management")))] struct ApiDoc; pub fn openapi() -> utoipa::openapi::OpenApi { ApiDoc::openapi() diff --git a/crates/project/src/adapters/member_http.rs b/crates/project/src/adapters/member_http.rs index 6d9a4be8..fbab6113 100644 --- a/crates/project/src/adapters/member_http.rs +++ b/crates/project/src/adapters/member_http.rs @@ -140,7 +140,7 @@ async fn remove_member( #[openapi( paths(add_member, list_members, remove_member), components(schemas(AddMemberRequest, MemberResponse)), - tags((name = "project", description = "项目管理")) + tags((name = "project", description = "Project management")) )] struct ApiDoc; pub fn openapi() -> utoipa::openapi::OpenApi { diff --git a/crates/project/src/adapters/pg.rs b/crates/project/src/adapters/pg.rs index 1867292a..1ec3b7f7 100644 --- a/crates/project/src/adapters/pg.rs +++ b/crates/project/src/adapters/pg.rs @@ -101,7 +101,7 @@ mod tests { use super::*; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_soft_delete_frees_name_and_partial_index_allows_recreate() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); diff --git a/crates/project/src/adapters/pg_template.rs b/crates/project/src/adapters/pg_template.rs index 08cc7289..988e25f4 100644 --- a/crates/project/src/adapters/pg_template.rs +++ b/crates/project/src/adapters/pg_template.rs @@ -128,7 +128,7 @@ mod tests { use serde_json::json; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_template_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -136,25 +136,25 @@ mod tests { sqlx::query("TRUNCATE ms_template").execute(&pool).await.expect("truncate"); let repo = PgTemplateRepository::new(pool.clone()); - let nu = NewTemplate::new("p1", "requirement", "默认", json!({"fields": [1]}), "admin") + let nu = NewTemplate::new("p1", "requirement", "default", json!({"fields": [1]}), "admin") .expect("valid"); let t = repo.insert(&nu).await.expect("insert"); assert!(t.created_at_ms > 0); assert_eq!(t.config, json!({"fields": [1]})); - assert!(repo.find_by_name("p1", "requirement", "默认").await.expect("q").is_some()); + assert!(repo.find_by_name("p1", "requirement", "default").await.expect("q").is_some()); - repo.insert(&NewTemplate::new("p1", "bug", "默认", json!({}), "admin").expect("valid")) + repo.insert(&NewTemplate::new("p1", "bug", "default", json!({}), "admin").expect("valid")) .await .expect("other kind"); assert_eq!(repo.list("p1", None).await.expect("list").len(), 2); assert_eq!(repo.list("p1", Some("requirement")).await.expect("list").len(), 1); let mut renamed = t.clone(); - renamed.name = "改名".to_string(); + renamed.name = "rename".to_string(); renamed.config = json!({"a": true}); let after = repo.update(&renamed).await.expect("update").expect("exists"); - assert_eq!(after.name, "改名"); + assert_eq!(after.name, "rename"); assert_eq!(after.config, json!({"a": true})); assert!(after.updated_at_ms >= after.created_at_ms); assert!(repo diff --git a/crates/project/src/adapters/template_http.rs b/crates/project/src/adapters/template_http.rs index 3dc43903..8687e46e 100644 --- a/crates/project/src/adapters/template_http.rs +++ b/crates/project/src/adapters/template_http.rs @@ -207,7 +207,7 @@ async fn delete_template( TemplateResponse, TemplateListResponse )), - tags((name = "template", description = "项目模板")) + tags((name = "template", description = "Project template")) )] struct ApiDoc; pub fn openapi() -> utoipa::openapi::OpenApi { @@ -256,7 +256,7 @@ mod tests { .oneshot(json_req( "POST", "/project/p1/template", - Some(r#"{"kind":"Requirement","name":"默认","config":{"fields":[1]}}"#), + Some(r#"{"kind":"Requirement","name":"default","config":{"fields":[1]}}"#), Some(&t), )) .await @@ -265,7 +265,7 @@ mod tests { let v = body_json(resp).await; assert_eq!(v["projectId"], "p1"); assert_eq!(v["kind"], "requirement"); // normalized to lowercase - assert_eq!(v["name"], "默认"); + assert_eq!(v["name"], "default"); assert_eq!(v["config"], serde_json::json!({"fields": [1]})); assert_eq!(v["createdBy"], "admin"); assert!(v["createdAt"].as_i64().expect("ms") > 0); @@ -279,7 +279,7 @@ mod tests { let v = body_json(resp).await; let items = v["items"].as_array().expect("items"); assert_eq!(items.len(), 1); - assert_eq!(items[0]["name"], "默认"); + assert_eq!(items[0]["name"], "default"); } #[tokio::test] @@ -327,7 +327,7 @@ mod tests { #[tokio::test] async fn create_duplicate_name_409() { let (app, t) = app_with("PROJECT:UPDATE").await; - let body = r#"{"kind":"requirement","name":"默认"}"#; + let body = r#"{"kind":"requirement","name":"default"}"#; let resp = app .clone() .oneshot(json_req("POST", "/project/p1/template", Some(body), Some(&t))) diff --git a/crates/project/src/application/template_service.rs b/crates/project/src/application/template_service.rs index a27b0ff0..b9c71320 100644 --- a/crates/project/src/application/template_service.rs +++ b/crates/project/src/application/template_service.rs @@ -105,7 +105,7 @@ mod tests { async fn create_then_list() { let s = svc(); let t = s - .create("p1", "Requirement", "默认", json!({"fields": []}), "admin") + .create("p1", "Requirement", "default", json!({"fields": []}), "admin") .await .expect("create"); assert_eq!(t.kind, "requirement"); @@ -114,33 +114,33 @@ mod tests { assert_eq!(t.created_at_ms, t.updated_at_ms); let list = s.list("p1", None).await.expect("list"); assert_eq!(list.len(), 1); - assert_eq!(list[0].name, "默认"); + assert_eq!(list[0].name, "default"); } #[tokio::test] async fn duplicate_name_same_project_kind_conflicts() { let s = svc(); - s.create("p1", "requirement", "默认", json!({}), "admin").await.expect("create"); + s.create("p1", "requirement", "default", json!({}), "admin").await.expect("create"); // kind/name are compared after normalization: case and whitespace cannot bypass uniqueness. assert_eq!( - s.create("p1", " Requirement ", " 默认 ", json!({}), "admin").await, + s.create("p1", " Requirement ", " default ", json!({}), "admin").await, Err(TemplateCmdError::NameExists) ); // A different kind or a different project does not conflict. - assert!(s.create("p1", "bug", "默认", json!({}), "admin").await.is_ok()); - assert!(s.create("p2", "requirement", "默认", json!({}), "admin").await.is_ok()); + assert!(s.create("p1", "bug", "default", json!({}), "admin").await.is_ok()); + assert!(s.create("p2", "requirement", "default", json!({}), "admin").await.is_ok()); } #[tokio::test] async fn update_name_and_config_independently() { let s = svc(); - let t = s.create("p1", "requirement", "默认", json!({}), "admin").await.expect("create"); - let t2 = s.update(&t.id, Some("改名"), None).await.expect("rename"); - assert_eq!(t2.name, "改名"); + let t = s.create("p1", "requirement", "default", json!({}), "admin").await.expect("create"); + let t2 = s.update(&t.id, Some("rename"), None).await.expect("rename"); + assert_eq!(t2.name, "rename"); assert_eq!(t2.config, json!({})); // config untouched assert!(t2.updated_at_ms > t2.created_at_ms); let t3 = s.update(&t.id, None, Some(json!({"a": 1}))).await.expect("reconfig"); - assert_eq!(t3.name, "改名"); // name untouched + assert_eq!(t3.name, "rename"); // name untouched assert_eq!(t3.config, json!({"a": 1})); } diff --git a/crates/project/src/domain/template.rs b/crates/project/src/domain/template.rs index ff922d8e..8c0ff221 100644 --- a/crates/project/src/domain/template.rs +++ b/crates/project/src/domain/template.rs @@ -125,7 +125,7 @@ mod tests { #[test] fn name_is_trimmed_and_length_checked() { - assert_eq!(normalize_name(" 默认模板 ").expect("ok"), "默认模板"); + assert_eq!(normalize_name(" default template ").expect("ok"), "default template"); assert_eq!(normalize_name(" "), Err(TemplateError::EmptyName)); assert_eq!(normalize_name(&"n".repeat(65)), Err(TemplateError::NameTooLong)); assert!(normalize_name(&"n".repeat(64)).is_ok()); @@ -142,11 +142,11 @@ mod tests { #[test] fn new_template_normalizes_and_validates() { - let t = NewTemplate::new(" p1 ", " Requirement ", " 默认 ", json!({}), " admin ") + let t = NewTemplate::new(" p1 ", " Requirement ", " default ", json!({}), " admin ") .expect("valid"); assert_eq!(t.project_id, "p1"); assert_eq!(t.kind, "requirement"); - assert_eq!(t.name, "默认"); + assert_eq!(t.name, "default"); assert_eq!(t.created_by, "admin"); } diff --git a/crates/project/tests/architecture.rs b/crates/project/tests/architecture.rs index 643f0b7d..0dd3436b 100644 --- a/crates/project/tests/architecture.rs +++ b/crates/project/tests/architecture.rs @@ -24,7 +24,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/rag/src/adapters.rs b/crates/rag/src/adapters.rs index 19484e49..c93e2622 100644 --- a/crates/rag/src/adapters.rs +++ b/crates/rag/src/adapters.rs @@ -47,7 +47,7 @@ impl Embedder for OpenAiEmbedder { let (url, key, model) = self.snapshot(); if key.is_empty() { return Err(RagError::Config( - "embedding API key not configured (系统参数 → RAG 配置)".into(), + "embedding API key not configured (System Params → RAG config)".into(), )); } // Volcano Engine multimodal embeddings take one {type:text} input at a time and return @@ -138,7 +138,7 @@ impl Chat for OpenAiChat { let (url, key, model, max_tokens) = self.snapshot(); if key.is_empty() { return Err(RagError::Config( - "LLM API key not configured (系统参数 → RAG 配置)".into(), + "LLM API key not configured (System Params → RAG config)".into(), )); } let resp = self @@ -182,7 +182,7 @@ impl Chat for OpenAiChat { let (url, key, model, max_tokens) = self.snapshot(); if key.is_empty() { return Err(RagError::Config( - "LLM API key not configured (系统参数 → RAG 配置)".into(), + "LLM API key not configured (System Params → RAG config)".into(), )); } let resp = self diff --git a/crates/rag/src/http.rs b/crates/rag/src/http.rs index 12b16a35..e6a7ddad 100644 --- a/crates/rag/src/http.rs +++ b/crates/rag/src/http.rs @@ -90,7 +90,7 @@ async fn require_project_member( } /// Map a RagError to an HTTP response. A missing/blank provider config is a precondition-not-met, not -/// a server fault → 503 with the actionable message (so the UI can say "先去系统参数配置 RAG"); genuine +/// a server fault → 503 with the actionable message (so the UI can say "configure RAG in system settings"); genuine /// failures stay 500. fn rag_error_response(e: RagError) -> Response { match e { @@ -100,7 +100,7 @@ fn rag_error_response(e: RagError) -> Response { } /// Machine-readable code for an SSE `error` event so the Q&A UI can give a specific hint (e.g. a -/// "去配置" prompt for an unconfigured provider) instead of just echoing the raw message. +/// "go configure" prompt for an unconfigured provider) instead of just echoing the raw message. fn rag_error_code(e: &RagError) -> &'static str { match e { RagError::Config(_) => "not_configured", diff --git a/crates/requirement/src/adapters/http.rs b/crates/requirement/src/adapters/http.rs index 0857d59a..a0f2df1a 100644 --- a/crates/requirement/src/adapters/http.rs +++ b/crates/requirement/src/adapters/http.rs @@ -472,7 +472,7 @@ async fn list_requirements( } } -#[utoipa::path(get, path = "/requirement/{id}", tag = "requirement", params(("id" = String, Path, description = "需求 id")), responses((status = 200, body = RequirementResponse), (status = 403), (status = 404)), security(("bearer" = [])))] +#[utoipa::path(get, path = "/requirement/{id}", tag = "requirement", params(("id" = String, Path, description = "Requirement id")), responses((status = 200, body = RequirementResponse), (status = 403), (status = 404)), security(("bearer" = [])))] async fn get_requirement( user: AuthUser, State(st): State, @@ -487,7 +487,7 @@ async fn get_requirement( } } -#[utoipa::path(get, path = "/requirement/{id}/version/{n}", tag = "requirement", params(("id" = String, Path, description = "需求 id"), ("n" = u32, Path, description = "版本号")), responses((status = 200, body = VersionResponse), (status = 403), (status = 404)), security(("bearer" = [])))] +#[utoipa::path(get, path = "/requirement/{id}/version/{n}", tag = "requirement", params(("id" = String, Path, description = "Requirement id"), ("n" = u32, Path, description = "Version number")), responses((status = 200, body = VersionResponse), (status = 403), (status = 404)), security(("bearer" = [])))] async fn get_version( user: AuthUser, State(st): State, @@ -623,7 +623,7 @@ async fn deliver_requirement( /// Advance a pipeline stage: optionally change status (first transition to IN_PROGRESS/DONE /// stamps timestamps; repeats do not overwrite) and optionally set/clear planned dates /// (empty string clears); unknown stage/status → 400. -#[utoipa::path(put, path = "/requirement/{id}/stage/{stage}", tag = "requirement", params(("id" = String, Path, description = "需求 id"), ("stage" = String, Path, description = "阶段:CREATED/AUDIT/REVIEW/DEV/TEST/ACCEPTANCE/DELIVERY")), request_body = StageBody, responses((status = 200, body = RequirementResponse), (status = 400), (status = 403), (status = 404)), security(("bearer" = [])))] +#[utoipa::path(put, path = "/requirement/{id}/stage/{stage}", tag = "requirement", params(("id" = String, Path, description = "Requirement id"), ("stage" = String, Path, description = "Stage:CREATED/AUDIT/REVIEW/DEV/TEST/ACCEPTANCE/DELIVERY")), request_body = StageBody, responses((status = 200, body = RequirementResponse), (status = 400), (status = 403), (status = 404)), security(("bearer" = [])))] async fn set_stage_route( user: AuthUser, State(st): State, @@ -801,7 +801,7 @@ async fn requirement_summary( set_stage_route, set_parent, get_children, get_changes ), components(schemas(CreateBody, ReviseBody, SetBaselineBody, RejectReviewBody, RenameBody, StageBody, SetParentBody, ReorderBody, SummaryResponse, VersionResponse, StageResponse, RequirementResponse, RequirementPage, VersionCreated, ChangeResponse, ChildrenResponse, ChangesResponse)), - tags((name = "requirement", description = "需求管理(多版本)")) + tags((name = "requirement", description = "Requirement management (multi-version)")) )] struct ApiDoc; @@ -954,7 +954,7 @@ mod tests { let (app, t) = app_with("REQUIREMENT:READ+ADD+UPDATE+DELETE").await; let r = app .clone() - .oneshot(req("POST", "/requirement", r#"{"projectId":"p1","title":"登录","description":"d","acceptanceCriteria":["c1"]}"#, Some(&t))) + .oneshot(req("POST", "/requirement", r#"{"projectId":"p1","title":"login","description":"d","acceptanceCriteria":["c1"]}"#, Some(&t))) .await .expect("r"); assert_eq!(r.status(), StatusCode::CREATED); @@ -1029,7 +1029,12 @@ mod tests { assert_eq!( app.clone() - .oneshot(req("PUT", &format!("/requirement/{id}"), r#"{"title":"登入"}"#, Some(&t))) + .oneshot(req( + "PUT", + &format!("/requirement/{id}"), + r#"{"title":"sign in"}"#, + Some(&t) + )) .await .expect("r") .status(), @@ -1080,7 +1085,7 @@ mod tests { let (app, t) = app_with("REQUIREMENT:READ+ADD+UPDATE").await; let r = app .clone() - .oneshot(req("POST", "/requirement", r#"{"projectId":"p1","title":"登录","acceptanceCriteria":["登录成功跳转首页","错误密码拒绝并提示"]}"#, Some(&t))) + .oneshot(req("POST", "/requirement", r#"{"projectId":"p1","title":"login","acceptanceCriteria":["login success redirects to home","wrong password rejected with a message"]}"#, Some(&t))) .await .expect("r"); assert_eq!(r.status(), StatusCode::CREATED); @@ -1111,14 +1116,17 @@ mod tests { assert_eq!(d["baselineVersion"], 1); assert_eq!( crits_at_baseline(&d), - vec!["登录成功跳转首页".to_string(), "错误密码拒绝并提示".to_string()] + vec![ + "login success redirects to home".to_string(), + "wrong password rejected with a message".to_string() + ] ); app.clone() .oneshot(req( "POST", &format!("/requirement/{id}/version"), - r#"{"description":"v2","acceptanceCriteria":["新增验收点"]}"#, + r#"{"description":"v2","acceptanceCriteria":["new acceptance criterion"]}"#, Some(&t), )) .await @@ -1137,7 +1145,7 @@ mod tests { ) .await; assert_eq!(d2["baselineVersion"], 2); - assert_eq!(crits_at_baseline(&d2), vec!["新增验收点".to_string()]); + assert_eq!(crits_at_baseline(&d2), vec!["new acceptance criterion".to_string()]); } #[tokio::test] @@ -1148,7 +1156,7 @@ mod tests { .oneshot(req( "POST", "/requirement", - r#"{"projectId":"p1","title":"登录","acceptanceCriteria":["c1"]}"#, + r#"{"projectId":"p1","title":"login","acceptanceCriteria":["c1"]}"#, Some(&t), )) .await @@ -1174,7 +1182,7 @@ mod tests { .oneshot(req( "POST", &format!("/requirement/{id}/review/reject"), - r#"{"comment":"验收标准不完整"}"#, + r#"{"comment":"acceptance criteria incomplete"}"#, Some(&t), )) .await @@ -1182,7 +1190,7 @@ mod tests { assert_eq!(rj.status(), StatusCode::OK); let v = body_json(rj).await; assert_eq!(v["status"], "DRAFT"); - assert_eq!(v["reviewComment"], "验收标准不完整"); + assert_eq!(v["reviewComment"], "acceptance criteria incomplete"); let sb = app .clone() @@ -1217,7 +1225,7 @@ mod tests { let (app, t) = app_with("REQUIREMENT:READ+ADD").await; let r = app .clone() - .oneshot(req("POST", "/requirement", r#"{"projectId":"p1","title":"登录"}"#, Some(&t))) + .oneshot(req("POST", "/requirement", r#"{"projectId":"p1","title":"login"}"#, Some(&t))) .await .expect("r"); assert_eq!(r.status(), StatusCode::CREATED); @@ -1242,7 +1250,7 @@ mod tests { .oneshot(req( "POST", "/requirement", - r#"{"projectId":"p1","title":"登录","priority":" p0 ","reqType":"tech_debt"}"#, + r#"{"projectId":"p1","title":"login","priority":" p0 ","reqType":"tech_debt"}"#, Some(&t), )) .await @@ -1261,7 +1269,7 @@ mod tests { .oneshot(req( "POST", "/requirement", - r#"{"projectId":"p1","title":"登录","priority":"P9"}"#, + r#"{"projectId":"p1","title":"login","priority":"P9"}"#, Some(&t) )) .await @@ -1273,7 +1281,7 @@ mod tests { app.oneshot(req( "POST", "/requirement", - r#"{"projectId":"p1","title":"登录","reqType":"EPIC"}"#, + r#"{"projectId":"p1","title":"login","reqType":"EPIC"}"#, Some(&t) )) .await @@ -1286,14 +1294,14 @@ mod tests { #[tokio::test] async fn put_updates_priority_and_req_type_and_omission_keeps_them() { let (app, t) = app_with("REQUIREMENT:READ+ADD+UPDATE").await; - let id = create_req(&app, &t, "登录").await; + let id = create_req(&app, &t, "login").await; let r = app .clone() .oneshot(req( "PUT", &format!("/requirement/{id}"), - r#"{"title":"登录","priority":"p1","reqType":"bugfix"}"#, + r#"{"title":"login","priority":"p1","reqType":"bugfix"}"#, Some(&t), )) .await @@ -1306,12 +1314,12 @@ mod tests { // Rename without priority/type keeps existing values. let r2 = app .clone() - .oneshot(req("PUT", &format!("/requirement/{id}"), r#"{"title":"登入"}"#, Some(&t))) + .oneshot(req("PUT", &format!("/requirement/{id}"), r#"{"title":"sign in"}"#, Some(&t))) .await .expect("r"); assert_eq!(r2.status(), StatusCode::OK); let v2 = body_json(r2).await; - assert_eq!(v2["title"], "登入"); + assert_eq!(v2["title"], "sign in"); assert_eq!(v2["priority"], "P1"); assert_eq!(v2["reqType"], "BUGFIX"); @@ -1320,7 +1328,7 @@ mod tests { app.oneshot(req( "PUT", &format!("/requirement/{id}"), - r#"{"title":"登入","priority":"高"}"#, + r#"{"title":"sign in","priority":"high"}"#, Some(&t) )) .await @@ -1360,7 +1368,7 @@ mod tests { #[tokio::test] async fn duplicate_title_409() { let (app, t) = app_with("REQUIREMENT:READ+ADD").await; - let body = r#"{"projectId":"p1","title":"唯一"}"#; + let body = r#"{"projectId":"p1","title":"unique"}"#; assert_eq!( app.clone() .oneshot(req("POST", "/requirement", body, Some(&t))) @@ -1378,14 +1386,14 @@ mod tests { #[tokio::test] async fn create_with_tags_due_date_and_parent_exposes_lifecycle_fields() { let (app, t) = app_with("REQUIREMENT:READ+ADD").await; - let parent = create_req(&app, &t, "父需求").await; + let parent = create_req(&app, &t, "parent requirement").await; let r = app .clone() .oneshot(req( "POST", "/requirement", &format!( - r#"{{"projectId":"p1","title":"子需求","tags":[" api ","api","web"],"dueDate":"2999-12-31","parentId":"{parent}"}}"# + r#"{{"projectId":"p1","title":"child requirement","tags":[" api ","api","web"],"dueDate":"2999-12-31","parentId":"{parent}"}}"# ), Some(&t), )) @@ -1423,7 +1431,7 @@ mod tests { .oneshot(req( "POST", "/requirement", - r#"{"projectId":"p1","title":"逾期需求","dueDate":"2000-01-01"}"#, + r#"{"projectId":"p1","title":"overdue requirement","dueDate":"2000-01-01"}"#, Some(&t), )) .await @@ -1440,7 +1448,7 @@ mod tests { .oneshot(req( "POST", "/requirement", - r#"{"projectId":"p1","title":"孤儿","parentId":"ghost"}"#, + r#"{"projectId":"p1","title":"orphan","parentId":"ghost"}"#, Some(&t) )) .await @@ -1452,7 +1460,7 @@ mod tests { app.oneshot(req( "POST", "/requirement", - r#"{"projectId":"p1","title":"坏日期","dueDate":"2026-13-01"}"#, + r#"{"projectId":"p1","title":"bad date","dueDate":"2026-13-01"}"#, Some(&t) )) .await @@ -1465,7 +1473,7 @@ mod tests { #[tokio::test] async fn stage_transition_stamps_updates_plan_and_rejects_invalid() { let (app, t) = app_with("REQUIREMENT:READ+ADD+UPDATE").await; - let id = create_req(&app, &t, "开发中").await; + let id = create_req(&app, &t, "in development").await; let r = app .clone() .oneshot(req( @@ -1639,13 +1647,13 @@ mod tests { #[tokio::test] async fn put_updates_tags_and_due_date_empty_string_clears() { let (app, t) = app_with("REQUIREMENT:READ+ADD+UPDATE").await; - let id = create_req(&app, &t, "登录").await; + let id = create_req(&app, &t, "login").await; let r = app .clone() .oneshot(req( "PUT", &format!("/requirement/{id}"), - r#"{"title":"登录","tags":["web","api"],"dueDate":"2999-06-01"}"#, + r#"{"title":"login","tags":["web","api"],"dueDate":"2999-06-01"}"#, Some(&t), )) .await @@ -1658,7 +1666,7 @@ mod tests { // Omitted keeps current. let r2 = app .clone() - .oneshot(req("PUT", &format!("/requirement/{id}"), r#"{"title":"登录"}"#, Some(&t))) + .oneshot(req("PUT", &format!("/requirement/{id}"), r#"{"title":"login"}"#, Some(&t))) .await .expect("r"); let v2 = body_json(r2).await; @@ -1670,7 +1678,7 @@ mod tests { .oneshot(req( "PUT", &format!("/requirement/{id}"), - r#"{"title":"登录","dueDate":""}"#, + r#"{"title":"login","dueDate":""}"#, Some(&t), )) .await @@ -1688,7 +1696,7 @@ mod tests { .oneshot(req( "POST", "/requirement", - r#"{"projectId":"p1","title":"登录","moduleId":"mod-1"}"#, + r#"{"projectId":"p1","title":"login","moduleId":"mod-1"}"#, Some(&t), )) .await @@ -1702,7 +1710,7 @@ mod tests { .oneshot(req( "POST", "/requirement", - r#"{"projectId":"p1","title":"注册"}"#, + r#"{"projectId":"p1","title":"register"}"#, Some(&t), )) .await @@ -1714,7 +1722,7 @@ mod tests { // PUT: omitted keeps; moduleId replaces; empty string unfiles. let r2 = app .clone() - .oneshot(req("PUT", &format!("/requirement/{id}"), r#"{"title":"登录"}"#, Some(&t))) + .oneshot(req("PUT", &format!("/requirement/{id}"), r#"{"title":"login"}"#, Some(&t))) .await .expect("r"); assert_eq!(body_json(r2).await["moduleId"], "mod-1"); @@ -1723,7 +1731,7 @@ mod tests { .oneshot(req( "PUT", &format!("/requirement/{id}"), - r#"{"title":"登录","moduleId":"mod-2"}"#, + r#"{"title":"login","moduleId":"mod-2"}"#, Some(&t), )) .await @@ -1734,7 +1742,7 @@ mod tests { .oneshot(req( "PUT", &format!("/requirement/{id}"), - r#"{"title":"登录","moduleId":""}"#, + r#"{"title":"login","moduleId":""}"#, Some(&t), )) .await @@ -1761,14 +1769,14 @@ mod tests { .oneshot(req( "POST", "/requirement", - r#"{"projectId":"p1","title":"登录","customFields":{" owner ":"alice","module":"登录"}}"#, + r#"{"projectId":"p1","title":"login","customFields":{" owner ":"alice","module":"login"}}"#, Some(&t), )) .await .expect("r"); assert_eq!(r.status(), StatusCode::CREATED); let v = body_json(r).await; - assert_eq!(v["customFields"], serde_json::json!({"owner": "alice", "module": "登录"})); + assert_eq!(v["customFields"], serde_json::json!({"owner": "alice", "module": "login"})); let id = v["id"].as_str().expect("id").to_string(); // PUT replaces wholesale; omitted keeps current. @@ -1777,7 +1785,7 @@ mod tests { .oneshot(req( "PUT", &format!("/requirement/{id}"), - r#"{"title":"登录","customFields":{"owner":"bob"}}"#, + r#"{"title":"login","customFields":{"owner":"bob"}}"#, Some(&t), )) .await @@ -1786,7 +1794,7 @@ mod tests { assert_eq!(body_json(r2).await["customFields"], serde_json::json!({"owner": "bob"})); let r3 = app .clone() - .oneshot(req("PUT", &format!("/requirement/{id}"), r#"{"title":"登录"}"#, Some(&t))) + .oneshot(req("PUT", &format!("/requirement/{id}"), r#"{"title":"login"}"#, Some(&t))) .await .expect("r"); assert_eq!(body_json(r3).await["customFields"], serde_json::json!({"owner": "bob"})); @@ -1796,7 +1804,7 @@ mod tests { .oneshot(req( "PUT", &format!("/requirement/{id}"), - r#"{"title":"登录","customFields":{}}"#, + r#"{"title":"login","customFields":{}}"#, Some(&t), )) .await @@ -1818,7 +1826,7 @@ mod tests { app.oneshot(req( "POST", "/requirement", - r#"{"projectId":"p1","title":"坏字段","customFields":{" ":"v"}}"#, + r#"{"projectId":"p1","title":"bad field","customFields":{" ":"v"}}"#, Some(&t) )) .await @@ -1831,12 +1839,12 @@ mod tests { #[tokio::test] async fn changes_endpoint_lists_newest_first() { let (app, t) = app_with("REQUIREMENT:READ+ADD+UPDATE").await; - let id = create_req(&app, &t, "登录").await; + let id = create_req(&app, &t, "login").await; app.clone() .oneshot(req( "PUT", &format!("/requirement/{id}"), - r#"{"title":"登入","priority":"P0"}"#, + r#"{"title":"sign in","priority":"P0"}"#, Some(&t), )) .await @@ -1866,8 +1874,8 @@ mod tests { assert!(items[0]["changedAt"].as_i64().is_some()); assert_eq!(items[1]["field"], "priority"); assert_eq!(items[2]["field"], "title"); - assert_eq!(items[2]["oldValue"], "登录"); - assert_eq!(items[2]["newValue"], "登入"); + assert_eq!(items[2]["oldValue"], "login"); + assert_eq!(items[2]["newValue"], "sign in"); // Creation hook is last (oldest). assert_eq!(items[3]["field"], "stage.CREATED"); } diff --git a/crates/requirement/src/adapters/in_memory.rs b/crates/requirement/src/adapters/in_memory.rs index 11f62ca6..69ca1934 100644 --- a/crates/requirement/src/adapters/in_memory.rs +++ b/crates/requirement/src/adapters/in_memory.rs @@ -217,7 +217,7 @@ mod tests { #[tokio::test] async fn insert_get_save_roundtrip() { let repo = InMemoryRequirementRepository::new(); - let nu = NewRequirement::new("p1", "登录", "d", &["c1".to_string()]).expect("v"); + let nu = NewRequirement::new("p1", "login", "d", &["c1".to_string()]).expect("v"); let mut r = repo.insert(&nu).await.expect("insert"); assert_eq!(r.id, "requirement-1"); @@ -291,7 +291,7 @@ mod tests { use crate::domain::{Stage, StageRow, StageStatus}; let repo = InMemoryRequirementRepository::new(); let r = repo - .insert(&NewRequirement::new("p1", "登录", "d", &[]).expect("v")) + .insert(&NewRequirement::new("p1", "login", "d", &[]).expect("v")) .await .expect("insert"); @@ -319,10 +319,10 @@ mod tests { #[tokio::test] async fn find_active_ignores_soft_deleted() { let repo = InMemoryRequirementRepository::new(); - let nu = NewRequirement::new("p1", "登录", "d", &[]).expect("v"); + let nu = NewRequirement::new("p1", "login", "d", &[]).expect("v"); let r = repo.insert(&nu).await.expect("insert"); - assert!(repo.find_active_by_title("p1", "登录").await.expect("q").is_some()); + assert!(repo.find_active_by_title("p1", "login").await.expect("q").is_some()); repo.soft_delete(&r.id); - assert!(repo.find_active_by_title("p1", "登录").await.expect("q").is_none()); + assert!(repo.find_active_by_title("p1", "login").await.expect("q").is_none()); } } diff --git a/crates/requirement/src/adapters/pg.rs b/crates/requirement/src/adapters/pg.rs index 6cbf9d5c..be5a3c21 100644 --- a/crates/requirement/src/adapters/pg.rs +++ b/crates/requirement/src/adapters/pg.rs @@ -531,7 +531,7 @@ mod tests { use super::*; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_versioning_baseline_and_soft_delete() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -542,25 +542,33 @@ mod tests { .expect("truncate"); let repo = PgRequirementRepository::new(pool.clone()); - let nu = NewRequirement::new("p1", "登录", "用邮箱登录", &["正确凭证登录".to_string()]) - .expect("valid"); + let nu = NewRequirement::new( + "p1", + "login", + "log in with email", + &["login with correct credentials".to_string()], + ) + .expect("valid"); let r = repo.insert(&nu).await.expect("insert"); let mut got = repo.get(&r.id).await.expect("get").expect("some"); assert_eq!(got.latest_version(), 1); assert_eq!(got.baseline_version, 1); - assert_eq!(got.baseline().acceptance_criteria[0].text, "正确凭证登录"); + assert_eq!(got.baseline().acceptance_criteria[0].text, "login with correct credentials"); // Default priority/type are persisted on insert. assert_eq!(got.priority, RequirementPriority::P2); assert_eq!(got.req_type, RequirementType::Feature); - got.revise("v2 描述", vec![AcceptanceCriterion { text: "新增标准".into() }]) + got.revise("v2 description", vec![AcceptanceCriterion { text: "new criterion".into() }]) .expect("revise"); repo.save(&got).await.expect("save"); let reloaded = repo.get(&r.id).await.expect("get").expect("some"); assert_eq!(reloaded.latest_version(), 2); assert_eq!(reloaded.baseline_version, 1); - assert_eq!(reloaded.version(1).expect("v1").acceptance_criteria[0].text, "正确凭证登录"); + assert_eq!( + reloaded.version(1).expect("v1").acceptance_criteria[0].text, + "login with correct credentials" + ); let mut r2 = reloaded; r2.set_baseline(2).expect("baseline"); @@ -572,15 +580,15 @@ mod tests { assert_eq!(after.priority, RequirementPriority::P0); assert_eq!(after.req_type, RequirementType::Bugfix); - assert!(repo.find_active_by_title("p1", "登录").await.expect("q").is_some()); + assert!(repo.find_active_by_title("p1", "login").await.expect("q").is_some()); r2.soft_delete(); repo.save(&r2).await.expect("save"); - assert!(repo.find_active_by_title("p1", "登录").await.expect("q").is_none()); + assert!(repo.find_active_by_title("p1", "login").await.expect("q").is_none()); assert!(repo.insert(&nu).await.is_ok()); } #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_lifecycle_fields_children_and_change_log_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -592,18 +600,18 @@ mod tests { let repo = PgRequirementRepository::new(pool.clone()); let parent = repo - .insert(&NewRequirement::new("p1", "父需求", "d", &[]).expect("valid")) + .insert(&NewRequirement::new("p1", "parent requirement", "d", &[]).expect("valid")) .await .expect("insert parent"); let cf = |pairs: &[(&str, &str)]| -> std::collections::BTreeMap { pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect() }; - let child_new = NewRequirement::new("p1", "子需求", "d", &[]) + let child_new = NewRequirement::new("p1", "child requirement", "d", &[]) .expect("valid") .with_tags(vec!["api".to_string(), "web".to_string()]) .with_due_date(Some("2026-12-31".to_string())) .with_parent(Some(parent.id.clone())) - .with_custom_fields(cf(&[("owner", "alice"), ("多选", "a,b,c")])) + .with_custom_fields(cf(&[("owner", "alice"), ("multi-select", "a,b,c")])) .with_module("mod-1"); let child = repo.insert(&child_new).await.expect("insert child"); assert!(child.created_at_ms > 0); @@ -616,7 +624,7 @@ mod tests { assert_eq!(got.parent_id.as_deref(), Some(parent.id.as_str())); assert_eq!(got.dev_status, WorkStatus::NotStarted); // Custom fields round-trip; the parent never wrote any, so it reads an empty map. - assert_eq!(got.custom_fields, cf(&[("owner", "alice"), ("多选", "a,b,c")])); + assert_eq!(got.custom_fields, cf(&[("owner", "alice"), ("multi-select", "a,b,c")])); assert!(parent.custom_fields.is_empty()); // Module id round-trips; no module defaults to empty string (unfiled). assert_eq!(got.module_id, "mod-1"); @@ -671,7 +679,7 @@ mod tests { } #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_stage_upsert_roundtrip_and_backfill_from_old_shape_row() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -685,7 +693,7 @@ mod tests { // —— upsert/read-back ——: a requirement with no stage rows reads back 7 PENDING defaults. let r = repo - .insert(&NewRequirement::new("p1", "阶段需求", "d", &[]).expect("valid")) + .insert(&NewRequirement::new("p1", "staged requirement", "d", &[]).expect("valid")) .await .expect("insert"); let rows = repo.stages(&r.id).await.expect("stages"); @@ -712,7 +720,7 @@ mod tests { "INSERT INTO ms_requirement \ (project_id, title, status, dev_status, test_status, dev_started_at, dev_finished_at, \ test_started_at, test_finished_at) \ - VALUES ('p1', '旧需求', 'BASELINED', 'IN_PROGRESS', 'DONE', \ + VALUES ('p1', 'legacy requirement', 'BASELINED', 'IN_PROGRESS', 'DONE', \ to_timestamp(1700000000), NULL, to_timestamp(1700000100), to_timestamp(1700000200)) \ RETURNING id, (extract(epoch from created_at) * 1000)::bigint AS created_at_ms", ) @@ -758,7 +766,7 @@ mod tests { } #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_list_active_batch_assembly_groups_versions_and_stages() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -775,16 +783,16 @@ mod tests { for i in 1..=3 { let nu = NewRequirement::new( "p1", - &format!("批量需求{i}"), + &format!("batch requirement {i}"), &format!("r{i} v1"), - &[format!("r{i} 标准1")], + &[format!("r{i} criterion 1")], ) .expect("valid"); let r = repo.insert(&nu).await.expect("insert"); let mut got = repo.get(&r.id).await.expect("get").expect("some"); got.revise( &format!("r{i} v2"), - vec![AcceptanceCriterion { text: format!("r{i} 标准2") }], + vec![AcceptanceCriterion { text: format!("r{i} criterion 2") }], ) .expect("revise"); repo.save(&got).await.expect("save"); @@ -813,10 +821,10 @@ mod tests { assert_eq!(r.versions.len(), 2, "requirement {n} versions"); assert_eq!(r.versions[0].version, 1); assert_eq!(r.versions[0].description, format!("r{n} v1")); - assert_eq!(r.versions[0].acceptance_criteria[0].text, format!("r{n} 标准1")); + assert_eq!(r.versions[0].acceptance_criteria[0].text, format!("r{n} criterion 1")); assert_eq!(r.versions[1].version, 2); assert_eq!(r.versions[1].description, format!("r{n} v2")); - assert_eq!(r.versions[1].acceptance_criteria[0].text, format!("r{n} 标准2")); + assert_eq!(r.versions[1].acceptance_criteria[0].text, format!("r{n} criterion 2")); assert_eq!(r.stages.len(), 7); } diff --git a/crates/requirement/src/application/create_requirement.rs b/crates/requirement/src/application/create_requirement.rs index 8feff65a..b0adce56 100644 --- a/crates/requirement/src/application/create_requirement.rs +++ b/crates/requirement/src/application/create_requirement.rs @@ -126,7 +126,7 @@ impl CreateRequirementUseCase { let mut row = StageRow::pending(Stage::Created); row.set_status(StageStatus::Done, req.created_at_ms); if let Err(e) = self.repo.upsert_stage(&req.id, &row).await { - tracing::warn!(requirement = %req.id, error = %e, "CREATED 阶段写入失败"); + tracing::warn!(requirement = %req.id, error = %e, "CREATED stage write failed"); return; } if let Some(slot) = req.stage_row_mut(Stage::Created) { @@ -139,7 +139,7 @@ impl CreateRequirementUseCase { new_value: StageStatus::Done.as_str().to_string(), }; if let Err(e) = self.repo.append_change(&req.id, &[entry]).await { - tracing::warn!(requirement = %req.id, error = %e, "CREATED 阶段变更日志写入失败"); + tracing::warn!(requirement = %req.id, error = %e, "CREATED stage change log write failed"); } } } @@ -155,7 +155,7 @@ mod tests { #[tokio::test] async fn creates_with_version_1_and_baseline_1() { - let r = uc().execute("p1", "登录", "desc", &["c1".to_string()]).await.expect("ok"); + let r = uc().execute("p1", "login", "desc", &["c1".to_string()]).await.expect("ok"); assert_eq!(r.latest_version(), 1); assert_eq!(r.baseline_version, 1); assert_eq!(r.baseline().acceptance_criteria.len(), 1); @@ -164,9 +164,9 @@ mod tests { #[tokio::test] async fn rejects_duplicate_title_in_same_project() { let uc = uc(); - uc.execute("p1", "登录", "d", &[]).await.expect("first"); + uc.execute("p1", "login", "d", &[]).await.expect("first"); assert_eq!( - uc.execute("p1", "登录", "d", &[]).await.unwrap_err(), + uc.execute("p1", "login", "d", &[]).await.unwrap_err(), CreateRequirementError::TitleAlreadyExists ); } @@ -174,23 +174,23 @@ mod tests { #[tokio::test] async fn allows_same_title_in_different_project() { let uc = uc(); - uc.execute("p1", "登录", "d", &[]).await.expect("ok"); - assert!(uc.execute("p2", "登录", "d", &[]).await.is_ok()); + uc.execute("p1", "login", "d", &[]).await.expect("ok"); + assert!(uc.execute("p2", "login", "d", &[]).await.is_ok()); } #[tokio::test] async fn recreatable_after_soft_delete() { let repo = Arc::new(InMemoryRequirementRepository::new()); let uc = CreateRequirementUseCase::new(repo.clone()); - let r = uc.execute("p1", "登录", "d", &[]).await.expect("ok"); + let r = uc.execute("p1", "login", "d", &[]).await.expect("ok"); repo.soft_delete(&r.id); - assert!(uc.execute("p1", "登录", "d", &[]).await.is_ok()); + assert!(uc.execute("p1", "login", "d", &[]).await.is_ok()); } #[tokio::test] async fn creates_with_default_priority_and_type() { use crate::domain::{RequirementPriority, RequirementType}; - let r = uc().execute("p1", "登录", "d", &[]).await.expect("ok"); + let r = uc().execute("p1", "login", "d", &[]).await.expect("ok"); assert_eq!(r.priority, RequirementPriority::P2); assert_eq!(r.req_type, RequirementType::Feature); } @@ -201,7 +201,7 @@ mod tests { let r = uc() .execute_with( "p1", - "登录", + "login", "d", &[], Some(" p0 "), @@ -225,7 +225,7 @@ mod tests { assert_eq!( uc().execute_with( "p1", - "登录", + "login", "d", &[], Some("P9"), @@ -245,7 +245,7 @@ mod tests { assert_eq!( uc().execute_with( "p1", - "登录", + "login", "d", &[], None, @@ -268,11 +268,11 @@ mod tests { async fn creates_with_tags_due_date_and_parent() { let repo = Arc::new(InMemoryRequirementRepository::new()); let uc = CreateRequirementUseCase::new(repo.clone()); - let parent = uc.execute("p1", "父需求", "d", &[]).await.expect("parent"); + let parent = uc.execute("p1", "parent requirement", "d", &[]).await.expect("parent"); let r = uc .execute_with( "p1", - "子需求", + "child requirement", "d", &[], None, @@ -294,7 +294,7 @@ mod tests { let r2 = uc .execute_with( "p1", - "另一条", + "another one", "d", &[], None, @@ -318,7 +318,7 @@ mod tests { let r = uc .execute_with( "p1", - "登录", + "login", "d", &[], None, @@ -334,7 +334,7 @@ mod tests { .await .expect("ok"); assert_eq!(r.module_id, "mod-1"); // trimmed - let r2 = uc.execute("p1", "注册", "d", &[]).await.expect("ok"); + let r2 = uc.execute("p1", "register", "d", &[]).await.expect("ok"); assert_eq!(r2.module_id, ""); // defaults to unfiled } @@ -344,7 +344,7 @@ mod tests { let r = uc .execute_with( "p1", - "登录", + "login", "d", &[], None, @@ -364,7 +364,7 @@ mod tests { let r2 = uc .execute_with( "p1", - "注册", + "register", "d", &[], None, @@ -386,7 +386,7 @@ mod tests { async fn creates_with_custom_fields_normalized_and_rejects_invalid() { let raw = BTreeMap::from([(" owner ".to_string(), "alice".to_string())]); let r = uc() - .execute_with("p1", "登录", "d", &[], None, None, &[], None, None, &raw, "", &[], "") + .execute_with("p1", "login", "d", &[], None, None, &[], None, None, &raw, "", &[], "") .await .expect("ok"); assert_eq!(r.custom_fields, BTreeMap::from([("owner".to_string(), "alice".to_string())])); @@ -395,7 +395,7 @@ mod tests { assert_eq!( uc().execute_with( "p1", - "注册", + "register", "d", &[], None, @@ -418,11 +418,11 @@ mod tests { async fn rejects_missing_or_cross_project_parent() { let repo = Arc::new(InMemoryRequirementRepository::new()); let uc = CreateRequirementUseCase::new(repo.clone()); - let other = uc.execute("p2", "别家", "d", &[]).await.expect("other"); + let other = uc.execute("p2", "other project", "d", &[]).await.expect("other"); assert_eq!( uc.execute_with( "p1", - "子", + "child", "d", &[], None, @@ -442,7 +442,7 @@ mod tests { assert_eq!( uc.execute_with( "p1", - "子", + "child", "d", &[], None, @@ -460,12 +460,12 @@ mod tests { CreateRequirementError::CrossProjectParent ); // A soft-deleted parent counts as missing. - let doomed = uc.execute("p1", "亡父", "d", &[]).await.expect("p"); + let doomed = uc.execute("p1", "dead parent", "d", &[]).await.expect("p"); repo.soft_delete(&doomed.id); assert_eq!( uc.execute_with( "p1", - "子", + "child", "d", &[], None, @@ -489,7 +489,7 @@ mod tests { assert_eq!( uc().execute_with( "p1", - "登录", + "login", "d", &[], None, @@ -512,7 +512,7 @@ mod tests { assert_eq!( uc().execute_with( "p1", - "登录", + "login", "d", &[], None, diff --git a/crates/requirement/src/application/requirement_admin.rs b/crates/requirement/src/application/requirement_admin.rs index 503e7da8..c1224342 100644 --- a/crates/requirement/src/application/requirement_admin.rs +++ b/crates/requirement/src/application/requirement_admin.rs @@ -334,7 +334,7 @@ impl RequirementService { let mut updated = row.clone(); updated.set_status(status, now_ms()); if let Err(e) = self.repo.upsert_stage(&req.id, &updated).await { - tracing::warn!(requirement = %req.id, stage = stage.as_str(), error = %e, "阶段钩子写入失败"); + tracing::warn!(requirement = %req.id, stage = stage.as_str(), error = %e, "failed to write stage hook"); return; } if let Some(slot) = req.stage_row_mut(stage) { @@ -342,7 +342,7 @@ impl RequirementService { } let entry = change(by, &format!("stage.{}", stage.as_str()), old.as_str(), status.as_str()); if let Err(e) = self.repo.append_change(&req.id, &[entry]).await { - tracing::warn!(requirement = %req.id, stage = stage.as_str(), error = %e, "阶段变更日志写入失败"); + tracing::warn!(requirement = %req.id, stage = stage.as_str(), error = %e, "failed to write stage change log"); } } @@ -490,7 +490,7 @@ mod tests { async fn seeded() -> (RequirementService, String) { let repo = Arc::new(InMemoryRequirementRepository::new()); let id = CreateRequirementUseCase::new(repo.clone()) - .execute("p1", "登录", "d", &["c1".to_string()]) + .execute("p1", "login", "d", &["c1".to_string()]) .await .expect("seed") .id; @@ -511,11 +511,11 @@ mod tests { #[tokio::test] async fn reject_review_persists_reason_and_baseline_clears_it() { let (svc, id) = seeded().await; - let r = svc.reject_review(&id, " 缺少异常路径 ", "u1").await.expect("reject"); - assert_eq!(r.review_comment.as_deref(), Some("缺少异常路径")); + let r = svc.reject_review(&id, " missing exception path ", "u1").await.expect("reject"); + assert_eq!(r.review_comment.as_deref(), Some("missing exception path")); assert_eq!( svc.get(&id).await.expect("get").review_comment.as_deref(), - Some("缺少异常路径") + Some("missing exception path") ); let p = svc.set_baseline(&id, 1, "u1").await.expect("baseline"); assert!(p.review_comment.is_none()); @@ -563,14 +563,14 @@ mod tests { async fn rename_to_taken_title_conflicts() { let repo = Arc::new(InMemoryRequirementRepository::new()); let create = CreateRequirementUseCase::new(repo.clone()); - let a = create.execute("p1", "登录", "d", &[]).await.expect("a").id; - create.execute("p1", "注册", "d", &[]).await.expect("b"); + let a = create.execute("p1", "login", "d", &[]).await.expect("a").id; + create.execute("p1", "register", "d", &[]).await.expect("b"); let svc = RequirementService::new(repo); assert_eq!( - svc.rename(&a, "注册", "u1").await.unwrap_err(), + svc.rename(&a, "register", "u1").await.unwrap_err(), RequirementCmdError::TitleExists ); - assert!(svc.rename(&a, "登入", "u1").await.is_ok()); + assert!(svc.rename(&a, "sign in", "u1").await.is_ok()); } #[tokio::test] @@ -578,22 +578,22 @@ mod tests { use crate::domain::{RequirementPriority, RequirementType}; let (svc, id) = seeded().await; let r = svc - .update(&id, "登录", Some(" p1 "), Some("bugfix"), None, None, None, None, "u1") + .update(&id, "login", Some(" p1 "), Some("bugfix"), None, None, None, None, "u1") .await .expect("update"); assert_eq!(r.priority, RequirementPriority::P1); assert_eq!(r.req_type, RequirementType::Bugfix); // None keeps existing values. let r2 = svc - .update(&id, "登入", None, None, None, None, None, None, "u1") + .update(&id, "sign in", None, None, None, None, None, None, "u1") .await .expect("update"); - assert_eq!(r2.title, "登入"); + assert_eq!(r2.title, "sign in"); assert_eq!(r2.priority, RequirementPriority::P1); assert_eq!(r2.req_type, RequirementType::Bugfix); // Invalid value fails validation. assert_eq!( - svc.update(&id, "登入", Some("P9"), None, None, None, None, None, "u1") + svc.update(&id, "sign in", Some("P9"), None, None, None, None, None, "u1") .await .unwrap_err(), RequirementCmdError::Validation(RequirementError::InvalidPriority("P9".into())) @@ -718,7 +718,7 @@ mod tests { async fn baseline_and_reject_hooks_drive_review_stage() { let (svc, id) = seeded().await; // Reject → review in progress. - let r = svc.reject_review(&id, "缺少异常路径", "u1").await.expect("reject"); + let r = svc.reject_review(&id, "missing exception path", "u1").await.expect("reject"); assert_eq!(stage_of(&r, Stage::Review).status, StageStatus::InProgress); // Baseline → review done. let r2 = svc.set_baseline(&id, 1, "u1").await.expect("baseline"); @@ -782,7 +782,7 @@ mod tests { #[tokio::test] async fn set_parent_links_and_children_lists() { - let (svc, ids) = seeded_many(&["根", "叶A", "叶B"]).await; + let (svc, ids) = seeded_many(&["root", "leaf A", "leaf B"]).await; svc.set_parent(&ids[1], Some(&ids[0]), "u1").await.expect("link"); svc.set_parent(&ids[2], Some(&ids[0]), "u1").await.expect("link"); let kids = svc.children(&ids[0]).await.expect("children"); @@ -836,7 +836,7 @@ mod tests { let r = svc .update( &id, - "登录", + "login", None, None, Some(&tags), @@ -851,20 +851,20 @@ mod tests { assert_eq!(r.due_date.as_deref(), Some("2026-12-31")); // Outer None keeps. let r2 = svc - .update(&id, "登录", None, None, None, None, None, None, "u1") + .update(&id, "login", None, None, None, None, None, None, "u1") .await .expect("update"); assert_eq!(r2.tags, vec!["api".to_string(), "web".to_string()]); assert_eq!(r2.due_date.as_deref(), Some("2026-12-31")); // Some(None) clears the due date. let r3 = svc - .update(&id, "登录", None, None, None, Some(None), None, None, "u1") + .update(&id, "login", None, None, None, Some(None), None, None, "u1") .await .expect("update"); assert!(r3.due_date.is_none()); // Invalid date fails validation. assert_eq!( - svc.update(&id, "登录", None, None, None, Some(Some("2026/12/31")), None, None, "u1") + svc.update(&id, "login", None, None, None, Some(Some("2026/12/31")), None, None, "u1") .await .unwrap_err(), RequirementCmdError::Validation(RequirementError::InvalidDueDate("2026/12/31".into())) @@ -876,22 +876,22 @@ mod tests { let (svc, id) = seeded().await; // Some replaces and logs a module change. let r = svc - .update(&id, "登录", None, None, None, None, None, Some(" mod-1 "), "u1") + .update(&id, "login", None, None, None, None, None, Some(" mod-1 "), "u1") .await .expect("update"); assert_eq!(r.module_id, "mod-1"); // trimmed // None keeps. let r2 = svc - .update(&id, "登录", None, None, None, None, None, None, "u1") + .update(&id, "login", None, None, None, None, None, None, "u1") .await .expect("update"); assert_eq!(r2.module_id, "mod-1"); // Same value logs nothing; empty string unfiles the module. - svc.update(&id, "登录", None, None, None, None, None, Some("mod-1"), "u1") + svc.update(&id, "login", None, None, None, None, None, Some("mod-1"), "u1") .await .expect("update"); let r3 = svc - .update(&id, "登录", None, None, None, None, None, Some(""), "u1") + .update(&id, "login", None, None, None, None, None, Some(""), "u1") .await .expect("update"); assert_eq!(r3.module_id, ""); @@ -914,29 +914,29 @@ mod tests { let r = svc .update( &id, - "登录", + "login", None, None, None, None, - Some(&cf(&[("owner", "alice"), ("module", "登录")])), + Some(&cf(&[("owner", "alice"), ("module", "login")])), None, "u1", ) .await .expect("update"); - assert_eq!(r.custom_fields, cf(&[("owner", "alice"), ("module", "登录")])); + assert_eq!(r.custom_fields, cf(&[("owner", "alice"), ("module", "login")])); // None keeps. let r2 = svc - .update(&id, "登录", None, None, None, None, None, None, "u1") + .update(&id, "login", None, None, None, None, None, None, "u1") .await .expect("update"); - assert_eq!(r2.custom_fields, cf(&[("owner", "alice"), ("module", "登录")])); + assert_eq!(r2.custom_fields, cf(&[("owner", "alice"), ("module", "login")])); // Wholesale replace: change owner, drop module, add env. let r3 = svc .update( &id, - "登录", + "login", None, None, None, @@ -950,7 +950,7 @@ mod tests { assert_eq!(r3.custom_fields, cf(&[("owner", "bob"), ("env", "prod")])); // Invalid key fails validation. assert_eq!( - svc.update(&id, "登录", None, None, None, None, Some(&cf(&[(" ", "v")])), None, "u1") + svc.update(&id, "login", None, None, None, None, Some(&cf(&[(" ", "v")])), None, "u1") .await .unwrap_err(), RequirementCmdError::Validation(RequirementError::EmptyCustomFieldKey) @@ -967,12 +967,12 @@ mod tests { [ ("custom.env", "", "prod"), ("custom.owner", "alice", "bob"), - ("custom.module", "登录", ""), + ("custom.module", "login", ""), ] ); // Resubmitting the same map appends nothing. let n = log.len(); - svc.update(&id, "登录", None, None, None, None, Some(&r3.custom_fields), None, "u1") + svc.update(&id, "login", None, None, None, None, Some(&r3.custom_fields), None, "u1") .await .expect("update"); assert_eq!(svc.changes(&id).await.expect("changes").len(), n); @@ -981,7 +981,7 @@ mod tests { #[tokio::test] async fn mutations_record_change_log_newest_first() { let (svc, id) = seeded().await; - svc.update(&id, "登入", Some("P0"), None, None, None, None, None, "alice") + svc.update(&id, "sign in", Some("P0"), None, None, None, None, None, "alice") .await .expect("update"); svc.revise(&id, "v2", &[], "bob").await.expect("revise"); @@ -1015,8 +1015,8 @@ mod tests { assert_eq!(log[3].new_value, "IN_PROGRESS"); assert_eq!(log[6].field, "version"); assert_eq!(log[6].new_value, "2"); - assert_eq!(log[8].old_value, "登录"); - assert_eq!(log[8].new_value, "登入"); + assert_eq!(log[8].old_value, "login"); + assert_eq!(log[8].new_value, "sign in"); // Timestamps are monotonic (descending). assert!(log.windows(2).all(|w| w[0].changed_at_ms >= w[1].changed_at_ms)); } @@ -1026,7 +1026,7 @@ mod tests { let (svc, id) = seeded().await; // Seed has only the creation-hook entry. assert_eq!(svc.changes(&id).await.expect("changes").len(), 1); - svc.update(&id, "登录", None, None, None, None, None, None, "u1").await.expect("update"); + svc.update(&id, "login", None, None, None, None, None, None, "u1").await.expect("update"); assert_eq!(svc.changes(&id).await.expect("changes").len(), 1); svc.set_stage(&id, "DEV", Some("IN_PROGRESS"), None, None, "u1").await.expect("stage"); svc.set_stage(&id, "DEV", Some("IN_PROGRESS"), None, None, "u1").await.expect("stage"); @@ -1037,7 +1037,7 @@ mod tests { async fn save_bumps_updated_at() { let (svc, id) = seeded().await; let before = svc.get(&id).await.expect("get").updated_at_ms; - svc.update(&id, "登入", None, None, None, None, None, None, "u1").await.expect("update"); + svc.update(&id, "sign in", None, None, None, None, None, None, "u1").await.expect("update"); let after = svc.get(&id).await.expect("get").updated_at_ms; assert!(after > before); } diff --git a/crates/requirement/src/domain/requirement.rs b/crates/requirement/src/domain/requirement.rs index 7f200693..f12edaeb 100644 --- a/crates/requirement/src/domain/requirement.rs +++ b/crates/requirement/src/domain/requirement.rs @@ -910,9 +910,12 @@ mod tests { fn new_req() -> NewRequirement { NewRequirement::new( "proj1", - " 登录功能 ", - " 用户可用邮箱登录 ", - &["可用正确邮箱+密码登录".to_string(), "错误密码拒绝".to_string()], + " login feature ", + " users can log in with email ", + &[ + "login with correct email + password".to_string(), + "wrong password rejected".to_string(), + ], ) .expect("valid") } @@ -920,8 +923,8 @@ mod tests { #[test] fn new_requirement_trims_and_parses_criteria() { let n = new_req(); - assert_eq!(n.title, "登录功能"); - assert_eq!(n.description, "用户可用邮箱登录"); + assert_eq!(n.title, "login feature"); + assert_eq!(n.description, "users can log in with email"); assert_eq!(n.acceptance_criteria.len(), 2); } @@ -950,11 +953,11 @@ mod tests { let mut r = Requirement::create("req-1", &new_req()); let v1_criteria = r.version(1).expect("v1").acceptance_criteria.clone(); - let n = r.revise("v2 描述", crit(&["新标准A"])).expect("revise"); + let n = r.revise("v2 description", crit(&["new criterion A"])).expect("revise"); assert_eq!(n, 2); assert_eq!(r.latest_version(), 2); assert_eq!(r.version(1).expect("v1").acceptance_criteria, v1_criteria); - assert_eq!(r.version(2).expect("v2").description, "v2 描述"); + assert_eq!(r.version(2).expect("v2").description, "v2 description"); let n3 = r.revise("v3", crit(&["X"])).expect("revise"); assert_eq!(n3, 3); @@ -996,8 +999,8 @@ mod tests { #[test] fn rename_validates() { let mut r = Requirement::create("req-1", &new_req()); - assert!(r.rename(" 新标题 ").is_ok()); - assert_eq!(r.title, "新标题"); + assert!(r.rename(" new title ").is_ok()); + assert_eq!(r.title, "new title"); assert_eq!(r.rename(" "), Err(RequirementError::EmptyTitle)); } @@ -1012,9 +1015,9 @@ mod tests { #[test] fn reject_review_records_reason_and_keeps_draft() { let mut r = Requirement::create("req-1", &new_req()); - r.reject_review(" 验收标准不完整 ").expect("reject"); + r.reject_review(" acceptance criteria incomplete ").expect("reject"); assert_eq!(r.status, RequirementStatus::Draft); - assert_eq!(r.review_comment.as_deref(), Some("验收标准不完整")); + assert_eq!(r.review_comment.as_deref(), Some("acceptance criteria incomplete")); } #[test] @@ -1027,7 +1030,7 @@ mod tests { #[test] fn pass_then_baseline_clears_prior_rejection() { let mut r = Requirement::create("req-1", &new_req()); - r.reject_review("缺少边界场景").expect("reject"); + r.reject_review("missing boundary scenarios").expect("reject"); r.set_baseline(1).expect("baseline"); assert_eq!(r.status, RequirementStatus::Baselined); assert!(r.review_comment.is_none()); @@ -1036,8 +1039,8 @@ mod tests { #[test] fn revise_clears_prior_rejection() { let mut r = Requirement::create("req-1", &new_req()); - r.reject_review("缺少边界场景").expect("reject"); - r.revise("v2", crit(&["补充边界"])).expect("revise"); + r.reject_review("missing boundary scenarios").expect("reject"); + r.revise("v2", crit(&["boundary cases added"])).expect("revise"); assert!(r.review_comment.is_none()); } @@ -1142,9 +1145,9 @@ mod tests { #[test] fn normalize_custom_fields_trims_keys_and_keeps_values() { - let raw = fields(&[(" owner ", "alice"), ("模块", "登录")]); + let raw = fields(&[(" owner ", "alice"), ("module", "login")]); let out = normalize_custom_fields(&raw).expect("ok"); - assert_eq!(out, fields(&[("owner", "alice"), ("模块", "登录")])); + assert_eq!(out, fields(&[("owner", "alice"), ("module", "login")])); assert_eq!(normalize_custom_fields(&BTreeMap::new()), Ok(BTreeMap::new())); } @@ -1308,7 +1311,7 @@ mod tests { assert_eq!(Stage::parse(" dev "), Some(Stage::Dev)); assert_eq!(Stage::parse("DESIGN"), None); assert_eq!(parse_stage("review"), Ok(Stage::Review)); - assert_eq!(parse_stage(" 上线 "), Err(RequirementError::InvalidStage("上线".into()))); + assert_eq!(parse_stage(" GO_LIVE "), Err(RequirementError::InvalidStage("GO_LIVE".into()))); for s in [StageStatus::Pending, StageStatus::InProgress, StageStatus::Done, StageStatus::Skipped] diff --git a/crates/requirement/tests/architecture.rs b/crates/requirement/tests/architecture.rs index 643f0b7d..0dd3436b 100644 --- a/crates/requirement/tests/architecture.rs +++ b/crates/requirement/tests/architecture.rs @@ -24,7 +24,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/runner/src/adapters/http.rs b/crates/runner/src/adapters/http.rs index 6e0eb01f..b3b7eb61 100644 --- a/crates/runner/src/adapters/http.rs +++ b/crates/runner/src/adapters/http.rs @@ -311,7 +311,7 @@ async fn run_probe( RegisterBody, AgentResponse, RunViaBody, RunCaseBody, ExecutionResponse, ProbeBody, ProbeReportResponse )), - tags((name = "runner", description = "远程执行 agent")) + tags((name = "runner", description = "Remote execution agent")) )] struct ApiDoc; @@ -379,7 +379,7 @@ mod tests { .clone() .oneshot(post( "/runner-agent", - r#"{"name":"测试环境","baseUrl":"http://10.0.0.5:9100","token":"s"}"#, + r#"{"name":"test environment","baseUrl":"http://10.0.0.5:9100","token":"s"}"#, Some(&t), )) .await @@ -434,7 +434,7 @@ mod tests { .clone() .oneshot(post( "/runner-agent", - r#"{"name":"环境A","baseUrl":"http://a:9100"}"#, + r#"{"name":"environment A","baseUrl":"http://a:9100"}"#, Some(&t), )) .await @@ -488,9 +488,10 @@ mod tests { let (app, t, caps, _c) = app_with_caps("RUNNER:READ+ADD+EXECUTE").await; caps.set("http://grpc-env:9100", &["http", "grpc"]); caps.set("http://sql-env:9100", &["http", "sql"]); - for (name, url) in - [("gRPC环境", "http://grpc-env:9100"), ("SQL环境", "http://sql-env:9100")] - { + for (name, url) in [ + ("gRPC environment", "http://grpc-env:9100"), + ("SQL environment", "http://sql-env:9100"), + ] { app.clone() .oneshot(post( "/runner-agent", @@ -512,7 +513,7 @@ mod tests { .expect("r"); assert_eq!(r.status(), StatusCode::OK); let v = json(r).await; - assert_eq!(v["agentName"], "gRPC环境"); + assert_eq!(v["agentName"], "gRPC environment"); assert_eq!(v["outcome"]["success"], true); let r = app @@ -520,7 +521,7 @@ mod tests { .oneshot(post("/runner/probe", r#"{"protocol":"sql","target":"x"}"#, Some(&t))) .await .expect("r"); - assert_eq!(json(r).await["agentName"], "SQL环境"); + assert_eq!(json(r).await["agentName"], "SQL environment"); let r = app .oneshot(post("/runner/probe", r#"{"protocol":"redis","target":"x"}"#, Some(&t))) diff --git a/crates/runner/src/application/mod.rs b/crates/runner/src/application/mod.rs index 6c8c4355..c4a38b5f 100644 --- a/crates/runner/src/application/mod.rs +++ b/crates/runner/src/application/mod.rs @@ -222,7 +222,7 @@ mod tests { async fn register_list_and_run_via() { let (svc, _store, _cases) = svc(); let a = svc - .register("测试环境", "http://10.0.0.5:9100", Some("t".into()), true) + .register("test environment", "http://10.0.0.5:9100", Some("t".into()), true) .await .expect("reg"); assert_eq!(svc.list().await.expect("list").len(), 1); @@ -258,7 +258,7 @@ mod tests { #[tokio::test] async fn run_stored_case_via_agent() { let (svc, _s, cases) = svc(); - let a = svc.register("环境A", "http://a:9100", None, true).await.expect("reg"); + let a = svc.register("environment A", "http://a:9100", None, true).await.expect("reg"); cases.seed("case1", spec(), vec![Assertion::StatusIs(200)]); let res = svc.run_case(&a.id, "case1").await.expect("run case"); @@ -269,7 +269,7 @@ mod tests { #[tokio::test] async fn run_unknown_case_is_not_found() { let (svc, _s, _c) = svc(); - let a = svc.register("环境A", "http://a:9100", None, true).await.expect("reg"); + let a = svc.register("environment A", "http://a:9100", None, true).await.expect("reg"); assert_eq!(svc.run_case(&a.id, "ghost").await.unwrap_err(), RunCaseError::CaseNotFound); } @@ -278,16 +278,19 @@ mod tests { let (svc, _s, _c, caps) = svc_full(); caps.set("http://grpc-env:9100", &["http", "grpc"]); caps.set("http://sql-env:9100", &["http", "sql"]); - let g = svc.register("gRPC环境", "http://grpc-env:9100", None, true).await.expect("reg"); + let g = svc + .register("gRPC environment", "http://grpc-env:9100", None, true) + .await + .expect("reg"); assert_eq!(g.protocols, vec!["http".to_string(), "grpc".to_string()]); - svc.register("SQL环境", "http://sql-env:9100", None, true).await.expect("reg"); + svc.register("SQL environment", "http://sql-env:9100", None, true).await.expect("reg"); let rep = svc.run_probe(&probe_req("grpc")).await.expect("probe"); - assert_eq!(rep.agent_name, "gRPC环境"); + assert_eq!(rep.agent_name, "gRPC environment"); assert!(rep.outcome.success); let rep = svc.run_probe(&probe_req("sql")).await.expect("probe"); - assert_eq!(rep.agent_name, "SQL环境"); + assert_eq!(rep.agent_name, "SQL environment"); assert_eq!(svc.executions(&rep.agent_id, 10).await.expect("e")[0].method, "sql"); diff --git a/crates/runner/src/domain/agent.rs b/crates/runner/src/domain/agent.rs index 0c6fa03c..bfb99c50 100644 --- a/crates/runner/src/domain/agent.rs +++ b/crates/runner/src/domain/agent.rs @@ -75,9 +75,14 @@ mod tests { #[test] fn validates_and_trims() { - let a = NewRunnerAgent::new(" 测试环境 ", "http://10.0.0.5:9100/", Some("t".into()), true) - .expect("ok"); - assert_eq!(a.name, "测试环境"); + let a = NewRunnerAgent::new( + " test environment ", + "http://10.0.0.5:9100/", + Some("t".into()), + true, + ) + .expect("ok"); + assert_eq!(a.name, "test environment"); assert_eq!(a.base_url, "http://10.0.0.5:9100"); assert_eq!(a.token.as_deref(), Some("t")); } diff --git a/crates/runner/tests/architecture.rs b/crates/runner/tests/architecture.rs index 4d60fc1f..45e97c6f 100644 --- a/crates/runner/tests/architecture.rs +++ b/crates/runner/tests/architecture.rs @@ -24,7 +24,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/runner/tests/pg_protocol.rs b/crates/runner/tests/pg_protocol.rs index cd75cad1..6ed0a443 100644 --- a/crates/runner/tests/pg_protocol.rs +++ b/crates/runner/tests/pg_protocol.rs @@ -10,7 +10,7 @@ fn db_url() -> String { } #[tokio::test] -#[ignore = "需要 PostgreSQL"] +#[ignore = "requires PostgreSQL"] async fn protocols_column_roundtrip_and_protocol_routing() { let pool = migrate::connect(&db_url()).await.expect("connect PG"); migrate::run(&pool).await.expect("run migrations"); diff --git a/crates/server/src/breakdown_route.rs b/crates/server/src/breakdown_route.rs index 4725cb9d..fffaa541 100644 --- a/crates/server/src/breakdown_route.rs +++ b/crates/server/src/breakdown_route.rs @@ -50,18 +50,18 @@ async fn seed_functional_cases( Ok(existing) if !existing.is_empty() => return, Ok(_) => {} Err(e) => { - tracing::warn!(requirement = %spec.requirement_id, "拆分后查功能用例覆盖失败: {e:?}"); + tracing::warn!(requirement = %spec.requirement_id, "failed to query functional case coverage after breakdown: {e:?}"); return; } } let drafted = match drafter { Some(d) => match d.draft(spec, tasks).await { Ok(v) => { - tracing::info!(requirement = %spec.requirement_id, cases = v.len(), "AI 起草测试用例"); + tracing::info!(requirement = %spec.requirement_id, cases = v.len(), "AI drafted test cases"); v } Err(e) => { - tracing::warn!(requirement = %spec.requirement_id, "AI 起草测试用例失败,回落模板: {e}"); + tracing::warn!(requirement = %spec.requirement_id, "AI test case drafting failed, falling back to template: {e}"); template_cases(spec, tasks) } }, @@ -76,7 +76,7 @@ async fn seed_functional_cases( let new = match NewFunctionalCase::new( project_id, &c.name, - "需求拆分", + "requirement breakdown", "", "", std::collections::BTreeMap::new(), @@ -84,7 +84,7 @@ async fn seed_functional_cases( ) { Ok(n) => n.with_created_by(Some(created_by)), Err(e) => { - tracing::warn!(requirement = %spec.requirement_id, name = %c.name, "生成功能用例失败: {e:?}"); + tracing::warn!(requirement = %spec.requirement_id, name = %c.name, "failed to build functional case: {e:?}"); continue; } }; @@ -95,12 +95,12 @@ async fn seed_functional_cases( .link_requirement_case(&spec.requirement_id, *idx, &created.id, project_id) .await { - tracing::warn!(requirement = %spec.requirement_id, criterion = idx, "关联覆盖失败: {e:?}"); + tracing::warn!(requirement = %spec.requirement_id, criterion = idx, "failed to link coverage: {e:?}"); } } } Err(e) => { - tracing::warn!(requirement = %spec.requirement_id, name = %c.name, "插入功能用例失败: {e:?}") + tracing::warn!(requirement = %spec.requirement_id, name = %c.name, "failed to insert functional case: {e:?}") } } } @@ -219,7 +219,7 @@ async fn breakdown_handler( Ok(v) => Some(v.id), Err(CreateVerificationError::AlreadyExists) => None, Err(e) => { - tracing::warn!(requirement = %spec.requirement_id, version, "breakdown 后自动开验证失败: {e:?}"); + tracing::warn!(requirement = %spec.requirement_id, version, "failed to auto-create verification after breakdown: {e:?}"); None } } diff --git a/crates/server/src/case_drafter.rs b/crates/server/src/case_drafter.rs index 95163a6b..5ca7be70 100644 --- a/crates/server/src/case_drafter.rs +++ b/crates/server/src/case_drafter.rs @@ -92,7 +92,7 @@ pub fn parse_drafted( steps: Vec, } let dtos: Vec = - serde_json::from_str(text).map_err(|e| format!("用例草稿解析失败: {e}"))?; + serde_json::from_str(text).map_err(|e| format!("failed to parse case drafts: {e}"))?; let cap = task_count.max(1) * 3; let out: Vec = dtos .into_iter() @@ -119,7 +119,7 @@ pub fn parse_drafted( .take(cap) .collect(); if out.is_empty() { - return Err("用例草稿为空".to_string()); + return Err("case drafts are empty".to_string()); } Ok(out) } diff --git a/crates/server/src/decomposition_run.rs b/crates/server/src/decomposition_run.rs index fd6bd35b..61c83a25 100644 --- a/crates/server/src/decomposition_run.rs +++ b/crates/server/src/decomposition_run.rs @@ -163,9 +163,9 @@ async fn run_decomposition( if total > 0 && verified == total { if let Err(e) = st.requirements.deliver(&final_dec.requirement_id, "orchestrator").await { - tracing::warn!(requirement = %final_dec.requirement_id, "自动标记交付失败(可能未定基线): {e:?}"); + tracing::warn!(requirement = %final_dec.requirement_id, "failed to auto-mark as delivered (baseline may not be set): {e:?}"); } else { - tracing::info!(requirement = %final_dec.requirement_id, "需求已自动标记交付(DELIVERED)"); + tracing::info!(requirement = %final_dec.requirement_id, "requirement auto-marked as delivered (DELIVERED)"); } } @@ -356,7 +356,7 @@ async fn reassign_handler( RunBody, RunResponse, GraphResponse, GraphNodeDto, GraphEdgeDto, SummaryResponse, ReassignBody, ReassignResponse )), - tags((name = "task", description = "任务编排")) + tags((name = "task", description = "Task orchestration")) )] struct ApiDoc; diff --git a/crates/server/src/import_scheduler.rs b/crates/server/src/import_scheduler.rs index ad018c66..55ece5dc 100644 --- a/crates/server/src/import_scheduler.rs +++ b/crates/server/src/import_scheduler.rs @@ -47,15 +47,15 @@ async fn fetch_doc( let scheme = if basic_auth { "Basic" } else { "Bearer" }; req = req.header("Authorization", format!("{scheme} {}", token.trim())); } - let resp = req.send().await.map_err(|e| format!("请求失败:{e}"))?; + let resp = req.send().await.map_err(|e| format!("request failed: {e}"))?; let status = resp.status(); if !status.is_success() { - return Err(format!("来源返回 HTTP {}", status.as_u16())); + return Err(format!("source returned HTTP {}", status.as_u16())); } - let text = resp.text().await.map_err(|e| format!("读取响应失败:{e}"))?; + let text = resp.text().await.map_err(|e| format!("failed to read response: {e}"))?; match format { ImportFormat::Jmeter => Ok(Value::String(text)), - _ => serde_json::from_str(&text).map_err(|e| format!("来源非法 JSON:{e}")), + _ => serde_json::from_str(&text).map_err(|e| format!("invalid JSON from source: {e}")), } } @@ -84,7 +84,7 @@ async fn run_import( let format = ImportFormat::from_source(&s.format); let doc = match fetch_doc(client, &s.source_url, &s.auth_token, s.basic_auth, format).await { Ok(d) => d, - Err(e) => return format!("拉取失败:{e}"), + Err(e) => return format!("fetch failed: {e}"), }; match import_uc .execute( @@ -100,9 +100,11 @@ async fn run_import( ) .await { - Ok(o) => format!("新增 {} / 覆盖 {} / 跳过 {}", o.created.len(), o.updated, o.skipped), - Err(ImportError::Parse(_)) => "导入失败:文档无法解析".to_string(), - Err(ImportError::Repo(_)) => "导入失败:存储错误".to_string(), + Ok(o) => { + format!("created {} / updated {} / skipped {}", o.created.len(), o.updated, o.skipped) + } + Err(ImportError::Parse(_)) => "import failed: document could not be parsed".to_string(), + Err(ImportError::Repo(_)) => "import failed: storage error".to_string(), } } @@ -226,7 +228,9 @@ async fn import_url( Json(json!({ "created": o.created.len(), "updated": o.updated, "skipped": o.skipped })), ) .into_response(), - Err(ImportError::Parse(_)) => (StatusCode::BAD_REQUEST, "来源文档无法解析").into_response(), + Err(ImportError::Parse(_)) => { + (StatusCode::BAD_REQUEST, "source document could not be parsed").into_response() + } Err(ImportError::Repo(_)) => { (StatusCode::INTERNAL_SERVER_ERROR, "storage error").into_response() } diff --git a/crates/server/src/judge.rs b/crates/server/src/judge.rs index 9b1125cd..dec3253a 100644 --- a/crates/server/src/judge.rs +++ b/crates/server/src/judge.rs @@ -29,12 +29,13 @@ impl Judge for HttpJudge { match self.client.post(&self.url).json(&body).send().await { Ok(r) if r.status().is_success() => match r.json::().await { Ok(jr) => Verdict { passed: jr.passed, reason: jr.reason }, - Err(e) => { - Verdict { passed: false, reason: format!("judge 响应解析失败: {e}") } - } + Err(e) => Verdict { + passed: false, + reason: format!("failed to parse judge response: {e}"), + }, }, Ok(r) => Verdict { passed: false, reason: format!("judge HTTP {}", r.status()) }, - Err(e) => Verdict { passed: false, reason: format!("judge 不可达: {e}") }, + Err(e) => Verdict { passed: false, reason: format!("judge unreachable: {e}") }, } } } diff --git a/crates/server/src/llm.rs b/crates/server/src/llm.rs index 3654e122..b4cc98dd 100644 --- a/crates/server/src/llm.rs +++ b/crates/server/src/llm.rs @@ -141,7 +141,7 @@ impl LlmClient { wire = self.wire_name(), model = %self.model, prompt_version, latency_ms = started.elapsed().as_millis() as u64, input_tokens = usage.input, output_tokens = usage.output, attempt, - "LLM 调用完成" + "LLM call completed" ); return Ok(text); } @@ -152,13 +152,13 @@ impl LlmClient { break; } let wait = retry_after.unwrap_or(delay); - tracing::warn!(attempt, "LLM 可重试失败,{wait:?} 后重试: {last}"); + tracing::warn!(attempt, "LLM retryable failure, retry after {wait:?}: {last}"); tokio::time::sleep(wait).await; delay = (delay * 2).min(Duration::from_secs(20)); } } } - Err(format!("LLM 重试 {} 次仍失败: {last}", self.max_retries)) + Err(format!("LLM still failing after {} retries: {last}", self.max_retries)) } fn wire_name(&self) -> &'static str { @@ -200,13 +200,15 @@ impl LlmClient { }; let resp = rb.send().await.map_err(|e| SendErr::Retryable { - msg: format!("LLM 不可达: {e}"), + msg: format!("LLM unreachable: {e}"), retry_after: None, })?; let status = resp.status(); if status.is_success() { - let val: serde_json::Value = - resp.json().await.map_err(|e| SendErr::Fatal(format!("LLM 响应解析失败: {e}")))?; + let val: serde_json::Value = resp + .json() + .await + .map_err(|e| SendErr::Fatal(format!("failed to parse the LLM response: {e}")))?; return self.parse_ok(&val).map_err(SendErr::Fatal); } let retry_after = resp @@ -230,7 +232,7 @@ impl LlmClient { let text = val .pointer("/choices/0/message/content") .and_then(|c| c.as_str()) - .ok_or_else(|| "LLM 无 choices".to_string())? + .ok_or_else(|| "LLM response has no choices".to_string())? .to_string(); let u = val.get("usage"); let usage = Usage { @@ -248,7 +250,7 @@ impl LlmClient { Wire::Anthropic => { // The safety classifier may refuse: on stop_reason=refusal, content is empty, so treat it as an error. if val.get("stop_reason").and_then(|s| s.as_str()) == Some("refusal") { - return Err("LLM 拒答(refusal)".to_string()); + return Err("LLM declined to answer (refusal)".to_string()); } let text = val .get("content") @@ -262,7 +264,7 @@ impl LlmClient { }) .unwrap_or_default(); if text.is_empty() { - return Err("LLM 无 content".to_string()); + return Err("LLM response has no content".to_string()); } let u = val.get("usage"); let usage = Usage { @@ -326,7 +328,7 @@ impl Planner for LlmPlanner { .await .map_err(PlanError::Backend)?; let dtos: Vec = serde_json::from_str(extract_json(&text)) - .map_err(|e| PlanError::Backend(format!("规划结果解析失败: {e}")))?; + .map_err(|e| PlanError::Backend(format!("failed to parse the plan result: {e}")))?; Ok(dtos .into_iter() .map(|d| PlannedTask { @@ -370,11 +372,11 @@ impl LlmPrdDrafter { pub async fn draft(&self, raw: &str) -> Result { let text = self.client.complete(PRD_PROMPT_V, PRD_SYSTEM, raw).await?; let mut d: PrdDraft = serde_json::from_str(extract_json(&text)) - .map_err(|e| format!("PRD 草稿解析失败: {e}"))?; + .map_err(|e| format!("failed to parse the PRD draft: {e}"))?; d.title = d.title.trim().to_string(); d.acceptance_criteria.retain(|c| !c.trim().is_empty()); if d.title.is_empty() { - return Err("PRD 草稿缺标题".to_string()); + return Err("PRD draft is missing a title".to_string()); } Ok(d) } @@ -468,7 +470,9 @@ impl Judge for LlmJudge { match self.client.complete(JUDGE_PROMPT_V, JUDGE_SYSTEM, &user).await { Ok(text) => match serde_json::from_str::(extract_json(&text)) { Ok(v) => Verdict { passed: v.passed, reason: v.reason }, - Err(e) => Verdict { passed: false, reason: format!("裁决解析失败: {e}") }, + Err(e) => { + Verdict { passed: false, reason: format!("failed to parse verdict: {e}") } + } }, // Fail closed: an LLM error counts as not passed. Err(e) => Verdict { passed: false, reason: e }, @@ -522,12 +526,14 @@ impl AgentExecutor for LlmExecutor { .await .map_err(ExecError::Backend)?; let d: DeliverableDto = serde_json::from_str(extract_json(&text)) - .map_err(|e| ExecError::Backend(format!("交付物解析失败: {e}")))?; + .map_err(|e| ExecError::Backend(format!("failed to parse the deliverable: {e}")))?; let reference = if d.reference.is_empty() { format!("llm://{}", spec.task_id) } else { d.reference }; - if let Ok(ev) = - NewExecutionEvent::new(EventKind::Decision, "LLM 执行者产出交付物", Some(&d.summary)) - { + if let Ok(ev) = NewExecutionEvent::new( + EventKind::Decision, + "LLM executor produced a deliverable", + Some(&d.summary), + ) { sink.emit(ev).await; } Ok(DispatchOutcome::Completed { @@ -603,28 +609,28 @@ mod tests { RequirementSpec { requirement_id: "r1".into(), requirement_version: 1, - title: "登录".into(), - description: "邮箱登录".into(), - acceptance_criteria: vec!["登录成功".into()], + title: "login".into(), + description: "email login".into(), + acceptance_criteria: vec!["login success".into()], } } #[tokio::test] async fn llm_planner_parses_tasks_even_with_prose() { - let url = serve_openai("好的,这是计划:\n```json\n[{\"title\":\"实现登录\",\"acceptanceCriteria\":[\"登录成功\"],\"dependencies\":[]}]\n```").await; + let url = serve_openai("Sure, here is the plan:\n```json\n[{\"title\":\"implement login\",\"acceptanceCriteria\":[\"login success\"],\"dependencies\":[]}]\n```").await; let p = LlmPlanner::new(LlmClient::new(url, None, "m")); let tasks = p.plan(&spec()).await.expect("plan"); assert_eq!(tasks.len(), 1); - assert_eq!(tasks[0].title, "实现登录"); + assert_eq!(tasks[0].title, "implement login"); } #[tokio::test] async fn llm_judge_parses_verdict() { - let url = serve_openai("{\"passed\": false, \"reason\": \"缺测试\"}").await; + let url = serve_openai("{\"passed\": false, \"reason\": \"missing tests\"}").await; let j = LlmJudge::new(LlmClient::new(url, None, "m")); let v = j .judge( - &["登录成功".into()], + &["login success".into()], &DeliverableView { kind: "DIFF".into(), reference: "b".into(), @@ -633,12 +639,14 @@ mod tests { ) .await; assert!(!v.passed); - assert_eq!(v.reason, "缺测试"); + assert_eq!(v.reason, "missing tests"); } #[tokio::test] async fn llm_executor_parses_deliverable_and_emits_event() { - let url = serve_openai("{\"reference\":\"branch:feat\",\"summary\":\"实现完成\"}").await; + let url = + serve_openai("{\"reference\":\"branch:feat\",\"summary\":\"implementation done\"}") + .await; let e = LlmExecutor::new(LlmClient::new(url, None, "m")); let ws = WorkSpec { attempt_id: "a".into(), @@ -655,7 +663,7 @@ mod tests { match e.dispatch(&ws, &NoopEventSink).await.expect("dispatch") { DispatchOutcome::Completed { deliverable } => { assert_eq!(deliverable.reference, "branch:feat"); - assert_eq!(deliverable.summary, "实现完成"); + assert_eq!(deliverable.summary, "implementation done"); } other => panic!("expected Completed, got {other:?}"), } @@ -681,14 +689,14 @@ mod tests { #[tokio::test] async fn anthropic_wire_parses_content_blocks() { let url = serve_anthropic( - "[{\"title\":\"实现登录\",\"acceptanceCriteria\":[],\"dependencies\":[]}]", + "[{\"title\":\"implement login\",\"acceptanceCriteria\":[],\"dependencies\":[]}]", ) .await; let p = LlmPlanner::new( LlmClient::new(url, Some("k".into()), "claude-opus-4-8").with_wire(Wire::Anthropic), ); let tasks = p.plan(&spec()).await.expect("plan"); - assert_eq!(tasks[0].title, "实现登录"); + assert_eq!(tasks[0].title, "implement login"); } #[tokio::test] diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 8457ce09..9575e27f 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -203,7 +203,7 @@ async fn main() -> Result<(), Box> { // (keeps local dev working) but log a warning that is hard to miss. if matches!(cfg.admin_pw.as_str(), "admin" | "change-me" | "s3cret") { tracing::warn!( - "SHEPHERD_ADMIN_PASSWORD 使用弱默认值({:?});生产部署必须改为强随机口令", + "SHEPHERD_ADMIN_PASSWORD uses a weak default ({:?}); production deployments must switch to a strong random password", cfg.admin_pw ); } @@ -500,7 +500,7 @@ async fn main() -> Result<(), Box> { }; let n = q.reclaim_dead(&live, std::time::Duration::from_millis(grace_ms)).await; if n > 0 { - tracing::warn!(requeued = n, "fleet reaper: 重投死 runtime 任务"); + tracing::warn!(requeued = n, "fleet reaper: requeued dead runtime tasks"); } } }); @@ -601,11 +601,13 @@ async fn main() -> Result<(), Box> { Arc::new(HttpTaskDispatcher::new(url.clone())) } _ if cfg.runner_noop => { - tracing::warn!("api runner: Noop(SHEPHERD_RUNNER=noop)—— 批量运行不会产出结果"); + tracing::warn!( + "api runner: Noop (SHEPHERD_RUNNER=noop) — batch runs will produce no results" + ); Arc::new(api_test::adapters::NoopDispatcher) } _ => { - tracing::info!("api runner: 本地原生 Rust runner(默认)"); + tracing::info!("api runner: local native Rust runner (default)"); Arc::new( LocalRunnerDispatcher::new( Arc::new(PgCaseSpecSource::new(pool.clone())), diff --git a/crates/server/src/mcp_tools.rs b/crates/server/src/mcp_tools.rs index 86b24de1..17902ee6 100644 --- a/crates/server/src/mcp_tools.rs +++ b/crates/server/src/mcp_tools.rs @@ -678,7 +678,7 @@ async fn mcp_handler( let mint = is_initialize(&body); let resp = st.server.dispatch(body, &UserCaps(&user)).await; let errored = resp.as_ref().map(|r| r.get("error").is_some()).unwrap_or(false); - tracing::info!(target: "mcp_audit", user = %user.user_id, method, tool, ok = !errored, "MCP 调用"); + tracing::info!(target: "mcp_audit", user = %user.user_id, method, tool, ok = !errored, "MCP call"); let new_sid = if mint { Some(st.mcp_sessions.mint(&user.user_id)) } else { None }; let mut response = match resp { diff --git a/crates/server/src/openapi.rs b/crates/server/src/openapi.rs index a913c5be..e8363480 100644 --- a/crates/server/src/openapi.rs +++ b/crates/server/src/openapi.rs @@ -6,9 +6,9 @@ use utoipa::{Modify, OpenApi}; #[derive(OpenApi)] #[openapi( - info(title = "Shepherd API", version = env!("CARGO_PKG_VERSION"), description = "AI 研发监督平台 REST API"), + info(title = "Shepherd API", version = env!("CARGO_PKG_VERSION"), description = "Shepherd AI R&D supervision platform REST API"), modifiers(&BearerAddon), - tags((name = "auth", description = "鉴权")) + tags((name = "auth", description = "Authentication")) )] struct Base; diff --git a/crates/server/src/orchestration.rs b/crates/server/src/orchestration.rs index 7a115e09..88286931 100644 --- a/crates/server/src/orchestration.rs +++ b/crates/server/src/orchestration.rs @@ -148,7 +148,7 @@ impl Reviser for ExecutorReviser { reference: deliverable.reference, summary: deliverable.summary, }), - Ok(_) => Err(OrchError::Gateway("修订未产出交付物".into())), + Ok(_) => Err(OrchError::Gateway("revision produced no deliverable".into())), Err(e) => Err(OrchError::Gateway(format!("{e:?}"))), } } @@ -171,17 +171,17 @@ impl OrchestratorObserver { } match self.requirements.deliver(&dec.requirement_id, "orchestrator").await { Ok(_) => { - tracing::info!(requirement = %dec.requirement_id, "全部任务验证 → 需求自动交付(DELIVERED)"); + tracing::info!(requirement = %dec.requirement_id, "all tasks verified → requirement auto-delivered (DELIVERED)"); self.bus.publish(McpEvent { kind: "requirement", status: "delivered".into(), attempt_id: String::new(), task_id: String::new(), - message: format!("需求 {} 已交付", dec.requirement_id), + message: format!("requirement {} delivered", dec.requirement_id), }); } Err(e) => { - tracing::warn!(requirement = %dec.requirement_id, "需求自动交付失败(可能未定基线): {e:?}") + tracing::warn!(requirement = %dec.requirement_id, "requirement auto-delivery failed (baseline may not be set): {e:?}") } } } @@ -231,9 +231,9 @@ impl DeliveryObserver for OrchestratorObserver { { if let Some(v) = outcome.verdict { let msg = if v.passed { - format!("验证门通过: {}", v.reason) + format!("verification gate passed: {}", v.reason) } else { - format!("验证门未通过: {}", v.reason) + format!("verification gate failed: {}", v.reason) }; let _ = self.recorder.record_event(&attempt.id, "VERDICT", &msg, None).await; self.bus.publish(McpEvent { diff --git a/crates/server/src/perf_run.rs b/crates/server/src/perf_run.rs index ef7a6571..deb04a90 100644 --- a/crates/server/src/perf_run.rs +++ b/crates/server/src/perf_run.rs @@ -58,16 +58,20 @@ pub fn router( let sink: Option> = std::env::var("PERF_SAMPLES_PATH").ok().filter(|p| !p.trim().is_empty()).and_then(|root| { if let Err(e) = std::fs::create_dir_all(&root) { - tracing::warn!("PERF_SAMPLES_PATH 不可建({root}): {e};样本下沉关闭"); + tracing::warn!( + "PERF_SAMPLES_PATH cannot be created ({root}): {e}; sample sink disabled" + ); return None; } match ParquetObjectStoreSink::new_local(&root, "perf") { Ok(s) => { - tracing::info!("perf 样本下沉已启用 → {root}"); + tracing::info!("perf sample sink enabled → {root}"); Some(Arc::new(s) as Arc) } Err(e) => { - tracing::warn!("perf 样本下沉初始化失败: {e:?};仅存聚合"); + tracing::warn!( + "perf sample sink initialization failed: {e:?}; storing aggregates only" + ); None } } @@ -267,7 +271,7 @@ async fn run_perf( Some(s) => match s.write(&id, &samples).await { Ok(key) => Some(key), Err(e) => { - tracing::warn!(report = %id, "perf 样本下沉失败: {e:?}"); + tracing::warn!(report = %id, "perf sample sink write failed: {e:?}"); None } }, @@ -467,7 +471,7 @@ async fn run_scenario_perf( Some(s) => match s.write(&id, &samples).await { Ok(key) => Some(key), Err(e) => { - tracing::warn!(report = %id, "perf 场景样本下沉失败: {e:?}"); + tracing::warn!(report = %id, "perf scenario sample sink write failed: {e:?}"); None } }, @@ -487,7 +491,7 @@ async fn run_scenario_perf( #[openapi( paths(run_perf, run_scenario_perf, get_report), components(schemas(RunPerfBody, RunPerfResponse, RunScenarioPerfBody, RunScenarioPerfResponse)), - tags((name = "perf", description = "原生压测")) + tags((name = "perf", description = "Native load testing")) )] struct ApiDoc; diff --git a/crates/server/src/plan_run.rs b/crates/server/src/plan_run.rs index 81fda9ac..3f1df53f 100644 --- a/crates/server/src/plan_run.rs +++ b/crates/server/src/plan_run.rs @@ -148,7 +148,9 @@ impl PlanRunner { case_id, CaseStatus::Block, Some(CaseResult { - body: Some("用例规格未找到(非 ms_api_case / 非场景)".into()), + body: Some( + "case spec not found (neither ms_api_case nor a scenario)".into(), + ), ..Default::default() }), ) @@ -157,9 +159,9 @@ impl PlanRunner { } e => { let msg = match e { - RunError::CycleOrDepth => "场景引用成环或过深", - RunError::NoSteps => "场景无可执行步骤", - _ => "场景执行失败", + RunError::CycleOrDepth => "scenario reference is cyclic or too deep", + RunError::NoSteps => "scenario has no executable steps", + _ => "scenario execution failed", }; let _ = self .cases @@ -506,7 +508,7 @@ async fn run_plan( match st.plan_runner.run(&id, body.environment_id.as_deref(), body.pool_id.as_deref()).await { Ok(s) => { // Manual runs snapshot into the run history like scheduled runs do, - // so the 执行历史 tab reflects them. + // so the execution history tab reflects them. let stats = PlanStatisticsUseCase::new(Arc::new(PgPlanRepository::new(st.pool.clone()))); let run_uc = @@ -601,7 +603,7 @@ async fn run_plan_case( } #[derive(OpenApi)] -#[openapi(paths(run_plan, run_plan_case), components(schemas(RunPlanResponse, RunPlanBody, RunPlanCaseResponse)), tags((name = "test-plan", description = "测试计划")))] +#[openapi(paths(run_plan, run_plan_case), components(schemas(RunPlanResponse, RunPlanBody, RunPlanCaseResponse)), tags((name = "test-plan", description = "Test plan")))] struct ApiDoc; pub fn openapi() -> utoipa::openapi::OpenApi { diff --git a/crates/server/src/planner.rs b/crates/server/src/planner.rs index 9cd94862..c93355d8 100644 --- a/crates/server/src/planner.rs +++ b/crates/server/src/planner.rs @@ -40,14 +40,14 @@ impl Planner for HttpPlanner { .json(&body) .send() .await - .map_err(|e| PlanError::Backend(format!("planner 不可达: {e}")))?; + .map_err(|e| PlanError::Backend(format!("planner unreachable: {e}")))?; if !resp.status().is_success() { return Err(PlanError::Backend(format!("planner HTTP {}", resp.status()))); } let dtos: Vec = resp .json() .await - .map_err(|e| PlanError::Backend(format!("planner 响应解析失败: {e}")))?; + .map_err(|e| PlanError::Backend(format!("failed to parse planner response: {e}")))?; Ok(dtos .into_iter() .map(|d| PlannedTask { diff --git a/crates/server/src/pool_runner_ws.rs b/crates/server/src/pool_runner_ws.rs index 2670bc5f..dbded3fc 100644 --- a/crates/server/src/pool_runner_ws.rs +++ b/crates/server/src/pool_runner_ws.rs @@ -801,7 +801,7 @@ async fn runner_status( } /// Per-pool connected runner details (name / cap / in-flight) for the pool -/// detail page's 执行机接入 panel and the run-target indicators. +/// detail page's executor-connection panel and the run-target indicators. async fn runner_status_detail( _user: webauth::AuthUser, State(st): State, diff --git a/crates/server/src/prd_draft_route.rs b/crates/server/src/prd_draft_route.rs index d3916596..2d59b7fb 100644 --- a/crates/server/src/prd_draft_route.rs +++ b/crates/server/src/prd_draft_route.rs @@ -116,14 +116,15 @@ async fn draft_handler( Some(d) => match d.draft(raw).await { Ok(v) => (v, "llm"), Err(e) => { - tracing::warn!("PRD AI 起草失败,回落启发式: {e}"); + tracing::warn!("PRD AI drafting failed, falling back to heuristic: {e}"); (heuristic_draft(raw), "heuristic") } }, None => (heuristic_draft(raw), "heuristic"), }; if draft.title.is_empty() { - return (StatusCode::BAD_REQUEST, "无法从素材中提取需求").into_response(); + return (StatusCode::BAD_REQUEST, "cannot extract a requirement from the raw material") + .into_response(); } let priority = match draft.priority.trim().to_ascii_uppercase().as_str() { p @ ("P0" | "P1" | "P2" | "P3") => p.to_string(), @@ -148,20 +149,24 @@ mod tests { #[test] fn heuristic_extracts_title_bullets_and_description() { - let raw = "支持手机号登录\n用户希望免密快捷进入。\n- 验证码 60 秒内送达\n- 错误验证码明确报错\n1. 三次失败锁定五分钟"; + let raw = "support phone number login\nusers want quick passwordless entry\n- verification code arrives within 60 seconds\n- wrong verification code reports a clear error\n1. lock for five minutes after three failures"; let d = heuristic_draft(raw); - assert_eq!(d.title, "支持手机号登录"); - assert_eq!(d.description, "用户希望免密快捷进入。"); + assert_eq!(d.title, "support phone number login"); + assert_eq!(d.description, "users want quick passwordless entry"); assert_eq!( d.acceptance_criteria, - vec!["验证码 60 秒内送达", "错误验证码明确报错", "三次失败锁定五分钟"] + vec![ + "verification code arrives within 60 seconds", + "wrong verification code reports a clear error", + "lock for five minutes after three failures" + ] ); } #[test] fn heuristic_handles_prose_only() { - let d = heuristic_draft("就一句话的想法"); - assert_eq!(d.title, "就一句话的想法"); + let d = heuristic_draft("just a one-sentence idea"); + assert_eq!(d.title, "just a one-sentence idea"); assert!(d.acceptance_criteria.is_empty()); } } diff --git a/crates/server/src/references_route.rs b/crates/server/src/references_route.rs index 38a58f1d..2369509f 100644 --- a/crates/server/src/references_route.rs +++ b/crates/server/src/references_route.rs @@ -68,7 +68,7 @@ async fn list_references(State(st): State, Path(id): Path) -> Router { .layer(axum::middleware::from_fn(timeout_by_route)) .layer(RequestBodyLimitLayer::new(2 * 1024 * 1024)); if let Some(cors) = cors_layer() { - tracing::info!("CORS 允许列表已启用"); + tracing::info!("CORS allowlist enabled"); app = app.layer(cors); } if let Some(rl) = RateLimiter::from_env() { - tracing::info!("限流已启用(每客户端令牌桶)"); + tracing::info!("rate limiting enabled (per-client token bucket)"); app = app.layer(axum::middleware::from_fn_with_state(rl, ratelimit::layer)); } // Unified error bodies: normalize plain-text 4xx/5xx into problem+json diff --git a/crates/server/src/scenario_run.rs b/crates/server/src/scenario_run.rs index 5ae8d031..832475a0 100644 --- a/crates/server/src/scenario_run.rs +++ b/crates/server/src/scenario_run.rs @@ -1323,7 +1323,7 @@ fn run_error_response(e: RunError) -> Response { #[openapi( paths(run_scenario, batch_run_scenarios, build_scenario_report, create_scenario_share, public_scenario_report), components(schemas(RunScenarioBody, RunScenarioResponse, BatchRunBody, BatchRunResponse, BatchRunItem, ExecutedOn, ScenarioReportResponse, ReportResultItem, ReqInfo, ShareBody, ShareResponse)), - tags((name = "api-scenario", description = "场景")) + tags((name = "api-scenario", description = "Scenarios")) )] struct ApiDoc; diff --git a/crates/server/tests/scenarios.rs b/crates/server/tests/scenarios.rs index 2180c7c0..3692696d 100644 --- a/crates/server/tests/scenarios.rs +++ b/crates/server/tests/scenarios.rs @@ -60,7 +60,7 @@ impl TestServer { // Reap the child on the timeout path too, or a failing test leaves an orphan server holding the port. let _ = child.kill(); let _ = child.wait(); - panic!("server 未在超时内就绪(确认 PostgreSQL 在 {})", db_url()); + panic!("server did not become ready within the timeout (check PostgreSQL at {})", db_url()); } async fn login(&self) -> String { @@ -122,7 +122,7 @@ fn proj() -> String { } #[tokio::test] -#[ignore = "需要 PostgreSQL"] +#[ignore = "requires PostgreSQL"] async fn scenario_auth_and_rbac() { let s = TestServer::start().await; let p = proj(); @@ -155,7 +155,7 @@ async fn scenario_auth_and_rbac() { } #[tokio::test] -#[ignore = "需要 PostgreSQL"] +#[ignore = "requires PostgreSQL"] async fn scenario_requirement_versioning() { let s = TestServer::start().await; let p = proj(); @@ -163,7 +163,7 @@ async fn scenario_requirement_versioning() { let rid = s .post( "/requirement", - json!({"projectId":&p,"title":"登录","acceptanceCriteria":["正确凭证登录"]}), + json!({"projectId":&p,"title":"login","acceptanceCriteria":["login with correct credentials"]}), Some(&t), ) .await["id"] @@ -173,7 +173,7 @@ async fn scenario_requirement_versioning() { assert_eq!( s.post( &format!("/requirement/{rid}/version"), - json!({"description":"v2","acceptanceCriteria":["支持飞书"]}), + json!({"description":"v2","acceptanceCriteria":["supports Feishu"]}), Some(&t) ) .await["version"], @@ -182,7 +182,7 @@ async fn scenario_requirement_versioning() { assert_eq!(s.get(&format!("/requirement/{rid}"), &t).await["baselineVersion"], 1); assert_eq!( s.get(&format!("/requirement/{rid}/version/1"), &t).await["acceptanceCriteria"][0], - "正确凭证登录" + "login with correct credentials" ); let v = s.put(&format!("/requirement/{rid}/baseline"), json!({"version":2}), Some(&t)).await; assert_eq!(v["baselineVersion"], 2); @@ -190,16 +190,19 @@ async fn scenario_requirement_versioning() { } #[tokio::test] -#[ignore = "需要 PostgreSQL"] +#[ignore = "requires PostgreSQL"] async fn scenario_shepherd_full_chain() { let s = TestServer::start().await; let p = proj(); let t = s.login().await; - let rid = s.post("/requirement", json!({"projectId":&p,"title":"登录特性","acceptanceCriteria":["登录成功","错误密码拒绝"]}), Some(&t)).await["id"].as_str().unwrap().to_string(); + let rid = s.post("/requirement", json!({"projectId":&p,"title":"login feature","acceptanceCriteria":["login success","wrong password rejected"]}), Some(&t)).await["id"].as_str().unwrap().to_string(); let bd = s.post(&format!("/requirement/{rid}/breakdown"), Value::Null, Some(&t)).await; let did = bd["id"].as_str().unwrap().to_string(); assert_eq!(bd["tasks"].as_array().unwrap().len(), 3); - let vid = bd["verificationId"].as_str().expect("breakdown 应自动开验证账本").to_string(); + let vid = bd["verificationId"] + .as_str() + .expect("breakdown should open the verification ledger automatically") + .to_string(); let _ = s .post( &format!("/verification/{vid}/link"), @@ -207,7 +210,7 @@ async fn scenario_shepherd_full_chain() { Some(&t), ) .await; - let _ = s.post("/delivery", json!({"decompositionId":&did,"taskId":"t1","title":"实现登录API","executor":"CLAUDE_CODE"}), Some(&t)).await; + let _ = s.post("/delivery", json!({"decompositionId":&did,"taskId":"t1","title":"implement login API","executor":"CLAUDE_CODE"}), Some(&t)).await; let dec = s.get(&format!("/decomposition/{did}"), &t).await; assert_eq!(dec["tasks"][0]["status"], "VERIFIED"); let rep = s.get(&format!("/verification/{vid}/report"), &t).await; @@ -222,11 +225,11 @@ async fn serve_mock_llm() -> String { post(|Json(body): Json| async move { let sys = body["messages"][0]["content"].as_str().unwrap_or(""); let content = if sys.contains("规划器") { - r#"[{"title":"LLM任务A","description":"","acceptanceCriteria":["登录成功"],"dependencies":[]},{"title":"LLM任务B","description":"","acceptanceCriteria":["错误密码拒绝"],"dependencies":[0]}]"# + r#"[{"title":"LLM task A","description":"","acceptanceCriteria":["login success"],"dependencies":[]},{"title":"LLM task B","description":"","acceptanceCriteria":["wrong password rejected"],"dependencies":[0]}]"# } else if sys.contains("评审") { - r#"{"passed":false,"reason":"LLM 判定不通过(缺测试)"}"# + r#"{"passed":false,"reason":"LLM verdict: not passed (missing tests)"}"# } else if sys.contains("执行者") { - r#"{"reference":"branch:llm-feat","summary":"LLM 执行者完成"}"# + r#"{"reference":"branch:llm-feat","summary":"LLM executor finished"}"# } else { "{}" }; @@ -240,7 +243,7 @@ async fn serve_mock_llm() -> String { } #[tokio::test] -#[ignore = "需要 PostgreSQL"] +#[ignore = "requires PostgreSQL"] async fn scenario_llm_agent_end_to_end() { let llm_url = serve_mock_llm().await; let s = TestServer::start_with_env(&[("SHEPHERD_LLM_URL", &llm_url)]).await; @@ -248,7 +251,7 @@ async fn scenario_llm_agent_end_to_end() { let t = s.login().await; let rid = s - .post("/requirement", json!({"projectId":&p,"title":"登录特性","acceptanceCriteria":["登录成功","错误密码拒绝"]}), Some(&t)) + .post("/requirement", json!({"projectId":&p,"title":"login feature","acceptanceCriteria":["login success","wrong password rejected"]}), Some(&t)) .await["id"] .as_str() .unwrap() @@ -257,12 +260,15 @@ async fn scenario_llm_agent_end_to_end() { let bd = s.post(&format!("/requirement/{rid}/breakdown"), Value::Null, Some(&t)).await; let did = bd["id"].as_str().unwrap().to_string(); let tasks = bd["tasks"].as_array().unwrap(); - assert_eq!(tasks.len(), 2, "应由 LLM 规划器产出 2 任务,而非启发式的 3: {bd}"); - assert_eq!(tasks[0]["title"], "LLM任务A"); + assert_eq!(tasks.len(), 2, "LLM planner should produce 2 tasks, not the heuristic 3: {bd}"); + assert_eq!(tasks[0]["title"], "LLM task A"); - let _ = s.post("/delivery", json!({"decompositionId":&did,"taskId":"t1","title":"实现登录API","executor":"CLAUDE_CODE"}), Some(&t)).await; + let _ = s.post("/delivery", json!({"decompositionId":&did,"taskId":"t1","title":"implement login API","executor":"CLAUDE_CODE"}), Some(&t)).await; let dec = s.get(&format!("/decomposition/{did}"), &t).await; - assert_ne!(dec["tasks"][0]["status"], "VERIFIED", "LLM 判不通过时任务不应 VERIFIED: {dec}"); + assert_ne!( + dec["tasks"][0]["status"], "VERIFIED", + "task must not be VERIFIED when the LLM verdict fails: {dec}" + ); } async fn serve_mock_llm_selfcorrect() -> String { @@ -277,16 +283,16 @@ async fn serve_mock_llm_selfcorrect() -> String { async move { let sys = body["messages"][0]["content"].as_str().unwrap_or(""); let content: String = if sys.contains("规划器") { - r#"[{"title":"LLM任务A","description":"","acceptanceCriteria":["登录成功"],"dependencies":[]}]"#.into() + r#"[{"title":"LLM task A","description":"","acceptanceCriteria":["login success"],"dependencies":[]}]"#.into() } else if sys.contains("评审") { // First round fails (triggers revision); subsequent rounds pass. if jc.fetch_add(1, Ordering::SeqCst) == 0 { - r#"{"passed":false,"reason":"首轮缺测试"}"#.into() + r#"{"passed":false,"reason":"first round missing tests"}"#.into() } else { - r#"{"passed":true,"reason":"修订后达标"}"#.into() + r#"{"passed":true,"reason":"meets criteria after revision"}"#.into() } } else if sys.contains("执行者") { - r#"{"reference":"branch:llm","summary":"实现/修订完成"}"#.into() + r#"{"reference":"branch:llm","summary":"implementation/revision finished"}"#.into() } else { "{}".into() }; @@ -301,7 +307,7 @@ async fn serve_mock_llm_selfcorrect() -> String { } #[tokio::test] -#[ignore = "需要 PostgreSQL"] +#[ignore = "requires PostgreSQL"] async fn scenario_llm_self_correction_loop() { let llm_url = serve_mock_llm_selfcorrect().await; let s = TestServer::start_with_env(&[ @@ -314,7 +320,7 @@ async fn scenario_llm_self_correction_loop() { let rid = s .post( "/requirement", - json!({"projectId":&p,"title":"登录","acceptanceCriteria":["登录成功"]}), + json!({"projectId":&p,"title":"login","acceptanceCriteria":["login success"]}), Some(&t), ) .await["id"] @@ -325,13 +331,16 @@ async fn scenario_llm_self_correction_loop() { .as_str() .unwrap() .to_string(); - let _ = s.post("/delivery", json!({"decompositionId":&did,"taskId":"t1","title":"实现登录","executor":"CLAUDE_CODE"}), Some(&t)).await; + let _ = s.post("/delivery", json!({"decompositionId":&did,"taskId":"t1","title":"implement login","executor":"CLAUDE_CODE"}), Some(&t)).await; let dec = s.get(&format!("/decomposition/{did}"), &t).await; - assert_eq!(dec["tasks"][0]["status"], "VERIFIED", "自纠正后应通过验证门: {dec}"); + assert_eq!( + dec["tasks"][0]["status"], "VERIFIED", + "should pass the verification gate after self-correction: {dec}" + ); } #[tokio::test] -#[ignore = "需要 PostgreSQL"] +#[ignore = "requires PostgreSQL"] async fn scenario_parallel_decomposition_run() { let s = TestServer::start().await; let p = proj(); @@ -339,7 +348,7 @@ async fn scenario_parallel_decomposition_run() { let rid = s .post( "/requirement", - json!({"projectId":&p,"title":"并行特性","acceptanceCriteria":["a"]}), + json!({"projectId":&p,"title":"parallel feature","acceptanceCriteria":["a"]}), Some(&t), ) .await["id"] @@ -356,29 +365,33 @@ async fn scenario_parallel_decomposition_run() { let deps: Vec = deps.into_iter().map(String::from).collect(); json!({"title":title,"acceptanceCriteria":["x"],"dependencies":deps}) }; - s.post(&format!("/decomposition/{did}/task"), add("根", vec![]), Some(&t)).await; - s.post(&format!("/decomposition/{did}/task"), add("左", vec!["t1"]), Some(&t)).await; - s.post(&format!("/decomposition/{did}/task"), add("右", vec!["t1"]), Some(&t)).await; - s.post(&format!("/decomposition/{did}/task"), add("汇", vec!["t2", "t3"]), Some(&t)).await; + s.post(&format!("/decomposition/{did}/task"), add("root", vec![]), Some(&t)).await; + s.post(&format!("/decomposition/{did}/task"), add("left", vec!["t1"]), Some(&t)).await; + s.post(&format!("/decomposition/{did}/task"), add("right", vec!["t1"]), Some(&t)).await; + s.post(&format!("/decomposition/{did}/task"), add("join", vec!["t2", "t3"]), Some(&t)).await; let run = s.post(&format!("/decomposition/{did}/run"), json!({"maxConcurrency":4}), Some(&t)).await; assert_eq!(run["total"], 4); - assert_eq!(run["verified"], 4, "钻石 DAG 应全部验证: {run}"); + assert_eq!(run["verified"], 4, "the diamond DAG should be fully verified: {run}"); assert_eq!(run["failed"], 0); assert_eq!(run["blocked"], 0); let rounds = run["rounds"].as_u64().unwrap(); - assert!((3..=5).contains(&rounds), "应按依赖分层调度(≥3 轮): {run}"); + assert!((3..=5).contains(&rounds), "should schedule in dependency layers (≥3 rounds): {run}"); } #[tokio::test] -#[ignore = "需要 PostgreSQL"] +#[ignore = "requires PostgreSQL"] async fn scenario_skill_compose() { let s = TestServer::start().await; let p = proj(); let t = s.login().await; let base = s - .post("/skill", json!({"projectId":&p,"name":"基础","instructions":"遵循六边形"}), Some(&t)) + .post( + "/skill", + json!({"projectId":&p,"name":"basic","instructions":"follows hexagonal architecture"}), + Some(&t), + ) .await["id"] .as_str() .unwrap() @@ -386,7 +399,7 @@ async fn scenario_skill_compose() { let rust = s .post( "/skill", - json!({"projectId":&p,"name":"Rust","instructions":"用 thiserror","includes":[&base]}), + json!({"projectId":&p,"name":"Rust","instructions":"using thiserror","includes":[&base]}), Some(&t), ) .await["id"] @@ -396,11 +409,11 @@ async fn scenario_skill_compose() { let comp = s.post("/skill/compose", json!({"projectId":&p,"skillIds":[&rust]}), Some(&t)).await; assert_eq!(comp["skillIds"][0], base); let instr = comp["instructions"].as_str().unwrap(); - assert!(instr.contains("遵循六边形") && instr.contains("用 thiserror")); + assert!(instr.contains("follows hexagonal architecture") && instr.contains("using thiserror")); } #[tokio::test] -#[ignore = "需要 PostgreSQL"] +#[ignore = "requires PostgreSQL"] async fn scenario_mcp_tools() { let s = TestServer::start().await; let t = s.login().await; @@ -424,12 +437,12 @@ async fn scenario_mcp_tools() { assert!(names.contains(&"shepherd_create_requirement")); assert!(names.contains(&"shepherd_breakdown")); assert!(names.len() >= 10); - let call: Value = s.post("/mcp", json!({"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"shepherd_create_requirement","arguments":{"projectId":proj(),"title":"经 MCP 建的需求"}}}), Some(&t)).await; + let call: Value = s.post("/mcp", json!({"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"shepherd_create_requirement","arguments":{"projectId":proj(),"title":"requirement created via MCP"}}}), Some(&t)).await; assert_eq!(call["result"]["isError"], false); } #[tokio::test] -#[ignore = "需要 PostgreSQL"] +#[ignore = "requires PostgreSQL"] async fn scenario_api_definition_to_run() { let s = TestServer::start().await; let t = s.login().await; @@ -449,16 +462,16 @@ async fn scenario_api_definition_to_run() { .as_str() .unwrap() .to_string(); - let def = s.post("/api/definition", json!({"projectId":&p,"name":"登录接口","protocol":"HTTP","method":"POST","path":"/auth/login"}), Some(&t)).await; + let def = s.post("/api/definition", json!({"projectId":&p,"name":"login interface","protocol":"HTTP","method":"POST","path":"/auth/login"}), Some(&t)).await; assert_eq!(def["status"], "DRAFT"); let def_id = def["id"].as_str().unwrap().to_string(); - let case_id = s.post(&format!("/api/definition/{def_id}/case"), json!({"name":"正确凭证","method":"POST","url":"https://example.com/auth/login","assertions":[]}), Some(&t)).await["id"] + let case_id = s.post(&format!("/api/definition/{def_id}/case"), json!({"name":"correct credentials","method":"POST","url":"https://example.com/auth/login","assertions":[]}), Some(&t)).await["id"] .as_str().unwrap().to_string(); assert_eq!( s.status( "POST", &format!("/api/definition/{def_id}/mock"), - json!({"name":"登录200","responseStatus":200}), + json!({"name":"login 200","responseStatus":200}), Some(&t) ) .await, @@ -468,7 +481,7 @@ async fn scenario_api_definition_to_run() { s.get(&format!("/api/definition?projectId={p}"), &t).await.as_array().unwrap().len(), 1 ); - let scn = s.post("/api/scenario", json!({"projectId":&p,"name":"登录冒烟"}), Some(&t)).await + let scn = s.post("/api/scenario", json!({"projectId":&p,"name":"login smoke"}), Some(&t)).await ["id"] .as_str() .unwrap() @@ -500,7 +513,7 @@ async fn scenario_api_definition_to_run() { } #[tokio::test] -#[ignore = "需要 PostgreSQL"] +#[ignore = "requires PostgreSQL"] async fn scenario_standalone_case_and_executions() { let s = TestServer::start().await; let t = s.login().await; @@ -520,7 +533,7 @@ async fn scenario_standalone_case_and_executions() { .as_str() .unwrap() .to_string(); - let case = s.post("/api/case", json!({"projectId":&p,"name":"独立用例","method":"GET","url":"https://example.com/ping"}), Some(&t)).await; + let case = s.post("/api/case", json!({"projectId":&p,"name":"standalone case","method":"GET","url":"https://example.com/ping"}), Some(&t)).await; assert_eq!(case["apiDefinitionId"], ""); let case_id = case["id"].as_str().unwrap().to_string(); let page = s.get(&format!("/api/case?projectId={p}¤t=1&pageSize=10"), &t).await; @@ -530,8 +543,9 @@ async fn scenario_standalone_case_and_executions() { let cexec = s.get(&format!("/api/case/{case_id}/executions?current=1&pageSize=10"), &t).await; assert_eq!(cexec["total"], 0); assert!(cexec["items"].is_array()); - let scn = s.post("/api/scenario", json!({"projectId":&p,"name":"空执行场景"}), Some(&t)).await - ["id"] + let scn = s + .post("/api/scenario", json!({"projectId":&p,"name":"empty execution scenario"}), Some(&t)) + .await["id"] .as_str() .unwrap() .to_string(); diff --git a/crates/shepherd-cli/src/client.rs b/crates/shepherd-cli/src/client.rs index 89f79848..14e0968d 100644 --- a/crates/shepherd-cli/src/client.rs +++ b/crates/shepherd-cli/src/client.rs @@ -17,8 +17,8 @@ pub type R = Result>; pub static JSON_OUTPUT: AtomicBool = AtomicBool::new(false); pub const NO_KEY_HINT: &str = - "未配置 API key:执行 `shepherd login --api-key sak_…` 或设 SHEPHERD_API_KEY\ -(关键可在 个人中心 → API KEY 或 POST /system/apikey 签发)"; + "API key not configured: run `shepherd login --api-key sak_…` or set SHEPHERD_API_KEY\ + (issue a key from Profile → API KEY or POST /system/apikey)"; #[derive(Serialize, Deserialize, Default, Clone)] pub struct Config { @@ -148,7 +148,7 @@ impl Client { pub fn fetch_text(&self, url: &str) -> R { let resp = self.http.get(url).send()?; if !resp.status().is_success() { - return Err(format!("拉取 {url} 失败:HTTP {}", resp.status()).into()); + return Err(format!("failed to fetch {url}: HTTP {}", resp.status()).into()); } Ok(resp.text()?) } @@ -171,7 +171,7 @@ pub fn pretty(v: &Value) { /// Print a freshly-minted API key (returned only once) before the normal output. pub fn print_key(v: &Value) { if let Some(k) = v.get("key").and_then(|x| x.as_str()) { - println!(" 已创建,密钥(仅此一次可见):\n{k}"); + println!(" Created. Key (visible only this once):\n{k}"); } pretty(v); } @@ -182,8 +182,8 @@ pub fn cell(v: &Value) -> String { Value::String(s) => s.clone(), Value::Bool(b) => b.to_string(), Value::Number(n) => n.to_string(), - Value::Array(a) => format!("[{} 项]", a.len()), - Value::Object(o) => format!("{{{} 字段}}", o.len()), + Value::Array(a) => format!("[{} items]", a.len()), + Value::Object(o) => format!("{{{} fields}}", o.len()), } } @@ -194,7 +194,7 @@ pub fn render_human(v: &Value) { let cur = v.get("current").map(cell).unwrap_or_default(); let pages = v.get("totalPages").map(cell).unwrap_or_default(); if !total.is_empty() { - println!("\n共 {total} 条 · 第 {cur} 页 / 共 {pages} 页"); + println!("\n{total} items · page {cur} / {pages}"); } return; } @@ -207,7 +207,7 @@ pub fn render_human(v: &Value) { pub fn render_table(items: &[Value]) { if items.is_empty() { - println!("(空)"); + println!("(empty)"); return; } let mut cols: Vec = Vec::new(); @@ -280,8 +280,9 @@ pub fn status_assertions(expect_status: Option) -> Value { pub fn parse_headers(items: &[String]) -> R { let mut arr = Vec::with_capacity(items.len()); for it in items { - let (name, value) = - it.split_once(':').ok_or_else(|| format!("--header 需 'Name: value' 格式:{it}"))?; + let (name, value) = it + .split_once(':') + .ok_or_else(|| format!("--header requires 'Name: value' format: {it}"))?; arr.push(json!({"name": name.trim(), "value": value.trim()})); } Ok(Value::Array(arr)) @@ -290,42 +291,43 @@ pub fn parse_headers(items: &[String]) -> R { pub fn parse_vars(items: &[String]) -> R { let mut map = serde_json::Map::with_capacity(items.len()); for it in items { - let (k, v) = it.split_once('=').ok_or_else(|| format!("--var 需 'key=value' 格式:{it}"))?; + let (k, v) = + it.split_once('=').ok_or_else(|| format!("--var requires 'key=value' format: {it}"))?; map.insert(k.trim().to_string(), json!(v)); } Ok(Value::Object(map)) } -const TPL_REQUIREMENT: &str = "# 需求模板 +const TPL_REQUIREMENT: &str = "# Requirement template -标题: 用户登录 -描述: 支持邮箱 + 密码登录,签发会话令牌。 +Title: User login +Description: Support email + password login, issuing a session token. -验收标准: -- 正确凭证返回 token -- 错误凭证返回 401 -- 令牌过期后需重新登录 +Acceptance criteria: +- correct credentials return a token +- wrong credentials return 401 +- login again after the token expires -改好后录入(需先 `shepherd login`): +Once edited, submit it (run `shepherd login` first): shepherd req add --project \\ - --title \"用户登录\" \\ - --description \"支持邮箱 + 密码登录,签发会话令牌。\" \\ - --criteria \"正确凭证返回 token,错误凭证返回 401,令牌过期后需重新登录\" + --title \"User login\" \\ + --description \"Support email + password login, issuing a session token.\" \\ + --criteria \"correct credentials return a token,wrong credentials return 401,login again after the token expires\" "; -const TPL_GETTING_STARTED: &str = "# Shepherd 上手 +const TPL_GETTING_STARTED: &str = "# Getting started with Shepherd -前置:一个运行中的 server(默认 http://localhost:8088)。 +Prerequisite: a running server (default http://localhost:8088). -1. 配置认证:`shepherd login --url http://localhost:8088 --api-key sak_…` - (API key 在 个人中心 → API KEY 或 `POST /system/apikey` 签发;也可设环境变量 SHEPHERD_API_KEY) -2. 录入需求:见 `requirements/example.md` -3. 拆分任务:`shepherd decompose --req --version 1` -4. 派发执行:`shepherd dispatch --decomp --task --executor CLAUDE_CODE` -5. 验证 / 复查:`shepherd verify --help`、`shepherd decomposition --help` +1. Configure auth: `shepherd login --url http://localhost:8088 --api-key sak_…` + (issue an API key from Profile → API KEY or `POST /system/apikey`; the SHEPHERD_API_KEY env var also works) +2. Add requirements: see `requirements/example.md` +3. Decompose into tasks: `shepherd decompose --req --version 1` +4. Dispatch execution: `shepherd dispatch --decomp --task --executor CLAUDE_CODE` +5. Verify / review: `shepherd verify --help`, `shepherd decomposition --help` -各命令的完整参数见 `shepherd <命令> --help`。 +See `shepherd --help` for the full options of each command. "; /// Scaffold file manifest (relative path -> contents). Pure function for testability. @@ -344,7 +346,7 @@ pub fn normalize_agent(t: &str) -> R { // The brand is one word, but the claude-code spelling invites code-buddy; accept both. "codebuddy" | "code_buddy" => Ok("CODEBUDDY".into()), other => Err(format!( - "未知 agent 类型: {other}(支持 claude-code | codex | opencode | codebuddy)" + "unknown agent type: {other} (supported: claude-code | codex | opencode | codebuddy)" ) .into()), } @@ -352,9 +354,9 @@ pub fn normalize_agent(t: &str) -> R { pub fn gen_suite(c: &Client, project: &str, base: &str, no_scenario: bool) -> R<()> { let defs = c.get(&format!("/api/definition?projectId={project}"), true)?; - let list = defs.as_array().ok_or("接口定义列表不是数组")?; + let list = defs.as_array().ok_or("api definition list is not an array")?; if list.is_empty() { - return Err("项目内没有接口定义,先 `apidef import`".into()); + return Err("no api definitions in the project; run `apidef import` first".into()); } let (mut cases, mut scenarios, mut steps, mut failed) = (0u32, 0u32, 0u32, 0u32); for d in list { @@ -380,18 +382,18 @@ pub fn gen_suite(c: &Client, project: &str, base: &str, no_scenario: bool) -> R< true, ) }; - let ok = match mk_case(&format!("成功·期望{success_code}"), success_code) { + let ok = match mk_case(&format!("success·expect {success_code}"), success_code) { Ok(v) => v, Err(e) => { - eprintln!(" {name}: 成功用例创建失败:{e}"); + eprintln!(" {name}: failed to create the success case: {e}"); failed += 1; continue; } }; - let bad = match mk_case("失败·期望401", 401) { + let bad = match mk_case("fail·expect 401", 401) { Ok(v) => v, Err(e) => { - eprintln!(" {name}: 失败用例创建失败:{e}"); + eprintln!(" {name}: failed to create the failure case: {e}"); failed += 1; continue; } @@ -407,12 +409,12 @@ pub fn gen_suite(c: &Client, project: &str, base: &str, no_scenario: bool) -> R< } let sc = match c.post( "/api/scenario", - json!({"projectId": project, "name": format!("{name} 场景")}), + json!({"projectId": project, "name": format!("{name} scenario")}), true, ) { Ok(v) => v, Err(e) => { - eprintln!(" {name}: 场景创建失败:{e}"); + eprintln!(" {name}: failed to create the scenario: {e}"); failed += 1; continue; } @@ -426,12 +428,12 @@ pub fn gen_suite(c: &Client, project: &str, base: &str, no_scenario: bool) -> R< true, ) { Ok(_) => steps += 1, - Err(e) => eprintln!(" {name}: 步骤{order}添加失败:{e}"), + Err(e) => eprintln!(" {name}: failed to add step {order}: {e}"), } } } println!( - " 生成完成:{} 个接口 → {cases} 条用例、{scenarios} 条场景、{steps} 个步骤(失败 {failed})", + " Generation done: {} api definitions → {cases} cases, {scenarios} scenarios, {steps} steps ({failed} failed)", list.len() ); Ok(()) diff --git a/crates/shepherd-cli/src/commands/agent.rs b/crates/shepherd-cli/src/commands/agent.rs index ec073b13..2c11cdf1 100644 --- a/crates/shepherd-cli/src/commands/agent.rs +++ b/crates/shepherd-cli/src/commands/agent.rs @@ -23,24 +23,27 @@ pub fn run(cmd: AgentCmd) -> R<()> { cfg.agent = Some(executor.clone()); cfg.save()?; println!( - " 已连接 agent: {executor} 服务 {} {}", + " connected agent: {executor} server {} {}", cfg.url, - if healthy { "(可达)" } else { "(暂不可达)" } + if healthy { "(reachable)" } else { "(not reachable yet)" } ); } AgentCmd::Status => { let cfg = Config::load(); let healthy = Client::new(cfg.clone())?.get("/healthz", false).is_ok(); - println!("服务 : {}", cfg.url); - println!("API key: {}", if cfg.api_key.is_empty() { "未配置" } else { "已配置" }); - println!("agent : {}", cfg.agent.as_deref().unwrap_or("(未连接,默认 CLAUDE_CODE)")); - println!("健康 : {}", if healthy { "可达" } else { "不可达" }); + println!("server : {}", cfg.url); + println!("API key: {}", if cfg.api_key.is_empty() { "not set" } else { "set" }); + println!( + "agent : {}", + cfg.agent.as_deref().unwrap_or("(not connected, defaults to CLAUDE_CODE)") + ); + println!("health : {}", if healthy { "reachable" } else { "unreachable" }); } AgentCmd::Disconnect => { let mut cfg = Config::load(); cfg.agent = None; cfg.save()?; - println!("已断开 agent 连接(dispatch 回落默认 CLAUDE_CODE)"); + println!("agent disconnected (dispatch falls back to CLAUDE_CODE)"); } }; Ok(()) diff --git a/crates/shepherd-cli/src/commands/apidef.rs b/crates/shepherd-cli/src/commands/apidef.rs index 6c261f91..6730d555 100644 --- a/crates/shepherd-cli/src/commands/apidef.rs +++ b/crates/shepherd-cli/src/commands/apidef.rs @@ -196,10 +196,10 @@ pub fn run(cmd: ApidefCmd) -> R<()> { let raw = match (url, file) { (Some(u), _) => c.fetch_text(&u)?, (None, Some(f)) => std::fs::read_to_string(&f)?, - (None, None) => return Err("需指定 --file 或 --url".into()), + (None, None) => return Err("either --file or --url must be specified".into()), }; - let content: Value = - serde_json::from_str(&raw).map_err(|e| format!("导入内容不是合法 JSON: {e}"))?; + let content: Value = serde_json::from_str(&raw) + .map_err(|e| format!("import content is not valid JSON: {e}"))?; pretty(&c.post( "/api/definition/import", json!({"projectId": project, "content": content}), @@ -287,16 +287,16 @@ pub fn run(cmd: ApidefCmd) -> R<()> { } ApidefCmd::Delete { id } => { c.delete(&format!("/api/definition/{id}"), true)?; - println!(" 已删除接口定义 {id}"); + println!(" deleted API definition {id}"); } ApidefCmd::References { id } => { pretty(&c.get(&format!("/api/definition/{id}/references"), true)?) } ApidefCmd::Spec { id, spec_json } => { let spec: Value = serde_json::from_str(&spec_json) - .map_err(|e| format!("--spec-json 不是合法 JSON: {e}"))?; + .map_err(|e| format!("--spec-json is not valid JSON: {e}"))?; c.put(&format!("/api/definition/{id}/spec"), json!({ "spec": spec }), true)?; - println!(" 已更新接口定义 {id} 的规格"); + println!(" updated spec of API definition {id}"); } ApidefCmd::Module { id, module_id, unset } => { let mid = if unset { None } else { module_id }; @@ -306,8 +306,8 @@ pub fn run(cmd: ApidefCmd) -> R<()> { true, )?; println!( - " 接口定义 {id} 已{}", - if mid.is_some() { "移入模块" } else { "移出到未归类" } + " API definition {id} {}", + if mid.is_some() { "moved into module" } else { "moved out to uncategorized" } ); } ApidefCmd::Status { id, status } => { @@ -316,7 +316,7 @@ pub fn run(cmd: ApidefCmd) -> R<()> { json!({ "status": status }), true, )?; - println!(" 已设置接口定义 {id} 状态为 {status}"); + println!(" set API definition {id} status to {status}"); } ApidefCmd::Changes { id } => { pretty(&c.get(&format!("/api/definition/{id}/changes"), true)?) diff --git a/crates/shepherd-cli/src/commands/apikey.rs b/crates/shepherd-cli/src/commands/apikey.rs index 369e439c..9a68dc2d 100644 --- a/crates/shepherd-cli/src/commands/apikey.rs +++ b/crates/shepherd-cli/src/commands/apikey.rs @@ -61,11 +61,11 @@ pub fn run(cmd: ApikeyCmd) -> R<()> { ApikeyCmd::Mine => pretty(&c.get("/system/apikey/mine", true)?), ApikeyCmd::Delete { id } => { c.delete(&format!("/system/apikey/{id}"), true)?; - println!(" 已吊销 {id}"); + println!(" revoked {id}"); } ApikeyCmd::Enable { id, disable } => { c.put(&format!("/system/apikey/{id}/enabled"), json!({"enabled": !disable}), true)?; - println!(" API key {id} 已{}", if disable { "禁用" } else { "启用" }); + println!(" API key {id} {}", if disable { "disabled" } else { "enabled" }); } }; Ok(()) diff --git a/crates/shepherd-cli/src/commands/auth.rs b/crates/shepherd-cli/src/commands/auth.rs index ccd85ac9..656fc822 100644 --- a/crates/shepherd-cli/src/commands/auth.rs +++ b/crates/shepherd-cli/src/commands/auth.rs @@ -38,7 +38,7 @@ pub fn run(cmd: AuthCmd) -> R<()> { let v = c.post("/auth/login", json!({"username": username, "password": password}), false)?; println!( - " 登录成功,会话 token:\n{}", + " login success, session token:\n{}", v.get("token").and_then(|t| t.as_str()).unwrap_or("") ); pretty(&v); @@ -46,7 +46,7 @@ pub fn run(cmd: AuthCmd) -> R<()> { AuthCmd::Logout => { let c = Client::new(cfg)?; c.post("/auth/logout", json!({}), true)?; - println!(" 已登出"); + println!(" logged out"); } AuthCmd::Refresh => { let c = Client::new(cfg)?; @@ -63,7 +63,7 @@ pub fn run(cmd: AuthCmd) -> R<()> { json!({"oldPassword": old_password, "newPassword": new_password}), true, )?; - println!(" 密码已修改"); + println!(" password changed"); } AuthCmd::Oidc { provider } => { let base = cfg.url.trim_end_matches('/'); diff --git a/crates/shepherd-cli/src/commands/comment.rs b/crates/shepherd-cli/src/commands/comment.rs index dad18a9d..adc955c3 100644 --- a/crates/shepherd-cli/src/commands/comment.rs +++ b/crates/shepherd-cli/src/commands/comment.rs @@ -39,7 +39,7 @@ pub fn run(cmd: CommentCmd) -> R<()> { ), CommentCmd::Delete { id } => { c.delete(&format!("/comment/{id}"), true)?; - println!(" 已删除评论 {id}"); + println!(" deleted comment {id}"); } }; Ok(()) diff --git a/crates/shepherd-cli/src/commands/debug.rs b/crates/shepherd-cli/src/commands/debug.rs index 685e1e53..cbb2740a 100644 --- a/crates/shepherd-cli/src/commands/debug.rs +++ b/crates/shepherd-cli/src/commands/debug.rs @@ -36,7 +36,7 @@ pub fn run(cmd: DebugCmd) -> R<()> { for h in &headers { let (k, v) = h .split_once(':') - .ok_or_else(|| format!("--header 需 'Name: value' 格式:{h}"))?; + .ok_or_else(|| format!("--header requires 'Name: value' format: {h}"))?; hdr_arr.push(json!({ "key": k.trim(), "value": v.trim() })); } let mut metadata = serde_json::Map::new(); @@ -47,7 +47,7 @@ pub fn run(cmd: DebugCmd) -> R<()> { } let assertions: Value = match assertions_json { Some(aj) => serde_json::from_str(&aj) - .map_err(|e| format!("--assertions-json 不是合法 JSON: {e}"))?, + .map_err(|e| format!("--assertions-json is not valid JSON: {e}"))?, None => json!([]), }; let req = json!({ diff --git a/crates/shepherd-cli/src/commands/fcase.rs b/crates/shepherd-cli/src/commands/fcase.rs index 393a4ca5..d735894e 100644 --- a/crates/shepherd-cli/src/commands/fcase.rs +++ b/crates/shepherd-cli/src/commands/fcase.rs @@ -55,7 +55,7 @@ pub fn run(cmd: FcaseCmd) -> R<()> { let bytes = c.get_bytes(&format!("/functional-case/export?projectId={project}"), true)?; std::fs::write(&out, &bytes)?; - println!(" 已导出 {} 字节 → {out}", bytes.len()); + println!(" exported {} bytes → {out}", bytes.len()); } FcaseCmd::Import { project, file } => { let bytes = std::fs::read(&file)?; diff --git a/crates/shepherd-cli/src/commands/import.rs b/crates/shepherd-cli/src/commands/import.rs index e6261d43..68608120 100644 --- a/crates/shepherd-cli/src/commands/import.rs +++ b/crates/shepherd-cli/src/commands/import.rs @@ -171,7 +171,7 @@ pub fn run(cmd: ImportCmd) -> R<()> { } ImportCmd::ScheduleDelete { id } => { c.delete(&format!("/api/import-schedule/{id}"), true)?; - println!(" 已删除导入计划 {id}"); + println!(" deleted import schedule {id}"); } ImportCmd::ScheduleEnable { id, disable } => { c.put( @@ -179,7 +179,7 @@ pub fn run(cmd: ImportCmd) -> R<()> { json!({ "enabled": !disable }), true, )?; - println!(" 导入计划 {id} 已{}", if disable { "禁用" } else { "启用" }); + println!(" import schedule {id} {}", if disable { "disabled" } else { "enabled" }); } ImportCmd::ScheduleRun { id } => { pretty(&c.post(&format!("/api/import-schedule/{id}/run"), json!({}), true)?) diff --git a/crates/shepherd-cli/src/commands/llm.rs b/crates/shepherd-cli/src/commands/llm.rs index 76f1f1a0..ab6552bf 100644 --- a/crates/shepherd-cli/src/commands/llm.rs +++ b/crates/shepherd-cli/src/commands/llm.rs @@ -74,7 +74,7 @@ pub fn run(cmd: LlmCmd) -> R<()> { } LlmCmd::Delete { id } => { c.delete(&format!("/me/llm-model/{id}"), true)?; - println!(" 已删除 LLM model {id}"); + println!(" deleted LLM model {id}"); } }; Ok(()) diff --git a/crates/shepherd-cli/src/commands/mcp.rs b/crates/shepherd-cli/src/commands/mcp.rs index 66e86be6..928b162c 100644 --- a/crates/shepherd-cli/src/commands/mcp.rs +++ b/crates/shepherd-cli/src/commands/mcp.rs @@ -33,7 +33,7 @@ pub fn run(cmd: McpCmd) -> R<()> { McpCmd::Call { method, params_json, id } => { let params: Value = match params_json { Some(p) => serde_json::from_str(&p) - .map_err(|e| format!("--params-json 不是合法 JSON: {e}"))?, + .map_err(|e| format!("--params-json is not valid JSON: {e}"))?, None => Value::Null, }; let mut body = json!({ "jsonrpc": "2.0", "method": method }); @@ -53,7 +53,7 @@ pub fn run(cmd: McpCmd) -> R<()> { McpCmd::ToolsCall { name, args_json } => { let args: Value = match args_json { Some(a) => serde_json::from_str(&a) - .map_err(|e| format!("--args-json 不是合法 JSON: {e}"))?, + .map_err(|e| format!("--args-json is not valid JSON: {e}"))?, None => json!({}), }; pretty(&c.post( diff --git a/crates/shepherd-cli/src/commands/notice.rs b/crates/shepherd-cli/src/commands/notice.rs index dbc04582..691a8efa 100644 --- a/crates/shepherd-cli/src/commands/notice.rs +++ b/crates/shepherd-cli/src/commands/notice.rs @@ -179,11 +179,11 @@ pub fn run(cmd: NoticeCmd) -> R<()> { None => "/notice/read-all".to_string(), }; c.post(&path, json!({}), true)?; - println!(" 已全部已读"); + println!(" marked all as read"); } NoticeCmd::Read { id } => { c.post(&format!("/notice/{id}/read"), json!({}), true)?; - println!(" 已标记已读 {id}"); + println!(" marked {id} as read"); } NoticeCmd::Robots { cmd } => match cmd { NoticeRobotCmd::List => pretty(&c.get("/notice/robots", true)?), @@ -220,7 +220,7 @@ pub fn run(cmd: NoticeCmd) -> R<()> { } NoticeRobotCmd::Delete { id } => { c.delete(&format!("/notice/robots/{id}"), true)?; - println!(" 已删除 robot {id}"); + println!(" deleted robot {id}"); } NoticeRobotCmd::Test { id } => { pretty(&c.post(&format!("/notice/robots/{id}/test"), json!({}), true)?); @@ -261,7 +261,7 @@ pub fn run(cmd: NoticeCmd) -> R<()> { )?), NoticeRuleCmd::Delete { id } => { c.delete(&format!("/notice/rules/{id}"), true)?; - println!(" 已删除 rule {id}"); + println!(" deleted rule {id}"); } }, }; diff --git a/crates/shepherd-cli/src/commands/oidc_admin.rs b/crates/shepherd-cli/src/commands/oidc_admin.rs index 136710d1..a7ef50be 100644 --- a/crates/shepherd-cli/src/commands/oidc_admin.rs +++ b/crates/shepherd-cli/src/commands/oidc_admin.rs @@ -128,7 +128,7 @@ pub fn run(cmd: OidcAdminCmd) -> R<()> { } OidcAdminCmd::Delete { provider_key } => { c.delete(&format!("/system/oidc/providers/{provider_key}"), true)?; - println!(" 已删除 OIDC provider: {provider_key}"); + println!(" deleted OIDC provider: {provider_key}"); } } Ok(()) diff --git a/crates/shepherd-cli/src/commands/pfile.rs b/crates/shepherd-cli/src/commands/pfile.rs index 110dc84e..0032086e 100644 --- a/crates/shepherd-cli/src/commands/pfile.rs +++ b/crates/shepherd-cli/src/commands/pfile.rs @@ -84,32 +84,37 @@ pub fn run(cmd: PfileCmd) -> R<()> { body["moduleId"] = json!(m); } let v = c.post("/api/project-file", body, true)?; - println!(" 已上传 {}", v.get("id").and_then(|x| x.as_str()).unwrap_or("")); + println!(" uploaded {}", v.get("id").and_then(|x| x.as_str()).unwrap_or("")); pretty(&v); } PfileCmd::Delete { id } => { c.delete(&format!("/api/project-file/{id}"), true)?; - println!(" 已删除文件 {id}"); + println!(" deleted file {id}"); } PfileCmd::Move { id, module_id, unset } => { let mid = if unset { None } else { module_id }; c.put(&format!("/api/project-file/{id}/module"), json!({ "moduleId": mid }), true)?; - println!(" 文件 {id} 已{}", if mid.is_some() { "移入模块" } else { "移出到未归类" }); + println!( + " file {id} {}", + if mid.is_some() { "moved into module" } else { "moved out to uncategorized" } + ); } PfileCmd::Download { id, out } => { let v = c.get(&format!("/api/project-file/{id}/download"), true)?; - let b64 = - v.get("contentBase64").and_then(|x| x.as_str()).ok_or("响应缺少 contentBase64")?; + let b64 = v + .get("contentBase64") + .and_then(|x| x.as_str()) + .ok_or("response is missing contentBase64")?; let bytes = base64::engine::general_purpose::STANDARD .decode(b64.trim()) - .map_err(|e| format!("base64 解码失败:{e}"))?; + .map_err(|e| format!("base64 decode failed: {e}"))?; std::fs::write(&out, &bytes)?; - println!(" 已下载 {} 字节 → {out}", bytes.len()); + println!(" downloaded {} bytes → {out}", bytes.len()); } PfileCmd::Raw { id, out } => { let bytes = c.get_bytes(&format!("/api/project-file/{id}/raw"), true)?; std::fs::write(&out, &bytes)?; - println!(" 已写出 {out} ({} 字节)", bytes.len()); + println!(" wrote {out} ({} bytes)", bytes.len()); } }; Ok(()) diff --git a/crates/shepherd-cli/src/commands/plan.rs b/crates/shepherd-cli/src/commands/plan.rs index ad3b9407..6490b64f 100644 --- a/crates/shepherd-cli/src/commands/plan.rs +++ b/crates/shepherd-cli/src/commands/plan.rs @@ -112,7 +112,7 @@ pub fn run(cmd: PlanCmd) -> R<()> { PlanCmd::Result { id, case, status, latency_ms, status_code, body, assertions_json } => { let assertions: Value = match assertions_json { Some(aj) => serde_json::from_str(&aj) - .map_err(|e| format!("--assertions-json 不是合法 JSON: {e}"))?, + .map_err(|e| format!("--assertions-json is not valid JSON: {e}"))?, None => json!([]), }; pretty(&c.post( diff --git a/crates/shepherd-cli/src/commands/root.rs b/crates/shepherd-cli/src/commands/root.rs index c0e1e9c1..baf4d2eb 100644 --- a/crates/shepherd-cli/src/commands/root.rs +++ b/crates/shepherd-cli/src/commands/root.rs @@ -10,15 +10,17 @@ pub fn run_init(dir: String, force: bool) -> R<()> { for (rel, contents) in scaffold_files() { let path = root.join(rel); if path.exists() && !force { - return Err(format!("已存在 {}(加 --force 覆盖)", path.display()).into()); + return Err( + format!("{} already exists (use --force to overwrite)", path.display()).into() + ); } if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } std::fs::write(&path, contents)?; - println!("写入 {}", path.display()); + println!("wrote {}", path.display()); } - println!("下一步:编辑 requirements/example.md,然后 `shepherd login` 并按其中命令录入需求。"); + println!("Next: edit requirements/example.md, then run `shepherd login` and follow the commands in it to enter requirements."); Ok(()) } @@ -34,10 +36,10 @@ pub fn run_login(url: String, api_key: Option) -> R<()> { let healthy = Client::new(cfg.clone())?.get("/healthz", false).is_ok(); cfg.save()?; println!( - " 已保存 {} 的 API key → {} 服务{}", + " saved API key for {} → {}; server is {}", cfg.url, config_path().display(), - if healthy { "可达" } else { "暂不可达" } + if healthy { "reachable" } else { "not reachable yet" } ); Ok(()) } @@ -46,7 +48,7 @@ pub fn run_logout() -> R<()> { let mut cfg = Config::load(); cfg.api_key.clear(); cfg.save()?; - println!(" 已清除本地 API key(要让 key 失效,请在服务端 API KEY 管理里吊销)"); + println!(" cleared local API key (to invalidate the key itself, revoke it in the server-side API KEY management)"); Ok(()) } @@ -75,7 +77,7 @@ pub fn run_dispatch( let c = Client::new(cfg)?; let mut instr = instructions; if !skills.is_empty() { - let project = project.ok_or("--skills 需配合 --project")?; + let project = project.ok_or("--skills requires --project")?; let comp = c.post("/skill/compose", json!({"projectId": project, "skillIds": skills}), true)?; let composed = comp["instructions"].as_str().unwrap_or("").to_string(); diff --git a/crates/shepherd-cli/src/commands/scenario.rs b/crates/shepherd-cli/src/commands/scenario.rs index c96f8aaf..239ae143 100644 --- a/crates/shepherd-cli/src/commands/scenario.rs +++ b/crates/shepherd-cli/src/commands/scenario.rs @@ -115,14 +115,14 @@ pub fn run(cmd: ScenarioCmd) -> R<()> { if let Some(m) = method { let assertions = match assertions_json { Some(aj) => serde_json::from_str(&aj) - .map_err(|e| format!("--assertions-json 不是合法 JSON: {e}"))?, + .map_err(|e| format!("--assertions-json is not valid JSON: {e}"))?, None => status_assertions(expect_status), }; step["request"] = json!({"method": m, "url": url.unwrap_or_default(), "body": body, "assertions": assertions}); } if let Some(cj) = control_json { step["control"] = serde_json::from_str(&cj) - .map_err(|e| format!("--control-json 不是合法 JSON: {e}"))?; + .map_err(|e| format!("--control-json is not valid JSON: {e}"))?; } pretty(&c.post(&format!("/api/scenario/{scenario}/step"), step, true)?) } diff --git a/crates/shepherd-cli/src/main.rs b/crates/shepherd-cli/src/main.rs index 2a83f8ad..816265a2 100644 --- a/crates/shepherd-cli/src/main.rs +++ b/crates/shepherd-cli/src/main.rs @@ -13,7 +13,7 @@ use crate::client::{JSON_OUTPUT, R}; use crate::commands::*; #[derive(Parser)] -#[command(name = "shepherd", version, about = "Shepherd —— AI 研发监督平台 CLI")] +#[command(name = "shepherd", version, about = "Shepherd — CLI for the AI R&D supervision platform")] struct Cli { /// Output raw JSON (default renders human-readable tables / key-values). For scripts/pipes. #[arg(long, global = true)] @@ -353,8 +353,8 @@ mod tests { .expect("client"); let err = c.get("/organization", true).expect_err("no key must fail before any request"); let msg = err.to_string(); - assert!(msg.contains("SHEPHERD_API_KEY"), "要点名环境变量: {msg}"); - assert!(msg.contains("/system/apikey"), "要给签发指引: {msg}"); + assert!(msg.contains("SHEPHERD_API_KEY"), "must name the environment variable: {msg}"); + assert!(msg.contains("/system/apikey"), "must give issuance guidance: {msg}"); } #[test] diff --git a/crates/skill/src/adapters/http.rs b/crates/skill/src/adapters/http.rs index 8292193c..3907350c 100644 --- a/crates/skill/src/adapters/http.rs +++ b/crates/skill/src/adapters/http.rs @@ -247,7 +247,7 @@ async fn compose( #[openapi( paths(create_skill, list_skills, compose, get_skill, update_skill, delete_skill), components(schemas(CreateBody, UpdateBody, ComposeBody, SkillResponse, ComposeResponse)), - tags((name = "skill", description = "AI Skill 编排")) + tags((name = "skill", description = "AI skill orchestration")) )] struct ApiDoc; pub fn openapi() -> utoipa::openapi::OpenApi { @@ -295,14 +295,14 @@ mod tests { .oneshot(req( "POST", "/skill", - r#"{"projectId":"p1","name":"基础","instructions":"遵循六边形"}"#, + r#"{"projectId":"p1","name":"basic","instructions":"follows hexagonal architecture"}"#, Some(&t), )) .await .expect("r"); assert_eq!(a.status(), StatusCode::CREATED); let aid = json(a).await["id"].as_str().expect("id").to_string(); - let b = app.clone().oneshot(req("POST", "/skill", &format!(r#"{{"projectId":"p1","name":"Rust","instructions":"用 thiserror","includes":["{aid}"]}}"#), Some(&t))).await.expect("r"); + let b = app.clone().oneshot(req("POST", "/skill", &format!(r#"{{"projectId":"p1","name":"Rust","instructions":"using thiserror","includes":["{aid}"]}}"#), Some(&t))).await.expect("r"); let bid = json(b).await["id"].as_str().expect("id").to_string(); let c = app @@ -318,8 +318,8 @@ mod tests { assert_eq!(c.status(), StatusCode::OK); let cv = json(c).await; assert_eq!(cv["skillIds"][0], aid); - assert!(cv["instructions"].as_str().expect("s").contains("遵循六边形")); - assert!(cv["instructions"].as_str().expect("s").contains("用 thiserror")); + assert!(cv["instructions"].as_str().expect("s").contains("follows hexagonal architecture")); + assert!(cv["instructions"].as_str().expect("s").contains("using thiserror")); assert_eq!( app.clone() diff --git a/crates/skill/src/adapters/pg.rs b/crates/skill/src/adapters/pg.rs index 63334ad4..ccdcf846 100644 --- a/crates/skill/src/adapters/pg.rs +++ b/crates/skill/src/adapters/pg.rs @@ -112,7 +112,7 @@ mod tests { use super::*; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_compose_roundtrip() { use crate::domain::SkillLibrary; let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); @@ -122,12 +122,15 @@ mod tests { let repo = PgSkillRepository::new(pool.clone()); let a = repo - .insert(&NewSkill::new("p1", "基础", "", "遵循六边形", &[]).expect("v")) + .insert( + &NewSkill::new("p1", "basic", "", "follows hexagonal architecture", &[]) + .expect("v"), + ) .await .expect("a"); let _b = repo .insert( - &NewSkill::new("p1", "Rust", "", "用 thiserror", std::slice::from_ref(&a.id)) + &NewSkill::new("p1", "Rust", "", "using thiserror", std::slice::from_ref(&a.id)) .expect("v"), ) .await @@ -137,7 +140,8 @@ mod tests { let comp = lib.compose(std::slice::from_ref(&_b.id)).expect("compose"); assert_eq!(comp.skill_ids[0], a.id); assert!( - comp.instructions.contains("遵循六边形") && comp.instructions.contains("用 thiserror") + comp.instructions.contains("follows hexagonal architecture") + && comp.instructions.contains("using thiserror") ); } } diff --git a/crates/skill/src/application/create_skill.rs b/crates/skill/src/application/create_skill.rs index bdcfdc84..b90ed51d 100644 --- a/crates/skill/src/application/create_skill.rs +++ b/crates/skill/src/application/create_skill.rs @@ -53,7 +53,7 @@ mod tests { #[tokio::test] async fn creates_and_rejects_duplicate() { let uc = uc(); - uc.execute("p1", "Rust", "", "用 thiserror", &[]).await.expect("ok"); + uc.execute("p1", "Rust", "", "using thiserror", &[]).await.expect("ok"); assert_eq!( uc.execute("p1", "Rust", "", "x", &[]).await.unwrap_err(), CreateSkillError::NameAlreadyExists diff --git a/crates/skill/src/application/skill_admin.rs b/crates/skill/src/application/skill_admin.rs index 80fd6493..bffceda6 100644 --- a/crates/skill/src/application/skill_admin.rs +++ b/crates/skill/src/application/skill_admin.rs @@ -101,17 +101,21 @@ mod tests { async fn compose_via_service_expands_includes() { let (svc, repo) = seeded().await; let create = CreateSkillUseCase::new(repo); - let a = create.execute("p1", "基础", "", "遵循六边形", &[]).await.expect("a").id; + let a = create + .execute("p1", "basic", "", "follows hexagonal architecture", &[]) + .await + .expect("a") + .id; let b = create - .execute("p1", "Rust", "", "用 thiserror", std::slice::from_ref(&a)) + .execute("p1", "Rust", "", "using thiserror", std::slice::from_ref(&a)) .await .expect("b") .id; let comp = svc.compose("p1", &[b]).await.expect("compose"); assert_eq!(comp.skill_ids, vec![a.clone(), comp.skill_ids[1].clone()]); - assert!(comp.instructions.contains("遵循六边形")); - assert!(comp.instructions.contains("用 thiserror")); + assert!(comp.instructions.contains("follows hexagonal architecture")); + assert!(comp.instructions.contains("using thiserror")); } #[tokio::test] diff --git a/crates/skill/src/domain/skill.rs b/crates/skill/src/domain/skill.rs index d7723f49..4ba3c88d 100644 --- a/crates/skill/src/domain/skill.rs +++ b/crates/skill/src/domain/skill.rs @@ -210,9 +210,9 @@ mod tests { NewSkill::new("p", "n", "", " ", &[]).unwrap_err(), SkillError::EmptyInstructions ); - let s = NewSkill::new("p", " Rust 规范 ", "", " 用 thiserror ", &[]).expect("ok"); - assert_eq!(s.name, "Rust 规范"); - assert_eq!(s.instructions, "用 thiserror"); + let s = NewSkill::new("p", " Rust conventions ", "", " using thiserror ", &[]).expect("ok"); + assert_eq!(s.name, "Rust conventions"); + assert_eq!(s.instructions, "using thiserror"); } #[test] diff --git a/crates/skill/tests/architecture.rs b/crates/skill/tests/architecture.rs index 643f0b7d..0dd3436b 100644 --- a/crates/skill/tests/architecture.rs +++ b/crates/skill/tests/architecture.rs @@ -24,7 +24,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/system-setting/src/adapters/apikey_http.rs b/crates/system-setting/src/adapters/apikey_http.rs index c609ebba..6b4a4b0c 100644 --- a/crates/system-setting/src/adapters/apikey_http.rs +++ b/crates/system-setting/src/adapters/apikey_http.rs @@ -272,7 +272,7 @@ async fn revoke_apikey( #[openapi( paths(create_apikey, list_apikeys, revoke_apikey, create_my_apikey, list_my_apikeys, set_apikey_enabled), components(schemas(CreateApiKeyBody, CreateMyApiKeyBody, CreatedApiKey, ApiKeyItem, ApiKeyList, SetEnabledBody)), - tags((name = "apikey", description = "API Key(agent 派发凭证)")) + tags((name = "apikey", description = "API key (agent dispatch credential)")) )] struct ApiDoc; diff --git a/crates/system-setting/src/adapters/http.rs b/crates/system-setting/src/adapters/http.rs index 0853d1fd..d4941748 100644 --- a/crates/system-setting/src/adapters/http.rs +++ b/crates/system-setting/src/adapters/http.rs @@ -1140,7 +1140,7 @@ async fn revoke_role( create_role, list_roles, get_role, update_role, delete_role, grant_role, revoke_role ), components(schemas(LoginRequest, LoginResponse, MeResponse, ChangePasswordBody, CreateUserRequest, UserResponse, UserPage, UpdateUserBody, OrgResponse, OrgBody, OrgPage, RoleResponse, RoleBody, RolePage, GrantBody)), - tags((name = "auth", description = "鉴权"), (name = "user", description = "用户管理"), (name = "organization", description = "组织"), (name = "role", description = "角色 / 授权")) + tags((name = "auth", description = "Authentication"), (name = "user", description = "User management"), (name = "organization", description = "Organization"), (name = "role", description = "Role / authorization")) )] struct ApiDoc; @@ -1160,7 +1160,7 @@ pub fn openapi() -> utoipa::openapi::OpenApi { #[openapi( paths(oidc_list_providers, oidc_get_provider, oidc_create_provider, oidc_update_provider, oidc_delete_provider), components(schemas(OidcProviderView, OidcProviderCreate, OidcProviderUpdate)), - tags((name = "oidc", description = "OIDC 身份提供方")) + tags((name = "oidc", description = "OIDC identity provider")) )] struct OidcApiDoc; diff --git a/crates/system-setting/src/adapters/llm_model_http.rs b/crates/system-setting/src/adapters/llm_model_http.rs index 45f5f735..b8022d75 100644 --- a/crates/system-setting/src/adapters/llm_model_http.rs +++ b/crates/system-setting/src/adapters/llm_model_http.rs @@ -189,7 +189,7 @@ async fn delete_model( #[openapi( paths(list_models, create_model, update_model, delete_model), components(schemas(LlmModelItem, LlmModelList, CreateLlmModelBody, UpdateLlmModelBody)), - tags((name = "llm-model", description = "个人模型设置")) + tags((name = "llm-model", description = "Personal model settings")) )] struct ApiDoc; diff --git a/crates/system-setting/src/adapters/oidc.rs b/crates/system-setting/src/adapters/oidc.rs index e648deee..8c586b71 100644 --- a/crates/system-setting/src/adapters/oidc.rs +++ b/crates/system-setting/src/adapters/oidc.rs @@ -592,7 +592,7 @@ mod tests { ) .route( "/cgi-bin/user/get", - get(|| async { Json(serde_json::json!({"errcode":0,"name":"张三"})) }), + get(|| async { Json(serde_json::json!({"errcode":0,"name":"Zhang San"})) }), ); serve(app).await } @@ -639,7 +639,7 @@ mod tests { let id = p.exchange("auth-code").await.expect("exchange"); assert_eq!(id.provider, "wecom"); assert_eq!(id.open_id, "zhangsan"); - assert_eq!(id.display_name, "张三"); + assert_eq!(id.display_name, "Zhang San"); } async fn spawn_lark() -> String { diff --git a/crates/system-setting/src/adapters/pg.rs b/crates/system-setting/src/adapters/pg.rs index 12d10271..aa75f504 100644 --- a/crates/system-setting/src/adapters/pg.rs +++ b/crates/system-setting/src/adapters/pg.rs @@ -1027,7 +1027,7 @@ mod tests { use super::*; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_org_crud_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -1058,7 +1058,7 @@ mod tests { } #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_auth_credential_and_session_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -1088,7 +1088,7 @@ mod tests { } #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_apikey_crud_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -1132,7 +1132,7 @@ mod tests { } #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_apikey_owner_and_expiry_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -1164,7 +1164,7 @@ mod tests { } #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_llm_model_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -1213,7 +1213,7 @@ mod tests { } #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_user_roundtrip_and_directory() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -1233,7 +1233,7 @@ mod tests { } #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_oidc_provider_roundtrip() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); diff --git a/crates/system-setting/src/ports/user_directory.rs b/crates/system-setting/src/ports/user_directory.rs index dbdf1b61..cd264502 100644 --- a/crates/system-setting/src/ports/user_directory.rs +++ b/crates/system-setting/src/ports/user_directory.rs @@ -5,7 +5,7 @@ use thiserror::Error; #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum DirectoryError { - #[error("CFT provenance check failed (检测到用户创建途径异常)")] + #[error("CFT provenance check failed (abnormal user creation path detected)")] ProvenanceCheckFailed, #[error("storage backend error: {0}")] Backend(String), diff --git a/crates/system-setting/tests/architecture.rs b/crates/system-setting/tests/architecture.rs index 6e44f688..3043f257 100644 --- a/crates/system-setting/tests/architecture.rs +++ b/crates/system-setting/tests/architecture.rs @@ -29,7 +29,7 @@ fn scan(dir: &Path) { for pat in [format!("use {token}"), format!("{token}::")] { assert!( !src.contains(&pat), - "纯层禁止引用 IO crate:{} 含 `{pat}`(IO 只能在 adapters)", + "pure layers must not reference IO crates: {} contains `{pat}` (IO belongs only in adapters)", path.display() ); } diff --git a/crates/task/src/adapters/heuristic_planner.rs b/crates/task/src/adapters/heuristic_planner.rs index 1643d244..cd16a4d3 100644 --- a/crates/task/src/adapters/heuristic_planner.rs +++ b/crates/task/src/adapters/heuristic_planner.rs @@ -18,7 +18,7 @@ impl Planner for HeuristicPlanner { async fn plan(&self, spec: &RequirementSpec) -> Result, PlanError> { if spec.acceptance_criteria.is_empty() { return Ok(vec![PlannedTask { - title: format!("实现 {}", spec.title), + title: format!("implement {}", spec.title), description: spec.description.clone(), acceptance_criteria: Vec::new(), dependencies: Vec::new(), @@ -28,7 +28,7 @@ impl Planner for HeuristicPlanner { .acceptance_criteria .iter() .map(|c| PlannedTask { - title: format!("实现: {c}"), + title: format!("implement: {c}"), description: String::new(), acceptance_criteria: vec![c.clone()], dependencies: Vec::new(), @@ -37,8 +37,8 @@ impl Planner for HeuristicPlanner { if tasks.len() > 1 { let deps: Vec = (0..tasks.len()).collect(); tasks.push(PlannedTask { - title: format!("集成验证: {}", spec.title), - description: "集成并验证全部验收标准".into(), + title: format!("integration verification: {}", spec.title), + description: "integrate and verify all acceptance criteria".into(), acceptance_criteria: spec.acceptance_criteria.clone(), dependencies: deps, }); @@ -55,7 +55,7 @@ mod tests { RequirementSpec { requirement_id: "r1".into(), requirement_version: 1, - title: "登录".into(), + title: "login".into(), description: "d".into(), acceptance_criteria: criteria.iter().map(|s| s.to_string()).collect(), } @@ -63,10 +63,13 @@ mod tests { #[tokio::test] async fn one_task_per_criterion_plus_integration() { - let plan = HeuristicPlanner.plan(&spec(&["登录成功", "错误密码拒绝"])).await.expect("plan"); + let plan = HeuristicPlanner + .plan(&spec(&["login success", "wrong password rejected"])) + .await + .expect("plan"); assert_eq!(plan.len(), 3); assert_eq!(plan[2].dependencies, vec![0, 1]); - assert_eq!(plan[0].acceptance_criteria, vec!["登录成功".to_string()]); + assert_eq!(plan[0].acceptance_criteria, vec!["login success".to_string()]); } #[tokio::test] diff --git a/crates/task/src/adapters/http.rs b/crates/task/src/adapters/http.rs index b25a4ffb..7b1090c3 100644 --- a/crates/task/src/adapters/http.rs +++ b/crates/task/src/adapters/http.rs @@ -363,7 +363,7 @@ async fn transition_task( #[openapi( paths(create_decomposition, breakdown_requirement, get_decomposition, ready_tasks, add_task, dispatch_task, transition_task, set_task_points, set_task_assignee), components(schemas(CreateBody, BreakdownBody, AddTaskBody, StatusBody, PointsBody, AssigneeBody, TaskResponse, DecompositionResponse, TaskCreated)), - tags((name = "task", description = "任务拆分 DAG")) + tags((name = "task", description = "Task decomposition DAG")) )] struct ApiDoc; @@ -557,7 +557,7 @@ mod tests { let (app, t) = app_with("TASK:READ+ADD+EXECUTE+UPDATE").await; let r = app .clone() - .oneshot(req("POST", "/decomposition/breakdown", r#"{"requirementId":"req1","requirementVersion":1,"title":"登录","acceptanceCriteria":["登录成功","错误密码拒绝"]}"#, Some(&t))) + .oneshot(req("POST", "/decomposition/breakdown", r#"{"requirementId":"req1","requirementVersion":1,"title":"login","acceptanceCriteria":["login success","wrong password rejected"]}"#, Some(&t))) .await .expect("r"); assert_eq!(r.status(), StatusCode::CREATED); diff --git a/crates/task/src/adapters/pg.rs b/crates/task/src/adapters/pg.rs index 01093a73..fc99dc67 100644 --- a/crates/task/src/adapters/pg.rs +++ b/crates/task/src/adapters/pg.rs @@ -201,7 +201,7 @@ mod tests { use crate::domain::NewTask; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_dag_persistence_and_readiness() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -213,7 +213,7 @@ mod tests { let repo = PgTaskRepository::new(pool.clone()); let mut d = repo.create_decomposition("req1", 1).await.expect("create"); - d.add_task(NewTask::new("A", "", &["验收A".into()], &[]).expect("v")).expect("a"); + d.add_task(NewTask::new("A", "", &["acceptance A".into()], &[]).expect("v")).expect("a"); d.add_task(NewTask::new("B", "", &[], &["t1".into()]).expect("v")).expect("b"); repo.save(&d).await.expect("save"); diff --git a/crates/task/src/application/breakdown.rs b/crates/task/src/application/breakdown.rs index b8178cf9..d1dedec9 100644 --- a/crates/task/src/application/breakdown.rs +++ b/crates/task/src/application/breakdown.rs @@ -87,7 +87,7 @@ mod tests { RequirementSpec { requirement_id: "req1".into(), requirement_version: 1, - title: "登录".into(), + title: "login".into(), description: "d".into(), acceptance_criteria: criteria.iter().map(|s| s.to_string()).collect(), } @@ -99,7 +99,10 @@ mod tests { #[tokio::test] async fn breakdown_builds_dag_from_plan() { - let d = uc().execute(&spec(&["登录成功", "错误密码拒绝"])).await.expect("breakdown"); + let d = uc() + .execute(&spec(&["login success", "wrong password rejected"])) + .await + .expect("breakdown"); assert_eq!(d.tasks.len(), 3); assert_eq!( d.task("t3").expect("t3").dependencies, diff --git a/crates/task/src/application/task_admin.rs b/crates/task/src/application/task_admin.rs index 9c42738a..36a54e2b 100644 --- a/crates/task/src/application/task_admin.rs +++ b/crates/task/src/application/task_admin.rs @@ -153,7 +153,7 @@ impl TaskService { .iter() .find(|t| t.id == task_id) .map(|t| t.status) - .expect("任务在成功推进后必存在"); + .expect("task must exist after a successful advance"); self.repo.save_task_status(decomposition_id, task_id, status).await?; Ok(()) } @@ -231,8 +231,16 @@ mod tests { h3.await.expect("join t3").expect("t3 advanced"); let d = svc.get(&did).await.expect("get"); - assert_eq!(d.task("t2").expect("t2").status, TaskStatus::Verified, "t2 应保持 Verified"); - assert_eq!(d.task("t3").expect("t3").status, TaskStatus::Verified, "t3 不应被兄弟回写覆盖"); + assert_eq!( + d.task("t2").expect("t2").status, + TaskStatus::Verified, + "t2 should stay Verified" + ); + assert_eq!( + d.task("t3").expect("t3").status, + TaskStatus::Verified, + "t3 should not be overwritten by a sibling write-back" + ); } #[tokio::test] diff --git a/crates/task/tests/architecture.rs b/crates/task/tests/architecture.rs index 643f0b7d..0dd3436b 100644 --- a/crates/task/tests/architecture.rs +++ b/crates/task/tests/architecture.rs @@ -24,7 +24,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/test-plan/src/adapters/http.rs b/crates/test-plan/src/adapters/http.rs index 404f1067..5dc98c27 100644 --- a/crates/test-plan/src/adapters/http.rs +++ b/crates/test-plan/src/adapters/http.rs @@ -394,7 +394,7 @@ async fn statistics( } } -#[utoipa::path(get, path = "/test-plan/{id}/report", tag = "test-plan", params(("id" = String, Path)), responses((status = 200, description = "HTML 报告"), (status = 403), (status = 404)), security(("bearer" = [])))] +#[utoipa::path(get, path = "/test-plan/{id}/report", tag = "test-plan", params(("id" = String, Path)), responses((status = 200, description = "HTML report"), (status = 403), (status = 404)), security(("bearer" = [])))] async fn report(user: AuthUser, State(st): State, Path(id): Path) -> Response { if !user.can("TEST_PLAN", "READ") { return (StatusCode::FORBIDDEN, "permission denied").into_response(); @@ -435,7 +435,7 @@ fn markdown_response(md: String) -> Response { (StatusCode::OK, [("content-type", "text/markdown; charset=utf-8")], md).into_response() } -#[utoipa::path(get, path = "/test-plan/{id}/report.md", tag = "test-plan", params(("id" = String, Path)), responses((status = 200, description = "Markdown 报告"), (status = 403), (status = 404)), security(("bearer" = [])))] +#[utoipa::path(get, path = "/test-plan/{id}/report.md", tag = "test-plan", params(("id" = String, Path)), responses((status = 200, description = "Markdown report"), (status = 403), (status = 404)), security(("bearer" = [])))] async fn report_md( user: AuthUser, State(st): State, @@ -489,7 +489,7 @@ async fn create_plan_share( } /// Public (unauthenticated) read of a shared plan report (Markdown) by token. -#[utoipa::path(get, path = "/public/test-plan-report/{token}", tag = "test-plan", params(("token" = String, Path)), responses((status = 200, description = "Markdown 报告"), (status = 404)))] +#[utoipa::path(get, path = "/public/test-plan-report/{token}", tag = "test-plan", params(("token" = String, Path)), responses((status = 200, description = "Markdown report"), (status = 404)))] async fn public_plan_report(State(st): State, Path(token): Path) -> Response { let plan_id = sqlx::query_scalar::<_, String>( "SELECT report_id FROM ms_report_share \ @@ -693,7 +693,7 @@ async fn list_cases( } #[derive(OpenApi)] -#[openapi(paths(create_plan, list_plans, plan_detail, update_plan, save_planning, statistics, report, report_md, create_plan_share, public_plan_report, link_case, unlink_case, record_result, list_cases), components(schemas(CreatePlanRequest, PlanResponse, PlanListItemResponse, PlanDetailResponse, UpdatePlanRequest, SavePlanningResponse, StatisticsResponse, PlanShareResponse, LinkCaseRequest, RecordResultRequest, AssertionResultDto, PlanCaseResponse, StepResultDto)), tags((name = "test-plan", description = "测试计划")))] +#[openapi(paths(create_plan, list_plans, plan_detail, update_plan, save_planning, statistics, report, report_md, create_plan_share, public_plan_report, link_case, unlink_case, record_result, list_cases), components(schemas(CreatePlanRequest, PlanResponse, PlanListItemResponse, PlanDetailResponse, UpdatePlanRequest, SavePlanningResponse, StatisticsResponse, PlanShareResponse, LinkCaseRequest, RecordResultRequest, AssertionResultDto, PlanCaseResponse, StepResultDto)), tags((name = "test-plan", description = "Test plan")))] struct ApiDoc; pub fn openapi() -> utoipa::openapi::OpenApi { ApiDoc::openapi() @@ -748,7 +748,7 @@ mod tests { let resp = app .oneshot(post( "/test-plan", - r#"{"projectId":"p1","name":"冒烟","type":"TEST_PLAN"}"#, + r#"{"projectId":"p1","name":"smoke","type":"TEST_PLAN"}"#, Some(&t), )) .await @@ -799,7 +799,9 @@ mod tests { async fn link_record_then_report_shows_case_detail() { let repo = InMemoryPlanRepository::new(); let plan = repo - .seed(NewPlan::new("p1", "冒烟", crate::domain::PlanType::Plan, ROOT_GROUP).expect("v")) + .seed( + NewPlan::new("p1", "smoke", crate::domain::PlanType::Plan, ROOT_GROUP).expect("v"), + ) .await; let (app, t) = app_with(repo).await; let pid = &plan.id; @@ -807,13 +809,13 @@ mod tests { .clone() .oneshot(post( &format!("/test-plan/{pid}/cases"), - r#"{"caseId":"c1","name":"健康检查"}"#, + r#"{"caseId":"c1","name":"health check"}"#, Some(&t), )) .await .expect("link"); assert_eq!(r.status(), StatusCode::CREATED); - let body = r#"{"status":"SUCCESS","latencyMs":12,"responseSize":3,"statusCode":200,"body":"ok","assertions":[{"item":"状态码","actual":"200","condition":"等于","expected":"200","passed":true}]}"#; + let body = r#"{"status":"SUCCESS","latencyMs":12,"responseSize":3,"statusCode":200,"body":"ok","assertions":[{"item":"status code","actual":"200","condition":"equals","expected":"200","passed":true}]}"#; let r = app .clone() .oneshot(post(&format!("/test-plan/{pid}/cases/c1/result"), body, Some(&t))) @@ -835,7 +837,7 @@ mod tests { axum::body::to_bytes(r.into_body(), usize::MAX).await.expect("b").to_vec(), ) .expect("utf8"); - assert!(html.contains("健康检查")); + assert!(html.contains("health check")); assert!(html.contains("报告明细")); assert!(html.contains("断言项")); assert!(html.contains("通过率100.0%")); @@ -845,7 +847,9 @@ mod tests { async fn unlink_case_204_then_404() { let repo = InMemoryPlanRepository::new(); let plan = repo - .seed(NewPlan::new("p1", "冒烟", crate::domain::PlanType::Plan, ROOT_GROUP).expect("v")) + .seed( + NewPlan::new("p1", "smoke", crate::domain::PlanType::Plan, ROOT_GROUP).expect("v"), + ) .await; let (app, t) = app_with(repo).await; let pid = &plan.id; @@ -853,7 +857,7 @@ mod tests { .clone() .oneshot(post( &format!("/test-plan/{pid}/cases"), - r#"{"caseId":"c1","name":"健康检查"}"#, + r#"{"caseId":"c1","name":"health check"}"#, Some(&t), )) .await @@ -894,7 +898,7 @@ mod tests { let resp = app .oneshot(post( "/test-plan", - r#"{"projectId":"p1","name":"组中组","type":"GROUP","groupId":"g1"}"#, + r#"{"projectId":"p1","name":"nested group","type":"GROUP","groupId":"g1"}"#, Some(&t), )) .await @@ -916,7 +920,7 @@ mod tests { async fn statistics_returns_rates() { let repo = InMemoryPlanRepository::new(); let plan = - repo.seed(NewPlan::new("p1", "冒烟", PlanType::Plan, ROOT_GROUP).expect("v")).await; + repo.seed(NewPlan::new("p1", "smoke", PlanType::Plan, ROOT_GROUP).expect("v")).await; repo.set_counts( &plan.id, CaseCounts { pending: 1, success: 2, error: 1, ..Default::default() }, @@ -961,10 +965,12 @@ mod tests { async fn detail_update_roundtrip() { let repo = InMemoryPlanRepository::new(); let plan = repo - .seed(NewPlan::new("p1", "冒烟", crate::domain::PlanType::Plan, ROOT_GROUP).expect("v")) + .seed( + NewPlan::new("p1", "smoke", crate::domain::PlanType::Plan, ROOT_GROUP).expect("v"), + ) .await; let (app, t) = app_with(repo).await; - let body = r#"{"name":"回归","description":"说明","tags":["核心"],"moduleId":"m1","startAt":1000,"endAt":2000,"allowDuplicateCases":false,"autoUpdateStatus":false,"passThreshold":80}"#; + let body = r#"{"name":"regression","description":"description","tags":["core"],"moduleId":"m1","startAt":1000,"endAt":2000,"allowDuplicateCases":false,"autoUpdateStatus":false,"passThreshold":80}"#; let resp = app .clone() .oneshot(put_req(&format!("/test-plan/{}", plan.id), body, &t)) @@ -983,9 +989,9 @@ mod tests { .expect("get"); assert_eq!(resp.status(), StatusCode::OK); let v = json_body(resp).await; - assert_eq!(v["name"], "回归"); - assert_eq!(v["description"], "说明"); - assert_eq!(v["tags"], serde_json::json!(["核心"])); + assert_eq!(v["name"], "regression"); + assert_eq!(v["description"], "description"); + assert_eq!(v["tags"], serde_json::json!(["core"])); assert_eq!(v["moduleId"], "m1"); assert_eq!(v["startAt"], 1000); assert_eq!(v["allowDuplicateCases"], false); @@ -997,10 +1003,12 @@ mod tests { async fn planning_roundtrip_and_link_sync() { let repo = InMemoryPlanRepository::new(); let plan = repo - .seed(NewPlan::new("p1", "冒烟", crate::domain::PlanType::Plan, ROOT_GROUP).expect("v")) + .seed( + NewPlan::new("p1", "smoke", crate::domain::PlanType::Plan, ROOT_GROUP).expect("v"), + ) .await; let (app, t) = app_with(repo).await; - let doc = r#"{"nodes":[{"id":"n1","name":"接口用例","kind":"category","children":[{"id":"n2","name":"登录","kind":"point","caseIds":["c1"],"scenarioIds":["s1"],"config":{"mode":"serial"}}]}],"caseNames":{"c1":"健康检查"}}"#; + let doc = r#"{"nodes":[{"id":"n1","name":"interface case","kind":"category","children":[{"id":"n2","name":"login","kind":"point","caseIds":["c1"],"scenarioIds":["s1"],"config":{"mode":"serial"}}]}],"caseNames":{"c1":"health check"}}"#; let resp = app .clone() .oneshot(put_req(&format!("/test-plan/{}/planning", plan.id), doc, &t)) @@ -1086,7 +1094,7 @@ mod tests { .clone() .oneshot(post( "/test-plan", - r#"{"projectId":"p1","name":"冒烟","type":"TEST_PLAN"}"#, + r#"{"projectId":"p1","name":"smoke","type":"TEST_PLAN"}"#, Some(&t), )) .await @@ -1098,13 +1106,13 @@ mod tests { .clone() .oneshot(post( "/test-plan", - r#"{"projectId":"p1","name":"回归组","type":"GROUP"}"#, + r#"{"projectId":"p1","name":"regression group","type":"GROUP"}"#, Some(&t), )) .await .expect("create group"); assert_eq!(r.status(), StatusCode::CREATED); - let body = r#"{"tags":["核心"],"moduleId":"m1","startAt":1000}"#; + let body = r#"{"tags":["core"],"moduleId":"m1","startAt":1000}"#; let r = app.clone().oneshot(put_req(&format!("/test-plan/{id}"), body, &t)).await.expect("put"); assert_eq!(r.status(), StatusCode::OK); @@ -1115,12 +1123,12 @@ mod tests { let items = v.as_array().expect("array"); assert_eq!(items.len(), 2); // Newest first: the group was created after the plan. - assert_eq!(items[0]["name"], "回归组"); + assert_eq!(items[0]["name"], "regression group"); assert_eq!(items[0]["type"], "GROUP"); let p = &items[1]; assert_eq!(p["id"].as_str(), Some(id.as_str())); assert_eq!(p["createdBy"], "admin"); - assert_eq!(p["tags"], serde_json::json!(["核心"])); + assert_eq!(p["tags"], serde_json::json!(["core"])); assert_eq!(p["moduleId"], "m1"); assert_eq!(p["startAt"], 1000); assert_eq!(p["groupId"], "NONE"); @@ -1131,8 +1139,8 @@ mod tests { async fn list_scoped_to_project_and_hides_archived() { let (app, t) = app_with(InMemoryPlanRepository::new()).await; for body in [ - r#"{"projectId":"p1","name":"甲","type":"TEST_PLAN"}"#, - r#"{"projectId":"p2","name":"乙","type":"TEST_PLAN"}"#, + r#"{"projectId":"p1","name":"alpha","type":"TEST_PLAN"}"#, + r#"{"projectId":"p2","name":"beta","type":"TEST_PLAN"}"#, ] { let r = app.clone().oneshot(post("/test-plan", body, Some(&t))).await.expect("create"); assert_eq!(r.status(), StatusCode::CREATED); diff --git a/crates/test-plan/src/adapters/pg.rs b/crates/test-plan/src/adapters/pg.rs index 1d68a8cd..6514897c 100644 --- a/crates/test-plan/src/adapters/pg.rs +++ b/crates/test-plan/src/adapters/pg.rs @@ -606,7 +606,7 @@ mod tests { use super::*; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_plan_group_children_and_counts() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -619,11 +619,11 @@ mod tests { let repo = PgPlanRepository::new(pool.clone()); let group = repo - .insert(&NewPlan::new("proj1", "组", PlanType::Group, "NONE").expect("v")) + .insert(&NewPlan::new("proj1", "group", PlanType::Group, "NONE").expect("v")) .await .expect("group"); let child = repo - .insert(&NewPlan::new("proj1", "子", PlanType::Plan, &group.id).expect("v")) + .insert(&NewPlan::new("proj1", "child", PlanType::Plan, &group.id).expect("v")) .await .expect("child"); @@ -636,18 +636,18 @@ mod tests { assert_eq!(kids[0].id, child.id); assert_eq!(repo.case_counts(&child.id).await.expect("c"), CaseCounts::default()); - repo.link_case(&child.id, "ca", "用例A").await.expect("link a"); - repo.link_case(&child.id, "cb", "用例B").await.expect("link b"); - repo.link_case(&child.id, "cc", "用例C").await.expect("link c"); - repo.link_case(&child.id, "ca", "用例A").await.expect("link a again"); + repo.link_case(&child.id, "ca", "case A").await.expect("link a"); + repo.link_case(&child.id, "cb", "case B").await.expect("link b"); + repo.link_case(&child.id, "cc", "case C").await.expect("link c"); + repo.link_case(&child.id, "ca", "case A").await.expect("link a again"); let res = CaseResult { latency_ms: 12, response_size: 3, status_code: Some(200), assertions: vec![AssertionResult { - item: "状态码".into(), + item: "status code".into(), actual: "200".into(), - condition: "等于".into(), + condition: "equals".into(), expected: "200".into(), passed: true, reason: String::new(), @@ -676,7 +676,7 @@ mod tests { let cases = repo.list_cases(&child.id).await.expect("list"); assert_eq!(cases.len(), 3); let ca = cases.iter().find(|x| x.case_id == "ca").expect("ca"); - assert_eq!(ca.name, "用例A"); + assert_eq!(ca.name, "case A"); assert_eq!(ca.status, CaseStatus::Success); let r = ca.result.as_ref().expect("result"); assert_eq!(r.latency_ms, 12); diff --git a/crates/test-plan/src/application/create_plan.rs b/crates/test-plan/src/application/create_plan.rs index 8dee83de..3a0c7025 100644 --- a/crates/test-plan/src/application/create_plan.rs +++ b/crates/test-plan/src/application/create_plan.rs @@ -69,8 +69,8 @@ mod tests { #[tokio::test] async fn creates_root_plan() { let uc = CreatePlanUseCase::new(Arc::new(InMemoryPlanRepository::new())); - let p = uc.execute("proj1", "冒烟", PlanType::Plan, ROOT_GROUP).await.expect("ok"); - assert_eq!(p.name, "冒烟"); + let p = uc.execute("proj1", "smoke", PlanType::Plan, ROOT_GROUP).await.expect("ok"); + assert_eq!(p.name, "smoke"); assert!(!p.archived); } @@ -79,7 +79,7 @@ mod tests { let repo = Arc::new(InMemoryPlanRepository::new()); let uc = CreatePlanUseCase::new(repo.clone()); let p = uc - .execute_as("proj1", "冒烟", PlanType::Plan, ROOT_GROUP, Some("admin")) + .execute_as("proj1", "smoke", PlanType::Plan, ROOT_GROUP, Some("admin")) .await .expect("ok"); assert_eq!(p.created_by.as_deref(), Some("admin")); @@ -87,14 +87,14 @@ mod tests { assert_eq!(list.len(), 1); assert_eq!(list[0].0.created_by.as_deref(), Some("admin")); // No user: created_by stays empty. - let anon = uc.execute("proj1", "匿名", PlanType::Plan, ROOT_GROUP).await.expect("ok"); + let anon = uc.execute("proj1", "anonymous", PlanType::Plan, ROOT_GROUP).await.expect("ok"); assert_eq!(anon.created_by, None); } #[tokio::test] async fn nested_group_rejected_by_domain() { let uc = CreatePlanUseCase::new(Arc::new(InMemoryPlanRepository::new())); - let err = uc.execute("proj1", "组中组", PlanType::Group, "g1").await.unwrap_err(); + let err = uc.execute("proj1", "nested group", PlanType::Group, "g1").await.unwrap_err(); assert_eq!(err, CreatePlanError::Validation(PlanError::GroupCannotBeNested)); } @@ -102,17 +102,20 @@ mod tests { async fn plan_in_existing_group_ok() { let repo = InMemoryPlanRepository::new(); let uc = CreatePlanUseCase::new(Arc::new(repo.clone())); - let group = - uc.execute("proj1", "回归组", PlanType::Group, ROOT_GROUP).await.expect("group"); + let group = uc + .execute("proj1", "regression group", PlanType::Group, ROOT_GROUP) + .await + .expect("group"); - let child = uc.execute("proj1", "子计划", PlanType::Plan, &group.id).await.expect("child"); + let child = + uc.execute("proj1", "child plan", PlanType::Plan, &group.id).await.expect("child"); assert_eq!(child.group_id, group.id); } #[tokio::test] async fn plan_in_missing_group_rejected() { let uc = CreatePlanUseCase::new(Arc::new(InMemoryPlanRepository::new())); - let err = uc.execute("proj1", "孤儿", PlanType::Plan, "ghost").await.unwrap_err(); + let err = uc.execute("proj1", "orphan", PlanType::Plan, "ghost").await.unwrap_err(); assert_eq!(err, CreatePlanError::InvalidGroup); } @@ -120,8 +123,8 @@ mod tests { async fn plan_parented_to_a_non_group_rejected() { let repo = InMemoryPlanRepository::new(); let uc = CreatePlanUseCase::new(Arc::new(repo.clone())); - let plain = uc.execute("proj1", "普通", PlanType::Plan, ROOT_GROUP).await.expect("plan"); - let err = uc.execute("proj1", "子", PlanType::Plan, &plain.id).await.unwrap_err(); + let plain = uc.execute("proj1", "plain", PlanType::Plan, ROOT_GROUP).await.expect("plan"); + let err = uc.execute("proj1", "child", PlanType::Plan, &plain.id).await.unwrap_err(); assert_eq!(err, CreatePlanError::InvalidGroup); } } diff --git a/crates/test-plan/src/application/manage_plan.rs b/crates/test-plan/src/application/manage_plan.rs index 38ed635d..8095051a 100644 --- a/crates/test-plan/src/application/manage_plan.rs +++ b/crates/test-plan/src/application/manage_plan.rs @@ -63,9 +63,9 @@ mod tests { #[tokio::test] async fn list_scoped_to_project() { let repo = Arc::new(InMemoryPlanRepository::new()); - seed(&repo, "p1", "冒烟").await; - seed(&repo, "p1", "回归").await; - seed(&repo, "p2", "别的项目").await; + seed(&repo, "p1", "smoke").await; + seed(&repo, "p1", "regression").await; + seed(&repo, "p2", "another project").await; let uc = ManagePlanUseCase::new(repo); let list = uc.list("p1").await.expect("list"); assert_eq!(list.len(), 2); @@ -75,11 +75,11 @@ mod tests { #[tokio::test] async fn rename_then_list_reflects() { let repo = Arc::new(InMemoryPlanRepository::new()); - let p = seed(&repo, "p1", "旧名").await; + let p = seed(&repo, "p1", "old name").await; let uc = ManagePlanUseCase::new(repo); - uc.rename(&p.id, "新名").await.expect("rename"); + uc.rename(&p.id, "new name").await.expect("rename"); let list = uc.list("p1").await.expect("list"); - assert_eq!(list[0].name, "新名"); + assert_eq!(list[0].name, "new name"); } #[tokio::test] @@ -99,7 +99,7 @@ mod tests { #[tokio::test] async fn delete_removes_from_list() { let repo = Arc::new(InMemoryPlanRepository::new()); - let p = seed(&repo, "p1", "待删").await; + let p = seed(&repo, "p1", "to delete").await; let uc = ManagePlanUseCase::new(repo); uc.delete(&p.id).await.expect("delete"); assert!(uc.list("p1").await.expect("list").is_empty()); diff --git a/crates/test-plan/src/application/plan_cases.rs b/crates/test-plan/src/application/plan_cases.rs index 030e54cf..329d85b4 100644 --- a/crates/test-plan/src/application/plan_cases.rs +++ b/crates/test-plan/src/application/plan_cases.rs @@ -50,8 +50,8 @@ mod tests { #[tokio::test] async fn unlink_removes_linked_case() { let uc = uc(); - uc.link("p1", "c1", "登录").await.expect("link"); - uc.link("p1", "c2", "登出").await.expect("link"); + uc.link("p1", "c1", "login").await.expect("link"); + uc.link("p1", "c2", "logout").await.expect("link"); assert!(uc.unlink("p1", "c1").await.expect("unlink")); let left = uc.list("p1").await.expect("list"); assert_eq!(left.len(), 1); @@ -61,7 +61,7 @@ mod tests { #[tokio::test] async fn unlink_missing_case_returns_false() { let uc = uc(); - uc.link("p1", "c1", "登录").await.expect("link"); + uc.link("p1", "c1", "login").await.expect("link"); assert!(!uc.unlink("p1", "ghost").await.expect("unlink")); assert!(!uc.unlink("other-plan", "c1").await.expect("unlink")); assert_eq!(uc.list("p1").await.expect("list").len(), 1); diff --git a/crates/test-plan/src/application/plan_planning.rs b/crates/test-plan/src/application/plan_planning.rs index a53122bb..15a1a9d9 100644 --- a/crates/test-plan/src/application/plan_planning.rs +++ b/crates/test-plan/src/application/plan_planning.rs @@ -199,7 +199,8 @@ mod tests { async fn seeded() -> (Arc, Plan) { let repo = Arc::new(InMemoryPlanRepository::new()); - let p = repo.seed(NewPlan::new("p1", "冒烟", PlanType::Plan, ROOT_GROUP).expect("v")).await; + let p = + repo.seed(NewPlan::new("p1", "smoke", PlanType::Plan, ROOT_GROUP).expect("v")).await; (repo, p) } @@ -214,9 +215,9 @@ mod tests { let (repo, p) = seeded().await; let uc = PlanAdminUseCase::new(repo); let patch = PlanPatch { - name: Some("回归".into()), - description: Some("回归说明".into()), - tags: Some(vec!["核心".into(), " ".into()]), + name: Some("regression".into()), + description: Some("regression description".into()), + tags: Some(vec!["core".into(), " ".into()]), module_id: Some("m1".into()), start_at_ms: Some(1000), end_at_ms: Some(2000), @@ -225,9 +226,9 @@ mod tests { ..Default::default() }; let (plan, meta, _) = uc.update(&p.id, patch).await.expect("update"); - assert_eq!(plan.name, "回归"); - assert_eq!(meta.description, "回归说明"); - assert_eq!(meta.tags, vec!["核心".to_string()]); + assert_eq!(plan.name, "regression"); + assert_eq!(meta.description, "regression description"); + assert_eq!(meta.tags, vec!["core".to_string()]); assert_eq!(meta.module_id.as_deref(), Some("m1")); assert_eq!(meta.start_at_ms, Some(1000)); assert!(!meta.allow_duplicate_cases); @@ -238,7 +239,7 @@ mod tests { .await .expect("clear module"); assert_eq!(meta2.module_id, None); - assert_eq!(meta2.description, "回归说明"); + assert_eq!(meta2.description, "regression description"); } #[tokio::test] @@ -265,16 +266,16 @@ mod tests { let uc = PlanAdminUseCase::new(repo.clone()); let doc = json!({ "nodes": [{ - "id": "n1", "name": "功能用例", "kind": "category", + "id": "n1", "name": "functional case", "kind": "category", "caseIds": ["c1"], "children": [{ - "id": "n2", "name": "登录", "kind": "point", + "id": "n2", "name": "login", "kind": "point", "caseIds": ["c1", "c2"], "scenarioIds": ["s1"], "config": {"inherit": false, "mode": "serial"} }] }], - "caseNames": {"c1": "健康检查", "c2": "登录接口"}, - "scenarioNames": {"s1": "登录链路"} + "caseNames": {"c1": "health check", "c2": "login interface"}, + "scenarioNames": {"s1": "login flow"} }); let linked = uc.save_planning(&p.id, doc.clone()).await.expect("save"); assert_eq!(linked, 3); @@ -282,8 +283,8 @@ mod tests { assert_eq!(stored, Some(doc)); let cases = repo.list_cases(&p.id).await.expect("cases"); assert_eq!(cases.len(), 3); - assert!(cases.iter().any(|c| c.case_id == "c1" && c.name == "健康检查")); - assert!(cases.iter().any(|c| c.case_id == "s1" && c.name == "登录链路")); + assert!(cases.iter().any(|c| c.case_id == "c1" && c.name == "health check")); + assert!(cases.iter().any(|c| c.case_id == "s1" && c.name == "login flow")); } #[tokio::test] @@ -325,7 +326,7 @@ mod tests { let (repo, p) = seeded().await; let uc = PlanAdminUseCase::new(repo.clone()); // A manual link (cases tab) with no planning doc coverage. - repo.link_case(&p.id, "m1", "手动用例").await.expect("manual link"); + repo.link_case(&p.id, "m1", "manual case").await.expect("manual link"); uc.save_planning(&p.id, json!({"nodes": []})).await.expect("save empty"); assert_eq!(repo.list_cases(&p.id).await.expect("cases").len(), 1); } diff --git a/crates/test-plan/src/application/plan_statistics.rs b/crates/test-plan/src/application/plan_statistics.rs index 217df517..3579901d 100644 --- a/crates/test-plan/src/application/plan_statistics.rs +++ b/crates/test-plan/src/application/plan_statistics.rs @@ -95,7 +95,7 @@ mod tests { async fn plan_statistics_from_own_counts() { let repo = InMemoryPlanRepository::new(); let plan = - repo.seed(NewPlan::new("proj1", "冒烟", PlanType::Plan, ROOT_GROUP).expect("v")).await; + repo.seed(NewPlan::new("proj1", "smoke", PlanType::Plan, ROOT_GROUP).expect("v")).await; repo.set_counts( &plan.id, CaseCounts { pending: 1, success: 2, error: 1, ..Default::default() }, @@ -114,8 +114,9 @@ mod tests { #[tokio::test] async fn group_aggregates_children_counts_and_status() { let repo = InMemoryPlanRepository::new(); - let group = - repo.seed(NewPlan::new("proj1", "组", PlanType::Group, ROOT_GROUP).expect("v")).await; + let group = repo + .seed(NewPlan::new("proj1", "group", PlanType::Group, ROOT_GROUP).expect("v")) + .await; let a = repo.seed(NewPlan::new("proj1", "A", PlanType::Plan, &group.id).expect("v")).await; let b = repo.seed(NewPlan::new("proj1", "B", PlanType::Plan, &group.id).expect("v")).await; repo.set_counts(&a.id, CaseCounts { success: 2, ..Default::default() }); @@ -132,8 +133,9 @@ mod tests { #[tokio::test] async fn empty_group_is_prepared() { let repo = InMemoryPlanRepository::new(); - let group = - repo.seed(NewPlan::new("proj1", "空组", PlanType::Group, ROOT_GROUP).expect("v")).await; + let group = repo + .seed(NewPlan::new("proj1", "empty group", PlanType::Group, ROOT_GROUP).expect("v")) + .await; repo.set_threshold(&group.id, 0.5); let uc = PlanStatisticsUseCase::new(Arc::new(repo)); let s = uc.execute(&group.id).await.expect("ok"); diff --git a/crates/test-plan/src/application/scheduling.rs b/crates/test-plan/src/application/scheduling.rs index aae34109..5b73beda 100644 --- a/crates/test-plan/src/application/scheduling.rs +++ b/crates/test-plan/src/application/scheduling.rs @@ -107,7 +107,7 @@ mod tests { async fn scheduled_run_snapshots_stats() { let repo = InMemoryPlanRepository::new(); let plan = - repo.seed(NewPlan::new("p1", "冒烟", PlanType::Plan, ROOT_GROUP).expect("v")).await; + repo.seed(NewPlan::new("p1", "smoke", PlanType::Plan, ROOT_GROUP).expect("v")).await; repo.set_counts(&plan.id, CaseCounts { success: 3, error: 1, ..Default::default() }); let store = Arc::new(InMemoryScheduleStore::new()); let stats = PlanStatisticsUseCase::new(Arc::new(repo)); diff --git a/crates/test-plan/src/domain/plan.rs b/crates/test-plan/src/domain/plan.rs index e23b53bc..4b040098 100644 --- a/crates/test-plan/src/domain/plan.rs +++ b/crates/test-plan/src/domain/plan.rs @@ -135,7 +135,7 @@ mod tests { #[test] fn group_at_root_is_ok() { - let p = NewPlan::new("p1", "回归组", PlanType::Group, ROOT_GROUP).expect("ok"); + let p = NewPlan::new("p1", "regression group", PlanType::Group, ROOT_GROUP).expect("ok"); assert_eq!(p.plan_type, PlanType::Group); assert!(!p.belongs_to_group()); } @@ -143,14 +143,14 @@ mod tests { #[test] fn group_nested_in_another_group_is_rejected() { assert_eq!( - NewPlan::new("p1", "组中组", PlanType::Group, "g123"), + NewPlan::new("p1", "nested group", PlanType::Group, "g123"), Err(PlanError::GroupCannotBeNested) ); } #[test] fn plan_inside_group_is_ok() { - let p = NewPlan::new("p1", "冒烟", PlanType::Plan, "g123").expect("ok"); + let p = NewPlan::new("p1", "smoke", PlanType::Plan, "g123").expect("ok"); assert!(p.belongs_to_group()); } diff --git a/crates/test-plan/tests/architecture.rs b/crates/test-plan/tests/architecture.rs index 782cc5fc..2ea8b518 100644 --- a/crates/test-plan/tests/architecture.rs +++ b/crates/test-plan/tests/architecture.rs @@ -21,7 +21,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } diff --git a/crates/verification/src/adapters/http.rs b/crates/verification/src/adapters/http.rs index 8878f393..80ed8a9a 100644 --- a/crates/verification/src/adapters/http.rs +++ b/crates/verification/src/adapters/http.rs @@ -267,7 +267,7 @@ async fn sync_task( #[openapi( paths(create_verification, get_verification, get_report, link, sync_task), components(schemas(CreateBody, LinkBody, SyncBody, LinkResponse, CriterionResponse, VerificationResponse, GapResponse, ReportResponse)), - tags((name = "verification", description = "完整性验证")) + tags((name = "verification", description = "Integrity verification")) )] struct ApiDoc; pub fn openapi() -> utoipa::openapi::OpenApi { @@ -316,7 +316,7 @@ mod tests { let (app, t) = app_with("VERIFICATION:READ+ADD+UPDATE").await; let r = app .clone() - .oneshot(req("POST", "/verification", r#"{"requirementId":"req1","requirementVersion":1,"criteria":["登录成功","错误密码拒绝"]}"#, Some(&t))) + .oneshot(req("POST", "/verification", r#"{"requirementId":"req1","requirementVersion":1,"criteria":["login success","wrong password rejected"]}"#, Some(&t))) .await .expect("r"); assert_eq!(r.status(), StatusCode::CREATED); diff --git a/crates/verification/src/adapters/pg.rs b/crates/verification/src/adapters/pg.rs index 4dfea3f9..b36d70e6 100644 --- a/crates/verification/src/adapters/pg.rs +++ b/crates/verification/src/adapters/pg.rs @@ -164,7 +164,7 @@ mod tests { use super::*; #[tokio::test] - #[ignore = "需要 DATABASE_URL 指向一个 PostgreSQL 实例"] + #[ignore = "requires DATABASE_URL pointing to a PostgreSQL instance"] async fn pg_traceability_and_completeness() { let url = std::env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); @@ -175,8 +175,12 @@ mod tests { .expect("truncate"); let repo = PgVerificationRepository::new(pool.clone()); - let new = NewVerification::new("req1", 1, &["登录成功".into(), "错误密码拒绝".into()]) - .expect("valid"); + let new = NewVerification::new( + "req1", + 1, + &["login success".into(), "wrong password rejected".into()], + ) + .expect("valid"); let v = repo.create(&new).await.expect("create"); assert_eq!(repo.get(&v.id).await.expect("g").expect("s").criteria.len(), 2); diff --git a/crates/verification/src/application/verification_admin.rs b/crates/verification/src/application/verification_admin.rs index 82965911..350d44e2 100644 --- a/crates/verification/src/application/verification_admin.rs +++ b/crates/verification/src/application/verification_admin.rs @@ -102,7 +102,7 @@ mod tests { async fn seeded() -> (VerificationService, String) { let repo = Arc::new(InMemoryVerificationRepository::new()); let id = CreateVerificationUseCase::new(repo.clone()) - .execute("req1", 1, &["登录成功".into(), "错误密码拒绝".into()]) + .execute("req1", 1, &["login success".into(), "wrong password rejected".into()]) .await .expect("seed") .id; @@ -132,8 +132,12 @@ mod tests { #[tokio::test] async fn link_by_criteria_texts_then_sync_completes() { let (svc, id) = seeded().await; - svc.link_by_criteria_texts(&id, "d1", "t1", &["登录成功".into()]).await.expect("link0"); - svc.link_by_criteria_texts(&id, "d1", "t2", &["错误密码拒绝".into()]).await.expect("link1"); + svc.link_by_criteria_texts(&id, "d1", "t1", &["login success".into()]) + .await + .expect("link0"); + svc.link_by_criteria_texts(&id, "d1", "t2", &["wrong password rejected".into()]) + .await + .expect("link1"); let r = svc.report(&id).await.expect("report"); assert_eq!(r.gaps.iter().filter(|g| g.kind == GapKind::Unverified).count(), 2); svc.sync_task(&id, "d1", "t1", true).await.expect("sync0"); diff --git a/crates/verification/src/domain/verification.rs b/crates/verification/src/domain/verification.rs index 00825365..68e20eb7 100644 --- a/crates/verification/src/domain/verification.rs +++ b/crates/verification/src/domain/verification.rs @@ -271,8 +271,13 @@ mod tests { use super::*; fn v() -> Verification { - Verification::create("v1", "req1", 1, &["登录成功".into(), "错误密码拒绝".into()]) - .expect("valid") + Verification::create( + "v1", + "req1", + 1, + &["login success".into(), "wrong password rejected".into()], + ) + .expect("valid") } #[test] @@ -353,17 +358,25 @@ mod tests { #[test] fn link_task_by_texts_matches_and_dedups() { let mut v = v(); - let n = v.link_task_by_texts("d1", "t1", &[" 登录成功 ".into(), "无关标准".into()]); + let n = v.link_task_by_texts( + "d1", + "t1", + &[" login success ".into(), "unrelated criterion".into()], + ); assert_eq!(n, 1); assert_eq!(v.criterion(0).expect("c0").status(), CriterionStatus::Pending); assert_eq!(v.criterion(1).expect("c1").status(), CriterionStatus::Uncovered); - assert_eq!(v.link_task_by_texts("d1", "t1", &["登录成功".into()]), 0); + assert_eq!(v.link_task_by_texts("d1", "t1", &["login success".into()]), 0); } #[test] fn link_task_by_texts_integration_task_covers_all() { let mut v = v(); - let n = v.link_task_by_texts("d1", "t9", &["登录成功".into(), "错误密码拒绝".into()]); + let n = v.link_task_by_texts( + "d1", + "t9", + &["login success".into(), "wrong password rejected".into()], + ); assert_eq!(n, 2); v.sync_task("d1", "t9", true); assert!(v.is_complete()); diff --git a/crates/verification/tests/architecture.rs b/crates/verification/tests/architecture.rs index 643f0b7d..0dd3436b 100644 --- a/crates/verification/tests/architecture.rs +++ b/crates/verification/tests/architecture.rs @@ -24,7 +24,11 @@ fn scan(dir: &Path) { let src = fs::read_to_string(&path).expect("read file"); for token in FORBIDDEN { for pat in [format!("use {token}"), format!("{token}::")] { - assert!(!src.contains(&pat), "{} 含 `{pat}`(IO 只能在 adapters)", path.display()); + assert!( + !src.contains(&pat), + "{} contains `{pat}` (IO belongs only in adapters)", + path.display() + ); } } } From e9b0037ff0d2b7bc61172ac80db6266d3b16ee9b Mon Sep 17 00:00:00 2001 From: liberxue <5831248+Liberxue@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:07:39 +0800 Subject: [PATCH 3/3] feat(ui) ms-step-fillbg && add ms-step-grow --- .github/workflows/ci.yml | 19 ++ README.md | 72 +----- README.zh-CN.md | 69 +----- crates/server/src/scenario_run.rs | 9 +- docs/COMMENT_CONVENTIONS.md | 2 + docs/COMMENT_CONVENTIONS.zh-CN.md | 87 ++++++++ docs/DEPLOYMENT.md | 2 + docs/DEPLOYMENT.zh-CN.md | 2 + docs/EXECUTORS.md | 2 + docs/EXECUTORS.zh-CN.md | 2 + docs/INSTALL.md | 2 + docs/INSTALL.zh-CN.md | 135 ++++++++++++ docs/USAGE.md | 4 +- docs/USAGE.zh-CN.md | 4 +- docs/link-check-report.md | 219 +++++++++++++++++++ out.md | 219 +++++++++++++++++++ scripts/api-coverage.sh | 10 +- scripts/docs-link-check.py | 279 ++++++++++++++++++++++++ web/src/components/LiveRunBadge.tsx | 18 +- web/src/components/plan/PlanMindmap.tsx | 4 +- web/src/index.css | 11 +- 21 files changed, 1039 insertions(+), 132 deletions(-) create mode 100644 docs/COMMENT_CONVENTIONS.zh-CN.md create mode 100644 docs/INSTALL.zh-CN.md create mode 100644 docs/link-check-report.md create mode 100644 out.md create mode 100755 scripts/docs-link-check.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b94d02e9..a59a30ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,25 @@ jobs: - uses: taiki-e/install-action@cargo-audit - run: cargo audit + # —— 文档门:多语言文档链接 / 跨语言配对 校验(纯 Python,无需编译)—— + docs: + name: docs link check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: check doc links + run: python3 scripts/docs-link-check.py --report + # 不论通过与否都留存报告,便于排查失效链接。 + - name: upload report + if: always() + uses: actions/upload-artifact@v4 + with: + name: docs-link-check-report + path: docs/link-check-report.md + # —— 前端构建门:类型检查 + 产物构建,防 web/ 腐化 —— web: name: web build diff --git a/README.md b/README.md index 11dcc1f1..b327220e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ AI ROI · human-AI efficiency analytics · software engineering modeling · Task [![Rust](https://img.shields.io/badge/Rust-2021-orange.svg)](https://www.rust-lang.org/) [![License](https://img.shields.io/badge/License-GPL--2.0-blue.svg)](LICENSE) [![status](https://img.shields.io/badge/status-experimental-yellow.svg)](#status) -  ·   [简体中文](README.zh-CN.md) | English +  ·   [简体中文](README.zh-CN.md) · English Shepherd is a platform for supervising AI-driven development. AI can write code, but it won't judge whether it actually finished the requirement, and it won't be accountable for the result. Instead of building another "smarter agent," Shepherd sits around the agent: it breaks requirements down for AI executors to work on, puts a human approval step at two points (design and verification), and keeps a record of the whole thing. @@ -17,9 +17,6 @@ Shepherd is a platform for supervising AI-driven development. AI can write code, Shepherd — scenario run report - - - ## How it works A requirement roughly travels like this: @@ -81,39 +78,6 @@ On top of that rule you get, per project and per requirement: It measures shipped outcomes rather than keystrokes. The numbers come out smaller and more boring than "90% of our code is AI-written" claims — which is the point. -## Running it - -```bash -# 1) A Postgres; migrations are applied automatically on startup -docker run -d --name shep-pg \ - -e POSTGRES_USER=msuser -e POSTGRES_PASSWORD=mspass -e POSTGRES_DB=mstest \ - -p 55432:5432 postgres:16-alpine - -# 2) Start the server (the root sets default-members, so plain `cargo run` works) -DATABASE_URL=postgres://msuser:mspass@localhost:55432/mstest \ -SHEPHERD_ADMIN_PASSWORD=s3cret \ -cargo run # → http://localhost:8088 -``` - -```bash -# Log in for a token -curl -s localhost:8088/auth/login -H 'content-type: application/json' \ - -d '{"username":"admin","password":"s3cret"}' -``` - -The web console, and starting an executor on an internal machine: - -```bash -cd web && npm install && npm run dev # Vite console - -SHEPHERD_AGENT_FLEET=1 cargo run # server in fleet mode -SHEPHERD_BASE=http://:8088 SHEPHERD_CAPS=CLAUDE_CODE cargo run -p agent-runtime -``` - - - -Prefer one command? `docker compose -f deploy/docker/docker-compose.yml up --build` brings up the whole stack (server + agent-runtime + web + Postgres + Redis) — see the [usage guide](docs/USAGE.md) and [deployment guide](docs/DEPLOYMENT.md). - ## What's in here Each business module is its own crate, laid out hexagonally: `domain` / `ports` / `application` are pure logic with no IO by default, while the database and HTTP live in `adapters` behind feature flags. `tests/architecture.rs` scans the source and fails the build if a pure layer ever imports an IO crate like sqlx or axum — that keeps the layering from quietly eroding over time. @@ -145,37 +109,21 @@ web/ React + antd frontend ``` -
-Main environment variables - -Server (consolidated into a typed `ServerConfig`): - -| Variable | Default | Meaning | -|---|---|---| -| `DATABASE_URL` | local mstest | PG connection string | -| `SHEPHERD_BIND` | `0.0.0.0:8088` | main API listen address | -| `SHEPHERD_ADMIN_PASSWORD` | `admin` | admin password upserted idempotently on boot | -| `SHEPHERD_AGENT_FLEET` | — | set to enable fleet mode | -| `SHEPHERD_FLEET_REDIS` | — | set to use a Redis distributed queue / registry | -| `SHEPHERD_FEISHU_*` / `SHEPHERD_WECOM_*` | — | OIDC third-party login | - -Executor `agent-runtime`: - -| Variable | Default | Meaning | -|---|---|---| -| `SHEPHERD_BASE` | `http://127.0.0.1:9180` | server address | -| `SHEPHERD_CAPS` | `CLAUDE_CODE` | comma-separated capabilities (which tasks to claim) | -| `AGENT_CONCURRENCY` | `1` | max concurrent tasks | -| `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` | `claude` / `codex exec` / `opencode run` | each CLI invocation | -| `AGENT_MOCK` | — | set to use the mock backend (no real CLI) | -
+> Full environment-variable reference and per-crate configuration live in the [Usage guide](docs/USAGE.md#5-configuration-environment-variables). + +## Get started + +Prefer one command? `docker compose -f deploy/docker/docker-compose.yml up --build` brings up the whole stack (server + agent-runtime + web + Postgres + Redis). + +For everything else — from-source run, the full environment-variable reference, the web console, fleet & executor setup, and the HTTP API — see the **[Usage guide](docs/USAGE.md)** ([中文](docs/USAGE.zh-CN.md)). Production deployment (Helm, Terraform, CI/CD, day-2 ops) is covered in the **[Deployment & ops guide](docs/DEPLOYMENT.md)** ([中文](docs/DEPLOYMENT.zh-CN.md)). ## Documentation - **[Usage guide](docs/USAGE.md)** ([中文](docs/USAGE.zh-CN.md)) — concepts, quick start, the full configuration reference, the web console, fleet & executor setup, and the HTTP API. - **[Deployment & ops](docs/DEPLOYMENT.md)** ([中文](docs/DEPLOYMENT.zh-CN.md)) — Docker Compose, Kubernetes via Helm (`deploy/helm/shepherd`), multi-cloud Terraform (`deploy/terraform/{aws,gcp,azure}`), and CI/CD auto-deploy. -- **[Install agent-runtime](docs/INSTALL.md)** — Homebrew, Windows (Scoop / PowerShell), Linux binary, and registering to a server. +- **[Install agent-runtime](docs/INSTALL.md)** ([中文](docs/INSTALL.zh-CN.md)) — Homebrew, Windows (Scoop / PowerShell), Linux binary, and registering to a server. - **[Running AI executors](docs/EXECUTORS.md)** ([中文](docs/EXECUTORS.zh-CN.md)) — Claude Code / Codex / OpenCode / CodeBuddy behind `agent-runtime`. +- **[Comment conventions](docs/COMMENT_CONVENTIONS.md)** ([中文](docs/COMMENT_CONVENTIONS.zh-CN.md)) — rules for code comments across the workspace. ## How it compares diff --git a/README.zh-CN.md b/README.zh-CN.md index 538bd5ae..24713077 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -8,7 +8,7 @@ AI ROI、人效分析、软件工程建模、Task Graph。 [![Rust](https://img.shields.io/badge/Rust-2021-orange.svg)](https://www.rust-lang.org/) [![License](https://img.shields.io/badge/License-GPL--2.0-blue.svg)](LICENSE) [![status](https://img.shields.io/badge/status-experimental-yellow.svg)](#状态) -  ·   简体中文 | [English](README.md) +  ·   简体中文 · [English](README.md) Shepherd 是一个还在早期阶段的研发监督平台。AI 现在能写代码了,但它不会判断自己有没有真正做完需求,也不会替结果负责。与其再造一个"更聪明的 agent",Shepherd 做的是 agent 外面那一层——把需求拆给 AI 执行机去干,然后在设计和验证两个环节卡上人工审批,过程留痕。 @@ -81,39 +81,6 @@ Shepherd 是一个还在早期阶段的研发监督平台。AI 现在能写代 它量的是交付结果,不是敲键盘。所以这些数字会比「我们 90% 的代码是 AI 写的」那种说法难看得多,但也更实。 -## 跑起来 - -```bash -# 1) 一个 Postgres,迁移会在启动时自动施加 -docker run -d --name shep-pg \ - -e POSTGRES_USER=msuser -e POSTGRES_PASSWORD=mspass -e POSTGRES_DB=mstest \ - -p 55432:5432 postgres:16-alpine - -# 2) 起服务(根目录设了 default-members,直接 cargo run) -DATABASE_URL=postgres://msuser:mspass@localhost:55432/mstest \ -SHEPHERD_ADMIN_PASSWORD=s3cret \ -cargo run # → http://localhost:8088 -``` - -```bash -# 登录拿令牌 -curl -s localhost:8088/auth/login -H 'content-type: application/json' \ - -d '{"username":"admin","password":"s3cret"}' -``` - -前端控制台,以及在内网机上起一台执行机: - -```bash -cd web && npm install && npm run dev # Vite 控制台 - -SHEPHERD_AGENT_FLEET=1 cargo run # 服务端开机群模式 -SHEPHERD_BASE=http://:8088 SHEPHERD_CAPS=CLAUDE_CODE cargo run -p agent-runtime -``` - - - -想一条命令起全栈?`docker compose -f deploy/docker/docker-compose.yml up --build` 会拉起整套服务(server + agent-runtime + web + Postgres + Redis)——详见[使用指南](docs/USAGE.zh-CN.md)与[部署指南](docs/DEPLOYMENT.zh-CN.md)。 - ## 里面有什么 每个业务模块是一个独立 crate,按六边形分层:`domain` / `ports` / `application` 是纯逻辑、默认不碰 IO,数据库和 HTTP 都在 `adapters` 里用 feature 开关。`tests/architecture.rs` 会扫源码,纯层一旦引了 sqlx / axum 这类 IO crate 就让构建挂掉——免得分层写着写着被写穿。 @@ -145,37 +112,21 @@ web/ React + antd 前端 ``` -
-主要环境变量 - -服务端(收拢在 typed `ServerConfig`): - -| 变量 | 默认 | 含义 | -|---|---|---| -| `DATABASE_URL` | 本地 mstest | PG 连接串 | -| `SHEPHERD_BIND` | `0.0.0.0:8088` | 主 API 监听 | -| `SHEPHERD_ADMIN_PASSWORD` | `admin` | 启动时幂等 upsert 的 admin 密码 | -| `SHEPHERD_AGENT_FLEET` | — | 设置即开机群模式 | -| `SHEPHERD_FLEET_REDIS` | — | 设置即用 Redis 分布式队列 / 注册表 | -| `SHEPHERD_FEISHU_*` / `SHEPHERD_WECOM_*` | — | OIDC 第三方登录 | - -执行机 `agent-runtime`: - -| 变量 | 默认 | 含义 | -|---|---|---| -| `SHEPHERD_BASE` | `http://127.0.0.1:9180` | 服务端地址 | -| `SHEPHERD_CAPS` | `CLAUDE_CODE` | 逗号分隔的能力(认领哪类任务) | -| `AGENT_CONCURRENCY` | `1` | 并发任务上限 | -| `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` | `claude` / `codex exec` / `opencode run` | 各 CLI 调用 | -| `AGENT_MOCK` | — | 设置即用 mock 后端(免真实 CLI) | -
+> 完整环境变量与逐 crate 配置见[使用指南](docs/USAGE.zh-CN.md#5-配置环境变量)。 + +## 快速开始 + +想一条命令起全栈?`docker compose -f deploy/docker/docker-compose.yml up --build` 会拉起整套服务(server + agent-runtime + web + Postgres + Redis)。 + +其余内容——源码方式运行、完整环境变量参考、Web 控制台、机群与执行机配置、HTTP API——见**[使用指南](docs/USAGE.zh-CN.md)**([English](docs/USAGE.md))。生产部署(Helm、Terraform、CI/CD、Day-2 运维)见**[部署与运维指南](docs/DEPLOYMENT.zh-CN.md)**([English](docs/DEPLOYMENT.md))。 ## 文档 - **[使用指南](docs/USAGE.zh-CN.md)**([English](docs/USAGE.md))—— 概念、快速上手、完整配置参考、Web 控制台、机群与执行机配置、HTTP API。 - **[部署与运维](docs/DEPLOYMENT.zh-CN.md)**([English](docs/DEPLOYMENT.md))—— Docker Compose、Kubernetes(Helm,`deploy/helm/shepherd`)、多云 Terraform(`deploy/terraform/{aws,gcp,azure}`)、CI/CD 自动部署。 +- **[安装 agent-runtime](docs/INSTALL.zh-CN.md)**([English](docs/INSTALL.md))—— Homebrew、Windows(Scoop / PowerShell)、Linux 二进制,以及接入服务端。 - **[AI 执行者运行指南](docs/EXECUTORS.zh-CN.md)**([English](docs/EXECUTORS.md))—— 在 `agent-runtime` 下运行 Claude Code / Codex / OpenCode / CodeBuddy。 -- **[安装 agent-runtime](docs/INSTALL.md)** —— Homebrew、Windows(Scoop / PowerShell)、Linux 二进制,以及接入服务端(目前为英文)。 +- **[注释规范](docs/COMMENT_CONVENTIONS.zh-CN.md)**([English](docs/COMMENT_CONVENTIONS.md))—— 全仓库代码注释的统一约定。 ## 和别的方案比 diff --git a/crates/server/src/scenario_run.rs b/crates/server/src/scenario_run.rs index 832475a0..b4a2782d 100644 --- a/crates/server/src/scenario_run.rs +++ b/crates/server/src/scenario_run.rs @@ -212,7 +212,14 @@ impl ScenarioRunner { pool_id: Option<&str>, ) -> Result { let (nodes, count) = self.prepare(scenario_id).await?; - let env = self.resolve_env(environment_id).await?; + // Env fallback: when the caller does not pin an environment, use the + // scenario's own configured environment (meta.envId) so scenarios that + // declare a default base URL still resolve relative paths correctly. + let effective_env = match environment_id.filter(|s| !s.trim().is_empty()) { + Some(eid) => Some(eid.to_string()), + None => self.default_env_of(scenario_id).await, + }; + let env = self.resolve_env(effective_env.as_deref()).await?; let report_id = self .reports .create("SERIAL", count as i32) diff --git a/docs/COMMENT_CONVENTIONS.md b/docs/COMMENT_CONVENTIONS.md index c13dcfcd..4c6b5180 100644 --- a/docs/COMMENT_CONVENTIONS.md +++ b/docs/COMMENT_CONVENTIONS.md @@ -1,5 +1,7 @@ # Comment Conventions +[简体中文](COMMENT_CONVENTIONS.zh-CN.md) · English + Unified rules for comments in this workspace. The goal is consistent, English, rustdoc-friendly comments across every crate. diff --git a/docs/COMMENT_CONVENTIONS.zh-CN.md b/docs/COMMENT_CONVENTIONS.zh-CN.md new file mode 100644 index 00000000..0ad2e6e8 --- /dev/null +++ b/docs/COMMENT_CONVENTIONS.zh-CN.md @@ -0,0 +1,87 @@ +# 注释规范 + +简体中文 · [English](COMMENT_CONVENTIONS.md) + +本仓库注释的统一规则。目标是让每个 crate 的注释都保持一致、用英文、对 rustdoc 友好。 + +## 1. 注释形式 + +| 语法 | 用途 | +| ----------- | --------------------------------------------------- | +| `//!` | 模块级或 crate 级文档(放在文件顶部)。 | +| `///` | 紧跟其后的条目的文档。 | +| `//` | 行内说明、`TODO`、理由——非公开 API。 | +| `// TODO:` | 待办工作(英文)。优先附上 issue 链接。 | + +- 凡是公开面的一部分(导出的类型、函数、方法、模块),用 `///` / `//!`;内部推理用 `//`。 +- 文档文本不要用 `/* ... */` 块注释;正文用行注释,这样 `cargo doc` 才能收录。行内 `/* */` 用于临时注释代码没问题。 + +## 2. 语言 + +- 所有**文档文本**都用**英文**:`//`、`///`、`//!` 注释行,`utoipa` 的 `description = "..."` 属性,以及 `#[error(...)]` / `#[ignore = "..."]` / `#[schema(...)]` 里的消息。 +- 不要用中文、拼音或中英混杂写文档。 +- **例外:** 作为测试夹具或 mock 数据的字符串字面量(例如 `#[test]` / `r#"..."#` 里的 `"登录"`、`"张三"`)。这些是有意用来跑 i18n 的,保持原样。 + +## 3. 风格 + +- **首字母大写。** 每句以句号结尾(`.`)。 +- `//`、`///`、`//!` 后**空一格**:`/// Returns the count.` +- 用**第三人称或祈使句**作为摘要句: + - `/// Returns the number of active users.` + - `/// Parses the raw payload into a `Proposal`.` + - 避免 `This function returns...`;以动作开头。 +- 注释**精简**;不要复述代码一眼就看得懂的东西。解释 *为什么*、约束,以及不直观的行为。 +- 结构体 / 枚举字段,优先用以句号结尾的名词短语:`/// The user's display name.` + +## 4. 代码与标识符 + +- 类型、函数、模块、路径用反引号包起来:`Vec`、`parse()`、`crate::domain::Proposal`、`Result`。 +- 超过一行的行内代码片段,用围栏代码块: + + ```rust + let req = Proposal::new(..)?; + ``` + +## 5. Markdown + +- 文档注释(`///`、`//!`)支持 Markdown:列表、链接、表格、代码围栏。 +- 列表用 `-`;仅当列表需要引导句时,在首项前空一行。 + +## 6. 示例 + +```rust +//! HTTP adapters for the requirement crate. +//! Exposes REST endpoints backed by the PostgreSQL repository. + +/// Creates a new requirement from the given draft. +/// +/// Returns an error if the title is empty after trimming. +pub fn create(draft: Draft) -> Result { + // Reject empty titles early to avoid a round-trip to the DB. + if draft.title.trim().is_empty() { + return Err(Error::EmptyTitle); + } + // ... +} +``` + +## 7. 检查 + +确认注释行里没有残留中文: + +```bash +# 粗检:标出 // 行里、字符串字面量之外的中文字符 +python3 - <<'PY' +import os, re +def cjk(s): return any('\u4e00' <= c <= '\u9fff' for c in s) +for root, _, fs in os.walk('crates'): + for f in fs: + if not f.endswith('.rs'): + continue + for i, line in enumerate(open(os.path.join(root, f), encoding='utf-8', errors='ignore'), 1): + s = re.sub(r'"[^"]*"', '""', line) + s = re.sub(r"'[^']*'", "''", s) + if cjk(s) and re.match(r'\s*//', line): + print(f'{os.path.join(root, f)}:{i}: {line.rstrip()}') +PY +``` diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 6348c2a9..f5f08633 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,5 +1,7 @@ # Shepherd — Deployment & Operations +[简体中文](DEPLOYMENT.zh-CN.md) · English + This guide covers building images, running locally, deploying to Kubernetes via Helm, provisioning cloud infrastructure with Terraform, the CI/CD pipeline, and Day-2 operations. For end-user and API usage see [USAGE.md](./USAGE.md). diff --git a/docs/DEPLOYMENT.zh-CN.md b/docs/DEPLOYMENT.zh-CN.md index 6db8176c..f9e5f72d 100644 --- a/docs/DEPLOYMENT.zh-CN.md +++ b/docs/DEPLOYMENT.zh-CN.md @@ -1,5 +1,7 @@ # Shepherd — 部署与运维 +简体中文 · [English](DEPLOYMENT.md) + 本指南涵盖镜像构建、本地运行、通过 Helm 部署到 Kubernetes、使用 Terraform 预置云基础设施、 CI/CD 流水线以及 Day-2 运维。终端用户与 API 使用方式见 [USAGE.zh-CN.md](./USAGE.zh-CN.md)。 diff --git a/docs/EXECUTORS.md b/docs/EXECUTORS.md index bbaa562e..86ecd10c 100644 --- a/docs/EXECUTORS.md +++ b/docs/EXECUTORS.md @@ -1,5 +1,7 @@ # Shepherd — Running AI Executors +[简体中文](EXECUTORS.zh-CN.md) · English + How to run each supported AI executor (Claude Code / Codex / OpenCode / CodeBuddy) behind `agent-runtime`. For fleet architecture and server-side setup see [USAGE.md §7](./USAGE.md); for image builds and deployment see [DEPLOYMENT.md](./DEPLOYMENT.md). diff --git a/docs/EXECUTORS.zh-CN.md b/docs/EXECUTORS.zh-CN.md index 728862f9..e4700416 100644 --- a/docs/EXECUTORS.zh-CN.md +++ b/docs/EXECUTORS.zh-CN.md @@ -1,5 +1,7 @@ # Shepherd — AI 执行者运行指南 +简体中文 · [English](EXECUTORS.md) + 介绍如何在 `agent-runtime` 下运行各家 AI 执行者(Claude Code / Codex / OpenCode / CodeBuddy)。机群架构与服务端配置见 [USAGE.zh-CN.md §7](./USAGE.zh-CN.md), 镜像构建与部署见 [DEPLOYMENT.zh-CN.md](./DEPLOYMENT.zh-CN.md)。 diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 764b7024..350dfb20 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -1,5 +1,7 @@ # Install +[简体中文](INSTALL.zh-CN.md) · English + How to get `agent-runtime` running on your machine. `agent-runtime` is the executor that registers to a Shepherd server, long-polls for work, and shells out to your AI CLI (Claude Code / Codex / OpenCode / CodeBuddy). The server and web console are deployed separately — see [Deployment & ops](DEPLOYMENT.md) and the [GHCR docker-compose](../deploy/docker/docker-compose.ghcr.yml) for a single-host Linux deploy. Pick the install method for your OS: diff --git a/docs/INSTALL.zh-CN.md b/docs/INSTALL.zh-CN.md new file mode 100644 index 00000000..9785d298 --- /dev/null +++ b/docs/INSTALL.zh-CN.md @@ -0,0 +1,135 @@ +# 安装 agent-runtime + +简体中文 · [English](INSTALL.md) + +如何在本机跑起 `agent-runtime`。`agent-runtime` 是执行机,它向 Shepherd 服务端注册、长轮询认领任务,并调用你的 AI CLI(Claude Code / Codex / OpenCode / CodeBuddy)。服务端和 Web 控制台需另行部署——见[部署与运维](DEPLOYMENT.zh-CN.md),单机 Linux 部署可参考 [GHCR docker-compose](../deploy/docker/docker-compose.ghcr.yml)。 + +按系统选择安装方式: + +| 系统 | 推荐 | 备选 | +|---|---|---| +| macOS | Homebrew | 手动二进制 | +| Windows | Scoop / 一键 PowerShell | 手动二进制 | +| Linux | 手动二进制 (tar.gz) | Docker | + +所有二进制制品都附在每个 `v*` 标签的 GitHub Release 中:`agent-runtime-x86_64-unknown-linux-gnu.tar.gz`、`aarch64-unknown-linux-gnu`、`x86_64-apple-darwin`、`aarch64-apple-darwin`、`x86_64-pc-windows-msvc.zip`,以及 `checksums.txt`。 + +> **制品未签名。** `agent-runtime` 是纯 Rust(rustls-tls,无 OpenSSL),所以交叉编译出的二进制不带代码签名。macOS Gatekeeper 和 Windows SmartScreen 都会先拦截,需要你手动放行——见下文各系统说明。 + +--- + +## macOS — Homebrew + +Formula 在 `muxso/homebrew-shepherd` 这个 tap 里,Brew 会根据 Release 元数据自动更新。 + +```bash +brew install muxso/shepherd/agent-runtime +agent-runtime --help +``` + +日后升级: + +```bash +brew upgrade muxso/shepherd/agent-runtime +``` + +### 首次运行放行 Gatekeeper(仅一次) + +因为二进制未签名,macOS 会拒绝打开。清除一次隔离标志即可: + +```bash +xattr -d com.apple.quarantine "$(brew --prefix muxso/shepherd/agent-runtime)/bin/agent-runtime" +``` + +如果你是用 tarball 手动装的(没走 brew),把 `xattr` 指向你放二进制的位置,例如: + +```bash +xattr -d com.apple.quarantine /usr/local/bin/agent-runtime +``` + +--- + +## Windows — Scoop 或 PowerShell + +### 方式 A:Scoop(推荐) + +```powershell +scoop bucket add shepherd https://github.com/muxso/scoop-bucket +scoop install shepherd-agent-runtime +agent-runtime --help +``` + +Scoop 的 manifest 里有 `autoupdate`,会跟随新 Release,所以 `scoop update` 就能拉到新版本。 + +### 方式 B:一键 PowerShell 安装 + +下载最新 Windows zip,解压到 `%LOCALAPPDATA%\shepherd`,并加入用户 PATH: + +```powershell +irm https://raw.githubusercontent.com/muxso/Shepherd/main/scripts/install.ps1 | iex +``` + +之后**重开**一个终端(PATH 变动对已经打开的 shell 不生效),然后: + +```powershell +agent-runtime --help +``` + +> 若 SmartScreen 拦截,选 "More info → Run anyway",或手动放行一次:`Unblock-File -Path "$env:LOCALAPPDATA\shepherd\agent-runtime.exe"`。 + +--- + +## Linux — 手动二进制 + +```bash +# 选你的架构 +curl -sSL https://github.com/muxso/Shepherd/releases/latest/download/agent-runtime-x86_64-unknown-linux-gnu.tar.gz -o rt.tar.gz +sudo tar -xzf rt.tar.gz -C /usr/local/bin agent-runtime +agent-runtime --help +``` + +ARM64 用 `agent-runtime-aarch64-unknown-linux-gnu.tar.gz`。Linux 下无需隔离 / SmartScreen 这一步。 + +--- + +## 接入服务端 + +装好后,把执行机注册到你的 Shepherd 服务端。你需要服务端的 base URL 和一个 **agent key**(`AccessKey.SecretKey`,形如 `sak_<16hex>.<32hex>`),在 Web 控制台的 *API Keys* 下创建。 + +```bash +SHEPHERD_BASE=http://:8088 \ +SHEPHERD_CAPS=CLAUDE_CODE \ +SHEPHERD_AGENT_KEY=sak_f7a83a85536c4391.3b9f... \ +agent-runtime +``` + +| 变量 | 默认值 | 含义 | +|---|---|---| +| `SHEPHERD_BASE` | `http://127.0.0.1:9180` | 执行机长轮询的服务端地址 | +| `SHEPHERD_CAPS` | `CLAUDE_CODE` | 逗号分隔的能力(认领哪类任务) | +| `SHEPHERD_AGENT_KEY` | — | 来自 *API Keys* 的 `AccessKey.SecretKey` | +| `AGENT_CONCURRENCY` | `1` | 并发任务上限 | +| `CLAUDE_BIN` / `CODEX_CMD` / `OPENCODE_CMD` | `claude` / `codex exec` / `opencode run` | 各 CLI 调用 | +| `AGENT_MOCK` | — | 设置即用 mock 后端(免真实 CLI) | + +针对 **CodeBuddy**,用同样方式注册并让它认领任务: + +```bash +SHEPHERD_BASE=http://:8088 \ +SHEPHERD_CAPS=CODEBUDDY \ +SHEPHERD_AGENT_KEY=sak_xxxx.yyyy \ +agent-runtime +``` + +更多执行机后端与接线细节见[运行 AI 执行者](EXECUTORS.zh-CN.md)。 + +--- + +## 校验下载 + +每个 Release 都附带 `checksums.txt`(SHA-256)。信任二进制前先校验: + +```bash +curl -sSL https://github.com/muxso/Shepherd/releases/latest/download/checksums.txt -o checksums.txt +sha256sum -c checksums.txt +``` diff --git a/docs/USAGE.md b/docs/USAGE.md index 610b2857..e921c987 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -1,6 +1,8 @@ # Shepherd — Usage Guide -> Audience: anyone running, operating, or evaluating Shepherd. For production deployment (Helm, Terraform, CI/CD, day-2 ops) see [DEPLOYMENT.md](DEPLOYMENT.md). 简体中文版见 [USAGE.zh-CN.md](USAGE.zh-CN.md). +[简体中文](USAGE.zh-CN.md) · English + +> Audience: anyone running, operating, or evaluating Shepherd. For production deployment (Helm, Terraform, CI/CD, day-2 ops) see [DEPLOYMENT.md](DEPLOYMENT.md). Shepherd is an AI-development *supervision* platform: from idea to delivery — safer, more reliable quality gates. It breaks a requirement down for AI executors to work on, puts a **human approval gate at two points** (design and verification), and keeps a record of the whole loop. diff --git a/docs/USAGE.zh-CN.md b/docs/USAGE.zh-CN.md index a8b242ee..4a60ee40 100644 --- a/docs/USAGE.zh-CN.md +++ b/docs/USAGE.zh-CN.md @@ -1,6 +1,8 @@ # Shepherd — 使用指南 -> 适用对象:运行、运维或评估 Shepherd 的人。生产部署(Helm、Terraform、CI/CD、Day-2 运维)见 [DEPLOYMENT.zh-CN.md](DEPLOYMENT.zh-CN.md)。English version: [USAGE.md](USAGE.md)。 +简体中文 · [English](USAGE.md) + +> 适用对象:运行、运维或评估 Shepherd 的人。生产部署(Helm、Terraform、CI/CD、Day-2 运维)见 [DEPLOYMENT.zh-CN.md](DEPLOYMENT.zh-CN.md)。 Shepherd 是一个 AI 研发**监督**平台:从 idea 到交付,更安全、更可靠的质量把关。它把需求拆给 AI 执行者去做,并在**两个环节设人工审批门**(设计、验证),把整条链路完整记录在案。 diff --git a/docs/link-check-report.md b/docs/link-check-report.md new file mode 100644 index 00000000..bd732a9a --- /dev/null +++ b/docs/link-check-report.md @@ -0,0 +1,219 @@ +# 多语言文档链接检查报告 / Documentation Link Check Report + +- 生成日期 / Generated: **2026-08-09** +- 检查范围 / Scope: 12 个文档(README + `docs/*.md`,英文 + 简体中文) +- 链接总数 / Total links checked: **130** +- 有效 / Valid: **130** 无效 / Broken: **0** + +## 结论 / Result + +> ✅ 所有内部文档链接、跨语言切换、章节锚点与资源引用均有效。 + +## 跨语言配对完整性 / Cross-language pairing + +| 文档组 / Group | 文件 | 中英齐全 / EN+ZH | +|---|---|---| +| `README` | `README.md`, `README.zh-CN.md` | ✅ | +| `docs/COMMENT_CONVENTIONS` | `COMMENT_CONVENTIONS.md`, `COMMENT_CONVENTIONS.zh-CN.md` | ✅ | +| `docs/DEPLOYMENT` | `DEPLOYMENT.md`, `DEPLOYMENT.zh-CN.md` | ✅ | +| `docs/EXECUTORS` | `EXECUTORS.md`, `EXECUTORS.zh-CN.md` | ✅ | +| `docs/INSTALL` | `INSTALL.md`, `INSTALL.zh-CN.md` | ✅ | +| `docs/USAGE` | `USAGE.md`, `USAGE.zh-CN.md` | ✅ | + +## 各文档链接清单 / Link inventory + +### README.md (17 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 11 | doc | `README.zh-CN.md` | OK | 🔁 | +| 112 | doc | `docs/USAGE.md#5-configuration-environment-variables` | OK | | +| 118 | doc | `docs/USAGE.md` | OK | | +| 118 | doc | `docs/USAGE.zh-CN.md` | OK | 🔁 | +| 118 | doc | `docs/DEPLOYMENT.md` | OK | | +| 118 | doc | `docs/DEPLOYMENT.zh-CN.md` | OK | 🔁 | +| 122 | doc | `docs/USAGE.md` | OK | | +| 122 | doc | `docs/USAGE.zh-CN.md` | OK | 🔁 | +| 123 | doc | `docs/DEPLOYMENT.md` | OK | | +| 123 | doc | `docs/DEPLOYMENT.zh-CN.md` | OK | 🔁 | +| 124 | doc | `docs/INSTALL.md` | OK | | +| 124 | doc | `docs/INSTALL.zh-CN.md` | OK | 🔁 | +| 125 | doc | `docs/EXECUTORS.md` | OK | | +| 125 | doc | `docs/EXECUTORS.zh-CN.md` | OK | 🔁 | +| 126 | doc | `docs/COMMENT_CONVENTIONS.md` | OK | | +| 126 | doc | `docs/COMMENT_CONVENTIONS.zh-CN.md` | OK | 🔁 | +| 159 | file | `LICENSE` | OK | | + +### README.zh-CN.md (17 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 11 | doc | `README.md` | OK | 🔁 | +| 115 | doc | `docs/USAGE.zh-CN.md#5-配置环境变量` | OK | | +| 121 | doc | `docs/USAGE.zh-CN.md` | OK | | +| 121 | doc | `docs/USAGE.md` | OK | 🔁 | +| 121 | doc | `docs/DEPLOYMENT.zh-CN.md` | OK | | +| 121 | doc | `docs/DEPLOYMENT.md` | OK | 🔁 | +| 125 | doc | `docs/USAGE.zh-CN.md` | OK | | +| 125 | doc | `docs/USAGE.md` | OK | 🔁 | +| 126 | doc | `docs/DEPLOYMENT.zh-CN.md` | OK | | +| 126 | doc | `docs/DEPLOYMENT.md` | OK | 🔁 | +| 127 | doc | `docs/INSTALL.zh-CN.md` | OK | | +| 127 | doc | `docs/INSTALL.md` | OK | 🔁 | +| 128 | doc | `docs/EXECUTORS.zh-CN.md` | OK | | +| 128 | doc | `docs/EXECUTORS.md` | OK | 🔁 | +| 129 | doc | `docs/COMMENT_CONVENTIONS.zh-CN.md` | OK | | +| 129 | doc | `docs/COMMENT_CONVENTIONS.md` | OK | 🔁 | +| 162 | file | `LICENSE` | OK | | + +### docs/COMMENT_CONVENTIONS.md (1 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `COMMENT_CONVENTIONS.zh-CN.md` | OK | 🔁 | + +### docs/COMMENT_CONVENTIONS.zh-CN.md (1 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `COMMENT_CONVENTIONS.md` | OK | 🔁 | + +### docs/DEPLOYMENT.md (21 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `DEPLOYMENT.zh-CN.md` | OK | 🔁 | +| 7 | doc | `./USAGE.md` | OK | | +| 12 | anchor(same-file) | `#topology` | OK | | +| 13 | anchor(same-file) | `#building-images` | OK | | +| 14 | anchor(same-file) | `#local-stack-docker-compose` | OK | | +| 15 | anchor(same-file) | `#kubernetes-via-helm` | OK | | +| 16 | anchor(same-file) | `#multi-cloud-terraform` | OK | | +| 17 | anchor(same-file) | `#cicd-auto-deploy` | OK | | +| 18 | anchor(same-file) | `#day-2-operations` | OK | | +| 74 | file | `../deploy/docker/` | OK | | +| 107 | anchor(same-file) | `#cicd-auto-deploy` | OK | | +| 114 | file | `../deploy/docker/docker-compose.yml` | OK | | +| 147 | doc | `./USAGE.md` | OK | | +| 153 | file | `../deploy/helm/shepherd/` | OK | | +| 197 | file | `../deploy/helm/shepherd/values.yaml` | OK | | +| 277 | file | `../deploy/terraform/` | OK | | +| 285 | file | `../deploy/terraform/aws/` | OK | | +| 286 | file | `../deploy/terraform/gcp/` | OK | | +| 287 | file | `../deploy/terraform/azure/` | OK | | +| 345 | file | `../.github/workflows/` | OK | | +| 514 | doc | `./USAGE.md` | OK | | + +### docs/DEPLOYMENT.zh-CN.md (21 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `DEPLOYMENT.md` | OK | 🔁 | +| 6 | doc | `./USAGE.zh-CN.md` | OK | | +| 11 | anchor(same-file) | `#topology` | OK | | +| 12 | anchor(same-file) | `#building-images` | OK | | +| 13 | anchor(same-file) | `#local-stack-docker-compose` | OK | | +| 14 | anchor(same-file) | `#kubernetes-via-helm` | OK | | +| 15 | anchor(same-file) | `#multi-cloud-terraform` | OK | | +| 16 | anchor(same-file) | `#cicd-auto-deploy` | OK | | +| 17 | anchor(same-file) | `#day-2-operations` | OK | | +| 69 | file | `../deploy/docker/` | OK | | +| 101 | anchor(same-file) | `#cicd-auto-deploy` | OK | | +| 108 | file | `../deploy/docker/docker-compose.yml` | OK | | +| 140 | doc | `./USAGE.zh-CN.md` | OK | | +| 146 | file | `../deploy/helm/shepherd/` | OK | | +| 188 | file | `../deploy/helm/shepherd/values.yaml` | OK | | +| 262 | file | `../deploy/terraform/` | OK | | +| 269 | file | `../deploy/terraform/aws/` | OK | | +| 270 | file | `../deploy/terraform/gcp/` | OK | | +| 271 | file | `../deploy/terraform/azure/` | OK | | +| 326 | file | `../.github/workflows/` | OK | | +| 483 | doc | `./USAGE.zh-CN.md` | OK | | + +### docs/EXECUTORS.md (17 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `EXECUTORS.zh-CN.md` | OK | 🔁 | +| 7 | doc | `./USAGE.md` | OK | | +| 7 | doc | `./DEPLOYMENT.md` | OK | | +| 9 | anchor(same-file) | `#how-dispatch-reaches-a-cli` | OK | | +| 10 | anchor(same-file) | `#common-runtime-configuration` | OK | | +| 11 | anchor(same-file) | `#claude-code` | OK | | +| 12 | anchor(same-file) | `#codex` | OK | | +| 13 | anchor(same-file) | `#opencode` | OK | | +| 14 | anchor(same-file) | `#codebuddy` | OK | | +| 15 | anchor(same-file) | `#one-runtime-multiple-executors` | OK | | +| 16 | anchor(same-file) | `#mock-executor-no-cli` | OK | | +| 17 | anchor(same-file) | `#windows` | OK | | +| 18 | anchor(same-file) | `#docker-and-host-clis` | OK | | +| 19 | anchor(same-file) | `#shared-checkout-different-branches` | OK | | +| 20 | anchor(same-file) | `#dispatching-to-a-specific-executor` | OK | | +| 21 | anchor(same-file) | `#pool-runners-scenario-execution` | OK | | +| 22 | anchor(same-file) | `#troubleshooting` | OK | | + +### docs/EXECUTORS.zh-CN.md (17 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `EXECUTORS.md` | OK | 🔁 | +| 6 | doc | `./USAGE.zh-CN.md` | OK | | +| 7 | doc | `./DEPLOYMENT.zh-CN.md` | OK | | +| 9 | anchor(same-file) | `#派发如何到达-cli` | OK | | +| 10 | anchor(same-file) | `#runtime-通用配置` | OK | | +| 11 | anchor(same-file) | `#claude-code` | OK | | +| 12 | anchor(same-file) | `#codex` | OK | | +| 13 | anchor(same-file) | `#opencode` | OK | | +| 14 | anchor(same-file) | `#codebuddy` | OK | | +| 15 | anchor(same-file) | `#一个-runtime-承接多种执行者` | OK | | +| 16 | anchor(same-file) | `#mock-执行者无需-cli` | OK | | +| 17 | anchor(same-file) | `#windows` | OK | | +| 18 | anchor(same-file) | `#docker-与宿主机-cli` | OK | | +| 19 | anchor(same-file) | `#共用检出不同分支` | OK | | +| 20 | anchor(same-file) | `#指定执行者派发` | OK | | +| 21 | anchor(same-file) | `#资源池执行机场景执行` | OK | | +| 22 | anchor(same-file) | `#排障` | OK | | + +### docs/INSTALL.md (4 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `INSTALL.zh-CN.md` | OK | 🔁 | +| 5 | doc | `DEPLOYMENT.md` | OK | | +| 5 | file | `../deploy/docker/docker-compose.ghcr.yml` | OK | | +| 124 | doc | `EXECUTORS.md` | OK | | + +### docs/INSTALL.zh-CN.md (4 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `INSTALL.md` | OK | 🔁 | +| 5 | doc | `DEPLOYMENT.zh-CN.md` | OK | | +| 5 | file | `../deploy/docker/docker-compose.ghcr.yml` | OK | | +| 124 | doc | `EXECUTORS.zh-CN.md` | OK | | + +### docs/USAGE.md (5 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `USAGE.zh-CN.md` | OK | 🔁 | +| 5 | doc | `DEPLOYMENT.md` | OK | | +| 89 | doc | `DEPLOYMENT.md` | OK | | +| 282 | doc | `./EXECUTORS.md` | OK | | +| 356 | doc | `DEPLOYMENT.md` | OK | | + +### docs/USAGE.zh-CN.md (5 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `USAGE.md` | OK | 🔁 | +| 5 | doc | `DEPLOYMENT.zh-CN.md` | OK | | +| 89 | doc | `DEPLOYMENT.zh-CN.md` | OK | | +| 278 | doc | `./EXECUTORS.zh-CN.md` | OK | | +| 352 | doc | `DEPLOYMENT.zh-CN.md` | OK | | + +## 说明 / Notes + +- 检查方式 / Method: 移除 HTML 注释后提取 Markdown 链接,对文件目标做存在性校验,对 `.md` 目标按 GitHub 风格 slug(小写、保留字母/数字/连字符、空格转 `-`、丢弃 `。、()#` 等标点)校验章节锚点。 +- 外部链接(http/https/mailto)与注释中的占位链接(如 `docs/assets/demo.gif`)未计入有效/无效判定。 +- 所有文档顶部均已统一语言切换导航 `[简体中文](x.zh-CN.md) · English` / `简体中文 · [English](x.md)`。 diff --git a/out.md b/out.md new file mode 100644 index 00000000..bd732a9a --- /dev/null +++ b/out.md @@ -0,0 +1,219 @@ +# 多语言文档链接检查报告 / Documentation Link Check Report + +- 生成日期 / Generated: **2026-08-09** +- 检查范围 / Scope: 12 个文档(README + `docs/*.md`,英文 + 简体中文) +- 链接总数 / Total links checked: **130** +- 有效 / Valid: **130** 无效 / Broken: **0** + +## 结论 / Result + +> ✅ 所有内部文档链接、跨语言切换、章节锚点与资源引用均有效。 + +## 跨语言配对完整性 / Cross-language pairing + +| 文档组 / Group | 文件 | 中英齐全 / EN+ZH | +|---|---|---| +| `README` | `README.md`, `README.zh-CN.md` | ✅ | +| `docs/COMMENT_CONVENTIONS` | `COMMENT_CONVENTIONS.md`, `COMMENT_CONVENTIONS.zh-CN.md` | ✅ | +| `docs/DEPLOYMENT` | `DEPLOYMENT.md`, `DEPLOYMENT.zh-CN.md` | ✅ | +| `docs/EXECUTORS` | `EXECUTORS.md`, `EXECUTORS.zh-CN.md` | ✅ | +| `docs/INSTALL` | `INSTALL.md`, `INSTALL.zh-CN.md` | ✅ | +| `docs/USAGE` | `USAGE.md`, `USAGE.zh-CN.md` | ✅ | + +## 各文档链接清单 / Link inventory + +### README.md (17 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 11 | doc | `README.zh-CN.md` | OK | 🔁 | +| 112 | doc | `docs/USAGE.md#5-configuration-environment-variables` | OK | | +| 118 | doc | `docs/USAGE.md` | OK | | +| 118 | doc | `docs/USAGE.zh-CN.md` | OK | 🔁 | +| 118 | doc | `docs/DEPLOYMENT.md` | OK | | +| 118 | doc | `docs/DEPLOYMENT.zh-CN.md` | OK | 🔁 | +| 122 | doc | `docs/USAGE.md` | OK | | +| 122 | doc | `docs/USAGE.zh-CN.md` | OK | 🔁 | +| 123 | doc | `docs/DEPLOYMENT.md` | OK | | +| 123 | doc | `docs/DEPLOYMENT.zh-CN.md` | OK | 🔁 | +| 124 | doc | `docs/INSTALL.md` | OK | | +| 124 | doc | `docs/INSTALL.zh-CN.md` | OK | 🔁 | +| 125 | doc | `docs/EXECUTORS.md` | OK | | +| 125 | doc | `docs/EXECUTORS.zh-CN.md` | OK | 🔁 | +| 126 | doc | `docs/COMMENT_CONVENTIONS.md` | OK | | +| 126 | doc | `docs/COMMENT_CONVENTIONS.zh-CN.md` | OK | 🔁 | +| 159 | file | `LICENSE` | OK | | + +### README.zh-CN.md (17 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 11 | doc | `README.md` | OK | 🔁 | +| 115 | doc | `docs/USAGE.zh-CN.md#5-配置环境变量` | OK | | +| 121 | doc | `docs/USAGE.zh-CN.md` | OK | | +| 121 | doc | `docs/USAGE.md` | OK | 🔁 | +| 121 | doc | `docs/DEPLOYMENT.zh-CN.md` | OK | | +| 121 | doc | `docs/DEPLOYMENT.md` | OK | 🔁 | +| 125 | doc | `docs/USAGE.zh-CN.md` | OK | | +| 125 | doc | `docs/USAGE.md` | OK | 🔁 | +| 126 | doc | `docs/DEPLOYMENT.zh-CN.md` | OK | | +| 126 | doc | `docs/DEPLOYMENT.md` | OK | 🔁 | +| 127 | doc | `docs/INSTALL.zh-CN.md` | OK | | +| 127 | doc | `docs/INSTALL.md` | OK | 🔁 | +| 128 | doc | `docs/EXECUTORS.zh-CN.md` | OK | | +| 128 | doc | `docs/EXECUTORS.md` | OK | 🔁 | +| 129 | doc | `docs/COMMENT_CONVENTIONS.zh-CN.md` | OK | | +| 129 | doc | `docs/COMMENT_CONVENTIONS.md` | OK | 🔁 | +| 162 | file | `LICENSE` | OK | | + +### docs/COMMENT_CONVENTIONS.md (1 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `COMMENT_CONVENTIONS.zh-CN.md` | OK | 🔁 | + +### docs/COMMENT_CONVENTIONS.zh-CN.md (1 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `COMMENT_CONVENTIONS.md` | OK | 🔁 | + +### docs/DEPLOYMENT.md (21 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `DEPLOYMENT.zh-CN.md` | OK | 🔁 | +| 7 | doc | `./USAGE.md` | OK | | +| 12 | anchor(same-file) | `#topology` | OK | | +| 13 | anchor(same-file) | `#building-images` | OK | | +| 14 | anchor(same-file) | `#local-stack-docker-compose` | OK | | +| 15 | anchor(same-file) | `#kubernetes-via-helm` | OK | | +| 16 | anchor(same-file) | `#multi-cloud-terraform` | OK | | +| 17 | anchor(same-file) | `#cicd-auto-deploy` | OK | | +| 18 | anchor(same-file) | `#day-2-operations` | OK | | +| 74 | file | `../deploy/docker/` | OK | | +| 107 | anchor(same-file) | `#cicd-auto-deploy` | OK | | +| 114 | file | `../deploy/docker/docker-compose.yml` | OK | | +| 147 | doc | `./USAGE.md` | OK | | +| 153 | file | `../deploy/helm/shepherd/` | OK | | +| 197 | file | `../deploy/helm/shepherd/values.yaml` | OK | | +| 277 | file | `../deploy/terraform/` | OK | | +| 285 | file | `../deploy/terraform/aws/` | OK | | +| 286 | file | `../deploy/terraform/gcp/` | OK | | +| 287 | file | `../deploy/terraform/azure/` | OK | | +| 345 | file | `../.github/workflows/` | OK | | +| 514 | doc | `./USAGE.md` | OK | | + +### docs/DEPLOYMENT.zh-CN.md (21 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `DEPLOYMENT.md` | OK | 🔁 | +| 6 | doc | `./USAGE.zh-CN.md` | OK | | +| 11 | anchor(same-file) | `#topology` | OK | | +| 12 | anchor(same-file) | `#building-images` | OK | | +| 13 | anchor(same-file) | `#local-stack-docker-compose` | OK | | +| 14 | anchor(same-file) | `#kubernetes-via-helm` | OK | | +| 15 | anchor(same-file) | `#multi-cloud-terraform` | OK | | +| 16 | anchor(same-file) | `#cicd-auto-deploy` | OK | | +| 17 | anchor(same-file) | `#day-2-operations` | OK | | +| 69 | file | `../deploy/docker/` | OK | | +| 101 | anchor(same-file) | `#cicd-auto-deploy` | OK | | +| 108 | file | `../deploy/docker/docker-compose.yml` | OK | | +| 140 | doc | `./USAGE.zh-CN.md` | OK | | +| 146 | file | `../deploy/helm/shepherd/` | OK | | +| 188 | file | `../deploy/helm/shepherd/values.yaml` | OK | | +| 262 | file | `../deploy/terraform/` | OK | | +| 269 | file | `../deploy/terraform/aws/` | OK | | +| 270 | file | `../deploy/terraform/gcp/` | OK | | +| 271 | file | `../deploy/terraform/azure/` | OK | | +| 326 | file | `../.github/workflows/` | OK | | +| 483 | doc | `./USAGE.zh-CN.md` | OK | | + +### docs/EXECUTORS.md (17 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `EXECUTORS.zh-CN.md` | OK | 🔁 | +| 7 | doc | `./USAGE.md` | OK | | +| 7 | doc | `./DEPLOYMENT.md` | OK | | +| 9 | anchor(same-file) | `#how-dispatch-reaches-a-cli` | OK | | +| 10 | anchor(same-file) | `#common-runtime-configuration` | OK | | +| 11 | anchor(same-file) | `#claude-code` | OK | | +| 12 | anchor(same-file) | `#codex` | OK | | +| 13 | anchor(same-file) | `#opencode` | OK | | +| 14 | anchor(same-file) | `#codebuddy` | OK | | +| 15 | anchor(same-file) | `#one-runtime-multiple-executors` | OK | | +| 16 | anchor(same-file) | `#mock-executor-no-cli` | OK | | +| 17 | anchor(same-file) | `#windows` | OK | | +| 18 | anchor(same-file) | `#docker-and-host-clis` | OK | | +| 19 | anchor(same-file) | `#shared-checkout-different-branches` | OK | | +| 20 | anchor(same-file) | `#dispatching-to-a-specific-executor` | OK | | +| 21 | anchor(same-file) | `#pool-runners-scenario-execution` | OK | | +| 22 | anchor(same-file) | `#troubleshooting` | OK | | + +### docs/EXECUTORS.zh-CN.md (17 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `EXECUTORS.md` | OK | 🔁 | +| 6 | doc | `./USAGE.zh-CN.md` | OK | | +| 7 | doc | `./DEPLOYMENT.zh-CN.md` | OK | | +| 9 | anchor(same-file) | `#派发如何到达-cli` | OK | | +| 10 | anchor(same-file) | `#runtime-通用配置` | OK | | +| 11 | anchor(same-file) | `#claude-code` | OK | | +| 12 | anchor(same-file) | `#codex` | OK | | +| 13 | anchor(same-file) | `#opencode` | OK | | +| 14 | anchor(same-file) | `#codebuddy` | OK | | +| 15 | anchor(same-file) | `#一个-runtime-承接多种执行者` | OK | | +| 16 | anchor(same-file) | `#mock-执行者无需-cli` | OK | | +| 17 | anchor(same-file) | `#windows` | OK | | +| 18 | anchor(same-file) | `#docker-与宿主机-cli` | OK | | +| 19 | anchor(same-file) | `#共用检出不同分支` | OK | | +| 20 | anchor(same-file) | `#指定执行者派发` | OK | | +| 21 | anchor(same-file) | `#资源池执行机场景执行` | OK | | +| 22 | anchor(same-file) | `#排障` | OK | | + +### docs/INSTALL.md (4 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `INSTALL.zh-CN.md` | OK | 🔁 | +| 5 | doc | `DEPLOYMENT.md` | OK | | +| 5 | file | `../deploy/docker/docker-compose.ghcr.yml` | OK | | +| 124 | doc | `EXECUTORS.md` | OK | | + +### docs/INSTALL.zh-CN.md (4 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `INSTALL.md` | OK | 🔁 | +| 5 | doc | `DEPLOYMENT.zh-CN.md` | OK | | +| 5 | file | `../deploy/docker/docker-compose.ghcr.yml` | OK | | +| 124 | doc | `EXECUTORS.zh-CN.md` | OK | | + +### docs/USAGE.md (5 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `USAGE.zh-CN.md` | OK | 🔁 | +| 5 | doc | `DEPLOYMENT.md` | OK | | +| 89 | doc | `DEPLOYMENT.md` | OK | | +| 282 | doc | `./EXECUTORS.md` | OK | | +| 356 | doc | `DEPLOYMENT.md` | OK | | + +### docs/USAGE.zh-CN.md (5 链接) + +| 行 | 类型 | 目标 | 状态 | 跨语言 | +|---|---|---|---|---| +| 3 | doc | `USAGE.md` | OK | 🔁 | +| 5 | doc | `DEPLOYMENT.zh-CN.md` | OK | | +| 89 | doc | `DEPLOYMENT.zh-CN.md` | OK | | +| 278 | doc | `./EXECUTORS.zh-CN.md` | OK | | +| 352 | doc | `DEPLOYMENT.zh-CN.md` | OK | | + +## 说明 / Notes + +- 检查方式 / Method: 移除 HTML 注释后提取 Markdown 链接,对文件目标做存在性校验,对 `.md` 目标按 GitHub 风格 slug(小写、保留字母/数字/连字符、空格转 `-`、丢弃 `。、()#` 等标点)校验章节锚点。 +- 外部链接(http/https/mailto)与注释中的占位链接(如 `docs/assets/demo.gif`)未计入有效/无效判定。 +- 所有文档顶部均已统一语言切换导航 `[简体中文](x.zh-CN.md) · English` / `简体中文 · [English](x.md)`。 diff --git a/scripts/api-coverage.sh b/scripts/api-coverage.sh index e76a7ce5..ecf49f47 100644 --- a/scripts/api-coverage.sh +++ b/scripts/api-coverage.sh @@ -158,7 +158,15 @@ sec "环境 / 资源池 / 批量" call POST /api/environment "{\"projectId\":\"$PJ\",\"name\":\"env\",\"baseUrl\":\"$B\"}"; sc "建环境" "200 201"; jchk "baseUrl" 'd.get("baseUrl")=="'$B'"'; ENV=$(jval 'd["id"]') call GET "/api/environment?projectId=$PJ"; sc "环境列表" 200 call PUT "/api/environment/$ENV" "{\"projectId\":\"$PJ\",\"name\":\"env2\",\"baseUrl\":\"$B\"}"; sc "改环境" "200 201"; jchk "名已改" 'd.get("name")=="env2"' -call POST /api/resource-pool '{"name":"本地池"}'; sc "建资源池" "200 201"; jchk "池名" 'd.get("name")=="本地池"' +# 资源池全局按名唯一:用固定名做幂等 upsert(存在则复用,不存在则创建),避免每轮留下孤儿池。 +POOL_NAME="自测资源池" +POOL_LIST=$(curl -s -H "$A" "$B/api/resource-pool") +POOL_ID=$(printf '%s' "$POOL_LIST" | python3 -c "import sys,json;d=json.load(sys.stdin);print(next((p['id'] for p in d if p.get('name')=='$POOL_NAME'),''))") +if [ -n "$POOL_ID" ]; then + call GET "/api/resource-pool/$POOL_ID"; sc "复用资源池" 200; jchk "池名" "d.get(\"name\")==\"$POOL_NAME\"" +else + call POST /api/resource-pool "{\"name\":\"$POOL_NAME\"}"; sc "建资源池" "200 201"; jchk "池名" "d.get(\"name\")==\"$POOL_NAME\"" +fi call GET /api/resource-pool; sc "资源池列表" 200 call POST /api/batch-run "{\"projectId\":\"$PJ\",\"caseIds\":[\"$CASE\"],\"runMode\":\"PARALLEL\"}"; sc "批量运行(无池→400 / 有则 200)" "200 400" diff --git a/scripts/docs-link-check.py b/scripts/docs-link-check.py new file mode 100755 index 00000000..783e1bdf --- /dev/null +++ b/scripts/docs-link-check.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +"""多语言文档链接检查 / Documentation link checker. + +扫描仓库内的 Markdown 文档(README + docs/*.md,英文 + 简体中文),校验: + +1. 内部链接目标文件是否存在(文档、资源、deploy/ 等任意文件); +2. 章节锚点是否解析(GitHub 风格 slug,含显式 `{#id}` 与 ``); +3. 跨语言配对完整性(每个文档组是否 EN + ZH-CN 成对存在)。 + +用法: + python3 scripts/docs-link-check.py [--root ] [--report [path]] [--quiet] + +- 默认打印通过/失败摘要到 stdout; +- `--report`(可省略路径,默认 docs/link-check-report.md)写出完整 Markdown 报告; +- 退出码:0 = 全部有效,1 = 存在无效链接或跨语言缺失,2 = 参数错误。 + +HTML 注释中的占位链接(如 docs/assets/demo.gif)与外部链接(http/https/mailto)不计入判定。 +""" +import argparse +import datetime +import glob +import os +import re +import sys + +IMG_EXT = (".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp") +DEFAULT_REPORT_REL = "docs/link-check-report.md" +LINK_RE = re.compile(r"(!?)\[([^\]]*)\]\((.*?)\)") +EXPLICIT_RE = re.compile(r"\{#([^}]+)\}") +AID_RE = re.compile(r'", re.S) + + +def slug(text: str) -> str: + """GitHub 风格标题 slug:小写,保留字母/数字/连字符,空格转 '-',丢弃其余标点(含 。、()# 等)。""" + s = text.strip().lower() + out = [] + for ch in s: + if ch.isspace(): + out.append("-") + elif ch.isalnum() or ch == "-": + out.append(ch) + return re.sub(r"-+", "-", "".join(out)).strip("-") + + +def collect_anchors(path: str) -> set: + ids = set() + in_fence = False + with open(path, encoding="utf-8") as f: + for line in f: + if line.lstrip().startswith("```"): + in_fence = not in_fence + continue + if in_fence: + continue + m = re.match(r"^#{1,6}\s+(.*?)\s*#*\s*$", line) + if m: + h = m.group(1) + ids.add(slug(h)) + for ex in EXPLICIT_RE.findall(h): + ids.add(ex.strip()) + for aid in AID_RE.findall(line): + ids.add(aid.strip()) + return ids + + +def discover_docs(root: str, exclude: set = None) -> list: + exclude = exclude or set() + docs = ["README.md", "README.zh-CN.md"] + docs += sorted( + os.path.relpath(p, root) + for p in glob.glob(os.path.join(root, "docs", "*.md")) + ) + # 仅保留真实存在的文件(例如 README.zh-CN.md 缺失时不报错),并排除生成的报告本身 + return [ + d + for d in docs + if os.path.isfile(os.path.join(root, d)) and d not in exclude + ] + + +def check(root: str, docs: list) -> tuple: + anchors = {d: collect_anchors(os.path.join(root, d)) for d in docs} + records = [] + for src in docs: + with open(os.path.join(root, src), encoding="utf-8") as f: + lines = f.readlines() + for i, line in enumerate(lines, 1): + cl = COMMENT_RE.sub("", line) + for _is_img, _text, raw in LINK_RE.findall(cl): + if raw.startswith(("http://", "https://", "mailto:")): + continue + parts = raw.split("#", 1) + path = parts[0].strip() + anchor = parts[1] if len(parts) > 1 else "" + if path == "": + check_path = src + ttype = "anchor(same-file)" + else: + check_path = os.path.normpath( + os.path.join(os.path.dirname(src), path) + ) + if path.lower().endswith(IMG_EXT) or path.startswith( + ("docs/assets/", "web/") + ): + ttype = "asset" + elif path.endswith(".md"): + ttype = "doc" + else: + ttype = "file" + exists = os.path.isfile(os.path.join(root, check_path)) or os.path.isdir( + os.path.join(root, check_path) + ) + status = "OK" if exists else "BROKEN" + if ( + exists + and anchor + and check_path in anchors + and anchor not in anchors[check_path] + ): + status = "BROKEN" + xl = ( + path.endswith(".zh-CN.md") + and not src.endswith(".zh-CN.md") + ) or ( + src.endswith(".zh-CN.md") + and path.endswith(".md") + and not path.endswith(".zh-CN.md") + ) + records.append( + dict( + src=src, + line=i, + target=raw, + type=ttype, + status=status, + xl=bool(xl), + ) + ) + return records, anchors + + +def check_pairing(docs: list) -> list: + groups = {} + for d in docs: + base = re.sub(r"\.zh-CN$", "", os.path.splitext(d)[0]) + groups.setdefault(base, set()).add(d) + rows = [] + for base, fs in sorted(groups.items()): + has_en = any(not f.endswith(".zh-CN.md") for f in fs) + has_zh = any(f.endswith(".zh-CN.md") for f in fs) + rows.append((base, sorted(fs), has_en and has_zh)) + return rows + + +def build_report(root, docs, records, pairing) -> str: + total = len(records) + broken = [r for r in records if r["status"] != "OK"] + missing_pairs = [r for r in pairing if not r[2]] + now = datetime.date.today().isoformat() + L = [] + L.append("# 多语言文档链接检查报告 / Documentation Link Check Report") + L.append("") + L.append(f"- 生成日期 / Generated: **{now}**") + L.append( + f"- 检查范围 / Scope: {len(docs)} 个文档(README + `docs/*.md`,英文 + 简体中文)" + ) + L.append(f"- 链接总数 / Total links checked: **{total}**") + L.append( + f"- 有效 / Valid: **{total - len(broken)}** 无效 / Broken: **{len(broken)}**" + ) + L.append("") + L.append("## 结论 / Result") + L.append("") + if broken or missing_pairs: + L.append( + f"> ❌ 发现 {len(broken)} 个无效链接、{len(missing_pairs)} 组跨语言缺失,详见下方清单。" + ) + else: + L.append( + "> ✅ 所有内部文档链接、跨语言切换、章节锚点与资源引用均有效。" + ) + L.append("") + L.append("## 跨语言配对完整性 / Cross-language pairing") + L.append("") + L.append("| 文档组 / Group | 文件 | 中英齐全 / EN+ZH |") + L.append("|---|---|---|") + for base, fs, complete in pairing: + names = ", ".join("`" + os.path.basename(f) + "`" for f in fs) + L.append(f"| `{base}` | {names} | {'✅' if complete else '⚠️ 缺失'} |") + L.append("") + L.append("## 各文档链接清单 / Link inventory") + L.append("") + for src in docs: + src_links = [r for r in records if r["src"] == src] + L.append(f"### {src} ({len(src_links)} 链接)") + L.append("") + L.append("| 行 | 类型 | 目标 | 状态 | 跨语言 |") + L.append("|---|---|---|---|---|") + for r in src_links: + L.append( + f"| {r['line']} | {r['type']} | `{r['target']}` | {r['status']} | {'🔁' if r['xl'] else ''} |" + ) + L.append("") + if broken: + L.append("## 无效链接清单 / Broken links") + L.append("") + for r in broken: + L.append(f"- `{r['src']}:{r['line']}` → `{r['target']}`") + L.append("") + L.append("## 说明 / Notes") + L.append("") + L.append( + "- 检查方式 / Method: 移除 HTML 注释后提取 Markdown 链接,对文件目标做存在性校验,对 `.md` 目标按 GitHub 风格 slug(小写、保留字母/数字/连字符、空格转 `-`、丢弃 `。、()#` 等标点)校验章节锚点。" + ) + L.append( + "- 外部链接(http/https/mailto)与注释中的占位链接(如 `docs/assets/demo.gif`)未计入有效/无效判定。" + ) + L.append( + "- 所有文档顶部均已统一语言切换导航 `[简体中文](x.zh-CN.md) · English` / `简体中文 · [English](x.md)`。" + ) + return "\n".join(L) + "\n" + + +def main(): + parser = argparse.ArgumentParser(description="Check multilingual doc links.") + parser.add_argument("--root", default=None, help="repo root (default: parent of this script)") + parser.add_argument( + "--report", + nargs="?", + const="docs/link-check-report.md", + default=None, + help="write Markdown report to this path (default: docs/link-check-report.md)", + ) + parser.add_argument("--quiet", action="store_true", help="only print pass/fail summary") + args = parser.parse_args() + + root = os.path.abspath(args.root) if args.root else os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + report_path = ( + args.report + if args.report and os.path.isabs(args.report) + else os.path.join(root, args.report) + if args.report + else None + ) + exclude = {DEFAULT_REPORT_REL} + if report_path: + exclude.add(os.path.relpath(report_path, root)) + docs = discover_docs(root, exclude) + records, _ = check(root, docs) + pairing = check_pairing(docs) + + total = len(records) + broken = [r for r in records if r["status"] != "OK"] + missing_pairs = [r for r in pairing if not r[2]] + ok = total - len(broken) + + if not args.quiet: + print(f"Scanned {len(docs)} docs, {total} links: {ok} valid, {len(broken)} broken.") + for r in broken: + print(f" BROKEN {r['src']}:{r['line']} -> {r['target']}") + for base, fs, _ in missing_pairs: + print(f" MISSING PAIR {base}: {sorted(fs)}") + + if args.report: + with open(report_path, "w", encoding="utf-8") as f: + f.write(build_report(root, docs, records, pairing)) + if not args.quiet: + print(f"Report written to {report_path}") + + if broken or missing_pairs: + print("❌ Documentation link check FAILED") + sys.exit(1) + print("✅ Documentation link check passed") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/web/src/components/LiveRunBadge.tsx b/web/src/components/LiveRunBadge.tsx index ace77fa7..8d2712c3 100644 --- a/web/src/components/LiveRunBadge.tsx +++ b/web/src/components/LiveRunBadge.tsx @@ -5,10 +5,24 @@ import { fmtDuration } from './ScenarioReport' export function LiveElapsed({ since }: { since: number }) { const [, tick] = useState(0) useEffect(() => { - const id = window.setInterval(() => tick((n) => n + 1), 100) + const id = window.setInterval(() => tick((n) => n + 1), 500) return () => window.clearInterval(id) }, []) - return {fmtDuration(Math.max(Date.now() - since, 0))} + return ( + + {fmtDuration(Math.max(Date.now() - since, 0))} + + ) } /** Inline live-run pill: looping brand sweep (.ms-step-fillbg.live) + label + count-up elapsed. */ diff --git a/web/src/components/plan/PlanMindmap.tsx b/web/src/components/plan/PlanMindmap.tsx index a3447838..f4b66e04 100644 --- a/web/src/components/plan/PlanMindmap.tsx +++ b/web/src/components/plan/PlanMindmap.tsx @@ -557,9 +557,9 @@ export default function PlanMindmap({ planId, projectId }: { planId: string; pro alignItems: 'center', borderRadius: 8, cursor: isRoot ? 'default' : 'pointer', - background: isRoot ? 'var(--brand)' : 'var(--panel)', + background: isRoot ? 'var(--brand-mindmap-root)' : 'var(--panel)', color: isRoot ? '#fff' : 'var(--text)', - border: isRoot ? '1px solid var(--brand)' : `1px solid ${isSelected ? 'var(--brand)' : 'var(--border)'}`, + border: isRoot ? '1px solid var(--brand-mindmap-root)' : `1px solid ${isSelected ? 'var(--brand)' : 'var(--border)'}`, boxShadow: isSelected ? '0 0 0 2px var(--brand-soft)' : '0 1px 3px rgba(0,0,0,0.06)', }} > diff --git a/web/src/index.css b/web/src/index.css index aa1ffb6b..0ce6fe35 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -6,6 +6,7 @@ --brand: #1664ff; /* standard blue */ --brand-hover: #4086ff; --brand-soft: #eaf2ff; /* faint brand bg for selected/hover */ + --brand-mindmap-root: #225ed9; /* deeper, less glaring blue for the mindmap root node */ --accent: #6ad2ff; /* secondary cyan-blue for charts/decoration */ --success: #00b42a; --error: #f53f3f; @@ -32,6 +33,7 @@ --brand: #4086ff; /* standard blue, brightened for the dark gray canvas */ --brand-hover: #5b9aff; --brand-soft: #1c2b4a; /* dark blue bg for selected rows */ + --brand-mindmap-root: #3b7ce0; /* less bright against the dark canvas */ --accent: #6ad2ff; --success: #27c346; --error: #f76965; @@ -284,17 +286,20 @@ body::before { pointer-events: none; } .ms-step-fillbg.run { - width: 0; + width: 100%; + transform: scaleX(0); + transform-origin: left; background: rgba(22, 100, 255, 0.12); animation: ms-step-grow 0.7s ease-out forwards; + will-change: transform; } .ms-step-fillbg.done { width: 100%; transition: background 0.2s ease; } @keyframes ms-step-grow { - from { width: 0; } - to { width: 100%; } + from { transform: scaleX(0); } + to { transform: scaleX(1); } } /* Live execution (WS events): looping brand-tinted sweep while the step runs. */ .ms-step-fillbg.live {